schwab_cli/commands/
agent.rs1use 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}