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",
|
"id": "kiro-auth",
|
||||||
"providers": ["kiro"],
|
"providers": ["kiro-cli"],
|
||||||
"configSchema": {
|
"configSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"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 { extractKiroCliToken, isTokenExpired } from "./cli-credentials.js";
|
||||||
import { findKiroCli } from "./cli-detector.js";
|
import { findKiroCli } from "./cli-detector.js";
|
||||||
|
|
||||||
const PROVIDER_ID = "kiro";
|
const PROVIDER_ID = "kiro-cli";
|
||||||
const PROVIDER_LABEL = "Kiro";
|
const PROVIDER_LABEL = "Kiro CLI";
|
||||||
const DEFAULT_MODEL = "kiro/auto";
|
const DEFAULT_MODEL = "kiro-cli/auto";
|
||||||
|
|
||||||
const kiroPlugin = {
|
const kiroPlugin = {
|
||||||
id: "kiro-auth",
|
id: "kiro-auth",
|
||||||
@ -17,7 +17,7 @@ const kiroPlugin = {
|
|||||||
id: PROVIDER_ID,
|
id: PROVIDER_ID,
|
||||||
label: PROVIDER_LABEL,
|
label: PROVIDER_LABEL,
|
||||||
docsPath: "/providers/models",
|
docsPath: "/providers/models",
|
||||||
aliases: ["kiro-cli", "amazon-q", "q-developer"],
|
aliases: ["kiro", "amazon-q", "q-developer"],
|
||||||
envVars: [],
|
envVars: [],
|
||||||
auth: [
|
auth: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -25,6 +25,18 @@ const CLAUDE_MODEL_ALIASES: Record<string, string> = {
|
|||||||
"claude-haiku-3-5": "haiku",
|
"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 = {
|
const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
|
||||||
command: "claude",
|
command: "claude",
|
||||||
args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"],
|
args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"],
|
||||||
@ -42,7 +54,12 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
|
|||||||
modelAliases: CLAUDE_MODEL_ALIASES,
|
modelAliases: CLAUDE_MODEL_ALIASES,
|
||||||
sessionArg: "--session-id",
|
sessionArg: "--session-id",
|
||||||
sessionMode: "always",
|
sessionMode: "always",
|
||||||
sessionIdFields: ["session_id", "sessionId", "conversation_id", "conversationId"],
|
sessionIdFields: [
|
||||||
|
"session_id",
|
||||||
|
"sessionId",
|
||||||
|
"conversation_id",
|
||||||
|
"conversationId",
|
||||||
|
],
|
||||||
systemPromptArg: "--append-system-prompt",
|
systemPromptArg: "--append-system-prompt",
|
||||||
systemPromptMode: "append",
|
systemPromptMode: "append",
|
||||||
systemPromptWhen: "first",
|
systemPromptWhen: "first",
|
||||||
@ -52,7 +69,15 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
|
|||||||
|
|
||||||
const DEFAULT_CODEX_BACKEND: CliBackendConfig = {
|
const DEFAULT_CODEX_BACKEND: CliBackendConfig = {
|
||||||
command: "codex",
|
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: [
|
resumeArgs: [
|
||||||
"exec",
|
"exec",
|
||||||
"resume",
|
"resume",
|
||||||
@ -74,6 +99,17 @@ const DEFAULT_CODEX_BACKEND: CliBackendConfig = {
|
|||||||
serialize: true,
|
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 {
|
function normalizeBackendKey(key: string): string {
|
||||||
return normalizeProviderId(key);
|
return normalizeProviderId(key);
|
||||||
}
|
}
|
||||||
@ -88,7 +124,10 @@ function pickBackendConfig(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig): CliBackendConfig {
|
function mergeBackendConfig(
|
||||||
|
base: CliBackendConfig,
|
||||||
|
override?: CliBackendConfig,
|
||||||
|
): CliBackendConfig {
|
||||||
if (!override) return { ...base };
|
if (!override) return { ...base };
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@ -96,7 +135,9 @@ function mergeBackendConfig(base: CliBackendConfig, override?: CliBackendConfig)
|
|||||||
args: override.args ?? base.args,
|
args: override.args ?? base.args,
|
||||||
env: { ...base.env, ...override.env },
|
env: { ...base.env, ...override.env },
|
||||||
modelAliases: { ...base.modelAliases, ...override.modelAliases },
|
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,
|
sessionIdFields: override.sessionIdFields ?? base.sessionIdFields,
|
||||||
sessionArgs: override.sessionArgs ?? base.sessionArgs,
|
sessionArgs: override.sessionArgs ?? base.sessionArgs,
|
||||||
resumeArgs: override.resumeArgs ?? base.resumeArgs,
|
resumeArgs: override.resumeArgs ?? base.resumeArgs,
|
||||||
@ -107,6 +148,7 @@ export function resolveCliBackendIds(cfg?: ClawdbotConfig): Set<string> {
|
|||||||
const ids = new Set<string>([
|
const ids = new Set<string>([
|
||||||
normalizeBackendKey("claude-cli"),
|
normalizeBackendKey("claude-cli"),
|
||||||
normalizeBackendKey("codex-cli"),
|
normalizeBackendKey("codex-cli"),
|
||||||
|
normalizeBackendKey("kiro-cli"),
|
||||||
]);
|
]);
|
||||||
const configured = cfg?.agents?.defaults?.cliBackends ?? {};
|
const configured = cfg?.agents?.defaults?.cliBackends ?? {};
|
||||||
for (const key of Object.keys(configured)) {
|
for (const key of Object.keys(configured)) {
|
||||||
@ -135,6 +177,12 @@ export function resolveCliBackendConfig(
|
|||||||
if (!command) return null;
|
if (!command) return null;
|
||||||
return { id: normalized, config: { ...merged, command } };
|
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;
|
if (!override) return null;
|
||||||
const command = override.command?.trim();
|
const command = override.command?.trim();
|
||||||
|
|||||||
@ -9,7 +9,13 @@ export type ModelRef = {
|
|||||||
model: string;
|
model: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
export type ThinkLevel =
|
||||||
|
| "off"
|
||||||
|
| "minimal"
|
||||||
|
| "low"
|
||||||
|
| "medium"
|
||||||
|
| "high"
|
||||||
|
| "xhigh";
|
||||||
|
|
||||||
export type ModelAliasIndex = {
|
export type ModelAliasIndex = {
|
||||||
byAlias: Map<string, { alias: string; ref: ModelRef }>;
|
byAlias: Map<string, { alias: string; ref: ModelRef }>;
|
||||||
@ -36,8 +42,11 @@ export function isCliProvider(provider: string, cfg?: ClawdbotConfig): boolean {
|
|||||||
const normalized = normalizeProviderId(provider);
|
const normalized = normalizeProviderId(provider);
|
||||||
if (normalized === "claude-cli") return true;
|
if (normalized === "claude-cli") return true;
|
||||||
if (normalized === "codex-cli") return true;
|
if (normalized === "codex-cli") return true;
|
||||||
|
if (normalized === "kiro-cli") return true;
|
||||||
const backends = cfg?.agents?.defaults?.cliBackends ?? {};
|
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 {
|
function normalizeAnthropicModelId(model: string): string {
|
||||||
@ -55,7 +64,10 @@ function normalizeProviderModelId(provider: string, model: string): string {
|
|||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseModelRef(raw: string, defaultProvider: string): ModelRef | null {
|
export function parseModelRef(
|
||||||
|
raw: string,
|
||||||
|
defaultProvider: string,
|
||||||
|
): ModelRef | null {
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
const slash = trimmed.indexOf("/");
|
const slash = trimmed.indexOf("/");
|
||||||
@ -83,7 +95,9 @@ export function buildModelAliasIndex(params: {
|
|||||||
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
||||||
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
|
const parsed = parseModelRef(String(keyRaw ?? ""), params.defaultProvider);
|
||||||
if (!parsed) continue;
|
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;
|
if (!alias) continue;
|
||||||
const aliasKey = normalizeAliasKey(alias);
|
const aliasKey = normalizeAliasKey(alias);
|
||||||
byAlias.set(aliasKey, { alias, ref: parsed });
|
byAlias.set(aliasKey, { alias, ref: parsed });
|
||||||
@ -121,7 +135,10 @@ export function resolveConfiguredModelRef(params: {
|
|||||||
defaultModel: string;
|
defaultModel: string;
|
||||||
}): ModelRef {
|
}): ModelRef {
|
||||||
const rawModel = (() => {
|
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();
|
if (typeof raw === "string") return raw.trim();
|
||||||
return raw?.primary?.trim() ?? "";
|
return raw?.primary?.trim() ?? "";
|
||||||
})();
|
})();
|
||||||
@ -205,7 +222,9 @@ export function buildAllowedModelSet(params: {
|
|||||||
defaultModel && params.defaultProvider
|
defaultModel && params.defaultProvider
|
||||||
? modelKey(params.defaultProvider, defaultModel)
|
? modelKey(params.defaultProvider, defaultModel)
|
||||||
: undefined;
|
: 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 (allowAny) {
|
||||||
if (defaultKey) catalogKeys.add(defaultKey);
|
if (defaultKey) catalogKeys.add(defaultKey);
|
||||||
@ -217,7 +236,10 @@ export function buildAllowedModelSet(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allowedKeys = new Set<string>();
|
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) {
|
for (const raw of rawAllowlist) {
|
||||||
const parsed = parseModelRef(String(raw), params.defaultProvider);
|
const parsed = parseModelRef(String(raw), params.defaultProvider);
|
||||||
if (!parsed) continue;
|
if (!parsed) continue;
|
||||||
@ -277,7 +299,9 @@ export function getModelRefStatus(params: {
|
|||||||
const key = modelKey(params.ref.provider, params.ref.model);
|
const key = modelKey(params.ref.provider, params.ref.model);
|
||||||
return {
|
return {
|
||||||
key,
|
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,
|
allowAny: allowed.allowAny,
|
||||||
allowed: allowed.allowAny || allowed.allowedKeys.has(key),
|
allowed: allowed.allowAny || allowed.allowedKeys.has(key),
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user