Skip to main content

systemprompt_cli/commands/admin/agents/
logs_disk.rs

1//! Disk-log rendering for `admin agents logs`.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::path::Path;
7use std::process::Command;
8
9use anyhow::{Context, Result, anyhow};
10
11use super::logs::LogsArgs;
12use super::types::AgentLogsOutput;
13use crate::CliConfig;
14use crate::interactive::{Prompter, resolve_required};
15use crate::shared::CommandOutput;
16use crate::shared::disk_logs::{display_names, find_log_file, list_log_files, read_log_lines};
17use systemprompt_models::artifacts::ListItem;
18
19pub(super) fn execute_disk_mode(
20    args: &LogsArgs,
21    prompter: &dyn Prompter,
22    config: &CliConfig,
23    logs_path: &Path,
24) -> Result<CommandOutput> {
25    if !logs_path.exists() {
26        return Err(anyhow!(
27            "Logs directory does not exist: {}",
28            logs_path.display()
29        ));
30    }
31
32    if args.agent.is_none() && !config.is_interactive() {
33        let log_files = list_log_files(logs_path, "agent-")?;
34        let output = AgentLogsOutput {
35            agent: None,
36            source: "disk".to_owned(),
37            logs: vec![],
38            log_files,
39        };
40        let items: Vec<ListItem> = output
41            .log_files
42            .iter()
43            .map(|file| ListItem::new(file.clone(), String::new(), file.clone()))
44            .collect();
45        return Ok(CommandOutput::list(items).with_title("Available Agent Log Files"));
46    }
47
48    let agent = resolve_required(args.agent.clone(), "agent", config, || {
49        prompt_log_selection(prompter, logs_path)
50    })?;
51
52    let log_file = find_log_file(logs_path, "agent-", &agent)?;
53    let logs = read_log_lines(&log_file, args.lines, |_| true)?;
54
55    Ok(CommandOutput::card_value(
56        format!("Agent Logs (Disk): {}", agent),
57        &AgentLogsOutput {
58            agent: Some(agent),
59            source: "disk".to_owned(),
60            logs,
61            log_files: vec![],
62        },
63    ))
64}
65
66pub(super) fn execute_follow_mode(
67    args: &LogsArgs,
68    prompter: &dyn Prompter,
69    config: &CliConfig,
70    logs_path: &Path,
71) -> Result<CommandOutput> {
72    if !logs_path.exists() {
73        return Err(anyhow!(
74            "Logs directory does not exist: {}",
75            logs_path.display()
76        ));
77    }
78
79    let agent = resolve_required(args.agent.clone(), "agent", config, || {
80        prompt_log_selection(prompter, logs_path)
81    })?;
82
83    let log_file = find_log_file(logs_path, "agent-", &agent)?;
84
85    let status = Command::new("tail")
86        .arg("-f")
87        .arg(&log_file)
88        .status()
89        .context("Failed to execute tail -f")?;
90
91    if !status.success() {
92        return Err(anyhow!("tail -f exited with non-zero status"));
93    }
94
95    Ok(CommandOutput::card_value(
96        "Agent Logs",
97        &AgentLogsOutput {
98            agent: Some(agent),
99            source: "disk".to_owned(),
100            logs: vec![],
101            log_files: vec![],
102        },
103    ))
104}
105
106fn prompt_log_selection(prompter: &dyn Prompter, logs_dir: &Path) -> Result<String> {
107    let log_files = list_log_files(logs_dir, "agent-")?;
108    select_agent_from_log_files(prompter, &log_files, logs_dir)
109}
110
111pub fn select_agent_from_log_files(
112    prompter: &dyn Prompter,
113    log_files: &[String],
114    logs_dir: &Path,
115) -> Result<String> {
116    if log_files.is_empty() {
117        return Err(anyhow!(
118            "No agent log files found in {}",
119            logs_dir.display()
120        ));
121    }
122
123    let agents = display_names(log_files, "agent-");
124
125    let selection = prompter
126        .select("Select agent logs to view", &agents)
127        .context("Failed to get agent selection")?;
128
129    Ok(agents[selection].clone())
130}