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
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, Kiro, and GitHub Copilot with Semantic Code Intelligence
### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Antigravity, Kiro, GitHub Copilot, and ZCode with Semantic Code Intelligence

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

Expand Down Expand Up @@ -36,6 +36,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.
[![Antigravity](https://img.shields.io/badge/Antigravity-supported-blueviolet.svg)](#supported-agents)
[![Kiro](https://img.shields.io/badge/Kiro-supported-blueviolet.svg)](#supported-agents)
[![GitHub Copilot](https://img.shields.io/badge/GitHub_Copilot-supported-blueviolet.svg)](#supported-agents)
[![ZCode](https://img.shields.io/badge/ZCode-supported-blueviolet.svg)](#supported-agents)

<br>

Expand Down Expand Up @@ -105,7 +106,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, Kiro, and GitHub Copilot (VS Code, Copilot CLI, JetBrains IDEs) — 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, GitHub Copilot (VS Code, Copilot CLI, JetBrains IDEs), and ZCode — 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 @@ -389,7 +390,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**, **GitHub Copilot** (VS Code, Copilot CLI, JetBrains IDEs)
- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**, **GitHub Copilot** (VS Code, Copilot CLI, JetBrains IDEs), **ZCode**
- 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 Down Expand Up @@ -420,7 +421,7 @@ codegraph install --print-config copilot-vscode # same, for Copilot in VS C

### 2. Restart Your Agent

Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro / VS Code, the Copilot CLI, or your JetBrains IDE for GitHub Copilot) for the MCP server to load.
Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro / VS Code, the Copilot CLI, your JetBrains IDE for GitHub Copilot, or a ZCode session) for the MCP server to load.

### 3. Initialize Projects

Expand Down Expand Up @@ -802,6 +803,7 @@ is written):
- **Antigravity IDE**
- **Kiro**
- **GitHub Copilot** — Copilot Chat in VS Code (`copilot-vscode`), the Copilot CLI (`copilot-cli`), and the Copilot plugin in JetBrains IDEs (`copilot-jetbrains`)
- **ZCode** — MCP entry is ZCode's nested `mcp.servers` shape in `~/.zcode/cli/config.json` (global) or `<repo>/.zcode/config.json` (local); entries carry no `type` key. The prompt-hook uses ZCode's process-shaped hooks (`type: "process"` + `args`) and sets `hooks.enabled: true` — ZCode runs config-file hooks only when that flag is set. Instructions go to `~/.zcode/AGENTS.md` / `<repo>/AGENTS.md`

## Supported Languages

Expand Down Expand Up @@ -904,7 +906,7 @@ MIT

<div align="center">

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

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

Expand Down
189 changes: 189 additions & 0 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import { ALL_TARGETS, getTarget, resolveTargetFlag } from '../src/installer/targ
import { uninstallTargets, refreshTargets } from '../src/installer';
import { upsertTomlTable, removeTomlTable, buildTomlTable } from '../src/installer/targets/toml';
import { cleanupLegacyHooks, writePromptHookEntry, removePromptHookEntry } from '../src/installer/targets/claude';
import {
writePromptHookEntry as zcodeWritePromptHookEntry,
removePromptHookEntry as zcodeRemovePromptHookEntry,
} from '../src/installer/targets/zcode';

function mkTmpDir(label: string): string {
return fs.mkdtempSync(path.join(os.tmpdir(), `cg-targets-${label}-`));
Expand Down Expand Up @@ -154,6 +158,13 @@ describe('Installer targets — contract', () => {
delete seed.mcpServers;
seed.servers = { other: { command: 'x' } };
}
// ZCode's config.json nests servers under `mcp.servers`
// (and carries sibling top-level keys like `plugins`).
if (target.id === 'zcode') {
delete seed.mcpServers;
seed.mcp = { servers: { other: { command: 'x' } } };
seed.plugins = { enabledPlugins: { 'some@plugin': true } };
}
fs.writeFileSync(jsonPath, JSON.stringify(seed, null, 2) + '\n');

target.install(location, { autoAllow: true });
Expand All @@ -165,6 +176,13 @@ describe('Installer targets — contract', () => {
expect(after.mcp.servers.codegraph.codemode).toBe(false);
expect(after.mcp.servers.codegraph.disabled).toBe(false);
expect(after.mcp.codegraph).toBeUndefined();
} else if (target.id === 'zcode') {
expect(after.mcp.servers.other).toBeDefined();
expect(after.mcp.servers.codegraph).toBeDefined();
// A Claude-style `type` key must not creep back in, and
// sibling top-level keys survive.
expect(after.mcp.servers.codegraph.type).toBeUndefined();
expect(after.plugins.enabledPlugins['some@plugin']).toBe(true);
} else if (target.id === 'copilot-vscode' || target.id === 'copilot-jetbrains') {
expect(after.servers.other).toBeDefined();
expect(after.servers.codegraph).toBeDefined();
Expand Down Expand Up @@ -1350,6 +1368,7 @@ describe('Installer targets — registry', () => {
expect(getTarget('copilot-vscode')?.id).toBe('copilot-vscode');
expect(getTarget('copilot-cli')?.id).toBe('copilot-cli');
expect(getTarget('copilot-jetbrains')?.id).toBe('copilot-jetbrains');
expect(getTarget('zcode')?.id).toBe('zcode');
expect(getTarget('not-a-real-target')).toBeUndefined();
});

Expand Down Expand Up @@ -2878,3 +2897,173 @@ describe('Installer targets — Codex CODEX_HOME override (#1627)', () => {
expect(fs.existsSync(path.join(custom, 'config.toml'))).toBe(false);
});
});

describe('Installer targets — ZCode', () => {
let tmpHome: string;
let tmpCwd: string;
let origCwd: string;
let homeRestore: { restore: () => void };

beforeEach(() => {
tmpHome = mkTmpDir('zcode-home');
tmpCwd = mkTmpDir('zcode-cwd');
origCwd = process.cwd();
process.chdir(tmpCwd);
homeRestore = setHome(tmpHome);
});

afterEach(() => {
homeRestore.restore();
process.chdir(origCwd);
fs.rmSync(tmpHome, { recursive: true, force: true });
fs.rmSync(tmpCwd, { recursive: true, force: true });
});

// ZCode hook entries are process-shaped (`{ type: 'process', command,
// args, timeoutMs }`), not Claude's shell-string form, and the command
// is platform-aware for the same #1466 reason (Windows must spawn
// `codegraph.cmd` — a bare `codegraph` doesn't resolve PATHEXT).
const ZCODE_HOOK_CMD = process.platform === 'win32' ? 'codegraph.cmd' : 'codegraph';
const globalConfig = () => path.join(tmpHome, '.zcode', 'cli', 'config.json');
const globalAgents = () => path.join(tmpHome, '.zcode', 'AGENTS.md');
const hookCommands = (c: any): string[] =>
(c.hooks?.UserPromptSubmit ?? []).flatMap((g: any) => (g.hooks ?? []).map((h: any) => h.command));

it('install with promptHook:true writes nested mcp.servers AND an enabled process-shaped hook', () => {
const zcode = getTarget('zcode')!;
const result = zcode.install('global', { autoAllow: true, promptHook: true });
expect(result.files.map((f) => f.path)).toEqual(
expect.arrayContaining([globalConfig(), globalAgents()]),
);

const c = JSON.parse(fs.readFileSync(globalConfig(), 'utf-8'));
// Nested shape — a top-level mcpServers key would silently no-op.
expect(c.mcpServers).toBeUndefined();
expect(c.mcp.servers.codegraph).toEqual({
command: 'codegraph',
args: ['serve', '--mcp'],
});
expect(c.mcp.servers.codegraph.type).toBeUndefined();

// Config-file hooks are inert unless hooks.enabled is true.
expect(c.hooks.enabled).toBe(true);
expect(c.hooks.UserPromptSubmit).toHaveLength(1);
expect(c.hooks.UserPromptSubmit[0].hooks[0]).toEqual({
type: 'process',
command: ZCODE_HOOK_CMD,
args: ['prompt-hook'],
timeoutMs: 30000,
});

// Instructions land in the user-scope AGENTS.md.
const md = fs.readFileSync(globalAgents(), 'utf-8');
expect(md).toContain('<!-- CODEGRAPH_START -->');
expect(md).toContain('codegraph explore');
});

it('install without promptHook does NOT create a hooks block', () => {
getTarget('zcode')!.install('global', { autoAllow: true });
const c = JSON.parse(fs.readFileSync(globalConfig(), 'utf-8'));
expect(c.hooks).toBeUndefined();
});

it('re-install with promptHook:true is byte-identical (no duplicate hook)', () => {
const zcode = getTarget('zcode')!;
zcode.install('global', { autoAllow: true, promptHook: true });
const first = fs.readFileSync(globalConfig(), 'utf-8');
const second = zcode.install('global', { autoAllow: true, promptHook: true });
expect(second.files.every((f) => f.action === 'unchanged')).toBe(true);
expect(fs.readFileSync(globalConfig(), 'utf-8')).toBe(first);
expect(hookCommands(JSON.parse(first)).filter((x) => x === ZCODE_HOOK_CMD)).toHaveLength(1);
});

it('promptHook:false strips the hook (opt-out round-trips); enabled stays as-is', () => {
const zcode = getTarget('zcode')!;
zcode.install('global', { autoAllow: true, promptHook: true });
zcode.install('global', { autoAllow: true, promptHook: false });
const c = JSON.parse(fs.readFileSync(globalConfig(), 'utf-8'));
expect(hookCommands(c)).not.toContain(ZCODE_HOOK_CMD);
expect(c.hooks.UserPromptSubmit).toBeUndefined();
// `enabled` provenance is unknowable (the user may rely on it for
// other config hooks), so removal deliberately leaves it behind.
expect(c.hooks.enabled).toBe(true);
});

it('normalizes a hand-copied Claude-style entry (drops `type`) and stays idempotent', () => {
fs.mkdirSync(path.dirname(globalConfig()), { recursive: true });
fs.writeFileSync(globalConfig(), JSON.stringify({
mcp: { servers: { codegraph: { type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] } } },
}, null, 2) + '\n');

const first = getTarget('zcode')!.install('global', { autoAllow: true });
expect(first.files[0].action).toBe('updated');
const c = JSON.parse(fs.readFileSync(globalConfig(), 'utf-8'));
expect(c.mcp.servers.codegraph).toEqual({ command: 'codegraph', args: ['serve', '--mcp'] });

const second = getTarget('zcode')!.install('global', { autoAllow: true });
expect(second.files[0].action).toBe('unchanged');
});

it('zcodeWritePromptHookEntry preserves a sibling process hook; uninstall keeps it', () => {
fs.mkdirSync(path.dirname(globalConfig()), { recursive: true });
fs.writeFileSync(globalConfig(), JSON.stringify({
hooks: {
UserPromptSubmit: [
{ hooks: [{ type: 'process', command: 'my-own-hook', args: ['run'] }] },
],
},
}, null, 2) + '\n');

expect(zcodeWritePromptHookEntry('global').action).toBe('updated');
let c = JSON.parse(fs.readFileSync(globalConfig(), 'utf-8'));
expect(hookCommands(c)).toEqual(['my-own-hook', ZCODE_HOOK_CMD]);
expect(c.hooks.enabled).toBe(true);

getTarget('zcode')!.uninstall('global');
c = JSON.parse(fs.readFileSync(globalConfig(), 'utf-8'));
expect(hookCommands(c)).toEqual(['my-own-hook']);
});

it('zcodeRemovePromptHookEntry accepts the other platform\'s command spelling', () => {
fs.mkdirSync(path.dirname(globalConfig()), { recursive: true });
const otherCmd = process.platform === 'win32' ? 'codegraph' : 'codegraph.cmd';
fs.writeFileSync(globalConfig(), JSON.stringify({
hooks: {
enabled: true,
UserPromptSubmit: [{ hooks: [{ type: 'process', command: otherCmd, args: ['prompt-hook'], timeoutMs: 30000 }] }],
},
}, null, 2) + '\n');

expect(zcodeRemovePromptHookEntry('global').action).toBe('removed');
const c = JSON.parse(fs.readFileSync(globalConfig(), 'utf-8'));
expect(c.hooks.UserPromptSubmit).toBeUndefined();
expect(c.hooks.enabled).toBe(true);
});

it('uninstall strips the AGENTS.md block but keeps user content; local install writes workspace files only', () => {
fs.mkdirSync(tmpCwd, { recursive: true });
fs.writeFileSync(path.join(tmpCwd, 'AGENTS.md'), '# My project rules\n\nkeep me\n');
getTarget('zcode')!.install('local', { autoAllow: true, promptHook: true });

// Workspace scope: ./.zcode/config.json + ./AGENTS.md.
expect(fs.existsSync(path.join(tmpCwd, '.zcode', 'config.json'))).toBe(true);
expect(fs.existsSync(path.join(tmpCwd, 'AGENTS.md'))).toBe(true);
expect(fs.existsSync(globalConfig())).toBe(false);

getTarget('zcode')!.uninstall('local');
const md = fs.readFileSync(path.join(tmpCwd, 'AGENTS.md'), 'utf-8');
expect(md).not.toContain('CODEGRAPH');
expect(md).toContain('keep me');
const c = JSON.parse(fs.readFileSync(path.join(tmpCwd, '.zcode', 'config.json'), 'utf-8'));
expect(c.mcp).toBeUndefined();
});

it('printConfig shows the nested mcp.servers shape and writes nothing', () => {
const out = getTarget('zcode')!.printConfig('global');
expect(out).toContain('mcp');
expect(out).toContain('"codegraph"');
const parsed = JSON.parse(out.slice(out.indexOf('{')));
expect(parsed.mcp.servers.codegraph).toEqual({ command: 'codegraph', args: ['serve', '--mcp'] });
expect(fs.existsSync(globalConfig())).toBe(false);
});
});
4 changes: 2 additions & 2 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2568,7 +2568,7 @@ program
*/
program
.command('install')
.description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, GitHub Copilot)')
.description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, GitHub Copilot, ZCode)')
.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 @@ -2682,7 +2682,7 @@ program
*/
program
.command('uninstall')
.description('Remove codegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, GitHub Copilot)')
.description('Remove codegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, GitHub Copilot, ZCode)')
.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
12 changes: 6 additions & 6 deletions src/installer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,21 +205,21 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
}
}

// Step 4¾: front-load prompt hook (Claude Code only). A UserPromptSubmit hook
// Step 4¾: front-load prompt hook (Claude Code and ZCode). A UserPromptSubmit hook
// that runs `codegraph prompt-hook` — it injects codegraph_explore context on
// structural ("how / where / trace / impact") prompts so the agent reliably
// reaches for the graph instead of grepping. Opt-in, default-yes. Only Claude
// Code has UserPromptSubmit, so it's offered only when Claude is a target;
// other targets ignore the option. `undefined` (no Claude / not asked) leaves
// any existing hook untouched.
// Code and ZCode have UserPromptSubmit, so it's offered only when one of them
// is a target; other targets ignore the option. `undefined` (neither targeted /
// not asked) leaves any existing hook untouched.
let promptHook: boolean | undefined;
if (targets.some((t) => t.id === 'claude')) {
if (targets.some((t) => t.id === 'claude' || t.id === 'zcode')) {
if (useDefaults) {
promptHook = true; // --yes → on
} else {
const ans = await clack.confirm({
message:
'Front-load CodeGraph on “how / where / trace” prompts? Auto-injects structural context so answers need fewer steps (adds a moment to those prompts; Claude Code only).',
'Front-load CodeGraph on “how / where / trace” prompts? Auto-injects structural context so answers need fewer steps (adds a moment to those prompts; Claude Code / ZCode only).',
initialValue: true,
});
if (clack.isCancel(ans)) {
Expand Down
2 changes: 2 additions & 0 deletions src/installer/targets/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { kiroTarget } from './kiro';
import { copilotVscodeTarget } from './copilot-vscode';
import { copilotCliTarget } from './copilot-cli';
import { copilotJetbrainsTarget } from './copilot-jetbrains';
import { zcodeTarget } from './zcode';

export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
claudeTarget,
Expand All @@ -32,6 +33,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
copilotVscodeTarget,
copilotCliTarget,
copilotJetbrainsTarget,
zcodeTarget,
]);

export function getTarget(id: string): AgentTarget | undefined {
Expand Down
2 changes: 1 addition & 1 deletion src/installer/targets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type Location = 'global' | 'local';
* lookup. New targets add a value here when they're added to the
* registry. Keep these short and lowercase.
*/
export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro' | 'copilot-vscode' | 'copilot-cli' | 'copilot-jetbrains';
export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro' | 'copilot-vscode' | 'copilot-cli' | 'copilot-jetbrains' | 'zcode';

/**
* Result of `target.detect(location)`.
Expand Down
Loading