diff --git a/README.md b/README.md index 4e4507410f4..6a8f4c916eb 100644 --- a/README.md +++ b/README.md @@ -128,11 +128,12 @@ Inside a project's Studio, the same design system streams out multiple artifact | [Pi Agent](https://github.com/badlogic/pi-mono) | ✅ Supported | `od mcp install pi` | | [Mistral Vibe CLI](https://github.com/mistralai/mistral-vibe) | ✅ Supported | `od mcp install vibe` | | [Hermes Agent](https://github.com/nousresearch/hermes-agent) | ✅ Supported | `od mcp install hermes` | +| [Kimchi](https://github.com/getkimchi/kimchi) | ✅ Supported | `od mcp install kimchi` | `od mcp install --print` for a dry-run preview · `--uninstall` to remove · full list with `od mcp install --help`.

- The 25 coding-agent CLIs Open Design supports — Claude Code · Codex · OpenCode · Hermes · Antigravity · Vela · Grok Build · Kimi · Cursor Agent · Qwen · Qoder · GitHub Copilot · Pi · Kiro · Kilo · Mistral Vibe · DeepSeek · Reasonix · Aider · Amp · CodeBuddy · Mimo · AtomCode · Devin · Trae + The 26 coding-agent CLIs Open Design supports — Claude Code · Codex · OpenCode · Hermes · Antigravity · Vela · Grok Build · Kimi · Cursor Agent · Qwen · Qoder · GitHub Copilot · Pi · Kiro · Kilo · Mistral Vibe · DeepSeek · Reasonix · Aider · Amp · CodeBuddy · Mimo · AtomCode · Devin · Trae · Kimchi

**No CLI installed?** The BYOK proxy at `POST /api/proxy/{anthropic,openai,azure,google,ollama,senseaudio}/stream` gives you the same loop (no process spawn) — paste `baseUrl` + `apiKey` + `model`, with presets for OpenAI, Atlas Cloud, Anthropic, Azure OpenAI, Google Gemini, Ollama, LM Studio, vLLM, or any OpenAI-compatible endpoint. Atlas Cloud uses `https://api.atlascloud.ai/v1` with your own key and OpenAI-compatible model ids such as `qwen/qwen3.5-flash`. Per-target SSRF protection blocks internal IPs / link-local / CGNAT at the daemon edge. @@ -571,7 +572,7 @@ Plugin registry endpoint: `GET /api/plugins`. Directory overview → [`plugins/R ┌──────────────────────────────────────────────────────────────────┐ │ Local runtime definitions come from runtimes/registry.ts; │ │ the base registry has 26 definitions (including byok-opencode), │ - │ backed by 25 distinct local CLI executables because byok-opencode shares │ + │ backed by 26 distinct local CLI executables because byok-opencode shares │ │ the OpenCode executable. See docs/agent-adapters.md. │ │ composes a functional skill or design template + DESIGN.md; writes files │ └──────────────────────────────────────────────────────────────────┘ diff --git a/apps/daemon/src/app-config.ts b/apps/daemon/src/app-config.ts index 0c2c8356dfc..7d450349210 100644 --- a/apps/daemon/src/app-config.ts +++ b/apps/daemon/src/app-config.ts @@ -211,6 +211,7 @@ const AGENT_CLI_ENV_KEYS: ReadonlyMap> = new Map([ ['kimi', new Set(['KIMI_BIN'])], ['kiro', new Set(['KIRO_BIN'])], ['kilo', new Set(['KILO_BIN'])], + ['kimchi', new Set(['KIMCHI_BIN', 'KIMCHI_API_KEY'])], ['opencode', new Set(['OPENCODE_BIN'])], ['pi', new Set(['PI_BIN'])], ['qoder', new Set(['QODER_BIN'])], diff --git a/apps/daemon/src/mcp-agent-install.ts b/apps/daemon/src/mcp-agent-install.ts index dee1a1a234a..f67c59fa7d9 100644 --- a/apps/daemon/src/mcp-agent-install.ts +++ b/apps/daemon/src/mcp-agent-install.ts @@ -46,6 +46,7 @@ export const AGENT_SLUGS = [ 'kiro', 'trae', 'opencode', + 'kimchi', ] as const; export type AgentSlug = (typeof AGENT_SLUGS)[number]; @@ -298,6 +299,15 @@ export function planAgentInstall( serverKey: serverName, entry: jsonEntry(spec), }; + case 'kimchi': + return { + kind: 'json', + slug, + configPath: path.join(home, '.config', 'kimchi', 'harness', 'mcp.json'), + keyPath: ['mcpServers'], + serverKey: serverName, + entry: jsonEntry(spec), + }; // ----- Unverified formats: print-only, never write ----- case 'vibe': diff --git a/apps/daemon/src/memory-llm.ts b/apps/daemon/src/memory-llm.ts index 0a22c23b735..436082b3296 100644 --- a/apps/daemon/src/memory-llm.ts +++ b/apps/daemon/src/memory-llm.ts @@ -283,8 +283,8 @@ function chatProtocolFromAgentId(agentId) { if (id === 'claude') return 'anthropic'; if (id === 'gemini') return 'google'; // Codex, OpenCode, Qwen, DeepSeek, Kimi, Copilot, Pi, Kiro, Kilo, - // Vibe, Devin, Hermes, Cursor-Agent, Qoder all use the OpenAI chat- - // completions wire format. + // Vibe, Devin, Hermes, Cursor-Agent, Qoder, Kimchi all use the + // OpenAI chat-completions wire format. if ( id === 'codex' || id === 'opencode' @@ -300,6 +300,7 @@ function chatProtocolFromAgentId(agentId) { || id === 'hermes' || id === 'cursor-agent' || id === 'qoder' + || id === 'kimchi' ) { return 'openai'; } diff --git a/apps/daemon/src/runtimes/defs/kimchi.ts b/apps/daemon/src/runtimes/defs/kimchi.ts new file mode 100644 index 00000000000..b471f457721 --- /dev/null +++ b/apps/daemon/src/runtimes/defs/kimchi.ts @@ -0,0 +1,73 @@ +import path from "node:path"; +import { + DEFAULT_MODEL_OPTION, + execAgentFile, + parsePiModels, +} from "./shared.js"; +import type { RuntimeAgentDef } from "../types.js"; + +export const kimchiAgentDef = { + id: "kimchi", + name: "Kimchi", + bin: "kimchi", + versionArgs: ["--version"], + // Kimchi is built on the pi-mono SDK (same engine as pi), so it inherits + // pi's `--mode rpc` JSON-RPC-over-stdio transport, `--list-models` TSV + // output, `--thinking` levels, and `--append-system-prompt` for extra + // directories. Using pi-rpc instead of ACP gives us multimodal image + // support, reasoning levels, richer event stream, and avoids the ACP + // `mcpServers` rejection issue. + versionProbeTimeoutMs: 15_000, + fetchModels: async (resolvedBin, env) => { + try { + // Unlike pi (which prints to stderr), kimchi outputs the TSV table + // to stdout. Stderr carries config permission warnings only. + const { stdout } = await execAgentFile(resolvedBin, ["--list-models"], { + env, + timeout: 60_000, + maxBuffer: 8 * 1024 * 1024, + }); + const parsed = parsePiModels(stdout); + if (!parsed || parsed.length === 0) return null; + return parsed; + } catch { + return null; + } + }, + fallbackModels: [ + DEFAULT_MODEL_OPTION, + { id: "kimi-k2.7", label: "kimi-k2.7" }, + { id: "minimax-m3", label: "minimax-m3" }, + { id: "deepseek-v4-flash", label: "deepseek-v4-flash" }, + ], + reasoningOptions: [ + { id: "default", label: "Default" }, + { id: "off", label: "Off" }, + { id: "minimal", label: "Minimal" }, + { id: "low", label: "Low" }, + { id: "medium", label: "Medium" }, + { id: "high", label: "High" }, + { id: "xhigh", label: "XHigh" }, + ], + buildArgs: (_prompt, _imagePaths, extraAllowedDirs = [], options = {}) => { + const args = ["--mode", "rpc"]; + if (options.model && options.model !== "default") { + args.push("--model", options.model); + } + if (options.reasoning && options.reasoning !== "default") { + args.push("--thinking", options.reasoning); + } + const dirs = (extraAllowedDirs || []).filter( + (d) => typeof d === "string" && path.isAbsolute(d), + ); + for (const d of dirs) { + args.push("--append-system-prompt", d); + } + return args; + }, + promptViaStdin: true, + streamFormat: "pi-rpc", + supportsImagePaths: true, + installUrl: "https://kimchi.dev", + docsUrl: "https://docs.kimchi.dev", +} satisfies RuntimeAgentDef; diff --git a/apps/daemon/src/runtimes/executables.ts b/apps/daemon/src/runtimes/executables.ts index a9b3c46adff..13014dc66f4 100644 --- a/apps/daemon/src/runtimes/executables.ts +++ b/apps/daemon/src/runtimes/executables.ts @@ -27,6 +27,7 @@ const AGENT_BIN_ENV_KEYS = new Map([ ['kimi', 'KIMI_BIN'], ['kiro', 'KIRO_BIN'], ['kilo', 'KILO_BIN'], + ['kimchi', 'KIMCHI_BIN'], ['mimo', 'MIMO_BIN'], ['opencode', 'OPENCODE_BIN'], ['byok-opencode', 'OPENCODE_BIN'], diff --git a/apps/daemon/src/runtimes/registry.ts b/apps/daemon/src/runtimes/registry.ts index cbf9a39d614..b60e3696490 100644 --- a/apps/daemon/src/runtimes/registry.ts +++ b/apps/daemon/src/runtimes/registry.ts @@ -23,6 +23,7 @@ import { antigravityAgentDef } from './defs/antigravity.js'; import { codebuddyAgentDef } from './defs/codebuddy.js'; import { reasonixAgentDef } from './defs/reasonix.js'; import { mimoAgentDef } from './defs/mimo.js'; +import { kimchiAgentDef } from './defs/kimchi.js'; import { atomcodeAgentDef } from './defs/atomcode.js'; import { readLocalAgentProfileDefs as readLocalAgentProfileDefsFromFile } from './local-profiles.js'; import type { RuntimeAgentDef } from './types.js'; @@ -53,6 +54,7 @@ const BASE_AGENT_DEFS: RuntimeAgentDef[] = [ reasonixAgentDef, codebuddyAgentDef, mimoAgentDef, + kimchiAgentDef, atomcodeAgentDef, ]; diff --git a/apps/daemon/tests/runtimes/env-and-detection.test.ts b/apps/daemon/tests/runtimes/env-and-detection.test.ts index ba75870cfc0..1ca1fb5c655 100644 --- a/apps/daemon/tests/runtimes/env-and-detection.test.ts +++ b/apps/daemon/tests/runtimes/env-and-detection.test.ts @@ -489,6 +489,7 @@ test('resolveAgentExecutable supports configured binary overrides for non-Codex ['deepseek', 'deepseek', 'DEEPSEEK_BIN'], ['trae-cli', 'traecli', 'TRAE_CLI_BIN'], ['aider', 'aider', 'AIDER_BIN'], + ['kimchi', 'kimchi', 'KIMCHI_BIN'], ]; const dir = mkdtempSync(join(tmpdir(), 'od-agent-bin-overrides-')); try { diff --git a/apps/daemon/tests/runtimes/helpers/test-helpers.ts b/apps/daemon/tests/runtimes/helpers/test-helpers.ts index bcd2a3c80ce..01fd1a6b081 100644 --- a/apps/daemon/tests/runtimes/helpers/test-helpers.ts +++ b/apps/daemon/tests/runtimes/helpers/test-helpers.ts @@ -89,6 +89,7 @@ export const qoder = requireAgent('qoder'); export const qwen = requireAgent('qwen'); export const opencode = requireAgent('opencode'); export const mimo = requireAgent('mimo'); +export const kimchi = requireAgent('kimchi'); export const grokBuild = requireAgent('grok-build'); export const aider = requireAgent('aider'); export const antigravity = requireAgent('antigravity'); diff --git a/apps/web/public/agent-icons/kimchi.png b/apps/web/public/agent-icons/kimchi.png new file mode 100644 index 00000000000..d774d724207 Binary files /dev/null and b/apps/web/public/agent-icons/kimchi.png differ diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 0e16dc21a35..dd7f6c3419c 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -346,6 +346,7 @@ const CANONICAL_AGENT_ORDER = [ 'pi', 'kiro', 'kilo', + 'kimchi', 'vibe', 'deepseek', 'aider', diff --git a/apps/web/src/components/AgentIcon.tsx b/apps/web/src/components/AgentIcon.tsx index 6b80e0b0a3b..543e9b5d046 100644 --- a/apps/web/src/components/AgentIcon.tsx +++ b/apps/web/src/components/AgentIcon.tsx @@ -27,6 +27,7 @@ const ICON_EXT: Record = { hermes: 'svg', 'grok-build': 'svg', kimi: 'svg', + kimchi: 'png', pi: 'svg', kiro: 'svg', kilo: 'svg', diff --git a/apps/web/src/components/HandoffButton.tsx b/apps/web/src/components/HandoffButton.tsx index f853f42334f..9838399cad7 100644 --- a/apps/web/src/components/HandoffButton.tsx +++ b/apps/web/src/components/HandoffButton.tsx @@ -74,6 +74,7 @@ const CLI_ORDER = [ 'devin', 'kiro', 'kilo', + 'kimchi', 'vibe', 'antigravity', 'aider', @@ -95,6 +96,7 @@ const FALLBACK_CLI_TARGETS: CliTarget[] = [ { id: 'deepseek', name: 'DeepSeek TUI', bin: 'deepseek', available: false }, { id: 'kimi', name: 'Kimi CLI', bin: 'kimi', available: false }, { id: 'hermes', name: 'Hermes', bin: 'hermes', available: false }, + { id: 'kimchi', name: 'Kimchi', bin: 'kimchi', available: false }, { id: 'devin', name: 'Devin for Terminal', bin: 'devin', available: false }, { id: 'kiro', name: 'Kiro CLI', bin: 'kiro-cli', available: false }, { id: 'kilo', name: 'Kilo', bin: 'kilo', available: false }, diff --git a/apps/web/src/components/MemoryModelInline.tsx b/apps/web/src/components/MemoryModelInline.tsx index 64f316cfb67..e6e0b2c5b0d 100644 --- a/apps/web/src/components/MemoryModelInline.tsx +++ b/apps/web/src/components/MemoryModelInline.tsx @@ -123,6 +123,7 @@ function chatProtocolFromAgent( || id === 'hermes' || id === 'cursor-agent' || id === 'qoder' + || id === 'kimchi' ) { return 'openai'; } diff --git a/docs/agent-adapters.md b/docs/agent-adapters.md index b6fb195ac77..6c48fc893da 100644 --- a/docs/agent-adapters.md +++ b/docs/agent-adapters.md @@ -350,7 +350,18 @@ the active-run staging implementation is in shared failure classifier emits DeepSeek-specific guidance for those two configuration paths instead of returning the raw non-actionable error. -### 5.12 Plain stream artifact handoff +### 5.12 Kimchi + +- Invocation: `kimchi --mode rpc [--model ] [--thinking ] [--append-system-prompt …]`, with the composed prompt delivered over stdin via JSON-RPC. Kimchi is built on the [pi-mono](https://github.com/badlogic/pi-mono) coding agent SDK and inherits pi's RPC transport. Using `--mode rpc` (instead of `--mode acp`) gives us multimodal image support, reasoning levels, richer event stream, and avoids the ACP `mcpServers` rejection issue. +- Streaming: `pi-rpc` JSON-RPC over stdio. Events include `agent_start`, `turn_start/end`, `message_update` (text deltas, thinking deltas, tool calls), `tool_execution_start/end`, `compaction_start`, `auto_retry_start/end`, `extension_error`. `apps/daemon/src/pi-rpc.ts` maps these onto the same UI event set as the other stream parsers. +- Models: dynamic — `kimchi --list-models` prints a TSV table to stdout that the daemon parses into provider/model picker entries. Fallback hints for the default orchestrator/builder/explorer models (`kimi-k2.7`, `minimax-m3`, `deepseek-v4-flash`) are shipped for when the list command times out. +- Images: kimchi's RPC `prompt` command supports an `images` field (base64-encoded `ImageContent` objects), inherited from pi-mono. The daemon reads validated `imagePaths` at session attach time and includes them in the prompt command. +- Thinking: the daemon exposes kimchi's `--thinking` levels (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`) in the Settings model picker. +- Skills: prompt injection in v1. `extraAllowedDirs` (skill seed and design-system directories) are forwarded as `--append-system-prompt` repeatable flags so the agent knows these directories exist and can Read files inside them. Same pattern as the pi adapter. +- MCP: Kimchi does not accept `mcpServers` in the ACP `session/new` message — it rejects with `Invalid params: mcpServers is not supported`. MCP servers are configured via `~/.config/kimchi/harness/mcp.json` (same `mcpServers` JSON schema as Claude Desktop / Cursor). `od mcp install kimchi` writes to the JSON config file directly. +- Auth: API key resolved from `KIMCHI_API_KEY` env var or `~/.config/kimchi/config.json`. Run `kimchi setup` for interactive first-time configuration. + +### 5.13 Plain stream artifact handoff Adapters with `streamFormat: 'plain'` do not expose structured file-write tool calls to the daemon. Their stdout is still a valid artifact handoff when the model emits Anthropic-style source blocks: diff --git a/packages/contracts/src/analytics/events/mappers.ts b/packages/contracts/src/analytics/events/mappers.ts index dc872c6f50b..e0acc97eb02 100644 --- a/packages/contracts/src/analytics/events/mappers.ts +++ b/packages/contracts/src/analytics/events/mappers.ts @@ -194,6 +194,8 @@ export function agentIdToTracking(agentId: string | null | undefined): TrackingC return 'pi'; case 'kilo': return 'kilo'; + case 'kimchi': + return 'kimchi'; case 'amr': return 'amr'; default: diff --git a/packages/contracts/src/analytics/events/shared-enums.ts b/packages/contracts/src/analytics/events/shared-enums.ts index 12c9861232e..ef2b7403f50 100644 --- a/packages/contracts/src/analytics/events/shared-enums.ts +++ b/packages/contracts/src/analytics/events/shared-enums.ts @@ -159,6 +159,7 @@ export type TrackingCliProviderId = | 'github_copilot_cli' | 'pi' | 'kilo' + | 'kimchi' | 'amr' | 'other'; diff --git a/packages/contracts/src/analytics/events/ui-click.ts b/packages/contracts/src/analytics/events/ui-click.ts index 519df6cbab8..42f1cfd1f4e 100644 --- a/packages/contracts/src/analytics/events/ui-click.ts +++ b/packages/contracts/src/analytics/events/ui-click.ts @@ -1183,7 +1183,7 @@ export const TRACKING_HANDOFF_TARGET_IDS = [ // code-agent CLIs (HandoffButton CLI_ORDER; qoder / antigravity already above) 'amr', 'claude', 'codex', 'opencode', 'cursor-agent', 'gemini', 'qwen', 'copilot', 'grok-build', 'deepseek', 'kimi', 'hermes', 'devin', 'kiro', - 'kilo', 'vibe', 'aider', 'trae-cli', 'pi', 'reasonix', + 'kilo', 'kimchi', 'vibe', 'aider', 'trae-cli', 'pi', 'reasonix', ] as const; export type TrackingHandoffTargetId =