svamp-cli 0.2.36 → 0.2.38

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.
@@ -148,7 +148,7 @@ async function sessionBroadcast(action, args) {
148
148
  console.log(`Broadcast sent: ${action}`);
149
149
  }
150
150
  async function connectToMachineService() {
151
- const { connectAndGetMachine } = await import('./commands-Cik0LIAl.mjs');
151
+ const { connectAndGetMachine } = await import('./commands-B8Q1ig7j.mjs');
152
152
  return connectAndGetMachine();
153
153
  }
154
154
  async function inboxSend(targetSessionId, opts) {
@@ -165,7 +165,7 @@ async function inboxSend(targetSessionId, opts) {
165
165
  }
166
166
  const { server, machine } = await connectToMachineService();
167
167
  try {
168
- const { resolveSessionId } = await import('./commands-Cik0LIAl.mjs');
168
+ const { resolveSessionId } = await import('./commands-B8Q1ig7j.mjs');
169
169
  const sessions = await machine.listSessions();
170
170
  const match = resolveSessionId(sessions, targetSessionId);
171
171
  const fullTargetId = match.sessionId;
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-BKBv5P9v.mjs';
1
+ import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-D-1qvfLH.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -36,7 +36,7 @@ async function main() {
36
36
  await logoutFromHypha();
37
37
  } else if (subcommand === "daemon") {
38
38
  if (daemonSubcommand === "restart") {
39
- const { restartDaemon } = await import('./run-BKBv5P9v.mjs').then(function (n) { return n.k; });
39
+ const { restartDaemon } = await import('./run-D-1qvfLH.mjs').then(function (n) { return n.k; });
40
40
  await restartDaemon();
41
41
  process.exit(0);
42
42
  }
@@ -82,10 +82,11 @@ async function main() {
82
82
  process.exit(0);
83
83
  } else if (daemonSubcommand === "start-supervised") {
84
84
  const { spawn: spawnChild } = await import('child_process');
85
- const { appendFileSync, mkdirSync, existsSync: fsExists } = await import('fs');
85
+ const { appendFileSync, mkdirSync } = await import('fs');
86
86
  const { join: pathJoin } = await import('path');
87
87
  const osModule = await import('os');
88
88
  const svampHome = process.env.SVAMP_HOME || pathJoin(osModule.homedir(), ".svamp");
89
+ mkdirSync(svampHome, { recursive: true });
89
90
  const logsDir = pathJoin(svampHome, "logs");
90
91
  mkdirSync(logsDir, { recursive: true });
91
92
  const logFile = pathJoin(logsDir, "daemon-supervised.log");
@@ -97,13 +98,27 @@ async function main() {
97
98
  } catch {
98
99
  }
99
100
  };
101
+ const {
102
+ acquireSupervisorLockWithRetry,
103
+ releaseSupervisorLock,
104
+ findOrphanedSyncPids,
105
+ killOrphanedSyncs
106
+ } = await import('./supervisorLock-DwNAn0VN.mjs');
100
107
  const supervisorPidFile = pathJoin(svampHome, "supervisor.pid");
108
+ const lock = acquireSupervisorLockWithRetry(supervisorPidFile, process.pid);
109
+ if (!lock.acquired) {
110
+ log(`Another supervisor is already running (PID ${lock.heldBy}) \u2014 exiting`);
111
+ console.error(`svamp daemon: another supervisor is already running (PID ${lock.heldBy}).`);
112
+ process.exit(0);
113
+ }
101
114
  try {
102
- appendFileSync(supervisorPidFile, "");
103
- } catch {
115
+ const orphans = findOrphanedSyncPids(process.pid);
116
+ if (orphans.length > 0) {
117
+ await killOrphanedSyncs(orphans, { log, gracePeriodMs: 3e3 });
118
+ }
119
+ } catch (err) {
120
+ log(`Orphan scan failed (non-fatal): ${err?.message || err}`);
104
121
  }
105
- const { writeFileSync: wfs } = await import('fs');
106
- wfs(supervisorPidFile, String(process.pid), "utf-8");
107
122
  const extraSyncArgs = [];
108
123
  if (args.includes("--no-auto-continue")) extraSyncArgs.push("--no-auto-continue");
109
124
  const BASE_DELAY_MS = 2e3;
@@ -188,11 +203,7 @@ async function main() {
188
203
  setTimeout(() => clearInterval(checkStop), delay + 100);
189
204
  });
190
205
  }
191
- try {
192
- const { unlinkSync: us } = await import('fs');
193
- us(supervisorPidFile);
194
- } catch {
195
- }
206
+ releaseSupervisorLock(supervisorPidFile, process.pid);
196
207
  process.exit(0);
197
208
  } else if (daemonSubcommand === "start-sync") {
198
209
  const noAutoContinue = args.includes("--no-auto-continue");
@@ -246,7 +257,7 @@ async function main() {
246
257
  console.error("svamp serve: Serve commands are not available in sandboxed sessions.");
247
258
  process.exit(1);
248
259
  }
249
- const { handleServeCommand } = await import('./serveCommands-Cgq6Vif4.mjs');
260
+ const { handleServeCommand } = await import('./serveCommands-B2vQJyUt.mjs');
250
261
  await handleServeCommand();
251
262
  process.exit(0);
252
263
  } else if (subcommand === "process" || subcommand === "proc") {
@@ -255,7 +266,7 @@ async function main() {
255
266
  console.error("svamp process: Process commands are not available in sandboxed sessions.");
256
267
  process.exit(1);
257
268
  }
258
- const { processCommand } = await import('./commands-B4qmyXeM.mjs');
269
+ const { processCommand } = await import('./commands-BpW-hioi.mjs');
259
270
  let machineId;
260
271
  const processArgs = args.slice(1);
261
272
  const mIdx = processArgs.findIndex((a) => a === "--machine" || a === "-m");
@@ -273,7 +284,7 @@ async function main() {
273
284
  } else if (!subcommand || subcommand === "start") {
274
285
  await handleInteractiveCommand();
275
286
  } else if (subcommand === "--version" || subcommand === "-v") {
276
- const pkg = await import('./package-DDUrcHYU.mjs').catch(() => ({ default: { version: "unknown" } }));
287
+ const pkg = await import('./package-CcU8sNV_.mjs').catch(() => ({ default: { version: "unknown" } }));
277
288
  console.log(`svamp version: ${pkg.default.version}`);
278
289
  } else {
279
290
  console.error(`Unknown command: ${subcommand}`);
@@ -282,7 +293,7 @@ async function main() {
282
293
  }
283
294
  }
284
295
  async function handleInteractiveCommand() {
285
- const { runInteractive } = await import('./run-sAfJWYld.mjs');
296
+ const { runInteractive } = await import('./run-BStlzTCG.mjs');
286
297
  const interactiveArgs = subcommand === "start" ? args.slice(1) : args;
287
298
  let directory = process.cwd();
288
299
  let resumeSessionId;
@@ -327,7 +338,7 @@ async function handleAgentCommand() {
327
338
  return;
328
339
  }
329
340
  if (agentArgs[0] === "list") {
330
- const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-BKBv5P9v.mjs').then(function (n) { return n.i; });
341
+ const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-D-1qvfLH.mjs').then(function (n) { return n.i; });
331
342
  console.log("Known agents:");
332
343
  for (const [name, config2] of Object.entries(KNOWN_ACP_AGENTS)) {
333
344
  console.log(` ${name.padEnd(12)} ${config2.command} ${config2.args.join(" ")} (ACP)`);
@@ -339,7 +350,7 @@ async function handleAgentCommand() {
339
350
  console.log('Use "svamp agent -- <command> [args]" for a custom ACP agent.');
340
351
  return;
341
352
  }
342
- const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-BKBv5P9v.mjs').then(function (n) { return n.i; });
353
+ const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-D-1qvfLH.mjs').then(function (n) { return n.i; });
343
354
  let cwd = process.cwd();
344
355
  const filteredArgs = [];
345
356
  for (let i = 0; i < agentArgs.length; i++) {
@@ -363,12 +374,12 @@ async function handleAgentCommand() {
363
374
  console.log(`Starting ${config.agentName} agent in ${cwd}...`);
364
375
  let backend;
365
376
  if (KNOWN_MCP_AGENTS[config.agentName]) {
366
- const { CodexMcpBackend } = await import('./run-BKBv5P9v.mjs').then(function (n) { return n.j; });
377
+ const { CodexMcpBackend } = await import('./run-D-1qvfLH.mjs').then(function (n) { return n.j; });
367
378
  backend = new CodexMcpBackend({ cwd, log: logFn });
368
379
  } else {
369
- const { AcpBackend } = await import('./run-BKBv5P9v.mjs').then(function (n) { return n.h; });
370
- const { GeminiTransport } = await import('./run-BKBv5P9v.mjs').then(function (n) { return n.G; });
371
- const { DefaultTransport } = await import('./run-BKBv5P9v.mjs').then(function (n) { return n.D; });
380
+ const { AcpBackend } = await import('./run-D-1qvfLH.mjs').then(function (n) { return n.h; });
381
+ const { GeminiTransport } = await import('./run-D-1qvfLH.mjs').then(function (n) { return n.G; });
382
+ const { DefaultTransport } = await import('./run-D-1qvfLH.mjs').then(function (n) { return n.D; });
372
383
  const transportHandler = config.agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(config.agentName);
373
384
  backend = new AcpBackend({
374
385
  agentName: config.agentName,
@@ -495,7 +506,7 @@ async function handleSessionCommand() {
495
506
  process.exit(1);
496
507
  }
497
508
  }
498
- const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-Cik0LIAl.mjs');
509
+ const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-B8Q1ig7j.mjs');
499
510
  const parseFlagStr = (flag, shortFlag) => {
500
511
  for (let i = 1; i < sessionArgs.length; i++) {
501
512
  if ((sessionArgs[i] === flag || shortFlag) && i + 1 < sessionArgs.length) {
@@ -555,7 +566,7 @@ async function handleSessionCommand() {
555
566
  allowDomain.push(sessionArgs[++i]);
556
567
  }
557
568
  }
558
- const { parseShareArg } = await import('./commands-Cik0LIAl.mjs');
569
+ const { parseShareArg } = await import('./commands-B8Q1ig7j.mjs');
559
570
  const shareEntries = share.map((s) => parseShareArg(s));
560
571
  await sessionSpawn(agent, dir, targetMachineId, {
561
572
  message,
@@ -641,7 +652,7 @@ async function handleSessionCommand() {
641
652
  console.error("Usage: svamp session approve <session-id> [request-id] [--json]");
642
653
  process.exit(1);
643
654
  }
644
- const { sessionApprove } = await import('./commands-Cik0LIAl.mjs');
655
+ const { sessionApprove } = await import('./commands-B8Q1ig7j.mjs');
645
656
  const approveReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
646
657
  await sessionApprove(sessionArgs[1], approveReqId, targetMachineId, {
647
658
  json: hasFlag("--json")
@@ -651,7 +662,7 @@ async function handleSessionCommand() {
651
662
  console.error("Usage: svamp session deny <session-id> [request-id] [--json]");
652
663
  process.exit(1);
653
664
  }
654
- const { sessionDeny } = await import('./commands-Cik0LIAl.mjs');
665
+ const { sessionDeny } = await import('./commands-B8Q1ig7j.mjs');
655
666
  const denyReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
656
667
  await sessionDeny(sessionArgs[1], denyReqId, targetMachineId, {
657
668
  json: hasFlag("--json")
@@ -687,7 +698,7 @@ async function handleSessionCommand() {
687
698
  console.error("Usage: svamp session set-title <title>");
688
699
  process.exit(1);
689
700
  }
690
- const { sessionSetTitle } = await import('./agentCommands-oLVbSvBZ.mjs');
701
+ const { sessionSetTitle } = await import('./agentCommands-Djr_xWX-.mjs');
691
702
  await sessionSetTitle(title);
692
703
  } else if (sessionSubcommand === "set-link") {
693
704
  const url = sessionArgs[1];
@@ -696,7 +707,7 @@ async function handleSessionCommand() {
696
707
  process.exit(1);
697
708
  }
698
709
  const label = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
699
- const { sessionSetLink } = await import('./agentCommands-oLVbSvBZ.mjs');
710
+ const { sessionSetLink } = await import('./agentCommands-Djr_xWX-.mjs');
700
711
  await sessionSetLink(url, label);
701
712
  } else if (sessionSubcommand === "notify") {
702
713
  const message = sessionArgs[1];
@@ -705,7 +716,7 @@ async function handleSessionCommand() {
705
716
  process.exit(1);
706
717
  }
707
718
  const level = parseFlagStr("--level") || "info";
708
- const { sessionNotify } = await import('./agentCommands-oLVbSvBZ.mjs');
719
+ const { sessionNotify } = await import('./agentCommands-Djr_xWX-.mjs');
709
720
  await sessionNotify(message, level);
710
721
  } else if (sessionSubcommand === "broadcast") {
711
722
  const action = sessionArgs[1];
@@ -713,7 +724,7 @@ async function handleSessionCommand() {
713
724
  console.error("Usage: svamp session broadcast <action> [args...]\nActions: open-canvas <url> [label], close-canvas, toast <message>");
714
725
  process.exit(1);
715
726
  }
716
- const { sessionBroadcast } = await import('./agentCommands-oLVbSvBZ.mjs');
727
+ const { sessionBroadcast } = await import('./agentCommands-Djr_xWX-.mjs');
717
728
  await sessionBroadcast(action, sessionArgs.slice(2).filter((a) => !a.startsWith("--")));
718
729
  } else if (sessionSubcommand === "inbox") {
719
730
  const inboxSubcmd = sessionArgs[1];
@@ -724,7 +735,7 @@ async function handleSessionCommand() {
724
735
  process.exit(1);
725
736
  }
726
737
  if (agentSessionId) {
727
- const { inboxSend } = await import('./agentCommands-oLVbSvBZ.mjs');
738
+ const { inboxSend } = await import('./agentCommands-Djr_xWX-.mjs');
728
739
  await inboxSend(sessionArgs[2], {
729
740
  body: sessionArgs[3],
730
741
  subject: parseFlagStr("--subject"),
@@ -739,7 +750,7 @@ async function handleSessionCommand() {
739
750
  }
740
751
  } else if (inboxSubcmd === "list" || inboxSubcmd === "ls") {
741
752
  if (agentSessionId && !sessionArgs[2]) {
742
- const { inboxList } = await import('./agentCommands-oLVbSvBZ.mjs');
753
+ const { inboxList } = await import('./agentCommands-Djr_xWX-.mjs');
743
754
  await inboxList({
744
755
  unread: hasFlag("--unread"),
745
756
  limit: parseFlagInt("--limit"),
@@ -761,7 +772,7 @@ async function handleSessionCommand() {
761
772
  process.exit(1);
762
773
  }
763
774
  if (agentSessionId && !sessionArgs[3]) {
764
- const { inboxList } = await import('./agentCommands-oLVbSvBZ.mjs');
775
+ const { inboxList } = await import('./agentCommands-Djr_xWX-.mjs');
765
776
  await sessionInboxRead(agentSessionId, sessionArgs[2], targetMachineId);
766
777
  } else if (sessionArgs[3]) {
767
778
  await sessionInboxRead(sessionArgs[2], sessionArgs[3], targetMachineId);
@@ -771,7 +782,7 @@ async function handleSessionCommand() {
771
782
  }
772
783
  } else if (inboxSubcmd === "reply") {
773
784
  if (agentSessionId && sessionArgs[2] && sessionArgs[3] && !sessionArgs[4]) {
774
- const { inboxReply } = await import('./agentCommands-oLVbSvBZ.mjs');
785
+ const { inboxReply } = await import('./agentCommands-Djr_xWX-.mjs');
775
786
  await inboxReply(sessionArgs[2], sessionArgs[3]);
776
787
  } else if (sessionArgs[2] && sessionArgs[3] && sessionArgs[4]) {
777
788
  await sessionInboxReply(sessionArgs[2], sessionArgs[3], sessionArgs[4], targetMachineId);
@@ -807,7 +818,7 @@ async function handleMachineCommand() {
807
818
  return;
808
819
  }
809
820
  if (machineSubcommand === "share") {
810
- const { machineShare } = await import('./commands-Cik0LIAl.mjs');
821
+ const { machineShare } = await import('./commands-B8Q1ig7j.mjs');
811
822
  let machineId;
812
823
  const shareArgs = [];
813
824
  for (let i = 1; i < machineArgs.length; i++) {
@@ -837,7 +848,7 @@ async function handleMachineCommand() {
837
848
  }
838
849
  await machineShare(machineId, { add, remove, list, configPath, showConfig });
839
850
  } else if (machineSubcommand === "exec") {
840
- const { machineExec } = await import('./commands-Cik0LIAl.mjs');
851
+ const { machineExec } = await import('./commands-B8Q1ig7j.mjs');
841
852
  let machineId;
842
853
  let cwd;
843
854
  const cmdParts = [];
@@ -857,7 +868,7 @@ async function handleMachineCommand() {
857
868
  }
858
869
  await machineExec(machineId, command, cwd);
859
870
  } else if (machineSubcommand === "info") {
860
- const { machineInfo } = await import('./commands-Cik0LIAl.mjs');
871
+ const { machineInfo } = await import('./commands-B8Q1ig7j.mjs');
861
872
  let machineId;
862
873
  for (let i = 1; i < machineArgs.length; i++) {
863
874
  if ((machineArgs[i] === "--machine" || machineArgs[i] === "-m") && i + 1 < machineArgs.length) {
@@ -877,10 +888,10 @@ async function handleMachineCommand() {
877
888
  level = machineArgs[++i];
878
889
  }
879
890
  }
880
- const { machineNotify } = await import('./agentCommands-oLVbSvBZ.mjs');
891
+ const { machineNotify } = await import('./agentCommands-Djr_xWX-.mjs');
881
892
  await machineNotify(message, level);
882
893
  } else if (machineSubcommand === "ls") {
883
- const { machineLs } = await import('./commands-Cik0LIAl.mjs');
894
+ const { machineLs } = await import('./commands-B8Q1ig7j.mjs');
884
895
  let machineId;
885
896
  let showHidden = false;
886
897
  let path;
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { execSync } from 'node:child_process';
3
3
  import { resolve, join } from 'node:path';
4
4
  import os from 'node:os';
5
- import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-BKBv5P9v.mjs';
5
+ import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-D-1qvfLH.mjs';
6
6
  import 'os';
7
7
  import 'fs/promises';
8
8
  import 'fs';
@@ -1,11 +1,11 @@
1
1
  import { writeFileSync, readFileSync } from 'fs';
2
2
  import { resolve } from 'path';
3
- import { connectAndGetMachine } from './commands-Cik0LIAl.mjs';
3
+ import { connectAndGetMachine } from './commands-B8Q1ig7j.mjs';
4
4
  import 'node:fs';
5
5
  import 'node:child_process';
6
6
  import 'node:path';
7
7
  import 'node:os';
8
- import './run-BKBv5P9v.mjs';
8
+ import './run-D-1qvfLH.mjs';
9
9
  import 'os';
10
10
  import 'fs/promises';
11
11
  import 'url';
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-BKBv5P9v.mjs';
1
+ export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-D-1qvfLH.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -0,0 +1,63 @@
1
+ var name = "svamp-cli";
2
+ var version = "0.2.38";
3
+ var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
+ var author = "Amun AI AB";
5
+ var license = "SEE LICENSE IN LICENSE";
6
+ var type = "module";
7
+ var bin = {
8
+ svamp: "./bin/svamp.mjs"
9
+ };
10
+ var files = [
11
+ "dist",
12
+ "bin"
13
+ ];
14
+ var main = "./dist/index.mjs";
15
+ var exports$1 = {
16
+ ".": "./dist/index.mjs",
17
+ "./cli": "./dist/cli.mjs"
18
+ };
19
+ var scripts = {
20
+ build: "rm -rf dist && tsc --noEmit && pkgroll",
21
+ typecheck: "tsc --noEmit",
22
+ test: "npx tsx test/test-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-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-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:hypha": "node --no-warnings test/test-hypha-service.mjs",
24
+ dev: "tsx src/cli.ts",
25
+ "dev:daemon": "tsx src/cli.ts daemon start-sync",
26
+ "test:e2e": "node --no-warnings test/e2e-session-tests.mjs",
27
+ "test:frpc": "npx tsx test/test-frpc-e2e.mjs"
28
+ };
29
+ var dependencies = {
30
+ "@agentclientprotocol/sdk": "^0.14.1",
31
+ "@modelcontextprotocol/sdk": "^1.25.3",
32
+ "hypha-rpc": "0.21.36",
33
+ "node-pty": "1.2.0-beta.11",
34
+ ws: "^8.18.0",
35
+ yaml: "^2.8.2",
36
+ zod: "^3.24.4"
37
+ };
38
+ var devDependencies = {
39
+ "@types/node": ">=20",
40
+ "@types/ws": "^8.5.14",
41
+ pkgroll: "^2.14.2",
42
+ tsx: "^4.20.6",
43
+ typescript: "5.9.3"
44
+ };
45
+ var packageManager = "yarn@1.22.22";
46
+ var _package = {
47
+ name: name,
48
+ version: version,
49
+ description: description,
50
+ author: author,
51
+ license: license,
52
+ type: type,
53
+ bin: bin,
54
+ files: files,
55
+ main: main,
56
+ exports: exports$1,
57
+ scripts: scripts,
58
+ dependencies: dependencies,
59
+ devDependencies: devDependencies,
60
+ packageManager: packageManager
61
+ };
62
+
63
+ export { author, bin, _package as default, dependencies, description, devDependencies, exports$1 as exports, files, license, main, name, packageManager, scripts, type, version };
@@ -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-BKBv5P9v.mjs';
5
+ import { c as connectToHypha, a as registerSessionService } from './run-D-1qvfLH.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -4922,6 +4922,14 @@ var credentialStaging = /*#__PURE__*/Object.freeze({
4922
4922
  stageCredentialsForSharing: stageCredentialsForSharing
4923
4923
  });
4924
4924
 
4925
+ function shouldIsolate(input) {
4926
+ if (input.forceIsolation) return true;
4927
+ const sessionCtx = input.sessionSecurityContext;
4928
+ if (sessionCtx === null) return false;
4929
+ if (sessionCtx !== void 0) return true;
4930
+ return input.optionsSecurityContext !== null && input.optionsSecurityContext !== void 0;
4931
+ }
4932
+
4925
4933
  const DEFAULT_PROBE_INTERVAL_S = 10;
4926
4934
  const DEFAULT_PROBE_TIMEOUT_S = 5;
4927
4935
  const DEFAULT_PROBE_FAILURE_THRESHOLD = 3;
@@ -6300,6 +6308,20 @@ async function startDaemon(options) {
6300
6308
  process.on("SIGINT", () => requestShutdown("os-signal"));
6301
6309
  process.on("SIGTERM", () => requestShutdown("os-signal"));
6302
6310
  process.on("SIGUSR1", () => requestShutdown("os-signal-cleanup"));
6311
+ const { watchParentLiveness } = await import('./supervisorLock-DwNAn0VN.mjs');
6312
+ const cancelParentWatchdog = watchParentLiveness({
6313
+ intervalMs: 5e3,
6314
+ onParentDeath: () => {
6315
+ logger.log("Supervisor process died \u2014 sync daemon shutting down to avoid orphan state");
6316
+ requestShutdown("supervisor-died");
6317
+ }
6318
+ });
6319
+ process.on("exit", () => {
6320
+ try {
6321
+ cancelParentWatchdog();
6322
+ } catch {
6323
+ }
6324
+ });
6303
6325
  process.on("uncaughtException", (error) => {
6304
6326
  if (shutdownRequested) return;
6305
6327
  logger.error("Uncaught exception (non-fatal, daemon continues):", error);
@@ -6440,7 +6462,7 @@ async function startDaemon(options) {
6440
6462
  const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
6441
6463
  await supervisor.init();
6442
6464
  const tunnels = /* @__PURE__ */ new Map();
6443
- const { ServeManager } = await import('./serveManager-XPnOLqQ4.mjs');
6465
+ const { ServeManager } = await import('./serveManager-MqY_0frw.mjs');
6444
6466
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
6445
6467
  ensureAutoInstalledSkills(logger).catch(() => {
6446
6468
  });
@@ -6585,8 +6607,7 @@ async function startDaemon(options) {
6585
6607
  }
6586
6608
  });
6587
6609
  }, buildIsolationConfig2 = function(dir) {
6588
- if (!options2.forceIsolation && !sessionMetadata.sharing?.enabled) return null;
6589
- if (sessionMetadata.securityContext === null) return null;
6610
+ if (!shouldIsolateSession()) return null;
6590
6611
  const method = isolationCapabilities.preferred;
6591
6612
  if (!method) return null;
6592
6613
  const detail = isolationCapabilities.details[method];
@@ -6603,10 +6624,10 @@ async function startDaemon(options) {
6603
6624
  trustOverride: true
6604
6625
  };
6605
6626
  }
6606
- if (sessionMetadata.sharing?.enabled && stagedCredentials) {
6627
+ if (stagedCredentials) {
6607
6628
  config.credentialStagingPath = stagedCredentials.homePath;
6608
6629
  }
6609
- const activeSecurityContext = sessionMetadata.securityContext ?? options2.securityContext;
6630
+ const activeSecurityContext = getActiveSecurityContext();
6610
6631
  if (activeSecurityContext) {
6611
6632
  config = applySecurityContext(config, activeSecurityContext);
6612
6633
  }
@@ -6743,6 +6764,12 @@ async function startDaemon(options) {
6743
6764
  "auto-approve-all": "bypassPermissions"
6744
6765
  };
6745
6766
  const toClaudePermissionMode = (mode) => CLAUDE_PERMISSION_MODE_MAP[mode] || mode;
6767
+ const getActiveSecurityContext = () => sessionMetadata.securityContext ?? options2.securityContext;
6768
+ const shouldIsolateSession = () => shouldIsolate({
6769
+ forceIsolation: options2.forceIsolation,
6770
+ sessionSecurityContext: sessionMetadata.securityContext,
6771
+ optionsSecurityContext: options2.securityContext
6772
+ });
6746
6773
  let isolationCleanupFiles = [];
6747
6774
  const spawnClaude = (initialMessage, meta) => {
6748
6775
  const effectiveMeta = { ...lastSpawnMeta, ...meta };
@@ -6786,7 +6813,7 @@ async function startDaemon(options) {
6786
6813
  if (wrapped.cleanupFiles) isolationCleanupFiles = wrapped.cleanupFiles;
6787
6814
  sessionMetadata = { ...sessionMetadata, isolationMethod: isoConfig.method };
6788
6815
  logger.log(`[Session ${sessionId}] Isolation: ${isoConfig.method} (binary: ${isoConfig.binaryPath})`);
6789
- } else if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
6816
+ } else if (shouldIsolateSession()) {
6790
6817
  logger.log(`[Session ${sessionId}] WARNING: No isolation runtime (nono/docker/podman) available. Session is NOT sandboxed.`);
6791
6818
  sessionMetadata = { ...sessionMetadata, isolationMethod: void 0 };
6792
6819
  } else {
@@ -6797,18 +6824,15 @@ async function startDaemon(options) {
6797
6824
  delete spawnEnv.CLAUDECODE;
6798
6825
  spawnEnv.SVAMP_SESSION_ID = sessionId;
6799
6826
  delete spawnEnv.SVAMP_SANDBOXED;
6800
- if (sessionMetadata.sharing?.enabled && stagedCredentials) {
6827
+ if (isoConfig && stagedCredentials) {
6801
6828
  Object.assign(spawnEnv, stagedCredentials.env);
6802
6829
  const filtered = {};
6803
6830
  for (const [k, v] of Object.entries(spawnEnv)) {
6804
6831
  if (v !== void 0) filtered[k] = v;
6805
6832
  }
6806
6833
  spawnEnv = sanitizeEnvForSharing(filtered);
6807
- logger.log(`[Session ${sessionId}] Credential staging: HOME=${stagedCredentials.homePath}`);
6808
- if (sessionMetadata.securityContext) {
6809
- spawnEnv.SVAMP_SANDBOXED = "1";
6810
- logger.log(`[Session ${sessionId}] Sandbox mode: ON (securityContext present)`);
6811
- }
6834
+ spawnEnv.SVAMP_SANDBOXED = "1";
6835
+ logger.log(`[Session ${sessionId}] Credential staging: HOME=${stagedCredentials.homePath}, SVAMP_SANDBOXED=1`);
6812
6836
  }
6813
6837
  const child = spawn$1(spawnCommand, spawnArgs, {
6814
6838
  cwd: directory,
@@ -7419,6 +7443,14 @@ The automated loop has finished. Review the progress above and let me know if yo
7419
7443
  return { success: false, message: "Session was stopped during restart." };
7420
7444
  }
7421
7445
  if (claudeResumeId) {
7446
+ if (!stagedCredentials && shouldIsolateSession()) {
7447
+ try {
7448
+ stagedCredentials = await stageCredentialsForSharing(sessionId);
7449
+ logger.log(`[Session ${sessionId}] Credentials staged at ${stagedCredentials.homePath} (runtime enable)`);
7450
+ } catch (err) {
7451
+ logger.log(`[Session ${sessionId}] WARNING: Runtime credential staging failed: ${err.message}.`);
7452
+ }
7453
+ }
7422
7454
  spawnClaude(void 0, { permissionMode: currentPermissionMode });
7423
7455
  sessionService.updateMetadata(sessionMetadata);
7424
7456
  logger.log(`[Session ${sessionId}] Claude respawned with --resume ${claudeResumeId}`);
@@ -7435,12 +7467,12 @@ The automated loop has finished. Review the progress above and let me know if yo
7435
7467
  isRestartingClaude = false;
7436
7468
  }
7437
7469
  };
7438
- if (sessionMetadata.sharing?.enabled) {
7470
+ if (shouldIsolateSession()) {
7439
7471
  try {
7440
7472
  stagedCredentials = await stageCredentialsForSharing(sessionId);
7441
7473
  logger.log(`[Session ${sessionId}] Credentials staged at ${stagedCredentials.homePath}`);
7442
7474
  } catch (err) {
7443
- logger.log(`[Session ${sessionId}] WARNING: Credential staging failed: ${err.message}. Shared users may have access to ~/.claude history.`);
7475
+ logger.log(`[Session ${sessionId}] WARNING: Credential staging failed: ${err.message}.`);
7444
7476
  }
7445
7477
  }
7446
7478
  let processMessageQueueRef;
@@ -8357,7 +8389,8 @@ The automated loop has finished. Review the progress above and let me know if yo
8357
8389
  const writeSvampConfigPatchAcp = svampConfigChecker.writeConfig;
8358
8390
  const permissionHandler = new HyphaPermissionHandler(shouldAutoAllow2, logger.log);
8359
8391
  let agentIsoConfig;
8360
- if ((options2.forceIsolation || sessionMetadata.sharing?.enabled) && isolationCapabilities.preferred) {
8392
+ const agentShouldIsolate = Boolean(options2.forceIsolation) || Boolean(options2.securityContext);
8393
+ if (agentShouldIsolate && isolationCapabilities.preferred) {
8361
8394
  const method = isolationCapabilities.preferred;
8362
8395
  const detail = isolationCapabilities.details[method];
8363
8396
  if (detail.found && detail.verified !== false) {
@@ -52,7 +52,7 @@ async function handleServeCommand() {
52
52
  }
53
53
  }
54
54
  async function serveAdd(args, machineId) {
55
- const { connectAndGetMachine } = await import('./commands-Cik0LIAl.mjs');
55
+ const { connectAndGetMachine } = await import('./commands-B8Q1ig7j.mjs');
56
56
  const pos = positionalArgs(args);
57
57
  const name = pos[0];
58
58
  if (!name) {
@@ -84,7 +84,7 @@ async function serveAdd(args, machineId) {
84
84
  }
85
85
  }
86
86
  async function serveRemove(args, machineId) {
87
- const { connectAndGetMachine } = await import('./commands-Cik0LIAl.mjs');
87
+ const { connectAndGetMachine } = await import('./commands-B8Q1ig7j.mjs');
88
88
  const pos = positionalArgs(args);
89
89
  const name = pos[0];
90
90
  if (!name) {
@@ -104,7 +104,7 @@ async function serveRemove(args, machineId) {
104
104
  }
105
105
  }
106
106
  async function serveList(args, machineId) {
107
- const { connectAndGetMachine } = await import('./commands-Cik0LIAl.mjs');
107
+ const { connectAndGetMachine } = await import('./commands-B8Q1ig7j.mjs');
108
108
  const all = hasFlag(args, "--all", "-a");
109
109
  const json = hasFlag(args, "--json");
110
110
  const sessionId = getFlag(args, "--session");
@@ -137,7 +137,7 @@ async function serveList(args, machineId) {
137
137
  }
138
138
  }
139
139
  async function serveInfo(machineId) {
140
- const { connectAndGetMachine } = await import('./commands-Cik0LIAl.mjs');
140
+ const { connectAndGetMachine } = await import('./commands-B8Q1ig7j.mjs');
141
141
  const { machine, server } = await connectAndGetMachine(machineId);
142
142
  try {
143
143
  const info = await machine.serveInfo();
@@ -258,13 +258,14 @@ class ServeManager {
258
258
  // auth proxy public port
259
259
  caddyPort = 0;
260
260
  // Caddy internal port
261
- frpcTunnel = null;
261
+ /** Per-mount frpc tunnel — each mount gets its own subdomain `static-<name>-<hash>`. */
262
+ mountTunnels = /* @__PURE__ */ new Map();
263
+ /** hostname (lowercased, no port) → mount name. Updated when tunnels connect. */
264
+ hostToMount = /* @__PURE__ */ new Map();
262
265
  caddy = null;
263
266
  proxyServer = null;
264
- serviceUrl = null;
265
267
  auth = null;
266
268
  persistFile;
267
- serviceName = "static-serve";
268
269
  log;
269
270
  hyphaServerUrl;
270
271
  constructor(svampHome, logger, hyphaServerUrl) {
@@ -300,19 +301,31 @@ class ServeManager {
300
301
  if (this.caddy?.isRunning) {
301
302
  await this.caddy.addMount(name, resolvedDir);
302
303
  }
304
+ await this.startMountTunnel(name);
303
305
  this.persist();
304
306
  const url = this.getMountUrl(name);
305
- this.log(`Mount added: ${name} \u2192 ${resolvedDir} (${url})`);
306
- return { url, mount };
307
+ this.log(`Mount added: ${name} \u2192 ${resolvedDir} (${url ?? "tunnel pending"})`);
308
+ return { url: url || "", mount };
307
309
  }
308
310
  /**
309
- * Remove a mount. If no mounts remain, stop Caddy + tunnel.
311
+ * Remove a mount. If no mounts remain, stop Caddy + auth proxy.
310
312
  */
311
313
  async removeMount(name) {
312
314
  if (!this.mounts.has(name)) {
313
315
  throw new Error(`Mount '${name}' not found`);
314
316
  }
315
317
  this.mounts.delete(name);
318
+ const tunnel = this.mountTunnels.get(name);
319
+ if (tunnel) {
320
+ try {
321
+ tunnel.destroy();
322
+ } catch {
323
+ }
324
+ this.mountTunnels.delete(name);
325
+ }
326
+ for (const [host, mountName] of this.hostToMount.entries()) {
327
+ if (mountName === name) this.hostToMount.delete(host);
328
+ }
316
329
  if (this.caddy?.isRunning) {
317
330
  try {
318
331
  await this.caddy.removeMount(name);
@@ -337,16 +350,21 @@ class ServeManager {
337
350
  return all;
338
351
  }
339
352
  /**
340
- * Get server info (URL, port, running state, mount count).
353
+ * Get server info each mount has its own URL (per-mount subdomain).
341
354
  */
342
355
  getInfo() {
343
356
  const running = this.caddy?.isRunning ?? false;
357
+ const firstMount = this.mounts.values().next().value;
358
+ const firstUrl = firstMount ? this.getMountUrl(firstMount.name) : null;
344
359
  return {
345
- url: this.serviceUrl,
360
+ url: firstUrl,
346
361
  port: running ? this.caddy?.port ?? this.port : 0,
347
362
  running,
348
363
  mountCount: this.mounts.size,
349
- mounts: Array.from(this.mounts.values())
364
+ mounts: Array.from(this.mounts.values()).map((m) => ({
365
+ ...m,
366
+ url: this.getMountUrl(m.name) || void 0
367
+ }))
350
368
  };
351
369
  }
352
370
  /**
@@ -370,6 +388,13 @@ class ServeManager {
370
388
  if (restoredCount > 0) {
371
389
  this.log(`Restoring ${restoredCount} mount(s)...`);
372
390
  await this.ensureRunning();
391
+ for (const m of this.mounts.values()) {
392
+ try {
393
+ await this.startMountTunnel(m.name);
394
+ } catch (err) {
395
+ this.log(`Failed to start tunnel for restored mount '${m.name}': ${err.message}`);
396
+ }
397
+ }
373
398
  this.persist();
374
399
  }
375
400
  } catch (err) {
@@ -377,14 +402,18 @@ class ServeManager {
377
402
  }
378
403
  }
379
404
  /**
380
- * Shut down auth proxy + Caddy + frpc tunnel.
405
+ * Shut down auth proxy + Caddy + all per-mount frpc tunnels.
381
406
  */
382
407
  async shutdown() {
383
- if (this.frpcTunnel) {
384
- this.frpcTunnel.destroy();
385
- this.frpcTunnel = null;
386
- this.log("frpc tunnel stopped");
408
+ for (const [name, tunnel] of this.mountTunnels.entries()) {
409
+ try {
410
+ tunnel.destroy();
411
+ } catch {
412
+ }
413
+ this.log(`frpc tunnel for '${name}' stopped`);
387
414
  }
415
+ this.mountTunnels.clear();
416
+ this.hostToMount.clear();
388
417
  if (this.proxyServer) {
389
418
  await new Promise((resolve) => this.proxyServer.close(() => resolve()));
390
419
  this.proxyServer = null;
@@ -395,18 +424,19 @@ class ServeManager {
395
424
  this.log("Caddy stopped");
396
425
  }
397
426
  this.auth?.destroy();
398
- this.serviceUrl = null;
399
427
  }
400
428
  // ── Internal ─────────────────────────────────────────────────────────
429
+ /** Get the public URL for a mount (mount-specific subdomain). */
401
430
  getMountUrl(name) {
402
- const base = this.serviceUrl || `http://127.0.0.1:${this.port}`;
403
- return `${base}/${name}/`;
431
+ const tunnel = this.mountTunnels.get(name);
432
+ const url = tunnel?.getUrls().get(this.port);
433
+ if (url) return `${url}/`;
434
+ if (this.port) return `http://127.0.0.1:${this.port}/${name}/`;
435
+ return null;
404
436
  }
405
437
  persist() {
406
438
  const state = {
407
- mounts: Array.from(this.mounts.values()),
408
- serviceName: this.serviceName,
409
- serviceUrl: this.serviceUrl
439
+ mounts: Array.from(this.mounts.values())
410
440
  };
411
441
  try {
412
442
  fs.writeFileSync(this.persistFile, JSON.stringify(state, null, 2));
@@ -414,7 +444,7 @@ class ServeManager {
414
444
  this.log(`Error persisting serve state: ${err.message}`);
415
445
  }
416
446
  }
417
- /** Start auth proxy + Caddy + frpc tunnel if not already running. */
447
+ /** Start auth proxy + Caddy if not already running. Per-mount tunnels are started separately. */
418
448
  async ensureRunning() {
419
449
  if (this.caddy?.isRunning) return;
420
450
  this.caddyPort = await findFreePort();
@@ -434,14 +464,27 @@ class ServeManager {
434
464
  this.log(`Caddy file server started on 127.0.0.1:${this.caddyPort}`);
435
465
  await this.startAuthProxy();
436
466
  this.log(`Auth proxy started on 127.0.0.1:${this.port}`);
437
- await this.ensureTunnel();
438
467
  }
439
468
  /** Start a lightweight Node.js HTTP proxy with auth + upload support. */
440
469
  startAuthProxy() {
441
470
  return new Promise((resolve, reject) => {
442
471
  const server = http.createServer(async (req, res) => {
443
472
  const url = new URL(req.url || "/", `http://127.0.0.1:${this.port}`);
444
- if (url.pathname === "/__login__") {
473
+ const incomingHost = (req.headers.host || "").split(":")[0].toLowerCase();
474
+ const hostMount = this.hostToMount.get(incomingHost);
475
+ let mountName;
476
+ let mountResolvedByHost = false;
477
+ let basePath;
478
+ if (hostMount && this.mounts.has(hostMount)) {
479
+ mountName = hostMount;
480
+ mountResolvedByHost = true;
481
+ basePath = url.pathname;
482
+ } else {
483
+ mountName = url.pathname.split("/").filter(Boolean)[0];
484
+ basePath = mountName ? url.pathname.slice(`/${mountName}`.length) || "/" : url.pathname;
485
+ }
486
+ const mount = mountName ? this.mounts.get(mountName) : void 0;
487
+ if (basePath === "/__login__" || url.pathname === "/__login__") {
445
488
  const returnUrl = url.searchParams.get("return") || "/";
446
489
  const safeReturn = returnUrl.startsWith("/__login__") ? "/" : returnUrl;
447
490
  const html = this.auth ? this.auth.getLoginPageHtml(safeReturn) : "<h1>Auth not configured</h1>";
@@ -452,8 +495,6 @@ class ServeManager {
452
495
  res.end(html);
453
496
  return;
454
497
  }
455
- const mountName = url.pathname.split("/").filter(Boolean)[0];
456
- const mount = mountName ? this.mounts.get(mountName) : void 0;
457
498
  if (mount && mount.access !== "public") {
458
499
  const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
459
500
  const allowed = this.auth ? this.auth.isAuthorized(userEmail, mount.access, mount.ownerEmail) : false;
@@ -469,8 +510,7 @@ class ServeManager {
469
510
  }
470
511
  }
471
512
  if (req.method === "PUT" && mount) {
472
- const subPath = url.pathname.slice(`/${mountName}`.length) || "/";
473
- const filePath = path.join(mount.directory, subPath);
513
+ const filePath = path.join(mount.directory, basePath);
474
514
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
475
515
  const ws = fs.createWriteStream(filePath);
476
516
  req.pipe(ws);
@@ -485,8 +525,7 @@ class ServeManager {
485
525
  return;
486
526
  }
487
527
  if (req.method === "DELETE" && mount) {
488
- const subPath = url.pathname.slice(`/${mountName}`.length) || "/";
489
- const filePath = path.join(mount.directory, subPath);
528
+ const filePath = path.join(mount.directory, basePath);
490
529
  try {
491
530
  fs.unlinkSync(filePath);
492
531
  res.writeHead(204);
@@ -497,10 +536,15 @@ class ServeManager {
497
536
  }
498
537
  return;
499
538
  }
539
+ let proxyPath = req.url || "/";
540
+ if (mountResolvedByHost && mountName) {
541
+ const search = url.search || "";
542
+ proxyPath = `/${mountName}${basePath === "/" ? "/" : basePath}${search}`;
543
+ }
500
544
  const proxyReq = http.request({
501
545
  hostname: "127.0.0.1",
502
546
  port: this.caddyPort,
503
- path: req.url,
547
+ path: proxyPath,
504
548
  method: req.method,
505
549
  headers: req.headers
506
550
  }, (proxyRes) => {
@@ -520,42 +564,82 @@ class ServeManager {
520
564
  server.on("error", reject);
521
565
  });
522
566
  }
523
- /** Get frpc tunnel health status, or null if no tunnel. */
567
+ /** Get aggregate health: returns the status of the worst-failing per-mount tunnel. */
524
568
  getTunnelHealth() {
525
- return this.frpcTunnel?.status ?? null;
569
+ if (this.mountTunnels.size === 0) return null;
570
+ let worst = null;
571
+ for (const t of this.mountTunnels.values()) {
572
+ const s = t.status;
573
+ if (worst === null) {
574
+ worst = s;
575
+ continue;
576
+ }
577
+ if (s.failingDurationMs > worst.failingDurationMs) worst = s;
578
+ }
579
+ return worst;
526
580
  }
527
- /** Destroy and recreate the frpc tunnel with fresh config. */
581
+ /** Destroy and recreate all per-mount tunnels with fresh configs. */
528
582
  async recreateTunnel() {
529
- if (this.frpcTunnel) {
530
- this.log("Recreating frpc tunnel (persistent failure detected)...");
531
- this.frpcTunnel.destroy();
532
- this.frpcTunnel = null;
583
+ this.log("Recreating all per-mount frpc tunnels (persistent failure detected)...");
584
+ const names = Array.from(this.mountTunnels.keys());
585
+ for (const name of names) {
586
+ const t = this.mountTunnels.get(name);
587
+ if (t) {
588
+ try {
589
+ t.destroy();
590
+ } catch {
591
+ }
592
+ }
593
+ this.mountTunnels.delete(name);
594
+ }
595
+ this.hostToMount.clear();
596
+ for (const name of names) {
597
+ try {
598
+ await this.startMountTunnel(name);
599
+ } catch (err) {
600
+ this.log(`Failed to restart tunnel for '${name}': ${err.message}`);
601
+ }
533
602
  }
534
- await this.ensureTunnel();
535
603
  }
536
- /** Start frpc tunnel for the Caddy port. */
537
- async ensureTunnel() {
604
+ /** Start a per-mount frpc tunnel pointing the auth-proxy port to a dedicated subdomain. */
605
+ async startMountTunnel(mountName) {
606
+ if (this.mountTunnels.has(mountName)) return;
607
+ if (!this.port) throw new Error("Auth proxy not running \u2014 call ensureRunning() first");
608
+ const subdomainSafe = mountName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
609
+ const tunnelName = `static-${subdomainSafe}`;
538
610
  try {
539
611
  const { FrpcTunnel } = await import('./frpc-DzRFx60H.mjs');
540
- this.frpcTunnel = new FrpcTunnel({
541
- name: this.serviceName,
612
+ const tunnel = new FrpcTunnel({
613
+ name: tunnelName,
542
614
  ports: [this.port],
543
- onError: (err) => this.log(`frpc error: ${err.message}`),
615
+ onError: (err) => this.log(`frpc error [${mountName}]: ${err.message}`),
544
616
  onConnect: () => {
545
- const url = this.frpcTunnel?.getUrls().get(this.port);
546
- if (url) {
547
- this.serviceUrl = url;
548
- this.log(`frpc tunnel connected. URL: ${this.serviceUrl}`);
617
+ const url2 = tunnel.getUrls().get(this.port);
618
+ if (url2) {
619
+ try {
620
+ const host = new URL(url2).hostname.toLowerCase();
621
+ this.hostToMount.set(host, mountName);
622
+ } catch {
623
+ }
624
+ this.log(`frpc tunnel connected for '${mountName}'. URL: ${url2}/`);
549
625
  }
550
626
  },
551
- onDisconnect: () => this.log("frpc tunnel disconnected, will auto-reconnect...")
627
+ onDisconnect: () => this.log(`frpc tunnel for '${mountName}' disconnected, will auto-reconnect...`)
552
628
  });
553
- await this.frpcTunnel.connect();
554
- this.serviceUrl = this.frpcTunnel.getUrls().get(this.port) || null;
555
- this.log(`frpc tunnel started. URL: ${this.serviceUrl}`);
629
+ await tunnel.connect();
630
+ this.mountTunnels.set(mountName, tunnel);
631
+ const url = tunnel.getUrls().get(this.port);
632
+ if (url) {
633
+ try {
634
+ const host = new URL(url).hostname.toLowerCase();
635
+ this.hostToMount.set(host, mountName);
636
+ } catch {
637
+ }
638
+ this.log(`frpc tunnel started for '${mountName}'. URL: ${url}/`);
639
+ }
556
640
  } catch (err) {
557
- this.log(`Warning: could not expose server externally: ${err.message}`);
558
- this.log(`Server available locally at http://127.0.0.1:${this.port}`);
641
+ this.log(`Warning: could not expose mount '${mountName}' externally: ${err.message}`);
642
+ this.log(`Mount '${mountName}' available locally at http://127.0.0.1:${this.port}/${mountName}/`);
559
643
  }
560
644
  }
561
645
  }
@@ -0,0 +1,157 @@
1
+ import { existsSync, readFileSync, unlinkSync, openSync, writeSync, closeSync } from 'node:fs';
2
+ import { execFileSync } from 'node:child_process';
3
+
4
+ function acquireSupervisorLock(pidFile, pid = process.pid) {
5
+ try {
6
+ const fd = openSync(pidFile, "wx");
7
+ try {
8
+ writeSync(fd, String(pid));
9
+ } finally {
10
+ closeSync(fd);
11
+ }
12
+ return { acquired: true };
13
+ } catch (err) {
14
+ if (err?.code !== "EEXIST") throw err;
15
+ }
16
+ let existingPid = 0;
17
+ try {
18
+ existingPid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
19
+ } catch {
20
+ }
21
+ if (!existingPid || Number.isNaN(existingPid) || existingPid <= 0) {
22
+ try {
23
+ unlinkSync(pidFile);
24
+ } catch {
25
+ }
26
+ return { acquired: false, staleCleaned: true };
27
+ }
28
+ if (isPidAlive(existingPid)) {
29
+ return { acquired: false, heldBy: existingPid };
30
+ }
31
+ try {
32
+ unlinkSync(pidFile);
33
+ } catch {
34
+ }
35
+ return { acquired: false, staleCleaned: true };
36
+ }
37
+ function acquireSupervisorLockWithRetry(pidFile, pid = process.pid) {
38
+ let result = acquireSupervisorLock(pidFile, pid);
39
+ if (!result.acquired && result.staleCleaned) {
40
+ result = acquireSupervisorLock(pidFile, pid);
41
+ }
42
+ return { acquired: result.acquired, heldBy: result.heldBy };
43
+ }
44
+ function releaseSupervisorLock(pidFile, pid = process.pid) {
45
+ try {
46
+ if (!existsSync(pidFile)) return;
47
+ const content = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
48
+ if (content === pid) unlinkSync(pidFile);
49
+ } catch {
50
+ }
51
+ }
52
+ function isPidAlive(pid) {
53
+ if (!pid || Number.isNaN(pid) || pid <= 0) return false;
54
+ try {
55
+ process.kill(pid, 0);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+ function findOrphanedSyncPids(currentSupervisorPid = process.pid) {
62
+ if (process.platform === "win32") return [];
63
+ const pids = [];
64
+ try {
65
+ const output = execFileSync("pgrep", ["-af", "svamp daemon start-sync"], {
66
+ encoding: "utf-8",
67
+ timeout: 5e3
68
+ });
69
+ for (const line of output.split("\n")) {
70
+ const trimmed = line.trim();
71
+ if (!trimmed) continue;
72
+ const match = trimmed.match(/^(\d+)\s/);
73
+ if (!match) continue;
74
+ const pid = parseInt(match[1], 10);
75
+ if (Number.isNaN(pid) || pid <= 0) continue;
76
+ if (pid === process.pid) continue;
77
+ const ppid = getPpid(pid);
78
+ if (ppid === currentSupervisorPid) continue;
79
+ pids.push(pid);
80
+ }
81
+ } catch (err) {
82
+ if (err?.status !== 1) ;
83
+ }
84
+ return pids;
85
+ }
86
+ function getPpid(pid) {
87
+ if (process.platform === "win32") return 0;
88
+ try {
89
+ const out = execFileSync("ps", ["-o", "ppid=", "-p", String(pid)], {
90
+ encoding: "utf-8",
91
+ timeout: 2e3
92
+ }).trim();
93
+ const ppid = parseInt(out, 10);
94
+ return Number.isNaN(ppid) ? 0 : ppid;
95
+ } catch {
96
+ return 0;
97
+ }
98
+ }
99
+ async function killOrphanedSyncs(pids, opts = {}) {
100
+ if (pids.length === 0) return;
101
+ const log = opts.log || (() => {
102
+ });
103
+ const gracePeriodMs = opts.gracePeriodMs ?? 3e3;
104
+ log(`Killing ${pids.length} orphan sync daemon(s): ${pids.join(", ")}`);
105
+ for (const pid of pids) {
106
+ try {
107
+ process.kill(pid, "SIGTERM");
108
+ } catch {
109
+ }
110
+ }
111
+ const pollStep = 100;
112
+ const steps = Math.max(1, Math.ceil(gracePeriodMs / pollStep));
113
+ for (let i = 0; i < steps; i++) {
114
+ await new Promise((r) => setTimeout(r, pollStep));
115
+ if (pids.every((p) => !isPidAlive(p))) {
116
+ log("All orphan daemons exited cleanly");
117
+ return;
118
+ }
119
+ }
120
+ const survivors = pids.filter(isPidAlive);
121
+ if (survivors.length > 0) {
122
+ log(`Force-killing survivors: ${survivors.join(", ")}`);
123
+ for (const pid of survivors) {
124
+ try {
125
+ process.kill(pid, "SIGKILL");
126
+ } catch {
127
+ }
128
+ }
129
+ }
130
+ }
131
+ function watchParentLiveness(opts) {
132
+ if (process.env.SVAMP_SUPERVISED !== "1") {
133
+ return () => {
134
+ };
135
+ }
136
+ const supervisorPid = process.ppid;
137
+ if (!supervisorPid || supervisorPid === 1) {
138
+ return () => {
139
+ };
140
+ }
141
+ const intervalMs = opts.intervalMs ?? 5e3;
142
+ let fired = false;
143
+ const timer = setInterval(() => {
144
+ if (fired) return;
145
+ if (!isPidAlive(supervisorPid)) {
146
+ fired = true;
147
+ clearInterval(timer);
148
+ opts.onParentDeath();
149
+ }
150
+ }, intervalMs);
151
+ timer.unref?.();
152
+ return () => {
153
+ clearInterval(timer);
154
+ };
155
+ }
156
+
157
+ export { acquireSupervisorLock, acquireSupervisorLockWithRetry, findOrphanedSyncPids, getPpid, isPidAlive, killOrphanedSyncs, releaseSupervisorLock, watchParentLiveness };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.36",
3
+ "version": "0.2.38",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "build": "rm -rf dist && tsc --noEmit && pkgroll",
22
22
  "typecheck": "tsc --noEmit",
23
- "test": "npx tsx test/test-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && 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-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-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-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",
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",
@@ -1,63 +0,0 @@
1
- var name = "svamp-cli";
2
- var version = "0.2.36";
3
- var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
- var author = "Amun AI AB";
5
- var license = "SEE LICENSE IN LICENSE";
6
- var type = "module";
7
- var bin = {
8
- svamp: "./bin/svamp.mjs"
9
- };
10
- var files = [
11
- "dist",
12
- "bin"
13
- ];
14
- var main = "./dist/index.mjs";
15
- var exports$1 = {
16
- ".": "./dist/index.mjs",
17
- "./cli": "./dist/cli.mjs"
18
- };
19
- var scripts = {
20
- build: "rm -rf dist && tsc --noEmit && pkgroll",
21
- typecheck: "tsc --noEmit",
22
- test: "npx tsx test/test-authorize.mjs && npx tsx test/test-session-helpers.mjs && npx tsx test/test-cli-routing.mjs && npx tsx test/test-security-context.mjs && npx tsx test/test-ralph-loop.mjs && npx tsx test/test-message-helpers.mjs && npx tsx test/test-agent-config.mjs && npx tsx test/test-wrap-command.mjs && npx tsx test/test-credential-staging.mjs && npx tsx test/test-output-formatters.mjs && npx tsx test/test-agent-types.mjs && npx tsx test/test-transport.mjs && npx tsx test/test-session-update-handlers.mjs && npx tsx test/test-session-scanner.mjs && npx tsx test/test-hypha-client.mjs && npx tsx test/test-hook-settings.mjs && npx tsx test/test-session-service-logic.mjs && npx tsx test/test-daemon-persistence.mjs && npx tsx test/test-detect-isolation.mjs && npx tsx test/test-machine-service-logic.mjs && npx tsx test/test-interactive-helpers.mjs && npx tsx test/test-codex-backend.mjs && npx tsx test/test-acp-backend.mjs && npx tsx test/test-acp-bridge.mjs && npx tsx test/test-hook-server.mjs && npx tsx test/test-session-commands.mjs && npx tsx test/test-interactive-console.mjs && npx tsx test/test-session-messages.mjs && npx tsx test/test-skills.mjs && npx tsx test/test-agent-grouping.mjs && npx tsx test/test-ralph-loop-integration.mjs && npx tsx test/test-ralph-loop-modes.mjs && npx tsx test/test-machine-list-directory.mjs && npx tsx test/test-service-commands.mjs && npx tsx test/test-supervisor.mjs && npx tsx test/test-clear-detection.mjs && npx tsx test/test-session-consolidation.mjs && npx tsx test/test-inbox.mjs && 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:hypha": "node --no-warnings test/test-hypha-service.mjs",
24
- dev: "tsx src/cli.ts",
25
- "dev:daemon": "tsx src/cli.ts daemon start-sync",
26
- "test:e2e": "node --no-warnings test/e2e-session-tests.mjs",
27
- "test:frpc": "npx tsx test/test-frpc-e2e.mjs"
28
- };
29
- var dependencies = {
30
- "@agentclientprotocol/sdk": "^0.14.1",
31
- "@modelcontextprotocol/sdk": "^1.25.3",
32
- "hypha-rpc": "0.21.36",
33
- "node-pty": "1.2.0-beta.11",
34
- ws: "^8.18.0",
35
- yaml: "^2.8.2",
36
- zod: "^3.24.4"
37
- };
38
- var devDependencies = {
39
- "@types/node": ">=20",
40
- "@types/ws": "^8.5.14",
41
- pkgroll: "^2.14.2",
42
- tsx: "^4.20.6",
43
- typescript: "5.9.3"
44
- };
45
- var packageManager = "yarn@1.22.22";
46
- var _package = {
47
- name: name,
48
- version: version,
49
- description: description,
50
- author: author,
51
- license: license,
52
- type: type,
53
- bin: bin,
54
- files: files,
55
- main: main,
56
- exports: exports$1,
57
- scripts: scripts,
58
- dependencies: dependencies,
59
- devDependencies: devDependencies,
60
- packageManager: packageManager
61
- };
62
-
63
- export { author, bin, _package as default, dependencies, description, devDependencies, exports$1 as exports, files, license, main, name, packageManager, scripts, type, version };