diff --git a/cli/src/cli/args.ts b/cli/src/cli/args.ts index beadad1d..377fdcf5 100644 --- a/cli/src/cli/args.ts +++ b/cli/src/cli/args.ts @@ -12,7 +12,7 @@ export function createProgram(): Command { program .name("ralphy") .description( - "Autonomous AI Coding Loop - Supports Claude Code, OpenCode, Codex, Cursor, Qwen-Code, Factory Droid and GitHub Copilot", + "Autonomous AI Coding Loop - Supports Claude Code, OpenCode, Codex, Cursor, Qwen-Code, Factory Droid, GitHub Copilot and Kimi Code", ) .version(VERSION) .argument("[task]", "Single task to execute (brownfield mode)") @@ -30,6 +30,7 @@ export function createProgram(): Command { .option("--droid", "Use Factory Droid") .option("--copilot", "Use GitHub Copilot") .option("--gemini", "Use Gemini CLI") + .option("--kimi", "Use Kimi Code") .option("--dry-run", "Show what would be done without executing") .option("--max-iterations ", "Maximum iterations (0 = unlimited)", "0") .option("--max-retries ", "Maximum retries per task", "3") @@ -98,6 +99,7 @@ export function parseArgs(args: string[]): { else if (opts.droid) aiEngine = "droid"; else if (opts.copilot) aiEngine = "copilot"; else if (opts.gemini) aiEngine = "gemini"; + else if (opts.kimi) aiEngine = "kimi"; // Determine model override (--sonnet is shortcut for --model sonnet) const modelOverride = opts.sonnet ? "sonnet" : opts.model || undefined; diff --git a/cli/src/engines/index.ts b/cli/src/engines/index.ts index 4b7fc70e..d0239635 100644 --- a/cli/src/engines/index.ts +++ b/cli/src/engines/index.ts @@ -8,6 +8,7 @@ export * from "./qwen.ts"; export * from "./droid.ts"; export * from "./copilot.ts"; export * from "./gemini.ts"; +export * from "./kimi.ts"; import { ClaudeEngine } from "./claude.ts"; import { CodexEngine } from "./codex.ts"; @@ -15,6 +16,7 @@ import { CopilotEngine } from "./copilot.ts"; import { CursorEngine } from "./cursor.ts"; import { DroidEngine } from "./droid.ts"; import { GeminiEngine } from "./gemini.ts"; +import { KimiEngine } from "./kimi.ts"; import { OpenCodeEngine } from "./opencode.ts"; import { QwenEngine } from "./qwen.ts"; import type { AIEngine, AIEngineName } from "./types.ts"; @@ -40,6 +42,8 @@ export function createEngine(name: AIEngineName): AIEngine { return new CopilotEngine(); case "gemini": return new GeminiEngine(); + case "kimi": + return new KimiEngine(); default: throw new Error(`Unknown AI engine: ${name}`); } diff --git a/cli/src/engines/kimi.ts b/cli/src/engines/kimi.ts new file mode 100644 index 00000000..78f922e0 --- /dev/null +++ b/cli/src/engines/kimi.ts @@ -0,0 +1,163 @@ +import { + BaseAIEngine, + checkForErrors, + detectStepFromOutput, + execCommand, + execCommandStreaming, + formatCommandError, + parseStreamJsonResult, +} from "./base.ts"; +import type { AIResult, EngineOptions, ProgressCallback } from "./types.ts"; + +const isWindows = process.platform === "win32"; + +/** + * Kimi Code CLI AI Engine + * https://github.com/MoonshotAI/kimi-cli + */ +export class KimiEngine extends BaseAIEngine { + name = "Kimi Code"; + cliCommand = "kimi"; + + async execute(prompt: string, workDir: string, options?: EngineOptions): Promise { + const args = ["--yolo", "--output-format", "stream-json"]; + if (options?.modelOverride) { + args.push("--model", options.modelOverride); + } + // Add any additional engine-specific arguments + if (options?.engineArgs && options.engineArgs.length > 0) { + args.push(...options.engineArgs); + } + + // On Windows, pass prompt via stdin to avoid cmd.exe argument parsing issues with multi-line content + let stdinContent: string | undefined; + if (isWindows) { + args.push("-p"); + stdinContent = prompt; + } else { + args.push("-p", prompt); + } + + const { stdout, stderr, exitCode } = await execCommand( + this.cliCommand, + args, + workDir, + undefined, + stdinContent, + ); + + const output = stdout + stderr; + + // Check for errors + const error = checkForErrors(output); + if (error) { + return { + success: false, + response: "", + inputTokens: 0, + outputTokens: 0, + error, + }; + } + + // Parse result (same stream-json format as Claude/Qwen/Gemini) + const { response, inputTokens, outputTokens } = parseStreamJsonResult(output); + + // If command failed with non-zero exit code, provide a meaningful error + if (exitCode !== 0) { + return { + success: false, + response, + inputTokens, + outputTokens, + error: formatCommandError(exitCode, output), + }; + } + + return { + success: true, + response, + inputTokens, + outputTokens, + }; + } + + async executeStreaming( + prompt: string, + workDir: string, + onProgress: ProgressCallback, + options?: EngineOptions, + ): Promise { + const args = ["--yolo", "--output-format", "stream-json"]; + if (options?.modelOverride) { + args.push("--model", options.modelOverride); + } + // Add any additional engine-specific arguments + if (options?.engineArgs && options.engineArgs.length > 0) { + args.push(...options.engineArgs); + } + + // On Windows, pass prompt via stdin to avoid cmd.exe argument parsing issues with multi-line content + let stdinContent: string | undefined; + if (isWindows) { + args.push("-p"); + stdinContent = prompt; + } else { + args.push("-p", prompt); + } + + const outputLines: string[] = []; + + const { exitCode } = await execCommandStreaming( + this.cliCommand, + args, + workDir, + (line) => { + outputLines.push(line); + + // Detect and report step changes + const step = detectStepFromOutput(line); + if (step) { + onProgress(step); + } + }, + undefined, + stdinContent, + ); + + const output = outputLines.join("\n"); + + // Check for errors + const error = checkForErrors(output); + if (error) { + return { + success: false, + response: "", + inputTokens: 0, + outputTokens: 0, + error, + }; + } + + // Parse result (same stream-json format as Claude/Qwen/Gemini) + const { response, inputTokens, outputTokens } = parseStreamJsonResult(output); + + // If command failed with non-zero exit code, provide a meaningful error + if (exitCode !== 0) { + return { + success: false, + response, + inputTokens, + outputTokens, + error: formatCommandError(exitCode, output), + }; + } + + return { + success: true, + response, + inputTokens, + outputTokens, + }; + } +} diff --git a/cli/src/engines/types.ts b/cli/src/engines/types.ts index 6e9f9c8f..ab08d506 100644 --- a/cli/src/engines/types.ts +++ b/cli/src/engines/types.ts @@ -58,4 +58,5 @@ export type AIEngineName = | "qwen" | "droid" | "copilot" - | "gemini"; + | "gemini" + | "kimi";