From 68245c4673e2b7d4056c1cdaae0398b301a8e51c Mon Sep 17 00:00:00 2001 From: DevinZeng Date: Wed, 17 Jun 2026 18:45:59 +0800 Subject: [PATCH] feat: add CodeBuddy support Signed-off-by: DevinZeng --- README.md | 9 +- docs/index.html | 10 +- scripts/setup-windows.ps1 | 42 +++++- scripts/setup.sh | 70 +++++++++ src/cli/cli.c | 295 +++++++++++++++++++++++++++++++++++++- src/cli/cli.h | 12 ++ src/discover/discover.c | 2 +- src/main.c | 2 +- tests/test_cli.c | 131 +++++++++++++++++ tests/test_discover.c | 5 + 10 files changed, 560 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b48a297f..afbed141 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Tests](https://img.shields.io/badge/tests-5604_passing-brightgreen)](https://github.com/DeusData/codebase-memory-mcp) [![Languages](https://img.shields.io/badge/languages-158-orange)](https://github.com/DeusData/codebase-memory-mcp) [![Hybrid LSP](https://img.shields.io/badge/Hybrid_LSP-9_languages-blue)](#hybrid-lsp) -[![Agents](https://img.shields.io/badge/agents-11-purple)](https://github.com/DeusData/codebase-memory-mcp) +[![Agents](https://img.shields.io/badge/agents-12-purple)](https://github.com/DeusData/codebase-memory-mcp) [![Pure C](https://img.shields.io/badge/pure_C-zero_dependencies-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 single static binary 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 158 languages, enhanced with [**Hybrid LSP** semantic type resolution](#hybrid-lsp) for Python, TypeScript / JavaScript / JSX / TSX, PHP, C#, Go, C, C++, Java, Kotlin, and Rust — producing a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. 14 MCP tools. Zero dependencies. Plug and play across 11 coding agents. +High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-sitter/) AST analysis across all 158 languages, enhanced with [**Hybrid LSP** semantic type resolution](#hybrid-lsp) for Python, TypeScript / JavaScript / JSX / TSX, PHP, C#, Go, C, C++, Java, Kotlin, and Rust — producing a persistent knowledge graph of functions, classes, call chains, HTTP routes, and cross-service links. 14 MCP tools. Zero dependencies. Plug and play across 12 coding agents. > **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** — single static binary for macOS (arm64/amd64), Linux (arm64/amd64), and Windows (amd64). No Docker, no runtime dependencies, no API keys. Download → `install` → restart agent → done. - **158 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. -- **11 agents, one command** — `install` auto-detects Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, OpenClaw, and Kiro — configures MCP entries, instruction files, and pre-tool hooks for each. +- **12 agents, one command** — `install` auto-detects Claude Code, CodeBuddy, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, OpenClaw, and Kiro — configures MCP entries, instruction files, and pre-tool hooks for each. - **Built-in graph visualization** — 3D interactive UI at `localhost:9749` (optional UI binary variant). - **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. - **14 MCP tools** — search, trace, architecture, impact analysis, Cypher queries, dead code detection, cross-service HTTP linking, ADR management, and more. @@ -199,7 +199,7 @@ The result is similar in spirit to graphify's `graphify-out/` directory, but as ## How It Works -codebase-memory-mcp is a **structural analysis backend** — it builds and queries the knowledge graph. It does **not** include an LLM. Instead, it relies on your MCP client (Claude Code, or any MCP-compatible agent) to be the intelligence layer. +codebase-memory-mcp is a **structural analysis backend** — it builds and queries the knowledge graph. It does **not** include an LLM. Instead, it relies on your MCP client (Claude Code, CodeBuddy, or any MCP-compatible agent) to be the intelligence layer. ``` You: "what calls ProcessOrder?" @@ -335,6 +335,7 @@ Restart your agent. Verify with `/mcp` — you should see `codebase-memory-mcp` | Agent | MCP Config | Instructions | Hooks | |-------|-----------|-------------|-------| | Claude Code | `.claude/.mcp.json` | 4 Skills | PreToolUse (Grep/Glob graph augment, non-blocking) | +| CodeBuddy | `~/.codebuddy/.mcp.json` | 4 Skills | PreToolUse (Grep/Glob graph augment, non-blocking) | | Codex CLI | `.codex/config.toml` | `.codex/AGENTS.md` | SessionStart reminder | | Gemini CLI | `.gemini/settings.json` | `.gemini/GEMINI.md` | BeforeTool (grep reminder) + SessionStart reminder | | Zed | `settings.json` (JSONC) | — | — | diff --git a/docs/index.html b/docs/index.html index 7e732406..5a541392 100644 --- a/docs/index.html +++ b/docs/index.html @@ -4,8 +4,8 @@ codebase-memory-mcp — Code Intelligence Knowledge Graph for AI Coding Agents - - + + @@ -157,7 +157,7 @@ "name": "Which AI coding agents work with codebase-memory-mcp?", "acceptedAnswer": { "@type": "Answer", - "text": "A single install command configures 11 agents: Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, OpenClaw, and Kiro. Any MCP-compatible client can use the server." + "text": "A single install command configures 12 agents: Claude Code, CodeBuddy, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, OpenClaw, and Kiro. Any MCP-compatible client can use the server." } }, { @@ -487,7 +487,7 @@

What is codebase-memory-mcp?

It is a structural-analysis backend, not a chatbot: there is no embedded LLM and no API key. Your - MCP client (Claude Code, or any MCP-compatible agent) is the intelligence layer; codebase-memory-mcp + MCP client (Claude Code, CodeBuddy, or any MCP-compatible agent) is the intelligence layer; codebase-memory-mcp builds and serves the graph. All processing happens locally — your code never leaves your machine.

@@ -526,7 +526,7 @@

How do I install codebase-memory-mcp?

"Index this project"

- One command configures all 11 supported agents: Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode, + One command configures all 12 supported agents: Claude Code, CodeBuddy, Codex CLI, Gemini CLI, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, OpenClaw, and Kiro — with MCP entries, instruction files, and pre-tool hooks for each. Windows users run install.ps1. Also available via npm, pip, Homebrew, Scoop, Winget, Chocolatey, AUR, and go install. diff --git a/scripts/setup-windows.ps1 b/scripts/setup-windows.ps1 index 592a9cd4..019ca608 100644 --- a/scripts/setup-windows.ps1 +++ b/scripts/setup-windows.ps1 @@ -16,8 +16,8 @@ $InstallDir = Join-Path $env:LOCALAPPDATA "codebase-memory-mcp" # --- Helpers --- function Write-Ok($msg) { Write-Host " $msg" -ForegroundColor Green } -function Write-Fail($msg) { Write-Host " $msg" -ForegroundColor Red } -function Write-Warn($msg) { Write-Host " $msg" -ForegroundColor Yellow } +function Write-Fail($msg) { Write-Host " $msg" -ForegroundColor Red } +function Write-Warn($msg) { Write-Host " $msg" -ForegroundColor Yellow } function Read-SettingsJson($Path) { # PS5.1-compatible: ConvertFrom-Json returns PSCustomObject, not Hashtable. @@ -83,6 +83,36 @@ function Configure-ClaudeCode($McpConfig) { } } +function Configure-CodeBuddy($McpConfig) { + Write-Host "" + $answer = Read-Host "Configure CodeBuddy to use codebase-memory-mcp? [y/N]" + $codebuddyDir = if ($env:CODEBUDDY_CONFIG_DIR) { $env:CODEBUDDY_CONFIG_DIR } else { Join-Path $env:USERPROFILE ".codebuddy" } + $mcpPath = Join-Path $codebuddyDir ".mcp.json" + $mcpDir = Split-Path $mcpPath -Parent + + if ($answer -match '^[Yy]$') { + if (-not (Test-Path $mcpDir)) { + New-Item -ItemType Directory -Path $mcpDir -Force | Out-Null + } + + $settings = Read-SettingsJson $mcpPath + + if (-not $settings.Contains("mcpServers")) { + $settings["mcpServers"] = [ordered]@{} + } + + $settings["mcpServers"]["codebase-memory-mcp"] = $McpConfig + Write-SettingsJson $mcpPath $settings + Write-Ok "Updated $mcpPath" + } else { + Write-Host "" + Write-Host " Add this to $mcpPath:" -ForegroundColor White + Write-Host "" + $snippet = @{ mcpServers = @{ "codebase-memory-mcp" = $McpConfig } } + $snippet | ConvertTo-Json -Depth 10 | Write-Host + } +} + function Test-WSL { try { $null = wsl.exe --status 2>&1 @@ -233,9 +263,10 @@ if ($FromSource) { } Configure-ClaudeCode $mcpConfig + Configure-CodeBuddy $mcpConfig Write-Host "" - Write-Ok "Done! Restart Claude Code and verify with /mcp" + Write-Ok "Done! Restart Claude Code or CodeBuddy and verify with /mcp" Write-Host "" Write-Host " To uninstall:" -ForegroundColor White Write-Host " wsl.exe -- rm $wslBinaryPath" @@ -309,16 +340,17 @@ if ($FromSource) { } } - # Configure Claude Code + # Configure Claude Code / CodeBuddy $mcpConfig = [ordered]@{ type = "stdio" command = $binaryPath } Configure-ClaudeCode $mcpConfig + Configure-CodeBuddy $mcpConfig Write-Host "" - Write-Ok "Done! Restart Claude Code and verify with /mcp" + Write-Ok "Done! Restart Claude Code or CodeBuddy and verify with /mcp" Write-Host "" Write-Host " To uninstall:" -ForegroundColor White Write-Host " Remove-Item -Recurse -Force '$InstallDir'" diff --git a/scripts/setup.sh b/scripts/setup.sh index 81a0508d..1e93bc98 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -276,6 +276,75 @@ print() fi } +configure_codebuddy() { + echo "" + local binary_path="${INSTALL_DIR}/${BINARY_NAME}" + local codebuddy_config_dir="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" + local mcp_file="${codebuddy_config_dir}/.mcp.json" + + printf "%s" "${BOLD}Configure CodeBuddy to use codebase-memory-mcp? [y/N] ${RESET}" + read -r answer + if [[ ! "$answer" =~ ^[Yy]$ ]]; then + echo "" + info "Add this to ${mcp_file}:" + echo "" + echo ' {' + echo ' "mcpServers": {' + echo ' "codebase-memory-mcp": {' + echo ' "type": "stdio",' + echo " \"command\": \"${binary_path}\"" + echo ' }' + echo ' }' + echo ' }' + return + fi + + local mcp_entry + mcp_entry=$(cat </dev/null; then + if [ -f "$mcp_file" ]; then + local tmp + tmp=$(mktemp) + jq --argjson entry "$mcp_entry" '.mcpServers["codebase-memory-mcp"] = $entry' "$mcp_file" > "$tmp" + mv "$tmp" "$mcp_file" + else + echo "{}" | jq --argjson entry "$mcp_entry" '.mcpServers["codebase-memory-mcp"] = $entry' > "$mcp_file" + fi + ok "Updated ${mcp_file}" + elif command -v python3 &>/dev/null; then + python3 -c " +import json, os +path = os.path.expanduser('$mcp_file') +data = {} +if os.path.exists(path): + with open(path) as f: + data = json.load(f) +data.setdefault('mcpServers', {})['codebase-memory-mcp'] = json.loads('$mcp_entry') +with open(path, 'w') as f: + json.dump(data, f, indent=2) +print() +" + ok "Updated ${mcp_file}" + else + warn "Neither jq nor python3 found — cannot auto-configure." + echo "" + info "Add this to ${mcp_file} manually:" + echo "" + echo ' "mcpServers": {' + echo ' "codebase-memory-mcp": {' + echo ' "type": "stdio",' + echo " \"command\": \"${binary_path}\"" + echo ' }' + echo ' }' + fi +} + # --- PATH check --- check_path() { @@ -317,6 +386,7 @@ else fi configure_claude +configure_codebuddy check_path # --- Git hooks --- diff --git a/src/cli/cli.c b/src/cli/cli.c index 4228dbaa..9f01e437 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -1014,6 +1014,22 @@ static void cbm_claude_user_root(const char *home_dir, char *out, size_t out_sz) } } +/* Resolve the CodeBuddy config dir. + * Honors $CODEBUDDY_CONFIG_DIR; falls back to "$home_dir/.codebuddy". */ +static void cbm_codebuddy_config_dir(const char *home_dir, char *out, size_t out_sz) { + if (out_sz == 0) { + return; + } + out[0] = '\0'; + char env_buf[CLI_BUF_1K]; + const char *env = cbm_safe_getenv("CODEBUDDY_CONFIG_DIR", env_buf, sizeof(env_buf), NULL); + if (env && env[0]) { + snprintf(out, out_sz, "%s", env); + } else if (home_dir && home_dir[0]) { + snprintf(out, out_sz, "%s/.codebuddy", home_dir); + } +} + /* Build the hook command string written into Claude Code's settings.json. * Honors $CLAUDE_CONFIG_DIR. When CLAUDE_CONFIG_DIR is unset, preserves the * legacy tilde-expanded form so settings.json stays portable across HOME values. */ @@ -1031,6 +1047,23 @@ static void cbm_resolve_hook_command(const char *script_name, char *out, size_t } } +/* Build the hook command string written into CodeBuddy's settings.json. + * Honors $CODEBUDDY_CONFIG_DIR. When CODEBUDDY_CONFIG_DIR is unset, preserves the + * tilde-expanded form so settings.json stays portable across HOME values. */ +static void cbm_resolve_codebuddy_hook_command(const char *script_name, char *out, size_t out_sz) { + if (out_sz == 0) { + return; + } + out[0] = '\0'; + char env_buf[CLI_BUF_1K]; + const char *env = cbm_safe_getenv("CODEBUDDY_CONFIG_DIR", env_buf, sizeof(env_buf), NULL); + if (env && env[0]) { + snprintf(out, out_sz, "%s/hooks/%s", env, script_name); + } else { + snprintf(out, out_sz, "~/.codebuddy/hooks/%s", script_name); + } +} + cbm_detected_agents_t cbm_detect_agents(const char *home_dir) { cbm_detected_agents_t agents; memset(&agents, 0, sizeof(agents)); @@ -1101,6 +1134,10 @@ cbm_detected_agents_t cbm_detect_agents(const char *home_dir) { snprintf(path, sizeof(path), "%s/.kiro", home_dir); agents.kiro = dir_exists(path); + /* CodeBuddy: ~/.codebuddy/ (or $CODEBUDDY_CONFIG_DIR) */ + cbm_codebuddy_config_dir(home_dir, path, sizeof(path)); + agents.codebuddy = path[0] != '\0' && dir_exists(path); + return agents; } @@ -1650,6 +1687,9 @@ int cbm_remove_antigravity_mcp(const char *config_path) { * in-process deadline well under this. */ #define CMM_HOOK_TIMEOUT_SEC 5 +/* SessionStart reminder script name (installed to hooks/ dir). */ +#define CMM_SESSION_REMINDER_SCRIPT "cbm-session-reminder" + /* Old matcher values from previous versions — recognized during upgrade so * upsert/remove can clean them up before inserting the current matcher. * Per-agent lists (no shared global): each caller passes its own. */ @@ -1857,6 +1897,38 @@ int cbm_remove_claude_hooks(const char *settings_path) { }); } +/* ── CodeBuddy pre-tool hooks ───────────────────────────────── */ + +/* CodeBuddy uses the same hook format as Claude Code. + * Old matchers shared with Claude Code since the hook format is identical. */ +static const char *const cmm_codebuddy_old_matchers[] = { + "Grep|Glob|Read|Search", + "Grep|Glob|Read", + NULL, +}; + +int cbm_upsert_codebuddy_hooks(const char *settings_path) { + char command[CLI_BUF_1K]; + cbm_resolve_codebuddy_hook_command(CMM_HOOK_GATE_SCRIPT, command, sizeof(command)); + return upsert_hooks_json((hooks_upsert_args_t){ + .settings_path = settings_path, + .hook_event = "PreToolUse", + .matcher_str = CMM_HOOK_MATCHER, + .command_str = command, + .old_matchers = cmm_codebuddy_old_matchers, + .timeout_sec = CMM_HOOK_TIMEOUT_SEC, + }); +} + +int cbm_remove_codebuddy_hooks(const char *settings_path) { + return remove_hooks_json((hooks_remove_args_t){ + .settings_path = settings_path, + .hook_event = "PreToolUse", + .matcher_str = CMM_HOOK_MATCHER, + .old_matchers = cmm_codebuddy_old_matchers, + }); +} + /* Install the search-augmenter shim to ~/.claude/hooks/. * The shim is a thin wrapper that delegates to ` hook-augment`, * which adds graph context to Grep/Glob calls. It NEVER blocks a tool call: @@ -1911,8 +1983,126 @@ void cbm_install_hook_gate_script(const char *home, const char *binary_path) { #endif } -/* SessionStart hook: remind agent to use MCP tools on every context reset. */ -#define CMM_SESSION_REMINDER_SCRIPT "cbm-session-reminder" +/* Install the search-augmenter shim to ~/.codebuddy/hooks/. + * Same logic as cbm_install_hook_gate_script but for CodeBuddy. */ +static void cbm_install_codebuddy_hook_gate_script(const char *home, const char *binary_path) { + if (!home || !binary_path) { + return; + } + if (strchr(binary_path, '"') != NULL) { + return; + } + char config_dir[CLI_BUF_1K]; + cbm_codebuddy_config_dir(home, config_dir, sizeof(config_dir)); + if (!config_dir[0]) { + return; + } + char hooks_dir[CLI_BUF_1K]; + snprintf(hooks_dir, sizeof(hooks_dir), "%s/hooks", config_dir); + cbm_mkdir_p(hooks_dir, CLI_OCTAL_PERM); + + char script_path[CLI_BUF_1K]; + snprintf(script_path, sizeof(script_path), "%s/" CMM_HOOK_GATE_SCRIPT, hooks_dir); + + FILE *f = fopen(script_path, "w"); + if (!f) { + return; + } + (void)fprintf(f, + "#!/bin/bash\n" + "# codebase-memory-mcp search augmenter (CodeBuddy PreToolUse).\n" + "# Despite the name this NEVER blocks a tool call - it only adds\n" + "# graph context. Any failure is silent (exit 0, no output).\n" + "BIN=\"%s\"\n" + "[ -x \"$BIN\" ] || exit 0\n" + "\"$BIN\" hook-augment 2>/dev/null\n" + "exit 0\n", + binary_path); +#ifndef _WIN32 + fchmod(fileno(f), CLI_OCTAL_PERM); +#endif + (void)fclose(f); +#ifdef _WIN32 + chmod(script_path, CLI_OCTAL_PERM); +#endif +} + +/* Install SessionStart reminder script to ~/.codebuddy/hooks/. + * Same logic as cbm_install_session_reminder_script but for CodeBuddy. */ +static void cbm_install_codebuddy_session_reminder_script(const char *home) { + if (!home) { + return; + } + char config_dir[CLI_BUF_1K]; + cbm_codebuddy_config_dir(home, config_dir, sizeof(config_dir)); + if (!config_dir[0]) { + return; + } + char hooks_dir[CLI_BUF_1K]; + snprintf(hooks_dir, sizeof(hooks_dir), "%s/hooks", config_dir); + cbm_mkdir_p(hooks_dir, CLI_OCTAL_PERM); + + char script_path[CLI_BUF_1K]; + snprintf(script_path, sizeof(script_path), "%s/" CMM_SESSION_REMINDER_SCRIPT, hooks_dir); + + FILE *f = fopen(script_path, "w"); + if (!f) { + return; + } + (void)fprintf( + f, "#!/bin/bash\n" + "# SessionStart hook: remind agent to use codebase-memory-mcp tools.\n" + "# Installed by codebase-memory-mcp. Fires on startup/resume/clear/compact.\n" + "cat << 'REMINDER'\n" + "CRITICAL - Code Discovery Protocol:\n" + "1. ALWAYS use codebase-memory-mcp tools FIRST for ANY code exploration:\n" + " - search_graph(name_pattern/label/qn_pattern) to find functions/classes/routes\n" + " - trace_path(function_name, mode=calls|data_flow|cross_service) for call chains\n" + " - get_code_snippet(qualified_name) for exact symbol source (precise ranges)\n" + " - query_graph(query) for complex Cypher patterns\n" + " - get_architecture(aspects) for project structure\n" + " - search_code(pattern) for text search (graph-augmented grep)\n" + "2. Use Grep/Glob/Read freely for text, configs, non-code files, and\n" + " always Read a file before editing it.\n" + "3. If a project is not indexed yet, run index_repository FIRST.\n" + "REMINDER\n"); +#ifndef _WIN32 + fchmod(fileno(f), CLI_OCTAL_PERM); +#endif + (void)fclose(f); +#ifdef _WIN32 + chmod(script_path, CLI_OCTAL_PERM); +#endif +} + +int cbm_upsert_codebuddy_session_hooks(const char *settings_path) { + static const char *matchers[] = {"startup", "resume", "clear", "compact"}; + char command[CLI_BUF_1K]; + cbm_resolve_codebuddy_hook_command(CMM_SESSION_REMINDER_SCRIPT, command, sizeof(command)); + int rc = 0; + for (size_t i = 0; i < sizeof(matchers) / sizeof(matchers[0]); i++) { + if (upsert_hooks_json((hooks_upsert_args_t){.settings_path = settings_path, + .hook_event = "SessionStart", + .matcher_str = matchers[i], + .command_str = command}) != 0) { + rc = CLI_ERR; + } + } + return rc; +} + +int cbm_remove_codebuddy_session_hooks(const char *settings_path) { + static const char *matchers[] = {"startup", "resume", "clear", "compact"}; + int rc = 0; + for (size_t i = 0; i < sizeof(matchers) / sizeof(matchers[0]); i++) { + if (remove_hooks_json((hooks_remove_args_t){.settings_path = settings_path, + .hook_event = "SessionStart", + .matcher_str = matchers[i]}) != 0) { + rc = CLI_ERR; + } + } + return rc; +} static void cbm_install_session_reminder_script(const char *home) { if (!home) { @@ -2892,6 +3082,7 @@ static void print_detected_agents(const cbm_detected_agents_t *a) { {a->cursor, "Cursor"}, {a->openclaw, "OpenClaw"}, {a->kiro, "Kiro"}, + {a->codebuddy, "CodeBuddy"}, }; printf("Detected agents:"); bool any = false; @@ -3023,6 +3214,73 @@ static void install_claude_code_config(const char *home, const char *binary_path } } +/* Install CodeBuddy-specific configs (skills, MCP, hooks). + * Mirrors Claude Code, but MCP lives in .codebuddy/.mcp.json. */ +static void install_codebuddy_config(const char *home, const char *binary_path, bool force, + bool dry_run) { + char config_dir[CLI_BUF_1K]; + cbm_codebuddy_config_dir(home, config_dir, sizeof(config_dir)); + + char skills_dir[CLI_BUF_1K]; + snprintf(skills_dir, sizeof(skills_dir), "%s/skills", config_dir); + + /* Plan mode: record the planned writes and return without mutating (#388). */ + if (g_install_plan) { + char p[CLI_BUF_1K]; + plan_record("CodeBuddy", "skills", skills_dir); + snprintf(p, sizeof(p), "%s/.mcp.json", config_dir); + plan_record("CodeBuddy", "mcp_config", p); + snprintf(p, sizeof(p), "%s/settings.json", config_dir); + plan_record("CodeBuddy", "mcp_config", p); + snprintf(p, sizeof(p), "%s/hooks/%s", config_dir, CMM_HOOK_GATE_SCRIPT); + plan_record("CodeBuddy", "hook", p); + snprintf(p, sizeof(p), "%s/hooks/%s", config_dir, CMM_SESSION_REMINDER_SCRIPT); + plan_record("CodeBuddy", "hook", p); + return; + } + + printf("CodeBuddy:\n"); + + int skill_count = cbm_install_skills(skills_dir, force, dry_run); + printf(" skills: %d installed\n", skill_count); + + if (cbm_remove_old_monolithic_skill(skills_dir, dry_run)) { + printf(" removed old monolithic skill\n"); + } + + char mcp_path[CLI_BUF_1K]; + snprintf(mcp_path, sizeof(mcp_path), "%s/.mcp.json", config_dir); + if (!dry_run) { + cbm_install_editor_mcp(binary_path, mcp_path); + } + printf(" mcp: %s\n", mcp_path); + + char settings_path[CLI_BUF_1K]; + snprintf(settings_path, sizeof(settings_path), "%s/settings.json", config_dir); + if (!dry_run) { + cbm_upsert_codebuddy_hooks(settings_path); + cbm_install_codebuddy_hook_gate_script(home, binary_path); + cbm_install_codebuddy_session_reminder_script(home); + cbm_upsert_codebuddy_session_hooks(settings_path); + } + printf(" hooks: PreToolUse (Grep/Glob search-graph augmenter, non-blocking)\n"); + printf(" hooks: SessionStart (MCP usage reminder on startup/resume/clear/compact)\n"); + + /* Migration nudge: when CODEBUDDY_CONFIG_DIR is set and a legacy ~/.codebuddy tree + * still exists, mention it so users can clean up stale artifacts. */ + if (home && home[0]) { + char legacy_dir[CLI_BUF_1K]; + snprintf(legacy_dir, sizeof(legacy_dir), "%s/.codebuddy", home); + if (strcmp(legacy_dir, config_dir) != 0 && dir_exists(legacy_dir)) { + (void)fprintf(stderr, + " note: $CODEBUDDY_CONFIG_DIR=%s used; legacy %s still exists.\n" + " Remove stale {skills,hooks,settings.json,.mcp.json} there if " + "no longer needed.\n", + config_dir, legacy_dir); + } + } +} + /* Install MCP config + optional instructions for a generic agent. */ static void install_generic_agent_config(const char *label, const char *binary_path, const char *config_path, const char *instr_path, @@ -3217,6 +3475,9 @@ static void cbm_install_agent_configs(const char *home, const char *binary_path, if (agents.claude_code) { install_claude_code_config(home, binary_path, force, dry_run); } + if (agents.codebuddy) { + install_codebuddy_config(home, binary_path, force, dry_run); + } install_cli_agent_configs(&agents, home, binary_path, dry_run); install_editor_agent_configs(&agents, home, binary_path, dry_run); } @@ -3304,6 +3565,7 @@ char *cbm_build_install_plan_json(const char *home, const char *binary_path) { {det.cursor, "cursor"}, {det.openclaw, "openclaw"}, {det.kiro, "kiro"}, + {det.codebuddy, "codebuddy"}, }; yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); @@ -3488,6 +3750,32 @@ static void uninstall_claude_code(const char *home, bool dry_run) { printf(" removed PreToolUse + SessionStart hooks\n"); } +/* Remove CodeBuddy agent configs. */ +static void uninstall_codebuddy(const char *home, bool dry_run) { + char config_dir[CLI_BUF_1K]; + cbm_codebuddy_config_dir(home, config_dir, sizeof(config_dir)); + + char skills_dir[CLI_BUF_1K]; + snprintf(skills_dir, sizeof(skills_dir), "%s/skills", config_dir); + int removed = cbm_remove_skills(skills_dir, dry_run); + printf("CodeBuddy: removed %d skill(s)\n", removed); + + char mcp_path[CLI_BUF_1K]; + snprintf(mcp_path, sizeof(mcp_path), "%s/.mcp.json", config_dir); + if (!dry_run) { + cbm_remove_editor_mcp(mcp_path); + } + printf(" removed MCP config entry\n"); + + char settings_path[CLI_BUF_1K]; + snprintf(settings_path, sizeof(settings_path), "%s/settings.json", config_dir); + if (!dry_run) { + cbm_remove_codebuddy_hooks(settings_path); + cbm_remove_codebuddy_session_hooks(settings_path); + } + printf(" removed PreToolUse + SessionStart hooks\n"); +} + /* Remove MCP + instructions for a generic agent. */ typedef struct { @@ -3657,6 +3945,9 @@ int cbm_cmd_uninstall(int argc, char **argv) { if (agents.claude_code) { uninstall_claude_code(home, dry_run); } + if (agents.codebuddy) { + uninstall_codebuddy(home, dry_run); + } uninstall_cli_agents(&agents, home, dry_run); uninstall_editor_agents(&agents, home, dry_run); diff --git a/src/cli/cli.h b/src/cli/cli.h index 384af45a..33f9717b 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -122,6 +122,7 @@ typedef struct { bool cursor; /* ~/.cursor/ exists */ bool openclaw; /* ~/.openclaw/ exists */ bool kiro; /* ~/.kiro/ exists */ + bool codebuddy; /* ~/.codebuddy/ exists */ } cbm_detected_agents_t; /* Detect which coding agents are installed. @@ -175,6 +176,15 @@ int cbm_upsert_claude_hooks(const char *settings_path); * Returns 0 on success. */ int cbm_remove_claude_hooks(const char *settings_path); +/* Upsert a PreToolUse hook in ~/.codebuddy/settings.json for CodeBuddy. + * Adds a Grep|Glob matcher that reminds to use MCP tools. + * Returns 0 on success. */ +int cbm_upsert_codebuddy_hooks(const char *settings_path); + +/* Remove our PreToolUse hook from CodeBuddy settings.json. + * Returns 0 on success. */ +int cbm_remove_codebuddy_hooks(const char *settings_path); + /* Write the PreToolUse gate shim to /.claude/hooks/. The shim is a thin * wrapper that invokes the compiled `hook-augment` and writes to stdout only — * it must never create a predictable temp/state file (issue #384). Exposed for @@ -196,6 +206,8 @@ int cbm_upsert_codex_hooks(const char *config_path); int cbm_remove_codex_hooks(const char *config_path); int cbm_upsert_gemini_session_hooks(const char *settings_path); int cbm_remove_gemini_session_hooks(const char *settings_path); +int cbm_upsert_codebuddy_session_hooks(const char *settings_path); +int cbm_remove_codebuddy_session_hooks(const char *settings_path); /* ── PATH management ──────────────────────────────────────────── */ diff --git a/src/discover/discover.c b/src/discover/discover.c index 314c00c5..e26bf0b6 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -28,7 +28,7 @@ static const char *ALWAYS_SKIP_DIRS[] = { /* VCS */ ".git", ".hg", ".svn", ".worktrees", /* IDE */ - ".idea", ".vs", ".vscode", ".eclipse", ".claude", + ".idea", ".vs", ".vscode", ".eclipse", ".claude", ".codebuddy", /* Python */ ".cache", ".eggs", ".env", ".mypy_cache", ".nox", ".pytest_cache", ".ruff_cache", ".tox", ".venv", "__pycache__", "env", "htmlcov", "site-packages", "venv", diff --git a/src/main.c b/src/main.c index f2b72cba..0cd0db8f 100644 --- a/src/main.c +++ b/src/main.c @@ -295,7 +295,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("\nSupported agents (auto-detected):\n"); - printf(" Claude Code, Codex CLI, Gemini CLI, Zed, OpenCode,\n"); + printf(" Claude Code, CodeBuddy, Codex CLI, Gemini CLI, Zed, OpenCode,\n"); printf(" Antigravity, Aider, KiloCode, Kiro\n"); printf("\nTools: index_repository, search_graph, query_graph, trace_path,\n"); printf(" get_code_snippet, get_graph_schema, get_architecture, search_code,\n"); diff --git a/tests/test_cli.c b/tests/test_cli.c index add43138..bd0fef73 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -1482,6 +1482,63 @@ TEST(cli_detect_agents_finds_codex) { PASS(); } +TEST(cli_detect_agents_finds_codebuddy) { + 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/.codebuddy", tmpdir); + test_mkdirp(dir); + + /* Unset CODEBUDDY_CONFIG_DIR so detection is exercised against home_dir/.codebuddy */ + const char *saved = getenv("CODEBUDDY_CONFIG_DIR"); + char *saved_copy = saved ? strdup(saved) : NULL; + cbm_unsetenv("CODEBUDDY_CONFIG_DIR"); + + cbm_detected_agents_t agents = cbm_detect_agents(tmpdir); + ASSERT_TRUE(agents.codebuddy); + + if (saved_copy) { + cbm_setenv("CODEBUDDY_CONFIG_DIR", saved_copy, 1); + free(saved_copy); + } + + test_rmdir_r(tmpdir); + PASS(); +} + +TEST(cli_detect_agents_finds_codebuddy_via_env) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + /* Config dir lives OUTSIDE home_dir/.codebuddy, pointed at by CODEBUDDY_CONFIG_DIR. */ + char ccd[512]; + snprintf(ccd, sizeof(ccd), "%s/custom-codebuddy", tmpdir); + test_mkdirp(ccd); + + const char *saved = getenv("CODEBUDDY_CONFIG_DIR"); + char *saved_copy = saved ? strdup(saved) : NULL; + cbm_setenv("CODEBUDDY_CONFIG_DIR", ccd, 1); + + /* home_dir has no .codebuddy, but detection must still find CodeBuddy via the env var. */ + cbm_detected_agents_t agents = cbm_detect_agents(tmpdir); + ASSERT_TRUE(agents.codebuddy); + + if (saved_copy) { + cbm_setenv("CODEBUDDY_CONFIG_DIR", saved_copy, 1); + free(saved_copy); + } else { + cbm_unsetenv("CODEBUDDY_CONFIG_DIR"); + } + + test_rmdir_r(tmpdir); + PASS(); +} + /* issue #222: Cursor (~/.cursor/) must be detected so install/update registers * the MCP server in ~/.cursor/mcp.json — previously it was never discovered. */ TEST(cli_detect_agents_finds_cursor_issue222) { @@ -1597,6 +1654,29 @@ TEST(cli_gemini_session_hook_parity) { PASS(); } +TEST(cli_codebuddy_session_hook_parity) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codebuddyhook-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char cfg[512]; + snprintf(cfg, sizeof(cfg), "%s/settings.json", tmpdir); + + ASSERT_EQ(cbm_upsert_codebuddy_session_hooks(cfg), 0); + const char *d = read_test_file(cfg); + ASSERT_NOT_NULL(d); + ASSERT(strstr(d, "SessionStart") != NULL); + ASSERT(strstr(d, "search_graph") != NULL); + + ASSERT_EQ(cbm_remove_codebuddy_session_hooks(cfg), 0); + d = read_test_file(cfg); + ASSERT_NULL(strstr(d, "SessionStart")); + + test_rmdir_r(tmpdir); + PASS(); +} + TEST(cli_detect_agents_finds_gemini) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX"); @@ -2244,6 +2324,50 @@ TEST(cli_remove_claude_hooks) { PASS(); } +TEST(cli_upsert_codebuddy_hook_fresh) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codebuddy-hook-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char settingspath[512]; + snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir); + + int rc = cbm_upsert_codebuddy_hooks(settingspath); + ASSERT_EQ(rc, 0); + + const char *data = read_test_file(settingspath); + ASSERT_NOT_NULL(data); + ASSERT(strstr(data, "PreToolUse") != NULL); + ASSERT(strstr(data, "\"Grep|Glob\"") != NULL); + ASSERT(strstr(data, "Glob|Read") == NULL); + ASSERT(strstr(data, "cbm-code-discovery-gate") != NULL); + + test_rmdir_r(tmpdir); + PASS(); +} + +TEST(cli_remove_codebuddy_hooks) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-codebuddy-hook-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + char settingspath[512]; + snprintf(settingspath, sizeof(settingspath), "%s/settings.json", tmpdir); + + cbm_upsert_codebuddy_hooks(settingspath); + int rc = cbm_remove_codebuddy_hooks(settingspath); + ASSERT_EQ(rc, 0); + + const char *data = read_test_file(settingspath); + ASSERT_NOT_NULL(data); + ASSERT(strstr(data, "Grep|Glob|Read") == NULL); + + test_rmdir_r(tmpdir); + PASS(); +} + /* ═══════════════════════════════════════════════════════════════════ * Group D: Pre-Tool Hook Upsert — Gemini CLI / Antigravity * ═══════════════════════════════════════════════════════════════════ */ @@ -2667,10 +2791,13 @@ SUITE(cli) { RUN_TEST(cli_detect_agents_finds_claude); RUN_TEST(cli_detect_agents_finds_claude_via_env); RUN_TEST(cli_detect_agents_finds_codex); + RUN_TEST(cli_detect_agents_finds_codebuddy); + RUN_TEST(cli_detect_agents_finds_codebuddy_via_env); RUN_TEST(cli_detect_agents_finds_cursor_issue222); RUN_TEST(cli_install_plan_receipt_no_mutation_issue388); RUN_TEST(cli_codex_session_hook_issue330); RUN_TEST(cli_gemini_session_hook_parity); + RUN_TEST(cli_codebuddy_session_hook_parity); RUN_TEST(cli_detect_agents_finds_gemini); RUN_TEST(cli_detect_agents_finds_zed); RUN_TEST(cli_detect_agents_finds_antigravity); @@ -2710,6 +2837,10 @@ SUITE(cli) { RUN_TEST(cli_upsert_claude_hook_preserves_others); RUN_TEST(cli_remove_claude_hooks); + /* CodeBuddy hooks (2 tests — group D) */ + RUN_TEST(cli_upsert_codebuddy_hook_fresh); + RUN_TEST(cli_remove_codebuddy_hooks); + /* Gemini CLI hooks (4 tests — group D) */ RUN_TEST(cli_upsert_gemini_hook_fresh); RUN_TEST(cli_upsert_gemini_hook_existing); diff --git a/tests/test_discover.c b/tests/test_discover.c index af51a928..2ec391a7 100644 --- a/tests/test_discover.c +++ b/tests/test_discover.c @@ -57,6 +57,10 @@ TEST(skip_claude) { ASSERT_TRUE(cbm_should_skip_dir(".claude", CBM_MODE_FULL)); PASS(); } +TEST(skip_codebuddy) { + ASSERT_TRUE(cbm_should_skip_dir(".codebuddy", CBM_MODE_FULL)); + PASS(); +} /* Not skipped in full mode */ TEST(no_skip_src) { @@ -719,6 +723,7 @@ SUITE(discover) { RUN_TEST(skip_coverage); RUN_TEST(skip_idea); RUN_TEST(skip_claude); + RUN_TEST(skip_codebuddy); /* Not skipped */ RUN_TEST(no_skip_src);