diff --git a/README.md b/README.md index 598ac5b0..f68bceee 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ [![Codex CLI](https://img.shields.io/badge/Codex_CLI-supported-blueviolet.svg)](#) [![opencode](https://img.shields.io/badge/opencode-supported-blueviolet.svg)](#) [![Hermes Agent](https://img.shields.io/badge/Hermes_Agent-supported-blueviolet.svg)](#) +[![Kiro](https://img.shields.io/badge/Kiro-supported-blueviolet.svg)](#) @@ -41,7 +42,7 @@ npx @colbymchenry/codegraph # zero-install, or: npm i -g @colbymchenry/codegraph ``` -CodeGraph bundles its own runtime — nothing to compile, no native build, works the same everywhere. The interactive installer auto-configures your agent(s) — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent. +CodeGraph bundles its own runtime — nothing to compile, no native build, works the same everywhere. The interactive installer auto-configures your agent(s) — Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Kiro. ### Initialize Projects @@ -161,7 +162,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** +- Ask which agent(s) to configure — auto-detects installed ones from: **Claude Code**, **Cursor**, **Codex CLI**, **opencode**, **Hermes Agent**, **Kiro** - 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 + an instructions file (e.g. `CLAUDE.md`, `.cursor/rules/codegraph.mdc`, `~/.codex/AGENTS.md`) @@ -187,7 +188,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) for the MCP server to load. +Restart your agent (Claude Code / Cursor / Codex CLI / opencode / Hermes Agent / Kiro) for the MCP server to load. ### 3. Initialize Projects diff --git a/__tests__/installer-targets.test.ts b/__tests__/installer-targets.test.ts index 44e90d68..28cf6ac7 100644 --- a/__tests__/installer-targets.test.ts +++ b/__tests__/installer-targets.test.ts @@ -363,6 +363,33 @@ describe('Installer targets — partial-state idempotency', () => { expect(body).toContain('custom:\n keep: true'); }); + it('kiro: writes to .kiro/settings/mcp.json (nested settings dir, not .kiro root)', () => { + const kiro = getTarget('kiro')!; + const local = kiro.install('local', { autoAllow: false }); + expect(local.files[0].path.replace(/\\/g, '/')).toMatch(/\/\.kiro\/settings\/mcp\.json$/); + expect(fs.existsSync(path.join(tmpCwd, '.kiro', 'mcp.json'))).toBe(false); + + const global = kiro.install('global', { autoAllow: false }); + expect(global.files[0].path.replace(/\\/g, '/')).toMatch(/\/\.kiro\/settings\/mcp\.json$/); + expect(fs.existsSync(path.join(tmpHome, '.kiro', 'mcp.json'))).toBe(false); + }); + + it('kiro: install writes mcpServers.codegraph and uninstall strips it cleanly', () => { + const kiro = getTarget('kiro')!; + kiro.install('local', { autoAllow: false }); + const file = path.join(tmpCwd, '.kiro', 'settings', 'mcp.json'); + const after = JSON.parse(fs.readFileSync(file, 'utf-8')); + expect(after.mcpServers.codegraph).toEqual({ + type: 'stdio', + command: 'codegraph', + args: ['serve', '--mcp'], + }); + + kiro.uninstall('local'); + const final = JSON.parse(fs.readFileSync(file, 'utf-8')); + expect(final.mcpServers).toBeUndefined(); + }); + it('opencode: uninstall removes only mcp.codegraph, preserves comments and siblings', () => { const opencode = getTarget('opencode')!; const dir = path.join(tmpHome, '.config', 'opencode'); @@ -616,6 +643,7 @@ describe('Installer targets — registry', () => { expect(getTarget('codex')?.id).toBe('codex'); expect(getTarget('opencode')?.id).toBe('opencode'); expect(getTarget('hermes')?.id).toBe('hermes'); + expect(getTarget('kiro')?.id).toBe('kiro'); expect(getTarget('not-a-real-target')).toBeUndefined(); }); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index dac8ce1e..b6c9100d 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -1341,7 +1341,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, Kiro)') .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') diff --git a/src/installer/targets/kiro.ts b/src/installer/targets/kiro.ts new file mode 100644 index 00000000..102710af --- /dev/null +++ b/src/installer/targets/kiro.ts @@ -0,0 +1,124 @@ +/** + * Kiro target (kiro.dev). + * + * - MCP server entry to `~/.kiro/settings/mcp.json` (global = user + * scope) or `./.kiro/settings/mcp.json` (local = workspace scope). + * Same `{ mcpServers: { codegraph: {...} } }` shape as Claude / + * Cursor. Kiro auto-reconnects on file save, so no restart note is + * emitted (unlike Cursor). + * Docs: https://kiro.dev/docs/mcp/configuration/ + * + * Kiro reads both files and merges with workspace precedence — same + * pattern as Cursor and opencode. Nothing here needs an `--path` + * workaround like Cursor's: Kiro launches MCP servers with the + * workspace as cwd, so codegraph's normal `process.cwd()` resolution + * finds `.codegraph/` correctly. + * + * No permissions / auto-allow surface — Kiro doesn't expose one the + * installer can populate, so `autoAllow` is silently ignored. No + * project-local instructions / steering surface is written by this + * target; users who want a Kiro steering file can drop one under + * `.kiro/steering/` themselves. + */ + +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 kiroConfigDir(loc: Location): string { + return loc === 'global' + ? path.join(os.homedir(), '.kiro') + : path.join(process.cwd(), '.kiro'); +} + +function mcpJsonPath(loc: Location): string { + return path.join(kiroConfigDir(loc), 'settings', 'mcp.json'); +} + +class KiroTarget implements AgentTarget { + readonly id = 'kiro' as const; + readonly displayName = 'Kiro'; + readonly docsUrl = 'https://kiro.dev/docs/mcp/configuration/'; + + supportsLocation(_loc: Location): boolean { + return true; + } + + detect(loc: Location): DetectionResult { + const mcpPath = mcpJsonPath(loc); + const config = readJsonFile(mcpPath); + const alreadyConfigured = !!config.mcpServers?.codegraph; + // "Installed" heuristic: presence of a `.kiro` dir at the + // location. Documented Kiro install flow doesn't promise a binary + // on PATH, but both the user-global config and the workspace + // config live under `.kiro/`, so its existence is the strongest + // signal we have. + const installed = fs.existsSync(kiroConfigDir(loc)); + return { installed, alreadyConfigured, configPath: mcpPath }; + } + + install(loc: Location, _opts: InstallOptions): WriteResult { + return { files: [writeMcpEntry(loc)] }; + } + + uninstall(loc: Location): WriteResult { + const mcpPath = mcpJsonPath(loc); + const config = readJsonFile(mcpPath); + if (!config.mcpServers?.codegraph) { + return { files: [{ path: mcpPath, action: 'not-found' }] }; + } + delete config.mcpServers.codegraph; + if (Object.keys(config.mcpServers).length === 0) { + delete config.mcpServers; + } + writeJsonFile(mcpPath, config); + return { files: [{ path: mcpPath, action: 'removed' }] }; + } + + printConfig(loc: Location): string { + const target = mcpJsonPath(loc); + const snippet = JSON.stringify( + { mcpServers: { codegraph: getMcpServerConfig() } }, + null, + 2, + ); + return `# Add to ${target}\n\n${snippet}\n`; + } + + describePaths(loc: Location): string[] { + return [mcpJsonPath(loc)]; + } +} + +function writeMcpEntry(loc: Location): WriteResult['files'][number] { + const file = mcpJsonPath(loc); + 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 kiroTarget: AgentTarget = new KiroTarget(); diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts index 0091ab64..3a0322d8 100644 --- a/src/installer/targets/registry.ts +++ b/src/installer/targets/registry.ts @@ -13,6 +13,7 @@ import { cursorTarget } from './cursor'; import { codexTarget } from './codex'; import { opencodeTarget } from './opencode'; import { hermesTarget } from './hermes'; +import { kiroTarget } from './kiro'; export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ claudeTarget, @@ -20,6 +21,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ codexTarget, opencodeTarget, hermesTarget, + kiroTarget, ]); export function getTarget(id: string): AgentTarget | undefined { diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts index 290f13ce..5343bd95 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'; +export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode' | 'hermes' | 'kiro'; /** * Result of `target.detect(location)`.