svamp-cli 0.2.60 → 0.2.63

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-B0kLKYmc.mjs');
151
+ const { connectAndGetMachine } = await import('./commands-CEazgqvj.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-B0kLKYmc.mjs');
168
+ const { resolveSessionId } = await import('./commands-CEazgqvj.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-Dhliie9Z.mjs';
1
+ import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-DypcS01S.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -7,6 +7,7 @@ import 'url';
7
7
  import 'child_process';
8
8
  import 'crypto';
9
9
  import 'node:fs';
10
+ import 'util';
10
11
  import 'node:crypto';
11
12
  import 'node:path';
12
13
  import 'node:child_process';
@@ -43,7 +44,7 @@ async function main() {
43
44
  console.error(`svamp daemon restart: ${err.message || err}`);
44
45
  process.exit(1);
45
46
  }
46
- const { restartDaemon } = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.u; });
47
+ const { restartDaemon } = await import('./run-DypcS01S.mjs').then(function (n) { return n.u; });
47
48
  await restartDaemon();
48
49
  process.exit(0);
49
50
  }
@@ -279,7 +280,7 @@ async function main() {
279
280
  console.error("svamp service: Service commands are not available in sandboxed sessions.");
280
281
  process.exit(1);
281
282
  }
282
- const { handleServiceCommand } = await import('./commands-BSPSCqBa.mjs');
283
+ const { handleServiceCommand } = await import('./commands-CY-HbTVN.mjs');
283
284
  await handleServiceCommand();
284
285
  } else if (subcommand === "serve") {
285
286
  const { isSandboxed: isSandboxedServe } = await import('./sandboxDetect-DNTcbgWD.mjs');
@@ -287,7 +288,7 @@ async function main() {
287
288
  console.error("svamp serve: Serve commands are not available in sandboxed sessions.");
288
289
  process.exit(1);
289
290
  }
290
- const { handleServeCommand } = await import('./serveCommands-CMSmcjsp.mjs');
291
+ const { handleServeCommand } = await import('./serveCommands-CtAX-CMI.mjs');
291
292
  await handleServeCommand();
292
293
  process.exit(0);
293
294
  } else if (subcommand === "process" || subcommand === "proc") {
@@ -296,7 +297,7 @@ async function main() {
296
297
  console.error("svamp process: Process commands are not available in sandboxed sessions.");
297
298
  process.exit(1);
298
299
  }
299
- const { processCommand } = await import('./commands-qvlNefLf.mjs');
300
+ const { processCommand } = await import('./commands-BFeHwsR-.mjs');
300
301
  let machineId;
301
302
  const processArgs = args.slice(1);
302
303
  const mIdx = processArgs.findIndex((a) => a === "--machine" || a === "-m");
@@ -314,7 +315,7 @@ async function main() {
314
315
  } else if (!subcommand || subcommand === "start") {
315
316
  await handleInteractiveCommand();
316
317
  } else if (subcommand === "--version" || subcommand === "-v") {
317
- const pkg = await import('./package-B7BB3yVn.mjs').catch(() => ({ default: { version: "unknown" } }));
318
+ const pkg = await import('./package-QQovcM_B.mjs').catch(() => ({ default: { version: "unknown" } }));
318
319
  console.log(`svamp version: ${pkg.default.version}`);
319
320
  } else {
320
321
  console.error(`Unknown command: ${subcommand}`);
@@ -323,7 +324,7 @@ async function main() {
323
324
  }
324
325
  }
325
326
  async function handleInteractiveCommand() {
326
- const { runInteractive } = await import('./run-DmWqzmqX.mjs');
327
+ const { runInteractive } = await import('./run-DB2WIjmZ.mjs');
327
328
  const interactiveArgs = subcommand === "start" ? args.slice(1) : args;
328
329
  let directory = process.cwd();
329
330
  let resumeSessionId;
@@ -368,7 +369,7 @@ async function handleAgentCommand() {
368
369
  return;
369
370
  }
370
371
  if (agentArgs[0] === "list") {
371
- const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.p; });
372
+ const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-DypcS01S.mjs').then(function (n) { return n.p; });
372
373
  console.log("Known agents:");
373
374
  for (const [name, config2] of Object.entries(KNOWN_ACP_AGENTS)) {
374
375
  console.log(` ${name.padEnd(12)} ${config2.command} ${config2.args.join(" ")} (ACP)`);
@@ -380,7 +381,7 @@ async function handleAgentCommand() {
380
381
  console.log('Use "svamp agent -- <command> [args]" for a custom ACP agent.');
381
382
  return;
382
383
  }
383
- const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.p; });
384
+ const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-DypcS01S.mjs').then(function (n) { return n.p; });
384
385
  let cwd = process.cwd();
385
386
  const filteredArgs = [];
386
387
  for (let i = 0; i < agentArgs.length; i++) {
@@ -404,12 +405,12 @@ async function handleAgentCommand() {
404
405
  console.log(`Starting ${config.agentName} agent in ${cwd}...`);
405
406
  let backend;
406
407
  if (KNOWN_MCP_AGENTS[config.agentName]) {
407
- const { CodexMcpBackend } = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.q; });
408
+ const { CodexMcpBackend } = await import('./run-DypcS01S.mjs').then(function (n) { return n.q; });
408
409
  backend = new CodexMcpBackend({ cwd, log: logFn });
409
410
  } else {
410
- const { AcpBackend } = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.o; });
411
- const { GeminiTransport } = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.G; });
412
- const { DefaultTransport } = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.D; });
411
+ const { AcpBackend } = await import('./run-DypcS01S.mjs').then(function (n) { return n.o; });
412
+ const { GeminiTransport } = await import('./run-DypcS01S.mjs').then(function (n) { return n.G; });
413
+ const { DefaultTransport } = await import('./run-DypcS01S.mjs').then(function (n) { return n.D; });
413
414
  const transportHandler = config.agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(config.agentName);
414
415
  backend = new AcpBackend({
415
416
  agentName: config.agentName,
@@ -536,7 +537,7 @@ async function handleSessionCommand() {
536
537
  process.exit(1);
537
538
  }
538
539
  }
539
- const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-B0kLKYmc.mjs');
540
+ const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-CEazgqvj.mjs');
540
541
  const parseFlagStr = (flag, shortFlag) => {
541
542
  for (let i = 1; i < sessionArgs.length; i++) {
542
543
  if ((sessionArgs[i] === flag || shortFlag) && i + 1 < sessionArgs.length) {
@@ -596,7 +597,7 @@ async function handleSessionCommand() {
596
597
  allowDomain.push(sessionArgs[++i]);
597
598
  }
598
599
  }
599
- const { parseShareArg } = await import('./commands-B0kLKYmc.mjs');
600
+ const { parseShareArg } = await import('./commands-CEazgqvj.mjs');
600
601
  const shareEntries = share.map((s) => parseShareArg(s));
601
602
  await sessionSpawn(agent, dir, targetMachineId, {
602
603
  message,
@@ -682,7 +683,7 @@ async function handleSessionCommand() {
682
683
  console.error("Usage: svamp session approve <session-id> [request-id] [--json]");
683
684
  process.exit(1);
684
685
  }
685
- const { sessionApprove } = await import('./commands-B0kLKYmc.mjs');
686
+ const { sessionApprove } = await import('./commands-CEazgqvj.mjs');
686
687
  const approveReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
687
688
  await sessionApprove(sessionArgs[1], approveReqId, targetMachineId, {
688
689
  json: hasFlag("--json")
@@ -692,7 +693,7 @@ async function handleSessionCommand() {
692
693
  console.error("Usage: svamp session deny <session-id> [request-id] [--json]");
693
694
  process.exit(1);
694
695
  }
695
- const { sessionDeny } = await import('./commands-B0kLKYmc.mjs');
696
+ const { sessionDeny } = await import('./commands-CEazgqvj.mjs');
696
697
  const denyReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
697
698
  await sessionDeny(sessionArgs[1], denyReqId, targetMachineId, {
698
699
  json: hasFlag("--json")
@@ -728,7 +729,7 @@ async function handleSessionCommand() {
728
729
  console.error("Usage: svamp session set-title <title>");
729
730
  process.exit(1);
730
731
  }
731
- const { sessionSetTitle } = await import('./agentCommands-8IVIOSvH.mjs');
732
+ const { sessionSetTitle } = await import('./agentCommands-BH6CIqc7.mjs');
732
733
  await sessionSetTitle(title);
733
734
  } else if (sessionSubcommand === "set-link") {
734
735
  const url = sessionArgs[1];
@@ -737,7 +738,7 @@ async function handleSessionCommand() {
737
738
  process.exit(1);
738
739
  }
739
740
  const label = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
740
- const { sessionSetLink } = await import('./agentCommands-8IVIOSvH.mjs');
741
+ const { sessionSetLink } = await import('./agentCommands-BH6CIqc7.mjs');
741
742
  await sessionSetLink(url, label);
742
743
  } else if (sessionSubcommand === "notify") {
743
744
  const message = sessionArgs[1];
@@ -746,7 +747,7 @@ async function handleSessionCommand() {
746
747
  process.exit(1);
747
748
  }
748
749
  const level = parseFlagStr("--level") || "info";
749
- const { sessionNotify } = await import('./agentCommands-8IVIOSvH.mjs');
750
+ const { sessionNotify } = await import('./agentCommands-BH6CIqc7.mjs');
750
751
  await sessionNotify(message, level);
751
752
  } else if (sessionSubcommand === "broadcast") {
752
753
  const action = sessionArgs[1];
@@ -754,7 +755,7 @@ async function handleSessionCommand() {
754
755
  console.error("Usage: svamp session broadcast <action> [args...]\nActions: open-canvas <url> [label], close-canvas, toast <message>");
755
756
  process.exit(1);
756
757
  }
757
- const { sessionBroadcast } = await import('./agentCommands-8IVIOSvH.mjs');
758
+ const { sessionBroadcast } = await import('./agentCommands-BH6CIqc7.mjs');
758
759
  await sessionBroadcast(action, sessionArgs.slice(2).filter((a) => !a.startsWith("--")));
759
760
  } else if (sessionSubcommand === "inbox") {
760
761
  const inboxSubcmd = sessionArgs[1];
@@ -765,7 +766,7 @@ async function handleSessionCommand() {
765
766
  process.exit(1);
766
767
  }
767
768
  if (agentSessionId) {
768
- const { inboxSend } = await import('./agentCommands-8IVIOSvH.mjs');
769
+ const { inboxSend } = await import('./agentCommands-BH6CIqc7.mjs');
769
770
  await inboxSend(sessionArgs[2], {
770
771
  body: sessionArgs[3],
771
772
  subject: parseFlagStr("--subject"),
@@ -780,7 +781,7 @@ async function handleSessionCommand() {
780
781
  }
781
782
  } else if (inboxSubcmd === "list" || inboxSubcmd === "ls") {
782
783
  if (agentSessionId && !sessionArgs[2]) {
783
- const { inboxList } = await import('./agentCommands-8IVIOSvH.mjs');
784
+ const { inboxList } = await import('./agentCommands-BH6CIqc7.mjs');
784
785
  await inboxList({
785
786
  unread: hasFlag("--unread"),
786
787
  limit: parseFlagInt("--limit"),
@@ -802,7 +803,7 @@ async function handleSessionCommand() {
802
803
  process.exit(1);
803
804
  }
804
805
  if (agentSessionId && !sessionArgs[3]) {
805
- const { inboxList } = await import('./agentCommands-8IVIOSvH.mjs');
806
+ const { inboxList } = await import('./agentCommands-BH6CIqc7.mjs');
806
807
  await sessionInboxRead(agentSessionId, sessionArgs[2], targetMachineId);
807
808
  } else if (sessionArgs[3]) {
808
809
  await sessionInboxRead(sessionArgs[2], sessionArgs[3], targetMachineId);
@@ -812,7 +813,7 @@ async function handleSessionCommand() {
812
813
  }
813
814
  } else if (inboxSubcmd === "reply") {
814
815
  if (agentSessionId && sessionArgs[2] && sessionArgs[3] && !sessionArgs[4]) {
815
- const { inboxReply } = await import('./agentCommands-8IVIOSvH.mjs');
816
+ const { inboxReply } = await import('./agentCommands-BH6CIqc7.mjs');
816
817
  await inboxReply(sessionArgs[2], sessionArgs[3]);
817
818
  } else if (sessionArgs[2] && sessionArgs[3] && sessionArgs[4]) {
818
819
  await sessionInboxReply(sessionArgs[2], sessionArgs[3], sessionArgs[4], targetMachineId);
@@ -848,7 +849,7 @@ async function handleMachineCommand() {
848
849
  return;
849
850
  }
850
851
  if (machineSubcommand === "share") {
851
- const { machineShare } = await import('./commands-B0kLKYmc.mjs');
852
+ const { machineShare } = await import('./commands-CEazgqvj.mjs');
852
853
  let machineId;
853
854
  const shareArgs = [];
854
855
  for (let i = 1; i < machineArgs.length; i++) {
@@ -878,7 +879,7 @@ async function handleMachineCommand() {
878
879
  }
879
880
  await machineShare(machineId, { add, remove, list, configPath, showConfig });
880
881
  } else if (machineSubcommand === "exec") {
881
- const { machineExec } = await import('./commands-B0kLKYmc.mjs');
882
+ const { machineExec } = await import('./commands-CEazgqvj.mjs');
882
883
  let machineId;
883
884
  let cwd;
884
885
  const cmdParts = [];
@@ -898,7 +899,7 @@ async function handleMachineCommand() {
898
899
  }
899
900
  await machineExec(machineId, command, cwd);
900
901
  } else if (machineSubcommand === "info") {
901
- const { machineInfo } = await import('./commands-B0kLKYmc.mjs');
902
+ const { machineInfo } = await import('./commands-CEazgqvj.mjs');
902
903
  let machineId;
903
904
  for (let i = 1; i < machineArgs.length; i++) {
904
905
  if ((machineArgs[i] === "--machine" || machineArgs[i] === "-m") && i + 1 < machineArgs.length) {
@@ -918,10 +919,10 @@ async function handleMachineCommand() {
918
919
  level = machineArgs[++i];
919
920
  }
920
921
  }
921
- const { machineNotify } = await import('./agentCommands-8IVIOSvH.mjs');
922
+ const { machineNotify } = await import('./agentCommands-BH6CIqc7.mjs');
922
923
  await machineNotify(message, level);
923
924
  } else if (machineSubcommand === "ls") {
924
- const { machineLs } = await import('./commands-B0kLKYmc.mjs');
925
+ const { machineLs } = await import('./commands-CEazgqvj.mjs');
925
926
  let machineId;
926
927
  let showHidden = false;
927
928
  let path;
@@ -1388,7 +1389,7 @@ async function applyClaudeAuthFlags(argv) {
1388
1389
  "--use-hypha-proxy, --use-claude-login, and --anthropic-base-url/--anthropic-api-key are mutually exclusive"
1389
1390
  );
1390
1391
  }
1391
- const mod = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.t; });
1392
+ const mod = await import('./run-DypcS01S.mjs').then(function (n) { return n.t; });
1392
1393
  if (hasHypha) {
1393
1394
  mod.setClaudeAuthHyphaProxy();
1394
1395
  console.log("Claude auth configured: hypha-proxy (uses HYPHA_TOKEN live at each spawn).");
@@ -1426,7 +1427,7 @@ async function applyDaemonShareFlag(argv) {
1426
1427
  }
1427
1428
  }
1428
1429
  if (collected.length === 0) return;
1429
- const { updateEnvFile } = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.t; });
1430
+ const { updateEnvFile } = await import('./run-DypcS01S.mjs').then(function (n) { return n.t; });
1430
1431
  const seen = /* @__PURE__ */ new Set();
1431
1432
  const deduped = collected.filter((e) => {
1432
1433
  const k = e.toLowerCase();
@@ -1439,7 +1440,7 @@ async function applyDaemonShareFlag(argv) {
1439
1440
  }
1440
1441
  async function handleDaemonAuthCommand(argv) {
1441
1442
  const sub = (argv[0] || "status").toLowerCase();
1442
- const mod = await import('./run-Dhliie9Z.mjs').then(function (n) { return n.t; });
1443
+ const mod = await import('./run-DypcS01S.mjs').then(function (n) { return n.t; });
1443
1444
  if (sub === "--help" || sub === "-h" || sub === "help") {
1444
1445
  console.log(`
1445
1446
  svamp daemon auth \u2014 Configure how Claude subprocesses authenticate
@@ -1,16 +1,17 @@
1
1
  import { writeFileSync, readFileSync } from 'fs';
2
2
  import { resolve } from 'path';
3
- import { connectAndGetMachine } from './commands-B0kLKYmc.mjs';
3
+ import { connectAndGetMachine } from './commands-CEazgqvj.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-Dhliie9Z.mjs';
8
+ import './run-DypcS01S.mjs';
9
9
  import 'os';
10
10
  import 'fs/promises';
11
11
  import 'url';
12
12
  import 'child_process';
13
13
  import 'crypto';
14
+ import 'util';
14
15
  import 'node:crypto';
15
16
  import '@agentclientprotocol/sdk';
16
17
  import '@modelcontextprotocol/sdk/client/index.js';
@@ -149,6 +150,7 @@ function normalizeSpec(raw) {
149
150
  console.error("Error: spec.command is required");
150
151
  process.exit(1);
151
152
  }
153
+ const sessionId = typeof raw.sessionId === "string" ? raw.sessionId : process.env.SVAMP_SESSION_ID || void 0;
152
154
  return {
153
155
  name: raw.name,
154
156
  command: raw.command,
@@ -169,7 +171,8 @@ function normalizeSpec(raw) {
169
171
  // ttl: 0 / negative / missing all mean "no TTL, run forever". Any positive value is seconds.
170
172
  ttl: raw.ttl !== void 0 && Number(raw.ttl) > 0 ? Number(raw.ttl) : void 0,
171
173
  serviceGroup: raw.serviceGroup,
172
- ports: Array.isArray(raw.ports) ? raw.ports.map(Number) : void 0
174
+ ports: Array.isArray(raw.ports) ? raw.ports.map(Number) : void 0,
175
+ ...sessionId ? { sessionId } : {}
173
176
  };
174
177
  }
175
178
  function statusIcon(status) {
@@ -439,6 +442,7 @@ async function startCommand(args, machineId) {
439
442
  }
440
443
  env[pair.slice(0, eq)] = pair.slice(eq + 1);
441
444
  }
445
+ const sessionId = process.env.SVAMP_SESSION_ID || void 0;
442
446
  const spec = {
443
447
  name: idOrName,
444
448
  command,
@@ -449,7 +453,8 @@ async function startCommand(args, machineId) {
449
453
  maxRestarts,
450
454
  restartDelay,
451
455
  ...probe ? { probe } : {},
452
- ...ttl !== void 0 ? { ttl } : {}
456
+ ...ttl !== void 0 ? { ttl } : {},
457
+ ...sessionId ? { sessionId } : {}
453
458
  };
454
459
  const saveFile = getFlag(flagArgs, "--save");
455
460
  const { server, machine } = await connectAndGetMachine(machineId);
@@ -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 { n as normalizeAllowedUser, l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha, i as buildSessionShareUrl, j as buildMachineShareUrl } from './run-Dhliie9Z.mjs';
5
+ import { n as normalizeAllowedUser, l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha, i as buildSessionShareUrl, j as buildMachineShareUrl } from './run-DypcS01S.mjs';
6
6
  import 'os';
7
7
  import 'fs/promises';
8
8
  import 'fs';
@@ -10,6 +10,7 @@ import 'path';
10
10
  import 'url';
11
11
  import 'child_process';
12
12
  import 'crypto';
13
+ import 'util';
13
14
  import 'node:crypto';
14
15
  import '@agentclientprotocol/sdk';
15
16
  import '@modelcontextprotocol/sdk/client/index.js';
@@ -68,7 +68,7 @@ async function serviceExpose(args) {
68
68
  });
69
69
  return;
70
70
  }
71
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
71
+ const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
72
72
  const { server, machine } = await connectAndGetMachine();
73
73
  try {
74
74
  const status = await machine.tunnelStart({
@@ -132,7 +132,7 @@ async function serviceServe(args) {
132
132
  }
133
133
  async function serviceList(_args) {
134
134
  try {
135
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
135
+ const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
136
136
  const { server, machine } = await connectAndGetMachine();
137
137
  try {
138
138
  const tunnels = await machine.tunnelList({});
@@ -161,7 +161,7 @@ async function serviceDelete(args) {
161
161
  process.exit(1);
162
162
  }
163
163
  try {
164
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
164
+ const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
165
165
  const { server, machine } = await connectAndGetMachine();
166
166
  try {
167
167
  await machine.tunnelStop({ name });
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-Dhliie9Z.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-DypcS01S.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -7,6 +7,7 @@ import 'url';
7
7
  import 'child_process';
8
8
  import 'crypto';
9
9
  import 'node:fs';
10
+ import 'util';
10
11
  import 'node:crypto';
11
12
  import 'node:path';
12
13
  import 'node:child_process';
@@ -1,5 +1,5 @@
1
1
  var name = "svamp-cli";
2
- var version = "0.2.60";
2
+ var version = "0.2.63";
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 { resolve, join } from 'node:path';
4
4
  import { existsSync, readFileSync, watch } from 'node:fs';
5
- import { c as connectToHypha, a as registerSessionService, k as generateHookSettings } from './run-Dhliie9Z.mjs';
5
+ import { c as connectToHypha, a as registerSessionService, k as generateHookSettings } from './run-DypcS01S.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -13,6 +13,7 @@ import 'path';
13
13
  import 'url';
14
14
  import 'child_process';
15
15
  import 'crypto';
16
+ import 'util';
16
17
  import '@agentclientprotocol/sdk';
17
18
  import '@modelcontextprotocol/sdk/client/index.js';
18
19
  import '@modelcontextprotocol/sdk/client/stdio.js';
@@ -3,12 +3,13 @@ import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writ
3
3
  import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync as unlinkSync$1, watch, rmdirSync, readdirSync } from 'fs';
4
4
  import path__default, { join, dirname, resolve, basename } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { spawn as spawn$1, execSync as execSync$1 } from 'child_process';
6
+ import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
8
  import { existsSync, readFileSync, writeFileSync as writeFileSync$1, mkdirSync as mkdirSync$1, appendFileSync, unlinkSync } from 'node:fs';
9
+ import { promisify } from 'util';
9
10
  import { randomUUID, createHash } from 'node:crypto';
10
11
  import { join as join$1 } from 'node:path';
11
- import { spawn, execSync, execFile, execFileSync } from 'node:child_process';
12
+ import { spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
12
13
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
13
14
  import os, { homedir, platform } from 'node:os';
14
15
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
@@ -16,7 +17,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
16
17
  import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
17
18
  import { z } from 'zod';
18
19
  import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1 } from 'node:fs/promises';
19
- import { promisify } from 'node:util';
20
+ import { promisify as promisify$1 } from 'node:util';
20
21
 
21
22
  let connectToServerFn = null;
22
23
  async function getConnectToServer() {
@@ -72,9 +73,10 @@ const ROLE_HIERARCHY = {
72
73
  admin: 2
73
74
  };
74
75
  function normalizeAllowedUser(input, addedBy) {
76
+ const role = input.role && input.role in ROLE_HIERARCHY ? input.role : "admin";
75
77
  return {
76
- email: input.email.trim(),
77
- role: "admin",
78
+ email: input.email.trim().toLowerCase(),
79
+ role,
78
80
  addedAt: typeof input.addedAt === "number" ? input.addedAt : Date.now(),
79
81
  addedBy: input.addedBy ?? addedBy
80
82
  };
@@ -88,7 +90,8 @@ function resolveRoleLevel(sharing, userEmail) {
88
90
  (u) => u.email.toLowerCase() === userEmail.toLowerCase()
89
91
  );
90
92
  if (sharedUser) {
91
- level = ROLE_HIERARCHY.admin;
93
+ const storedLevel = ROLE_HIERARCHY[sharedUser.role];
94
+ level = typeof storedLevel === "number" ? storedLevel : ROLE_HIERARCHY.admin;
92
95
  }
93
96
  }
94
97
  if (sharing.publicAccess) {
@@ -302,6 +305,98 @@ function applySecurityContext(baseConfig, context) {
302
305
  return config;
303
306
  }
304
307
 
308
+ const execFileAsync$1 = promisify(execFile);
309
+ function parseEtime(s) {
310
+ if (!s) return 0;
311
+ const dayParts = s.split("-");
312
+ let dayPrefix = 0;
313
+ let rest = s;
314
+ if (dayParts.length === 2) {
315
+ dayPrefix = parseInt(dayParts[0], 10) || 0;
316
+ rest = dayParts[1];
317
+ }
318
+ const parts = rest.split(":").map((p) => parseInt(p, 10) || 0);
319
+ let total = 0;
320
+ if (parts.length === 3) total = parts[0] * 3600 + parts[1] * 60 + parts[2];
321
+ else if (parts.length === 2) total = parts[0] * 60 + parts[1];
322
+ else if (parts.length === 1) total = parts[0];
323
+ return dayPrefix * 86400 + total;
324
+ }
325
+ async function readProcessTable() {
326
+ if (process.platform === "win32") return [];
327
+ let stdout;
328
+ try {
329
+ const result = await execFileAsync$1("ps", ["-A", "-o", "pid=,ppid=,etime=,%cpu=,rss=,command="], {
330
+ maxBuffer: 8 * 1024 * 1024
331
+ // 8MB — plenty for any realistic process table
332
+ });
333
+ stdout = result.stdout;
334
+ } catch {
335
+ return [];
336
+ }
337
+ const rows = [];
338
+ for (const line of stdout.split("\n")) {
339
+ const trimmed = line.trim();
340
+ if (!trimmed) continue;
341
+ const parts = trimmed.split(/\s+/);
342
+ if (parts.length < 6) continue;
343
+ const pid = parseInt(parts[0], 10);
344
+ const ppid = parseInt(parts[1], 10);
345
+ if (!pid || isNaN(ppid)) continue;
346
+ const etimeSec = parseEtime(parts[2]);
347
+ const cpu = parseFloat(parts[3]) || 0;
348
+ const rssKb = parseInt(parts[4], 10) || 0;
349
+ const command = parts.slice(5).join(" ");
350
+ rows.push({ pid, ppid, etimeSec, cpu, rssKb, command });
351
+ }
352
+ return rows;
353
+ }
354
+ async function detectDescendants(rootPid, excludePids = /* @__PURE__ */ new Set()) {
355
+ if (!rootPid || rootPid < 1) return [];
356
+ const table = await readProcessTable();
357
+ if (table.length === 0) return [];
358
+ const byPpid = /* @__PURE__ */ new Map();
359
+ for (const row of table) {
360
+ const list = byPpid.get(row.ppid);
361
+ if (list) list.push(row);
362
+ else byPpid.set(row.ppid, [row]);
363
+ }
364
+ const descendants = [];
365
+ const visited = /* @__PURE__ */ new Set([rootPid]);
366
+ const queue = [rootPid];
367
+ const MAX_NODES = 5e3;
368
+ while (queue.length > 0 && descendants.length < MAX_NODES) {
369
+ const next = queue.shift();
370
+ const children = byPpid.get(next);
371
+ if (!children) continue;
372
+ for (const child of children) {
373
+ if (visited.has(child.pid)) continue;
374
+ visited.add(child.pid);
375
+ queue.push(child.pid);
376
+ if (!excludePids.has(child.pid)) {
377
+ descendants.push(child);
378
+ }
379
+ }
380
+ }
381
+ descendants.sort((a, b) => a.etimeSec - b.etimeSec);
382
+ return descendants;
383
+ }
384
+ async function killDescendant(rootPid, targetPid, signal = "SIGTERM") {
385
+ if (!rootPid || rootPid < 1) return { killed: false, reason: "no root pid" };
386
+ if (!targetPid || targetPid < 1) return { killed: false, reason: "invalid target pid" };
387
+ if (targetPid === rootPid) return { killed: false, reason: "cannot kill session root pid" };
388
+ const descendants = await detectDescendants(rootPid);
389
+ if (!descendants.some((d) => d.pid === targetPid)) {
390
+ return { killed: false, reason: "pid is not a descendant of this session" };
391
+ }
392
+ try {
393
+ process.kill(targetPid, signal);
394
+ return { killed: true };
395
+ } catch (err) {
396
+ return { killed: false, reason: err?.message ?? "kill failed" };
397
+ }
398
+ }
399
+
305
400
  function getParamNames(fn) {
306
401
  const src = fn.toString();
307
402
  const match = src.match(/^(?:async\s+)?(?:function\s*\w*)?\s*\(([^)]*)\)/);
@@ -375,6 +470,20 @@ async function registerMachineService(server, machineId, metadata, daemonState,
375
470
  }
376
471
  }
377
472
  };
473
+ const ROLE_RANK = { view: 1, interact: 2, admin: 3 };
474
+ const authorizeSessionAccess = async (sessionId, requiredRole, context) => {
475
+ try {
476
+ authorizeRequest(context, currentMetadata.sharing, requiredRole);
477
+ return;
478
+ } catch {
479
+ }
480
+ const rpc = handlers.getSessionRPCHandlers?.(sessionId);
481
+ if (!rpc) throw new Error(`Session '${sessionId}' not found`);
482
+ const role = await rpc.getEffectiveRole(context).catch(() => null);
483
+ if (!role || (ROLE_RANK[role] ?? 0) < ROLE_RANK[requiredRole]) {
484
+ throw new Error(`Access denied: ${requiredRole} role required on session ${sessionId}`);
485
+ }
486
+ };
378
487
  const notifyListeners = (update) => {
379
488
  const snapshot = [...listeners];
380
489
  for (let i = snapshot.length - 1; i >= 0; i--) {
@@ -737,10 +846,22 @@ async function registerMachineService(server, machineId, metadata, daemonState,
737
846
  authorizeRequest(context, currentMetadata.sharing, "view");
738
847
  return { sharing: currentMetadata.sharing || null };
739
848
  },
849
+ /** Returns the caller's effective role on this machine (null if no access). */
850
+ getEffectiveRole: async (context) => {
851
+ authorizeRequest(context, currentMetadata.sharing, "view");
852
+ const role = getEffectiveRole(context, currentMetadata.sharing);
853
+ return { role };
854
+ },
740
855
  updateSharing: async (newSharing, context) => {
741
856
  authorizeRequest(context, currentMetadata.sharing, "admin");
742
- if (currentMetadata.sharing && context?.user?.email && currentMetadata.sharing.owner && context.user.email.toLowerCase() !== currentMetadata.sharing.owner.toLowerCase()) {
743
- throw new Error("Only the machine owner can update sharing settings");
857
+ const currentOwner = currentMetadata.sharing?.owner;
858
+ const callerEmail = context?.user?.email;
859
+ const callerIsOwner = !!currentOwner && !!callerEmail && callerEmail.toLowerCase() === currentOwner.toLowerCase();
860
+ if (currentOwner && newSharing.owner && newSharing.owner.toLowerCase() !== currentOwner.toLowerCase() && !callerIsOwner) {
861
+ throw new Error("Only the machine owner can transfer ownership");
862
+ }
863
+ if (currentOwner && !newSharing.owner) {
864
+ newSharing = { ...newSharing, owner: currentOwner };
744
865
  }
745
866
  if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
746
867
  newSharing = { ...newSharing, owner: context.user.email };
@@ -1096,6 +1217,126 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1096
1217
  if (!handlers.supervisor) return [];
1097
1218
  return handlers.supervisor.getLogs(params.idOrName, params.last ?? 50);
1098
1219
  },
1220
+ // ── Session-scoped Tasks panel ───────────────────────────────────
1221
+ // Frontend-facing views that filter by sessionId and also walk the
1222
+ // OS process tree to surface unsupervised child processes (e.g.
1223
+ // `node server.js &` spawned from a bash tool call). These are
1224
+ // separate from the global `process*` RPCs above so that a shared
1225
+ // user can manage their own session's tasks without machine-level
1226
+ // access.
1227
+ /**
1228
+ * Snapshot of everything the session's Tasks panel needs to render:
1229
+ * - supervised: ProcessSpec/state filtered to this session
1230
+ * - detected: descendants of the agent PID that aren't supervised
1231
+ */
1232
+ sessionProcessList: async (params, context) => {
1233
+ await authorizeSessionAccess(params.sessionId, "view", context);
1234
+ const supervisor = handlers.supervisor;
1235
+ const all = supervisor ? supervisor.list() : [];
1236
+ const supervised = all.filter((p) => p.spec.sessionId === params.sessionId);
1237
+ const agentPid = handlers.getSessionPid?.(params.sessionId);
1238
+ let detected = [];
1239
+ if (agentPid) {
1240
+ const supervisedPids = /* @__PURE__ */ new Set();
1241
+ for (const info of all) {
1242
+ if (info.state.pid) supervisedPids.add(info.state.pid);
1243
+ }
1244
+ detected = await detectDescendants(agentPid, supervisedPids);
1245
+ }
1246
+ return { supervised, detected, agentPid };
1247
+ },
1248
+ /**
1249
+ * Cheap counts for the input-bar badge — avoids the cost of a full
1250
+ * detection walk when the panel is closed. Counts are still
1251
+ * accurate enough to indicate "is anything running for this session".
1252
+ */
1253
+ sessionProcessCounts: async (params, context) => {
1254
+ await authorizeSessionAccess(params.sessionId, "view", context);
1255
+ const supervisor = handlers.supervisor;
1256
+ const all = supervisor ? supervisor.list() : [];
1257
+ const supervised = all.filter((p) => p.spec.sessionId === params.sessionId);
1258
+ const supervisedRunning = supervised.filter((p) => p.state.status === "running" || p.state.status === "starting").length;
1259
+ const agentPid = handlers.getSessionPid?.(params.sessionId);
1260
+ let detected = 0;
1261
+ if (agentPid) {
1262
+ const supervisedPids = /* @__PURE__ */ new Set();
1263
+ for (const info of all) if (info.state.pid) supervisedPids.add(info.state.pid);
1264
+ const list = await detectDescendants(agentPid, supervisedPids);
1265
+ detected = list.length;
1266
+ }
1267
+ return { supervised: supervised.length, supervisedRunning, detected, agentPid };
1268
+ },
1269
+ /** Logs for a supervised process, gated by session ownership. */
1270
+ sessionProcessLogs: async (params, context) => {
1271
+ await authorizeSessionAccess(params.sessionId, "view", context);
1272
+ if (!handlers.supervisor) return [];
1273
+ const info = handlers.supervisor.get(params.idOrName);
1274
+ if (!info || info.spec.sessionId !== params.sessionId) return [];
1275
+ return handlers.supervisor.getLogs(params.idOrName, params.last ?? 100);
1276
+ },
1277
+ /**
1278
+ * Lifecycle actions on a session-bound supervised process.
1279
+ * `remove` requires admin; start/stop/restart require interact.
1280
+ */
1281
+ sessionProcessAction: async (params, context) => {
1282
+ const requiredRole = params.action === "remove" ? "admin" : "interact";
1283
+ await authorizeSessionAccess(params.sessionId, requiredRole, context);
1284
+ if (!handlers.supervisor) throw new Error("Process supervisor not available");
1285
+ const info = handlers.supervisor.get(params.idOrName);
1286
+ if (!info) throw new Error(`Process '${params.idOrName}' not found`);
1287
+ if (info.spec.sessionId !== params.sessionId) {
1288
+ throw new Error(`Process '${params.idOrName}' does not belong to this session`);
1289
+ }
1290
+ switch (params.action) {
1291
+ case "start":
1292
+ return handlers.supervisor.start(params.idOrName);
1293
+ case "stop":
1294
+ return handlers.supervisor.stop(params.idOrName);
1295
+ case "restart":
1296
+ return handlers.supervisor.restart(params.idOrName);
1297
+ case "remove":
1298
+ return handlers.supervisor.remove(params.idOrName);
1299
+ }
1300
+ },
1301
+ /**
1302
+ * Kill an unsupervised descendant of the agent. Verified by the
1303
+ * detection helper so callers can only target PIDs that are
1304
+ * actually children of this session.
1305
+ */
1306
+ sessionProcessKill: async (params, context) => {
1307
+ await authorizeSessionAccess(params.sessionId, "admin", context);
1308
+ const agentPid = handlers.getSessionPid?.(params.sessionId);
1309
+ if (!agentPid) return { killed: false, reason: "no agent pid for session" };
1310
+ return killDescendant(agentPid, Number(params.pid), params.signal ?? "SIGTERM");
1311
+ },
1312
+ /**
1313
+ * Promote a detected child into supervision. Kills the original
1314
+ * PID and re-spawns the same command under the supervisor, so the
1315
+ * new process is properly tracked (PID re-parenting isn't
1316
+ * achievable without ptrace).
1317
+ */
1318
+ sessionProcessAdopt: async (params, context) => {
1319
+ await authorizeSessionAccess(params.sessionId, "admin", context);
1320
+ if (!handlers.supervisor) throw new Error("Process supervisor not available");
1321
+ const agentPid = handlers.getSessionPid?.(params.sessionId);
1322
+ if (!agentPid) throw new Error("No agent pid for session");
1323
+ if (params.pid && params.pid > 0) {
1324
+ const res = await killDescendant(agentPid, Number(params.pid), "SIGTERM");
1325
+ if (!res.killed && res.reason !== "pid is not a descendant of this session") ;
1326
+ }
1327
+ const spec = {
1328
+ name: params.name,
1329
+ command: params.command,
1330
+ args: params.args ?? [],
1331
+ workdir: params.workdir ?? process.cwd(),
1332
+ keepAlive: params.keepAlive ?? false,
1333
+ maxRestarts: 0,
1334
+ restartDelay: 2,
1335
+ sessionId: params.sessionId
1336
+ };
1337
+ const result = await handlers.supervisor.apply(spec);
1338
+ return result.info;
1339
+ },
1099
1340
  // ── Service / Tunnel management ──────────────────────────────────
1100
1341
  /** List active tunnels (replaces the old agent-sandbox serviceList). */
1101
1342
  serviceList: async (context) => {
@@ -1804,8 +2045,14 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1804
2045
  },
1805
2046
  updateSharing: async (newSharing, context) => {
1806
2047
  authorizeRequest(context, metadata.sharing, "admin");
1807
- if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
1808
- throw new Error("Only the session owner can update sharing settings");
2048
+ const currentOwner = metadata.sharing?.owner;
2049
+ const callerEmail = context?.user?.email;
2050
+ const callerIsOwner = !!currentOwner && !!callerEmail && callerEmail.toLowerCase() === currentOwner.toLowerCase();
2051
+ if (currentOwner && newSharing.owner && newSharing.owner.toLowerCase() !== currentOwner.toLowerCase() && !callerIsOwner) {
2052
+ throw new Error("Only the session owner can transfer ownership");
2053
+ }
2054
+ if (currentOwner && !newSharing.owner) {
2055
+ newSharing = { ...newSharing, owner: currentOwner };
1809
2056
  }
1810
2057
  if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
1811
2058
  newSharing = { ...newSharing, owner: context.user.email };
@@ -1828,12 +2075,10 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1828
2075
  return { success: true, sharing: newSharing };
1829
2076
  },
1830
2077
  /** Update security context and restart the agent process with new rules.
1831
- * Pass '__disable__' sentinel (from frontend) or null to disable isolation entirely. */
2078
+ * Pass '__disable__' sentinel (from frontend) or null to disable isolation entirely.
2079
+ * Admin tier (owner or admin-shared user) can perform this. */
1832
2080
  updateSecurityContext: async (newSecurityContext, context) => {
1833
2081
  authorizeRequest(context, metadata.sharing, "admin");
1834
- if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
1835
- throw new Error("Only the session owner can update security context");
1836
- }
1837
2082
  if (!callbacks.onUpdateSecurityContext) {
1838
2083
  throw new Error("Security context updates are not supported for this session");
1839
2084
  }
@@ -4655,7 +4900,7 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
4655
4900
  GeminiTransport: GeminiTransport
4656
4901
  });
4657
4902
 
4658
- const execFileAsync = promisify(execFile);
4903
+ const execFileAsync = promisify$1(execFile$1);
4659
4904
  const SVAMP_TOOLS_DIR = join$1(homedir(), ".svamp", "tools");
4660
4905
  const SVAMP_BIN_DIR = join$1(SVAMP_TOOLS_DIR, "bin");
4661
4906
  async function checkCommand(command, versionArgs) {
@@ -5401,9 +5646,13 @@ class ServeAuth {
5401
5646
  }
5402
5647
  /**
5403
5648
  * Check if a user email is authorized for a mount's access level.
5649
+ * Note: 'link' mounts are gated by the auth proxy at the path-segment
5650
+ * level (capability token), not by this method — they should never reach
5651
+ * here. We treat them as deny-by-default just in case.
5404
5652
  */
5405
5653
  isAuthorized(email, access, ownerEmail) {
5406
5654
  if (access === "public") return true;
5655
+ if (access === "link") return false;
5407
5656
  if (!email) return false;
5408
5657
  if (access === "owner") {
5409
5658
  return !!ownerEmail && email.toLowerCase() === ownerEmail.toLowerCase();
@@ -7441,7 +7690,7 @@ async function startDaemon(options) {
7441
7690
  const list = loadExposedTunnels().filter((t) => t.name !== name);
7442
7691
  saveExposedTunnels(list);
7443
7692
  }
7444
- const { ServeManager } = await import('./serveManager-C9pzi-2O.mjs');
7693
+ const { ServeManager } = await import('./serveManager-G9PiLNKm.mjs');
7445
7694
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
7446
7695
  ensureAutoInstalledSkills(logger).catch(() => {
7447
7696
  });
@@ -7738,6 +7987,9 @@ async function startDaemon(options) {
7738
7987
  let isKillingClaude = false;
7739
7988
  let isRestartingClaude = false;
7740
7989
  let isSwitchingMode = false;
7990
+ const OVERLOAD_BAIL_THRESHOLD = 3;
7991
+ let consecutiveOverloadRetries = 0;
7992
+ let overloadBailedThisTurn = false;
7741
7993
  let checkSvampConfig;
7742
7994
  let cleanupSvampConfig;
7743
7995
  const VALID_CLAUDE_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
@@ -7993,13 +8245,21 @@ async function startDaemon(options) {
7993
8245
  logger.log(`[Session ${sessionId}] Startup failure detected \u2014 scheduling silent retry without --resume`);
7994
8246
  startupFailureRetryPending = true;
7995
8247
  lastErrorMessagePushed = true;
8248
+ } else if (overloadBailedThisTurn) {
8249
+ logger.log(`[Session ${sessionId}] Suppressing duplicate error \u2014 overload hint already pushed for this turn`);
8250
+ lastErrorMessagePushed = true;
7996
8251
  } else {
7997
8252
  const lower = resultText.toLowerCase();
8253
+ const isOverload = msg.api_error_status === 529 || lower.includes("529") || lower.includes("overload");
7998
8254
  const isLoginIssue = lower.includes("login") || lower.includes("logged in") || lower.includes("auth") || lower.includes("api key") || lower.includes("unauthorized");
7999
8255
  const isResumeIssue = lower.includes("tool_use.name") || lower.includes("invalid_request") || lower.includes("messages.");
8000
8256
  const isBillingIssue = lower.includes("credit") || lower.includes("balance") || lower.includes("billing") || lower.includes("quota") || lower.includes("subscription") || lower.includes("payment");
8001
8257
  let hint = "";
8002
- if (isBillingIssue) {
8258
+ if (isOverload) {
8259
+ const onHyphaProxy = (process.env.SVAMP_CLAUDE_PROXY || "").toLowerCase() === "hypha";
8260
+ const proxyHint = onHyphaProxy ? "" : "\n\u2022 Switch the daemon to the Hypha proxy (rotates across accounts on 529): `svamp daemon auth use-hypha-proxy && svamp daemon restart`";
8261
+ hint = "\n\nAnthropic returned HTTP 529 Overloaded \u2014 server-side capacity, not your account. This session may be pinned to a hot backend shard.\n\u2022 Start a fresh session (different conversation id \u2192 different shard).\n\u2022 Wait a minute and resend.\n\u2022 Check https://status.claude.com." + proxyHint;
8262
+ } else if (isBillingIssue) {
8003
8263
  hint = "\n\nCheck your Claude account credits or subscription at https://console.anthropic.com.";
8004
8264
  } else if (isLoginIssue) {
8005
8265
  checkAndRefreshOAuthToken(true, logger).then((r) => {
@@ -8287,6 +8547,8 @@ The automated loop has finished. Review the progress above and let me know if yo
8287
8547
  sessionService.pushMessage(msg, "agent");
8288
8548
  } else if (msg.type === "system" && msg.subtype === "init") {
8289
8549
  lastAssistantText = "";
8550
+ consecutiveOverloadRetries = 0;
8551
+ overloadBailedThisTurn = false;
8290
8552
  if (!userMessagePending) {
8291
8553
  turnInitiatedByUser = false;
8292
8554
  logger.log(`[Session ${sessionId}] SDK-initiated turn (likely stale task_notification)`);
@@ -8331,6 +8593,34 @@ The automated loop has finished. Review the progress above and let me know if yo
8331
8593
  sessionService.updateMetadata(sessionMetadata);
8332
8594
  }
8333
8595
  sessionService.pushMessage(msg, "session");
8596
+ } else if (msg.type === "system" && msg.subtype === "api_retry") {
8597
+ if (msg.error_status === 529) {
8598
+ consecutiveOverloadRetries++;
8599
+ if (consecutiveOverloadRetries >= OVERLOAD_BAIL_THRESHOLD && !overloadBailedThisTurn) {
8600
+ overloadBailedThisTurn = true;
8601
+ const onHyphaProxy = (process.env.SVAMP_CLAUDE_PROXY || "").toLowerCase() === "hypha";
8602
+ const proxyHint = onHyphaProxy ? "" : "\n\u2022 Switch the daemon to the Hypha proxy (which rotates across accounts on 529): `svamp daemon auth use-hypha-proxy && svamp daemon restart`";
8603
+ const hint = `Anthropic returned HTTP 529 Overloaded ${consecutiveOverloadRetries} times in a row. This is server-side capacity, not your account \u2014 typically a hot backend shard pinned to this session. Stopping early so you don't wait for the SDK's full ~4 min retry storm.
8604
+
8605
+ **Try:**
8606
+ \u2022 Start a fresh session (different conversation id \u2192 different shard).
8607
+ \u2022 Wait a minute and resend.
8608
+ \u2022 Check https://status.claude.com.` + proxyHint;
8609
+ logger.log(`[Session ${sessionId}] Bailing on ${consecutiveOverloadRetries}\xD7 consecutive 529 retries \u2014 sending interrupt`);
8610
+ sessionService.pushMessage({ type: "message", message: hint, level: "warning" }, "event");
8611
+ try {
8612
+ if (claudeProcess && !claudeProcess.killed && claudeProcess.stdin) {
8613
+ const interruptMsg = JSON.stringify({ type: "control_request", request: { type: "interrupt" } });
8614
+ claudeProcess.stdin.write(interruptMsg + "\n");
8615
+ }
8616
+ } catch (err) {
8617
+ logger.log(`[Session ${sessionId}] Failed to send overload interrupt: ${err.message}`);
8618
+ }
8619
+ }
8620
+ } else {
8621
+ consecutiveOverloadRetries = 0;
8622
+ }
8623
+ sessionService.pushMessage(msg, "agent");
8334
8624
  } else if (msg.type === "system" && msg.subtype === "task_notification" && msg.status === "completed") {
8335
8625
  backgroundTaskCount = Math.max(0, backgroundTaskCount - 1);
8336
8626
  if (backgroundTaskNames.length > 0) {
@@ -9820,6 +10110,14 @@ The automated loop has finished. Review the progress above and let me know if yo
9820
10110
  }
9821
10111
  return ids;
9822
10112
  },
10113
+ getSessionPid: (sessionId) => {
10114
+ for (const [, session] of pidToTrackedSession) {
10115
+ if (session.svampSessionId === sessionId && !session.stopped) {
10116
+ return session.childProcess?.pid;
10117
+ }
10118
+ }
10119
+ return void 0;
10120
+ },
9823
10121
  supervisor,
9824
10122
  tunnels,
9825
10123
  serveManager,
@@ -54,17 +54,20 @@ async function handleServeCommand() {
54
54
  }
55
55
  }
56
56
  async function serveAdd(args, machineId) {
57
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
58
58
  const pos = positionalArgs(args);
59
59
  const name = pos[0];
60
60
  if (!name) {
61
- console.error("Usage: svamp serve [add] <name> [directory] [--public | --access email1,email2]");
61
+ console.error("Usage: svamp serve [add] <name> [directory] [--public | --owner | --access email1,email2]");
62
+ console.error(" Default: capability URL (link mode) \u2014 anyone with the URL can view, no login required.");
62
63
  process.exit(1);
63
64
  }
64
65
  const directory = path.resolve(pos[1] || ".");
65
- let access = "owner";
66
+ let access = "link";
66
67
  if (hasFlag(args, "--public")) {
67
68
  access = "public";
69
+ } else if (hasFlag(args, "--owner")) {
70
+ access = "owner";
68
71
  } else {
69
72
  const accessFlag = getFlag(args, "--access");
70
73
  if (accessFlag) {
@@ -75,8 +78,12 @@ async function serveAdd(args, machineId) {
75
78
  try {
76
79
  const result = await machine.serveAdd({ name, directory, access });
77
80
  console.log(`Mount added: ${name} \u2192 ${directory}`);
78
- console.log(`Access: ${access === "public" ? "public" : access === "owner" ? "owner only" : access.join(", ")}`);
81
+ const accessLabel = access === "public" ? "public (no auth)" : access === "link" ? "link (capability URL \u2014 anyone with the URL can view)" : access === "owner" ? "owner only (Hypha login)" : `${access.join(", ")} (Hypha login)`;
82
+ console.log(`Access: ${accessLabel}`);
79
83
  console.log(`URL: ${result.url}`);
84
+ if (access === "link") {
85
+ console.log(`(URL embeds a long capability token; keep it secret to keep the mount private.)`);
86
+ }
80
87
  } catch (err) {
81
88
  console.error(`Error: ${err.message || err}`);
82
89
  process.exit(1);
@@ -86,7 +93,7 @@ async function serveAdd(args, machineId) {
86
93
  }
87
94
  }
88
95
  async function serveApply(args, machineId) {
89
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
96
+ const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
90
97
  const fs = await import('fs');
91
98
  const yaml = await import('yaml');
92
99
  const file = positionalArgs(args)[0];
@@ -107,7 +114,10 @@ async function serveApply(args, machineId) {
107
114
  console.error(" warmup_timeout_ms: 30000");
108
115
  console.error(" idle_timeout_sec: 600 # 0 = always running");
109
116
  console.error(" wake_on_request: true # spawn lazily on first request");
110
- console.error(" access: public # public | owner (default) | [emails]");
117
+ console.error(" access: link # link (default) | public | owner | [emails]");
118
+ console.error(" # link \u2014 capability URL, anyone with the URL can view");
119
+ console.error(" # public \u2014 fully open, short URL");
120
+ console.error(" # owner / [emails] \u2014 Hypha login required");
111
121
  process.exit(1);
112
122
  }
113
123
  if (!fs.existsSync(file)) {
@@ -150,7 +160,8 @@ async function serveApply(args, machineId) {
150
160
  directory: parsed.directory ? path.resolve(parsed.directory) : void 0,
151
161
  process: proc,
152
162
  sessionId: parsed.sessionId ?? parsed.session_id,
153
- access: parsed.access ?? "owner",
163
+ access: parsed.access ?? "link",
164
+ // default flipped from 'owner' to 'link' (capability URL)
154
165
  ownerEmail: parsed.ownerEmail ?? parsed.owner_email
155
166
  };
156
167
  const { machine, server } = await connectAndGetMachine(machineId);
@@ -171,7 +182,7 @@ async function serveApply(args, machineId) {
171
182
  }
172
183
  }
173
184
  async function serveRemove(args, machineId) {
174
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
185
+ const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
175
186
  const pos = positionalArgs(args);
176
187
  const name = pos[0];
177
188
  if (!name) {
@@ -191,7 +202,7 @@ async function serveRemove(args, machineId) {
191
202
  }
192
203
  }
193
204
  async function serveList(args, machineId) {
194
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
205
+ const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
195
206
  const all = hasFlag(args, "--all", "-a");
196
207
  const json = hasFlag(args, "--json");
197
208
  const sessionId = getFlag(args, "--session");
@@ -224,7 +235,7 @@ async function serveList(args, machineId) {
224
235
  }
225
236
  }
226
237
  async function serveInfo(machineId) {
227
- const { connectAndGetMachine } = await import('./commands-B0kLKYmc.mjs');
238
+ const { connectAndGetMachine } = await import('./commands-CEazgqvj.mjs');
228
239
  const { machine, server } = await connectAndGetMachine(machineId);
229
240
  try {
230
241
  const info = await machine.serveInfo();
@@ -258,10 +269,14 @@ Usage:
258
269
  svamp serve list [--all] [--json] List mounts (default: current session only)
259
270
  svamp serve info Show server status and URL
260
271
 
261
- Access control (default: owner only):
262
- --public Allow anyone to access (no auth)
263
- --access email1,email2 Allow specific users (comma-separated emails)
264
- (no flag) Owner only \u2014 requires Hypha login
272
+ Access tiers (default: link):
273
+ (no flag) Capability URL \u2014 anyone with the URL can view.
274
+ URL embeds a long random token (~192-bit entropy)
275
+ as the first path segment. No login required.
276
+ Best for embedding in agent artifact iframes.
277
+ --public Fully public \u2014 no auth, short URL.
278
+ --owner Hypha login required, owner-only.
279
+ --access email1,email2 Hypha login required, specific allowed emails.
265
280
 
266
281
  Options:
267
282
  -m, --machine <id> Target a specific machine
@@ -270,12 +285,13 @@ Options:
270
285
  --json Output as JSON
271
286
 
272
287
  Examples:
273
- svamp serve my-report ./output # Owner-only (default)
274
- svamp serve dashboard ./dist --public # Anyone can access
275
- svamp serve data ./csv --access a@x.com,b@y.com # Specific users
276
- svamp serve apply my-app.yaml # Declarative apply
277
- svamp serve list --all # Show all mounts
278
- svamp serve remove my-report # Stop serving
288
+ svamp serve my-report ./output # Default: capability URL
289
+ svamp serve dashboard ./dist --public # Anyone, short URL
290
+ svamp serve data ./csv --owner # Hypha login required
291
+ svamp serve data ./csv --access a@x.com,b@y.com # Specific Hypha users
292
+ svamp serve apply my-app.yaml # Declarative apply
293
+ svamp serve list --all # Show all mounts
294
+ svamp serve remove my-report # Stop serving
279
295
 
280
296
  Declarative apply (svamp serve apply <yaml>):
281
297
  name: my-app
@@ -1,14 +1,15 @@
1
1
  import { spawn } from 'child_process';
2
+ import * as crypto from 'crypto';
2
3
  import * as fs from 'fs';
3
4
  import * as http from 'http';
4
5
  import * as net from 'net';
5
6
  import * as path from 'path';
6
- import { S as ServeAuth, h as hasCookieToken } from './run-Dhliie9Z.mjs';
7
+ import { S as ServeAuth, h as hasCookieToken } from './run-DypcS01S.mjs';
7
8
  import 'os';
8
9
  import 'fs/promises';
9
10
  import 'url';
10
- import 'crypto';
11
11
  import 'node:fs';
12
+ import 'util';
12
13
  import 'node:crypto';
13
14
  import 'node:path';
14
15
  import 'node:child_process';
@@ -21,6 +22,9 @@ import 'zod';
21
22
  import 'node:fs/promises';
22
23
  import 'node:util';
23
24
 
25
+ function generateLinkToken() {
26
+ return crypto.randomBytes(16).toString("hex");
27
+ }
24
28
  function findFreePort() {
25
29
  return new Promise((resolve, reject) => {
26
30
  const srv = net.createServer();
@@ -88,7 +92,7 @@ class ServeManager {
88
92
  * Throws if a mount with the same name already exists, preserving the
89
93
  * pre-existing semantics that callers may depend on.
90
94
  */
91
- async addMount(name, directory, sessionId, access = "owner", ownerEmail) {
95
+ async addMount(name, directory, sessionId, access = "link", ownerEmail) {
92
96
  if (this.mounts.has(name)) {
93
97
  throw new Error(`Mount '${name}' already exists. Remove it first or choose a different name.`);
94
98
  }
@@ -123,13 +127,18 @@ class ServeManager {
123
127
  if (this.mounts.has(spec.name)) {
124
128
  await this.removeMount(spec.name);
125
129
  }
130
+ const access = spec.access ?? "link";
126
131
  const mount = {
127
132
  name: spec.name,
128
133
  directory: resolvedDir,
129
134
  process: spec.process,
130
135
  sessionId: spec.sessionId,
131
136
  ownerEmail: spec.ownerEmail,
132
- access: spec.access ?? "owner",
137
+ access,
138
+ // Generate a capability token if access is 'link' and we don't
139
+ // already have one from persisted state (token is stable across
140
+ // restarts).
141
+ linkToken: access === "link" ? spec.linkToken || generateLinkToken() : void 0,
133
142
  addedAt: Date.now()
134
143
  };
135
144
  this.mounts.set(spec.name, mount);
@@ -432,11 +441,18 @@ class ServeManager {
432
441
  }, 3e4);
433
442
  }
434
443
  // ── Internal ─────────────────────────────────────────────────────────
435
- /** Get the public URL for a mount (mount-specific subdomain). */
444
+ /**
445
+ * Get the public URL for a mount (mount-specific subdomain).
446
+ * For `access: 'link'` mounts, the subdomain itself carries the capability —
447
+ * the tunnel registers a long random suffix (~128 bits of entropy), so the
448
+ * URL is identical in shape to other tiers, just longer. Content serves
449
+ * from the root, so HTML with absolute paths (`/style.css`, `/app.js`)
450
+ * works unchanged.
451
+ */
436
452
  getMountUrl(name) {
437
453
  const tunnel = this.mountTunnels.get(name);
438
- const url = tunnel?.getUrls().get(this.port);
439
- if (url) return `${url}/`;
454
+ const tunnelUrl = tunnel?.getUrls().get(this.port);
455
+ if (tunnelUrl) return `${tunnelUrl}/`;
440
456
  if (this.port) return `http://127.0.0.1:${this.port}/${name}/`;
441
457
  return null;
442
458
  }
@@ -523,7 +539,7 @@ class ServeManager {
523
539
  res.end(html);
524
540
  return;
525
541
  }
526
- if (mount && mount.access !== "public") {
542
+ if (mount && mount.access !== "public" && mount.access !== "link") {
527
543
  const userEmail = this.auth ? await this.auth.authenticate(req).catch(() => null) : null;
528
544
  const allowed = this.auth ? this.auth.isAuthorized(userEmail, mount.access, mount.ownerEmail) : false;
529
545
  if (!allowed) {
@@ -668,12 +684,15 @@ class ServeManager {
668
684
  if (!this.port) throw new Error("Auth proxy not running \u2014 call ensureRunning() first");
669
685
  const subdomainSafe = mountName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
670
686
  const tunnelName = `static-${subdomainSafe}`;
687
+ const mount = this.mounts.get(mountName);
688
+ const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, `static-${subdomainSafe}-${mount.linkToken}`]]) : void 0;
671
689
  try {
672
690
  const { FrpcTunnel } = await import('./frpc-j60b46eU.mjs');
673
691
  let tunnel;
674
692
  tunnel = new FrpcTunnel({
675
693
  name: tunnelName,
676
694
  ports: [this.port],
695
+ subdomains: subdomainOverride,
677
696
  // End-to-end probe: the daemon's health loop watches probe.ok
678
697
  // to detect ghosted tunnel registrations (frpc says "connected"
679
698
  // but no traffic actually flows). The sentinel route is served
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.60",
3
+ "version": "0.2.63",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",