feat: add kiro-cli as native CLI backend provider
This commit is contained in:
parent
1b8ca82503
commit
7064c2fc7e
@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "kiro-auth",
|
||||
"providers": ["kiro"],
|
||||
"providers": ["kiro-cli"],
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
124
extensions/kiro-auth/cli-chat.ts
Normal file
124
extensions/kiro-auth/cli-chat.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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),
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user