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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph init` now builds the initial index by default — you no longer need the `-i`/`--index` flag (it's still accepted, so existing commands and scripts keep working). (#483)
- Go: Gin middleware chains now connect end-to-end in `codegraph_trace` and `codegraph_explore` — following a request reaches the middleware and route handlers registered via `.Use()` / `.GET()` instead of dead-ending where the framework dispatches the chain dynamically.
- `codegraph_explore` now sizes its response to the *answer* instead of the file count: it shows the mechanism and the exact methods you asked about in full — even when they're buried deep in a large file — while collapsing the redundant interchangeable implementations of an interface (an HTTP interceptor chain, a query-compiler family) down to signatures. Fewer tokens for a more complete answer, so on the flows that used to occasionally cost more than plain grep/read it's now clearly cheaper — and the win holds across small, medium, and large codebases. Distinct, non-interchangeable code is shown in full as before. Disable with `CODEGRAPH_ADAPTIVE_EXPLORE=0`.
- GitHub Copilot CLI: `codegraph install --target=copilot` now configures the Copilot CLI agent, writing MCP server entries to `~/.copilot/mcp-config.json` (global) or `.mcp.json` (local/workspace).

### Fixes

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# CodeGraph

### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Antigravity, and Kiro with Semantic Code Intelligence
### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Antigravity, Kiro, and Copilot CLI with Semantic Code Intelligence

**~25% cheaper · ~62% fewer tool calls · 100% local**

Expand All @@ -24,6 +24,7 @@
[![Gemini](https://img.shields.io/badge/Gemini-supported-blueviolet.svg)](#supported-agents)
[![Antigravity](https://img.shields.io/badge/Antigravity-supported-blueviolet.svg)](#supported-agents)
[![Kiro](https://img.shields.io/badge/Kiro-supported-blueviolet.svg)](#supported-agents)
[![Copilot CLI](https://img.shields.io/badge/Copilot_CLI-supported-blueviolet.svg)](#supported-agents)

</div>

Expand All @@ -46,7 +47,7 @@ npx @colbymchenry/codegraph # zero-install, or:
npm i -g @colbymchenry/codegraph
```

<sub>CodeGraph bundles its own runtime — nothing to compile, no native build, works the same everywhere. The interactive installer auto-configures your agent(s) — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro.</sub>
<sub>CodeGraph bundles its own runtime — nothing to compile, no native build, works the same everywhere. The interactive installer auto-configures your agent(s) — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, Copilot CLI.</sub>

### Initialize Projects

Expand Down Expand Up @@ -571,6 +572,7 @@ is written):
- **Gemini CLI**
- **Antigravity IDE**
- **Kiro**
- **Copilot CLI**

## Supported Languages

Expand Down
66 changes: 66 additions & 0 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,71 @@ describe('Installer targets — partial-state idempotency', () => {
expect(paths.some((p) => p.endsWith('/.kiro/steering/codegraph.md'))).toBe(false);
});

it('copilot: local install writes ./.mcp.json with "mcpServers" key', () => {
const copilot = getTarget('copilot')!;
const result = copilot.install('local', { autoAllow: true });
const mcpPath = path.join(tmpCwd, '.mcp.json');
expect(result.files.some((f) => f.path === mcpPath)).toBe(true);
expect(fs.existsSync(mcpPath)).toBe(true);

const cfg = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
// Copilot CLI uses "mcpServers" (same as Claude/Cursor).
expect(cfg.mcpServers.codegraph).toBeDefined();
expect(cfg.mcpServers.codegraph.type).toBe('stdio');
expect(cfg.mcpServers.codegraph.command).toBeDefined();
expect(cfg.mcpServers.codegraph.args).toEqual(['serve', '--mcp']);
});

it('copilot: global install writes to ~/.copilot/mcp-config.json', () => {
const copilot = getTarget('copilot')!;
const result = copilot.install('global', { autoAllow: true });
const mcpPath = path.join(tmpHome, '.copilot', 'mcp-config.json');
expect(result.files.some((f) => f.path === mcpPath)).toBe(true);
expect(fs.existsSync(mcpPath)).toBe(true);

const cfg = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
expect(cfg.mcpServers.codegraph).toBeDefined();
expect(cfg.mcpServers.codegraph.type).toBe('stdio');
});

it('copilot: install preserves a pre-existing sibling MCP server in .mcp.json', () => {
const copilot = getTarget('copilot')!;
const mcpPath = path.join(tmpCwd, '.mcp.json');
fs.mkdirSync(path.dirname(mcpPath), { recursive: true });
fs.writeFileSync(mcpPath, JSON.stringify({
mcpServers: { other: { type: 'stdio', command: 'uvx', args: ['other-server'] } },
}, null, 2) + '\n');

copilot.install('local', { autoAllow: true });

const after = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
expect(after.mcpServers.other).toBeDefined();
expect(after.mcpServers.codegraph).toBeDefined();
});

it('copilot: uninstall strips codegraph but leaves sibling MCP servers intact', () => {
const copilot = getTarget('copilot')!;
const mcpPath = path.join(tmpCwd, '.mcp.json');
fs.mkdirSync(path.dirname(mcpPath), { recursive: true });
fs.writeFileSync(mcpPath, JSON.stringify({
mcpServers: { other: { type: 'stdio', command: 'uvx', args: ['other-server'] } },
}, null, 2) + '\n');

copilot.install('local', { autoAllow: true });
copilot.uninstall('local');

const after = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
expect(after.mcpServers.other).toBeDefined();
expect(after.mcpServers.codegraph).toBeUndefined();
});

it('copilot: printConfig returns mcpServers format', () => {
const copilot = getTarget('copilot')!;
const output = copilot.printConfig('local');
expect(output).toContain('"mcpServers"');
expect(output).toContain('codegraph');
});

it('antigravity: install writes to LEGACY ~/.gemini/antigravity/mcp_config.json when no migration marker', () => {
const antigravity = getTarget('antigravity')!;
antigravity.install('global', { autoAllow: true });
Expand Down Expand Up @@ -1098,6 +1163,7 @@ describe('Installer targets — registry', () => {
expect(getTarget('gemini')?.id).toBe('gemini');
expect(getTarget('antigravity')?.id).toBe('antigravity');
expect(getTarget('kiro')?.id).toBe('kiro');
expect(getTarget('copilot')?.id).toBe('copilot');
expect(getTarget('not-a-real-target')).toBeUndefined();
});

Expand Down
4 changes: 2 additions & 2 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1607,7 +1607,7 @@ program
*/
program
.command('install')
.description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)')
.description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Copilot CLI)')
.option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
.option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
.option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on')
Expand Down Expand Up @@ -1674,7 +1674,7 @@ program
*/
program
.command('uninstall')
.description('Remove codegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)')
.description('Remove codegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Copilot CLI)')
.option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "all". Default: all')
.option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt')
.option('-y, --yes', 'Non-interactive: defaults to --location=global --target=all')
Expand Down
87 changes: 79 additions & 8 deletions src/installer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,35 +257,79 @@ export interface UninstallReport {
*
* Each target's `uninstall()` is already safe to call when nothing was
* installed (it returns `not-found` actions), so this is safe to run
* across every target unconditionally.
* across every target unconditionally. We still gate on `detect()`
* for targets that share a config file (e.g. `.mcp.json` is used by
* both Claude Code and Copilot CLI for local installs) — without the
* gate, one agent's uninstall would strip the other agent's entry
* from the shared file.
*
* When a shared config file has its codegraph entry removed, other
* targets that also write to that same file are marked "removed" as
* well — because the entry they rely on no longer exists. A note
* explains the cascade so the user understands what happened.
*/
export function uninstallTargets(
targets: readonly AgentTarget[],
location: Location,
): UninstallReport[] {
return targets.map((target) => {
// Track config paths that had their codegraph entry removed.
// When multiple targets share a file (e.g. .mcp.json), removing
// the entry for one target removes it for all of them.
const removedConfigPaths = new Set<string>();
const reports: UninstallReport[] = [];

for (const target of targets) {
if (!target.supportsLocation(location)) {
const only: Location = location === 'local' ? 'global' : 'local';
return {
reports.push({
id: target.id,
displayName: target.displayName,
status: 'unsupported' as const,
removedPaths: [],
notes: [`no ${location} config — this agent is ${only}-only`],
};
});
continue;
}
if (!target.detect(location).installed) {
reports.push({
id: target.id,
displayName: target.displayName,
status: 'not-configured' as const,
removedPaths: [],
notes: [],
});
continue;
}

// Check if a prior target already removed the entry from this
// target's config file (shared-file cascade).
const cfgPath = target.detect(location).configPath;
if (cfgPath && removedConfigPaths.has(cfgPath)) {
reports.push({
id: target.id,
displayName: target.displayName,
status: 'removed' as const,
removedPaths: [cfgPath],
notes: [`shared config file (${cfgPath}) — entry was already removed by a previous uninstall`],
});
continue;
}

const result = target.uninstall(location);
const removedPaths = result.files
.filter((f) => f.action === 'removed')
.map((f) => f.path);
return {
for (const p of removedPaths) removedConfigPaths.add(p);
reports.push({
id: target.id,
displayName: target.displayName,
status: removedPaths.length > 0 ? ('removed' as const) : ('not-configured' as const),
removedPaths,
notes: result.notes ?? [],
};
});
});
}

return reports;
}

/**
Expand Down Expand Up @@ -317,7 +361,7 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise<void>
const sel = await clack.select({
message: 'Remove CodeGraph from all your projects, or just this one?',
options: [
{ value: 'global' as const, label: 'All projects (global)', hint: '~/.claude, ~/.cursor, ~/.codex, ~/.config/opencode, ~/.hermes, ~/.gemini, ~/.kiro' },
{ value: 'global' as const, label: 'All projects (global)', hint: '~/.claude, ~/.cursor, ~/.codex, ~/.config/opencode, ~/.hermes, ~/.gemini, ~/.kiro, ~/.copilot' },
{ value: 'local' as const, label: 'Just this project (local)', hint: './.claude, ./.cursor, ./opencode.jsonc, ./.gemini, ./.kiro' },
],
initialValue: 'global' as const,
Expand Down Expand Up @@ -353,13 +397,40 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise<void>
for (const p of r.removedPaths) {
clack.log.success(`${r.displayName}: removed ${tildify(p)}`);
}
if (r.notes.length > 0) {
for (const n of r.notes) {
clack.log.warn(`${r.displayName}: ${n}`);
}
}
} else if (r.status === 'not-configured') {
clack.log.info(`${r.displayName}: not configured — nothing to remove`);
} else {
clack.log.info(`${r.displayName}: skipped — ${r.notes[0] ?? 'unsupported location'}`);
}
}

// Step 3b: warn about shared-config cascade. When multiple targets
// write to the same file (e.g. .mcp.json is shared by Claude Code,
// Cursor, and Copilot CLI for local installs), removing the
// codegraph entry for one target also removes it for the others.
const removedPaths = new Set(removed.flatMap((r) => r.removedPaths));
if (removedPaths.size > 0) {
const affected = ALL_TARGETS.filter((t) => {
if (reports.some((r) => r.id === t.id && r.status === 'removed')) return false;
if (!t.supportsLocation(location)) return false;
const det = t.detect(location);
return det.installed && det.configPath && removedPaths.has(det.configPath);
});
if (affected.length > 0) {
const names = affected.map((t) => t.displayName).join(', ');
const files = [...removedPaths].map((p) => tildify(p)).join(', ');
clack.log.warn(
`Shared config: ${files} is also used by ${names}. ` +
`The codegraph entry was removed from that file — those agents will need re-installing.`,
);
}
}

// Step 4: for local uninstall, the index dir is separate — point at
// `uninit` so the user knows it's still there (and how to remove it).
if (location === 'local' && fs.existsSync(path.join(process.cwd(), '.codegraph'))) {
Expand Down
8 changes: 7 additions & 1 deletion src/installer/targets/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,15 @@ class ClaudeCodeTarget implements AgentTarget {
// For "installed" we infer from the existence of either the dir
// (global) or the project marker file (local). Cheap and avoids
// shelling out to `claude --version`.
// For local: .mcp.json is shared infrastructure (Copilot CLI
// also writes to it), so we CANNOT use it as a Claude-specific
// detection signal. Only .claude/ is Claude's own marker.
// Copilot has no equivalent local dir, so it keeps .mcp.json
// as its detection signal. The shared-config cascade in
// uninstallTargets handles the conflict when both are installed.
const installed = loc === 'global'
? fs.existsSync(configDir(loc)) || fs.existsSync(mcpPath)
: fs.existsSync(mcpPath) || fs.existsSync(configDir(loc));
: fs.existsSync(configDir(loc));
return { installed, alreadyConfigured, configPath: mcpPath };
}

Expand Down
Loading