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.
@@ -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 path, { join, dirname, resolve, basename } from 'path';
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
- listeners: [],
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
- const update = { type: "output", content: filtered, sessionId };
729
- for (const listener of session.listeners) {
730
- try {
731
- listener(update);
732
- } catch {
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
- const update = { type: "exit", content: "", sessionId, exitCode, signal };
738
- for (const listener of session.listeners) {
739
- try {
740
- listener(update);
741
- } catch {
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
- * Attach to a terminal session for real-time output streaming.
768
- * The onData callback receives { type: 'output'|'exit', content, sessionId }.
776
+ * Read buffered output from a terminal session (polling).
777
+ * Returns { output, exited, exitCode? }. Drains the buffer.
769
778
  */
770
- terminalAttach: async (params, context) => {
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
- if (typeof params.onData !== "function") throw new Error("onData callback is required");
775
- session.listeners.push(params.onData);
776
- return { success: true, cols: session.cols, rows: session.rows };
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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(path.join(this.persistDir, file), "utf-8");
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 = path.join(this.persistDir, `${spec.id}.json`);
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(path.join(this.persistDir, `${id}.json`));
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 delayMs = spec.restartDelay * 1e3;
4502
- console.log(`[SUPERVISOR] Scheduling restart of '${spec.name}' in ${delayMs}ms (restart #${state.restartCount + 1})`);
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}/${MAX_FAILURES})`);
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("Aborting stale HTTP stream to trigger reconnection");
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" && shutdownReq.source === "version-update") {
7884
- process.exit(1);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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.75",
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.29",
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",