svamp-cli 0.1.21 → 0.1.22
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.
- package/dist/cli.mjs +8 -8
- package/dist/commands-CKpC8R9T.mjs +481 -0
- package/dist/index.mjs +1 -1
- package/dist/package-JqEt5Ib4.mjs +57 -0
- package/dist/run-DwK3dfHd.mjs +3875 -0
- package/package.json +2 -2
|
@@ -0,0 +1,3875 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { existsSync as existsSync$1, readFileSync as readFileSync$1, writeFileSync as writeFileSync$1, mkdirSync as mkdirSync$1, copyFileSync, unlinkSync, rmdirSync } from 'fs';
|
|
4
|
+
import { dirname, join as join$1, resolve, basename } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { spawn as spawn$1 } from 'child_process';
|
|
7
|
+
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { existsSync, readFileSync, mkdirSync, appendFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
13
|
+
|
|
14
|
+
let connectToServerFn = null;
|
|
15
|
+
async function getConnectToServer() {
|
|
16
|
+
if (!connectToServerFn) {
|
|
17
|
+
const mod = await import('hypha-rpc');
|
|
18
|
+
connectToServerFn = mod.connectToServer || mod.default && mod.default.connectToServer || mod.hyphaWebsocketClient && mod.hyphaWebsocketClient.connectToServer;
|
|
19
|
+
if (!connectToServerFn) {
|
|
20
|
+
throw new Error("Could not find connectToServer in hypha-rpc module");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return connectToServerFn;
|
|
24
|
+
}
|
|
25
|
+
async function connectToHypha(config) {
|
|
26
|
+
const connectToServer = await getConnectToServer();
|
|
27
|
+
const workspace = config.token ? parseWorkspaceFromToken(config.token) : void 0;
|
|
28
|
+
const server = await connectToServer({
|
|
29
|
+
server_url: config.serverUrl,
|
|
30
|
+
token: config.token,
|
|
31
|
+
client_id: config.clientId,
|
|
32
|
+
name: config.name || "svamp-cli",
|
|
33
|
+
workspace,
|
|
34
|
+
...config.transport ? { transport: config.transport } : {}
|
|
35
|
+
});
|
|
36
|
+
return server;
|
|
37
|
+
}
|
|
38
|
+
function parseWorkspaceFromToken(token) {
|
|
39
|
+
try {
|
|
40
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
41
|
+
const scope = payload.scope || "";
|
|
42
|
+
const match = scope.match(/wid:([^\s]+)/);
|
|
43
|
+
return match ? match[1] : void 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function getHyphaServerUrl() {
|
|
49
|
+
return process.env.HYPHA_SERVER_URL || "http://localhost:9527";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function registerMachineService(server, machineId, metadata, daemonState, handlers) {
|
|
53
|
+
let currentMetadata = { ...metadata };
|
|
54
|
+
let currentDaemonState = { ...daemonState };
|
|
55
|
+
let metadataVersion = 1;
|
|
56
|
+
let daemonStateVersion = 1;
|
|
57
|
+
const listeners = [];
|
|
58
|
+
const removeListener = (listener, reason) => {
|
|
59
|
+
const idx = listeners.indexOf(listener);
|
|
60
|
+
if (idx >= 0) {
|
|
61
|
+
listeners.splice(idx, 1);
|
|
62
|
+
console.log(`[HYPHA MACHINE] Listener removed (${reason}), remaining: ${listeners.length}`);
|
|
63
|
+
const rintfId = listener._rintf_service_id;
|
|
64
|
+
if (rintfId) {
|
|
65
|
+
server.unregisterService(rintfId).catch(() => {
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const notifyListeners = (update) => {
|
|
71
|
+
for (let i = listeners.length - 1; i >= 0; i--) {
|
|
72
|
+
try {
|
|
73
|
+
const result = listeners[i].onUpdate(update);
|
|
74
|
+
if (result && typeof result.catch === "function") {
|
|
75
|
+
const listener = listeners[i];
|
|
76
|
+
result.catch((err) => {
|
|
77
|
+
console.error(`[HYPHA MACHINE] Async listener error:`, err);
|
|
78
|
+
removeListener(listener, "async error");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(`[HYPHA MACHINE] Listener error:`, err);
|
|
83
|
+
removeListener(listeners[i], "sync error");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const serviceInfo = await server.registerService(
|
|
88
|
+
{
|
|
89
|
+
id: "default",
|
|
90
|
+
name: `Svamp Machine (${metadata.displayName || machineId})`,
|
|
91
|
+
type: "svamp-machine",
|
|
92
|
+
config: { visibility: "public" },
|
|
93
|
+
// Machine info
|
|
94
|
+
getMachineInfo: async () => ({
|
|
95
|
+
machineId,
|
|
96
|
+
metadata: currentMetadata,
|
|
97
|
+
metadataVersion,
|
|
98
|
+
daemonState: currentDaemonState,
|
|
99
|
+
daemonStateVersion
|
|
100
|
+
}),
|
|
101
|
+
// Heartbeat
|
|
102
|
+
heartbeat: async () => ({
|
|
103
|
+
time: Date.now(),
|
|
104
|
+
status: currentDaemonState.status,
|
|
105
|
+
machineId
|
|
106
|
+
}),
|
|
107
|
+
// List active sessions on this machine
|
|
108
|
+
listSessions: async () => {
|
|
109
|
+
return handlers.getTrackedSessions();
|
|
110
|
+
},
|
|
111
|
+
// Spawn a new session
|
|
112
|
+
spawnSession: async (options) => {
|
|
113
|
+
const result = await handlers.spawnSession({
|
|
114
|
+
...options,
|
|
115
|
+
machineId
|
|
116
|
+
});
|
|
117
|
+
if (result.type === "success" && result.sessionId) {
|
|
118
|
+
notifyListeners({
|
|
119
|
+
type: "new-session",
|
|
120
|
+
sessionId: result.sessionId,
|
|
121
|
+
machineId
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
},
|
|
126
|
+
// Stop a session
|
|
127
|
+
stopSession: async (sessionId) => {
|
|
128
|
+
const result = handlers.stopSession(sessionId);
|
|
129
|
+
notifyListeners({
|
|
130
|
+
type: "session-stopped",
|
|
131
|
+
sessionId,
|
|
132
|
+
machineId
|
|
133
|
+
});
|
|
134
|
+
return result;
|
|
135
|
+
},
|
|
136
|
+
// Stop the daemon
|
|
137
|
+
stopDaemon: async () => {
|
|
138
|
+
handlers.requestShutdown();
|
|
139
|
+
return { success: true };
|
|
140
|
+
},
|
|
141
|
+
// Metadata management (with optimistic concurrency)
|
|
142
|
+
getMetadata: async () => ({
|
|
143
|
+
metadata: currentMetadata,
|
|
144
|
+
version: metadataVersion
|
|
145
|
+
}),
|
|
146
|
+
updateMetadata: async (newMetadata, expectedVersion) => {
|
|
147
|
+
if (expectedVersion !== metadataVersion) {
|
|
148
|
+
return {
|
|
149
|
+
result: "version-mismatch",
|
|
150
|
+
version: metadataVersion,
|
|
151
|
+
metadata: currentMetadata
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
currentMetadata = newMetadata;
|
|
155
|
+
metadataVersion++;
|
|
156
|
+
notifyListeners({
|
|
157
|
+
type: "update-machine",
|
|
158
|
+
machineId,
|
|
159
|
+
metadata: { value: currentMetadata, version: metadataVersion }
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
result: "success",
|
|
163
|
+
version: metadataVersion,
|
|
164
|
+
metadata: currentMetadata
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
// Daemon state management
|
|
168
|
+
getDaemonState: async () => ({
|
|
169
|
+
daemonState: currentDaemonState,
|
|
170
|
+
version: daemonStateVersion
|
|
171
|
+
}),
|
|
172
|
+
updateDaemonState: async (newState, expectedVersion) => {
|
|
173
|
+
if (expectedVersion !== daemonStateVersion) {
|
|
174
|
+
return {
|
|
175
|
+
result: "version-mismatch",
|
|
176
|
+
version: daemonStateVersion,
|
|
177
|
+
daemonState: currentDaemonState
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
currentDaemonState = newState;
|
|
181
|
+
daemonStateVersion++;
|
|
182
|
+
notifyListeners({
|
|
183
|
+
type: "update-machine",
|
|
184
|
+
machineId,
|
|
185
|
+
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
result: "success",
|
|
189
|
+
version: daemonStateVersion,
|
|
190
|
+
daemonState: currentDaemonState
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
// Register a listener for real-time updates (app calls this with _rintf callback)
|
|
194
|
+
registerListener: async (callback) => {
|
|
195
|
+
listeners.push(callback);
|
|
196
|
+
console.log(`[HYPHA MACHINE] Listener registered (total: ${listeners.length})`);
|
|
197
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
198
|
+
},
|
|
199
|
+
// Shell access
|
|
200
|
+
bash: async (command, cwd) => {
|
|
201
|
+
const { exec } = await import('child_process');
|
|
202
|
+
const { homedir } = await import('os');
|
|
203
|
+
return new Promise((resolve) => {
|
|
204
|
+
exec(command, { cwd: cwd || homedir(), timeout: 3e4, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
205
|
+
if (err) {
|
|
206
|
+
resolve({ success: false, stdout: stdout || "", stderr: stderr || err.message, exitCode: err.code ?? 1 });
|
|
207
|
+
} else {
|
|
208
|
+
resolve({ success: true, stdout, stderr: stderr || "", exitCode: 0 });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
// WISE voice — create ephemeral token for OpenAI Realtime API
|
|
214
|
+
wiseCreateEphemeralToken: async (params) => {
|
|
215
|
+
const apiKey = params.apiKey || process.env.OPENAI_API_KEY;
|
|
216
|
+
if (!apiKey) {
|
|
217
|
+
return { success: false, error: "No OpenAI API key found. Set OPENAI_API_KEY or pass apiKey." };
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const response = await fetch("https://api.openai.com/v1/realtime/client_secrets", {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: {
|
|
223
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
224
|
+
"Content-Type": "application/json"
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
session: {
|
|
228
|
+
type: "realtime",
|
|
229
|
+
model: params.model || "gpt-realtime-mini"
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
return { success: false, error: `OpenAI API error: ${response.status}` };
|
|
235
|
+
}
|
|
236
|
+
const result = await response.json();
|
|
237
|
+
return { success: true, clientSecret: result.value };
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to create token" };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{ overwrite: true }
|
|
244
|
+
);
|
|
245
|
+
console.log(`[HYPHA MACHINE] Machine service registered: ${serviceInfo.id}`);
|
|
246
|
+
return {
|
|
247
|
+
serviceInfo,
|
|
248
|
+
updateMetadata: (newMetadata) => {
|
|
249
|
+
currentMetadata = newMetadata;
|
|
250
|
+
metadataVersion++;
|
|
251
|
+
notifyListeners({
|
|
252
|
+
type: "update-machine",
|
|
253
|
+
machineId,
|
|
254
|
+
metadata: { value: currentMetadata, version: metadataVersion }
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
updateDaemonState: (newState) => {
|
|
258
|
+
currentDaemonState = newState;
|
|
259
|
+
daemonStateVersion++;
|
|
260
|
+
notifyListeners({
|
|
261
|
+
type: "update-machine",
|
|
262
|
+
machineId,
|
|
263
|
+
daemonState: { value: currentDaemonState, version: daemonStateVersion }
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
disconnect: async () => {
|
|
267
|
+
await server.unregisterService(serviceInfo.id);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function loadMessages(messagesDir, sessionId) {
|
|
273
|
+
const filePath = join(messagesDir, "messages.jsonl");
|
|
274
|
+
if (!existsSync(filePath)) return [];
|
|
275
|
+
try {
|
|
276
|
+
const lines = readFileSync(filePath, "utf-8").split("\n").filter((l) => l.trim());
|
|
277
|
+
const messages = [];
|
|
278
|
+
for (const line of lines) {
|
|
279
|
+
try {
|
|
280
|
+
messages.push(JSON.parse(line));
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return messages.slice(-5e3);
|
|
285
|
+
} catch {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function appendMessage(messagesDir, sessionId, msg) {
|
|
290
|
+
const filePath = join(messagesDir, "messages.jsonl");
|
|
291
|
+
if (!existsSync(messagesDir)) {
|
|
292
|
+
mkdirSync(messagesDir, { recursive: true });
|
|
293
|
+
}
|
|
294
|
+
appendFileSync(filePath, JSON.stringify(msg) + "\n");
|
|
295
|
+
}
|
|
296
|
+
async function registerSessionService(server, sessionId, initialMetadata, initialAgentState, callbacks, options) {
|
|
297
|
+
const messages = options?.messagesDir ? loadMessages(options.messagesDir) : [];
|
|
298
|
+
let nextSeq = messages.length > 0 ? messages[messages.length - 1].seq + 1 : 1;
|
|
299
|
+
let metadata = { ...initialMetadata };
|
|
300
|
+
let metadataVersion = 1;
|
|
301
|
+
let agentState = initialAgentState ? { ...initialAgentState } : null;
|
|
302
|
+
let agentStateVersion = 1;
|
|
303
|
+
let lastActivity = {
|
|
304
|
+
active: false,
|
|
305
|
+
thinking: false,
|
|
306
|
+
mode: "remote",
|
|
307
|
+
time: Date.now()
|
|
308
|
+
};
|
|
309
|
+
const listeners = [];
|
|
310
|
+
const removeListener = (listener, reason) => {
|
|
311
|
+
const idx = listeners.indexOf(listener);
|
|
312
|
+
if (idx >= 0) {
|
|
313
|
+
listeners.splice(idx, 1);
|
|
314
|
+
console.log(`[HYPHA SESSION ${sessionId}] Listener removed (${reason}), remaining: ${listeners.length}`);
|
|
315
|
+
const rintfId = listener._rintf_service_id;
|
|
316
|
+
if (rintfId) {
|
|
317
|
+
server.unregisterService(rintfId).catch(() => {
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const notifyListeners = (update) => {
|
|
323
|
+
for (let i = listeners.length - 1; i >= 0; i--) {
|
|
324
|
+
try {
|
|
325
|
+
const result = listeners[i].onUpdate(update);
|
|
326
|
+
if (result && typeof result.catch === "function") {
|
|
327
|
+
const listener = listeners[i];
|
|
328
|
+
result.catch((err) => {
|
|
329
|
+
console.error(`[HYPHA SESSION ${sessionId}] Async listener error:`, err);
|
|
330
|
+
removeListener(listener, "async error");
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
console.error(`[HYPHA SESSION ${sessionId}] Listener error:`, err);
|
|
335
|
+
removeListener(listeners[i], "sync error");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const pushMessage = (content, role = "agent") => {
|
|
340
|
+
let wrappedContent;
|
|
341
|
+
if (role === "agent") {
|
|
342
|
+
const data = { ...content };
|
|
343
|
+
if ((data.type === "assistant" || data.type === "user") && !data.uuid) {
|
|
344
|
+
data.uuid = randomUUID();
|
|
345
|
+
}
|
|
346
|
+
wrappedContent = { role: "agent", content: { type: "output", data } };
|
|
347
|
+
} else if (role === "session") {
|
|
348
|
+
wrappedContent = { role: "session", content: { type: "session", data: content } };
|
|
349
|
+
} else {
|
|
350
|
+
const text = typeof content === "string" ? content : content?.text || content?.content || JSON.stringify(content);
|
|
351
|
+
wrappedContent = { role: "user", content: { type: "text", text } };
|
|
352
|
+
}
|
|
353
|
+
const msg = {
|
|
354
|
+
id: randomUUID(),
|
|
355
|
+
seq: nextSeq++,
|
|
356
|
+
content: wrappedContent,
|
|
357
|
+
localId: null,
|
|
358
|
+
createdAt: Date.now(),
|
|
359
|
+
updatedAt: Date.now()
|
|
360
|
+
};
|
|
361
|
+
messages.push(msg);
|
|
362
|
+
if (options?.messagesDir) {
|
|
363
|
+
appendMessage(options.messagesDir, sessionId, msg);
|
|
364
|
+
}
|
|
365
|
+
notifyListeners({
|
|
366
|
+
type: "new-message",
|
|
367
|
+
sessionId,
|
|
368
|
+
message: msg
|
|
369
|
+
});
|
|
370
|
+
return msg;
|
|
371
|
+
};
|
|
372
|
+
const serviceInfo = await server.registerService(
|
|
373
|
+
{
|
|
374
|
+
id: `svamp-session-${sessionId}`,
|
|
375
|
+
name: `Svamp Session ${sessionId.slice(0, 8)}`,
|
|
376
|
+
type: "svamp-session",
|
|
377
|
+
config: { visibility: "public" },
|
|
378
|
+
// ── Messages ──
|
|
379
|
+
getMessages: async (afterSeq, limit) => {
|
|
380
|
+
const after = afterSeq ?? 0;
|
|
381
|
+
const lim = Math.min(limit ?? 100, 500);
|
|
382
|
+
const filtered = messages.filter((m) => m.seq > after);
|
|
383
|
+
const page = filtered.slice(0, lim);
|
|
384
|
+
return {
|
|
385
|
+
messages: page,
|
|
386
|
+
hasMore: filtered.length > lim
|
|
387
|
+
};
|
|
388
|
+
},
|
|
389
|
+
sendMessage: async (content, localId, meta) => {
|
|
390
|
+
if (localId) {
|
|
391
|
+
const existing = messages.find((m) => m.localId === localId);
|
|
392
|
+
if (existing) {
|
|
393
|
+
return { id: existing.id, seq: existing.seq, localId: existing.localId };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
let parsed = content;
|
|
397
|
+
if (typeof parsed === "string") {
|
|
398
|
+
try {
|
|
399
|
+
parsed = JSON.parse(parsed);
|
|
400
|
+
} catch {
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (parsed && typeof parsed.content === "string" && !parsed.role) {
|
|
404
|
+
try {
|
|
405
|
+
const inner = JSON.parse(parsed.content);
|
|
406
|
+
if (inner && typeof inner === "object") parsed = inner;
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const wrappedContent = parsed && parsed.role === "user" ? { role: "user", content: parsed.content } : { role: "user", content: { type: "text", text: typeof parsed === "string" ? parsed : JSON.stringify(parsed) } };
|
|
411
|
+
const msg = {
|
|
412
|
+
id: randomUUID(),
|
|
413
|
+
seq: nextSeq++,
|
|
414
|
+
content: wrappedContent,
|
|
415
|
+
localId: localId || randomUUID(),
|
|
416
|
+
createdAt: Date.now(),
|
|
417
|
+
updatedAt: Date.now()
|
|
418
|
+
};
|
|
419
|
+
messages.push(msg);
|
|
420
|
+
if (options?.messagesDir) {
|
|
421
|
+
appendMessage(options.messagesDir, sessionId, msg);
|
|
422
|
+
}
|
|
423
|
+
notifyListeners({
|
|
424
|
+
type: "new-message",
|
|
425
|
+
sessionId,
|
|
426
|
+
message: msg
|
|
427
|
+
});
|
|
428
|
+
callbacks.onUserMessage(content, meta);
|
|
429
|
+
return { id: msg.id, seq: msg.seq, localId: msg.localId };
|
|
430
|
+
},
|
|
431
|
+
// ── Metadata ──
|
|
432
|
+
getMetadata: async () => ({
|
|
433
|
+
metadata,
|
|
434
|
+
version: metadataVersion
|
|
435
|
+
}),
|
|
436
|
+
updateMetadata: async (newMetadata, expectedVersion) => {
|
|
437
|
+
if (expectedVersion !== void 0 && expectedVersion !== metadataVersion) {
|
|
438
|
+
return {
|
|
439
|
+
result: "version-mismatch",
|
|
440
|
+
version: metadataVersion,
|
|
441
|
+
metadata
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
metadata = newMetadata;
|
|
445
|
+
metadataVersion++;
|
|
446
|
+
notifyListeners({
|
|
447
|
+
type: "update-session",
|
|
448
|
+
sessionId,
|
|
449
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
450
|
+
});
|
|
451
|
+
return {
|
|
452
|
+
result: "success",
|
|
453
|
+
version: metadataVersion,
|
|
454
|
+
metadata
|
|
455
|
+
};
|
|
456
|
+
},
|
|
457
|
+
// ── Agent State ──
|
|
458
|
+
getAgentState: async () => ({
|
|
459
|
+
agentState,
|
|
460
|
+
version: agentStateVersion
|
|
461
|
+
}),
|
|
462
|
+
updateAgentState: async (newState, expectedVersion) => {
|
|
463
|
+
if (expectedVersion !== void 0 && expectedVersion !== agentStateVersion) {
|
|
464
|
+
return {
|
|
465
|
+
result: "version-mismatch",
|
|
466
|
+
version: agentStateVersion,
|
|
467
|
+
agentState
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
agentState = newState;
|
|
471
|
+
agentStateVersion++;
|
|
472
|
+
notifyListeners({
|
|
473
|
+
type: "update-session",
|
|
474
|
+
sessionId,
|
|
475
|
+
agentState: { value: agentState, version: agentStateVersion }
|
|
476
|
+
});
|
|
477
|
+
return {
|
|
478
|
+
result: "success",
|
|
479
|
+
version: agentStateVersion,
|
|
480
|
+
agentState
|
|
481
|
+
};
|
|
482
|
+
},
|
|
483
|
+
// ── Session Control RPCs ──
|
|
484
|
+
abort: async () => {
|
|
485
|
+
callbacks.onAbort();
|
|
486
|
+
return { success: true };
|
|
487
|
+
},
|
|
488
|
+
permissionResponse: async (params) => {
|
|
489
|
+
callbacks.onPermissionResponse(params);
|
|
490
|
+
return { success: true };
|
|
491
|
+
},
|
|
492
|
+
switchMode: async (mode) => {
|
|
493
|
+
callbacks.onSwitchMode(mode);
|
|
494
|
+
return { success: true };
|
|
495
|
+
},
|
|
496
|
+
restartClaude: async () => {
|
|
497
|
+
return await callbacks.onRestartClaude();
|
|
498
|
+
},
|
|
499
|
+
killSession: async () => {
|
|
500
|
+
callbacks.onKillSession();
|
|
501
|
+
return { success: true };
|
|
502
|
+
},
|
|
503
|
+
// ── Activity ──
|
|
504
|
+
keepAlive: async (thinking, mode) => {
|
|
505
|
+
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
506
|
+
notifyListeners({
|
|
507
|
+
type: "activity",
|
|
508
|
+
sessionId,
|
|
509
|
+
...lastActivity
|
|
510
|
+
});
|
|
511
|
+
},
|
|
512
|
+
sessionEnd: async () => {
|
|
513
|
+
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
514
|
+
notifyListeners({
|
|
515
|
+
type: "activity",
|
|
516
|
+
sessionId,
|
|
517
|
+
...lastActivity
|
|
518
|
+
});
|
|
519
|
+
},
|
|
520
|
+
// ── Activity State Query ──
|
|
521
|
+
getActivityState: async () => {
|
|
522
|
+
return { ...lastActivity, sessionId };
|
|
523
|
+
},
|
|
524
|
+
// ── File Operations (optional) ──
|
|
525
|
+
readFile: async (path) => {
|
|
526
|
+
if (!callbacks.onReadFile) throw new Error("readFile not supported");
|
|
527
|
+
return await callbacks.onReadFile(path);
|
|
528
|
+
},
|
|
529
|
+
writeFile: async (path, content) => {
|
|
530
|
+
if (!callbacks.onWriteFile) throw new Error("writeFile not supported");
|
|
531
|
+
await callbacks.onWriteFile(path, content);
|
|
532
|
+
return { success: true };
|
|
533
|
+
},
|
|
534
|
+
listDirectory: async (path) => {
|
|
535
|
+
if (!callbacks.onListDirectory) throw new Error("listDirectory not supported");
|
|
536
|
+
return await callbacks.onListDirectory(path);
|
|
537
|
+
},
|
|
538
|
+
bash: async (command, cwd) => {
|
|
539
|
+
if (!callbacks.onBash) throw new Error("bash not supported");
|
|
540
|
+
return await callbacks.onBash(command, cwd);
|
|
541
|
+
},
|
|
542
|
+
ripgrep: async (args, cwd) => {
|
|
543
|
+
if (!callbacks.onRipgrep) throw new Error("ripgrep not supported");
|
|
544
|
+
try {
|
|
545
|
+
const stdout = await callbacks.onRipgrep(args, cwd);
|
|
546
|
+
return { success: true, stdout, stderr: "", exitCode: 0 };
|
|
547
|
+
} catch (err) {
|
|
548
|
+
return { success: false, stdout: "", stderr: err.message || "", exitCode: 1, error: err.message };
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
getDirectoryTree: async (path, maxDepth) => {
|
|
552
|
+
if (!callbacks.onGetDirectoryTree) throw new Error("getDirectoryTree not supported");
|
|
553
|
+
return await callbacks.onGetDirectoryTree(path, maxDepth ?? 3);
|
|
554
|
+
},
|
|
555
|
+
// ── Listener Registration ──
|
|
556
|
+
registerListener: async (callback) => {
|
|
557
|
+
listeners.push(callback);
|
|
558
|
+
console.log(`[HYPHA SESSION ${sessionId}] Listener registered (total: ${listeners.length}), replaying ${messages.length} messages`);
|
|
559
|
+
for (const msg of messages) {
|
|
560
|
+
try {
|
|
561
|
+
const result = callback.onUpdate({
|
|
562
|
+
type: "new-message",
|
|
563
|
+
sessionId,
|
|
564
|
+
message: msg
|
|
565
|
+
});
|
|
566
|
+
if (result && typeof result.catch === "function") {
|
|
567
|
+
result.catch((err) => {
|
|
568
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error:`, err);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
} catch (err) {
|
|
572
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error:`, err);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
const result = callback.onUpdate({
|
|
577
|
+
type: "update-session",
|
|
578
|
+
sessionId,
|
|
579
|
+
metadata: { value: metadata, version: metadataVersion },
|
|
580
|
+
agentState: { value: agentState, version: agentStateVersion }
|
|
581
|
+
});
|
|
582
|
+
if (result && typeof result.catch === "function") {
|
|
583
|
+
result.catch(() => {
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
} catch {
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const result = callback.onUpdate({
|
|
590
|
+
type: "activity",
|
|
591
|
+
sessionId,
|
|
592
|
+
...lastActivity
|
|
593
|
+
});
|
|
594
|
+
if (result && typeof result.catch === "function") {
|
|
595
|
+
result.catch(() => {
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
} catch {
|
|
599
|
+
}
|
|
600
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
{ overwrite: true }
|
|
604
|
+
);
|
|
605
|
+
console.log(`[HYPHA SESSION] Session service registered: ${serviceInfo.id}`);
|
|
606
|
+
return {
|
|
607
|
+
serviceInfo,
|
|
608
|
+
pushMessage,
|
|
609
|
+
get _agentState() {
|
|
610
|
+
return agentState;
|
|
611
|
+
},
|
|
612
|
+
updateMetadata: (newMetadata) => {
|
|
613
|
+
metadata = newMetadata;
|
|
614
|
+
metadataVersion++;
|
|
615
|
+
notifyListeners({
|
|
616
|
+
type: "update-session",
|
|
617
|
+
sessionId,
|
|
618
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
619
|
+
});
|
|
620
|
+
},
|
|
621
|
+
updateAgentState: (newAgentState) => {
|
|
622
|
+
agentState = newAgentState;
|
|
623
|
+
agentStateVersion++;
|
|
624
|
+
notifyListeners({
|
|
625
|
+
type: "update-session",
|
|
626
|
+
sessionId,
|
|
627
|
+
agentState: { value: agentState, version: agentStateVersion }
|
|
628
|
+
});
|
|
629
|
+
},
|
|
630
|
+
sendKeepAlive: (thinking, mode) => {
|
|
631
|
+
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
632
|
+
notifyListeners({
|
|
633
|
+
type: "activity",
|
|
634
|
+
sessionId,
|
|
635
|
+
...lastActivity
|
|
636
|
+
});
|
|
637
|
+
},
|
|
638
|
+
sendSessionEnd: () => {
|
|
639
|
+
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
640
|
+
notifyListeners({
|
|
641
|
+
type: "activity",
|
|
642
|
+
sessionId,
|
|
643
|
+
...lastActivity
|
|
644
|
+
});
|
|
645
|
+
},
|
|
646
|
+
disconnect: async () => {
|
|
647
|
+
await server.unregisterService(serviceInfo.id);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function registerDebugService(server, machineId, deps) {
|
|
653
|
+
const serviceInfo = await server.registerService(
|
|
654
|
+
{
|
|
655
|
+
id: `svamp-debug-${machineId}`,
|
|
656
|
+
name: `Svamp Debug ${machineId.slice(0, 8)}`,
|
|
657
|
+
type: "svamp-debug",
|
|
658
|
+
config: { visibility: "public" },
|
|
659
|
+
healthCheck: async () => ({
|
|
660
|
+
ok: true,
|
|
661
|
+
machineId,
|
|
662
|
+
uptime: process.uptime(),
|
|
663
|
+
timestamp: Date.now()
|
|
664
|
+
}),
|
|
665
|
+
getSessionStates: async () => {
|
|
666
|
+
const sessions = deps.getTrackedSessions();
|
|
667
|
+
const states = [];
|
|
668
|
+
for (const s of sessions) {
|
|
669
|
+
let activityState = null;
|
|
670
|
+
try {
|
|
671
|
+
const svc = deps.getSessionService(s.sessionId);
|
|
672
|
+
if (svc) {
|
|
673
|
+
activityState = await svc.getActivityState?.();
|
|
674
|
+
}
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
states.push({
|
|
678
|
+
sessionId: s.sessionId,
|
|
679
|
+
active: s.active,
|
|
680
|
+
pid: s.pid,
|
|
681
|
+
startedBy: s.startedBy,
|
|
682
|
+
directory: s.directory,
|
|
683
|
+
activityState
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return states;
|
|
687
|
+
},
|
|
688
|
+
getMachineInfo: async () => ({
|
|
689
|
+
machineId,
|
|
690
|
+
hostname: (await import('os')).hostname(),
|
|
691
|
+
platform: (await import('os')).platform(),
|
|
692
|
+
uptime: process.uptime(),
|
|
693
|
+
sessions: deps.getTrackedSessions().length
|
|
694
|
+
}),
|
|
695
|
+
// ── Artifact Sync RPCs ──
|
|
696
|
+
getArtifactSyncStatus: async () => {
|
|
697
|
+
const sync = deps.getArtifactSync?.();
|
|
698
|
+
if (!sync) return { error: "Artifact sync not available" };
|
|
699
|
+
return sync.getStatus();
|
|
700
|
+
},
|
|
701
|
+
forceSyncAll: async () => {
|
|
702
|
+
const sync = deps.getArtifactSync?.();
|
|
703
|
+
const sessionsDir = deps.getSessionsDir?.();
|
|
704
|
+
if (!sync || !sessionsDir) return { error: "Artifact sync not available" };
|
|
705
|
+
await sync.syncAll(sessionsDir, machineId);
|
|
706
|
+
return { success: true, status: sync.getStatus() };
|
|
707
|
+
},
|
|
708
|
+
listRemoteSessions: async () => {
|
|
709
|
+
const sync = deps.getArtifactSync?.();
|
|
710
|
+
if (!sync) return [];
|
|
711
|
+
return await sync.listRemoteSessions();
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
{ overwrite: true }
|
|
715
|
+
);
|
|
716
|
+
return {
|
|
717
|
+
disconnect: async () => {
|
|
718
|
+
await server.unregisterService(serviceInfo.id);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const COLLECTION_ALIAS = "svamp-agent-sessions";
|
|
724
|
+
class SessionArtifactSync {
|
|
725
|
+
server;
|
|
726
|
+
artifactManager = null;
|
|
727
|
+
collectionId = null;
|
|
728
|
+
initialized = false;
|
|
729
|
+
syncing = false;
|
|
730
|
+
lastSyncAt = null;
|
|
731
|
+
syncedSessions = /* @__PURE__ */ new Set();
|
|
732
|
+
error = null;
|
|
733
|
+
syncTimers = /* @__PURE__ */ new Map();
|
|
734
|
+
log;
|
|
735
|
+
constructor(server, log) {
|
|
736
|
+
this.server = server;
|
|
737
|
+
this.log = log;
|
|
738
|
+
}
|
|
739
|
+
async init() {
|
|
740
|
+
try {
|
|
741
|
+
this.artifactManager = await this.server.getService("public/artifact-manager");
|
|
742
|
+
if (!this.artifactManager) {
|
|
743
|
+
this.log("[ARTIFACT SYNC] Artifact manager service not available");
|
|
744
|
+
this.error = "Artifact manager not available";
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
await this.ensureCollection();
|
|
748
|
+
this.initialized = true;
|
|
749
|
+
this.log("[ARTIFACT SYNC] Initialized successfully");
|
|
750
|
+
} catch (err) {
|
|
751
|
+
this.error = `Init failed: ${err.message}`;
|
|
752
|
+
this.log(`[ARTIFACT SYNC] Init failed: ${err.message}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
async ensureCollection() {
|
|
756
|
+
try {
|
|
757
|
+
const existing = await this.artifactManager.read({
|
|
758
|
+
artifact_id: COLLECTION_ALIAS,
|
|
759
|
+
_rkwargs: true
|
|
760
|
+
});
|
|
761
|
+
this.collectionId = existing.id;
|
|
762
|
+
this.log(`[ARTIFACT SYNC] Found existing collection: ${this.collectionId}`);
|
|
763
|
+
} catch {
|
|
764
|
+
const collection = await this.artifactManager.create({
|
|
765
|
+
alias: COLLECTION_ALIAS,
|
|
766
|
+
type: "collection",
|
|
767
|
+
manifest: {
|
|
768
|
+
name: "Svamp Agent Sessions",
|
|
769
|
+
description: "Cloud-persisted session data for cross-machine continuity"
|
|
770
|
+
},
|
|
771
|
+
_rkwargs: true
|
|
772
|
+
});
|
|
773
|
+
this.collectionId = collection.id;
|
|
774
|
+
await this.artifactManager.commit({
|
|
775
|
+
artifact_id: this.collectionId,
|
|
776
|
+
_rkwargs: true
|
|
777
|
+
});
|
|
778
|
+
this.log(`[ARTIFACT SYNC] Created new collection: ${this.collectionId}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Upload a file to an artifact using the presigned URL pattern:
|
|
783
|
+
* 1. put_file() returns a presigned upload URL
|
|
784
|
+
* 2. HTTP PUT the content to that URL
|
|
785
|
+
*/
|
|
786
|
+
async uploadFile(artifactId, filePath, content) {
|
|
787
|
+
const putUrl = await this.artifactManager.put_file({
|
|
788
|
+
artifact_id: artifactId,
|
|
789
|
+
file_path: filePath,
|
|
790
|
+
_rkwargs: true
|
|
791
|
+
});
|
|
792
|
+
if (!putUrl || typeof putUrl !== "string") {
|
|
793
|
+
throw new Error(`put_file returned invalid URL for ${filePath}: ${putUrl}`);
|
|
794
|
+
}
|
|
795
|
+
const resp = await fetch(putUrl, {
|
|
796
|
+
method: "PUT",
|
|
797
|
+
body: content,
|
|
798
|
+
headers: { "Content-Type": "application/octet-stream" }
|
|
799
|
+
});
|
|
800
|
+
if (!resp.ok) {
|
|
801
|
+
throw new Error(`Upload failed for ${filePath}: ${resp.status} ${resp.statusText}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Download a file from an artifact using the presigned URL pattern:
|
|
806
|
+
* 1. get_file() returns a presigned download URL
|
|
807
|
+
* 2. HTTP GET the content from that URL
|
|
808
|
+
*/
|
|
809
|
+
async downloadFile(artifactId, filePath) {
|
|
810
|
+
const getUrl = await this.artifactManager.get_file({
|
|
811
|
+
artifact_id: artifactId,
|
|
812
|
+
file_path: filePath,
|
|
813
|
+
_rkwargs: true
|
|
814
|
+
});
|
|
815
|
+
if (!getUrl || typeof getUrl !== "string") return null;
|
|
816
|
+
const resp = await fetch(getUrl);
|
|
817
|
+
if (!resp.ok) return null;
|
|
818
|
+
return await resp.text();
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Sync a session's metadata and messages to the artifact store.
|
|
822
|
+
* Creates the artifact if it doesn't exist, updates if it does.
|
|
823
|
+
*/
|
|
824
|
+
async syncSession(sessionId, sessionsDir, metadata, machineId) {
|
|
825
|
+
if (!this.initialized || !this.artifactManager || !this.collectionId) {
|
|
826
|
+
this.log(`[ARTIFACT SYNC] Not initialized, skipping sync for ${sessionId}`);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
this.syncing = true;
|
|
830
|
+
try {
|
|
831
|
+
const artifactAlias = `session-${sessionId}`;
|
|
832
|
+
const sessionJsonPath = join(sessionsDir, "session.json");
|
|
833
|
+
const messagesPath = join(sessionsDir, "messages.jsonl");
|
|
834
|
+
const sessionData = existsSync(sessionJsonPath) ? JSON.parse(readFileSync(sessionJsonPath, "utf-8")) : null;
|
|
835
|
+
const messagesExist = existsSync(messagesPath);
|
|
836
|
+
const messageCount = messagesExist ? readFileSync(messagesPath, "utf-8").split("\n").filter((l) => l.trim()).length : 0;
|
|
837
|
+
let artifactId;
|
|
838
|
+
try {
|
|
839
|
+
const existing = await this.artifactManager.read({
|
|
840
|
+
artifact_id: artifactAlias,
|
|
841
|
+
_rkwargs: true
|
|
842
|
+
});
|
|
843
|
+
artifactId = existing.id;
|
|
844
|
+
await this.artifactManager.edit({
|
|
845
|
+
artifact_id: artifactId,
|
|
846
|
+
manifest: {
|
|
847
|
+
sessionId,
|
|
848
|
+
machineId,
|
|
849
|
+
metadata,
|
|
850
|
+
messageCount,
|
|
851
|
+
lastSyncAt: Date.now(),
|
|
852
|
+
...sessionData || {}
|
|
853
|
+
},
|
|
854
|
+
_rkwargs: true
|
|
855
|
+
});
|
|
856
|
+
} catch {
|
|
857
|
+
const artifact = await this.artifactManager.create({
|
|
858
|
+
alias: artifactAlias,
|
|
859
|
+
parent_id: this.collectionId,
|
|
860
|
+
type: "session",
|
|
861
|
+
manifest: {
|
|
862
|
+
sessionId,
|
|
863
|
+
machineId,
|
|
864
|
+
metadata,
|
|
865
|
+
messageCount,
|
|
866
|
+
createdAt: Date.now(),
|
|
867
|
+
lastSyncAt: Date.now(),
|
|
868
|
+
...sessionData || {}
|
|
869
|
+
},
|
|
870
|
+
_rkwargs: true
|
|
871
|
+
});
|
|
872
|
+
artifactId = artifact.id;
|
|
873
|
+
}
|
|
874
|
+
if (sessionData) {
|
|
875
|
+
await this.uploadFile(artifactId, "session.json", JSON.stringify(sessionData, null, 2));
|
|
876
|
+
}
|
|
877
|
+
if (messagesExist) {
|
|
878
|
+
const messagesContent = readFileSync(messagesPath, "utf-8");
|
|
879
|
+
await this.uploadFile(artifactId, "messages.jsonl", messagesContent);
|
|
880
|
+
}
|
|
881
|
+
await this.artifactManager.commit({
|
|
882
|
+
artifact_id: artifactId,
|
|
883
|
+
_rkwargs: true
|
|
884
|
+
});
|
|
885
|
+
this.syncedSessions.add(sessionId);
|
|
886
|
+
this.lastSyncAt = Date.now();
|
|
887
|
+
this.log(`[ARTIFACT SYNC] Synced session ${sessionId} (${messageCount} messages)`);
|
|
888
|
+
} catch (err) {
|
|
889
|
+
this.error = `Sync failed for ${sessionId}: ${err.message}`;
|
|
890
|
+
this.log(`[ARTIFACT SYNC] Sync failed for ${sessionId}: ${err.message}`);
|
|
891
|
+
} finally {
|
|
892
|
+
this.syncing = false;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Schedule a debounced sync for a session (e.g., on each new message).
|
|
897
|
+
* Waits 30 seconds before syncing to batch multiple messages.
|
|
898
|
+
*/
|
|
899
|
+
scheduleDebouncedSync(sessionId, sessionsDir, metadata, machineId, delayMs = 3e4) {
|
|
900
|
+
const existing = this.syncTimers.get(sessionId);
|
|
901
|
+
if (existing) clearTimeout(existing);
|
|
902
|
+
const timer = setTimeout(() => {
|
|
903
|
+
this.syncSession(sessionId, sessionsDir, metadata, machineId).catch(() => {
|
|
904
|
+
});
|
|
905
|
+
this.syncTimers.delete(sessionId);
|
|
906
|
+
}, delayMs);
|
|
907
|
+
this.syncTimers.set(sessionId, timer);
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Download a session from artifact store to local disk.
|
|
911
|
+
*/
|
|
912
|
+
async downloadSession(sessionId, targetDir) {
|
|
913
|
+
if (!this.initialized || !this.artifactManager) return null;
|
|
914
|
+
try {
|
|
915
|
+
const artifactAlias = `session-${sessionId}`;
|
|
916
|
+
const artifact = await this.artifactManager.read({
|
|
917
|
+
artifact_id: artifactAlias,
|
|
918
|
+
_rkwargs: true
|
|
919
|
+
});
|
|
920
|
+
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
921
|
+
try {
|
|
922
|
+
const data = await this.downloadFile(artifact.id, "session.json");
|
|
923
|
+
if (data) {
|
|
924
|
+
writeFileSync(join(targetDir, "session.json"), data);
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
const data = await this.downloadFile(artifact.id, "messages.jsonl");
|
|
930
|
+
if (data) {
|
|
931
|
+
writeFileSync(join(targetDir, "messages.jsonl"), data);
|
|
932
|
+
}
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
sessionData: artifact.manifest,
|
|
937
|
+
messageCount: artifact.manifest?.messageCount || 0
|
|
938
|
+
};
|
|
939
|
+
} catch (err) {
|
|
940
|
+
this.log(`[ARTIFACT SYNC] Download failed for ${sessionId}: ${err.message}`);
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* List all sessions stored in the artifact collection.
|
|
946
|
+
*/
|
|
947
|
+
async listRemoteSessions() {
|
|
948
|
+
if (!this.initialized || !this.artifactManager || !this.collectionId) return [];
|
|
949
|
+
try {
|
|
950
|
+
const children = await this.artifactManager.list({
|
|
951
|
+
parent_id: this.collectionId,
|
|
952
|
+
_rkwargs: true
|
|
953
|
+
});
|
|
954
|
+
return (children || []).map((child) => ({
|
|
955
|
+
sessionId: child.manifest?.sessionId || child.alias?.replace("session-", ""),
|
|
956
|
+
machineId: child.manifest?.machineId,
|
|
957
|
+
messageCount: child.manifest?.messageCount || 0,
|
|
958
|
+
lastSyncAt: child.manifest?.lastSyncAt || 0
|
|
959
|
+
}));
|
|
960
|
+
} catch (err) {
|
|
961
|
+
this.log(`[ARTIFACT SYNC] List failed: ${err.message}`);
|
|
962
|
+
return [];
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Get current sync status.
|
|
967
|
+
*/
|
|
968
|
+
getStatus() {
|
|
969
|
+
return {
|
|
970
|
+
initialized: this.initialized,
|
|
971
|
+
collectionId: this.collectionId,
|
|
972
|
+
lastSyncAt: this.lastSyncAt,
|
|
973
|
+
syncing: this.syncing,
|
|
974
|
+
syncedSessions: [...this.syncedSessions],
|
|
975
|
+
error: this.error
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Force sync all known sessions using the session index.
|
|
980
|
+
* @param svampHome The ~/.svamp/ directory (reads sessions-index.json)
|
|
981
|
+
*/
|
|
982
|
+
async syncAll(svampHome, machineId) {
|
|
983
|
+
if (!this.initialized) return;
|
|
984
|
+
const indexFile = join(svampHome, "sessions-index.json");
|
|
985
|
+
if (!existsSync(indexFile)) return;
|
|
986
|
+
try {
|
|
987
|
+
const index = JSON.parse(readFileSync(indexFile, "utf-8"));
|
|
988
|
+
for (const [sessionId, entry] of Object.entries(index)) {
|
|
989
|
+
const sessionDir = join(entry.directory, ".svamp", sessionId);
|
|
990
|
+
if (existsSync(join(sessionDir, "session.json"))) {
|
|
991
|
+
await this.syncSession(sessionId, sessionDir, void 0, machineId);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Cleanup timers on shutdown.
|
|
999
|
+
*/
|
|
1000
|
+
destroy() {
|
|
1001
|
+
for (const timer of this.syncTimers.values()) {
|
|
1002
|
+
clearTimeout(timer);
|
|
1003
|
+
}
|
|
1004
|
+
this.syncTimers.clear();
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const DEFAULT_TIMEOUTS = {
|
|
1009
|
+
init: 6e4,
|
|
1010
|
+
toolCall: 12e4,
|
|
1011
|
+
think: 3e4
|
|
1012
|
+
};
|
|
1013
|
+
class DefaultTransport {
|
|
1014
|
+
agentName;
|
|
1015
|
+
constructor(agentName = "generic-acp") {
|
|
1016
|
+
this.agentName = agentName;
|
|
1017
|
+
}
|
|
1018
|
+
getInitTimeout() {
|
|
1019
|
+
return DEFAULT_TIMEOUTS.init;
|
|
1020
|
+
}
|
|
1021
|
+
filterStdoutLine(line) {
|
|
1022
|
+
const trimmed = line.trim();
|
|
1023
|
+
if (!trimmed) return null;
|
|
1024
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
|
|
1025
|
+
try {
|
|
1026
|
+
const parsed = JSON.parse(trimmed);
|
|
1027
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1028
|
+
return line;
|
|
1029
|
+
} catch {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
handleStderr(_text, _context) {
|
|
1034
|
+
return { message: null };
|
|
1035
|
+
}
|
|
1036
|
+
getToolPatterns() {
|
|
1037
|
+
return [];
|
|
1038
|
+
}
|
|
1039
|
+
isInvestigationTool(_toolCallId, _toolKind) {
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
getToolCallTimeout(_toolCallId, toolKind) {
|
|
1043
|
+
if (toolKind === "think") return DEFAULT_TIMEOUTS.think;
|
|
1044
|
+
return DEFAULT_TIMEOUTS.toolCall;
|
|
1045
|
+
}
|
|
1046
|
+
extractToolNameFromId(_toolCallId) {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
determineToolName(toolName, _toolCallId, _input, _context) {
|
|
1050
|
+
return toolName;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
var DefaultTransport$1 = /*#__PURE__*/Object.freeze({
|
|
1055
|
+
__proto__: null,
|
|
1056
|
+
DefaultTransport: DefaultTransport
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
const DEFAULT_IDLE_TIMEOUT_MS = 500;
|
|
1060
|
+
const DEFAULT_TOOL_CALL_TIMEOUT_MS = 12e4;
|
|
1061
|
+
function parseArgsFromContent(content) {
|
|
1062
|
+
if (Array.isArray(content)) return { items: content };
|
|
1063
|
+
if (content && typeof content === "object" && content !== null) {
|
|
1064
|
+
return content;
|
|
1065
|
+
}
|
|
1066
|
+
return {};
|
|
1067
|
+
}
|
|
1068
|
+
function extractErrorDetail(content) {
|
|
1069
|
+
if (!content) return void 0;
|
|
1070
|
+
if (typeof content === "string") return content;
|
|
1071
|
+
if (typeof content === "object" && content !== null && !Array.isArray(content)) {
|
|
1072
|
+
const obj = content;
|
|
1073
|
+
if (obj.error) {
|
|
1074
|
+
const error = obj.error;
|
|
1075
|
+
if (typeof error === "string") return error;
|
|
1076
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
1077
|
+
const errObj = error;
|
|
1078
|
+
if (typeof errObj.message === "string") return errObj.message;
|
|
1079
|
+
}
|
|
1080
|
+
return JSON.stringify(error);
|
|
1081
|
+
}
|
|
1082
|
+
if (typeof obj.message === "string") return obj.message;
|
|
1083
|
+
const status = typeof obj.status === "string" ? obj.status : void 0;
|
|
1084
|
+
const reason = typeof obj.reason === "string" ? obj.reason : void 0;
|
|
1085
|
+
return status || reason || JSON.stringify(obj).substring(0, 500);
|
|
1086
|
+
}
|
|
1087
|
+
return void 0;
|
|
1088
|
+
}
|
|
1089
|
+
function formatDuration(startTime) {
|
|
1090
|
+
if (!startTime) return "unknown";
|
|
1091
|
+
const duration = Date.now() - startTime;
|
|
1092
|
+
return `${(duration / 1e3).toFixed(2)}s`;
|
|
1093
|
+
}
|
|
1094
|
+
function handleAgentMessageChunk(update, ctx) {
|
|
1095
|
+
const content = update.content;
|
|
1096
|
+
if (!content || typeof content !== "object" || !("text" in content)) {
|
|
1097
|
+
return { handled: false };
|
|
1098
|
+
}
|
|
1099
|
+
const text = content.text;
|
|
1100
|
+
if (typeof text !== "string") return { handled: false };
|
|
1101
|
+
const isThinking = /^\*\*[^*]+\*\*\n/.test(text);
|
|
1102
|
+
if (isThinking) {
|
|
1103
|
+
ctx.emit({ type: "event", name: "thinking", payload: { text, streaming: true } });
|
|
1104
|
+
} else {
|
|
1105
|
+
ctx.emit({ type: "model-output", textDelta: text });
|
|
1106
|
+
ctx.clearIdleTimeout();
|
|
1107
|
+
const idleTimeoutMs = ctx.transport.getIdleTimeout?.() ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
1108
|
+
ctx.setIdleTimeout(() => {
|
|
1109
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
1110
|
+
ctx.emitIdleStatus();
|
|
1111
|
+
}
|
|
1112
|
+
}, idleTimeoutMs);
|
|
1113
|
+
}
|
|
1114
|
+
return { handled: true };
|
|
1115
|
+
}
|
|
1116
|
+
function handleAgentThoughtChunk(update, ctx) {
|
|
1117
|
+
const content = update.content;
|
|
1118
|
+
if (!content || typeof content !== "object" || !("text" in content)) {
|
|
1119
|
+
return { handled: false };
|
|
1120
|
+
}
|
|
1121
|
+
const text = content.text;
|
|
1122
|
+
if (typeof text !== "string") return { handled: false };
|
|
1123
|
+
ctx.emit({ type: "event", name: "thinking", payload: { text, streaming: true } });
|
|
1124
|
+
return { handled: true };
|
|
1125
|
+
}
|
|
1126
|
+
function startToolCall(toolCallId, toolKind, update, ctx, source) {
|
|
1127
|
+
const startTime = Date.now();
|
|
1128
|
+
const toolKindStr = typeof toolKind === "string" ? toolKind : void 0;
|
|
1129
|
+
const isInvestigation = ctx.transport.isInvestigationTool?.(toolCallId, toolKindStr) ?? false;
|
|
1130
|
+
const extractedName = ctx.transport.extractToolNameFromId?.(toolCallId);
|
|
1131
|
+
const realToolName = extractedName ?? (toolKindStr || "unknown");
|
|
1132
|
+
ctx.toolCallIdToNameMap.set(toolCallId, realToolName);
|
|
1133
|
+
ctx.activeToolCalls.add(toolCallId);
|
|
1134
|
+
ctx.toolCallStartTimes.set(toolCallId, startTime);
|
|
1135
|
+
ctx.log(`Tool call START: ${toolCallId} (${toolKind} -> ${realToolName})${isInvestigation ? " [INVESTIGATION]" : ""} (from ${source})`);
|
|
1136
|
+
const timeoutMs = ctx.transport.getToolCallTimeout?.(toolCallId, toolKindStr) ?? DEFAULT_TOOL_CALL_TIMEOUT_MS;
|
|
1137
|
+
if (!ctx.toolCallTimeouts.has(toolCallId)) {
|
|
1138
|
+
const timeout = setTimeout(() => {
|
|
1139
|
+
ctx.activeToolCalls.delete(toolCallId);
|
|
1140
|
+
ctx.toolCallStartTimes.delete(toolCallId);
|
|
1141
|
+
ctx.toolCallTimeouts.delete(toolCallId);
|
|
1142
|
+
ctx.log(`Tool call TIMEOUT: ${toolCallId} (${toolKind}) after ${(timeoutMs / 1e3).toFixed(0)}s`);
|
|
1143
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
1144
|
+
ctx.emitIdleStatus();
|
|
1145
|
+
}
|
|
1146
|
+
}, timeoutMs);
|
|
1147
|
+
ctx.toolCallTimeouts.set(toolCallId, timeout);
|
|
1148
|
+
}
|
|
1149
|
+
ctx.clearIdleTimeout();
|
|
1150
|
+
ctx.emit({ type: "status", status: "running" });
|
|
1151
|
+
const args = parseArgsFromContent(update.content);
|
|
1152
|
+
if (update.locations && Array.isArray(update.locations)) {
|
|
1153
|
+
args.locations = update.locations;
|
|
1154
|
+
}
|
|
1155
|
+
ctx.emit({
|
|
1156
|
+
type: "tool-call",
|
|
1157
|
+
toolName: toolKindStr || "unknown",
|
|
1158
|
+
args,
|
|
1159
|
+
callId: toolCallId
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
function completeToolCall(toolCallId, toolKind, content, ctx) {
|
|
1163
|
+
const startTime = ctx.toolCallStartTimes.get(toolCallId);
|
|
1164
|
+
const duration = formatDuration(startTime);
|
|
1165
|
+
const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
|
|
1166
|
+
ctx.activeToolCalls.delete(toolCallId);
|
|
1167
|
+
ctx.toolCallStartTimes.delete(toolCallId);
|
|
1168
|
+
const timeout = ctx.toolCallTimeouts.get(toolCallId);
|
|
1169
|
+
if (timeout) {
|
|
1170
|
+
clearTimeout(timeout);
|
|
1171
|
+
ctx.toolCallTimeouts.delete(toolCallId);
|
|
1172
|
+
}
|
|
1173
|
+
ctx.log(`Tool call COMPLETED: ${toolCallId} (${toolKindStr}) - ${duration}. Active: ${ctx.activeToolCalls.size}`);
|
|
1174
|
+
ctx.emit({ type: "tool-result", toolName: toolKindStr, result: content, callId: toolCallId });
|
|
1175
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
1176
|
+
ctx.clearIdleTimeout();
|
|
1177
|
+
ctx.emitIdleStatus();
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function failToolCall(toolCallId, status, toolKind, content, ctx) {
|
|
1181
|
+
const startTime = ctx.toolCallStartTimes.get(toolCallId);
|
|
1182
|
+
const duration = formatDuration(startTime);
|
|
1183
|
+
const toolKindStr = typeof toolKind === "string" ? toolKind : "unknown";
|
|
1184
|
+
ctx.activeToolCalls.delete(toolCallId);
|
|
1185
|
+
ctx.toolCallStartTimes.delete(toolCallId);
|
|
1186
|
+
const timeout = ctx.toolCallTimeouts.get(toolCallId);
|
|
1187
|
+
if (timeout) {
|
|
1188
|
+
clearTimeout(timeout);
|
|
1189
|
+
ctx.toolCallTimeouts.delete(toolCallId);
|
|
1190
|
+
}
|
|
1191
|
+
const errorDetail = extractErrorDetail(content);
|
|
1192
|
+
ctx.log(`Tool call ${status.toUpperCase()}: ${toolCallId} (${toolKindStr}) - ${duration}. Active: ${ctx.activeToolCalls.size}`);
|
|
1193
|
+
ctx.emit({
|
|
1194
|
+
type: "tool-result",
|
|
1195
|
+
toolName: toolKindStr,
|
|
1196
|
+
result: errorDetail ? { error: errorDetail, status } : { error: `Tool call ${status}`, status },
|
|
1197
|
+
callId: toolCallId
|
|
1198
|
+
});
|
|
1199
|
+
if (ctx.activeToolCalls.size === 0) {
|
|
1200
|
+
ctx.clearIdleTimeout();
|
|
1201
|
+
ctx.emitIdleStatus();
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function handleToolCallUpdate(update, ctx) {
|
|
1205
|
+
const status = update.status;
|
|
1206
|
+
const toolCallId = update.toolCallId;
|
|
1207
|
+
if (!toolCallId) return { handled: false };
|
|
1208
|
+
const toolKind = update.kind || "unknown";
|
|
1209
|
+
let toolCallCountSincePrompt = ctx.toolCallCountSincePrompt;
|
|
1210
|
+
if (status === "in_progress" || status === "pending") {
|
|
1211
|
+
if (!ctx.activeToolCalls.has(toolCallId)) {
|
|
1212
|
+
toolCallCountSincePrompt++;
|
|
1213
|
+
startToolCall(toolCallId, toolKind, update, ctx, "tool_call_update");
|
|
1214
|
+
}
|
|
1215
|
+
} else if (status === "completed") {
|
|
1216
|
+
completeToolCall(toolCallId, toolKind, update.content, ctx);
|
|
1217
|
+
} else if (status === "failed" || status === "cancelled") {
|
|
1218
|
+
failToolCall(toolCallId, status, toolKind, update.content, ctx);
|
|
1219
|
+
}
|
|
1220
|
+
return { handled: true, toolCallCountSincePrompt };
|
|
1221
|
+
}
|
|
1222
|
+
function handleToolCall(update, ctx) {
|
|
1223
|
+
const toolCallId = update.toolCallId;
|
|
1224
|
+
const status = update.status;
|
|
1225
|
+
const isInProgress = !status || status === "in_progress" || status === "pending";
|
|
1226
|
+
if (!toolCallId || !isInProgress) return { handled: false };
|
|
1227
|
+
if (ctx.activeToolCalls.has(toolCallId)) return { handled: true };
|
|
1228
|
+
startToolCall(toolCallId, update.kind, update, ctx, "tool_call");
|
|
1229
|
+
return { handled: true };
|
|
1230
|
+
}
|
|
1231
|
+
function handleLegacyMessageChunk(update, ctx) {
|
|
1232
|
+
if (!update.messageChunk) return { handled: false };
|
|
1233
|
+
const chunk = update.messageChunk;
|
|
1234
|
+
if (chunk.textDelta) {
|
|
1235
|
+
ctx.emit({ type: "model-output", textDelta: chunk.textDelta });
|
|
1236
|
+
return { handled: true };
|
|
1237
|
+
}
|
|
1238
|
+
return { handled: false };
|
|
1239
|
+
}
|
|
1240
|
+
function handlePlanUpdate(update, ctx) {
|
|
1241
|
+
if (!update.plan) return { handled: false };
|
|
1242
|
+
ctx.emit({ type: "event", name: "plan", payload: update.plan });
|
|
1243
|
+
return { handled: true };
|
|
1244
|
+
}
|
|
1245
|
+
function handleThinkingUpdate(update, ctx) {
|
|
1246
|
+
if (!update.thinking) return { handled: false };
|
|
1247
|
+
ctx.emit({ type: "event", name: "thinking", payload: update.thinking });
|
|
1248
|
+
return { handled: true };
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function delay(ms) {
|
|
1252
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1253
|
+
}
|
|
1254
|
+
const RETRY_CONFIG = {
|
|
1255
|
+
maxAttempts: 3,
|
|
1256
|
+
baseDelayMs: 1e3,
|
|
1257
|
+
maxDelayMs: 5e3
|
|
1258
|
+
};
|
|
1259
|
+
function nodeToWebStreams(stdin, stdout) {
|
|
1260
|
+
const writable = new WritableStream({
|
|
1261
|
+
write(chunk) {
|
|
1262
|
+
return new Promise((resolve, reject) => {
|
|
1263
|
+
const ok = stdin.write(chunk, (err) => {
|
|
1264
|
+
if (err) reject(err);
|
|
1265
|
+
});
|
|
1266
|
+
if (ok) resolve();
|
|
1267
|
+
else stdin.once("drain", resolve);
|
|
1268
|
+
});
|
|
1269
|
+
},
|
|
1270
|
+
close() {
|
|
1271
|
+
return new Promise((resolve) => {
|
|
1272
|
+
stdin.end(resolve);
|
|
1273
|
+
});
|
|
1274
|
+
},
|
|
1275
|
+
abort(reason) {
|
|
1276
|
+
stdin.destroy(reason instanceof Error ? reason : new Error(String(reason)));
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
const readable = new ReadableStream({
|
|
1280
|
+
start(controller) {
|
|
1281
|
+
stdout.on("data", (chunk) => {
|
|
1282
|
+
controller.enqueue(new Uint8Array(chunk));
|
|
1283
|
+
});
|
|
1284
|
+
stdout.on("end", () => {
|
|
1285
|
+
controller.close();
|
|
1286
|
+
});
|
|
1287
|
+
stdout.on("error", (err) => {
|
|
1288
|
+
controller.error(err);
|
|
1289
|
+
});
|
|
1290
|
+
},
|
|
1291
|
+
cancel() {
|
|
1292
|
+
stdout.destroy();
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
return { writable, readable };
|
|
1296
|
+
}
|
|
1297
|
+
async function withRetry(operation, opts) {
|
|
1298
|
+
let lastError = null;
|
|
1299
|
+
const log = opts.log || (() => {
|
|
1300
|
+
});
|
|
1301
|
+
for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
|
|
1302
|
+
try {
|
|
1303
|
+
return await operation();
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
if (error instanceof Error) {
|
|
1306
|
+
lastError = error;
|
|
1307
|
+
} else if (typeof error === "object" && error !== null) {
|
|
1308
|
+
const obj = error;
|
|
1309
|
+
const msg = typeof obj.message === "string" ? obj.message : JSON.stringify(error);
|
|
1310
|
+
lastError = new Error(msg);
|
|
1311
|
+
} else {
|
|
1312
|
+
lastError = new Error(String(error));
|
|
1313
|
+
}
|
|
1314
|
+
const shouldRetry = opts.shouldRetry ? opts.shouldRetry(lastError) : true;
|
|
1315
|
+
if (attempt < opts.maxAttempts && shouldRetry) {
|
|
1316
|
+
const delayMs = Math.min(opts.baseDelayMs * Math.pow(2, attempt - 1), opts.maxDelayMs);
|
|
1317
|
+
log(`${opts.operationName} failed (attempt ${attempt}/${opts.maxAttempts}): ${lastError.message}. Retrying in ${delayMs}ms...`);
|
|
1318
|
+
await delay(delayMs);
|
|
1319
|
+
} else {
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
throw lastError;
|
|
1325
|
+
}
|
|
1326
|
+
class AcpBackend {
|
|
1327
|
+
constructor(options) {
|
|
1328
|
+
this.options = options;
|
|
1329
|
+
this.transport = options.transportHandler ?? new DefaultTransport(options.agentName);
|
|
1330
|
+
this.log = options.log || ((...args) => {
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
listeners = [];
|
|
1334
|
+
process = null;
|
|
1335
|
+
connection = null;
|
|
1336
|
+
acpSessionId = null;
|
|
1337
|
+
disposed = false;
|
|
1338
|
+
activeToolCalls = /* @__PURE__ */ new Set();
|
|
1339
|
+
toolCallTimeouts = /* @__PURE__ */ new Map();
|
|
1340
|
+
toolCallStartTimes = /* @__PURE__ */ new Map();
|
|
1341
|
+
toolCallIdToNameMap = /* @__PURE__ */ new Map();
|
|
1342
|
+
recentPromptHadChangeTitle = false;
|
|
1343
|
+
toolCallCountSincePrompt = 0;
|
|
1344
|
+
idleTimeout = null;
|
|
1345
|
+
transport;
|
|
1346
|
+
log;
|
|
1347
|
+
// Promise resolver for waitForIdle
|
|
1348
|
+
idleResolver = null;
|
|
1349
|
+
waitingForResponse = false;
|
|
1350
|
+
onMessage(handler) {
|
|
1351
|
+
this.listeners.push(handler);
|
|
1352
|
+
}
|
|
1353
|
+
offMessage(handler) {
|
|
1354
|
+
const index = this.listeners.indexOf(handler);
|
|
1355
|
+
if (index !== -1) this.listeners.splice(index, 1);
|
|
1356
|
+
}
|
|
1357
|
+
emit(msg) {
|
|
1358
|
+
if (this.disposed) return;
|
|
1359
|
+
for (const listener of this.listeners) {
|
|
1360
|
+
try {
|
|
1361
|
+
listener(msg);
|
|
1362
|
+
} catch {
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
/** Get the underlying child process (for cleanup) */
|
|
1367
|
+
getProcess() {
|
|
1368
|
+
return this.process;
|
|
1369
|
+
}
|
|
1370
|
+
async startSession(initialPrompt) {
|
|
1371
|
+
if (this.disposed) throw new Error("Backend has been disposed");
|
|
1372
|
+
const sessionId = randomUUID();
|
|
1373
|
+
this.emit({ type: "status", status: "starting" });
|
|
1374
|
+
let startupStatusErrorEmitted = false;
|
|
1375
|
+
try {
|
|
1376
|
+
const args = this.options.args || [];
|
|
1377
|
+
if (process.platform === "win32") {
|
|
1378
|
+
const fullCommand = [this.options.command, ...args].join(" ");
|
|
1379
|
+
this.process = spawn("cmd.exe", ["/c", fullCommand], {
|
|
1380
|
+
cwd: this.options.cwd,
|
|
1381
|
+
env: { ...process.env, ...this.options.env },
|
|
1382
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1383
|
+
windowsHide: true
|
|
1384
|
+
});
|
|
1385
|
+
} else {
|
|
1386
|
+
this.process = spawn(this.options.command, args, {
|
|
1387
|
+
cwd: this.options.cwd,
|
|
1388
|
+
env: { ...process.env, ...this.options.env },
|
|
1389
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
|
|
1393
|
+
throw new Error("Failed to create stdio pipes");
|
|
1394
|
+
}
|
|
1395
|
+
let startupFailure = null;
|
|
1396
|
+
let startupFailureSettled = false;
|
|
1397
|
+
let rejectStartupFailure = null;
|
|
1398
|
+
const startupFailurePromise = new Promise((_, reject) => {
|
|
1399
|
+
rejectStartupFailure = (error) => {
|
|
1400
|
+
if (startupFailureSettled) return;
|
|
1401
|
+
startupFailureSettled = true;
|
|
1402
|
+
startupFailure = error;
|
|
1403
|
+
reject(error);
|
|
1404
|
+
};
|
|
1405
|
+
});
|
|
1406
|
+
const signalStartupFailure = (error) => {
|
|
1407
|
+
rejectStartupFailure?.(error);
|
|
1408
|
+
};
|
|
1409
|
+
this.process.stderr.on("data", (data) => {
|
|
1410
|
+
const text = data.toString();
|
|
1411
|
+
if (!text.trim()) return;
|
|
1412
|
+
const hasActiveInvestigation = this.transport.isInvestigationTool ? Array.from(this.activeToolCalls).some((id) => this.transport.isInvestigationTool(id)) : false;
|
|
1413
|
+
const context = {
|
|
1414
|
+
activeToolCalls: this.activeToolCalls,
|
|
1415
|
+
hasActiveInvestigation
|
|
1416
|
+
};
|
|
1417
|
+
this.log(`[ACP] stderr: ${text.trim()}`);
|
|
1418
|
+
if (this.transport.handleStderr) {
|
|
1419
|
+
const result = this.transport.handleStderr(text, context);
|
|
1420
|
+
if (result.message) this.emit(result.message);
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
this.process.on("error", (err) => {
|
|
1424
|
+
signalStartupFailure(err);
|
|
1425
|
+
this.log(`[ACP] Process error: ${err.message}`);
|
|
1426
|
+
startupStatusErrorEmitted = true;
|
|
1427
|
+
this.emit({ type: "status", status: "error", detail: err.message });
|
|
1428
|
+
});
|
|
1429
|
+
this.process.on("exit", (code, signal) => {
|
|
1430
|
+
if (!this.disposed && code !== 0 && code !== null) {
|
|
1431
|
+
signalStartupFailure(new Error(`Exit code: ${code}`));
|
|
1432
|
+
this.log(`[ACP] Process exited: code=${code}, signal=${signal}`);
|
|
1433
|
+
this.emit({ type: "status", status: "stopped", detail: `Exit code: ${code}` });
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
const streams = nodeToWebStreams(this.process.stdin, this.process.stdout);
|
|
1437
|
+
const transport = this.transport;
|
|
1438
|
+
const logFn = this.log;
|
|
1439
|
+
const filteredReadable = new ReadableStream({
|
|
1440
|
+
async start(controller) {
|
|
1441
|
+
const reader = streams.readable.getReader();
|
|
1442
|
+
const decoder = new TextDecoder();
|
|
1443
|
+
const encoder = new TextEncoder();
|
|
1444
|
+
let buffer = "";
|
|
1445
|
+
let filteredCount = 0;
|
|
1446
|
+
try {
|
|
1447
|
+
while (true) {
|
|
1448
|
+
const { done, value } = await reader.read();
|
|
1449
|
+
if (done) {
|
|
1450
|
+
if (buffer.trim()) {
|
|
1451
|
+
const filtered = transport.filterStdoutLine?.(buffer);
|
|
1452
|
+
if (filtered === void 0) controller.enqueue(encoder.encode(buffer));
|
|
1453
|
+
else if (filtered !== null) controller.enqueue(encoder.encode(filtered));
|
|
1454
|
+
else filteredCount++;
|
|
1455
|
+
}
|
|
1456
|
+
if (filteredCount > 0) {
|
|
1457
|
+
logFn(`[ACP] Filtered ${filteredCount} non-JSON lines from ${transport.agentName} stdout`);
|
|
1458
|
+
}
|
|
1459
|
+
controller.close();
|
|
1460
|
+
break;
|
|
1461
|
+
}
|
|
1462
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1463
|
+
const lines = buffer.split("\n");
|
|
1464
|
+
buffer = lines.pop() || "";
|
|
1465
|
+
for (const line of lines) {
|
|
1466
|
+
if (!line.trim()) continue;
|
|
1467
|
+
const filtered = transport.filterStdoutLine?.(line);
|
|
1468
|
+
if (filtered === void 0) controller.enqueue(encoder.encode(line + "\n"));
|
|
1469
|
+
else if (filtered !== null) controller.enqueue(encoder.encode(filtered + "\n"));
|
|
1470
|
+
else filteredCount++;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
controller.error(error);
|
|
1475
|
+
} finally {
|
|
1476
|
+
reader.releaseLock();
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
const stream = ndJsonStream(streams.writable, filteredReadable);
|
|
1481
|
+
const client = {
|
|
1482
|
+
sessionUpdate: async (params) => {
|
|
1483
|
+
this.handleSessionUpdate(params);
|
|
1484
|
+
},
|
|
1485
|
+
requestPermission: async (params) => {
|
|
1486
|
+
const extendedParams = params;
|
|
1487
|
+
const toolCall = extendedParams.toolCall;
|
|
1488
|
+
let toolName = toolCall?.kind || toolCall?.toolName || extendedParams.kind || "Unknown tool";
|
|
1489
|
+
const toolCallId = toolCall?.id || randomUUID();
|
|
1490
|
+
const permissionId = toolCallId;
|
|
1491
|
+
let input = {};
|
|
1492
|
+
if (toolCall) {
|
|
1493
|
+
input = toolCall.input || toolCall.arguments || toolCall.content || {};
|
|
1494
|
+
} else {
|
|
1495
|
+
input = extendedParams.input || extendedParams.arguments || extendedParams.content || {};
|
|
1496
|
+
}
|
|
1497
|
+
const context = {
|
|
1498
|
+
recentPromptHadChangeTitle: this.recentPromptHadChangeTitle,
|
|
1499
|
+
toolCallCountSincePrompt: this.toolCallCountSincePrompt
|
|
1500
|
+
};
|
|
1501
|
+
toolName = this.transport.determineToolName?.(toolName, toolCallId, input, context) ?? toolName;
|
|
1502
|
+
this.toolCallCountSincePrompt++;
|
|
1503
|
+
const options = extendedParams.options || [];
|
|
1504
|
+
this.log(`[ACP] Permission request: tool=${toolName}, toolCallId=${toolCallId}`);
|
|
1505
|
+
this.emit({
|
|
1506
|
+
type: "permission-request",
|
|
1507
|
+
id: permissionId,
|
|
1508
|
+
reason: toolName,
|
|
1509
|
+
payload: {
|
|
1510
|
+
...params,
|
|
1511
|
+
permissionId,
|
|
1512
|
+
toolCallId,
|
|
1513
|
+
toolName,
|
|
1514
|
+
input,
|
|
1515
|
+
options: options.map((opt) => ({
|
|
1516
|
+
id: opt.optionId,
|
|
1517
|
+
name: opt.name,
|
|
1518
|
+
kind: opt.kind
|
|
1519
|
+
}))
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
if (this.options.permissionHandler) {
|
|
1523
|
+
try {
|
|
1524
|
+
const result = await this.options.permissionHandler.handleToolCall(toolCallId, toolName, input);
|
|
1525
|
+
let optionId = "cancel";
|
|
1526
|
+
if (result.decision === "approved" || result.decision === "approved_for_session") {
|
|
1527
|
+
const proceedOnceOption2 = options.find(
|
|
1528
|
+
(opt) => opt.optionId === "proceed_once" || opt.name?.toLowerCase().includes("once")
|
|
1529
|
+
);
|
|
1530
|
+
const proceedAlwaysOption = options.find(
|
|
1531
|
+
(opt) => opt.optionId === "proceed_always" || opt.name?.toLowerCase().includes("always")
|
|
1532
|
+
);
|
|
1533
|
+
if (result.decision === "approved_for_session" && proceedAlwaysOption) {
|
|
1534
|
+
optionId = proceedAlwaysOption.optionId || "proceed_always";
|
|
1535
|
+
} else if (proceedOnceOption2) {
|
|
1536
|
+
optionId = proceedOnceOption2.optionId || "proceed_once";
|
|
1537
|
+
} else if (options.length > 0) {
|
|
1538
|
+
optionId = options[0].optionId || "proceed_once";
|
|
1539
|
+
}
|
|
1540
|
+
this.emit({
|
|
1541
|
+
type: "tool-result",
|
|
1542
|
+
toolName,
|
|
1543
|
+
result: { status: "approved", decision: result.decision },
|
|
1544
|
+
callId: permissionId
|
|
1545
|
+
});
|
|
1546
|
+
} else {
|
|
1547
|
+
const cancelOption = options.find(
|
|
1548
|
+
(opt) => opt.optionId === "cancel" || opt.name?.toLowerCase().includes("cancel")
|
|
1549
|
+
);
|
|
1550
|
+
if (cancelOption) optionId = cancelOption.optionId || "cancel";
|
|
1551
|
+
this.emit({
|
|
1552
|
+
type: "tool-result",
|
|
1553
|
+
toolName,
|
|
1554
|
+
result: { status: "denied", decision: result.decision },
|
|
1555
|
+
callId: permissionId
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
this.log("[ACP] Error in permission handler:", error);
|
|
1561
|
+
return { outcome: { outcome: "selected", optionId: "cancel" } };
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
const proceedOnceOption = options.find(
|
|
1565
|
+
(opt) => opt.optionId === "proceed_once" || typeof opt.name === "string" && opt.name.toLowerCase().includes("once")
|
|
1566
|
+
);
|
|
1567
|
+
const defaultOptionId = proceedOnceOption?.optionId || (options.length > 0 && options[0].optionId ? options[0].optionId : "proceed_once");
|
|
1568
|
+
return { outcome: { outcome: "selected", optionId: defaultOptionId } };
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
this.connection = new ClientSideConnection(
|
|
1572
|
+
(agent) => client,
|
|
1573
|
+
stream
|
|
1574
|
+
);
|
|
1575
|
+
const initRequest = {
|
|
1576
|
+
protocolVersion: 1,
|
|
1577
|
+
clientCapabilities: {
|
|
1578
|
+
fs: { readTextFile: false, writeTextFile: false }
|
|
1579
|
+
},
|
|
1580
|
+
clientInfo: { name: "svamp-cli", version: "0.1.0" }
|
|
1581
|
+
};
|
|
1582
|
+
const initTimeout = this.transport.getInitTimeout();
|
|
1583
|
+
this.log(`[ACP] Initializing connection (timeout: ${initTimeout}ms)...`);
|
|
1584
|
+
const isNonRetryableStartupError = (error) => {
|
|
1585
|
+
const maybeErr = error;
|
|
1586
|
+
if (startupFailure && error === startupFailure) return true;
|
|
1587
|
+
if (maybeErr.code === "ENOENT" || maybeErr.code === "EACCES" || maybeErr.code === "EPIPE") return true;
|
|
1588
|
+
const msg = error.message.toLowerCase();
|
|
1589
|
+
if (msg.includes("api key") || msg.includes("not configured") || msg.includes("401") || msg.includes("403")) return true;
|
|
1590
|
+
return false;
|
|
1591
|
+
};
|
|
1592
|
+
await withRetry(
|
|
1593
|
+
async () => {
|
|
1594
|
+
let timeoutHandle = null;
|
|
1595
|
+
try {
|
|
1596
|
+
const result = await Promise.race([
|
|
1597
|
+
startupFailurePromise,
|
|
1598
|
+
this.connection.initialize(initRequest).then((res) => {
|
|
1599
|
+
if (timeoutHandle) {
|
|
1600
|
+
clearTimeout(timeoutHandle);
|
|
1601
|
+
timeoutHandle = null;
|
|
1602
|
+
}
|
|
1603
|
+
return res;
|
|
1604
|
+
}),
|
|
1605
|
+
new Promise((_, reject) => {
|
|
1606
|
+
timeoutHandle = setTimeout(() => {
|
|
1607
|
+
reject(new Error(`Initialize timeout after ${initTimeout}ms - ${this.transport.agentName} did not respond`));
|
|
1608
|
+
}, initTimeout);
|
|
1609
|
+
})
|
|
1610
|
+
]);
|
|
1611
|
+
return result;
|
|
1612
|
+
} finally {
|
|
1613
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1614
|
+
}
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
operationName: "Initialize",
|
|
1618
|
+
maxAttempts: RETRY_CONFIG.maxAttempts,
|
|
1619
|
+
baseDelayMs: RETRY_CONFIG.baseDelayMs,
|
|
1620
|
+
maxDelayMs: RETRY_CONFIG.maxDelayMs,
|
|
1621
|
+
shouldRetry: (error) => !isNonRetryableStartupError(error),
|
|
1622
|
+
log: this.log
|
|
1623
|
+
}
|
|
1624
|
+
);
|
|
1625
|
+
this.log(`[ACP] Initialize completed`);
|
|
1626
|
+
const mcpServers = this.options.mcpServers ? Object.entries(this.options.mcpServers).map(([name, config]) => {
|
|
1627
|
+
if (config.type === "http" && config.url) {
|
|
1628
|
+
return { name, url: config.url };
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
name,
|
|
1632
|
+
command: config.command || "",
|
|
1633
|
+
args: config.args || [],
|
|
1634
|
+
env: config.env ? Object.entries(config.env).map(([envName, envValue]) => ({ name: envName, value: envValue })) : []
|
|
1635
|
+
};
|
|
1636
|
+
}) : [];
|
|
1637
|
+
const newSessionRequest = {
|
|
1638
|
+
cwd: this.options.cwd,
|
|
1639
|
+
mcpServers
|
|
1640
|
+
};
|
|
1641
|
+
this.log(`[ACP] Creating new session...`);
|
|
1642
|
+
const sessionResponse = await withRetry(
|
|
1643
|
+
async () => {
|
|
1644
|
+
let timeoutHandle = null;
|
|
1645
|
+
try {
|
|
1646
|
+
const result = await Promise.race([
|
|
1647
|
+
startupFailurePromise,
|
|
1648
|
+
this.connection.newSession(newSessionRequest).then((res) => {
|
|
1649
|
+
if (timeoutHandle) {
|
|
1650
|
+
clearTimeout(timeoutHandle);
|
|
1651
|
+
timeoutHandle = null;
|
|
1652
|
+
}
|
|
1653
|
+
return res;
|
|
1654
|
+
}),
|
|
1655
|
+
new Promise((_, reject) => {
|
|
1656
|
+
timeoutHandle = setTimeout(() => {
|
|
1657
|
+
reject(new Error(`New session timeout after ${initTimeout}ms - ${this.transport.agentName} did not respond`));
|
|
1658
|
+
}, initTimeout);
|
|
1659
|
+
})
|
|
1660
|
+
]);
|
|
1661
|
+
return result;
|
|
1662
|
+
} finally {
|
|
1663
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1664
|
+
}
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
operationName: "NewSession",
|
|
1668
|
+
maxAttempts: RETRY_CONFIG.maxAttempts,
|
|
1669
|
+
baseDelayMs: RETRY_CONFIG.baseDelayMs,
|
|
1670
|
+
maxDelayMs: RETRY_CONFIG.maxDelayMs,
|
|
1671
|
+
shouldRetry: (error) => !isNonRetryableStartupError(error),
|
|
1672
|
+
log: this.log
|
|
1673
|
+
}
|
|
1674
|
+
);
|
|
1675
|
+
this.acpSessionId = sessionResponse.sessionId;
|
|
1676
|
+
this.log(`[ACP] Session created: ${this.acpSessionId}`);
|
|
1677
|
+
this.emitInitialSessionMetadata(sessionResponse);
|
|
1678
|
+
this.emitIdleStatus();
|
|
1679
|
+
if (initialPrompt) {
|
|
1680
|
+
this.sendPrompt(sessionId, initialPrompt).catch((error) => {
|
|
1681
|
+
this.log("[ACP] Error sending initial prompt:", error);
|
|
1682
|
+
this.emit({ type: "status", status: "error", detail: String(error) });
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
return { sessionId };
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
this.log("[ACP] Error starting session:", error);
|
|
1688
|
+
if (!startupStatusErrorEmitted) {
|
|
1689
|
+
this.emit({
|
|
1690
|
+
type: "status",
|
|
1691
|
+
status: "error",
|
|
1692
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
throw error;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
createHandlerContext() {
|
|
1699
|
+
return {
|
|
1700
|
+
transport: this.transport,
|
|
1701
|
+
activeToolCalls: this.activeToolCalls,
|
|
1702
|
+
toolCallStartTimes: this.toolCallStartTimes,
|
|
1703
|
+
toolCallTimeouts: this.toolCallTimeouts,
|
|
1704
|
+
toolCallIdToNameMap: this.toolCallIdToNameMap,
|
|
1705
|
+
idleTimeout: this.idleTimeout,
|
|
1706
|
+
toolCallCountSincePrompt: this.toolCallCountSincePrompt,
|
|
1707
|
+
emit: (msg) => this.emit(msg),
|
|
1708
|
+
emitIdleStatus: () => this.emitIdleStatus(),
|
|
1709
|
+
clearIdleTimeout: () => {
|
|
1710
|
+
if (this.idleTimeout) {
|
|
1711
|
+
clearTimeout(this.idleTimeout);
|
|
1712
|
+
this.idleTimeout = null;
|
|
1713
|
+
}
|
|
1714
|
+
},
|
|
1715
|
+
setIdleTimeout: (callback, ms) => {
|
|
1716
|
+
this.idleTimeout = setTimeout(() => {
|
|
1717
|
+
callback();
|
|
1718
|
+
this.idleTimeout = null;
|
|
1719
|
+
}, ms);
|
|
1720
|
+
},
|
|
1721
|
+
log: this.log
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
emitInitialSessionMetadata(sessionResponse) {
|
|
1725
|
+
if (Array.isArray(sessionResponse.configOptions)) {
|
|
1726
|
+
this.emit({
|
|
1727
|
+
type: "event",
|
|
1728
|
+
name: "config_options_update",
|
|
1729
|
+
payload: { configOptions: sessionResponse.configOptions }
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
if (sessionResponse.modes) {
|
|
1733
|
+
this.emit({ type: "event", name: "modes_update", payload: sessionResponse.modes });
|
|
1734
|
+
this.emit({ type: "event", name: "current_mode_update", payload: { currentModeId: sessionResponse.modes.currentModeId } });
|
|
1735
|
+
}
|
|
1736
|
+
if (sessionResponse.models) {
|
|
1737
|
+
this.emit({ type: "event", name: "models_update", payload: sessionResponse.models });
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
handleSessionUpdate(params) {
|
|
1741
|
+
const notification = params;
|
|
1742
|
+
const update = notification.update;
|
|
1743
|
+
if (!update) return;
|
|
1744
|
+
const sessionUpdateType = update.sessionUpdate;
|
|
1745
|
+
const updateType = sessionUpdateType;
|
|
1746
|
+
this.log(`[ACP] sessionUpdate: ${sessionUpdateType}`);
|
|
1747
|
+
const ctx = this.createHandlerContext();
|
|
1748
|
+
if (sessionUpdateType === "agent_message_chunk") {
|
|
1749
|
+
handleAgentMessageChunk(update, ctx);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (sessionUpdateType === "tool_call_update") {
|
|
1753
|
+
const result = handleToolCallUpdate(update, ctx);
|
|
1754
|
+
if (result.toolCallCountSincePrompt !== void 0) {
|
|
1755
|
+
this.toolCallCountSincePrompt = result.toolCallCountSincePrompt;
|
|
1756
|
+
}
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (sessionUpdateType === "agent_thought_chunk") {
|
|
1760
|
+
handleAgentThoughtChunk(update, ctx);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
if (sessionUpdateType === "tool_call") {
|
|
1764
|
+
handleToolCall(update, ctx);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (sessionUpdateType === "available_commands_update") {
|
|
1768
|
+
const commands = update.availableCommands;
|
|
1769
|
+
if (Array.isArray(commands)) {
|
|
1770
|
+
this.emit({ type: "event", name: "available_commands", payload: commands });
|
|
1771
|
+
}
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (updateType === "config_option_update" || updateType === "config_options_update") {
|
|
1775
|
+
const configOptions = update.configOptions;
|
|
1776
|
+
if (Array.isArray(configOptions)) {
|
|
1777
|
+
this.emit({ type: "event", name: "config_options_update", payload: { configOptions } });
|
|
1778
|
+
}
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
if (updateType === "current_mode_update") {
|
|
1782
|
+
const currentModeId = update.currentModeId;
|
|
1783
|
+
if (typeof currentModeId === "string" && currentModeId.length > 0) {
|
|
1784
|
+
this.emit({ type: "event", name: "current_mode_update", payload: { currentModeId } });
|
|
1785
|
+
}
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
handleLegacyMessageChunk(update, ctx);
|
|
1789
|
+
handlePlanUpdate(update, ctx);
|
|
1790
|
+
handleThinkingUpdate(update, ctx);
|
|
1791
|
+
}
|
|
1792
|
+
emitIdleStatus() {
|
|
1793
|
+
this.emit({ type: "status", status: "idle" });
|
|
1794
|
+
if (this.idleResolver) {
|
|
1795
|
+
this.idleResolver();
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
async sendPrompt(sessionId, prompt) {
|
|
1799
|
+
const promptHasChangeTitle = this.options.hasChangeTitleInstruction?.(prompt) ?? false;
|
|
1800
|
+
this.toolCallCountSincePrompt = 0;
|
|
1801
|
+
this.recentPromptHadChangeTitle = promptHasChangeTitle;
|
|
1802
|
+
if (this.disposed) throw new Error("Backend has been disposed");
|
|
1803
|
+
if (!this.connection || !this.acpSessionId) throw new Error("Session not started");
|
|
1804
|
+
this.emit({ type: "status", status: "running" });
|
|
1805
|
+
this.waitingForResponse = true;
|
|
1806
|
+
try {
|
|
1807
|
+
this.log(`[ACP] Sending prompt (length: ${prompt.length})`);
|
|
1808
|
+
const contentBlock = { type: "text", text: prompt };
|
|
1809
|
+
const promptRequest = {
|
|
1810
|
+
sessionId: this.acpSessionId,
|
|
1811
|
+
prompt: [contentBlock]
|
|
1812
|
+
};
|
|
1813
|
+
await this.connection.prompt(promptRequest);
|
|
1814
|
+
this.log("[ACP] Prompt request sent");
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
this.log("[ACP] Error sending prompt:", error);
|
|
1817
|
+
this.waitingForResponse = false;
|
|
1818
|
+
let errorDetail;
|
|
1819
|
+
if (error instanceof Error) {
|
|
1820
|
+
errorDetail = error.message;
|
|
1821
|
+
} else if (typeof error === "object" && error !== null) {
|
|
1822
|
+
const errObj = error;
|
|
1823
|
+
errorDetail = typeof errObj.message === "string" ? errObj.message : String(error);
|
|
1824
|
+
} else {
|
|
1825
|
+
errorDetail = String(error);
|
|
1826
|
+
}
|
|
1827
|
+
this.emit({ type: "status", status: "error", detail: errorDetail });
|
|
1828
|
+
throw error;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
async waitForResponseComplete(timeoutMs = 12e4) {
|
|
1832
|
+
if (!this.waitingForResponse) return;
|
|
1833
|
+
return new Promise((resolve, reject) => {
|
|
1834
|
+
const timeout = setTimeout(() => {
|
|
1835
|
+
this.idleResolver = null;
|
|
1836
|
+
this.waitingForResponse = false;
|
|
1837
|
+
reject(new Error("Timeout waiting for response to complete"));
|
|
1838
|
+
}, timeoutMs);
|
|
1839
|
+
this.idleResolver = () => {
|
|
1840
|
+
clearTimeout(timeout);
|
|
1841
|
+
this.idleResolver = null;
|
|
1842
|
+
this.waitingForResponse = false;
|
|
1843
|
+
resolve();
|
|
1844
|
+
};
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
async cancel(sessionId) {
|
|
1848
|
+
if (!this.connection || !this.acpSessionId) return;
|
|
1849
|
+
try {
|
|
1850
|
+
await this.connection.cancel({ sessionId: this.acpSessionId });
|
|
1851
|
+
this.emit({ type: "status", status: "stopped", detail: "Cancelled by user" });
|
|
1852
|
+
} catch (error) {
|
|
1853
|
+
this.log("[ACP] Error cancelling:", error);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
async respondToPermission(requestId, approved) {
|
|
1857
|
+
this.log(`[ACP] Permission response (UI only): ${requestId} = ${approved}`);
|
|
1858
|
+
this.emit({ type: "permission-response", id: requestId, approved });
|
|
1859
|
+
}
|
|
1860
|
+
async dispose() {
|
|
1861
|
+
if (this.disposed) return;
|
|
1862
|
+
this.log("[ACP] Disposing backend");
|
|
1863
|
+
this.disposed = true;
|
|
1864
|
+
if (this.connection && this.acpSessionId) {
|
|
1865
|
+
try {
|
|
1866
|
+
await Promise.race([
|
|
1867
|
+
this.connection.cancel({ sessionId: this.acpSessionId }),
|
|
1868
|
+
new Promise((resolve) => setTimeout(resolve, 2e3))
|
|
1869
|
+
]);
|
|
1870
|
+
} catch {
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
if (this.process) {
|
|
1874
|
+
this.process.kill("SIGTERM");
|
|
1875
|
+
await new Promise((resolve) => {
|
|
1876
|
+
const timeout = setTimeout(() => {
|
|
1877
|
+
if (this.process) this.process.kill("SIGKILL");
|
|
1878
|
+
resolve();
|
|
1879
|
+
}, 1e3);
|
|
1880
|
+
this.process?.once("exit", () => {
|
|
1881
|
+
clearTimeout(timeout);
|
|
1882
|
+
resolve();
|
|
1883
|
+
});
|
|
1884
|
+
});
|
|
1885
|
+
this.process = null;
|
|
1886
|
+
}
|
|
1887
|
+
if (this.idleTimeout) {
|
|
1888
|
+
clearTimeout(this.idleTimeout);
|
|
1889
|
+
this.idleTimeout = null;
|
|
1890
|
+
}
|
|
1891
|
+
this.listeners = [];
|
|
1892
|
+
this.connection = null;
|
|
1893
|
+
this.acpSessionId = null;
|
|
1894
|
+
this.activeToolCalls.clear();
|
|
1895
|
+
for (const timeout of this.toolCallTimeouts.values()) clearTimeout(timeout);
|
|
1896
|
+
this.toolCallTimeouts.clear();
|
|
1897
|
+
this.toolCallStartTimes.clear();
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
var acpBackend = /*#__PURE__*/Object.freeze({
|
|
1902
|
+
__proto__: null,
|
|
1903
|
+
AcpBackend: AcpBackend
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
const KNOWN_ACP_AGENTS = {
|
|
1907
|
+
gemini: { command: "gemini", args: ["--experimental-acp"] },
|
|
1908
|
+
opencode: { command: "opencode", args: ["acp"] }
|
|
1909
|
+
};
|
|
1910
|
+
function resolveAcpAgentConfig(cliArgs) {
|
|
1911
|
+
if (cliArgs.length === 0) {
|
|
1912
|
+
throw new Error("Usage: svamp agent <agent-name> or svamp agent -- <command> [args]");
|
|
1913
|
+
}
|
|
1914
|
+
if (cliArgs[0] === "--") {
|
|
1915
|
+
const command = cliArgs[1];
|
|
1916
|
+
if (!command) {
|
|
1917
|
+
throw new Error('Missing command after "--". Usage: svamp agent -- <command> [args]');
|
|
1918
|
+
}
|
|
1919
|
+
return {
|
|
1920
|
+
agentName: command,
|
|
1921
|
+
command,
|
|
1922
|
+
args: cliArgs.slice(2)
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
const agentName = cliArgs[0];
|
|
1926
|
+
const known = KNOWN_ACP_AGENTS[agentName];
|
|
1927
|
+
if (known) {
|
|
1928
|
+
const passthroughArgs = cliArgs.slice(1).filter((arg) => !(agentName === "opencode" && arg === "--acp"));
|
|
1929
|
+
return {
|
|
1930
|
+
agentName,
|
|
1931
|
+
command: known.command,
|
|
1932
|
+
args: [...known.args, ...passthroughArgs]
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
return {
|
|
1936
|
+
agentName,
|
|
1937
|
+
command: agentName,
|
|
1938
|
+
args: cliArgs.slice(1)
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
var acpAgentConfig = /*#__PURE__*/Object.freeze({
|
|
1943
|
+
__proto__: null,
|
|
1944
|
+
KNOWN_ACP_AGENTS: KNOWN_ACP_AGENTS,
|
|
1945
|
+
resolveAcpAgentConfig: resolveAcpAgentConfig
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, log, onTurnEnd) {
|
|
1949
|
+
let pendingText = "";
|
|
1950
|
+
let flushTimer = null;
|
|
1951
|
+
function flushText() {
|
|
1952
|
+
if (pendingText) {
|
|
1953
|
+
sessionService.pushMessage({
|
|
1954
|
+
type: "assistant",
|
|
1955
|
+
content: [{ type: "text", text: pendingText }]
|
|
1956
|
+
}, "agent");
|
|
1957
|
+
pendingText = "";
|
|
1958
|
+
}
|
|
1959
|
+
if (flushTimer) {
|
|
1960
|
+
clearTimeout(flushTimer);
|
|
1961
|
+
flushTimer = null;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
backend.onMessage((msg) => {
|
|
1965
|
+
switch (msg.type) {
|
|
1966
|
+
case "model-output": {
|
|
1967
|
+
if (msg.textDelta) {
|
|
1968
|
+
pendingText += msg.textDelta;
|
|
1969
|
+
if (!flushTimer) {
|
|
1970
|
+
flushTimer = setTimeout(flushText, 100);
|
|
1971
|
+
}
|
|
1972
|
+
} else if (msg.fullText) {
|
|
1973
|
+
flushText();
|
|
1974
|
+
sessionService.pushMessage({
|
|
1975
|
+
type: "assistant",
|
|
1976
|
+
content: [{ type: "text", text: msg.fullText }]
|
|
1977
|
+
}, "agent");
|
|
1978
|
+
}
|
|
1979
|
+
break;
|
|
1980
|
+
}
|
|
1981
|
+
case "status": {
|
|
1982
|
+
if (msg.status === "idle") {
|
|
1983
|
+
flushText();
|
|
1984
|
+
sessionService.sendKeepAlive(false);
|
|
1985
|
+
setMetadata((m) => ({ ...m, lifecycleState: "idle" }));
|
|
1986
|
+
onTurnEnd?.();
|
|
1987
|
+
} else if (msg.status === "running" || msg.status === "starting") {
|
|
1988
|
+
sessionService.sendKeepAlive(true);
|
|
1989
|
+
setMetadata((m) => ({ ...m, lifecycleState: "running" }));
|
|
1990
|
+
} else if (msg.status === "error") {
|
|
1991
|
+
flushText();
|
|
1992
|
+
sessionService.pushMessage({
|
|
1993
|
+
type: "assistant",
|
|
1994
|
+
content: [{ type: "text", text: `Error: ${msg.detail || "Unknown error"}` }]
|
|
1995
|
+
}, "agent");
|
|
1996
|
+
sessionService.sendKeepAlive(false);
|
|
1997
|
+
setMetadata((m) => ({ ...m, lifecycleState: "error" }));
|
|
1998
|
+
} else if (msg.status === "stopped") {
|
|
1999
|
+
flushText();
|
|
2000
|
+
sessionService.sendKeepAlive(false);
|
|
2001
|
+
setMetadata((m) => ({ ...m, lifecycleState: "stopped" }));
|
|
2002
|
+
}
|
|
2003
|
+
break;
|
|
2004
|
+
}
|
|
2005
|
+
case "tool-call": {
|
|
2006
|
+
flushText();
|
|
2007
|
+
sessionService.pushMessage({
|
|
2008
|
+
type: "assistant",
|
|
2009
|
+
content: [{
|
|
2010
|
+
type: "tool_use",
|
|
2011
|
+
id: msg.callId,
|
|
2012
|
+
name: msg.toolName,
|
|
2013
|
+
input: msg.args
|
|
2014
|
+
}]
|
|
2015
|
+
}, "agent");
|
|
2016
|
+
break;
|
|
2017
|
+
}
|
|
2018
|
+
case "tool-result": {
|
|
2019
|
+
sessionService.pushMessage({
|
|
2020
|
+
type: "assistant",
|
|
2021
|
+
content: [{
|
|
2022
|
+
type: "tool_result",
|
|
2023
|
+
tool_use_id: msg.callId,
|
|
2024
|
+
content: typeof msg.result === "string" ? msg.result : JSON.stringify(msg.result)
|
|
2025
|
+
}]
|
|
2026
|
+
}, "agent");
|
|
2027
|
+
break;
|
|
2028
|
+
}
|
|
2029
|
+
case "permission-request": {
|
|
2030
|
+
const currentRequests = { ...sessionService._agentState?.requests };
|
|
2031
|
+
const payload = msg.payload;
|
|
2032
|
+
sessionService.updateAgentState({
|
|
2033
|
+
controlledByUser: false,
|
|
2034
|
+
requests: {
|
|
2035
|
+
...currentRequests,
|
|
2036
|
+
[msg.id]: {
|
|
2037
|
+
tool: payload?.toolName || msg.reason,
|
|
2038
|
+
arguments: payload?.input || {},
|
|
2039
|
+
createdAt: Date.now()
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
break;
|
|
2044
|
+
}
|
|
2045
|
+
case "permission-response": {
|
|
2046
|
+
const reqs = { ...sessionService._agentState?.requests };
|
|
2047
|
+
const completedReqs = { ...sessionService._agentState?.completedRequests };
|
|
2048
|
+
const existingReq = reqs[msg.id];
|
|
2049
|
+
delete reqs[msg.id];
|
|
2050
|
+
sessionService.updateAgentState({
|
|
2051
|
+
controlledByUser: false,
|
|
2052
|
+
requests: reqs,
|
|
2053
|
+
completedRequests: {
|
|
2054
|
+
...completedReqs,
|
|
2055
|
+
[msg.id]: {
|
|
2056
|
+
...existingReq || {},
|
|
2057
|
+
completedAt: Date.now(),
|
|
2058
|
+
status: msg.approved ? "approved" : "denied"
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
break;
|
|
2063
|
+
}
|
|
2064
|
+
case "event": {
|
|
2065
|
+
if (msg.name === "thinking") {
|
|
2066
|
+
const payload = msg.payload;
|
|
2067
|
+
const text = payload?.text || JSON.stringify(payload);
|
|
2068
|
+
sessionService.pushMessage({
|
|
2069
|
+
type: "assistant",
|
|
2070
|
+
content: [{ type: "thinking", thinking: text }]
|
|
2071
|
+
}, "agent");
|
|
2072
|
+
} else {
|
|
2073
|
+
sessionService.pushMessage({
|
|
2074
|
+
type: "session_event",
|
|
2075
|
+
message: `[${msg.name}] ${JSON.stringify(msg.payload)}`
|
|
2076
|
+
}, "session");
|
|
2077
|
+
}
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
class HyphaPermissionHandler {
|
|
2084
|
+
constructor(shouldAutoAllow, log) {
|
|
2085
|
+
this.shouldAutoAllow = shouldAutoAllow;
|
|
2086
|
+
this.log = log;
|
|
2087
|
+
}
|
|
2088
|
+
pendingPermissions = /* @__PURE__ */ new Map();
|
|
2089
|
+
async handleToolCall(toolCallId, toolName, input) {
|
|
2090
|
+
if (this.shouldAutoAllow(toolName, input)) {
|
|
2091
|
+
this.log(`[ACP Permission] Auto-allowing ${toolName}`);
|
|
2092
|
+
return { decision: "approved" };
|
|
2093
|
+
}
|
|
2094
|
+
return new Promise((resolve) => {
|
|
2095
|
+
this.pendingPermissions.set(toolCallId, { resolve, toolName, input });
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* Called from the session service's onPermissionResponse callback
|
|
2100
|
+
*/
|
|
2101
|
+
resolvePermission(requestId, approved) {
|
|
2102
|
+
const pending = this.pendingPermissions.get(requestId);
|
|
2103
|
+
if (pending) {
|
|
2104
|
+
this.pendingPermissions.delete(requestId);
|
|
2105
|
+
pending.resolve({
|
|
2106
|
+
decision: approved ? "approved" : "denied"
|
|
2107
|
+
});
|
|
2108
|
+
} else {
|
|
2109
|
+
this.log(`[ACP Permission] No pending permission for id=${requestId}`);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
/** Reject all pending permissions (e.g. when process exits) */
|
|
2113
|
+
rejectAll(reason) {
|
|
2114
|
+
for (const [id, pending] of this.pendingPermissions) {
|
|
2115
|
+
pending.resolve({ decision: "denied" });
|
|
2116
|
+
}
|
|
2117
|
+
this.pendingPermissions.clear();
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
const GEMINI_TIMEOUTS = {
|
|
2122
|
+
init: 12e4,
|
|
2123
|
+
toolCall: 12e4,
|
|
2124
|
+
investigation: 6e5,
|
|
2125
|
+
think: 3e4,
|
|
2126
|
+
idle: 500
|
|
2127
|
+
};
|
|
2128
|
+
const GEMINI_TOOL_PATTERNS = [
|
|
2129
|
+
{
|
|
2130
|
+
name: "change_title",
|
|
2131
|
+
patterns: ["change_title", "change-title", "svamp__change_title", "mcp__svamp__change_title"],
|
|
2132
|
+
inputFields: ["title"],
|
|
2133
|
+
emptyInputDefault: true
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
name: "set_session_link",
|
|
2137
|
+
patterns: ["set_session_link", "set-session-link", "svamp__set_session_link", "mcp__svamp__set_session_link"],
|
|
2138
|
+
inputFields: ["url"]
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
name: "save_memory",
|
|
2142
|
+
patterns: ["save_memory", "save-memory"],
|
|
2143
|
+
inputFields: ["memory", "content"]
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
name: "think",
|
|
2147
|
+
patterns: ["think"],
|
|
2148
|
+
inputFields: ["thought", "thinking"]
|
|
2149
|
+
}
|
|
2150
|
+
];
|
|
2151
|
+
const AVAILABLE_MODELS = [
|
|
2152
|
+
"gemini-2.5-pro",
|
|
2153
|
+
"gemini-2.5-flash",
|
|
2154
|
+
"gemini-2.5-flash-lite"
|
|
2155
|
+
];
|
|
2156
|
+
class GeminiTransport {
|
|
2157
|
+
agentName = "gemini";
|
|
2158
|
+
getInitTimeout() {
|
|
2159
|
+
return GEMINI_TIMEOUTS.init;
|
|
2160
|
+
}
|
|
2161
|
+
filterStdoutLine(line) {
|
|
2162
|
+
const trimmed = line.trim();
|
|
2163
|
+
if (!trimmed) return null;
|
|
2164
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
|
|
2165
|
+
try {
|
|
2166
|
+
const parsed = JSON.parse(trimmed);
|
|
2167
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
2168
|
+
return line;
|
|
2169
|
+
} catch {
|
|
2170
|
+
return null;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
handleStderr(text, context) {
|
|
2174
|
+
const trimmed = text.trim();
|
|
2175
|
+
if (!trimmed) return { message: null, suppress: true };
|
|
2176
|
+
if (trimmed.includes("status 429") || trimmed.includes('code":429') || trimmed.includes("rateLimitExceeded") || trimmed.includes("RESOURCE_EXHAUSTED")) {
|
|
2177
|
+
return { message: null, suppress: false };
|
|
2178
|
+
}
|
|
2179
|
+
if (trimmed.includes("status 404") || trimmed.includes('code":404')) {
|
|
2180
|
+
const msg = {
|
|
2181
|
+
type: "status",
|
|
2182
|
+
status: "error",
|
|
2183
|
+
detail: `Model not found. Available models: ${AVAILABLE_MODELS.join(", ")}`
|
|
2184
|
+
};
|
|
2185
|
+
return { message: msg };
|
|
2186
|
+
}
|
|
2187
|
+
if (context.hasActiveInvestigation) {
|
|
2188
|
+
const hasError = trimmed.includes("timeout") || trimmed.includes("Timeout") || trimmed.includes("failed") || trimmed.includes("Failed") || trimmed.includes("error") || trimmed.includes("Error");
|
|
2189
|
+
if (hasError) {
|
|
2190
|
+
return { message: null, suppress: false };
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return { message: null };
|
|
2194
|
+
}
|
|
2195
|
+
getToolPatterns() {
|
|
2196
|
+
return GEMINI_TOOL_PATTERNS;
|
|
2197
|
+
}
|
|
2198
|
+
isInvestigationTool(toolCallId, toolKind) {
|
|
2199
|
+
const lowerId = toolCallId.toLowerCase();
|
|
2200
|
+
return lowerId.includes("codebase_investigator") || lowerId.includes("investigator") || typeof toolKind === "string" && toolKind.includes("investigator");
|
|
2201
|
+
}
|
|
2202
|
+
getToolCallTimeout(toolCallId, toolKind) {
|
|
2203
|
+
if (this.isInvestigationTool(toolCallId, toolKind)) return GEMINI_TIMEOUTS.investigation;
|
|
2204
|
+
if (toolKind === "think") return GEMINI_TIMEOUTS.think;
|
|
2205
|
+
return GEMINI_TIMEOUTS.toolCall;
|
|
2206
|
+
}
|
|
2207
|
+
getIdleTimeout() {
|
|
2208
|
+
return GEMINI_TIMEOUTS.idle;
|
|
2209
|
+
}
|
|
2210
|
+
extractToolNameFromId(toolCallId) {
|
|
2211
|
+
const lowerId = toolCallId.toLowerCase();
|
|
2212
|
+
for (const toolPattern of GEMINI_TOOL_PATTERNS) {
|
|
2213
|
+
for (const pattern of toolPattern.patterns) {
|
|
2214
|
+
if (lowerId.includes(pattern.toLowerCase())) {
|
|
2215
|
+
return toolPattern.name;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
return null;
|
|
2220
|
+
}
|
|
2221
|
+
isEmptyInput(input) {
|
|
2222
|
+
if (!input) return true;
|
|
2223
|
+
if (Array.isArray(input)) return input.length === 0;
|
|
2224
|
+
if (typeof input === "object") return Object.keys(input).length === 0;
|
|
2225
|
+
return false;
|
|
2226
|
+
}
|
|
2227
|
+
determineToolName(toolName, toolCallId, input, _context) {
|
|
2228
|
+
if (toolName !== "other" && toolName !== "Unknown tool") return toolName;
|
|
2229
|
+
const idToolName = this.extractToolNameFromId(toolCallId);
|
|
2230
|
+
if (idToolName) return idToolName;
|
|
2231
|
+
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
2232
|
+
const inputKeys = Object.keys(input);
|
|
2233
|
+
for (const toolPattern of GEMINI_TOOL_PATTERNS) {
|
|
2234
|
+
if (toolPattern.inputFields) {
|
|
2235
|
+
const hasMatchingField = toolPattern.inputFields.some(
|
|
2236
|
+
(field) => inputKeys.some((key) => key.toLowerCase() === field.toLowerCase())
|
|
2237
|
+
);
|
|
2238
|
+
if (hasMatchingField) return toolPattern.name;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
if (this.isEmptyInput(input) && toolName === "other") {
|
|
2243
|
+
const defaultTool = GEMINI_TOOL_PATTERNS.find((p) => p.emptyInputDefault);
|
|
2244
|
+
if (defaultTool) return defaultTool.name;
|
|
2245
|
+
}
|
|
2246
|
+
return toolName;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
|
|
2251
|
+
__proto__: null,
|
|
2252
|
+
GEMINI_TIMEOUTS: GEMINI_TIMEOUTS,
|
|
2253
|
+
GeminiTransport: GeminiTransport
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2256
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
2257
|
+
const __dirname$1 = dirname(__filename$1);
|
|
2258
|
+
function loadDotEnv() {
|
|
2259
|
+
const envDir = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
|
|
2260
|
+
const envFile = join$1(envDir, ".env");
|
|
2261
|
+
if (existsSync$1(envFile)) {
|
|
2262
|
+
const lines = readFileSync$1(envFile, "utf-8").split("\n");
|
|
2263
|
+
for (const line of lines) {
|
|
2264
|
+
const trimmed = line.trim();
|
|
2265
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2266
|
+
const eqIdx = trimmed.indexOf("=");
|
|
2267
|
+
if (eqIdx === -1) continue;
|
|
2268
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
2269
|
+
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
2270
|
+
if (!process.env[key]) {
|
|
2271
|
+
process.env[key] = value;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
loadDotEnv();
|
|
2277
|
+
const SVAMP_HOME = process.env.SVAMP_HOME || join$1(os.homedir(), ".svamp");
|
|
2278
|
+
const DAEMON_STATE_FILE = join$1(SVAMP_HOME, "daemon.state.json");
|
|
2279
|
+
const DAEMON_LOCK_FILE = join$1(SVAMP_HOME, "daemon.lock");
|
|
2280
|
+
const LOGS_DIR = join$1(SVAMP_HOME, "logs");
|
|
2281
|
+
const SESSION_INDEX_FILE = join$1(SVAMP_HOME, "sessions-index.json");
|
|
2282
|
+
function loadAgentConfig() {
|
|
2283
|
+
const configPath = join$1(SVAMP_HOME, "agent-config.json");
|
|
2284
|
+
if (existsSync$1(configPath)) {
|
|
2285
|
+
try {
|
|
2286
|
+
return JSON.parse(readFileSync$1(configPath, "utf-8"));
|
|
2287
|
+
} catch {
|
|
2288
|
+
return {};
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return {};
|
|
2292
|
+
}
|
|
2293
|
+
function getSessionSvampDir(directory) {
|
|
2294
|
+
return join$1(directory, ".svamp");
|
|
2295
|
+
}
|
|
2296
|
+
function getSessionDir(directory, sessionId) {
|
|
2297
|
+
return join$1(getSessionSvampDir(directory), sessionId);
|
|
2298
|
+
}
|
|
2299
|
+
function getSessionFilePath(directory, sessionId) {
|
|
2300
|
+
return join$1(getSessionDir(directory, sessionId), "session.json");
|
|
2301
|
+
}
|
|
2302
|
+
function getSessionMessagesPath(directory, sessionId) {
|
|
2303
|
+
return join$1(getSessionDir(directory, sessionId), "messages.jsonl");
|
|
2304
|
+
}
|
|
2305
|
+
function getSvampConfigPath(directory, sessionId) {
|
|
2306
|
+
return join$1(getSessionDir(directory, sessionId), "config.json");
|
|
2307
|
+
}
|
|
2308
|
+
function getLegacySvampConfigPath(directory) {
|
|
2309
|
+
return join$1(getSessionSvampDir(directory), "svamp-config.json");
|
|
2310
|
+
}
|
|
2311
|
+
function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata, sessionService, logger) {
|
|
2312
|
+
const configPath = getSvampConfigPath(directory, sessionId);
|
|
2313
|
+
const legacyConfigPath = getLegacySvampConfigPath(directory);
|
|
2314
|
+
let lastContent = "";
|
|
2315
|
+
let lastLegacyContent = "";
|
|
2316
|
+
if (existsSync$1(configPath)) {
|
|
2317
|
+
try {
|
|
2318
|
+
lastContent = readFileSync$1(configPath, "utf-8");
|
|
2319
|
+
} catch {
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
if (existsSync$1(legacyConfigPath)) {
|
|
2323
|
+
try {
|
|
2324
|
+
lastLegacyContent = readFileSync$1(legacyConfigPath, "utf-8");
|
|
2325
|
+
} catch {
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
function processConfig(config, meta) {
|
|
2329
|
+
if (typeof config.title === "string" && config.title.trim()) {
|
|
2330
|
+
const newTitle = config.title.trim();
|
|
2331
|
+
if (meta.summary?.text !== newTitle) {
|
|
2332
|
+
setMetadata((m) => ({
|
|
2333
|
+
...m,
|
|
2334
|
+
summary: { text: newTitle, updatedAt: Date.now() }
|
|
2335
|
+
}));
|
|
2336
|
+
sessionService.pushMessage({ type: "summary", summary: newTitle }, "session");
|
|
2337
|
+
logger.log(`[svampConfig] Title updated: "${newTitle}"`);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
if (config.session_link && typeof config.session_link.url === "string" && config.session_link.url.trim()) {
|
|
2341
|
+
const url = config.session_link.url.trim();
|
|
2342
|
+
const label = config.session_link.label || "View";
|
|
2343
|
+
if (meta.sessionLink?.url !== url || meta.sessionLink?.label !== label) {
|
|
2344
|
+
setMetadata((m) => ({
|
|
2345
|
+
...m,
|
|
2346
|
+
sessionLink: { url, label, updatedAt: Date.now() }
|
|
2347
|
+
}));
|
|
2348
|
+
const currentSummary = getMetadata().summary?.text;
|
|
2349
|
+
if (currentSummary) {
|
|
2350
|
+
sessionService.pushMessage({ type: "summary", summary: currentSummary }, "session");
|
|
2351
|
+
}
|
|
2352
|
+
logger.log(`[svampConfig] Session link updated: "${label}" \u2192 ${url}`);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return () => {
|
|
2357
|
+
try {
|
|
2358
|
+
const meta = getMetadata();
|
|
2359
|
+
if (existsSync$1(configPath)) {
|
|
2360
|
+
const content = readFileSync$1(configPath, "utf-8");
|
|
2361
|
+
if (content !== lastContent) {
|
|
2362
|
+
lastContent = content;
|
|
2363
|
+
processConfig(JSON.parse(content), meta);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (existsSync$1(legacyConfigPath)) {
|
|
2368
|
+
const content = readFileSync$1(legacyConfigPath, "utf-8");
|
|
2369
|
+
if (content !== lastLegacyContent) {
|
|
2370
|
+
lastLegacyContent = content;
|
|
2371
|
+
processConfig(JSON.parse(content), meta);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
} catch (err) {
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
function loadSessionIndex() {
|
|
2379
|
+
if (!existsSync$1(SESSION_INDEX_FILE)) return {};
|
|
2380
|
+
try {
|
|
2381
|
+
return JSON.parse(readFileSync$1(SESSION_INDEX_FILE, "utf-8"));
|
|
2382
|
+
} catch {
|
|
2383
|
+
return {};
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
function saveSessionIndex(index) {
|
|
2387
|
+
writeFileSync$1(SESSION_INDEX_FILE, JSON.stringify(index, null, 2), "utf-8");
|
|
2388
|
+
}
|
|
2389
|
+
function saveSession(session) {
|
|
2390
|
+
const sessionDir = getSessionDir(session.directory, session.sessionId);
|
|
2391
|
+
if (!existsSync$1(sessionDir)) {
|
|
2392
|
+
mkdirSync$1(sessionDir, { recursive: true });
|
|
2393
|
+
}
|
|
2394
|
+
writeFileSync$1(
|
|
2395
|
+
getSessionFilePath(session.directory, session.sessionId),
|
|
2396
|
+
JSON.stringify(session, null, 2),
|
|
2397
|
+
"utf-8"
|
|
2398
|
+
);
|
|
2399
|
+
const index = loadSessionIndex();
|
|
2400
|
+
index[session.sessionId] = { directory: session.directory, createdAt: session.createdAt };
|
|
2401
|
+
saveSessionIndex(index);
|
|
2402
|
+
}
|
|
2403
|
+
function deletePersistedSession(sessionId) {
|
|
2404
|
+
const index = loadSessionIndex();
|
|
2405
|
+
const entry = index[sessionId];
|
|
2406
|
+
if (entry) {
|
|
2407
|
+
const sessionFile = getSessionFilePath(entry.directory, sessionId);
|
|
2408
|
+
try {
|
|
2409
|
+
if (existsSync$1(sessionFile)) unlinkSync(sessionFile);
|
|
2410
|
+
} catch {
|
|
2411
|
+
}
|
|
2412
|
+
const messagesFile = getSessionMessagesPath(entry.directory, sessionId);
|
|
2413
|
+
try {
|
|
2414
|
+
if (existsSync$1(messagesFile)) unlinkSync(messagesFile);
|
|
2415
|
+
} catch {
|
|
2416
|
+
}
|
|
2417
|
+
const configFile = getSvampConfigPath(entry.directory, sessionId);
|
|
2418
|
+
try {
|
|
2419
|
+
if (existsSync$1(configFile)) unlinkSync(configFile);
|
|
2420
|
+
} catch {
|
|
2421
|
+
}
|
|
2422
|
+
const sessionDir = getSessionDir(entry.directory, sessionId);
|
|
2423
|
+
try {
|
|
2424
|
+
rmdirSync(sessionDir);
|
|
2425
|
+
} catch {
|
|
2426
|
+
}
|
|
2427
|
+
delete index[sessionId];
|
|
2428
|
+
saveSessionIndex(index);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
function loadPersistedSessions() {
|
|
2432
|
+
const sessions = [];
|
|
2433
|
+
const index = loadSessionIndex();
|
|
2434
|
+
for (const [sessionId, entry] of Object.entries(index)) {
|
|
2435
|
+
const filePath = getSessionFilePath(entry.directory, sessionId);
|
|
2436
|
+
try {
|
|
2437
|
+
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
2438
|
+
if (data.sessionId && data.directory) {
|
|
2439
|
+
sessions.push(data);
|
|
2440
|
+
}
|
|
2441
|
+
} catch {
|
|
2442
|
+
delete index[sessionId];
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
saveSessionIndex(index);
|
|
2446
|
+
return sessions;
|
|
2447
|
+
}
|
|
2448
|
+
function ensureHomeDir() {
|
|
2449
|
+
if (!existsSync$1(SVAMP_HOME)) {
|
|
2450
|
+
mkdirSync$1(SVAMP_HOME, { recursive: true });
|
|
2451
|
+
}
|
|
2452
|
+
if (!existsSync$1(LOGS_DIR)) {
|
|
2453
|
+
mkdirSync$1(LOGS_DIR, { recursive: true });
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
function createLogger() {
|
|
2457
|
+
ensureHomeDir();
|
|
2458
|
+
const logFile = join$1(LOGS_DIR, `daemon-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.log`);
|
|
2459
|
+
return {
|
|
2460
|
+
logFilePath: logFile,
|
|
2461
|
+
log: (...args) => {
|
|
2462
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
2463
|
+
`;
|
|
2464
|
+
fs.appendFile(logFile, line).catch(() => {
|
|
2465
|
+
});
|
|
2466
|
+
if (process.env.DEBUG) {
|
|
2467
|
+
console.log(...args);
|
|
2468
|
+
}
|
|
2469
|
+
},
|
|
2470
|
+
error: (...args) => {
|
|
2471
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ERROR: ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
2472
|
+
`;
|
|
2473
|
+
fs.appendFile(logFile, line).catch(() => {
|
|
2474
|
+
});
|
|
2475
|
+
console.error(...args);
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
function writeDaemonStateFile(state) {
|
|
2480
|
+
ensureHomeDir();
|
|
2481
|
+
writeFileSync$1(DAEMON_STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
|
|
2482
|
+
}
|
|
2483
|
+
function readDaemonStateFile() {
|
|
2484
|
+
try {
|
|
2485
|
+
if (!existsSync$1(DAEMON_STATE_FILE)) return null;
|
|
2486
|
+
return JSON.parse(readFileSync$1(DAEMON_STATE_FILE, "utf-8"));
|
|
2487
|
+
} catch {
|
|
2488
|
+
return null;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
function cleanupDaemonStateFile() {
|
|
2492
|
+
try {
|
|
2493
|
+
if (existsSync$1(DAEMON_STATE_FILE)) {
|
|
2494
|
+
fs.unlink(DAEMON_STATE_FILE).catch(() => {
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
if (existsSync$1(DAEMON_LOCK_FILE)) {
|
|
2498
|
+
fs.unlink(DAEMON_LOCK_FILE).catch(() => {
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
} catch {
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
function isDaemonAlive() {
|
|
2505
|
+
const state = readDaemonStateFile();
|
|
2506
|
+
if (!state) return false;
|
|
2507
|
+
try {
|
|
2508
|
+
process.kill(state.pid, 0);
|
|
2509
|
+
return true;
|
|
2510
|
+
} catch {
|
|
2511
|
+
return false;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
async function startDaemon() {
|
|
2515
|
+
const logger = createLogger();
|
|
2516
|
+
const agentConfig = loadAgentConfig();
|
|
2517
|
+
if (Object.keys(agentConfig).length > 0) {
|
|
2518
|
+
logger.log("Loaded agent config:", JSON.stringify(agentConfig));
|
|
2519
|
+
}
|
|
2520
|
+
let shutdownRequested = false;
|
|
2521
|
+
let requestShutdown;
|
|
2522
|
+
const resolvesWhenShutdownRequested = new Promise((resolve2) => {
|
|
2523
|
+
requestShutdown = (source, errorMessage) => {
|
|
2524
|
+
if (shutdownRequested) return;
|
|
2525
|
+
shutdownRequested = true;
|
|
2526
|
+
logger.log(`Requesting shutdown (source: ${source}, errorMessage: ${errorMessage})`);
|
|
2527
|
+
setTimeout(() => {
|
|
2528
|
+
logger.log("Forced exit after timeout");
|
|
2529
|
+
process.exit(1);
|
|
2530
|
+
}, 5e3);
|
|
2531
|
+
resolve2({ source, errorMessage });
|
|
2532
|
+
};
|
|
2533
|
+
});
|
|
2534
|
+
let isReconnecting = false;
|
|
2535
|
+
process.on("SIGINT", () => requestShutdown("os-signal"));
|
|
2536
|
+
process.on("SIGTERM", () => requestShutdown("os-signal"));
|
|
2537
|
+
process.on("uncaughtException", (error) => {
|
|
2538
|
+
if (shutdownRequested) return;
|
|
2539
|
+
logger.error("Uncaught exception:", error);
|
|
2540
|
+
requestShutdown("exception", error.message);
|
|
2541
|
+
});
|
|
2542
|
+
const TRANSIENT_REJECTION_PATTERNS = [
|
|
2543
|
+
"_rintf",
|
|
2544
|
+
// _rintf callback timeouts (app disconnected)
|
|
2545
|
+
"Method call timed out",
|
|
2546
|
+
// RPC timeout during reconnection
|
|
2547
|
+
"WebSocket",
|
|
2548
|
+
// WebSocket errors during reconnect
|
|
2549
|
+
"ECONNRESET",
|
|
2550
|
+
// TCP connection reset
|
|
2551
|
+
"ECONNREFUSED",
|
|
2552
|
+
// Server temporarily unreachable
|
|
2553
|
+
"EPIPE",
|
|
2554
|
+
// Broken pipe (write to closed socket)
|
|
2555
|
+
"ETIMEDOUT",
|
|
2556
|
+
// Connection timed out
|
|
2557
|
+
"socket hang up",
|
|
2558
|
+
// HTTP socket closed unexpectedly
|
|
2559
|
+
"network",
|
|
2560
|
+
// Generic network errors
|
|
2561
|
+
"Connection closed",
|
|
2562
|
+
// WebSocket closed during RPC call
|
|
2563
|
+
"connection was closed",
|
|
2564
|
+
// hypha-rpc connection closed message
|
|
2565
|
+
"Client disconnected",
|
|
2566
|
+
// Hypha client disconnect events
|
|
2567
|
+
"fetch failed"
|
|
2568
|
+
// Fetch API errors during reconnect
|
|
2569
|
+
];
|
|
2570
|
+
let unhandledRejectionCount = 0;
|
|
2571
|
+
let unhandledRejectionResetTimer = null;
|
|
2572
|
+
const UNHANDLED_REJECTION_THRESHOLD = 10;
|
|
2573
|
+
const UNHANDLED_REJECTION_WINDOW_MS = 6e4;
|
|
2574
|
+
process.on("unhandledRejection", (reason) => {
|
|
2575
|
+
if (shutdownRequested) return;
|
|
2576
|
+
const msg = String(reason);
|
|
2577
|
+
logger.error("Unhandled rejection:", reason);
|
|
2578
|
+
const isTransient = TRANSIENT_REJECTION_PATTERNS.some((p) => msg.toLowerCase().includes(p.toLowerCase()));
|
|
2579
|
+
if (isTransient || isReconnecting) {
|
|
2580
|
+
logger.log(`Ignoring transient rejection (reconnecting=${isReconnecting}): ${msg.slice(0, 200)}`);
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
unhandledRejectionCount++;
|
|
2584
|
+
if (!unhandledRejectionResetTimer) {
|
|
2585
|
+
unhandledRejectionResetTimer = setTimeout(() => {
|
|
2586
|
+
unhandledRejectionCount = 0;
|
|
2587
|
+
unhandledRejectionResetTimer = null;
|
|
2588
|
+
}, UNHANDLED_REJECTION_WINDOW_MS);
|
|
2589
|
+
}
|
|
2590
|
+
if (unhandledRejectionCount >= UNHANDLED_REJECTION_THRESHOLD) {
|
|
2591
|
+
logger.log(`Too many unhandled rejections (${unhandledRejectionCount} in ${UNHANDLED_REJECTION_WINDOW_MS / 1e3}s), shutting down`);
|
|
2592
|
+
requestShutdown("exception", msg);
|
|
2593
|
+
} else {
|
|
2594
|
+
logger.log(`Unhandled rejection ${unhandledRejectionCount}/${UNHANDLED_REJECTION_THRESHOLD} (not crashing yet): ${msg.slice(0, 200)}`);
|
|
2595
|
+
}
|
|
2596
|
+
});
|
|
2597
|
+
if (isDaemonAlive()) {
|
|
2598
|
+
console.log("Svamp daemon is already running");
|
|
2599
|
+
process.exit(0);
|
|
2600
|
+
}
|
|
2601
|
+
const hyphaServerUrl = process.env.HYPHA_SERVER_URL;
|
|
2602
|
+
if (!hyphaServerUrl) {
|
|
2603
|
+
console.error("HYPHA_SERVER_URL is required.");
|
|
2604
|
+
console.error('Run "svamp login <server-url>" first, or set it in .env or environment.');
|
|
2605
|
+
process.exit(1);
|
|
2606
|
+
}
|
|
2607
|
+
const hyphaToken = process.env.HYPHA_TOKEN;
|
|
2608
|
+
const hyphaWorkspace = process.env.HYPHA_WORKSPACE;
|
|
2609
|
+
const hyphaClientId = process.env.HYPHA_CLIENT_ID;
|
|
2610
|
+
if (!hyphaToken) {
|
|
2611
|
+
logger.log('Warning: No HYPHA_TOKEN set. Run "svamp login" to authenticate.');
|
|
2612
|
+
logger.log("Connecting anonymously...");
|
|
2613
|
+
}
|
|
2614
|
+
const machineIdFile = join$1(SVAMP_HOME, "machine-id");
|
|
2615
|
+
let machineId = process.env.SVAMP_MACHINE_ID;
|
|
2616
|
+
if (!machineId) {
|
|
2617
|
+
if (existsSync$1(machineIdFile)) {
|
|
2618
|
+
machineId = readFileSync$1(machineIdFile, "utf-8").trim();
|
|
2619
|
+
}
|
|
2620
|
+
if (!machineId) {
|
|
2621
|
+
machineId = `machine-${os.hostname()}-${randomUUID$1().slice(0, 8)}`;
|
|
2622
|
+
try {
|
|
2623
|
+
writeFileSync$1(machineIdFile, machineId, "utf-8");
|
|
2624
|
+
} catch {
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
logger.log("Starting svamp daemon...");
|
|
2629
|
+
logger.log(` Server: ${hyphaServerUrl}`);
|
|
2630
|
+
logger.log(` Workspace: ${hyphaWorkspace || "(default)"}`);
|
|
2631
|
+
logger.log(` Machine ID: ${machineId}`);
|
|
2632
|
+
let server = null;
|
|
2633
|
+
try {
|
|
2634
|
+
logger.log("Connecting to Hypha server...");
|
|
2635
|
+
server = await connectToHypha({
|
|
2636
|
+
serverUrl: hyphaServerUrl,
|
|
2637
|
+
token: hyphaToken,
|
|
2638
|
+
name: `svamp-machine-${machineId}`,
|
|
2639
|
+
...hyphaClientId ? { clientId: hyphaClientId } : {}
|
|
2640
|
+
});
|
|
2641
|
+
logger.log(`Connected to Hypha (workspace: ${server.config.workspace})`);
|
|
2642
|
+
server.on("disconnected", (reason) => {
|
|
2643
|
+
logger.log(`Hypha connection permanently lost: ${reason}`);
|
|
2644
|
+
isReconnecting = false;
|
|
2645
|
+
requestShutdown("hypha-disconnected", String(reason));
|
|
2646
|
+
});
|
|
2647
|
+
server.on("services_registered", () => {
|
|
2648
|
+
if (isReconnecting) {
|
|
2649
|
+
logger.log("Hypha reconnection successful \u2014 services re-registered");
|
|
2650
|
+
isReconnecting = false;
|
|
2651
|
+
}
|
|
2652
|
+
});
|
|
2653
|
+
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
2654
|
+
const getCurrentChildren = () => {
|
|
2655
|
+
return Array.from(pidToTrackedSession.values()).map((s) => ({
|
|
2656
|
+
sessionId: s.svampSessionId || `PID-${s.pid}`,
|
|
2657
|
+
pid: s.pid,
|
|
2658
|
+
startedBy: s.startedBy,
|
|
2659
|
+
directory: s.directory,
|
|
2660
|
+
active: !s.stopped && s.hyphaService != null
|
|
2661
|
+
}));
|
|
2662
|
+
};
|
|
2663
|
+
const spawnSession = async (options) => {
|
|
2664
|
+
logger.log("Spawning session:", JSON.stringify(options));
|
|
2665
|
+
const { directory, approvedNewDirectoryCreation = true, resumeSessionId } = options;
|
|
2666
|
+
if (resumeSessionId) {
|
|
2667
|
+
for (const tracked of pidToTrackedSession.values()) {
|
|
2668
|
+
if (!tracked.stopped && tracked.svampSessionId && tracked.resumeSessionId === resumeSessionId) {
|
|
2669
|
+
logger.log(`Dedup: found existing active session ${tracked.svampSessionId} for resume ID ${resumeSessionId}`);
|
|
2670
|
+
return { type: "success", sessionId: tracked.svampSessionId };
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
try {
|
|
2675
|
+
await fs.access(directory);
|
|
2676
|
+
} catch {
|
|
2677
|
+
if (!approvedNewDirectoryCreation) {
|
|
2678
|
+
return { type: "requestToApproveDirectoryCreation", directory };
|
|
2679
|
+
}
|
|
2680
|
+
try {
|
|
2681
|
+
await fs.mkdir(directory, { recursive: true });
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
return { type: "error", errorMessage: `Failed to create directory: ${err.message}` };
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
const sessionId = options.sessionId || randomUUID$1();
|
|
2687
|
+
const agentName = options.agent || agentConfig.agent_type || "claude";
|
|
2688
|
+
if (agentName !== "claude" && KNOWN_ACP_AGENTS[agentName]) {
|
|
2689
|
+
return await spawnAcpSession(sessionId, directory, agentName, options, resumeSessionId);
|
|
2690
|
+
}
|
|
2691
|
+
try {
|
|
2692
|
+
let parseBashPermission2 = function(permission) {
|
|
2693
|
+
if (permission === "Bash") return;
|
|
2694
|
+
const match = permission.match(/^Bash\((.+?)\)$/);
|
|
2695
|
+
if (!match) return;
|
|
2696
|
+
const command = match[1];
|
|
2697
|
+
if (command.endsWith(":*")) {
|
|
2698
|
+
allowedBashPrefixes.add(command.slice(0, -2));
|
|
2699
|
+
} else {
|
|
2700
|
+
allowedBashLiterals.add(command);
|
|
2701
|
+
}
|
|
2702
|
+
}, shouldAutoAllow2 = function(toolName, toolInput) {
|
|
2703
|
+
if (toolName === "Bash") {
|
|
2704
|
+
const inputObj = toolInput;
|
|
2705
|
+
if (inputObj?.command) {
|
|
2706
|
+
if (allowedBashLiterals.has(inputObj.command)) return true;
|
|
2707
|
+
for (const prefix of allowedBashPrefixes) {
|
|
2708
|
+
if (inputObj.command.startsWith(prefix)) return true;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
} else if (allowedTools.has(toolName)) {
|
|
2712
|
+
return true;
|
|
2713
|
+
}
|
|
2714
|
+
if (currentPermissionMode === "bypassPermissions") return true;
|
|
2715
|
+
if (currentPermissionMode === "acceptEdits" && EDIT_TOOLS.has(toolName)) return true;
|
|
2716
|
+
return false;
|
|
2717
|
+
}, killAndWaitForExit2 = function(proc, signal = "SIGTERM", timeoutMs = 1e4) {
|
|
2718
|
+
return new Promise((resolve2) => {
|
|
2719
|
+
if (!proc || proc.exitCode !== null) {
|
|
2720
|
+
resolve2();
|
|
2721
|
+
return;
|
|
2722
|
+
}
|
|
2723
|
+
const timeout = setTimeout(() => {
|
|
2724
|
+
try {
|
|
2725
|
+
proc.kill("SIGKILL");
|
|
2726
|
+
} catch {
|
|
2727
|
+
}
|
|
2728
|
+
resolve2();
|
|
2729
|
+
}, timeoutMs);
|
|
2730
|
+
proc.on("exit", () => {
|
|
2731
|
+
clearTimeout(timeout);
|
|
2732
|
+
resolve2();
|
|
2733
|
+
});
|
|
2734
|
+
if (!proc.killed) {
|
|
2735
|
+
proc.kill(signal);
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
};
|
|
2739
|
+
var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2, killAndWaitForExit = killAndWaitForExit2;
|
|
2740
|
+
let sessionMetadata = {
|
|
2741
|
+
path: directory,
|
|
2742
|
+
host: os.hostname(),
|
|
2743
|
+
version: "0.1.0",
|
|
2744
|
+
machineId,
|
|
2745
|
+
homeDir: os.homedir(),
|
|
2746
|
+
svampHomeDir: SVAMP_HOME,
|
|
2747
|
+
svampLibDir: join$1(__dirname$1, ".."),
|
|
2748
|
+
svampToolsDir: join$1(__dirname$1, "..", "tools"),
|
|
2749
|
+
startedFromDaemon: true,
|
|
2750
|
+
startedBy: "daemon",
|
|
2751
|
+
lifecycleState: resumeSessionId ? "idle" : "starting"
|
|
2752
|
+
};
|
|
2753
|
+
let claudeProcess = null;
|
|
2754
|
+
const allPersisted = loadPersistedSessions();
|
|
2755
|
+
const persisted = allPersisted.find((p) => p.sessionId === sessionId) || (resumeSessionId ? allPersisted.find((p) => p.claudeResumeId === resumeSessionId) : void 0);
|
|
2756
|
+
let claudeResumeId = persisted?.claudeResumeId || (resumeSessionId || void 0);
|
|
2757
|
+
let currentPermissionMode = persisted?.permissionMode || "default";
|
|
2758
|
+
if (claudeResumeId) {
|
|
2759
|
+
sessionMetadata = { ...sessionMetadata, claudeSessionId: claudeResumeId };
|
|
2760
|
+
}
|
|
2761
|
+
if (persisted && persisted.sessionId !== sessionId) {
|
|
2762
|
+
const oldDir = persisted.directory || directory;
|
|
2763
|
+
const newSessionDir = getSessionDir(directory, sessionId);
|
|
2764
|
+
if (!existsSync$1(newSessionDir)) mkdirSync$1(newSessionDir, { recursive: true });
|
|
2765
|
+
const oldMsgFile = getSessionMessagesPath(oldDir, persisted.sessionId);
|
|
2766
|
+
const newMsgFile = getSessionMessagesPath(directory, sessionId);
|
|
2767
|
+
try {
|
|
2768
|
+
if (existsSync$1(oldMsgFile) && !existsSync$1(newMsgFile)) {
|
|
2769
|
+
copyFileSync(oldMsgFile, newMsgFile);
|
|
2770
|
+
logger.log(`[Session ${sessionId}] Copied messages from old session ${persisted.sessionId}`);
|
|
2771
|
+
}
|
|
2772
|
+
} catch (err) {
|
|
2773
|
+
logger.log(`[Session ${sessionId}] Failed to copy messages: ${err.message}`);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
const allowedTools = /* @__PURE__ */ new Set();
|
|
2777
|
+
const allowedBashLiterals = /* @__PURE__ */ new Set();
|
|
2778
|
+
const allowedBashPrefixes = /* @__PURE__ */ new Set();
|
|
2779
|
+
const EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "Write", "NotebookEdit"]);
|
|
2780
|
+
const pendingPermissions = /* @__PURE__ */ new Map();
|
|
2781
|
+
let backgroundTaskCount = 0;
|
|
2782
|
+
let backgroundTaskNames = [];
|
|
2783
|
+
let userMessagePending = false;
|
|
2784
|
+
let turnInitiatedByUser = true;
|
|
2785
|
+
let isKillingClaude = false;
|
|
2786
|
+
let checkSvampConfig;
|
|
2787
|
+
const spawnClaude = (initialMessage, meta) => {
|
|
2788
|
+
const permissionMode = meta?.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
2789
|
+
currentPermissionMode = permissionMode;
|
|
2790
|
+
const model = meta?.model || agentConfig.default_model || void 0;
|
|
2791
|
+
const appendSystemPrompt = meta?.appendSystemPrompt || agentConfig.append_system_prompt || void 0;
|
|
2792
|
+
const args = [
|
|
2793
|
+
"--output-format",
|
|
2794
|
+
"stream-json",
|
|
2795
|
+
"--input-format",
|
|
2796
|
+
"stream-json",
|
|
2797
|
+
"--verbose",
|
|
2798
|
+
"--permission-prompt-tool",
|
|
2799
|
+
"stdio",
|
|
2800
|
+
"--permission-mode",
|
|
2801
|
+
permissionMode
|
|
2802
|
+
];
|
|
2803
|
+
if (model) args.push("--model", model);
|
|
2804
|
+
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
2805
|
+
if (claudeResumeId) args.push("--resume", claudeResumeId);
|
|
2806
|
+
logger.log(`[Session ${sessionId}] Spawning Claude: claude ${args.join(" ")} (cwd: ${directory})`);
|
|
2807
|
+
const spawnEnv = { ...process.env };
|
|
2808
|
+
delete spawnEnv.CLAUDECODE;
|
|
2809
|
+
const child = spawn$1("claude", args, {
|
|
2810
|
+
cwd: directory,
|
|
2811
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2812
|
+
env: spawnEnv,
|
|
2813
|
+
shell: process.platform === "win32"
|
|
2814
|
+
});
|
|
2815
|
+
claudeProcess = child;
|
|
2816
|
+
logger.log(`[Session ${sessionId}] Claude PID: ${child.pid}, stdin: ${!!child.stdin}, stdout: ${!!child.stdout}, stderr: ${!!child.stderr}`);
|
|
2817
|
+
child.on("error", (err) => {
|
|
2818
|
+
logger.log(`[Session ${sessionId}] Claude process error: ${err.message}`);
|
|
2819
|
+
sessionService.pushMessage({
|
|
2820
|
+
type: "assistant",
|
|
2821
|
+
content: [{
|
|
2822
|
+
type: "text",
|
|
2823
|
+
text: `Error: Failed to start Claude Code CLI: ${err.message}
|
|
2824
|
+
|
|
2825
|
+
Please ensure Claude Code CLI is installed on this machine. You can install it with:
|
|
2826
|
+
\`npm install -g @anthropic-ai/claude-code\``
|
|
2827
|
+
}]
|
|
2828
|
+
}, "agent");
|
|
2829
|
+
sessionService.sendKeepAlive(false);
|
|
2830
|
+
});
|
|
2831
|
+
let stdoutBuffer = "";
|
|
2832
|
+
child.stdout?.on("data", (chunk) => {
|
|
2833
|
+
stdoutBuffer += chunk.toString();
|
|
2834
|
+
const lines = stdoutBuffer.split("\n");
|
|
2835
|
+
stdoutBuffer = lines.pop() || "";
|
|
2836
|
+
for (const line of lines) {
|
|
2837
|
+
if (!line.trim()) continue;
|
|
2838
|
+
logger.log(`[Session ${sessionId}] stdout line (${line.length} chars): ${line.slice(0, 100)}`);
|
|
2839
|
+
try {
|
|
2840
|
+
const msg = JSON.parse(line);
|
|
2841
|
+
logger.log(`[Session ${sessionId}] Parsed type=${msg.type} subtype=${msg.subtype || "n/a"}`);
|
|
2842
|
+
if (msg.type === "control_request" && msg.request?.subtype === "can_use_tool") {
|
|
2843
|
+
const requestId = msg.request_id;
|
|
2844
|
+
const toolName = msg.request.tool_name;
|
|
2845
|
+
const toolInput = msg.request.input;
|
|
2846
|
+
logger.log(`[Session ${sessionId}] Permission request: ${requestId} tool=${toolName}`);
|
|
2847
|
+
if (shouldAutoAllow2(toolName, toolInput)) {
|
|
2848
|
+
logger.log(`[Session ${sessionId}] Auto-allowing ${toolName} (mode=${currentPermissionMode})`);
|
|
2849
|
+
if (claudeProcess && !claudeProcess.killed && claudeProcess.stdin) {
|
|
2850
|
+
const controlResponse = JSON.stringify({
|
|
2851
|
+
type: "control_response",
|
|
2852
|
+
response: {
|
|
2853
|
+
subtype: "success",
|
|
2854
|
+
request_id: requestId,
|
|
2855
|
+
response: { behavior: "allow", updatedInput: toolInput || {} }
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
claudeProcess.stdin.write(controlResponse + "\n");
|
|
2859
|
+
}
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
const permissionPromise = new Promise((resolve2) => {
|
|
2863
|
+
pendingPermissions.set(requestId, { resolve: resolve2, toolName, input: toolInput });
|
|
2864
|
+
});
|
|
2865
|
+
const currentRequests = { ...sessionService._agentState?.requests };
|
|
2866
|
+
sessionService.updateAgentState({
|
|
2867
|
+
controlledByUser: false,
|
|
2868
|
+
requests: {
|
|
2869
|
+
...currentRequests,
|
|
2870
|
+
[requestId]: {
|
|
2871
|
+
tool: toolName,
|
|
2872
|
+
arguments: toolInput,
|
|
2873
|
+
createdAt: Date.now()
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2877
|
+
permissionPromise.then((result) => {
|
|
2878
|
+
if (claudeProcess && !claudeProcess.killed && claudeProcess.stdin) {
|
|
2879
|
+
const controlResponse = JSON.stringify({
|
|
2880
|
+
type: "control_response",
|
|
2881
|
+
response: {
|
|
2882
|
+
subtype: "success",
|
|
2883
|
+
request_id: requestId,
|
|
2884
|
+
response: result
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2887
|
+
logger.log(`[Session ${sessionId}] Sending control_response: ${controlResponse.slice(0, 200)}`);
|
|
2888
|
+
claudeProcess.stdin.write(controlResponse + "\n");
|
|
2889
|
+
}
|
|
2890
|
+
const reqs = { ...sessionService._agentState?.requests };
|
|
2891
|
+
delete reqs[requestId];
|
|
2892
|
+
sessionService.updateAgentState({
|
|
2893
|
+
controlledByUser: false,
|
|
2894
|
+
requests: reqs,
|
|
2895
|
+
completedRequests: {
|
|
2896
|
+
...sessionService._agentState?.completedRequests,
|
|
2897
|
+
[requestId]: {
|
|
2898
|
+
tool: toolName,
|
|
2899
|
+
arguments: toolInput,
|
|
2900
|
+
completedAt: Date.now(),
|
|
2901
|
+
status: result.behavior === "allow" ? "approved" : "denied"
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2905
|
+
}).catch((err) => {
|
|
2906
|
+
logger.log(`[Session ${sessionId}] Permission handler error (request ${requestId}): ${err}`);
|
|
2907
|
+
});
|
|
2908
|
+
} else if (msg.type === "control_cancel_request") {
|
|
2909
|
+
const requestId = msg.request_id;
|
|
2910
|
+
logger.log(`[Session ${sessionId}] Permission cancel: ${requestId}`);
|
|
2911
|
+
const pending = pendingPermissions.get(requestId);
|
|
2912
|
+
if (pending) {
|
|
2913
|
+
pending.resolve({ behavior: "deny", message: "Cancelled" });
|
|
2914
|
+
pendingPermissions.delete(requestId);
|
|
2915
|
+
}
|
|
2916
|
+
} else if (msg.type === "control_response") {
|
|
2917
|
+
logger.log(`[Session ${sessionId}] Control response: ${JSON.stringify(msg).slice(0, 200)}`);
|
|
2918
|
+
} else if (msg.type === "assistant" || msg.type === "result") {
|
|
2919
|
+
if (msg.type === "assistant" && Array.isArray(msg.content)) {
|
|
2920
|
+
for (const block of msg.content) {
|
|
2921
|
+
if (block.type === "tool_use" && block.input?.run_in_background === true) {
|
|
2922
|
+
backgroundTaskCount++;
|
|
2923
|
+
const label = block.tool_name || block.name || "unknown";
|
|
2924
|
+
backgroundTaskNames.push(label);
|
|
2925
|
+
logger.log(`[Session ${sessionId}] Background task launched: ${label} (count=${backgroundTaskCount})`);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
if (msg.type === "result") {
|
|
2930
|
+
if (!turnInitiatedByUser) {
|
|
2931
|
+
logger.log(`[Session ${sessionId}] Skipping stale result from SDK-initiated turn`);
|
|
2932
|
+
const hasBackgroundTasks = backgroundTaskCount > 0;
|
|
2933
|
+
if (hasBackgroundTasks) {
|
|
2934
|
+
const taskInfo = `Background tasks still running (${backgroundTaskCount}): ${backgroundTaskNames.join(", ")}`;
|
|
2935
|
+
sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
|
|
2936
|
+
}
|
|
2937
|
+
sessionService.sendKeepAlive(false);
|
|
2938
|
+
turnInitiatedByUser = true;
|
|
2939
|
+
continue;
|
|
2940
|
+
}
|
|
2941
|
+
sessionService.sendKeepAlive(false);
|
|
2942
|
+
checkSvampConfig?.();
|
|
2943
|
+
if (backgroundTaskCount > 0) {
|
|
2944
|
+
const taskInfo = `Background tasks still running (${backgroundTaskCount}): ${backgroundTaskNames.join(", ")}`;
|
|
2945
|
+
logger.log(`[Session ${sessionId}] ${taskInfo}`);
|
|
2946
|
+
sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
sessionService.pushMessage(msg, "agent");
|
|
2950
|
+
if (msg.session_id) {
|
|
2951
|
+
claudeResumeId = msg.session_id;
|
|
2952
|
+
}
|
|
2953
|
+
} else if (msg.type === "system" && msg.subtype === "init") {
|
|
2954
|
+
if (!userMessagePending) {
|
|
2955
|
+
turnInitiatedByUser = false;
|
|
2956
|
+
logger.log(`[Session ${sessionId}] SDK-initiated turn (likely stale task_notification)`);
|
|
2957
|
+
}
|
|
2958
|
+
userMessagePending = false;
|
|
2959
|
+
if (msg.session_id) {
|
|
2960
|
+
claudeResumeId = msg.session_id;
|
|
2961
|
+
sessionService.updateMetadata({
|
|
2962
|
+
...sessionMetadata,
|
|
2963
|
+
claudeSessionId: msg.session_id
|
|
2964
|
+
});
|
|
2965
|
+
saveSession({
|
|
2966
|
+
sessionId,
|
|
2967
|
+
directory,
|
|
2968
|
+
claudeResumeId,
|
|
2969
|
+
permissionMode: currentPermissionMode,
|
|
2970
|
+
metadata: sessionMetadata,
|
|
2971
|
+
createdAt: Date.now(),
|
|
2972
|
+
machineId
|
|
2973
|
+
});
|
|
2974
|
+
artifactSync.scheduleDebouncedSync(sessionId, getSessionDir(directory, sessionId), sessionMetadata, machineId);
|
|
2975
|
+
}
|
|
2976
|
+
sessionService.pushMessage(msg, "session");
|
|
2977
|
+
} else if (msg.type === "system" && msg.subtype === "task_notification" && msg.status === "completed") {
|
|
2978
|
+
backgroundTaskCount = Math.max(0, backgroundTaskCount - 1);
|
|
2979
|
+
if (backgroundTaskNames.length > 0) {
|
|
2980
|
+
const completed = backgroundTaskNames.shift();
|
|
2981
|
+
logger.log(`[Session ${sessionId}] Background task completed: ${completed} (remaining=${backgroundTaskCount})`);
|
|
2982
|
+
}
|
|
2983
|
+
sessionService.pushMessage(msg, "agent");
|
|
2984
|
+
} else {
|
|
2985
|
+
sessionService.pushMessage(msg, "agent");
|
|
2986
|
+
}
|
|
2987
|
+
} catch {
|
|
2988
|
+
logger.log(`[Session ${sessionId}] Claude stdout (non-JSON): ${line}`);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
});
|
|
2992
|
+
child.stderr?.on("data", (chunk) => {
|
|
2993
|
+
logger.log(`[Session ${sessionId}] Claude stderr: ${chunk.toString().trim()}`);
|
|
2994
|
+
});
|
|
2995
|
+
child.on("exit", (code, signal) => {
|
|
2996
|
+
logger.log(`[Session ${sessionId}] Claude exited: code=${code}, signal=${signal}`);
|
|
2997
|
+
claudeProcess = null;
|
|
2998
|
+
for (const [id, pending] of pendingPermissions) {
|
|
2999
|
+
pending.resolve({ behavior: "deny", message: "Claude process exited" });
|
|
3000
|
+
}
|
|
3001
|
+
pendingPermissions.clear();
|
|
3002
|
+
if (code !== 0 && code !== null && !claudeResumeId) {
|
|
3003
|
+
sessionService.pushMessage({
|
|
3004
|
+
type: "assistant",
|
|
3005
|
+
content: [{
|
|
3006
|
+
type: "text",
|
|
3007
|
+
text: `Error: Claude process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}.
|
|
3008
|
+
|
|
3009
|
+
This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
3010
|
+
}]
|
|
3011
|
+
}, "agent");
|
|
3012
|
+
}
|
|
3013
|
+
sessionService.updateMetadata({
|
|
3014
|
+
...sessionMetadata,
|
|
3015
|
+
lifecycleState: claudeResumeId ? "idle" : "stopped"
|
|
3016
|
+
});
|
|
3017
|
+
sessionService.sendKeepAlive(false);
|
|
3018
|
+
if (claudeResumeId && !trackedSession.stopped) {
|
|
3019
|
+
saveSession({
|
|
3020
|
+
sessionId,
|
|
3021
|
+
directory,
|
|
3022
|
+
claudeResumeId,
|
|
3023
|
+
permissionMode: currentPermissionMode,
|
|
3024
|
+
metadata: sessionMetadata,
|
|
3025
|
+
createdAt: Date.now(),
|
|
3026
|
+
machineId
|
|
3027
|
+
});
|
|
3028
|
+
artifactSync.syncSession(sessionId, getSessionDir(directory, sessionId), sessionMetadata, machineId).catch(() => {
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
if (initialMessage && child.stdin) {
|
|
3033
|
+
const stdinMsg = JSON.stringify({
|
|
3034
|
+
type: "user",
|
|
3035
|
+
message: { role: "user", content: initialMessage }
|
|
3036
|
+
});
|
|
3037
|
+
child.stdin.write(stdinMsg + "\n");
|
|
3038
|
+
}
|
|
3039
|
+
return child;
|
|
3040
|
+
};
|
|
3041
|
+
const sessionService = await registerSessionService(
|
|
3042
|
+
server,
|
|
3043
|
+
sessionId,
|
|
3044
|
+
sessionMetadata,
|
|
3045
|
+
{ controlledByUser: false },
|
|
3046
|
+
{
|
|
3047
|
+
onUserMessage: (content, meta) => {
|
|
3048
|
+
logger.log(`[Session ${sessionId}] User message received`);
|
|
3049
|
+
userMessagePending = true;
|
|
3050
|
+
turnInitiatedByUser = true;
|
|
3051
|
+
let text;
|
|
3052
|
+
let msgMeta = meta;
|
|
3053
|
+
try {
|
|
3054
|
+
let parsed = typeof content === "string" ? JSON.parse(content) : content;
|
|
3055
|
+
if (parsed?.content && typeof parsed.content === "string") {
|
|
3056
|
+
try {
|
|
3057
|
+
const inner = JSON.parse(parsed.content);
|
|
3058
|
+
if (inner && typeof inner === "object") parsed = inner;
|
|
3059
|
+
} catch {
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
text = parsed?.content?.text || parsed?.text || (typeof parsed === "string" ? parsed : JSON.stringify(parsed));
|
|
3063
|
+
if (parsed?.meta) msgMeta = { ...msgMeta, ...parsed.meta };
|
|
3064
|
+
} catch {
|
|
3065
|
+
text = typeof content === "string" ? content : JSON.stringify(content);
|
|
3066
|
+
}
|
|
3067
|
+
if (msgMeta?.permissionMode) {
|
|
3068
|
+
currentPermissionMode = msgMeta.permissionMode;
|
|
3069
|
+
logger.log(`[Session ${sessionId}] Permission mode updated to: ${currentPermissionMode}`);
|
|
3070
|
+
}
|
|
3071
|
+
if (isKillingClaude) {
|
|
3072
|
+
logger.log(`[Session ${sessionId}] Message received while restarting Claude, ignoring to prevent ghost process`);
|
|
3073
|
+
sessionService.sendKeepAlive(false);
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
3077
|
+
spawnClaude(text, msgMeta);
|
|
3078
|
+
} else {
|
|
3079
|
+
const stdinMsg = JSON.stringify({
|
|
3080
|
+
type: "user",
|
|
3081
|
+
message: { role: "user", content: text }
|
|
3082
|
+
});
|
|
3083
|
+
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
3084
|
+
}
|
|
3085
|
+
sessionService.sendKeepAlive(true);
|
|
3086
|
+
},
|
|
3087
|
+
onAbort: () => {
|
|
3088
|
+
logger.log(`[Session ${sessionId}] Abort requested`);
|
|
3089
|
+
if (claudeProcess && !claudeProcess.killed) {
|
|
3090
|
+
claudeProcess.kill("SIGINT");
|
|
3091
|
+
}
|
|
3092
|
+
},
|
|
3093
|
+
onPermissionResponse: (params) => {
|
|
3094
|
+
logger.log(`[Session ${sessionId}] Permission response:`, JSON.stringify(params));
|
|
3095
|
+
const requestId = params.id;
|
|
3096
|
+
const pending = pendingPermissions.get(requestId);
|
|
3097
|
+
if (params.mode) {
|
|
3098
|
+
logger.log(`[Session ${sessionId}] Permission mode changed to: ${params.mode}`);
|
|
3099
|
+
currentPermissionMode = params.mode;
|
|
3100
|
+
}
|
|
3101
|
+
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
3102
|
+
for (const tool of params.allowTools) {
|
|
3103
|
+
if (tool.startsWith("Bash(") || tool === "Bash") {
|
|
3104
|
+
parseBashPermission2(tool);
|
|
3105
|
+
} else {
|
|
3106
|
+
allowedTools.add(tool);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
logger.log(`[Session ${sessionId}] Updated allowed tools: ${[...allowedTools].join(", ")}`);
|
|
3110
|
+
}
|
|
3111
|
+
if (pending) {
|
|
3112
|
+
pendingPermissions.delete(requestId);
|
|
3113
|
+
if (params.approved) {
|
|
3114
|
+
pending.resolve({
|
|
3115
|
+
behavior: "allow",
|
|
3116
|
+
updatedInput: pending.input || {}
|
|
3117
|
+
});
|
|
3118
|
+
} else {
|
|
3119
|
+
pending.resolve({
|
|
3120
|
+
behavior: "deny",
|
|
3121
|
+
message: params.reason || "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed."
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
} else {
|
|
3125
|
+
logger.log(`[Session ${sessionId}] No pending permission for id=${requestId}`);
|
|
3126
|
+
}
|
|
3127
|
+
},
|
|
3128
|
+
onSwitchMode: async (mode) => {
|
|
3129
|
+
logger.log(`[Session ${sessionId}] Switch mode: ${mode}`);
|
|
3130
|
+
currentPermissionMode = mode;
|
|
3131
|
+
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
3132
|
+
isKillingClaude = true;
|
|
3133
|
+
await killAndWaitForExit2(claudeProcess);
|
|
3134
|
+
isKillingClaude = false;
|
|
3135
|
+
spawnClaude(void 0, { permissionMode: mode });
|
|
3136
|
+
}
|
|
3137
|
+
},
|
|
3138
|
+
onRestartClaude: async () => {
|
|
3139
|
+
logger.log(`[Session ${sessionId}] Restart Claude requested`);
|
|
3140
|
+
try {
|
|
3141
|
+
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
3142
|
+
isKillingClaude = true;
|
|
3143
|
+
sessionService.updateMetadata({
|
|
3144
|
+
...sessionMetadata,
|
|
3145
|
+
lifecycleState: "restarting"
|
|
3146
|
+
});
|
|
3147
|
+
await killAndWaitForExit2(claudeProcess);
|
|
3148
|
+
isKillingClaude = false;
|
|
3149
|
+
}
|
|
3150
|
+
if (claudeResumeId) {
|
|
3151
|
+
spawnClaude(void 0, { permissionMode: currentPermissionMode });
|
|
3152
|
+
logger.log(`[Session ${sessionId}] Claude respawned with --resume ${claudeResumeId}`);
|
|
3153
|
+
return { success: true, message: "Claude process restarted successfully." };
|
|
3154
|
+
} else {
|
|
3155
|
+
logger.log(`[Session ${sessionId}] No resume ID \u2014 cannot restart`);
|
|
3156
|
+
return { success: false, message: "No session to resume. Send a message to start a new session." };
|
|
3157
|
+
}
|
|
3158
|
+
} catch (err) {
|
|
3159
|
+
isKillingClaude = false;
|
|
3160
|
+
logger.log(`[Session ${sessionId}] Restart failed: ${err.message}`);
|
|
3161
|
+
return { success: false, message: `Restart failed: ${err.message}` };
|
|
3162
|
+
}
|
|
3163
|
+
},
|
|
3164
|
+
onKillSession: () => {
|
|
3165
|
+
logger.log(`[Session ${sessionId}] Kill session requested`);
|
|
3166
|
+
stopSession(sessionId);
|
|
3167
|
+
},
|
|
3168
|
+
onBash: async (command, cwd) => {
|
|
3169
|
+
logger.log(`[Session ${sessionId}] Bash: ${command} (cwd: ${cwd || directory})`);
|
|
3170
|
+
const { exec } = await import('child_process');
|
|
3171
|
+
return new Promise((resolve2) => {
|
|
3172
|
+
exec(command, { cwd: cwd || directory, timeout: 3e4, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3173
|
+
if (err) {
|
|
3174
|
+
resolve2({ success: false, stdout: stdout || "", stderr: stderr || err.message, exitCode: err.code ?? 1 });
|
|
3175
|
+
} else {
|
|
3176
|
+
resolve2({ success: true, stdout, stderr: stderr || "", exitCode: 0 });
|
|
3177
|
+
}
|
|
3178
|
+
});
|
|
3179
|
+
});
|
|
3180
|
+
},
|
|
3181
|
+
onRipgrep: async (args, cwd) => {
|
|
3182
|
+
const { exec } = await import('child_process');
|
|
3183
|
+
const rgCwd = cwd || directory;
|
|
3184
|
+
return new Promise((resolve2, reject) => {
|
|
3185
|
+
exec(`rg ${args}`, { cwd: rgCwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
|
|
3186
|
+
if (err && !stdout) {
|
|
3187
|
+
reject(new Error(err.message));
|
|
3188
|
+
} else {
|
|
3189
|
+
resolve2(stdout || "");
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
});
|
|
3193
|
+
},
|
|
3194
|
+
onReadFile: async (path) => {
|
|
3195
|
+
const resolvedPath = resolve(directory, path);
|
|
3196
|
+
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
3197
|
+
throw new Error("Path outside working directory");
|
|
3198
|
+
}
|
|
3199
|
+
const buffer = await fs.readFile(resolvedPath);
|
|
3200
|
+
return buffer.toString("base64");
|
|
3201
|
+
},
|
|
3202
|
+
onWriteFile: async (path, content) => {
|
|
3203
|
+
const resolvedPath = resolve(directory, path);
|
|
3204
|
+
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
3205
|
+
throw new Error("Path outside working directory");
|
|
3206
|
+
}
|
|
3207
|
+
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
3208
|
+
await fs.writeFile(resolvedPath, Buffer.from(content, "base64"));
|
|
3209
|
+
},
|
|
3210
|
+
onListDirectory: async (path) => {
|
|
3211
|
+
const resolvedDir = resolve(directory, path || ".");
|
|
3212
|
+
if (!resolvedDir.startsWith(resolve(directory))) {
|
|
3213
|
+
throw new Error("Path outside working directory");
|
|
3214
|
+
}
|
|
3215
|
+
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
|
|
3216
|
+
return entries.map((e) => ({ name: e.name, isDirectory: e.isDirectory() }));
|
|
3217
|
+
},
|
|
3218
|
+
onGetDirectoryTree: async (treePath, maxDepth) => {
|
|
3219
|
+
async function buildTree(p, name, depth) {
|
|
3220
|
+
try {
|
|
3221
|
+
const stats = await fs.stat(p);
|
|
3222
|
+
const node = {
|
|
3223
|
+
name,
|
|
3224
|
+
path: p,
|
|
3225
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
3226
|
+
size: stats.size,
|
|
3227
|
+
modified: stats.mtime.getTime()
|
|
3228
|
+
};
|
|
3229
|
+
if (stats.isDirectory() && depth < maxDepth) {
|
|
3230
|
+
const entries = await fs.readdir(p, { withFileTypes: true });
|
|
3231
|
+
const children = [];
|
|
3232
|
+
await Promise.all(entries.map(async (entry) => {
|
|
3233
|
+
if (entry.isSymbolicLink()) return;
|
|
3234
|
+
const childPath = join$1(p, entry.name);
|
|
3235
|
+
const childNode = await buildTree(childPath, entry.name, depth + 1);
|
|
3236
|
+
if (childNode) children.push(childNode);
|
|
3237
|
+
}));
|
|
3238
|
+
children.sort((a, b) => {
|
|
3239
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
3240
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
3241
|
+
return a.name.localeCompare(b.name);
|
|
3242
|
+
});
|
|
3243
|
+
node.children = children;
|
|
3244
|
+
}
|
|
3245
|
+
return node;
|
|
3246
|
+
} catch {
|
|
3247
|
+
return null;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
const resolvedPath = resolve(directory, treePath);
|
|
3251
|
+
const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
|
|
3252
|
+
return { success: !!tree, tree };
|
|
3253
|
+
}
|
|
3254
|
+
},
|
|
3255
|
+
{ messagesDir: getSessionDir(directory, sessionId) }
|
|
3256
|
+
);
|
|
3257
|
+
checkSvampConfig = createSvampConfigChecker(
|
|
3258
|
+
directory,
|
|
3259
|
+
sessionId,
|
|
3260
|
+
() => sessionMetadata,
|
|
3261
|
+
(updater) => {
|
|
3262
|
+
sessionMetadata = updater(sessionMetadata);
|
|
3263
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
3264
|
+
},
|
|
3265
|
+
sessionService,
|
|
3266
|
+
logger
|
|
3267
|
+
);
|
|
3268
|
+
const trackedSession = {
|
|
3269
|
+
startedBy: "daemon",
|
|
3270
|
+
pid: process.pid,
|
|
3271
|
+
svampSessionId: sessionId,
|
|
3272
|
+
hyphaService: sessionService,
|
|
3273
|
+
checkSvampConfig,
|
|
3274
|
+
directory,
|
|
3275
|
+
resumeSessionId: claudeResumeId,
|
|
3276
|
+
get childProcess() {
|
|
3277
|
+
return claudeProcess || void 0;
|
|
3278
|
+
}
|
|
3279
|
+
};
|
|
3280
|
+
pidToTrackedSession.set(process.pid + Math.floor(Math.random() * 1e5), trackedSession);
|
|
3281
|
+
sessionService.updateMetadata({
|
|
3282
|
+
...sessionMetadata,
|
|
3283
|
+
lifecycleState: "idle"
|
|
3284
|
+
});
|
|
3285
|
+
logger.log(`Session ${sessionId} registered on Hypha, waiting for first message to spawn Claude`);
|
|
3286
|
+
return {
|
|
3287
|
+
type: "success",
|
|
3288
|
+
sessionId,
|
|
3289
|
+
message: `Session registered on Hypha as svamp-session-${sessionId}`
|
|
3290
|
+
};
|
|
3291
|
+
} catch (err) {
|
|
3292
|
+
logger.error(`Failed to spawn session:`, err);
|
|
3293
|
+
return {
|
|
3294
|
+
type: "error",
|
|
3295
|
+
errorMessage: `Failed to register session service: ${err.message}`
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
};
|
|
3299
|
+
const spawnAcpSession = async (sessionId, directory, agentName, options, resumeSessionId) => {
|
|
3300
|
+
logger.log(`[ACP] Spawning ${agentName} session: ${sessionId}`);
|
|
3301
|
+
try {
|
|
3302
|
+
let parseBashPermission2 = function(permission) {
|
|
3303
|
+
if (permission === "Bash") return;
|
|
3304
|
+
const match = permission.match(/^Bash\((.+?)\)$/);
|
|
3305
|
+
if (!match) return;
|
|
3306
|
+
const command = match[1];
|
|
3307
|
+
if (command.endsWith(":*")) {
|
|
3308
|
+
allowedBashPrefixes.add(command.slice(0, -2));
|
|
3309
|
+
} else {
|
|
3310
|
+
allowedBashLiterals.add(command);
|
|
3311
|
+
}
|
|
3312
|
+
}, shouldAutoAllow2 = function(toolName, toolInput) {
|
|
3313
|
+
if (toolName === "Bash") {
|
|
3314
|
+
const inputObj = toolInput;
|
|
3315
|
+
if (inputObj?.command) {
|
|
3316
|
+
if (allowedBashLiterals.has(inputObj.command)) return true;
|
|
3317
|
+
for (const prefix of allowedBashPrefixes) {
|
|
3318
|
+
if (inputObj.command.startsWith(prefix)) return true;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
} else if (allowedTools.has(toolName)) {
|
|
3322
|
+
return true;
|
|
3323
|
+
}
|
|
3324
|
+
if (currentPermissionMode === "bypassPermissions") return true;
|
|
3325
|
+
if (currentPermissionMode === "acceptEdits" && EDIT_TOOLS.has(toolName)) return true;
|
|
3326
|
+
return false;
|
|
3327
|
+
};
|
|
3328
|
+
var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2;
|
|
3329
|
+
let sessionMetadata = {
|
|
3330
|
+
path: directory,
|
|
3331
|
+
host: os.hostname(),
|
|
3332
|
+
version: "0.1.0",
|
|
3333
|
+
machineId,
|
|
3334
|
+
homeDir: os.homedir(),
|
|
3335
|
+
svampHomeDir: SVAMP_HOME,
|
|
3336
|
+
svampLibDir: join$1(__dirname$1, ".."),
|
|
3337
|
+
svampToolsDir: join$1(__dirname$1, "..", "tools"),
|
|
3338
|
+
startedFromDaemon: true,
|
|
3339
|
+
startedBy: "daemon",
|
|
3340
|
+
lifecycleState: "starting",
|
|
3341
|
+
flavor: agentName
|
|
3342
|
+
};
|
|
3343
|
+
let currentPermissionMode = "default";
|
|
3344
|
+
const allowedTools = /* @__PURE__ */ new Set();
|
|
3345
|
+
const allowedBashLiterals = /* @__PURE__ */ new Set();
|
|
3346
|
+
const allowedBashPrefixes = /* @__PURE__ */ new Set();
|
|
3347
|
+
const EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "Write", "NotebookEdit"]);
|
|
3348
|
+
const sessionService = await registerSessionService(
|
|
3349
|
+
server,
|
|
3350
|
+
sessionId,
|
|
3351
|
+
sessionMetadata,
|
|
3352
|
+
{ controlledByUser: false },
|
|
3353
|
+
{
|
|
3354
|
+
onUserMessage: (content, meta) => {
|
|
3355
|
+
logger.log(`[ACP Session ${sessionId}] User message received`);
|
|
3356
|
+
let text;
|
|
3357
|
+
let msgMeta = meta;
|
|
3358
|
+
try {
|
|
3359
|
+
let parsed = typeof content === "string" ? JSON.parse(content) : content;
|
|
3360
|
+
if (parsed?.content && typeof parsed.content === "string") {
|
|
3361
|
+
try {
|
|
3362
|
+
const inner = JSON.parse(parsed.content);
|
|
3363
|
+
if (inner && typeof inner === "object") parsed = inner;
|
|
3364
|
+
} catch {
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
text = parsed?.content?.text || parsed?.text || (typeof parsed === "string" ? parsed : JSON.stringify(parsed));
|
|
3368
|
+
if (parsed?.meta) msgMeta = { ...msgMeta, ...parsed.meta };
|
|
3369
|
+
} catch {
|
|
3370
|
+
text = typeof content === "string" ? content : JSON.stringify(content);
|
|
3371
|
+
}
|
|
3372
|
+
if (msgMeta?.permissionMode) {
|
|
3373
|
+
currentPermissionMode = msgMeta.permissionMode;
|
|
3374
|
+
}
|
|
3375
|
+
acpBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
3376
|
+
logger.error(`[ACP Session ${sessionId}] Error sending prompt:`, err);
|
|
3377
|
+
});
|
|
3378
|
+
},
|
|
3379
|
+
onAbort: () => {
|
|
3380
|
+
logger.log(`[ACP Session ${sessionId}] Abort requested`);
|
|
3381
|
+
acpBackend.cancel(sessionId).catch(() => {
|
|
3382
|
+
});
|
|
3383
|
+
},
|
|
3384
|
+
onPermissionResponse: (params) => {
|
|
3385
|
+
logger.log(`[ACP Session ${sessionId}] Permission response:`, JSON.stringify(params));
|
|
3386
|
+
const requestId = params.id;
|
|
3387
|
+
if (params.mode) currentPermissionMode = params.mode;
|
|
3388
|
+
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
3389
|
+
for (const tool of params.allowTools) {
|
|
3390
|
+
if (tool.startsWith("Bash(") || tool === "Bash") {
|
|
3391
|
+
parseBashPermission2(tool);
|
|
3392
|
+
} else {
|
|
3393
|
+
allowedTools.add(tool);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
permissionHandler.resolvePermission(requestId, params.approved);
|
|
3398
|
+
},
|
|
3399
|
+
onSwitchMode: (mode) => {
|
|
3400
|
+
logger.log(`[ACP Session ${sessionId}] Switch mode: ${mode}`);
|
|
3401
|
+
currentPermissionMode = mode;
|
|
3402
|
+
},
|
|
3403
|
+
onRestartClaude: async () => {
|
|
3404
|
+
logger.log(`[ACP Session ${sessionId}] Restart agent requested`);
|
|
3405
|
+
return { success: false, message: "Restart is not supported for this agent type." };
|
|
3406
|
+
},
|
|
3407
|
+
onKillSession: () => {
|
|
3408
|
+
logger.log(`[ACP Session ${sessionId}] Kill session requested`);
|
|
3409
|
+
stopSession(sessionId);
|
|
3410
|
+
},
|
|
3411
|
+
onBash: async (command, cwd) => {
|
|
3412
|
+
const { exec } = await import('child_process');
|
|
3413
|
+
return new Promise((resolve2) => {
|
|
3414
|
+
exec(command, { cwd: cwd || directory, timeout: 3e4, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3415
|
+
if (err) {
|
|
3416
|
+
resolve2({ success: false, stdout: stdout || "", stderr: stderr || err.message, exitCode: err.code ?? 1 });
|
|
3417
|
+
} else {
|
|
3418
|
+
resolve2({ success: true, stdout, stderr: stderr || "", exitCode: 0 });
|
|
3419
|
+
}
|
|
3420
|
+
});
|
|
3421
|
+
});
|
|
3422
|
+
},
|
|
3423
|
+
onRipgrep: async (args, cwd) => {
|
|
3424
|
+
const { exec } = await import('child_process');
|
|
3425
|
+
const rgCwd = cwd || directory;
|
|
3426
|
+
return new Promise((resolve2, reject) => {
|
|
3427
|
+
exec(`rg ${args}`, { cwd: rgCwd, timeout: 3e4, maxBuffer: 5 * 1024 * 1024 }, (err, stdout) => {
|
|
3428
|
+
if (err && !stdout) {
|
|
3429
|
+
reject(new Error(err.message));
|
|
3430
|
+
} else {
|
|
3431
|
+
resolve2(stdout || "");
|
|
3432
|
+
}
|
|
3433
|
+
});
|
|
3434
|
+
});
|
|
3435
|
+
},
|
|
3436
|
+
onReadFile: async (path) => {
|
|
3437
|
+
const resolvedPath = resolve(directory, path);
|
|
3438
|
+
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
3439
|
+
throw new Error("Path outside working directory");
|
|
3440
|
+
}
|
|
3441
|
+
const buffer = await fs.readFile(resolvedPath);
|
|
3442
|
+
return buffer.toString("base64");
|
|
3443
|
+
},
|
|
3444
|
+
onWriteFile: async (path, content) => {
|
|
3445
|
+
const resolvedPath = resolve(directory, path);
|
|
3446
|
+
if (!resolvedPath.startsWith(resolve(directory))) {
|
|
3447
|
+
throw new Error("Path outside working directory");
|
|
3448
|
+
}
|
|
3449
|
+
await fs.mkdir(dirname(resolvedPath), { recursive: true });
|
|
3450
|
+
await fs.writeFile(resolvedPath, Buffer.from(content, "base64"));
|
|
3451
|
+
},
|
|
3452
|
+
onListDirectory: async (path) => {
|
|
3453
|
+
const resolvedDir = resolve(directory, path || ".");
|
|
3454
|
+
if (!resolvedDir.startsWith(resolve(directory))) {
|
|
3455
|
+
throw new Error("Path outside working directory");
|
|
3456
|
+
}
|
|
3457
|
+
const entries = await fs.readdir(resolvedDir, { withFileTypes: true });
|
|
3458
|
+
return entries.map((e) => ({ name: e.name, isDirectory: e.isDirectory() }));
|
|
3459
|
+
},
|
|
3460
|
+
onGetDirectoryTree: async (treePath, maxDepth) => {
|
|
3461
|
+
async function buildTree(p, name, depth) {
|
|
3462
|
+
try {
|
|
3463
|
+
const stats = await fs.stat(p);
|
|
3464
|
+
const node = {
|
|
3465
|
+
name,
|
|
3466
|
+
path: p,
|
|
3467
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
3468
|
+
size: stats.size,
|
|
3469
|
+
modified: stats.mtime.getTime()
|
|
3470
|
+
};
|
|
3471
|
+
if (stats.isDirectory() && depth < maxDepth) {
|
|
3472
|
+
const entries = await fs.readdir(p, { withFileTypes: true });
|
|
3473
|
+
const children = [];
|
|
3474
|
+
await Promise.all(entries.map(async (entry) => {
|
|
3475
|
+
if (entry.isSymbolicLink()) return;
|
|
3476
|
+
const childPath = join$1(p, entry.name);
|
|
3477
|
+
const childNode = await buildTree(childPath, entry.name, depth + 1);
|
|
3478
|
+
if (childNode) children.push(childNode);
|
|
3479
|
+
}));
|
|
3480
|
+
children.sort((a, b) => {
|
|
3481
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
3482
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
3483
|
+
return a.name.localeCompare(b.name);
|
|
3484
|
+
});
|
|
3485
|
+
node.children = children;
|
|
3486
|
+
}
|
|
3487
|
+
return node;
|
|
3488
|
+
} catch {
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
const resolvedPath = resolve(directory, treePath);
|
|
3493
|
+
const tree = await buildTree(resolvedPath, basename(resolvedPath), 0);
|
|
3494
|
+
return { success: !!tree, tree };
|
|
3495
|
+
}
|
|
3496
|
+
},
|
|
3497
|
+
{ messagesDir: getSessionDir(directory, sessionId) }
|
|
3498
|
+
);
|
|
3499
|
+
const checkSvampConfig = createSvampConfigChecker(
|
|
3500
|
+
directory,
|
|
3501
|
+
sessionId,
|
|
3502
|
+
() => sessionMetadata,
|
|
3503
|
+
(updater) => {
|
|
3504
|
+
sessionMetadata = updater(sessionMetadata);
|
|
3505
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
3506
|
+
},
|
|
3507
|
+
sessionService,
|
|
3508
|
+
logger
|
|
3509
|
+
);
|
|
3510
|
+
const transportHandler = agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(agentName);
|
|
3511
|
+
const permissionHandler = new HyphaPermissionHandler(shouldAutoAllow2, logger.log);
|
|
3512
|
+
const acpConfig = KNOWN_ACP_AGENTS[agentName];
|
|
3513
|
+
const acpBackend = new AcpBackend({
|
|
3514
|
+
agentName,
|
|
3515
|
+
cwd: directory,
|
|
3516
|
+
command: acpConfig.command,
|
|
3517
|
+
args: acpConfig.args,
|
|
3518
|
+
env: options.environmentVariables,
|
|
3519
|
+
permissionHandler,
|
|
3520
|
+
transportHandler,
|
|
3521
|
+
log: logger.log
|
|
3522
|
+
});
|
|
3523
|
+
bridgeAcpToSession(
|
|
3524
|
+
acpBackend,
|
|
3525
|
+
sessionService,
|
|
3526
|
+
() => sessionMetadata,
|
|
3527
|
+
(updater) => {
|
|
3528
|
+
sessionMetadata = updater(sessionMetadata);
|
|
3529
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
3530
|
+
},
|
|
3531
|
+
logger.log,
|
|
3532
|
+
checkSvampConfig
|
|
3533
|
+
);
|
|
3534
|
+
const trackedSession = {
|
|
3535
|
+
startedBy: "daemon",
|
|
3536
|
+
pid: process.pid,
|
|
3537
|
+
svampSessionId: sessionId,
|
|
3538
|
+
hyphaService: sessionService,
|
|
3539
|
+
checkSvampConfig,
|
|
3540
|
+
directory,
|
|
3541
|
+
resumeSessionId,
|
|
3542
|
+
get childProcess() {
|
|
3543
|
+
return acpBackend.getProcess() || void 0;
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
3546
|
+
pidToTrackedSession.set(process.pid + Math.floor(Math.random() * 1e5), trackedSession);
|
|
3547
|
+
logger.log(`[ACP Session ${sessionId}] Starting ${agentName} backend...`);
|
|
3548
|
+
acpBackend.startSession().then(() => {
|
|
3549
|
+
logger.log(`[ACP Session ${sessionId}] ${agentName} backend started, waiting for first message`);
|
|
3550
|
+
}).catch((err) => {
|
|
3551
|
+
logger.error(`[ACP Session ${sessionId}] Failed to start ${agentName}:`, err);
|
|
3552
|
+
sessionService.pushMessage({
|
|
3553
|
+
type: "assistant",
|
|
3554
|
+
content: [{
|
|
3555
|
+
type: "text",
|
|
3556
|
+
text: `Error: Failed to start ${agentName} agent: ${err.message}
|
|
3557
|
+
|
|
3558
|
+
Please ensure the ${agentName} CLI is installed.`
|
|
3559
|
+
}]
|
|
3560
|
+
}, "agent");
|
|
3561
|
+
sessionService.sendKeepAlive(false);
|
|
3562
|
+
});
|
|
3563
|
+
return {
|
|
3564
|
+
type: "success",
|
|
3565
|
+
sessionId,
|
|
3566
|
+
message: `ACP session (${agentName}) registered as svamp-session-${sessionId}`
|
|
3567
|
+
};
|
|
3568
|
+
} catch (err) {
|
|
3569
|
+
logger.error(`[ACP] Failed to spawn ${agentName} session:`, err);
|
|
3570
|
+
return {
|
|
3571
|
+
type: "error",
|
|
3572
|
+
errorMessage: `Failed to spawn ${agentName} session: ${err.message}`
|
|
3573
|
+
};
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3576
|
+
const stopSession = (sessionId) => {
|
|
3577
|
+
logger.log(`Stopping session: ${sessionId}`);
|
|
3578
|
+
for (const [pid, session] of pidToTrackedSession) {
|
|
3579
|
+
if (session.svampSessionId === sessionId) {
|
|
3580
|
+
session.stopped = true;
|
|
3581
|
+
session.hyphaService?.disconnect().catch(() => {
|
|
3582
|
+
});
|
|
3583
|
+
if (session.childProcess) {
|
|
3584
|
+
try {
|
|
3585
|
+
session.childProcess.kill("SIGTERM");
|
|
3586
|
+
} catch {
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
pidToTrackedSession.delete(pid);
|
|
3590
|
+
deletePersistedSession(sessionId);
|
|
3591
|
+
logger.log(`Session ${sessionId} stopped`);
|
|
3592
|
+
return true;
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
logger.log(`Session ${sessionId} not found`);
|
|
3596
|
+
return false;
|
|
3597
|
+
};
|
|
3598
|
+
const defaultHomeDir = existsSync$1("/data") ? "/data" : os.homedir();
|
|
3599
|
+
const machineMetadata = {
|
|
3600
|
+
host: os.hostname(),
|
|
3601
|
+
platform: os.platform(),
|
|
3602
|
+
svampVersion: "0.1.0 (hypha)",
|
|
3603
|
+
homeDir: defaultHomeDir,
|
|
3604
|
+
svampHomeDir: SVAMP_HOME,
|
|
3605
|
+
svampLibDir: join$1(__dirname$1, ".."),
|
|
3606
|
+
displayName: process.env.SVAMP_DISPLAY_NAME || void 0
|
|
3607
|
+
};
|
|
3608
|
+
const initialDaemonState = {
|
|
3609
|
+
status: "running",
|
|
3610
|
+
pid: process.pid,
|
|
3611
|
+
startedAt: Date.now()
|
|
3612
|
+
};
|
|
3613
|
+
const machineService = await registerMachineService(
|
|
3614
|
+
server,
|
|
3615
|
+
machineId,
|
|
3616
|
+
machineMetadata,
|
|
3617
|
+
initialDaemonState,
|
|
3618
|
+
{
|
|
3619
|
+
spawnSession,
|
|
3620
|
+
stopSession,
|
|
3621
|
+
requestShutdown: () => requestShutdown("hypha-app"),
|
|
3622
|
+
getTrackedSessions: getCurrentChildren
|
|
3623
|
+
}
|
|
3624
|
+
);
|
|
3625
|
+
logger.log(`Machine service registered: svamp-machine-${machineId}`);
|
|
3626
|
+
const artifactSync = new SessionArtifactSync(server, logger.log);
|
|
3627
|
+
const debugService = await registerDebugService(server, machineId, {
|
|
3628
|
+
machineId,
|
|
3629
|
+
getTrackedSessions: getCurrentChildren,
|
|
3630
|
+
getSessionService: (sessionId) => {
|
|
3631
|
+
for (const [, session] of pidToTrackedSession) {
|
|
3632
|
+
if (session.svampSessionId === sessionId) {
|
|
3633
|
+
return session.hyphaService;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
return null;
|
|
3637
|
+
},
|
|
3638
|
+
getArtifactSync: () => artifactSync,
|
|
3639
|
+
getSessionsDir: () => SVAMP_HOME
|
|
3640
|
+
// Legacy; debug service uses session index now
|
|
3641
|
+
});
|
|
3642
|
+
logger.log(`Debug service registered: svamp-debug-${machineId}`);
|
|
3643
|
+
const persistedSessions = loadPersistedSessions();
|
|
3644
|
+
if (persistedSessions.length > 0) {
|
|
3645
|
+
logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
|
|
3646
|
+
for (const persisted of persistedSessions) {
|
|
3647
|
+
try {
|
|
3648
|
+
const isOrphaned = persisted.machineId && persisted.machineId !== machineId;
|
|
3649
|
+
if (isOrphaned) {
|
|
3650
|
+
logger.log(`Session ${persisted.sessionId} is from a different machine (${persisted.machineId} vs ${machineId}), marking as orphaned`);
|
|
3651
|
+
}
|
|
3652
|
+
const result = await spawnSession({
|
|
3653
|
+
directory: persisted.directory,
|
|
3654
|
+
sessionId: persisted.sessionId,
|
|
3655
|
+
resumeSessionId: persisted.claudeResumeId
|
|
3656
|
+
});
|
|
3657
|
+
if (result.type === "success") {
|
|
3658
|
+
logger.log(`Restored session ${persisted.sessionId} (resume=${persisted.claudeResumeId})`);
|
|
3659
|
+
if (isOrphaned) {
|
|
3660
|
+
for (const [, tracked] of pidToTrackedSession) {
|
|
3661
|
+
if (tracked.svampSessionId === persisted.sessionId && tracked.hyphaService) {
|
|
3662
|
+
tracked.hyphaService.updateMetadata({
|
|
3663
|
+
...persisted.metadata || {},
|
|
3664
|
+
isOrphaned: true,
|
|
3665
|
+
originalMachineId: persisted.machineId
|
|
3666
|
+
});
|
|
3667
|
+
break;
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
} else {
|
|
3672
|
+
logger.log(`Failed to restore session ${persisted.sessionId}: ${result.type}`);
|
|
3673
|
+
}
|
|
3674
|
+
} catch (err) {
|
|
3675
|
+
logger.error(`Error restoring session ${persisted.sessionId}:`, err.message);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
(async () => {
|
|
3680
|
+
try {
|
|
3681
|
+
await artifactSync.init();
|
|
3682
|
+
logger.log(`[ARTIFACT SYNC] Ready (upload-only, remote sessions stay in artifact store)`);
|
|
3683
|
+
} catch (err) {
|
|
3684
|
+
logger.log(`[ARTIFACT SYNC] Background init failed: ${err.message}`);
|
|
3685
|
+
}
|
|
3686
|
+
})();
|
|
3687
|
+
let appToken;
|
|
3688
|
+
try {
|
|
3689
|
+
appToken = await server.generateToken({});
|
|
3690
|
+
logger.log(`App connection token generated`);
|
|
3691
|
+
} catch (err) {
|
|
3692
|
+
logger.log("Could not generate token (server may not support it):", err);
|
|
3693
|
+
}
|
|
3694
|
+
const localState = {
|
|
3695
|
+
pid: process.pid,
|
|
3696
|
+
startTime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3697
|
+
version: "0.1.0",
|
|
3698
|
+
hyphaServerUrl,
|
|
3699
|
+
workspace: server.config.workspace,
|
|
3700
|
+
machineId
|
|
3701
|
+
};
|
|
3702
|
+
writeDaemonStateFile(localState);
|
|
3703
|
+
console.log("Svamp daemon started successfully!");
|
|
3704
|
+
console.log(` Machine ID: ${machineId}`);
|
|
3705
|
+
console.log(` Hypha server: ${hyphaServerUrl}`);
|
|
3706
|
+
console.log(` Workspace: ${server.config.workspace}`);
|
|
3707
|
+
if (appToken) {
|
|
3708
|
+
console.log(` App token: ${appToken}`);
|
|
3709
|
+
}
|
|
3710
|
+
console.log(` Service: svamp-machine-${machineId}`);
|
|
3711
|
+
console.log(` Log file: ${logger.logFilePath}`);
|
|
3712
|
+
let consecutiveHeartbeatFailures = 0;
|
|
3713
|
+
const MAX_HEARTBEAT_FAILURES = 5;
|
|
3714
|
+
const heartbeatInterval = setInterval(async () => {
|
|
3715
|
+
try {
|
|
3716
|
+
const state = readDaemonStateFile();
|
|
3717
|
+
if (state && state.pid === process.pid) {
|
|
3718
|
+
state.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
|
|
3719
|
+
writeDaemonStateFile(state);
|
|
3720
|
+
}
|
|
3721
|
+
} catch {
|
|
3722
|
+
}
|
|
3723
|
+
for (const [key, session] of pidToTrackedSession) {
|
|
3724
|
+
const child = session.childProcess;
|
|
3725
|
+
if (child && child.pid) {
|
|
3726
|
+
try {
|
|
3727
|
+
process.kill(child.pid, 0);
|
|
3728
|
+
} catch {
|
|
3729
|
+
logger.log(`Removing stale session (child PID ${child.pid} dead): ${session.svampSessionId}`);
|
|
3730
|
+
session.hyphaService?.disconnect().catch(() => {
|
|
3731
|
+
});
|
|
3732
|
+
pidToTrackedSession.delete(key);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
const serverAny = server;
|
|
3737
|
+
const rpcReconnecting = serverAny._connection?._reconnecting === true;
|
|
3738
|
+
if (rpcReconnecting && !isReconnecting) {
|
|
3739
|
+
isReconnecting = true;
|
|
3740
|
+
logger.log("Detected hypha-rpc reconnection in progress");
|
|
3741
|
+
}
|
|
3742
|
+
if (isReconnecting) {
|
|
3743
|
+
consecutiveHeartbeatFailures++;
|
|
3744
|
+
logger.log(`Heartbeat skipped (reconnecting), failures=${consecutiveHeartbeatFailures}/${MAX_HEARTBEAT_FAILURES}`);
|
|
3745
|
+
if (consecutiveHeartbeatFailures >= MAX_HEARTBEAT_FAILURES) {
|
|
3746
|
+
logger.log("Too many consecutive heartbeat failures during reconnection. Shutting down.");
|
|
3747
|
+
requestShutdown("heartbeat-timeout", "Reconnection taking too long");
|
|
3748
|
+
}
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
try {
|
|
3752
|
+
await Promise.race([
|
|
3753
|
+
server.listServices({}),
|
|
3754
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Heartbeat ping timed out after 30s")), 3e4))
|
|
3755
|
+
]);
|
|
3756
|
+
if (consecutiveHeartbeatFailures > 0) {
|
|
3757
|
+
logger.log(`Heartbeat recovered after ${consecutiveHeartbeatFailures} failures`);
|
|
3758
|
+
}
|
|
3759
|
+
consecutiveHeartbeatFailures = 0;
|
|
3760
|
+
isReconnecting = false;
|
|
3761
|
+
} catch (err) {
|
|
3762
|
+
consecutiveHeartbeatFailures++;
|
|
3763
|
+
logger.log(`Hypha keep-alive ping failed (${consecutiveHeartbeatFailures}/${MAX_HEARTBEAT_FAILURES}): ${err.message}`);
|
|
3764
|
+
if (!isReconnecting) {
|
|
3765
|
+
isReconnecting = true;
|
|
3766
|
+
logger.log("Entering reconnection state \u2014 hypha-rpc will attempt to reconnect");
|
|
3767
|
+
}
|
|
3768
|
+
if (consecutiveHeartbeatFailures >= MAX_HEARTBEAT_FAILURES) {
|
|
3769
|
+
logger.log(`Heartbeat failed ${MAX_HEARTBEAT_FAILURES} consecutive times. Shutting down.`);
|
|
3770
|
+
requestShutdown("heartbeat-timeout", err.message);
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
}, 6e4);
|
|
3774
|
+
const cleanup = async (source) => {
|
|
3775
|
+
logger.log(`Cleaning up (source: ${source})...`);
|
|
3776
|
+
clearInterval(heartbeatInterval);
|
|
3777
|
+
if (unhandledRejectionResetTimer) clearTimeout(unhandledRejectionResetTimer);
|
|
3778
|
+
machineService.updateDaemonState({
|
|
3779
|
+
...initialDaemonState,
|
|
3780
|
+
status: "shutting-down",
|
|
3781
|
+
shutdownRequestedAt: Date.now(),
|
|
3782
|
+
shutdownSource: source
|
|
3783
|
+
});
|
|
3784
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3785
|
+
for (const [pid, session] of pidToTrackedSession) {
|
|
3786
|
+
session.hyphaService?.disconnect().catch(() => {
|
|
3787
|
+
});
|
|
3788
|
+
if (session.childProcess) {
|
|
3789
|
+
try {
|
|
3790
|
+
session.childProcess.kill("SIGTERM");
|
|
3791
|
+
} catch {
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
try {
|
|
3796
|
+
await machineService.disconnect();
|
|
3797
|
+
} catch {
|
|
3798
|
+
}
|
|
3799
|
+
try {
|
|
3800
|
+
await debugService.disconnect();
|
|
3801
|
+
} catch {
|
|
3802
|
+
}
|
|
3803
|
+
artifactSync.destroy();
|
|
3804
|
+
try {
|
|
3805
|
+
await server.disconnect();
|
|
3806
|
+
} catch {
|
|
3807
|
+
}
|
|
3808
|
+
cleanupDaemonStateFile();
|
|
3809
|
+
logger.log("Cleanup complete");
|
|
3810
|
+
};
|
|
3811
|
+
const shutdownReq = await resolvesWhenShutdownRequested;
|
|
3812
|
+
await cleanup(shutdownReq.source);
|
|
3813
|
+
process.exit(0);
|
|
3814
|
+
} catch (error) {
|
|
3815
|
+
logger.error("Fatal error:", error);
|
|
3816
|
+
cleanupDaemonStateFile();
|
|
3817
|
+
if (server) {
|
|
3818
|
+
try {
|
|
3819
|
+
await server.disconnect();
|
|
3820
|
+
} catch {
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
process.exit(1);
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
async function stopDaemon() {
|
|
3827
|
+
const state = readDaemonStateFile();
|
|
3828
|
+
if (!state) {
|
|
3829
|
+
console.log("No daemon running");
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
try {
|
|
3833
|
+
process.kill(state.pid, 0);
|
|
3834
|
+
process.kill(state.pid, "SIGTERM");
|
|
3835
|
+
console.log(`Sent SIGTERM to daemon PID ${state.pid}`);
|
|
3836
|
+
for (let i = 0; i < 30; i++) {
|
|
3837
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
3838
|
+
try {
|
|
3839
|
+
process.kill(state.pid, 0);
|
|
3840
|
+
} catch {
|
|
3841
|
+
console.log("Daemon stopped");
|
|
3842
|
+
cleanupDaemonStateFile();
|
|
3843
|
+
return;
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
console.log("Daemon did not stop in time, sending SIGKILL");
|
|
3847
|
+
process.kill(state.pid, "SIGKILL");
|
|
3848
|
+
} catch {
|
|
3849
|
+
console.log("Daemon is not running (stale state file)");
|
|
3850
|
+
}
|
|
3851
|
+
cleanupDaemonStateFile();
|
|
3852
|
+
}
|
|
3853
|
+
function daemonStatus() {
|
|
3854
|
+
const state = readDaemonStateFile();
|
|
3855
|
+
if (!state) {
|
|
3856
|
+
console.log("Status: Not running");
|
|
3857
|
+
return;
|
|
3858
|
+
}
|
|
3859
|
+
const alive = isDaemonAlive();
|
|
3860
|
+
console.log(`Status: ${alive ? "Running" : "Dead (stale state)"}`);
|
|
3861
|
+
console.log(` PID: ${state.pid}`);
|
|
3862
|
+
console.log(` Started: ${state.startTime}`);
|
|
3863
|
+
console.log(` Hypha server: ${state.hyphaServerUrl}`);
|
|
3864
|
+
if (state.workspace) {
|
|
3865
|
+
console.log(` Workspace: ${state.workspace}`);
|
|
3866
|
+
}
|
|
3867
|
+
if (state.lastHeartbeat) {
|
|
3868
|
+
console.log(` Last heartbeat: ${state.lastHeartbeat}`);
|
|
3869
|
+
}
|
|
3870
|
+
if (!alive) {
|
|
3871
|
+
cleanupDaemonStateFile();
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
export { DefaultTransport$1 as D, GeminiTransport$1 as G, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, acpBackend as e, acpAgentConfig as f, getHyphaServerUrl as g, registerMachineService as r, startDaemon as s };
|