Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
Expand Down
71 changes: 64 additions & 7 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)', () => {
Expand Down Expand Up @@ -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)', () => {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand Down
6 changes: 3 additions & 3 deletions src/installer/targets/antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -130,7 +130,7 @@ function resolveCodegraphCommand(): string {
} catch {
/* fall through to bare name */
}
return 'codegraph-vba';
return 'codegraph';
}

/**
Expand Down
33 changes: 22 additions & 11 deletions src/installer/targets/hermes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,8 @@ function escapeRegExp(value: string): string {

function renderCodeGraphMcpChild(): string[] {
return [
' codegraph-vba:',
' command: codegraph-vba',
' codegraph:',
' command: codegraph',
' args:',
' - serve',
' - --mcp',
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down
48 changes: 28 additions & 20 deletions src/installer/targets/opencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,14 @@ function parseConfig(text: string): Record<string, any> {
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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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' };
}

Expand All @@ -221,19 +228,24 @@ 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' };
}

/**
* 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.
*/
Expand All @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion src/installer/targets/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
};
}
Expand Down
Loading