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
251 changes: 251 additions & 0 deletions .claude/skills/add-agent-target/SKILL.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### New Features

- Added support for Qwen Code as an install target. Run `codegraph install --target=qwen` to wire the MCP server into Qwen Code's `~/.qwen/settings.json` (global) or `.qwen/settings.json` (project-local).
- The `codegraph_node` MCP tool can now **read a whole source file like the built-in Read tool — only faster, served from the index**. Pass a file path with no symbol and it returns that file's current source with line numbers (the same `<n>⇥<line>` shape Read produces, so an assistant can edit straight from it), narrowable with `offset`/`limit` exactly like Read, plus a one-line note of which files depend on it (the file's blast radius). Use it anywhere you'd reach for Read on an indexed source file. Pass `symbolsOnly: true` for just the file's structure. Configuration/data files (`.yml` / `.properties`) are summarized by key only, never dumped, so secrets in them are never surfaced. The agent-facing guidance was also retuned so assistants reach for codegraph while *implementing* a change (not only when answering questions), since one codegraph call returns the same bytes plus the blast radius, faster than re-reading the file.
- New `codegraph upgrade` command updates CodeGraph to the latest release in place — it detects how you installed (the standalone `install.sh` / `install.ps1` bundle, npm, or npx) and does the right thing for each, on macOS, Linux, and Windows. Use `codegraph upgrade --check` to see whether an update is available without installing, or `codegraph upgrade <version>` to move to a specific version. After upgrading it reminds you to re-index your projects so they pick up the newer engine's improvements. (#679)
- `codegraph status` now flags when a project's index was built by an older engine than the one you're running and recommends re-indexing (also surfaced in `codegraph status --json`), so you know when a `codegraph index -f` or `codegraph sync` will add coverage a newer release introduced.
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# CodeGraph

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

**~16% cheaper · ~58% fewer tool calls · 100% local**

Expand All @@ -24,6 +24,7 @@
[![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)
[![Qwen Code](https://img.shields.io/badge/Qwen_Code-supported-blueviolet.svg)](#supported-agents)

<br>

Expand Down Expand Up @@ -67,7 +68,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. (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 Qwen Code — 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. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)</sub>

### 3. Initialize each project

Expand Down Expand Up @@ -320,7 +321,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**, **Qwen Code**
- 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 (the codegraph usage guide is delivered by the MCP server itself, so no instructions file is added to `CLAUDE.md` / `AGENTS.md` / etc.)
Expand All @@ -346,7 +347,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 / Qwen Code) for the MCP server to load.

### 3. Initialize Projects

Expand Down Expand Up @@ -611,6 +612,7 @@ is written):
- **Gemini CLI**
- **Antigravity IDE**
- **Kiro**
- **Qwen Code**

## Supported Languages

Expand Down
48 changes: 48 additions & 0 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,54 @@ describe('Installer targets — partial-state idempotency', () => {
expect(body).not.toContain('CODEGRAPH_START');
});

it('qwen: install writes settings.json (mcpServers.codegraph) and no instructions file (#529)', () => {
const qwen = getTarget('qwen')!;
const result = qwen.install('global', { autoAllow: true });
const settings = path.join(tmpHome, '.qwen', 'settings.json');
expect(result.files.some((f) => f.path === settings)).toBe(true);

const cfg = JSON.parse(fs.readFileSync(settings, 'utf-8'));
expect(cfg.mcpServers.codegraph).toEqual({ type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'] });
});

it('qwen: install preserves pre-existing settings (modelProviders survives)', () => {
const qwen = getTarget('qwen')!;
const settings = path.join(tmpHome, '.qwen', 'settings.json');
fs.mkdirSync(path.dirname(settings), { recursive: true });
fs.writeFileSync(settings, JSON.stringify({
modelProviders: { openai: [{ id: 'qwen3.6-plus', baseUrl: 'https://example.com' }] },
}, null, 2) + '\n');

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

const after = JSON.parse(fs.readFileSync(settings, 'utf-8'));
expect(after.modelProviders?.openai?.[0]?.id).toBe('qwen3.6-plus');
expect(after.mcpServers?.codegraph).toBeDefined();
});

it('qwen: uninstall strips codegraph but leaves pre-existing settings (modelProviders) intact', () => {
const qwen = getTarget('qwen')!;
const settings = path.join(tmpHome, '.qwen', 'settings.json');
fs.mkdirSync(path.dirname(settings), { recursive: true });
fs.writeFileSync(settings, JSON.stringify({
modelProviders: { openai: [{ id: 'qwen3.6-plus', baseUrl: 'https://example.com' }] },
}, null, 2) + '\n');

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

const after = JSON.parse(fs.readFileSync(settings, 'utf-8'));
expect(after.modelProviders?.openai?.[0]?.id).toBe('qwen3.6-plus');
expect(after.mcpServers).toBeUndefined();
});

it('qwen: local install writes ./.qwen/settings.json', () => {
const qwen = getTarget('qwen')!;
const result = qwen.install('local', { autoAllow: true });
const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
expect(paths.some((p) => p.endsWith('/.qwen/settings.json'))).toBe(true);
});

it('kiro: install writes settings/mcp.json (mcpServers.codegraph) and no steering doc (#529)', () => {
const kiro = getTarget('kiro')!;
const result = kiro.install('global', { autoAllow: true });
Expand Down
4 changes: 2 additions & 2 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1591,7 +1591,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, Qwen Code)')
.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 @@ -1658,7 +1658,7 @@ program
*/
program
.command('uninstall')
.description('Remove codegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)')
.description('Remove codegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro, Qwen Code)')
.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
2 changes: 1 addition & 1 deletion src/installer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* 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).
* Hermes Agent, Gemini CLI, Antigravity IDE, Qwen Code).
* Defaults to the Claude-only behavior for backwards compatibility
* when no targets are explicitly chosen and nothing else is detected.
*
Expand Down
124 changes: 124 additions & 0 deletions src/installer/targets/qwen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Qwen Code target.
*
* Qwen Code is an open-source AI coding agent that lives in the terminal.
* It reads MCP server configuration from JSON settings files:
*
* - Global (user scope): ~/.qwen/settings.json
* - Local (project scope): ./.qwen/settings.json
*
* The MCP server entry lives under the top-level `mcpServers` key, same
* shape as Claude / Cursor / Gemini.
*
* Qwen Code does not have a separate permissions / auto-allow concept —
* tool confirmation is handled per-server at runtime, so `autoAllow` is
* a no-op here.
*
* No instructions file is written (issue #529); usage guidance ships in
* the MCP server's `initialize` response.
*/

import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import {
AgentTarget,
DetectionResult,
InstallOptions,
Location,
WriteResult,
} from './types';
import {
getMcpServerConfig,
jsonDeepEqual,
readJsonFile,
writeJsonFile,
} from './shared';

function configDir(loc: Location): string {
return loc === 'global'
? path.join(os.homedir(), '.qwen')
: path.join(process.cwd(), '.qwen');
}

function settingsJsonPath(loc: Location): string {
return path.join(configDir(loc), 'settings.json');
}

class QwenTarget implements AgentTarget {
readonly id = 'qwen' as const;
readonly displayName = 'Qwen Code';
readonly docsUrl = 'https://qwenlm.github.io/qwen-code-docs/';

supportsLocation(_loc: Location): boolean {
return true;
}

detect(loc: Location): DetectionResult {
const file = settingsJsonPath(loc);
const config = readJsonFile(file);
const alreadyConfigured = !!config.mcpServers?.codegraph;
const installed = loc === 'global'
? fs.existsSync(configDir('global')) || fs.existsSync(file)
: fs.existsSync(file) || fs.existsSync(configDir('local'));
return { installed, alreadyConfigured, configPath: file };
}

install(loc: Location, _opts: InstallOptions): WriteResult {
const files: WriteResult['files'] = [];
files.push(writeMcpEntry(loc));
return { files };
}

uninstall(loc: Location): WriteResult {
const files: WriteResult['files'] = [];

const file = settingsJsonPath(loc);
const config = readJsonFile(file);
if (config.mcpServers?.codegraph) {
delete config.mcpServers.codegraph;
if (Object.keys(config.mcpServers).length === 0) {
delete config.mcpServers;
}
writeJsonFile(file, config);
files.push({ path: file, action: 'removed' });
} else {
files.push({ path: file, action: 'not-found' });
}

return { files };
}

printConfig(loc: Location): string {
const target = settingsJsonPath(loc);
const snippet = JSON.stringify({ mcpServers: { codegraph: getMcpServerConfig() } }, null, 2);
return `# Add to ${target}\n\n${snippet}\n`;
}

describePaths(loc: Location): string[] {
return [settingsJsonPath(loc)];
}
}

function writeMcpEntry(loc: Location): WriteResult['files'][number] {
const file = settingsJsonPath(loc);
const dir = path.dirname(file);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });

const existing = readJsonFile(file);
const before = existing.mcpServers?.codegraph;
const after = getMcpServerConfig();

if (jsonDeepEqual(before, after)) {
return { path: file, action: 'unchanged' };
}

const action: 'created' | 'updated' =
before ? 'updated' : (fs.existsSync(file) ? 'updated' : 'created');
if (!existing.mcpServers) existing.mcpServers = {};
existing.mcpServers.codegraph = after;
writeJsonFile(file, existing);
return { path: file, action };
}

export const qwenTarget: AgentTarget = new QwenTarget();
2 changes: 2 additions & 0 deletions src/installer/targets/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { hermesTarget } from './hermes';
import { geminiTarget } from './gemini';
import { antigravityTarget } from './antigravity';
import { kiroTarget } from './kiro';
import { qwenTarget } from './qwen';

export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
claudeTarget,
Expand All @@ -26,6 +27,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
geminiTarget,
antigravityTarget,
kiroTarget,
qwenTarget,
]);

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';
export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro' | 'qwen';

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