diff --git a/gitnexus-codebuddy-integration/README.md b/gitnexus-codebuddy-integration/README.md new file mode 100644 index 000000000..690b1052b --- /dev/null +++ b/gitnexus-codebuddy-integration/README.md @@ -0,0 +1,104 @@ +# GitNexus — CodeBuddy integration + +Static config that adds GitNexus knowledge-graph augmentation and skill files to CodeBuddy. + +## What you get + +| Layer | What it does | How it's installed | +| ---------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| **MCP** | `gitnexus` MCP server with 16 tools (`query`, `context`, `impact`, `detect_changes`, `rename`, …) | `npx gitnexus setup` writes `~/.codebuddy/.mcp.json` automatically. | +| **Skills** | `/gitnexus-exploring`, `/gitnexus-debugging`, `/gitnexus-impact-analysis`, `/gitnexus-refactoring`, `/gitnexus-cli`, `/gitnexus-pr-review`, `/gitnexus-guide` markdown skills | `npx gitnexus setup` copies them to `~/.codebuddy/skills/`. | +| **Hooks** | `PreToolUse` / `PostToolUse` hooks that enrich `Grep` / `Glob` / `Bash` tool calls with graph context, and detect stale index after git mutations | `npx gitnexus setup` writes `~/.codebuddy/settings.json` and copies hook scripts. | + +### What's installed by `gitnexus setup` + +| Step | Automated? | +| --------------------------------- | ---------- | +| `~/.codebuddy/.mcp.json` | ✅ | +| `~/.codebuddy/skills/gitnexus-*` | ✅ | +| `~/.codebuddy/settings.json` (hooks) | ✅ | +| `~/.codebuddy/hooks/gitnexus/` | ✅ | + +Both MCP config, skills, and hooks are global — they apply to all projects opened in CodeBuddy. + +## Hook contract + +CodeBuddy hooks are **fully compatible with the Claude Code hooks spec**. The hook receives a JSON event on stdin: + +```json +{ + "hook_event_name": "PreToolUse" | "PostToolUse", + "tool_name": "Grep" | "Glob" | "Bash", + "tool_input": { /* tool-specific */ }, + "cwd": "/absolute/path/to/project" +} +``` + +It writes augmentation context to stdout as: + +```json +{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "additionalContext": "[GitNexus] …" } } +``` + +Empty stdout means "no augmentation, continue normally" — the hook never blocks the tool. + +| Event | Matcher | Behavior | +| -------------- | ------------------- | ------------------------------------------------------------------------- | +| `PreToolUse` | `Grep\|Glob\|Bash` | Extracts search pattern from tool input, runs `gitnexus augment`, injects graph context before tool executes | +| `PostToolUse` | `Bash` | Detects `git commit/merge/rebase` → checks index staleness → notifies agent to reindex | + +## Verify + +1. Index the project: `npx gitnexus analyze` +2. Restart CodeBuddy or reload the window so it picks up the new MCP config. +3. In CodeBuddy, check the MCP panel — GitNexus should appear as a connected server. +4. Try a query: ask CodeBuddy "Show me the auth flow" — GitNexus tools will be available. + +## MCP config format + +CodeBuddy uses `~/.codebuddy/.mcp.json` (JSONC format): + +```jsonc +{ + "mcpServers": { + "gitnexus": { + "type": "stdio", + "command": "gitnexus", + "args": ["mcp"] + } + } +} +``` + +## Skills format + +Skills follow the Agent Skills standard installed at `~/.codebuddy/skills/gitnexus-*/SKILL.md`. Each skill includes YAML frontmatter with `name` and `description` fields that CodeBuddy uses to decide when to invoke the skill. + +## Configuration priority + +CodeBuddy uses two levels of configuration for hooks — project-level overrides user-level: + +| Level | Path | Priority | +| ------- | ----------------------------------------- | -------- | +| Project | `/.codebuddy/settings.json` | High | +| User | `~/.codebuddy/settings.json` | Low | + +`gitnexus setup` writes hooks to **user-level** (`~/.codebuddy/settings.json`), so they apply across all projects. To override or add project-specific hooks, create `/.codebuddy/settings.json` in your project. + +## Project-level setup (optional) + +For project-specific configuration, place these files in your project: + +``` +/ +└── .codebuddy/ + └── skills/ ← Project-specific skills +``` + +MCP servers configured at the project level (`.mcp.json` in project root) take precedence over user-level configuration. + +## Troubleshooting + +- **GitNexus not visible in MCP panel** — Check `~/.codebuddy/.mcp.json` exists and is valid JSONC. Run `npx gitnexus setup` to regenerate. +- **`gitnexus` command not found** — Install globally with `npm i -g gitnexus` so the `command: "gitnexus"` entry resolves from PATH. +- **No tools available** — Run `npx gitnexus analyze` in your project first, then reload CodeBuddy. diff --git a/gitnexus/src/cli/setup.ts b/gitnexus/src/cli/setup.ts index 8f52e0f2d..e4a3f9ed9 100644 --- a/gitnexus/src/cli/setup.ts +++ b/gitnexus/src/cli/setup.ts @@ -662,6 +662,167 @@ async function installCodexSkills(result: SetupResult): Promise { } } +// ─── CodeBuddy ────────────────────────────────────────────────────── + +async function setupCodeBuddy(result: SetupResult): Promise { + const codebuddyDir = path.join(os.homedir(), '.codebuddy'); + if (!(await dirExists(codebuddyDir))) { + result.skipped.push('CodeBuddy (not installed)'); + return; + } + + // CodeBuddy uses ~/.codebuddy/.mcp.json (JSONC format, supports comments) + const mcpPath = path.join(codebuddyDir, '.mcp.json'); + try { + const entry = getMcpEntry(); + const ok = await mergeJsoncFile(mcpPath, ['mcpServers', 'gitnexus'], { + type: 'stdio', + ...entry, + }); + if (ok) { + result.configured.push('CodeBuddy'); + } else { + result.errors.push( + 'CodeBuddy: .mcp.json is corrupt — skipping to preserve existing content', + ); + } + } catch (err: any) { + result.errors.push(`CodeBuddy: ${err.message}`); + } +} + +/** + * Install global CodeBuddy skills to ~/.codebuddy/skills/ + */ +async function installCodeBuddySkills(result: SetupResult): Promise { + const codebuddyDir = path.join(os.homedir(), '.codebuddy'); + if (!(await dirExists(codebuddyDir))) return; + + const skillsDir = path.join(codebuddyDir, 'skills'); + try { + const installed = await installSkillsTo(skillsDir); + if (installed.length > 0) { + result.configured.push( + `CodeBuddy skills (${installed.length} skills → ~/.codebuddy/skills/)`, + ); + } + } catch (err: any) { + result.errors.push(`CodeBuddy skills: ${err.message}`); + } +} + +/** + * Install GitNexus hooks to ~/.codebuddy/settings.json for CodeBuddy. + * CodeBuddy hooks are fully compatible with the Claude Code hooks spec, + * so we reuse the same hook scripts and event structure. + */ +async function installCodeBuddyHooks(result: SetupResult): Promise { + const codebuddyDir = path.join(os.homedir(), '.codebuddy'); + if (!(await dirExists(codebuddyDir))) return; + + const settingsPath = path.join(codebuddyDir, 'settings.json'); + + // Source hooks bundled within the gitnexus package (hooks/claude/) + const pluginHooksPath = path.join(__dirname, '..', '..', 'hooks', 'claude'); + + // Copy unified hook script to ~/.codebuddy/hooks/gitnexus/ + const destHooksDir = path.join(codebuddyDir, 'hooks', 'gitnexus'); + + try { + await fs.mkdir(destHooksDir, { recursive: true }); + + const src = path.join(pluginHooksPath, 'gitnexus-hook.cjs'); + const dest = path.join(destHooksDir, 'gitnexus-hook.cjs'); + try { + let content = await fs.readFile(src, 'utf-8'); + const resolvedCli = path.join(__dirname, '..', 'cli', 'index.js'); + const normalizedCli = path.resolve(resolvedCli).replace(/\\/g, '/'); + const jsonCli = JSON.stringify(normalizedCli); + content = content.replace( + "let cliPath = path.resolve(__dirname, '..', '..', 'dist', 'cli', 'index.js');", + `let cliPath = ${jsonCli};`, + ); + await fs.writeFile(dest, content, 'utf-8'); + } catch { + // Script not found in source — skip + } + + try { + await fs.copyFile( + path.join(pluginHooksPath, 'hook-lock.cjs'), + path.join(destHooksDir, 'hook-lock.cjs'), + ); + } catch { + // Helper not found in source — skip + } + + const hookPath = path.join(destHooksDir, 'gitnexus-hook.cjs').replace(/\\/g, '/'); + const escapedHookPath = hookPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + const hookCmd = `node "${escapedHookPath}"`; + + // Check which hook events need entries (idempotent: skip if already registered) + const parsed = await (async () => { + try { + const r = await fs.readFile(settingsPath, 'utf-8'); + return parseJsonc(r); + } catch { + return null; + } + })(); + + const hookEntries: Array<{ eventName: string; value: unknown }> = []; + + if (!hasGitnexusHook(parsed?.hooks, 'PreToolUse')) { + hookEntries.push({ + eventName: 'PreToolUse', + value: { + matcher: 'Grep|Glob|Bash', + hooks: [ + { + type: 'command', + command: hookCmd, + timeout: 10, + statusMessage: 'Enriching with GitNexus graph context...', + }, + ], + }, + }); + } + if (!hasGitnexusHook(parsed?.hooks, 'PostToolUse')) { + hookEntries.push({ + eventName: 'PostToolUse', + value: { + matcher: 'Bash', + hooks: [ + { + type: 'command', + command: hookCmd, + timeout: 10, + statusMessage: 'Checking GitNexus index freshness...', + }, + ], + }, + }); + } + + if (hookEntries.length === 0) { + result.configured.push('CodeBuddy hooks (already configured)'); + return; + } + + const ok = await mergeHooksJsonc(settingsPath, hookEntries); + if (ok) { + result.configured.push('CodeBuddy hooks (PreToolUse, PostToolUse)'); + } else { + result.errors.push( + 'CodeBuddy hooks: settings.json is corrupt — skipping to preserve existing content', + ); + } + } catch (err: any) { + result.errors.push(`CodeBuddy hooks: ${err.message}`); + } +} + // ─── Main command ────────────────────────────────────────────────── export const setupCommand = async () => { @@ -685,6 +846,7 @@ export const setupCommand = async () => { await setupClaudeCode(result); await setupOpenCode(result); await setupCodex(result); + await setupCodeBuddy(result); // Install global skills for platforms that support them await installClaudeCodeSkills(result); @@ -692,6 +854,8 @@ export const setupCommand = async () => { await installCursorSkills(result); await installOpenCodeSkills(result); await installCodexSkills(result); + await installCodeBuddySkills(result); + await installCodeBuddyHooks(result); // Print results if (result.configured.length > 0) {