svamp-cli 0.2.36 → 0.2.37

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-CrTDsMDZ.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-CrTDsMDZ.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-Dz0TkCj6.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-Dz0TkCj6.mjs').then(function (n) { return n.k; });
40
40
  await restartDaemon();
41
41
  process.exit(0);
42
42
  }
@@ -246,7 +246,7 @@ async function main() {
246
246
  console.error("svamp serve: Serve commands are not available in sandboxed sessions.");
247
247
  process.exit(1);
248
248
  }
249
- const { handleServeCommand } = await import('./serveCommands-Cgq6Vif4.mjs');
249
+ const { handleServeCommand } = await import('./serveCommands-FnBtBifV.mjs');
250
250
  await handleServeCommand();
251
251
  process.exit(0);
252
252
  } else if (subcommand === "process" || subcommand === "proc") {
@@ -255,7 +255,7 @@ async function main() {
255
255
  console.error("svamp process: Process commands are not available in sandboxed sessions.");
256
256
  process.exit(1);
257
257
  }
258
- const { processCommand } = await import('./commands-B4qmyXeM.mjs');
258
+ const { processCommand } = await import('./commands-Dz_JXrcs.mjs');
259
259
  let machineId;
260
260
  const processArgs = args.slice(1);
261
261
  const mIdx = processArgs.findIndex((a) => a === "--machine" || a === "-m");
@@ -273,7 +273,7 @@ async function main() {
273
273
  } else if (!subcommand || subcommand === "start") {
274
274
  await handleInteractiveCommand();
275
275
  } else if (subcommand === "--version" || subcommand === "-v") {
276
- const pkg = await import('./package-DDUrcHYU.mjs').catch(() => ({ default: { version: "unknown" } }));
276
+ const pkg = await import('./package-DRX_LQ92.mjs').catch(() => ({ default: { version: "unknown" } }));
277
277
  console.log(`svamp version: ${pkg.default.version}`);
278
278
  } else {
279
279
  console.error(`Unknown command: ${subcommand}`);
@@ -282,7 +282,7 @@ async function main() {
282
282
  }
283
283
  }
284
284
  async function handleInteractiveCommand() {
285
- const { runInteractive } = await import('./run-sAfJWYld.mjs');
285
+ const { runInteractive } = await import('./run-BckEhiLq.mjs');
286
286
  const interactiveArgs = subcommand === "start" ? args.slice(1) : args;
287
287
  let directory = process.cwd();
288
288
  let resumeSessionId;
@@ -327,7 +327,7 @@ async function handleAgentCommand() {
327
327
  return;
328
328
  }
329
329
  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; });
330
+ const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-Dz0TkCj6.mjs').then(function (n) { return n.i; });
331
331
  console.log("Known agents:");
332
332
  for (const [name, config2] of Object.entries(KNOWN_ACP_AGENTS)) {
333
333
  console.log(` ${name.padEnd(12)} ${config2.command} ${config2.args.join(" ")} (ACP)`);
@@ -339,7 +339,7 @@ async function handleAgentCommand() {
339
339
  console.log('Use "svamp agent -- <command> [args]" for a custom ACP agent.');
340
340
  return;
341
341
  }
342
- const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-BKBv5P9v.mjs').then(function (n) { return n.i; });
342
+ const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-Dz0TkCj6.mjs').then(function (n) { return n.i; });
343
343
  let cwd = process.cwd();
344
344
  const filteredArgs = [];
345
345
  for (let i = 0; i < agentArgs.length; i++) {
@@ -363,12 +363,12 @@ async function handleAgentCommand() {
363
363
  console.log(`Starting ${config.agentName} agent in ${cwd}...`);
364
364
  let backend;
365
365
  if (KNOWN_MCP_AGENTS[config.agentName]) {
366
- const { CodexMcpBackend } = await import('./run-BKBv5P9v.mjs').then(function (n) { return n.j; });
366
+ const { CodexMcpBackend } = await import('./run-Dz0TkCj6.mjs').then(function (n) { return n.j; });
367
367
  backend = new CodexMcpBackend({ cwd, log: logFn });
368
368
  } 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; });
369
+ const { AcpBackend } = await import('./run-Dz0TkCj6.mjs').then(function (n) { return n.h; });
370
+ const { GeminiTransport } = await import('./run-Dz0TkCj6.mjs').then(function (n) { return n.G; });
371
+ const { DefaultTransport } = await import('./run-Dz0TkCj6.mjs').then(function (n) { return n.D; });
372
372
  const transportHandler = config.agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(config.agentName);
373
373
  backend = new AcpBackend({
374
374
  agentName: config.agentName,
@@ -495,7 +495,7 @@ async function handleSessionCommand() {
495
495
  process.exit(1);
496
496
  }
497
497
  }
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');
498
+ const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-CrTDsMDZ.mjs');
499
499
  const parseFlagStr = (flag, shortFlag) => {
500
500
  for (let i = 1; i < sessionArgs.length; i++) {
501
501
  if ((sessionArgs[i] === flag || shortFlag) && i + 1 < sessionArgs.length) {
@@ -555,7 +555,7 @@ async function handleSessionCommand() {
555
555
  allowDomain.push(sessionArgs[++i]);
556
556
  }
557
557
  }
558
- const { parseShareArg } = await import('./commands-Cik0LIAl.mjs');
558
+ const { parseShareArg } = await import('./commands-CrTDsMDZ.mjs');
559
559
  const shareEntries = share.map((s) => parseShareArg(s));
560
560
  await sessionSpawn(agent, dir, targetMachineId, {
561
561
  message,
@@ -641,7 +641,7 @@ async function handleSessionCommand() {
641
641
  console.error("Usage: svamp session approve <session-id> [request-id] [--json]");
642
642
  process.exit(1);
643
643
  }
644
- const { sessionApprove } = await import('./commands-Cik0LIAl.mjs');
644
+ const { sessionApprove } = await import('./commands-CrTDsMDZ.mjs');
645
645
  const approveReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
646
646
  await sessionApprove(sessionArgs[1], approveReqId, targetMachineId, {
647
647
  json: hasFlag("--json")
@@ -651,7 +651,7 @@ async function handleSessionCommand() {
651
651
  console.error("Usage: svamp session deny <session-id> [request-id] [--json]");
652
652
  process.exit(1);
653
653
  }
654
- const { sessionDeny } = await import('./commands-Cik0LIAl.mjs');
654
+ const { sessionDeny } = await import('./commands-CrTDsMDZ.mjs');
655
655
  const denyReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
656
656
  await sessionDeny(sessionArgs[1], denyReqId, targetMachineId, {
657
657
  json: hasFlag("--json")
@@ -687,7 +687,7 @@ async function handleSessionCommand() {
687
687
  console.error("Usage: svamp session set-title <title>");
688
688
  process.exit(1);
689
689
  }
690
- const { sessionSetTitle } = await import('./agentCommands-oLVbSvBZ.mjs');
690
+ const { sessionSetTitle } = await import('./agentCommands-C88l82pM.mjs');
691
691
  await sessionSetTitle(title);
692
692
  } else if (sessionSubcommand === "set-link") {
693
693
  const url = sessionArgs[1];
@@ -696,7 +696,7 @@ async function handleSessionCommand() {
696
696
  process.exit(1);
697
697
  }
698
698
  const label = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
699
- const { sessionSetLink } = await import('./agentCommands-oLVbSvBZ.mjs');
699
+ const { sessionSetLink } = await import('./agentCommands-C88l82pM.mjs');
700
700
  await sessionSetLink(url, label);
701
701
  } else if (sessionSubcommand === "notify") {
702
702
  const message = sessionArgs[1];
@@ -705,7 +705,7 @@ async function handleSessionCommand() {
705
705
  process.exit(1);
706
706
  }
707
707
  const level = parseFlagStr("--level") || "info";
708
- const { sessionNotify } = await import('./agentCommands-oLVbSvBZ.mjs');
708
+ const { sessionNotify } = await import('./agentCommands-C88l82pM.mjs');
709
709
  await sessionNotify(message, level);
710
710
  } else if (sessionSubcommand === "broadcast") {
711
711
  const action = sessionArgs[1];
@@ -713,7 +713,7 @@ async function handleSessionCommand() {
713
713
  console.error("Usage: svamp session broadcast <action> [args...]\nActions: open-canvas <url> [label], close-canvas, toast <message>");
714
714
  process.exit(1);
715
715
  }
716
- const { sessionBroadcast } = await import('./agentCommands-oLVbSvBZ.mjs');
716
+ const { sessionBroadcast } = await import('./agentCommands-C88l82pM.mjs');
717
717
  await sessionBroadcast(action, sessionArgs.slice(2).filter((a) => !a.startsWith("--")));
718
718
  } else if (sessionSubcommand === "inbox") {
719
719
  const inboxSubcmd = sessionArgs[1];
@@ -724,7 +724,7 @@ async function handleSessionCommand() {
724
724
  process.exit(1);
725
725
  }
726
726
  if (agentSessionId) {
727
- const { inboxSend } = await import('./agentCommands-oLVbSvBZ.mjs');
727
+ const { inboxSend } = await import('./agentCommands-C88l82pM.mjs');
728
728
  await inboxSend(sessionArgs[2], {
729
729
  body: sessionArgs[3],
730
730
  subject: parseFlagStr("--subject"),
@@ -739,7 +739,7 @@ async function handleSessionCommand() {
739
739
  }
740
740
  } else if (inboxSubcmd === "list" || inboxSubcmd === "ls") {
741
741
  if (agentSessionId && !sessionArgs[2]) {
742
- const { inboxList } = await import('./agentCommands-oLVbSvBZ.mjs');
742
+ const { inboxList } = await import('./agentCommands-C88l82pM.mjs');
743
743
  await inboxList({
744
744
  unread: hasFlag("--unread"),
745
745
  limit: parseFlagInt("--limit"),
@@ -761,7 +761,7 @@ async function handleSessionCommand() {
761
761
  process.exit(1);
762
762
  }
763
763
  if (agentSessionId && !sessionArgs[3]) {
764
- const { inboxList } = await import('./agentCommands-oLVbSvBZ.mjs');
764
+ const { inboxList } = await import('./agentCommands-C88l82pM.mjs');
765
765
  await sessionInboxRead(agentSessionId, sessionArgs[2], targetMachineId);
766
766
  } else if (sessionArgs[3]) {
767
767
  await sessionInboxRead(sessionArgs[2], sessionArgs[3], targetMachineId);
@@ -771,7 +771,7 @@ async function handleSessionCommand() {
771
771
  }
772
772
  } else if (inboxSubcmd === "reply") {
773
773
  if (agentSessionId && sessionArgs[2] && sessionArgs[3] && !sessionArgs[4]) {
774
- const { inboxReply } = await import('./agentCommands-oLVbSvBZ.mjs');
774
+ const { inboxReply } = await import('./agentCommands-C88l82pM.mjs');
775
775
  await inboxReply(sessionArgs[2], sessionArgs[3]);
776
776
  } else if (sessionArgs[2] && sessionArgs[3] && sessionArgs[4]) {
777
777
  await sessionInboxReply(sessionArgs[2], sessionArgs[3], sessionArgs[4], targetMachineId);
@@ -807,7 +807,7 @@ async function handleMachineCommand() {
807
807
  return;
808
808
  }
809
809
  if (machineSubcommand === "share") {
810
- const { machineShare } = await import('./commands-Cik0LIAl.mjs');
810
+ const { machineShare } = await import('./commands-CrTDsMDZ.mjs');
811
811
  let machineId;
812
812
  const shareArgs = [];
813
813
  for (let i = 1; i < machineArgs.length; i++) {
@@ -837,7 +837,7 @@ async function handleMachineCommand() {
837
837
  }
838
838
  await machineShare(machineId, { add, remove, list, configPath, showConfig });
839
839
  } else if (machineSubcommand === "exec") {
840
- const { machineExec } = await import('./commands-Cik0LIAl.mjs');
840
+ const { machineExec } = await import('./commands-CrTDsMDZ.mjs');
841
841
  let machineId;
842
842
  let cwd;
843
843
  const cmdParts = [];
@@ -857,7 +857,7 @@ async function handleMachineCommand() {
857
857
  }
858
858
  await machineExec(machineId, command, cwd);
859
859
  } else if (machineSubcommand === "info") {
860
- const { machineInfo } = await import('./commands-Cik0LIAl.mjs');
860
+ const { machineInfo } = await import('./commands-CrTDsMDZ.mjs');
861
861
  let machineId;
862
862
  for (let i = 1; i < machineArgs.length; i++) {
863
863
  if ((machineArgs[i] === "--machine" || machineArgs[i] === "-m") && i + 1 < machineArgs.length) {
@@ -877,10 +877,10 @@ async function handleMachineCommand() {
877
877
  level = machineArgs[++i];
878
878
  }
879
879
  }
880
- const { machineNotify } = await import('./agentCommands-oLVbSvBZ.mjs');
880
+ const { machineNotify } = await import('./agentCommands-C88l82pM.mjs');
881
881
  await machineNotify(message, level);
882
882
  } else if (machineSubcommand === "ls") {
883
- const { machineLs } = await import('./commands-Cik0LIAl.mjs');
883
+ const { machineLs } = await import('./commands-CrTDsMDZ.mjs');
884
884
  let machineId;
885
885
  let showHidden = false;
886
886
  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-Dz0TkCj6.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-CrTDsMDZ.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-Dz0TkCj6.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-Dz0TkCj6.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -1,5 +1,5 @@
1
1
  var name = "svamp-cli";
2
- var version = "0.2.36";
2
+ var version = "0.2.37";
3
3
  var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
4
  var author = "Amun AI AB";
5
5
  var license = "SEE LICENSE IN LICENSE";
@@ -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-Dz0TkCj6.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -6440,7 +6440,7 @@ async function startDaemon(options) {
6440
6440
  const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
6441
6441
  await supervisor.init();
6442
6442
  const tunnels = /* @__PURE__ */ new Map();
6443
- const { ServeManager } = await import('./serveManager-XPnOLqQ4.mjs');
6443
+ const { ServeManager } = await import('./serveManager-MqY_0frw.mjs');
6444
6444
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
6445
6445
  ensureAutoInstalledSkills(logger).catch(() => {
6446
6446
  });
@@ -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-CrTDsMDZ.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-CrTDsMDZ.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-CrTDsMDZ.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-CrTDsMDZ.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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.36",
3
+ "version": "0.2.37",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",