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

- GitHub Copilot is now a supported agent: `codegraph install` can configure Copilot Chat in VS Code (`copilot-vscode`), the GitHub Copilot CLI (`copilot-cli`), and the Copilot plugin in JetBrains IDEs (`copilot-jetbrains`). Installed Copilot surfaces are auto-detected like every other agent, existing MCP server entries in their config files are preserved, and `codegraph uninstall` reverses the setup cleanly. Restart VS Code or your JetBrains IDE after installing so Copilot picks up the server.

- The DeepSeek Harness (`dsh`) is now a supported agent: `codegraph install` wires the CodeGraph MCP server into `~/.dsh/cordis.patch.yml`, which applies to every dsh profile — the web GUI, headless one-shot runs, and custom profiles — and the harness hot-swaps the change in live via HMR. If an earlier install had put a codegraph entry into a single profile's patch file, it is migrated up to the shared layer automatically so the same server never mounts twice, and `codegraph uninstall` reverses the setup cleanly.

### Fixes

- C, C++, Objective-C and Rust unions are now indexed as first-class `union` nodes. A `union` declaration previously produced no symbol at all, so it never appeared in search or `codegraph_explore`, and anything attached to it disappeared with it — in Rust, every `impl SomeTrait for MyUnion` lost its edge, the methods from that impl were left pointing at a type the graph did not contain, and asking which types implement a trait quietly skipped the union ones. A union-shaped dispatch table in C now resolves its function pointers like a struct-shaped one. A `typedef union { … } Name;` in C keeps the typedef's name and is no longer mistaken for a plain type alias. Thanks @ctype-lab. Re-index after upgrading to pick up unions in existing projects. (#1515)
Expand Down
13 changes: 8 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 DeepSeek Harness 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)
[![DeepSeek Harness](https://img.shields.io/badge/DeepSeek_Harness-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 DeepSeek Harness (dsh) — 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 @@ -376,7 +377,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), **DeepSeek Harness** (dsh)
- 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 @@ -391,6 +392,7 @@ codegraph install --yes # auto-detect agents, insta
codegraph install --target=cursor,claude --yes # explicit target list
codegraph install --target=auto --location=local # detected agents, project-local
codegraph install --target=copilot-vscode,copilot-cli,copilot-jetbrains --yes # GitHub Copilot everywhere
codegraph install --target=dsh --yes # DeepSeek Harness (all dsh profiles)
codegraph install --print-config codex # print snippet, no file writes
codegraph install --print-config copilot-vscode # same, for Copilot in VS Code
```
Expand All @@ -405,7 +407,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, or your JetBrains IDE for GitHub Copilot / DeepSeek Harness) for the MCP server to load.

### 3. Initialize Projects

Expand Down Expand Up @@ -764,6 +766,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`)
- **DeepSeek Harness** (`dsh`) — writes the MCP server into `~/.dsh/cordis.patch.yml`, which applies to every dsh profile. DSH doesn't send the workspace root to the server, so query a project by passing `projectPath` to `codegraph_explore` (or pin `cwd`/`--path` in the entry to a specific project).

## Supported Languages

Expand Down Expand Up @@ -862,7 +865,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 DeepSeek Harness**

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

Expand Down
236 changes: 236 additions & 0 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ function setHome(dir: string): { restore: () => void } {
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
HERMES_HOME: process.env.HERMES_HOME,
COPILOT_HOME: process.env.COPILOT_HOME,
DSH_HOME: process.env.DSH_HOME,
};
process.env.HOME = dir;
process.env.USERPROFILE = dir;
process.env.APPDATA = path.join(dir, '.config');
process.env.XDG_CONFIG_HOME = path.join(dir, '.config');
delete process.env.HERMES_HOME;
delete process.env.COPILOT_HOME;
delete process.env.DSH_HOME;
return {
restore() {
if (prev.HOME === undefined) delete process.env.HOME; else process.env.HOME = prev.HOME;
Expand All @@ -55,6 +57,7 @@ function setHome(dir: string): { restore: () => void } {
if (prev.XDG_CONFIG_HOME === undefined) delete process.env.XDG_CONFIG_HOME; else process.env.XDG_CONFIG_HOME = prev.XDG_CONFIG_HOME;
if (prev.HERMES_HOME === undefined) delete process.env.HERMES_HOME; else process.env.HERMES_HOME = prev.HERMES_HOME;
if (prev.COPILOT_HOME === undefined) delete process.env.COPILOT_HOME; else process.env.COPILOT_HOME = prev.COPILOT_HOME;
if (prev.DSH_HOME === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = prev.DSH_HOME;
},
};
}
Expand Down Expand Up @@ -1269,6 +1272,238 @@ describe('Installer targets — partial-state idempotency', () => {
const stopCmds = (s.hooks?.Stop ?? []).flatMap((g: any) => (g.hooks ?? []).map((h: any) => h.command));
expect(stopCmds).toContain('codegraph sync-if-dirty');
});

it('dsh: install writes the home-level MCP entry, detects it, and is idempotent', () => {
const dsh = getTarget('dsh')!;
// DeepSeek Harness keeps all config under $DSH_HOME — no project-local.
expect(dsh.supportsLocation('local')).toBe(false);
const file = path.join(tmpHome, '.dsh', 'cordis.patch.yml');
expect(dsh.detect('global').alreadyConfigured).toBe(false);

const first = dsh.install('global', { autoAllow: true });
const mcp = first.files.find((f) => f.path === file)!;
expect(mcp.action).toBe('created');
expect(fs.existsSync(file)).toBe(true);
const body = fs.readFileSync(file, 'utf-8');
expect(body).toContain('- insert:');
expect(body).toContain('- id: mcp-codegraph');
expect(body).toContain("name: '@deepseek-ai/dsh-mcp-client'");
expect(body).toContain('serverName: codegraph');
expect(body).toContain('transport: stdio');
expect(body).toContain('command: codegraph');
expect(body).toContain('- serve');
expect(body).toContain('- --mcp');
expect(body).toContain('failOnStartupError: false');
expect(dsh.detect('global').alreadyConfigured).toBe(true);

const second = dsh.install('global', { autoAllow: true });
const mcp2 = second.files.find((f) => f.path === file)!;
expect(mcp2.action).toBe('unchanged');
expect(fs.readFileSync(file, 'utf-8')).toBe(body);
});

it('dsh: install preserves sibling entries in an existing home patch', () => {
const dsh = getTarget('dsh')!;
const dir = path.join(tmpHome, '.dsh');
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, 'cordis.patch.yml');
fs.writeFileSync(file, [
'# my patch layer',
'- insert:',
' - id: my-plugin',
' name: "./plugins/my-plugin.mjs"',
' config:',
' enabled: true',
'',
].join('\n'));

const result = dsh.install('global', { autoAllow: true });
expect(result.files.find((f) => f.path === file)?.action).toBe('updated');
const body = fs.readFileSync(file, 'utf-8');
expect(body).toContain('# my patch layer');
expect(body).toContain('- id: my-plugin');
expect(body).toContain('- id: mcp-codegraph');

const second = dsh.install('global', { autoAllow: true });
expect(second.files.find((f) => f.path === file)?.action).toBe('unchanged');
});

it('dsh: install rewrites a user-edited home entry to the canonical form, keeping the rest', () => {
const dsh = getTarget('dsh')!;
const dir = path.join(tmpHome, '.dsh');
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, 'cordis.patch.yml');
fs.writeFileSync(file, [
'- insert:',
' - id: mcp-codegraph',
" name: '@deepseek-ai/dsh-mcp-client'",
' config:',
' serverName: codegraph',
' transport: stdio',
" command: 'C:\\pinned\\node.exe'",
' args:',
" - 'C:\\pinned\\codegraph'",
' - serve',
' - --mcp',
" cwd: 'D:\\proj'",
' failOnStartupError: false',
' - id: other',
" name: './plugins/other.mjs'",
'',
].join('\n'));

const result = dsh.install('global', { autoAllow: true });
expect(result.files.find((f) => f.path === file)?.action).toBe('updated');
const body = fs.readFileSync(file, 'utf-8');
// Canonical command, pinned cwd dropped, sibling preserved.
expect(body).toContain(' command: codegraph');
expect(body).not.toContain('pinned');
expect(body).not.toContain('cwd:');
expect(body).toContain('- id: other');
});

it('dsh: install self-heals a per-profile codegraph entry into the home layer', () => {
const dsh = getTarget('dsh')!;
const profileDir = path.join(tmpHome, '.dsh', 'profiles', 'web');
fs.mkdirSync(profileDir, { recursive: true });
const profileFile = path.join(profileDir, 'cordis.patch.yml');
fs.writeFileSync(profileFile, [
'- insert:',
' - id: mcp-codegraph',
" name: '@deepseek-ai/dsh-mcp-client'",
' config:',
' serverName: codegraph',
' transport: stdio',
" command: 'C:\\manual\\node.exe'",
' args:',
" - 'C:\\manual\\codegraph'",
' - serve',
' - --mcp',
' failOnStartupError: false',
' - id: graphify',
" name: './plugins/graphify.mjs'",
' config:',
' enabled: true',
'',
].join('\n'));

// A per-profile entry alone counts as configured...
expect(dsh.detect('global').alreadyConfigured).toBe(true);

const result = dsh.install('global', { autoAllow: true });
// ...and install migrates it: home layer created, profile entry swept,
// sibling profile plugin kept.
expect(result.files.some((f) => f.path === path.join(tmpHome, '.dsh', 'cordis.patch.yml'))).toBe(true);
expect(result.files.find((f) => f.path === profileFile)?.action).toBe('removed');
const profileBody = fs.readFileSync(profileFile, 'utf-8');
expect(profileBody).not.toContain('mcp-codegraph');
expect(profileBody).not.toContain('manual');
expect(profileBody).toContain('graphify');
});

it('dsh: uninstall removes only the codegraph entry, preserving siblings', () => {
const dsh = getTarget('dsh')!;
const dir = path.join(tmpHome, '.dsh');
fs.mkdirSync(dir, { recursive: true });
const file = path.join(dir, 'cordis.patch.yml');
fs.writeFileSync(file, [
'# my patch layer',
'- insert:',
' - id: mcp-codegraph',
" name: '@deepseek-ai/dsh-mcp-client'",
' config:',
' serverName: codegraph',
' transport: stdio',
' command: codegraph',
' args:',
' - serve',
' - --mcp',
' failOnStartupError: false',
' - id: graphify',
" name: './plugins/graphify.mjs'",
' config:',
' enabled: true',
'',
].join('\n'));

const result = dsh.uninstall('global');
expect(result.files.find((f) => f.path === file)?.action).toBe('removed');
const body = fs.readFileSync(file, 'utf-8');
expect(body).not.toContain('mcp-codegraph');
expect(body).not.toContain('serverName: codegraph');
expect(body).toContain('# my patch layer');
expect(body).toContain('graphify');
expect(dsh.detect('global').alreadyConfigured).toBe(false);
});

it('dsh: uninstall deletes the patch file when it only held codegraph', () => {
const dsh = getTarget('dsh')!;
const file = path.join(tmpHome, '.dsh', 'cordis.patch.yml');
dsh.install('global', { autoAllow: true });
expect(fs.existsSync(file)).toBe(true);

dsh.uninstall('global');
expect(fs.existsSync(file)).toBe(false);
expect(dsh.detect('global').alreadyConfigured).toBe(false);
});

it('dsh: uninstall fully removes a profile-only install (detect/uninstall symmetric)', () => {
const dsh = getTarget('dsh')!;
const profileDir = path.join(tmpHome, '.dsh', 'profiles', 'headless');
fs.mkdirSync(profileDir, { recursive: true });
const profileFile = path.join(profileDir, 'cordis.patch.yml');
fs.writeFileSync(profileFile, [
'- insert:',
' - id: mcp-codegraph',
" name: '@deepseek-ai/dsh-mcp-client'",
' config:',
' serverName: codegraph',
' transport: stdio',
' command: codegraph',
' args:',
' - serve',
' - --mcp',
' failOnStartupError: false',
'',
].join('\n'));

// A profile-only entry is "configured"...
expect(dsh.detect('global').alreadyConfigured).toBe(true);

const result = dsh.uninstall('global');
// ...and uninstall removes it (home file absent → not-found, profile swept).
expect(result.files.find((f) => f.path === path.join(tmpHome, '.dsh', 'cordis.patch.yml'))?.action).toBe('not-found');
expect(result.files.find((f) => f.path === profileFile)?.action).toBe('removed');
expect(fs.existsSync(profileFile)).toBe(false);
expect(dsh.detect('global').alreadyConfigured).toBe(false);
});

it('dsh: honors DSH_HOME over ~/.dsh', () => {
const dsh = getTarget('dsh')!;
const custom = path.join(tmpHome, 'custom-dsh');
process.env.DSH_HOME = custom;
try {
const result = dsh.install('global', { autoAllow: true });
const file = path.join(custom, 'cordis.patch.yml');
expect(result.files.some((f) => f.path === file)).toBe(true);
expect(fs.existsSync(file)).toBe(true);
expect(dsh.detect('global').configPath).toBe(file);
} finally {
delete process.env.DSH_HOME;
}
});

it('dsh: printConfig prints a parseable YAML insert block without writing anything', () => {
const dsh = getTarget('dsh')!;
const before = listAllFiles(tmpHome).concat(listAllFiles(tmpCwd));
const out = dsh.printConfig('global');
expect(out.length).toBeGreaterThan(0);
expect(out).toContain('- insert:');
expect(out).toContain('- id: mcp-codegraph');
expect(out).toContain('serverName: codegraph');
const after = listAllFiles(tmpHome).concat(listAllFiles(tmpCwd));
expect(after.sort()).toEqual(before.sort());
});
});

describe('Installer targets — registry', () => {
Expand All @@ -1284,6 +1519,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('dsh')?.id).toBe('dsh');
expect(getTarget('not-a-real-target')).toBeUndefined();
});

Expand Down
Loading