aboutsummaryrefslogtreecommitdiff
path: root/src/cli/agent.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-10 15:24:24 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-10 15:33:45 +0300
commit36a1b7f86444871f0c38ff36510a6e3c2eec48c6 (patch)
tree4a2d100eef7f26af7feaa93ce661423c3994c54e /src/cli/agent.rs
parent8d13492926829c5cf0ea42c8613344cd65ff59fa (diff)
feat: distribute agent detection manifests
Diffstat (limited to 'src/cli/agent.rs')
-rw-r--r--src/cli/agent.rs216
1 files changed, 216 insertions, 0 deletions
diff --git a/src/cli/agent.rs b/src/cli/agent.rs
index 5005eda1..24f1880d 100644
--- a/src/cli/agent.rs
+++ b/src/cli/agent.rs
@@ -19,6 +19,7 @@ pub(super) fn run_agent_command(args: &[String]) -> std::io::Result<i32> {
"wait" => agent_wait(&args[1..]),
"attach" => agent_attach(&args[1..]),
"start" => agent_start(&args[1..]),
+ "explain" => agent_explain(&args[1..]),
"help" | "--help" | "-h" => {
print_agent_help();
Ok(0)
@@ -30,6 +31,219 @@ pub(super) fn run_agent_command(args: &[String]) -> std::io::Result<i32> {
}
}
+fn agent_explain(args: &[String]) -> std::io::Result<i32> {
+ let mut file = None;
+ let mut agent = None;
+ let mut json = false;
+ let mut target = None;
+
+ let mut index = 0;
+ while index < args.len() {
+ match args[index].as_str() {
+ "--file" => {
+ let Some(value) = args.get(index + 1) else {
+ eprintln!("missing value for --file");
+ return Ok(2);
+ };
+ file = Some(value.clone());
+ index += 2;
+ }
+ "--agent" => {
+ let Some(value) = args.get(index + 1) else {
+ eprintln!("missing value for --agent");
+ return Ok(2);
+ };
+ agent = Some(value.clone());
+ index += 2;
+ }
+ "--json" => {
+ json = true;
+ index += 1;
+ }
+ "--format" => {
+ let Some(value) = args.get(index + 1) else {
+ eprintln!("missing value for --format");
+ return Ok(2);
+ };
+ match value.as_str() {
+ "json" => json = true,
+ "text" => json = false,
+ other => {
+ eprintln!("invalid --format: {other} (expected text or json)");
+ return Ok(2);
+ }
+ }
+ index += 2;
+ }
+ "help" | "--help" | "-h" => {
+ eprintln!("usage: herdr agent explain <target> [--json]");
+ eprintln!("usage: herdr agent explain --file PATH --agent LABEL [--json]");
+ return Ok(0);
+ }
+ value if value.starts_with('-') => {
+ eprintln!("unknown option: {value}");
+ return Ok(2);
+ }
+ value => {
+ if target.is_some() {
+ eprintln!("usage: herdr agent explain <target> [--json]");
+ return Ok(2);
+ }
+ target = Some(value.to_string());
+ index += 1;
+ }
+ }
+ }
+
+ let explain = if let Some(path) = file {
+ if target.is_some() {
+ eprintln!("usage: herdr agent explain --file PATH --agent LABEL [--json]");
+ return Ok(2);
+ }
+ let Some(agent_label) = agent else {
+ eprintln!("herdr agent explain --file requires --agent LABEL");
+ return Ok(2);
+ };
+ let content = std::fs::read_to_string(path)?;
+ crate::detect::manifest::explain_to_json_value(&crate::detect::manifest::explain_for_label(
+ &agent_label,
+ &content,
+ ))
+ } else {
+ let Some(target) = target else {
+ eprintln!("usage: herdr agent explain <target> [--json]");
+ eprintln!("usage: herdr agent explain --file PATH --agent LABEL [--json]");
+ return Ok(2);
+ };
+ if agent.is_some() {
+ eprintln!("--agent is only valid with --file");
+ return Ok(2);
+ }
+
+ let response = super::send_request(&Request {
+ id: "cli:agent:explain".into(),
+ method: Method::AgentExplain(AgentTarget {
+ target: target.to_owned(),
+ }),
+ })?;
+ if response.get("error").is_some() {
+ eprintln!("{}", serde_json::to_string(&response).unwrap());
+ return Ok(1);
+ }
+ response["result"]["explain"].clone()
+ };
+
+ if json {
+ println!("{explain}");
+ } else {
+ print_agent_explain_text(&explain);
+ }
+ Ok(0)
+}
+
+fn print_agent_explain_text(explain: &serde_json::Value) {
+ println!("agent: {}", explain["agent"].as_str().unwrap_or("unknown"));
+ println!("state: {}", explain["state"].as_str().unwrap_or("unknown"));
+ println!(
+ "screen_detection_skipped: {}",
+ explain["screen_detection_skipped"]
+ .as_bool()
+ .unwrap_or(false)
+ );
+ if let Some(reason) = explain["screen_detection_skip_reason"].as_str() {
+ println!("screen_detection_skip_reason: {reason}");
+ }
+ println!(
+ "manifest: {}",
+ explain["manifest_source"].as_str().unwrap_or("none")
+ );
+ println!(
+ "manifest_version: {}",
+ explain["manifest_version"].as_str().unwrap_or("unknown")
+ );
+ println!(
+ "cached_remote_version: {}",
+ explain["cached_remote_version"].as_str().unwrap_or("none")
+ );
+ println!(
+ "local_override_shadowing_remote: {}",
+ explain["local_override_shadowing_remote"]
+ .as_bool()
+ .unwrap_or(false)
+ );
+ if let Some(status) = explain["remote_update_status"].as_str() {
+ println!("remote_update_status: {status}");
+ }
+ if let Some(error) = explain["remote_update_error"].as_str() {
+ println!("remote_update_error: {error}");
+ }
+ if let Some(rule) = explain["matched_rule"].as_object() {
+ println!(
+ "matched_rule: {} priority={} region={} state={}",
+ rule.get("id")
+ .and_then(|value| value.as_str())
+ .unwrap_or("-"),
+ rule.get("priority")
+ .and_then(|value| value.as_i64())
+ .unwrap_or(0),
+ rule.get("region")
+ .and_then(|value| value.as_str())
+ .unwrap_or("-"),
+ rule.get("state")
+ .and_then(|value| value.as_str())
+ .unwrap_or("unknown")
+ );
+ } else {
+ println!("matched_rule: none");
+ }
+ println!(
+ "visible: idle={} blocker={} working={}",
+ explain["visible_idle"].as_bool().unwrap_or(false),
+ explain["visible_blocker"].as_bool().unwrap_or(false),
+ explain["visible_working"].as_bool().unwrap_or(false)
+ );
+ if let Some(reason) = explain["fallback_reason"].as_str() {
+ println!("fallback_reason: {reason}");
+ }
+ if let Some(reason) = explain["skipped_update_reason"].as_str() {
+ println!("skipped_update_reason: {reason}");
+ }
+ if let Some(warning) = explain["warning"].as_str() {
+ println!("warning: {warning}");
+ }
+ if let Some(evaluated_rules) = explain["evaluated_rules"]
+ .as_array()
+ .filter(|rules| !rules.is_empty())
+ {
+ println!("evaluated_rules:");
+ for rule in evaluated_rules {
+ println!(
+ " {} matched={} priority={} region={} state={}",
+ rule["id"].as_str().unwrap_or("-"),
+ rule["matched"].as_bool().unwrap_or(false),
+ rule["priority"].as_i64().unwrap_or(0),
+ rule["region"].as_str().unwrap_or("-"),
+ rule["state"].as_str().unwrap_or("unknown")
+ );
+ let evidence = &rule["evidence"];
+ println!(
+ " matchers: contains={:?} regex={:?} line_regex={:?} all={} any={} not={}",
+ evidence["contains"],
+ evidence["regex"],
+ evidence["line_regex"],
+ evidence["all_count"].as_u64().unwrap_or(0),
+ evidence["any_count"].as_u64().unwrap_or(0),
+ evidence["not_count"].as_u64().unwrap_or(0)
+ );
+ println!(
+ " region: bytes={} preview={:?}",
+ evidence["region_bytes"].as_u64().unwrap_or(0),
+ evidence["region_preview"].as_str().unwrap_or("")
+ );
+ }
+ }
+}
+
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...>");
@@ -422,6 +636,8 @@ fn print_agent_help() {
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!(" herdr agent explain <target> [--json]");
+ eprintln!(" herdr agent explain --file PATH --agent LABEL [--json]");
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"