svamp-cli 0.2.256 → 0.2.258

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/bin/skills/artifact/SKILL.md +1 -1
  2. package/dist/{agentCommands-DfTICQUR.mjs → agentCommands-CQ89BZon.mjs} +5 -5
  3. package/dist/{auth-CaFzUB-H.mjs → auth-CaFsKYbx.mjs} +1 -1
  4. package/dist/cli.mjs +61 -61
  5. package/dist/{commands-MopZEqZ7.mjs → commands--X8f4eSr.mjs} +24 -13
  6. package/dist/{commands-Dp4OLJ80.mjs → commands-B6eGMF5H.mjs} +1 -1
  7. package/dist/{commands-Cgi-GKQk.mjs → commands-BgX0W0uK.mjs} +1 -1
  8. package/dist/{commands-BrTyEX8u.mjs → commands-D8l74hsL.mjs} +1 -1
  9. package/dist/{commands-CRvH25K7.mjs → commands-Dd8xEIf0.mjs} +1 -1
  10. package/dist/{commands-C6_ClNDg.mjs → commands-DrfYv705.mjs} +2 -2
  11. package/dist/{commands-DPKRLSJZ.mjs → commands-DutfM3U7.mjs} +31 -15
  12. package/dist/{fleet-DT3LusNA.mjs → fleet-UvQnIWT1.mjs} +1 -1
  13. package/dist/{frpc--8kUn3Em.mjs → frpc-IXxyfgPn.mjs} +1 -1
  14. package/dist/{headlessCli-DJIDtYga.mjs → headlessCli-BK7WSS3D.mjs} +2 -2
  15. package/dist/index.mjs +1 -1
  16. package/dist/{package-CBpvrZJs.mjs → package-BdZX5mDv.mjs} +2 -2
  17. package/dist/{rpc-DPlerDzK.mjs → rpc-CyACkgY1.mjs} +1 -1
  18. package/dist/{rpc-BV_9jVwX.mjs → rpc-D7DB3XCH.mjs} +1 -1
  19. package/dist/{run-BDX-RaEm.mjs → run-C1Unk4Zx.mjs} +108 -84
  20. package/dist/{run-19FUjQ96.mjs → run-C1ufZjIW.mjs} +1 -1
  21. package/dist/{scheduler-CueRBYz_.mjs → scheduler-7wve0Fy3.mjs} +1 -1
  22. package/dist/{serveCommands-DRP03-lg.mjs → serveCommands-iQJJLsMJ.mjs} +5 -5
  23. package/dist/{serveManager-CQGY6nFz.mjs → serveManager-BszV0oIl.mjs} +62 -74
  24. package/dist/{sideband-BQ4pkLIQ.mjs → sideband-DMPZ-ZoR.mjs} +1 -1
  25. package/dist/staticFileServer-FCEOLIYH.mjs +239 -0
  26. package/package.json +2 -2
  27. package/dist/caddy-CuTbE3NY.mjs +0 -322
@@ -1,10 +1,11 @@
1
- import { spawn } from 'child_process';
1
+ import { spawn, execSync } from 'child_process';
2
2
  import * as crypto from 'crypto';
3
3
  import * as fs from 'fs';
4
4
  import * as http from 'http';
5
5
  import * as net from 'net';
6
6
  import * as path from 'path';
7
- import { l as getHyphaServerUrl, S as ServeAuth, m as hasCookieToken } from './run-BDX-RaEm.mjs';
7
+ import { serveStaticMount } from './staticFileServer-FCEOLIYH.mjs';
8
+ import { l as getHyphaServerUrl, S as ServeAuth, m as hasCookieToken } from './run-C1Unk4Zx.mjs';
8
9
  import 'os';
9
10
  import 'fs/promises';
10
11
  import 'url';
@@ -56,6 +57,27 @@ async function tryReservePort(preferred) {
56
57
  });
57
58
  });
58
59
  }
60
+ function killOrphanedCaddy(log) {
61
+ if (process.platform === "win32") return;
62
+ try {
63
+ const out = execSync("ps -axo pid=,command=", { encoding: "utf-8" });
64
+ const pids = [];
65
+ for (const line of out.split("\n")) {
66
+ const m = /^\s*(\d+)\s+(.*)$/.exec(line);
67
+ if (!m) continue;
68
+ const [, pid, cmd] = m;
69
+ if (cmd.includes(".svamp/bin/caddy") && pid !== String(process.pid)) pids.push(pid);
70
+ }
71
+ for (const pid of pids) {
72
+ try {
73
+ process.kill(Number(pid), "SIGKILL");
74
+ } catch {
75
+ }
76
+ }
77
+ if (pids.length) log(`Reaped ${pids.length} orphaned Caddy process(es) \u2014 retired in favor of the native file server (#0358).`);
78
+ } catch {
79
+ }
80
+ }
59
81
  const MOUNT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
60
82
  function validateMountName(name) {
61
83
  if (!name || name.length > 128) {
@@ -68,18 +90,14 @@ function validateMountName(name) {
68
90
  class ServeManager {
69
91
  mounts = /* @__PURE__ */ new Map();
70
92
  port = 0;
71
- // auth proxy public port
72
- caddyPort = 0;
73
- // Caddy internal port
74
- /** Last-used ports loaded from disk — used as preferred values on next start
93
+ // public HTTP server port (serves static files natively — #0358)
94
+ /** Last-used port loaded from disk — used as the preferred value on next start
75
95
  * so external configs (tunnels, cron, bookmarks) survive daemon restarts. */
76
96
  persistedPort;
77
- persistedCaddyPort;
78
97
  /** Per-mount frpc tunnel — each mount gets its own subdomain `static-<name>-<hash>`. */
79
98
  mountTunnels = /* @__PURE__ */ new Map();
80
99
  /** hostname (lowercased, no port) → mount name. Updated when tunnels connect. */
81
100
  hostToMount = /* @__PURE__ */ new Map();
82
- caddy = null;
83
101
  proxyServer = null;
84
102
  auth = null;
85
103
  /** Live child processes for managed mounts. Keyed by mount name. */
@@ -159,9 +177,6 @@ class ServeManager {
159
177
  };
160
178
  this.mounts.set(spec.name, mount);
161
179
  await this.ensureRunning();
162
- if (resolvedDir && this.caddy?.isRunning) {
163
- await this.caddy.addMount(spec.name, resolvedDir);
164
- }
165
180
  await this.startMountTunnel(spec.name);
166
181
  if (spec.process && !spec.process.wakeOnRequest) {
167
182
  try {
@@ -198,13 +213,6 @@ class ServeManager {
198
213
  for (const [host, mountName] of this.hostToMount.entries()) {
199
214
  if (mountName === name) this.hostToMount.delete(host);
200
215
  }
201
- if (this.caddy?.isRunning) {
202
- try {
203
- await this.caddy.removeMount(name);
204
- } catch (err) {
205
- this.log(`Warning: Caddy mount removal failed: ${err.message}`);
206
- }
207
- }
208
216
  this.persist();
209
217
  this.log(`Mount removed: ${name}`);
210
218
  if (this.mounts.size === 0) {
@@ -225,12 +233,12 @@ class ServeManager {
225
233
  * Get server info — each mount has its own URL (per-mount subdomain).
226
234
  */
227
235
  getInfo() {
228
- const running = this.caddy?.isRunning ?? false;
236
+ const running = this.proxyServer != null;
229
237
  const firstMount = this.mounts.values().next().value;
230
238
  const firstUrl = firstMount ? this.getMountUrl(firstMount.name) : null;
231
239
  return {
232
240
  url: firstUrl,
233
- port: running ? this.caddy?.port ?? this.port : 0,
241
+ port: running ? this.port : 0,
234
242
  authProxyPort: running ? this.port : 0,
235
243
  running,
236
244
  mountCount: this.mounts.size,
@@ -249,7 +257,6 @@ class ServeManager {
249
257
  if (!fs.existsSync(this.persistFile)) return;
250
258
  const raw = JSON.parse(fs.readFileSync(this.persistFile, "utf-8"));
251
259
  this.persistedPort = raw.port;
252
- this.persistedCaddyPort = raw.caddyPort;
253
260
  if (!raw.mounts || raw.mounts.length === 0) return;
254
261
  let restoredCount = 0;
255
262
  for (const m of raw.mounts) {
@@ -327,11 +334,6 @@ class ServeManager {
327
334
  await new Promise((resolve) => this.proxyServer.close(() => resolve()));
328
335
  this.proxyServer = null;
329
336
  }
330
- if (this.caddy) {
331
- await this.caddy.stop();
332
- this.caddy = null;
333
- this.log("Caddy stopped");
334
- }
335
337
  this.auth?.destroy();
336
338
  }
337
339
  // ── Managed-process lifecycle ─────────────────────────────────────────
@@ -488,8 +490,7 @@ class ServeManager {
488
490
  persist() {
489
491
  const state = {
490
492
  mounts: Array.from(this.mounts.values()),
491
- ...this.port > 0 ? { port: this.port } : {},
492
- ...this.caddyPort > 0 ? { caddyPort: this.caddyPort } : {}
493
+ ...this.port > 0 ? { port: this.port } : {}
493
494
  };
494
495
  try {
495
496
  fs.writeFileSync(this.persistFile, JSON.stringify(state, null, 2));
@@ -499,32 +500,24 @@ class ServeManager {
499
500
  }
500
501
  /** Start auth proxy + Caddy if not already running. Per-mount tunnels are started separately. */
501
502
  async ensureRunning() {
502
- if (this.caddy?.isRunning) return;
503
- this.caddyPort = await tryReservePort(this.persistedCaddyPort);
503
+ if (this.proxyServer) return;
504
+ killOrphanedCaddy((m) => this.log(m));
504
505
  this.port = await tryReservePort(this.persistedPort);
505
- const adminPort = await findFreePort();
506
506
  if (this.persistedPort && this.port !== this.persistedPort) {
507
507
  this.log(`\u26A0 Previous serve port ${this.persistedPort} unavailable \u2014 using ${this.port}. Downstream configs referencing the old port will need updating.`);
508
508
  }
509
- if (this.persistedCaddyPort && this.caddyPort !== this.persistedCaddyPort) {
510
- this.log(`\u26A0 Previous Caddy port ${this.persistedCaddyPort} unavailable \u2014 using ${this.caddyPort} (Caddy is internal; no downstream impact).`);
511
- }
512
- const { CaddyManager } = await import('./caddy-CuTbE3NY.mjs');
513
- this.caddy = new CaddyManager({
514
- listenPort: this.caddyPort,
515
- adminPort,
516
- configDir: path.join(path.dirname(this.persistFile), "caddy"),
517
- log: (msg) => this.log(`[Caddy] ${msg}`)
518
- });
519
- for (const mount of this.mounts.values()) {
520
- if (mount.directory) {
521
- await this.caddy.addMount(mount.name, mount.directory);
522
- }
523
- }
524
- await this.caddy.start();
525
- this.log(`Caddy file server started on 127.0.0.1:${this.caddyPort}`);
526
509
  await this.startAuthProxy();
527
- this.log(`Auth proxy started on 127.0.0.1:${this.port}`);
510
+ this.log(`Static file server started on 127.0.0.1:${this.port}`);
511
+ }
512
+ /** Minimal mount-listing page for the server root (replaces Caddy's root route). */
513
+ mountListHtml() {
514
+ const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
515
+ const items = Array.from(this.mounts.values()).filter((m) => m.directory).map((m) => `<li><a href="/${esc(m.name)}/">${esc(m.name)}</a> \u2014 ${esc(m.directory || "")}</li>`).join("\n");
516
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Svamp File Server</title>
517
+ <style>body{font-family:system-ui,sans-serif;max-width:700px;margin:40px auto;padding:0 20px;color:#333}
518
+ h1{font-size:1.3rem}ul{list-style:none;padding:0}li{padding:8px 0;border-bottom:1px solid #eee}
519
+ a{color:#0969da;text-decoration:none;font-weight:500}a:hover{text-decoration:underline}</style></head>
520
+ <body><h1>Svamp File Server</h1><ul>${items || "<li>No mounts registered.</li>"}</ul></body></html>`;
528
521
  }
529
522
  /** Start a lightweight Node.js HTTP proxy with auth + upload support. */
530
523
  startAuthProxy() {
@@ -658,7 +651,7 @@ class ServeManager {
658
651
  const handle = this.managedProcs.get(mount.name);
659
652
  if (handle) handle.lastRequestAt = Date.now();
660
653
  const targetPath = mountResolvedByHost ? req.url || "/" : (basePath || "/") + (url.search || "");
661
- const proxyReq2 = http.request({
654
+ const proxyReq = http.request({
662
655
  hostname: "127.0.0.1",
663
656
  port: cfg.port,
664
657
  path: targetPath,
@@ -668,37 +661,32 @@ class ServeManager {
668
661
  res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
669
662
  proxyRes.pipe(res);
670
663
  });
671
- proxyReq2.on("error", (err) => {
664
+ proxyReq.on("error", (err) => {
672
665
  if (!res.headersSent) {
673
666
  res.writeHead(502);
674
667
  res.end(`Backend error: ${err.message}`);
675
668
  }
676
669
  });
677
- req.pipe(proxyReq2);
670
+ req.pipe(proxyReq);
678
671
  return;
679
672
  }
680
- let proxyPath = req.url || "/";
681
- if (mountResolvedByHost && mountName) {
682
- const search = url.search || "";
683
- proxyPath = `/${mountName}${basePath === "/" ? "/" : basePath}${search}`;
673
+ if (mount && mount.directory) {
674
+ serveStaticMount(req, res, {
675
+ rootDir: mount.directory,
676
+ relPath: basePath || "/",
677
+ // Path-based access is prefixed with /<mountName>; host-based has no prefix.
678
+ mountUrlPrefix: mountResolvedByHost ? "" : `/${mountName}`,
679
+ browse: true});
680
+ return;
681
+ }
682
+ const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS", "Access-Control-Allow-Headers": "*" };
683
+ if (url.pathname === "/" || basePath === "/") {
684
+ res.writeHead(200, { ...cors, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
685
+ res.end(this.mountListHtml());
686
+ } else {
687
+ res.writeHead(404, { ...cors, "Content-Type": "text/plain" });
688
+ res.end("Not found");
684
689
  }
685
- const proxyReq = http.request({
686
- hostname: "127.0.0.1",
687
- port: this.caddyPort,
688
- path: proxyPath,
689
- method: req.method,
690
- headers: req.headers
691
- }, (proxyRes) => {
692
- res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
693
- proxyRes.pipe(res);
694
- });
695
- proxyReq.on("error", (err) => {
696
- if (!res.headersSent) {
697
- res.writeHead(502);
698
- res.end("Proxy error");
699
- }
700
- });
701
- req.pipe(proxyReq);
702
690
  });
703
691
  this.proxyServer = server;
704
692
  server.listen(this.port, "127.0.0.1", () => resolve());
@@ -751,7 +739,7 @@ class ServeManager {
751
739
  const mount = this.mounts.get(mountName);
752
740
  const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
753
741
  try {
754
- const { FrpcTunnel } = await import('./frpc--8kUn3Em.mjs');
742
+ const { FrpcTunnel } = await import('./frpc-IXxyfgPn.mjs');
755
743
  let tunnel;
756
744
  tunnel = new FrpcTunnel({
757
745
  name: tunnelName,
@@ -1,4 +1,4 @@
1
- import { R as READ_ONLY_TOOLS, N as loadMachineContext, O as buildMachineInstructions, P as machineToolsForRole, Q as buildMachineTools } from './run-BDX-RaEm.mjs';
1
+ import { R as READ_ONLY_TOOLS, N as loadMachineContext, O as buildMachineInstructions, P as machineToolsForRole, Q as buildMachineTools } from './run-C1Unk4Zx.mjs';
2
2
  import 'node:child_process';
3
3
  import 'os';
4
4
  import 'fs/promises';
@@ -0,0 +1,239 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ const MIME = {
5
+ ".html": "text/html; charset=utf-8",
6
+ ".htm": "text/html; charset=utf-8",
7
+ ".js": "text/javascript; charset=utf-8",
8
+ ".mjs": "text/javascript; charset=utf-8",
9
+ ".css": "text/css; charset=utf-8",
10
+ ".json": "application/json; charset=utf-8",
11
+ ".map": "application/json; charset=utf-8",
12
+ ".txt": "text/plain; charset=utf-8",
13
+ ".md": "text/plain; charset=utf-8",
14
+ ".xml": "application/xml; charset=utf-8",
15
+ ".csv": "text/csv; charset=utf-8",
16
+ ".svg": "image/svg+xml",
17
+ ".png": "image/png",
18
+ ".jpg": "image/jpeg",
19
+ ".jpeg": "image/jpeg",
20
+ ".gif": "image/gif",
21
+ ".webp": "image/webp",
22
+ ".avif": "image/avif",
23
+ ".ico": "image/x-icon",
24
+ ".bmp": "image/bmp",
25
+ ".mp4": "video/mp4",
26
+ ".webm": "video/webm",
27
+ ".mov": "video/quicktime",
28
+ ".m4v": "video/mp4",
29
+ ".mp3": "audio/mpeg",
30
+ ".wav": "audio/wav",
31
+ ".ogg": "audio/ogg",
32
+ ".m4a": "audio/mp4",
33
+ ".flac": "audio/flac",
34
+ ".pdf": "application/pdf",
35
+ ".wasm": "application/wasm",
36
+ ".zip": "application/zip",
37
+ ".gz": "application/gzip",
38
+ ".tar": "application/x-tar",
39
+ ".woff": "font/woff",
40
+ ".woff2": "font/woff2",
41
+ ".ttf": "font/ttf",
42
+ ".otf": "font/otf",
43
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
44
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
45
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
46
+ };
47
+ function contentTypeFor(filePath) {
48
+ return MIME[path.extname(filePath).toLowerCase()] || "application/octet-stream";
49
+ }
50
+ function containedPath(rootDir, relPath) {
51
+ const base = path.resolve(rootDir);
52
+ const decoded = safeDecode(relPath);
53
+ const fp = path.resolve(path.join(base, decoded));
54
+ if (fp !== base && !fp.startsWith(base + path.sep)) return null;
55
+ return fp;
56
+ }
57
+ function safeDecode(p) {
58
+ try {
59
+ return decodeURIComponent(p);
60
+ } catch {
61
+ return p;
62
+ }
63
+ }
64
+ function parseRange(header, size) {
65
+ if (!header) return null;
66
+ const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
67
+ if (!m) return null;
68
+ const [, s, e] = m;
69
+ if (s === "" && e === "") return null;
70
+ let start, end;
71
+ if (s === "") {
72
+ const n = parseInt(e, 10);
73
+ if (n <= 0) return "unsatisfiable";
74
+ start = Math.max(0, size - n);
75
+ end = size - 1;
76
+ } else {
77
+ start = parseInt(s, 10);
78
+ end = e === "" ? size - 1 : parseInt(e, 10);
79
+ }
80
+ if (Number.isNaN(start) || Number.isNaN(end) || start > end || start >= size) return "unsatisfiable";
81
+ if (end >= size) end = size - 1;
82
+ return { start, end };
83
+ }
84
+ function directoryListingHtml(mountRelUrl, entries) {
85
+ const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
86
+ const base = mountRelUrl.endsWith("/") ? mountRelUrl : mountRelUrl + "/";
87
+ const rows = entries.sort((a, b) => a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1).map((e) => {
88
+ const href = base + encodeURIComponent(e.name) + (e.isDir ? "/" : "");
89
+ return `<li><a href="${esc(href)}">${esc(e.name)}${e.isDir ? "/" : ""}</a></li>`;
90
+ }).join("\n");
91
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${esc(base)}</title>
92
+ <style>body{font-family:system-ui,sans-serif;max-width:760px;margin:40px auto;padding:0 20px;color:#333}
93
+ h1{font-size:1.15rem;word-break:break-all}ul{list-style:none;padding:0}li{padding:6px 0;border-bottom:1px solid #eee}
94
+ a{color:#0969da;text-decoration:none}a:hover{text-decoration:underline}</style></head>
95
+ <body><h1>${esc(base)}</h1><ul>${rows || "<li>(empty)</li>"}</ul></body></html>`;
96
+ }
97
+ const CORS = {
98
+ "Access-Control-Allow-Origin": "*",
99
+ "Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
100
+ "Access-Control-Allow-Headers": "*"
101
+ };
102
+ function serveStaticMount(req, res, opts) {
103
+ const { rootDir, relPath, mountUrlPrefix } = opts;
104
+ const browse = opts.browse !== false;
105
+ const method = (req.method || "GET").toUpperCase();
106
+ if (method === "OPTIONS") {
107
+ res.writeHead(204, CORS);
108
+ res.end();
109
+ return;
110
+ }
111
+ if (method !== "GET" && method !== "HEAD") {
112
+ res.writeHead(405, { ...CORS, Allow: "GET, HEAD, OPTIONS" });
113
+ res.end("Method Not Allowed");
114
+ return;
115
+ }
116
+ let rootStat;
117
+ try {
118
+ rootStat = fs.statSync(rootDir);
119
+ } catch {
120
+ res.writeHead(404, CORS);
121
+ res.end("Not Found");
122
+ return;
123
+ }
124
+ if (rootStat.isFile()) {
125
+ sendFile(req, res, rootDir, rootStat);
126
+ return;
127
+ }
128
+ const target = containedPath(rootDir, relPath.replace(/^\/+/, ""));
129
+ if (!target) {
130
+ res.writeHead(403, { ...CORS, "Content-Type": "text/plain" });
131
+ res.end("Forbidden");
132
+ return;
133
+ }
134
+ let st;
135
+ try {
136
+ st = fs.statSync(target);
137
+ } catch {
138
+ res.writeHead(404, { ...CORS, "Content-Type": "text/plain" });
139
+ res.end("Not Found");
140
+ return;
141
+ }
142
+ if (st.isDirectory()) {
143
+ const reqPath = (req.url || "/").split("?")[0];
144
+ if (!reqPath.endsWith("/")) {
145
+ res.writeHead(302, { ...CORS, Location: reqPath + "/" });
146
+ res.end();
147
+ return;
148
+ }
149
+ const indexPath = path.join(target, "index.html");
150
+ if (fs.existsSync(indexPath) && fs.statSync(indexPath).isFile()) {
151
+ sendFile(req, res, indexPath, fs.statSync(indexPath));
152
+ return;
153
+ }
154
+ if (!browse) {
155
+ res.writeHead(403, { ...CORS, "Content-Type": "text/plain" });
156
+ res.end("Directory listing disabled");
157
+ return;
158
+ }
159
+ let entries;
160
+ try {
161
+ entries = fs.readdirSync(target, { withFileTypes: true }).filter((d) => !d.name.startsWith(".")).map((d) => ({ name: d.name, isDir: d.isDirectory() }));
162
+ } catch {
163
+ res.writeHead(500, CORS);
164
+ res.end("Listing failed");
165
+ return;
166
+ }
167
+ const listUrl = mountUrlPrefix + (relPath === "/" ? "/" : relPath);
168
+ const html = directoryListingHtml(listUrl, entries);
169
+ res.writeHead(200, { ...CORS, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
170
+ res.end(method === "HEAD" ? void 0 : html);
171
+ return;
172
+ }
173
+ if (st.isFile()) {
174
+ sendFile(req, res, target, st);
175
+ return;
176
+ }
177
+ res.writeHead(404, CORS);
178
+ res.end("Not Found");
179
+ }
180
+ function etagFor(st) {
181
+ return `"${st.size.toString(16)}-${Math.floor(st.mtimeMs).toString(16)}"`;
182
+ }
183
+ function sendFile(req, res, filePath, st) {
184
+ const method = (req.method || "GET").toUpperCase();
185
+ const etag = etagFor(st);
186
+ const lastMod = new Date(st.mtimeMs).toUTCString();
187
+ const baseHeaders = {
188
+ ...CORS,
189
+ "Content-Type": contentTypeFor(filePath),
190
+ "Accept-Ranges": "bytes",
191
+ ETag: etag,
192
+ "Last-Modified": lastMod,
193
+ "Cache-Control": "no-cache"
194
+ // revalidate via ETag; content can change under a mount
195
+ };
196
+ const inm = req.headers["if-none-match"];
197
+ const ims = req.headers["if-modified-since"];
198
+ if (inm && inm === etag || !inm && ims && new Date(ims).getTime() >= Math.floor(st.mtimeMs / 1e3) * 1e3) {
199
+ res.writeHead(304, baseHeaders);
200
+ res.end();
201
+ return;
202
+ }
203
+ const range = parseRange(req.headers.range, st.size);
204
+ if (range === "unsatisfiable") {
205
+ res.writeHead(416, { ...baseHeaders, "Content-Range": `bytes */${st.size}` });
206
+ res.end();
207
+ return;
208
+ }
209
+ if (range) {
210
+ const len = range.end - range.start + 1;
211
+ res.writeHead(206, { ...baseHeaders, "Content-Range": `bytes ${range.start}-${range.end}/${st.size}`, "Content-Length": String(len) });
212
+ if (method === "HEAD") {
213
+ res.end();
214
+ return;
215
+ }
216
+ pipeStream(fs.createReadStream(filePath, { start: range.start, end: range.end }), res);
217
+ return;
218
+ }
219
+ res.writeHead(200, { ...baseHeaders, "Content-Length": String(st.size) });
220
+ if (method === "HEAD") {
221
+ res.end();
222
+ return;
223
+ }
224
+ pipeStream(fs.createReadStream(filePath), res);
225
+ }
226
+ function pipeStream(stream, res) {
227
+ stream.on("error", () => {
228
+ try {
229
+ res.destroy();
230
+ } catch {
231
+ }
232
+ });
233
+ res.on("close", () => {
234
+ stream.destroy();
235
+ });
236
+ stream.pipe(res);
237
+ }
238
+
239
+ export { containedPath, contentTypeFor, directoryListingHtml, parseRange, serveStaticMount };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.256",
3
+ "version": "0.2.258",
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 bin/skills bin/commands && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && cp -r ../../skills/crew bin/skills/crew && cp -r ../../commands bin/commands && tsc --noEmit && pkgroll",
22
22
  "typecheck": "tsc --noEmit",
23
- "test": "npx tsx test/test-context-window.mjs && npx tsx test/test-ratelimit-retry.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-sharing-notify-sync.mjs && npx tsx test/test-staged-homes-sweep.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-isolation-decision.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-claude-auth.mjs && npx tsx test/test-btw-proxy-env.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-inbox-guard.mjs && npx tsx test/test-auto-topic.mjs && npx tsx test/test-project-info.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-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.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-supervisor-lock.mjs && node test/test-supervisor-restart.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox-store.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-inbox-cross-machine.mjs && npx tsx test/test-issue-store.mjs && npx tsx test/test-issue-close-gate.mjs && npx tsx test/test-loop-verify.mjs && npx tsx test/test-issue-rpc.mjs && npx tsx test/test-issue-pause.mjs && npx tsx test/test-workflow-store.mjs && npx tsx test/test-workflow-rpc.mjs && npx tsx test/test-workflow-scheduler.mjs && npx tsx test/test-cron.mjs && npx tsx test/test-workflow-runs.mjs && npx tsx test/test-workflow-idle.mjs && npx tsx test/test-serve-link-subdomain.mjs && npx tsx test/test-short-id.mjs && npx tsx test/test-transcript-edit.mjs && npx tsx test/test-edit-history.mjs && npx tsx test/test-friendly-name.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && npx tsx test/test-frpc-status.mjs && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-session-file.mjs && npx tsx test/test-channel-rpc.mjs && npx tsx test/test-wise-agent.mjs && npx tsx test/test-channel-agent.mjs && npx tsx test/test-channels-service.mjs && npx tsx test/test-hotreload-seam.mjs && npx tsx test/test-hot-reload.mjs && npx tsx test/test-hot-reload-live.mjs && npx tsx test/test-session-core.mjs && npx tsx test/test-channel-async-reply.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-shared-session-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-channel-upload.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs && npx tsx test/test-crew-merge.mjs && npx tsx test/test-crew-verdict-routing.mjs && npx tsx test/test-crew-standalone.mjs && npx tsx test/test-session-links.mjs && npx tsx test/test-issue-loop-pause-guard.mjs && npx tsx test/test-artifact-guard.mjs && npx tsx test/test-graceful-restart.mjs",
23
+ "test": "npx tsx test/test-context-window.mjs && npx tsx test/test-ratelimit-retry.mjs && npx tsx test/test-instance-config.mjs && npx tsx test/test-authorize.mjs && npx tsx test/test-normalize-allowed-user.mjs && npx tsx test/test-share-url.mjs && npx tsx test/test-update-sharing-normalization.mjs && npx tsx test/test-sharing-notify-sync.mjs && npx tsx test/test-staged-homes-sweep.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-isolation-decision.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-claude-auth.mjs && npx tsx test/test-btw-proxy-env.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-inbox-guard.mjs && npx tsx test/test-auto-topic.mjs && npx tsx test/test-project-info.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-session-send-query.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.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-supervisor-lock.mjs && node test/test-supervisor-restart.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox-store.mjs && npx tsx test/test-inbox.mjs && npx tsx test/test-inbox-cross-machine.mjs && npx tsx test/test-issue-store.mjs && npx tsx test/test-issue-close-gate.mjs && npx tsx test/test-loop-verify.mjs && npx tsx test/test-issue-rpc.mjs && npx tsx test/test-issue-pause.mjs && npx tsx test/test-workflow-store.mjs && npx tsx test/test-workflow-rpc.mjs && npx tsx test/test-workflow-scheduler.mjs && npx tsx test/test-cron.mjs && npx tsx test/test-workflow-runs.mjs && npx tsx test/test-workflow-idle.mjs && npx tsx test/test-serve-link-subdomain.mjs && npx tsx test/test-short-id.mjs && npx tsx test/test-transcript-edit.mjs && npx tsx test/test-edit-history.mjs && npx tsx test/test-friendly-name.mjs && npx tsx test/test-session-rpc-dispatch.mjs && npx tsx test/test-sandbox-cli.mjs && npx tsx test/test-serve-manager.mjs && npx tsx test/test-static-file-server.mjs && npx tsx test/test-serve-stability.mjs && npx tsx test/test-frpc-e2e.mjs --unit-only && npx tsx test/test-frpc-status.mjs && node test/pinnedClaudeCode.test.mjs && node test/fleet.test.mjs && npx tsx test/test-session-file.mjs && npx tsx test/test-channel-rpc.mjs && npx tsx test/test-wise-agent.mjs && npx tsx test/test-channel-agent.mjs && npx tsx test/test-channels-service.mjs && npx tsx test/test-hotreload-seam.mjs && npx tsx test/test-hot-reload.mjs && npx tsx test/test-hot-reload-live.mjs && npx tsx test/test-session-core.mjs && npx tsx test/test-channel-async-reply.mjs && npx tsx test/test-channel-binding.mjs && npx tsx test/test-channel-identity.mjs && npx tsx test/test-shared-session-identity.mjs && npx tsx test/test-wise-agent-auth.mjs && npx tsx test/test-channel-http.mjs && npx tsx test/test-channel-upload.mjs && npx tsx test/test-wise-voice.mjs && npx tsx test/test-wise-headless.mjs && npx tsx test/test-wise-machine.mjs && npx tsx test/test-crew-merge.mjs && npx tsx test/test-crew-verdict-routing.mjs && npx tsx test/test-crew-standalone.mjs && npx tsx test/test-session-links.mjs && npx tsx test/test-issue-loop-pause-guard.mjs && npx tsx test/test-artifact-guard.mjs && npx tsx test/test-graceful-restart.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",