Skip to main content

schwab_cli/commands/
agent.rs

1use std::io::{stdout, Write};
2use std::path::PathBuf;
3
4use anyhow::Result;
5use serde_json::json;
6
7use crate::agent::{
8    load_state, log_path, pid_path, run_agent_loop, spawn_background, state_summary, stop_daemon,
9};
10use crate::cli::AgentCommands;
11use crate::config::RuntimeConfig;
12use crate::output::{OutputFormat, ResponseEnvelope};
13use crate::rules::{rules_json_schema, RulesConfig};
14use crate::ui::context::DashboardContext;
15use crate::ui::dashboard::render_dashboard;
16use crate::ui::discover::resolve_rules_file;
17
18pub async fn run(runtime: &RuntimeConfig, command: AgentCommands) -> Result<()> {
19    match command {
20        AgentCommands::Schema => {
21            runtime.emit(ResponseEnvelope::ok("agent schema", rules_json_schema()));
22        }
23        AgentCommands::Validate { file } => {
24            let rules = RulesConfig::load(&file)?;
25            runtime.emit(ResponseEnvelope::ok(
26                "agent validate",
27                json!({
28                    "valid": true,
29                    "agent_id": rules.agent_id,
30                    "accounts": rules.accounts.len(),
31                    "watchlist": rules.watchlist,
32                    "llm_enabled": rules.llm.enabled,
33                    "telegram_enabled": rules.notify.telegram.enabled,
34                }),
35            ));
36        }
37        AgentCommands::Status { rules_file } => {
38            if runtime.output == OutputFormat::Json {
39                if let Some(rules_path) = rules_file {
40                    let ctx = DashboardContext::load(&rules_path)?;
41                    runtime.emit(ResponseEnvelope::ok("agent status", ctx.to_json()));
42                } else {
43                    emit_legacy_status(runtime)?;
44                }
45            } else if let Some(rules_path) = rules_file {
46                let ctx = DashboardContext::load(&rules_path)?;
47                print!("{}", render_dashboard(&ctx));
48                stdout().flush().ok();
49            } else if let Ok(rules_path) = resolve_rules_file(None, runtime.is_interactive()) {
50                let ctx = DashboardContext::load(&rules_path)?;
51                print!("{}", render_dashboard(&ctx));
52                stdout().flush().ok();
53            } else {
54                emit_legacy_status(runtime)?;
55            }
56        }
57        AgentCommands::Run {
58            file,
59            once,
60            background,
61        } => {
62            if background {
63                if once {
64                    anyhow::bail!("--background cannot be combined with --once");
65                }
66                let mut extra = Vec::new();
67                if runtime.dry_run {
68                    extra.push("--dry-run".into());
69                }
70                if runtime.trust {
71                    extra.push("--trust".into());
72                }
73                if runtime.yes {
74                    extra.push("--yes".into());
75                }
76                if runtime.output == OutputFormat::Json {
77                    extra.push("--json".into());
78                }
79                let pid = spawn_background(&file, &extra)?;
80                runtime.emit(ResponseEnvelope::ok(
81                    "agent background",
82                    json!({
83                        "pid": pid,
84                        "pid_file": pid_path(&file),
85                        "log_file": log_path(&file),
86                        "rules": file,
87                    }),
88                ));
89                return Ok(());
90            }
91            run_agent_loop(runtime, &file, once, None).await?;
92        }
93        AgentCommands::Stop { file } => {
94            stop_daemon(&file)?;
95            runtime.emit(ResponseEnvelope::ok(
96                "agent stop",
97                json!({ "stopped": true, "rules": file }),
98            ));
99        }
100    }
101    Ok(())
102}
103
104fn emit_legacy_status(runtime: &RuntimeConfig) -> Result<()> {
105    let path = directories::ProjectDirs::from("com", "schwabinvestbot", "schwab")
106        .map(|d| d.data_local_dir().join("agent-state.json"))
107        .unwrap_or_else(|| PathBuf::from("agent-state.json"));
108    let state = load_state(&path)?;
109    runtime.emit(ResponseEnvelope::ok(
110        "agent status",
111        json!({ "state_path": path, "state": state_summary(&state) }),
112    ));
113    Ok(())
114}