svamp-cli 0.2.24 → 0.2.25

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-CEiWVChH.mjs');
151
+ const { connectAndGetMachine } = await import('./commands-DRpGTxYD.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-CEiWVChH.mjs');
168
+ const { resolveSessionId } = await import('./commands-DRpGTxYD.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-Cq-vqhYX.mjs';
1
+ import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-BTlUAfV3.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-Cq-vqhYX.mjs').then(function (n) { return n.k; });
39
+ const { restartDaemon } = await import('./run-BTlUAfV3.mjs').then(function (n) { return n.k; });
40
40
  await restartDaemon();
41
41
  process.exit(0);
42
42
  }
@@ -238,7 +238,7 @@ async function main() {
238
238
  console.error("svamp service: Service commands are not available in sandboxed sessions.");
239
239
  process.exit(1);
240
240
  }
241
- const { handleServiceCommand } = await import('./commands-ybRzIOIu.mjs');
241
+ const { handleServiceCommand } = await import('./commands-DV2tFSXm.mjs');
242
242
  await handleServiceCommand();
243
243
  } else if (subcommand === "serve") {
244
244
  const { isSandboxed: isSandboxedServe } = await import('./sandboxDetect-DNTcbgWD.mjs');
@@ -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-Q6dCUe4U.mjs');
249
+ const { handleServeCommand } = await import('./serveCommands-Ci4DE-iQ.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-BjHspJLZ.mjs');
258
+ const { processCommand } = await import('./commands-ibBP43SF.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-BPrzOhTb.mjs').catch(() => ({ default: { version: "unknown" } }));
276
+ const pkg = await import('./package-BKGqkCGK.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-BZDgOpT5.mjs');
285
+ const { runInteractive } = await import('./run-DlcgeE63.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-Cq-vqhYX.mjs').then(function (n) { return n.i; });
330
+ const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-BTlUAfV3.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-Cq-vqhYX.mjs').then(function (n) { return n.i; });
342
+ const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-BTlUAfV3.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-Cq-vqhYX.mjs').then(function (n) { return n.j; });
366
+ const { CodexMcpBackend } = await import('./run-BTlUAfV3.mjs').then(function (n) { return n.j; });
367
367
  backend = new CodexMcpBackend({ cwd, log: logFn });
368
368
  } else {
369
- const { AcpBackend } = await import('./run-Cq-vqhYX.mjs').then(function (n) { return n.h; });
370
- const { GeminiTransport } = await import('./run-Cq-vqhYX.mjs').then(function (n) { return n.G; });
371
- const { DefaultTransport } = await import('./run-Cq-vqhYX.mjs').then(function (n) { return n.D; });
369
+ const { AcpBackend } = await import('./run-BTlUAfV3.mjs').then(function (n) { return n.h; });
370
+ const { GeminiTransport } = await import('./run-BTlUAfV3.mjs').then(function (n) { return n.G; });
371
+ const { DefaultTransport } = await import('./run-BTlUAfV3.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-CEiWVChH.mjs');
498
+ const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-DRpGTxYD.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-CEiWVChH.mjs');
558
+ const { parseShareArg } = await import('./commands-DRpGTxYD.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-CEiWVChH.mjs');
644
+ const { sessionApprove } = await import('./commands-DRpGTxYD.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-CEiWVChH.mjs');
654
+ const { sessionDeny } = await import('./commands-DRpGTxYD.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-XQqXEOXO.mjs');
690
+ const { sessionSetTitle } = await import('./agentCommands-MiAH1S9e.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-XQqXEOXO.mjs');
699
+ const { sessionSetLink } = await import('./agentCommands-MiAH1S9e.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-XQqXEOXO.mjs');
708
+ const { sessionNotify } = await import('./agentCommands-MiAH1S9e.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-XQqXEOXO.mjs');
716
+ const { sessionBroadcast } = await import('./agentCommands-MiAH1S9e.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-XQqXEOXO.mjs');
727
+ const { inboxSend } = await import('./agentCommands-MiAH1S9e.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-XQqXEOXO.mjs');
742
+ const { inboxList } = await import('./agentCommands-MiAH1S9e.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-XQqXEOXO.mjs');
764
+ const { inboxList } = await import('./agentCommands-MiAH1S9e.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-XQqXEOXO.mjs');
774
+ const { inboxReply } = await import('./agentCommands-MiAH1S9e.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-CEiWVChH.mjs');
810
+ const { machineShare } = await import('./commands-DRpGTxYD.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-CEiWVChH.mjs');
840
+ const { machineExec } = await import('./commands-DRpGTxYD.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-CEiWVChH.mjs');
860
+ const { machineInfo } = await import('./commands-DRpGTxYD.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-XQqXEOXO.mjs');
880
+ const { machineNotify } = await import('./agentCommands-MiAH1S9e.mjs');
881
881
  await machineNotify(message, level);
882
882
  } else if (machineSubcommand === "ls") {
883
- const { machineLs } = await import('./commands-CEiWVChH.mjs');
883
+ const { machineLs } = await import('./commands-DRpGTxYD.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-Cq-vqhYX.mjs';
5
+ import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-BTlUAfV3.mjs';
6
6
  import 'os';
7
7
  import 'fs/promises';
8
8
  import 'fs';
@@ -21,9 +21,7 @@ function positionalArgs(args) {
21
21
  const result = [];
22
22
  for (let i = 0; i < args.length; i++) {
23
23
  if (args[i].startsWith("--")) {
24
- if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
25
- i++;
26
- }
24
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) i++;
27
25
  continue;
28
26
  }
29
27
  result.push(args[i]);
@@ -57,7 +55,7 @@ async function serviceExpose(args) {
57
55
  console.error("Usage: svamp service expose <name> --port <port> [--port <port2>] [options]");
58
56
  process.exit(1);
59
57
  }
60
- const { runFrpcTunnel } = await import('./frpc-C9Qs-5jQ.mjs');
58
+ const { runFrpcTunnel } = await import('./frpc-BhS2e6r-.mjs');
61
59
  await runFrpcTunnel(name, ports, void 0, {
62
60
  group,
63
61
  groupKey,
@@ -90,17 +88,14 @@ async function serviceServe(args) {
90
88
  };
91
89
  process.on("SIGINT", cleanup);
92
90
  process.on("SIGTERM", cleanup);
93
- const { runFrpcTunnel } = await import('./frpc-C9Qs-5jQ.mjs');
91
+ const { runFrpcTunnel } = await import('./frpc-BhS2e6r-.mjs');
94
92
  await runFrpcTunnel(name, [caddyPort]);
95
93
  } catch (err) {
96
94
  console.error(`Error serving directory: ${err.message}`);
97
95
  process.exit(1);
98
96
  }
99
97
  }
100
- async function serviceTunnel(args) {
101
- await serviceExpose(args);
102
- }
103
- async function serviceList(args) {
98
+ async function serviceList(_args) {
104
99
  console.log("Active tunnels are managed by the svamp daemon.");
105
100
  console.log('Use "svamp service expose" to start a tunnel in the foreground.');
106
101
  }
@@ -122,12 +117,10 @@ async function handleServiceCommand() {
122
117
  return;
123
118
  }
124
119
  const commandArgs = serviceArgs.slice(1);
125
- if (sub === "expose") {
120
+ if (sub === "expose" || sub === "tunnel") {
126
121
  await serviceExpose(commandArgs);
127
122
  } else if (sub === "serve") {
128
123
  await serviceServe(commandArgs);
129
- } else if (sub === "tunnel") {
130
- await serviceTunnel(commandArgs);
131
124
  } else if (sub === "list" || sub === "ls") {
132
125
  await serviceList();
133
126
  } else if (sub === "delete" || sub === "rm") {
@@ -145,7 +138,6 @@ svamp service \u2014 Expose local services via frpc tunnels
145
138
  Usage:
146
139
  svamp service expose <name> --port <port> [options] Expose local ports via tunnel
147
140
  svamp service serve <name> [directory] [--no-listing] Serve static files via tunnel
148
- svamp service tunnel <name> --port <port> [options] Alias for expose
149
141
  svamp service list List active tunnels (daemon)
150
142
  svamp service delete <name> Stop a tunnel (daemon)
151
143
 
@@ -160,10 +152,9 @@ Options:
160
152
  Service Groups:
161
153
  Run the same command on different machines with --group to create a
162
154
  load-balanced service. frps round-robins traffic across all group members.
163
- Unhealthy backends are auto-removed when --health-check is enabled.
164
155
 
165
- Machine A: svamp service expose my-api --port 8000 --group my-api --group-key secret123
166
- Machine B: svamp service expose my-api --port 8000 --group my-api --group-key secret123
156
+ Machine A: svamp service expose my-api --port 8000 --group my-api --group-key s3cret
157
+ Machine B: svamp service expose my-api --port 8000 --group my-api --group-key s3cret
167
158
  \u2192 Both serve at the same URL, frps load-balances between them
168
159
 
169
160
  Examples:
@@ -172,8 +163,7 @@ Examples:
172
163
  svamp service expose my-api --port 8000 --port 3000
173
164
  svamp service expose my-api --port 8000 --group my-api --group-key s3cret
174
165
  svamp service serve my-site ./dist
175
- svamp service serve my-site
176
166
  `.trim());
177
167
  }
178
168
 
179
- export { handleServiceCommand, serviceDelete, serviceExpose, serviceList, serviceServe, serviceTunnel };
169
+ export { handleServiceCommand, serviceDelete, serviceExpose, serviceList, serviceServe };
@@ -1,11 +1,11 @@
1
1
  import { writeFileSync, readFileSync } from 'fs';
2
2
  import { resolve } from 'path';
3
- import { connectAndGetMachine } from './commands-CEiWVChH.mjs';
3
+ import { connectAndGetMachine } from './commands-DRpGTxYD.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-Cq-vqhYX.mjs';
8
+ import './run-BTlUAfV3.mjs';
9
9
  import 'os';
10
10
  import 'fs/promises';
11
11
  import 'url';
@@ -4,7 +4,7 @@ import { join } from 'path';
4
4
  import { homedir, platform, arch } from 'os';
5
5
  import { createHash } from 'crypto';
6
6
 
7
- const FRP_VERSION = "0.61.1";
7
+ const FRP_VERSION = "0.68.0";
8
8
  const BIN_DIR = join(homedir(), ".svamp", "bin");
9
9
  const FRPC_BIN = join(BIN_DIR, platform() === "win32" ? "frpc.exe" : "frpc");
10
10
  function isInCluster() {
@@ -62,7 +62,19 @@ function getFrpcPath() {
62
62
  return "frpc";
63
63
  }
64
64
  async function ensureFrpc(log) {
65
- if (existsSync(FRPC_BIN)) return FRPC_BIN;
65
+ if (existsSync(FRPC_BIN)) {
66
+ try {
67
+ const out = execSync(`"${FRPC_BIN}" --version`, { stdio: "pipe", timeout: 5e3 }).toString().trim();
68
+ if (out === FRP_VERSION) return FRPC_BIN;
69
+ (log || console.log)(`frpc version mismatch: ${out} \u2260 ${FRP_VERSION}, re-downloading...`);
70
+ } catch {
71
+ (log || console.log)("frpc binary broken or invalid signature, re-downloading...");
72
+ }
73
+ try {
74
+ unlinkSync(FRPC_BIN);
75
+ } catch {
76
+ }
77
+ }
66
78
  const logger = log || console.log;
67
79
  mkdirSync(BIN_DIR, { recursive: true });
68
80
  const url = getFrpcDownloadUrl();
@@ -165,6 +177,12 @@ class FrpcTunnel {
165
177
  logBuffer = [];
166
178
  maxLogLines = 200;
167
179
  proxies = [];
180
+ // Health tracking
181
+ _consecutiveErrors = 0;
182
+ _lastConnectedAt = 0;
183
+ _lastErrorAt = 0;
184
+ _firstErrorAt = 0;
185
+ _restartAttempts = 0;
168
186
  constructor(options) {
169
187
  this.options = options;
170
188
  this.log = options.log || ((msg) => console.log(`[FRPC] ${msg}`));
@@ -230,6 +248,10 @@ class FrpcTunnel {
230
248
  if (text.includes("start proxy success") || text.includes("login to server success")) {
231
249
  if (!this._connected) {
232
250
  this._connected = true;
251
+ this._lastConnectedAt = Date.now();
252
+ this._restartAttempts = 0;
253
+ this._consecutiveErrors = 0;
254
+ this._firstErrorAt = 0;
233
255
  this.options.onConnect?.();
234
256
  if (!resolved) {
235
257
  resolved = true;
@@ -238,6 +260,9 @@ class FrpcTunnel {
238
260
  }
239
261
  }
240
262
  if (text.includes("[E]") || text.includes("error")) {
263
+ this._consecutiveErrors++;
264
+ this._lastErrorAt = Date.now();
265
+ if (this._firstErrorAt === 0) this._firstErrorAt = Date.now();
241
266
  this.options.onError?.(new Error(text));
242
267
  }
243
268
  };
@@ -259,14 +284,16 @@ class FrpcTunnel {
259
284
  reject(new Error(`frpc exited with code ${code}`));
260
285
  }
261
286
  if (!this._destroyed && code !== 0) {
262
- this.log(`frpc exited with code ${code}, restarting in 3s...`);
287
+ this._restartAttempts++;
288
+ const delay = Math.min(3e3 * Math.pow(2, this._restartAttempts - 1), 6e4);
289
+ this.log(`frpc exited with code ${code}, restarting in ${Math.round(delay / 1e3)}s (attempt ${this._restartAttempts})...`);
263
290
  setTimeout(() => {
264
291
  if (!this._destroyed) {
265
292
  this.connect().catch((err) => {
266
293
  this.options.onError?.(err);
267
294
  });
268
295
  }
269
- }, 3e3);
296
+ }, delay);
270
297
  }
271
298
  });
272
299
  setTimeout(() => {
@@ -310,9 +337,22 @@ class FrpcTunnel {
310
337
  name: this.options.name,
311
338
  ports: this.options.ports,
312
339
  serverAddr: this.serverConfig.serverAddr,
313
- recentLogs: this.logBuffer.slice(-10)
340
+ recentLogs: this.logBuffer.slice(-10),
341
+ consecutiveErrors: this._consecutiveErrors,
342
+ lastConnectedAt: this._lastConnectedAt,
343
+ lastErrorAt: this._lastErrorAt,
344
+ firstErrorAt: this._firstErrorAt,
345
+ restartAttempts: this._restartAttempts,
346
+ failingDurationMs: this._firstErrorAt > 0 ? Date.now() - this._firstErrorAt : 0
314
347
  };
315
348
  }
349
+ /** Update the Hypha token. Rewrites config; takes effect on next frpc restart. */
350
+ updateToken(newToken) {
351
+ this.serverConfig.hyphaToken = newToken;
352
+ const configContent = generateFrpcConfig(this.serverConfig, this.proxies);
353
+ writeFileSync(this.configPath, configContent);
354
+ this.log("Config updated with fresh token");
355
+ }
316
356
  /** Get the public URLs for each port. */
317
357
  getUrls() {
318
358
  const urls = /* @__PURE__ */ new Map();
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-Cq-vqhYX.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-BTlUAfV3.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.24";
2
+ var version = "0.2.25";
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";
@@ -8,9 +8,9 @@ import { randomUUID as randomUUID$1 } from 'crypto';
8
8
  import { existsSync, readFileSync, writeFileSync as writeFileSync$1, mkdirSync as mkdirSync$1, appendFileSync } from 'node:fs';
9
9
  import { randomUUID, createHash } from 'node:crypto';
10
10
  import { join as join$1 } from 'node:path';
11
- import { spawn, execSync, execFile } from 'node:child_process';
11
+ import { spawn, execSync, execFile, execFileSync } from 'node:child_process';
12
12
  import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
13
- import { homedir } from 'node:os';
13
+ import { homedir, platform } from 'node:os';
14
14
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
15
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
16
16
  import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
@@ -350,6 +350,10 @@ async function registerMachineService(server, machineId, metadata, daemonState,
350
350
  let currentDaemonState = { ...daemonState };
351
351
  let metadataVersion = 1;
352
352
  let daemonStateVersion = 1;
353
+ let lastInboundRpcAt = Date.now();
354
+ const trackInbound = () => {
355
+ lastInboundRpcAt = Date.now();
356
+ };
353
357
  const listeners = [];
354
358
  const removeListener = (listener, reason) => {
355
359
  const idx = listeners.indexOf(listener);
@@ -394,6 +398,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
394
398
  config: { visibility: "unlisted", require_context: true },
395
399
  // Machine info
396
400
  getMachineInfo: async (context) => {
401
+ trackInbound();
397
402
  let hasMachineAccess = false;
398
403
  try {
399
404
  authorizeRequest(context, currentMetadata.sharing, "view");
@@ -435,6 +440,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
435
440
  },
436
441
  // Heartbeat
437
442
  heartbeat: async (context) => {
443
+ trackInbound();
438
444
  authorizeRequest(context, currentMetadata.sharing, "view");
439
445
  return {
440
446
  time: Date.now(),
@@ -446,6 +452,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
446
452
  // If machine sharing grants access, return all. Otherwise return only
447
453
  // sessions whose own sharing config grants access to this user.
448
454
  listSessions: async (context) => {
455
+ trackInbound();
449
456
  let hasMachineAccess = false;
450
457
  try {
451
458
  authorizeRequest(context, currentMetadata.sharing, "view");
@@ -473,6 +480,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
473
480
  * sessions whose sharing config grants them access.
474
481
  */
475
482
  getSessions: async (context) => {
483
+ trackInbound();
476
484
  let hasMachineAccess = false;
477
485
  try {
478
486
  authorizeRequest(context, currentMetadata.sharing, "view");
@@ -513,8 +521,12 @@ async function registerMachineService(server, machineId, metadata, daemonState,
513
521
  * Dispatch an RPC call to a specific session's handler.
514
522
  * This consolidates all session RPCs through the machine service,
515
523
  * eliminating the need for per-session Hypha service registration.
524
+ *
525
+ * `kwargs` is an object whose keys map to handler parameter names.
526
+ * Context is injected automatically — callers must NOT include it.
516
527
  */
517
528
  sessionRPC: async (sessionId, method, kwargs, context) => {
529
+ trackInbound();
518
530
  const rpc = handlers.getSessionRPCHandlers?.(sessionId);
519
531
  if (!rpc) {
520
532
  throw new Error(`Session ${sessionId} not found on this machine`);
@@ -545,6 +557,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
545
557
  * Delegates to the session store's registerListener.
546
558
  */
547
559
  registerSessionListener: async (sessionId, callback, context) => {
560
+ trackInbound();
548
561
  const rpc = handlers.getSessionRPCHandlers?.(sessionId);
549
562
  if (!rpc) {
550
563
  throw new Error(`Session ${sessionId} not found on this machine`);
@@ -564,6 +577,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
564
577
  },
565
578
  // Spawn a new session
566
579
  spawnSession: async (options, context) => {
580
+ trackInbound();
567
581
  authorizeRequest(context, currentMetadata.sharing, "interact");
568
582
  const callerEmail = context?.user?.email;
569
583
  const machineOwner = currentMetadata.sharing?.owner;
@@ -776,6 +790,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
776
790
  },
777
791
  // Register a listener for real-time updates (app calls this with _rintf callback)
778
792
  registerListener: async (callback, context) => {
793
+ trackInbound();
779
794
  authorizeRequest(context, currentMetadata.sharing, "view");
780
795
  listeners.push(callback);
781
796
  return { success: true, listenerId: listeners.length - 1 };
@@ -1092,7 +1107,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1092
1107
  const tunnels = handlers.tunnels;
1093
1108
  if (!tunnels) throw new Error("Tunnel management not available");
1094
1109
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
1095
- const { FrpcTunnel } = await import('./frpc-C9Qs-5jQ.mjs');
1110
+ const { FrpcTunnel } = await import('./frpc-BhS2e6r-.mjs');
1096
1111
  const tunnel = new FrpcTunnel({
1097
1112
  name: params.name,
1098
1113
  ports: params.ports,
@@ -1222,6 +1237,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1222
1237
  daemonState: { value: currentDaemonState, version: daemonStateVersion }
1223
1238
  });
1224
1239
  },
1240
+ getLastInboundRpcAt: () => lastInboundRpcAt,
1225
1241
  disconnect: async () => {
1226
1242
  const toRemove = [...listeners];
1227
1243
  for (const listener of toRemove) {
@@ -4780,12 +4796,32 @@ async function stageCredentialsForSharing(sessionId) {
4780
4796
  const stagedClaudeDir = join$1(tmpHome, ".claude");
4781
4797
  await mkdir(stagedClaudeDir, { recursive: true });
4782
4798
  const credentialFiles = ["credentials.json", ".credentials.json"];
4799
+ let credentialsCopied = false;
4783
4800
  for (const file of credentialFiles) {
4784
4801
  try {
4785
4802
  await copyFile(join$1(realClaudeDir, file), join$1(stagedClaudeDir, file));
4803
+ credentialsCopied = true;
4786
4804
  } catch {
4787
4805
  }
4788
4806
  }
4807
+ if (!credentialsCopied && platform() === "darwin") {
4808
+ const stagedCredFile = join$1(stagedClaudeDir, ".credentials.json");
4809
+ const hasExistingCredentials = existsSync(stagedCredFile);
4810
+ if (!hasExistingCredentials) {
4811
+ try {
4812
+ const keychainData = execFileSync("security", [
4813
+ "find-generic-password",
4814
+ "-s",
4815
+ "Claude Code-credentials",
4816
+ "-w"
4817
+ ], { encoding: "utf-8", timeout: 5e3 }).trim();
4818
+ if (keychainData.startsWith("{") || keychainData.startsWith("[")) {
4819
+ await writeFile(stagedCredFile, keychainData);
4820
+ }
4821
+ } catch {
4822
+ }
4823
+ }
4824
+ }
4789
4825
  try {
4790
4826
  await copyFile(join$1(realHome, ".gitconfig"), join$1(tmpHome, ".gitconfig"));
4791
4827
  } catch {
@@ -6174,6 +6210,11 @@ async function startDaemon(options) {
6174
6210
  const msg = String(reason);
6175
6211
  logger.error("Unhandled rejection:", reason);
6176
6212
  const isTransient = TRANSIENT_REJECTION_PATTERNS.some((p) => msg.toLowerCase().includes(p.toLowerCase()));
6213
+ const isSessionLoss = msg.toLowerCase().includes("session does not exist");
6214
+ if (isSessionLoss && consecutiveHeartbeatFailures === 0) {
6215
+ logger.log(`Session loss detected with healthy heartbeat \u2014 triggering immediate service probe`);
6216
+ consecutiveHeartbeatFailures = 1;
6217
+ }
6177
6218
  if (isTransient || consecutiveHeartbeatFailures > 0) {
6178
6219
  logger.log(`Ignoring transient rejection (heartbeatFailures=${consecutiveHeartbeatFailures}): ${msg.slice(0, 200)}`);
6179
6220
  return;
@@ -6256,7 +6297,7 @@ async function startDaemon(options) {
6256
6297
  const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
6257
6298
  await supervisor.init();
6258
6299
  const tunnels = /* @__PURE__ */ new Map();
6259
- const { ServeManager } = await import('./serveManager-BiyUnYbW.mjs');
6300
+ const { ServeManager } = await import('./serveManager-BTbXyL0i.mjs');
6260
6301
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
6261
6302
  ensureAutoInstalledSkills(logger).catch(() => {
6262
6303
  });
@@ -8695,13 +8736,15 @@ The automated loop has finished. Review the progress above and let me know if yo
8695
8736
  console.log(` Service: svamp-machine-${machineId}`);
8696
8737
  console.log(` Log file: ${logger.logFilePath}`);
8697
8738
  const HEARTBEAT_INTERVAL_MS = 1e4;
8698
- const PING_TIMEOUT_MS = 6e4;
8739
+ const PING_TIMEOUT_MS = 15e3;
8699
8740
  const POST_RECONNECT_GRACE_MS = 2e4;
8700
8741
  let heartbeatRunning = false;
8701
8742
  let lastReconnectAt = 0;
8743
+ let heartbeatCycle = 0;
8702
8744
  const heartbeatInterval = setInterval(async () => {
8703
8745
  if (heartbeatRunning) return;
8704
8746
  heartbeatRunning = true;
8747
+ heartbeatCycle++;
8705
8748
  try {
8706
8749
  try {
8707
8750
  const state = readDaemonStateFile();
@@ -8731,6 +8774,22 @@ The automated loop has finished. Review the progress above and let me know if yo
8731
8774
  }
8732
8775
  }
8733
8776
  }
8777
+ const INBOUND_SILENCE_THRESHOLD_MS = 12e4;
8778
+ const inboundSilenceMs = Date.now() - machineService.getLastInboundRpcAt();
8779
+ const hasActiveSessions = pidToTrackedSession.size > 0;
8780
+ if (hasActiveSessions && inboundSilenceMs > INBOUND_SILENCE_THRESHOLD_MS && consecutiveHeartbeatFailures === 0) {
8781
+ logger.log(`No inbound RPC for ${Math.round(inboundSilenceMs / 1e3)}s with ${pidToTrackedSession.size} active session(s) \u2014 zombie probe`);
8782
+ try {
8783
+ const machineServiceId = `${server.config.client_id}:default`;
8784
+ await Promise.race([
8785
+ server.getService(machineServiceId),
8786
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Zombie probe timed out")), PING_TIMEOUT_MS))
8787
+ ]);
8788
+ } catch (err) {
8789
+ logger.log(`Zombie detection probe failed: ${err.message} \u2014 forcing reconnection`);
8790
+ consecutiveHeartbeatFailures = 2;
8791
+ }
8792
+ }
8734
8793
  const inGrace = lastReconnectAt > 0 && Date.now() - lastReconnectAt < POST_RECONNECT_GRACE_MS;
8735
8794
  if (!inGrace) {
8736
8795
  try {
@@ -8756,7 +8815,7 @@ The automated loop has finished. Review the progress above and let me know if yo
8756
8815
  } else if (consecutiveHeartbeatFailures % 6 === 0) {
8757
8816
  logger.log(`Connection down for ${consecutiveHeartbeatFailures * HEARTBEAT_INTERVAL_MS / 1e3}s (${consecutiveHeartbeatFailures} failures, retrying indefinitely)`);
8758
8817
  }
8759
- if (consecutiveHeartbeatFailures === 1 || consecutiveHeartbeatFailures % 3 === 0) {
8818
+ if (consecutiveHeartbeatFailures === 2 || consecutiveHeartbeatFailures % 3 === 0) {
8760
8819
  const conn = server.rpc?._connection;
8761
8820
  const ws = conn?._websocket;
8762
8821
  if (ws?.readyState === 1) {
@@ -8782,6 +8841,22 @@ The automated loop has finished. Review the progress above and let me know if yo
8782
8841
  }
8783
8842
  }
8784
8843
  }
8844
+ const FRPC_FAILING_THRESHOLD_MS = 5 * 6e4;
8845
+ const serveHealth = serveManager.getTunnelHealth();
8846
+ if (serveHealth && serveHealth.failingDurationMs > FRPC_FAILING_THRESHOLD_MS) {
8847
+ logger.log(`Serve manager tunnel failing for ${Math.round(serveHealth.failingDurationMs / 1e3)}s (${serveHealth.consecutiveErrors} errors, ${serveHealth.restartAttempts} restarts) \u2014 recreating`);
8848
+ serveManager.recreateTunnel().catch((err) => {
8849
+ logger.log(`Failed to recreate serve tunnel: ${err.message}`);
8850
+ });
8851
+ }
8852
+ for (const [name, tunnel] of tunnels) {
8853
+ const health = tunnel.status;
8854
+ if (health.failingDurationMs > FRPC_FAILING_THRESHOLD_MS) {
8855
+ logger.log(`frpc tunnel '${name}' failing for ${Math.round(health.failingDurationMs / 1e3)}s \u2014 destroying stale tunnel`);
8856
+ tunnel.destroy();
8857
+ tunnels.delete(name);
8858
+ }
8859
+ }
8785
8860
  } finally {
8786
8861
  heartbeatRunning = false;
8787
8862
  }
@@ -8817,6 +8892,7 @@ The automated loop has finished. Review the progress above and let me know if yo
8817
8892
  shutdownSource: source
8818
8893
  });
8819
8894
  await new Promise((r) => setTimeout(r, 200));
8895
+ const shouldMarkStopped = source === "os-signal-cleanup";
8820
8896
  for (const [, session] of pidToTrackedSession) {
8821
8897
  session.hyphaService?.disconnect().catch(() => {
8822
8898
  });
@@ -8826,11 +8902,12 @@ The automated loop has finished. Review the progress above and let me know if yo
8826
8902
  } catch {
8827
8903
  }
8828
8904
  }
8829
- session.cleanupCredentials?.().catch(() => {
8830
- });
8905
+ if (shouldMarkStopped) {
8906
+ session.cleanupCredentials?.().catch(() => {
8907
+ });
8908
+ }
8831
8909
  session.cleanupSvampConfig?.();
8832
8910
  }
8833
- const shouldMarkStopped = source === "os-signal-cleanup";
8834
8911
  if (shouldMarkStopped) {
8835
8912
  try {
8836
8913
  const index = loadSessionIndex();
@@ -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-Cq-vqhYX.mjs';
5
+ import { c as connectToHypha, a as registerSessionService } from './run-BTlUAfV3.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -52,7 +52,7 @@ async function handleServeCommand() {
52
52
  }
53
53
  }
54
54
  async function serveAdd(args, machineId) {
55
- const { connectAndGetMachine } = await import('./commands-CEiWVChH.mjs');
55
+ const { connectAndGetMachine } = await import('./commands-DRpGTxYD.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-CEiWVChH.mjs');
87
+ const { connectAndGetMachine } = await import('./commands-DRpGTxYD.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-CEiWVChH.mjs');
107
+ const { connectAndGetMachine } = await import('./commands-DRpGTxYD.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-CEiWVChH.mjs');
140
+ const { connectAndGetMachine } = await import('./commands-DRpGTxYD.mjs');
141
141
  const { machine, server } = await connectAndGetMachine(machineId);
142
142
  try {
143
143
  const info = await machine.serveInfo();
@@ -530,10 +530,23 @@ button{padding:10px 24px;background:#0969da;color:#fff;border:none;border-radius
530
530
  server.on("error", reject);
531
531
  });
532
532
  }
533
+ /** Get frpc tunnel health status, or null if no tunnel. */
534
+ getTunnelHealth() {
535
+ return this.frpcTunnel?.status ?? null;
536
+ }
537
+ /** Destroy and recreate the frpc tunnel with fresh config. */
538
+ async recreateTunnel() {
539
+ if (this.frpcTunnel) {
540
+ this.log("Recreating frpc tunnel (persistent failure detected)...");
541
+ this.frpcTunnel.destroy();
542
+ this.frpcTunnel = null;
543
+ }
544
+ await this.ensureTunnel();
545
+ }
533
546
  /** Start frpc tunnel for the Caddy port. */
534
547
  async ensureTunnel() {
535
548
  try {
536
- const { FrpcTunnel } = await import('./frpc-C9Qs-5jQ.mjs');
549
+ const { FrpcTunnel } = await import('./frpc-BhS2e6r-.mjs');
537
550
  this.frpcTunnel = new FrpcTunnel({
538
551
  name: this.serviceName,
539
552
  ports: [this.port],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",