@anusornneal/chat-relay 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/agent/capability-scheduler.mjs +121 -0
- package/agent/connection-state.mjs +126 -0
- package/agent/desktop-manager.mjs +440 -319
- package/agent/file-manager.mjs +150 -5
- package/agent/lifecycle.mjs +101 -0
- package/agent/local-agent.mjs +259 -26
- package/agent/protocol.mjs +67 -0
- package/agent/windows-desktop-worker.ps1 +508 -380
- package/cli/index.mjs +41 -1
- package/cli/remote.mjs +51 -6
- package/package.json +1 -1
package/agent/local-agent.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
import { DesktopManager } from "./desktop-manager.mjs";
|
|
2
|
+
import { DesktopManager, createDesktopPlatformAdapter } from "./desktop-manager.mjs";
|
|
3
3
|
import { FileManager } from "./file-manager.mjs";
|
|
4
4
|
import { ProcessManager } from "./process-manager.mjs";
|
|
5
5
|
import { TerminalManager } from "./terminal-manager.mjs";
|
|
6
|
+
import { CapabilityScheduler } from "./capability-scheduler.mjs";
|
|
7
|
+
import { AgentConnectionState } from "./connection-state.mjs";
|
|
8
|
+
import { buildAgentHello } from "./protocol.mjs";
|
|
9
|
+
import { AgentLifecycle, AGENT_RESTART_EXIT_CODE } from "./lifecycle.mjs";
|
|
6
10
|
|
|
7
11
|
const relayUrl = process.env.RELAY_URL;
|
|
8
12
|
const agentToken = process.env.AGENT_TOKEN;
|
|
@@ -11,16 +15,59 @@ const agentName = process.env.AGENT_NAME || agentId;
|
|
|
11
15
|
const reconnectMs = Number(process.env.RECONNECT_MS ?? 2000);
|
|
12
16
|
const terminalEnabled = process.env.TERMINAL_ENABLED === "1";
|
|
13
17
|
const desktopEnabled = process.env.DESKTOP_ENABLED === "1";
|
|
18
|
+
const desktopAdapter = createDesktopPlatformAdapter({ platform: process.platform });
|
|
19
|
+
const agentHello = buildAgentHello({
|
|
20
|
+
platform: desktopAdapter.platform,
|
|
21
|
+
terminalEnabled,
|
|
22
|
+
desktopEnabled: desktopEnabled && desktopAdapter.supported,
|
|
23
|
+
});
|
|
14
24
|
const MAX_RESPONSE_BYTES = 60 * 1024;
|
|
15
25
|
const terminals = new TerminalManager({
|
|
16
26
|
batchConcurrency: process.env.TERMINAL_BATCH_CONCURRENCY,
|
|
17
27
|
maxQueuedJobs: process.env.TERMINAL_BATCH_MAX_QUEUED,
|
|
18
28
|
});
|
|
29
|
+
const scheduler = new CapabilityScheduler({
|
|
30
|
+
maxQueued: process.env.AGENT_MAX_QUEUED,
|
|
31
|
+
queueTimeoutMs: process.env.AGENT_QUEUE_TIMEOUT_MS,
|
|
32
|
+
fileConcurrency: process.env.AGENT_FILE_CONCURRENCY,
|
|
33
|
+
processConcurrency: process.env.AGENT_PROCESS_CONCURRENCY,
|
|
34
|
+
terminalExecConcurrency: process.env.AGENT_TERMINAL_EXEC_CONCURRENCY,
|
|
35
|
+
terminalControlConcurrency: process.env.AGENT_TERMINAL_CONTROL_CONCURRENCY,
|
|
36
|
+
desktopReadConcurrency: process.env.AGENT_DESKTOP_READ_CONCURRENCY,
|
|
37
|
+
});
|
|
38
|
+
const connectionState = new AgentConnectionState({
|
|
39
|
+
baseReconnectMs: reconnectMs,
|
|
40
|
+
maxReconnectMs: process.env.RECONNECT_MAX_MS,
|
|
41
|
+
heartbeatMs: process.env.AGENT_HEARTBEAT_MS,
|
|
42
|
+
});
|
|
19
43
|
const files = new FileManager(process.env.ALLOWED_ROOTS);
|
|
20
44
|
const processes = new ProcessManager();
|
|
21
|
-
const desktop = new DesktopManager({
|
|
45
|
+
const desktop = new DesktopManager({
|
|
46
|
+
enabled: desktopEnabled,
|
|
47
|
+
adapter: desktopAdapter,
|
|
48
|
+
controlMaxQueued: process.env.DESKTOP_CONTROL_MAX_QUEUED,
|
|
49
|
+
controlQueueTimeoutMs: process.env.AGENT_QUEUE_TIMEOUT_MS,
|
|
50
|
+
});
|
|
22
51
|
const recentCalls = [];
|
|
23
52
|
let reauthorizationRequired = false;
|
|
53
|
+
let activeSocket = null;
|
|
54
|
+
let reconnectTimer = null;
|
|
55
|
+
let heartbeatTimer = null;
|
|
56
|
+
let stopping = false;
|
|
57
|
+
|
|
58
|
+
function currentWorkSummary() {
|
|
59
|
+
const terminal = terminals.observability();
|
|
60
|
+
const terminalExec = scheduler.snapshot().terminalExec || {};
|
|
61
|
+
return {
|
|
62
|
+
activeSessions: terminal.sessions.filter((item) => item.status === "running").length,
|
|
63
|
+
activeBatchJobs: terminal.activeExecJobs,
|
|
64
|
+
queuedBatchJobs: terminal.queuedJobs,
|
|
65
|
+
activeTerminalExecs: terminalExec.active,
|
|
66
|
+
queuedTerminalExecs: terminalExec.queued,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lifecycle = new AgentLifecycle({ workSummary: currentWorkSummary });
|
|
24
71
|
|
|
25
72
|
if (!relayUrl || !agentToken) {
|
|
26
73
|
console.error("RELAY_URL and AGENT_TOKEN are required");
|
|
@@ -30,8 +77,8 @@ if (!relayUrl || !agentToken) {
|
|
|
30
77
|
const wsUrl = relayUrl.replace(/^http/, "ws").replace(/\/$/, "") +
|
|
31
78
|
`/agent?agentId=${encodeURIComponent(agentId)}`;
|
|
32
79
|
|
|
33
|
-
function serializeResponse(requestId, payload) {
|
|
34
|
-
const response = JSON.stringify({ requestId, payload });
|
|
80
|
+
function serializeResponse(requestId, payload, meta) {
|
|
81
|
+
const response = JSON.stringify({ requestId, payload, ...(meta ? { meta } : {}) });
|
|
35
82
|
if (Buffer.byteLength(response, "utf8") <= MAX_RESPONSE_BYTES) return response;
|
|
36
83
|
return JSON.stringify({
|
|
37
84
|
requestId,
|
|
@@ -45,8 +92,11 @@ function serializeResponse(requestId, payload) {
|
|
|
45
92
|
|
|
46
93
|
async function handlePayload(payload) {
|
|
47
94
|
if (!payload || typeof payload !== "object") return payload;
|
|
95
|
+
const action = String(payload.action ?? "");
|
|
96
|
+
const lifecycleGate = lifecycle.guard(action);
|
|
97
|
+
if (lifecycleGate) return lifecycleGate;
|
|
48
98
|
|
|
49
|
-
switch (
|
|
99
|
+
switch (action) {
|
|
50
100
|
case "ping":
|
|
51
101
|
return { ok: true, action: "pong", at: new Date().toISOString(), agentId, agentName };
|
|
52
102
|
|
|
@@ -56,12 +106,37 @@ async function handlePayload(payload) {
|
|
|
56
106
|
agentName,
|
|
57
107
|
terminalEnabled,
|
|
58
108
|
terminalBatch: terminals.getBatchConfig(),
|
|
109
|
+
concurrency: scheduler.snapshot(),
|
|
59
110
|
desktop: desktop.getConfig(),
|
|
60
111
|
allowedRoots: files.getRoots(),
|
|
61
|
-
reconnectMs,
|
|
112
|
+
reconnectMs: connectionState.baseReconnectMs,
|
|
113
|
+
connection: { ...connectionState.snapshot(), processId: process.pid },
|
|
114
|
+
lifecycle: lifecycle.snapshot(),
|
|
115
|
+
terminalContinuity: {
|
|
116
|
+
processId: process.pid,
|
|
117
|
+
activeSessions: terminals.list().filter((session) => session.status === "running").length,
|
|
118
|
+
},
|
|
119
|
+
protocol: {
|
|
120
|
+
protocolVersion: agentHello.protocolVersion,
|
|
121
|
+
agentVersion: agentHello.agentVersion,
|
|
122
|
+
platform: agentHello.platform,
|
|
123
|
+
arch: agentHello.arch,
|
|
124
|
+
capabilities: agentHello.capabilities,
|
|
125
|
+
},
|
|
62
126
|
});
|
|
63
127
|
case "agent.recentCalls":
|
|
64
128
|
return recentCalls.slice(-(Math.min(Math.max(Number(payload.limit) || 50, 1), 100)));
|
|
129
|
+
case "agent.lifecycle.status":
|
|
130
|
+
return { ok: true, lifecycle: lifecycle.snapshot() };
|
|
131
|
+
case "agent.lifecycle.drain":
|
|
132
|
+
return lifecycle.drain();
|
|
133
|
+
case "agent.lifecycle.resume":
|
|
134
|
+
return lifecycle.resume();
|
|
135
|
+
case "agent.lifecycle.restart": {
|
|
136
|
+
const result = lifecycle.requestRestart();
|
|
137
|
+
if (result.ok) setTimeout(restartAgentProcess, 150);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
65
140
|
|
|
66
141
|
case "fs.stat":
|
|
67
142
|
return files.stat(payload.path);
|
|
@@ -71,6 +146,10 @@ async function handlePayload(payload) {
|
|
|
71
146
|
return files.read(payload.path, payload.offset, payload.length, payload.maxBytes);
|
|
72
147
|
case "fs.readMany":
|
|
73
148
|
return files.readMany(payload.paths, payload.maxTotalBytes);
|
|
149
|
+
case "fs.batch":
|
|
150
|
+
return files.batch(payload.operations, payload.maxTotalBytes);
|
|
151
|
+
case "fs.artifact":
|
|
152
|
+
return files.exportArtifact(payload.path, payload.maxBytes);
|
|
74
153
|
case "fs.write":
|
|
75
154
|
return files.write(payload.path, payload.content, payload.mode);
|
|
76
155
|
case "fs.edit":
|
|
@@ -93,6 +172,14 @@ async function handlePayload(payload) {
|
|
|
93
172
|
|
|
94
173
|
case "desktop.screenshot":
|
|
95
174
|
return desktop.screenshot(payload);
|
|
175
|
+
case "desktop.clipboard.read":
|
|
176
|
+
return desktop.clipboardRead(payload);
|
|
177
|
+
case "desktop.clipboard.write":
|
|
178
|
+
return desktop.clipboardWrite(payload);
|
|
179
|
+
case "desktop.window.list":
|
|
180
|
+
return desktop.listWindows(payload);
|
|
181
|
+
case "desktop.window.focus":
|
|
182
|
+
return desktop.focusWindow(payload);
|
|
96
183
|
case "desktop.mouse.click":
|
|
97
184
|
return desktop.mouseClick(payload);
|
|
98
185
|
case "desktop.keyboard.input":
|
|
@@ -142,27 +229,120 @@ async function handlePayload(payload) {
|
|
|
142
229
|
}
|
|
143
230
|
}
|
|
144
231
|
|
|
145
|
-
function
|
|
232
|
+
function clearReconnectTimer() {
|
|
233
|
+
if (!reconnectTimer) return;
|
|
234
|
+
clearTimeout(reconnectTimer);
|
|
235
|
+
reconnectTimer = null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function stopHeartbeat() {
|
|
239
|
+
if (!heartbeatTimer) return;
|
|
240
|
+
clearInterval(heartbeatTimer);
|
|
241
|
+
heartbeatTimer = null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function sendHeartbeat(socket) {
|
|
245
|
+
if (socket !== activeSocket || socket.readyState !== WebSocket.OPEN) return;
|
|
246
|
+
const now = Date.now();
|
|
247
|
+
try {
|
|
248
|
+
socket.send(JSON.stringify({
|
|
249
|
+
control: "agent_heartbeat",
|
|
250
|
+
at: now,
|
|
251
|
+
agentId,
|
|
252
|
+
processId: process.pid,
|
|
253
|
+
heartbeatMs: connectionState.heartbeatMs,
|
|
254
|
+
lifecycle: lifecycle.snapshot(),
|
|
255
|
+
health: {
|
|
256
|
+
reconnectCount: connectionState.reconnectCount,
|
|
257
|
+
reconnectAttempt: connectionState.reconnectAttempt,
|
|
258
|
+
lastDisconnectReason: connectionState.lastDisconnectReason,
|
|
259
|
+
queues: scheduler.snapshot(),
|
|
260
|
+
},
|
|
261
|
+
}));
|
|
262
|
+
connectionState.markHeartbeat(now);
|
|
263
|
+
} catch {}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function startHeartbeat(socket) {
|
|
267
|
+
stopHeartbeat();
|
|
268
|
+
sendHeartbeat(socket);
|
|
269
|
+
heartbeatTimer = setInterval(() => sendHeartbeat(socket), connectionState.heartbeatMs);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function sendSocketResponse(socket, requestId, payload, meta) {
|
|
273
|
+
if (socket !== activeSocket || socket.readyState !== WebSocket.OPEN) return;
|
|
274
|
+
try { socket.send(serializeResponse(requestId, payload, meta)); } catch {}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function scheduleReconnect(code, reason) {
|
|
278
|
+
if (stopping || reauthorizationRequired) return;
|
|
279
|
+
connectionState.markDisconnected(code, reason);
|
|
280
|
+
const delay = connectionState.nextDelay();
|
|
281
|
+
connectionState.scheduleReconnect(delay);
|
|
282
|
+
clearReconnectTimer();
|
|
283
|
+
console.log(`Reconnect scheduled in ${delay}ms`);
|
|
284
|
+
reconnectTimer = setTimeout(() => {
|
|
285
|
+
reconnectTimer = null;
|
|
286
|
+
connect();
|
|
287
|
+
}, delay);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
function requireProtocolUpdate(details = {}) {
|
|
292
|
+
if (stopping) return;
|
|
293
|
+
stopping = true;
|
|
294
|
+
connectionState.markStopping("protocol_incompatible");
|
|
295
|
+
clearReconnectTimer();
|
|
296
|
+
stopHeartbeat();
|
|
297
|
+
const socket = activeSocket;
|
|
298
|
+
activeSocket = null;
|
|
299
|
+
try { socket?.terminate(); } catch {}
|
|
300
|
+
desktop.close();
|
|
301
|
+
const expected = details.expectedProtocolVersion ?? "current";
|
|
302
|
+
const received = details.receivedProtocolVersion ?? agentHello.protocolVersion;
|
|
303
|
+
console.error(`Agent protocol is incompatible (local=${received}, relay=${expected}). Update Chat Relay, then reconnect.`);
|
|
304
|
+
setImmediate(() => process.exit(3));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function requireReauthorization(reason = "credential_revoked") {
|
|
146
308
|
if (reauthorizationRequired) return;
|
|
147
309
|
reauthorizationRequired = true;
|
|
310
|
+
connectionState.markReauthorization(reason);
|
|
311
|
+
clearReconnectTimer();
|
|
312
|
+
stopHeartbeat();
|
|
313
|
+
const socket = activeSocket;
|
|
314
|
+
activeSocket = null;
|
|
315
|
+
try { socket?.terminate(); } catch {}
|
|
148
316
|
desktop.close();
|
|
149
|
-
process.exitCode = 2;
|
|
150
317
|
console.error("Agent credential was revoked or rejected.");
|
|
151
318
|
console.error('Recovery: run "chat-relay login --force", then "chat-relay remote".');
|
|
319
|
+
setImmediate(() => process.exit(2));
|
|
152
320
|
}
|
|
153
321
|
|
|
154
322
|
function connect() {
|
|
155
|
-
if (reauthorizationRequired) return;
|
|
323
|
+
if (stopping || reauthorizationRequired) return;
|
|
324
|
+
clearReconnectTimer();
|
|
325
|
+
connectionState.markConnecting();
|
|
156
326
|
console.log(`Connecting to ${wsUrl}`);
|
|
157
327
|
console.log(`Terminal access: ${terminalEnabled ? "enabled" : "disabled"}`);
|
|
158
|
-
|
|
328
|
+
const desktopConfig = desktop.getConfig();
|
|
329
|
+
console.log("Desktop access: " + (!desktopEnabled ? "disabled" : desktopConfig.supported ? "enabled" : `unsupported (${desktopConfig.platform})`));
|
|
330
|
+
|
|
159
331
|
const socket = new WebSocket(wsUrl, {
|
|
160
332
|
headers: { Authorization: `Bearer ${agentToken}` },
|
|
161
333
|
});
|
|
334
|
+
activeSocket = socket;
|
|
162
335
|
|
|
163
|
-
socket.on("open", () =>
|
|
336
|
+
socket.on("open", () => {
|
|
337
|
+
if (socket !== activeSocket) return;
|
|
338
|
+
socket.send(JSON.stringify(agentHello));
|
|
339
|
+
connectionState.markConnected();
|
|
340
|
+
startHeartbeat(socket);
|
|
341
|
+
console.log("Agent connected");
|
|
342
|
+
});
|
|
164
343
|
|
|
165
344
|
socket.on("message", async (raw) => {
|
|
345
|
+
if (socket !== activeSocket) return;
|
|
166
346
|
let message;
|
|
167
347
|
try {
|
|
168
348
|
message = JSON.parse(raw.toString());
|
|
@@ -171,8 +351,11 @@ function connect() {
|
|
|
171
351
|
}
|
|
172
352
|
|
|
173
353
|
if (message?.control === "credential_revoked") {
|
|
174
|
-
requireReauthorization();
|
|
175
|
-
|
|
354
|
+
requireReauthorization("credential_revoked");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (message?.control === "protocol_incompatible") {
|
|
358
|
+
requireProtocolUpdate(message);
|
|
176
359
|
return;
|
|
177
360
|
}
|
|
178
361
|
|
|
@@ -182,61 +365,111 @@ function connect() {
|
|
|
182
365
|
const action = message.payload && typeof message.payload === "object"
|
|
183
366
|
? String(message.payload.action ?? "unknown")
|
|
184
367
|
: "unknown";
|
|
368
|
+
let scheduleMeta = { lane: "unknown", queueWaitMs: 0, queueDepthAtStart: 0, activeAtStart: 0 };
|
|
369
|
+
let handlerDurationMs = 0;
|
|
185
370
|
try {
|
|
186
|
-
const result = await
|
|
371
|
+
const result = await scheduler.run(action, async (meta = {}) => {
|
|
372
|
+
scheduleMeta = { ...scheduleMeta, ...meta };
|
|
373
|
+
const handlerStartedAt = Date.now();
|
|
374
|
+
try {
|
|
375
|
+
return await handlePayload(message.payload);
|
|
376
|
+
} finally {
|
|
377
|
+
handlerDurationMs = Math.max(0, Date.now() - handlerStartedAt);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
187
380
|
recentCalls.push({
|
|
188
381
|
at: new Date().toISOString(),
|
|
189
382
|
action,
|
|
190
383
|
durationMs: Date.now() - startedAt,
|
|
384
|
+
queueWaitMs: scheduleMeta.queueWaitMs,
|
|
385
|
+
handlerDurationMs,
|
|
386
|
+
lane: scheduleMeta.lane,
|
|
191
387
|
ok: !(result && typeof result === "object" && result.ok === false),
|
|
192
388
|
});
|
|
193
389
|
if (recentCalls.length > 100) recentCalls.shift();
|
|
194
|
-
socket
|
|
390
|
+
sendSocketResponse(socket, message.requestId, result, {
|
|
391
|
+
agentQueueWaitMs: scheduleMeta.queueWaitMs,
|
|
392
|
+
agentHandlerMs: handlerDurationMs,
|
|
393
|
+
lane: scheduleMeta.lane,
|
|
394
|
+
});
|
|
195
395
|
} catch (error) {
|
|
196
396
|
recentCalls.push({
|
|
197
397
|
at: new Date().toISOString(),
|
|
198
398
|
action,
|
|
199
399
|
durationMs: Date.now() - startedAt,
|
|
400
|
+
queueWaitMs: scheduleMeta.queueWaitMs,
|
|
401
|
+
handlerDurationMs,
|
|
402
|
+
lane: scheduleMeta.lane,
|
|
200
403
|
ok: false,
|
|
201
404
|
error: error instanceof Error ? error.message : "agent_error",
|
|
202
405
|
});
|
|
203
406
|
if (recentCalls.length > 100) recentCalls.shift();
|
|
204
|
-
socket
|
|
407
|
+
sendSocketResponse(socket, message.requestId, {
|
|
205
408
|
ok: false,
|
|
206
409
|
error: error instanceof Error ? error.message : "agent_error",
|
|
207
|
-
})
|
|
410
|
+
});
|
|
208
411
|
}
|
|
209
412
|
});
|
|
210
413
|
|
|
211
414
|
socket.on("unexpected-response", (_request, response) => {
|
|
212
415
|
if (response.statusCode === 401 || response.statusCode === 403) {
|
|
213
416
|
response.resume();
|
|
214
|
-
requireReauthorization();
|
|
215
|
-
socket.terminate();
|
|
417
|
+
requireReauthorization("credential_rejected");
|
|
216
418
|
}
|
|
217
419
|
});
|
|
218
420
|
|
|
219
421
|
socket.on("close", (code, reason) => {
|
|
422
|
+
if (socket !== activeSocket) return;
|
|
423
|
+
activeSocket = null;
|
|
424
|
+
stopHeartbeat();
|
|
220
425
|
const reasonText = reason.toString();
|
|
221
426
|
console.log("Agent disconnected (" + code + ") " + reasonText);
|
|
222
427
|
if (code === 4001 || reasonText === "credential_revoked") {
|
|
223
|
-
requireReauthorization();
|
|
428
|
+
requireReauthorization("credential_revoked");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (code === 4002 || reasonText === "protocol_incompatible") {
|
|
432
|
+
requireProtocolUpdate({ receivedProtocolVersion: agentHello.protocolVersion });
|
|
224
433
|
return;
|
|
225
434
|
}
|
|
226
|
-
|
|
435
|
+
scheduleReconnect(code, reasonText || "socket_closed");
|
|
227
436
|
});
|
|
228
437
|
|
|
229
438
|
socket.on("error", (error) => {
|
|
230
|
-
if (!reauthorizationRequired)
|
|
439
|
+
if (socket === activeSocket && !stopping && !reauthorizationRequired) {
|
|
440
|
+
console.error("WebSocket error:", error.message);
|
|
441
|
+
}
|
|
231
442
|
});
|
|
232
443
|
}
|
|
233
444
|
|
|
234
|
-
function
|
|
445
|
+
function restartAgentProcess() {
|
|
446
|
+
if (stopping) return;
|
|
447
|
+
stopping = true;
|
|
448
|
+
connectionState.markStopping("restart_requested");
|
|
449
|
+
clearReconnectTimer();
|
|
450
|
+
stopHeartbeat();
|
|
451
|
+
const socket = activeSocket;
|
|
452
|
+
activeSocket = null;
|
|
453
|
+
try { socket?.close(1012, "restart_requested"); } catch {}
|
|
454
|
+
desktop.close();
|
|
455
|
+
setTimeout(() => process.exit(AGENT_RESTART_EXIT_CODE), 50);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function shutdown(reason) {
|
|
459
|
+
if (stopping) return;
|
|
460
|
+
stopping = true;
|
|
461
|
+
connectionState.markStopping(reason);
|
|
462
|
+
clearReconnectTimer();
|
|
463
|
+
stopHeartbeat();
|
|
464
|
+
const socket = activeSocket;
|
|
465
|
+
activeSocket = null;
|
|
466
|
+
try { socket?.close(1000, "client_shutdown"); } catch {}
|
|
235
467
|
desktop.close();
|
|
468
|
+
setTimeout(() => process.exit(0), 50);
|
|
236
469
|
}
|
|
237
470
|
|
|
238
|
-
process.once("SIGINT", shutdown);
|
|
239
|
-
process.once("SIGTERM", shutdown);
|
|
240
|
-
process.once("exit",
|
|
471
|
+
process.once("SIGINT", () => shutdown("SIGINT"));
|
|
472
|
+
process.once("SIGTERM", () => shutdown("SIGTERM"));
|
|
473
|
+
process.once("exit", () => desktop.close());
|
|
241
474
|
|
|
242
475
|
connect();
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const AGENT_PROTOCOL_VERSION = 1;
|
|
4
|
+
|
|
5
|
+
let cachedVersion = null;
|
|
6
|
+
|
|
7
|
+
export function getAgentVersion() {
|
|
8
|
+
if (cachedVersion) return cachedVersion;
|
|
9
|
+
try {
|
|
10
|
+
const packageUrl = new URL("../package.json", import.meta.url);
|
|
11
|
+
const parsed = JSON.parse(fs.readFileSync(packageUrl, "utf8"));
|
|
12
|
+
cachedVersion = typeof parsed.version === "string" && parsed.version ? parsed.version : "0.0.0";
|
|
13
|
+
} catch {
|
|
14
|
+
cachedVersion = "0.0.0";
|
|
15
|
+
}
|
|
16
|
+
return cachedVersion;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getAgentCapabilities(options = {}) {
|
|
20
|
+
const terminalEnabled = options.terminalEnabled === true;
|
|
21
|
+
const desktopEnabled = options.desktopEnabled === true;
|
|
22
|
+
const platform = String(options.platform || process.platform);
|
|
23
|
+
|
|
24
|
+
const capabilities = [
|
|
25
|
+
"transport.heartbeat",
|
|
26
|
+
"filesystem.stat",
|
|
27
|
+
"filesystem.list",
|
|
28
|
+
"filesystem.read",
|
|
29
|
+
"filesystem.readMany",
|
|
30
|
+
"filesystem.batch",
|
|
31
|
+
"filesystem.artifact",
|
|
32
|
+
"filesystem.write",
|
|
33
|
+
"filesystem.edit",
|
|
34
|
+
"filesystem.mkdir",
|
|
35
|
+
"filesystem.move",
|
|
36
|
+
"filesystem.delete",
|
|
37
|
+
"filesystem.search",
|
|
38
|
+
"process.list",
|
|
39
|
+
"process.kill",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
if (terminalEnabled) {
|
|
43
|
+
capabilities.push("terminal.exec", "terminal.sessions", "terminal.batch");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (desktopEnabled && platform === "win32") {
|
|
47
|
+
capabilities.push("desktop.screenshot", "desktop.control", "desktop.step");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return capabilities;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function buildAgentHello(options = {}) {
|
|
54
|
+
const platform = String(options.platform || process.platform);
|
|
55
|
+
return {
|
|
56
|
+
control: "agent_hello",
|
|
57
|
+
protocolVersion: Number(options.protocolVersion ?? AGENT_PROTOCOL_VERSION),
|
|
58
|
+
agentVersion: String(options.agentVersion || getAgentVersion()),
|
|
59
|
+
platform,
|
|
60
|
+
arch: String(options.arch || process.arch),
|
|
61
|
+
capabilities: getAgentCapabilities({
|
|
62
|
+
terminalEnabled: options.terminalEnabled === true,
|
|
63
|
+
desktopEnabled: options.desktopEnabled === true,
|
|
64
|
+
platform,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
}
|