droid-acp 0.1.0

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,1629 @@
1
+ import { spawn } from "node:child_process";
2
+ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
3
+ import { createInterface } from "node:readline";
4
+ import { randomUUID } from "node:crypto";
5
+ import { ReadableStream, WritableStream } from "node:stream/web";
6
+
7
+ //#region src/utils.ts
8
+ var Pushable = class {
9
+ queue = [];
10
+ resolvers = [];
11
+ done = false;
12
+ push(item) {
13
+ if (this.resolvers.length > 0) this.resolvers.shift()({
14
+ value: item,
15
+ done: false
16
+ });
17
+ else this.queue.push(item);
18
+ }
19
+ end() {
20
+ this.done = true;
21
+ while (this.resolvers.length > 0) this.resolvers.shift()({
22
+ value: void 0,
23
+ done: true
24
+ });
25
+ }
26
+ [Symbol.asyncIterator]() {
27
+ return { next: () => {
28
+ if (this.queue.length > 0) {
29
+ const value = this.queue.shift();
30
+ return Promise.resolve({
31
+ value,
32
+ done: false
33
+ });
34
+ }
35
+ if (this.done) return Promise.resolve({
36
+ value: void 0,
37
+ done: true
38
+ });
39
+ return new Promise((resolve) => {
40
+ this.resolvers.push(resolve);
41
+ });
42
+ } };
43
+ }
44
+ };
45
+ function nodeToWebWritable(nodeStream) {
46
+ return new WritableStream({ write(chunk) {
47
+ return new Promise((resolve, reject) => {
48
+ nodeStream.write(Buffer.from(chunk), (err) => {
49
+ if (err) reject(err);
50
+ else resolve();
51
+ });
52
+ });
53
+ } });
54
+ }
55
+ function nodeToWebReadable(nodeStream) {
56
+ return new ReadableStream({ start(controller) {
57
+ nodeStream.on("data", (chunk) => {
58
+ controller.enqueue(new Uint8Array(chunk));
59
+ });
60
+ nodeStream.on("end", () => controller.close());
61
+ nodeStream.on("error", (err) => controller.error(err));
62
+ } });
63
+ }
64
+ /** Check if running on Windows */
65
+ const isWindows = process.platform === "win32";
66
+ /**
67
+ * Find the droid executable path.
68
+ * Uses DROID_EXECUTABLE env var if set, otherwise defaults to "droid".
69
+ * On Windows, also checks for "droid.exe" if not explicitly set.
70
+ */
71
+ function findDroidExecutable() {
72
+ if (process.env.DROID_EXECUTABLE) return process.env.DROID_EXECUTABLE;
73
+ return "droid";
74
+ }
75
+
76
+ //#endregion
77
+ //#region src/droid-adapter.ts
78
+ function createDroidAdapter(options) {
79
+ let process$1 = null;
80
+ let sessionId = null;
81
+ const machineId = randomUUID();
82
+ const logger = options.logger ?? console;
83
+ const notificationHandlers = [];
84
+ const rawEventHandlers = [];
85
+ const exitHandlers = [];
86
+ let requestHandler = null;
87
+ let initResolve = null;
88
+ let initReject = null;
89
+ let isStreamingAssistant = false;
90
+ let pendingIdle = false;
91
+ let pendingIdleTimer = null;
92
+ const send = (method, params) => {
93
+ if (!process$1?.stdin?.writable) return;
94
+ const msg = {
95
+ jsonrpc: "2.0",
96
+ factoryApiVersion: "1.0.0",
97
+ type: "request",
98
+ method,
99
+ params,
100
+ id: randomUUID()
101
+ };
102
+ process$1.stdin.write(JSON.stringify(msg) + "\n");
103
+ logger.log("Sent:", method);
104
+ };
105
+ const emit = async (n) => {
106
+ for (const h of notificationHandlers) await h(n);
107
+ };
108
+ let processingChain = Promise.resolve();
109
+ const queueLine = (line) => {
110
+ processingChain = processingChain.then(() => handleLine(line));
111
+ };
112
+ const handleLine = async (line) => {
113
+ try {
114
+ const msg = JSON.parse(line);
115
+ for (const h of rawEventHandlers) await h(msg);
116
+ if (msg.type === "response" && "result" in msg && msg.result && "sessionId" in msg.result && initResolve) {
117
+ const r = msg.result;
118
+ sessionId = r.sessionId;
119
+ initResolve({
120
+ sessionId: r.sessionId,
121
+ session: r.session,
122
+ settings: r.settings,
123
+ availableModels: r.availableModels || []
124
+ });
125
+ initResolve = null;
126
+ initReject = null;
127
+ return;
128
+ }
129
+ if (msg.type === "response" && "error" in msg && msg.error && initReject) {
130
+ initReject(new Error(msg.error.message));
131
+ initResolve = null;
132
+ initReject = null;
133
+ return;
134
+ }
135
+ if (msg.type === "notification" && msg.method === "droid.session_notification") {
136
+ const notification = msg.params?.notification;
137
+ if (!notification) return;
138
+ switch (notification.type) {
139
+ case "settings_updated": {
140
+ const settings = notification.settings;
141
+ if (settings) await emit({
142
+ type: "settings_updated",
143
+ settings: {
144
+ modelId: typeof settings.modelId === "string" ? settings.modelId : void 0,
145
+ reasoningEffort: typeof settings.reasoningEffort === "string" ? settings.reasoningEffort : void 0,
146
+ autonomyLevel: typeof settings.autonomyLevel === "string" ? settings.autonomyLevel : void 0,
147
+ specModeModelId: typeof settings.specModeModelId === "string" ? settings.specModeModelId : void 0,
148
+ specModeReasoningEffort: typeof settings.specModeReasoningEffort === "string" ? settings.specModeReasoningEffort : void 0
149
+ }
150
+ });
151
+ break;
152
+ }
153
+ case "droid_working_state_changed": {
154
+ const newState = notification.newState;
155
+ await emit({
156
+ type: "working_state",
157
+ state: newState
158
+ });
159
+ if (newState === "streaming_assistant_message") {
160
+ isStreamingAssistant = true;
161
+ pendingIdle = false;
162
+ if (pendingIdleTimer) {
163
+ clearTimeout(pendingIdleTimer);
164
+ pendingIdleTimer = null;
165
+ }
166
+ } else if (newState === "idle") if (isStreamingAssistant) {
167
+ pendingIdle = true;
168
+ if (pendingIdleTimer) clearTimeout(pendingIdleTimer);
169
+ pendingIdleTimer = setTimeout(() => {
170
+ if (!pendingIdle) return;
171
+ pendingIdle = false;
172
+ isStreamingAssistant = false;
173
+ pendingIdleTimer = null;
174
+ emit({ type: "complete" });
175
+ }, 250);
176
+ } else await emit({ type: "complete" });
177
+ break;
178
+ }
179
+ case "create_message": {
180
+ const message = notification.message;
181
+ if (message) {
182
+ const blocks = Array.isArray(message.content) ? message.content : [];
183
+ const textParts = blocks.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text);
184
+ if (textParts.length > 0) await emit({
185
+ type: "message",
186
+ role: message.role,
187
+ text: textParts.join(""),
188
+ id: message.id
189
+ });
190
+ const toolUses = blocks.filter((c) => c.type === "tool_use");
191
+ for (const toolUseContent of toolUses) {
192
+ const id = toolUseContent.id ?? toolUseContent.toolUseId ?? toolUseContent.tool_use_id ?? toolUseContent.tool_call_id ?? toolUseContent.callId ?? toolUseContent.call_id ?? randomUUID();
193
+ const name = toolUseContent.name ?? toolUseContent.toolName ?? toolUseContent.tool_name;
194
+ await emit({
195
+ type: "message",
196
+ role: message.role,
197
+ id: message.id,
198
+ toolUse: {
199
+ id,
200
+ name: name || "unknown",
201
+ input: toolUseContent.input
202
+ }
203
+ });
204
+ }
205
+ if (message.role === "assistant") {
206
+ isStreamingAssistant = false;
207
+ if (pendingIdleTimer) {
208
+ clearTimeout(pendingIdleTimer);
209
+ pendingIdleTimer = null;
210
+ }
211
+ if (pendingIdle) {
212
+ await emit({ type: "complete" });
213
+ pendingIdle = false;
214
+ }
215
+ }
216
+ }
217
+ break;
218
+ }
219
+ case "tool_result": {
220
+ const toolUseIdRaw = notification.toolUseId ?? notification.tool_use_id ?? notification.tool_call_id ?? notification.callId ?? notification.call_id ?? notification.id;
221
+ const toolUseId = typeof toolUseIdRaw === "string" ? toolUseIdRaw : null;
222
+ const rawContent = notification.content ?? notification.value;
223
+ const content = typeof rawContent === "string" ? rawContent : JSON.stringify(rawContent ?? "", null, 2);
224
+ const isErrorRaw = notification.isError ?? notification.is_error;
225
+ const isError = typeof isErrorRaw === "boolean" ? isErrorRaw : false;
226
+ if (!toolUseId) {
227
+ logger.error("Missing tool_use_id/toolUseId for tool_result notification");
228
+ break;
229
+ }
230
+ await emit({
231
+ type: "tool_result",
232
+ toolUseId,
233
+ content,
234
+ isError
235
+ });
236
+ break;
237
+ }
238
+ case "error":
239
+ isStreamingAssistant = false;
240
+ pendingIdle = false;
241
+ await emit({
242
+ type: "error",
243
+ message: typeof notification.message === "string" ? notification.message : JSON.stringify(notification.message ?? "Unknown error")
244
+ });
245
+ break;
246
+ }
247
+ }
248
+ if (msg.type === "request") {
249
+ const requestId = msg.id;
250
+ const method = msg.method;
251
+ const params = msg.params;
252
+ if (requestHandler && method === "droid.request_permission") try {
253
+ const response = {
254
+ jsonrpc: "2.0",
255
+ factoryApiVersion: "1.0.0",
256
+ type: "response",
257
+ id: requestId,
258
+ result: await requestHandler(method, params)
259
+ };
260
+ if (process$1?.stdin) process$1.stdin.write(JSON.stringify(response) + "\n");
261
+ } catch (error) {
262
+ const response = {
263
+ jsonrpc: "2.0",
264
+ factoryApiVersion: "1.0.0",
265
+ type: "response",
266
+ id: requestId,
267
+ error: {
268
+ code: -32603,
269
+ message: error instanceof Error ? error.message : "Internal error"
270
+ }
271
+ };
272
+ if (process$1?.stdin) process$1.stdin.write(JSON.stringify(response) + "\n");
273
+ }
274
+ else if (method === "droid.request_permission") {
275
+ const response = {
276
+ jsonrpc: "2.0",
277
+ factoryApiVersion: "1.0.0",
278
+ type: "response",
279
+ id: requestId,
280
+ result: { selectedOption: "proceed_once" }
281
+ };
282
+ if (process$1?.stdin) process$1.stdin.write(JSON.stringify(response) + "\n");
283
+ logger.log("Auto-approved permission request (fallback):", requestId);
284
+ }
285
+ }
286
+ } catch (err) {
287
+ logger.error("Parse error:", err.message);
288
+ }
289
+ };
290
+ return {
291
+ async start() {
292
+ const executable = findDroidExecutable();
293
+ const args = [
294
+ "exec",
295
+ "--input-format",
296
+ "stream-jsonrpc",
297
+ "--output-format",
298
+ "stream-jsonrpc",
299
+ "--cwd",
300
+ options.cwd
301
+ ];
302
+ logger.log("Starting droid:", executable, args.join(" "));
303
+ process$1 = spawn(executable, args, {
304
+ stdio: [
305
+ "pipe",
306
+ "pipe",
307
+ "pipe"
308
+ ],
309
+ env: {
310
+ ...globalThis.process.env,
311
+ FORCE_COLOR: "0"
312
+ },
313
+ shell: isWindows,
314
+ windowsHide: true
315
+ });
316
+ if (process$1.stdout) createInterface({ input: process$1.stdout }).on("line", queueLine);
317
+ if (process$1.stderr) createInterface({ input: process$1.stderr }).on("line", (l) => logger.error("[droid stderr]", l));
318
+ process$1.on("error", (err) => {
319
+ if (initReject) initReject(err);
320
+ });
321
+ process$1.on("exit", (code) => {
322
+ logger.log("Droid exit:", code);
323
+ process$1 = null;
324
+ exitHandlers.forEach((h) => h(code));
325
+ });
326
+ return new Promise((resolve, reject) => {
327
+ initResolve = resolve;
328
+ initReject = reject;
329
+ send("droid.initialize_session", {
330
+ machineId,
331
+ cwd: options.cwd
332
+ });
333
+ const initTimeout = parseInt(globalThis.process.env.DROID_INIT_TIMEOUT || "60000", 10);
334
+ setTimeout(() => {
335
+ if (initReject) {
336
+ initReject(/* @__PURE__ */ new Error("Droid init timeout"));
337
+ initResolve = null;
338
+ initReject = null;
339
+ }
340
+ }, initTimeout);
341
+ });
342
+ },
343
+ sendMessage(text) {
344
+ this.sendUserMessage({ text });
345
+ },
346
+ sendUserMessage(message) {
347
+ if (!sessionId) return;
348
+ const params = {
349
+ sessionId,
350
+ text: message.text
351
+ };
352
+ if (Array.isArray(message.images) && message.images.length > 0) params.images = message.images;
353
+ send("droid.add_user_message", params);
354
+ },
355
+ setMode(level) {
356
+ if (!sessionId) return;
357
+ send("droid.update_session_settings", {
358
+ sessionId,
359
+ autonomyLevel: level
360
+ });
361
+ },
362
+ setModel(modelId) {
363
+ if (!sessionId) return;
364
+ send("droid.update_session_settings", {
365
+ sessionId,
366
+ modelId
367
+ });
368
+ },
369
+ onNotification(handler) {
370
+ notificationHandlers.push(handler);
371
+ },
372
+ onRawEvent(handler) {
373
+ rawEventHandlers.push(handler);
374
+ },
375
+ onRequest(handler) {
376
+ requestHandler = handler;
377
+ },
378
+ onExit(handler) {
379
+ exitHandlers.push(handler);
380
+ },
381
+ async stop() {
382
+ if (process$1) {
383
+ process$1.stdin?.end();
384
+ process$1.kill("SIGTERM");
385
+ process$1 = null;
386
+ }
387
+ },
388
+ isRunning() {
389
+ return process$1 !== null && !process$1.killed;
390
+ },
391
+ getSessionId() {
392
+ return sessionId;
393
+ }
394
+ };
395
+ }
396
+
397
+ //#endregion
398
+ //#region src/types.ts
399
+ const ACP_MODES = [
400
+ "off",
401
+ "low",
402
+ "medium",
403
+ "high",
404
+ "spec"
405
+ ];
406
+
407
+ //#endregion
408
+ //#region src/acp-agent.ts
409
+ const packageJson = {
410
+ name: "droid-acp",
411
+ version: "0.1.0"
412
+ };
413
+ function normalizeBase64DataUrl(data, fallbackMimeType) {
414
+ const trimmed = data.trim();
415
+ const match = trimmed.match(/^data:([^;,]+);base64,(.*)$/s);
416
+ if (match) return {
417
+ mimeType: match[1]?.trim() || fallbackMimeType,
418
+ base64: match[2]?.trim().replace(/\s+/g, "")
419
+ };
420
+ return {
421
+ mimeType: fallbackMimeType,
422
+ base64: trimmed.replace(/\s+/g, "")
423
+ };
424
+ }
425
+ function getAvailableCommands() {
426
+ return [
427
+ {
428
+ name: "help",
429
+ description: "Show available slash commands",
430
+ input: null
431
+ },
432
+ {
433
+ name: "model",
434
+ description: "Show or change the current model",
435
+ input: { hint: "[model_id]" }
436
+ },
437
+ {
438
+ name: "mode",
439
+ description: "Show or change the autonomy mode (off|low|medium|high|spec)",
440
+ input: { hint: "[mode]" }
441
+ },
442
+ {
443
+ name: "config",
444
+ description: "Show current session configuration",
445
+ input: null
446
+ },
447
+ {
448
+ name: "status",
449
+ description: "Show current session status",
450
+ input: null
451
+ }
452
+ ];
453
+ }
454
+ const ACP_MODE_TO_DROID_AUTONOMY = {
455
+ off: "normal",
456
+ low: "auto-low",
457
+ medium: "auto-medium",
458
+ high: "auto-high",
459
+ spec: "spec"
460
+ };
461
+ function droidAutonomyToAcpModeId(value) {
462
+ switch (value) {
463
+ case "normal": return "off";
464
+ case "auto-low": return "low";
465
+ case "auto-medium": return "medium";
466
+ case "auto-high": return "high";
467
+ case "spec": return "spec";
468
+ case "suggest": return "low";
469
+ case "full": return "high";
470
+ default: return null;
471
+ }
472
+ }
473
+ var DroidAcpAgent = class {
474
+ sessions = /* @__PURE__ */ new Map();
475
+ client;
476
+ logger;
477
+ constructor(client, logger) {
478
+ this.client = client;
479
+ this.logger = logger ?? console;
480
+ this.logger.log("DroidAcpAgent initialized");
481
+ }
482
+ async initialize(_request) {
483
+ this.logger.log("initialize");
484
+ return {
485
+ protocolVersion: 1,
486
+ agentCapabilities: { promptCapabilities: {
487
+ image: true,
488
+ embeddedContext: true
489
+ } },
490
+ agentInfo: {
491
+ name: packageJson.name,
492
+ title: "Factory Droid",
493
+ version: packageJson.version
494
+ },
495
+ authMethods: [{
496
+ id: "factory-api-key",
497
+ name: "Factory API Key",
498
+ description: "Set FACTORY_API_KEY environment variable"
499
+ }]
500
+ };
501
+ }
502
+ async authenticate(request) {
503
+ this.logger.log("authenticate:", request.methodId);
504
+ if (request.methodId === "factory-api-key") {
505
+ if (!process.env.FACTORY_API_KEY) throw new Error("FACTORY_API_KEY environment variable is not set");
506
+ return {};
507
+ }
508
+ throw new Error(`Unknown auth method: ${request.methodId}`);
509
+ }
510
+ async newSession(request) {
511
+ const cwd = request.cwd || process.cwd();
512
+ this.logger.log("newSession:", cwd);
513
+ const droid = createDroidAdapter({
514
+ cwd,
515
+ logger: this.logger
516
+ });
517
+ const initResult = await droid.start();
518
+ const sessionId = initResult.sessionId;
519
+ const initialMode = typeof initResult.settings?.autonomyLevel === "string" ? droidAutonomyToAcpModeId(initResult.settings.autonomyLevel) ?? "off" : "off";
520
+ const session = {
521
+ id: sessionId,
522
+ droid,
523
+ droidSessionId: initResult.sessionId,
524
+ model: initResult.settings?.modelId || "unknown",
525
+ mode: initialMode,
526
+ cancelled: false,
527
+ promptResolve: null,
528
+ activeToolCallIds: /* @__PURE__ */ new Set(),
529
+ toolCallStatus: /* @__PURE__ */ new Map(),
530
+ toolNames: /* @__PURE__ */ new Map(),
531
+ availableModels: initResult.availableModels,
532
+ cwd,
533
+ specChoice: null,
534
+ specChoicePromptSignature: null,
535
+ specPlanDetailsSignature: null,
536
+ specPlanDetailsToolCallId: null
537
+ };
538
+ droid.onNotification((n) => this.handleNotification(session, n));
539
+ if (process.env.DROID_DEBUG) droid.onRawEvent(async (event) => {
540
+ await this.client.sessionUpdate({
541
+ sessionId: session.id,
542
+ update: {
543
+ sessionUpdate: "agent_message_chunk",
544
+ content: {
545
+ type: "text",
546
+ text: `\n\`\`\`json\n${JSON.stringify(event, null, 2)}\n\`\`\`\n`
547
+ }
548
+ }
549
+ });
550
+ });
551
+ droid.onRequest(async (method, params) => {
552
+ if (method === "droid.request_permission") return this.handlePermission(session, params);
553
+ throw new Error("Method not supported");
554
+ });
555
+ droid.onExit((code) => {
556
+ this.logger.log("Droid exited, cleaning up session:", session.id, "code:", code);
557
+ if (session.promptResolve) {
558
+ session.promptResolve({ stopReason: "end_turn" });
559
+ session.promptResolve = null;
560
+ }
561
+ this.sessions.delete(session.id);
562
+ });
563
+ this.sessions.set(sessionId, session);
564
+ this.logger.log("Session created:", sessionId);
565
+ setTimeout(() => {
566
+ this.client.sessionUpdate({
567
+ sessionId,
568
+ update: {
569
+ sessionUpdate: "available_commands_update",
570
+ availableCommands: getAvailableCommands()
571
+ }
572
+ });
573
+ }, 0);
574
+ return {
575
+ sessionId,
576
+ models: {
577
+ availableModels: initResult.availableModels.map((m) => ({
578
+ modelId: m.id,
579
+ name: m.displayName
580
+ })),
581
+ currentModelId: initResult.settings?.modelId || "unknown"
582
+ },
583
+ modes: {
584
+ currentModeId: initialMode,
585
+ availableModes: [
586
+ {
587
+ id: "spec",
588
+ name: "Spec",
589
+ description: "Research and plan only - no code changes"
590
+ },
591
+ {
592
+ id: "off",
593
+ name: "Auto Off",
594
+ description: "Read-only mode - safe for reviewing planned changes without execution"
595
+ },
596
+ {
597
+ id: "low",
598
+ name: "Auto Low",
599
+ description: "Low-risk operations - file creation/modification, no system changes"
600
+ },
601
+ {
602
+ id: "medium",
603
+ name: "Auto Medium",
604
+ description: "Development operations - npm install, git commit, build commands"
605
+ },
606
+ {
607
+ id: "high",
608
+ name: "Auto High",
609
+ description: "Production operations - git push, deployments, database migrations"
610
+ }
611
+ ]
612
+ }
613
+ };
614
+ }
615
+ async prompt(request) {
616
+ const session = this.sessions.get(request.sessionId);
617
+ if (!session) throw new Error(`Session not found: ${request.sessionId}`);
618
+ if (session.cancelled) throw new Error("Session cancelled");
619
+ if (session.promptResolve) throw new Error("Another prompt is already in progress");
620
+ this.logger.log("prompt:", request.sessionId);
621
+ const textParts = [];
622
+ const images = [];
623
+ for (const chunk of request.prompt) switch (chunk.type) {
624
+ case "text":
625
+ textParts.push(chunk.text);
626
+ break;
627
+ case "image": {
628
+ const mimeType = chunk.mimeType || "application/octet-stream";
629
+ if (chunk.data) {
630
+ const normalized = normalizeBase64DataUrl(chunk.data, mimeType);
631
+ images.push({
632
+ type: "base64",
633
+ data: normalized.base64,
634
+ mediaType: normalized.mimeType
635
+ });
636
+ } else if (chunk.uri) textParts.push(`(image: ${chunk.uri})`);
637
+ break;
638
+ }
639
+ case "resource":
640
+ if ("text" in chunk.resource) {
641
+ const contextText = `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>`;
642
+ textParts.push(contextText);
643
+ } else if ("blob" in chunk.resource) {
644
+ const mimeType = chunk.resource.mimeType || "application/octet-stream";
645
+ const uri = chunk.resource.uri;
646
+ if (mimeType.startsWith("image/")) {
647
+ const data = chunk.resource.blob;
648
+ if (typeof data === "string" && data.length > 0) {
649
+ const normalized = normalizeBase64DataUrl(data, mimeType);
650
+ images.push({
651
+ type: "base64",
652
+ data: normalized.base64,
653
+ mediaType: normalized.mimeType
654
+ });
655
+ }
656
+ } else {
657
+ const note = uri ? `\n<context ref="${uri}">\n(binary resource: ${mimeType})\n</context>` : `\n(binary resource: ${mimeType})`;
658
+ textParts.push(note);
659
+ }
660
+ }
661
+ break;
662
+ case "resource_link":
663
+ textParts.push(`@${chunk.uri}`);
664
+ break;
665
+ default: break;
666
+ }
667
+ let text = textParts.join("\n").trim();
668
+ if (text.length === 0 && images.length > 0) text = "Please see the attached image(s).";
669
+ if (text.startsWith("/")) {
670
+ if (await this.handleSlashCommand(session, text)) return { stopReason: "end_turn" };
671
+ }
672
+ return new Promise((resolve) => {
673
+ const timeoutId = setTimeout(() => {
674
+ if (session.promptResolve) {
675
+ session.promptResolve({ stopReason: "end_turn" });
676
+ session.promptResolve = null;
677
+ }
678
+ }, 300 * 1e3);
679
+ session.promptResolve = resolve;
680
+ session.droid.sendUserMessage({
681
+ text,
682
+ images: images.length > 0 ? images : void 0
683
+ });
684
+ const originalResolve = session.promptResolve;
685
+ session.promptResolve = (result) => {
686
+ clearTimeout(timeoutId);
687
+ originalResolve?.(result);
688
+ };
689
+ });
690
+ }
691
+ async cancel(request) {
692
+ const session = this.sessions.get(request.sessionId);
693
+ if (session) {
694
+ this.logger.log("cancel:", request.sessionId);
695
+ session.cancelled = true;
696
+ if (session.promptResolve) {
697
+ session.promptResolve({ stopReason: "cancelled" });
698
+ session.promptResolve = null;
699
+ }
700
+ await session.droid.stop();
701
+ this.sessions.delete(request.sessionId);
702
+ }
703
+ }
704
+ async unstable_setSessionModel(request) {
705
+ const session = this.sessions.get(request.sessionId);
706
+ if (session) {
707
+ this.logger.log("setSessionModel:", request.modelId);
708
+ session.model = request.modelId;
709
+ session.droid.setModel(request.modelId);
710
+ }
711
+ }
712
+ async setSessionMode(request) {
713
+ const session = this.sessions.get(request.sessionId);
714
+ if (session) {
715
+ this.logger.log("setSessionMode:", request.modeId);
716
+ const modeId = ACP_MODES.includes(request.modeId) ? request.modeId : null;
717
+ if (modeId) {
718
+ session.mode = modeId;
719
+ session.droid.setMode(ACP_MODE_TO_DROID_AUTONOMY[modeId]);
720
+ }
721
+ }
722
+ return {};
723
+ }
724
+ async handleSlashCommand(session, text) {
725
+ const match = text.match(/^\/(\S+)(?:\s+(.*))?$/);
726
+ if (!match) return false;
727
+ const [, command, args] = match;
728
+ const trimmedArgs = args?.trim() || "";
729
+ switch (command.toLowerCase()) {
730
+ case "help": {
731
+ const helpText = ["**Available Commands:**\n", ...getAvailableCommands().map((cmd) => {
732
+ const inputHint = cmd.input && "hint" in cmd.input ? ` ${cmd.input.hint}` : "";
733
+ return `- \`/${cmd.name}${inputHint}\` - ${cmd.description}`;
734
+ })].join("\n");
735
+ await this.sendAgentMessage(session, helpText);
736
+ return true;
737
+ }
738
+ case "model":
739
+ if (trimmedArgs) {
740
+ const modelId = trimmedArgs;
741
+ const model = session.availableModels.find((m) => m.id === modelId || m.displayName.toLowerCase() === modelId.toLowerCase());
742
+ if (model) {
743
+ session.model = model.id;
744
+ session.droid.setModel(model.id);
745
+ await this.sendAgentMessage(session, `Model changed to: **${model.displayName}**`);
746
+ } else {
747
+ const available = session.availableModels.map((m) => `- ${m.id} (${m.displayName})`);
748
+ await this.sendAgentMessage(session, `Model "${modelId}" not found.\n\n**Available models:**\n${available.join("\n")}`);
749
+ }
750
+ } else {
751
+ const available = session.availableModels.map((m) => {
752
+ const current = m.id === session.model ? " **(current)**" : "";
753
+ return `- ${m.id} (${m.displayName})${current}`;
754
+ });
755
+ await this.sendAgentMessage(session, `**Current model:** ${session.model}\n\n**Available models:**\n${available.join("\n")}`);
756
+ }
757
+ return true;
758
+ case "mode": {
759
+ const inputMode = trimmedArgs.toLowerCase();
760
+ if (trimmedArgs && ACP_MODES.includes(inputMode)) {
761
+ session.mode = inputMode;
762
+ session.droid.setMode(ACP_MODE_TO_DROID_AUTONOMY[inputMode]);
763
+ await this.sendAgentMessage(session, `Autonomy mode changed to: **${inputMode}**`);
764
+ await this.client.sessionUpdate({
765
+ sessionId: session.id,
766
+ update: {
767
+ sessionUpdate: "current_mode_update",
768
+ currentModeId: inputMode
769
+ }
770
+ });
771
+ } else {
772
+ const modeList = ACP_MODES.map((m) => {
773
+ return `- ${m}${m === session.mode ? " **(current)**" : ""}`;
774
+ }).join("\n");
775
+ await this.sendAgentMessage(session, `**Current mode:** ${session.mode}\n\n**Available modes:**\n${modeList}`);
776
+ }
777
+ return true;
778
+ }
779
+ case "config": {
780
+ const config = [
781
+ `**Session Configuration:**`,
782
+ `- Session ID: ${session.id}`,
783
+ `- Working Directory: ${session.cwd}`,
784
+ `- Model: ${session.model}`,
785
+ `- Mode: ${session.mode}`
786
+ ].join("\n");
787
+ await this.sendAgentMessage(session, config);
788
+ return true;
789
+ }
790
+ case "status": {
791
+ const status = [
792
+ `**Session Status:**`,
793
+ `- Active Tool Calls: ${session.activeToolCallIds.size}`,
794
+ `- Droid Running: ${session.droid.isRunning()}`
795
+ ].join("\n");
796
+ await this.sendAgentMessage(session, status);
797
+ return true;
798
+ }
799
+ default:
800
+ await this.sendAgentMessage(session, `Unknown command: \`/${command}\`. Type \`/help\` to see available commands.`);
801
+ return true;
802
+ }
803
+ }
804
+ async sendAgentMessage(session, text) {
805
+ await this.client.sessionUpdate({
806
+ sessionId: session.id,
807
+ update: {
808
+ sessionUpdate: "agent_message_chunk",
809
+ content: {
810
+ type: "text",
811
+ text
812
+ }
813
+ }
814
+ });
815
+ }
816
+ extractSpecTitleAndPlan(rawInput) {
817
+ if (!rawInput || typeof rawInput !== "object") return {
818
+ title: null,
819
+ plan: null
820
+ };
821
+ const obj = rawInput;
822
+ const title = typeof obj.title === "string" ? obj.title : typeof obj.specTitle === "string" ? obj.specTitle : typeof obj.name === "string" ? obj.name : null;
823
+ const candidates = [
824
+ obj.plan,
825
+ obj.planMarkdown,
826
+ obj.markdown,
827
+ obj.content,
828
+ obj.text
829
+ ];
830
+ const toMarkdown = (value) => {
831
+ if (typeof value === "string") return value;
832
+ if (Array.isArray(value)) {
833
+ const joined = value.map((v) => typeof v === "string" ? v : JSON.stringify(v, null, 2)).join("\n").trim();
834
+ return joined.length > 0 ? joined : null;
835
+ }
836
+ if (value && typeof value === "object") {
837
+ const v = value;
838
+ if (typeof v.markdown === "string") return v.markdown;
839
+ if (typeof v.text === "string") return v.text;
840
+ const json = JSON.stringify(v, null, 2);
841
+ return json && json !== "{}" ? json : null;
842
+ }
843
+ return null;
844
+ };
845
+ for (const c of candidates) {
846
+ const plan = toMarkdown(c);
847
+ if (plan) return {
848
+ title,
849
+ plan
850
+ };
851
+ }
852
+ return {
853
+ title,
854
+ plan: null
855
+ };
856
+ }
857
+ planEntriesFromMarkdown(planMarkdown) {
858
+ const entries = [];
859
+ let inCodeFence = false;
860
+ for (const rawLine of planMarkdown.split("\n")) {
861
+ const line = rawLine.trim();
862
+ if (line.startsWith("```")) {
863
+ inCodeFence = !inCodeFence;
864
+ continue;
865
+ }
866
+ if (inCodeFence) continue;
867
+ if (!line) continue;
868
+ const checkbox = line.match(/^- \[([ xX~])\]\s+(.*)$/);
869
+ if (checkbox) {
870
+ const [, mark, content] = checkbox;
871
+ const status = mark === "x" || mark === "X" ? "completed" : mark === "~" ? "in_progress" : "pending";
872
+ entries.push({
873
+ content,
874
+ status,
875
+ priority: "medium"
876
+ });
877
+ continue;
878
+ }
879
+ const bullet = line.match(/^[-*]\s+(.*)$/);
880
+ if (bullet) {
881
+ entries.push({
882
+ content: bullet[1],
883
+ status: "pending",
884
+ priority: "medium"
885
+ });
886
+ continue;
887
+ }
888
+ const numbered = line.match(/^\d+\.\s+(.*)$/);
889
+ if (numbered) {
890
+ entries.push({
891
+ content: numbered[1],
892
+ status: "pending",
893
+ priority: "medium"
894
+ });
895
+ continue;
896
+ }
897
+ }
898
+ return entries.filter((e) => e.content.length > 0);
899
+ }
900
+ extractPlanChoices(planMarkdown) {
901
+ const explicitChoices = [];
902
+ const looseChoices = [];
903
+ const seenExplicit = /* @__PURE__ */ new Set();
904
+ const seenLoose = /* @__PURE__ */ new Set();
905
+ for (const rawLine of planMarkdown.split("\n")) {
906
+ const line = rawLine.trim();
907
+ if (!line) continue;
908
+ const stripped = line.replace(/^>\s+/, "").replace(/^#+\s*/, "").replace(/^[-*]\s+/, "").trim().replace(/^[*_`]+/, "").trim();
909
+ const explicit = stripped.match(/^(?:Option|方案)\s*([A-Z])\s*[::–—.)-]\s*(.+)$/i);
910
+ if (explicit) {
911
+ const id$1 = explicit[1].toUpperCase();
912
+ if (seenExplicit.has(id$1)) continue;
913
+ seenExplicit.add(id$1);
914
+ const title$1 = explicit[2].trim();
915
+ explicitChoices.push({
916
+ id: id$1,
917
+ title: title$1
918
+ });
919
+ continue;
920
+ }
921
+ const loose = stripped.match(/^([A-F])\s*[::–—.)-]\s*(.+)$/);
922
+ if (!loose) continue;
923
+ const id = loose[1].toUpperCase();
924
+ if (seenLoose.has(id)) continue;
925
+ seenLoose.add(id);
926
+ const title = loose[2].trim();
927
+ looseChoices.push({
928
+ id,
929
+ title
930
+ });
931
+ }
932
+ if (explicitChoices.length > 0) return explicitChoices;
933
+ if (looseChoices.length >= 2) return looseChoices;
934
+ return [];
935
+ }
936
+ handlePermission(session, params) {
937
+ const toolUse = params.toolUses?.[0]?.toolUse;
938
+ if (!toolUse) return Promise.resolve({ selectedOption: "proceed_once" });
939
+ const toolCallId = toolUse.id;
940
+ const toolName = toolUse.name;
941
+ const rawInput = toolUse.input;
942
+ const spec = toolName === "ExitSpecMode" ? this.extractSpecTitleAndPlan(rawInput) : null;
943
+ const command = typeof rawInput?.command === "string" ? rawInput.command : toolName === "ExitSpecMode" && typeof spec?.title === "string" ? spec.title : JSON.stringify(rawInput);
944
+ const commandSummary = command.length > 200 ? command.slice(0, 200) + "…" : command;
945
+ const isReadOnlyTool = toolName === "Read" || toolName === "Grep" || toolName === "Glob" || toolName === "LS";
946
+ const riskLevelRaw = rawInput?.riskLevel;
947
+ const riskLevel = riskLevelRaw === "low" || riskLevelRaw === "medium" || riskLevelRaw === "high" ? riskLevelRaw : isReadOnlyTool ? "low" : "medium";
948
+ this.logger.log("Permission request for tool:", toolCallId, "risk:", riskLevel, "mode:", session.mode);
949
+ const toolCallTitle = toolName === "ExitSpecMode" ? spec?.title ? `Exit spec mode: ${spec.title}` : "Exit spec mode" : `Running ${toolName} (${riskLevel}): ${commandSummary}`;
950
+ const toolCallKind = toolName === "ExitSpecMode" ? "switch_mode" : void 0;
951
+ const toolCallContent = toolName === "ExitSpecMode" && spec?.plan ? [{
952
+ type: "content",
953
+ content: {
954
+ type: "text",
955
+ text: spec.plan
956
+ }
957
+ }] : void 0;
958
+ const alreadyTracked = session.activeToolCallIds.has(toolCallId);
959
+ session.activeToolCallIds.add(toolCallId);
960
+ session.toolNames.set(toolCallId, toolName);
961
+ session.toolCallStatus.set(toolCallId, "pending");
962
+ if (alreadyTracked) this.client.sessionUpdate({
963
+ sessionId: session.id,
964
+ update: {
965
+ sessionUpdate: "tool_call_update",
966
+ toolCallId,
967
+ title: toolCallTitle,
968
+ status: "pending",
969
+ kind: toolCallKind,
970
+ content: toolCallContent,
971
+ rawInput
972
+ }
973
+ });
974
+ else this.client.sessionUpdate({
975
+ sessionId: session.id,
976
+ update: {
977
+ sessionUpdate: "tool_call",
978
+ toolCallId,
979
+ title: toolCallTitle,
980
+ status: "pending",
981
+ kind: toolCallKind,
982
+ content: toolCallContent,
983
+ rawInput
984
+ }
985
+ });
986
+ return this.decidePermission(session, {
987
+ toolCallId,
988
+ toolName,
989
+ command: commandSummary,
990
+ riskLevel,
991
+ rawInput,
992
+ droidOptions: this.extractDroidPermissionOptions(params)
993
+ });
994
+ }
995
+ extractDroidPermissionOptions(params) {
996
+ const candidates = [];
997
+ const maybePush = (value) => {
998
+ if (Array.isArray(value)) candidates.push(value);
999
+ };
1000
+ maybePush(params.options);
1001
+ const toolUses = params.toolUses;
1002
+ if (Array.isArray(toolUses)) for (const toolUse of toolUses) {
1003
+ if (!toolUse || typeof toolUse !== "object") continue;
1004
+ const tu = toolUse;
1005
+ maybePush(tu.options);
1006
+ const details = tu.details;
1007
+ if (details && typeof details === "object") maybePush(details.options);
1008
+ }
1009
+ for (const candidate of candidates) {
1010
+ const normalized = candidate.map((opt) => opt).map((opt) => ({
1011
+ value: typeof opt.value === "string" ? opt.value : null,
1012
+ label: typeof opt.label === "string" ? opt.label : null
1013
+ })).filter((opt) => !!opt.value && !!opt.label).map((opt) => ({
1014
+ value: opt.value,
1015
+ label: opt.label
1016
+ }));
1017
+ if (normalized.length > 0) return normalized;
1018
+ }
1019
+ return null;
1020
+ }
1021
+ mapExitSpecModeSelection(optionId) {
1022
+ switch (optionId) {
1023
+ case "off": return {
1024
+ nextMode: "off",
1025
+ droidSelectedOption: "proceed_once"
1026
+ };
1027
+ case "low": return {
1028
+ nextMode: "low",
1029
+ droidSelectedOption: "proceed_auto_run_low"
1030
+ };
1031
+ case "medium": return {
1032
+ nextMode: "medium",
1033
+ droidSelectedOption: "proceed_auto_run_medium"
1034
+ };
1035
+ case "high": return {
1036
+ nextMode: "high",
1037
+ droidSelectedOption: "proceed_auto_run_high"
1038
+ };
1039
+ case "spec": return {
1040
+ nextMode: "spec",
1041
+ droidSelectedOption: "cancel"
1042
+ };
1043
+ default: break;
1044
+ }
1045
+ switch (optionId) {
1046
+ case "proceed_once": return {
1047
+ nextMode: "off",
1048
+ droidSelectedOption: "proceed_once"
1049
+ };
1050
+ case "proceed_auto_run_low": return {
1051
+ nextMode: "low",
1052
+ droidSelectedOption: "proceed_auto_run_low"
1053
+ };
1054
+ case "proceed_auto_run_medium": return {
1055
+ nextMode: "medium",
1056
+ droidSelectedOption: "proceed_auto_run_medium"
1057
+ };
1058
+ case "proceed_auto_run_high": return {
1059
+ nextMode: "high",
1060
+ droidSelectedOption: "proceed_auto_run_high"
1061
+ };
1062
+ case "cancel": return {
1063
+ nextMode: "spec",
1064
+ droidSelectedOption: "cancel"
1065
+ };
1066
+ default: return {
1067
+ nextMode: null,
1068
+ droidSelectedOption: optionId
1069
+ };
1070
+ }
1071
+ }
1072
+ specApprovalOptions(droidOptions) {
1073
+ const has = (value) => droidOptions?.some((o) => o.value === value) === true;
1074
+ const options = [
1075
+ {
1076
+ modeId: "off",
1077
+ droidValue: "proceed_once",
1078
+ name: "Proceed (manual approvals)",
1079
+ kind: "allow_once"
1080
+ },
1081
+ {
1082
+ modeId: "low",
1083
+ droidValue: "proceed_auto_run_low",
1084
+ name: "Proceed (Auto Low)",
1085
+ kind: "allow_once"
1086
+ },
1087
+ {
1088
+ modeId: "medium",
1089
+ droidValue: "proceed_auto_run_medium",
1090
+ name: "Proceed (Auto Medium)",
1091
+ kind: "allow_once"
1092
+ },
1093
+ {
1094
+ modeId: "high",
1095
+ droidValue: "proceed_auto_run_high",
1096
+ name: "Proceed (Auto High)",
1097
+ kind: "allow_once"
1098
+ },
1099
+ {
1100
+ modeId: "spec",
1101
+ droidValue: "cancel",
1102
+ name: "No, keep iterating (stay in Spec)",
1103
+ kind: "reject_once"
1104
+ }
1105
+ ].filter((c) => !droidOptions || has(c.droidValue)).map((c) => ({
1106
+ optionId: c.modeId,
1107
+ name: c.name,
1108
+ kind: c.kind
1109
+ }));
1110
+ if (options.length > 0) return options;
1111
+ return droidOptions?.map((o) => ({
1112
+ optionId: o.value,
1113
+ name: o.label,
1114
+ kind: "allow_once"
1115
+ })) ?? [{
1116
+ optionId: "off",
1117
+ name: "Proceed (manual approvals)",
1118
+ kind: "allow_once"
1119
+ }, {
1120
+ optionId: "spec",
1121
+ name: "No, keep iterating (stay in Spec)",
1122
+ kind: "reject_once"
1123
+ }];
1124
+ }
1125
+ async decidePermission(session, params) {
1126
+ if (session.cancelled) {
1127
+ session.toolCallStatus.set(params.toolCallId, "completed");
1128
+ session.activeToolCallIds.delete(params.toolCallId);
1129
+ await this.client.sessionUpdate({
1130
+ sessionId: session.id,
1131
+ update: {
1132
+ sessionUpdate: "tool_call_update",
1133
+ toolCallId: params.toolCallId,
1134
+ status: "completed"
1135
+ }
1136
+ });
1137
+ return { selectedOption: "cancel" };
1138
+ }
1139
+ const permissionKindFromOptionValue = (value) => {
1140
+ switch (value) {
1141
+ case "proceed_once":
1142
+ case "proceed_edit": return "allow_once";
1143
+ case "proceed_auto_run_low":
1144
+ case "proceed_auto_run_medium":
1145
+ case "proceed_auto_run_high":
1146
+ case "proceed_auto_run":
1147
+ case "proceed_always": return "allow_always";
1148
+ case "cancel": return "reject_once";
1149
+ default: return "allow_once";
1150
+ }
1151
+ };
1152
+ const toAcpPermissionOption = (opt) => {
1153
+ const value = opt.value;
1154
+ let name = opt.label;
1155
+ switch (value) {
1156
+ case "proceed_once":
1157
+ name = "Allow once";
1158
+ break;
1159
+ case "proceed_always": {
1160
+ const labelLower = opt.label.toLowerCase();
1161
+ if (labelLower.includes("low")) {
1162
+ name = "Allow & auto-run low risk commands";
1163
+ break;
1164
+ }
1165
+ if (labelLower.includes("medium")) {
1166
+ name = "Allow & auto-run medium risk commands";
1167
+ break;
1168
+ }
1169
+ if (labelLower.includes("high")) {
1170
+ name = "Allow & auto-run high risk commands";
1171
+ break;
1172
+ }
1173
+ name = "Allow always";
1174
+ break;
1175
+ }
1176
+ case "proceed_auto_run_low":
1177
+ name = "Proceed & auto-run (low risk)";
1178
+ break;
1179
+ case "proceed_auto_run_medium":
1180
+ name = "Proceed & auto-run (medium risk)";
1181
+ break;
1182
+ case "proceed_auto_run_high":
1183
+ name = "Proceed & auto-run (high risk)";
1184
+ break;
1185
+ default: break;
1186
+ }
1187
+ return {
1188
+ optionId: value,
1189
+ name,
1190
+ kind: permissionKindFromOptionValue(value)
1191
+ };
1192
+ };
1193
+ const droidOptions = params.droidOptions?.length ? params.droidOptions : null;
1194
+ const acpOptions = params.toolName === "ExitSpecMode" ? this.specApprovalOptions(droidOptions) : droidOptions ? droidOptions.map(toAcpPermissionOption) : [{
1195
+ optionId: "proceed_once",
1196
+ name: "Allow once",
1197
+ kind: "allow_once"
1198
+ }, {
1199
+ optionId: "cancel",
1200
+ name: "Reject",
1201
+ kind: "reject_once"
1202
+ }];
1203
+ const spec = this.extractSpecTitleAndPlan(params.rawInput);
1204
+ const planTitle = spec.title;
1205
+ const planMarkdown = spec.plan;
1206
+ if (params.toolName === "ExitSpecMode" && planMarkdown) {
1207
+ const signature = `${planTitle ?? ""}\n${planMarkdown}`;
1208
+ if (session.specChoicePromptSignature !== signature) {
1209
+ session.specChoicePromptSignature = signature;
1210
+ session.specChoice = null;
1211
+ }
1212
+ if (session.specPlanDetailsSignature !== signature) {
1213
+ session.specPlanDetailsSignature = signature;
1214
+ session.specPlanDetailsToolCallId = `${params.toolCallId}:plan_details`;
1215
+ await this.client.sessionUpdate({
1216
+ sessionId: session.id,
1217
+ update: {
1218
+ sessionUpdate: "tool_call",
1219
+ toolCallId: session.specPlanDetailsToolCallId,
1220
+ title: planTitle ? `Plan details: ${planTitle}` : "Plan details",
1221
+ kind: "think",
1222
+ status: "completed",
1223
+ content: [{
1224
+ type: "content",
1225
+ content: {
1226
+ type: "text",
1227
+ text: planMarkdown
1228
+ }
1229
+ }]
1230
+ }
1231
+ });
1232
+ }
1233
+ if (session.specChoice === null) {
1234
+ const choices = this.extractPlanChoices(planMarkdown);
1235
+ if (choices.length > 0) {
1236
+ const detailsHint = session.specPlanDetailsToolCallId ? `Expand **${session.specPlanDetailsToolCallId}** to view the full plan details.` : "Expand the Plan details tool call to view the full plan details.";
1237
+ const choicePrompt = [
1238
+ planTitle ? `**${planTitle}**` : "**Choose an implementation option**",
1239
+ "",
1240
+ detailsHint,
1241
+ "Choose one to continue iterating in spec mode.",
1242
+ ...choices.map((c) => `- Option ${c.id}: ${c.title}`)
1243
+ ].filter((p) => p.length > 0).join("\n");
1244
+ const response = await this.client.requestPermission({
1245
+ sessionId: session.id,
1246
+ toolCall: {
1247
+ toolCallId: `${params.toolCallId}:choose_plan`,
1248
+ title: planTitle ? `Choose plan: ${planTitle}` : "Choose plan option",
1249
+ status: "pending",
1250
+ kind: "think",
1251
+ rawInput: { choices },
1252
+ content: [{
1253
+ type: "content",
1254
+ content: {
1255
+ type: "text",
1256
+ text: choicePrompt
1257
+ }
1258
+ }]
1259
+ },
1260
+ options: [...choices.map((c) => ({
1261
+ optionId: `choose_plan:${c.id}`,
1262
+ name: `Choose Option ${c.id}`,
1263
+ kind: "allow_once"
1264
+ })), {
1265
+ optionId: "choose_plan:skip",
1266
+ name: "Skip",
1267
+ kind: "reject_once"
1268
+ }]
1269
+ });
1270
+ const match = (response.outcome.outcome === "selected" ? response.outcome.optionId : "choose_plan:skip").match(/^choose_plan:([A-Z])$/);
1271
+ if (match) {
1272
+ const choiceId = match[1];
1273
+ session.specChoice = choiceId;
1274
+ await this.client.sessionUpdate({
1275
+ sessionId: session.id,
1276
+ update: {
1277
+ sessionUpdate: "tool_call_update",
1278
+ toolCallId: `${params.toolCallId}:choose_plan`,
1279
+ status: "completed"
1280
+ }
1281
+ });
1282
+ session.toolCallStatus.set(params.toolCallId, "completed");
1283
+ session.activeToolCallIds.delete(params.toolCallId);
1284
+ await this.client.sessionUpdate({
1285
+ sessionId: session.id,
1286
+ update: {
1287
+ sessionUpdate: "tool_call_update",
1288
+ toolCallId: params.toolCallId,
1289
+ status: "completed",
1290
+ content: [{
1291
+ type: "content",
1292
+ content: {
1293
+ type: "text",
1294
+ text: `Continuing in spec mode with Option ${choiceId}.`
1295
+ }
1296
+ }]
1297
+ }
1298
+ });
1299
+ await this.sendAgentMessage(session, `Selected **Option ${choiceId}**. Continuing in spec mode.`);
1300
+ setTimeout(() => {
1301
+ session.droid.sendMessage(`我选择方案 ${choiceId}。请基于该方案继续完善计划/关键改动点,并在准备好执行时再提示退出 spec。`);
1302
+ }, 0);
1303
+ return { selectedOption: "cancel" };
1304
+ }
1305
+ session.specChoice = "skip";
1306
+ }
1307
+ }
1308
+ }
1309
+ if (params.toolName === "ExitSpecMode" && planMarkdown) {
1310
+ const entries = this.planEntriesFromMarkdown(planMarkdown);
1311
+ if (entries.length > 0) await this.client.sessionUpdate({
1312
+ sessionId: session.id,
1313
+ update: {
1314
+ sessionUpdate: "plan",
1315
+ entries
1316
+ }
1317
+ });
1318
+ else if (planMarkdown.trim().length > 0) await this.client.sessionUpdate({
1319
+ sessionId: session.id,
1320
+ update: {
1321
+ sessionUpdate: "plan",
1322
+ entries: [{
1323
+ content: planMarkdown.trim(),
1324
+ status: "pending",
1325
+ priority: "medium"
1326
+ }]
1327
+ }
1328
+ });
1329
+ }
1330
+ let autoDecision = null;
1331
+ if (params.toolName === "ExitSpecMode") {
1332
+ autoDecision = null;
1333
+ this.logger.log("Prompting (ExitSpecMode)");
1334
+ } else if (session.mode === "high") {
1335
+ autoDecision = "proceed_always";
1336
+ this.logger.log("Auto-approved (high mode)");
1337
+ } else if (session.mode === "medium") {
1338
+ autoDecision = params.riskLevel === "high" ? null : "proceed_once";
1339
+ this.logger.log(autoDecision ? "Auto-approved (medium mode, low/med risk)" : "Prompting (medium mode)");
1340
+ } else if (session.mode === "low") {
1341
+ autoDecision = params.riskLevel === "low" ? "proceed_once" : null;
1342
+ this.logger.log(autoDecision ? "Auto-approved (low mode, low risk)" : "Prompting (low mode)");
1343
+ } else if (session.mode === "spec") if (params.riskLevel === "low") {
1344
+ autoDecision = "proceed_once";
1345
+ this.logger.log("Auto-approved (spec mode, low risk)");
1346
+ } else {
1347
+ autoDecision = "cancel";
1348
+ this.logger.log("Auto-rejected (spec mode, medium/high risk)");
1349
+ }
1350
+ else {
1351
+ autoDecision = null;
1352
+ this.logger.log("Prompting (off mode)");
1353
+ }
1354
+ if (autoDecision) {
1355
+ const selectedOption$1 = droidOptions?.some((o) => o.value === autoDecision) === true ? autoDecision : autoDecision === "cancel" ? "cancel" : droidOptions ? droidOptions?.find((o) => permissionKindFromOptionValue(o.value) === "allow_once")?.value ?? droidOptions?.find((o) => o.value !== "cancel")?.value ?? "proceed_once" : autoDecision;
1356
+ const isExitSpecMode$1 = params.toolName === "ExitSpecMode";
1357
+ const status$1 = selectedOption$1 === "cancel" || isExitSpecMode$1 ? "completed" : "in_progress";
1358
+ session.toolCallStatus.set(params.toolCallId, status$1);
1359
+ if (status$1 === "completed") session.activeToolCallIds.delete(params.toolCallId);
1360
+ await this.client.sessionUpdate({
1361
+ sessionId: session.id,
1362
+ update: {
1363
+ sessionUpdate: "tool_call_update",
1364
+ toolCallId: params.toolCallId,
1365
+ status: status$1,
1366
+ content: selectedOption$1 === "cancel" ? [{
1367
+ type: "content",
1368
+ content: {
1369
+ type: "text",
1370
+ text: `Permission denied for \`${params.toolName}\` (${params.riskLevel}).`
1371
+ }
1372
+ }] : void 0
1373
+ }
1374
+ });
1375
+ return { selectedOption: selectedOption$1 };
1376
+ }
1377
+ let permission;
1378
+ try {
1379
+ const title = params.toolName === "ExitSpecMode" ? planTitle ? `Exit spec mode: ${planTitle}` : "Exit spec mode" : `Running ${params.toolName} (${params.riskLevel}): ${params.command}`;
1380
+ permission = await this.client.requestPermission({
1381
+ sessionId: session.id,
1382
+ toolCall: {
1383
+ toolCallId: params.toolCallId,
1384
+ title,
1385
+ rawInput: params.rawInput
1386
+ },
1387
+ options: acpOptions
1388
+ });
1389
+ } catch (error) {
1390
+ this.logger.error("requestPermission failed:", error);
1391
+ session.toolCallStatus.set(params.toolCallId, "completed");
1392
+ session.activeToolCallIds.delete(params.toolCallId);
1393
+ await this.client.sessionUpdate({
1394
+ sessionId: session.id,
1395
+ update: {
1396
+ sessionUpdate: "tool_call_update",
1397
+ toolCallId: params.toolCallId,
1398
+ status: "completed",
1399
+ content: [{
1400
+ type: "content",
1401
+ content: {
1402
+ type: "text",
1403
+ text: `Permission request failed for \`${params.toolName}\`. Cancelling the operation.`
1404
+ }
1405
+ }]
1406
+ }
1407
+ });
1408
+ return { selectedOption: "cancel" };
1409
+ }
1410
+ let selectedOption = "cancel";
1411
+ if (permission.outcome.outcome === "selected") selectedOption = permission.outcome.optionId;
1412
+ else selectedOption = "cancel";
1413
+ if (params.toolName === "ExitSpecMode") {
1414
+ const mapped = this.mapExitSpecModeSelection(selectedOption);
1415
+ selectedOption = mapped.droidSelectedOption;
1416
+ if (mapped.nextMode) {
1417
+ session.mode = mapped.nextMode;
1418
+ await this.client.sessionUpdate({
1419
+ sessionId: session.id,
1420
+ update: {
1421
+ sessionUpdate: "current_mode_update",
1422
+ currentModeId: mapped.nextMode
1423
+ }
1424
+ });
1425
+ }
1426
+ }
1427
+ const isExitSpecMode = params.toolName === "ExitSpecMode";
1428
+ const status = selectedOption === "cancel" || isExitSpecMode ? "completed" : "in_progress";
1429
+ session.toolCallStatus.set(params.toolCallId, status);
1430
+ if (status === "completed") session.activeToolCallIds.delete(params.toolCallId);
1431
+ if (isExitSpecMode) await this.client.sessionUpdate({
1432
+ sessionId: session.id,
1433
+ update: {
1434
+ sessionUpdate: "tool_call_update",
1435
+ toolCallId: params.toolCallId,
1436
+ status,
1437
+ content: selectedOption === "cancel" ? [{
1438
+ type: "content",
1439
+ content: {
1440
+ type: "text",
1441
+ text: "Staying in Spec mode."
1442
+ }
1443
+ }] : void 0
1444
+ }
1445
+ });
1446
+ else if (selectedOption !== "cancel") await this.client.sessionUpdate({
1447
+ sessionId: session.id,
1448
+ update: {
1449
+ sessionUpdate: "tool_call_update",
1450
+ toolCallId: params.toolCallId,
1451
+ status
1452
+ }
1453
+ });
1454
+ return { selectedOption };
1455
+ }
1456
+ async handleNotification(session, n) {
1457
+ this.logger.log("notification:", n.type);
1458
+ switch (n.type) {
1459
+ case "settings_updated": {
1460
+ const autonomyLevel = typeof n.settings.autonomyLevel === "string" ? droidAutonomyToAcpModeId(n.settings.autonomyLevel) : null;
1461
+ if (autonomyLevel && autonomyLevel !== session.mode) {
1462
+ session.mode = autonomyLevel;
1463
+ await this.client.sessionUpdate({
1464
+ sessionId: session.id,
1465
+ update: {
1466
+ sessionUpdate: "current_mode_update",
1467
+ currentModeId: autonomyLevel
1468
+ }
1469
+ });
1470
+ }
1471
+ if (typeof n.settings.modelId === "string") session.model = n.settings.modelId;
1472
+ break;
1473
+ }
1474
+ case "message":
1475
+ if (n.role === "assistant") {
1476
+ if (n.toolUse) {
1477
+ if (n.toolUse.name === "TodoWrite") {
1478
+ const todos = n.toolUse.input?.todos;
1479
+ if (Array.isArray(todos)) {
1480
+ const toStatus = (status) => {
1481
+ switch (status) {
1482
+ case "pending":
1483
+ case "in_progress":
1484
+ case "completed": return status;
1485
+ default: return "pending";
1486
+ }
1487
+ };
1488
+ const entries = todos.map((t) => {
1489
+ const todo = t;
1490
+ return {
1491
+ content: typeof todo.content === "string" ? todo.content : "",
1492
+ status: toStatus(todo.status),
1493
+ priority: "medium"
1494
+ };
1495
+ }).filter((e) => e.content.length > 0);
1496
+ await this.client.sessionUpdate({
1497
+ sessionId: session.id,
1498
+ update: {
1499
+ sessionUpdate: "plan",
1500
+ entries
1501
+ }
1502
+ });
1503
+ }
1504
+ break;
1505
+ }
1506
+ const toolCallId = n.toolUse.id;
1507
+ const isExitSpecMode = n.toolUse.name === "ExitSpecMode";
1508
+ const existingStatus = session.toolCallStatus.get(toolCallId);
1509
+ if (existingStatus !== "completed" && existingStatus !== "failed") if (!session.activeToolCallIds.has(toolCallId)) {
1510
+ session.activeToolCallIds.add(toolCallId);
1511
+ session.toolNames.set(toolCallId, n.toolUse.name);
1512
+ const initialStatus = isExitSpecMode ? "pending" : "in_progress";
1513
+ session.toolCallStatus.set(toolCallId, initialStatus);
1514
+ const spec = isExitSpecMode ? this.extractSpecTitleAndPlan(n.toolUse.input) : null;
1515
+ await this.client.sessionUpdate({
1516
+ sessionId: session.id,
1517
+ update: {
1518
+ sessionUpdate: "tool_call",
1519
+ toolCallId,
1520
+ title: isExitSpecMode ? spec?.title ? `Exit spec mode: ${spec.title}` : "Exit spec mode" : `Running ${n.toolUse.name}`,
1521
+ kind: isExitSpecMode ? "switch_mode" : void 0,
1522
+ status: initialStatus,
1523
+ rawInput: n.toolUse.input,
1524
+ content: isExitSpecMode && spec?.plan ? [{
1525
+ type: "content",
1526
+ content: {
1527
+ type: "text",
1528
+ text: spec.plan
1529
+ }
1530
+ }] : void 0
1531
+ }
1532
+ });
1533
+ } else {
1534
+ const status = session.toolCallStatus.get(toolCallId);
1535
+ if (status !== "completed" && status !== "failed" && status !== "pending") {
1536
+ session.toolCallStatus.set(toolCallId, "in_progress");
1537
+ await this.client.sessionUpdate({
1538
+ sessionId: session.id,
1539
+ update: {
1540
+ sessionUpdate: "tool_call_update",
1541
+ toolCallId,
1542
+ status: "in_progress"
1543
+ }
1544
+ });
1545
+ }
1546
+ }
1547
+ }
1548
+ if (n.text) await this.client.sessionUpdate({
1549
+ sessionId: session.id,
1550
+ update: {
1551
+ sessionUpdate: "agent_message_chunk",
1552
+ content: {
1553
+ type: "text",
1554
+ text: n.text
1555
+ }
1556
+ }
1557
+ });
1558
+ }
1559
+ break;
1560
+ case "tool_result":
1561
+ if (!session.activeToolCallIds.has(n.toolUseId)) {
1562
+ session.activeToolCallIds.add(n.toolUseId);
1563
+ const name = session.toolNames.get(n.toolUseId) ?? "Tool";
1564
+ await this.client.sessionUpdate({
1565
+ sessionId: session.id,
1566
+ update: {
1567
+ sessionUpdate: "tool_call",
1568
+ toolCallId: n.toolUseId,
1569
+ title: `Running ${name}`,
1570
+ status: "in_progress"
1571
+ }
1572
+ });
1573
+ }
1574
+ const finalStatus = n.isError ? "failed" : "completed";
1575
+ await this.client.sessionUpdate({
1576
+ sessionId: session.id,
1577
+ update: {
1578
+ sessionUpdate: "tool_call_update",
1579
+ toolCallId: n.toolUseId,
1580
+ content: [{
1581
+ type: "content",
1582
+ content: {
1583
+ type: "text",
1584
+ text: n.content
1585
+ }
1586
+ }],
1587
+ rawOutput: n.content,
1588
+ status: finalStatus
1589
+ }
1590
+ });
1591
+ session.toolCallStatus.set(n.toolUseId, finalStatus);
1592
+ session.activeToolCallIds.delete(n.toolUseId);
1593
+ break;
1594
+ case "error":
1595
+ await this.client.sessionUpdate({
1596
+ sessionId: session.id,
1597
+ update: {
1598
+ sessionUpdate: "agent_message_chunk",
1599
+ content: {
1600
+ type: "text",
1601
+ text: `Error: ${n.message}`
1602
+ }
1603
+ }
1604
+ });
1605
+ if (session.promptResolve) {
1606
+ session.promptResolve({ stopReason: "end_turn" });
1607
+ session.promptResolve = null;
1608
+ }
1609
+ break;
1610
+ case "complete":
1611
+ if (session.promptResolve) {
1612
+ session.promptResolve({ stopReason: "end_turn" });
1613
+ session.promptResolve = null;
1614
+ }
1615
+ break;
1616
+ }
1617
+ }
1618
+ async cleanup() {
1619
+ for (const [, session] of this.sessions) await session.droid.stop();
1620
+ this.sessions.clear();
1621
+ }
1622
+ };
1623
+ function runAcp() {
1624
+ new AgentSideConnection((client) => new DroidAcpAgent(client), ndJsonStream(nodeToWebWritable(process.stdout), nodeToWebReadable(process.stdin)));
1625
+ }
1626
+
1627
+ //#endregion
1628
+ export { Pushable as a, createDroidAdapter as i, runAcp as n, findDroidExecutable as o, ACP_MODES as r, isWindows as s, DroidAcpAgent as t };
1629
+ //# sourceMappingURL=acp-agent-Ddz1S3Jm.mjs.map