svamp-cli 0.2.225 → 0.2.227

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.
@@ -0,0 +1,168 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { c as workflowSteps, d as workflowsDir } from './store-BTs0H_y0.mjs';
5
+
6
+ const MAX_RUNS_PER_WORKFLOW = 50;
7
+ const MAX_STREAM_CHARS = 8e3;
8
+ const STEP_TIMEOUT_MS = 12e4;
9
+ function capStream(s, max = MAX_STREAM_CHARS) {
10
+ if (s.length <= max) return s;
11
+ const head = Math.floor(max * 0.6);
12
+ const tail = max - head;
13
+ const omitted = s.length - max;
14
+ return `${s.slice(0, head)}
15
+ \u2026 [${omitted} chars omitted] \u2026
16
+ ${s.slice(s.length - tail)}`;
17
+ }
18
+ function runsDir(projectRoot) {
19
+ return join(workflowsDir(projectRoot), ".runs");
20
+ }
21
+ function runsFile(projectRoot, workflow) {
22
+ const safe = workflow.replace(/[^\w.-]+/g, "_");
23
+ return join(runsDir(projectRoot), `${safe}.jsonl`);
24
+ }
25
+ function shortRunId(seed) {
26
+ let h = 2166136261;
27
+ for (let i = 0; i < seed.length; i++) {
28
+ h ^= seed.charCodeAt(i);
29
+ h = Math.imul(h, 16777619);
30
+ }
31
+ return (h >>> 0).toString(36).padStart(7, "0").slice(0, 7);
32
+ }
33
+ function recordRun(projectRoot, run) {
34
+ try {
35
+ const dir = runsDir(projectRoot);
36
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
37
+ const file = runsFile(projectRoot, run.workflow);
38
+ const existing = loadRuns(projectRoot, run.workflow);
39
+ const idx = existing.findIndex((r) => r.id === run.id);
40
+ if (idx >= 0) existing[idx] = run;
41
+ else existing.push(run);
42
+ const kept = existing.slice(-MAX_RUNS_PER_WORKFLOW);
43
+ const tmp = `${file}.tmp`;
44
+ writeFileSync(tmp, kept.map((r) => JSON.stringify(r)).join("\n") + "\n");
45
+ renameSync(tmp, file);
46
+ } catch {
47
+ }
48
+ }
49
+ function loadRuns(projectRoot, workflow) {
50
+ try {
51
+ const file = runsFile(projectRoot, workflow);
52
+ if (!existsSync(file)) return [];
53
+ return readFileSync(file, "utf-8").split("\n").filter(Boolean).map((l) => {
54
+ try {
55
+ return JSON.parse(l);
56
+ } catch {
57
+ return null;
58
+ }
59
+ }).filter((r) => !!r);
60
+ } catch {
61
+ return [];
62
+ }
63
+ }
64
+ function listRuns(projectRoot, workflow, limit = MAX_RUNS_PER_WORKFLOW) {
65
+ return loadRuns(projectRoot, workflow).slice(-limit).reverse().map((r) => {
66
+ const failedStep = r.steps.findIndex((s) => s.exitCode !== 0 && s.exitCode !== null || s.timedOut);
67
+ const { steps, ...rest } = r;
68
+ return { ...rest, stepCount: steps.length, failedStep: failedStep >= 0 ? failedStep : void 0 };
69
+ });
70
+ }
71
+ function getRun(projectRoot, workflow, runId) {
72
+ return loadRuns(projectRoot, workflow).find((r) => r.id === runId) || null;
73
+ }
74
+ function defaultExecStep(root, cmd, env) {
75
+ return new Promise((resolve) => {
76
+ let stdout = "", stderr = "", timedOut = false, settled = false;
77
+ const finish = (exitCode) => {
78
+ if (settled) return;
79
+ settled = true;
80
+ resolve({ exitCode, stdout, stderr, timedOut });
81
+ };
82
+ try {
83
+ const child = spawn("sh", ["-c", cmd], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] });
84
+ const timer = setTimeout(() => {
85
+ timedOut = true;
86
+ try {
87
+ child.kill("SIGKILL");
88
+ } catch {
89
+ }
90
+ }, STEP_TIMEOUT_MS);
91
+ child.stdout?.on("data", (d) => {
92
+ stdout += d.toString();
93
+ if (stdout.length > MAX_STREAM_CHARS * 4) stdout = capStream(stdout);
94
+ });
95
+ child.stderr?.on("data", (d) => {
96
+ stderr += d.toString();
97
+ if (stderr.length > MAX_STREAM_CHARS * 4) stderr = capStream(stderr);
98
+ });
99
+ child.on("error", (e) => {
100
+ clearTimeout(timer);
101
+ stderr += `
102
+ [spawn error] ${e.message}`;
103
+ finish(null);
104
+ });
105
+ child.on("close", (code) => {
106
+ clearTimeout(timer);
107
+ finish(code);
108
+ });
109
+ } catch (e) {
110
+ stderr += `[spawn threw] ${e?.message || e}`;
111
+ finish(null);
112
+ }
113
+ });
114
+ }
115
+ async function runWorkflow(projectRoot, wf, opts) {
116
+ const now = opts.now || (() => {
117
+ const d = /* @__PURE__ */ new Date();
118
+ return { iso: d.toISOString(), ms: d.getTime() };
119
+ });
120
+ const exec = opts.execStep || defaultExecStep;
121
+ const start = now();
122
+ const id = shortRunId(`${wf.name}:${start.iso}:${start.ms}`);
123
+ const env = wf.session ? { ...process.env, SVAMP_SESSION_ID: wf.session } : process.env;
124
+ const run = {
125
+ id,
126
+ workflow: wf.name,
127
+ trigger: opts.trigger,
128
+ triggerContext: opts.triggerContext,
129
+ actor: opts.actor ?? wf.session ?? null,
130
+ startedAt: start.iso,
131
+ status: "running",
132
+ steps: []
133
+ };
134
+ recordRun(projectRoot, run);
135
+ const steps = workflowSteps(wf);
136
+ try {
137
+ for (let i = 0; i < steps.length; i++) {
138
+ const s0 = now();
139
+ const r = await exec(projectRoot, steps[i].run, env);
140
+ const s1 = now();
141
+ run.steps.push({
142
+ index: i,
143
+ cmd: steps[i].run,
144
+ exitCode: r.exitCode,
145
+ stdout: capStream(r.stdout),
146
+ stderr: capStream(r.stderr),
147
+ durationMs: s1.ms - s0.ms,
148
+ timedOut: r.timedOut || void 0
149
+ });
150
+ if (r.timedOut || r.exitCode !== 0) {
151
+ run.status = "failed";
152
+ break;
153
+ }
154
+ }
155
+ if (run.status === "running") run.status = "success";
156
+ } catch (e) {
157
+ run.status = "failed";
158
+ run.error = e?.message || String(e);
159
+ }
160
+ const end = now();
161
+ run.finishedAt = end.iso;
162
+ run.durationMs = end.ms - start.ms;
163
+ recordRun(projectRoot, run);
164
+ opts.log?.(`[workflow] run ${id} "${wf.name}" \u2192 ${run.status} (${run.steps.length} step${run.steps.length === 1 ? "" : "s"})`);
165
+ return run;
166
+ }
167
+
168
+ export { getRun as g, listRuns as l, runWorkflow as r };
@@ -0,0 +1,12 @@
1
+ function isSandboxed() {
2
+ return process.env.SVAMP_SANDBOXED === "1";
3
+ }
4
+ const SANDBOX_BLOCKED_MSG = "This command is not available in sandboxed sessions. Available commands: set-title, set-link";
5
+ function requireNotSandboxed(commandName) {
6
+ if (isSandboxed()) {
7
+ console.error(`${commandName}: ${SANDBOX_BLOCKED_MSG}`);
8
+ process.exit(1);
9
+ }
10
+ }
11
+
12
+ export { SANDBOX_BLOCKED_MSG, isSandboxed, requireNotSandboxed };
@@ -0,0 +1,102 @@
1
+ import { m as resolveProjectRoot, y as cronMatches } from './run-LEPH8aka.mjs';
2
+ import { l as listWorkflows, i as isWorkflowEnabled, w as workflowCrons } from './store-BTs0H_y0.mjs';
3
+ import { r as runWorkflow } from './runStore-CtptN7US.mjs';
4
+ import 'os';
5
+ import 'fs/promises';
6
+ import 'fs';
7
+ import 'path';
8
+ import 'url';
9
+ import 'child_process';
10
+ import 'crypto';
11
+ import 'node:crypto';
12
+ import 'node:fs';
13
+ import 'node:child_process';
14
+ import 'util';
15
+ import 'node:path';
16
+ import 'node:events';
17
+ import 'node:os';
18
+ import '@agentclientprotocol/sdk';
19
+ import '@modelcontextprotocol/sdk/client/index.js';
20
+ import '@modelcontextprotocol/sdk/client/stdio.js';
21
+ import '@modelcontextprotocol/sdk/types.js';
22
+ import 'zod';
23
+ import 'node:fs/promises';
24
+ import 'node:util';
25
+ import 'yaml';
26
+
27
+ function cronMatchesSafe(expr, date) {
28
+ try {
29
+ return cronMatches(expr, date);
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+ class WorkflowScheduler {
35
+ constructor(deps) {
36
+ this.deps = deps;
37
+ }
38
+ // Wall-clock minute we last processed, so multiple sub-minute ticks fire a workflow at most once
39
+ // per minute (cron granularity is one minute).
40
+ lastMinuteKey = null;
41
+ // In-flight runs (fire-and-forget). `idle()` awaits them — used by tests; harmless in prod.
42
+ pending = /* @__PURE__ */ new Set();
43
+ /** Await all in-flight scheduled runs (test helper; the daemon never needs to block on these). */
44
+ async idle() {
45
+ await Promise.all([...this.pending]);
46
+ }
47
+ static minuteKey(d) {
48
+ return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}-${d.getHours()}-${d.getMinutes()}`;
49
+ }
50
+ /** Fire all scheduled workflows whose cron matches `now`. Returns the names fired (for tests/logs). */
51
+ tick(now) {
52
+ const key = WorkflowScheduler.minuteKey(now);
53
+ if (key === this.lastMinuteKey) return [];
54
+ this.lastMinuteKey = key;
55
+ const fired = [];
56
+ const seen = /* @__PURE__ */ new Set();
57
+ for (const dir of this.deps.projectRoots()) {
58
+ let root;
59
+ try {
60
+ root = resolveProjectRoot(dir);
61
+ } catch {
62
+ continue;
63
+ }
64
+ if (seen.has(root)) continue;
65
+ seen.add(root);
66
+ let workflows;
67
+ try {
68
+ workflows = listWorkflows(root);
69
+ } catch {
70
+ continue;
71
+ }
72
+ for (const wf of workflows) {
73
+ if (!isWorkflowEnabled(wf)) continue;
74
+ const crons = workflowCrons(wf);
75
+ if (!crons.length) continue;
76
+ if (crons.some((c) => cronMatchesSafe(c, now))) {
77
+ this.fire(root, wf);
78
+ fired.push(wf.name);
79
+ }
80
+ }
81
+ }
82
+ return fired;
83
+ }
84
+ fire(root, wf) {
85
+ const owner = wf.session || null;
86
+ this.deps.log?.(`[workflow] schedule fired "${wf.name}" in ${root}${owner ? ` as ${owner}` : ""}`);
87
+ const p = runWorkflow(root, wf, {
88
+ trigger: "schedule",
89
+ actor: owner,
90
+ execStep: this.deps.execStep,
91
+ now: this.deps.now,
92
+ log: this.deps.log
93
+ }).catch((e) => {
94
+ this.deps.log?.(`[workflow] scheduled run threw for "${wf.name}": ${e?.message || e}`);
95
+ return void 0;
96
+ });
97
+ this.pending.add(p);
98
+ p.finally(() => this.pending.delete(p));
99
+ }
100
+ }
101
+
102
+ export { WorkflowScheduler, cronMatchesSafe };
@@ -0,0 +1,310 @@
1
+ import * as path from 'path';
2
+
3
+ function getFlag(args, flag) {
4
+ const idx = args.indexOf(flag);
5
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
6
+ }
7
+ function hasFlag(args, ...flags) {
8
+ return flags.some((f) => args.includes(f));
9
+ }
10
+ function positionalArgs(args) {
11
+ const result = [];
12
+ for (let i = 0; i < args.length; i++) {
13
+ if (args[i].startsWith("-")) {
14
+ if (!args[i].startsWith("--no-") && i + 1 < args.length && !args[i + 1].startsWith("-")) {
15
+ i++;
16
+ }
17
+ continue;
18
+ }
19
+ result.push(args[i]);
20
+ }
21
+ return result;
22
+ }
23
+ async function handleServeCommand() {
24
+ const args = process.argv.slice(3);
25
+ const sub = args[0];
26
+ let machineId;
27
+ const mIdx = args.findIndex((a) => a === "--machine" || a === "-m");
28
+ if (mIdx !== -1 && mIdx + 1 < args.length) {
29
+ machineId = args[mIdx + 1];
30
+ }
31
+ const filteredArgs = args.filter((_a, i) => {
32
+ if (args[i] === "--machine" || args[i] === "-m") return false;
33
+ if (i > 0 && (args[i - 1] === "--machine" || args[i - 1] === "-m")) return false;
34
+ return true;
35
+ });
36
+ if (sub === "--help" || sub === "-h") {
37
+ printServeHelp();
38
+ return;
39
+ }
40
+ if (sub === "remove" || sub === "rm") {
41
+ await serveRemove(filteredArgs.slice(1), machineId);
42
+ } else if (sub === "list" || sub === "ls") {
43
+ await serveList(filteredArgs.slice(1), machineId);
44
+ } else if (sub === "info") {
45
+ await serveInfo(machineId);
46
+ } else if (sub === "add") {
47
+ await serveAdd(filteredArgs.slice(1), machineId);
48
+ } else if (sub === "apply") {
49
+ await serveApply(filteredArgs.slice(1), machineId);
50
+ } else if (sub && !sub.startsWith("-")) {
51
+ await serveAdd(filteredArgs, machineId);
52
+ } else {
53
+ printServeHelp();
54
+ }
55
+ }
56
+ async function serveAdd(args, machineId) {
57
+ const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
58
+ const pos = positionalArgs(args);
59
+ const name = pos[0];
60
+ if (!name) {
61
+ console.error("Usage: svamp serve [add] <name> [directory] [--public | --owner | --access email1,email2]");
62
+ console.error(" Default: capability URL (link mode) \u2014 anyone with the URL can view, no login required.");
63
+ process.exit(1);
64
+ }
65
+ const directory = path.resolve(pos[1] || ".");
66
+ let access = "link";
67
+ if (hasFlag(args, "--public")) {
68
+ access = "public";
69
+ } else if (hasFlag(args, "--owner")) {
70
+ access = "owner";
71
+ } else {
72
+ const accessFlag = getFlag(args, "--access");
73
+ if (accessFlag) {
74
+ access = accessFlag.split(",").map((e) => e.trim()).filter(Boolean);
75
+ }
76
+ }
77
+ const { machine, server } = await connectAndGetMachine(machineId);
78
+ try {
79
+ const result = await machine.serveAdd({ name, directory, access });
80
+ console.log(`Mount added: ${name} \u2192 ${directory}`);
81
+ const accessLabel = access === "public" ? "public (no auth)" : access === "link" ? "link (capability URL \u2014 anyone with the URL can view)" : access === "owner" ? "owner only (Hypha login)" : `${access.join(", ")} (Hypha login)`;
82
+ console.log(`Access: ${accessLabel}`);
83
+ console.log(`URL: ${result.url}`);
84
+ if (access === "link") {
85
+ console.log(`(URL embeds a long capability token; keep it secret to keep the mount private.)`);
86
+ }
87
+ } catch (err) {
88
+ console.error(`Error: ${err.message || err}`);
89
+ process.exit(1);
90
+ } finally {
91
+ await server.disconnect().catch(() => {
92
+ });
93
+ }
94
+ }
95
+ async function serveApply(args, machineId) {
96
+ const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
97
+ const fs = await import('fs');
98
+ const yaml = await import('yaml');
99
+ const file = positionalArgs(args)[0];
100
+ if (!file) {
101
+ console.error("Usage: svamp serve apply <yaml-or-json-file> [--machine <id>]");
102
+ console.error("");
103
+ console.error("YAML format:");
104
+ console.error(" name: my-app");
105
+ console.error(" # Static mount:");
106
+ console.error(" directory: ./public");
107
+ console.error(" # OR managed-process mount:");
108
+ console.error(" process:");
109
+ console.error(" command: npm");
110
+ console.error(" args: [start]");
111
+ console.error(" port: 3000");
112
+ console.error(" workdir: .");
113
+ console.error(" warmup_path: / # default '/'");
114
+ console.error(" warmup_timeout_ms: 30000");
115
+ console.error(" idle_timeout_sec: 600 # 0 = always running");
116
+ console.error(" wake_on_request: true # spawn lazily on first request");
117
+ console.error(" access: link # link (default) | public | owner | [emails]");
118
+ console.error(" # link \u2014 capability URL, anyone with the URL can view");
119
+ console.error(" # public \u2014 fully open, short URL");
120
+ console.error(" # owner / [emails] \u2014 Hypha login required");
121
+ process.exit(1);
122
+ }
123
+ if (!fs.existsSync(file)) {
124
+ console.error(`Error: file not found: ${file}`);
125
+ process.exit(1);
126
+ }
127
+ const raw = fs.readFileSync(file, "utf-8");
128
+ let parsed;
129
+ try {
130
+ parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml.parse(raw);
131
+ } catch (err) {
132
+ console.error(`Error: failed to parse ${file}: ${err.message}`);
133
+ process.exit(1);
134
+ }
135
+ if (!parsed || typeof parsed !== "object") {
136
+ console.error("Error: top-level config must be an object");
137
+ process.exit(1);
138
+ }
139
+ if (!parsed.name) {
140
+ console.error("Error: name is required");
141
+ process.exit(1);
142
+ }
143
+ if (!parsed.directory && !parsed.process) {
144
+ console.error("Error: must specify either directory (static) or process (managed)");
145
+ process.exit(1);
146
+ }
147
+ const proc = parsed.process ? {
148
+ command: parsed.process.command,
149
+ args: parsed.process.args,
150
+ port: parsed.process.port,
151
+ workdir: parsed.process.workdir ? path.resolve(parsed.process.workdir) : void 0,
152
+ env: parsed.process.env,
153
+ warmupPath: parsed.process.warmupPath ?? parsed.process.warmup_path,
154
+ warmupTimeoutMs: parsed.process.warmupTimeoutMs ?? parsed.process.warmup_timeout_ms,
155
+ idleTimeoutSec: parsed.process.idleTimeoutSec ?? parsed.process.idle_timeout_sec,
156
+ wakeOnRequest: parsed.process.wakeOnRequest ?? parsed.process.wake_on_request
157
+ } : void 0;
158
+ const params = {
159
+ name: parsed.name,
160
+ directory: parsed.directory ? path.resolve(parsed.directory) : void 0,
161
+ process: proc,
162
+ sessionId: parsed.sessionId ?? parsed.session_id,
163
+ access: parsed.access ?? "link",
164
+ // default flipped from 'owner' to 'link' (capability URL)
165
+ ownerEmail: parsed.ownerEmail ?? parsed.owner_email
166
+ };
167
+ const { machine, server } = await connectAndGetMachine(machineId);
168
+ try {
169
+ const result = await machine.serveApply(params);
170
+ const kind = proc ? "managed" : "static";
171
+ const what = proc ? `${proc.command}${proc.args?.length ? " " + proc.args.join(" ") : ""} (port ${proc.port})` : params.directory;
172
+ console.log(`Mount applied (${kind}): ${params.name} \u2192 ${what}`);
173
+ if (proc?.wakeOnRequest) console.log("Wake-on-request: enabled (process starts on first incoming request)");
174
+ if (proc?.idleTimeoutSec) console.log(`Idle timeout: ${proc.idleTimeoutSec}s`);
175
+ console.log(`URL: ${result.url}`);
176
+ } catch (err) {
177
+ console.error(`Error: ${err.message || err}`);
178
+ process.exit(1);
179
+ } finally {
180
+ await server.disconnect().catch(() => {
181
+ });
182
+ }
183
+ }
184
+ async function serveRemove(args, machineId) {
185
+ const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
186
+ const pos = positionalArgs(args);
187
+ const name = pos[0];
188
+ if (!name) {
189
+ console.error("Usage: svamp serve remove <name>");
190
+ process.exit(1);
191
+ }
192
+ const { machine, server } = await connectAndGetMachine(machineId);
193
+ try {
194
+ await machine.serveRemove({ name });
195
+ console.log(`Mount '${name}' removed.`);
196
+ } catch (err) {
197
+ console.error(`Error: ${err.message || err}`);
198
+ process.exit(1);
199
+ } finally {
200
+ await server.disconnect().catch(() => {
201
+ });
202
+ }
203
+ }
204
+ async function serveList(args, machineId) {
205
+ const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
206
+ const all = hasFlag(args, "--all", "-a");
207
+ const json = hasFlag(args, "--json");
208
+ const sessionId = getFlag(args, "--session");
209
+ const { machine, server } = await connectAndGetMachine(machineId);
210
+ try {
211
+ const mounts = await machine.serveList({ sessionId, all });
212
+ if (json) {
213
+ console.log(JSON.stringify(mounts, null, 2));
214
+ } else if (mounts.length === 0) {
215
+ console.log(all ? "No mounts registered." : "No mounts for this session. Use --all to see all mounts.");
216
+ } else {
217
+ const label = all ? "All mounts" : `Mounts${sessionId ? ` (session ${sessionId.slice(0, 8)})` : ""}`;
218
+ console.log(`${label}:
219
+ `);
220
+ for (const m of mounts) {
221
+ const session = m.sessionId ? m.sessionId.slice(0, 8) : "-";
222
+ console.log(` ${m.name}`);
223
+ console.log(` Directory: ${m.directory}`);
224
+ console.log(` Session: ${session}`);
225
+ console.log(` Added: ${new Date(m.addedAt).toISOString().slice(0, 16).replace("T", " ")}`);
226
+ console.log("");
227
+ }
228
+ }
229
+ } catch (err) {
230
+ console.error(`Error: ${err.message || err}`);
231
+ process.exit(1);
232
+ } finally {
233
+ await server.disconnect().catch(() => {
234
+ });
235
+ }
236
+ }
237
+ async function serveInfo(machineId) {
238
+ const { connectAndGetMachine } = await import('./commands-CnlQyDxb.mjs');
239
+ const { machine, server } = await connectAndGetMachine(machineId);
240
+ try {
241
+ const info = await machine.serveInfo();
242
+ console.log(`Static file server:`);
243
+ console.log(` Status: ${info.running ? "running" : "stopped"}`);
244
+ console.log(` Port: ${info.port || "-"}`);
245
+ console.log(` URL: ${info.url || "(not exposed)"}`);
246
+ console.log(` Mounts: ${info.mountCount}`);
247
+ } catch (err) {
248
+ console.error(`Error: ${err.message || err}`);
249
+ process.exit(1);
250
+ } finally {
251
+ await server.disconnect().catch(() => {
252
+ });
253
+ }
254
+ }
255
+ function printServeHelp() {
256
+ console.log(`
257
+ svamp serve \u2014 Shared static file server
258
+
259
+ Serve local directories via a single daemon-managed HTTP server with one public URL.
260
+ Multiple sessions can register different mount points without port conflicts.
261
+
262
+ Usage:
263
+ svamp serve <name> [directory] Add a mount and print its URL (dir defaults to .)
264
+ svamp serve add <name> [directory] Same as above
265
+ svamp serve apply <yaml> Apply a declarative mount config (idempotent).
266
+ Supports static and managed-process mounts
267
+ with wake-on-request and idle timeout.
268
+ svamp serve remove <name> Remove a mount
269
+ svamp serve list [--all] [--json] List mounts (default: current session only)
270
+ svamp serve info Show server status and URL
271
+
272
+ Access tiers (default: link):
273
+ (no flag) Capability URL \u2014 anyone with the URL can view.
274
+ URL embeds a long random token (~192-bit entropy)
275
+ as the first path segment. No login required.
276
+ Best for embedding in agent artifact iframes.
277
+ --public Fully public \u2014 no auth, short URL.
278
+ --owner Hypha login required, owner-only.
279
+ --access email1,email2 Hypha login required, specific allowed emails.
280
+
281
+ Options:
282
+ -m, --machine <id> Target a specific machine
283
+ --session <id> Filter by session ID
284
+ --all, -a Show mounts from all sessions
285
+ --json Output as JSON
286
+
287
+ Examples:
288
+ svamp serve my-report ./output # Default: capability URL
289
+ svamp serve dashboard ./dist --public # Anyone, short URL
290
+ svamp serve data ./csv --owner # Hypha login required
291
+ svamp serve data ./csv --access a@x.com,b@y.com # Specific Hypha users
292
+ svamp serve apply my-app.yaml # Declarative apply
293
+ svamp serve list --all # Show all mounts
294
+ svamp serve remove my-report # Stop serving
295
+
296
+ Declarative apply (svamp serve apply <yaml>):
297
+ name: my-app
298
+ process:
299
+ command: npm
300
+ args: [start]
301
+ port: 3000
302
+ workdir: .
303
+ warmup_path: /
304
+ idle_timeout_sec: 600
305
+ wake_on_request: true
306
+ access: public
307
+ `);
308
+ }
309
+
310
+ export { handleServeCommand };