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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)](#)

</div>

Expand All @@ -41,7 +42,7 @@ npx @colbymchenry/codegraph # zero-install, or:
npm i -g @colbymchenry/codegraph
```

<sub>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.</sub>
<sub>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.</sub>

### Initialize Projects

Expand Down Expand Up @@ -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`)
Expand All @@ -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

Expand Down
28 changes: 28 additions & 0 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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();
});

Expand Down
2 changes: 1 addition & 1 deletion src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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
124 changes: 124 additions & 0 deletions src/installer/targets/kiro.ts
Original file line number Diff line number Diff line change
@@ -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();
2 changes: 2 additions & 0 deletions src/installer/targets/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ 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,
cursorTarget,
codexTarget,
opencodeTarget,
hermesTarget,
kiroTarget,
]);

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

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