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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### New Features

- `codegraph install` now detects and configures **Qoder**. Global install writes MCP to shared `mcp.json` under the platform config dir (`SharedClientCache`); local also writes `.qoder/rules/codegraph.md` so the agent uses `codegraph_explore`. Omits `--path` (MCP `roots/list`) and resolves an absolute `command` on macOS. Sibling servers preserved; re-runs idempotent.

## [1.5.0] - 2026-07-21

Expand Down Expand Up @@ -46,6 +49,7 @@ Full details in the entries below.
- The dynamic-dispatch analysis at the end of indexing now skips passes that provably can't produce anything for the project at hand: React re-render bridging when no class has a `render` method, React Native and Expo cross-platform pairing when the required languages aren't present, and MyBatis mapper linking when there's no mapper XML. Previously each of these scanned the whole graph before coming up empty — on a 4,000-file Java project that was about 0.9 seconds of wasted analysis per fresh index. The interface-implementation bridging pass also got cheaper on real work: it no longer re-fetches a hub interface's method list once per implementer, and classes that extend or implement nothing are skipped before any per-class lookups. Graphs remain byte-for-byte identical.
- Indexing large C and C++ codebases spends much less time in the function-pointer dispatch analysis (the pass that connects handler tables like a command table or an ops struct to their call sites): each source file is now read and prepared once instead of four times, files that can't contribute any dispatch wiring are skipped outright in the later linking steps, and on platforms with the native engine the per-file scanning itself now runs natively too. On a Linux-kernel-scale tree the pass runs about a third faster end-to-end, with graphs byte-for-byte identical; platforms without a native binary keep the same results on the previous path.


### Fixes

- Codex installs, upgrades, and uninstalls now preserve TOML array-of-table sections that appear after CodeGraph's MCP configuration instead of accidentally removing them. (#1351)
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Already installed? Run `codegraph upgrade`

Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.

### 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 Qoder with Semantic Code Intelligence

**The fastest complete code graph · surgical context · built for how agents actually work · 100% local**

Expand Down Expand Up @@ -35,6 +35,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.
[![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)
[![Qoder](https://img.shields.io/badge/Qoder-supported-blueviolet.svg)](#supported-agents)

<br>

Expand Down Expand Up @@ -104,7 +105,7 @@ In a **new terminal**, run the installer to connect CodeGraph to the agents you
codegraph install
```

<sub>Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. It only wires up your agent — it does **not** index any code; building each project's graph is the separate `codegraph init` in step 3. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)</sub>
<sub>Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and Qoder — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. It only wires up your agent — it does **not** index any code; building each project's graph is the separate `codegraph init` in step 3. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)</sub>

### 3. Initialize each project

Expand Down Expand Up @@ -371,7 +372,7 @@ npx @colbymchenry/codegraph
```

The installer will:
- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**
- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**, **Qoder**
- Prompt to install `codegraph` on your PATH (so agents can launch the MCP server)
- Ask whether configs apply to all your projects or just this one
- Write each chosen agent's MCP server config, plus a small marker-fenced CodeGraph section in the agent's instructions file (`CLAUDE.md` / `AGENTS.md` / `GEMINI.md`) — that's how subagents and non-MCP agents learn the `codegraph explore` command, since the MCP server's own guidance only reaches the main agent. Removed cleanly by `codegraph uninstall`.
Expand All @@ -398,7 +399,7 @@ codegraph install --print-config codex # print snippet, no file wr

### 2. Restart Your Agent

Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro) for the MCP server to load.
Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro / Qoder) for the MCP server to load.

### 3. Initialize Projects

Expand Down Expand Up @@ -756,6 +757,7 @@ is written):
- **Gemini CLI**
- **Antigravity IDE**
- **Kiro**
- **Qoder**

## Supported Languages

Expand Down Expand Up @@ -854,7 +856,7 @@ MIT

<div align="center">

**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro**
**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and Qoder**

[Report Bug](https://github.com/colbymchenry/codegraph/issues) · [Request Feature](https://github.com/colbymchenry/codegraph/issues)

Expand Down
206 changes: 206 additions & 0 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,212 @@ describe('Installer targets — partial-state idempotency', () => {
expect(cliAfter.mcpServers.codegraph).toBeDefined();
});

it('qoder: install writes SharedClientCache/mcp.json under the platform config dir', () => {
const qoder = getTarget('qoder')!;
const result = qoder.install('global', { autoAllow: true });
const file = result.files[0]!.path;
// Path lives under the platform-appropriate Qoder config dir.
expect(file.endsWith(path.join('Qoder', 'SharedClientCache', 'mcp.json'))).toBe(true);
expect(fs.existsSync(file)).toBe(true);
const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'));
expect(cfg.mcpServers.codegraph).toBeDefined();
expect(cfg.mcpServers.codegraph.type).toBe('stdio');
expect(cfg.mcpServers.codegraph.args).toEqual(['serve', '--mcp']);
// command must be a non-empty string. On macOS it may resolve to an
// absolute path via `command -v codegraph`; elsewhere the bare name
// is fine. Either way, it must end in `codegraph` — an empty or
// truncated string here would spawn ENOENT inside Qoder.
expect(typeof cfg.mcpServers.codegraph.command).toBe('string');
expect(cfg.mcpServers.codegraph.command.length).toBeGreaterThan(0);
expect(cfg.mcpServers.codegraph.command).toMatch(/codegraph(\.(cmd|exe|bat))?$/);
});

it('qoder: entry never carries --path or ${workspaceFolder} (Qoder does not substitute vars)', () => {
// Regression guard: Qoder passes MCP args to the child verbatim, so a
// literal `${workspaceFolder}` reaches codegraph as a bogus path and
// (pre-#964 codegraph) yields an empty tools/list. We install with no
// --path at all and rely on the MCP roots/list handshake instead.
const qoder = getTarget('qoder')!;
qoder.install('global', { autoAllow: true });
const cfg = JSON.parse(fs.readFileSync(qoder.describePaths('global')[0]!, 'utf-8'));
const args: string[] = cfg.mcpServers.codegraph.args;
expect(args).not.toContain('--path');
expect(args.some((a: string) => a.includes('${workspaceFolder}'))).toBe(false);
});

it('qoder: uninstall strips codegraph but leaves sibling MCP servers intact', () => {
const qoder = getTarget('qoder')!;
const file = qoder.describePaths('global')[0]!;
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, JSON.stringify({
mcpServers: { 'forge-gateway': { url: 'http://example.test/mcp' } },
}, null, 2) + '\n');

qoder.install('global', { autoAllow: true });
qoder.uninstall('global');

const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'));
expect(cfg.mcpServers['forge-gateway']).toBeDefined();
expect(cfg.mcpServers.codegraph).toBeUndefined();
});

it('qoder: --location=local writes both the project .qoder/rules/codegraph.md AND the shared MCP config (one command sets up this project, matching opencode/gemini local semantics)', () => {
const qoder = getTarget('qoder')!;
expect(qoder.supportsLocation('local')).toBe(true);

const result = qoder.install('local', { autoAllow: true });
// Two files touched: project-scoped `.qoder/rules/codegraph.md`
// (Qoder's own-whole-file rule surface, higher precedence than
// AGENTS.md) + shared user-scope mcp.json (Qoder has no
// project-local mcp.json path).
const ruleEntry = result.files.find((f) => f.path.endsWith(path.join('.qoder', 'rules', 'codegraph.md')));
const mcpEntry = result.files.find((f) => f.path.endsWith('mcp.json'));
expect(ruleEntry).toBeDefined();
expect(mcpEntry).toBeDefined();
// process.cwd() returns the realpath (macOS /var → /private/var), so
// resolve tmpCwd through realpath before comparing.
expect(ruleEntry!.path).toBe(path.join(fs.realpathSync(tmpCwd), '.qoder', 'rules', 'codegraph.md'));
expect(fs.existsSync(ruleEntry!.path)).toBe(true);
expect(fs.existsSync(mcpEntry!.path)).toBe(true);

// Rule file is own-whole-file (NO marker fence) and mentions codegraph.
const body = fs.readFileSync(ruleEntry!.path, 'utf-8');
expect(body).not.toContain('<!-- CODEGRAPH_START -->');
expect(body).not.toContain('<!-- CODEGRAPH_END -->');
expect(body.toLowerCase()).toContain('codegraph');
// Qoder-specific frontmatter: `trigger: always_on` makes this an
// Always Apply rule (in-context every turn). Without it Qoder
// classifies the rule as Model Decision and the agent may skip it,
// defeating the purpose of shipping the rule at all.
expect(body.startsWith('---\n')).toBe(true);
expect(body).toMatch(/^---\ntrigger: always_on\n---\n/);
// Streamlined English playbook inlined (not the minimal
// shared body) because Qoder's client does not consume MCP
// `initialize.instructions`. This version is conditional
// ("For indexed code"), has explicit triage ("When to use /
// When not to"), and softened trust guidance. Signature
// content must be present.
expect(body).toContain('codegraph_explore');
expect(body).toMatch(/When to use.*When not to/);
expect(body).toMatch(/Staleness/);
// Shell fallback for when MCP isn't reachable (with Qoder caveat).
expect(body).toMatch(/codegraph explore/);

// Rule file is written BEFORE the MCP entry — the Qoder-unique,
// project-scoped artefact should lead the output.
const ruleIdx = result.files.findIndex((f) => f === ruleEntry);
const mcpIdx = result.files.findIndex((f) => f === mcpEntry);
expect(ruleIdx).toBeLessThan(mcpIdx);

// MCP entry landed at the shared user-scope location with the
// expected shape (no --path, no ${workspaceFolder} — same
// guarantees as the global install).
const cfg = JSON.parse(fs.readFileSync(mcpEntry!.path, 'utf-8'));
expect(cfg.mcpServers.codegraph).toBeDefined();
expect(cfg.mcpServers.codegraph.args).toEqual(['serve', '--mcp']);
expect(cfg.mcpServers.codegraph.args).not.toContain('--path');

// Notes surface the (deliberate) shared-vs-project asymmetry.
expect(result.notes?.join(' ')).toMatch(/shared user-scope/i);

// Idempotent: second install is fully unchanged.
const second = qoder.install('local', { autoAllow: true });
for (const f of second.files) expect(f.action).toBe('unchanged');

// Uninstall local unlinks the rule file and leaves the shared MCP
// entry alone — another project on this machine may still rely on
// it; removal of the MCP entry is gated behind
// `--location=global` uninstall.
qoder.uninstall('local');
expect(fs.existsSync(ruleEntry!.path)).toBe(false);
const cfgAfter = JSON.parse(fs.readFileSync(mcpEntry!.path, 'utf-8'));
expect(cfgAfter.mcpServers?.codegraph).toBeDefined();
});

it('qoder: --location=local self-heals a legacy AGENTS.md CodeGraph fence left by pre-.qoder/rules builds', () => {
const qoder = getTarget('qoder')!;
const agentsMd = path.join(fs.realpathSync(tmpCwd), 'AGENTS.md');
// Simulate a repo upgraded from an earlier build that wrote the
// marker-fenced block into AGENTS.md.
fs.writeFileSync(
agentsMd,
'User content above.\n\n<!-- CODEGRAPH_START -->\nlegacy body\n<!-- CODEGRAPH_END -->\n\nUser content below.\n',
);

const result = qoder.install('local', { autoAllow: true });
// Legacy fence is stripped; user content is preserved.
const after = fs.readFileSync(agentsMd, 'utf-8');
expect(after).not.toContain('<!-- CODEGRAPH_START -->');
expect(after).not.toContain('<!-- CODEGRAPH_END -->');
expect(after).toContain('User content above');
expect(after).toContain('User content below');
// The cleanup is surfaced as a `removed` file action.
const cleanup = result.files.find((f) => f.path === agentsMd);
expect(cleanup?.action).toBe('removed');
});

it('qoder: printConfig(global) shows the mcp.json path and JSON snippet without touching the filesystem', () => {
const qoder = getTarget('qoder')!;
const before = fs.existsSync(qoder.describePaths('global')[0]!);
const out = qoder.printConfig('global');
// Mentions the shared mcp.json path.
expect(out).toContain('mcp.json');
// Contains a valid JSON snippet with the codegraph entry.
expect(out).toContain('codegraph');
expect(out).toContain('serve');
expect(out).toContain('--mcp');
// Must NOT have created any file.
const after = fs.existsSync(qoder.describePaths('global')[0]!);
expect(after).toBe(before);
});

it('qoder: printConfig(local) shows both the rule file and MCP snippet', () => {
const qoder = getTarget('qoder')!;
const out = qoder.printConfig('local');
// Rule file section.
expect(out).toContain('.qoder/rules/codegraph.md');
expect(out).toContain('trigger: always_on');
expect(out).toContain('codegraph_explore');
// MCP section.
expect(out).toContain('mcp.json');
expect(out).toContain('serve');
// Rule file content precedes the MCP snippet (same order as install).
const ruleIdx = out.indexOf('.qoder/rules/codegraph.md');
const mcpIdx = out.indexOf('mcp.json');
expect(ruleIdx).toBeLessThan(mcpIdx);
expect(ruleIdx).toBeGreaterThan(-1);
});

it('qoder: detect() reflects alreadyConfigured before and after install/uninstall', () => {
const qoder = getTarget('qoder')!;
// Before any install: nothing configured.
expect(qoder.detect('global').alreadyConfigured).toBe(false);
expect(qoder.detect('local').alreadyConfigured).toBe(false);

// After global install: global detect flips, local detect stays false
// (local configuredness is gated on the project rule file, not the
// shared MCP entry).
qoder.install('global', { autoAllow: true });
expect(qoder.detect('global').alreadyConfigured).toBe(true);
expect(qoder.detect('local').alreadyConfigured).toBe(false);

// After local install: both detect true.
qoder.install('local', { autoAllow: true });
expect(qoder.detect('global').alreadyConfigured).toBe(true);
expect(qoder.detect('local').alreadyConfigured).toBe(true);

// After local uninstall: local flips back to false; global stays true
// (shared MCP entry is intentionally left intact by local uninstall).
qoder.uninstall('local');
expect(qoder.detect('local').alreadyConfigured).toBe(false);
expect(qoder.detect('global').alreadyConfigured).toBe(true);

// After global uninstall: global also flips back to false.
qoder.uninstall('global');
expect(qoder.detect('global').alreadyConfigured).toBe(false);
expect(qoder.detect('local').alreadyConfigured).toBe(false);
});

it('hermes: install adds codegraph MCP server and cli toolset, preserving existing yaml', () => {
const hermes = getTarget('hermes')!;
const config = path.join(tmpHome, '.hermes', 'config.yaml');
Expand Down
2 changes: 1 addition & 1 deletion src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2232,7 +2232,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, Gemini CLI, Antigravity IDE, Kiro, Qoder)')
.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
Loading