feat(setup): add CodeBuddy IDE integration

Add MCP config, skills, and hooks support for CodeBuddy IDE in the
`gitnexus setup` command.

- MCP: writes `~/.codebuddy/.mcp.json` with stdio transport
- Skills: installs 7 GitNexus skills to `~/.codebuddy/skills/`
- Hooks: installs PreToolUse/PostToolUse hooks to
  `~/.codebuddy/settings.json` (fully compatible with Claude Code
  hooks spec, reuses same hook scripts)
- Adds `gitnexus-codebuddy-integration/README.md` with integration docs

Detection: checks for `~/.codebuddy/` directory existence.
This commit is contained in:
liubaolin 2026-05-14 16:42:37 +08:00
parent e01f0912bc
commit 18bb6137f5
2 changed files with 268 additions and 0 deletions

View file

@ -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 | `<workspace>/.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 `<workspace>/.codebuddy/settings.json` in your project.
## Project-level setup (optional)
For project-specific configuration, place these files in your project:
```
<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.

View file

@ -662,6 +662,167 @@ async function installCodexSkills(result: SetupResult): Promise<void> {
}
}
// ─── CodeBuddy ──────────────────────────────────────────────────────
async function setupCodeBuddy(result: SetupResult): Promise<void> {
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<void> {
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<void> {
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) {