1import { ProviderDriverKind, TextGenerationError } from "@t3tools/contracts";
2import { KiroSettings } from "../settings.ts";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import * as SecureRandom from "effect/crypto/SecureRandom";
5import * as Effect from "effect/Effect";
6import * as Schema from "effect/Schema";
7import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
8
9import type * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
10import {
11 defaultProviderContinuationIdentity,
12 type ProviderDriver,
13 type ProviderInstance,
14} from "@t3tools/provider-core/server/driver";
15import { ProviderDriverError } from "@t3tools/provider-core/server/errors";
16import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
17import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
18import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity";
19import { makeManualOnlyProviderMaintenanceCapabilities } from "@t3tools/provider-core/server/maintenanceResolver";
20import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider";
21import {
22 haveProviderSnapshotSettingsChanged,
23 makeProviderSnapshotSettingsSource,
24 type ProviderSnapshotSettings,
25} from "@t3tools/provider-core/server/snapshotSettings";
26import type { ProviderTextGeneration } from "@t3tools/provider-core/server/textGeneration";
27import { type KiroAdapterV2DriverEnv, makeKiroAdapterV2Driver } from "./adapter.ts";
28import {
29 buildInitialKiroProviderSnapshot,
30 checkKiroProviderStatus,
31 kiroDefaultModelId,
32} from "./status.ts";
33
34const DRIVER_KIND = ProviderDriverKind.make("kiro");
35const decodeKiroSettings = Schema.decodeSync(KiroSettings);
36// Kiro updates itself in the background (`kiro-cli update` reports only), so
37// T3 offers no one-click update.
38const MAINTENANCE = makeManualOnlyProviderMaintenanceCapabilities({
39 provider: DRIVER_KIND,
40 packageName: null,
41});
42
43const unsupportedTextGeneration: ProviderTextGeneration = (() => {
44 const unsupported = (operation: string) =>
45 Effect.fail(
46 new TextGenerationError({
47 operation,
48 detail: "Kiro does not provide application text generation.",
49 }),
50 );
51 return {
52 generateCommitMessage: () => unsupported("generateCommitMessage"),
53 generatePrContent: () => unsupported("generatePrContent"),
54 generateBranchName: () => unsupported("generateBranchName"),
55 generateThreadTitle: () => unsupported("generateThreadTitle"),
56 generateReviewGuide: () => unsupported("generateReviewGuide"),
57 generateTranscriptionPostProcessing: () => unsupported("generateTranscriptionPostProcessing"),
58 };
59})();
60
61export type KiroDriverEnv =
62 | KiroAdapterV2DriverEnv
63 | ChildProcessSpawner.ChildProcessSpawner
64 | SecureRandom.SecureRandom
65 | ProviderHost.ProviderHost;
66
67export const KiroDriver: ProviderDriver<KiroSettings, KiroDriverEnv> = {
68 driverKind: DRIVER_KIND,
69 metadata: {
70 displayName: "Kiro",
71 supportsMultipleInstances: true,
72 },
73 configSchema: KiroSettings,
74 defaultConfig: (): KiroSettings => decodeKiroSettings({}),
75 create: Effect.fn("KiroDriver.create")(function* ({
76 instanceId,
77 displayName,
78 accentColor,
79 environment,
80 enabled,
81 config,
82 }) {
83 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
84 const crypto = yield* SecureRandom.SecureRandom;
85 const processEnv = yield* mergeProviderInstanceEnvironment(
86 environment,
87 yield* HostProcess.Environment,
88 );
89 const continuationIdentity = defaultProviderContinuationIdentity({
90 driverKind: DRIVER_KIND,
91 instanceId,
92 });
93 const stampIdentity = withInstanceIdentity({
94 instanceId,
95 driverKind: DRIVER_KIND,
96 displayName,
97 accentColor,
98 continuationGroupKey: continuationIdentity.continuationKey,
99 });
100 const effectiveConfig = {
101 ...config,
102 enabled,
103 binaryPath: expandHomePath(config.binaryPath, yield* HostProcess.HomeDirectory),
104 } satisfies KiroSettings;
105 const snapshotSettings = yield* makeProviderSnapshotSettingsSource(effectiveConfig);
106 const snapshot = yield* makeManagedServerProvider<ProviderSnapshotSettings<KiroSettings>>({
107 resolveMaintenance: () => Effect.succeed(MAINTENANCE),
108 getSettings: snapshotSettings.getSettings,
109 streamSettings: snapshotSettings.streamSettings,
110 haveSettingsChanged: haveProviderSnapshotSettingsChanged,
111 initialSnapshot: (settings) =>
112 buildInitialKiroProviderSnapshot(settings.provider).pipe(Effect.map(stampIdentity)),
113 checkProvider: checkKiroProviderStatus(effectiveConfig, processEnv).pipe(
114 Effect.map(stampIdentity),
115 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
116 Effect.provideService(SecureRandom.SecureRandom, crypto),
117 ),
118 }).pipe(
119 Effect.mapError(
120 (cause) =>
121 new ProviderDriverError({
122 driver: DRIVER_KIND,
123 instanceId,
124 // The cause keeps the failure; the detail reaches the snapshot, so it stays fixed.
125 detail: "Failed to build Kiro snapshot.",
126 cause,
127 }),
128 ),
129 );
130
131 // "Kiro default" selects the default the snapshot read from Kiro's model list.
132 const orchestrationAdapter = yield* makeKiroAdapterV2Driver(
133 snapshot.getSnapshot.pipe(Effect.map((current) => kiroDefaultModelId(current.models))),
134 )
135 .create({
136 instanceId,
137 displayName,
138 accentColor,
139 environment,
140 enabled,
141 config: effectiveConfig,
142 })
143 .pipe(
144 Effect.mapError(
145 (cause) =>
146 new ProviderDriverError({
147 driver: DRIVER_KIND,
148 instanceId,
149 detail: "Failed to build Kiro orchestration adapter.",
150 cause,
151 }),
152 ),
153 );
154
155 return {
156 instanceId,
157 driverKind: DRIVER_KIND,
158 continuationIdentity,
159 displayName,
160 accentColor,
161 enabled,
162 snapshot,
163 orchestrationAdapter,
164 textGeneration: unsupportedTextGeneration,
165 } satisfies ProviderInstance;
166 }),
167};