From d6e1f2e16663c01922c5494f0eb7eabcf9537dd1 Mon Sep 17 00:00:00 2001 From: emaxe Date: Tue, 9 Jun 2026 14:18:09 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(installer):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=81=D0=BA=D0=B8=D0=BB=D0=BB=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20cod?= =?UTF-8?q?ing-=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=B0=20=D0=B2=20CodeGraph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скилл .claude/skills/add-agent-target/SKILL.md описывает end-to-end процесс добавления AgentTarget в src/installer/targets/: - разведка формата конфига агента (JSON/TOML/YAML/JSONC/Markdown) - добавление TargetId в types.ts - реализация AgentTarget (idempotency, atomic write, sibling preservation, legacy cleanup #529) - регистрация в registry.ts - тесты (parameterized contract + focused tests) - CHANGELOG под ## [Unreleased] --- .claude/skills/add-agent-target/SKILL.md | 251 +++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 .claude/skills/add-agent-target/SKILL.md diff --git a/.claude/skills/add-agent-target/SKILL.md b/.claude/skills/add-agent-target/SKILL.md new file mode 100644 index 000000000..922090e85 --- /dev/null +++ b/.claude/skills/add-agent-target/SKILL.md @@ -0,0 +1,251 @@ +--- +name: add-agent-target +description: Добавление нового coding-агента в инсталлер CodeGraph (src/installer/targets/) +metadata: + type: project +--- + +# Добавление нового coding-агента в CodeGraph + +Этот скилл описывает end-to-end процесс добавления поддержки нового MCP-совместимого coding-агента в инсталлер CodeGraph. Результат: агент появляется в `codegraph install --target=all`, получает свой MCP-конфиг, и проходит параметризованный контрактный тест в `__tests__/installer-targets.test.ts`. + +**Почему это важно:** инсталлер — критичная поверхность. Регрессия здесь ломает установку для всех новых пользователей молча. Каждое изменение в `src/installer/` требует тестов и CHANGELOG-записи (house rules про `0.7.x`). + +## Архитектура инсталлера (кратко) + +- `src/installer/targets/types.ts` — `AgentTarget` interface + `TargetId` union. +- `src/installer/targets/.ts` — реализация одного агента. +- `src/installer/targets/registry.ts` — реестр; новый агент = 1 файл + 1 entry в массиве `ALL_TARGETS`. +- `src/installer/targets/shared.ts` — хелперы (чтение/запись JSON, atomic write, TOML, markdown-секции, MCP-конфиг, permissions). +- `src/installer/instructions-template.ts` — маркеры `` / ``. +- `__tests__/installer-targets.test.ts` — параметризованные контрактные тесты **для всех** агентов. +- `CHANGELOG.md` — записывать под `## [Unreleased]` (не создавать новый блок версии заранее). + +--- + +## Пошаговый процесс + +### Шаг 1 — Понять формат конфига агента + +Прежде чем писать код, найти документацию агента по MCP и определить: + +1. **Где лежит конфиг:** `~/.agent/mcp.json`, `~/.config/agent/config.toml`, `./.agent.jsonc`, env-переменная (`$AGENT_HOME`) и т.д. +2. **Формат:** JSON, TOML, YAML, JSONC, plain text/markdown. +3. **Скоуп:** есть ли project-local конфиг или только global? Если только global — `supportsLocation('local')` возвращает `false`. +4. **Shape MCP-сервера:** `mcpServers.`, `mcp.`, `mcp_servers.`, массив, и т.д. +5. **Есть ли permissions / auto-allow?** Если нет — `autoAllow` в `install()` игнорируется. +6. **Есть ли instructions-файл?** Какой формат и где он лежит. + +> **Принцип #529 (жёсткий):** инсталлер **не пишет** usage-гайд в instructions-файл агента. Единый источник правды — ответ `initialize` MCP-сервера (`src/mcp/server-instructions.ts`). Инсталлер только **удаляет** legacy-блок, который старые версии записали, чтобы self-heal при апгрейде. Никогда не пиши новый `## CodeGraph` блок в `CLAUDE.md` / `AGENTS.md` / `.mdc` / etc. + +### Шаг 2 — Добавить TargetId в types.ts + +В `src/installer/targets/types.ts`: + +```typescript +export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro' | 'novoagent'; +``` + +### Шаг 3 — Создать `src/installer/targets/.ts` + +Класс должен имплементировать `AgentTarget`. Ниже — контракт каждого метода и паттерны. + +#### `readonly id` + +Строка, точно совпадает с `TargetId` в types.ts. Например: `readonly id = 'novoagent' as const;` + +#### `readonly displayName` + +Human-readable название для clack-промптов и логов. Например: `'Novo Agent'`. + +#### `readonly docsUrl` + +Опционально — ссылка на документацию агента по MCP. + +#### `supportsLocation(loc: Location): boolean` + +Вернуть `true` только для поддерживаемых локаций. Если агент не имеет project-local конфига (как Codex и Hermes), вернуть `false` для `'local'`. Оркестратор тогда skip-ает с понятным сообщением. + +#### `detect(loc: Location): DetectionResult` + +```typescript +interface DetectionResult { + installed: boolean; // heuristic: есть ли директория / конфиг агента + alreadyConfigured: boolean; // уже ли наш MCP-сервер записан в его конфиге + configPath?: string; // путь, который проверяли (для диагностики) +} +``` + +- `installed` — best-effort heuristic. Ложные срабатывания допустимы (пользователь просто выберет вручную), ложные пропуски — нет. +- `alreadyConfigured` — проверить, что в конфиге агента уже есть ключ `codegraph` / `mcp_servers.codegraph` / и т.д. + +#### `install(loc: Location, opts: InstallOptions): WriteResult` + +```typescript +interface WriteResult { + files: Array<{ path: string; action: 'created' | 'updated' | 'unchanged' | 'removed' | 'not-found' | 'kept' }>; + notes?: string[]; // однострочные подсказки, например "Restart Cursor to apply." +} +``` + +**Правила:** + +1. **Idempotency:** повторный `install` с идентичным конфигом должен вернуть `action: 'unchanged'` и **не перезаписывать** файл на диске. Это проверяется тестом. +2. **Соседи:** если в конфиге агента уже есть другие MCP-серверы, они должны сохраниться. Никогда не перезаписывать весь файл целиком — только вставить/заменить свой блок. +3. **Legacy cleanup:** если в прошлом инсталлер писал instructions (`CLAUDE.md`, `AGENTS.md`, `.mdc`) — вызвать `removeMarkedSection(..., CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END)` в `install`, чтобы self-heal при апгрейде. Действие `removed` добавить в `files`. +4. **Atomic write:** использовать `atomicWriteFileSync()` из `shared.ts` — пишет во `.tmp.`, затем `rename`. +5. **JSON targets:** использовать `readJsonFile`, `writeJsonFile`, `jsonDeepEqual` из `shared.ts` для проверки idempotency. +6. **TOML target (Codex):** использовать `buildTomlTable`, `upsertTomlTable`, `removeTomlTable` из `toml.ts`. +7. **JSONC target (opencode):** использовать `jsonc-parser` (`modify` + `applyEdits`) для surgical edit, сохраняющего комментарии. +8. **YAML target (Hermes):** пишем line-based парсер/сериализатор (см. `hermes.ts`), не тащим библиотеку. Сохраняем комментарии и соседние ключи. +9. **Permissions:** если агент поддерживает auto-allow, при `opts.autoAllow === true` записать permissions-список через `getCodeGraphPermissions()` из `shared.ts`. +10. **Cursor `--path` quirk:** если агент запускает MCP-сервер с неправильным cwd (как Cursor), в `args` MCP-конфига добавить `'--path'` с абсолютным путём (local) или `'${workspaceFolder}'` (global). См. `cursor.ts`. + +#### `uninstall(loc: Location): WriteResult` + +Должен **полностью** обратить `install`: +- Удалить MCP-запись из конфига (сохранить соседние серверы). +- Удалить permissions (если писались). +- Удалить legacy instructions-блок (`removeMarkedSection`), если он есть. +- Если файл стал пустым после удаления — удалить файл целиком (см. `codex.ts`, `cursor.ts`). +- Безопасно вызывать, когда ничего не было установлено: возвращать `not-found` / `kept`. + +#### `printConfig(loc: Location): string` + +Чистая функция (не трогает filesystem). Вернуть строку с пояснением "добавь это в X". Используется в `codegraph install --print-config ` и в README. + +#### `describePaths(loc: Location): string[]` + +Вернуть все пути, которые `install`/`uninstall` трогают при данной локации. Для оркестратора и dry-run. + +### Шаг 4 — Зарегистрировать в `registry.ts` + +В `src/installer/targets/registry.ts`: + +1. Добавить `import { novoagentTarget } from './novoagent';` +2. Добавить `novoagentTarget` в массив `ALL_TARGETS` (порядок = порядок в мультиселекте). +3. Порядок в `ALL_TARGETS` должен быть стабильным — не вставлять в середину без причины. + +### Шаг 5 — Тесты + +Параметризованный тест в `__tests__/installer-targets.test.ts` автоматически запускается для **всех** `ALL_TARGETS`. Но для нового агента, особенно если формат нестандартный (YAML, JSONC, TOML), нужно добавить **дополнительные** тест-кейсы в тот же файл или в отдельный focused describe. + +**Обязательные сценарии:** + +| Сценарий | Проверка | +|---|---| +| `install` пишет файлы | `detect.alreadyConfigured === true` после install | +| Idempotency | повторный `install` → все `action === 'unchanged'` | +| Sibling preservation | другой MCP-сервер в том же конфиге остаётся после install | +| Uninstall reverse | `uninstall` после `install` → `detect.alreadyConfigured === false` | +| `printConfig` | возвращает непустую строку | +| `describePaths` | возвращает пути, которые реально пишутся | +| `supportsLocation` | `local` skip-ается корректно для global-only агентов | +| Legacy instructions cleanup | если файл содержит `...`, install удаляет его | + +Тестовая инфраструктура: +- `setHome(tmpDir)` — перенаправляет `os.homedir()` через env-переменные (`$HOME`, `$USERPROFILE`, `$APPDATA`, `$XDG_CONFIG_HOME`, `$HERMES_HOME`). +- `process.chdir(tmpCwd)` — для локальных тестов. +- `afterEach` — удаляет `tmpHome` и `tmpCwd` через `fs.rmSync(..., { recursive: true, force: true })`. + +**Пример focused-теста для нового агента:** + +```typescript +describe('novoagent — specific', () => { + // ... setup setHome / chdir ... + + it('preserves sibling mcp server in json config', () => { + const target = getTarget('novoagent')!; + // ... write pre-existing config with another MCP server ... + target.install('global', { autoAllow: false }); + // ... assert other server still present ... + }); +}); +``` + +### Шаг 6 — CHANGELOG + +Добавить запись в `CHANGELOG.md` под `## [Unreleased]`, секция `### New Features` (если это новая фича) или `### Fixes`. + +Правила форматирования (см. CLAUDE.md > Releases > Writing changelog entries): +- **User-facing:** что изменилось и почему это важно пользователю. +- **Без internal путей:** не писать `src/installer/targets/novoagent.ts`, не писать имена функций/классов. +- **Можно:** название агента, команды (`codegraph install`), issue/PR-ссылки `(#NNN)`. +- **Пример:** + +```markdown +### New Features + +- Added support for Novo Agent as an install target. Run `codegraph install --target=novoagent` to wire the MCP server. (#NNN) +``` + +--- + +## Паттерны по форматам конфигов + +### JSON (`claude`, `cursor`) + +- Использовать `getMcpServerConfig()` из `shared.ts` для базового блока. +- JSON shape: `{ mcpServers: { codegraph: { type, command, args } } }`. +- Cursor: оборачивает `args` с `'--path', ...`. +- Idempotency: `jsonDeepEqual(before, after)` перед записью. + +### TOML (`codex`) + +- Использовать `buildTomlTable`, `upsertTomlTable`, `removeTomlTable` из `toml.ts`. +- Header: `mcp_servers.codegraph`. +- `printConfig` — строить строку через `buildTomlTable`. + +### JSONC (`opencode`) + +- Использовать `jsonc-parser` (`modify` + `applyEdits`), не `JSON.stringify`. +- Сохраняет комментарии и formatting при idempotent re-run. +- Добавить `$schema` если отсутствует при создании нового файла. + +### YAML (`hermes`) + +- Line-based парсер, не тянем YAML-библиотеку. +- `topLevelRange`, `childRange`, `listChildBlock` — ищем ключи по отступам. +- `platform_toolsets` может требовать отдельной записи (Hermes). Для других агентов YAML — возможно, только `mcp_servers`. + +### Markdown / rules file (`cursor`, `gemini`, `kiro`) + +- **Не пишем новые instructions.** +- Если файл был создан старым инсталлером (наш frontmatter + маркеры) — `removeRulesEntry()` удаляет весь файл, если в нём только наш контент; иначе удаляет только маркированный блок. +- Cursor `.mdc`: frontmatter YAML (`---\ndescription: ...\nalwaysApply: true\n---`). + +--- + +## Типичные ошибки и как их избежать + +| Ошибка | Почему плохо | Правильно | +|---|---|---| +| Перезаписать весь конфиг-файл целиком | Удаляет соседние MCP-серверы и пользовательские настройки | Surgical edit: только свой ключ | +| Писать `## CodeGraph` в instructions (#529) | Дублирует guidance из MCP initialize; агент читает дважды | Только `removeMarkedSection` для legacy cleanup | +| Забыть `jsonDeepEqual` / `arrayEqual` | Re-run выглядит как "updated", сбивает с толку | Проверять idempotency перед atomic write | +| Не atomic write | Процесс может упасть mid-write и оставить битый файл | `atomicWriteFileSync` из `shared.ts` | +| `uninstall` не удаляет legacy hooks/instructions | Пользователь после `uninstall` всё ещё видит codegraph | Удалять всё, что `install` писал, включая legacy | +| `supportsLocation('local') === true` для global-only агента | Локальный install создаёт файл, который агент никогда не прочитает | Вернуть `false` и честное сообщение в `notes` | +| Забыть `TargetId` в `types.ts` | TypeScript error в registry; CI не собирается | Добавить **до** создания target-файла | +| Нет тестов на sibling preservation | Регрессия может удалить другие MCP серверы | Добавить focused test с pre-existing конфигом | +| Не обновить `CHANGELOG.md` | Релизные ноты не отражают новую фичу | Запись в `## [Unreleased]` | + +--- + +## Чеклист перед тем, как считать задачу готовой + +- [ ] `TargetId` обновлён в `types.ts` +- [ ] `src/installer/targets/.ts` создан, имплементирует `AgentTarget` +- [ ] Используются хелперы из `shared.ts` (atomic write, JSON/TOML helpers, permissions, MCP config) +- [ ] Нет записи новых instructions — только `removeMarkedSection` для legacy cleanup (#529) +- [ ] `id` в target-файле совпадает с `TargetId` в types.ts +- [ ] `registry.ts` импортирует и включает новый target в `ALL_TARGETS` +- [ ] `install` idempotent: повторный вызов → `unchanged` +- [ ] `install` не затирает соседние MCP-серверы +- [ ] `uninstall` полностью откатывает `install` (включая permissions, legacy hooks, instructions) +- [ ] `printConfig` — чистая функция, возвращает непустую строку +- [ ] `describePaths` возвращает все пути, которые target трогает +- [ ] `supportsLocation` корректно ограничивает global-only агентов +- [ ] Тесты в `__tests__/installer-targets.test.ts` проходят (`npm test` или `npx vitest run __tests__/installer-targets.test.ts`) +- [ ] Добавлен focused test на sibling preservation / специфику формата агента +- [ ] `CHANGELOG.md` обновлён под `## [Unreleased]` From 1260558c6aa4f8e2246f4d6ecca60f2bfab9f7d5 Mon Sep 17 00:00:00 2001 From: emaxe Date: Tue, 9 Jun 2026 22:54:58 +0300 Subject: [PATCH 2/2] feat(installer): add Qwen Code as an install target Add Qwen Code (qwen) to the multi-agent installer. Qwen Code reads MCP server config from ~/.qwen/settings.json (global) or ./.qwen/settings.json (local) under the standard mcpServers.codegraph key, same shape as Claude/Cursor/Gemini. - Add 'qwen' to TargetId union - Add src/installer/targets/qwen.ts implementing AgentTarget - Register qwenTarget in ALL_TARGETS - Update README badges, CLI descriptions, and installer comments - Add 4 focused tests: install, sibling-preservation, uninstall, local - Update CHANGELOG Qwen Code has no separate permissions concept, so autoAllow is a no-op. No instructions file is written (issue #529). --- CHANGELOG.md | 1 + README.md | 10 ++- __tests__/installer-targets.test.ts | 48 +++++++++++ src/bin/codegraph.ts | 4 +- src/installer/index.ts | 2 +- src/installer/targets/qwen.ts | 124 ++++++++++++++++++++++++++++ src/installer/targets/registry.ts | 2 + src/installer/targets/types.ts | 2 +- 8 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 src/installer/targets/qwen.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c3badbbb8..b100c8644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` 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 ` 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. diff --git a/README.md b/README.md index ab147b548..313087e1b 100644 --- a/README.md +++ b/README.md @@ -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** @@ -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)
@@ -67,7 +68,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, 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.) +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.) ### 3. Initialize each project @@ -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.) @@ -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 @@ -611,6 +612,7 @@ is written): - **Gemini CLI** - **Antigravity IDE** - **Kiro** +- **Qwen Code** ## Supported Languages diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 27fcbd6e8..e6e8dcdc7 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -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 }); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 879dbb078..48f18f944 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -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 ', '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') @@ -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 ', '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 a9be118be..7de5b96ad 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -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. * diff --git a/src/installer/targets/qwen.ts b/src/installer/targets/qwen.ts new file mode 100644 index 000000000..c007e5f77 --- /dev/null +++ b/src/installer/targets/qwen.ts @@ -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(); diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts index 5e929d468..36ef3d64f 100644 --- a/src/installer/targets/registry.ts +++ b/src/installer/targets/registry.ts @@ -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, @@ -26,6 +27,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ geminiTarget, antigravityTarget, kiroTarget, + qwenTarget, ]); export function getTarget(id: string): AgentTarget | undefined { diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts index 4b3267e97..1974ed37d 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'; +export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'gemini' | 'antigravity' | 'kiro' | 'qwen'; /** * Result of `target.detect(location)`.