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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@ assets/generate-waitlist.py

# Local scratch tests (never commit)
__tests__/zz-scratch*

.history/
.qoder/
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]

### New Features

- `codegraph install` now detects and configures **Qoder**. `--location=global` writes the MCP entry to Qoder's shared `mcp.json` under `~/Library/Application Support/Qoder/SharedClientCache/` on macOS (macOS verified; the corresponding Electron config dir is used on Linux and Windows by convention), sibling MCP servers are left intact, and re-runs are idempotent. `--location=local` is a superset: it writes the same shared MCP entry **plus** a project-scoped rule file at `.qoder/rules/codegraph.md` with `trigger: always_on` frontmatter — Qoder auto-loads `.qoder/rules/*.md` as Always Apply rules (higher precedence than `AGENTS.md`), and since Qoder's client consumes the MCP `initialize.instructions` field weakly (tools list but the agent does not proactively reach for `codegraph_explore`), the rule file is the surface that actually steers the agent. The rule body is a Qoder-specific conditional playbook (not the minimal shared block other targets use) with explicit triage, staleness handling, and a PATH-aware shell fallback. Upgrading from an earlier build that wrote a marker-fenced section into `AGENTS.md` is handled automatically: both install and uninstall strip any legacy CodeGraph fence from `AGENTS.md` (self-heal). Two Qoder-specific MCP quirks are handled at install time: (a) Qoder does not expand `${workspaceFolder}` in MCP args in older builds, so codegraph is installed without a `--path` argument and instead relies on Qoder's MCP `roots/list` handshake to discover the workspace — if your Qoder build does not send roots, open the project from Qoder itself (not via `cd` in a terminal) so the client emits it; (b) Qoder launches from Dock/launchd on macOS with a stripped PATH, so the `command` is resolved to the absolute `codegraph` path from your login shell — the same trick already used for Antigravity IDE. Uninstall on `local` unlinks the rule file and strips any legacy `AGENTS.md` fence but leaves the shared MCP entry intact (another project may depend on it); `--location=global` uninstall removes the MCP entry itself.

### Fixes

- Callers and impact analysis no longer silently under-count a function that calls the same callee many times. When one caller contained several call sites to the same callee and an internal resolution batch boundary happened to split them, cleanup after the first batch removed the later sites' pending rows before they were ever attempted — their edges were never created, deterministically, and which edges went missing shifted with unrelated changes to the project's total reference count. Post-pass cleanup now targets the exact database row each processed reference came from. Found while validating the operator-call fix on nlohmann/json, where `write_cbor`'s 11 calls to `to_char_type` indexed as 10. (#1269)
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,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

**Surgical context · fewer tool calls · faster answers · 100% local**

Expand All @@ -30,6 +30,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 @@ -99,7 +100,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 @@ -412,7 +413,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 @@ -439,7 +440,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 @@ -770,6 +771,7 @@ is written):
- **Gemini CLI**
- **Antigravity IDE**
- **Kiro**
- **Qoder**

## Supported Languages

Expand Down Expand Up @@ -878,7 +880,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 @@ -2187,7 +2187,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