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 @@
[](#supported-agents)
[](#supported-agents)
[](#supported-agents)
+[](#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)`.