svamp-cli 0.2.7 → 0.2.9

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.
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
6
6
  import { spawn as spawn$1 } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
8
  import { existsSync, readFileSync, writeFileSync as writeFileSync$1, mkdirSync as mkdirSync$1, appendFileSync } from 'node:fs';
9
- import { randomUUID } from 'node:crypto';
9
+ import { randomUUID, createHash } from 'node:crypto';
10
10
  import { join as join$1 } from 'node:path';
11
11
  import { spawn, execSync, execFile } from 'node:child_process';
12
12
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
@@ -718,6 +718,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
718
718
  if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
719
719
  newSharing = { ...newSharing, owner: context.user.email };
720
720
  }
721
+ const oldSharing = currentMetadata.sharing;
721
722
  currentMetadata = { ...currentMetadata, sharing: newSharing };
722
723
  metadataVersion++;
723
724
  savePersistedMachineMetadata(metadata.svampHomeDir, {
@@ -730,6 +731,20 @@ async function registerMachineService(server, machineId, metadata, daemonState,
730
731
  machineId,
731
732
  metadata: { value: currentMetadata, version: metadataVersion }
732
733
  });
734
+ handlers.sharingNotificationSync?.syncSharing(
735
+ `machine-${machineId}`,
736
+ oldSharing,
737
+ newSharing,
738
+ {
739
+ machineId,
740
+ machineServiceId: `${server.config.workspace}/${server.config.client_id}:default`,
741
+ ownerWorkspace: server.config.workspace,
742
+ ownerEmail: newSharing.owner || "",
743
+ label: currentMetadata.displayName || machineId,
744
+ shareType: "machine"
745
+ }
746
+ ).catch(() => {
747
+ });
733
748
  return { success: true, sharing: newSharing };
734
749
  },
735
750
  // Get security context config
@@ -1110,6 +1125,41 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1110
1125
  ...client.status
1111
1126
  }));
1112
1127
  },
1128
+ // ── Shared static file server ────────────────────────────────────
1129
+ /** Add a mount to the shared static file server. */
1130
+ serveAdd: async (params, context) => {
1131
+ authorizeRequest(context, currentMetadata.sharing, "interact");
1132
+ const sm = handlers.serveManager;
1133
+ if (!sm) throw new Error("Serve manager not available");
1134
+ const ownerEmail = params.ownerEmail || context?.user?.email || void 0;
1135
+ const access = params.access || "owner";
1136
+ return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail);
1137
+ },
1138
+ /** Remove a mount from the shared static file server. */
1139
+ serveRemove: async (params, context) => {
1140
+ authorizeRequest(context, currentMetadata.sharing, "interact");
1141
+ const sm = handlers.serveManager;
1142
+ if (!sm) throw new Error("Serve manager not available");
1143
+ await sm.removeMount(params.name);
1144
+ return { removed: true };
1145
+ },
1146
+ /** List mounts. Optionally filter by sessionId or return all. */
1147
+ serveList: async (params, context) => {
1148
+ authorizeRequest(context, currentMetadata.sharing, "view");
1149
+ const sm = handlers.serveManager;
1150
+ if (!sm) throw new Error("Serve manager not available");
1151
+ if (params.all) {
1152
+ return sm.listMounts();
1153
+ }
1154
+ return sm.listMounts(params.sessionId);
1155
+ },
1156
+ /** Get shared static file server info. */
1157
+ serveInfo: async (context) => {
1158
+ authorizeRequest(context, currentMetadata.sharing, "view");
1159
+ const sm = handlers.serveManager;
1160
+ if (!sm) throw new Error("Serve manager not available");
1161
+ return sm.getInfo();
1162
+ },
1113
1163
  // WISE voice — create ephemeral token for OpenAI Realtime API
1114
1164
  wiseCreateEphemeralToken: async (params, context) => {
1115
1165
  authorizeRequest(context, currentMetadata.sharing, "interact");
@@ -1978,7 +2028,7 @@ async function registerDebugService(server, machineId, deps) {
1978
2028
  };
1979
2029
  }
1980
2030
 
1981
- const COLLECTION_ALIAS = "svamp-agent-sessions";
2031
+ const COLLECTION_ALIAS$1 = "svamp-agent-sessions";
1982
2032
  class SessionArtifactSync {
1983
2033
  server;
1984
2034
  artifactManager = null;
@@ -2013,14 +2063,14 @@ class SessionArtifactSync {
2013
2063
  async ensureCollection() {
2014
2064
  try {
2015
2065
  const existing = await this.artifactManager.read({
2016
- artifact_id: COLLECTION_ALIAS,
2066
+ artifact_id: COLLECTION_ALIAS$1,
2017
2067
  _rkwargs: true
2018
2068
  });
2019
2069
  this.collectionId = existing.id;
2020
2070
  this.log(`[ARTIFACT SYNC] Found existing collection: ${this.collectionId}`);
2021
2071
  } catch {
2022
2072
  const collection = await this.artifactManager.create({
2023
- alias: COLLECTION_ALIAS,
2073
+ alias: COLLECTION_ALIAS$1,
2024
2074
  type: "collection",
2025
2075
  manifest: {
2026
2076
  name: "Svamp Agent Sessions",
@@ -2299,6 +2349,170 @@ class SessionArtifactSync {
2299
2349
  }
2300
2350
  }
2301
2351
 
2352
+ const COLLECTION_ALIAS = "svamp-shared-sessions";
2353
+ function emailHash(email) {
2354
+ return createHash("sha256").update(email.toLowerCase()).digest("hex").slice(0, 12);
2355
+ }
2356
+ function notificationAlias(sessionId, recipientEmail) {
2357
+ return `share-${sessionId.slice(0, 8)}-${emailHash(recipientEmail)}`;
2358
+ }
2359
+ class SharingNotificationSync {
2360
+ server;
2361
+ artifactManager = null;
2362
+ collectionId = null;
2363
+ initialized = false;
2364
+ log;
2365
+ constructor(server, log) {
2366
+ this.server = server;
2367
+ this.log = log;
2368
+ }
2369
+ async init() {
2370
+ try {
2371
+ this.artifactManager = await this.server.getService("public/artifact-manager");
2372
+ if (!this.artifactManager) {
2373
+ this.log("[SHARING NOTIFY] Artifact manager not available");
2374
+ return;
2375
+ }
2376
+ await this.ensureCollection();
2377
+ this.initialized = true;
2378
+ this.log("[SHARING NOTIFY] Initialized");
2379
+ } catch (err) {
2380
+ this.log(`[SHARING NOTIFY] Init failed: ${err.message}`);
2381
+ }
2382
+ }
2383
+ async ensureCollection() {
2384
+ try {
2385
+ const existing = await this.artifactManager.read({
2386
+ artifact_id: COLLECTION_ALIAS,
2387
+ _rkwargs: true
2388
+ });
2389
+ this.collectionId = existing.id;
2390
+ } catch {
2391
+ const collection = await this.artifactManager.create({
2392
+ alias: COLLECTION_ALIAS,
2393
+ type: "collection",
2394
+ manifest: {
2395
+ name: "Svamp Shared Sessions",
2396
+ description: "Cross-workspace share notifications for session/machine bookmarks"
2397
+ },
2398
+ config: {
2399
+ permissions: { "*": "r", "@": "rw+" }
2400
+ },
2401
+ _rkwargs: true
2402
+ });
2403
+ this.collectionId = collection.id;
2404
+ await this.artifactManager.commit({
2405
+ artifact_id: this.collectionId,
2406
+ _rkwargs: true
2407
+ });
2408
+ this.log(`[SHARING NOTIFY] Created collection: ${this.collectionId}`);
2409
+ }
2410
+ }
2411
+ /**
2412
+ * Publish a share notification for a recipient.
2413
+ * Idempotent — uses deterministic alias, so re-sharing updates the existing artifact.
2414
+ */
2415
+ async notifyShare(params) {
2416
+ if (!this.initialized || !this.collectionId) return;
2417
+ const alias = notificationAlias(params.sessionId, params.recipientEmail);
2418
+ const manifest = {
2419
+ recipientEmail: params.recipientEmail.toLowerCase(),
2420
+ sessionId: params.sessionId,
2421
+ machineId: params.machineId,
2422
+ machineServiceId: params.machineServiceId,
2423
+ ownerWorkspace: params.ownerWorkspace,
2424
+ ownerEmail: params.ownerEmail,
2425
+ label: params.label || "",
2426
+ role: params.role,
2427
+ sharedAt: Date.now(),
2428
+ shareType: params.shareType || "session"
2429
+ };
2430
+ try {
2431
+ const existing = await this.artifactManager.read({
2432
+ artifact_id: alias,
2433
+ parent_id: this.collectionId,
2434
+ _rkwargs: true
2435
+ });
2436
+ await this.artifactManager.edit({
2437
+ artifact_id: existing.id,
2438
+ manifest,
2439
+ _rkwargs: true
2440
+ });
2441
+ await this.artifactManager.commit({
2442
+ artifact_id: existing.id,
2443
+ _rkwargs: true
2444
+ });
2445
+ } catch {
2446
+ try {
2447
+ const artifact = await this.artifactManager.create({
2448
+ alias,
2449
+ parent_id: this.collectionId,
2450
+ type: "share-notification",
2451
+ manifest,
2452
+ _rkwargs: true
2453
+ });
2454
+ await this.artifactManager.commit({
2455
+ artifact_id: artifact.id,
2456
+ _rkwargs: true
2457
+ });
2458
+ } catch (createErr) {
2459
+ this.log(`[SHARING NOTIFY] Failed to create notification for ${params.recipientEmail}: ${createErr.message}`);
2460
+ return;
2461
+ }
2462
+ }
2463
+ this.log(`[SHARING NOTIFY] Notified ${params.recipientEmail} about ${params.shareType || "session"} ${params.sessionId.slice(0, 8)}`);
2464
+ }
2465
+ /**
2466
+ * Remove a share notification when a user is unshared.
2467
+ */
2468
+ async removeNotification(sessionId, recipientEmail) {
2469
+ if (!this.initialized || !this.collectionId) return;
2470
+ const alias = notificationAlias(sessionId, recipientEmail);
2471
+ try {
2472
+ const existing = await this.artifactManager.read({
2473
+ artifact_id: alias,
2474
+ parent_id: this.collectionId,
2475
+ _rkwargs: true
2476
+ });
2477
+ await this.artifactManager.delete({
2478
+ artifact_id: existing.id,
2479
+ _rkwargs: true
2480
+ });
2481
+ this.log(`[SHARING NOTIFY] Removed notification for ${recipientEmail} on ${sessionId.slice(0, 8)}`);
2482
+ } catch {
2483
+ }
2484
+ }
2485
+ /**
2486
+ * Sync all sharing notifications for a session based on its current sharing config.
2487
+ * Adds notifications for new users, removes for removed users.
2488
+ */
2489
+ async syncSharing(sessionId, oldSharing, newSharing, context) {
2490
+ if (!this.initialized) return;
2491
+ const oldEmails = new Set(
2492
+ (oldSharing?.allowedUsers || []).map((u) => u.email.toLowerCase())
2493
+ );
2494
+ const newUsers = newSharing.allowedUsers || [];
2495
+ const newEmails = new Set(newUsers.map((u) => u.email.toLowerCase()));
2496
+ for (const user of newUsers) {
2497
+ if (!oldEmails.has(user.email.toLowerCase())) {
2498
+ this.notifyShare({
2499
+ recipientEmail: user.email,
2500
+ sessionId,
2501
+ role: user.role,
2502
+ ...context
2503
+ }).catch(() => {
2504
+ });
2505
+ }
2506
+ }
2507
+ for (const email of oldEmails) {
2508
+ if (!newEmails.has(email)) {
2509
+ this.removeNotification(sessionId, email).catch(() => {
2510
+ });
2511
+ }
2512
+ }
2513
+ }
2514
+ }
2515
+
2302
2516
  const DEFAULT_TIMEOUTS = {
2303
2517
  init: 6e4,
2304
2518
  toolCall: 12e4,
@@ -5933,6 +6147,8 @@ async function startDaemon(options) {
5933
6147
  const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
5934
6148
  await supervisor.init();
5935
6149
  const tunnels = /* @__PURE__ */ new Map();
6150
+ const { ServeManager } = await import('./serveManager-BPyT20Q8.mjs');
6151
+ const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
5936
6152
  ensureAutoInstalledSkills(logger).catch(() => {
5937
6153
  });
5938
6154
  preventMachineSleep(logger);
@@ -6131,6 +6347,7 @@ async function startDaemon(options) {
6131
6347
  const persisted = allPersisted.find((p) => p.sessionId === sessionId) || (resumeSessionId ? allPersisted.find((p) => p.claudeResumeId === resumeSessionId) : void 0);
6132
6348
  let claudeResumeId = persisted?.claudeResumeId || (resumeSessionId || void 0);
6133
6349
  let currentPermissionMode = options2.permissionMode || persisted?.permissionMode || "default";
6350
+ const sessionCreatedAt = persisted?.createdAt || Date.now();
6134
6351
  let lastSpawnMeta = persisted?.spawnMeta || {};
6135
6352
  let sessionWasProcessing = !!options2.wasProcessing;
6136
6353
  let lastAssistantText = "";
@@ -6437,7 +6654,7 @@ async function startDaemon(options) {
6437
6654
  permissionMode: currentPermissionMode,
6438
6655
  spawnMeta: lastSpawnMeta,
6439
6656
  metadata: sessionMetadata,
6440
- createdAt: Date.now(),
6657
+ createdAt: sessionCreatedAt,
6441
6658
  machineId,
6442
6659
  wasProcessing: false
6443
6660
  });
@@ -6468,7 +6685,7 @@ async function startDaemon(options) {
6468
6685
  permissionMode: currentPermissionMode,
6469
6686
  spawnMeta: lastSpawnMeta,
6470
6687
  metadata: sessionMetadata,
6471
- createdAt: Date.now(),
6688
+ createdAt: sessionCreatedAt,
6472
6689
  machineId,
6473
6690
  wasProcessing: false
6474
6691
  });
@@ -6697,7 +6914,7 @@ The automated loop has finished. Review the progress above and let me know if yo
6697
6914
  permissionMode: currentPermissionMode,
6698
6915
  spawnMeta: lastSpawnMeta,
6699
6916
  metadata: sessionMetadata,
6700
- createdAt: Date.now(),
6917
+ createdAt: sessionCreatedAt,
6701
6918
  machineId,
6702
6919
  wasProcessing: sessionWasProcessing
6703
6920
  });
@@ -6806,7 +7023,7 @@ The automated loop has finished. Review the progress above and let me know if yo
6806
7023
  permissionMode: currentPermissionMode,
6807
7024
  spawnMeta: lastSpawnMeta,
6808
7025
  metadata: sessionMetadata,
6809
- createdAt: Date.now(),
7026
+ createdAt: sessionCreatedAt,
6810
7027
  machineId,
6811
7028
  wasProcessing: false
6812
7029
  });
@@ -7028,7 +7245,7 @@ The automated loop has finished. Review the progress above and let me know if yo
7028
7245
  permissionMode: currentPermissionMode,
7029
7246
  spawnMeta: lastSpawnMeta,
7030
7247
  metadata: sessionMetadata,
7031
- createdAt: Date.now(),
7248
+ createdAt: sessionCreatedAt,
7032
7249
  machineId,
7033
7250
  wasProcessing: sessionWasProcessing
7034
7251
  });
@@ -7090,6 +7307,7 @@ The automated loop has finished. Review the progress above and let me know if yo
7090
7307
  },
7091
7308
  onSharingUpdate: (newSharing) => {
7092
7309
  logger.log(`[Session ${sessionId}] Sharing config updated \u2014 persisting to disk`);
7310
+ const oldSharing = sessionMetadata.sharing;
7093
7311
  sessionMetadata = { ...sessionMetadata, sharing: newSharing };
7094
7312
  if (!trackedSession.stopped) {
7095
7313
  saveSession({
@@ -7099,11 +7317,22 @@ The automated loop has finished. Review the progress above and let me know if yo
7099
7317
  permissionMode: currentPermissionMode,
7100
7318
  spawnMeta: lastSpawnMeta,
7101
7319
  metadata: sessionMetadata,
7102
- createdAt: Date.now(),
7320
+ createdAt: sessionCreatedAt,
7103
7321
  machineId,
7104
7322
  wasProcessing: sessionWasProcessing
7105
7323
  });
7106
7324
  }
7325
+ const ownerWorkspace = server.config.workspace;
7326
+ const machineServiceId = `${ownerWorkspace}/${server.config.client_id}:default`;
7327
+ sharingNotificationSync.syncSharing(sessionId, oldSharing, newSharing, {
7328
+ machineId,
7329
+ machineServiceId,
7330
+ ownerWorkspace,
7331
+ ownerEmail: newSharing.owner || "",
7332
+ label: sessionMetadata.summary?.text || "",
7333
+ shareType: "session"
7334
+ }).catch(() => {
7335
+ });
7107
7336
  },
7108
7337
  onApplySystemPrompt: async (prompt) => {
7109
7338
  logger.log(`[Session ${sessionId}] System prompt update requested \u2014 restarting agent`);
@@ -8027,6 +8256,9 @@ The automated loop has finished. Review the progress above and let me know if yo
8027
8256
  pid: process.pid,
8028
8257
  startedAt: Date.now()
8029
8258
  };
8259
+ const sharingNotificationSync = new SharingNotificationSync(server, logger.log);
8260
+ sharingNotificationSync.init().catch(() => {
8261
+ });
8030
8262
  const machineService = await registerMachineService(
8031
8263
  server,
8032
8264
  machineId,
@@ -8042,7 +8274,7 @@ The automated loop has finished. Review the progress above and let me know if yo
8042
8274
  getTrackedSessions: getCurrentChildren,
8043
8275
  getSessionRPCHandlers: (sessionId) => {
8044
8276
  for (const [, session] of pidToTrackedSession) {
8045
- if (session.svampSessionId === sessionId && session.sessionRPCHandlers) {
8277
+ if (session.svampSessionId === sessionId && !session.stopped && session.sessionRPCHandlers) {
8046
8278
  return session.sessionRPCHandlers;
8047
8279
  }
8048
8280
  }
@@ -8058,7 +8290,9 @@ The automated loop has finished. Review the progress above and let me know if yo
8058
8290
  return ids;
8059
8291
  },
8060
8292
  supervisor,
8061
- tunnels
8293
+ tunnels,
8294
+ serveManager,
8295
+ sharingNotificationSync
8062
8296
  }
8063
8297
  );
8064
8298
  logger.log(`Machine service registered: svamp-machine-${machineId}`);
@@ -8080,6 +8314,7 @@ The automated loop has finished. Review the progress above and let me know if yo
8080
8314
  // Legacy; debug service uses session index now
8081
8315
  });
8082
8316
  logger.log(`Debug service registered: svamp-debug-${machineId}`);
8317
+ await serveManager.restore();
8083
8318
  const persistedSessions = loadPersistedSessions();
8084
8319
  const sessionsToAutoContinue = [];
8085
8320
  const sessionsToRalphResume = [];
@@ -8420,6 +8655,8 @@ The automated loop has finished. Review the progress above and let me know if yo
8420
8655
  }
8421
8656
  await supervisor.stopAll().catch(() => {
8422
8657
  });
8658
+ await serveManager.shutdown().catch(() => {
8659
+ });
8423
8660
  for (const [name, client] of tunnels) {
8424
8661
  client.destroy();
8425
8662
  logger.log(`Tunnel '${name}' destroyed`);
@@ -2,7 +2,7 @@ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(im
2
2
  import os from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { mkdirSync, writeFileSync, existsSync, unlinkSync, readFileSync, watch } from 'node:fs';
5
- import { c as connectToHypha, a as registerSessionService } from './run-1sh7lcBI.mjs';
5
+ import { c as connectToHypha, a as registerSessionService } from './run-IDo93bqK.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -0,0 +1,191 @@
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 && !sub.startsWith("-")) {
49
+ await serveAdd(filteredArgs, machineId);
50
+ } else {
51
+ printServeHelp();
52
+ }
53
+ }
54
+ async function serveAdd(args, machineId) {
55
+ const { connectAndGetMachine } = await import('./commands-h1lFrJKK.mjs');
56
+ const pos = positionalArgs(args);
57
+ const name = pos[0];
58
+ if (!name) {
59
+ console.error("Usage: svamp serve [add] <name> [directory] [--public | --access email1,email2]");
60
+ process.exit(1);
61
+ }
62
+ const directory = path.resolve(pos[1] || ".");
63
+ let access = "owner";
64
+ if (hasFlag(args, "--public")) {
65
+ access = "public";
66
+ } else {
67
+ const accessFlag = getFlag(args, "--access");
68
+ if (accessFlag) {
69
+ access = accessFlag.split(",").map((e) => e.trim()).filter(Boolean);
70
+ }
71
+ }
72
+ const { machine, server } = await connectAndGetMachine(machineId);
73
+ try {
74
+ const result = await machine.serveAdd({ name, directory, access, _rkwargs: true });
75
+ console.log(`Mount added: ${name} \u2192 ${directory}`);
76
+ console.log(`Access: ${access === "public" ? "public" : access === "owner" ? "owner only" : access.join(", ")}`);
77
+ console.log(`URL: ${result.url}`);
78
+ } catch (err) {
79
+ console.error(`Error: ${err.message || err}`);
80
+ process.exit(1);
81
+ } finally {
82
+ await server.disconnect().catch(() => {
83
+ });
84
+ }
85
+ }
86
+ async function serveRemove(args, machineId) {
87
+ const { connectAndGetMachine } = await import('./commands-h1lFrJKK.mjs');
88
+ const pos = positionalArgs(args);
89
+ const name = pos[0];
90
+ if (!name) {
91
+ console.error("Usage: svamp serve remove <name>");
92
+ process.exit(1);
93
+ }
94
+ const { machine, server } = await connectAndGetMachine(machineId);
95
+ try {
96
+ await machine.serveRemove({ name, _rkwargs: true });
97
+ console.log(`Mount '${name}' removed.`);
98
+ } catch (err) {
99
+ console.error(`Error: ${err.message || err}`);
100
+ process.exit(1);
101
+ } finally {
102
+ await server.disconnect().catch(() => {
103
+ });
104
+ }
105
+ }
106
+ async function serveList(args, machineId) {
107
+ const { connectAndGetMachine } = await import('./commands-h1lFrJKK.mjs');
108
+ const all = hasFlag(args, "--all", "-a");
109
+ const json = hasFlag(args, "--json");
110
+ const sessionId = getFlag(args, "--session");
111
+ const { machine, server } = await connectAndGetMachine(machineId);
112
+ try {
113
+ const mounts = await machine.serveList({ sessionId, all, _rkwargs: true });
114
+ if (json) {
115
+ console.log(JSON.stringify(mounts, null, 2));
116
+ } else if (mounts.length === 0) {
117
+ console.log(all ? "No mounts registered." : "No mounts for this session. Use --all to see all mounts.");
118
+ } else {
119
+ const label = all ? "All mounts" : `Mounts${sessionId ? ` (session ${sessionId.slice(0, 8)})` : ""}`;
120
+ console.log(`${label}:
121
+ `);
122
+ for (const m of mounts) {
123
+ const session = m.sessionId ? m.sessionId.slice(0, 8) : "-";
124
+ console.log(` ${m.name}`);
125
+ console.log(` Directory: ${m.directory}`);
126
+ console.log(` Session: ${session}`);
127
+ console.log(` Added: ${new Date(m.addedAt).toISOString().slice(0, 16).replace("T", " ")}`);
128
+ console.log("");
129
+ }
130
+ }
131
+ } catch (err) {
132
+ console.error(`Error: ${err.message || err}`);
133
+ process.exit(1);
134
+ } finally {
135
+ await server.disconnect().catch(() => {
136
+ });
137
+ }
138
+ }
139
+ async function serveInfo(machineId) {
140
+ const { connectAndGetMachine } = await import('./commands-h1lFrJKK.mjs');
141
+ const { machine, server } = await connectAndGetMachine(machineId);
142
+ try {
143
+ const info = await machine.serveInfo({ _rkwargs: true });
144
+ console.log(`Static file server:`);
145
+ console.log(` Status: ${info.running ? "running" : "stopped"}`);
146
+ console.log(` Port: ${info.port || "-"}`);
147
+ console.log(` URL: ${info.url || "(not exposed)"}`);
148
+ console.log(` Mounts: ${info.mountCount}`);
149
+ } catch (err) {
150
+ console.error(`Error: ${err.message || err}`);
151
+ process.exit(1);
152
+ } finally {
153
+ await server.disconnect().catch(() => {
154
+ });
155
+ }
156
+ }
157
+ function printServeHelp() {
158
+ console.log(`
159
+ svamp serve \u2014 Shared static file server
160
+
161
+ Serve local directories via a single daemon-managed HTTP server with one public URL.
162
+ Multiple sessions can register different mount points without port conflicts.
163
+
164
+ Usage:
165
+ svamp serve <name> [directory] Add a mount and print its URL (dir defaults to .)
166
+ svamp serve add <name> [directory] Same as above
167
+ svamp serve remove <name> Remove a mount
168
+ svamp serve list [--all] [--json] List mounts (default: current session only)
169
+ svamp serve info Show server status and URL
170
+
171
+ Access control (default: owner only):
172
+ --public Allow anyone to access (no auth)
173
+ --access email1,email2 Allow specific users (comma-separated emails)
174
+ (no flag) Owner only \u2014 requires Hypha login
175
+
176
+ Options:
177
+ -m, --machine <id> Target a specific machine
178
+ --session <id> Filter by session ID
179
+ --all, -a Show mounts from all sessions
180
+ --json Output as JSON
181
+
182
+ Examples:
183
+ svamp serve my-report ./output # Owner-only (default)
184
+ svamp serve dashboard ./dist --public # Anyone can access
185
+ svamp serve data ./csv --access a@x.com,b@y.com # Specific users
186
+ svamp serve list --all # Show all mounts
187
+ svamp serve remove my-report # Stop serving
188
+ `);
189
+ }
190
+
191
+ export { handleServeCommand };