Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions site/src/content/docs/reference/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The interactive installer auto-detects and configures each supported agent — w
## Supported agents

- **Claude Code**
- **CodeBuddy**
- **Cursor**
- **Codex CLI**
- **opencode**
Expand Down
199 changes: 199 additions & 0 deletions src/installer/targets/codebuddy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* CodeBuddy target. Writes:
*
* - MCP server entry to `~/.codebuddy.json` (global = user scope, loads
* in every project) or `./.mcp.json` (local = project scope).
* - Permissions to `~/.codebuddy/settings.json` (global) or
* `./.codebuddy/settings.json` (local), gated on `autoAllow`.
* - Instructions to `~/.codebuddy/CODEBUDDY.md` (global) or
* `./.codebuddy/CODEBUDDY.md` (local).
*
* CodeBuddy follows the same config layout as Claude Code, with
* `.codebuddy` replacing `.claude` and `CODEBUDDY.md` replacing
* `CLAUDE.md`.
*/

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import {
AgentTarget,
DetectionResult,
InstallOptions,
Location,
WriteResult,
} from './types';
import {
getCodeGraphPermissions,
getMcpServerConfig,
jsonDeepEqual,
readJsonFile,
removeMarkedSection,
writeJsonFile,
} from './shared';
import {
CODEGRAPH_SECTION_END,
CODEGRAPH_SECTION_START,
} from '../instructions-template';

function configDir(loc: Location): string {
return loc === 'global'
? path.join(os.homedir(), '.codebuddy')
: path.join(process.cwd(), '.codebuddy');
}
function mcpJsonPath(loc: Location): string {
// global → ~/.codebuddy.json (user scope: visible in every project).
// local → ./.mcp.json (project scope).
return loc === 'global'
? path.join(os.homedir(), '.codebuddy.json')
: path.join(process.cwd(), '.mcp.json');
}
function settingsJsonPath(loc: Location): string {
return path.join(configDir(loc), 'settings.json');
}
function instructionsPath(loc: Location): string {
return path.join(configDir(loc), 'CODEBUDDY.md');
}

class CodeBuddyTarget implements AgentTarget {
readonly id = 'codebuddy' as const;
readonly displayName = 'CodeBuddy';
readonly docsUrl = 'https://cnb.cool/codebuddy/codebuddy-code';

supportsLocation(_loc: Location): boolean {
return true;
}

detect(loc: Location): DetectionResult {
const mcpPath = mcpJsonPath(loc);
const config = readJsonFile(mcpPath);
const alreadyConfigured = !!config.mcpServers?.codegraph;
const installed = loc === 'global'
? fs.existsSync(configDir(loc)) || fs.existsSync(mcpPath)
: fs.existsSync(mcpPath) || fs.existsSync(configDir(loc));
return { installed, alreadyConfigured, configPath: mcpPath };
}

install(loc: Location, opts: InstallOptions): WriteResult {
const files: WriteResult['files'] = [];

// 1. MCP server entry
files.push(writeMcpEntry(loc));

// 2. Permissions (only when autoAllow)
if (opts.autoAllow) {
files.push(writePermissionsEntry(loc));
}

// 3. CODEBUDDY.md instructions — no longer written. The codegraph
// usage guidance now ships solely in the MCP server's `initialize`
// response. Strip any block a previous install left behind.
const instrCleanup = removeInstructionsEntry(loc);
if (instrCleanup.action === 'removed') files.push(instrCleanup);

return { files };
}

uninstall(loc: Location): WriteResult {
const files: WriteResult['files'] = [];

// 1. MCP server entry
const mcpPath = mcpJsonPath(loc);
const config = readJsonFile(mcpPath);
if (config.mcpServers?.codegraph) {
delete config.mcpServers.codegraph;
if (Object.keys(config.mcpServers).length === 0) {
delete config.mcpServers;
}
writeJsonFile(mcpPath, config);
files.push({ path: mcpPath, action: 'removed' });
} else {
files.push({ path: mcpPath, action: 'not-found' });
}

// 2. Permissions
const settingsPath = settingsJsonPath(loc);
const settings = readJsonFile(settingsPath);
if (Array.isArray(settings.permissions?.allow)) {
const before = settings.permissions.allow.length;
settings.permissions.allow = settings.permissions.allow.filter(
(p: string) => !p.startsWith('mcp__codegraph__'),
);
if (settings.permissions.allow.length !== before) {
if (settings.permissions.allow.length === 0) {
delete settings.permissions.allow;
}
if (Object.keys(settings.permissions).length === 0) {
delete settings.permissions;
}
writeJsonFile(settingsPath, settings);
files.push({ path: settingsPath, action: 'removed' });
} else {
files.push({ path: settingsPath, action: 'not-found' });
}
} else {
files.push({ path: settingsPath, action: 'not-found' });
}

// 3. Instructions — strip the legacy CodeGraph block if present.
files.push(removeInstructionsEntry(loc));

return { files };
}

printConfig(loc: Location): string {
const target = mcpJsonPath(loc);
const snippet = JSON.stringify({ mcpServers: { codegraph: getMcpServerConfig() } }, null, 2);
return `# Add to ${target}\n\n${snippet}\n`;
}

describePaths(loc: Location): string[] {
return [mcpJsonPath(loc), settingsJsonPath(loc), instructionsPath(loc)];
}
}

export function writeMcpEntry(loc: Location): WriteResult['files'][number] {
const file = mcpJsonPath(loc);
const existing = readJsonFile(file);
const before = existing.mcpServers?.codegraph;
const after = getMcpServerConfig();

if (jsonDeepEqual(before, after)) {
return { path: file, action: 'unchanged' };
}
const action: 'created' | 'updated' = before ? 'updated' : (fs.existsSync(file) ? 'updated' : 'created');
if (!existing.mcpServers) existing.mcpServers = {};
existing.mcpServers.codegraph = after;
writeJsonFile(file, existing);
return { path: file, action };
}

export function writePermissionsEntry(loc: Location): WriteResult['files'][number] {
const file = settingsJsonPath(loc);
const settings = readJsonFile(file);
const created = !fs.existsSync(file);

if (!settings.permissions) settings.permissions = {};
if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = [];

const want = getCodeGraphPermissions();
const before = [...settings.permissions.allow];
for (const perm of want) {
if (!settings.permissions.allow.includes(perm)) {
settings.permissions.allow.push(perm);
}
}
if (jsonDeepEqual(before, settings.permissions.allow) && !created) {
return { path: file, action: 'unchanged' };
}
writeJsonFile(file, settings);
return { path: file, action: created ? 'created' : 'updated' };
}

export function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
const file = instructionsPath(loc);
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
return { path: file, action };
}

export const codebuddyTarget: AgentTarget = new CodeBuddyTarget();
2 changes: 2 additions & 0 deletions src/installer/targets/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import { AgentTarget, Location, TargetId } from './types';
import { claudeTarget } from './claude';
import { codebuddyTarget } from './codebuddy';
import { cursorTarget } from './cursor';
import { codexTarget } from './codex';
import { opencodeTarget } from './opencode';
Expand All @@ -19,6 +20,7 @@ import { kiroTarget } from './kiro';

export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
claudeTarget,
codebuddyTarget,
cursorTarget,
codexTarget,
opencodeTarget,
Expand Down
2 changes: 1 addition & 1 deletion src/installer/targets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type Location = 'global' | 'local';
* lookup. New targets add a value here when they're added to the
* registry. Keep these short and lowercase.
*/
export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro';
export type TargetId = 'claude' | 'codebuddy' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro';

/**
* Result of `target.detect(location)`.
Expand Down