feat: agent-detector process-tree detection + auto-wire hooks - #46
feat: agent-detector process-tree detection + auto-wire hooks#46getappz wants to merge 8 commits into
Conversation
Vendor agent-detector (MIT, dtcxzyw) detection logic into src/ponytail/detect.rs. Three-tier detection: 1. Parent process tree walk (with process-tree feature) 2. AI_AGENT / AGENT standard env vars 3. Tool-specific env vars for ~60 known agents Platform detection now uses process-tree first, falling back to env vars. Maps agent name to output format (Claude/Codex/ Copilot/Fallback). sysinfo optional dep behind 'process-tree' feature flag. Detects agents even when launched via plain shell (no env vars).
Ponytail hooks now auto-wired by 'agentflare init --agent X'. Skills already embedded via include_str! — no download needed. wire_ponytail_hooks() adds ponytail SessionStart/SubagentStart hooks + statusline to Claude Code, Cursor, and OpenCode settings.
Init now checks agent settings for existing ponytail npm plugin hooks before auto-wiring agentflare's built-in ponytail hooks. If detected, prints warning + uninstall instructions, skips wiring. User re-runs init after disabling the old plugin.
agentflare init --agent X -y skips all confirmation prompts and attempts to uninstall the existing ponytail npm plugin before auto-wiring agentflare's built-in ponytail hooks.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/ponytail/detect.rs (1)
111-144: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winFull-system process refresh on every detection call.
find_in_parent_treeallocates a newSystemand refreshes every process on the machine (ProcessesToUpdate::All, defaultProcessRefreshKind) just to walk a handful of ancestor PIDs and read their names. Per the sysinfo docs,"Refreshing all processes and their tasks can be quite expensive", and here it's only needed forname()/parent()of a small ancestor chain. Consider usingrefresh_processes_specificswith a minimalProcessRefreshKind(no CPU/memory/disk/tasks) to cut overhead, since detection presumably runs on every CLI invocation.♻️ Proposed fix
- let mut system = System::new(); - system.refresh_processes(ProcessesToUpdate::All, true); + let mut system = System::new(); + system.refresh_processes_specifics( + ProcessesToUpdate::All, + true, + ProcessRefreshKind::nothing(), + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/ponytail/detect.rs` around lines 111 - 144, `find_in_parent_tree` is doing a full `System::refresh_processes(ProcessesToUpdate::All, true)` on every call even though it only needs ancestor `name()` and `parent()` values. Update `find_in_parent_tree` to use a narrower sysinfo refresh path, such as `refresh_processes_specifics`, with a minimal `ProcessRefreshKind` that avoids CPU/memory/disk/tasks, while keeping the `current_pid`, `proc.name()`, and `proc.parent()` lookup logic unchanged. This should preserve the `AGENTS` matching and `is_cowork_override()` behavior but reduce detection overhead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/init.rs`:
- Around line 41-72: The confirmation flow in confirm/reconfigure logic has two
edge cases: in non-interactive stdin or EOF cases, empty input from read_line
should not be treated as approval when yes is false, and the opencode path
should not continue after a failed plugin uninstall. Update the confirmation
handling in the relevant init flow and the opencode uninstall branch so that
non-TTY/EOF input defaults to decline unless -y is set, and return false
immediately when opencode plugin uninstall fails instead of proceeding to wire
hooks.
- Around line 307-315: The already_wired guard in the ponytail wiring flow is
too broad because it treats any SessionStart containing "ponytail" as already
wired, which wrongly matches the npm plugin and skips agentflare wiring. Update
the check in wire_ponytail_claude_code to distinguish agentflare from the npm
ponytail plugin by aligning it with the has_existing_ponytail_claude logic, i.e.
only consider it already wired when the agentflare-specific hooks are present
and the session does not just contain the npm ponytail marker.
- Around line 345-379: Add an idempotency guard to the cursor wiring path in the
same way as wire_ponytail_claude_code and wire_ponytail_opencode so repeated
init --agent cursor runs do not append duplicate hooks. Update the cursor hook
setup logic in init.rs to detect when the ponytail commands are already present
in .cursor/hooks.json before writing sessionStart and beforeSubmitPrompt
entries, and skip re-adding them if they exist.
In `@src/ponytail/detect.rs`:
- Line 28: The Antigravity agent mapping in detect logic uses the wrong process
name, so update the AgentEntry for the "antigravity" symbol to match the actual
CLI binary. Replace the current process_names entry with the correct binary name
used by Antigravity, and keep the env_vars mapping unchanged so process
detection in detect.rs identifies the right agent instead of the unrelated one.
---
Nitpick comments:
In `@src/ponytail/detect.rs`:
- Around line 111-144: `find_in_parent_tree` is doing a full
`System::refresh_processes(ProcessesToUpdate::All, true)` on every call even
though it only needs ancestor `name()` and `parent()` values. Update
`find_in_parent_tree` to use a narrower sysinfo refresh path, such as
`refresh_processes_specifics`, with a minimal `ProcessRefreshKind` that avoids
CPU/memory/disk/tasks, while keeping the `current_pid`, `proc.name()`, and
`proc.parent()` lookup logic unchanged. This should preserve the `AGENTS`
matching and `is_cowork_override()` behavior but reduce detection overhead.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 2bf398f0-66b8-4ce5-8768-7a80d3dbea82
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (6)
Cargo.tomlsrc/init.rssrc/main.rssrc/ponytail/detect.rssrc/ponytail/mod.rssrc/ponytail/platform.rs
| if !yes { | ||
| print!(" Uninstall ponytail plugin? [Y/n] "); | ||
| let mut input = String::new(); | ||
| std::io::stdin().read_line(&mut input).ok(); | ||
| match input.trim().to_lowercase().as_str() { | ||
| "y" | "yes" | "" => {} | ||
| _ => { | ||
| println!(" Skipped. Re-run: agentflare init --agent {agent}"); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| match agent { | ||
| "opencode" => { | ||
| println!(" Running: opencode plugin uninstall ponytail@ponytail"); | ||
| match std::process::Command::new("opencode") | ||
| .args(["plugin", "uninstall", "ponytail@ponytail"]) | ||
| .output() | ||
| { | ||
| Ok(out) => { | ||
| if out.status.success() { | ||
| println!(" ok ponytail plugin uninstalled"); | ||
| } else { | ||
| let stderr = String::from_utf8_lossy(&out.stderr); | ||
| println!(" fail {stderr}"); | ||
| } | ||
| } | ||
| Err(e) => println!(" fail could not run opencode: {e}"), | ||
| } | ||
| true | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Two edge cases in the confirmation flow.
- Non-interactive stdin (EOF/pipe) with
yes == false:read_linereturnsOk(0)leavinginputempty, which matches the""arm and is treated as "yes" — silently proceeding to uninstall/rewire in CI or piped contexts. Consider treating a non-TTY/EOF as a decline unless-yis given. - opencode branch: if
opencode plugin uninstallfails (Line 64-69), the function still returnstrue, so hooks are wired despite the conflicting plugin remaining. Consider returningfalseon uninstall failure.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/init.rs` around lines 41 - 72, The confirmation flow in
confirm/reconfigure logic has two edge cases: in non-interactive stdin or EOF
cases, empty input from read_line should not be treated as approval when yes is
false, and the opencode path should not continue after a failed plugin
uninstall. Update the confirmation handling in the relevant init flow and the
opencode uninstall branch so that non-TTY/EOF input defaults to decline unless
-y is set, and return false immediately when opencode plugin uninstall fails
instead of proceeding to wire hooks.
| let already_wired = settings | ||
| .get("hooks") | ||
| .and_then(|h| h.get("SessionStart")) | ||
| .map(|v| v.to_string().contains("ponytail")) | ||
| .unwrap_or(false); | ||
| if already_wired { | ||
| println!(" skip ponytail hooks already wired in ~/.claude/settings.json"); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
already_wired conflates the npm ponytail plugin with agentflare, skipping the wiring it was asked to do.
This check only looks for the substring "ponytail" in SessionStart, but has_existing_ponytail_claude() (Line 86-94) uses ponytail && !agentflare to detect the npm plugin. In the migration flow, confirm_ponytail_migration first detects the npm plugin, the user confirms, and Claude uninstall is manual (deferred). When wire_ponytail_claude_code then runs, the npm plugin's "ponytail" string is still present, so already_wired is true and agentflare's hooks are never wired. Align this guard with the !contains("agentflare") logic so agentflare's own hooks are distinguished from the npm plugin's.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/init.rs` around lines 307 - 315, The already_wired guard in the ponytail
wiring flow is too broad because it treats any SessionStart containing
"ponytail" as already wired, which wrongly matches the npm plugin and skips
agentflare wiring. Update the check in wire_ponytail_claude_code to distinguish
agentflare from the npm ponytail plugin by aligning it with the
has_existing_ponytail_claude logic, i.e. only consider it already wired when the
agentflare-specific hooks are present and the session does not just contain the
npm ponytail marker.
| fn wire_ponytail_cursor() { | ||
| let path = cwd().join(".cursor").join("hooks.json"); | ||
| let bin = agentflare_binary(); | ||
|
|
||
| let mut content: Value = fs::read_to_string(&path) | ||
| .ok() | ||
| .and_then(|s| serde_json::from_str(&s).ok()) | ||
| .unwrap_or_else(|| json!({ "version": 1, "hooks": {} })); | ||
| if !content.is_object() { | ||
| content = json!({ "version": 1, "hooks": {} }); | ||
| } | ||
|
|
||
| let hooks = content.as_object_mut().unwrap() | ||
| .entry("hooks").or_insert_with(|| json!({})); | ||
| let hooks_obj = hooks.as_object_mut().unwrap(); | ||
|
|
||
| hooks_obj.entry("sessionStart").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({ | ||
| "command": format!("\"{bin}\" ponytail hook session-start"), | ||
| "type": "command", | ||
| "timeout": 30 | ||
| })); | ||
| hooks_obj.entry("beforeSubmitPrompt").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({ | ||
| "command": format!("\"{bin}\" ponytail hook prompt-submit"), | ||
| "type": "command", | ||
| "timeout": 10 | ||
| })); | ||
|
|
||
| if let Some(parent) = path.parent() { | ||
| let _ = fs::create_dir_all(parent); | ||
| } | ||
| match fs::write(&path, serde_json::to_string_pretty(&content).unwrap() + "\n") { | ||
| Ok(_) => println!(" ok ponytail hooks wired in .cursor/hooks.json"), | ||
| Err(e) => println!(" fail writing .cursor/hooks.json: {e}"), | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Missing idempotency check — cursor hooks get duplicated on every re-run.
Unlike wire_ponytail_claude_code and wire_ponytail_opencode, this function has no already_wired guard. Re-running init --agent cursor appends another sessionStart / beforeSubmitPrompt entry each time, accumulating duplicate ponytail commands in .cursor/hooks.json.
Proposed guard
let hooks = content.as_object_mut().unwrap()
.entry("hooks").or_insert_with(|| json!({}));
let hooks_obj = hooks.as_object_mut().unwrap();
+ let already_wired = hooks_obj
+ .get("sessionStart")
+ .map(|v| v.to_string().contains("ponytail"))
+ .unwrap_or(false);
+ if already_wired {
+ println!(" skip ponytail hooks already wired in .cursor/hooks.json");
+ return;
+ }
+
hooks_obj.entry("sessionStart").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn wire_ponytail_cursor() { | |
| let path = cwd().join(".cursor").join("hooks.json"); | |
| let bin = agentflare_binary(); | |
| let mut content: Value = fs::read_to_string(&path) | |
| .ok() | |
| .and_then(|s| serde_json::from_str(&s).ok()) | |
| .unwrap_or_else(|| json!({ "version": 1, "hooks": {} })); | |
| if !content.is_object() { | |
| content = json!({ "version": 1, "hooks": {} }); | |
| } | |
| let hooks = content.as_object_mut().unwrap() | |
| .entry("hooks").or_insert_with(|| json!({})); | |
| let hooks_obj = hooks.as_object_mut().unwrap(); | |
| hooks_obj.entry("sessionStart").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({ | |
| "command": format!("\"{bin}\" ponytail hook session-start"), | |
| "type": "command", | |
| "timeout": 30 | |
| })); | |
| hooks_obj.entry("beforeSubmitPrompt").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({ | |
| "command": format!("\"{bin}\" ponytail hook prompt-submit"), | |
| "type": "command", | |
| "timeout": 10 | |
| })); | |
| if let Some(parent) = path.parent() { | |
| let _ = fs::create_dir_all(parent); | |
| } | |
| match fs::write(&path, serde_json::to_string_pretty(&content).unwrap() + "\n") { | |
| Ok(_) => println!(" ok ponytail hooks wired in .cursor/hooks.json"), | |
| Err(e) => println!(" fail writing .cursor/hooks.json: {e}"), | |
| } | |
| } | |
| let already_wired = hooks_obj | |
| .get("sessionStart") | |
| .map(|v| v.to_string().contains("ponytail")) | |
| .unwrap_or(false); | |
| if already_wired { | |
| println!(" skip ponytail hooks already wired in .cursor/hooks.json"); | |
| return; | |
| } | |
| hooks_obj.entry("sessionStart").or_insert_with(|| json!([])).as_array_mut().unwrap().push(json!({ |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/init.rs` around lines 345 - 379, Add an idempotency guard to the cursor
wiring path in the same way as wire_ponytail_claude_code and
wire_ponytail_opencode so repeated init --agent cursor runs do not append
duplicate hooks. Update the cursor hook setup logic in init.rs to detect when
the ponytail commands are already present in .cursor/hooks.json before writing
sessionStart and beforeSubmitPrompt entries, and skip re-adding them if they
exist.
| AgentEntry { name: "cursor-cli", process_names: &[], env_vars: &["CURSOR_AGENT", "CURSOR_EXTENSION_HOST_ROLE"] }, | ||
| AgentEntry { name: "gemini", process_names: &["gemini"], env_vars: &["GEMINI_CLI"] }, | ||
| AgentEntry { name: "codex", process_names: &["codex"], env_vars: &["CODEX_SANDBOX", "CODEX_CI", "CODEX_THREAD_ID"] }, | ||
| AgentEntry { name: "antigravity", process_names: &["amp"], env_vars: &["ANTIGRAVITY_AGENT"] }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n## File context\n'
git ls-files src/ponytail/detect.rs
wc -l src/ponytail/detect.rs
cat -n src/ponytail/detect.rs | sed -n '1,120p'
printf '\n## Search for antigravity/amp references\n'
rg -n "antigravity|ANTIGRAVITY_AGENT|process_names: &\\[\"amp\"\\]|\\bamp\\b|\\bagy\\b" -S .Repository: getappz/agentflare
Length of output: 2225
🌐 Web query:
Google Antigravity CLI binary name agy amp process name
💡 Result:
The Google Antigravity CLI binary is named agy [1][2][3]. On Windows systems, this binary typically appears as agy.exe, while on Unix-like systems (Linux/macOS), it is named agy [4]. Regarding your mention of amp, this refers to a separate, unrelated technology known as Accelerated Mobile Pages (AMP) [5]. In the context of AMP development, there is a development tool binary also named amp (often used to start local dev servers), which is unrelated to the Google Antigravity platform [6][5]. Do not confuse these; the Antigravity CLI (agy) is Google's terminal-based coding agent that replaced the deprecated Gemini CLI [3][7], whereas AMP is an open-source web framework [5].
Citations:
- 1: https://antigravity.google/docs/cli-getting-started
- 2: https://codelabs.developers.google.com/antigravity-cli-hands-on
- 3: https://www.aibuilderclub.com/blog/antigravity-cli-guide
- 4: https://clashkr.com/en/blog/articles/google-antigravity-2-clash-verge-rev-routing-2026.html
- 5: https://en.wikipedia.org/wiki/Accelerated_Mobile_Pages
- 6: https://github.com/ampproject/amphtml/blob/main/docs/testing.md
- 7: https://explainx.ai/blog/antigravity-cli-features-sandbox-plugins-subagents-2026
Use agy for Antigravity, not amp. The Antigravity CLI binary is agy; amp is unrelated, so this entry will miss the real agent and can misclassify the wrong process.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/ponytail/detect.rs` at line 28, The Antigravity agent mapping in detect
logic uses the wrong process name, so update the AgentEntry for the
"antigravity" symbol to match the actual CLI binary. Replace the current
process_names entry with the correct binary name used by Antigravity, and keep
the env_vars mapping unchanged so process detection in detect.rs identifies the
right agent instead of the unrelated one.
wire_ponytail_opencode() was writing invalid keys (hooks, statusLine) to opencode.jsonc. OpenCode hooks are handled by the ponytail npm plugin — config doesn't support them.
Summary
Replaces env-var-only agent detection with process-tree walking (agent-detector style). Auto-wires ponytail hooks via \init --agent X. Detects existing npm ponytail plugin and offers to uninstall.
Closes #43
Changes
Summary by CodeRabbit
New Features
--yes/-yoption to skip prompts during init.Bug Fixes