Skip to content
Source
Sign inSign up

status.ts

474 lines16.6 KB

1import { resolveProviderProbeTimeoutMs } from "@t3tools/provider-core/server/snapshotProbe";
2import {
3 type ModelCapabilities,
4 ProviderDriverKind,
5 type ProviderOptionDescriptor,
6 type ServerProviderAuth,
7 type ServerProviderModel,
8} from "@t3tools/contracts";
9import type { KiroSettings } from "../settings.ts";
10import * as HostProcess from "@t3tools/shared/HostProcess";
11import { createModelCapabilities } from "@t3tools/shared/model";
12import { resolveSpawnCommand } from "@t3tools/shared/shell";
13import type * as SecureRandom from "effect/crypto/SecureRandom";
14import * as DateTime from "effect/DateTime";
15import * as Effect from "effect/Effect";
16import * as Option from "effect/Option";
17import * as Result from "effect/Result";
18import * as Schema from "effect/Schema";
19import { ChildProcess, type ChildProcessSpawner } from "effect/process";
20​
21import {
22 KIRO_AGENT_CONFIG_ID,
23 KIRO_SUPPORTED_RUNTIME_MODES,
24 makeKiroAcpRuntime,
25} from "./acpSupport.ts";
26import {
27 AUTH_PROBE_TIMEOUT_MS,
28 buildServerProvider,
29 COMPACT_SLASH_COMMAND,
30 isCommandMissingCause,
31 parseCliVersionResult,
32 buildSelectOptionDescriptor,
33 type ProviderProbeResult,
34 providerModelsFromSettings,
35 type ServerProviderDraft,
36 providerProbeEnvironment,
37 spawnAndCollect,
38} from "@t3tools/provider-core/server/snapshotProbe";
39​
40const KIRO_DRIVER_KIND = ProviderDriverKind.make("kiro");
41const KIRO_PRESENTATION = {
42 displayName: "Kiro",
43 showInteractionModeToggle: false,
44 supportedRuntimeModes: KIRO_SUPPORTED_RUNTIME_MODES,
45} as const;
46const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [] });
47const KIRO_DEFAULT_MODEL_SLUG = "default";
48const KIRO_FALLBACK_DEFAULT_MODEL = "auto";
49const KIRO_BUILT_IN_MODELS: ReadonlyArray<ServerProviderModel> = [
50 {
51 slug: KIRO_DEFAULT_MODEL_SLUG,
52 name: "Kiro default",
53 isCustom: false,
54 capabilities: EMPTY_CAPABILITIES,
55 },
56];
57const KIRO_API_KEY_ENV = "KIRO_API_KEY";
58const VERSION_PROBE_TIMEOUT_MS = 10_000;
59​
60/**
61 * The agent picker is not a model trait, but the composer reads options per
62 * model, so every model, custom ones included, carries it.
63 */
64const kiroModels = (
65 settings: KiroSettings,
66 listed: ReadonlyArray<ServerProviderModel> = KIRO_BUILT_IN_MODELS,
67 agents?: ProviderOptionDescriptor,
68) =>
69 providerModelsFromSettings(listed, settings.customModels, EMPTY_CAPABILITIES).map((model) => {
70 const descriptors = model.capabilities?.optionDescriptors ?? [];
71 return agents === undefined || descriptors.some((descriptor) => descriptor.id === agents.id)
72 ? model
73 : {
74 ...model,
75 capabilities: createModelCapabilities({ optionDescriptors: [...descriptors, agents] }),
76 };
77 });
78​
79const snapshot = (
80 settings: KiroSettings,
81 checkedAt: string,
82 probe: ProviderProbeResult,
83 listed?: ReadonlyArray<ServerProviderModel>,
84 agents?: ProviderOptionDescriptor,
85) =>
86 buildServerProvider({
87 // Gives the snapshot its version advisory; Kiro runs no enrichment pass.
88 driver: KIRO_DRIVER_KIND,
89 presentation: KIRO_PRESENTATION,
90 enabled: settings.enabled,
91 checkedAt,
92 models: kiroModels(settings, listed, agents),
93 // `/compact` runs `_kiro/session/compact`; see kiroCompactSession.
94 slashCommands: [COMPACT_SLASH_COMMAND],
95 probe,
96 });
97​
98export const buildInitialKiroProviderSnapshot = Effect.fn(
99 "KiroProvider.buildInitialKiroProviderSnapshot",
100)(function* (settings: KiroSettings): Effect.fn.Return<ServerProviderDraft> {
101 const now = yield* DateTime.now;
102 return snapshot(settings, DateTime.formatIso(now), {
103 installed: settings.enabled,
104 version: null,
105 status: "warning",
106 auth: { status: "unknown" },
107 message: settings.enabled
108 ? "Checking Kiro CLI availability..."
109 : "Kiro is disabled in T3 Code settings.",
110 });
111});
112​
113const runKiroCli = Effect.fn("KiroProvider.runKiroCli")(function* (
114 settings: KiroSettings,
115 args: ReadonlyArray<string>,
116 environment: NodeJS.ProcessEnv,
117) {
118 const command = settings.binaryPath || "kiro-cli";
119 const spawnCommand = yield* resolveSpawnCommand(command, args, { env: environment });
120 return yield* spawnAndCollect(
121 command,
122 ChildProcess.make(spawnCommand.command, spawnCommand.args, {
123 env: environment,
124 shell: spawnCommand.shell,
125 }),
126 );
127});
128​
129const KiroWhoami = Schema.fromJsonString(
130 Schema.Struct({
131 accountType: Schema.optional(Schema.NullOr(Schema.String)),
132 email: Schema.optional(Schema.NullOr(Schema.String)),
133 }),
134);
135const decodeKiroWhoami = Schema.decodeUnknownOption(KiroWhoami);
136​
137/**
138 * Reads `kiro-cli whoami --format json`: `{"account":null}` with exit 1 means
139 * signed out. Only the first line is decoded, since an Identity Center login
140 * follows it with a plain-text `Profile:` block.
141 */
142export function kiroAuthFromWhoami(
143 output: { readonly code: number; readonly stdout: string } | undefined,
144 environment: NodeJS.ProcessEnv,
145): ServerProviderAuth {
146 if (output === undefined) return { status: "unknown" };
147 if (output.code !== 0) {
148 return output.stdout.includes('"account":null')
149 ? { status: "unauthenticated" }
150 : { status: "unknown" };
151 }
152 const firstLine = output.stdout.trimStart().split("\n", 1)[0] ?? "";
153 const account = Option.getOrUndefined(decodeKiroWhoami(firstLine.trim()));
154 if (account?.accountType === "ApiKey" || environment[KIRO_API_KEY_ENV]?.trim()) {
155 return { status: "authenticated", type: "api_key", label: "Kiro API key" };
156 }
157 return {
158 status: "authenticated",
159 label: "Kiro account",
160 ...(account?.email ? { email: account.email } : {}),
161 };
163​
164const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
165const CLAUDE_4_6_EFFORT_LEVELS = ["low", "medium", "high", "max"];
166const GPT_5_6_EFFORT_LEVELS = ["none", ...CLAUDE_EFFORT_LEVELS];
167const KIMI_K3_EFFORT_LEVELS = ["low", "high", "max"];
168​
169/**
170 * Reasoning levels per Kiro model id and its default level, copied from what
171 * Kiro 2.32.0 advertises, since `--list-models` names none. Unlisted models take
172 * no effort; the adapter sends only a level the live session offers.
173 */
174const KIRO_MODEL_EFFORT: Readonly<
175 Record<string, { readonly levels: ReadonlyArray<string>; readonly defaultLevel: string }>
176> = {
177 "claude-fable-5.1": { levels: CLAUDE_EFFORT_LEVELS, defaultLevel: "high" },
178 "claude-opus-5.5": { levels: CLAUDE_EFFORT_LEVELS, defaultLevel: "medium" },
179 "claude-opus-5": { levels: CLAUDE_EFFORT_LEVELS, defaultLevel: "high" },
180 "claude-opus-4.8": { levels: CLAUDE_EFFORT_LEVELS, defaultLevel: "high" },
181 "claude-opus-4.7": { levels: CLAUDE_EFFORT_LEVELS, defaultLevel: "xhigh" },
182 "claude-opus-4.6": { levels: CLAUDE_4_6_EFFORT_LEVELS, defaultLevel: "high" },
183 "claude-sonnet-5.5": { levels: CLAUDE_EFFORT_LEVELS, defaultLevel: "high" },
184 "claude-sonnet-5": { levels: CLAUDE_EFFORT_LEVELS, defaultLevel: "high" },
185 "claude-sonnet-4.6": { levels: CLAUDE_4_6_EFFORT_LEVELS, defaultLevel: "high" },
186 "gpt-5.6-sol": { levels: GPT_5_6_EFFORT_LEVELS, defaultLevel: "high" },
187 "gpt-5.6-terra": { levels: GPT_5_6_EFFORT_LEVELS, defaultLevel: "high" },
188 "gpt-5.6-luna": { levels: GPT_5_6_EFFORT_LEVELS, defaultLevel: "high" },
189 "kimi-k3": { levels: KIMI_K3_EFFORT_LEVELS, defaultLevel: "max" },
190};
191const KIRO_EFFORT_LABELS: Readonly<Record<string, string>> = {
192 none: "None",
193 low: "Low",
194 medium: "Medium",
195 high: "High",
196 xhigh: "Extra High",
197 max: "Max",
198};
199​
200function kiroModelCapabilities(modelId: string): ModelCapabilities {
201 const effort = KIRO_MODEL_EFFORT[modelId];
202 if (effort === undefined) return EMPTY_CAPABILITIES;
203 return createModelCapabilities({
204 optionDescriptors: [
205 buildSelectOptionDescriptor({
206 id: "reasoningEffort",
207 label: "Reasoning",
208 options: effort.levels.map((level) => ({
209 value: level,
210 label: KIRO_EFFORT_LABELS[level] ?? level,
211 isDefault: level === effort.defaultLevel,
212 })),
213 }),
214 ],
215 });
217​
218const KiroModelList = Schema.fromJsonString(
219 Schema.Struct({
220 models: Schema.Array(
221 Schema.Struct({
222 model_id: Schema.String,
223 model_name: Schema.optional(Schema.String),
224 }),
225 ),
226 default_model: Schema.optional(Schema.String),
227 }),
228);
229const decodeKiroModelList = Schema.decodeUnknownOption(KiroModelList);
230​
231/**
232 * Reads `kiro-cli chat --list-models --format json`, the account's models. The
233 * default entry keeps its name but takes the "default" slug, with Kiro's id as its
234 * alias. Sessions advertise models only after they start; anything unparsable
235 * keeps the built-in list.
236 */
237function kiroModelsFromList(
238 output: { readonly code: number; readonly stdout: string } | undefined,
239): ReadonlyArray<ServerProviderModel> | undefined {
240 if (output === undefined || output.code !== 0) return undefined;
241 const list = Option.getOrUndefined(decodeKiroModelList(output.stdout.trim()));
242 if (list === undefined || list.models.length === 0) return undefined;
243 const defaultId = list.default_model ?? KIRO_FALLBACK_DEFAULT_MODEL;
244 const listedDefault = list.models.find((model) => model.model_id === defaultId);
245 const defaultModel: ServerProviderModel = {
246 slug: KIRO_DEFAULT_MODEL_SLUG,
247 // Named after the model it runs, so a named default isn't hidden behind "Kiro default".
248 name: listedDefault ? (listedDefault.model_name ?? listedDefault.model_id) : "Kiro default",
249 aliases: [defaultId],
250 isCustom: false,
251 isDefault: true,
252 // A new session on Kiro's default gets no model write, so Kiro never
253 // advertises its effort option and a level could not be applied.
254 capabilities: EMPTY_CAPABILITIES,
255 };
256 const models = list.models.map((model) =>
257 model === listedDefault
258 ? defaultModel
259 : {
260 slug: model.model_id,
261 name: model.model_name ?? model.model_id,
262 isCustom: false,
263 capabilities: kiroModelCapabilities(model.model_id),
264 },
265 );
266 // New threads select "default", so it is listed even when Kiro's list leaves its default out.
267 return models.includes(defaultModel) ? models : [defaultModel, ...models];
269​
270/**
271 * Kiro's own id for "Kiro default": the alias the model list gave it, else
272 * `auto`, Kiro's documented default.
273 */
274export function kiroDefaultModelId(models: ReadonlyArray<ServerProviderModel>): string {
275 return (
276 models.find((model) => model.slug === KIRO_DEFAULT_MODEL_SLUG)?.aliases?.[0] ??
277 KIRO_FALLBACK_DEFAULT_MODEL
278 );
280​
281const KiroConfigTemplate = Schema.Struct({
282 configOptions: Schema.Array(
283 Schema.Struct({
284 id: Schema.String,
285 type: Schema.String,
286 currentValue: Schema.optional(Schema.Unknown),
287 options: Schema.optional(
288 Schema.Array(
289 Schema.Struct({
290 value: Schema.String,
291 name: Schema.String,
292 description: Schema.optional(Schema.NullOr(Schema.String)),
293 }),
294 ),
295 ),
296 }),
297 ),
298});
299const decodeKiroConfigTemplate = Schema.decodeUnknownOption(KiroConfigTemplate);
300​
301/**
302 * The thread's agent picker, from Kiro's `_kiro/config/template` result, without
303 * opening a session. The template reads no workspace, so a project's
304 * `.kiro/agents` are not offered.
305 */
306export function kiroAgentDescriptor(template: unknown): ProviderOptionDescriptor | undefined {
307 const decoded = Option.getOrUndefined(decodeKiroConfigTemplate(template));
308 const mode = decoded?.configOptions.find(
309 (option) => option.id === KIRO_AGENT_CONFIG_ID && option.type === "select",
310 );
311 const defaultAgent = typeof mode?.currentValue === "string" ? mode.currentValue : undefined;
312 const choices = (mode?.options ?? []).flatMap((choice) => {
313 const id = choice.value.trim();
314 const label = choice.name.trim() || id;
315 const description = choice.description?.trim();
316 return id.length === 0
317 ? []
318 : [
319 {
320 id,
321 label,
322 ...(description ? { description } : {}),
323 ...(id === defaultAgent ? { isDefault: true } : {}),
324 },
325 ];
326 });
327 // One agent is nothing to choose between.
328 if (choices.length < 2) return undefined;
329 return {
330 id: KIRO_AGENT_CONFIG_ID,
331 label: "Agent",
332 type: "select",
333 options: choices,
334 ...(defaultAgent !== undefined && choices.some((choice) => choice.isDefault)
335 ? { currentValue: defaultAgent }
336 : {}),
337 };
339​
340/**
341 * Asks Kiro for its agents with `initialize` and `_kiro/config/template` and
342 * nothing more: no `session/new`, so no MCP server starts and no session is
343 * stored. The process runs in the server's working directory.
344 */
345const probeKiroAgents = Effect.fn("KiroProvider.probeKiroAgents")(function* (
346 settings: KiroSettings,
347 environment: NodeJS.ProcessEnv,
348) {
349 const runtime = yield* makeKiroAcpRuntime({
350 settings,
351 environment,
352 cwd: yield* HostProcess.WorkingDirectory,
353 clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" },
354 });
355 yield* runtime.initialize();
356 return kiroAgentDescriptor(yield* runtime.request("_kiro/config/template", {}));
357}, Effect.scoped);
358​
359export const checkKiroProviderStatus = Effect.fn("checkKiroProviderStatus")(function* (
360 settings: KiroSettings,
361 environment: NodeJS.ProcessEnv,
362): Effect.fn.Return<
363 ServerProviderDraft,
364 never,
365 ChildProcessSpawner.ChildProcessSpawner | SecureRandom.SecureRandom
366> {
367 const checkedAt = DateTime.formatIso(yield* DateTime.now);
368 if (!settings.enabled) {
369 return snapshot(settings, checkedAt, {
370 installed: false,
371 version: null,
372 status: "warning",
373 auth: { status: "unknown" },
374 message: "Kiro is disabled in T3 Code settings.",
375 });
376 }
377​
378 const probeEnvironment = providerProbeEnvironment(environment);
379 const versionResult = yield* runKiroCli(settings, ["--version"], probeEnvironment).pipe(
380 Effect.timeoutOption(
381 resolveProviderProbeTimeoutMs("kiro", VERSION_PROBE_TIMEOUT_MS, environment),
382 ),
383 Effect.result,
384 );
385 if (Result.isFailure(versionResult) || Option.isNone(versionResult.success)) {
386 const missing = Result.isFailure(versionResult) && isCommandMissingCause(versionResult.failure);
387 return snapshot(settings, checkedAt, {
388 installed: !missing,
389 version: null,
390 status: "error",
391 auth: { status: "unknown" },
392 message: missing
393 ? "Kiro CLI (`kiro-cli`) is not installed or not on PATH."
394 : "Failed to run `kiro-cli --version`.",
395 });
396 }
397 const versionOutput = versionResult.success.value;
398 const version = parseCliVersionResult(versionOutput);
399 if (versionOutput.code !== 0) {
400 return snapshot(settings, checkedAt, {
401 installed: true,
402 version,
403 status: "error",
404 auth: { status: "unknown" },
405 message: "Kiro CLI is installed but failed to run.",
406 });
407 }
408​
409 const whoamiResult = yield* runKiroCli(
410 settings,
411 ["whoami", "--format", "json"],
412 probeEnvironment,
413 ).pipe(
414 Effect.timeoutOption(resolveProviderProbeTimeoutMs("kiro", AUTH_PROBE_TIMEOUT_MS, environment)),
415 Effect.result,
416 );
417 const auth = kiroAuthFromWhoami(
418 Result.isSuccess(whoamiResult) ? Option.getOrUndefined(whoamiResult.success) : undefined,
419 environment,
420 );
421 if (auth.status === "unauthenticated") {
422 return snapshot(settings, checkedAt, {
423 installed: true,
424 version,
425 status: "error",
426 auth,
427 message: "Kiro CLI is installed but not signed in. Run `kiro-cli login`.",
428 });
429 }
430 // Signed out, `--list-models` starts a browser login instead, and the agent
431 // probe fails to start, so both run only once `whoami` has confirmed the
432 // account. Without an agent list the composer shows no agent picker.
433 const [listed, agents] =
434 auth.status === "authenticated"
435 ? yield* Effect.all(
436 [
437 runKiroCli(
438 settings,
439 ["chat", "--list-models", "--format", "json"],
440 probeEnvironment,
441 ).pipe(
442 Effect.timeoutOption(
443 resolveProviderProbeTimeoutMs("kiro", AUTH_PROBE_TIMEOUT_MS, environment),
444 ),
445 Effect.map((output) => kiroModelsFromList(Option.getOrUndefined(output))),
446 Effect.orElseSucceed(() => undefined),
447 ),
448 probeKiroAgents(settings, probeEnvironment).pipe(
449 Effect.timeoutOption(
450 resolveProviderProbeTimeoutMs("kiro", AUTH_PROBE_TIMEOUT_MS, environment),
451 ),
452 Effect.map(Option.getOrUndefined),
453 Effect.orElseSucceed(() => undefined),
454 ),
455 ],
456 { concurrency: "unbounded" },
457 )
458 : [undefined, undefined];
459 return snapshot(
460 settings,
461 checkedAt,
462 {
463 installed: true,
464 version,
465 status: auth.status === "authenticated" ? "ready" : "warning",
466 auth,
467 ...(auth.status === "authenticated"
468 ? {}
469 : { message: "Could not confirm the Kiro sign-in. Run `kiro-cli whoami` to check." }),
470 },
471 listed,
472 agents,
473 );
474});