feat: add kiro-cli as native CLI backend provider

This commit is contained in:
Jaime Abril 2026-01-26 23:17:19 +01:00
parent 1b8ca82503
commit 7064c2fc7e
No known key found for this signature in database
5 changed files with 213 additions and 17 deletions

View File

@ -1,6 +1,6 @@
{
"id": "kiro-auth",
"providers": ["kiro"],
"providers": ["kiro-cli"],
"configSchema": {
"type": "object",
"additionalProperties": false,

View File

@ -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<string> {
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";
}
}

View File

@ -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: [
{

View File

@ -25,6 +25,18 @@ const CLAUDE_MODEL_ALIASES: Record<string, string> = {
"claude-haiku-3-5": "haiku",
};
const KIRO_MODEL_ALIASES: Record<string, string> = {
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<string> {
const ids = new Set<string>([
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();

View File

@ -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<string, { alias: string; ref: ModelRef }>;
@ -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<string>();
const configuredProviders = (params.cfg.models?.providers ?? {}) as Record<string, unknown>;
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),
};