From 27f79e97c5b771bd5241a8da1f97b3daff00e45b Mon Sep 17 00:00:00 2001 From: DevinZeng Date: Sat, 30 May 2026 00:59:28 +0800 Subject: [PATCH] feat: add CodeBuddy target support Add CodeBuddy as a new installer target. CodeBuddy follows the same config layout as Claude Code, using .codebuddy/ directory and CODEBUDDY.md instead of .claude/ and CLAUDE.md. Closes https://github.com/colbymchenry/codegraph/issues/164 --- .../content/docs/reference/integrations.md | 1 + src/installer/targets/codebuddy.ts | 199 ++++++++++++++++++ src/installer/targets/registry.ts | 2 + src/installer/targets/types.ts | 2 +- 4 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 src/installer/targets/codebuddy.ts diff --git a/site/src/content/docs/reference/integrations.md b/site/src/content/docs/reference/integrations.md index 67e7b96e4..f71c0cf6e 100644 --- a/site/src/content/docs/reference/integrations.md +++ b/site/src/content/docs/reference/integrations.md @@ -8,6 +8,7 @@ The interactive installer auto-detects and configures each supported agent — w ## Supported agents - **Claude Code** +- **CodeBuddy** - **Cursor** - **Codex CLI** - **opencode** diff --git a/src/installer/targets/codebuddy.ts b/src/installer/targets/codebuddy.ts new file mode 100644 index 000000000..0153eabf5 --- /dev/null +++ b/src/installer/targets/codebuddy.ts @@ -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(); diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts index 5e929d468..03e7a6b66 100644 --- a/src/installer/targets/registry.ts +++ b/src/installer/targets/registry.ts @@ -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'; @@ -19,6 +20,7 @@ import { kiroTarget } from './kiro'; export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ claudeTarget, + codebuddyTarget, cursorTarget, codexTarget, opencodeTarget, diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts index 4b3267e97..c28c8015e 100644 --- a/src/installer/targets/types.ts +++ b/src/installer/targets/types.ts @@ -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)`.