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 @@
[](https://github.com/DeusData/codebase-memory-mcp)
[](https://github.com/DeusData/codebase-memory-mcp)
[](#hybrid-lsp)
-[](https://github.com/DeusData/codebase-memory-mcp)
+[](https://github.com/DeusData/codebase-memory-mcp)
[](https://github.com/DeusData/codebase-memory-mcp)
[](https://github.com/DeusData/codebase-memory-mcp/releases/latest)
[](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);