kiro-telegram-bot 1.5.1

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.
Files changed (83) hide show
  1. package/.env.example +104 -0
  2. package/LICENSE +21 -0
  3. package/README.md +517 -0
  4. package/bin/kiro-tg.mjs +21 -0
  5. package/docs/INSTALL.md +143 -0
  6. package/docs/ops/RELEASE_CHECKLIST.md +39 -0
  7. package/package.json +70 -0
  8. package/scripts/mq.ts +25 -0
  9. package/scripts/setup.mjs +78 -0
  10. package/src/acp/client.ts +456 -0
  11. package/src/acp/server-handlers.ts +85 -0
  12. package/src/acp/transport.ts +50 -0
  13. package/src/acp/types.ts +136 -0
  14. package/src/agents/catalog.ts +44 -0
  15. package/src/app/json-store.ts +54 -0
  16. package/src/app/reasoning.ts +30 -0
  17. package/src/app/settings-store.ts +31 -0
  18. package/src/app/stt.ts +53 -0
  19. package/src/app/types.ts +48 -0
  20. package/src/app/usage.ts +32 -0
  21. package/src/bot/auth.ts +27 -0
  22. package/src/bot/bot.ts +154 -0
  23. package/src/bot/chat-controller.ts +251 -0
  24. package/src/bot/commands.ts +48 -0
  25. package/src/bot/deps.ts +47 -0
  26. package/src/bot/handlers/control.ts +94 -0
  27. package/src/bot/handlers/history.ts +58 -0
  28. package/src/bot/handlers/kill.ts +69 -0
  29. package/src/bot/handlers/mcp.ts +205 -0
  30. package/src/bot/handlers/menu.ts +204 -0
  31. package/src/bot/handlers/message.ts +93 -0
  32. package/src/bot/handlers/photo.ts +108 -0
  33. package/src/bot/handlers/projects.ts +83 -0
  34. package/src/bot/handlers/running.ts +104 -0
  35. package/src/bot/handlers/session-card.ts +65 -0
  36. package/src/bot/handlers/sessions.ts +131 -0
  37. package/src/bot/handlers/system.ts +51 -0
  38. package/src/bot/handlers/tasks.ts +223 -0
  39. package/src/bot/handlers/usage.ts +33 -0
  40. package/src/bot/handlers/voice.ts +53 -0
  41. package/src/bot/image-return.ts +69 -0
  42. package/src/bot/menu/keyboard.ts +47 -0
  43. package/src/bot/menu/refresh.ts +13 -0
  44. package/src/bot/menu/status-panel.ts +78 -0
  45. package/src/bot/permission-service.ts +149 -0
  46. package/src/bot/prompt-content.ts +49 -0
  47. package/src/bot/prompt-retry.ts +70 -0
  48. package/src/bot/registry.ts +178 -0
  49. package/src/bot/session-runtime.ts +670 -0
  50. package/src/bot/telegram-io.ts +109 -0
  51. package/src/bot/typing.ts +35 -0
  52. package/src/bot/wizard/task-wizard.ts +214 -0
  53. package/src/cli.ts +125 -0
  54. package/src/config.ts +190 -0
  55. package/src/index.ts +74 -0
  56. package/src/logger.ts +78 -0
  57. package/src/mcp/config.ts +103 -0
  58. package/src/mcp/probe.ts +218 -0
  59. package/src/mcp/types.ts +68 -0
  60. package/src/projects/manager.ts +88 -0
  61. package/src/render/chunk.ts +57 -0
  62. package/src/render/diff.ts +48 -0
  63. package/src/render/escape.ts +22 -0
  64. package/src/render/markdown.ts +126 -0
  65. package/src/render/subagent.ts +75 -0
  66. package/src/render/tool-call.ts +102 -0
  67. package/src/service/index.ts +24 -0
  68. package/src/service/linux.ts +83 -0
  69. package/src/service/macos.ts +91 -0
  70. package/src/service/platform.ts +59 -0
  71. package/src/service/types.ts +34 -0
  72. package/src/service/windows.ts +103 -0
  73. package/src/sessions/history.ts +181 -0
  74. package/src/sessions/store.ts +133 -0
  75. package/src/sessions/tail.ts +86 -0
  76. package/src/sessions/types.ts +26 -0
  77. package/src/stream/streamer.ts +167 -0
  78. package/src/tasks/runner.ts +82 -0
  79. package/src/tasks/schedule.ts +142 -0
  80. package/src/tasks/scheduler.ts +53 -0
  81. package/src/tasks/store.ts +80 -0
  82. package/src/tasks/types.ts +33 -0
  83. package/tsconfig.json +19 -0
@@ -0,0 +1,456 @@
1
+ /**
2
+ * ACP client — spawns `kiro-cli acp` and speaks JSON-RPC 2.0 over stdio.
3
+ *
4
+ * One process manages many sessions. Callers create/load sessions and send
5
+ * prompts; streamed `session/update` notifications are re-emitted as
6
+ * "session-update" events keyed by sessionId.
7
+ */
8
+ import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
9
+ import { EventEmitter } from "node:events";
10
+ import { createLogger } from "../logger.js";
11
+ import { handleServerRequest, type ServerHandlerOptions } from "./server-handlers.js";
12
+ import { JsonRpcTransport } from "./transport.js";
13
+ import type {
14
+ ContentBlock,
15
+ InitializeResult,
16
+ JsonRpcMessage,
17
+ PendingStage,
18
+ PermissionOutcome,
19
+ PromptResult,
20
+ RequestPermissionParams,
21
+ SessionNotificationParams,
22
+ SessionUpdate,
23
+ SubagentInfo,
24
+ SubagentListUpdate,
25
+ } from "./types.js";
26
+
27
+ const log = createLogger("acp:client");
28
+
29
+ /** JSON-RPC error codes that usually mean "transient backend hiccup". */
30
+ const TRANSIENT_CODES = new Set([-32603, -32500, -32000, 500, 502, 503, 504, 429]);
31
+ const TRANSIENT_RE =
32
+ /internal error|high volume|experiencing|overloaded|temporar|unavailable|rate.?limit|too many requests|try again|capacity|\b50[234]\b|\b429\b/i;
33
+
34
+ /** Error that preserves the agent's JSON-RPC error code and data payload. */
35
+ export class AcpError extends Error {
36
+ constructor(
37
+ message: string,
38
+ readonly code?: number,
39
+ readonly data?: unknown,
40
+ ) {
41
+ super(message);
42
+ this.name = "AcpError";
43
+ }
44
+ }
45
+
46
+ /** Heuristic: is this prompt failure likely transient and safe to retry? */
47
+ export function isTransientAcpError(err: Error): boolean {
48
+ const code = (err as AcpError).code;
49
+ if (typeof code === "number" && TRANSIENT_CODES.has(code)) return true;
50
+ return TRANSIENT_RE.test(err.message);
51
+ }
52
+
53
+ /** Compact, log/Telegram-safe stringification of an error's data payload. */
54
+ function shortJson(v: unknown): string {
55
+ try {
56
+ const s = typeof v === "string" ? v : JSON.stringify(v);
57
+ return s.length > 300 ? `${s.slice(0, 300)}\u2026` : s;
58
+ } catch {
59
+ return String(v);
60
+ }
61
+ }
62
+
63
+ export interface AcpClientOptions {
64
+ kiroCliPath: string;
65
+ workspace: string;
66
+ trustAllTools: boolean;
67
+ agent?: string;
68
+ requestTimeoutMs?: number;
69
+ autoRestart?: boolean;
70
+ /** Reject a prompt only after this long with no streaming activity. */
71
+ promptIdleTimeoutMs?: number;
72
+ /** Absolute safety cap for a single prompt. */
73
+ promptMaxMs?: number;
74
+ }
75
+
76
+ interface Pending {
77
+ resolve: (v: unknown) => void;
78
+ reject: (e: Error) => void;
79
+ cleanup: () => void;
80
+ method: string;
81
+ }
82
+
83
+ export declare interface AcpClient {
84
+ on(e: "session-update", l: (sessionId: string, update: SessionUpdate) => void): this;
85
+ on(e: "notification", l: (method: string, params: unknown) => void): this;
86
+ on(e: "exit", l: (code: number | null) => void): this;
87
+ on(e: "restarted", l: () => void): this;
88
+ on(e: "subagents", l: (subagents: SubagentInfo[], pending: PendingStage[]) => void): this;
89
+ emit(e: "session-update", sessionId: string, update: SessionUpdate): boolean;
90
+ emit(e: "notification", method: string, params: unknown): boolean;
91
+ emit(e: "exit", code: number | null): boolean;
92
+ emit(e: "restarted"): boolean;
93
+ emit(e: "subagents", subagents: SubagentInfo[], pending: PendingStage[]): boolean;
94
+ }
95
+
96
+ export class AcpClient extends EventEmitter {
97
+ private proc?: ChildProcessWithoutNullStreams;
98
+ private transport?: JsonRpcTransport;
99
+ private nextId = 1;
100
+ private readonly pending = new Map<number | string, Pending>();
101
+ private readonly timeout: number;
102
+ private readonly promptIdleMs: number;
103
+ private readonly promptMaxMs: number;
104
+ /** Last time we saw streaming activity for a session (epoch ms). */
105
+ private readonly lastActivity = new Map<string, number>();
106
+ private stopped = false;
107
+ private restartAttempts = 0;
108
+ private restartTimer?: NodeJS.Timeout;
109
+ agentInfo?: { name?: string; version?: string };
110
+ capabilities?: InitializeResult["agentCapabilities"];
111
+ /** Available agent "modes" advertised by Kiro for new sessions. */
112
+ availableModes: Array<{ id: string; name: string; description?: string }> = [];
113
+ currentModeId?: string;
114
+ /** Available models advertised by Kiro (from session/new or session/load). */
115
+ availableModels: Array<{ modelId: string; name: string; description?: string }> = [];
116
+ currentModelId?: string;
117
+ /** Latest metadata per session (context usage %, effort). */
118
+ private readonly metadata = new Map<string, { contextUsagePercentage?: number; effort?: string }>();
119
+ /** Latest process-global subagent ("crew") list reported by Kiro. */
120
+ private subagents: SubagentInfo[] = [];
121
+ private pendingStages: PendingStage[] = [];
122
+ /** Optional handler for tool permission requests (set by the bot layer). */
123
+ permissionHandler?: (params: RequestPermissionParams) => Promise<PermissionOutcome>;
124
+
125
+ constructor(private readonly opts: AcpClientOptions) {
126
+ super();
127
+ this.setMaxListeners(0); // one session-update listener per chat runtime
128
+ this.timeout = opts.requestTimeoutMs ?? 120_000;
129
+ this.promptIdleMs = opts.promptIdleTimeoutMs ?? 900_000; // 15 min with no activity
130
+ this.promptMaxMs = opts.promptMaxMs ?? 6 * 60 * 60_000; // 6 h hard cap
131
+ }
132
+
133
+ /** Spawn the process and run the ACP initialize handshake. */
134
+ async start(): Promise<void> {
135
+ this.stopped = false;
136
+ await this.connect();
137
+ }
138
+
139
+ private async connect(): Promise<void> {
140
+ const args = ["acp"];
141
+ if (this.opts.trustAllTools) args.push("--trust-all-tools");
142
+ if (this.opts.agent) args.push("--agent", this.opts.agent);
143
+
144
+ log.info(`spawning: ${this.opts.kiroCliPath} ${args.join(" ")}`);
145
+ this.proc = spawn(this.opts.kiroCliPath, args, {
146
+ stdio: ["pipe", "pipe", "pipe"],
147
+ cwd: this.opts.workspace,
148
+ env: { ...process.env, KIRO_LOG_LEVEL: process.env.KIRO_LOG_LEVEL || "error" },
149
+ }) as ChildProcessWithoutNullStreams;
150
+
151
+ this.proc.on("exit", (code) => {
152
+ log.warn(`kiro-cli acp exited (code ${code})`);
153
+ this.failAllPending(new Error(`kiro-cli acp exited (code ${code})`));
154
+ this.emit("exit", code);
155
+ this.maybeRestart();
156
+ });
157
+ this.proc.on("error", (err) => {
158
+ log.error("failed to spawn kiro-cli:", err.message);
159
+ this.failAllPending(err);
160
+ });
161
+
162
+ this.transport = new JsonRpcTransport(this.proc);
163
+ this.transport.on("message", (m: JsonRpcMessage) => this.onMessage(m));
164
+
165
+ const init = (await this.request("initialize", {
166
+ protocolVersion: 1,
167
+ clientCapabilities: {
168
+ fs: { readTextFile: true, writeTextFile: true },
169
+ terminal: true,
170
+ },
171
+ clientInfo: { name: "kiro-telegram-bot", version: "1.0.0" },
172
+ })) as InitializeResult;
173
+
174
+ this.agentInfo = init.agentInfo;
175
+ this.capabilities = init.agentCapabilities;
176
+ this.restartAttempts = 0;
177
+ this.subagents = [];
178
+ this.pendingStages = [];
179
+ log.info(`connected: ${init.agentInfo?.name ?? "kiro"} ${init.agentInfo?.version ?? ""}`.trim());
180
+ }
181
+
182
+ /** Restart the agent with exponential backoff after an unexpected exit. */
183
+ private maybeRestart(): void {
184
+ if (this.stopped || !this.opts.autoRestart) return;
185
+ const delay = Math.min(30_000, 1000 * 2 ** this.restartAttempts);
186
+ this.restartAttempts += 1;
187
+ log.warn(`auto-restarting ACP in ${delay}ms (attempt ${this.restartAttempts})`);
188
+ this.restartTimer = setTimeout(() => {
189
+ this.connect()
190
+ .then(() => {
191
+ log.info("ACP reconnected");
192
+ this.emit("restarted");
193
+ })
194
+ .catch((e) => {
195
+ log.error("ACP restart failed:", (e as Error).message);
196
+ this.maybeRestart();
197
+ });
198
+ }, delay);
199
+ }
200
+
201
+ get supportsLoadSession(): boolean {
202
+ return Boolean(this.capabilities?.loadSession);
203
+ }
204
+
205
+ /** PID of the bot's own kiro-cli acp process (to avoid killing ourselves). */
206
+ get pid(): number | undefined {
207
+ return this.proc?.pid;
208
+ }
209
+
210
+ async newSession(cwd: string): Promise<string> {
211
+ const res = (await this.request("session/new", { cwd, mcpServers: [] })) as { sessionId: string };
212
+ this.parseSessionExtras(res);
213
+ return res.sessionId;
214
+ }
215
+
216
+ async loadSession(sessionId: string, cwd: string): Promise<void> {
217
+ const res = await this.request("session/load", { sessionId, cwd, mcpServers: [] });
218
+ this.parseSessionExtras(res);
219
+ }
220
+
221
+ hasMode(id: string): boolean {
222
+ return this.availableModes.some((m) => m.id === id);
223
+ }
224
+
225
+ hasModel(id: string): boolean {
226
+ return id === "auto" || this.availableModels.some((m) => m.modelId === id);
227
+ }
228
+
229
+ /** Capture available modes (agents) and models from a session response. */
230
+ private parseSessionExtras(result: unknown): void {
231
+ const r = result as {
232
+ modes?: { currentModeId?: string; availableModes?: Array<{ id: string; name: string; description?: string }> };
233
+ models?: { currentModelId?: string; availableModels?: Array<{ modelId: string; name: string; description?: string }> };
234
+ };
235
+ if (r?.modes?.availableModes?.length) this.availableModes = r.modes.availableModes;
236
+ if (r?.modes?.currentModeId) this.currentModeId = r.modes.currentModeId;
237
+ if (r?.models?.availableModels?.length) this.availableModels = r.models.availableModels;
238
+ if (r?.models?.currentModelId) this.currentModelId = r.models.currentModelId;
239
+ }
240
+
241
+ /**
242
+ * Send a prompt. Resolves when the turn ends. Instead of a fixed timeout
243
+ * (which kills long turns), this rejects only after `promptIdleMs` with no
244
+ * streaming activity, or after the absolute `promptMaxMs` cap.
245
+ *
246
+ * Transient-error auto-retry (with backoff and user feedback) is orchestrated
247
+ * one level up in the bot runtime — see `SessionRuntime.runPromptWithRetries`.
248
+ */
249
+ prompt(sessionId: string, content: ContentBlock[]): Promise<PromptResult> {
250
+ return new Promise<PromptResult>((resolve, reject) => {
251
+ const id = this.nextId++;
252
+ const start = Date.now();
253
+ this.lastActivity.set(sessionId, start);
254
+ const watch = setInterval(() => {
255
+ const idle = Date.now() - (this.lastActivity.get(sessionId) ?? start);
256
+ const total = Date.now() - start;
257
+ if (total > this.promptMaxMs) {
258
+ this.pending.delete(id);
259
+ clearInterval(watch);
260
+ reject(new Error(`Prompt exceeded the ${Math.round(this.promptMaxMs / 60_000)}min cap`));
261
+ } else if (idle > this.promptIdleMs) {
262
+ this.pending.delete(id);
263
+ clearInterval(watch);
264
+ reject(new Error(`No agent activity for ${Math.round(idle / 1000)}s — giving up`));
265
+ }
266
+ }, 15_000);
267
+ this.pending.set(id, {
268
+ resolve: (v) => resolve(v as PromptResult),
269
+ reject,
270
+ cleanup: () => clearInterval(watch),
271
+ method: "session/prompt",
272
+ });
273
+ try {
274
+ this.transport!.send({ jsonrpc: "2.0", id, method: "session/prompt", params: { sessionId, prompt: content } });
275
+ } catch (e) {
276
+ clearInterval(watch);
277
+ this.pending.delete(id);
278
+ reject(e as Error);
279
+ }
280
+ });
281
+ }
282
+
283
+ async cancel(sessionId: string): Promise<void> {
284
+ try {
285
+ // session/cancel is a notification in ACP.
286
+ this.transport?.send({ jsonrpc: "2.0", method: "session/cancel", params: { sessionId } });
287
+ } catch (e) {
288
+ log.debug("cancel failed:", (e as Error).message);
289
+ }
290
+ }
291
+
292
+ async setModel(sessionId: string, modelId: string): Promise<void> {
293
+ await this.request("session/set_model", { sessionId, modelId });
294
+ }
295
+
296
+ async setMode(sessionId: string, modeId: string): Promise<void> {
297
+ await this.request("session/set_mode", { sessionId, modeId });
298
+ this.currentModeId = modeId;
299
+ }
300
+
301
+ /** Execute a Kiro slash command via the _kiro.dev extension. */
302
+ async executeCommand(sessionId: string, command: string): Promise<unknown> {
303
+ return this.request("_kiro.dev/commands/execute", { sessionId, command });
304
+ }
305
+
306
+ stop(): void {
307
+ this.stopped = true;
308
+ if (this.restartTimer) clearTimeout(this.restartTimer);
309
+ this.proc?.kill();
310
+ this.proc = undefined;
311
+ }
312
+
313
+ /** Manually restart the agent (used by the /restart command). */
314
+ async restart(): Promise<void> {
315
+ this.stopped = true;
316
+ if (this.restartTimer) clearTimeout(this.restartTimer);
317
+ this.proc?.kill();
318
+ this.proc = undefined;
319
+ this.stopped = false;
320
+ await this.connect();
321
+ this.emit("restarted");
322
+ }
323
+
324
+ // ── JSON-RPC plumbing ──────────────────────────────────────────────────────
325
+
326
+ private request(method: string, params: unknown): Promise<unknown> {
327
+ return new Promise((resolve, reject) => {
328
+ const id = this.nextId++;
329
+ const timer = setTimeout(() => {
330
+ this.pending.delete(id);
331
+ reject(new Error(`Timeout after ${this.timeout}ms: ${method}`));
332
+ }, this.timeout);
333
+ this.pending.set(id, { resolve, reject, cleanup: () => clearTimeout(timer), method });
334
+ try {
335
+ this.transport!.send({ jsonrpc: "2.0", id, method, params });
336
+ } catch (e) {
337
+ clearTimeout(timer);
338
+ this.pending.delete(id);
339
+ reject(e as Error);
340
+ }
341
+ });
342
+ }
343
+
344
+ /** Build a rich Error from a JSON-RPC error object, and log it. */
345
+ private toAcpError(error: { code: number; message: string; data?: unknown }, method: string): AcpError {
346
+ const codeStr = typeof error.code === "number" ? ` [${error.code}]` : "";
347
+ const detail = error.data === undefined ? "" : ` — ${shortJson(error.data)}`;
348
+ const text = `${error.message || "ACP error"}${codeStr}${detail}`;
349
+ log.warn(`${method} failed: ${text}`);
350
+ return new AcpError(text, error.code, error.data);
351
+ }
352
+
353
+ private onMessage(msg: JsonRpcMessage): void {
354
+ // Response to one of our requests.
355
+ if (msg.id !== undefined && msg.id !== null && this.pending.has(msg.id) && msg.method === undefined) {
356
+ const p = this.pending.get(msg.id)!;
357
+ p.cleanup();
358
+ this.pending.delete(msg.id);
359
+ if (msg.error) p.reject(this.toAcpError(msg.error, p.method));
360
+ else p.resolve(msg.result);
361
+ return;
362
+ }
363
+
364
+ // Request from the agent (has both id and method) — needs a response.
365
+ if (msg.id !== undefined && msg.id !== null && msg.method) {
366
+ void this.respondToServerRequest(msg.id, msg.method, (msg.params as Record<string, unknown>) || {});
367
+ return;
368
+ }
369
+
370
+ // Notification (method, no id).
371
+ if (msg.method) {
372
+ this.routeNotification(msg.method, msg.params);
373
+ }
374
+ }
375
+
376
+ private async respondToServerRequest(
377
+ id: number | string,
378
+ method: string,
379
+ params: Record<string, unknown>,
380
+ ): Promise<void> {
381
+ const handlerOpts: ServerHandlerOptions = {
382
+ workspace: this.opts.workspace,
383
+ trustAllTools: this.opts.trustAllTools,
384
+ };
385
+ try {
386
+ let result: unknown;
387
+ if (method === "session/request_permission" && this.permissionHandler) {
388
+ result = await this.permissionHandler(params as unknown as RequestPermissionParams);
389
+ } else {
390
+ result = await handleServerRequest(method, params, handlerOpts);
391
+ }
392
+ this.transport?.send({ jsonrpc: "2.0", id, result });
393
+ } catch (err) {
394
+ this.transport?.send({
395
+ jsonrpc: "2.0",
396
+ id,
397
+ error: { code: -32000, message: (err as Error).message },
398
+ });
399
+ }
400
+ }
401
+
402
+ private routeNotification(method: string, params: unknown): void {
403
+ if (method === "session/update") {
404
+ const p = params as SessionNotificationParams;
405
+ if (p?.sessionId && p.update) {
406
+ this.lastActivity.set(p.sessionId, Date.now()); // keeps long, active turns alive
407
+ this.emit("session-update", p.sessionId, p.update);
408
+ return;
409
+ }
410
+ }
411
+ if (method === "_kiro.dev/metadata") {
412
+ const p = params as { sessionId?: string; contextUsagePercentage?: number; effort?: string };
413
+ if (p?.sessionId) {
414
+ this.metadata.set(p.sessionId, {
415
+ contextUsagePercentage: p.contextUsagePercentage,
416
+ effort: p.effort,
417
+ });
418
+ }
419
+ }
420
+ if (method === "_kiro.dev/subagent/list_update") {
421
+ const p = (params as SubagentListUpdate) || {};
422
+ this.subagents = Array.isArray(p.subagents) ? p.subagents : [];
423
+ this.pendingStages = Array.isArray(p.pendingStages) ? p.pendingStages : [];
424
+ this.emit("subagents", this.subagents, this.pendingStages);
425
+ }
426
+ this.emit("notification", method, params);
427
+ }
428
+
429
+ /** Latest process-global subagent list (a copy). */
430
+ currentSubagents(): SubagentInfo[] {
431
+ return this.subagents.slice();
432
+ }
433
+
434
+ /** Latest pending pipeline stages (a copy). */
435
+ currentPendingStages(): PendingStage[] {
436
+ return this.pendingStages.slice();
437
+ }
438
+
439
+ /** Look up a subagent by its (own) session id. */
440
+ subagentById(sessionId: string): SubagentInfo | undefined {
441
+ return this.subagents.find((s) => s.sessionId === sessionId);
442
+ }
443
+
444
+ /** Latest context-usage % / effort reported for a session. */
445
+ metadataFor(sessionId: string | undefined): { contextUsagePercentage?: number; effort?: string } | undefined {
446
+ return sessionId ? this.metadata.get(sessionId) : undefined;
447
+ }
448
+
449
+ private failAllPending(err: Error): void {
450
+ for (const [, p] of this.pending) {
451
+ p.cleanup();
452
+ p.reject(err);
453
+ }
454
+ this.pending.clear();
455
+ }
456
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Handlers for requests the Kiro agent sends back to us (client) during a turn:
3
+ * file reads/writes, terminal execution, and permission prompts.
4
+ *
5
+ * With --trust-all-tools, Kiro usually executes tools itself, but the protocol
6
+ * still allows it to delegate fs/terminal work to the client, so we implement them.
7
+ */
8
+ import { execSync } from "node:child_process";
9
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { dirname, isAbsolute, join } from "node:path";
11
+ import type { RequestPermissionParams } from "./types.js";
12
+ import { createLogger } from "../logger.js";
13
+
14
+ const log = createLogger("acp:server");
15
+
16
+ export interface ServerHandlerOptions {
17
+ workspace: string;
18
+ /** When true, auto-approve permission requests. */
19
+ trustAllTools: boolean;
20
+ }
21
+
22
+ function resolveInWorkspace(p: string, workspace: string): string {
23
+ return isAbsolute(p) ? p : join(workspace, p);
24
+ }
25
+
26
+ /**
27
+ * Dispatch a request initiated by the agent. Returns a JSON-RPC `result`.
28
+ * Throws on unsupported methods (caller converts to a JSON-RPC error).
29
+ */
30
+ export async function handleServerRequest(
31
+ method: string,
32
+ params: Record<string, unknown>,
33
+ opts: ServerHandlerOptions,
34
+ ): Promise<unknown> {
35
+ switch (method) {
36
+ case "fs/readTextFile": {
37
+ const p = resolveInWorkspace(String(params.path), opts.workspace);
38
+ return { content: readFileSync(p, "utf-8") };
39
+ }
40
+
41
+ case "fs/writeTextFile": {
42
+ const p = resolveInWorkspace(String(params.path), opts.workspace);
43
+ mkdirSync(dirname(p), { recursive: true });
44
+ writeFileSync(p, String(params.content ?? ""), "utf-8");
45
+ return { success: true };
46
+ }
47
+
48
+ case "terminal/execute": {
49
+ const output = execSync(String(params.command), {
50
+ cwd: opts.workspace,
51
+ timeout: 60_000,
52
+ encoding: "utf-8",
53
+ maxBuffer: 16 * 1024 * 1024,
54
+ });
55
+ return { output };
56
+ }
57
+
58
+ case "session/request_permission": {
59
+ return choosePermission(params as unknown as RequestPermissionParams, opts);
60
+ }
61
+
62
+ default:
63
+ throw new Error(`Unsupported client method: ${method}`);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Auto-approve permission requests when trustAllTools is set. Prefers an
69
+ * "allow always" option, then "allow once", else the first non-reject option.
70
+ */
71
+ function choosePermission(
72
+ params: RequestPermissionParams,
73
+ opts: ServerHandlerOptions,
74
+ ): unknown {
75
+ const options = params.options || [];
76
+ if (!opts.trustAllTools || options.length === 0) {
77
+ const reject = options.find((o) => /reject|deny|no/i.test(o.kind || o.name || ""));
78
+ return { outcome: { outcome: "cancelled" }, optionId: reject?.optionId };
79
+ }
80
+ const allowAlways = options.find((o) => /allow.*always|always/i.test(o.kind || o.name || ""));
81
+ const allowOnce = options.find((o) => /allow|approve|yes|once/i.test(o.kind || o.name || ""));
82
+ const chosen = allowAlways || allowOnce || options[0];
83
+ log.debug("auto-permission ->", chosen?.name || chosen?.optionId);
84
+ return { outcome: { outcome: "selected", optionId: chosen?.optionId } };
85
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Newline-delimited JSON-RPC framing over a child process's stdio.
3
+ * Parses incoming lines and emits typed messages; writes outgoing messages.
4
+ */
5
+ import type { ChildProcessWithoutNullStreams } from "node:child_process";
6
+ import { EventEmitter } from "node:events";
7
+ import type { JsonRpcMessage } from "./types.js";
8
+ import { createLogger } from "../logger.js";
9
+
10
+ const log = createLogger("acp:transport");
11
+
12
+ export class JsonRpcTransport extends EventEmitter {
13
+ private buffer = "";
14
+
15
+ constructor(private readonly proc: ChildProcessWithoutNullStreams) {
16
+ super();
17
+ proc.stdout.setEncoding("utf-8");
18
+ proc.stdout.on("data", (chunk: string) => this.onData(chunk));
19
+ proc.stderr.setEncoding("utf-8");
20
+ proc.stderr.on("data", (chunk: string) => {
21
+ const msg = chunk.trim();
22
+ if (msg) log.debug("[kiro stderr]", msg.slice(0, 500));
23
+ });
24
+ }
25
+
26
+ private onData(chunk: string): void {
27
+ this.buffer += chunk;
28
+ let idx: number;
29
+ while ((idx = this.buffer.indexOf("\n")) !== -1) {
30
+ const line = this.buffer.slice(0, idx).trim();
31
+ this.buffer = this.buffer.slice(idx + 1);
32
+ if (!line) continue;
33
+ let parsed: JsonRpcMessage;
34
+ try {
35
+ parsed = JSON.parse(line) as JsonRpcMessage;
36
+ } catch {
37
+ log.debug("non-JSON line ignored:", line.slice(0, 200));
38
+ continue;
39
+ }
40
+ this.emit("message", parsed);
41
+ }
42
+ }
43
+
44
+ send(msg: object): void {
45
+ if (!this.proc.stdin.writable) {
46
+ throw new Error("ACP process stdin is not writable");
47
+ }
48
+ this.proc.stdin.write(JSON.stringify(msg) + "\n");
49
+ }
50
+ }