From 278ba79283b40992b81517a872a96b49a4431e8b Mon Sep 17 00:00:00 2001 From: davidwe Date: Mon, 5 Oct 2026 10:40:14 +0200 Subject: [PATCH 1/2] feat: add agentty agent support to the install CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add automatic detection, skill installation, MCP registration, uninstall, and --clients selection for the agentty coding agent (https://github.com/1ay1/agentty), following the established detected-agent pattern (OpenHands template): - Detection: ~/.agentty/ directory or agentty CLI on PATH; new cbm_detected_agents_t.agentty field probed in cbm_detect_agents() - Selection: --clients=agentty token via the CLI_CLIENTS table - Install: codebase-memory skill to ~/.agentty/skills/ (Claude-compatible SKILL.md; user scope = trusted on load) and MCP server entry in ~/.agentty/mcp.json (standard mcpServers shape via the existing cbm_install_editor_mcp adapter). Instructions/hooks/tiered profiles deliberately withheld — agentty documents no AGENTS.md equivalent. - Uninstall: owned-document matching counterpart; user-modified files and foreign servers survive; skill dir removed only when empty - Docs: README agent table row, pkg/npm/README.md (40 detected), docs/llms.txt + docs/index.html + badge (46 surfaces / 40 detected), contract test cli_supported_agent_surfaces_match_installers and the --help enumeration aligned to 46/40 in lockstep Tests: detection, --clients vocabulary + leak guard, install plan and schema checks, and an install/uninstall roundtrip asserting foreign-server and user-skill preservation in both directions. Signed-off-by: davidwe --- README.md | 11 ++-- docs/index.html | 10 ++-- docs/llms.txt | 2 +- pkg/npm/README.md | 6 +- src/cli/cli.c | 23 +++++++ src/cli/cli.h | 1 + src/main.c | 4 +- tests/test_cli.c | 149 ++++++++++++++++++++++++++++++++++++++++++---- 8 files changed, 178 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index f644775a73..aa233af584 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Tests](https://img.shields.io/badge/tests-8050_passing-brightgreen)](https://github.com/DeusData/codebase-memory-mcp) [![Languages](https://img.shields.io/badge/languages-162-orange)](https://github.com/DeusData/codebase-memory-mcp) [![Hybrid LSP](https://img.shields.io/badge/Hybrid_LSP-10_languages-blue)](#hybrid-lsp) -[![Agents](https://img.shields.io/badge/agent_surfaces-45-purple)](https://github.com/DeusData/codebase-memory-mcp) +[![Agents](https://img.shields.io/badge/agent_surfaces-46-purple)](https://github.com/DeusData/codebase-memory-mcp) [![Pure C](https://img.shields.io/badge/pure_C-no_language_runtime-blue)](https://github.com/DeusData/codebase-memory-mcp) [![Platform](https://img.shields.io/badge/macOS_%7C_Linux_%7C_Windows-supported-lightgrey)](https://github.com/DeusData/codebase-memory-mcp/releases/latest) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/DeusData/codebase-memory-mcp/badge)](https://scorecard.dev/viewer/?uri=github.com/DeusData/codebase-memory-mcp) @@ -16,7 +16,7 @@ **The fastest and most efficient code intelligence engine for AI coding agents.** Full-indexes an average repository in milliseconds, the Linux kernel (28M LOC, 75K files) in 3 minutes. Answers structural queries in under 1ms. Ships as a native executable with a small verified runtime-asset set for macOS, Linux, and Windows — download, run `install`, done. -High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST analysis across all 162 languages, enhanced with [**Hybrid LSP** semantic type resolution](#hybrid-lsp) for Python, TypeScript / JavaScript / JSX / TSX, PHP, C#, Go, C, C++, Java, Kotlin, Rust, and Perl — producing a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. 17 MCP tools. No language runtime, hosted service, or API key. Plug and play across 45 supported automatic/conditional client surfaces. +High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST analysis across all 162 languages, enhanced with [**Hybrid LSP** semantic type resolution](#hybrid-lsp) for Python, TypeScript / JavaScript / JSX / TSX, PHP, C#, Go, C, C++, Java, Kotlin, Rust, and Perl — producing a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. 17 MCP tools. No language runtime, hosted service, or API key. Plug and play across 46 supported automatic/conditional client surfaces. > **Research** — The design and benchmarks behind this project are described in the preprint [*Codebase-Memory: Tree-Sitter-Based Knowledge Graphs for LLM Code Exploration via MCP*](https://arxiv.org/abs/2603.27277) (arXiv:2603.27277). Evaluated across 31 real-world repositories: 83% answer quality, 10× fewer tokens, 2.1× fewer tool calls vs. file-by-file exploration. @@ -34,7 +34,7 @@ High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-si - **Plug and play** — native executable plus authenticated release-owned assets for macOS (arm64/amd64), Linux (arm64/amd64), and Windows (amd64). The native install needs no Docker, language runtime, or API keys. Download → `install` → restart agent → done. - **162 languages** — vendored tree-sitter grammars compiled into the binary. Nothing to install, nothing that breaks. - **120x fewer tokens** — 5 structural queries: ~3,400 tokens vs ~412,000 via file-by-file search. One graph query replaces dozens of grep/read cycles. -- **45 supported automatic/conditional client surfaces** — `install` configures detected clients and safely activates conditional clients only when their documented platform, marker, or explicit existing config path is present. See [Multi-Agent Support](#multi-agent-support) for the complete matrix and manual/UI-only boundaries. +- **46 supported automatic/conditional client surfaces** — `install` configures detected clients and safely activates conditional clients only when their documented platform, marker, or explicit existing config path is present. See [Multi-Agent Support](#multi-agent-support) for the complete matrix and manual/UI-only boundaries. - **Built-in graph visualization** — 3D interactive UI at `localhost:9749`, served from the binary itself. - **Infrastructure-as-code indexing** — Dockerfiles, Kubernetes manifests, and Kustomize overlays indexed as graph nodes with cross-references. `Resource` nodes for K8s kinds, `Module` nodes for Kustomize overlays with `IMPORTS` edges to referenced resources. - **17 MCP tools** — search, trace, architecture, impact analysis, targeted index-coverage checks, Cypher queries, dead code detection, cross-service HTTP linking, ADR management, and more. @@ -476,7 +476,7 @@ Restart your agent. Verify with `/mcp` — you should see `codebase-memory-mcp` ## Multi-Agent Support -`install` configures 45 client surfaces: 39 detected automatically and 6 +`install` configures 46 client surfaces: 40 detected automatically and 6 conditional or explicit. “Conditional” means the installer writes only when the documented platform or an explicit, already-existing config path proves the target is active. It never flips experimental feature flags, enables plugins, @@ -543,6 +543,7 @@ overwrite user-modified agents. | Pi | Detected | — | `~/.pi/agent/AGENTS.md` + skill; MCP/subagents require an explicit reviewed extension | | IBM Bob IDE | Conditional | Existing `~/.bob/mcp.json` | Shared rule + IDE skill; no invented hook or agent | | Oh My Pi (omp) | Detected | Effective agent directory (`OMP_PROFILE` / `PI_CODING_AGENT_DIR`; default `~/.omp/agent/mcp.json`) | Skill and three direct-MCP graph-tool subagents (Scout/Verify/Auditor); preserves user `AGENTS.md` | +| agentty | Detected | `~/.agentty/mcp.json` | Skill in `~/.agentty/skills` (Claude-compatible SKILL.md) | | Sourcegraph Cody | Explicit opt-in | Existing `$CBM_CODY_CONFIG_PATH` | MCP only | For Codex, install keeps only a tiny managed activation pointer in global @@ -891,7 +892,7 @@ src/ main.c Entry point (MCP stdio server + CLI + install/update/config) daemon/ Per-account session coordination, IPC, lifecycle, shared jobs/watchers mcp/ MCP server (17 tools, JSON-RPC 2.0, session detection, auto-index) - cli/ Install/uninstall/update/config (45 client surfaces, hooks, instructions) + cli/ Install/uninstall/update/config (46 client surfaces, hooks, instructions) store/ SQLite graph storage (nodes, edges, traversal, search, Louvain) pipeline/ Multi-pass indexing (structure → definitions → calls → HTTP links → config → tests) cypher/ Cypher query lexer, parser, planner, executor diff --git a/docs/index.html b/docs/index.html index fdfde941e6..88480526e1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,7 +4,7 @@ codebase-memory-mcp — Code Intelligence Knowledge Graph for AI Coding Agents - + @@ -68,7 +68,7 @@ "Infrastructure-as-code indexing for Dockerfiles, Kubernetes, and Kustomize", "Built-in 3D graph visualization UI", "Auto-sync background watcher for incremental re-indexing", - "One command configures 45 automatic/conditional client surfaces" + "One command configures 46 automatic/conditional client surfaces" ], "author": { "@type": "Organization", @@ -157,7 +157,7 @@ "name": "Which AI coding agents work with codebase-memory-mcp?", "acceptedAnswer": { "@type": "Answer", - "text": "A single install command configures 45 automatic/conditional client surfaces. The 39 detected surfaces are Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob Shell, Pochi, Pi, and Oh My Pi (omp). Continue / cn, Visual Studio, TRAE, Roo Code, IBM Bob IDE, and Sourcegraph Cody are conditional or explicit integrations. Qodo, Warp MCP, JetBrains AI/ACP, GitHub Copilot coding agent, Jules, CodeRabbit, Replit, BLACKBOX AI, Plandex, and SWE-agent require manual, UI, cloud, or repository-managed setup and are not counted among the 45." + "text": "A single install command configures 46 automatic/conditional client surfaces. The 40 detected surfaces are Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob Shell, Pochi, Pi, Oh My Pi (omp), and agentty. Continue / cn, Visual Studio, TRAE, Roo Code, IBM Bob IDE, and Sourcegraph Cody are conditional or explicit integrations. Qodo, Warp MCP, JetBrains AI/ACP, GitHub Copilot coding agent, Jules, CodeRabbit, Replit, BLACKBOX AI, Plandex, and SWE-agent require manual, UI, cloud, or repository-managed setup and are not counted among the 46." } }, { @@ -527,12 +527,12 @@

How do I install codebase-memory-mcp?

"Index this project"

- One command configures 45 automatic/conditional client surfaces. Detected automatically (39): + One command configures 46 automatic/conditional client surfaces. Detected automatically (40): Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Amazon Q Developer IDE, - CodeBuddy Code CLI, IBM Bob Shell, Pochi, Pi, and Oh My Pi (omp). + CodeBuddy Code CLI, IBM Bob Shell, Pochi, Pi, Oh My Pi (omp), and agentty.

Conditional or explicit (6): Continue / cn, Visual Studio, TRAE, Roo Code, IBM Bob IDE, diff --git a/docs/llms.txt b/docs/llms.txt index f3c23970b5..b0f76c2e6f 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -12,7 +12,7 @@ - Semantic & similarity edges: SEMANTICALLY_RELATED (vocabulary-mismatch matches) and SIMILAR_TO (MinHash + LSH near-clone / duplicate detection). - Cross-repo intelligence: CROSS_* edges link nodes across multiple repos indexed in one store; multi-galaxy 3D layout and cross-repo architecture summary. - Cross-service linking: HTTP route ↔ call-site matching, plus gRPC/GraphQL/tRPC detection and pub/sub channels (EMITS/LISTENS_ON for Socket.IO, EventEmitter, generic buses). -- Supported agents: 45 automatic/conditional client surfaces (39 automatically detected + 6 conditional/explicit): Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Continue / cn, Visual Studio, TRAE, Roo Code, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob IDE, IBM Bob Shell, Pochi, Pi, Oh My Pi (omp), and Sourcegraph Cody. +- Supported agents: 46 automatic/conditional client surfaces (40 automatically detected + 6 conditional/explicit): Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Continue / cn, Visual Studio, TRAE, Roo Code, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob IDE, IBM Bob Shell, Pochi, Pi, Oh My Pi (omp), agentty, and Sourcegraph Cody. - Agent profiles: documented custom-agent formats receive Scout (fast/provisional), Verify (default/task-directed), and Auditor (bounded/full verification) definitions. Every direct tier checks exact path/scope coverage with check_index_coverage and falls back to source for flagged gaps; unsafe child-MCP formats use explicit parent handoff. Kiro and Junie use positive-allowlist Scout/Analysis server profiles (7/11 tools); Qoder combines named-server selection with exact tier-specific MCP tool IDs, while Factory uses exact registered IDs without additive whole-server exposure. Foreign Junie aliases are preserved and force parent handoff. - Performance: Linux kernel (28M LOC, 75K files) full index in 3 minutes → 4.81M nodes, 7.72M edges; Cypher queries in under 1ms. - Distribution: verified runtime set (native executable plus authenticated release-owned assets); fully static Linux builds are available in the `-portable` archives. Also available through npm, PyPI, Homebrew, Scoop, Winget, Chocolatey, AUR, and `go install`. diff --git a/pkg/npm/README.md b/pkg/npm/README.md index c12a2d9c63..d41821a31a 100644 --- a/pkg/npm/README.md +++ b/pkg/npm/README.md @@ -7,7 +7,7 @@ **The fastest and most efficient code intelligence engine for AI coding agents.** Full-indexes an average repository in milliseconds, the Linux kernel (28M LOC, 75K files) in 3 minutes. Answers structural queries in under 1ms. This npm wrapper downloads, verifies, and caches the selected native runtime set: the executable, its authenticated integration asset, and—when requested—the content-addressed UI pack. -High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST analysis across 162 languages — producing a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. 17 MCP tools. No hosted service or API key; this package requires Node.js to manage and launch the cached native runtime set. Plug and play across 45 automatic/conditional client surfaces. +High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST analysis across 162 languages — producing a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. 17 MCP tools. No hosted service or API key; this package requires Node.js to manage and launch the cached native runtime set. Plug and play across 46 automatic/conditional client surfaces. ## Installation @@ -31,8 +31,8 @@ Restart your agent. Say **"Index this project"** — done. - **Plug and play** — verified native runtime sets for macOS (arm64/amd64), Linux (arm64/amd64), and Windows (arm64/amd64). No Docker or API keys; Node.js owns package download, cache repair, and launch. - **162 languages** — vendored tree-sitter grammars compiled into the binary. Nothing to install, nothing that breaks. - **120x fewer tokens** — 5 structural queries: ~3,400 tokens vs ~412,000 via file-by-file search. -- **45 supported automatic/conditional client surfaces** — `install` configures the appropriate MCP, durable-context, and documented hook surfaces without widening client permissions. -- **Detected automatically (39)** — Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob Shell, Pochi, Pi, and Oh My Pi (omp). +- **46 supported automatic/conditional client surfaces** — `install` configures the appropriate MCP, durable-context, and documented hook surfaces without widening client permissions. +- **Detected automatically (40)** — Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf, Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands, Cline, Warp, Qwen Code, GitHub Copilot CLI, Factory Droid, Crush, Goose, Mistral Vibe, Grok Build, Qoder CLI, Kimi Code CLI, GitLab Duo CLI, Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Amazon Q Developer IDE, CodeBuddy Code CLI, IBM Bob Shell, Pochi, Pi, Oh My Pi (omp), and agentty. - **Conditional or explicit (6)** — Continue / cn, Visual Studio, TRAE, Roo Code, IBM Bob IDE, and Sourcegraph Cody. Bob IDE is touched only when `~/.bob/mcp.json` already exists. - **New documented adapters** — CodeBuddy uses `~/.codebuddy/.mcp.json` while preserving active older files; Bob Shell uses `~/.bob/mcp_settings.json`; Pochi uses the `mcp` section in `~/.pochi/config.jsonc`; Amazon Q Developer IDE defaults to `~/.aws/amazonq/default.json` while preserving either documented alternative. - **Lifecycle hooks stay conservative** — Kimi uses `UserPromptSubmit`; on macOS/Linux, GitLab Duo gets a fail-open user `SessionStart`, while Devin gets `UserPromptSubmit`, `PostCompaction`, and a deduplicated `SessionStart` when Claude does not already provide it. Qoder, GitLab Duo, Devin, and Factory hooks are withheld on Windows without a documented shell/executor contract. Cline's auto-activating file hooks are withheld because their context output is not reliably consumed, CodeBuddy beta hooks are not auto-installed, and Cursor context hooks remain withheld. diff --git a/src/cli/cli.c b/src/cli/cli.c index df51e723b4..665cbf5c8f 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -3295,6 +3295,9 @@ cbm_detected_agents_t cbm_detect_agents(const char *home_dir) { cbm_grok_config_dir(home_dir, path, sizeof(path)); agents.grok = dir_exists(path) || cbm_agent_cli_exists("grok", home_dir); + snprintf(path, sizeof(path), "%s/.agentty", home_dir); + agents.agentty = dir_exists(path) || cbm_agent_cli_exists("agentty", home_dir); + return agents; } @@ -10524,6 +10527,15 @@ static void install_additional_agent_configs(const cbm_detected_agents_t *agents "contract)\n"); } } + if (agents->agentty) { + char cp[CLI_BUF_1K]; + char skills_dir[CLI_BUF_1K]; + snprintf(cp, sizeof(cp), "%s/.agentty/mcp.json", home); + snprintf(skills_dir, sizeof(skills_dir), "%s/.agentty/skills", home); + install_generic_agent_config("agentty", binary_path, cp, NULL, dry_run, + cbm_install_editor_mcp); + install_agent_skill("agentty", skills_dir, force, dry_run); + } } static int cbm_install_agent_configs_in_scope(const char *home, const char *binary_path, bool force, @@ -10706,6 +10718,7 @@ static const cli_client_def_t CLI_CLIENTS[] = { CLI_CLIENT(goose, "goose", "Goose"), CLI_CLIENT(mistral_vibe, "mistral-vibe", "Mistral Vibe"), CLI_CLIENT(grok, "grok", "Grok Build"), + CLI_CLIENT(agentty, "agentty", "agentty"), }; enum { CLI_CLIENT_COUNT = sizeof(CLI_CLIENTS) / sizeof(CLI_CLIENTS[0]) }; @@ -11006,6 +11019,7 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b {det.goose, "goose"}, {det.mistral_vibe, "mistral-vibe"}, {det.grok, "grok"}, + {det.agentty, "agentty"}, }; yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); @@ -12785,6 +12799,15 @@ static void uninstall_additional_agents(const cbm_detected_agents_t *agents, con }, dry_run); } + if (agents->agentty) { + char cp[CLI_BUF_1K]; + char skills_dir[CLI_BUF_1K]; + snprintf(cp, sizeof(cp), "%s/.agentty/mcp.json", home); + snprintf(skills_dir, sizeof(skills_dir), "%s/.agentty/skills", home); + uninstall_agent_mcp_instr((mcp_uninstall_args_t){"agentty", cp, NULL}, dry_run, + cbm_remove_editor_mcp_owned); + uninstall_agent_skill("agentty", skills_dir, dry_run); + } } typedef struct { diff --git a/src/cli/cli.h b/src/cli/cli.h index e5e10417cd..0663c06031 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -208,6 +208,7 @@ typedef struct { bool goose; /* Goose config or CLI exists */ bool mistral_vibe; /* $VIBE_HOME, ~/.vibe/, or vibe CLI exists */ bool grok; /* $GROK_HOME, ~/.grok/, or grok CLI exists */ + bool agentty; /* ~/.agentty/ or agentty CLI exists */ } cbm_detected_agents_t; /* Detect which coding agents are installed. diff --git a/src/main.c b/src/main.c index 6aabf5a92b..7171ed4157 100644 --- a/src/main.c +++ b/src/main.c @@ -1123,7 +1123,7 @@ static void print_help(void) { printf(" --ui=false Disable HTTP graph visualization (persisted)\n"); printf(" --port=N Set UI port (default 9749, persisted)\n"); printf(" --tool-profile=analysis|scout Expose a restricted inspection surface\n"); - printf("\nSupported automatic/conditional client surfaces (45):\n"); + printf("\nSupported automatic/conditional client surfaces (46):\n"); printf(" Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode,\n"); printf(" Antigravity, Aider, KiloCode, VS Code, Cursor, Windsurf,\n"); printf(" Augment / Auggie, OpenClaw, Kiro, Junie, Hermes, OpenHands,\n"); @@ -1132,7 +1132,7 @@ static void print_help(void) { printf(" Rovo Dev CLI, Amp, Devin CLI / Local, Tabnine, Continue / cn,\n"); printf(" Visual Studio, TRAE, Roo Code, Amazon Q Developer IDE,\n"); printf(" CodeBuddy Code CLI, IBM Bob IDE, IBM Bob Shell, Pochi, Pi,\n"); - printf(" Sourcegraph Cody, Oh My Pi (omp)\n"); + printf(" Sourcegraph Cody, Oh My Pi (omp), agentty\n"); printf(" Conditional/explicit targets are changed only when their documented\n"); printf(" platform, marker, or explicit existing config path is present.\n"); printf(" Manual/UI MCP boundaries: Qodo, Warp, JetBrains AI/ACP, Replit,\n"); diff --git a/tests/test_cli.c b/tests/test_cli.c index a77abc0a31..4e80da18b2 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -6743,6 +6743,23 @@ TEST(cli_detect_agents_finds_cursor_issue222) { PASS(); } +TEST(cli_detect_agents_finds_agentty) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char dir[512]; + snprintf(dir, sizeof(dir), "%s/.agentty", tmpdir); + test_mkdirp(dir); + + cbm_detected_agents_t agents = cbm_detect_agents(tmpdir); + ASSERT_TRUE(agents.agentty); + + test_rmdir_r(tmpdir); + PASS(); +} + /* issue #388: `install --plan` must emit a machine-readable receipt of planned * writes WITHOUT mutating any config (the pre-mutation trust primitive). */ TEST(cli_install_plan_receipt_no_mutation_issue388) { @@ -6860,14 +6877,15 @@ TEST(cli_supported_agent_surfaces_match_installers) { "Pi", "Sourcegraph Cody", "Oh My Pi (omp)", + "agentty", }; - ASSERT_EQ(sizeof(required_agents) / sizeof(required_agents[0]), 45U); + ASSERT_EQ(sizeof(required_agents) / sizeof(required_agents[0]), 46U); char *data = read_test_file_alloc("README.md"); if (!data) FAIL("could not read README.md for supported-agent contract"); - if (!strstr(data, "45 supported automatic/conditional client surfaces")) { + if (!strstr(data, "46 supported automatic/conditional client surfaces")) { free(data); - FAIL("README must describe all 45 automatic/conditional client surfaces accurately"); + FAIL("README must describe all 46 automatic/conditional client surfaces accurately"); } for (size_t i = 0; i < sizeof(required_agents) / sizeof(required_agents[0]); i++) { if (!strstr(data, required_agents[i])) { @@ -6880,9 +6898,9 @@ TEST(cli_supported_agent_surfaces_match_installers) { data = read_test_file_alloc("pkg/npm/README.md"); if (!data) FAIL("could not read npm README for supported-agent contract"); - if (!strstr(data, "45 supported automatic/conditional client surfaces")) { + if (!strstr(data, "46 supported automatic/conditional client surfaces")) { free(data); - FAIL("npm README must describe all 45 automatic/conditional client surfaces accurately"); + FAIL("npm README must describe all 46 automatic/conditional client surfaces accurately"); } for (size_t i = 0; i < sizeof(required_agents) / sizeof(required_agents[0]); i++) { if (!strstr(data, required_agents[i])) { @@ -6895,9 +6913,9 @@ TEST(cli_supported_agent_surfaces_match_installers) { data = read_test_file_alloc("docs/index.html"); if (!data) FAIL("could not read docs/index.html for supported-agent contract"); - if (!strstr(data, "configures 45 automatic/conditional client surfaces")) { + if (!strstr(data, "configures 46 automatic/conditional client surfaces")) { free(data); - FAIL("landing page must describe all 45 automatic/conditional client surfaces accurately"); + FAIL("landing page must describe all 46 automatic/conditional client surfaces accurately"); } for (size_t i = 0; i < sizeof(required_agents) / sizeof(required_agents[0]); i++) { if (!strstr(data, required_agents[i])) { @@ -6916,7 +6934,7 @@ TEST(cli_supported_agent_surfaces_match_installers) { FAIL("CLI help must list every automatic/conditional client surface"); } } - if (!strstr(data, "Supported automatic/conditional client surfaces (45)")) { + if (!strstr(data, "Supported automatic/conditional client surfaces (46)")) { free(data); FAIL("CLI help must not describe all conditional surfaces as auto-detected"); } @@ -6925,10 +6943,10 @@ TEST(cli_supported_agent_surfaces_match_installers) { data = read_test_file_alloc("docs/llms.txt"); if (!data) FAIL("could not read docs/llms.txt for supported-agent contract"); - if (!strstr(data, "45 automatic/conditional client surfaces") || - !strstr(data, "39 automatically detected") || !strstr(data, "6 conditional/explicit")) { + if (!strstr(data, "46 automatic/conditional client surfaces") || + !strstr(data, "40 automatically detected") || !strstr(data, "6 conditional/explicit")) { free(data); - FAIL("llms.txt must describe the 45-surface 39+6 support matrix accurately"); + FAIL("llms.txt must describe the 46-surface 40+6 support matrix accurately"); } for (size_t i = 0; i < sizeof(required_agents) / sizeof(required_agents[0]); i++) { if (!strstr(data, required_agents[i])) { @@ -6976,6 +6994,7 @@ TEST(cli_new_agent_install_plans_use_documented_paths) { #endif ".vibe", ".grok", + ".agentty", }; char path[768]; for (size_t i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) { @@ -7026,6 +7045,9 @@ TEST(cli_new_agent_install_plans_use_documented_paths) { "/.grok/rules/codebase-memory.md", "/.grok/skills/codebase-memory/SKILL.md", "/.grok/agents/codebase-memory.md", + "\"agentty\"", + "/.agentty/mcp.json", + "/.agentty/skills/codebase-memory/SKILL.md", }; const char *missing = NULL; for (size_t i = 0; json && i < sizeof(expected) / sizeof(expected[0]); i++) { @@ -7085,6 +7107,7 @@ TEST(cli_new_agent_configs_use_documented_schemas) { #endif ".vibe", ".grok", + ".agentty", }; char path[768]; for (size_t i = 0; i < sizeof(dirs) / sizeof(dirs[0]); i++) { @@ -7193,6 +7216,13 @@ TEST(cli_new_agent_configs_use_documented_schemas) { snprintf(path, sizeof(path), "%s/.grok/rules/codebase-memory.md", tmpdir); schemas_ok = schemas_ok && test_file_contains_all(path, durable_hint, 3); + /* agentty consumes the standard mcpServers shape and Claude-compatible + * SKILL.md — same fixtures as OpenHands/Cline. */ + snprintf(path, sizeof(path), "%s/.agentty/mcp.json", tmpdir); + schemas_ok = schemas_ok && test_file_contains_all(path, standard_json, 3); + snprintf(path, sizeof(path), "%s/.agentty/skills/codebase-memory/SKILL.md", tmpdir); + schemas_ok = schemas_ok && test_file_contains_all(path, shared_skill, 3); + for (size_t i = 0; i < sizeof(env_names) / sizeof(env_names[0]); i++) { restore_test_env(env_names[i], saved_env[i]); } @@ -7202,6 +7232,89 @@ TEST(cli_new_agent_configs_use_documented_schemas) { PASS(); } +TEST(cli_agentty_install_then_uninstall_roundtrip) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-agentty-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char *saved_home = save_test_env("HOME"); + char *saved_path = save_test_env("PATH"); + char *saved_cache = save_test_env("CBM_CACHE_DIR"); + cbm_setenv("HOME", tmpdir, 1); + cbm_setenv("PATH", tmpdir, 1); + cbm_unsetenv("CBM_CACHE_DIR"); + + char agentty_dir[512]; + char mcp_path[512]; + char skills_dir[512]; + char skill_path[640]; + char user_dir[512]; + char user_file[640]; + char binary[512]; + snprintf(agentty_dir, sizeof(agentty_dir), "%s/.agentty", tmpdir); + snprintf(mcp_path, sizeof(mcp_path), "%s/mcp.json", agentty_dir); + snprintf(skills_dir, sizeof(skills_dir), "%s/skills", agentty_dir); + snprintf(skill_path, sizeof(skill_path), "%s/codebase-memory/SKILL.md", skills_dir); + snprintf(user_dir, sizeof(user_dir), "%s/keepme", skills_dir); + snprintf(user_file, sizeof(user_file), "%s/SKILL.md", user_dir); +#ifdef _WIN32 + snprintf(binary, sizeof(binary), "%s/.local/bin/codebase-memory-mcp.exe", tmpdir); +#else + snprintf(binary, sizeof(binary), "%s/.local/bin/codebase-memory-mcp", tmpdir); +#endif + + test_mkdirp(agentty_dir); + test_mkdirp(user_dir); + write_test_file(user_file, "user-owned skill\n"); + write_test_file(binary, ""); + /* Seed a foreign server so install/uninstall must preserve it. */ + write_test_file(mcp_path, "{\"mcpServers\":{\"other-mcp\":{\"command\":\"/opt/other\"}}}\n"); + + cbm_install_agent_configs(tmpdir, binary, false, false); + + struct stat state; + const char *const installed_markers[] = {"mcpServers", "codebase-memory-mcp", binary}; + bool installed = test_file_contains_all(mcp_path, installed_markers, 3) && + stat(skill_path, &state) == 0; + char *mcp_after_install = read_test_file_alloc(mcp_path); + bool foreign_survives_install = mcp_after_install && + strstr(mcp_after_install, "\"other-mcp\"") != NULL; + + char *argv[] = {"uninstall", "--yes"}; + int uninstall_rc = cli_test_cmd_uninstall(2, argv); + + bool skill_removed = stat(skill_path, &state) != 0; + char *mcp_after_uninstall = read_test_file_alloc(mcp_path); + bool mcp_entry_removed = + mcp_after_uninstall && strstr(mcp_after_uninstall, "codebase-memory-mcp") == NULL; + bool user_kept = stat(user_file, &state) == 0; + bool foreign_kept = mcp_after_uninstall && strstr(mcp_after_uninstall, "\"other-mcp\"") != NULL; + + free(mcp_after_install); + free(mcp_after_uninstall); + restore_test_env("HOME", saved_home); + restore_test_env("PATH", saved_path); + restore_test_env("CBM_CACHE_DIR", saved_cache); + test_rmdir_r(tmpdir); + + if (!installed) + FAIL("install must register the codebase-memory server and skill for agentty"); + if (!foreign_survives_install) + FAIL("install must preserve a pre-existing foreign server in ~/.agentty/mcp.json"); + if (!skill_removed) + FAIL("uninstall must remove the codebase-memory skill directory"); + if (!mcp_entry_removed) + FAIL("uninstall must remove the owned codebase-memory MCP entry"); + if (!user_kept) + FAIL("uninstall must not touch user-owned skills in ~/.agentty/skills/"); + if (!foreign_kept) + FAIL("uninstall must preserve foreign servers in ~/.agentty/mcp.json"); + if (uninstall_rc != 0) + FAIL("uninstall must exit successfully for the agentty roundtrip"); + PASS(); +} + TEST(cli_agent_reinstall_preserves_foreign_policy_entries) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-agent-policy-XXXXXX"); @@ -17332,6 +17445,16 @@ TEST(cli_clients_selector_vocabulary_is_complete_and_strict_issue1558) { ASSERT_FALSE(registry.claude_code); ASSERT_FALSE(registry.cursor); + /* agentty is a detected agent, but it must still be selectable and + * filtered: a selection that omits it must zero it, or a user asking for + * "claude,codex" silently gets agentty configured too. */ + cbm_detected_agents_t agentty = all; + ASSERT_TRUE(cbm_cli_clients_apply_selection_for_testing("agentty", &agentty)); + ASSERT_TRUE(agentty.agentty); + cbm_detected_agents_t leak = all; + ASSERT_TRUE(cbm_cli_clients_apply_selection_for_testing("claude,codex", &leak)); + ASSERT_FALSE(leak.agentty); + /* Every token in the table must resolve — a client added to detection but * forgotten here is invisible to the selector. */ for (size_t i = 0; i < cbm_cli_clients_count_for_testing(); i++) { @@ -17846,7 +17969,7 @@ SUITE(cli) { RUN_TEST(cli_yaml_parse_empty); RUN_TEST(cli_yaml_has); - /* Agent detection (6 tests — group A) */ + /* Agent detection (7 tests — group A) */ RUN_TEST(cli_detect_agents_finds_claude); RUN_TEST(cli_detect_agents_finds_claude_via_env); RUN_TEST(cli_detect_claude_empty_dir_not_detected_issue1180); @@ -17859,6 +17982,8 @@ SUITE(cli) { RUN_TEST(cli_detect_agents_finds_codex); RUN_TEST(cli_detect_agents_finds_grok); RUN_TEST(cli_detect_agents_finds_cursor_issue222); + RUN_TEST(cli_detect_agents_finds_agentty); + RUN_TEST(cli_agentty_install_then_uninstall_roundtrip); RUN_TEST(cli_install_plan_receipt_no_mutation_issue388); RUN_TEST(cli_supported_agent_surfaces_match_installers); RUN_TEST(cli_new_agent_install_plans_use_documented_paths); From 5ba7c67213d32120f71dda09e19ca1a6124bb1d7 Mon Sep 17 00:00:00 2001 From: davidwed Date: Thu, 8 Oct 2026 11:46:36 +0200 Subject: [PATCH 2/2] feat(cli): register agentty in the agent-client registry, honor $AGENTTY_MCP_CONFIG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Move agentty from the legacy detector to the table-driven client registry (src/cli/agent_clients.c), the home of every client added since Grok Build. The entry carries its stability class, capability bits (MCP | SKILL), detection command, and install/remove callbacks, which gives detection output, --clients selection, --plan receipts, and uninstall from that one row. The legacy path — cbm_detected_agents_t.agentty, the CLI_CLIENTS token, the plan-names row, and the hand-written install/uninstall blocks — is removed, so print_detected_agents no longer shows a machine with only agentty as "(none)" while installing anyway. 2. Honor $AGENTTY_MCP_CONFIG in both directions. When the variable is set, install and uninstall target that explicit file (with leading-~ expansion against HOME, matching CLAUDE_CONFIG_DIR/GROK_HOME/ VIBE_HOME) and leave ~/.agentty/mcp.json untouched, closing the gap where install reported success while agentty read only its override. Also drop the stale "7 tests" count from the suite comment and cover the registry entry, the env override, and the override-aware install/uninstall roundtrip in the CLI and registry suites. Signed-off-by: davidwed --- README.md | 2 +- src/cli/agent_clients.c | 50 +++++++++++++++++ src/cli/agent_clients.h | 8 +++ src/cli/cli.c | 35 ++++-------- src/cli/cli.h | 4 +- tests/test_agent_clients.c | 3 +- tests/test_cli.c | 111 ++++++++++++++++++++++++++++++++++--- 7 files changed, 179 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e300c66764..d52dc5e308 100644 --- a/README.md +++ b/README.md @@ -544,7 +544,7 @@ overwrite user-modified agents. | Pi | Detected | — | `~/.pi/agent/AGENTS.md` + skill; MCP/subagents require an explicit reviewed extension | | IBM Bob IDE | Conditional | Existing `~/.bob/mcp.json` | Shared rule + IDE skill; no invented hook or agent | | Oh My Pi (omp) | Detected | Effective agent directory (`OMP_PROFILE` / `PI_CODING_AGENT_DIR`; default `~/.omp/agent/mcp.json`) | Skill and three direct-MCP graph-tool subagents (Scout/Verify/Auditor); preserves user `AGENTS.md` | -| agentty | Detected | `~/.agentty/mcp.json` | Skill in `~/.agentty/skills` (Claude-compatible SKILL.md) | +| agentty | Detected | `~/.agentty/mcp.json` (overridden by `$AGENTTY_MCP_CONFIG`) | Skill in `~/.agentty/skills` (Claude-compatible SKILL.md) | | Sourcegraph Cody | Explicit opt-in | Existing `$CBM_CODY_CONFIG_PATH` | MCP only | For Codex, install keeps only a tiny managed activation pointer in global diff --git a/src/cli/agent_clients.c b/src/cli/agent_clients.c index 4c07a583d1..b3b6deb48a 100644 --- a/src/cli/agent_clients.c +++ b/src/cli/agent_clients.c @@ -76,6 +76,9 @@ static const cbm_agent_client_profile_t agent_profiles[CBM_AGENT_CLIENT_COUNT] = {CBM_AGENT_CLIENT_OMP, "omp", "Oh My Pi (omp)", CBM_AGENT_STABLE, CBM_AGENT_CAP_MCP | CBM_AGENT_CAP_SKILL | CBM_AGENT_CAP_AGENT, "omp", agent_install_callback, agent_remove_callback}, + {CBM_AGENT_CLIENT_AGENTTY, "agentty", "agentty", CBM_AGENT_STABLE, + CBM_AGENT_CAP_MCP | CBM_AGENT_CAP_SKILL, "agentty", agent_install_callback, + agent_remove_callback}, }; size_t cbm_agent_client_count(void) { @@ -601,6 +604,15 @@ int cbm_agent_client_resolve_path(cbm_agent_client_id_t id, int written = snprintf(path_out, path_out_size, "%s", options->cody_config_path); return written >= 0 && (size_t)written < path_out_size ? 0 : -1; } + case CBM_AGENT_CLIENT_AGENTTY: { + char override[1024]; + if (cbm_agent_env_config_override("AGENTTY_MCP_CONFIG", options->home_dir, override, + sizeof(override))) { + int written = snprintf(path_out, path_out_size, "%s", override); + return written >= 0 && (size_t)written < path_out_size ? 0 : -1; + } + return agent_join_path(path_out, path_out_size, options->home_dir, ".agentty/mcp.json"); + } default: return -1; } @@ -663,6 +675,8 @@ static int agent_client_marker_path(cbm_agent_client_id_t id, return agent_join_path(path_out, path_out_size, options->home_dir, ".omp/agent"); case CBM_AGENT_CLIENT_PI: return agent_join_path(path_out, path_out_size, options->home_dir, ".pi/agent"); + case CBM_AGENT_CLIENT_AGENTTY: + return agent_join_path(path_out, path_out_size, options->home_dir, ".agentty"); default: return 1; } @@ -1466,6 +1480,7 @@ static bool agent_json_client(cbm_agent_client_id_t id) { case CBM_AGENT_CLIENT_IBM_BOB_SHELL: case CBM_AGENT_CLIENT_POCHI: case CBM_AGENT_CLIENT_SOURCEGRAPH_CODY: + case CBM_AGENT_CLIENT_AGENTTY: return true; case CBM_AGENT_CLIENT_CONTINUE: case CBM_AGENT_CLIENT_PI: @@ -1489,6 +1504,41 @@ int cbm_agent_client_install_mcp(cbm_agent_client_id_t id, const char *config_pa : CBM_AGENT_EDIT_NOT_APPLICABLE; } +bool cbm_agent_env_config_override(const char *env_name, const char *home_dir, char *resolved, + size_t resolved_size) { + if (!env_name || !resolved || resolved_size == 0U) { + return false; + } + char value[1024]; + const char *configured = getenv(env_name); + if (!configured || configured[0] == '\0' || strlen(configured) >= sizeof(value)) { + return false; + } + snprintf(value, sizeof(value), "%s", configured); + /* Expand a leading ~ against the home directory, matching the + * CLAUDE_CONFIG_DIR / GROK_HOME handling in cli.c. */ + if (value[0] == '~' && (value[1] == '\0' || value[1] == '/')) { + if (!home_dir || home_dir[0] == '\0') { + return false; + } + char expanded[1024]; + int written = value[1] == '\0' + ? snprintf(expanded, sizeof(expanded), "%s", home_dir) + : snprintf(expanded, sizeof(expanded), "%s%s", home_dir, value + 1); + if (written < 0 || (size_t)written >= sizeof(expanded) || + (size_t)written >= resolved_size) { + return false; + } + snprintf(resolved, resolved_size, "%s", expanded); + return true; + } + if (strlen(value) >= resolved_size) { + return false; + } + snprintf(resolved, resolved_size, "%s", value); + return true; +} + int cbm_agent_client_remove_mcp(cbm_agent_client_id_t id, const char *config_path, const char *binary_path) { if (!config_path || config_path[0] == '\0' || !binary_path || binary_path[0] == '\0' || diff --git a/src/cli/agent_clients.h b/src/cli/agent_clients.h index ccf9f81a16..ef1a62ede8 100644 --- a/src/cli/agent_clients.h +++ b/src/cli/agent_clients.h @@ -32,6 +32,7 @@ typedef enum { CBM_AGENT_CLIENT_PI, CBM_AGENT_CLIENT_SOURCEGRAPH_CODY, CBM_AGENT_CLIENT_OMP, + CBM_AGENT_CLIENT_AGENTTY, CBM_AGENT_CLIENT_COUNT } cbm_agent_client_id_t; @@ -115,6 +116,13 @@ int cbm_agent_client_install_mcp(cbm_agent_client_id_t id, const char *config_pa int cbm_agent_client_remove_mcp(cbm_agent_client_id_t id, const char *config_path, const char *binary_path); +/* Shared env-override resolution for clients whose documented config path can + * be redirected by an environment variable (CLAUDE_CONFIG_DIR, GROK_HOME, + * VIBE_HOME, AGENTTY_MCP_CONFIG). Expands a leading ~ against the home + * directory; returns false when the variable is unset or empty. */ +bool cbm_agent_env_config_override(const char *env_name, const char *home_dir, char *resolved, + size_t resolved_size); + #ifdef __cplusplus } #endif diff --git a/src/cli/cli.c b/src/cli/cli.c index 5fb06c043b..f3698d4638 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -3295,9 +3295,6 @@ cbm_detected_agents_t cbm_detect_agents(const char *home_dir) { cbm_grok_config_dir(home_dir, path, sizeof(path)); agents.grok = dir_exists(path) || cbm_agent_cli_exists("grok", home_dir); - snprintf(path, sizeof(path), "%s/.agentty", home_dir); - agents.agentty = dir_exists(path) || cbm_agent_cli_exists("agentty", home_dir); - return agents; } @@ -9649,6 +9646,10 @@ static void install_agent_client_registry(const char *home, const char *binary_p install_pi_durable_context(home, binary_path, force, dry_run); } else if (profile->id == CBM_AGENT_CLIENT_OMP) { install_omp_durable_context(®istry, force, dry_run); + } else if (profile->id == CBM_AGENT_CLIENT_AGENTTY) { + char skills_dir[CLI_BUF_1K]; + snprintf(skills_dir, sizeof(skills_dir), "%s/.agentty/skills", home); + install_agent_skill(profile->display_name, skills_dir, force, dry_run); } } } @@ -10527,15 +10528,6 @@ static void install_additional_agent_configs(const cbm_detected_agents_t *agents "contract)\n"); } } - if (agents->agentty) { - char cp[CLI_BUF_1K]; - char skills_dir[CLI_BUF_1K]; - snprintf(cp, sizeof(cp), "%s/.agentty/mcp.json", home); - snprintf(skills_dir, sizeof(skills_dir), "%s/.agentty/skills", home); - install_generic_agent_config("agentty", binary_path, cp, NULL, dry_run, - cbm_install_editor_mcp); - install_agent_skill("agentty", skills_dir, force, dry_run); - } } static int cbm_install_agent_configs_in_scope(const char *home, const char *binary_path, bool force, @@ -10718,7 +10710,6 @@ static const cli_client_def_t CLI_CLIENTS[] = { CLI_CLIENT(goose, "goose", "Goose"), CLI_CLIENT(mistral_vibe, "mistral-vibe", "Mistral Vibe"), CLI_CLIENT(grok, "grok", "Grok Build"), - CLI_CLIENT(agentty, "agentty", "agentty"), }; enum { CLI_CLIENT_COUNT = sizeof(CLI_CLIENTS) / sizeof(CLI_CLIENTS[0]) }; @@ -10821,6 +10812,10 @@ const char *cbm_cli_clients_token_for_testing(size_t index) { void cbm_cli_set_client_selection_for_testing(const char *spec) { g_client_selection = spec; } +bool cbm_cli_selects_registry_client_for_testing(const char *stable_id) { + const cbm_agent_client_profile_t *profile = cbm_agent_client_by_stable_id(stable_id); + return profile && cli_clients_selects_registry_client(g_client_selection, profile->id); +} #endif /* ── Subcommand: install ──────────────────────────────────────── */ @@ -11019,7 +11014,6 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b {det.goose, "goose"}, {det.mistral_vibe, "mistral-vibe"}, {det.grok, "grok"}, - {det.agentty, "agentty"}, }; yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); @@ -12149,6 +12143,10 @@ static void uninstall_agent_client_registry(const char *home, bool dry_run) { uninstall_pi_durable_context(home, dry_run); } else if (profile->id == CBM_AGENT_CLIENT_OMP) { uninstall_omp_durable_context(®istry, dry_run); + } else if (profile->id == CBM_AGENT_CLIENT_AGENTTY) { + char skills_dir[CLI_BUF_1K]; + snprintf(skills_dir, sizeof(skills_dir), "%s/.agentty/skills", home); + uninstall_agent_skill(profile->display_name, skills_dir, dry_run); } } } @@ -12799,15 +12797,6 @@ static void uninstall_additional_agents(const cbm_detected_agents_t *agents, con }, dry_run); } - if (agents->agentty) { - char cp[CLI_BUF_1K]; - char skills_dir[CLI_BUF_1K]; - snprintf(cp, sizeof(cp), "%s/.agentty/mcp.json", home); - snprintf(skills_dir, sizeof(skills_dir), "%s/.agentty/skills", home); - uninstall_agent_mcp_instr((mcp_uninstall_args_t){"agentty", cp, NULL}, dry_run, - cbm_remove_editor_mcp_owned); - uninstall_agent_skill("agentty", skills_dir, dry_run); - } } typedef struct { diff --git a/src/cli/cli.h b/src/cli/cli.h index 0663c06031..0f281a687c 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -208,7 +208,6 @@ typedef struct { bool goose; /* Goose config or CLI exists */ bool mistral_vibe; /* $VIBE_HOME, ~/.vibe/, or vibe CLI exists */ bool grok; /* $GROK_HOME, ~/.grok/, or grok CLI exists */ - bool agentty; /* ~/.agentty/ or agentty CLI exists */ } cbm_detected_agents_t; /* Detect which coding agents are installed. @@ -232,6 +231,9 @@ bool cbm_cli_clients_apply_selection_for_testing(const char *spec, cbm_detected_ size_t cbm_cli_clients_count_for_testing(void); const char *cbm_cli_clients_token_for_testing(size_t index); void cbm_cli_set_client_selection_for_testing(const char *spec); +/* True when the current --clients selection includes the registry client id + * (registry-backed path of the same selector). */ +bool cbm_cli_selects_registry_client_for_testing(const char *stable_id); #endif #ifdef CBM_CLI_ENABLE_TEST_API diff --git a/tests/test_agent_clients.c b/tests/test_agent_clients.c index df8515e73c..c30b8af483 100644 --- a/tests/test_agent_clients.c +++ b/tests/test_agent_clients.c @@ -162,7 +162,7 @@ TEST(agent_clients_registry_is_stable_and_callback_driven) { "qoder", "kimi", "gitlab-duo", "rovo-dev", "amp", "devin", "tabnine", "continue", "visual-studio", "trae", "roo-code", "amazon-q", "codebuddy", "ibm-bob-ide", "ibm-bob-shell", "pochi", "pi", "sourcegraph-cody", - "omp", + "omp", "agentty", }; static const uint32_t expected_capabilities[] = { CBM_AGENT_CAP_MCP | CBM_AGENT_CAP_SKILL | CBM_AGENT_CAP_AGENT | CBM_AGENT_CAP_HOOK, @@ -184,6 +184,7 @@ TEST(agent_clients_registry_is_stable_and_callback_driven) { CBM_AGENT_CAP_INSTRUCTIONS | CBM_AGENT_CAP_SKILL, CBM_AGENT_CAP_MCP, CBM_AGENT_CAP_MCP | CBM_AGENT_CAP_SKILL | CBM_AGENT_CAP_AGENT, + CBM_AGENT_CAP_MCP | CBM_AGENT_CAP_SKILL, }; ASSERT_EQ(cbm_agent_client_count(), CBM_AGENT_CLIENT_COUNT); ASSERT_EQ(CBM_AGENT_CLIENT_COUNT, sizeof(expected) / sizeof(expected[0])); diff --git a/tests/test_cli.c b/tests/test_cli.c index 4e80da18b2..eae34bce71 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -6749,13 +6750,70 @@ TEST(cli_detect_agents_finds_agentty) { if (!cbm_mkdtemp(tmpdir)) FAIL("cbm_mkdtemp failed"); + /* agentty lives in the table-driven registry, not the legacy detector. */ + const cbm_agent_client_profile_t *profile = cbm_agent_client_by_stable_id("agentty"); + ASSERT_NOT_NULL(profile); + char dir[512]; snprintf(dir, sizeof(dir), "%s/.agentty", tmpdir); test_mkdirp(dir); - cbm_detected_agents_t agents = cbm_detect_agents(tmpdir); - ASSERT_TRUE(agents.agentty); + cbm_agent_client_resolve_options_t options; + memset(&options, 0, sizeof(options)); + options.home_dir = tmpdir; + ASSERT_TRUE(cbm_agent_client_detect(CBM_AGENT_CLIENT_AGENTTY, &options)); + + char config_path[512]; + ASSERT_EQ(cbm_agent_client_resolve_path(CBM_AGENT_CLIENT_AGENTTY, &options, config_path, + sizeof(config_path)), + 0); + char expected[512]; + snprintf(expected, sizeof(expected), "%s/.agentty/mcp.json", tmpdir); + ASSERT_STR_EQ(config_path, expected); + + test_rmdir_r(tmpdir); + PASS(); +} + +TEST(cli_agentty_mcp_config_env_override) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-agentty-env-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char *saved = save_test_env("AGENTTY_MCP_CONFIG"); + char override_path[512]; + snprintf(override_path, sizeof(override_path), "%s/override.json", tmpdir); + cbm_setenv("AGENTTY_MCP_CONFIG", override_path, 1); + + cbm_agent_client_resolve_options_t options; + memset(&options, 0, sizeof(options)); + options.home_dir = tmpdir; + + char config_path[512]; + ASSERT_EQ(cbm_agent_client_resolve_path(CBM_AGENT_CLIENT_AGENTTY, &options, config_path, + sizeof(config_path)), + 0); + ASSERT_STR_EQ(config_path, override_path); + /* Tilde expansion goes against the home directory. */ + cbm_setenv("AGENTTY_MCP_CONFIG", "~/agentty-override.json", 1); + ASSERT_EQ(cbm_agent_client_resolve_path(CBM_AGENT_CLIENT_AGENTTY, &options, config_path, + sizeof(config_path)), + 0); + char expected[512]; + snprintf(expected, sizeof(expected), "%s/agentty-override.json", tmpdir); + ASSERT_STR_EQ(config_path, expected); + + /* Unset falls back to ~/.agentty/mcp.json. */ + cbm_unsetenv("AGENTTY_MCP_CONFIG"); + ASSERT_EQ(cbm_agent_client_resolve_path(CBM_AGENT_CLIENT_AGENTTY, &options, config_path, + sizeof(config_path)), + 0); + snprintf(expected, sizeof(expected), "%s/.agentty/mcp.json", tmpdir); + ASSERT_STR_EQ(config_path, expected); + + restore_test_env("AGENTTY_MCP_CONFIG", saved); test_rmdir_r(tmpdir); PASS(); } @@ -6880,6 +6938,9 @@ TEST(cli_supported_agent_surfaces_match_installers) { "agentty", }; ASSERT_EQ(sizeof(required_agents) / sizeof(required_agents[0]), 46U); + /* The table-driven registry must carry agentty too — the legacy detector no + * longer knows about it. */ + ASSERT_NOT_NULL(cbm_agent_client_by_stable_id("agentty")); char *data = read_test_file_alloc("README.md"); if (!data) FAIL("could not read README.md for supported-agent contract"); @@ -7241,6 +7302,7 @@ TEST(cli_agentty_install_then_uninstall_roundtrip) { char *saved_home = save_test_env("HOME"); char *saved_path = save_test_env("PATH"); char *saved_cache = save_test_env("CBM_CACHE_DIR"); + char *saved_override = save_test_env("AGENTTY_MCP_CONFIG"); cbm_setenv("HOME", tmpdir, 1); cbm_setenv("PATH", tmpdir, 1); cbm_unsetenv("CBM_CACHE_DIR"); @@ -7291,11 +7353,40 @@ TEST(cli_agentty_install_then_uninstall_roundtrip) { bool user_kept = stat(user_file, &state) == 0; bool foreign_kept = mcp_after_uninstall && strstr(mcp_after_uninstall, "\"other-mcp\"") != NULL; + /* $AGENTTY_MCP_CONFIG redirects both install and uninstall: agentty never + * reads ~/.agentty/mcp.json while the variable points at an explicit + * file, so writing there would silently no-op on the agent's side. */ + char override_path[640]; + snprintf(override_path, sizeof(override_path), "%s/override-mcp.json", tmpdir); + cbm_setenv("AGENTTY_MCP_CONFIG", override_path, 1); + cbm_install_agent_configs(tmpdir, binary, false, false); + struct stat override_state; + const char *const override_markers[] = {"mcpServers", "codebase-memory-mcp", binary}; + if (!test_file_contains_all(override_path, override_markers, 3) || + stat(override_path, &override_state) != 0) + FAIL("install must honor $AGENTTY_MCP_CONFIG and write the override file"); + char *default_after_override = read_test_file_alloc(mcp_path); + bool default_untouched = + !default_after_override || strstr(default_after_override, "codebase-memory-mcp") == NULL; + free(default_after_override); + if (!default_untouched) + FAIL("install must not re-add the entry to ~/.agentty/mcp.json while " + "$AGENTTY_MCP_CONFIG is set"); + argv[0] = "uninstall"; + uninstall_rc = cli_test_cmd_uninstall(2, argv); + char *override_after_uninstall = read_test_file_alloc(override_path); + bool override_entry_removed = + override_after_uninstall && strstr(override_after_uninstall, "codebase-memory-mcp") == NULL; + free(override_after_uninstall); + if (!override_entry_removed) + FAIL("uninstall must honor $AGENTTY_MCP_CONFIG and clean the override file"); + free(mcp_after_install); free(mcp_after_uninstall); restore_test_env("HOME", saved_home); restore_test_env("PATH", saved_path); restore_test_env("CBM_CACHE_DIR", saved_cache); + restore_test_env("AGENTTY_MCP_CONFIG", saved_override); test_rmdir_r(tmpdir); if (!installed) @@ -17445,15 +17536,18 @@ TEST(cli_clients_selector_vocabulary_is_complete_and_strict_issue1558) { ASSERT_FALSE(registry.claude_code); ASSERT_FALSE(registry.cursor); - /* agentty is a detected agent, but it must still be selectable and - * filtered: a selection that omits it must zero it, or a user asking for - * "claude,codex" silently gets agentty configured too. */ + /* agentty is a registry client: selectable through the shared selector + * vocabulary, and a selection that omits it must drop it (a registry + * install only ever runs when cli_clients_selects_registry_client passes). */ cbm_detected_agents_t agentty = all; ASSERT_TRUE(cbm_cli_clients_apply_selection_for_testing("agentty", &agentty)); - ASSERT_TRUE(agentty.agentty); cbm_detected_agents_t leak = all; ASSERT_TRUE(cbm_cli_clients_apply_selection_for_testing("claude,codex", &leak)); - ASSERT_FALSE(leak.agentty); + cbm_cli_set_client_selection_for_testing("agentty"); + ASSERT_TRUE(cbm_cli_selects_registry_client_for_testing("agentty")); + cbm_cli_set_client_selection_for_testing("claude,codex"); + ASSERT_FALSE(cbm_cli_selects_registry_client_for_testing("agentty")); + cbm_cli_set_client_selection_for_testing(NULL); /* Every token in the table must resolve — a client added to detection but * forgotten here is invisible to the selector. */ @@ -17969,7 +18063,7 @@ SUITE(cli) { RUN_TEST(cli_yaml_parse_empty); RUN_TEST(cli_yaml_has); - /* Agent detection (7 tests — group A) */ + /* Agent detection */ RUN_TEST(cli_detect_agents_finds_claude); RUN_TEST(cli_detect_agents_finds_claude_via_env); RUN_TEST(cli_detect_claude_empty_dir_not_detected_issue1180); @@ -17983,6 +18077,7 @@ SUITE(cli) { RUN_TEST(cli_detect_agents_finds_grok); RUN_TEST(cli_detect_agents_finds_cursor_issue222); RUN_TEST(cli_detect_agents_finds_agentty); + RUN_TEST(cli_agentty_mcp_config_env_override); RUN_TEST(cli_agentty_install_then_uninstall_roundtrip); RUN_TEST(cli_install_plan_receipt_no_mutation_issue388); RUN_TEST(cli_supported_agent_surfaces_match_installers);