svamp-cli 0.1.75 → 0.1.76
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/agentCommands-NVZzP_Vo.mjs +298 -0
- package/dist/cli.mjs +200 -93
- package/dist/{commands-Dq8WSqvt.mjs → commands-7Iw1nFwf.mjs} +2 -2
- package/dist/{commands-BLjcT1Vl.mjs → commands-CADr1mQg.mjs} +66 -2
- package/dist/{commands-UFi0_ESV.mjs → commands-DJoYOM_1.mjs} +25 -25
- package/dist/{commands-a7p1jW-t.mjs → commands-lJ8V7MJE.mjs} +109 -37
- package/dist/index.mjs +1 -1
- package/dist/{package-BFnad6d1.mjs → package-Dpz1MLO4.mjs} +3 -3
- package/dist/{run-Dy5lxT3M.mjs → run-B29grSMh.mjs} +1 -1
- package/dist/{run-lhAjX4NB.mjs → run-BnFGIK0c.mjs} +172 -36
- package/dist/staticServer-B-S9sl6E.mjs +198 -0
- package/package.json +3 -3
- package/dist/agentCommands-C6iGblcL.mjs +0 -156
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os__default from 'os';
|
|
2
2
|
import fs, { mkdir as mkdir$1, readdir, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
|
|
3
3
|
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync, watch, rmdirSync } from 'fs';
|
|
4
|
-
import
|
|
4
|
+
import path__default, { join, dirname, resolve, basename } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { spawn as spawn$1 } from 'child_process';
|
|
7
7
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
@@ -715,7 +715,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
715
715
|
const session = {
|
|
716
716
|
id: sessionId,
|
|
717
717
|
pty: ptyProcess,
|
|
718
|
-
|
|
718
|
+
outputBuffer: [],
|
|
719
|
+
exited: false,
|
|
719
720
|
cols,
|
|
720
721
|
rows,
|
|
721
722
|
createdAt: Date.now(),
|
|
@@ -725,23 +726,31 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
725
726
|
ptyProcess.onData((data) => {
|
|
726
727
|
const filtered = filterForXterm(data);
|
|
727
728
|
if (!filtered) return;
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
729
|
+
session.outputBuffer.push(filtered);
|
|
730
|
+
if (session.outputBuffer.length > 1e3) {
|
|
731
|
+
session.outputBuffer.splice(0, session.outputBuffer.length - 500);
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
server.emit({
|
|
735
|
+
type: "svamp:terminal-output",
|
|
736
|
+
data: { type: "output", content: filtered, sessionId },
|
|
737
|
+
to: "*"
|
|
738
|
+
});
|
|
739
|
+
} catch {
|
|
734
740
|
}
|
|
735
741
|
});
|
|
736
742
|
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
+
session.exited = true;
|
|
744
|
+
session.exitCode = exitCode;
|
|
745
|
+
session.exitSignal = signal;
|
|
746
|
+
try {
|
|
747
|
+
server.emit({
|
|
748
|
+
type: "svamp:terminal-output",
|
|
749
|
+
data: { type: "exit", content: "", sessionId, exitCode, signal },
|
|
750
|
+
to: "*"
|
|
751
|
+
});
|
|
752
|
+
} catch {
|
|
743
753
|
}
|
|
744
|
-
terminalSessions.delete(sessionId);
|
|
745
754
|
});
|
|
746
755
|
return { sessionId, cols, rows };
|
|
747
756
|
},
|
|
@@ -764,16 +773,24 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
764
773
|
return { success: true };
|
|
765
774
|
},
|
|
766
775
|
/**
|
|
767
|
-
*
|
|
768
|
-
*
|
|
776
|
+
* Read buffered output from a terminal session (polling).
|
|
777
|
+
* Returns { output, exited, exitCode? }. Drains the buffer.
|
|
769
778
|
*/
|
|
770
|
-
|
|
779
|
+
terminalRead: async (params, context) => {
|
|
771
780
|
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
772
781
|
const session = terminalSessions.get(params.sessionId);
|
|
773
782
|
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
783
|
+
const output = session.outputBuffer.splice(0).join("");
|
|
784
|
+
const result = {
|
|
785
|
+
output,
|
|
786
|
+
exited: session.exited
|
|
787
|
+
};
|
|
788
|
+
if (session.exited) {
|
|
789
|
+
result.exitCode = session.exitCode;
|
|
790
|
+
result.exitSignal = session.exitSignal;
|
|
791
|
+
if (!output) terminalSessions.delete(params.sessionId);
|
|
792
|
+
}
|
|
793
|
+
return result;
|
|
777
794
|
},
|
|
778
795
|
/** Stop (kill) a terminal session. */
|
|
779
796
|
terminalStop: async (params, context) => {
|
|
@@ -792,7 +809,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
792
809
|
cols: s.cols,
|
|
793
810
|
rows: s.rows,
|
|
794
811
|
cwd: s.cwd,
|
|
795
|
-
createdAt: s.createdAt
|
|
812
|
+
createdAt: s.createdAt,
|
|
813
|
+
exited: s.exited
|
|
796
814
|
}));
|
|
797
815
|
},
|
|
798
816
|
// Machine-level directory listing (read-only, view role)
|
|
@@ -999,6 +1017,28 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
999
1017
|
};
|
|
1000
1018
|
}
|
|
1001
1019
|
|
|
1020
|
+
function isStructuredMessage(msg) {
|
|
1021
|
+
return !!(msg.from && msg.subject);
|
|
1022
|
+
}
|
|
1023
|
+
function escapeXml(s) {
|
|
1024
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1025
|
+
}
|
|
1026
|
+
function formatInboxMessageXml(msg) {
|
|
1027
|
+
if (!isStructuredMessage(msg)) return msg.body;
|
|
1028
|
+
const attrs = [`message-id="${escapeXml(msg.messageId)}"`];
|
|
1029
|
+
if (msg.from) attrs.push(`from="${escapeXml(msg.from)}"`);
|
|
1030
|
+
if (msg.fromSession) attrs.push(`from-session="${escapeXml(msg.fromSession)}"`);
|
|
1031
|
+
if (msg.to) attrs.push(`to="${escapeXml(msg.to)}"`);
|
|
1032
|
+
if (msg.subject) attrs.push(`subject="${escapeXml(msg.subject)}"`);
|
|
1033
|
+
if (msg.urgency) attrs.push(`urgency="${msg.urgency}"`);
|
|
1034
|
+
if (msg.replyTo) attrs.push(`reply-to="${escapeXml(msg.replyTo)}"`);
|
|
1035
|
+
if (msg.cc && msg.cc.length > 0) attrs.push(`cc="${msg.cc.map(escapeXml).join(",")}"`);
|
|
1036
|
+
if (msg.threadId) attrs.push(`thread-id="${escapeXml(msg.threadId)}"`);
|
|
1037
|
+
attrs.push(`timestamp="${msg.timestamp}"`);
|
|
1038
|
+
return `<svamp-message ${attrs.join(" ")}>
|
|
1039
|
+
${msg.body}
|
|
1040
|
+
</svamp-message>`;
|
|
1041
|
+
}
|
|
1002
1042
|
function loadMessages(messagesDir, sessionId) {
|
|
1003
1043
|
const filePath = join$1(messagesDir, "messages.jsonl");
|
|
1004
1044
|
if (!existsSync(filePath)) return [];
|
|
@@ -1040,6 +1080,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1040
1080
|
mode: "remote",
|
|
1041
1081
|
time: Date.now()
|
|
1042
1082
|
};
|
|
1083
|
+
const inbox = [];
|
|
1084
|
+
const INBOX_MAX = 100;
|
|
1043
1085
|
const listeners = [];
|
|
1044
1086
|
const removeListener = (listener, reason) => {
|
|
1045
1087
|
const idx = listeners.indexOf(listener);
|
|
@@ -1383,6 +1425,43 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
1383
1425
|
}
|
|
1384
1426
|
return await callbacks.onApplySystemPrompt(prompt);
|
|
1385
1427
|
},
|
|
1428
|
+
// ── Inbox ──
|
|
1429
|
+
sendInboxMessage: async (message, context) => {
|
|
1430
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1431
|
+
inbox.push(message);
|
|
1432
|
+
while (inbox.length > INBOX_MAX) inbox.shift();
|
|
1433
|
+
callbacks.onInboxMessage?.(message);
|
|
1434
|
+
notifyListeners({ type: "inbox-update", sessionId, message });
|
|
1435
|
+
return { success: true, messageId: message.messageId };
|
|
1436
|
+
},
|
|
1437
|
+
getInbox: async (opts, context) => {
|
|
1438
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1439
|
+
let result = [...inbox];
|
|
1440
|
+
if (opts?.unread) result = result.filter((m) => !m.read);
|
|
1441
|
+
result.sort((a, b) => b.timestamp - a.timestamp);
|
|
1442
|
+
if (opts?.limit && opts.limit > 0) result = result.slice(0, opts.limit);
|
|
1443
|
+
return { messages: result, total: inbox.length };
|
|
1444
|
+
},
|
|
1445
|
+
markInboxRead: async (messageId, context) => {
|
|
1446
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1447
|
+
const msg = inbox.find((m) => m.messageId === messageId);
|
|
1448
|
+
if (!msg) return { success: false, error: "Message not found" };
|
|
1449
|
+
msg.read = true;
|
|
1450
|
+
notifyListeners({ type: "inbox-update", sessionId, message: msg });
|
|
1451
|
+
return { success: true };
|
|
1452
|
+
},
|
|
1453
|
+
clearInbox: async (opts, context) => {
|
|
1454
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1455
|
+
if (opts?.all) {
|
|
1456
|
+
inbox.length = 0;
|
|
1457
|
+
} else {
|
|
1458
|
+
for (let i = inbox.length - 1; i >= 0; i--) {
|
|
1459
|
+
if (inbox[i].read) inbox.splice(i, 1);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
notifyListeners({ type: "inbox-update", sessionId, cleared: true });
|
|
1463
|
+
return { success: true, remaining: inbox.length };
|
|
1464
|
+
},
|
|
1386
1465
|
// ── Listener Registration ──
|
|
1387
1466
|
registerListener: async (callback, context) => {
|
|
1388
1467
|
authorizeRequest(context, metadata.sharing, "view");
|
|
@@ -4167,6 +4246,8 @@ function sanitizeEnvForSharing(env) {
|
|
|
4167
4246
|
const DEFAULT_PROBE_INTERVAL_S = 10;
|
|
4168
4247
|
const DEFAULT_PROBE_TIMEOUT_S = 5;
|
|
4169
4248
|
const DEFAULT_PROBE_FAILURE_THRESHOLD = 3;
|
|
4249
|
+
const MAX_RESTART_DELAY_S = 300;
|
|
4250
|
+
const BACKOFF_RESET_WINDOW_MS = 6e4;
|
|
4170
4251
|
const MAX_LOG_LINES = 300;
|
|
4171
4252
|
class ProcessSupervisor {
|
|
4172
4253
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -4364,7 +4445,7 @@ class ProcessSupervisor {
|
|
|
4364
4445
|
for (const file of files) {
|
|
4365
4446
|
if (!file.endsWith(".json")) continue;
|
|
4366
4447
|
try {
|
|
4367
|
-
const raw = await readFile(
|
|
4448
|
+
const raw = await readFile(path__default.join(this.persistDir, file), "utf-8");
|
|
4368
4449
|
const spec = JSON.parse(raw);
|
|
4369
4450
|
const entry = this.makeEntry(spec);
|
|
4370
4451
|
this.entries.set(spec.id, entry);
|
|
@@ -4385,14 +4466,14 @@ class ProcessSupervisor {
|
|
|
4385
4466
|
}
|
|
4386
4467
|
}
|
|
4387
4468
|
async persistSpec(spec) {
|
|
4388
|
-
const filePath =
|
|
4469
|
+
const filePath = path__default.join(this.persistDir, `${spec.id}.json`);
|
|
4389
4470
|
const tmpPath = filePath + ".tmp";
|
|
4390
4471
|
await writeFile$1(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4391
4472
|
await rename(tmpPath, filePath);
|
|
4392
4473
|
}
|
|
4393
4474
|
async deleteSpec(id) {
|
|
4394
4475
|
try {
|
|
4395
|
-
await unlink(
|
|
4476
|
+
await unlink(path__default.join(this.persistDir, `${id}.json`));
|
|
4396
4477
|
} catch {
|
|
4397
4478
|
}
|
|
4398
4479
|
}
|
|
@@ -4498,8 +4579,19 @@ class ProcessSupervisor {
|
|
|
4498
4579
|
state.status = "failed";
|
|
4499
4580
|
return;
|
|
4500
4581
|
}
|
|
4501
|
-
const
|
|
4502
|
-
|
|
4582
|
+
const uptime = state.startedAt ? Date.now() - state.startedAt : 0;
|
|
4583
|
+
const baseDelay = spec.restartDelay * 1e3;
|
|
4584
|
+
let delayMs;
|
|
4585
|
+
if (uptime > BACKOFF_RESET_WINDOW_MS) {
|
|
4586
|
+
state.restartCount = 0;
|
|
4587
|
+
delayMs = baseDelay;
|
|
4588
|
+
} else {
|
|
4589
|
+
const backoffExponent = Math.min(state.restartCount, 10);
|
|
4590
|
+
delayMs = Math.min(baseDelay * Math.pow(2, backoffExponent), MAX_RESTART_DELAY_S * 1e3);
|
|
4591
|
+
const jitter = (Math.random() * 0.2 - 0.1) * delayMs;
|
|
4592
|
+
delayMs = Math.max(baseDelay, Math.round(delayMs + jitter));
|
|
4593
|
+
}
|
|
4594
|
+
console.log(`[SUPERVISOR] Scheduling restart of '${spec.name}' in ${delayMs}ms (restart #${state.restartCount + 1}, uptime=${Math.round(uptime / 1e3)}s)`);
|
|
4503
4595
|
entry.restartTimer = setTimeout(() => {
|
|
4504
4596
|
if (entry.stopping) return;
|
|
4505
4597
|
state.restartCount++;
|
|
@@ -6335,6 +6427,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6335
6427
|
logger.log(`[Session ${sessionId}] Retrying startup without --resume`);
|
|
6336
6428
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6337
6429
|
sessionService.updateMetadata(sessionMetadata);
|
|
6430
|
+
sessionService.sendKeepAlive(true);
|
|
6338
6431
|
spawnClaude(startupRetryMessage);
|
|
6339
6432
|
return;
|
|
6340
6433
|
}
|
|
@@ -6596,6 +6689,25 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6596
6689
|
logger.log(`[Session ${sessionId}] Kill session requested`);
|
|
6597
6690
|
stopSession(sessionId);
|
|
6598
6691
|
},
|
|
6692
|
+
onInboxMessage: (message) => {
|
|
6693
|
+
if (trackedSession?.stopped) return;
|
|
6694
|
+
logger.log(`[Session ${sessionId}] Inbox message received (urgency: ${message.urgency || "normal"}, from: ${message.from || "unknown"})`);
|
|
6695
|
+
if (message.urgency === "urgent") {
|
|
6696
|
+
const formatted = formatInboxMessageXml(message);
|
|
6697
|
+
logger.log(`[Session ${sessionId}] Delivering urgent inbox message to agent`);
|
|
6698
|
+
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
6699
|
+
spawnClaude(formatted);
|
|
6700
|
+
} else {
|
|
6701
|
+
const stdinMsg = JSON.stringify({
|
|
6702
|
+
type: "user",
|
|
6703
|
+
message: { role: "user", content: formatted }
|
|
6704
|
+
});
|
|
6705
|
+
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
6706
|
+
}
|
|
6707
|
+
signalProcessing(true);
|
|
6708
|
+
sessionWasProcessing = true;
|
|
6709
|
+
}
|
|
6710
|
+
},
|
|
6599
6711
|
onMetadataUpdate: (newMeta) => {
|
|
6600
6712
|
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
6601
6713
|
sessionMetadata = {
|
|
@@ -6938,6 +7050,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6938
7050
|
}
|
|
6939
7051
|
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6940
7052
|
sessionService.updateMetadata(sessionMetadata);
|
|
7053
|
+
sessionService.sendKeepAlive(true);
|
|
6941
7054
|
agentBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
6942
7055
|
logger.error(`[${agentName} Session ${sessionId}] Error sending prompt:`, err);
|
|
6943
7056
|
if (!acpStopped) {
|
|
@@ -6999,6 +7112,25 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6999
7112
|
logger.log(`[${agentName} Session ${sessionId}] Kill session requested`);
|
|
7000
7113
|
stopSession(sessionId);
|
|
7001
7114
|
},
|
|
7115
|
+
onInboxMessage: (message) => {
|
|
7116
|
+
if (acpStopped) return;
|
|
7117
|
+
logger.log(`[${agentName} Session ${sessionId}] Inbox message received (urgency: ${message.urgency || "normal"}, from: ${message.from || "unknown"})`);
|
|
7118
|
+
if (message.urgency === "urgent" && acpBackendReady) {
|
|
7119
|
+
const formatted = formatInboxMessageXml(message);
|
|
7120
|
+
logger.log(`[${agentName} Session ${sessionId}] Delivering urgent inbox message to agent`);
|
|
7121
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
7122
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
7123
|
+
sessionService.sendKeepAlive(true);
|
|
7124
|
+
agentBackend.sendPrompt(sessionId, formatted).catch((err) => {
|
|
7125
|
+
logger.error(`[${agentName} Session ${sessionId}] Error delivering urgent inbox message: ${err.message}`);
|
|
7126
|
+
if (!acpStopped) {
|
|
7127
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
7128
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
7129
|
+
sessionService.sendSessionEnd();
|
|
7130
|
+
}
|
|
7131
|
+
});
|
|
7132
|
+
}
|
|
7133
|
+
},
|
|
7002
7134
|
onMetadataUpdate: (newMeta) => {
|
|
7003
7135
|
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
7004
7136
|
sessionMetadata = {
|
|
@@ -7691,7 +7823,6 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7691
7823
|
console.log(` Log file: ${logger.logFilePath}`);
|
|
7692
7824
|
const HEARTBEAT_INTERVAL_MS = 1e4;
|
|
7693
7825
|
const PING_TIMEOUT_MS = 6e4;
|
|
7694
|
-
const MAX_FAILURES = 60;
|
|
7695
7826
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
7696
7827
|
let heartbeatRunning = false;
|
|
7697
7828
|
let lastReconnectAt = 0;
|
|
@@ -7762,7 +7893,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7762
7893
|
if (consecutiveHeartbeatFailures === 1) {
|
|
7763
7894
|
logger.log(`Ping failed: ${err.message}`);
|
|
7764
7895
|
} else if (consecutiveHeartbeatFailures % 6 === 0) {
|
|
7765
|
-
logger.log(`Connection down for ${consecutiveHeartbeatFailures * HEARTBEAT_INTERVAL_MS / 1e3}s (${consecutiveHeartbeatFailures}
|
|
7896
|
+
logger.log(`Connection down for ${consecutiveHeartbeatFailures * HEARTBEAT_INTERVAL_MS / 1e3}s (${consecutiveHeartbeatFailures} failures, retrying indefinitely)`);
|
|
7766
7897
|
}
|
|
7767
7898
|
if (consecutiveHeartbeatFailures === 1 || consecutiveHeartbeatFailures % 3 === 0) {
|
|
7768
7899
|
const conn = server.rpc?._connection;
|
|
@@ -7775,17 +7906,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7775
7906
|
}
|
|
7776
7907
|
}
|
|
7777
7908
|
if (conn?._reader) {
|
|
7778
|
-
logger.log("
|
|
7909
|
+
logger.log("Cancelling stale HTTP stream reader to trigger reconnection");
|
|
7779
7910
|
try {
|
|
7780
7911
|
conn._reader.cancel?.("Stale connection");
|
|
7781
7912
|
} catch {
|
|
7782
7913
|
}
|
|
7914
|
+
} else if (conn?._abort_controller) {
|
|
7915
|
+
logger.log("Aborting stale HTTP stream to trigger reconnection");
|
|
7916
|
+
try {
|
|
7917
|
+
conn._abort_controller.abort();
|
|
7918
|
+
} catch {
|
|
7919
|
+
}
|
|
7783
7920
|
}
|
|
7784
7921
|
}
|
|
7785
|
-
if (consecutiveHeartbeatFailures >= MAX_FAILURES) {
|
|
7786
|
-
logger.log(`Heartbeat failed ${MAX_FAILURES} times. Shutting down.`);
|
|
7787
|
-
requestShutdown("heartbeat-timeout", err.message);
|
|
7788
|
-
}
|
|
7789
7922
|
}
|
|
7790
7923
|
}
|
|
7791
7924
|
} finally {
|
|
@@ -7880,8 +8013,11 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7880
8013
|
};
|
|
7881
8014
|
const shutdownReq = await resolvesWhenShutdownRequested;
|
|
7882
8015
|
await cleanup(shutdownReq.source);
|
|
7883
|
-
if (process.env.SVAMP_SUPERVISED === "1"
|
|
7884
|
-
|
|
8016
|
+
if (process.env.SVAMP_SUPERVISED === "1") {
|
|
8017
|
+
const intentionalSources = ["os-signal", "os-signal-cleanup", "hypha-app"];
|
|
8018
|
+
if (!intentionalSources.includes(shutdownReq.source)) {
|
|
8019
|
+
process.exit(1);
|
|
8020
|
+
}
|
|
7885
8021
|
}
|
|
7886
8022
|
process.exit(0);
|
|
7887
8023
|
} catch (error) {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as net from 'net';
|
|
5
|
+
|
|
6
|
+
const MIME_TYPES = {
|
|
7
|
+
".html": "text/html; charset=utf-8",
|
|
8
|
+
".htm": "text/html; charset=utf-8",
|
|
9
|
+
".css": "text/css; charset=utf-8",
|
|
10
|
+
".js": "application/javascript; charset=utf-8",
|
|
11
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
12
|
+
".json": "application/json; charset=utf-8",
|
|
13
|
+
".xml": "application/xml; charset=utf-8",
|
|
14
|
+
".csv": "text/csv; charset=utf-8",
|
|
15
|
+
".txt": "text/plain; charset=utf-8",
|
|
16
|
+
".md": "text/markdown; charset=utf-8",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".gif": "image/gif",
|
|
21
|
+
".svg": "image/svg+xml",
|
|
22
|
+
".ico": "image/x-icon",
|
|
23
|
+
".webp": "image/webp",
|
|
24
|
+
".avif": "image/avif",
|
|
25
|
+
".woff": "font/woff",
|
|
26
|
+
".woff2": "font/woff2",
|
|
27
|
+
".ttf": "font/ttf",
|
|
28
|
+
".otf": "font/otf",
|
|
29
|
+
".eot": "application/vnd.ms-fontobject",
|
|
30
|
+
".pdf": "application/pdf",
|
|
31
|
+
".zip": "application/zip",
|
|
32
|
+
".gz": "application/gzip",
|
|
33
|
+
".tar": "application/x-tar",
|
|
34
|
+
".wasm": "application/wasm",
|
|
35
|
+
".mp4": "video/mp4",
|
|
36
|
+
".webm": "video/webm",
|
|
37
|
+
".mp3": "audio/mpeg",
|
|
38
|
+
".ogg": "audio/ogg",
|
|
39
|
+
".wav": "audio/wav",
|
|
40
|
+
".yaml": "text/yaml; charset=utf-8",
|
|
41
|
+
".yml": "text/yaml; charset=utf-8",
|
|
42
|
+
".toml": "text/plain; charset=utf-8"
|
|
43
|
+
};
|
|
44
|
+
function getMimeType(filePath) {
|
|
45
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
46
|
+
return MIME_TYPES[ext] || "application/octet-stream";
|
|
47
|
+
}
|
|
48
|
+
function setCorsHeaders(res) {
|
|
49
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
50
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
|
51
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
52
|
+
res.setHeader("Access-Control-Max-Age", "86400");
|
|
53
|
+
}
|
|
54
|
+
async function findAvailablePort(startPort) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const server = net.createServer();
|
|
57
|
+
server.listen(startPort, "127.0.0.1", () => {
|
|
58
|
+
const addr = server.address();
|
|
59
|
+
server.close(() => resolve(addr.port));
|
|
60
|
+
});
|
|
61
|
+
server.on("error", () => {
|
|
62
|
+
if (startPort < 65535) {
|
|
63
|
+
findAvailablePort(startPort + 1).then(resolve, reject);
|
|
64
|
+
} else {
|
|
65
|
+
reject(new Error("No available ports"));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function generateDirectoryListing(dirPath, urlPath) {
|
|
71
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
72
|
+
const rows = entries.sort((a, b) => {
|
|
73
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
74
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
75
|
+
return a.name.localeCompare(b.name);
|
|
76
|
+
}).map((entry) => {
|
|
77
|
+
const name = entry.isDirectory() ? `${entry.name}/` : entry.name;
|
|
78
|
+
const href = path.posix.join(urlPath, entry.name) + (entry.isDirectory() ? "/" : "");
|
|
79
|
+
let size = "";
|
|
80
|
+
if (!entry.isDirectory()) {
|
|
81
|
+
try {
|
|
82
|
+
const stat = fs.statSync(path.join(dirPath, entry.name));
|
|
83
|
+
size = formatSize(stat.size);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return `<tr><td><a href="${escapeHtml(href)}">${escapeHtml(name)}</a></td><td>${size}</td></tr>`;
|
|
88
|
+
}).join("\n");
|
|
89
|
+
const parent = urlPath !== "/" ? `<tr><td><a href="${escapeHtml(path.posix.dirname(urlPath))}/">..</a></td><td></td></tr>
|
|
90
|
+
` : "";
|
|
91
|
+
return `<!DOCTYPE html>
|
|
92
|
+
<html><head><meta charset="utf-8"><title>Index of ${escapeHtml(urlPath)}</title>
|
|
93
|
+
<style>body{font-family:monospace;margin:2em}table{border-collapse:collapse}td{padding:4px 16px}a{text-decoration:none;color:#0366d6}a:hover{text-decoration:underline}tr:hover{background:#f6f8fa}</style>
|
|
94
|
+
</head><body><h1>Index of ${escapeHtml(urlPath)}</h1><table>${parent}${rows}</table></body></html>`;
|
|
95
|
+
}
|
|
96
|
+
function escapeHtml(s) {
|
|
97
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
98
|
+
}
|
|
99
|
+
function formatSize(bytes) {
|
|
100
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
101
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
102
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
103
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
104
|
+
}
|
|
105
|
+
async function startStaticServer(options) {
|
|
106
|
+
const { directory, listing = true } = options;
|
|
107
|
+
const rootDir = path.resolve(directory);
|
|
108
|
+
if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
|
|
109
|
+
throw new Error(`Not a directory: ${rootDir}`);
|
|
110
|
+
}
|
|
111
|
+
const server = http.createServer((req, res) => {
|
|
112
|
+
setCorsHeaders(res);
|
|
113
|
+
if (req.method === "OPTIONS") {
|
|
114
|
+
res.writeHead(204);
|
|
115
|
+
res.end();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
119
|
+
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
120
|
+
res.end("Method Not Allowed");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const url = new URL(req.url || "/", `http://localhost`);
|
|
124
|
+
const decodedPath = decodeURIComponent(url.pathname);
|
|
125
|
+
const normalizedPath = path.normalize(decodedPath);
|
|
126
|
+
if (normalizedPath.includes("..")) {
|
|
127
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
128
|
+
res.end("Forbidden");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const filePath = path.join(rootDir, normalizedPath);
|
|
132
|
+
if (!filePath.startsWith(rootDir)) {
|
|
133
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
134
|
+
res.end("Forbidden");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const stat = fs.statSync(filePath);
|
|
139
|
+
if (stat.isDirectory()) {
|
|
140
|
+
const indexPath = path.join(filePath, "index.html");
|
|
141
|
+
if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
|
|
142
|
+
serveFile(indexPath, req, res);
|
|
143
|
+
} else if (listing) {
|
|
144
|
+
if (!decodedPath.endsWith("/")) {
|
|
145
|
+
res.writeHead(301, { Location: decodedPath + "/" });
|
|
146
|
+
res.end();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const html = generateDirectoryListing(filePath, decodedPath);
|
|
150
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
151
|
+
res.end(html);
|
|
152
|
+
} else {
|
|
153
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
154
|
+
res.end("Not Found");
|
|
155
|
+
}
|
|
156
|
+
} else if (stat.isFile()) {
|
|
157
|
+
serveFile(filePath, req, res);
|
|
158
|
+
} else {
|
|
159
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
160
|
+
res.end("Not Found");
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
164
|
+
res.end("Not Found");
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
const port = await findAvailablePort(options.port || 18080);
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
server.listen(port, "127.0.0.1", () => {
|
|
170
|
+
resolve({
|
|
171
|
+
server,
|
|
172
|
+
port,
|
|
173
|
+
close: () => server.close()
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
server.on("error", reject);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function serveFile(filePath, req, res) {
|
|
180
|
+
const stat = fs.statSync(filePath);
|
|
181
|
+
const contentType = getMimeType(filePath);
|
|
182
|
+
res.writeHead(200, {
|
|
183
|
+
"Content-Type": contentType,
|
|
184
|
+
"Content-Length": stat.size,
|
|
185
|
+
"Cache-Control": "no-cache"
|
|
186
|
+
});
|
|
187
|
+
if (req.method === "HEAD") {
|
|
188
|
+
res.end();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const stream = fs.createReadStream(filePath);
|
|
192
|
+
stream.pipe(res);
|
|
193
|
+
stream.on("error", () => {
|
|
194
|
+
res.end();
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { startStaticServer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svamp-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.76",
|
|
4
4
|
"description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
|
|
5
5
|
"author": "Amun AI AB",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "rm -rf dist && tsc --noEmit && pkgroll",
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
|
-
"test": "npx tsx test/test-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs",
|
|
23
|
+
"test": "npx tsx test/test-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.mjs",
|
|
24
24
|
"test:hypha": "node --no-warnings test/test-hypha-service.mjs",
|
|
25
25
|
"dev": "tsx src/cli.ts",
|
|
26
26
|
"dev:daemon": "tsx src/cli.ts daemon start-sync",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@agentclientprotocol/sdk": "^0.14.1",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
32
|
-
"hypha-rpc": "0.21.
|
|
32
|
+
"hypha-rpc": "0.21.34",
|
|
33
33
|
"node-pty": "^1.1.0",
|
|
34
34
|
"ws": "^8.18.0",
|
|
35
35
|
"yaml": "^2.8.2",
|