From 7a7edeac8e21b573ac2acdf36f4f5aa2c2bdd3ee Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Thu, 17 Sep 2026 01:03:39 +0800 Subject: [PATCH 1/3] feat: auto-register codedb in more MCP clients The installer only wired Claude Code, Codex, Gemini CLI, Cursor, Windsurf, and Devin. Agents using oh-my-pi, Hermes, Qwen Code, ZCode, Trae, Cline, Copilot CLI, Antigravity, Kiro, and OpenCode still had to edit config by hand. Register those clients when their config dir or CLI is present, using each tool's documented file and schema. Skip malformed JSON instead of replacing the file. codedb nuke removes the same entries. Co-Authored-By: Codegraff --- CHANGELOG.md | 2 + README.md | 6 +- docs/mcp.md | 171 +++++++++++++++++++- install/install.sh | 294 ++++++++++++++++++++++++++++++++-- src/nuke.zig | 200 ++++++++++++++++++++--- src/test_mcp.zig | 59 +++++++ website/app/index.zig | 2 +- website/app/install_script.sh | 294 ++++++++++++++++++++++++++++++++-- website/dist/install.sh | 294 ++++++++++++++++++++++++++++++++-- 9 files changed, 1237 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 497679d9..61ffabaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Auto-register codedb in additional detected MCP clients: oh-my-pi, Hermes, Qwen Code, ZCode, Trae, Cline, Copilot CLI, Antigravity, Kiro, and OpenCode. `codedb nuke` removes the matching entries. Invalid JSON is left untouched. + ## 0.2.5856 - 2026-09-19 - Skip `.worktrees` during indexing and listing so Git worktree forests cannot exhaust the macOS directory watch budget (#754). A scan root that is itself a worktree checkout is still indexed. diff --git a/README.md b/README.md index c7965f7e..ee55d495 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ the map**: focused source context, symbols, callers, outlines, and dependencies. Your coding client uses its own native tools to make the edit. **A context engine, not an editor.** codedb has no edit capability. Use it with -Graff, Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Devin, or another MCP client. +Graff, Claude Code, Codex, Gemini CLI, Cursor, Windsurf, Devin, oh-my-pi, Hermes, Qwen Code, ZCode, Trae, Cline, Copilot CLI, Antigravity, Kiro, OpenCode, or another MCP client. ## 🧭 Give your agent a compass @@ -62,7 +62,7 @@ embeddings**; choose `semantic=local` for local-only retrieval. See curl -fsSL https://codedb.codegraff.com/install.sh | bash ``` -Downloads the binary for your platform and auto-registers codedb as an MCP server in **Claude Code**, **Codex**, **Gemini CLI**, **Cursor**, **Windsurf**, and **Devin** — each written directly and additively into that tool's config (only when the tool is present). The installer prints the exact `codedb mcp` command it registered plus hook setup pointers for Codex and Claude Code. +Downloads the binary for your platform and auto-registers codedb as an MCP server in **Claude Code**, **Codex**, **Gemini CLI**, **Cursor**, **Windsurf**, **Devin**, **oh-my-pi**, **Hermes**, **Qwen Code**, **ZCode**, **Trae**, **Cline**, **Copilot CLI**, **Antigravity**, **Kiro**, and **OpenCode** — each written directly and additively into that tool's config (only when the tool is present). The installer prints the exact `codedb mcp` command it registered plus hook setup pointers for Codex and Claude Code. On Windows, run this command inside WSL only if you want the Linux binary inside WSL. For the native Windows binary, use PowerShell below. @@ -134,7 +134,7 @@ over stdio, with an optional localhost HTTP server. ## Documentation -- **[MCP setup](docs/mcp.md)** — per-client configurations (Claude Desktop, Cursor, VS Code, Claude Code, Codex CLI, Gemini CLI), root resolution, troubleshooting +- **[MCP setup](docs/mcp.md)** — per-client configurations (Claude Desktop, Cursor, VS Code, Claude Code, Codex CLI, Gemini CLI, oh-my-pi, Hermes, Qwen Code, ZCode, Trae, Cline, Copilot CLI, Antigravity, Kiro, OpenCode), root resolution, troubleshooting - **[Skill base & context files](docs/skills.md)** — `agents.md` / `CLAUDE.md` / `GEMINI.md`, `.codedbrc`, per-developer memory - **[CLI reference](docs/cli.md)** — every command, every flag - **[Architecture](docs/architecture.md)** — engine internals, index layout diff --git a/docs/mcp.md b/docs/mcp.md index f098a344..c6bb49de 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -27,8 +27,10 @@ curl -fsSL https://codedb.codegraff.com/install.sh | bash The installer downloads the binary for your platform, drops it in `~/bin` (or `$CODEDB_DIR` when set), and auto-registers codedb as an MCP server in every client it can find — Claude Code, Codex, -Gemini CLI, Cursor, Windsurf, and Devin. It prints the exact `codedb mcp` command it -registered. +Gemini CLI, Cursor, Windsurf, Devin, oh-my-pi, Hermes, Qwen Code, ZCode, +Trae, Cline, Copilot CLI, Antigravity, Kiro, and OpenCode. It prints the exact `codedb mcp` command it +registered. Only clients that already look installed (config dir or CLI on +`PATH`) are touched. ### Windows x86_64 (native) @@ -126,10 +128,9 @@ codex mcp add codedb -- /absolute/path/to/codedb mcp codex mcp add codedb -- "$env:LOCALAPPDATA\Programs\codedb\codedb.exe" mcp ``` -### Gemini CLI / opencode +### Gemini CLI -Both read MCP configuration from `~/.gemini/mcp.json` (Gemini) and -`~/.config/opencode/mcp.json` (opencode): +Edit `~/.gemini/settings.json`: ```json { @@ -142,6 +143,166 @@ Both read MCP configuration from `~/.gemini/mcp.json` (Gemini) and } ``` +### oh-my-pi + +User config is `~/.omp/agent/mcp.json` (or `~/.omp/profiles//agent/mcp.json` +when a named profile is active): + +```json +{ + "mcpServers": { + "codedb": { + "command": "/absolute/path/to/codedb", + "args": ["mcp"] + } + } +} +``` + +### Hermes + +Add a stdio server under `mcp_servers` in `~/.hermes/config.yaml`: + +```yaml +mcp_servers: + codedb: + command: "/absolute/path/to/codedb" + args: ["mcp"] +``` + +### Qwen Code + +Edit `~/.qwen/settings.json`: + +```json +{ + "mcpServers": { + "codedb": { + "command": "/absolute/path/to/codedb", + "args": ["mcp"] + } + } +} +``` + +### ZCode + +User config is `~/.zcode/cli/config.json` (`mcp.servers`): + +```json +{ + "mcp": { + "servers": { + "codedb": { + "command": "/absolute/path/to/codedb", + "args": ["mcp"] + } + } + } +} +``` + +### Trae / TraeCode + +User-level: `~/.trae/mcp.json`. Trae IDE also reads +`~/Library/Application Support/Trae/User/settings/mcp.json` (macOS) or +`%APPDATA%\Trae\User\settings\mcp.json` (Windows): + +```json +{ + "mcpServers": { + "codedb": { + "command": "/absolute/path/to/codedb", + "args": ["mcp"] + } + } +} +``` + +### Cline + +CLI/SDK: `~/.cline/data/settings/cline_mcp_settings.json`. The VS Code/Cursor +extension uses the same `mcpServers` object in its `cline_mcp_settings.json`: + +```json +{ + "mcpServers": { + "codedb": { + "command": "/absolute/path/to/codedb", + "args": ["mcp"] + } + } +} +``` + +### GitHub Copilot CLI + +Edit `~/.copilot/mcp-config.json`, or run +`copilot mcp add codedb -- /absolute/path/to/codedb mcp`: + +```json +{ + "mcpServers": { + "codedb": { + "type": "local", + "command": "/absolute/path/to/codedb", + "args": ["mcp"], + "tools": ["*"] + } + } +} +``` + +### Google Antigravity + +Global config is `~/.gemini/config/mcp_config.json` (older IDE builds also +read `~/.gemini/antigravity/mcp_config.json`): + +```json +{ + "mcpServers": { + "codedb": { + "command": "/absolute/path/to/codedb", + "args": ["mcp"] + } + } +} +``` + +### Kiro + +User config is `~/.kiro/settings/mcp.json`: + +```json +{ + "mcpServers": { + "codedb": { + "command": "/absolute/path/to/codedb", + "args": ["mcp"] + } + } +} +``` + +### OpenCode + +Edit `~/.config/opencode/opencode.json`. Current OpenCode uses a `mcp` map +with `type: "local"` and a command array (v2 nested `mcp.servers` is also +accepted): + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "codedb": { + "type": "local", + "command": ["/absolute/path/to/codedb", "mcp"], + "enabled": true + } + } +} +``` + ### Tool profile (smaller tools/list) Agent harnesses default to `CODEDB_TOOLS_PROFILE=mini`: five one-shot tools diff --git a/install/install.sh b/install/install.sh index a8b2d92a..2c494904 100644 --- a/install/install.sh +++ b/install/install.sh @@ -197,24 +197,249 @@ _register_json_mcp() { local config="$1" local codedb_bin="$2" local label="$3" - python3 - "$config" "$codedb_bin" << 'PYEOF' -import json, sys, os -config_path, codedb_bin = sys.argv[1], sys.argv[2] -try: + _register_mcp "$config" "$codedb_bin" "$label" "mcpServers" +} + +_client_present() { + local dir="$1" + shift + if [ -n "$dir" ] && [ -d "$dir" ]; then + return 0 + fi + local cmd + for cmd in "$@"; do + [ -n "$cmd" ] && command -v "$cmd" >/dev/null 2>&1 && return 0 + done + return 1 +} + +# flavor: mcpServers | mcp.servers | opencode | copilot | hermes +_register_mcp() { + local config="$1" + local codedb_bin="$2" + local label="$3" + local flavor="$4" + local rc=0 + python3 - "$config" "$codedb_bin" "$flavor" << 'PYEOF' || rc=$? +import json, os, re, sys + +config_path, codedb_bin, flavor = sys.argv[1], sys.argv[2], sys.argv[3] + +def load_json(path): + try: + with open(path) as f: + data = json.load(f) + except FileNotFoundError: + return {} + except json.JSONDecodeError: + sys.exit(4) + return data if isinstance(data, dict) else {} + +def save_json(path, data): + d = os.path.dirname(path) + if d: + os.makedirs(d, exist_ok=True) + with open(path, "w") as f: + json.dump(data, f, indent=2) + f.write("\n") + +stdio = {"command": codedb_bin, "args": ["mcp"]} + +if flavor == "mcpServers": + data = load_json(config_path) + data.setdefault("mcpServers", {})["codedb"] = stdio + save_json(config_path, data) +elif flavor == "mcp.servers": + data = load_json(config_path) + data.setdefault("mcp", {}).setdefault("servers", {})["codedb"] = stdio + save_json(config_path, data) +elif flavor == "opencode": + data = load_json(config_path) + mcp = data.setdefault("mcp", {}) + if not isinstance(mcp, dict): + mcp = {} + data["mcp"] = mcp + entry = {"type": "local", "command": [codedb_bin, "mcp"], "enabled": True} + if isinstance(mcp.get("servers"), dict): + mcp["servers"]["codedb"] = entry + else: + mcp["codedb"] = entry + save_json(config_path, data) +elif flavor == "copilot": + data = load_json(config_path) + data.setdefault("mcpServers", {})["codedb"] = { + "type": "local", + "command": codedb_bin, + "args": ["mcp"], + "tools": ["*"], + } + save_json(config_path, data) +elif flavor == "hermes": + quoted = json.dumps(codedb_bin) + entry = [" codedb:", f" command: {quoted}", ' args: ["mcp"]'] + d = os.path.dirname(config_path) + if d: + os.makedirs(d, exist_ok=True) + if not os.path.isfile(config_path) or os.path.getsize(config_path) == 0: + with open(config_path, "w") as f: + f.write("mcp_servers:\n" + "\n".join(entry) + "\n") + sys.exit(0) with open(config_path) as f: - data = json.load(f) -except (FileNotFoundError, json.JSONDecodeError): - data = {} -servers = data.setdefault("mcpServers", {}) -servers["codedb"] = {"command": codedb_bin, "args": ["mcp"]} -d = os.path.dirname(config_path) -if d: - os.makedirs(d, exist_ok=True) -with open(config_path, "w") as f: - json.dump(data, f, indent=2) - f.write("\n") + raw = f.read() + nl = "\r\n" if "\r\n" in raw else "\n" + lines = raw.splitlines(keepends=True) + + def body(line): + return line.split("#", 1)[0].rstrip("\r\n") + + def indent_of(line): + s = body(line) + if not s.strip(): + return None + return len(s) - len(s.lstrip(" ")) + + mcp_idx = None + for i, line in enumerate(lines): + if re.match(r"^mcp_servers:\s*$", body(line)): + mcp_idx = i + break + if mcp_idx is None: + for i, line in enumerate(lines): + if re.match(r"^mcp_servers:\s+\S", body(line)): + sys.exit(5) + if raw and not raw.endswith(("\n", "\r\n")): + raw += nl + extra = "" if raw.endswith(nl) else nl + with open(config_path, "w") as f: + f.write(raw + extra + "mcp_servers:" + nl + nl.join(entry) + nl) + sys.exit(0) + + codedb_start = None + codedb_end = None + i = mcp_idx + 1 + while i < len(lines): + ind = indent_of(lines[i]) + if ind is None: + i += 1 + continue + if ind == 0: + break + if ind == 2 and re.match(r"^\s{2}codedb:\s*", body(lines[i])): + codedb_start = i + j = i + 1 + while j < len(lines): + indj = indent_of(lines[j]) + if indj is None: + j += 1 + continue + if indj <= 2: + break + j += 1 + codedb_end = j + break + i += 1 + + block = [e + nl for e in entry] + if codedb_start is not None: + new_lines = lines[:codedb_start] + block + lines[codedb_end:] + else: + new_lines = lines[: mcp_idx + 1] + block + lines[mcp_idx + 1 :] + with open(config_path, "w") as f: + f.writelines(new_lines) +else: + sys.exit(2) PYEOF - printf " ${G}✓${N} %-12s ${D}→ %s${N}\n" "$label" "$config" + case "$rc" in + 0) printf " ${G}✓${N} %-12s ${D}→ %s${N}\n" "$label" "$config" ;; + 4) printf " ${Y}%-12s skip (invalid JSON) → %s${N}\n" "$label" "$config" ;; + 5) printf " ${Y}%-12s skip (inline YAML mcp_servers) → %s${N}\n" "$label" "$config" ;; + *) printf " ${Y}%-12s registration failed → %s${N}\n" "$label" "$config" ;; + esac + return 0 +} + +register_detected_clients() { + local codedb_bin="$1" + if ! command -v python3 >/dev/null 2>&1; then + printf " ${D}extra clients: skip (python3 not found)${N}\n" + return + fi + + if _client_present "$HOME/.omp" omp; then + _register_mcp "$HOME/.omp/agent/mcp.json" "$codedb_bin" "oh-my-pi" "mcpServers" + fi + if _client_present "$HOME/.hermes" hermes; then + _register_mcp "$HOME/.hermes/config.yaml" "$codedb_bin" "hermes" "hermes" + fi + if _client_present "$HOME/.qwen" qwen; then + _register_mcp "$HOME/.qwen/settings.json" "$codedb_bin" "qwen-code" "mcpServers" + fi + if _client_present "$HOME/.zcode" zcode; then + _register_mcp "$HOME/.zcode/cli/config.json" "$codedb_bin" "zcode" "mcp.servers" + fi + + if _client_present "$HOME/.trae" trae traecli; then + _register_mcp "$HOME/.trae/mcp.json" "$codedb_bin" "trae" "mcpServers" + fi + local trae_root trae_cfg + for trae_root in \ + "$HOME/Library/Application Support/Trae" \ + "$HOME/Library/Application Support/Trae CN" \ + "$HOME/AppData/Roaming/Trae" \ + "$HOME/AppData/Roaming/Trae CN" + do + [ -d "$trae_root" ] || continue + if [ -d "$trae_root/User/settings" ] || [ -f "$trae_root/User/settings/mcp.json" ]; then + trae_cfg="$trae_root/User/settings/mcp.json" + elif [ -d "$trae_root/User" ] || [ -f "$trae_root/User/mcp.json" ]; then + trae_cfg="$trae_root/User/mcp.json" + else + continue + fi + _register_mcp "$trae_cfg" "$codedb_bin" "trae" "mcpServers" + done + + if _client_present "$HOME/.cline" cline; then + _register_mcp "$HOME/.cline/data/settings/cline_mcp_settings.json" "$codedb_bin" "cline" "mcpServers" + fi + local cline_cfg cline_ext + for cline_cfg in \ + "$HOME/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/.config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/AppData/Roaming/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/AppData/Roaming/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" + do + cline_ext="$(dirname "$(dirname "$cline_cfg")")" + if [ -d "$cline_ext" ]; then + _register_mcp "$cline_cfg" "$codedb_bin" "cline" "mcpServers" + fi + done + + if _client_present "$HOME/.copilot" copilot; then + _register_mcp "$HOME/.copilot/mcp-config.json" "$codedb_bin" "copilot" "copilot" + fi + + if _client_present "$HOME/.gemini/antigravity" agy antigravity; then + _register_mcp "$HOME/.gemini/config/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" + if [ -d "$HOME/.gemini/antigravity" ]; then + _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" + fi + fi + + if _client_present "$HOME/.kiro" kiro kiro-cli; then + _register_mcp "$HOME/.kiro/settings/mcp.json" "$codedb_bin" "kiro" "mcpServers" + fi + + if _client_present "$HOME/.config/opencode" opencode; then + if [ -f "$HOME/.config/opencode/opencode.jsonc" ] && [ ! -f "$HOME/.config/opencode/opencode.json" ]; then + printf " ${Y}%-12s skip (opencode.jsonc is not rewritten) → %s${N}\n" \ + "opencode" "$HOME/.config/opencode/opencode.jsonc" + else + _register_mcp "$HOME/.config/opencode/opencode.json" "$codedb_bin" "opencode" "opencode" + fi + fi } DEEPWIKI_URL="https://mcp.deepwiki.com/mcp" @@ -230,8 +455,12 @@ config_path, entry_json = sys.argv[1], sys.argv[2] try: with open(config_path) as f: data = json.load(f) -except (FileNotFoundError, json.JSONDecodeError): +except FileNotFoundError: data = {} +except json.JSONDecodeError: + sys.exit(4) # never clobber a malformed file +if not isinstance(data, dict): + sys.exit(4) servers = data.setdefault("mcpServers", {}) if "deepwiki" in servers: sys.exit(3) # already configured — never clobber a user's own entry @@ -246,6 +475,7 @@ PYEOF case "$rc" in 0) printf " ${G}✓${N} %-12s ${D}→ %s (+deepwiki)${N}\n" "$label" "$config" ;; 3) printf " ${D}%-12s deepwiki already configured → %s${N}\n" "$label" "$config" ;; + 4) printf " ${Y}%-12s skip deepwiki (invalid JSON) → %s${N}\n" "$label" "$config" ;; *) printf " ${Y}%-12s deepwiki registration failed → %s${N}\n" "$label" "$config" ;; esac return 0 @@ -288,6 +518,35 @@ register_deepwiki() { _register_deepwiki_json "$HOME/.config/devin/config.json" "devin" \ "{\"serverUrl\":\"$DEEPWIKI_URL\"}" fi + # Detected extra clients that use a standard mcpServers JSON object. + if [ -d "$HOME/.omp" ] || command -v omp >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.omp/agent/mcp.json" "oh-my-pi" \ + "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.qwen" ] || command -v qwen >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.qwen/settings.json" "qwen-code" \ + "{\"httpUrl\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.trae" ] || command -v trae >/dev/null 2>&1 || command -v traecli >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.trae/mcp.json" "trae" \ + "{\"url\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.cline" ] || command -v cline >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.cline/data/settings/cline_mcp_settings.json" "cline" \ + "{\"type\":\"streamableHttp\",\"url\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.copilot" ] || command -v copilot >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.copilot/mcp-config.json" "copilot" \ + "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\",\"tools\":[\"*\"]}" + fi + if [ -d "$HOME/.gemini/antigravity" ] || command -v agy >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.gemini/config/mcp_config.json" "antigravity" \ + "{\"serverUrl\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.kiro" ] || command -v kiro >/dev/null 2>&1 || command -v kiro-cli >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.kiro/settings/mcp.json" "kiro" \ + "{\"url\":\"$DEEPWIKI_URL\"}" + fi # Codex: TOML, url key selects the streamable-HTTP transport. local codex_cfg="$HOME/.codex/config.toml" if grep -q '\[mcp_servers\.deepwiki\]' "$codex_cfg" 2>/dev/null; then @@ -605,6 +864,7 @@ main() { register_gemini "$dest" register_cursor "$dest" register_windsurf_devin "$dest" + register_detected_clients "$dest" register_deepwiki register_hooks print_hook_notes "$dest" diff --git a/src/nuke.zig b/src/nuke.zig index 8c16773f..b72ac1bf 100644 --- a/src/nuke.zig +++ b/src/nuke.zig @@ -295,17 +295,45 @@ fn removeRegisteredSnapshots(io: std.Io, allocator: std.mem.Allocator, home: []c fn deregisterInstalledIntegrations(io: std.Io, allocator: std.mem.Allocator, home: []const u8) usize { var removed: usize = 0; - const claude_config = std.fmt.allocPrint(allocator, "{s}/.claude.json", .{home}) catch return removed; - defer allocator.free(claude_config); - if (deregisterJsonIntegrationFile(io, allocator, claude_config) catch false) removed += 1; - - const gemini_config = std.fmt.allocPrint(allocator, "{s}/.gemini/settings.json", .{home}) catch return removed; - defer allocator.free(gemini_config); - if (deregisterJsonIntegrationFile(io, allocator, gemini_config) catch false) removed += 1; + const json_rel_paths = [_][]const u8{ + ".claude.json", + ".gemini/settings.json", + ".cursor/mcp.json", + ".codeium/windsurf/mcp_config.json", + ".config/devin/config.json", + ".omp/agent/mcp.json", + ".qwen/settings.json", + ".zcode/cli/config.json", + ".trae/mcp.json", + ".cline/data/settings/cline_mcp_settings.json", + ".copilot/mcp-config.json", + ".gemini/config/mcp_config.json", + ".gemini/antigravity/mcp_config.json", + ".kiro/settings/mcp.json", + ".config/opencode/opencode.json", + ".config/opencode/mcp.json", + "Library/Application Support/Trae/User/settings/mcp.json", + "Library/Application Support/Trae CN/User/settings/mcp.json", + "Library/Application Support/Trae CN/User/mcp.json", + "Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", + "Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", + "AppData/Roaming/Trae/User/settings/mcp.json", + "AppData/Roaming/Trae CN/User/settings/mcp.json", + "AppData/Roaming/Trae CN/User/mcp.json", + "AppData/Roaming/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", + "AppData/Roaming/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", + ".config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", + ".config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", + }; + for (json_rel_paths) |rel| { + const path = std.fmt.allocPrint(allocator, "{s}/{s}", .{ home, rel }) catch return removed; + defer allocator.free(path); + if (deregisterJsonIntegrationFile(io, allocator, path) catch false) removed += 1; + } - const cursor_config = std.fmt.allocPrint(allocator, "{s}/.cursor/mcp.json", .{home}) catch return removed; - defer allocator.free(cursor_config); - if (deregisterJsonIntegrationFile(io, allocator, cursor_config) catch false) removed += 1; + const hermes_config = std.fmt.allocPrint(allocator, "{s}/.hermes/config.yaml", .{home}) catch return removed; + defer allocator.free(hermes_config); + if (deregisterYamlIntegrationFile(io, allocator, hermes_config) catch false) removed += 1; const codex_config = std.fmt.allocPrint(allocator, "{s}/.codex/config.toml", .{home}) catch return removed; defer allocator.free(codex_config); @@ -318,16 +346,6 @@ fn deregisterInstalledIntegrations(io: std.Io, allocator: std.mem.Allocator, hom defer allocator.free(codex_agents); if (codex_setup.deregisterCodexPolicyFile(io, allocator, codex_agents) catch false) removed += 1; - // Windsurf and Devin are registered via mcpsync; both store servers under a - // standard `mcpServers` object, so the JSON deregister handles them too. - const windsurf_config = std.fmt.allocPrint(allocator, "{s}/.codeium/windsurf/mcp_config.json", .{home}) catch return removed; - defer allocator.free(windsurf_config); - if (deregisterJsonIntegrationFile(io, allocator, windsurf_config) catch false) removed += 1; - - const devin_config = std.fmt.allocPrint(allocator, "{s}/.config/devin/config.json", .{home}) catch return removed; - defer allocator.free(devin_config); - if (deregisterJsonIntegrationFile(io, allocator, devin_config) catch false) removed += 1; - return removed; } @@ -389,6 +407,16 @@ pub fn deregisterCodexIntegrationFile(io: std.Io, allocator: std.mem.Allocator, return true; } +pub fn deregisterYamlIntegrationFile(io: std.Io, allocator: std.mem.Allocator, path: []const u8) !bool { + const content = (try readOptionalConfigFile(io, allocator, path)) orelse return false; + defer allocator.free(content); + + const rewritten = try removeYamlMcpServerEntry(allocator, content, "codedb") orelse return false; + defer allocator.free(rewritten); + try rewriteConfigFile(io, allocator, path, rewritten); + return true; +} + fn rewriteConfigFile(io: std.Io, allocator: std.mem.Allocator, path: []const u8, content: []const u8) !void { if (std.mem.trim(u8, content, " \t\r\n").len == 0) { std.Io.Dir.cwd().deleteFile(io, path) catch |err| switch (err) { @@ -410,18 +438,54 @@ fn rewriteConfigFile(io: std.Io, allocator: std.mem.Allocator, path: []const u8, try std.Io.Dir.rename(std.Io.Dir.cwd(), tmp_path, std.Io.Dir.cwd(), path, io); } +fn looksLikeMcpServerObject(value: std.json.Value) bool { + if (value != .object) return false; + return value.object.get("command") != null or value.object.get("type") != null or value.object.get("url") != null or value.object.get("httpUrl") != null or value.object.get("serverUrl") != null; +} + +fn removeNamedServerObject(value: *std.json.Value, server_name: []const u8) bool { + if (value.* != .object) return false; + const existing = value.object.get(server_name) orelse return false; + if (!looksLikeMcpServerObject(existing)) return false; + return value.object.swapRemove(server_name); +} + pub fn removeJsonMcpServerEntry(allocator: std.mem.Allocator, content: []const u8, server_name: []const u8) !?[]u8 { var parsed = std.json.parseFromSlice(std.json.Value, allocator, content, .{}) catch return null; defer parsed.deinit(); if (parsed.value != .object) return null; - const servers_value = parsed.value.object.getPtr("mcpServers") orelse return null; - if (servers_value.* != .object) return null; - if (!servers_value.object.swapRemove(server_name)) return null; - if (servers_value.object.count() == 0) { - _ = parsed.value.object.swapRemove("mcpServers"); + var removed = false; + + if (parsed.value.object.getPtr("mcpServers")) |servers_value| { + if (removeNamedServerObject(servers_value, server_name)) { + removed = true; + if (servers_value.object.count() == 0) { + _ = parsed.value.object.swapRemove("mcpServers"); + } + } } + if (!removed) { + if (parsed.value.object.getPtr("mcp")) |mcp_value| { + if (mcp_value.* == .object) { + if (mcp_value.object.getPtr("servers")) |servers_value| { + if (removeNamedServerObject(servers_value, server_name)) { + removed = true; + if (servers_value.object.count() == 0) { + _ = mcp_value.object.swapRemove("servers"); + } + } + } + if (!removed and removeNamedServerObject(mcp_value, server_name)) { + removed = true; + } + } + } + } + + if (!removed) return null; + const json = try std.json.Stringify.valueAlloc(allocator, parsed.value, .{ .whitespace = .indent_2 }); errdefer allocator.free(json); @@ -433,6 +497,92 @@ pub fn removeJsonMcpServerEntry(allocator: std.mem.Allocator, content: []const u return try out.toOwnedSlice(allocator); } +fn yamlLineBody(line: []const u8) []const u8 { + const no_cr = std.mem.trimEnd(u8, line, "\r"); + const comment_start = std.mem.indexOfScalar(u8, no_cr, '#') orelse return no_cr; + return std.mem.trimEnd(u8, no_cr[0..comment_start], " \t"); +} + +fn yamlIndent(line: []const u8) ?usize { + const body = yamlLineBody(line); + if (std.mem.trim(u8, body, " \t").len == 0) return null; + var indent: usize = 0; + for (body) |ch| { + if (ch == ' ') { + indent += 1; + } else { + break; + } + } + return indent; +} + +pub fn removeYamlMcpServerEntry(allocator: std.mem.Allocator, content: []const u8, server_name: []const u8) !?[]u8 { + const key = try std.fmt.allocPrint(allocator, "{s}:", .{server_name}); + defer allocator.free(key); + + var line_start: usize = 0; + var mcp_start: ?usize = null; + var mcp_end: usize = content.len; + while (line_start < content.len) { + const line_end = std.mem.indexOfScalarPos(u8, content, line_start, '\n') orelse content.len; + const line = content[line_start..line_end]; + const indent = yamlIndent(line); + if (indent) |ind| { + if (mcp_start == null) { + if (ind == 0 and std.mem.eql(u8, std.mem.trim(u8, yamlLineBody(line), " \t"), "mcp_servers:")) { + mcp_start = line_start; + } + } else if (ind == 0) { + mcp_end = line_start; + break; + } + } + line_start = if (line_end < content.len) line_end + 1 else content.len; + } + const section_start = mcp_start orelse return null; + + var child_start: usize = section_start; + const first_nl = std.mem.indexOfScalarPos(u8, content, section_start, '\n') orelse return null; + child_start = first_nl + 1; + + var remove_start: ?usize = null; + var remove_end: usize = 0; + var pos = child_start; + while (pos < mcp_end) { + const line_end = std.mem.indexOfScalarPos(u8, content, pos, '\n') orelse mcp_end; + const line = content[pos..line_end]; + const indent = yamlIndent(line); + if (indent) |ind| { + if (ind == 2) { + const body = std.mem.trim(u8, yamlLineBody(line), " \t"); + if (std.mem.startsWith(u8, body, key) and (body.len == key.len or body[key.len] == ' ')) { + remove_start = pos; + var scan = if (line_end < mcp_end) line_end + 1 else mcp_end; + while (scan < mcp_end) { + const next_end = std.mem.indexOfScalarPos(u8, content, scan, '\n') orelse mcp_end; + const next_indent = yamlIndent(content[scan..next_end]); + if (next_indent) |next_ind| { + if (next_ind <= 2) break; + } + scan = if (next_end < mcp_end) next_end + 1 else mcp_end; + } + remove_end = scan; + break; + } + } + } + pos = if (line_end < mcp_end) line_end + 1 else mcp_end; + } + + const start = remove_start orelse return null; + var out: std.ArrayList(u8) = .empty; + defer out.deinit(allocator); + try out.appendSlice(allocator, content[0..start]); + try out.appendSlice(allocator, content[remove_end..]); + return try out.toOwnedSlice(allocator); +} + pub fn removeCodexMcpServerBlock(allocator: std.mem.Allocator, content: []const u8, server_name: []const u8) !?[]u8 { const header = try std.fmt.allocPrint(allocator, "[mcp_servers.{s}]", .{server_name}); defer allocator.free(header); diff --git a/src/test_mcp.zig b/src/test_mcp.zig index 6e22d7e3..1ea716cf 100644 --- a/src/test_mcp.zig +++ b/src/test_mcp.zig @@ -634,6 +634,65 @@ test "nuke: removeJsonMcpServerEntry removes empty mcpServers object" { try testing.expect(std.mem.indexOf(u8, output, "\"theme\"") != null); } +test "nuke: removeJsonMcpServerEntry drops mcp.servers codedb" { + const input = + \\{ + \\ "mcp": { + \\ "servers": { + \\ "codedb": { "command": "/Users/me/bin/codedb", "args": ["mcp"] }, + \\ "other": { "command": "other", "args": [] } + \\ } + \\ } + \\} + ; + + const output = (try nuke_mod.removeJsonMcpServerEntry(testing.allocator, input, "codedb")) orelse + return error.TestUnexpectedResult; + defer testing.allocator.free(output); + + try testing.expect(std.mem.indexOf(u8, output, "\"codedb\"") == null); + try testing.expect(std.mem.indexOf(u8, output, "\"other\"") != null); +} + +test "nuke: removeJsonMcpServerEntry drops opencode mcp.codedb" { + const input = + \\{ + \\ "mcp": { + \\ "codedb": { "type": "local", "command": ["/Users/me/bin/codedb", "mcp"] }, + \\ "other": { "type": "local", "command": ["other"] } + \\ } + \\} + ; + + const output = (try nuke_mod.removeJsonMcpServerEntry(testing.allocator, input, "codedb")) orelse + return error.TestUnexpectedResult; + defer testing.allocator.free(output); + + try testing.expect(std.mem.indexOf(u8, output, "\"codedb\"") == null); + try testing.expect(std.mem.indexOf(u8, output, "\"other\"") != null); +} + +test "nuke: removeYamlMcpServerEntry drops hermes codedb only" { + const input = + \\model: gpt + \\mcp_servers: + \\ codedb: + \\ command: "/Users/me/bin/codedb" + \\ args: ["mcp"] + \\ other: + \\ command: "other" + \\ + ; + + const output = (try nuke_mod.removeYamlMcpServerEntry(testing.allocator, input, "codedb")) orelse + return error.TestUnexpectedResult; + defer testing.allocator.free(output); + + try testing.expect(std.mem.indexOf(u8, output, "codedb:") == null); + try testing.expect(std.mem.indexOf(u8, output, " other:") != null); + try testing.expect(std.mem.indexOf(u8, output, "model: gpt") != null); +} + test "nuke: removeCodexMcpServerBlock removes codedb block only" { const input = \\[mcp_servers.codedb] diff --git a/website/app/index.zig b/website/app/index.zig index bf6d0262..200c2ab8 100644 --- a/website/app/index.zig +++ b/website/app/index.zig @@ -349,7 +349,7 @@ const html = \\
\\
\\
Give your agent a brain
- \\
One command. Auto-registers in Claude Code, Codex, Gemini CLI, and Cursor.
+ \\
One command. Auto-registers in Claude Code, Codex, Gemini CLI, Cursor, and other detected MCP clients.
\\
\\ $ curl -fsSL https://codedb.codegraff.com/install.sh | bash \\
diff --git a/website/app/install_script.sh b/website/app/install_script.sh index a8b2d92a..2c494904 100644 --- a/website/app/install_script.sh +++ b/website/app/install_script.sh @@ -197,24 +197,249 @@ _register_json_mcp() { local config="$1" local codedb_bin="$2" local label="$3" - python3 - "$config" "$codedb_bin" << 'PYEOF' -import json, sys, os -config_path, codedb_bin = sys.argv[1], sys.argv[2] -try: + _register_mcp "$config" "$codedb_bin" "$label" "mcpServers" +} + +_client_present() { + local dir="$1" + shift + if [ -n "$dir" ] && [ -d "$dir" ]; then + return 0 + fi + local cmd + for cmd in "$@"; do + [ -n "$cmd" ] && command -v "$cmd" >/dev/null 2>&1 && return 0 + done + return 1 +} + +# flavor: mcpServers | mcp.servers | opencode | copilot | hermes +_register_mcp() { + local config="$1" + local codedb_bin="$2" + local label="$3" + local flavor="$4" + local rc=0 + python3 - "$config" "$codedb_bin" "$flavor" << 'PYEOF' || rc=$? +import json, os, re, sys + +config_path, codedb_bin, flavor = sys.argv[1], sys.argv[2], sys.argv[3] + +def load_json(path): + try: + with open(path) as f: + data = json.load(f) + except FileNotFoundError: + return {} + except json.JSONDecodeError: + sys.exit(4) + return data if isinstance(data, dict) else {} + +def save_json(path, data): + d = os.path.dirname(path) + if d: + os.makedirs(d, exist_ok=True) + with open(path, "w") as f: + json.dump(data, f, indent=2) + f.write("\n") + +stdio = {"command": codedb_bin, "args": ["mcp"]} + +if flavor == "mcpServers": + data = load_json(config_path) + data.setdefault("mcpServers", {})["codedb"] = stdio + save_json(config_path, data) +elif flavor == "mcp.servers": + data = load_json(config_path) + data.setdefault("mcp", {}).setdefault("servers", {})["codedb"] = stdio + save_json(config_path, data) +elif flavor == "opencode": + data = load_json(config_path) + mcp = data.setdefault("mcp", {}) + if not isinstance(mcp, dict): + mcp = {} + data["mcp"] = mcp + entry = {"type": "local", "command": [codedb_bin, "mcp"], "enabled": True} + if isinstance(mcp.get("servers"), dict): + mcp["servers"]["codedb"] = entry + else: + mcp["codedb"] = entry + save_json(config_path, data) +elif flavor == "copilot": + data = load_json(config_path) + data.setdefault("mcpServers", {})["codedb"] = { + "type": "local", + "command": codedb_bin, + "args": ["mcp"], + "tools": ["*"], + } + save_json(config_path, data) +elif flavor == "hermes": + quoted = json.dumps(codedb_bin) + entry = [" codedb:", f" command: {quoted}", ' args: ["mcp"]'] + d = os.path.dirname(config_path) + if d: + os.makedirs(d, exist_ok=True) + if not os.path.isfile(config_path) or os.path.getsize(config_path) == 0: + with open(config_path, "w") as f: + f.write("mcp_servers:\n" + "\n".join(entry) + "\n") + sys.exit(0) with open(config_path) as f: - data = json.load(f) -except (FileNotFoundError, json.JSONDecodeError): - data = {} -servers = data.setdefault("mcpServers", {}) -servers["codedb"] = {"command": codedb_bin, "args": ["mcp"]} -d = os.path.dirname(config_path) -if d: - os.makedirs(d, exist_ok=True) -with open(config_path, "w") as f: - json.dump(data, f, indent=2) - f.write("\n") + raw = f.read() + nl = "\r\n" if "\r\n" in raw else "\n" + lines = raw.splitlines(keepends=True) + + def body(line): + return line.split("#", 1)[0].rstrip("\r\n") + + def indent_of(line): + s = body(line) + if not s.strip(): + return None + return len(s) - len(s.lstrip(" ")) + + mcp_idx = None + for i, line in enumerate(lines): + if re.match(r"^mcp_servers:\s*$", body(line)): + mcp_idx = i + break + if mcp_idx is None: + for i, line in enumerate(lines): + if re.match(r"^mcp_servers:\s+\S", body(line)): + sys.exit(5) + if raw and not raw.endswith(("\n", "\r\n")): + raw += nl + extra = "" if raw.endswith(nl) else nl + with open(config_path, "w") as f: + f.write(raw + extra + "mcp_servers:" + nl + nl.join(entry) + nl) + sys.exit(0) + + codedb_start = None + codedb_end = None + i = mcp_idx + 1 + while i < len(lines): + ind = indent_of(lines[i]) + if ind is None: + i += 1 + continue + if ind == 0: + break + if ind == 2 and re.match(r"^\s{2}codedb:\s*", body(lines[i])): + codedb_start = i + j = i + 1 + while j < len(lines): + indj = indent_of(lines[j]) + if indj is None: + j += 1 + continue + if indj <= 2: + break + j += 1 + codedb_end = j + break + i += 1 + + block = [e + nl for e in entry] + if codedb_start is not None: + new_lines = lines[:codedb_start] + block + lines[codedb_end:] + else: + new_lines = lines[: mcp_idx + 1] + block + lines[mcp_idx + 1 :] + with open(config_path, "w") as f: + f.writelines(new_lines) +else: + sys.exit(2) PYEOF - printf " ${G}✓${N} %-12s ${D}→ %s${N}\n" "$label" "$config" + case "$rc" in + 0) printf " ${G}✓${N} %-12s ${D}→ %s${N}\n" "$label" "$config" ;; + 4) printf " ${Y}%-12s skip (invalid JSON) → %s${N}\n" "$label" "$config" ;; + 5) printf " ${Y}%-12s skip (inline YAML mcp_servers) → %s${N}\n" "$label" "$config" ;; + *) printf " ${Y}%-12s registration failed → %s${N}\n" "$label" "$config" ;; + esac + return 0 +} + +register_detected_clients() { + local codedb_bin="$1" + if ! command -v python3 >/dev/null 2>&1; then + printf " ${D}extra clients: skip (python3 not found)${N}\n" + return + fi + + if _client_present "$HOME/.omp" omp; then + _register_mcp "$HOME/.omp/agent/mcp.json" "$codedb_bin" "oh-my-pi" "mcpServers" + fi + if _client_present "$HOME/.hermes" hermes; then + _register_mcp "$HOME/.hermes/config.yaml" "$codedb_bin" "hermes" "hermes" + fi + if _client_present "$HOME/.qwen" qwen; then + _register_mcp "$HOME/.qwen/settings.json" "$codedb_bin" "qwen-code" "mcpServers" + fi + if _client_present "$HOME/.zcode" zcode; then + _register_mcp "$HOME/.zcode/cli/config.json" "$codedb_bin" "zcode" "mcp.servers" + fi + + if _client_present "$HOME/.trae" trae traecli; then + _register_mcp "$HOME/.trae/mcp.json" "$codedb_bin" "trae" "mcpServers" + fi + local trae_root trae_cfg + for trae_root in \ + "$HOME/Library/Application Support/Trae" \ + "$HOME/Library/Application Support/Trae CN" \ + "$HOME/AppData/Roaming/Trae" \ + "$HOME/AppData/Roaming/Trae CN" + do + [ -d "$trae_root" ] || continue + if [ -d "$trae_root/User/settings" ] || [ -f "$trae_root/User/settings/mcp.json" ]; then + trae_cfg="$trae_root/User/settings/mcp.json" + elif [ -d "$trae_root/User" ] || [ -f "$trae_root/User/mcp.json" ]; then + trae_cfg="$trae_root/User/mcp.json" + else + continue + fi + _register_mcp "$trae_cfg" "$codedb_bin" "trae" "mcpServers" + done + + if _client_present "$HOME/.cline" cline; then + _register_mcp "$HOME/.cline/data/settings/cline_mcp_settings.json" "$codedb_bin" "cline" "mcpServers" + fi + local cline_cfg cline_ext + for cline_cfg in \ + "$HOME/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/.config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/AppData/Roaming/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/AppData/Roaming/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" + do + cline_ext="$(dirname "$(dirname "$cline_cfg")")" + if [ -d "$cline_ext" ]; then + _register_mcp "$cline_cfg" "$codedb_bin" "cline" "mcpServers" + fi + done + + if _client_present "$HOME/.copilot" copilot; then + _register_mcp "$HOME/.copilot/mcp-config.json" "$codedb_bin" "copilot" "copilot" + fi + + if _client_present "$HOME/.gemini/antigravity" agy antigravity; then + _register_mcp "$HOME/.gemini/config/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" + if [ -d "$HOME/.gemini/antigravity" ]; then + _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" + fi + fi + + if _client_present "$HOME/.kiro" kiro kiro-cli; then + _register_mcp "$HOME/.kiro/settings/mcp.json" "$codedb_bin" "kiro" "mcpServers" + fi + + if _client_present "$HOME/.config/opencode" opencode; then + if [ -f "$HOME/.config/opencode/opencode.jsonc" ] && [ ! -f "$HOME/.config/opencode/opencode.json" ]; then + printf " ${Y}%-12s skip (opencode.jsonc is not rewritten) → %s${N}\n" \ + "opencode" "$HOME/.config/opencode/opencode.jsonc" + else + _register_mcp "$HOME/.config/opencode/opencode.json" "$codedb_bin" "opencode" "opencode" + fi + fi } DEEPWIKI_URL="https://mcp.deepwiki.com/mcp" @@ -230,8 +455,12 @@ config_path, entry_json = sys.argv[1], sys.argv[2] try: with open(config_path) as f: data = json.load(f) -except (FileNotFoundError, json.JSONDecodeError): +except FileNotFoundError: data = {} +except json.JSONDecodeError: + sys.exit(4) # never clobber a malformed file +if not isinstance(data, dict): + sys.exit(4) servers = data.setdefault("mcpServers", {}) if "deepwiki" in servers: sys.exit(3) # already configured — never clobber a user's own entry @@ -246,6 +475,7 @@ PYEOF case "$rc" in 0) printf " ${G}✓${N} %-12s ${D}→ %s (+deepwiki)${N}\n" "$label" "$config" ;; 3) printf " ${D}%-12s deepwiki already configured → %s${N}\n" "$label" "$config" ;; + 4) printf " ${Y}%-12s skip deepwiki (invalid JSON) → %s${N}\n" "$label" "$config" ;; *) printf " ${Y}%-12s deepwiki registration failed → %s${N}\n" "$label" "$config" ;; esac return 0 @@ -288,6 +518,35 @@ register_deepwiki() { _register_deepwiki_json "$HOME/.config/devin/config.json" "devin" \ "{\"serverUrl\":\"$DEEPWIKI_URL\"}" fi + # Detected extra clients that use a standard mcpServers JSON object. + if [ -d "$HOME/.omp" ] || command -v omp >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.omp/agent/mcp.json" "oh-my-pi" \ + "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.qwen" ] || command -v qwen >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.qwen/settings.json" "qwen-code" \ + "{\"httpUrl\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.trae" ] || command -v trae >/dev/null 2>&1 || command -v traecli >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.trae/mcp.json" "trae" \ + "{\"url\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.cline" ] || command -v cline >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.cline/data/settings/cline_mcp_settings.json" "cline" \ + "{\"type\":\"streamableHttp\",\"url\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.copilot" ] || command -v copilot >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.copilot/mcp-config.json" "copilot" \ + "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\",\"tools\":[\"*\"]}" + fi + if [ -d "$HOME/.gemini/antigravity" ] || command -v agy >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.gemini/config/mcp_config.json" "antigravity" \ + "{\"serverUrl\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.kiro" ] || command -v kiro >/dev/null 2>&1 || command -v kiro-cli >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.kiro/settings/mcp.json" "kiro" \ + "{\"url\":\"$DEEPWIKI_URL\"}" + fi # Codex: TOML, url key selects the streamable-HTTP transport. local codex_cfg="$HOME/.codex/config.toml" if grep -q '\[mcp_servers\.deepwiki\]' "$codex_cfg" 2>/dev/null; then @@ -605,6 +864,7 @@ main() { register_gemini "$dest" register_cursor "$dest" register_windsurf_devin "$dest" + register_detected_clients "$dest" register_deepwiki register_hooks print_hook_notes "$dest" diff --git a/website/dist/install.sh b/website/dist/install.sh index a8b2d92a..2c494904 100644 --- a/website/dist/install.sh +++ b/website/dist/install.sh @@ -197,24 +197,249 @@ _register_json_mcp() { local config="$1" local codedb_bin="$2" local label="$3" - python3 - "$config" "$codedb_bin" << 'PYEOF' -import json, sys, os -config_path, codedb_bin = sys.argv[1], sys.argv[2] -try: + _register_mcp "$config" "$codedb_bin" "$label" "mcpServers" +} + +_client_present() { + local dir="$1" + shift + if [ -n "$dir" ] && [ -d "$dir" ]; then + return 0 + fi + local cmd + for cmd in "$@"; do + [ -n "$cmd" ] && command -v "$cmd" >/dev/null 2>&1 && return 0 + done + return 1 +} + +# flavor: mcpServers | mcp.servers | opencode | copilot | hermes +_register_mcp() { + local config="$1" + local codedb_bin="$2" + local label="$3" + local flavor="$4" + local rc=0 + python3 - "$config" "$codedb_bin" "$flavor" << 'PYEOF' || rc=$? +import json, os, re, sys + +config_path, codedb_bin, flavor = sys.argv[1], sys.argv[2], sys.argv[3] + +def load_json(path): + try: + with open(path) as f: + data = json.load(f) + except FileNotFoundError: + return {} + except json.JSONDecodeError: + sys.exit(4) + return data if isinstance(data, dict) else {} + +def save_json(path, data): + d = os.path.dirname(path) + if d: + os.makedirs(d, exist_ok=True) + with open(path, "w") as f: + json.dump(data, f, indent=2) + f.write("\n") + +stdio = {"command": codedb_bin, "args": ["mcp"]} + +if flavor == "mcpServers": + data = load_json(config_path) + data.setdefault("mcpServers", {})["codedb"] = stdio + save_json(config_path, data) +elif flavor == "mcp.servers": + data = load_json(config_path) + data.setdefault("mcp", {}).setdefault("servers", {})["codedb"] = stdio + save_json(config_path, data) +elif flavor == "opencode": + data = load_json(config_path) + mcp = data.setdefault("mcp", {}) + if not isinstance(mcp, dict): + mcp = {} + data["mcp"] = mcp + entry = {"type": "local", "command": [codedb_bin, "mcp"], "enabled": True} + if isinstance(mcp.get("servers"), dict): + mcp["servers"]["codedb"] = entry + else: + mcp["codedb"] = entry + save_json(config_path, data) +elif flavor == "copilot": + data = load_json(config_path) + data.setdefault("mcpServers", {})["codedb"] = { + "type": "local", + "command": codedb_bin, + "args": ["mcp"], + "tools": ["*"], + } + save_json(config_path, data) +elif flavor == "hermes": + quoted = json.dumps(codedb_bin) + entry = [" codedb:", f" command: {quoted}", ' args: ["mcp"]'] + d = os.path.dirname(config_path) + if d: + os.makedirs(d, exist_ok=True) + if not os.path.isfile(config_path) or os.path.getsize(config_path) == 0: + with open(config_path, "w") as f: + f.write("mcp_servers:\n" + "\n".join(entry) + "\n") + sys.exit(0) with open(config_path) as f: - data = json.load(f) -except (FileNotFoundError, json.JSONDecodeError): - data = {} -servers = data.setdefault("mcpServers", {}) -servers["codedb"] = {"command": codedb_bin, "args": ["mcp"]} -d = os.path.dirname(config_path) -if d: - os.makedirs(d, exist_ok=True) -with open(config_path, "w") as f: - json.dump(data, f, indent=2) - f.write("\n") + raw = f.read() + nl = "\r\n" if "\r\n" in raw else "\n" + lines = raw.splitlines(keepends=True) + + def body(line): + return line.split("#", 1)[0].rstrip("\r\n") + + def indent_of(line): + s = body(line) + if not s.strip(): + return None + return len(s) - len(s.lstrip(" ")) + + mcp_idx = None + for i, line in enumerate(lines): + if re.match(r"^mcp_servers:\s*$", body(line)): + mcp_idx = i + break + if mcp_idx is None: + for i, line in enumerate(lines): + if re.match(r"^mcp_servers:\s+\S", body(line)): + sys.exit(5) + if raw and not raw.endswith(("\n", "\r\n")): + raw += nl + extra = "" if raw.endswith(nl) else nl + with open(config_path, "w") as f: + f.write(raw + extra + "mcp_servers:" + nl + nl.join(entry) + nl) + sys.exit(0) + + codedb_start = None + codedb_end = None + i = mcp_idx + 1 + while i < len(lines): + ind = indent_of(lines[i]) + if ind is None: + i += 1 + continue + if ind == 0: + break + if ind == 2 and re.match(r"^\s{2}codedb:\s*", body(lines[i])): + codedb_start = i + j = i + 1 + while j < len(lines): + indj = indent_of(lines[j]) + if indj is None: + j += 1 + continue + if indj <= 2: + break + j += 1 + codedb_end = j + break + i += 1 + + block = [e + nl for e in entry] + if codedb_start is not None: + new_lines = lines[:codedb_start] + block + lines[codedb_end:] + else: + new_lines = lines[: mcp_idx + 1] + block + lines[mcp_idx + 1 :] + with open(config_path, "w") as f: + f.writelines(new_lines) +else: + sys.exit(2) PYEOF - printf " ${G}✓${N} %-12s ${D}→ %s${N}\n" "$label" "$config" + case "$rc" in + 0) printf " ${G}✓${N} %-12s ${D}→ %s${N}\n" "$label" "$config" ;; + 4) printf " ${Y}%-12s skip (invalid JSON) → %s${N}\n" "$label" "$config" ;; + 5) printf " ${Y}%-12s skip (inline YAML mcp_servers) → %s${N}\n" "$label" "$config" ;; + *) printf " ${Y}%-12s registration failed → %s${N}\n" "$label" "$config" ;; + esac + return 0 +} + +register_detected_clients() { + local codedb_bin="$1" + if ! command -v python3 >/dev/null 2>&1; then + printf " ${D}extra clients: skip (python3 not found)${N}\n" + return + fi + + if _client_present "$HOME/.omp" omp; then + _register_mcp "$HOME/.omp/agent/mcp.json" "$codedb_bin" "oh-my-pi" "mcpServers" + fi + if _client_present "$HOME/.hermes" hermes; then + _register_mcp "$HOME/.hermes/config.yaml" "$codedb_bin" "hermes" "hermes" + fi + if _client_present "$HOME/.qwen" qwen; then + _register_mcp "$HOME/.qwen/settings.json" "$codedb_bin" "qwen-code" "mcpServers" + fi + if _client_present "$HOME/.zcode" zcode; then + _register_mcp "$HOME/.zcode/cli/config.json" "$codedb_bin" "zcode" "mcp.servers" + fi + + if _client_present "$HOME/.trae" trae traecli; then + _register_mcp "$HOME/.trae/mcp.json" "$codedb_bin" "trae" "mcpServers" + fi + local trae_root trae_cfg + for trae_root in \ + "$HOME/Library/Application Support/Trae" \ + "$HOME/Library/Application Support/Trae CN" \ + "$HOME/AppData/Roaming/Trae" \ + "$HOME/AppData/Roaming/Trae CN" + do + [ -d "$trae_root" ] || continue + if [ -d "$trae_root/User/settings" ] || [ -f "$trae_root/User/settings/mcp.json" ]; then + trae_cfg="$trae_root/User/settings/mcp.json" + elif [ -d "$trae_root/User" ] || [ -f "$trae_root/User/mcp.json" ]; then + trae_cfg="$trae_root/User/mcp.json" + else + continue + fi + _register_mcp "$trae_cfg" "$codedb_bin" "trae" "mcpServers" + done + + if _client_present "$HOME/.cline" cline; then + _register_mcp "$HOME/.cline/data/settings/cline_mcp_settings.json" "$codedb_bin" "cline" "mcpServers" + fi + local cline_cfg cline_ext + for cline_cfg in \ + "$HOME/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/.config/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/AppData/Roaming/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" \ + "$HOME/AppData/Roaming/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" + do + cline_ext="$(dirname "$(dirname "$cline_cfg")")" + if [ -d "$cline_ext" ]; then + _register_mcp "$cline_cfg" "$codedb_bin" "cline" "mcpServers" + fi + done + + if _client_present "$HOME/.copilot" copilot; then + _register_mcp "$HOME/.copilot/mcp-config.json" "$codedb_bin" "copilot" "copilot" + fi + + if _client_present "$HOME/.gemini/antigravity" agy antigravity; then + _register_mcp "$HOME/.gemini/config/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" + if [ -d "$HOME/.gemini/antigravity" ]; then + _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" + fi + fi + + if _client_present "$HOME/.kiro" kiro kiro-cli; then + _register_mcp "$HOME/.kiro/settings/mcp.json" "$codedb_bin" "kiro" "mcpServers" + fi + + if _client_present "$HOME/.config/opencode" opencode; then + if [ -f "$HOME/.config/opencode/opencode.jsonc" ] && [ ! -f "$HOME/.config/opencode/opencode.json" ]; then + printf " ${Y}%-12s skip (opencode.jsonc is not rewritten) → %s${N}\n" \ + "opencode" "$HOME/.config/opencode/opencode.jsonc" + else + _register_mcp "$HOME/.config/opencode/opencode.json" "$codedb_bin" "opencode" "opencode" + fi + fi } DEEPWIKI_URL="https://mcp.deepwiki.com/mcp" @@ -230,8 +455,12 @@ config_path, entry_json = sys.argv[1], sys.argv[2] try: with open(config_path) as f: data = json.load(f) -except (FileNotFoundError, json.JSONDecodeError): +except FileNotFoundError: data = {} +except json.JSONDecodeError: + sys.exit(4) # never clobber a malformed file +if not isinstance(data, dict): + sys.exit(4) servers = data.setdefault("mcpServers", {}) if "deepwiki" in servers: sys.exit(3) # already configured — never clobber a user's own entry @@ -246,6 +475,7 @@ PYEOF case "$rc" in 0) printf " ${G}✓${N} %-12s ${D}→ %s (+deepwiki)${N}\n" "$label" "$config" ;; 3) printf " ${D}%-12s deepwiki already configured → %s${N}\n" "$label" "$config" ;; + 4) printf " ${Y}%-12s skip deepwiki (invalid JSON) → %s${N}\n" "$label" "$config" ;; *) printf " ${Y}%-12s deepwiki registration failed → %s${N}\n" "$label" "$config" ;; esac return 0 @@ -288,6 +518,35 @@ register_deepwiki() { _register_deepwiki_json "$HOME/.config/devin/config.json" "devin" \ "{\"serverUrl\":\"$DEEPWIKI_URL\"}" fi + # Detected extra clients that use a standard mcpServers JSON object. + if [ -d "$HOME/.omp" ] || command -v omp >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.omp/agent/mcp.json" "oh-my-pi" \ + "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.qwen" ] || command -v qwen >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.qwen/settings.json" "qwen-code" \ + "{\"httpUrl\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.trae" ] || command -v trae >/dev/null 2>&1 || command -v traecli >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.trae/mcp.json" "trae" \ + "{\"url\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.cline" ] || command -v cline >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.cline/data/settings/cline_mcp_settings.json" "cline" \ + "{\"type\":\"streamableHttp\",\"url\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.copilot" ] || command -v copilot >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.copilot/mcp-config.json" "copilot" \ + "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\",\"tools\":[\"*\"]}" + fi + if [ -d "$HOME/.gemini/antigravity" ] || command -v agy >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.gemini/config/mcp_config.json" "antigravity" \ + "{\"serverUrl\":\"$DEEPWIKI_URL\"}" + fi + if [ -d "$HOME/.kiro" ] || command -v kiro >/dev/null 2>&1 || command -v kiro-cli >/dev/null 2>&1; then + _register_deepwiki_json "$HOME/.kiro/settings/mcp.json" "kiro" \ + "{\"url\":\"$DEEPWIKI_URL\"}" + fi # Codex: TOML, url key selects the streamable-HTTP transport. local codex_cfg="$HOME/.codex/config.toml" if grep -q '\[mcp_servers\.deepwiki\]' "$codex_cfg" 2>/dev/null; then @@ -605,6 +864,7 @@ main() { register_gemini "$dest" register_cursor "$dest" register_windsurf_devin "$dest" + register_detected_clients "$dest" register_deepwiki register_hooks print_hook_notes "$dest" From 565238036b7a2e29dc13972a45f5d59910d3fbfd Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Sat, 19 Sep 2026 23:06:29 +0800 Subject: [PATCH 2/3] fix: antigravity writes its own config; nuke Trae User/mcp.json Do not write Antigravity into ~/.gemini/config/mcp_config.json (Gemini CLI already uses settings.json). Register and Deepwiki only under ~/.gemini/antigravity/mcp_config.json. Keep the stray Gemini path in nuke so older installs still deregister. Nuke the non-CN Trae User/mcp.json path the installer can create. Co-Authored-By: Codegraff --- install/install.sh | 11 +++++------ src/nuke.zig | 2 ++ website/app/install_script.sh | 11 +++++------ website/dist/install.sh | 11 +++++------ 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/install/install.sh b/install/install.sh index 2c494904..71853166 100644 --- a/install/install.sh +++ b/install/install.sh @@ -422,10 +422,9 @@ register_detected_clients() { fi if _client_present "$HOME/.gemini/antigravity" agy antigravity; then - _register_mcp "$HOME/.gemini/config/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" - if [ -d "$HOME/.gemini/antigravity" ]; then - _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" - fi + # Antigravity's own config — not Gemini CLI's ~/.gemini/settings.json + # and not ~/.gemini/config/mcp_config.json. + _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" fi if _client_present "$HOME/.kiro" kiro kiro-cli; then @@ -539,8 +538,8 @@ register_deepwiki() { _register_deepwiki_json "$HOME/.copilot/mcp-config.json" "copilot" \ "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\",\"tools\":[\"*\"]}" fi - if [ -d "$HOME/.gemini/antigravity" ] || command -v agy >/dev/null 2>&1; then - _register_deepwiki_json "$HOME/.gemini/config/mcp_config.json" "antigravity" \ + if _client_present "$HOME/.gemini/antigravity" agy antigravity; then + _register_deepwiki_json "$HOME/.gemini/antigravity/mcp_config.json" "antigravity" \ "{\"serverUrl\":\"$DEEPWIKI_URL\"}" fi if [ -d "$HOME/.kiro" ] || command -v kiro >/dev/null 2>&1 || command -v kiro-cli >/dev/null 2>&1; then diff --git a/src/nuke.zig b/src/nuke.zig index b72ac1bf..b93d3434 100644 --- a/src/nuke.zig +++ b/src/nuke.zig @@ -313,11 +313,13 @@ fn deregisterInstalledIntegrations(io: std.Io, allocator: std.mem.Allocator, hom ".config/opencode/opencode.json", ".config/opencode/mcp.json", "Library/Application Support/Trae/User/settings/mcp.json", + "Library/Application Support/Trae/User/mcp.json", "Library/Application Support/Trae CN/User/settings/mcp.json", "Library/Application Support/Trae CN/User/mcp.json", "Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", "Library/Application Support/Cursor/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", "AppData/Roaming/Trae/User/settings/mcp.json", + "AppData/Roaming/Trae/User/mcp.json", "AppData/Roaming/Trae CN/User/settings/mcp.json", "AppData/Roaming/Trae CN/User/mcp.json", "AppData/Roaming/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", diff --git a/website/app/install_script.sh b/website/app/install_script.sh index 2c494904..71853166 100644 --- a/website/app/install_script.sh +++ b/website/app/install_script.sh @@ -422,10 +422,9 @@ register_detected_clients() { fi if _client_present "$HOME/.gemini/antigravity" agy antigravity; then - _register_mcp "$HOME/.gemini/config/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" - if [ -d "$HOME/.gemini/antigravity" ]; then - _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" - fi + # Antigravity's own config — not Gemini CLI's ~/.gemini/settings.json + # and not ~/.gemini/config/mcp_config.json. + _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" fi if _client_present "$HOME/.kiro" kiro kiro-cli; then @@ -539,8 +538,8 @@ register_deepwiki() { _register_deepwiki_json "$HOME/.copilot/mcp-config.json" "copilot" \ "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\",\"tools\":[\"*\"]}" fi - if [ -d "$HOME/.gemini/antigravity" ] || command -v agy >/dev/null 2>&1; then - _register_deepwiki_json "$HOME/.gemini/config/mcp_config.json" "antigravity" \ + if _client_present "$HOME/.gemini/antigravity" agy antigravity; then + _register_deepwiki_json "$HOME/.gemini/antigravity/mcp_config.json" "antigravity" \ "{\"serverUrl\":\"$DEEPWIKI_URL\"}" fi if [ -d "$HOME/.kiro" ] || command -v kiro >/dev/null 2>&1 || command -v kiro-cli >/dev/null 2>&1; then diff --git a/website/dist/install.sh b/website/dist/install.sh index 2c494904..71853166 100644 --- a/website/dist/install.sh +++ b/website/dist/install.sh @@ -422,10 +422,9 @@ register_detected_clients() { fi if _client_present "$HOME/.gemini/antigravity" agy antigravity; then - _register_mcp "$HOME/.gemini/config/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" - if [ -d "$HOME/.gemini/antigravity" ]; then - _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" - fi + # Antigravity's own config — not Gemini CLI's ~/.gemini/settings.json + # and not ~/.gemini/config/mcp_config.json. + _register_mcp "$HOME/.gemini/antigravity/mcp_config.json" "$codedb_bin" "antigravity" "mcpServers" fi if _client_present "$HOME/.kiro" kiro kiro-cli; then @@ -539,8 +538,8 @@ register_deepwiki() { _register_deepwiki_json "$HOME/.copilot/mcp-config.json" "copilot" \ "{\"type\":\"http\",\"url\":\"$DEEPWIKI_URL\",\"tools\":[\"*\"]}" fi - if [ -d "$HOME/.gemini/antigravity" ] || command -v agy >/dev/null 2>&1; then - _register_deepwiki_json "$HOME/.gemini/config/mcp_config.json" "antigravity" \ + if _client_present "$HOME/.gemini/antigravity" agy antigravity; then + _register_deepwiki_json "$HOME/.gemini/antigravity/mcp_config.json" "antigravity" \ "{\"serverUrl\":\"$DEEPWIKI_URL\"}" fi if [ -d "$HOME/.kiro" ] || command -v kiro >/dev/null 2>&1 || command -v kiro-cli >/dev/null 2>&1; then From 18c39b7bf7943711abae4e1e4c125bf59d88dbd7 Mon Sep 17 00:00:00 2001 From: justrach <54503978+justrach@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:05:53 +0800 Subject: [PATCH 3/3] release: 0.2.5857 register more MCP clients 0.2.5856 shipped before the installer learned oh-my-pi, Hermes, Qwen Code, ZCode, Trae, Cline, Copilot CLI, Antigravity, Kiro, and OpenCode. Those clients still needed a hand-edited MCP config. This release stamps that registration, and the Antigravity/Trae path correction, as 0.2.5857. Antigravity writes only ~/.gemini/antigravity/mcp_config.json so it does not collide with Gemini CLI. Nuke still clears the stray Gemini path and the non-CN Trae User/mcp.json the installer can create. Malformed JSON is left untouched. Retrieval and the watcher are unchanged. Co-Authored-By: Codegraff --- CHANGELOG.md | 3 +++ build.zig.zon | 2 +- docs/releases/0.2.5857.md | 20 ++++++++++++++++++++ npm/package.json | 2 +- src/release_info.zig | 2 +- 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 docs/releases/0.2.5857.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ffabaf..9b5c93f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## Unreleased +## 0.2.5857 - 2026-09-26 + - Auto-register codedb in additional detected MCP clients: oh-my-pi, Hermes, Qwen Code, ZCode, Trae, Cline, Copilot CLI, Antigravity, Kiro, and OpenCode. `codedb nuke` removes the matching entries. Invalid JSON is left untouched. +- Antigravity registers only under `~/.gemini/antigravity/mcp_config.json`, not Gemini CLI's settings file. `codedb nuke` also removes the non-CN Trae `User/mcp.json` path. ## 0.2.5856 - 2026-09-19 diff --git a/build.zig.zon b/build.zig.zon index fc650664..65cf2f0c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .codedb2, .fingerprint = 0x6e18d96ca2a31757, - .version = "0.2.5856", + .version = "0.2.5857", .minimum_zig_version = "0.17.0-dev.813+2153f8143", .dependencies = .{ .nanoregex = .{ diff --git a/docs/releases/0.2.5857.md b/docs/releases/0.2.5857.md new file mode 100644 index 00000000..8bc2afeb --- /dev/null +++ b/docs/releases/0.2.5857.md @@ -0,0 +1,20 @@ +# CodeDB 0.2.5857 + +The installer now registers codedb in the MCP clients it can already detect, instead of leaving ten of them for a hand edit. + +## More MCP clients + +0.2.5856 and earlier auto-registered Claude Code, Codex, Gemini CLI, Cursor, Windsurf, and Devin. Agents on oh-my-pi, Hermes, Qwen Code, ZCode, Trae, Cline, Copilot CLI, Antigravity, Kiro, and OpenCode still had to paste a config snippet. + +The installer writes each client's documented file and schema when that client's config directory or CLI is already present. Malformed JSON is left untouched rather than replaced. `codedb nuke` removes the same entries. + +Antigravity does not share Gemini CLI's settings file. Registration and Deepwiki go only to `~/.gemini/antigravity/mcp_config.json`. The stray `~/.gemini/config/mcp_config.json` path stays in nuke so an older install still deregisters. Nuke also removes the non-CN Trae `User/mcp.json` path the installer can create. + +Retrieval, the watcher, and hosted embeddings are unchanged. + +## Verification + +```sh +zig test src/test_mcp.zig --test-filter 'nuke:' +zig test src/test_mcp.zig --test-filter 'installers:' +``` diff --git a/npm/package.json b/npm/package.json index 188de345..4ff4e1f9 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "codedeebee", - "version": "0.2.5856", + "version": "0.2.5857", "description": "Zig code intelligence MCP server — npx launcher for the codedb native binary", "license": "MIT", "author": "justrach", diff --git a/src/release_info.zig b/src/release_info.zig index 861eaec6..c356cb07 100644 --- a/src/release_info.zig +++ b/src/release_info.zig @@ -1 +1 @@ -pub const semver = "0.2.5856"; +pub const semver = "0.2.5857";