diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d8ab6f79..de56c298c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index f8bb60bbc..75a260c10 100644 --- a/README.md +++ b/README.md @@ -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** @@ -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)
@@ -105,7 +106,7 @@ In a **new terminal**, run the installer to connect CodeGraph to the agents you codegraph install ``` -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.) +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.) ### 3. Initialize each project @@ -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`. @@ -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 ``` @@ -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 @@ -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 @@ -862,7 +865,7 @@ MIT
-**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) diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 0d185a9d8..5253853f7 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -40,6 +40,7 @@ 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; @@ -47,6 +48,7 @@ function setHome(dir: string): { restore: () => void } { 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; @@ -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; }, }; } @@ -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', () => { @@ -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(); }); diff --git a/site/src/content/docs/getting-started/installation.md b/site/src/content/docs/getting-started/installation.md index 4f9b90986..6338011a7 100644 --- a/site/src/content/docs/getting-started/installation.md +++ b/site/src/content/docs/getting-started/installation.md @@ -11,7 +11,7 @@ npx @colbymchenry/codegraph The installer will: -- Ask which agent(s) to configure — auto-detecting installed ones from **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, and **Kiro**. +- Ask which agent(s) to configure — auto-detecting installed ones from **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**, and **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`). Cursor and Kiro get the MCP config only. Removed cleanly by `codegraph uninstall`. @@ -38,7 +38,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 / DeepSeek Harness) for the MCP server to load. ## 3. Initialize projects diff --git a/site/src/content/docs/getting-started/introduction.md b/site/src/content/docs/getting-started/introduction.md index 44d6546c1..180436725 100644 --- a/site/src/content/docs/getting-started/introduction.md +++ b/site/src/content/docs/getting-started/introduction.md @@ -5,7 +5,7 @@ description: What CodeGraph is, and why it makes AI coding agents faster and mor CodeGraph is a **local-first code-intelligence tool**. It parses your codebase with [tree-sitter](https://tree-sitter.github.io/), stores every symbol, edge, and file in a local SQLite database, and exposes the result as a queryable **knowledge graph** — over the [Model Context Protocol (MCP)](/codegraph/reference/mcp-server/), a CLI, and a TypeScript library. -It exists to make AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro — **answer structural questions without scanning files**. Instead of fanning out across `grep`, `glob`, and `Read` to reconstruct how code fits together, an agent queries a pre-built index and gets the answer in a handful of calls. +It exists to make AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and DeepSeek Harness — **answer structural questions without scanning files**. Instead of fanning out across `grep`, `glob`, and `Read` to reconstruct how code fits together, an agent queries a pre-built index and gets the answer in a handful of calls. ## Why it matters diff --git a/site/src/content/docs/getting-started/quickstart.md b/site/src/content/docs/getting-started/quickstart.md index e1ea543f0..1d2774976 100644 --- a/site/src/content/docs/getting-started/quickstart.md +++ b/site/src/content/docs/getting-started/quickstart.md @@ -25,7 +25,7 @@ Already have Node? `npm i -g @colbymchenry/codegraph` works on any version. Code codegraph install ``` -Auto-detects and configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro — wiring the CodeGraph MCP server into each. This step connects your agents only; it does **not** index any code. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs the installer in one go.) +Auto-detects and configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and DeepSeek Harness (dsh) — wiring the CodeGraph MCP server into each. This step connects your agents only; it does **not** index any code. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs the installer in one go.) ## 3. Initialize each project diff --git a/site/src/content/docs/guides/indexing.md b/site/src/content/docs/guides/indexing.md index 1518c3d0a..a492c2b00 100644 --- a/site/src/content/docs/guides/indexing.md +++ b/site/src/content/docs/guides/indexing.md @@ -24,7 +24,7 @@ codegraph sync # incremental — only changed files ## Stay fresh automatically -**You don't need to run `codegraph sync` by hand during an agent session.** When your agent (Claude Code, Cursor, Codex, opencode, Hermes, Gemini, Antigravity, Kiro) launches `codegraph serve --mcp`, three layers cooperate to keep the index in step with your code — and to never give the agent a quiet wrong answer in the small window between an edit and the next sync. +**You don't need to run `codegraph sync` by hand during an agent session.** When your agent (Claude Code, Cursor, Codex, opencode, Hermes, Gemini, Antigravity, Kiro, DeepSeek Harness) launches `codegraph serve --mcp`, three layers cooperate to keep the index in step with your code — and to never give the agent a quiet wrong answer in the small window between an edit and the next sync. ### 1. File watcher with debounced auto-sync (always on) diff --git a/site/src/content/docs/reference/integrations.md b/site/src/content/docs/reference/integrations.md index 1b4b87c89..5851a7ed8 100644 --- a/site/src/content/docs/reference/integrations.md +++ b/site/src/content/docs/reference/integrations.md @@ -15,6 +15,7 @@ The interactive installer auto-detects and configures each supported agent — w - **Gemini CLI** - **Antigravity IDE** - **Kiro** +- **DeepSeek Harness** (`dsh`) — writes the MCP server into `~/.dsh/cordis.patch.yml`, which applies to every dsh profile Run `npx @colbymchenry/codegraph` and pick your agent(s); see [Installation](/codegraph/getting-started/installation/) for the non-interactive flags. diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index a469c00b4..aa9418b27 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -94,7 +94,7 @@ const install = 'npx @colbymchenry/codegraph';

MCP server

Expose the graph to Claude Code, Cursor, Codex, opencode, Hermes, - Gemini, Antigravity, and Kiro over MCP — agents answer in a handful of calls. + Gemini, Antigravity, Kiro, and DeepSeek Harness over MCP — agents answer in a handful of calls.

diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 067cdd3e0..71747725e 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -2246,7 +2246,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, DeepSeek Harness)') .option('-t, --target ', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt') .option('-l, --location ', 'Install location: "global" or "local". Default: prompt') .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on') @@ -2346,7 +2346,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, DeepSeek Harness)') .option('-t, --target ', 'Target agent(s): comma-separated ids, or "all". Default: all') .option('-l, --location ', 'Uninstall location: "global" or "local". Default: prompt') .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=all') diff --git a/src/installer/index.ts b/src/installer/index.ts index edeb4ac94..50b8ef20c 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -3,8 +3,9 @@ * * Multi-target: writes MCP server config + instructions for the * agents the user picks (Claude Code, Cursor, Codex CLI, opencode, - * Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, and GitHub - * Copilot in VS Code / the Copilot CLI / JetBrains IDEs). + * Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, GitHub + * Copilot in VS Code / the Copilot CLI / JetBrains IDEs, and DeepSeek + * Harness). * Defaults to the Claude-only behavior for backwards compatibility * when no targets are explicitly chosen and nothing else is detected. * @@ -468,7 +469,7 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise 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, ~/.copilot, ~/.config/github-copilot' }, + { value: 'global' as const, label: 'All projects (global)', hint: '~/.claude, ~/.cursor, ~/.codex, ~/.config/opencode, ~/.hermes, ~/.gemini, ~/.kiro, ~/.copilot, ~/.config/github-copilot, ~/.dsh' }, { value: 'local' as const, label: 'Just this project (local)', hint: './.claude, ./.cursor, ./.vscode, ./opencode.jsonc, ./.gemini, ./.kiro' }, ], initialValue: 'global' as const, diff --git a/src/installer/targets/dsh.ts b/src/installer/targets/dsh.ts new file mode 100644 index 000000000..f1bc5547c --- /dev/null +++ b/src/installer/targets/dsh.ts @@ -0,0 +1,399 @@ +/** + * DeepSeek Harness (dsh) target. + * + * The DeepSeek Harness keeps ALL of its state under `$DSH_HOME` (default + * `~/.dsh`, overridable via the `DSH_HOME` env var) — there is no + * project-local config, so this target is global-only like Codex / + * Hermes. Within the home there are two user-editable patch layers: + * + * - `$DSH_HOME/cordis.patch.yml` — the HOME-level layer, applied + * over EVERY profile's own layer (web, headless, custom, ...). This is + * the single, machine-wide place to put an MCP server. + * - `$DSH_HOME/profiles//cordis.patch.yml` — one layer PER profile. + * + * We write to the home-level file: one write configures codegraph for every + * DSH profile, and the boot watches that file live (`watchUserPatches`), so + * the dsh-mcp-client plugin hot-swaps the server via HMR without a restart. + * + * The MCP server is a Cordis plugin entry inserted through the patch + * file's `- insert:` root form: + * + * - insert: + * - id: mcp-codegraph + * name: '@deepseek-ai/dsh-mcp-client' + * config: + * serverName: codegraph + * transport: stdio + * command: codegraph + * args: + * - serve + * - --mcp + * failOnStartupError: false + * + * (dsh-mcp-client schema: `serverName` namespaces the tools as + * `mcp__codegraph__*`; `failOnStartupError: false` keeps the UI booting + * even if the server can't start.) + * + * Self-heal: a pre-installer user may have put a codegraph entry in a + * per-profile `cordis.patch.yml` (as was done by hand before this target + * existed). Because the home-level layer composes AFTER a profile's layer, + * a same-`serverName` entry left in a profile would collide with the + * home-level one — dsh-mcp-client fails the later duplicate at load. So + * install AND uninstall both sweep codegraph entries out of every profile + * patch, exactly like opencode's legacy-%APPDATA% cleanup: install migrates + * the entry up to the shared layer, and uninstall removes codegraph from the + * agent entirely (home + any profile), keeping detect() and uninstall() + * symmetric even for a profile-only install. + * + * Project root: DSH never sends a workspace root (dsh-mcp-client connects + * with no roots capability) and never chdir()s to the workspace, so a + * global `command: codegraph` entry resolves the project from the spawned + * process's inherited cwd — the harness launch dir, not an indexed project. + * The agent reaches the graph by passing `projectPath` (which the server's + * no-root-index guidance points at), or the user can pin `cwd` / `--path` + * on the entry for a specific project. + * + * No instructions file is written. The MCP server's `initialize` + * instructions are the single source of truth for agent-facing tool + * guidance (issue #529), so the installer deliberately writes no duplicate + * block — DSH's own dsh-agent-instructions does read a user-global + * `$DSH_HOME/AGENTS.md`, but that is the user's personal file and is left + * untouched (matching the no-duplicate-block policy every target follows). + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { + AgentTarget, + DetectionResult, + InstallOptions, + Location, + WriteResult, +} from './types'; +import { + arrayEqual, + atomicWriteFileSync, + escapeRegExp, + joinLines, + readTextFile, + splitLines, +} from './shared'; + +/** Stable entry id the installer owns, also used as the duplicate marker. */ +const ENTRY_ID = 'mcp-codegraph'; +/** Cordis plugin that connects to external MCP servers. */ +const MCP_CLIENT_PLUGIN = '@deepseek-ai/dsh-mcp-client'; +/** Per-profile patch filename, same at home level and inside each profile. */ +const PATCH_FILENAME = 'cordis.patch.yml'; +/** A root `- insert:` patch header (inserts entries into the tree root). */ +const INSERT_HEADER_RE = /^- insert:\s*(?:#.*)?$/; + +function dshHome(): string { + const env = process.env.DSH_HOME; + return env && env.trim().length > 0 + ? path.resolve(env) + : path.join(os.homedir(), '.dsh'); +} + +/** The home-level patch layer, applied over every profile. */ +function homePatchPath(): string { + return path.join(dshHome(), PATCH_FILENAME); +} + +/** Every profile's own patch file, if any. */ +function profilePatchPaths(): string[] { + const profilesDir = path.join(dshHome(), 'profiles'); + if (!fs.existsSync(profilesDir)) return []; + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(profilesDir, { withFileTypes: true }); + } catch { + return []; + } + return entries + .filter((e) => e.isDirectory()) + .map((e) => path.join(profilesDir, e.name, PATCH_FILENAME)); +} + +/** + * The canonical `mcp-codegraph` entry, indented for a root `- insert:` + * list (entry at 4 spaces, keys at 6, config keys at 8, args at 10 — + * matching what DSH itself renders). + */ +function renderCodeGraphEntry(): string[] { + return [ + ' - id: mcp-codegraph', + ` name: '${MCP_CLIENT_PLUGIN}'`, + ' config:', + ' serverName: codegraph', + ' transport: stdio', + ' command: codegraph', + ' args:', + ' - serve', + ' - --mcp', + ' failOnStartupError: false', + ]; +} + +/** Index of the first line that is a `- id: ` list item (any indent). */ +function findEntryLine(lines: string[], id: string): number { + const pattern = new RegExp(`^\\s+- id: ${escapeRegExp(id)}\\s*$`); + for (let i = 0; i < lines.length; i++) { + if (pattern.test(lines[i] ?? '')) return i; + } + return -1; +} + +/** + * `[start, end)` of a list-item entry whose first line is `lines[start]` + * (a `- id: ...` line at some indent). Runs until the next line that starts + * a list item at the SAME or a shallower indent (a sibling or a top-level + * patch), or EOF. Blank lines and comments don't end the entry. + */ +function entryRange(lines: string[], start: number): { start: number; end: number } { + const indent = (lines[start] ?? '').match(/^( *)- /)?.[1]?.length ?? 0; + let lastContent = start + 1; + for (let i = start + 1; i < lines.length; i++) { + const line = lines[i] ?? ''; + if (line.trim() === '') continue; + const m = line.match(/^( *)- /); + if (m && (m[1]?.length ?? 0) <= indent) { + // A sibling entry (or top-level patch) at the same/shallower indent + // ends this one — at the boundary line, trailing blanks excluded. + return { start, end: i }; + } + lastContent = i + 1; + } + // EOF — end at the last content line so a trailing blank line left by + // the file's final newline is not counted as part of the entry. + return { start, end: lastContent }; +} + +/** + * Ensure exactly one canonical codegraph entry, inside a root `- insert:` + * block, preserving every other line of the file. Returns the new content + * and whether it changed (byte-compare used by the caller for idempotency). + */ +function upsertCodeGraphEntry(content: string): { content: string; changed: boolean } { + const lines = splitLines(content); + const canonical = renderCodeGraphEntry(); + const existing = findEntryLine(lines, ENTRY_ID); + + if (existing !== -1) { + const range = entryRange(lines, existing); + const current = lines.slice(range.start, range.end); + if (arrayEqual(current, canonical)) return { content, changed: false }; + lines.splice(range.start, range.end - range.start, ...canonical); + return { content: joinLines(lines), changed: true }; + } + + // No entry yet. Prefer appending into the LAST root `- insert:` block + // (so a home patch already carrying other plugins gets one more entry); + // otherwise start a new top-level `- insert:` block. + let insertHeader = -1; + for (let i = 0; i < lines.length; i++) { + if (INSERT_HEADER_RE.test(lines[i] ?? '')) insertHeader = i; + } + + if (insertHeader !== -1) { + // Insert as the first entry of that block (right after the header) so + // existing sibling entries and any trailing comments stay untouched. + lines.splice(insertHeader + 1, 0, ...canonical); + return { content: joinLines(lines), changed: true }; + } + + // No root insert block — start a new one. A brand-new file gets the block + // directly; an existing patch file gets it after a blank-line separator. + const fresh = lines.length === 1 && lines[0] === ''; + if (fresh) { + return { content: '- insert:\n' + canonical.join('\n') + '\n', changed: true }; + } + const text = joinLines(lines); + return { + content: text + '\n' + '- insert:\n' + canonical.join('\n') + '\n', + changed: true, + }; +} + +/** + * Remove every codegraph entry from a patch file. Drops a root `- insert:` + * block that becomes empty, and the whole file if only comments remain. + */ +function removeCodeGraphEntry(content: string): { + content: string; + removed: boolean; + empty: boolean; +} { + const lines = splitLines(content); + let removed = false; + + // Remove every codegraph entry (not just the first) via the same + // ENTRY_ID marker the rest of the file keys off. + for (let idx = findEntryLine(lines, ENTRY_ID); idx !== -1; idx = findEntryLine(lines, ENTRY_ID)) { + const range = entryRange(lines, idx); + lines.splice(range.start, range.end - range.start); + removed = true; + } + + // Drop root `- insert:` blocks that now hold no entries. + const out: string[] = []; + for (let i = 0; i < lines.length; i++) { + const line = lines[i] ?? ''; + if (INSERT_HEADER_RE.test(line)) { + // Consume this block's blank lines and nested list items only. + let j = i + 1; + let hasNested = false; + while (j < lines.length) { + const l = lines[j] ?? ''; + if (l.trim() === '') { + j++; + continue; + } + if (/^ {2,}\S/.test(l)) { + hasNested = true; + j++; + continue; + } + break; + } + if (!hasNested) { + i = j; // block emptied — drop it and its trailing blanks + continue; + } + } + out.push(line); + } + + if (!removed) return { content, removed: false, empty: false }; + const joined = joinLines(out); + const empty = joined.trim() === '' || out.every((l) => l.trim() === '' || l.trim().startsWith('#')); + return { content: joined, removed: true, empty }; +} + +function hasCodeGraphEntry(content: string): boolean { + return findEntryLine(splitLines(content), ENTRY_ID) !== -1; +} + +class DshTarget implements AgentTarget { + readonly id = 'dsh' as const; + readonly displayName = 'DeepSeek Harness'; + readonly docsUrl = 'https://github.com/deepseek-ai/deepseek-harness'; + + supportsLocation(loc: Location): boolean { + return loc === 'global'; + } + + detect(loc: Location): DetectionResult { + if (loc !== 'global') { + return { installed: false, alreadyConfigured: false }; + } + const home = homePatchPath(); + const installed = fs.existsSync(dshHome()) || fs.existsSync(home); + let alreadyConfigured = hasCodeGraphEntry(readTextFile(home)); + // A per-profile entry also counts as configured (install will migrate + // it up to the home-level layer to avoid a same-serverName collision). + if (!alreadyConfigured) { + alreadyConfigured = profilePatchPaths().some((p) => hasCodeGraphEntry(readTextFile(p))); + } + return { installed, alreadyConfigured, configPath: home }; + } + + install(loc: Location, _opts: InstallOptions): WriteResult { + if (loc !== 'global') { + return { + files: [], + notes: ['DeepSeek Harness config lives under $DSH_HOME; re-run with --location=global.'], + }; + } + const files: WriteResult['files'] = []; + + const file = homePatchPath(); + const existed = fs.existsSync(file); + const before = readTextFile(file); + const { content, changed } = upsertCodeGraphEntry(before); + if (changed) { + atomicWriteFileSync(file, content); + files.push({ path: file, action: existed ? 'updated' : 'created' }); + } else { + files.push({ path: file, action: 'unchanged' }); + } + + // Self-heal: migrate any per-profile codegraph entry up to the + // home-level layer so the same `serverName` never mounts twice. + files.push(...sweepProfileEntries()); + + return { + files, + notes: ['DSH applies MCP changes live via HMR; start a new session if the tools don\'t appear.'], + }; + } + + uninstall(loc: Location): WriteResult { + if (loc !== 'global') return { files: [] }; + const files: WriteResult['files'] = []; + const file = homePatchPath(); + + if (fs.existsSync(file)) { + const before = readTextFile(file); + const { content, removed, empty } = removeCodeGraphEntry(before); + if (removed) { + if (empty) { + try { fs.unlinkSync(file); } catch { /* ignore */ } + } else { + atomicWriteFileSync(file, content); + } + files.push({ path: file, action: 'removed' }); + } else { + files.push({ path: file, action: 'not-found' }); + } + } else { + files.push({ path: file, action: 'not-found' }); + } + + // Full reversal: sweep any per-profile codegraph entry too, so a + // profile-only install (which detect() reports as configured) is removed + // as well — keeping detect() and uninstall() symmetric. + files.push(...sweepProfileEntries()); + + return { files }; + } + + printConfig(loc: Location): string { + if (loc !== 'global') { + return '# DeepSeek Harness config lives under $DSH_HOME; use --location=global.\n'; + } + const snippet = ['- insert:', ...renderCodeGraphEntry()].join('\n'); + return `# Add to ${homePatchPath()}\n\n${snippet}\n`; + } + + describePaths(loc: Location): string[] { + return loc === 'global' ? [homePatchPath()] : []; + } +} + +/** + * Strip a codegraph entry from every per-profile `cordis.patch.yml` + * (home-level composes after the profile layer, so a leftover same-name + * entry there would collide). Returns only files actually changed, keeping + * install output quiet when there is nothing to heal. + */ +function sweepProfileEntries(): WriteResult['files'] { + const out: WriteResult['files'] = []; + for (const file of profilePatchPaths()) { + if (!fs.existsSync(file)) continue; + const before = readTextFile(file); + if (!hasCodeGraphEntry(before)) continue; + const { content, removed, empty } = removeCodeGraphEntry(before); + if (!removed) continue; + if (empty) { + try { fs.unlinkSync(file); } catch { /* ignore */ } + } else { + atomicWriteFileSync(file, content); + } + out.push({ path: file, action: 'removed' }); + } + return out; +} + +export const dshTarget: AgentTarget = new DshTarget(); \ No newline at end of file diff --git a/src/installer/targets/hermes.ts b/src/installer/targets/hermes.ts index eacab6d3d..7084c0dbb 100644 --- a/src/installer/targets/hermes.ts +++ b/src/installer/targets/hermes.ts @@ -24,7 +24,14 @@ import { Location, WriteResult, } from './types'; -import { atomicWriteFileSync } from './shared'; +import { + arrayEqual, + atomicWriteFileSync, + escapeRegExp, + joinLines, + readTextFile, + splitLines, +} from './shared'; type LineRange = { start: number; end: number }; @@ -42,7 +49,7 @@ class HermesTarget implements AgentTarget { return { installed: false, alreadyConfigured: false }; } const file = configPath(); - const content = readText(file); + const content = readTextFile(file); const installed = fs.existsSync(hermesHome()) || fs.existsSync(file); return { installed, @@ -71,7 +78,7 @@ class HermesTarget implements AgentTarget { return { files: [{ path: file, action: 'not-found' }] }; } - const before = readText(file); + const before = readTextFile(file); const after = removeCodeGraphToolset(removeCodeGraphMcpServer(before)); if (after === before) { return { files: [{ path: file, action: 'not-found' }] }; @@ -112,18 +119,10 @@ function configPath(): string { return path.join(hermesHome(), 'config.yaml'); } -function readText(file: string): string { - try { - return fs.readFileSync(file, 'utf-8'); - } catch { - return ''; - } -} - function writeHermesConfig(): WriteResult['files'][number] { const file = configPath(); const existed = fs.existsSync(file); - const before = readText(file); + const before = readTextFile(file); const afterMcp = upsertCodeGraphMcpServer(before); const after = upsertCodeGraphToolset(afterMcp); @@ -138,15 +137,6 @@ function ensureTrailingNewline(text: string): string { return text.endsWith('\n') ? text : text + '\n'; } -function splitLines(content: string): string[] { - return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'); -} - -function joinLines(lines: string[]): string { - while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop(); - return lines.join('\n') + '\n'; -} - function topLevelRange(lines: string[], key: string): LineRange | null { const start = lines.findIndex((line) => line.trim() === `${key}:`); if (start === -1) return null; @@ -245,10 +235,6 @@ function listChildBlock( return { start, end, itemIndent }; } -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - function renderCodeGraphMcpChild(): string[] { return [ ' codegraph:', @@ -349,8 +335,4 @@ function removeCodeGraphToolset(content: string): string { return joinLines(next); } -function arrayEqual(a: string[], b: string[]): boolean { - return a.length === b.length && a.every((value, idx) => value === b[idx]); -} - export const hermesTarget: AgentTarget = new HermesTarget(); diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts index 3798b39ad..a58930cda 100644 --- a/src/installer/targets/registry.ts +++ b/src/installer/targets/registry.ts @@ -19,6 +19,7 @@ import { kiroTarget } from './kiro'; import { copilotVscodeTarget } from './copilot-vscode'; import { copilotCliTarget } from './copilot-cli'; import { copilotJetbrainsTarget } from './copilot-jetbrains'; +import { dshTarget } from './dsh'; export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ claudeTarget, @@ -32,6 +33,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ copilotVscodeTarget, copilotCliTarget, copilotJetbrainsTarget, + dshTarget, ]); export function getTarget(id: string): AgentTarget | undefined { diff --git a/src/installer/targets/shared.ts b/src/installer/targets/shared.ts index 364f40427..50a31c53c 100644 --- a/src/installer/targets/shared.ts +++ b/src/installer/targets/shared.ts @@ -127,6 +127,40 @@ export function jsonDeepEqual(a: unknown, b: unknown): boolean { return ak.every((k) => jsonDeepEqual(ao[k], bo[k])); } +/** + * Read a text file, returning `''` when missing or unreadable. Shared by the + * line-based YAML targets (Hermes, DeepSeek Harness) which treat an absent + * config file the same as an empty one. + */ +export function readTextFile(filePath: string): string { + try { + return fs.readFileSync(filePath, 'utf-8'); + } catch { + return ''; + } +} + +/** Split file content into lines, normalizing CRLF / CR to LF. */ +export function splitLines(content: string): string[] { + return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'); +} + +/** Re-join lines, dropping a single trailing blank line and ending with `\n`. */ +export function joinLines(lines: string[]): string { + while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop(); + return lines.join('\n') + '\n'; +} + +/** Escape a string so it is literal inside a RegExp. */ +export function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** Order-insensitive array equality by value (element-wise `===`). */ +export function arrayEqual(a: string[], b: string[]): boolean { + return a.length === b.length && a.every((value, idx) => value === b[idx]); +} + /** * Replace or append a marker-delimited section in a markdown-ish file. * diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts index 022ab28e8..29e290e5f 100644 --- a/src/installer/targets/types.ts +++ b/src/installer/targets/types.ts @@ -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' | 'dsh'; /** * Result of `target.detect(location)`.