svamp-cli 0.2.342 → 0.2.346

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 (67) hide show
  1. package/bin/skills/artifact/SKILL.md +1 -1
  2. package/bin/skills/crew/SKILL.md +21 -3
  3. package/dist/DefaultTransport-vfp78fug.mjs +47 -0
  4. package/dist/GeminiTransport-3OkhtFRO.mjs +130 -0
  5. package/dist/KimiTransport-D8n009Tr.mjs +141 -0
  6. package/dist/acpAgentConfig-CObOPlA8.mjs +48 -0
  7. package/dist/acpBackend-BZ1MJhQV.mjs +922 -0
  8. package/dist/{adminCommands-CRTBDZan.mjs → adminCommands-BvsYnRDA.mjs} +2 -18
  9. package/dist/{agentCommands-BuO988tJ.mjs → agentCommands-B2Nl7Fof.mjs} +6 -23
  10. package/dist/api-BHjyC0Ph.mjs +157 -0
  11. package/dist/{auth--WS0wezs.mjs → auth-CRz8TsEl.mjs} +1 -22
  12. package/dist/bootStatus-DqtNgJch.mjs +143 -0
  13. package/dist/claudeAuth-iAerdaMr.mjs +247 -0
  14. package/dist/cli.mjs +2939 -5
  15. package/dist/codexAppServerBackend-VGsYtbqg.mjs +786 -0
  16. package/dist/codexProvider-BtxYc5Rj.mjs +68 -0
  17. package/dist/{commands-DXM17M6n.mjs → commands-BB1Kcyt-.mjs} +13 -19
  18. package/dist/{commands-DU4B-4Gh.mjs → commands-BOpoVBOM.mjs} +55 -25
  19. package/dist/{commands-C0HQiu-S.mjs → commands-C2aODeVc.mjs} +58 -33
  20. package/dist/{commands-Cwxy-C5N.mjs → commands-CiRCwjkJ.mjs} +10 -32
  21. package/dist/{commands-DDuGBSsz.mjs → commands-D1pZq3d4.mjs} +103 -41
  22. package/dist/{commands-BKb1g8Gq.mjs → commands-D69mnkYu.mjs} +20 -30
  23. package/dist/{commands-BsKgZ5sQ.mjs → commands-DW5RnHZw.mjs} +2 -19
  24. package/dist/{commands-DXLOKJOj.mjs → commands-H4PsTGn8.mjs} +58 -26
  25. package/dist/cron-hHGdb5kU.mjs +132 -0
  26. package/dist/detectIsolation-CzoDCEnh.mjs +319 -0
  27. package/dist/envFlag-COOuJ3AH.mjs +11 -0
  28. package/dist/fileLock-BMPwCrB6.mjs +80 -0
  29. package/dist/{fleet-BHzlAvnu.mjs → fleet-DM_Q7w69.mjs} +85 -24
  30. package/dist/flushExit-C-i6SGjj.mjs +37 -0
  31. package/dist/friendlyName-1UZcyDhu.mjs +586 -0
  32. package/dist/{headlessCli-C2gvOpOt.mjs → headlessCli-C8HzQvis.mjs} +3 -21
  33. package/dist/hookSettings-DYMTUgVA.mjs +10399 -0
  34. package/dist/{httpServer-Be1Hcddr.mjs → httpServer-BzEDbOGn.mjs} +14 -5
  35. package/dist/hyphaClient-ybivOjZo.mjs +49 -0
  36. package/dist/index.mjs +40 -8
  37. package/dist/instance-CeHz5C8v.mjs +203 -0
  38. package/dist/kimiProvider-DbxODebR.mjs +69 -0
  39. package/dist/kimiSetup-NGsoYB5h.mjs +48 -0
  40. package/dist/loopVerify-B47sSjxe.mjs +444 -0
  41. package/dist/machineTools-DygM1jXW.mjs +580 -0
  42. package/dist/model-CUHojegn.mjs +66 -0
  43. package/dist/{notifyCommands-0aZUwKQK.mjs → notifyCommands-D-IKIxiq.mjs} +3 -18
  44. package/dist/package-DtuINqf5.mjs +64 -0
  45. package/dist/pinnedCodex-XW4qvOAe.mjs +11 -0
  46. package/dist/pinnedNono-5ZHb6328.mjs +15 -0
  47. package/dist/{rpc-rAQQ1q8t.mjs → rpc-BB6RF4-F.mjs} +38 -6
  48. package/dist/{rpc-B2Lc2BAB.mjs → rpc-Cf0grcru.mjs} +28 -20
  49. package/dist/{run-C7QVfyw4.mjs → run-ChdsgaSD.mjs} +17 -9
  50. package/dist/run-DkjwF-zA.mjs +9695 -0
  51. package/dist/sandboxDetect-Cdzsj5TC.mjs +19 -0
  52. package/dist/{scheduler-CzF4lOe_.mjs → scheduler-BY30ZrB9.mjs} +38 -6
  53. package/dist/securityContext-Cm5UTCuD.mjs +218 -0
  54. package/dist/serveAuth-DbVLRCRR.mjs +418 -0
  55. package/dist/{serveCommands-EIEqdaVu.mjs → serveCommands-CUHUiYnZ.mjs} +13 -34
  56. package/dist/sharingNotificationSync-qGIQ-3yf.mjs +359 -0
  57. package/dist/{sideband-BoutiRJi.mjs → sideband-68HhGT0i.mjs} +1 -21
  58. package/dist/store-Bn0ByYev.mjs +347 -0
  59. package/dist/store-fi0f-ff1.mjs +184 -0
  60. package/dist/types-57J3Re0n.mjs +17 -0
  61. package/dist/wantsHelp-6bMOeNUz.mjs +5 -0
  62. package/dist/wrapCommand-CdNawSaa.mjs +117 -0
  63. package/package.json +3 -3
  64. package/dist/cli-B24CmIli.mjs +0 -2780
  65. package/dist/package-CTbGYaJG.mjs +0 -64
  66. package/dist/run-CF4kAVPW.mjs +0 -26508
  67. package/dist/sandboxDetect-DNTcbgWD.mjs +0 -12
@@ -0,0 +1,922 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { DefaultTransport } from './DefaultTransport-vfp78fug.mjs';
5
+ import { w as wrapWithIsolation } from './wrapCommand-CdNawSaa.mjs';
6
+ import 'node:fs';
7
+ import 'node:os';
8
+ import 'node:path';
9
+
10
+ const DEFAULT_IDLE_TIMEOUT_MS = 500;
11
+ const DEFAULT_TOOL_CALL_TIMEOUT_MS = 12e4;
12
+ function parseArgsFromContent(content) {
13
+ if (Array.isArray(content)) return { items: content };
14
+ if (content && typeof content === "object" && content !== null) {
15
+ return content;
16
+ }
17
+ return {};
18
+ }
19
+ function extractErrorDetail(content) {
20
+ if (!content) return void 0;
21
+ if (typeof content === "string") return content;
22
+ if (typeof content === "object" && content !== null && !Array.isArray(content)) {
23
+ const obj = content;
24
+ if (obj.error) {
25
+ const error = obj.error;
26
+ if (typeof error === "string") return error;
27
+ if (error && typeof error === "object" && "message" in error) {
28
+ const errObj = error;
29
+ if (typeof errObj.message === "string") return errObj.message;
30
+ }
31
+ return JSON.stringify(error);
32
+ }
33
+ if (typeof obj.message === "string") return obj.message;
34
+ const status = typeof obj.status === "string" ? obj.status : void 0;
35
+ const reason = typeof obj.reason === "string" ? obj.reason : void 0;
36
+ return status || reason || JSON.stringify(obj).substring(0, 500);
37
+ }
38
+ return void 0;
39
+ }
40
+ function formatDuration(startTime) {
41
+ if (!startTime) return "unknown";
42
+ const duration = Date.now() - startTime;
43
+ return `${(duration / 1e3).toFixed(2)}s`;
44
+ }
45
+ function handleAgentMessageChunk(update, ctx) {
46
+ const content = update.content;
47
+ if (!content || typeof content !== "object" || !("text" in content)) {
48
+ return { handled: false };
49
+ }
50
+ const text = content.text;
51
+ if (typeof text !== "string") return { handled: false };
52
+ const isThinking = /^\*\*[^*]+\*\*\n/.test(text);
53
+ if (isThinking) {
54
+ ctx.emit({ type: "event", name: "thinking", payload: { text, streaming: true } });
55
+ } else {
56
+ ctx.emit({ type: "model-output", textDelta: text });
57
+ ctx.clearIdleTimeout();
58
+ const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
59
+ ctx.setIdleTimeout(() => {
60
+ if (ctx.activeToolCalls.size === 0) {
61
+ ctx.emitIdleStatus();
62
+ }
63
+ }, idleTimeoutMs);
64
+ }
65
+ return { handled: true };
66
+ }
67
+ function handleAgentThoughtChunk(update, ctx) {
68
+ const content = update.content;
69
+ if (!content || typeof content !== "object" || !("text" in content)) {
70
+ return { handled: false };
71
+ }
72
+ const text = content.text;
73
+ if (typeof text !== "string") return { handled: false };
74
+ ctx.emit({ type: "event", name: "thinking", payload: { text, streaming: true } });
75
+ return { handled: true };
76
+ }
77
+ function startToolCall(toolCallId, toolKind, update, ctx, source) {
78
+ const startTime = Date.now();
79
+ const toolKindStr = typeof toolKind === "string" ? toolKind : void 0;
80
+ const isInvestigation = ctx.transport.isInvestigationTool?.(toolCallId, toolKindStr) ?? false;
81
+ const extractedName = ctx.transport.extractToolNameFromId?.(toolCallId);
82
+ const realToolName = extractedName ?? (toolKindStr || "unknown");
83
+ ctx.toolCallIdToNameMap.set(toolCallId, realToolName);
84
+ ctx.activeToolCalls.add(toolCallId);
85
+ ctx.toolCallStartTimes.set(toolCallId, startTime);
86
+ ctx.log(`Tool call START: ${toolCallId} (${toolKind} -> ${realToolName})${isInvestigation ? " [INVESTIGATION]" : ""} (from ${source})`);
87
+ const timeoutMs = ctx.transport.getToolCallTimeout?.(toolCallId, toolKindStr) ?? DEFAULT_TOOL_CALL_TIMEOUT_MS;
88
+ if (!ctx.toolCallTimeouts.has(toolCallId)) {
89
+ const timeout = setTimeout(() => {
90
+ ctx.activeToolCalls.delete(toolCallId);
91
+ ctx.toolCallStartTimes.delete(toolCallId);
92
+ ctx.toolCallTimeouts.delete(toolCallId);
93
+ ctx.toolCallIdToNameMap.delete(toolCallId);
94
+ ctx.log(`Tool call TIMEOUT: ${toolCallId} (${toolKind}) after ${(timeoutMs / 1e3).toFixed(0)}s`);
95
+ if (ctx.activeToolCalls.size === 0) {
96
+ ctx.emitIdleStatus();
97
+ }
98
+ }, timeoutMs);
99
+ ctx.toolCallTimeouts.set(toolCallId, timeout);
100
+ }
101
+ ctx.clearIdleTimeout();
102
+ ctx.emit({ type: "status", status: "running" });
103
+ const args = parseArgsFromContent(update.content);
104
+ if (update.locations && Array.isArray(update.locations)) {
105
+ args.locations = update.locations;
106
+ }
107
+ ctx.emit({
108
+ type: "tool-call",
109
+ toolName: toolKindStr || "unknown",
110
+ args,
111
+ callId: toolCallId
112
+ });
113
+ }
114
+ function completeToolCall(toolCallId, toolKind, content, ctx) {
115
+ const startTime = ctx.toolCallStartTimes.get(toolCallId);
116
+ const duration = formatDuration(startTime);
117
+ const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
118
+ ctx.activeToolCalls.delete(toolCallId);
119
+ ctx.toolCallStartTimes.delete(toolCallId);
120
+ ctx.toolCallIdToNameMap.delete(toolCallId);
121
+ const timeout = ctx.toolCallTimeouts.get(toolCallId);
122
+ if (timeout) {
123
+ clearTimeout(timeout);
124
+ ctx.toolCallTimeouts.delete(toolCallId);
125
+ }
126
+ ctx.log(`Tool call COMPLETED: ${toolCallId} (${toolKindStr}) - ${duration}. Active: ${ctx.activeToolCalls.size}`);
127
+ ctx.emit({ type: "tool-result", toolName: toolKindStr, result: content, callId: toolCallId });
128
+ if (ctx.activeToolCalls.size === 0) {
129
+ ctx.clearIdleTimeout();
130
+ const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
131
+ ctx.setIdleTimeout(() => {
132
+ if (ctx.activeToolCalls.size === 0) {
133
+ ctx.emitIdleStatus();
134
+ }
135
+ }, idleTimeoutMs);
136
+ }
137
+ }
138
+ function failToolCall(toolCallId, status, toolKind, content, ctx) {
139
+ const startTime = ctx.toolCallStartTimes.get(toolCallId);
140
+ const duration = formatDuration(startTime);
141
+ const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
142
+ ctx.activeToolCalls.delete(toolCallId);
143
+ ctx.toolCallStartTimes.delete(toolCallId);
144
+ ctx.toolCallIdToNameMap.delete(toolCallId);
145
+ const timeout = ctx.toolCallTimeouts.get(toolCallId);
146
+ if (timeout) {
147
+ clearTimeout(timeout);
148
+ ctx.toolCallTimeouts.delete(toolCallId);
149
+ }
150
+ const errorDetail = extractErrorDetail(content);
151
+ ctx.log(`Tool call ${status.toUpperCase()}: ${toolCallId} (${toolKindStr}) - ${duration}. Active: ${ctx.activeToolCalls.size}`);
152
+ ctx.emit({
153
+ type: "tool-result",
154
+ toolName: toolKindStr,
155
+ result: errorDetail ? { error: errorDetail, status } : { error: `Tool call ${status}`, status },
156
+ callId: toolCallId
157
+ });
158
+ if (ctx.activeToolCalls.size === 0) {
159
+ ctx.clearIdleTimeout();
160
+ const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
161
+ ctx.setIdleTimeout(() => {
162
+ if (ctx.activeToolCalls.size === 0) {
163
+ ctx.emitIdleStatus();
164
+ }
165
+ }, idleTimeoutMs);
166
+ }
167
+ }
168
+ function handleToolCallUpdate(update, ctx) {
169
+ const status = update.status;
170
+ const toolCallId = update.toolCallId;
171
+ if (!toolCallId) return { handled: false };
172
+ const toolKind = update.kind || "unknown";
173
+ let toolCallCountSincePrompt = ctx.toolCallCountSincePrompt;
174
+ if (status === "in_progress" || status === "pending") {
175
+ if (!ctx.activeToolCalls.has(toolCallId)) {
176
+ toolCallCountSincePrompt++;
177
+ startToolCall(toolCallId, toolKind, update, ctx, "tool_call_update");
178
+ }
179
+ } else if (status === "completed") {
180
+ completeToolCall(toolCallId, toolKind, update.content, ctx);
181
+ } else if (status === "failed" || status === "cancelled") {
182
+ failToolCall(toolCallId, status, toolKind, update.content, ctx);
183
+ }
184
+ return { handled: true, toolCallCountSincePrompt };
185
+ }
186
+ function handleToolCall(update, ctx) {
187
+ const toolCallId = update.toolCallId;
188
+ const status = update.status;
189
+ const isInProgress = !status || status === "in_progress" || status === "pending";
190
+ if (!toolCallId || !isInProgress) return { handled: false };
191
+ if (ctx.activeToolCalls.has(toolCallId)) return { handled: true };
192
+ startToolCall(toolCallId, update.kind, update, ctx, "tool_call");
193
+ return { handled: true };
194
+ }
195
+ function handleLegacyMessageChunk(update, ctx) {
196
+ if (!update.messageChunk) return { handled: false };
197
+ const chunk = update.messageChunk;
198
+ if (chunk.textDelta) {
199
+ ctx.emit({ type: "model-output", textDelta: chunk.textDelta });
200
+ return { handled: true };
201
+ }
202
+ return { handled: false };
203
+ }
204
+ function handlePlanUpdate(update, ctx) {
205
+ if (!update.plan) return { handled: false };
206
+ ctx.emit({ type: "event", name: "plan", payload: update.plan });
207
+ return { handled: true };
208
+ }
209
+ function handleThinkingUpdate(update, ctx) {
210
+ if (!update.thinking) return { handled: false };
211
+ ctx.emit({ type: "event", name: "thinking", payload: update.thinking });
212
+ return { handled: true };
213
+ }
214
+
215
+ function delay(ms) {
216
+ return new Promise((resolve) => setTimeout(resolve, ms));
217
+ }
218
+ const RETRY_CONFIG = {
219
+ maxAttempts: 3,
220
+ baseDelayMs: 1e3,
221
+ maxDelayMs: 5e3
222
+ };
223
+ function nodeToWebStreams(stdin, stdout) {
224
+ const writable = new WritableStream({
225
+ write(chunk) {
226
+ return new Promise((resolve, reject) => {
227
+ const ok = stdin.write(chunk, (err) => {
228
+ if (err) reject(err);
229
+ });
230
+ if (ok) resolve();
231
+ else stdin.once("drain", resolve);
232
+ });
233
+ },
234
+ close() {
235
+ return new Promise((resolve) => {
236
+ stdin.end(resolve);
237
+ });
238
+ },
239
+ abort(reason) {
240
+ stdin.destroy(reason instanceof Error ? reason : new Error(String(reason)));
241
+ }
242
+ });
243
+ const readable = new ReadableStream({
244
+ start(controller) {
245
+ stdout.on("data", (chunk) => {
246
+ controller.enqueue(new Uint8Array(chunk));
247
+ });
248
+ stdout.on("end", () => {
249
+ controller.close();
250
+ });
251
+ stdout.on("error", (err) => {
252
+ controller.error(err);
253
+ });
254
+ },
255
+ cancel() {
256
+ stdout.destroy();
257
+ }
258
+ });
259
+ return { writable, readable };
260
+ }
261
+ async function withRetry(operation, opts) {
262
+ let lastError = null;
263
+ const log = opts.log || (() => {
264
+ });
265
+ for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
266
+ try {
267
+ return await operation();
268
+ } catch (error) {
269
+ if (error instanceof Error) {
270
+ lastError = error;
271
+ } else if (typeof error === "object" && error !== null) {
272
+ const obj = error;
273
+ const msg = typeof obj.message === "string" ? obj.message : JSON.stringify(error);
274
+ lastError = new Error(msg);
275
+ } else {
276
+ lastError = new Error(String(error));
277
+ }
278
+ const shouldRetry = opts.shouldRetry ? opts.shouldRetry(lastError) : true;
279
+ if (attempt < opts.maxAttempts && shouldRetry) {
280
+ const delayMs = Math.min(opts.baseDelayMs * Math.pow(2, attempt - 1), opts.maxDelayMs);
281
+ log(`${opts.operationName} failed (attempt ${attempt}/${opts.maxAttempts}): ${lastError.message}. Retrying in ${delayMs}ms...`);
282
+ await delay(delayMs);
283
+ } else {
284
+ break;
285
+ }
286
+ }
287
+ }
288
+ throw lastError;
289
+ }
290
+ class AcpBackend {
291
+ constructor(options) {
292
+ this.options = options;
293
+ this.transport = options.transportHandler ?? new DefaultTransport(options.agentName);
294
+ this.log = options.log || ((...args) => {
295
+ });
296
+ }
297
+ listeners = [];
298
+ process = null;
299
+ connection = null;
300
+ acpSessionId = null;
301
+ disposed = false;
302
+ activeToolCalls = /* @__PURE__ */ new Set();
303
+ toolCallTimeouts = /* @__PURE__ */ new Map();
304
+ toolCallStartTimes = /* @__PURE__ */ new Map();
305
+ toolCallIdToNameMap = /* @__PURE__ */ new Map();
306
+ recentPromptHadChangeTitle = false;
307
+ toolCallCountSincePrompt = 0;
308
+ idleTimeout = null;
309
+ transport;
310
+ log;
311
+ // Promise resolver for waitForIdle
312
+ idleResolver = null;
313
+ waitingForResponse = false;
314
+ onMessage(handler) {
315
+ this.listeners.push(handler);
316
+ }
317
+ offMessage(handler) {
318
+ const index = this.listeners.indexOf(handler);
319
+ if (index !== -1) this.listeners.splice(index, 1);
320
+ }
321
+ emit(msg) {
322
+ if (this.disposed) return;
323
+ for (const listener of this.listeners) {
324
+ try {
325
+ listener(msg);
326
+ } catch {
327
+ }
328
+ }
329
+ }
330
+ /** Get the underlying child process (for cleanup) */
331
+ getProcess() {
332
+ return this.process;
333
+ }
334
+ async startSession(initialPrompt) {
335
+ if (this.disposed) throw new Error("Backend has been disposed");
336
+ const sessionId = randomUUID();
337
+ this.emit({ type: "status", status: "starting" });
338
+ let startupStatusErrorEmitted = false;
339
+ try {
340
+ const args = this.options.args || [];
341
+ let spawnCommand = this.options.command;
342
+ let spawnArgs = args;
343
+ let isoEnv = {};
344
+ let isoCleanupFiles = [];
345
+ if (this.options.isolationConfig) {
346
+ const wrapped = wrapWithIsolation(spawnCommand, spawnArgs, this.options.isolationConfig);
347
+ spawnCommand = wrapped.command;
348
+ spawnArgs = wrapped.args;
349
+ if (wrapped.env) isoEnv = wrapped.env;
350
+ if (wrapped.cleanupFiles) isoCleanupFiles = wrapped.cleanupFiles;
351
+ this.log(`[ACP] Isolation: ${this.options.isolationConfig.method}`);
352
+ }
353
+ const spawnEnv = { ...process.env, ...this.options.env, ...isoEnv };
354
+ if (process.platform === "win32") {
355
+ const fullCommand = [spawnCommand, ...spawnArgs].join(" ");
356
+ this.process = spawn("cmd.exe", ["/c", fullCommand], {
357
+ cwd: this.options.cwd,
358
+ env: spawnEnv,
359
+ stdio: ["pipe", "pipe", "pipe"],
360
+ windowsHide: true
361
+ });
362
+ } else {
363
+ this.process = spawn(spawnCommand, spawnArgs, {
364
+ cwd: this.options.cwd,
365
+ env: spawnEnv,
366
+ stdio: ["pipe", "pipe", "pipe"]
367
+ });
368
+ }
369
+ if (isoCleanupFiles.length > 0) {
370
+ this.process.on("exit", async () => {
371
+ const { rm } = await import('node:fs/promises');
372
+ for (const f of isoCleanupFiles) rm(f, { force: true }).catch(() => {
373
+ });
374
+ });
375
+ }
376
+ if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
377
+ throw new Error("Failed to create stdio pipes");
378
+ }
379
+ let startupFailure = null;
380
+ let startupFailureSettled = false;
381
+ let rejectStartupFailure = null;
382
+ const startupFailurePromise = new Promise((_, reject) => {
383
+ rejectStartupFailure = (error) => {
384
+ if (startupFailureSettled) return;
385
+ startupFailureSettled = true;
386
+ startupFailure = error;
387
+ reject(error);
388
+ };
389
+ });
390
+ const signalStartupFailure = (error) => {
391
+ rejectStartupFailure?.(error);
392
+ };
393
+ this.process.stderr.on("data", (data) => {
394
+ const text = data.toString();
395
+ if (!text.trim()) return;
396
+ const hasActiveInvestigation = this.transport.isInvestigationTool ? Array.from(this.activeToolCalls).some((id) => this.transport.isInvestigationTool(id)) : false;
397
+ const context = {
398
+ activeToolCalls: this.activeToolCalls,
399
+ hasActiveInvestigation
400
+ };
401
+ this.log(`[ACP] stderr: ${text.trim()}`);
402
+ if (this.transport.handleStderr) {
403
+ const result = this.transport.handleStderr(text, context);
404
+ if (result.message) this.emit(result.message);
405
+ }
406
+ });
407
+ this.process.on("error", (err) => {
408
+ signalStartupFailure(err);
409
+ this.log(`[ACP] Process error: ${err.message}`);
410
+ startupStatusErrorEmitted = true;
411
+ this.emit({ type: "status", status: "error", detail: err.message });
412
+ });
413
+ this.process.on("exit", (code, signal) => {
414
+ this.process = null;
415
+ if (this.disposed) return;
416
+ if (code !== 0 && code !== null) {
417
+ signalStartupFailure(new Error(`Exit code: ${code}`));
418
+ this.log(`[ACP] Process exited: code=${code}, signal=${signal}`);
419
+ this.emit({ type: "status", status: "stopped", detail: `Exit code: ${code}` });
420
+ } else if (code === null && this.waitingForResponse) {
421
+ this.log(`[ACP] Process killed by signal: ${signal} (mid-turn)`);
422
+ this.waitingForResponse = false;
423
+ if (this.idleResolver) {
424
+ this.idleResolver();
425
+ this.idleResolver = null;
426
+ }
427
+ this.emit({ type: "status", status: "stopped", detail: `Process killed by signal: ${signal}` });
428
+ } else if (code === 0 && this.waitingForResponse) {
429
+ this.log(`[ACP] Process exited cleanly but response was pending \u2014 emitting idle`);
430
+ this.emitIdleStatus();
431
+ }
432
+ });
433
+ const streams = nodeToWebStreams(this.process.stdin, this.process.stdout);
434
+ const transport = this.transport;
435
+ const logFn = this.log;
436
+ const filteredReadable = new ReadableStream({
437
+ async start(controller) {
438
+ const reader = streams.readable.getReader();
439
+ const decoder = new TextDecoder();
440
+ const encoder = new TextEncoder();
441
+ let buffer = "";
442
+ let filteredCount = 0;
443
+ try {
444
+ while (true) {
445
+ const { done, value } = await reader.read();
446
+ if (done) {
447
+ if (buffer.trim()) {
448
+ const filtered = transport.filterStdoutLine?.(buffer);
449
+ if (filtered === void 0) controller.enqueue(encoder.encode(buffer));
450
+ else if (filtered !== null) controller.enqueue(encoder.encode(filtered));
451
+ else filteredCount++;
452
+ }
453
+ if (filteredCount > 0) {
454
+ logFn(`[ACP] Filtered ${filteredCount} non-JSON lines from ${transport.agentName} stdout`);
455
+ }
456
+ controller.close();
457
+ break;
458
+ }
459
+ buffer += decoder.decode(value, { stream: true });
460
+ const lines = buffer.split("\n");
461
+ buffer = lines.pop() || "";
462
+ for (const line of lines) {
463
+ if (!line.trim()) continue;
464
+ const filtered = transport.filterStdoutLine?.(line);
465
+ if (filtered === void 0) controller.enqueue(encoder.encode(line + "\n"));
466
+ else if (filtered !== null) controller.enqueue(encoder.encode(filtered + "\n"));
467
+ else filteredCount++;
468
+ }
469
+ }
470
+ } catch (error) {
471
+ controller.error(error);
472
+ } finally {
473
+ reader.releaseLock();
474
+ }
475
+ }
476
+ });
477
+ const stream = ndJsonStream(streams.writable, filteredReadable);
478
+ const client = {
479
+ sessionUpdate: async (params) => {
480
+ this.handleSessionUpdate(params);
481
+ },
482
+ requestPermission: async (params) => {
483
+ const extendedParams = params;
484
+ const toolCall = extendedParams.toolCall;
485
+ let toolName = toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool";
486
+ const toolCallId = toolCall?.id || randomUUID();
487
+ const permissionId = toolCallId;
488
+ let input = {};
489
+ if (toolCall) {
490
+ input = toolCall.input || toolCall.arguments || toolCall.content || {};
491
+ } else {
492
+ input = extendedParams.input || extendedParams.arguments || extendedParams.content || {};
493
+ }
494
+ const context = {
495
+ recentPromptHadChangeTitle: this.recentPromptHadChangeTitle,
496
+ toolCallCountSincePrompt: this.toolCallCountSincePrompt
497
+ };
498
+ toolName = this.transport.determineToolName?.(toolName, toolCallId, input, context) ?? toolName;
499
+ this.toolCallCountSincePrompt++;
500
+ const options = extendedParams.options || [];
501
+ this.log(`[ACP] Permission request: tool=${toolName}, toolCallId=${toolCallId}`);
502
+ this.emit({
503
+ type: "permission-request",
504
+ id: permissionId,
505
+ reason: toolName,
506
+ payload: {
507
+ ...params,
508
+ permissionId,
509
+ toolCallId,
510
+ toolName,
511
+ input,
512
+ options: options.map((opt) => ({
513
+ id: opt.optionId,
514
+ name: opt.name,
515
+ kind: opt.kind
516
+ }))
517
+ }
518
+ });
519
+ if (this.options.permissionHandler) {
520
+ try {
521
+ const result = await this.options.permissionHandler.handleToolCall(toolCallId, toolName, input);
522
+ let optionId = "cancel";
523
+ if (result.decision === "approved" || result.decision === "approved_for_session") {
524
+ const proceedOnceOption2 = options.find(
525
+ (opt) => opt.optionId === "proceed_once" || opt.name?.toLowerCase().includes("once")
526
+ );
527
+ const proceedAlwaysOption = options.find(
528
+ (opt) => opt.optionId === "proceed_always" || opt.name?.toLowerCase().includes("always")
529
+ );
530
+ if (result.decision === "approved_for_session" && proceedAlwaysOption) {
531
+ optionId = proceedAlwaysOption.optionId || "proceed_always";
532
+ } else if (proceedOnceOption2) {
533
+ optionId = proceedOnceOption2.optionId || "proceed_once";
534
+ } else if (options.length > 0) {
535
+ optionId = options[0].optionId || "proceed_once";
536
+ }
537
+ this.emit({
538
+ type: "tool-result",
539
+ toolName,
540
+ result: { status: "approved", decision: result.decision },
541
+ callId: permissionId
542
+ });
543
+ } else {
544
+ const cancelOption = options.find(
545
+ (opt) => opt.optionId === "cancel" || opt.name?.toLowerCase().includes("cancel")
546
+ );
547
+ if (cancelOption) optionId = cancelOption.optionId || "cancel";
548
+ this.emit({
549
+ type: "tool-result",
550
+ toolName,
551
+ result: { status: "denied", decision: result.decision },
552
+ callId: permissionId
553
+ });
554
+ }
555
+ return { outcome: { outcome: "selected", optionId } };
556
+ } catch (error) {
557
+ this.log("[ACP] Error in permission handler:", error);
558
+ return { outcome: { outcome: "selected", optionId: "cancel" } };
559
+ }
560
+ }
561
+ const proceedOnceOption = options.find(
562
+ (opt) => opt.optionId === "proceed_once" || typeof opt.name === "string" && opt.name.toLowerCase().includes("once")
563
+ );
564
+ const defaultOptionId = proceedOnceOption?.optionId || (options.length > 0 && options[0].optionId ? options[0].optionId : "proceed_once");
565
+ return { outcome: { outcome: "selected", optionId: defaultOptionId } };
566
+ }
567
+ };
568
+ this.connection = new ClientSideConnection(
569
+ (agent) => client,
570
+ stream
571
+ );
572
+ const initRequest = {
573
+ protocolVersion: 1,
574
+ clientCapabilities: {
575
+ fs: { readTextFile: false, writeTextFile: false }
576
+ },
577
+ clientInfo: { name: "svamp-cli", version: "0.1.0" }
578
+ };
579
+ const initTimeout = this.transport.getInitTimeout();
580
+ this.log(`[ACP] Initializing connection (timeout: ${initTimeout}ms)...`);
581
+ const isNonRetryableStartupError = (error) => {
582
+ const maybeErr = error;
583
+ if (startupFailure && error === startupFailure) return true;
584
+ if (maybeErr.code === "ENOENT" || maybeErr.code === "EACCES" || maybeErr.code === "EPIPE") return true;
585
+ if (maybeErr.code === "DISPOSED") return true;
586
+ const msg = error.message.toLowerCase();
587
+ if (msg.includes("api key") || msg.includes("not configured") || msg.includes("401") || msg.includes("403")) return true;
588
+ return false;
589
+ };
590
+ await withRetry(
591
+ async () => {
592
+ if (this.disposed) throw Object.assign(new Error("Backend disposed during startup retry"), { code: "DISPOSED" });
593
+ let timeoutHandle = null;
594
+ try {
595
+ const result = await Promise.race([
596
+ startupFailurePromise,
597
+ this.connection.initialize(initRequest).then((res) => {
598
+ if (timeoutHandle) {
599
+ clearTimeout(timeoutHandle);
600
+ timeoutHandle = null;
601
+ }
602
+ return res;
603
+ }),
604
+ new Promise((_, reject) => {
605
+ timeoutHandle = setTimeout(() => {
606
+ reject(new Error(`Initialize timeout after ${initTimeout}ms - ${this.transport.agentName} did not respond`));
607
+ }, initTimeout);
608
+ })
609
+ ]);
610
+ return result;
611
+ } finally {
612
+ if (timeoutHandle) clearTimeout(timeoutHandle);
613
+ }
614
+ },
615
+ {
616
+ operationName: "Initialize",
617
+ maxAttempts: RETRY_CONFIG.maxAttempts,
618
+ baseDelayMs: RETRY_CONFIG.baseDelayMs,
619
+ maxDelayMs: RETRY_CONFIG.maxDelayMs,
620
+ shouldRetry: (error) => !isNonRetryableStartupError(error),
621
+ log: this.log
622
+ }
623
+ );
624
+ this.log(`[ACP] Initialize completed`);
625
+ const mcpServers = this.options.mcpServers ? Object.entries(this.options.mcpServers).map(([name, config]) => {
626
+ if (config.type === "http" && config.url) {
627
+ return { name, url: config.url };
628
+ }
629
+ return {
630
+ name,
631
+ command: config.command || "",
632
+ args: config.args || [],
633
+ env: config.env ? Object.entries(config.env).map(([envName, envValue]) => ({ name: envName, value: envValue })) : []
634
+ };
635
+ }) : [];
636
+ const newSessionRequest = {
637
+ cwd: this.options.cwd,
638
+ mcpServers
639
+ };
640
+ this.log(`[ACP] Creating new session...`);
641
+ const sessionResponse = await withRetry(
642
+ async () => {
643
+ if (this.disposed) throw Object.assign(new Error("Backend disposed during startup retry"), { code: "DISPOSED" });
644
+ let timeoutHandle = null;
645
+ try {
646
+ const result = await Promise.race([
647
+ startupFailurePromise,
648
+ this.connection.newSession(newSessionRequest).then((res) => {
649
+ if (timeoutHandle) {
650
+ clearTimeout(timeoutHandle);
651
+ timeoutHandle = null;
652
+ }
653
+ return res;
654
+ }),
655
+ new Promise((_, reject) => {
656
+ timeoutHandle = setTimeout(() => {
657
+ reject(new Error(`New session timeout after ${initTimeout}ms - ${this.transport.agentName} did not respond`));
658
+ }, initTimeout);
659
+ })
660
+ ]);
661
+ return result;
662
+ } finally {
663
+ if (timeoutHandle) clearTimeout(timeoutHandle);
664
+ }
665
+ },
666
+ {
667
+ operationName: "NewSession",
668
+ maxAttempts: RETRY_CONFIG.maxAttempts,
669
+ baseDelayMs: RETRY_CONFIG.baseDelayMs,
670
+ maxDelayMs: RETRY_CONFIG.maxDelayMs,
671
+ shouldRetry: (error) => !isNonRetryableStartupError(error),
672
+ log: this.log
673
+ }
674
+ );
675
+ this.acpSessionId = sessionResponse.sessionId;
676
+ this.log(`[ACP] Session created: ${this.acpSessionId}`);
677
+ this.emitInitialSessionMetadata(sessionResponse);
678
+ this.emitIdleStatus();
679
+ if (initialPrompt) {
680
+ this.sendPrompt(sessionId, initialPrompt).catch((error) => {
681
+ this.log("[ACP] Error sending initial prompt:", error);
682
+ this.emit({ type: "status", status: "error", detail: String(error) });
683
+ });
684
+ }
685
+ return { sessionId };
686
+ } catch (error) {
687
+ this.log("[ACP] Error starting session:", error);
688
+ if (!startupStatusErrorEmitted) {
689
+ this.emit({
690
+ type: "status",
691
+ status: "error",
692
+ detail: error instanceof Error ? error.message : String(error)
693
+ });
694
+ }
695
+ throw error;
696
+ }
697
+ }
698
+ createHandlerContext() {
699
+ return {
700
+ transport: this.transport,
701
+ activeToolCalls: this.activeToolCalls,
702
+ toolCallStartTimes: this.toolCallStartTimes,
703
+ toolCallTimeouts: this.toolCallTimeouts,
704
+ toolCallIdToNameMap: this.toolCallIdToNameMap,
705
+ idleTimeout: this.idleTimeout,
706
+ toolCallCountSincePrompt: this.toolCallCountSincePrompt,
707
+ emit: (msg) => this.emit(msg),
708
+ emitIdleStatus: () => this.emitIdleStatus(),
709
+ clearIdleTimeout: () => {
710
+ if (this.idleTimeout) {
711
+ clearTimeout(this.idleTimeout);
712
+ this.idleTimeout = null;
713
+ }
714
+ },
715
+ setIdleTimeout: (callback, ms) => {
716
+ this.idleTimeout = setTimeout(() => {
717
+ callback();
718
+ this.idleTimeout = null;
719
+ }, ms);
720
+ },
721
+ log: this.log
722
+ };
723
+ }
724
+ emitInitialSessionMetadata(sessionResponse) {
725
+ if (Array.isArray(sessionResponse.configOptions)) {
726
+ this.emit({
727
+ type: "event",
728
+ name: "config_options_update",
729
+ payload: { configOptions: sessionResponse.configOptions }
730
+ });
731
+ }
732
+ if (sessionResponse.modes) {
733
+ this.emit({ type: "event", name: "modes_update", payload: sessionResponse.modes });
734
+ this.emit({ type: "event", name: "current_mode_update", payload: { currentModeId: sessionResponse.modes.currentModeId } });
735
+ }
736
+ if (sessionResponse.models) {
737
+ this.emit({ type: "event", name: "models_update", payload: sessionResponse.models });
738
+ }
739
+ }
740
+ handleSessionUpdate(params) {
741
+ const notification = params;
742
+ const update = notification.update;
743
+ if (!update) return;
744
+ const sessionUpdateType = update.sessionUpdate;
745
+ const updateType = sessionUpdateType;
746
+ this.log(`[ACP] sessionUpdate: ${sessionUpdateType}`);
747
+ const ctx = this.createHandlerContext();
748
+ if (sessionUpdateType === "agent_message_chunk") {
749
+ handleAgentMessageChunk(update, ctx);
750
+ return;
751
+ }
752
+ if (sessionUpdateType === "tool_call_update") {
753
+ const result = handleToolCallUpdate(update, ctx);
754
+ if (result.toolCallCountSincePrompt !== void 0) {
755
+ this.toolCallCountSincePrompt = result.toolCallCountSincePrompt;
756
+ }
757
+ return;
758
+ }
759
+ if (sessionUpdateType === "agent_thought_chunk") {
760
+ handleAgentThoughtChunk(update, ctx);
761
+ return;
762
+ }
763
+ if (sessionUpdateType === "tool_call") {
764
+ handleToolCall(update, ctx);
765
+ return;
766
+ }
767
+ if (sessionUpdateType === "available_commands_update") {
768
+ const commands = update.availableCommands;
769
+ if (Array.isArray(commands)) {
770
+ this.emit({ type: "event", name: "available_commands", payload: commands });
771
+ }
772
+ return;
773
+ }
774
+ if (updateType === "config_option_update" || updateType === "config_options_update") {
775
+ const configOptions = update.configOptions;
776
+ if (Array.isArray(configOptions)) {
777
+ this.emit({ type: "event", name: "config_options_update", payload: { configOptions } });
778
+ }
779
+ return;
780
+ }
781
+ if (updateType === "current_mode_update") {
782
+ const currentModeId = update.currentModeId;
783
+ if (typeof currentModeId === "string" && currentModeId.length > 0) {
784
+ this.emit({ type: "event", name: "current_mode_update", payload: { currentModeId } });
785
+ }
786
+ return;
787
+ }
788
+ handleLegacyMessageChunk(update, ctx);
789
+ handlePlanUpdate(update, ctx);
790
+ handleThinkingUpdate(update, ctx);
791
+ }
792
+ emitIdleStatus() {
793
+ const resolver = this.idleResolver;
794
+ this.idleResolver = null;
795
+ this.waitingForResponse = false;
796
+ this.emit({ type: "status", status: "idle" });
797
+ if (resolver) {
798
+ const newPromptInFlight = this.waitingForResponse;
799
+ resolver();
800
+ if (newPromptInFlight) {
801
+ this.waitingForResponse = true;
802
+ }
803
+ }
804
+ }
805
+ async sendPrompt(sessionId, prompt) {
806
+ const promptHasChangeTitle = this.options.hasChangeTitleInstruction?.(prompt) ?? false;
807
+ this.toolCallCountSincePrompt = 0;
808
+ this.recentPromptHadChangeTitle = promptHasChangeTitle;
809
+ if (this.disposed) throw new Error("Backend has been disposed");
810
+ if (!this.connection || !this.acpSessionId) throw new Error("Session not started");
811
+ this.emit({ type: "status", status: "running" });
812
+ this.waitingForResponse = true;
813
+ try {
814
+ this.log(`[ACP] Sending prompt (length: ${prompt.length})`);
815
+ const contentBlock = { type: "text", text: prompt };
816
+ const promptRequest = {
817
+ sessionId: this.acpSessionId,
818
+ prompt: [contentBlock]
819
+ };
820
+ await this.connection.prompt(promptRequest);
821
+ this.log("[ACP] Prompt request sent");
822
+ } catch (error) {
823
+ this.log("[ACP] Error sending prompt:", error);
824
+ this.waitingForResponse = false;
825
+ let errorDetail;
826
+ if (error instanceof Error) {
827
+ errorDetail = error.message;
828
+ } else if (typeof error === "object" && error !== null) {
829
+ const errObj = error;
830
+ errorDetail = typeof errObj.message === "string" ? errObj.message : String(error);
831
+ } else {
832
+ errorDetail = String(error);
833
+ }
834
+ this.emit({ type: "status", status: "error", detail: errorDetail });
835
+ throw error;
836
+ }
837
+ }
838
+ async waitForResponseComplete(timeoutMs = 12e4) {
839
+ if (!this.waitingForResponse) return;
840
+ return new Promise((resolve, reject) => {
841
+ const timeout = setTimeout(() => {
842
+ this.idleResolver = null;
843
+ this.waitingForResponse = false;
844
+ reject(new Error("Timeout waiting for response to complete"));
845
+ }, timeoutMs);
846
+ this.idleResolver = () => {
847
+ clearTimeout(timeout);
848
+ this.idleResolver = null;
849
+ this.waitingForResponse = false;
850
+ resolve();
851
+ };
852
+ });
853
+ }
854
+ async cancel(sessionId) {
855
+ if (!this.connection || !this.acpSessionId) return;
856
+ this.waitingForResponse = false;
857
+ if (this.idleResolver) {
858
+ this.idleResolver();
859
+ this.idleResolver = null;
860
+ }
861
+ try {
862
+ await this.connection.cancel({ sessionId: this.acpSessionId });
863
+ this.emit({ type: "status", status: "cancelled", detail: "Cancelled by user" });
864
+ } catch (error) {
865
+ this.log("[ACP] Error cancelling:", error);
866
+ }
867
+ }
868
+ async respondToPermission(requestId, approved) {
869
+ this.log(`[ACP] Permission response (UI only): ${requestId} = ${approved}`);
870
+ this.emit({ type: "permission-response", id: requestId, approved });
871
+ }
872
+ async dispose() {
873
+ if (this.disposed) return;
874
+ this.log("[ACP] Disposing backend");
875
+ this.disposed = true;
876
+ if (this.connection && this.acpSessionId) {
877
+ try {
878
+ await Promise.race([
879
+ this.connection.cancel({ sessionId: this.acpSessionId }),
880
+ new Promise((resolve) => setTimeout(resolve, 2e3))
881
+ ]);
882
+ } catch {
883
+ }
884
+ }
885
+ if (this.process) {
886
+ try {
887
+ this.process.kill("SIGTERM");
888
+ } catch {
889
+ }
890
+ await new Promise((resolve) => {
891
+ const timeout = setTimeout(() => {
892
+ try {
893
+ if (this.process) this.process.kill("SIGKILL");
894
+ } catch {
895
+ }
896
+ resolve();
897
+ }, 1e3);
898
+ const done = () => {
899
+ clearTimeout(timeout);
900
+ resolve();
901
+ };
902
+ this.process?.once("exit", done);
903
+ this.process?.once("close", done);
904
+ });
905
+ this.process = null;
906
+ }
907
+ if (this.idleTimeout) {
908
+ clearTimeout(this.idleTimeout);
909
+ this.idleTimeout = null;
910
+ }
911
+ this.listeners = [];
912
+ this.connection = null;
913
+ this.acpSessionId = null;
914
+ this.activeToolCalls.clear();
915
+ for (const timeout of this.toolCallTimeouts.values()) clearTimeout(timeout);
916
+ this.toolCallTimeouts.clear();
917
+ this.toolCallStartTimes.clear();
918
+ this.toolCallIdToNameMap.clear();
919
+ }
920
+ }
921
+
922
+ export { AcpBackend };