diff --git a/README.md b/README.md index ab147b548..8e230cd1b 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 Kilo 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) +[![Kilo](https://img.shields.io/badge/Kilo-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 Kilo — 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 @@ -611,6 +612,7 @@ is written): - **Gemini CLI** - **Antigravity IDE** - **Kiro** +- **Kilo** ## Supported Languages diff --git a/src/installer/index.ts b/src/installer/index.ts index a9be118be..0a2e88390 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -318,8 +318,8 @@ export async function runUninstaller(opts: RunUninstallerOptions): Promise const sel = await clack.select({ message: 'Remove CodeGraph from all your projects, or just this one?', options: [ - { value: 'global' as const, label: 'All projects (global)', hint: '~/.claude, ~/.cursor, ~/.codex, ~/.config/opencode, ~/.hermes, ~/.gemini, ~/.kiro' }, - { value: 'local' as const, label: 'Just this project (local)', hint: './.claude, ./.cursor, ./opencode.jsonc, ./.gemini, ./.kiro' }, + { value: 'global' as const, label: 'All projects (global)', hint: '~/.claude, ~/.cursor, ~/.codex, ~/.config/opencode, ~/.hermes, ~/.gemini, ~/.kiro, ~/.config/kilo' }, + { value: 'local' as const, label: 'Just this project (local)', hint: './.claude, ./.cursor, ./opencode.jsonc, ./.gemini, ./.kiro, kilo.json' }, ], initialValue: 'global' as const, }); diff --git a/src/installer/targets/kilo.ts b/src/installer/targets/kilo.ts new file mode 100644 index 000000000..73932a189 --- /dev/null +++ b/src/installer/targets/kilo.ts @@ -0,0 +1,145 @@ +/** + * Kilo CLI / IDE target. Writes: + * + * - MCP server entry to `kilo.json` (project) or + * `~/.config/kilo/kilo.json` (global). Standard `mcp.codegraph` + * shape using Kilo's MCP config format. + * + * Kilo supports MCP via `kilo.json` with the following structure: + * + * { + * "mcp": { + * "codegraph": { + * "type": "local", + * "command": ["codegraph", "serve", "--mcp"], + * "enabled": true, + * "timeout": 30000 + * } + * } + * } + * + * No permissions concept — Kilo manages tool permissions through its + * own permission system in `kilo.json`. + * + * Docs: https://kilo.ai/docs + */ + +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(), '.config', 'kilo') + : path.join(process.cwd(), '.kilo'); +} +function kiloJsonPath(loc: Location): string { + return path.join(configDir(loc), 'kilo.json'); +} + +class KiloTarget implements AgentTarget { + readonly id = 'kilo' as const; + readonly displayName = 'Kilo'; + readonly docsUrl = 'https://kilo.ai/docs'; + + supportsLocation(_loc: Location): boolean { + return true; + } + + detect(loc: Location): DetectionResult { + const file = kiloJsonPath(loc); + const config = readJsonFile(file); + const alreadyConfigured = !!config.mcp?.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, + notes: ['Restart Kilo for MCP changes to take effect.'], + }; + } + + uninstall(loc: Location): WriteResult { + const files: WriteResult['files'] = []; + + const file = kiloJsonPath(loc); + const config = readJsonFile(file); + if (config.mcp?.codegraph) { + delete config.mcp.codegraph; + if (Object.keys(config.mcp).length === 0) { + delete config.mcp; + } + 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 = kiloJsonPath(loc); + const snippet = JSON.stringify({ + mcp: { + codegraph: getKiloMcpConfig(), + }, + }, null, 2); + return `# Add to ${target}\n\n${snippet}\n`; + } + + describePaths(loc: Location): string[] { + return [kiloJsonPath(loc)]; + } +} + +function getKiloMcpConfig(): Record { + const base = getMcpServerConfig(); + return { + type: 'local', + command: base.command, + enabled: true, + timeout: 30000, + }; +} + +function writeMcpEntry(loc: Location): WriteResult['files'][number] { + const file = kiloJsonPath(loc); + const dir = path.dirname(file); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + const existing = readJsonFile(file); + const before = existing.mcp?.codegraph; + const after = getKiloMcpConfig(); + + if (jsonDeepEqual(before, after)) { + return { path: file, action: 'unchanged' }; + } + const action: 'created' | 'updated' = + before ? 'updated' : (fs.existsSync(file) ? 'updated' : 'created'); + if (!existing.mcp) existing.mcp = {}; + existing.mcp.codegraph = after; + writeJsonFile(file, existing); + return { path: file, action }; +} + +export const kiloTarget: AgentTarget = new KiloTarget(); diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts index 5e929d468..c6137ab7c 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 { kiloTarget } from './kilo'; export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ claudeTarget, @@ -26,6 +27,7 @@ export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ geminiTarget, antigravityTarget, kiroTarget, + kiloTarget, ]); export function getTarget(id: string): AgentTarget | undefined { diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts index 4b3267e97..a028b4a4f 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' | 'kilo'; /** * Result of `target.detect(location)`.