use std::io::{self, Read, Write};
use std::process::ExitCode;
use std::time::{SystemTime, UNIX_EPOCH};
use clap::{Parser, Subcommand};
use agent_status::agents::AgentName;
use agent_status::commands::{build_entry, build_extension, format_list, format_status};
use agent_status::state::StateStore;
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Cmd,
}
#[derive(Subcommand)]
enum Cmd {
Set {
#[arg(default_value = "attention")]
event: String,
#[arg(long, value_enum)]
agent: AgentName,
},
Clear {
#[arg(long, value_enum)]
agent: AgentName,
},
Status,
List,
AgentExtension {
#[arg(long, value_enum)]
agent: AgentName,
},
}
fn main() -> ExitCode {
let cli = Cli::parse();
let store = StateStore::from_env();
let result = match cli.command {
Cmd::Set { event, agent } => run_set(&store, agent, &event),
Cmd::Clear { agent } => run_clear(&store, agent),
Cmd::Status => run_status(&store, &mut io::stdout().lock()),
Cmd::List => run_list(&store, &mut io::stdout().lock()),
Cmd::AgentExtension { agent } => run_agent_extension(agent, &mut io::stdout().lock()),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("agent-status: {e}");
ExitCode::from(1)
}
}
}
fn run_set(store: &StateStore, agent_name: AgentName, event: &str) -> io::Result<()> {
let agent = agent_name.agent();
let mut buf = String::new();
io::stdin().read_to_string(&mut buf)?;
let Some(session_id) = agent.extract_session_id(&buf) else {
return Ok(());
};
let cwd = std::env::var("CLAUDE_PROJECT_DIR")
.ok()
.filter(|s| !s.is_empty())
.or_else(|| {
std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().into_owned())
})
.unwrap_or_default();
let pane = std::env::var("TMUX_PANE").unwrap_or_default();
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let message = agent.extract_message(&buf);
let pid = std::os::unix::process::parent_id();
let entry = build_entry(
agent.name(),
event,
&cwd,
&pane,
ts,
message.as_deref(),
Some(pid),
);
store.write(&session_id, &entry)?;
refresh_tmux();
Ok(())
}
fn run_clear(store: &StateStore, agent_name: AgentName) -> io::Result<()> {
let agent = agent_name.agent();
let mut buf = String::new();
io::stdin().read_to_string(&mut buf)?;
let Some(session_id) = agent.extract_session_id(&buf) else {
return Ok(());
};
if store.remove(&session_id)? {
refresh_tmux();
}
Ok(())
}
fn run_status(store: &StateStore, out: &mut impl Write) -> io::Result<()> {
let entries = store.list()?;
if let Some(line) = format_status(&entries) {
writeln!(out, "{line}")?;
}
Ok(())
}
fn run_list(store: &StateStore, out: &mut impl Write) -> io::Result<()> {
let entries = store.list()?;
write!(out, "{}", format_list(&entries))?;
Ok(())
}
fn run_agent_extension(agent_name: AgentName, out: &mut impl Write) -> io::Result<()> {
let bin_path = std::env::current_exe()?;
let bin_str = bin_path.to_string_lossy();
let extension = build_extension(&bin_str, agent_name);
let extension_path = extension_path_for(&extension.filename);
if let Some(parent) = extension_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&extension_path, extension.content)?;
writeln!(out, "{}", extension_path.display())?;
Ok(())
}
fn extension_path_for(filename: &str) -> std::path::PathBuf {
let base = std::env::var_os("XDG_RUNTIME_DIR")
.map_or_else(|| std::path::PathBuf::from("/tmp"), std::path::PathBuf::from);
base.join("agent-status").join("extensions").join(filename)
}
fn refresh_tmux() {
let _ = std::process::Command::new("tmux")
.args(["refresh-client", "-S"])
.stderr(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.status();
}