Skip to content

Commit b9c441b

Browse files
committed
Add Qwen Code installer target
1 parent c5bd6e2 commit b9c441b

8 files changed

Lines changed: 262 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99

1010
## [Unreleased]
1111

12+
### New Features
13+
14+
- `codegraph install` can now configure **Qwen Code**, wiring CodeGraph in while preserving existing Qwen settings and adding the short CodeGraph hint Qwen can read in agent context.
1215

1316
## [1.1.2] - 2026-06-28
1417

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Already installed? Run `codegraph upgrade` to update in place.
88

99
Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.
1010

11-
### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Antigravity, and Kiro with Semantic Code Intelligence
11+
### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Qwen Code, Antigravity, and Kiro with Semantic Code Intelligence
1212

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

@@ -28,6 +28,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.
2828
[![opencode](https://img.shields.io/badge/opencode-supported-blueviolet.svg)](#supported-agents)
2929
[![Hermes Agent](https://img.shields.io/badge/Hermes_Agent-supported-blueviolet.svg)](#supported-agents)
3030
[![Gemini](https://img.shields.io/badge/Gemini-supported-blueviolet.svg)](#supported-agents)
31+
[![Qwen Code](https://img.shields.io/badge/Qwen_Code-supported-blueviolet.svg)](#supported-agents)
3132
[![Antigravity](https://img.shields.io/badge/Antigravity-supported-blueviolet.svg)](#supported-agents)
3233
[![Kiro](https://img.shields.io/badge/Kiro-supported-blueviolet.svg)](#supported-agents)
3334

@@ -76,7 +77,7 @@ In a **new terminal**, run the installer to connect CodeGraph to the agents you
7677
codegraph install
7778
```
7879

79-
<sub>Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. It only wires up your agent — it does **not** index any code; building each project's graph is the separate `codegraph init` in step 3. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)</sub>
80+
<sub>Detects and auto-configures Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Qwen Code, Antigravity IDE, and Kiro — wiring the CodeGraph MCP server into each. **This is the step that connects CodeGraph to your agent;** installing the CLI in step 1 does not do it on its own. It only wires up your agent — it does **not** index any code; building each project's graph is the separate `codegraph init` in step 3. (Shortcut: `npx @colbymchenry/codegraph` downloads and runs this in one go.)</sub>
8081

8182
### 3. Initialize each project
8283

@@ -252,7 +253,7 @@ The reliable, universal payoff is **surgical context and speed**: CodeGraph coll
252253
<details>
253254
<summary><strong>How auto-syncing works — and why you don't need to run <code>codegraph sync</code> manually</strong></summary>
254255

255-
When your agent (Claude Code, Cursor, Codex, opencode) launches `codegraph serve --mcp`, three layers keep the index in step with your code — and make sure the agent never gets a silent wrong answer in the brief window between an edit and the next sync:
256+
When your agent (Claude Code, Cursor, Codex, opencode, Gemini CLI, Qwen Code, and others) launches `codegraph serve --mcp`, three layers keep the index in step with your code — and make sure the agent never gets a silent wrong answer in the brief window between an edit and the next sync:
256257

257258
1. **File watcher with debounced auto-sync.** A native FSEvents / inotify / ReadDirectoryChangesW watcher captures every source-file create / modify / delete and triggers a re-index after a debounce window (default `2000ms`, tunable via `CODEGRAPH_WATCH_DEBOUNCE_MS`, clamped to `[100ms, 60s]`). Bursts of edits collapse into a single sync.
258259

@@ -342,10 +343,10 @@ npx @colbymchenry/codegraph
342343
```
343344

344345
The installer will:
345-
- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Antigravity IDE**, **Kiro**
346+
- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Gemini CLI**, **Qwen Code**, **Antigravity IDE**, **Kiro**
346347
- Prompt to install `codegraph` on your PATH (so agents can launch the MCP server)
347348
- Ask whether configs apply to all your projects or just this one
348-
- 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`.
349+
- Write each chosen agent's MCP server config, plus a small marker-fenced CodeGraph section in the agent's instructions file where supported (`CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / `QWEN.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`.
349350
- Set up auto-allow permissions when Claude Code is one of the targets
350351

351352
The installer **wires up your agents only — it does not index your code.** After it finishes, build each project's graph yourself with `codegraph init` (step 3). One global `codegraph install` covers every project; you run `codegraph init` once per project.
@@ -369,7 +370,7 @@ codegraph install --print-config codex # print snippet, no file wr
369370

370371
### 2. Restart Your Agent
371372

372-
Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Antigravity IDE / Kiro) for the MCP server to load.
373+
Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Gemini CLI / Qwen Code / Antigravity IDE / Kiro) for the MCP server to load.
373374

374375
### 3. Initialize Projects
375376

@@ -674,15 +675,15 @@ See [Get Started](#get-started) for the one-line install commands.
674675
## Supported Agents
675676

676677
The interactive installer auto-detects and configures each of these — wiring up
677-
the MCP server (which delivers its own usage guidance, so no instructions file
678-
is written):
678+
the MCP server plus a short context-file hint where the agent supports one:
679679

680680
- **Claude Code**
681681
- **Cursor**
682682
- **Codex CLI**
683683
- **opencode**
684684
- **Hermes Agent**
685685
- **Gemini CLI**
686+
- **Qwen Code**
686687
- **Antigravity IDE**
687688
- **Kiro**
688689

@@ -783,7 +784,7 @@ MIT
783784

784785
<div align="center">
785786

786-
**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, and Kiro**
787+
**Made for AI coding agents — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Qwen Code, Antigravity IDE, and Kiro**
787788

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

__tests__/installer-targets.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,85 @@ describe('Installer targets — partial-state idempotency', () => {
413413
expect(body).not.toContain('CODEGRAPH_START');
414414
});
415415

416+
it('qwen: install writes settings.json (mcpServers.codegraph) and the QWEN.md block (#704)', () => {
417+
const qwen = getTarget('qwen')!;
418+
const result = qwen.install('global', { autoAllow: true });
419+
const settings = path.join(tmpHome, '.qwen', 'settings.json');
420+
const qwenMd = path.join(tmpHome, '.qwen', 'QWEN.md');
421+
expect(result.files.some((f) => f.path === settings)).toBe(true);
422+
expect(result.files.some((f) => f.path === qwenMd)).toBe(true);
423+
expect(fs.existsSync(qwenMd)).toBe(true);
424+
expect(fs.readFileSync(qwenMd, 'utf-8')).toContain('codegraph explore');
425+
426+
const cfg = JSON.parse(fs.readFileSync(settings, 'utf-8'));
427+
expect(cfg.mcpServers.codegraph).toEqual({ command: 'codegraph', args: ['serve', '--mcp'] });
428+
expect(cfg.mcpServers.codegraph.type).toBeUndefined();
429+
});
430+
431+
it('qwen: printConfig uses the documented command/args MCP shape without type', () => {
432+
const qwen = getTarget('qwen')!;
433+
const out = qwen.printConfig('global');
434+
const jsonStart = out.indexOf('{');
435+
const cfg = JSON.parse(out.slice(jsonStart));
436+
437+
expect(cfg.mcpServers.codegraph).toEqual({ command: 'codegraph', args: ['serve', '--mcp'] });
438+
expect(cfg.mcpServers.codegraph.type).toBeUndefined();
439+
});
440+
441+
it('qwen: install preserves pre-existing settings (security.auth survives)', () => {
442+
const qwen = getTarget('qwen')!;
443+
const settings = path.join(tmpHome, '.qwen', 'settings.json');
444+
fs.mkdirSync(path.dirname(settings), { recursive: true });
445+
fs.writeFileSync(settings, JSON.stringify({
446+
security: { auth: { selectedType: 'oauth-personal' } },
447+
}, null, 2) + '\n');
448+
449+
qwen.install('global', { autoAllow: true });
450+
451+
const after = JSON.parse(fs.readFileSync(settings, 'utf-8'));
452+
expect(after.security?.auth?.selectedType).toBe('oauth-personal');
453+
expect(after.mcpServers?.codegraph).toBeDefined();
454+
});
455+
456+
it('qwen: uninstall strips codegraph but leaves pre-existing settings (security.auth) intact', () => {
457+
const qwen = getTarget('qwen')!;
458+
const settings = path.join(tmpHome, '.qwen', 'settings.json');
459+
fs.mkdirSync(path.dirname(settings), { recursive: true });
460+
fs.writeFileSync(settings, JSON.stringify({
461+
security: { auth: { selectedType: 'oauth-personal' } },
462+
}, null, 2) + '\n');
463+
464+
qwen.install('global', { autoAllow: true });
465+
qwen.uninstall('global');
466+
467+
const after = JSON.parse(fs.readFileSync(settings, 'utf-8'));
468+
expect(after.security?.auth?.selectedType).toBe('oauth-personal');
469+
expect(after.mcpServers).toBeUndefined();
470+
});
471+
472+
it('qwen: local install writes ./.qwen/settings.json and the project-root ./QWEN.md block (#704)', () => {
473+
const qwen = getTarget('qwen')!;
474+
const result = qwen.install('local', { autoAllow: true });
475+
const paths = result.files.map((f) => f.path.replace(/\\/g, '/'));
476+
expect(paths.some((p) => p.endsWith('/.qwen/settings.json'))).toBe(true);
477+
expect(paths.some((p) => p.endsWith('/QWEN.md'))).toBe(true);
478+
expect(fs.existsSync(path.join(process.cwd(), 'QWEN.md'))).toBe(true);
479+
});
480+
481+
it('qwen: uninstall strips a leftover QWEN.md codegraph block, keeping user content', () => {
482+
const qwen = getTarget('qwen')!;
483+
const qwenMd = path.join(tmpHome, '.qwen', 'QWEN.md');
484+
fs.mkdirSync(path.dirname(qwenMd), { recursive: true });
485+
fs.writeFileSync(qwenMd, `# My personal Qwen context\n\nAlways respond concisely.\n\n${LEGACY_BLOCK}\n`);
486+
487+
qwen.uninstall('global');
488+
489+
const body = fs.readFileSync(qwenMd, 'utf-8');
490+
expect(body).toContain('# My personal Qwen context');
491+
expect(body).toContain('Always respond concisely.');
492+
expect(body).not.toContain('CODEGRAPH_START');
493+
});
494+
416495
it('kiro: install writes settings/mcp.json (mcpServers.codegraph) and no steering doc (#529)', () => {
417496
const kiro = getTarget('kiro')!;
418497
const result = kiro.install('global', { autoAllow: true });
@@ -1185,6 +1264,7 @@ describe('Installer targets — registry', () => {
11851264
expect(getTarget('opencode')?.id).toBe('opencode');
11861265
expect(getTarget('hermes')?.id).toBe('hermes');
11871266
expect(getTarget('gemini')?.id).toBe('gemini');
1267+
expect(getTarget('qwen')?.id).toBe('qwen');
11881268
expect(getTarget('antigravity')?.id).toBe('antigravity');
11891269
expect(getTarget('kiro')?.id).toBe('kiro');
11901270
expect(getTarget('not-a-real-target')).toBeUndefined();

src/bin/codegraph.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1987,7 +1987,7 @@ program
19871987
*/
19881988
program
19891989
.command('install')
1990-
.description('Install codegraph MCP server into one or more agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)')
1990+
.description('Install codegraph MCP server into one or more supported agents')
19911991
.option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
19921992
.option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
19931993
.option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on')
@@ -2054,7 +2054,7 @@ program
20542054
*/
20552055
program
20562056
.command('uninstall')
2057-
.description('Remove codegraph from your agents (Claude Code, Cursor, Codex CLI, opencode, Hermes Agent)')
2057+
.description('Remove codegraph from your supported agents')
20582058
.option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "all". Default: all')
20592059
.option('-l, --location <where>', 'Uninstall location: "global" or "local". Default: prompt')
20602060
.option('-y, --yes', 'Non-interactive: defaults to --location=global --target=all')

src/installer/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* Multi-target: writes MCP server config + instructions for the
55
* agents the user picks (Claude Code, Cursor, Codex CLI, opencode,
6-
* Hermes Agent, Gemini CLI, Antigravity IDE).
6+
* Hermes Agent, Gemini CLI, Qwen Code, Antigravity IDE).
77
* Defaults to the Claude-only behavior for backwards compatibility
88
* when no targets are explicitly chosen and nothing else is detected.
99
*

src/installer/targets/qwen.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* Qwen Code target. Writes:
3+
*
4+
* - MCP server entry to `~/.qwen/settings.json` (global) or
5+
* `./.qwen/settings.json` (local) under the standard
6+
* `mcpServers.codegraph` key. Qwen Code is Gemini CLI-derived but
7+
* documents stdio servers as `command` + `args` entries, without a
8+
* `type: "stdio"` discriminator.
9+
* - Instructions to `~/.qwen/QWEN.md` (global) or `./QWEN.md`
10+
* (local). Qwen Code's hierarchical context loader reads QWEN.md,
11+
* matching Gemini CLI's GEMINI.md behavior.
12+
*
13+
* No permissions concept — Qwen Code gates MCP invocations through the
14+
* per-server `trust` field and UI prompts. We leave `trust` unset so
15+
* the user controls confirmation prompts.
16+
*
17+
* Docs:
18+
* https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/
19+
*/
20+
21+
import * as fs from 'fs';
22+
import * as path from 'path';
23+
import * as os from 'os';
24+
import {
25+
AgentTarget,
26+
DetectionResult,
27+
InstallOptions,
28+
Location,
29+
WriteResult,
30+
} from './types';
31+
import {
32+
jsonDeepEqual,
33+
readJsonFile,
34+
removeMarkedSection,
35+
writeJsonFile,
36+
upsertInstructionsEntry,
37+
} from './shared';
38+
import {
39+
CODEGRAPH_SECTION_END,
40+
CODEGRAPH_SECTION_START,
41+
} from '../instructions-template';
42+
43+
function configDir(loc: Location): string {
44+
return loc === 'global'
45+
? path.join(os.homedir(), '.qwen')
46+
: path.join(process.cwd(), '.qwen');
47+
}
48+
49+
function settingsJsonPath(loc: Location): string {
50+
return path.join(configDir(loc), 'settings.json');
51+
}
52+
53+
function instructionsPath(loc: Location): string {
54+
return loc === 'global'
55+
? path.join(configDir('global'), 'QWEN.md')
56+
: path.join(process.cwd(), 'QWEN.md');
57+
}
58+
59+
class QwenTarget implements AgentTarget {
60+
readonly id = 'qwen' as const;
61+
readonly displayName = 'Qwen Code';
62+
readonly docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/';
63+
64+
supportsLocation(_loc: Location): boolean {
65+
return true;
66+
}
67+
68+
detect(loc: Location): DetectionResult {
69+
const file = settingsJsonPath(loc);
70+
const config = readJsonFile(file);
71+
const alreadyConfigured = !!config.mcpServers?.codegraph;
72+
const installed = loc === 'global'
73+
? fs.existsSync(configDir('global')) || fs.existsSync(file)
74+
: fs.existsSync(file) || fs.existsSync(configDir('local'));
75+
return { installed, alreadyConfigured, configPath: file };
76+
}
77+
78+
install(loc: Location, _opts: InstallOptions): WriteResult {
79+
const files: WriteResult['files'] = [];
80+
files.push(writeMcpEntry(loc));
81+
files.push(upsertInstructionsEntry(instructionsPath(loc)));
82+
return { files };
83+
}
84+
85+
uninstall(loc: Location): WriteResult {
86+
const files: WriteResult['files'] = [];
87+
88+
const file = settingsJsonPath(loc);
89+
const config = readJsonFile(file);
90+
if (config.mcpServers?.codegraph) {
91+
delete config.mcpServers.codegraph;
92+
if (Object.keys(config.mcpServers).length === 0) {
93+
delete config.mcpServers;
94+
}
95+
writeJsonFile(file, config);
96+
files.push({ path: file, action: 'removed' });
97+
} else {
98+
files.push({ path: file, action: 'not-found' });
99+
}
100+
101+
files.push(removeInstructionsEntry(loc));
102+
103+
return { files };
104+
}
105+
106+
printConfig(loc: Location): string {
107+
const target = settingsJsonPath(loc);
108+
const snippet = JSON.stringify({ mcpServers: { codegraph: buildQwenMcpEntry() } }, null, 2);
109+
return `# Add to ${target}\n\n${snippet}\n`;
110+
}
111+
112+
describePaths(loc: Location): string[] {
113+
return [settingsJsonPath(loc), instructionsPath(loc)];
114+
}
115+
}
116+
117+
function writeMcpEntry(loc: Location): WriteResult['files'][number] {
118+
const file = settingsJsonPath(loc);
119+
const dir = path.dirname(file);
120+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
121+
122+
const existing = readJsonFile(file);
123+
const before = existing.mcpServers?.codegraph;
124+
const after = buildQwenMcpEntry();
125+
126+
if (jsonDeepEqual(before, after)) {
127+
return { path: file, action: 'unchanged' };
128+
}
129+
const action: 'created' | 'updated' =
130+
before ? 'updated' : (fs.existsSync(file) ? 'updated' : 'created');
131+
if (!existing.mcpServers) existing.mcpServers = {};
132+
existing.mcpServers.codegraph = after;
133+
writeJsonFile(file, existing);
134+
return { path: file, action };
135+
}
136+
137+
function buildQwenMcpEntry(): { command: string; args: string[] } {
138+
return {
139+
command: 'codegraph',
140+
args: ['serve', '--mcp'],
141+
};
142+
}
143+
144+
/**
145+
* Strip the marker-delimited CodeGraph block from QWEN.md if a prior
146+
* install wrote one.
147+
*/
148+
function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
149+
const file = instructionsPath(loc);
150+
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
151+
return { path: file, action };
152+
}
153+
154+
export const qwenTarget: AgentTarget = new QwenTarget();

src/installer/targets/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { codexTarget } from './codex';
1414
import { opencodeTarget } from './opencode';
1515
import { hermesTarget } from './hermes';
1616
import { geminiTarget } from './gemini';
17+
import { qwenTarget } from './qwen';
1718
import { antigravityTarget } from './antigravity';
1819
import { kiroTarget } from './kiro';
1920

@@ -24,6 +25,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([
2425
opencodeTarget,
2526
hermesTarget,
2627
geminiTarget,
28+
qwenTarget,
2729
antigravityTarget,
2830
kiroTarget,
2931
]);

0 commit comments

Comments
 (0)