svamp-cli 0.2.68 → 0.2.71

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.
@@ -754,12 +754,36 @@ async function registerMachineService(server, machineId, metadata, daemonState,
754
754
  }
755
755
  return result;
756
756
  },
757
- // Stop a session
758
- stopSession: async (sessionId, context) => {
757
+ // Archive a session (non-destructive — preserves claudeResumeId for resume)
758
+ archiveSession: async (sessionId, context) => {
759
759
  authorizeRequest(context, currentMetadata.sharing, "admin");
760
- const result = handlers.stopSession(sessionId);
760
+ const result = handlers.archiveSession(sessionId);
761
761
  notifyListeners({
762
- type: "session-stopped",
762
+ type: "session-archived",
763
+ sessionId,
764
+ machineId
765
+ });
766
+ return result;
767
+ },
768
+ // Resume an archived session (spawns the agent with --resume <claudeResumeId>)
769
+ resumeSession: async (sessionId, context) => {
770
+ authorizeRequest(context, currentMetadata.sharing, "admin");
771
+ const result = await handlers.resumeSession(sessionId);
772
+ if (result.success) {
773
+ notifyListeners({
774
+ type: "session-resumed",
775
+ sessionId,
776
+ machineId
777
+ });
778
+ }
779
+ return result;
780
+ },
781
+ // Permanently delete a session (wipes session.json + messages.jsonl + .svamp/{id}/)
782
+ deleteSession: async (sessionId, context) => {
783
+ authorizeRequest(context, currentMetadata.sharing, "admin");
784
+ const result = handlers.deleteSession(sessionId);
785
+ notifyListeners({
786
+ type: "session-deleted",
763
787
  sessionId,
764
788
  machineId
765
789
  });
@@ -1116,9 +1140,10 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1116
1140
  const { join: join2, resolve } = await import('path');
1117
1141
  const { homedir } = await import('os');
1118
1142
  const targetPath = resolve(path || homedir());
1143
+ const effectiveRole = getEffectiveRole(context, currentMetadata.sharing);
1119
1144
  const home = homedir();
1120
- const isOwner = !currentMetadata.sharing?.enabled || context?.user?.email && currentMetadata.sharing.owner && context.user.email.toLowerCase() === currentMetadata.sharing.owner.toLowerCase();
1121
- if (!isOwner && targetPath !== home && !targetPath.startsWith(home + "/")) {
1145
+ const restrictedToHome = effectiveRole === "view";
1146
+ if (restrictedToHome && targetPath !== home && !targetPath.startsWith(home + "/")) {
1122
1147
  throw new Error(`Access denied: path must be within ${home}`);
1123
1148
  }
1124
1149
  const showHidden = options?.showHidden ?? false;
@@ -1572,7 +1597,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1572
1597
  }
1573
1598
 
1574
1599
  function isStructuredMessage(msg) {
1575
- return !!(msg.from && msg.subject);
1600
+ return !!(msg.from || msg.fromSession || msg.subject || msg.replyTo || msg.threadId);
1576
1601
  }
1577
1602
  function escapeXml(s) {
1578
1603
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -1960,9 +1985,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1960
1985
  authorizeRequest(context, metadata.sharing, "admin");
1961
1986
  return await callbacks.onRestartClaude();
1962
1987
  },
1963
- killSession: async (context) => {
1988
+ archiveSession: async (context) => {
1964
1989
  authorizeRequest(context, metadata.sharing, "admin");
1965
- callbacks.onKillSession();
1990
+ callbacks.onArchiveSession();
1966
1991
  return { success: true };
1967
1992
  },
1968
1993
  // ── Activity ──
@@ -2240,6 +2265,8 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
2240
2265
  claudeSessionId,
2241
2266
  "--fork-session",
2242
2267
  "--no-session-persistence",
2268
+ "--permission-mode",
2269
+ "bypassPermissions",
2243
2270
  "--output-format",
2244
2271
  "json",
2245
2272
  "--max-turns",
@@ -5676,10 +5703,16 @@ class ServeAuth {
5676
5703
  return access.some((e) => e.toLowerCase() === email.toLowerCase());
5677
5704
  }
5678
5705
  /**
5679
- * Generate the login page HTML. Loads the `hypha-rpc` JS SDK from CDN and
5680
- * calls `hyphaWebsocketClient.login({ server_url, login_callback })`, which
5681
- * handles opening the login URL, polling, and token retrieval internally.
5682
- * Matches the pattern used by bioimage.io — proven to work.
5706
+ * Generate the login page HTML.
5707
+ *
5708
+ * Uses the Hypha-Login HTTP endpoints directly (no SDK / no WebSocket):
5709
+ * 1. GET /public/services/hypha-login/start → { login_url, key }
5710
+ * 2. open popup to login_url so the user signs in
5711
+ * 3. GET /public/services/hypha-login/check?key=… → token (long-polled)
5712
+ *
5713
+ * This matches the proven pattern in svamp-app/.../share/[id].tsx and
5714
+ * removes a class of hangs we previously saw when the JS SDK never
5715
+ * invoked its `login_callback` (button-stuck-after-click symptom).
5683
5716
  */
5684
5717
  getLoginPageHtml(redirectUrl) {
5685
5718
  return `<!DOCTYPE html>
@@ -5702,7 +5735,6 @@ button:disabled{background:#94d3a2;cursor:wait}
5702
5735
  a{color:#0969da;text-decoration:none}
5703
5736
  a:hover{text-decoration:underline}
5704
5737
  </style>
5705
- <script src="https://cdn.jsdelivr.net/npm/hypha-rpc@0.21.28/dist/hypha-rpc-websocket.min.js"><\/script>
5706
5738
  </head><body>
5707
5739
  <div class="card">
5708
5740
  <h1>Sign in required</h1>
@@ -5724,35 +5756,62 @@ function setError(msg) {
5724
5756
  }
5725
5757
 
5726
5758
  btn.addEventListener('click', async () => {
5727
- if (typeof hyphaWebsocketClient === 'undefined' || !hyphaWebsocketClient.login) {
5728
- setError('Hypha SDK failed to load. Check your network connection and retry.');
5759
+ // Open the popup SYNCHRONOUSLY inside the click handler so the browser's
5760
+ // popup blocker treats it as user-initiated. The HTTP request below is
5761
+ // awaited, by which time the user-gesture flag has cleared and any
5762
+ // window.open() call would be silently blocked.
5763
+ const popup = window.open('about:blank', '_blank');
5764
+ if (!popup) {
5765
+ setError('Popup was blocked by the browser. Please allow popups for this site and retry.');
5729
5766
  return;
5730
5767
  }
5768
+ try { popup.document.write('<html><body style="font-family:system-ui;padding:24px;color:#656d76">Opening Hypha sign-in\u2026</body></html>'); } catch (e) {}
5731
5769
 
5732
5770
  btn.disabled = true;
5733
- statusEl.textContent = 'Opening Hypha sign-in\u2026';
5771
+ statusEl.textContent = 'Contacting Hypha\u2026';
5734
5772
 
5735
5773
  try {
5736
- const token = await hyphaWebsocketClient.login({
5737
- server_url: hyphaServer,
5738
- login_callback: (context) => {
5739
- statusEl.textContent = 'Waiting for you to sign in\u2026';
5740
- // Open the login URL in a new tab. Called synchronously from
5741
- // inside the SDK's login() after the user's button click, so
5742
- // popup blockers generally allow it.
5743
- window.open(context.login_url, '_blank');
5744
- },
5745
- });
5774
+ // 1. Ask the Hypha-Login service for a login URL + polling key.
5775
+ const startResp = await fetch(hyphaServer + '/public/services/hypha-login/start');
5776
+ if (!startResp.ok) throw new Error('Login start failed: HTTP ' + startResp.status);
5777
+ const ctx = await startResp.json();
5778
+ if (!ctx || !ctx.login_url || !ctx.key) throw new Error('Login start returned no URL/key');
5779
+
5780
+ // 2. Redirect the popup to the actual login UI.
5781
+ statusEl.textContent = 'Waiting for you to sign in\u2026';
5782
+ try { popup.location.href = ctx.login_url; }
5783
+ catch (e) { window.open(ctx.login_url, '_blank'); }
5784
+
5785
+ // 3. Long-poll the check endpoint until the user finishes signing in.
5786
+ // Each request waits up to 3 s server-side; we retry for ~3 minutes total.
5787
+ const checkBase = ctx.check_url || (hyphaServer + '/public/services/hypha-login/check');
5788
+ let token = null;
5789
+ for (let i = 0; i < 60; i++) {
5790
+ if (popup.closed) {
5791
+ // User dismissed the popup before completing \u2014 keep polling briefly
5792
+ // (the report endpoint may still fire even if window is gone).
5793
+ }
5794
+ try {
5795
+ const r = await fetch(checkBase + '?key=' + encodeURIComponent(ctx.key) + '&timeout=3');
5796
+ if (r.ok) {
5797
+ const body = await r.json();
5798
+ const t = typeof body === 'string' ? body : (body && (body.token || body));
5799
+ if (typeof t === 'string' && t.length > 20) { token = t; break; }
5800
+ }
5801
+ } catch (e) { /* transient \u2014 retry */ }
5802
+ }
5746
5803
 
5747
- if (!token) throw new Error('No token returned from login');
5804
+ if (!token) throw new Error('Timed out waiting for sign-in. Please try again.');
5748
5805
 
5749
- // Set the cookie so subsequent requests to this origin are authenticated.
5806
+ // 4. Set the cookie so subsequent requests to this origin are authenticated.
5750
5807
  const secure = location.protocol === 'https:' ? '; Secure' : '';
5751
5808
  document.cookie = cookieName + '=' + token + '; path=/; SameSite=Lax' + secure;
5752
5809
 
5810
+ try { popup.close(); } catch (e) {}
5753
5811
  statusEl.innerHTML = '<span class="ok">Signed in. Redirecting\u2026</span>';
5754
5812
  setTimeout(() => { window.location.replace(redirectUrl); }, 300);
5755
5813
  } catch (err) {
5814
+ try { popup.close(); } catch (e) {}
5756
5815
  setError('Login failed: ' + (err && err.message ? err.message : err));
5757
5816
  }
5758
5817
  });
@@ -7470,6 +7529,43 @@ function deletePersistedSession(sessionId) {
7470
7529
  saveSessionIndex(index);
7471
7530
  }
7472
7531
  }
7532
+ function markSessionAsArchived(sessionId) {
7533
+ const index = loadSessionIndex();
7534
+ const entry = index[sessionId];
7535
+ if (!entry) return false;
7536
+ const filePath = getSessionFilePath(entry.directory, sessionId);
7537
+ if (!existsSync$1(filePath)) return false;
7538
+ try {
7539
+ const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
7540
+ if (data.stopped === true) return true;
7541
+ data.stopped = true;
7542
+ const tmpPath = filePath + ".tmp";
7543
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
7544
+ renameSync(tmpPath, filePath);
7545
+ return true;
7546
+ } catch {
7547
+ return false;
7548
+ }
7549
+ }
7550
+ function clearSessionArchivedFlag(sessionId) {
7551
+ const index = loadSessionIndex();
7552
+ const entry = index[sessionId];
7553
+ if (!entry) return null;
7554
+ const filePath = getSessionFilePath(entry.directory, sessionId);
7555
+ if (!existsSync$1(filePath)) return null;
7556
+ try {
7557
+ const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
7558
+ if (data.stopped) {
7559
+ delete data.stopped;
7560
+ const tmpPath = filePath + ".tmp";
7561
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf-8");
7562
+ renameSync(tmpPath, filePath);
7563
+ }
7564
+ return data;
7565
+ } catch {
7566
+ return null;
7567
+ }
7568
+ }
7473
7569
  function loadPersistedSessions() {
7474
7570
  const sessions = [];
7475
7571
  const index = loadSessionIndex();
@@ -7771,7 +7867,7 @@ async function startDaemon(options) {
7771
7867
  const list = loadExposedTunnels().filter((t) => t.name !== name);
7772
7868
  saveExposedTunnels(list);
7773
7869
  }
7774
- const { ServeManager } = await import('./serveManager-DWQtF8NK.mjs');
7870
+ const { ServeManager } = await import('./serveManager-Bzjw2bO6.mjs');
7775
7871
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
7776
7872
  ensureAutoInstalledSkills(logger).catch(() => {
7777
7873
  });
@@ -7956,6 +8052,7 @@ async function startDaemon(options) {
7956
8052
  startedFromDaemon: true,
7957
8053
  startedBy: "daemon",
7958
8054
  lifecycleState: resumeSessionId ? "idle" : "starting",
8055
+ ...resumeSessionId && { claudeSessionId: resumeSessionId },
7959
8056
  sharing: options2.sharing,
7960
8057
  securityContext: options2.securityContext,
7961
8058
  tags: options2.tags,
@@ -7971,7 +8068,7 @@ async function startDaemon(options) {
7971
8068
  const allPersisted = loadPersistedSessions();
7972
8069
  const persisted = allPersisted.find((p) => p.sessionId === sessionId) || (resumeSessionId ? allPersisted.find((p) => p.claudeResumeId === resumeSessionId) : void 0);
7973
8070
  let claudeResumeId = persisted?.claudeResumeId || (resumeSessionId || void 0);
7974
- let currentPermissionMode = options2.permissionMode || persisted?.permissionMode || "default";
8071
+ let currentPermissionMode = options2.permissionMode || persisted?.permissionMode || "bypassPermissions";
7975
8072
  const sessionCreatedAt = persisted?.createdAt || Date.now();
7976
8073
  let lastSpawnMeta = persisted?.spawnMeta || {};
7977
8074
  let sessionWasProcessing = !!options2.wasProcessing;
@@ -8679,7 +8776,16 @@ The automated loop has finished. Review the progress above and let me know if yo
8679
8776
  artifactSync.scheduleDebouncedSync(sessionId, getSessionDir(directory, sessionId), sessionMetadata, machineId);
8680
8777
  }
8681
8778
  if (isResumeFailure) {
8682
- logger.log(`[Session ${sessionId}] Resume failed \u2014 Claude started fresh session (tried: ${persisted?.claudeResumeId ?? "unknown"}, got: ${msg.session_id})`);
8779
+ const triedId = persisted?.claudeResumeId ?? "unknown";
8780
+ logger.log(`[Session ${sessionId}] Resume failed \u2014 Claude started fresh session (tried: ${triedId}, got: ${msg.session_id})`);
8781
+ sessionService.pushMessage(
8782
+ {
8783
+ type: "message",
8784
+ message: `Resume incomplete \u2014 Claude could not restore the previous conversation. A new Claude session was started (expected ${triedId.slice(0, 8)}\u2026, got ${msg.session_id.slice(0, 8)}\u2026). Earlier messages remain in the history above for reference, but Claude does not have them in its context.`,
8785
+ level: "warning"
8786
+ },
8787
+ "event"
8788
+ );
8683
8789
  } else if (isConversationClear) {
8684
8790
  logger.log(`[Session ${sessionId}] Conversation cleared (/clear) \u2014 new Claude session: ${msg.session_id}`);
8685
8791
  sessionService.clearMessages();
@@ -8941,6 +9047,11 @@ The automated loop has finished. Review the progress above and let me know if yo
8941
9047
  claudeResumeId,
8942
9048
  "--fork-session",
8943
9049
  "--no-session-persistence",
9050
+ // /btw is non-interactive; without bypass the
9051
+ // forked Claude pauses on tool prompts and the
9052
+ // user just sees a hanging side-channel.
9053
+ "--permission-mode",
9054
+ "bypassPermissions",
8944
9055
  "--output-format",
8945
9056
  "stream-json",
8946
9057
  "--verbose"
@@ -9075,6 +9186,9 @@ The automated loop has finished. Review the progress above and let me know if yo
9075
9186
  },
9076
9187
  onSwitchMode: async (mode) => {
9077
9188
  const normalizedMode = toClaudePermissionMode(mode);
9189
+ if (currentPermissionMode === normalizedMode) {
9190
+ return;
9191
+ }
9078
9192
  logger.log(`[Session ${sessionId}] Switch mode: ${mode}${mode !== normalizedMode ? ` \u2192 ${normalizedMode}` : ""}`);
9079
9193
  if (isRestartingClaude || isSwitchingMode) {
9080
9194
  logger.log(`[Session ${sessionId}] Switch mode deferred \u2014 restart/switch already in progress`);
@@ -9153,15 +9267,14 @@ The automated loop has finished. Review the progress above and let me know if yo
9153
9267
  lastSpawnMeta = { ...lastSpawnMeta, appendSystemPrompt: prompt };
9154
9268
  return await restartClaudeHandler();
9155
9269
  },
9156
- onKillSession: () => {
9157
- logger.log(`[Session ${sessionId}] Kill session requested`);
9158
- stopSession(sessionId);
9270
+ onArchiveSession: () => {
9271
+ logger.log(`[Session ${sessionId}] Archive session requested`);
9272
+ archiveSession(sessionId);
9159
9273
  },
9160
9274
  onInboxMessage: (message) => {
9161
9275
  if (trackedSession?.stopped) return;
9162
9276
  logger.log(`[Session ${sessionId}] Inbox message received (urgency: ${message.urgency || "normal"}, from: ${message.from || "unknown"})`);
9163
9277
  const formatted = formatInboxMessageXml(message);
9164
- sessionService.markInboxRead(message.messageId);
9165
9278
  if (message.urgency === "urgent") {
9166
9279
  logger.log(`[Session ${sessionId}] Delivering urgent inbox message to agent`);
9167
9280
  sessionService.pushMessage(formatted, "user");
@@ -9511,7 +9624,7 @@ The automated loop has finished. Review the progress above and let me know if yo
9511
9624
  tags: options2.tags,
9512
9625
  parentSessionId: options2.parentSessionId
9513
9626
  };
9514
- let currentPermissionMode = options2.permissionMode || "default";
9627
+ let currentPermissionMode = options2.permissionMode || "bypassPermissions";
9515
9628
  const allowedTools = /* @__PURE__ */ new Set();
9516
9629
  const allowedBashLiterals = /* @__PURE__ */ new Set();
9517
9630
  const allowedBashPrefixes = /* @__PURE__ */ new Set();
@@ -9605,6 +9718,7 @@ The automated loop has finished. Review the progress above and let me know if yo
9605
9718
  agentBackend.respondToPermission?.(requestId, params.approved);
9606
9719
  },
9607
9720
  onSwitchMode: (mode) => {
9721
+ if (currentPermissionMode === mode) return;
9608
9722
  logger.log(`[${agentName} Session ${sessionId}] Switch mode: ${mode}`);
9609
9723
  currentPermissionMode = mode;
9610
9724
  },
@@ -9624,15 +9738,14 @@ The automated loop has finished. Review the progress above and let me know if yo
9624
9738
  onApplySystemPrompt: async () => {
9625
9739
  return { success: false, message: "System prompt updates with restart are not yet supported for this agent type." };
9626
9740
  },
9627
- onKillSession: () => {
9628
- logger.log(`[${agentName} Session ${sessionId}] Kill session requested`);
9629
- stopSession(sessionId);
9741
+ onArchiveSession: () => {
9742
+ logger.log(`[${agentName} Session ${sessionId}] Archive session requested`);
9743
+ archiveSession(sessionId);
9630
9744
  },
9631
9745
  onInboxMessage: (message) => {
9632
9746
  if (acpStopped) return;
9633
9747
  logger.log(`[${agentName} Session ${sessionId}] Inbox message received (urgency: ${message.urgency || "normal"}, from: ${message.from || "unknown"})`);
9634
9748
  const formatted = formatInboxMessageXml(message);
9635
- sessionService.markInboxRead(message.messageId);
9636
9749
  if (message.urgency === "urgent" && acpBackendReady) {
9637
9750
  logger.log(`[${agentName} Session ${sessionId}] Delivering urgent inbox message to agent`);
9638
9751
  sessionService.pushMessage(formatted, "user");
@@ -10082,7 +10195,7 @@ The automated loop has finished. Review the progress above and let me know if yo
10082
10195
  "event"
10083
10196
  );
10084
10197
  sessionService.sendSessionEnd();
10085
- stopSession(sessionId);
10198
+ deleteSession(sessionId);
10086
10199
  });
10087
10200
  return {
10088
10201
  type: "success",
@@ -10097,8 +10210,7 @@ The automated loop has finished. Review the progress above and let me know if yo
10097
10210
  };
10098
10211
  }
10099
10212
  };
10100
- const stopSession = (sessionId) => {
10101
- logger.log(`Stopping session: ${sessionId}`);
10213
+ const teardownTrackedSession = (sessionId) => {
10102
10214
  for (const [pid, session] of pidToTrackedSession) {
10103
10215
  if (session.svampSessionId === sessionId) {
10104
10216
  session.stopped = true;
@@ -10117,16 +10229,64 @@ The automated loop has finished. Review the progress above and let me know if yo
10117
10229
  session.cleanupSvampConfig?.();
10118
10230
  artifactSync.cancelSync(sessionId);
10119
10231
  pidToTrackedSession.delete(pid);
10120
- deletePersistedSession(sessionId);
10121
- logger.log(`Session ${sessionId} stopped`);
10122
10232
  return true;
10123
10233
  }
10124
10234
  }
10125
10235
  artifactSync.cancelSync(sessionId);
10126
- deletePersistedSession(sessionId);
10127
- logger.log(`Session ${sessionId} not found in memory, cleaned up persisted state`);
10128
10236
  return false;
10129
10237
  };
10238
+ const archiveSession = (sessionId) => {
10239
+ logger.log(`Archiving session: ${sessionId}`);
10240
+ const wasInMemory = teardownTrackedSession(sessionId);
10241
+ const markedArchived = markSessionAsArchived(sessionId);
10242
+ if (wasInMemory || markedArchived) {
10243
+ logger.log(`Session ${sessionId} archived (inMemory=${wasInMemory}, persisted=${markedArchived})`);
10244
+ return true;
10245
+ }
10246
+ logger.log(`Session ${sessionId} not found in memory or on disk; nothing to archive`);
10247
+ return false;
10248
+ };
10249
+ const resumeSession = async (sessionId) => {
10250
+ logger.log(`Resuming session: ${sessionId}`);
10251
+ for (const session of pidToTrackedSession.values()) {
10252
+ if (session.svampSessionId === sessionId && !session.stopped) {
10253
+ logger.log(`Session ${sessionId} already running \u2014 resume is a no-op`);
10254
+ return { success: true, sessionId, message: "Session is already running" };
10255
+ }
10256
+ }
10257
+ const persisted = clearSessionArchivedFlag(sessionId);
10258
+ if (!persisted) {
10259
+ return { success: false, message: `Session ${sessionId} has no persisted record to resume` };
10260
+ }
10261
+ try {
10262
+ const result = await spawnSession({
10263
+ directory: persisted.directory,
10264
+ sessionId: persisted.sessionId,
10265
+ resumeSessionId: persisted.claudeResumeId,
10266
+ sharing: persisted.metadata?.sharing,
10267
+ securityContext: persisted.metadata?.securityContext,
10268
+ forceIsolation: !!persisted.metadata?.isolationMethod,
10269
+ parentSessionId: persisted.metadata?.parentSessionId,
10270
+ permissionMode: persisted.permissionMode
10271
+ });
10272
+ if (result.type === "success") {
10273
+ logger.log(`Resumed session ${sessionId} (claudeResumeId=${persisted.claudeResumeId || "none"})`);
10274
+ return { success: true, sessionId: result.sessionId };
10275
+ }
10276
+ markSessionAsArchived(sessionId);
10277
+ return { success: false, message: result.errorMessage || `spawnSession returned ${result.type}` };
10278
+ } catch (err) {
10279
+ markSessionAsArchived(sessionId);
10280
+ return { success: false, message: err.message };
10281
+ }
10282
+ };
10283
+ const deleteSession = (sessionId) => {
10284
+ logger.log(`Deleting session: ${sessionId}`);
10285
+ teardownTrackedSession(sessionId);
10286
+ deletePersistedSession(sessionId);
10287
+ logger.log(`Session ${sessionId} deleted`);
10288
+ return true;
10289
+ };
10130
10290
  const restartSession = async (sessionId) => {
10131
10291
  for (const session of pidToTrackedSession.values()) {
10132
10292
  if (session.svampSessionId === sessionId && !session.stopped) {
@@ -10204,7 +10364,9 @@ The automated loop has finished. Review the progress above and let me know if yo
10204
10364
  initialDaemonState,
10205
10365
  {
10206
10366
  spawnSession,
10207
- stopSession,
10367
+ archiveSession,
10368
+ resumeSession,
10369
+ deleteSession,
10208
10370
  restartSession,
10209
10371
  requestShutdown: () => {
10210
10372
  logger.log("Shutdown requested via hypha-app (ignored \u2014 daemon never self-terminates)");
@@ -54,7 +54,7 @@ async function handleServeCommand() {
54
54
  }
55
55
  }
56
56
  async function serveAdd(args, machineId) {
57
- const { connectAndGetMachine } = await import('./commands-D5kCHCfX.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-B0zqVia0.mjs');
58
58
  const pos = positionalArgs(args);
59
59
  const name = pos[0];
60
60
  if (!name) {
@@ -93,7 +93,7 @@ async function serveAdd(args, machineId) {
93
93
  }
94
94
  }
95
95
  async function serveApply(args, machineId) {
96
- const { connectAndGetMachine } = await import('./commands-D5kCHCfX.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-B0zqVia0.mjs');
97
97
  const fs = await import('fs');
98
98
  const yaml = await import('yaml');
99
99
  const file = positionalArgs(args)[0];
@@ -182,7 +182,7 @@ async function serveApply(args, machineId) {
182
182
  }
183
183
  }
184
184
  async function serveRemove(args, machineId) {
185
- const { connectAndGetMachine } = await import('./commands-D5kCHCfX.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-B0zqVia0.mjs');
186
186
  const pos = positionalArgs(args);
187
187
  const name = pos[0];
188
188
  if (!name) {
@@ -202,7 +202,7 @@ async function serveRemove(args, machineId) {
202
202
  }
203
203
  }
204
204
  async function serveList(args, machineId) {
205
- const { connectAndGetMachine } = await import('./commands-D5kCHCfX.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-B0zqVia0.mjs');
206
206
  const all = hasFlag(args, "--all", "-a");
207
207
  const json = hasFlag(args, "--json");
208
208
  const sessionId = getFlag(args, "--session");
@@ -235,7 +235,7 @@ async function serveList(args, machineId) {
235
235
  }
236
236
  }
237
237
  async function serveInfo(machineId) {
238
- const { connectAndGetMachine } = await import('./commands-D5kCHCfX.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-B0zqVia0.mjs');
239
239
  const { machine, server } = await connectAndGetMachine(machineId);
240
240
  try {
241
241
  const info = await machine.serveInfo();
@@ -4,7 +4,7 @@ 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 { S as ServeAuth, h as hasCookieToken } from './run-HuBXfVSz.mjs';
7
+ import { S as ServeAuth, h as hasCookieToken } from './run-h-QVSVFd.mjs';
8
8
  import 'os';
9
9
  import 'fs/promises';
10
10
  import 'url';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.68",
3
+ "version": "0.2.71",
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 && mkdir -p bin/skills && cp -r ../../skills/artifact bin/skills/artifact && tsc --noEmit && pkgroll",
22
22
  "typecheck": "tsc --noEmit",
23
- "test": "npx tsx test/test-context-window.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-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-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-claude-auth.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-supervisor-lock.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.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",
23
+ "test": "npx tsx test/test-context-window.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-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-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-claude-auth.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-session-send-query.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-supervisor-lock.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.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",
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",