use crate::monitor::process::scan_for_agents;
use anyhow::Result;
use colored::*;
use crossterm::{
event::{self, Event, KeyCode, KeyModifiers},
terminal,
};
pub async fn run() -> Result<()> {
print_banner();
println!(" {} scanning for running AI agents...", "→".dimmed());
println!();
let agents = scan_for_agents();
if agents.is_empty() {
print_no_agents();
} else {
print_agent_list(&agents);
if let Some(selected) = pick_agent(&agents)? {
println!();
println!(
" {} {}",
"attaching to".dimmed(),
format!("{} (pid {})", selected.name, selected.pid)
.bold()
.white()
);
println!();
let args: Vec<String> = std::env::args().collect();
let bin = &args[0];
let status = std::process::Command::new(bin)
.args(["attach", "--pid", &selected.pid.to_string()])
.status()?;
std::process::exit(status.code().unwrap_or(0));
}
}
Ok(())
}
fn print_banner() {
let version = env!("CARGO_PKG_VERSION");
println!();
println!(
" {} {}",
"sandspy".bold().white(),
format!("v{version}").dimmed()
);
println!(" {}", "AI agent security monitor".dimmed());
println!();
println!(" {}", "─".repeat(62).dimmed());
println!();
}
fn print_agent_list(agents: &[crate::events::AgentInfo]) {
println!(
" {} {} running agent{} detected\n",
"✓".green().bold(),
agents.len().to_string().bold().white(),
if agents.len() == 1 { "" } else { "s" }
);
for (i, agent) in agents.iter().enumerate() {
let uptime = format_uptime(agent.uptime_secs);
println!(
" {} {} {} {}",
format!("[{}]", i + 1).bold().cyan(),
agent.name.bold().white(),
format!("pid {}", agent.pid).dimmed(),
uptime.dimmed()
);
}
println!();
println!(
" {}",
"select an agent to monitor [1-9] / ctrl+c to exit".dimmed()
);
println!();
}
fn print_no_agents() {
println!(" {} no running AI agents detected\n", "○".yellow());
println!(" {}", "usage".bold().white());
println!();
let cmds = [
(
"sandspy watch \"claude-code\"",
"monitor a launched command",
),
("sandspy attach --name cursor", "attach to a running agent"),
("sandspy demo", "run a simulated 25-second session"),
("sandspy daemon start", "background monitoring mode"),
];
for (cmd, desc) in &cmds {
println!(" {} {}", cmd.bold().white(), desc.dimmed());
}
println!();
println!(" {}", "supported agents".bold().white());
println!();
let agents = [
"Claude Code",
"Cursor",
"Codex CLI",
"Gemini CLI",
"Windsurf",
"Cline",
"Aider",
"Continue",
"Antigravity",
"OpenClaw",
];
let joined = agents.join(" · ");
println!(" {}", joined.dimmed());
println!();
}
fn pick_agent(agents: &[crate::events::AgentInfo]) -> Result<Option<&crate::events::AgentInfo>> {
terminal::enable_raw_mode()?;
let result = loop {
if let Ok(Event::Key(key)) = event::read() {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
break None;
}
if key.code == KeyCode::Char('q') || key.code == KeyCode::Esc {
break None;
}
if let KeyCode::Char(c) = key.code {
if let Some(digit) = c.to_digit(10) {
let idx = digit as usize;
if idx >= 1 && idx <= agents.len() {
break Some(&agents[idx - 1]);
}
}
}
}
};
terminal::disable_raw_mode()?;
Ok(result)
}
fn format_uptime(secs: u64) -> String {
if secs < 60 {
format!("up {}s", secs)
} else if secs < 3600 {
format!("up {}m", secs / 60)
} else {
format!("up {}h {}m", secs / 3600, (secs % 3600) / 60)
}
}