aboutsummaryrefslogtreecommitdiff
path: root/src/cli/agent.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli/agent.rs')
-rw-r--r--src/cli/agent.rs429
1 files changed, 429 insertions, 0 deletions
diff --git a/src/cli/agent.rs b/src/cli/agent.rs
new file mode 100644
index 00000000..5005eda1
--- /dev/null
+++ b/src/cli/agent.rs
@@ -0,0 +1,429 @@
+use crate::api::schema::{
+ AgentReadParams, AgentRenameParams, AgentSendParams, AgentStartParams, AgentStatus,
+ AgentTarget, EmptyParams, Method, ReadFormat, ReadSource, Request, Subscription,
+};
+
+pub(super) fn run_agent_command(args: &[String]) -> std::io::Result<i32> {
+ let Some(subcommand) = args.first().map(|arg| arg.as_str()) else {
+ print_agent_help();
+ return Ok(2);
+ };
+
+ match subcommand {
+ "list" => agent_list(&args[1..]),
+ "get" => agent_get(&args[1..]),
+ "read" => agent_read(&args[1..]),
+ "send" => agent_send(&args[1..]),
+ "rename" => agent_rename(&args[1..]),
+ "focus" => agent_focus(&args[1..]),
+ "wait" => agent_wait(&args[1..]),
+ "attach" => agent_attach(&args[1..]),
+ "start" => agent_start(&args[1..]),
+ "help" | "--help" | "-h" => {
+ print_agent_help();
+ Ok(0)
+ }
+ _ => {
+ print_agent_help();
+ Ok(2)
+ }
+ }
+}
+
+fn agent_start(args: &[String]) -> std::io::Result<i32> {
+ let Some(name) = args.first() else {
+ eprintln!("usage: herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>");
+ return Ok(2);
+ };
+
+ let Some(separator) = args.iter().position(|arg| arg == "--") else {
+ eprintln!("usage: herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>");
+ return Ok(2);
+ };
+ if separator == args.len() - 1 {
+ eprintln!("agent start requires argv after --");
+ return Ok(2);
+ }
+
+ let mut cwd = None;
+ let mut workspace_id = None;
+ let mut tab_id = None;
+ let mut split = None;
+ let mut focus = false;
+
+ let mut index = 1;
+ while index < separator {
+ match args[index].as_str() {
+ "--cwd" => {
+ let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else {
+ eprintln!("missing value for --cwd");
+ return Ok(2);
+ };
+ cwd = Some(value.clone());
+ index += 2;
+ }
+ "--workspace" => {
+ let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else {
+ eprintln!("missing value for --workspace");
+ return Ok(2);
+ };
+ workspace_id = Some(super::normalize_workspace_id(value));
+ index += 2;
+ }
+ "--tab" => {
+ let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else {
+ eprintln!("missing value for --tab");
+ return Ok(2);
+ };
+ tab_id = Some(super::normalize_tab_id(value));
+ index += 2;
+ }
+ "--split" => {
+ let Some(value) = args.get(index + 1).filter(|_| index + 1 < separator) else {
+ eprintln!("missing value for --split");
+ return Ok(2);
+ };
+ split = Some(super::parse_split_direction(value)?);
+ index += 2;
+ }
+ "--focus" => {
+ focus = true;
+ index += 1;
+ }
+ "--no-focus" => {
+ focus = false;
+ index += 1;
+ }
+ other => {
+ eprintln!("unknown option: {other}");
+ return Ok(2);
+ }
+ }
+ }
+
+ super::print_response(&super::send_request(&Request {
+ id: "cli:agent:start".into(),
+ method: Method::AgentStart(AgentStartParams {
+ name: name.clone(),
+ cwd,
+ workspace_id,
+ tab_id,
+ split,
+ focus,
+ argv: args[separator + 1..].to_vec(),
+ }),
+ })?)
+}
+
+fn agent_list(args: &[String]) -> std::io::Result<i32> {
+ if !args.is_empty() {
+ eprintln!("usage: herdr agent list");
+ return Ok(2);
+ }
+
+ super::print_response(&super::send_request(&Request {
+ id: "cli:agent:list".into(),
+ method: Method::AgentList(EmptyParams::default()),
+ })?)
+}
+
+fn agent_get(args: &[String]) -> std::io::Result<i32> {
+ let Some(target) = args.first() else {
+ eprintln!("usage: herdr agent get <target>");
+ return Ok(2);
+ };
+ if args.len() != 1 {
+ eprintln!("usage: herdr agent get <target>");
+ return Ok(2);
+ }
+
+ super::print_response(&super::send_request(&Request {
+ id: "cli:agent:get".into(),
+ method: Method::AgentGet(AgentTarget {
+ target: target.clone(),
+ }),
+ })?)
+}
+
+fn agent_focus(args: &[String]) -> std::io::Result<i32> {
+ let Some(target) = args.first() else {
+ eprintln!("usage: herdr agent focus <target>");
+ return Ok(2);
+ };
+ if args.len() != 1 {
+ eprintln!("usage: herdr agent focus <target>");
+ return Ok(2);
+ }
+
+ super::print_response(&super::send_request(&Request {
+ id: "cli:agent:focus".into(),
+ method: Method::AgentFocus(AgentTarget {
+ target: target.clone(),
+ }),
+ })?)
+}
+
+fn agent_attach(args: &[String]) -> std::io::Result<i32> {
+ let (target, takeover) =
+ match super::parse_attach_target(args, "usage: herdr agent attach <target> [--takeover]") {
+ Ok(parsed) => parsed,
+ Err(code) => return Ok(code),
+ };
+
+ let response = resolve_agent_target(&target, "cli:agent:attach:resolve")?;
+ if response.get("error").is_some() {
+ eprintln!("{}", serde_json::to_string(&response).unwrap());
+ return Ok(1);
+ }
+ let Some(terminal_id) = response["result"]["agent"]["terminal_id"].as_str() else {
+ eprintln!("agent attach failed: response did not include terminal_id");
+ return Ok(1);
+ };
+ crate::client::run_terminal_attach(terminal_id.to_owned(), takeover)?;
+ Ok(0)
+}
+
+fn agent_wait(args: &[String]) -> std::io::Result<i32> {
+ let Some(target) = args.first() else {
+ eprintln!("usage: herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]");
+ return Ok(2);
+ };
+
+ let mut timeout_ms = None;
+ let mut desired_status = None;
+
+ let mut index = 1;
+ while index < args.len() {
+ match args[index].as_str() {
+ "--status" => {
+ let Some(value) = args.get(index + 1) else {
+ eprintln!("missing value for --status");
+ return Ok(2);
+ };
+ desired_status = Some(parse_agent_wait_status(value)?);
+ index += 2;
+ }
+ "--timeout" => {
+ let Some(value) = args.get(index + 1) else {
+ eprintln!("missing value for --timeout");
+ return Ok(2);
+ };
+ timeout_ms = Some(super::parse_u64_flag("--timeout", value)?);
+ index += 2;
+ }
+ "help" | "--help" | "-h" => {
+ eprintln!("usage: herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]");
+ return Ok(0);
+ }
+ other => {
+ eprintln!("unknown option: {other}");
+ return Ok(2);
+ }
+ }
+ }
+
+ let Some(agent_status) = desired_status else {
+ eprintln!("missing required --status");
+ return Ok(2);
+ };
+
+ let response = resolve_agent_target(target, "cli:agent:wait:resolve")?;
+ if response.get("error").is_some() {
+ eprintln!("{}", serde_json::to_string(&response).unwrap());
+ return Ok(1);
+ }
+ if response["result"]["agent"]["agent_status"]
+ .as_str()
+ .is_some_and(|current| agent_wait_status_satisfied(agent_status, current))
+ {
+ println!("{}", serde_json::to_string(&response).unwrap());
+ return Ok(0);
+ }
+
+ let Some(pane_id) = response["result"]["agent"]["pane_id"].as_str() else {
+ eprintln!("agent wait failed: response did not include pane_id");
+ return Ok(1);
+ };
+
+ let subscriptions = if agent_status == AgentStatus::Idle {
+ vec![
+ Subscription::PaneAgentStatusChanged {
+ pane_id: pane_id.to_owned(),
+ agent_status: Some(AgentStatus::Idle),
+ },
+ Subscription::PaneAgentStatusChanged {
+ pane_id: pane_id.to_owned(),
+ agent_status: Some(AgentStatus::Done),
+ },
+ ]
+ } else {
+ vec![Subscription::PaneAgentStatusChanged {
+ pane_id: pane_id.to_owned(),
+ agent_status: Some(agent_status),
+ }]
+ };
+
+ super::wait_for_agent_change(
+ Request {
+ id: "cli:agent:wait".into(),
+ method: Method::EventsSubscribe(crate::api::schema::EventsSubscribeParams {
+ subscriptions,
+ }),
+ },
+ timeout_ms,
+ "timed out waiting for agent status change",
+ )
+}
+
+fn resolve_agent_target(target: &str, request_id: &str) -> std::io::Result<serde_json::Value> {
+ super::send_request(&Request {
+ id: request_id.into(),
+ method: Method::AgentGet(AgentTarget {
+ target: target.to_owned(),
+ }),
+ })
+}
+
+fn agent_rename(args: &[String]) -> std::io::Result<i32> {
+ let Some(target) = args.first() else {
+ eprintln!("usage: herdr agent rename <target> <name>|--clear");
+ return Ok(2);
+ };
+ if args.len() < 2 {
+ eprintln!("usage: herdr agent rename <target> <name>|--clear");
+ return Ok(2);
+ }
+ let name = if args.len() == 2 && args[1] == "--clear" {
+ None
+ } else {
+ Some(args[1..].join(" "))
+ };
+
+ super::print_response(&super::send_request(&Request {
+ id: "cli:agent:rename".into(),
+ method: Method::AgentRename(AgentRenameParams {
+ target: target.clone(),
+ name,
+ }),
+ })?)
+}
+
+fn agent_send(args: &[String]) -> std::io::Result<i32> {
+ if args.len() < 2 {
+ eprintln!("usage: herdr agent send <target> <text>");
+ return Ok(2);
+ }
+
+ super::print_response(&super::send_request(&Request {
+ id: "cli:agent:send".into(),
+ method: Method::AgentSend(AgentSendParams {
+ target: args[0].clone(),
+ text: args[1..].join(" "),
+ }),
+ })?)
+}
+
+fn agent_read(args: &[String]) -> std::io::Result<i32> {
+ let Some(target) = args.first() else {
+ eprintln!("usage: herdr agent read <target> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]");
+ return Ok(2);
+ };
+
+ let mut source = ReadSource::Recent;
+ let mut lines = None;
+ let mut format = ReadFormat::Text;
+ let mut strip_ansi = true;
+
+ let mut index = 1;
+ while index < args.len() {
+ match args[index].as_str() {
+ "--source" => {
+ let Some(value) = args.get(index + 1) else {
+ eprintln!("missing value for --source");
+ return Ok(2);
+ };
+ source = super::parse_read_source(value)?;
+ index += 2;
+ }
+ "--lines" => {
+ let Some(value) = args.get(index + 1) else {
+ eprintln!("missing value for --lines");
+ return Ok(2);
+ };
+ lines = Some(super::parse_u32_flag("--lines", value)?);
+ index += 2;
+ }
+ "--format" => {
+ let Some(value) = args.get(index + 1) else {
+ eprintln!("missing value for --format");
+ return Ok(2);
+ };
+ format = super::parse_read_format(value)?;
+ strip_ansi = !matches!(format, ReadFormat::Ansi);
+ index += 2;
+ }
+ "--ansi" => {
+ format = ReadFormat::Ansi;
+ strip_ansi = false;
+ index += 1;
+ }
+ other => {
+ eprintln!("unknown option: {other}");
+ return Ok(2);
+ }
+ }
+ }
+
+ super::print_response(&super::send_request(&Request {
+ id: "cli:agent:read".into(),
+ method: Method::AgentRead(AgentReadParams {
+ target: target.clone(),
+ source,
+ lines,
+ format,
+ strip_ansi,
+ }),
+ })?)
+}
+
+fn agent_wait_status_satisfied(desired: AgentStatus, current: &str) -> bool {
+ match desired {
+ AgentStatus::Idle => matches!(current, "idle" | "done"),
+ AgentStatus::Working => current == "working",
+ AgentStatus::Blocked => current == "blocked",
+ AgentStatus::Unknown => current == "unknown",
+ AgentStatus::Done => false,
+ }
+}
+
+fn parse_agent_wait_status(value: &str) -> std::io::Result<AgentStatus> {
+ match value {
+ "idle" => Ok(AgentStatus::Idle),
+ "working" => Ok(AgentStatus::Working),
+ "blocked" => Ok(AgentStatus::Blocked),
+ "unknown" => Ok(AgentStatus::Unknown),
+ "done" => Err(std::io::Error::other(
+ "done is a UI attention state; use idle for CLI agent completion waits",
+ )),
+ _ => Err(std::io::Error::other(format!(
+ "invalid agent status: {value} (expected idle, working, blocked, or unknown)"
+ ))),
+ }
+}
+
+fn print_agent_help() {
+ eprintln!("herdr agent commands:");
+ eprintln!(" herdr agent list");
+ eprintln!(" herdr agent get <target>");
+ eprintln!(" herdr agent read <target> [--source visible|recent|recent-unwrapped] [--lines N] [--format text|ansi] [--ansi]");
+ eprintln!(" herdr agent send <target> <text>");
+ eprintln!(" herdr agent rename <target> <name>|--clear");
+ eprintln!(" herdr agent focus <target>");
+ eprintln!(" herdr agent wait <target> --status <idle|working|blocked|unknown> [--timeout MS]");
+ eprintln!(" herdr agent attach <target> [--takeover]");
+ eprintln!(" herdr agent start <name> [--cwd PATH] [--workspace ID] [--tab ID] [--split right|down] [--focus|--no-focus] -- <argv...>");
+ eprintln!(" targets accept terminal ids, unique agent names, detected/reported agent labels, and legacy pane ids");
+ eprintln!(
+ " agent send writes literal text; use pane run when you want command text plus Enter"
+ );
+}