From 7064c2fc7eaa2a524c27ed24631526a084bc8fec Mon Sep 17 00:00:00 2001 From: Jaime Abril Date: Mon, 26 Jan 2026 23:17:19 +0100 Subject: [PATCH] feat: add kiro-cli as native CLI backend provider --- extensions/kiro-auth/clawdbot.plugin.json | 2 +- extensions/kiro-auth/cli-chat.ts | 124 ++++++++++++++++++++++ extensions/kiro-auth/index.ts | 8 +- src/agents/cli-backends.ts | 56 +++++++++- src/agents/model-selection.ts | 40 +++++-- 5 files changed, 213 insertions(+), 17 deletions(-) create mode 100644 extensions/kiro-auth/cli-chat.ts diff --git a/extensions/kiro-auth/clawdbot.plugin.json b/extensions/kiro-auth/clawdbot.plugin.json index 24eae4067..7982e4ebc 100644 --- a/extensions/kiro-auth/clawdbot.plugin.json +++ b/extensions/kiro-auth/clawdbot.plugin.json @@ -1,6 +1,6 @@ { "id": "kiro-auth", - "providers": ["kiro"], + "providers": ["kiro-cli"], "configSchema": { "type": "object", "additionalProperties": false, diff --git a/extensions/kiro-auth/cli-chat.ts b/extensions/kiro-auth/cli-chat.ts new file mode 100644 index 000000000..db9491a8a --- /dev/null +++ b/extensions/kiro-auth/cli-chat.ts @@ -0,0 +1,124 @@ +/** + * Subprocess wrapper for kiro-cli chat. + * + * Since Kiro uses AWS Smithy/Coral protocol (not standard REST), + * we use kiro-cli as a subprocess for actual inference. + */ + +import { spawn } from "node:child_process"; +import { findKiroCli } from "./cli-detector.js"; + +/** + * Available Kiro models. + * - auto: Models chosen by task (1x credit) + * - claude-sonnet-4: Hybrid reasoning and coding (1.3x credit) + * - claude-sonnet-4.5: Latest Claude Sonnet (1.3x credit) + * - claude-haiku-4.5: Latest Claude Haiku (0.4x credit) + * - claude-opus-4.5: Latest Claude Opus (premium) + */ +export type KiroModel = + | "auto" + | "claude-sonnet-4" + | "claude-sonnet-4.5" + | "claude-haiku-4.5" + | "claude-opus-4.5"; + +export type KiroChatOptions = { + /** Model to use (default: auto) */ + model?: KiroModel; + /** Trust all tools without prompting */ + trustAllTools?: boolean; + /** Timeout in milliseconds */ + timeoutMs?: number; +}; + +/** + * Sends a chat message via kiro-cli subprocess. + * + * @param prompt The message to send + * @param options Chat options + * @returns The response from kiro-cli + * @throws Error if kiro-cli is not found or fails + */ +export async function kiroChat( + prompt: string, + options: KiroChatOptions = {}, +): Promise { + const cliPath = findKiroCli(); + if (!cliPath) { + throw new Error("kiro-cli not found. Install with: brew install kiro-cli"); + } + + const args = ["chat", "--no-interactive"]; + + if (options.model && options.model !== "auto") { + args.push("--model", options.model); + } + + if (options.trustAllTools) { + args.push("--trust-all-tools"); + } + + return new Promise((resolve, reject) => { + const proc = spawn(cliPath, args, { + stdio: ["pipe", "pipe", "pipe"], + timeout: options.timeoutMs, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout.on("data", (data: Buffer) => { + stdout += data.toString(); + }); + + proc.stderr.on("data", (data: Buffer) => { + stderr += data.toString(); + }); + + proc.on("close", (code) => { + if (code === 0) { + resolve(stdout.trim()); + } else { + reject( + new Error(`kiro-cli failed (code ${code}): ${stderr || stdout}`), + ); + } + }); + + proc.on("error", (err) => { + reject(new Error(`Failed to spawn kiro-cli: ${err.message}`)); + }); + + // Send prompt and close stdin + proc.stdin.write(prompt + "\n"); + proc.stdin.end(); + }); +} + +/** + * Maps Clawdbot model IDs to Kiro model names. + */ +export function mapModelToKiro(modelId: string): KiroModel { + // Strip provider prefix if present + const model = modelId.replace(/^kiro\//, ""); + + switch (model) { + case "auto": + return "auto"; + case "claude-sonnet-4": + case "sonnet-4": + return "claude-sonnet-4"; + case "claude-sonnet-4.5": + case "sonnet-4.5": + return "claude-sonnet-4.5"; + case "claude-haiku-4.5": + case "haiku-4.5": + return "claude-haiku-4.5"; + case "claude-opus-4.5": + case "opus-4.5": + return "claude-opus-4.5"; + default: + return "auto"; + } +} diff --git a/extensions/kiro-auth/index.ts b/extensions/kiro-auth/index.ts index 9954d1ecf..2a7240f5f 100644 --- a/extensions/kiro-auth/index.ts +++ b/extensions/kiro-auth/index.ts @@ -3,9 +3,9 @@ import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; import { extractKiroCliToken, isTokenExpired } from "./cli-credentials.js"; import { findKiroCli } from "./cli-detector.js"; -const PROVIDER_ID = "kiro"; -const PROVIDER_LABEL = "Kiro"; -const DEFAULT_MODEL = "kiro/auto"; +const PROVIDER_ID = "kiro-cli"; +const PROVIDER_LABEL = "Kiro CLI"; +const DEFAULT_MODEL = "kiro-cli/auto"; const kiroPlugin = { id: "kiro-auth", @@ -17,7 +17,7 @@ const kiroPlugin = { id: PROVIDER_ID, label: PROVIDER_LABEL, docsPath: "/providers/models", - aliases: ["kiro-cli", "amazon-q", "q-developer"], + aliases: ["kiro", "amazon-q", "q-developer"], envVars: [], auth: [ { diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index f21c04f52..93fd86d53 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -25,6 +25,18 @@ const CLAUDE_MODEL_ALIASES: Record = { "claude-haiku-3-5": "haiku", }; +const KIRO_MODEL_ALIASES: Record = { + auto: "auto", + "claude-sonnet-4": "claude-sonnet-4", + "sonnet-4": "claude-sonnet-4", + "claude-sonnet-4.5": "claude-sonnet-4.5", + "sonnet-4.5": "claude-sonnet-4.5", + "claude-haiku-4.5": "claude-haiku-4.5", + "haiku-4.5": "claude-haiku-4.5", + "claude-opus-4.5": "claude-opus-4.5", + "opus-4.5": "claude-opus-4.5", +}; + const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { command: "claude", args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"], @@ -42,7 +54,12 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { modelAliases: CLAUDE_MODEL_ALIASES, sessionArg: "--session-id", sessionMode: "always", - sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"], + sessionIdFields: [ + "session_id", + "sessionId", + "conversation_id", + "conversationId", + ], systemPromptArg: "--append-system-prompt", systemPromptMode: "append", systemPromptWhen: "first", @@ -52,7 +69,15 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { const DEFAULT_CODEX_BACKEND: CliBackendConfig = { command: "codex", - args: ["exec", "--json", "--color", "never", "--sandbox", "read-only", "--skip-git-repo-check"], + args: [ + "exec", + "--json", + "--color", + "never", + "--sandbox", + "read-only", + "--skip-git-repo-check", + ], resumeArgs: [ "exec", "resume", @@ -74,6 +99,17 @@ const DEFAULT_CODEX_BACKEND: CliBackendConfig = { serialize: true, }; +const DEFAULT_KIRO_BACKEND: CliBackendConfig = { + command: "kiro-cli", + args: ["chat", "--no-interactive"], + output: "text", + input: "stdin", + modelArg: "--model", + modelAliases: KIRO_MODEL_ALIASES, + sessionMode: "none", + serialize: true, +}; + function normalizeBackendKey(key: string): string { return normalizeProviderId(key); } @@ -88,7 +124,10 @@ function pickBackendConfig( return undefined; } -function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig): CliBackendConfig { +function mergeBackendConfig( + base: CliBackendConfig, + override?: CliBackendConfig, +): CliBackendConfig { if (!override) return { ...base }; return { ...base, @@ -96,7 +135,9 @@ function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig) args: override.args ?? base.args, env: { ...base.env, ...override.env }, modelAliases: { ...base.modelAliases, ...override.modelAliases }, - clearEnv: Array.from(new Set([...(base.clearEnv ?? []), ...(override.clearEnv ?? [])])), + clearEnv: Array.from( + new Set([...(base.clearEnv ?? []), ...(override.clearEnv ?? [])]), + ), sessionIdFields: override.sessionIdFields ?? base.sessionIdFields, sessionArgs: override.sessionArgs ?? base.sessionArgs, resumeArgs: override.resumeArgs ?? base.resumeArgs, @@ -107,6 +148,7 @@ export function resolveCliBackendIds(cfg?: ClawdbotConfig): Set { const ids = new Set([ normalizeBackendKey("claude-cli"), normalizeBackendKey("codex-cli"), + normalizeBackendKey("kiro-cli"), ]); const configured = cfg?.agents?.defaults?.cliBackends ?? {}; for (const key of Object.keys(configured)) { @@ -135,6 +177,12 @@ export function resolveCliBackendConfig( if (!command) return null; return { id: normalized, config: { ...merged, command } }; } + if (normalized === "kiro-cli") { + const merged = mergeBackendConfig(DEFAULT_KIRO_BACKEND, override); + const command = merged.command?.trim(); + if (!command) return null; + return { id: normalized, config: { ...merged, command } }; + } if (!override) return null; const command = override.command?.trim(); diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index e05370edd..c8be1de18 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -9,7 +9,13 @@ export type ModelRef = { model: string; }; -export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; +export type ThinkLevel = + | "off" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh"; export type ModelAliasIndex = { byAlias: Map; @@ -36,8 +42,11 @@ export function isCliProvider(provider: string, cfg?: ClawdbotConfig): boolean { const normalized = normalizeProviderId(provider); if (normalized === "claude-cli") return true; if (normalized === "codex-cli") return true; + if (normalized === "kiro-cli") return true; const backends = cfg?.agents?.defaults?.cliBackends ?? {}; - return Object.keys(backends).some((key) => normalizeProviderId(key) === normalized); + return Object.keys(backends).some( + (key) => normalizeProviderId(key) === normalized, + ); } function normalizeAnthropicModelId(model: string): string { @@ -55,7 +64,10 @@ function normalizeProviderModelId(provider: string, model: string): string { return model; } -export function parseModelRef(raw: string, defaultProvider: string): ModelRef | null { +export function parseModelRef( + raw: string, + defaultProvider: string, +): ModelRef | null { const trimmed = raw.trim(); if (!trimmed) return null; const slash = trimmed.indexOf("/"); @@ -83,7 +95,9 @@ export function buildModelAliasIndex(params: { for (const [keyRaw, entryRaw] of Object.entries(rawModels)) { const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider); if (!parsed) continue; - const alias = String((entryRaw as { alias?: string } | undefined)?.alias ?? "").trim(); + const alias = String( + (entryRaw as { alias?: string } | undefined)?.alias ?? "", + ).trim(); if (!alias) continue; const aliasKey = normalizeAliasKey(alias); byAlias.set(aliasKey, { alias, ref: parsed }); @@ -121,7 +135,10 @@ export function resolveConfiguredModelRef(params: { defaultModel: string; }): ModelRef { const rawModel = (() => { - const raw = params.cfg.agents?.defaults?.model as { primary?: string } | string | undefined; + const raw = params.cfg.agents?.defaults?.model as + | { primary?: string } + | string + | undefined; if (typeof raw === "string") return raw.trim(); return raw?.primary?.trim() ?? ""; })(); @@ -205,7 +222,9 @@ export function buildAllowedModelSet(params: { defaultModel && params.defaultProvider ? modelKey(params.defaultProvider, defaultModel) : undefined; - const catalogKeys = new Set(params.catalog.map((entry) => modelKey(entry.provider, entry.id))); + const catalogKeys = new Set( + params.catalog.map((entry) => modelKey(entry.provider, entry.id)), + ); if (allowAny) { if (defaultKey) catalogKeys.add(defaultKey); @@ -217,7 +236,10 @@ export function buildAllowedModelSet(params: { } const allowedKeys = new Set(); - const configuredProviders = (params.cfg.models?.providers ?? {}) as Record; + const configuredProviders = (params.cfg.models?.providers ?? {}) as Record< + string, + unknown + >; for (const raw of rawAllowlist) { const parsed = parseModelRef(String(raw), params.defaultProvider); if (!parsed) continue; @@ -277,7 +299,9 @@ export function getModelRefStatus(params: { const key = modelKey(params.ref.provider, params.ref.model); return { key, - inCatalog: params.catalog.some((entry) => modelKey(entry.provider, entry.id) === key), + inCatalog: params.catalog.some( + (entry) => modelKey(entry.provider, entry.id) === key, + ), allowAny: allowed.allowAny, allowed: allowed.allowAny || allowed.allowedKeys.has(key), };