From 1af17df6a0a2a6e9863d841e1e0822c391f1c9dc Mon Sep 17 00:00:00 2001 From: andres Date: Sat, 19 Sep 2026 12:13:43 +0200 Subject: [PATCH] fix(installer): write the canonical codegraph MCP entry gentle-ai and other harnesses only recognise an MCP entry keyed and launched as codegraph. Every target now writes the upstream shape (codegraph serve --mcp); opencode and Hermes migrate the legacy codegraph-vba key, and opencode keeps extra fields such as environment. The codegraph-vba CLI alias stays for Dysflow. Closes #328 --- CHANGELOG.md | 4 ++ README.md | 2 +- __tests__/installer-targets.test.ts | 71 +++++++++++++++++++++++++--- src/installer/targets/antigravity.ts | 6 +-- src/installer/targets/hermes.ts | 33 ++++++++----- src/installer/targets/opencode.ts | 48 +++++++++++-------- src/installer/targets/shared.ts | 5 +- 7 files changed, 126 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b35aa1..1cea07b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixes + +- `codegraph-vba install` now writes the same MCP server entry as upstream CodeGraph (`codegraph serve --mcp`, under the `codegraph` key), so tools that manage CodeGraph for you, such as gentle-ai, recognise this fork as CodeGraph. Entries left under the old `codegraph-vba` key in opencode and Hermes are migrated, and any extra settings you added to the opencode entry are kept. (#328) + ## [2.1.0] - 2026-09-19 diff --git a/README.md b/README.md index 9b884c35..5bb698f1 100644 --- a/README.md +++ b/README.md @@ -612,7 +612,7 @@ irm https://raw.githubusercontent.com/ardelperal/codegraph-vba/main/install.ps1 "mcpServers": { "codegraph": { "type": "stdio", - "command": "codegraph-vba", + "command": "codegraph", "args": ["serve", "--mcp"] } } diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index ca6449c3..f075b3fb 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -143,7 +143,7 @@ describe('Installer targets — contract', () => { const after = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')); if (target.id === 'opencode') { expect(after.mcp.other).toBeDefined(); - expect(after.mcp['codegraph-vba']).toBeDefined(); + expect(after.mcp.codegraph).toBeDefined(); } else { expect(after.mcpServers.other).toBeDefined(); expect(after.mcpServers.codegraph).toBeDefined(); @@ -285,7 +285,7 @@ describe('Installer targets — partial-state idempotency', () => { expect(afterInstall).toContain('// top-level note about my opencode setup'); expect(afterInstall).toContain('/* multi-line block comment'); expect(afterInstall).toContain('// pinned'); - expect(afterInstall).toContain('"codegraph-vba"'); + expect(afterInstall).toContain('"codegraph"'); expect(afterInstall).toContain('"providers"'); // Idempotent re-run reports unchanged, file is byte-identical. @@ -356,7 +356,7 @@ describe('Installer targets — partial-state idempotency', () => { expect(fs.readFileSync(geminiMd, 'utf-8')).toContain('codegraph explore'); const cfg = JSON.parse(fs.readFileSync(settings, 'utf-8')); - expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph-vba', args: ['serve', '--mcp'] }); + expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] }); }); it('gemini: install preserves pre-existing settings (security.auth survives)', () => { @@ -423,7 +423,7 @@ describe('Installer targets — partial-state idempotency', () => { expect(fs.existsSync(steering)).toBe(false); const cfg = JSON.parse(fs.readFileSync(mcp, 'utf-8')); - expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph-vba', args: ['serve', '--mcp'] }); + expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] }); }); it('kiro: install deletes a leftover steering codegraph.md (self-heal) (#529)', () => { @@ -722,7 +722,7 @@ describe('Installer targets — partial-state idempotency', () => { const body = fs.readFileSync(config, 'utf-8'); expect(body).toContain('model:\n default: qwen-3.7'); expect(body).toContain('mcp_servers:\n other:\n command: other'); - expect(body).toContain(' codegraph-vba:\n command: codegraph-vba'); + expect(body).toContain(' codegraph:\n command: codegraph'); expect(body).toContain(' - hermes-cli'); expect(body).toContain(' - mcp-codegraph'); expect(body).toContain(' discord:\n - hermes-discord'); @@ -816,7 +816,7 @@ describe('Installer targets — partial-state idempotency', () => { hermes.install('global', { autoAllow: true }); const installed = fs.readFileSync(config, 'utf-8'); expect(installed).toContain('- mcp-codegraph'); - expect(installed).toContain('codegraph-vba:'); + expect(installed).toContain(' codegraph:'); hermes.uninstall('global'); const body = fs.readFileSync(config, 'utf-8'); @@ -826,6 +826,63 @@ describe('Installer targets — partial-state idempotency', () => { expect(body).toContain(' telegram:\n - hermes-telegram'); }); + // gentle-ai only recognises `mcp.codegraph` with command + // ["codegraph","serve","--mcp"] (issue #328). An earlier fork release wrote + // `mcp["codegraph-vba"]`; a user may also already own `mcp.codegraph` with + // their own `environment` (e.g. CODEGRAPH_MCP_TOOLS). + it('opencode: install migrates the legacy codegraph-vba key and keeps the user environment', () => { + const opencode = getTarget('opencode')!; + const dir = path.join(tmpHome, '.config', 'opencode'); + fs.mkdirSync(dir, { recursive: true }); + const file = path.join(dir, 'opencode.json'); + fs.writeFileSync(file, JSON.stringify({ + $schema: 'https://opencode.ai/config.json', + mcp: { + 'codegraph-vba': { type: 'local', command: ['codegraph-vba', 'serve', '--mcp'], enabled: true }, + codegraph: { + type: 'local', command: ['codegraph', 'serve', '--mcp'], enabled: true, + environment: { CODEGRAPH_MCP_TOOLS: 'explore,callers,impact' }, + }, + }, + }, null, 2)); + + expect(opencode.detect('global').alreadyConfigured).toBe(true); + expect(opencode.install('global', { autoAllow: true }).files[0].action).toBe('updated'); + + const mcp = JSON.parse(fs.readFileSync(file, 'utf-8')).mcp; + expect(mcp['codegraph-vba']).toBeUndefined(); + expect(mcp.codegraph).toEqual({ + type: 'local', command: ['codegraph', 'serve', '--mcp'], enabled: true, + environment: { CODEGRAPH_MCP_TOOLS: 'explore,callers,impact' }, + }); + expect(opencode.install('global', { autoAllow: true }).files[0].action).toBe('unchanged'); + }); + + it('hermes: install migrates the legacy codegraph-vba server and uninstall removes both keys', () => { + const hermes = getTarget('hermes')!; + const config = path.join(tmpHome, '.hermes', 'config.yaml'); + fs.mkdirSync(path.dirname(config), { recursive: true }); + fs.writeFileSync(config, [ + 'mcp_servers:', + ' codegraph-vba:', + ' command: codegraph-vba', + ' args:', + ' - serve', + ' - --mcp', + '', + ].join('\n')); + + expect(hermes.detect('global').alreadyConfigured).toBe(true); + hermes.install('global', { autoAllow: true }); + const installed = fs.readFileSync(config, 'utf-8'); + expect(installed).not.toContain('codegraph-vba'); + expect(installed).toContain(' codegraph:\n command: codegraph'); + + fs.writeFileSync(config, installed.replace('mcp_servers:\n', 'mcp_servers:\n codegraph-vba:\n command: codegraph-vba\n')); + hermes.uninstall('global'); + expect(fs.readFileSync(config, 'utf-8')).not.toMatch(/codegraph(-vba)?:/); + }); + it('opencode: uninstall removes only mcp.codegraph, preserves comments and siblings', () => { const opencode = getTarget('opencode')!; const dir = path.join(tmpHome, '.config', 'opencode'); @@ -844,7 +901,7 @@ describe('Installer targets — partial-state idempotency', () => { opencode.install('global', { autoAllow: true }); const afterInstall = fs.readFileSync(file, 'utf-8'); - expect(afterInstall).toContain('"codegraph-vba"'); + expect(afterInstall).toContain('"codegraph"'); expect(afterInstall).toContain('"other"'); opencode.uninstall('global'); diff --git a/src/installer/targets/antigravity.ts b/src/installer/targets/antigravity.ts index 45cae967..1c128491 100644 --- a/src/installer/targets/antigravity.ts +++ b/src/installer/targets/antigravity.ts @@ -118,9 +118,9 @@ function preferredMcpConfigPath(): string { * nvm-managed tools like ours. */ function resolveCodegraphCommand(): string { - if (process.platform !== 'darwin') return 'codegraph-vba'; + if (process.platform !== 'darwin') return 'codegraph'; try { - const resolved = execSync('command -v codegraph-vba || which codegraph-vba', { + const resolved = execSync('command -v codegraph || which codegraph', { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], shell: '/bin/bash', @@ -130,7 +130,7 @@ function resolveCodegraphCommand(): string { } catch { /* fall through to bare name */ } - return 'codegraph-vba'; + return 'codegraph'; } /** diff --git a/src/installer/targets/hermes.ts b/src/installer/targets/hermes.ts index 86495430..463c0783 100644 --- a/src/installer/targets/hermes.ts +++ b/src/installer/targets/hermes.ts @@ -251,8 +251,8 @@ function escapeRegExp(value: string): string { function renderCodeGraphMcpChild(): string[] { return [ - ' codegraph-vba:', - ' command: codegraph-vba', + ' codegraph:', + ' command: codegraph', ' args:', ' - serve', ' - --mcp', @@ -269,13 +269,29 @@ function renderCodeGraphMcpBlock(): string[] { function hasCodeGraphMcpServer(content: string): boolean { const lines = splitLines(content); const parent = topLevelRange(lines, 'mcp_servers'); - return !!parent && !!childRange(lines, parent, 'codegraph-vba'); + return !!parent && (!!childRange(lines, parent, 'codegraph') || !!childRange(lines, parent, LEGACY_MCP_KEY)); } -function upsertCodeGraphMcpServer(content: string): string { +/** + * Key an earlier fork release wrote. Install migrates it to the canonical + * `codegraph` key and uninstall removes it, so a user never ends up with two + * servers (issue #328). + */ +const LEGACY_MCP_KEY = 'codegraph-vba'; + +function removeChild(content: string, key: string): string { const lines = splitLines(content); const parent = topLevelRange(lines, 'mcp_servers'); - const child = parent ? childRange(lines, parent, 'codegraph-vba') : null; + const child = parent ? childRange(lines, parent, key) : null; + if (!child) return content; + lines.splice(child.start, child.end - child.start); + return joinLines(lines); +} + +function upsertCodeGraphMcpServer(content: string): string { + const lines = splitLines(removeChild(content, LEGACY_MCP_KEY)); + const parent = topLevelRange(lines, 'mcp_servers'); + const child = parent ? childRange(lines, parent, 'codegraph') : null; const replacement = renderCodeGraphMcpChild(); if (!parent) { @@ -297,12 +313,7 @@ function upsertCodeGraphMcpServer(content: string): string { } function removeCodeGraphMcpServer(content: string): string { - const lines = splitLines(content); - const parent = topLevelRange(lines, 'mcp_servers'); - const child = parent ? childRange(lines, parent, 'codegraph-vba') : null; - if (!child) return content; - lines.splice(child.start, child.end - child.start); - return joinLines(lines); + return removeChild(removeChild(content, 'codegraph'), LEGACY_MCP_KEY); } function upsertCodeGraphToolset(content: string): string { diff --git a/src/installer/targets/opencode.ts b/src/installer/targets/opencode.ts index a9836684..fe847507 100644 --- a/src/installer/targets/opencode.ts +++ b/src/installer/targets/opencode.ts @@ -118,11 +118,14 @@ function parseConfig(text: string): Record { function getOpencodeServerEntry(): { type: string; command: string[]; enabled: boolean } { return { type: 'local', - command: ['codegraph-vba', 'serve', '--mcp'], + command: ['codegraph', 'serve', '--mcp'], enabled: true, }; } +/** Key earlier fork releases wrote; migrated to `codegraph` (issue #328). */ +const LEGACY_MCP_KEY = 'codegraph-vba'; + const FORMATTING = { tabSize: 2, insertSpaces: true, eol: '\n' }; class OpencodeTarget implements AgentTarget { @@ -137,12 +140,10 @@ class OpencodeTarget implements AgentTarget { detect(loc: Location): DetectionResult { const file = configPath(loc); const config = parseConfig(readConfigText(file)); - // Both `codegraph-vba` (current post-fork key the installer writes) and - // `codegraph` (legacy pre-fork key a much older release used to write) - // count as "already configured" — the legacy check is purely a - // backward-compat signal so a re-install on top of a pre-fork entry - // reports "Updated" instead of falsely adding a duplicate entry. - const alreadyConfigured = !!config.mcp?.['codegraph-vba'] || !!config.mcp?.codegraph; + // `codegraph` is the canonical key; `codegraph-vba` is the key earlier + // fork releases wrote. Either counts as configured so a re-install + // migrates it instead of reporting a fresh add. + const alreadyConfigured = !!config.mcp?.codegraph || !!config.mcp?.[LEGACY_MCP_KEY]; // Global: the XDG dir is what current opencode creates on first run; the // legacy %APPDATA% dir still counts as "opencode present" so a re-install // can sweep the stale pre-#535 entry out of it. @@ -204,10 +205,16 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] { } const config = parseConfig(text); - const before = config.mcp?.['codegraph-vba']; - const after = getOpencodeServerEntry(); + const before = config.mcp?.codegraph; + // Keep fields we don't own (e.g. `environment`) — the canonical key may + // already hold a user's own entry; we only pin type/command/enabled. + const after = { + ...(before && typeof before === 'object' && !Array.isArray(before) ? before : {}), + ...getOpencodeServerEntry(), + }; + const hasLegacy = !!config.mcp?.[LEGACY_MCP_KEY]; - if (jsonDeepEqual(before, after)) { + if (jsonDeepEqual(before, after) && !hasLegacy) { return { path: file, action: 'unchanged' }; } @@ -221,10 +228,15 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] { // Surgical edit — preserves comments, formatting, and order of // every key we don't touch. - const edits = modify(text, ['mcp', 'codegraph-vba'], after, { + const edits = modify(text, ['mcp', 'codegraph'], after, { formattingOptions: FORMATTING, }); - const updated = applyEdits(text, edits); + let updated = applyEdits(text, edits); + if (hasLegacy) { + updated = applyEdits(updated, modify(updated, ['mcp', LEGACY_MCP_KEY], undefined, { + formattingOptions: FORMATTING, + })); + } atomicWriteFileSync(file, updated); return { path: file, action: existed ? 'updated' : 'created' }; @@ -232,8 +244,8 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] { /** * Surgically drop our MCP entry from one config file, sweeping BOTH the - * current post-fork key (`codegraph-vba`) AND the legacy pre-fork key - * (`codegraph`) that pre-1.x installers wrote. Leaves sibling servers, + * canonical key (`codegraph`) AND the legacy key (`codegraph-vba`) that + * earlier fork releases wrote. Leaves sibling servers, * comments, and formatting untouched; drops an emptied `mcp` wrapper too. * Shared by uninstall and the legacy-%APPDATA% sweep. */ @@ -242,16 +254,12 @@ function removeMcpEntryAt(file: string): WriteResult['files'][number] { const text = readConfigText(file); const config = parseConfig(text); - // Sweep BOTH historical keys. `codegraph-vba` is the canonical current - // entry; `codegraph` is the pre-fork key a much older release wrote — - // a real user upgrading from pre-fork has it on disk and opencode would - // never read it (no key matches), so leaving it behind is just stale - // state. Removing both keys keeps the legacy-cleanup promise intact. + // Sweep both keys so an uninstall never leaves a stale legacy entry. const mcp = config.mcp; if (!mcp || typeof mcp !== 'object') return { path: file, action: 'not-found' }; let updated = text; - for (const key of ['codegraph-vba', 'codegraph']) { + for (const key of ['codegraph', LEGACY_MCP_KEY]) { if (mcp[key]) { const edits = modify(updated, ['mcp', key], undefined, { formattingOptions: FORMATTING }); updated = applyEdits(updated, edits); diff --git a/src/installer/targets/shared.ts b/src/installer/targets/shared.ts index 418b4623..ba3b07e8 100644 --- a/src/installer/targets/shared.ts +++ b/src/installer/targets/shared.ts @@ -24,7 +24,10 @@ import { export function getMcpServerConfig(): { type: string; command: string; args: string[] } { return { type: 'stdio', - command: 'codegraph-vba', + // The canonical upstream command: gentle-ai and other harnesses only + // recognise an entry whose command is `codegraph` (issue #328). The + // bundle ships `codegraph` as a launcher of this fork. + command: 'codegraph', args: ['serve', '--mcp'], }; }