@inceptionstack/roundhouse 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,448 @@
1
+ /**
2
+ * kiro-adapter.ts — Kiro CLI AgentAdapter for Roundhouse
3
+ *
4
+ * Drives kiro-cli over ACP (Agent Control Protocol) via JSON-RPC stdio.
5
+ * Extends BaseAdapter to fulfill the fixed interface contract.
6
+ *
7
+ * Architecture:
8
+ * - One kiro-cli process hosts all sessions (spawned lazily on first prompt)
9
+ * - Sessions are per-thread, serialized via a queue to prevent concurrent prompts
10
+ * - ACP events are mapped to AgentStreamEvent for the gateway
11
+ */
12
+
13
+ import { homedir } from "node:os";
14
+ import { resolve } from "node:path";
15
+ import type { AgentAdapterFactory, AgentMessage, AgentResponse, AgentStreamEvent, AdapterInfo } from "../../types.js";
16
+ import { BaseAdapter } from "../base-adapter.js";
17
+ import { spawnKiroCli, shutdownProcess, getKiroCliVersion, type AcpProcess, type InitializeResult, type SessionNewResult } from "./acp/index.js";
18
+ import { SessionStore, type SessionEntry } from "./session.js";
19
+ import { normalizeToolName } from "./tool-names.js";
20
+
21
+ // ── Types ────────────────────────────────────────────
22
+
23
+ interface KiroAdapterConfig {
24
+ cwd: string;
25
+ agentName: string;
26
+ flushAgentName: string;
27
+ maxIdleMs: number;
28
+ autoApproveTools: string[];
29
+ }
30
+
31
+ // ── Factory ──────────────────────────────────────────
32
+
33
+ export const createKiroAgentAdapter: AgentAdapterFactory = (config) => {
34
+ return new KiroAdapter({
35
+ cwd: (config.cwd as string) ?? homedir(),
36
+ agentName: (config.agentName as string) ?? "roundhouse",
37
+ flushAgentName: (config.flushAgentName as string) ?? "roundhouse-flush",
38
+ maxIdleMs: (config.maxIdleMs as number) ?? 30 * 60 * 1000,
39
+ autoApproveTools: (config.autoApproveTools as string[]) ?? ["read", "grep", "glob", "web_fetch", "web_search"],
40
+ });
41
+ };
42
+
43
+ // ── KiroAdapter ──────────────────────────────────────
44
+
45
+ class KiroAdapter extends BaseAdapter {
46
+ readonly name = "kiro";
47
+
48
+ private readonly config: KiroAdapterConfig;
49
+ private readonly store: SessionStore;
50
+ private readonly threadQueues = new Map<string, { queue: Array<() => Promise<void>>; running: boolean }>();
51
+ private readonly kiroVersion: string;
52
+
53
+ private mainProcess: AcpProcess | null = null;
54
+ private reaperInterval: ReturnType<typeof setInterval> | null = null;
55
+
56
+ constructor(config: KiroAdapterConfig) {
57
+ super();
58
+ this.config = config;
59
+ const sessionsDir = resolve(homedir(), ".roundhouse", "sessions");
60
+ this.store = new SessionStore({ sessionsDir, maxIdleMs: config.maxIdleMs });
61
+ this.kiroVersion = getKiroCliVersion() ?? "unknown";
62
+ }
63
+
64
+ // ── Required: prompt ─────────────────────────────────
65
+
66
+ async prompt(threadId: string, message: AgentMessage): Promise<AgentResponse> {
67
+ return this.enqueue(threadId, () => this.doPrompt(threadId, message));
68
+ }
69
+
70
+ // ── Required: promptStream ───────────────────────────
71
+
72
+ promptStream(threadId: string, message: AgentMessage): AsyncIterable<AgentStreamEvent> {
73
+ const events: AgentStreamEvent[] = [];
74
+ let done = false;
75
+ let error: Error | null = null;
76
+ let resolveWait: (() => void) | null = null;
77
+ let innerIterator: AsyncIterator<AgentStreamEvent> | null = null;
78
+
79
+ this.enqueue(threadId, async () => {
80
+ try {
81
+ const gen = this.doPromptStream(threadId, message);
82
+ innerIterator = gen[Symbol.asyncIterator]();
83
+ let result = await innerIterator.next();
84
+ while (!result.done && !done) {
85
+ events.push(result.value);
86
+ resolveWait?.();
87
+ result = await innerIterator.next();
88
+ }
89
+ } catch (e: any) {
90
+ error = e;
91
+ } finally {
92
+ done = true;
93
+ resolveWait?.();
94
+ }
95
+ });
96
+
97
+ return {
98
+ [Symbol.asyncIterator]() {
99
+ return {
100
+ async next(): Promise<IteratorResult<AgentStreamEvent>> {
101
+ while (events.length === 0 && !done) {
102
+ await new Promise<void>((r) => { resolveWait = r; });
103
+ resolveWait = null;
104
+ }
105
+ if (events.length > 0) return { done: false, value: events.shift()! };
106
+ if (error) throw error;
107
+ return { done: true, value: undefined };
108
+ },
109
+ async return() {
110
+ done = true;
111
+ innerIterator?.return?.();
112
+ return { done: true, value: undefined } as IteratorResult<AgentStreamEvent>;
113
+ },
114
+ async throw(e: any) {
115
+ done = true;
116
+ error = e;
117
+ return { done: true, value: undefined } as IteratorResult<AgentStreamEvent>;
118
+ },
119
+ };
120
+ },
121
+ };
122
+ }
123
+
124
+ // ── Required: dispose ────────────────────────────────
125
+
126
+ async dispose(): Promise<void> {
127
+ if (this.reaperInterval) {
128
+ clearInterval(this.reaperInterval);
129
+ this.reaperInterval = null;
130
+ }
131
+ if (this.mainProcess) {
132
+ await shutdownProcess(this.mainProcess);
133
+ this.mainProcess = null;
134
+ }
135
+ this.threadQueues.clear();
136
+ }
137
+
138
+ // ── Optional overrides ───────────────────────────────
139
+
140
+ async abort(threadId: string): Promise<void> {
141
+ const session = this.store.get(threadId);
142
+ if (!session || !this.mainProcess) return;
143
+ await this.mainProcess.client.call("session/cancel", { sessionId: session.sessionId }).catch(() => {});
144
+ }
145
+
146
+ async restart(threadId: string): Promise<void> {
147
+ this.store.delete(threadId);
148
+ this.threadQueues.delete(threadId);
149
+ }
150
+
151
+ async compact(threadId: string): Promise<{ tokensBefore: number; tokensAfter: number | null } | null> {
152
+ const session = this.store.get(threadId);
153
+ if (!session || !this.mainProcess) return null;
154
+
155
+ const before = session.contextTokens ?? 0;
156
+ await this.mainProcess.client.call("_kiro.dev/commands/execute", {
157
+ sessionId: session.sessionId,
158
+ command: "/compact",
159
+ });
160
+ const after = this.store.get(threadId)?.contextTokens ?? null;
161
+ return { tokensBefore: before, tokensAfter: after };
162
+ }
163
+
164
+ getInfo(threadId?: string): AdapterInfo {
165
+ const session = threadId ? this.store.get(threadId) : undefined;
166
+ return {
167
+ version: this.kiroVersion,
168
+ model: session?.model ?? this.config.agentName ?? "unknown",
169
+ activeSessions: this.store.size,
170
+ cwd: this.config.cwd,
171
+ contextTokens: session?.contextTokens ?? null,
172
+ contextWindow: session?.contextWindow ?? null,
173
+ contextPercent: session?.contextTokens && session?.contextWindow
174
+ ? Math.round((session.contextTokens / session.contextWindow) * 100)
175
+ : null,
176
+ hasMemoryExtension: false,
177
+ memoryTools: [],
178
+ extensions: [],
179
+ };
180
+ }
181
+
182
+ // ── Private: process lifecycle ───────────────────────
183
+
184
+ private async ensureProcess(): Promise<AcpProcess> {
185
+ if (this.mainProcess && !this.mainProcess.client.isClosed) return this.mainProcess;
186
+
187
+ this.mainProcess = spawnKiroCli({ agentName: this.config.agentName, cwd: this.config.cwd });
188
+
189
+ await this.mainProcess.client.call<InitializeResult>("initialize", {
190
+ protocolVersion: "1.0",
191
+ clientInfo: { name: "roundhouse", version: "0.4.3" },
192
+ });
193
+
194
+ if (!this.reaperInterval) {
195
+ this.reaperInterval = setInterval(() => this.reapIdleSessions(), 60_000);
196
+ }
197
+
198
+ return this.mainProcess;
199
+ }
200
+
201
+ private async ensureSession(threadId: string): Promise<SessionEntry> {
202
+ const existing = this.store.get(threadId);
203
+ if (existing) return existing;
204
+
205
+ const proc = await this.ensureProcess();
206
+
207
+ const persistedId = this.store.loadPersistedSessionId(threadId);
208
+ if (persistedId) {
209
+ try {
210
+ await proc.client.call("session/load", { sessionId: persistedId });
211
+ const entry: SessionEntry = {
212
+ sessionId: persistedId,
213
+ threadId,
214
+ createdAt: Date.now(),
215
+ lastUsed: Date.now(),
216
+ inFlight: false,
217
+ contextTokens: null,
218
+ contextWindow: null,
219
+ model: null,
220
+ };
221
+ this.store.set(threadId, entry);
222
+ return entry;
223
+ } catch {
224
+ // Session no longer valid — create new
225
+ }
226
+ }
227
+
228
+ const result = await proc.client.call<SessionNewResult>("session/new", {});
229
+ const entry: SessionEntry = {
230
+ sessionId: result.sessionId,
231
+ threadId,
232
+ createdAt: Date.now(),
233
+ lastUsed: Date.now(),
234
+ inFlight: false,
235
+ contextTokens: null,
236
+ contextWindow: null,
237
+ model: null,
238
+ };
239
+ this.store.set(threadId, entry);
240
+ return entry;
241
+ }
242
+
243
+ // ── Private: prompt logic ────────────────────────────
244
+
245
+ private async doPrompt(threadId: string, message: AgentMessage): Promise<AgentResponse> {
246
+ const session = await this.ensureSession(threadId);
247
+ this.store.markInFlight(threadId, true);
248
+
249
+ try {
250
+ const proc = this.mainProcess!;
251
+ const text = this.formatMessage(message);
252
+
253
+ const responsePromise = proc.client.call<void>("session/prompt", {
254
+ sessionId: session.sessionId,
255
+ text,
256
+ });
257
+
258
+ let fullText = "";
259
+
260
+ const onTextChunk = (params: any) => {
261
+ if (params?.sessionId === session.sessionId) {
262
+ fullText += params.text ?? "";
263
+ }
264
+ };
265
+
266
+ const onPermission = (params: any) => {
267
+ if (params?.sessionId === session.sessionId) {
268
+ proc.client.notify("permission/response", {
269
+ tool_call_id: params.tool_call_id,
270
+ decision: "approved",
271
+ });
272
+ }
273
+ };
274
+
275
+ const onSessionUpdate = (params: any) => {
276
+ if (params?.sessionId === session.sessionId) {
277
+ this.store.updateContext(threadId, params.context_tokens ?? null, params.context_window ?? null, params.model);
278
+ }
279
+ };
280
+
281
+ proc.client.on("text_chunk", onTextChunk);
282
+ proc.client.on("permission_request", onPermission);
283
+ proc.client.on("session/update", onSessionUpdate);
284
+
285
+ try {
286
+ await responsePromise;
287
+ } finally {
288
+ proc.client.off("text_chunk", onTextChunk);
289
+ proc.client.off("permission_request", onPermission);
290
+ proc.client.off("session/update", onSessionUpdate);
291
+ }
292
+
293
+ return { text: fullText };
294
+ } finally {
295
+ this.store.markInFlight(threadId, false);
296
+ }
297
+ }
298
+
299
+ private async *doPromptStream(threadId: string, message: AgentMessage): AsyncIterable<AgentStreamEvent> {
300
+ const session = await this.ensureSession(threadId);
301
+ this.store.markInFlight(threadId, true);
302
+
303
+ try {
304
+ const proc = this.mainProcess!;
305
+ const text = this.formatMessage(message);
306
+
307
+ const events: AgentStreamEvent[] = [];
308
+ let done = false;
309
+ let promptError: Error | null = null;
310
+ let resolveWait: (() => void) | null = null;
311
+
312
+ function push(ev: AgentStreamEvent) {
313
+ events.push(ev);
314
+ resolveWait?.();
315
+ }
316
+
317
+ const onTextChunk = (params: any) => {
318
+ if (params?.sessionId === session.sessionId) {
319
+ push({ type: "text_delta", text: params.text ?? "" });
320
+ }
321
+ };
322
+
323
+ const onToolCall = (params: any) => {
324
+ if (params?.sessionId === session.sessionId) {
325
+ push({ type: "tool_start", toolName: normalizeToolName(params.title ?? params.tool_name ?? ""), toolCallId: params.tool_call_id });
326
+ }
327
+ };
328
+
329
+ const onToolResult = (params: any) => {
330
+ if (params?.sessionId === session.sessionId) {
331
+ push({ type: "tool_end", toolName: normalizeToolName(params.tool_name ?? ""), toolCallId: params.tool_call_id, isError: (params.exit_code ?? 0) !== 0 });
332
+ }
333
+ };
334
+
335
+ const onPermission = (params: any) => {
336
+ if (params?.sessionId === session.sessionId) {
337
+ proc.client.notify("permission/response", {
338
+ tool_call_id: params.tool_call_id,
339
+ decision: "approved",
340
+ });
341
+ }
342
+ };
343
+
344
+ const onComplete = (params: any) => {
345
+ if (params?.sessionId === session.sessionId) {
346
+ if (params.stop_reason === "end_turn") {
347
+ push({ type: "turn_end" });
348
+ }
349
+ push({ type: "agent_end" });
350
+ done = true;
351
+ resolveWait?.();
352
+ }
353
+ };
354
+
355
+ const onSessionUpdate = (params: any) => {
356
+ if (params?.sessionId === session.sessionId) {
357
+ this.store.updateContext(threadId, params.context_tokens ?? null, params.context_window ?? null, params.model);
358
+ }
359
+ };
360
+
361
+ proc.client.on("text_chunk", onTextChunk);
362
+ proc.client.on("tool_call", onToolCall);
363
+ proc.client.on("tool_result", onToolResult);
364
+ proc.client.on("permission_request", onPermission);
365
+ proc.client.on("complete", onComplete);
366
+ proc.client.on("session/update", onSessionUpdate);
367
+
368
+ // Fire the prompt (don't await — events stream in)
369
+ proc.client.call("session/prompt", { sessionId: session.sessionId, text }).catch((err) => {
370
+ push({ type: "agent_end" });
371
+ done = true;
372
+ promptError = err;
373
+ resolveWait?.();
374
+ });
375
+
376
+ try {
377
+ while (!done || events.length > 0) {
378
+ if (events.length > 0) {
379
+ yield events.shift()!;
380
+ } else if (!done) {
381
+ await new Promise<void>((r) => { resolveWait = r; });
382
+ resolveWait = null;
383
+ }
384
+ }
385
+ if (promptError) throw promptError;
386
+ } finally {
387
+ proc.client.off("text_chunk", onTextChunk);
388
+ proc.client.off("tool_call", onToolCall);
389
+ proc.client.off("tool_result", onToolResult);
390
+ proc.client.off("permission_request", onPermission);
391
+ proc.client.off("complete", onComplete);
392
+ proc.client.off("session/update", onSessionUpdate);
393
+ }
394
+ } finally {
395
+ this.store.markInFlight(threadId, false);
396
+ }
397
+ }
398
+
399
+ // ── Private: utilities ───────────────────────────────
400
+
401
+ private enqueue<T>(threadId: string, fn: () => Promise<T>): Promise<T> {
402
+ let tq = this.threadQueues.get(threadId);
403
+ if (!tq) {
404
+ tq = { queue: [], running: false };
405
+ this.threadQueues.set(threadId, tq);
406
+ }
407
+
408
+ return new Promise<T>((resolve, reject) => {
409
+ tq!.queue.push(async () => {
410
+ try { resolve(await fn()); }
411
+ catch (e) { reject(e); }
412
+ });
413
+ this.drainQueue(threadId);
414
+ });
415
+ }
416
+
417
+ private async drainQueue(threadId: string): Promise<void> {
418
+ const tq = this.threadQueues.get(threadId);
419
+ if (!tq || tq.running) return;
420
+ tq.running = true;
421
+ while (tq.queue.length > 0) {
422
+ const task = tq.queue.shift()!;
423
+ await task();
424
+ }
425
+ tq.running = false;
426
+ }
427
+
428
+ private reapIdleSessions(): void {
429
+ const idle = this.store.getIdleSessions();
430
+ for (const threadId of idle) {
431
+ this.store.delete(threadId);
432
+ this.threadQueues.delete(threadId);
433
+ }
434
+ }
435
+
436
+ private formatMessage(msg: AgentMessage): string {
437
+ let text = msg.text;
438
+ if (msg.attachments && msg.attachments.length > 0) {
439
+ const manifest = msg.attachments.map((a) => ({
440
+ id: a.id, type: a.mediaType, name: a.name,
441
+ localPath: a.localPath, mime: a.mime,
442
+ sizeBytes: a.sizeBytes, untrusted: true,
443
+ }));
444
+ text += `\n\nChat attachments saved locally. Inspect files with tools before making claims. Transcripts are approximate; use the raw file if exact wording matters.\n\`\`\`json\n${JSON.stringify(manifest, null, 2)}\n\`\`\``;
445
+ }
446
+ return text;
447
+ }
448
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * session.ts — Kiro session state management
3
+ *
4
+ * Tracks active sessions per thread, handles idle reaping,
5
+ * and persists session IDs for potential resumption.
6
+ */
7
+
8
+ import { resolve } from "node:path";
9
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "node:fs";
10
+ import { randomBytes } from "node:crypto";
11
+
12
+ export interface SessionEntry {
13
+ sessionId: string;
14
+ threadId: string;
15
+ createdAt: number;
16
+ lastUsed: number;
17
+ inFlight: boolean;
18
+ contextTokens: number | null;
19
+ contextWindow: number | null;
20
+ model: string | null;
21
+ }
22
+
23
+ export interface SessionStoreOptions {
24
+ sessionsDir: string;
25
+ maxIdleMs?: number;
26
+ }
27
+
28
+ /**
29
+ * Manages kiro session entries per thread.
30
+ * Persistence is a simple JSON file per thread for session resumption.
31
+ */
32
+ export class SessionStore {
33
+ private sessions = new Map<string, SessionEntry>();
34
+ private readonly maxIdleMs: number;
35
+ private readonly sessionsDir: string;
36
+
37
+ constructor(opts: SessionStoreOptions) {
38
+ this.sessionsDir = opts.sessionsDir;
39
+ this.maxIdleMs = opts.maxIdleMs ?? 30 * 60 * 1000; // 30 min
40
+ }
41
+
42
+ get(threadId: string): SessionEntry | undefined {
43
+ return this.sessions.get(threadId);
44
+ }
45
+
46
+ set(threadId: string, entry: SessionEntry): void {
47
+ this.sessions.set(threadId, entry);
48
+ this.persistSession(threadId, entry);
49
+ }
50
+
51
+ delete(threadId: string): void {
52
+ this.sessions.delete(threadId);
53
+ }
54
+
55
+ get size(): number {
56
+ return this.sessions.size;
57
+ }
58
+
59
+ markInFlight(threadId: string, inFlight: boolean): void {
60
+ const entry = this.sessions.get(threadId);
61
+ if (entry) {
62
+ entry.inFlight = inFlight;
63
+ if (!inFlight) entry.lastUsed = Date.now();
64
+ }
65
+ }
66
+
67
+ updateContext(threadId: string, tokens: number | null, window: number | null, model?: string): void {
68
+ const entry = this.sessions.get(threadId);
69
+ if (entry) {
70
+ entry.contextTokens = tokens;
71
+ entry.contextWindow = window;
72
+ if (model) entry.model = model;
73
+ }
74
+ }
75
+
76
+ /** Return thread IDs of sessions that are idle and not in-flight. */
77
+ getIdleSessions(): string[] {
78
+ const now = Date.now();
79
+ const idle: string[] = [];
80
+ for (const [threadId, entry] of this.sessions) {
81
+ if (!entry.inFlight && (now - entry.lastUsed) > this.maxIdleMs) {
82
+ idle.push(threadId);
83
+ }
84
+ }
85
+ return idle;
86
+ }
87
+
88
+ /** Load persisted session ID for a thread (for session/load attempts). */
89
+ loadPersistedSessionId(threadId: string): string | null {
90
+ const filePath = this.sessionFilePath(threadId);
91
+ if (!existsSync(filePath)) return null;
92
+ try {
93
+ const data = JSON.parse(readFileSync(filePath, "utf8"));
94
+ return data.sessionId ?? null;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ // ── Private ──────────────────────────────────────────
101
+
102
+ private persistSession(threadId: string, entry: SessionEntry): void {
103
+ const dir = this.threadDir(threadId);
104
+ mkdirSync(dir, { recursive: true });
105
+ const filePath = resolve(dir, "kiro.json");
106
+ const tmpPath = filePath + "." + randomBytes(4).toString("hex") + ".tmp";
107
+ writeFileSync(tmpPath, JSON.stringify({
108
+ sessionId: entry.sessionId,
109
+ createdAt: entry.createdAt,
110
+ lastUsed: entry.lastUsed,
111
+ }) + "\n");
112
+ renameSync(tmpPath, filePath);
113
+ }
114
+
115
+ private sessionFilePath(threadId: string): string {
116
+ return resolve(this.threadDir(threadId), "kiro.json");
117
+ }
118
+
119
+ private threadDir(threadId: string): string {
120
+ // Sanitize thread ID for filesystem use
121
+ const dirName = threadId.replace(/[^a-zA-Z0-9_-]/g, "_");
122
+ return resolve(this.sessionsDir, dirName);
123
+ }
124
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * tool-names.ts — Normalize kiro-cli tool titles to canonical names
3
+ *
4
+ * kiro-cli decorates tool titles with human-friendly prefixes.
5
+ * This module strips them for consistent matching.
6
+ */
7
+
8
+ const TITLE_PREFIXES = ["Running: ", "Reading "] as const;
9
+
10
+ /** Strip known kiro-cli title prefixes to get the canonical tool name. */
11
+ export function normalizeToolName(raw: string): string {
12
+ for (const prefix of TITLE_PREFIXES) {
13
+ if (raw.startsWith(prefix)) return raw.slice(prefix.length);
14
+ }
15
+ return raw;
16
+ }
17
+
18
+ /**
19
+ * Match a pattern against a tool name.
20
+ * Supports: "*" (all), "prefix*", "*suffix", "*contains*", exact match.
21
+ * Case-insensitive.
22
+ */
23
+ export function toolMatches(pattern: string, name: string): boolean {
24
+ if (pattern === "*") return true;
25
+ const p = pattern.toLowerCase();
26
+ const n = name.toLowerCase();
27
+ if (p === n) return true;
28
+
29
+ // Glob patterns
30
+ if (p.startsWith("*") && p.endsWith("*") && p.length > 2) {
31
+ return n.includes(p.slice(1, -1));
32
+ }
33
+ if (p.endsWith("*")) return n.startsWith(p.slice(0, -1));
34
+ if (p.startsWith("*")) return n.endsWith(p.slice(1));
35
+
36
+ return false;
37
+ }
@@ -1,9 +1,12 @@
1
1
  /**
2
- * agents/pi.ts — Pi agent adapter
2
+ * agents/pi/pi-adapter.ts — Pi agent adapter
3
3
  *
4
4
  * Wraps pi's SDK (createAgentSession) as an AgentAdapter.
5
5
  * One persistent session per thread, stored at:
6
6
  * ~/.roundhouse/sessions/<thread_id>/<session>.jsonl
7
+ *
8
+ * TODO: Migrate from factory+object-literal to class extending BaseAdapter
9
+ * (separate PR — large file, needs careful testing)
7
10
  */
8
11
 
9
12
  import { mkdir } from "node:fs/promises";
@@ -23,9 +26,9 @@ import {
23
26
  type AgentSessionEvent,
24
27
  } from "@mariozechner/pi-coding-agent";
25
28
 
26
- import type { AgentAdapter, AgentAdapterFactory, AgentMessage, AgentResponse, AgentStreamEvent } from "../types";
27
- import { SESSIONS_DIR } from "../config";
28
- import { DEBUG_STREAM, threadIdToDir } from "../util";
29
+ import type { AgentAdapter, AgentAdapterFactory, AgentMessage, AgentResponse, AgentStreamEvent } from "../../types";
30
+ import { SESSIONS_DIR } from "../../config";
31
+ import { DEBUG_STREAM, threadIdToDir } from "../../util";
29
32
 
30
33
  interface SessionEntry {
31
34
  session: AgentSession;
@@ -601,7 +604,7 @@ export const createPiAgentAdapter: AgentAdapterFactory = (config) => {
601
604
  // Read agent version
602
605
  let version = "unknown";
603
606
  try {
604
- const piPkgPath = join(__piAdapterDir, "..", "..", "node_modules", "@mariozechner", "pi-coding-agent", "package.json");
607
+ const piPkgPath = join(__piAdapterDir, "..", "..", "..", "node_modules", "@mariozechner", "pi-coding-agent", "package.json");
605
608
  version = JSON.parse(readFileSync(piPkgPath, "utf8")).version;
606
609
  } catch {}
607
610
 
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import type { AgentAdapterFactory } from "../types";
9
- import { createPiAgentAdapter } from "./pi";
9
+ import { createPiAgentAdapter } from "./pi/pi-adapter";
10
+ import { createKiroAgentAdapter } from "./kiro/kiro-adapter";
10
11
  import { homedir } from "node:os";
11
12
  import { resolve } from "node:path";
12
13
 
@@ -80,13 +81,34 @@ const piDefinition: AgentDefinition = {
80
81
  // setup-specific helpers (execOrFail, atomicWriteJson, etc.)
81
82
  };
82
83
 
84
+ // ── Kiro Definition ──────────────────────────────────
85
+
86
+ const kiroDefinition: AgentDefinition = {
87
+ type: "kiro",
88
+ name: "Kiro",
89
+ factory: createKiroAgentAdapter,
90
+ available: true,
91
+ packages: [
92
+ {
93
+ name: "Kiro CLI",
94
+ packageName: "kiro-cli",
95
+ install: "global",
96
+ binary: "kiro-cli",
97
+ },
98
+ ],
99
+ sdkPackage: undefined,
100
+ configDefaults: { cwd: homedir() },
101
+ configDirs: [
102
+ resolve(homedir(), ".kiro", "agents"),
103
+ resolve(homedir(), ".kiro", "settings"),
104
+ ],
105
+ };
106
+
83
107
  // ── Registry ─────────────────────────────────────────
84
108
 
85
109
  const definitions = new Map<string, AgentDefinition>();
86
110
  definitions.set("pi", piDefinition);
87
-
88
- // Future:
89
- // definitions.set("kiro", kiroDefinition);
111
+ definitions.set("kiro", kiroDefinition);
90
112
 
91
113
  // ── Public API ───────────────────────────────────────
92
114