From eeebde6095c73d0e3861b636c1e9220419c54d61 Mon Sep 17 00:00:00 2001 From: Dave Carlton Date: Sun, 24 May 2026 07:02:03 -0500 Subject: [PATCH] feat(connect): add Kilo CLI adapter Adds a connect adapter for Kilo (kilo-code), a fork of OpenCode. Kilo uses a different MCP config shape with a top-level "mcp" key and command as an array, rather than the standard mcpServers object. The adapter: - Detects Kilo via ~/.config/kilo/ directory - Writes to kilo.jsonc > kilo.json > opencode.jsonc > opencode.json - Uses the correct MCP entry shape: { type: "local", command: [...], environment: {...}, enabled: true } - Notes that Kilo implements memory hooks natively (no plugin needed) --- src/cli/connect/index.ts | 2 + src/cli/connect/kilo.ts | 112 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/cli/connect/kilo.ts diff --git a/src/cli/connect/index.ts b/src/cli/connect/index.ts index 48c23c66..0b3fbbed 100644 --- a/src/cli/connect/index.ts +++ b/src/cli/connect/index.ts @@ -9,6 +9,7 @@ import { adapter as hermes } from "./hermes.js"; import { adapter as openclaw } from "./openclaw.js"; import { adapter as openhuman } from "./openhuman.js"; import { adapter as pi } from "./pi.js"; +import { adapter as kilo } from "./kilo.js"; export const ADAPTERS: readonly ConnectAdapter[] = [ claudeCode, @@ -19,6 +20,7 @@ export const ADAPTERS: readonly ConnectAdapter[] = [ hermes, pi, openhuman, + kilo, ]; export function resolveAdapter(name: string): ConnectAdapter | null { diff --git a/src/cli/connect/kilo.ts b/src/cli/connect/kilo.ts new file mode 100644 index 00000000..0bb40571 --- /dev/null +++ b/src/cli/connect/kilo.ts @@ -0,0 +1,112 @@ +import { existsSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import type { ConnectAdapter, ConnectOptions, ConnectResult } from "./types.js"; +import { + backupFile, + logAlreadyWired, + logBackup, + logInstalled, + readJsonSafe, + writeJsonAtomic, +} from "./util.js"; + +type McpEntry = { + type: string; + command: string[]; + environment?: Record; + enabled?: boolean; +}; + +type KiloConfig = { + mcp?: Record; + [key: string]: unknown; +}; + +// Env values use ${VAR} expansion so the wired MCP entry inherits +// AGENTMEMORY_URL / AGENTMEMORY_SECRET from the user's shell. +const AGENTMEMORY_MCP_BLOCK: McpEntry = { + type: "local", + command: ["npx", "-y", "@agentmemory/mcp"], + environment: { + AGENTMEMORY_URL: "${AGENTMEMORY_URL}", + AGENTMEMORY_SECRET: "${AGENTMEMORY_SECRET}", + }, + enabled: true, +}; + +function entryMatches(entry: unknown): boolean { + if (!entry || typeof entry !== "object") return false; + const e = entry as Record; + const cmd = Array.isArray(e["command"]) ? (e["command"] as string[]) : []; + return cmd.some((c) => c.includes("@agentmemory/mcp")); +} + +const configDir = join(homedir(), ".config", "kilo"); +const configFiles = ["kilo.jsonc", "kilo.json", "opencode.jsonc", "opencode.json"]; + +function findConfigFile(): string | null { + for (const file of configFiles) { + const fullPath = join(configDir, file); + if (existsSync(fullPath)) return fullPath; + } + return null; +} + +export const adapter: ConnectAdapter = { + name: "kilo", + displayName: "Kilo", + docs: "https://github.com/rohitg00/agentmemory/tree/main/integrations/kilo", + protocolNote: "→ Using MCP. Kilo implements memory hooks natively in its codebase.", + + detect(): boolean { + return existsSync(configDir) || findConfigFile() !== null; + }, + + async install(opts: ConnectOptions): Promise { + const configPath = findConfigFile() ?? join(configDir, "kilo.json"); + const existing = readJsonSafe(configPath); + const next: KiloConfig = existing ? { ...existing } : {}; + const mcp: Record = { + ...((next.mcp as Record) ?? {}), + }; + + const alreadyHas = entryMatches(mcp["agentmemory"]); + if (alreadyHas && !opts.force) { + logAlreadyWired("Kilo", configPath); + return { kind: "already-wired", mutatedPath: configPath }; + } + + if (opts.dryRun) { + console.log( + `[dry-run] Would ${alreadyHas ? "overwrite" : "add"} mcp.agentmemory in ${configPath}`, + ); + return { kind: "installed", mutatedPath: configPath }; + } + + let backupPath: string | undefined; + if (existsSync(configPath)) { + backupPath = backupFile(configPath, "kilo"); + logBackup(backupPath); + } + + mcp["agentmemory"] = AGENTMEMORY_MCP_BLOCK; + next.mcp = mcp; + writeJsonAtomic(configPath, next); + + const verify = readJsonSafe(configPath); + if (!entryMatches(verify?.mcp?.["agentmemory"])) { + console.error( + `Verification failed: ${configPath} did not contain mcp.agentmemory after write.`, + ); + return { kind: "skipped", reason: "verification-failed" }; + } + + logInstalled("Kilo", configPath); + return { + kind: "installed", + mutatedPath: configPath, + ...(backupPath !== undefined && { backupPath }), + }; + }, +};