svamp-cli 0.2.46 → 0.2.47

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-B6zZtCuh.mjs');
151
+ const { connectAndGetMachine } = await import('./commands-JWrmpGcs.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-B6zZtCuh.mjs');
168
+ const { resolveSessionId } = await import('./commands-JWrmpGcs.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-wfhhtkl1.mjs';
1
+ import { s as startDaemon, b as stopDaemon, d as daemonStatus } from './run-6umeTX-K.mjs';
2
2
  import 'os';
3
3
  import 'fs/promises';
4
4
  import 'fs';
@@ -42,7 +42,7 @@ async function main() {
42
42
  console.error(`svamp daemon restart: ${err.message || err}`);
43
43
  process.exit(1);
44
44
  }
45
- const { restartDaemon } = await import('./run-wfhhtkl1.mjs').then(function (n) { return n.o; });
45
+ const { restartDaemon } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.o; });
46
46
  await restartDaemon();
47
47
  process.exit(0);
48
48
  }
@@ -277,7 +277,7 @@ async function main() {
277
277
  console.error("svamp service: Service commands are not available in sandboxed sessions.");
278
278
  process.exit(1);
279
279
  }
280
- const { handleServiceCommand } = await import('./commands-DsCkxa3k.mjs');
280
+ const { handleServiceCommand } = await import('./commands-TyAIFJx-.mjs');
281
281
  await handleServiceCommand();
282
282
  } else if (subcommand === "serve") {
283
283
  const { isSandboxed: isSandboxedServe } = await import('./sandboxDetect-DNTcbgWD.mjs');
@@ -285,7 +285,7 @@ async function main() {
285
285
  console.error("svamp serve: Serve commands are not available in sandboxed sessions.");
286
286
  process.exit(1);
287
287
  }
288
- const { handleServeCommand } = await import('./serveCommands-DhtNhaur.mjs');
288
+ const { handleServeCommand } = await import('./serveCommands-FUE8m232.mjs');
289
289
  await handleServeCommand();
290
290
  process.exit(0);
291
291
  } else if (subcommand === "process" || subcommand === "proc") {
@@ -294,7 +294,7 @@ async function main() {
294
294
  console.error("svamp process: Process commands are not available in sandboxed sessions.");
295
295
  process.exit(1);
296
296
  }
297
- const { processCommand } = await import('./commands-Cs5-T_lh.mjs');
297
+ const { processCommand } = await import('./commands-BJR_98XX.mjs');
298
298
  let machineId;
299
299
  const processArgs = args.slice(1);
300
300
  const mIdx = processArgs.findIndex((a) => a === "--machine" || a === "-m");
@@ -312,7 +312,7 @@ async function main() {
312
312
  } else if (!subcommand || subcommand === "start") {
313
313
  await handleInteractiveCommand();
314
314
  } else if (subcommand === "--version" || subcommand === "-v") {
315
- const pkg = await import('./package-CyXoMaC5.mjs').catch(() => ({ default: { version: "unknown" } }));
315
+ const pkg = await import('./package-CNFS7wvh.mjs').catch(() => ({ default: { version: "unknown" } }));
316
316
  console.log(`svamp version: ${pkg.default.version}`);
317
317
  } else {
318
318
  console.error(`Unknown command: ${subcommand}`);
@@ -321,7 +321,7 @@ async function main() {
321
321
  }
322
322
  }
323
323
  async function handleInteractiveCommand() {
324
- const { runInteractive } = await import('./run-yJ1mtT8S.mjs');
324
+ const { runInteractive } = await import('./run-DR7E3IZL.mjs');
325
325
  const interactiveArgs = subcommand === "start" ? args.slice(1) : args;
326
326
  let directory = process.cwd();
327
327
  let resumeSessionId;
@@ -366,7 +366,7 @@ async function handleAgentCommand() {
366
366
  return;
367
367
  }
368
368
  if (agentArgs[0] === "list") {
369
- const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-wfhhtkl1.mjs').then(function (n) { return n.j; });
369
+ const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.j; });
370
370
  console.log("Known agents:");
371
371
  for (const [name, config2] of Object.entries(KNOWN_ACP_AGENTS)) {
372
372
  console.log(` ${name.padEnd(12)} ${config2.command} ${config2.args.join(" ")} (ACP)`);
@@ -378,7 +378,7 @@ async function handleAgentCommand() {
378
378
  console.log('Use "svamp agent -- <command> [args]" for a custom ACP agent.');
379
379
  return;
380
380
  }
381
- const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-wfhhtkl1.mjs').then(function (n) { return n.j; });
381
+ const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.j; });
382
382
  let cwd = process.cwd();
383
383
  const filteredArgs = [];
384
384
  for (let i = 0; i < agentArgs.length; i++) {
@@ -402,12 +402,12 @@ async function handleAgentCommand() {
402
402
  console.log(`Starting ${config.agentName} agent in ${cwd}...`);
403
403
  let backend;
404
404
  if (KNOWN_MCP_AGENTS[config.agentName]) {
405
- const { CodexMcpBackend } = await import('./run-wfhhtkl1.mjs').then(function (n) { return n.k; });
405
+ const { CodexMcpBackend } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.k; });
406
406
  backend = new CodexMcpBackend({ cwd, log: logFn });
407
407
  } else {
408
- const { AcpBackend } = await import('./run-wfhhtkl1.mjs').then(function (n) { return n.i; });
409
- const { GeminiTransport } = await import('./run-wfhhtkl1.mjs').then(function (n) { return n.G; });
410
- const { DefaultTransport } = await import('./run-wfhhtkl1.mjs').then(function (n) { return n.D; });
408
+ const { AcpBackend } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.i; });
409
+ const { GeminiTransport } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.G; });
410
+ const { DefaultTransport } = await import('./run-6umeTX-K.mjs').then(function (n) { return n.D; });
411
411
  const transportHandler = config.agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(config.agentName);
412
412
  backend = new AcpBackend({
413
413
  agentName: config.agentName,
@@ -534,7 +534,7 @@ async function handleSessionCommand() {
534
534
  process.exit(1);
535
535
  }
536
536
  }
537
- const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-B6zZtCuh.mjs');
537
+ const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionInboxSend, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxClear } = await import('./commands-JWrmpGcs.mjs');
538
538
  const parseFlagStr = (flag, shortFlag) => {
539
539
  for (let i = 1; i < sessionArgs.length; i++) {
540
540
  if ((sessionArgs[i] === flag || shortFlag) && i + 1 < sessionArgs.length) {
@@ -594,7 +594,7 @@ async function handleSessionCommand() {
594
594
  allowDomain.push(sessionArgs[++i]);
595
595
  }
596
596
  }
597
- const { parseShareArg } = await import('./commands-B6zZtCuh.mjs');
597
+ const { parseShareArg } = await import('./commands-JWrmpGcs.mjs');
598
598
  const shareEntries = share.map((s) => parseShareArg(s));
599
599
  await sessionSpawn(agent, dir, targetMachineId, {
600
600
  message,
@@ -680,7 +680,7 @@ async function handleSessionCommand() {
680
680
  console.error("Usage: svamp session approve <session-id> [request-id] [--json]");
681
681
  process.exit(1);
682
682
  }
683
- const { sessionApprove } = await import('./commands-B6zZtCuh.mjs');
683
+ const { sessionApprove } = await import('./commands-JWrmpGcs.mjs');
684
684
  const approveReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
685
685
  await sessionApprove(sessionArgs[1], approveReqId, targetMachineId, {
686
686
  json: hasFlag("--json")
@@ -690,7 +690,7 @@ async function handleSessionCommand() {
690
690
  console.error("Usage: svamp session deny <session-id> [request-id] [--json]");
691
691
  process.exit(1);
692
692
  }
693
- const { sessionDeny } = await import('./commands-B6zZtCuh.mjs');
693
+ const { sessionDeny } = await import('./commands-JWrmpGcs.mjs');
694
694
  const denyReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
695
695
  await sessionDeny(sessionArgs[1], denyReqId, targetMachineId, {
696
696
  json: hasFlag("--json")
@@ -726,7 +726,7 @@ async function handleSessionCommand() {
726
726
  console.error("Usage: svamp session set-title <title>");
727
727
  process.exit(1);
728
728
  }
729
- const { sessionSetTitle } = await import('./agentCommands-Bqjcj1EX.mjs');
729
+ const { sessionSetTitle } = await import('./agentCommands-BuGwfYhd.mjs');
730
730
  await sessionSetTitle(title);
731
731
  } else if (sessionSubcommand === "set-link") {
732
732
  const url = sessionArgs[1];
@@ -735,7 +735,7 @@ async function handleSessionCommand() {
735
735
  process.exit(1);
736
736
  }
737
737
  const label = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
738
- const { sessionSetLink } = await import('./agentCommands-Bqjcj1EX.mjs');
738
+ const { sessionSetLink } = await import('./agentCommands-BuGwfYhd.mjs');
739
739
  await sessionSetLink(url, label);
740
740
  } else if (sessionSubcommand === "notify") {
741
741
  const message = sessionArgs[1];
@@ -744,7 +744,7 @@ async function handleSessionCommand() {
744
744
  process.exit(1);
745
745
  }
746
746
  const level = parseFlagStr("--level") || "info";
747
- const { sessionNotify } = await import('./agentCommands-Bqjcj1EX.mjs');
747
+ const { sessionNotify } = await import('./agentCommands-BuGwfYhd.mjs');
748
748
  await sessionNotify(message, level);
749
749
  } else if (sessionSubcommand === "broadcast") {
750
750
  const action = sessionArgs[1];
@@ -752,7 +752,7 @@ async function handleSessionCommand() {
752
752
  console.error("Usage: svamp session broadcast <action> [args...]\nActions: open-canvas <url> [label], close-canvas, toast <message>");
753
753
  process.exit(1);
754
754
  }
755
- const { sessionBroadcast } = await import('./agentCommands-Bqjcj1EX.mjs');
755
+ const { sessionBroadcast } = await import('./agentCommands-BuGwfYhd.mjs');
756
756
  await sessionBroadcast(action, sessionArgs.slice(2).filter((a) => !a.startsWith("--")));
757
757
  } else if (sessionSubcommand === "inbox") {
758
758
  const inboxSubcmd = sessionArgs[1];
@@ -763,7 +763,7 @@ async function handleSessionCommand() {
763
763
  process.exit(1);
764
764
  }
765
765
  if (agentSessionId) {
766
- const { inboxSend } = await import('./agentCommands-Bqjcj1EX.mjs');
766
+ const { inboxSend } = await import('./agentCommands-BuGwfYhd.mjs');
767
767
  await inboxSend(sessionArgs[2], {
768
768
  body: sessionArgs[3],
769
769
  subject: parseFlagStr("--subject"),
@@ -778,7 +778,7 @@ async function handleSessionCommand() {
778
778
  }
779
779
  } else if (inboxSubcmd === "list" || inboxSubcmd === "ls") {
780
780
  if (agentSessionId && !sessionArgs[2]) {
781
- const { inboxList } = await import('./agentCommands-Bqjcj1EX.mjs');
781
+ const { inboxList } = await import('./agentCommands-BuGwfYhd.mjs');
782
782
  await inboxList({
783
783
  unread: hasFlag("--unread"),
784
784
  limit: parseFlagInt("--limit"),
@@ -800,7 +800,7 @@ async function handleSessionCommand() {
800
800
  process.exit(1);
801
801
  }
802
802
  if (agentSessionId && !sessionArgs[3]) {
803
- const { inboxList } = await import('./agentCommands-Bqjcj1EX.mjs');
803
+ const { inboxList } = await import('./agentCommands-BuGwfYhd.mjs');
804
804
  await sessionInboxRead(agentSessionId, sessionArgs[2], targetMachineId);
805
805
  } else if (sessionArgs[3]) {
806
806
  await sessionInboxRead(sessionArgs[2], sessionArgs[3], targetMachineId);
@@ -810,7 +810,7 @@ async function handleSessionCommand() {
810
810
  }
811
811
  } else if (inboxSubcmd === "reply") {
812
812
  if (agentSessionId && sessionArgs[2] && sessionArgs[3] && !sessionArgs[4]) {
813
- const { inboxReply } = await import('./agentCommands-Bqjcj1EX.mjs');
813
+ const { inboxReply } = await import('./agentCommands-BuGwfYhd.mjs');
814
814
  await inboxReply(sessionArgs[2], sessionArgs[3]);
815
815
  } else if (sessionArgs[2] && sessionArgs[3] && sessionArgs[4]) {
816
816
  await sessionInboxReply(sessionArgs[2], sessionArgs[3], sessionArgs[4], targetMachineId);
@@ -846,7 +846,7 @@ async function handleMachineCommand() {
846
846
  return;
847
847
  }
848
848
  if (machineSubcommand === "share") {
849
- const { machineShare } = await import('./commands-B6zZtCuh.mjs');
849
+ const { machineShare } = await import('./commands-JWrmpGcs.mjs');
850
850
  let machineId;
851
851
  const shareArgs = [];
852
852
  for (let i = 1; i < machineArgs.length; i++) {
@@ -876,7 +876,7 @@ async function handleMachineCommand() {
876
876
  }
877
877
  await machineShare(machineId, { add, remove, list, configPath, showConfig });
878
878
  } else if (machineSubcommand === "exec") {
879
- const { machineExec } = await import('./commands-B6zZtCuh.mjs');
879
+ const { machineExec } = await import('./commands-JWrmpGcs.mjs');
880
880
  let machineId;
881
881
  let cwd;
882
882
  const cmdParts = [];
@@ -896,7 +896,7 @@ async function handleMachineCommand() {
896
896
  }
897
897
  await machineExec(machineId, command, cwd);
898
898
  } else if (machineSubcommand === "info") {
899
- const { machineInfo } = await import('./commands-B6zZtCuh.mjs');
899
+ const { machineInfo } = await import('./commands-JWrmpGcs.mjs');
900
900
  let machineId;
901
901
  for (let i = 1; i < machineArgs.length; i++) {
902
902
  if ((machineArgs[i] === "--machine" || machineArgs[i] === "-m") && i + 1 < machineArgs.length) {
@@ -916,10 +916,10 @@ async function handleMachineCommand() {
916
916
  level = machineArgs[++i];
917
917
  }
918
918
  }
919
- const { machineNotify } = await import('./agentCommands-Bqjcj1EX.mjs');
919
+ const { machineNotify } = await import('./agentCommands-BuGwfYhd.mjs');
920
920
  await machineNotify(message, level);
921
921
  } else if (machineSubcommand === "ls") {
922
- const { machineLs } = await import('./commands-B6zZtCuh.mjs');
922
+ const { machineLs } = await import('./commands-JWrmpGcs.mjs');
923
923
  let machineId;
924
924
  let showHidden = false;
925
925
  let path;
@@ -1381,7 +1381,7 @@ async function applyClaudeAuthFlags(argv) {
1381
1381
  "--use-hypha-proxy, --use-claude-login, and --anthropic-base-url/--anthropic-api-key are mutually exclusive"
1382
1382
  );
1383
1383
  }
1384
- const mod = await import('./run-wfhhtkl1.mjs').then(function (n) { return n.n; });
1384
+ const mod = await import('./run-6umeTX-K.mjs').then(function (n) { return n.n; });
1385
1385
  if (hasHypha) {
1386
1386
  mod.setClaudeAuthHyphaProxy();
1387
1387
  console.log("Claude auth configured: hypha-proxy (uses HYPHA_TOKEN live at each spawn).");
@@ -1404,7 +1404,7 @@ async function applyClaudeAuthFlags(argv) {
1404
1404
  }
1405
1405
  async function handleDaemonAuthCommand(argv) {
1406
1406
  const sub = (argv[0] || "status").toLowerCase();
1407
- const mod = await import('./run-wfhhtkl1.mjs').then(function (n) { return n.n; });
1407
+ const mod = await import('./run-6umeTX-K.mjs').then(function (n) { return n.n; });
1408
1408
  if (sub === "--help" || sub === "-h" || sub === "help") {
1409
1409
  console.log(`
1410
1410
  svamp daemon auth \u2014 Configure how Claude subprocesses authenticate
@@ -1,11 +1,11 @@
1
1
  import { writeFileSync, readFileSync } from 'fs';
2
2
  import { resolve } from 'path';
3
- import { connectAndGetMachine } from './commands-B6zZtCuh.mjs';
3
+ import { connectAndGetMachine } from './commands-JWrmpGcs.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-wfhhtkl1.mjs';
8
+ import './run-6umeTX-K.mjs';
9
9
  import 'os';
10
10
  import 'fs/promises';
11
11
  import 'url';
@@ -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-wfhhtkl1.mjs';
5
+ import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-6umeTX-K.mjs';
6
6
  import 'os';
7
7
  import 'fs/promises';
8
8
  import 'fs';
@@ -97,7 +97,7 @@ async function serviceServe(args) {
97
97
  }
98
98
  async function serviceList(_args) {
99
99
  try {
100
- const { connectAndGetMachine } = await import('./commands-B6zZtCuh.mjs');
100
+ const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
101
101
  const { server, machine } = await connectAndGetMachine();
102
102
  try {
103
103
  const tunnels = await machine.tunnelList({});
@@ -126,7 +126,7 @@ async function serviceDelete(args) {
126
126
  process.exit(1);
127
127
  }
128
128
  try {
129
- const { connectAndGetMachine } = await import('./commands-B6zZtCuh.mjs');
129
+ const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
130
130
  const { server, machine } = await connectAndGetMachine();
131
131
  try {
132
132
  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-wfhhtkl1.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-6umeTX-K.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.46";
2
+ var version = "0.2.47";
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";
@@ -1149,6 +1149,26 @@ async function registerMachineService(server, machineId, metadata, daemonState,
1149
1149
  const access = params.access || "owner";
1150
1150
  return sm.addMount(params.name, params.directory, params.sessionId, access, ownerEmail);
1151
1151
  },
1152
+ /**
1153
+ * Apply a mount declaratively. Replaces existing mount with same name.
1154
+ * Supports static (directory) and managed-process (process) mounts.
1155
+ * Used by `svamp serve apply <yaml>`.
1156
+ */
1157
+ serveApply: async (params, context) => {
1158
+ authorizeRequest(context, currentMetadata.sharing, "interact");
1159
+ const sm = handlers.serveManager;
1160
+ if (!sm) throw new Error("Serve manager not available");
1161
+ const ownerEmail = params.ownerEmail || context?.user?.email || void 0;
1162
+ const access = params.access ?? "owner";
1163
+ return sm.applyMount({
1164
+ name: params.name,
1165
+ directory: params.directory,
1166
+ process: params.process,
1167
+ sessionId: params.sessionId,
1168
+ access,
1169
+ ownerEmail
1170
+ });
1171
+ },
1152
1172
  /** Remove a mount from the shared static file server. */
1153
1173
  serveRemove: async (params, context) => {
1154
1174
  authorizeRequest(context, currentMetadata.sharing, "interact");
@@ -1325,6 +1345,19 @@ function loadMessagesFromDiskReverse(messagesDir, beforeSeq, limit) {
1325
1345
  return { messages: [], hasMore: false };
1326
1346
  }
1327
1347
  }
1348
+ function countMessagesOnDisk(messagesDir, fallbackInMemory) {
1349
+ const filePath = join$1(messagesDir, "messages.jsonl");
1350
+ if (!existsSync(filePath)) return fallbackInMemory;
1351
+ try {
1352
+ const data = readFileSync(filePath, "utf-8");
1353
+ let count = 0;
1354
+ for (let i = 0; i < data.length; i++) if (data.charCodeAt(i) === 10) count++;
1355
+ if (data.length > 0 && data[data.length - 1] !== "\n") count++;
1356
+ return count;
1357
+ } catch {
1358
+ return fallbackInMemory;
1359
+ }
1360
+ }
1328
1361
  function appendMessage(messagesDir, sessionId, msg) {
1329
1362
  try {
1330
1363
  const filePath = join$1(messagesDir, "messages.jsonl");
@@ -1464,6 +1497,12 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
1464
1497
  hasMore: filtered.length > lim
1465
1498
  };
1466
1499
  },
1500
+ getMessageCount: async (context) => {
1501
+ authorizeRequest(context, metadata.sharing, "view");
1502
+ const latestSeq = nextSeq - 1;
1503
+ const count = options?.messagesDir ? countMessagesOnDisk(options.messagesDir, messages.length) : messages.length;
1504
+ return { count, latestSeq };
1505
+ },
1467
1506
  getLatestMessages: async (beforeSeq, limit, context) => {
1468
1507
  authorizeRequest(context, metadata.sharing, "view");
1469
1508
  const lim = Math.min(limit ?? 100, 500);
@@ -6874,7 +6913,7 @@ async function startDaemon(options) {
6874
6913
  const supervisor = new ProcessSupervisor(join(SVAMP_HOME, "processes"));
6875
6914
  await supervisor.init();
6876
6915
  const tunnels = /* @__PURE__ */ new Map();
6877
- const { ServeManager } = await import('./serveManager-CUcu_V3q.mjs');
6916
+ const { ServeManager } = await import('./serveManager-RvRL-weX.mjs');
6878
6917
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
6879
6918
  ensureAutoInstalledSkills(logger).catch(() => {
6880
6919
  });
@@ -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, h as generateHookSettings } from './run-wfhhtkl1.mjs';
5
+ import { c as connectToHypha, a as registerSessionService, h as generateHookSettings } from './run-6umeTX-K.mjs';
6
6
  import { createServer } from 'node:http';
7
7
  import { spawn } from 'node:child_process';
8
8
  import { createInterface } from 'node:readline';
@@ -45,6 +45,8 @@ async function handleServeCommand() {
45
45
  await serveInfo(machineId);
46
46
  } else if (sub === "add") {
47
47
  await serveAdd(filteredArgs.slice(1), machineId);
48
+ } else if (sub === "apply") {
49
+ await serveApply(filteredArgs.slice(1), machineId);
48
50
  } else if (sub && !sub.startsWith("-")) {
49
51
  await serveAdd(filteredArgs, machineId);
50
52
  } else {
@@ -52,7 +54,7 @@ async function handleServeCommand() {
52
54
  }
53
55
  }
54
56
  async function serveAdd(args, machineId) {
55
- const { connectAndGetMachine } = await import('./commands-B6zZtCuh.mjs');
57
+ const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
56
58
  const pos = positionalArgs(args);
57
59
  const name = pos[0];
58
60
  if (!name) {
@@ -83,8 +85,93 @@ async function serveAdd(args, machineId) {
83
85
  });
84
86
  }
85
87
  }
88
+ async function serveApply(args, machineId) {
89
+ const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
90
+ const fs = await import('fs');
91
+ const yaml = await import('yaml');
92
+ const file = positionalArgs(args)[0];
93
+ if (!file) {
94
+ console.error("Usage: svamp serve apply <yaml-or-json-file> [--machine <id>]");
95
+ console.error("");
96
+ console.error("YAML format:");
97
+ console.error(" name: my-app");
98
+ console.error(" # Static mount:");
99
+ console.error(" directory: ./public");
100
+ console.error(" # OR managed-process mount:");
101
+ console.error(" process:");
102
+ console.error(" command: npm");
103
+ console.error(" args: [start]");
104
+ console.error(" port: 3000");
105
+ console.error(" workdir: .");
106
+ console.error(" warmup_path: / # default '/'");
107
+ console.error(" warmup_timeout_ms: 30000");
108
+ console.error(" idle_timeout_sec: 600 # 0 = always running");
109
+ console.error(" wake_on_request: true # spawn lazily on first request");
110
+ console.error(" access: public # public | owner (default) | [emails]");
111
+ process.exit(1);
112
+ }
113
+ if (!fs.existsSync(file)) {
114
+ console.error(`Error: file not found: ${file}`);
115
+ process.exit(1);
116
+ }
117
+ const raw = fs.readFileSync(file, "utf-8");
118
+ let parsed;
119
+ try {
120
+ parsed = file.endsWith(".json") ? JSON.parse(raw) : yaml.parse(raw);
121
+ } catch (err) {
122
+ console.error(`Error: failed to parse ${file}: ${err.message}`);
123
+ process.exit(1);
124
+ }
125
+ if (!parsed || typeof parsed !== "object") {
126
+ console.error("Error: top-level config must be an object");
127
+ process.exit(1);
128
+ }
129
+ if (!parsed.name) {
130
+ console.error("Error: name is required");
131
+ process.exit(1);
132
+ }
133
+ if (!parsed.directory && !parsed.process) {
134
+ console.error("Error: must specify either directory (static) or process (managed)");
135
+ process.exit(1);
136
+ }
137
+ const proc = parsed.process ? {
138
+ command: parsed.process.command,
139
+ args: parsed.process.args,
140
+ port: parsed.process.port,
141
+ workdir: parsed.process.workdir ? path.resolve(parsed.process.workdir) : void 0,
142
+ env: parsed.process.env,
143
+ warmupPath: parsed.process.warmupPath ?? parsed.process.warmup_path,
144
+ warmupTimeoutMs: parsed.process.warmupTimeoutMs ?? parsed.process.warmup_timeout_ms,
145
+ idleTimeoutSec: parsed.process.idleTimeoutSec ?? parsed.process.idle_timeout_sec,
146
+ wakeOnRequest: parsed.process.wakeOnRequest ?? parsed.process.wake_on_request
147
+ } : void 0;
148
+ const params = {
149
+ name: parsed.name,
150
+ directory: parsed.directory ? path.resolve(parsed.directory) : void 0,
151
+ process: proc,
152
+ sessionId: parsed.sessionId ?? parsed.session_id,
153
+ access: parsed.access ?? "owner",
154
+ ownerEmail: parsed.ownerEmail ?? parsed.owner_email
155
+ };
156
+ const { machine, server } = await connectAndGetMachine(machineId);
157
+ try {
158
+ const result = await machine.serveApply(params);
159
+ const kind = proc ? "managed" : "static";
160
+ const what = proc ? `${proc.command}${proc.args?.length ? " " + proc.args.join(" ") : ""} (port ${proc.port})` : params.directory;
161
+ console.log(`Mount applied (${kind}): ${params.name} \u2192 ${what}`);
162
+ if (proc?.wakeOnRequest) console.log("Wake-on-request: enabled (process starts on first incoming request)");
163
+ if (proc?.idleTimeoutSec) console.log(`Idle timeout: ${proc.idleTimeoutSec}s`);
164
+ console.log(`URL: ${result.url}`);
165
+ } catch (err) {
166
+ console.error(`Error: ${err.message || err}`);
167
+ process.exit(1);
168
+ } finally {
169
+ await server.disconnect().catch(() => {
170
+ });
171
+ }
172
+ }
86
173
  async function serveRemove(args, machineId) {
87
- const { connectAndGetMachine } = await import('./commands-B6zZtCuh.mjs');
174
+ const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
88
175
  const pos = positionalArgs(args);
89
176
  const name = pos[0];
90
177
  if (!name) {
@@ -104,7 +191,7 @@ async function serveRemove(args, machineId) {
104
191
  }
105
192
  }
106
193
  async function serveList(args, machineId) {
107
- const { connectAndGetMachine } = await import('./commands-B6zZtCuh.mjs');
194
+ const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
108
195
  const all = hasFlag(args, "--all", "-a");
109
196
  const json = hasFlag(args, "--json");
110
197
  const sessionId = getFlag(args, "--session");
@@ -137,7 +224,7 @@ async function serveList(args, machineId) {
137
224
  }
138
225
  }
139
226
  async function serveInfo(machineId) {
140
- const { connectAndGetMachine } = await import('./commands-B6zZtCuh.mjs');
227
+ const { connectAndGetMachine } = await import('./commands-JWrmpGcs.mjs');
141
228
  const { machine, server } = await connectAndGetMachine(machineId);
142
229
  try {
143
230
  const info = await machine.serveInfo();
@@ -164,6 +251,9 @@ Multiple sessions can register different mount points without port conflicts.
164
251
  Usage:
165
252
  svamp serve <name> [directory] Add a mount and print its URL (dir defaults to .)
166
253
  svamp serve add <name> [directory] Same as above
254
+ svamp serve apply <yaml> Apply a declarative mount config (idempotent).
255
+ Supports static and managed-process mounts
256
+ with wake-on-request and idle timeout.
167
257
  svamp serve remove <name> Remove a mount
168
258
  svamp serve list [--all] [--json] List mounts (default: current session only)
169
259
  svamp serve info Show server status and URL
@@ -183,8 +273,21 @@ Examples:
183
273
  svamp serve my-report ./output # Owner-only (default)
184
274
  svamp serve dashboard ./dist --public # Anyone can access
185
275
  svamp serve data ./csv --access a@x.com,b@y.com # Specific users
276
+ svamp serve apply my-app.yaml # Declarative apply
186
277
  svamp serve list --all # Show all mounts
187
278
  svamp serve remove my-report # Stop serving
279
+
280
+ Declarative apply (svamp serve apply <yaml>):
281
+ name: my-app
282
+ process:
283
+ command: npm
284
+ args: [start]
285
+ port: 3000
286
+ workdir: .
287
+ warmup_path: /
288
+ idle_timeout_sec: 600
289
+ wake_on_request: true
290
+ access: public
188
291
  `);
189
292
  }
190
293
 
@@ -1,3 +1,4 @@
1
+ import { spawn } from 'child_process';
1
2
  import * as fs from 'fs';
2
3
  import * as http from 'http';
3
4
  import * as net from 'net';
@@ -281,6 +282,10 @@ class ServeManager {
281
282
  caddy = null;
282
283
  proxyServer = null;
283
284
  auth = null;
285
+ /** Live child processes for managed mounts. Keyed by mount name. */
286
+ managedProcs = /* @__PURE__ */ new Map();
287
+ /** Single timer that scans managed mounts every 30s for idle eviction. */
288
+ idleTimer = null;
284
289
  persistFile;
285
290
  log;
286
291
  hyphaServerUrl;
@@ -292,35 +297,72 @@ class ServeManager {
292
297
  }
293
298
  // ── Public API ───────────────────────────────────────────────────────
294
299
  /**
295
- * Add a mount and start Caddy + frpc tunnel if not already running.
296
- * Returns the public URL for this mount.
300
+ * Add a static mount (backward-compatible thin wrapper around applyMount).
301
+ * Throws if a mount with the same name already exists, preserving the
302
+ * pre-existing semantics that callers may depend on.
297
303
  */
298
304
  async addMount(name, directory, sessionId, access = "owner", ownerEmail) {
299
- validateMountName(name);
300
- const resolvedDir = path.resolve(directory);
301
- if (!fs.existsSync(resolvedDir)) {
302
- throw new Error(`Path does not exist: ${resolvedDir}`);
303
- }
304
305
  if (this.mounts.has(name)) {
305
306
  throw new Error(`Mount '${name}' already exists. Remove it first or choose a different name.`);
306
307
  }
308
+ return this.applyMount({ name, directory, sessionId, access, ownerEmail });
309
+ }
310
+ /**
311
+ * Apply a mount declaratively. Unifies static and managed-process mounts:
312
+ * pass `directory` for static, `process` for managed (or both — `process`
313
+ * takes precedence for routing). Replaces an existing mount with the same
314
+ * name (idempotent), so repeated `svamp serve apply` is safe.
315
+ *
316
+ * Backward-compatible: existing `addMount(name, directory, ...)` callers
317
+ * still work and produce the same result as `applyMount({name, directory, ...})`.
318
+ */
319
+ async applyMount(spec) {
320
+ validateMountName(spec.name);
321
+ if (!spec.directory && !spec.process) {
322
+ throw new Error(`Mount '${spec.name}': must specify either directory (static) or process (managed)`);
323
+ }
324
+ if (spec.process) {
325
+ if (!spec.process.command) {
326
+ throw new Error(`Mount '${spec.name}': process.command is required`);
327
+ }
328
+ if (!spec.process.port || spec.process.port <= 0) {
329
+ throw new Error(`Mount '${spec.name}': process.port must be a positive integer`);
330
+ }
331
+ }
332
+ const resolvedDir = spec.directory ? path.resolve(spec.directory) : void 0;
333
+ if (resolvedDir && !fs.existsSync(resolvedDir)) {
334
+ throw new Error(`Path does not exist: ${resolvedDir}`);
335
+ }
336
+ if (this.mounts.has(spec.name)) {
337
+ await this.removeMount(spec.name);
338
+ }
307
339
  const mount = {
308
- name,
340
+ name: spec.name,
309
341
  directory: resolvedDir,
310
- sessionId,
311
- ownerEmail,
312
- access,
342
+ process: spec.process,
343
+ sessionId: spec.sessionId,
344
+ ownerEmail: spec.ownerEmail,
345
+ access: spec.access ?? "owner",
313
346
  addedAt: Date.now()
314
347
  };
315
- this.mounts.set(name, mount);
348
+ this.mounts.set(spec.name, mount);
316
349
  await this.ensureRunning();
317
- if (this.caddy?.isRunning) {
318
- await this.caddy.addMount(name, resolvedDir);
350
+ if (resolvedDir && this.caddy?.isRunning) {
351
+ await this.caddy.addMount(spec.name, resolvedDir);
319
352
  }
320
- await this.startMountTunnel(name);
353
+ await this.startMountTunnel(spec.name);
354
+ if (spec.process && !spec.process.wakeOnRequest) {
355
+ try {
356
+ await this.ensureManagedRunning(spec.name);
357
+ } catch (err) {
358
+ this.log(`Mount '${spec.name}': initial process start failed: ${err.message}`);
359
+ }
360
+ }
361
+ this.ensureIdleTimer();
321
362
  this.persist();
322
- const url = this.getMountUrl(name);
323
- this.log(`Mount added: ${name} \u2192 ${resolvedDir} (${url ?? "tunnel pending"})`);
363
+ const url = this.getMountUrl(spec.name);
364
+ const what = spec.process ? `${spec.process.command}${spec.process.args?.length ? " " + spec.process.args.join(" ") : ""} (port ${spec.process.port})` : resolvedDir;
365
+ this.log(`Mount applied: ${spec.name} \u2192 ${what} (${url ?? "tunnel pending"})`);
324
366
  return { url: url || "", mount };
325
367
  }
326
368
  /**
@@ -331,6 +373,8 @@ class ServeManager {
331
373
  throw new Error(`Mount '${name}' not found`);
332
374
  }
333
375
  this.mounts.delete(name);
376
+ await this.stopManagedProcess(name).catch(() => {
377
+ });
334
378
  const tunnel = this.mountTunnels.get(name);
335
379
  if (tunnel) {
336
380
  try {
@@ -397,12 +441,16 @@ class ServeManager {
397
441
  if (!raw.mounts || raw.mounts.length === 0) return;
398
442
  let restoredCount = 0;
399
443
  for (const m of raw.mounts) {
400
- if (fs.existsSync(m.directory)) {
401
- this.mounts.set(m.name, m);
402
- restoredCount++;
403
- } else {
444
+ if (m.directory && !fs.existsSync(m.directory)) {
404
445
  this.log(`Skipping mount '${m.name}': directory no longer exists (${m.directory})`);
446
+ continue;
447
+ }
448
+ if (!m.directory && !m.process) {
449
+ this.log(`Skipping mount '${m.name}': neither directory nor process configured`);
450
+ continue;
405
451
  }
452
+ this.mounts.set(m.name, m);
453
+ restoredCount++;
406
454
  }
407
455
  if (restoredCount > 0) {
408
456
  this.log(`Restoring ${restoredCount} mount(s)...`);
@@ -413,7 +461,15 @@ class ServeManager {
413
461
  } catch (err) {
414
462
  this.log(`Failed to start tunnel for restored mount '${m.name}': ${err.message}`);
415
463
  }
464
+ if (m.process && !m.process.wakeOnRequest) {
465
+ try {
466
+ await this.ensureManagedRunning(m.name);
467
+ } catch (err) {
468
+ this.log(`Restored managed process '${m.name}' failed to start: ${err.message}`);
469
+ }
470
+ }
416
471
  }
472
+ this.ensureIdleTimer();
417
473
  this.persist();
418
474
  }
419
475
  } catch (err) {
@@ -421,9 +477,18 @@ class ServeManager {
421
477
  }
422
478
  }
423
479
  /**
424
- * Shut down auth proxy + Caddy + all per-mount frpc tunnels.
480
+ * Shut down auth proxy + Caddy + all per-mount frpc tunnels + all
481
+ * managed processes.
425
482
  */
426
483
  async shutdown() {
484
+ for (const name of Array.from(this.managedProcs.keys())) {
485
+ await this.stopManagedProcess(name).catch(() => {
486
+ });
487
+ }
488
+ if (this.idleTimer) {
489
+ clearInterval(this.idleTimer);
490
+ this.idleTimer = null;
491
+ }
427
492
  for (const [name, tunnel] of this.mountTunnels.entries()) {
428
493
  try {
429
494
  tunnel.destroy();
@@ -444,6 +509,141 @@ class ServeManager {
444
509
  }
445
510
  this.auth?.destroy();
446
511
  }
512
+ // ── Managed-process lifecycle ─────────────────────────────────────────
513
+ /**
514
+ * Ensure the managed process for a mount is running and warm. If a warmup
515
+ * is already in flight, awaits the same promise (no duplicate spawns).
516
+ * Throws if warmup probe never returns 200/3xx within warmupTimeoutMs.
517
+ */
518
+ async ensureManagedRunning(name) {
519
+ const mount = this.mounts.get(name);
520
+ if (!mount?.process) return;
521
+ const handle = this.managedProcs.get(name);
522
+ if (handle && handle.warmupPromise) {
523
+ return handle.warmupPromise;
524
+ }
525
+ if (handle && handle.child.exitCode === null && !handle.child.killed) {
526
+ handle.lastRequestAt = Date.now();
527
+ return;
528
+ }
529
+ const cfg = mount.process;
530
+ const child = spawn(cfg.command, cfg.args ?? [], {
531
+ cwd: cfg.workdir ?? process.cwd(),
532
+ env: { ...process.env, ...cfg.env ?? {} },
533
+ stdio: ["ignore", "pipe", "pipe"],
534
+ detached: false
535
+ });
536
+ child.stdout?.on("data", (d) => {
537
+ const line = d.toString().trimEnd();
538
+ if (line) this.log(`[${name}] ${line}`);
539
+ });
540
+ child.stderr?.on("data", (d) => {
541
+ const line = d.toString().trimEnd();
542
+ if (line) this.log(`[${name}] ${line}`);
543
+ });
544
+ child.on("exit", (code, signal) => {
545
+ this.log(`Managed process '${name}' exited (code=${code}, signal=${signal})`);
546
+ const h = this.managedProcs.get(name);
547
+ if (h && h.child === child) this.managedProcs.delete(name);
548
+ });
549
+ const warmupPath = cfg.warmupPath ?? "/";
550
+ const warmupTimeoutMs = cfg.warmupTimeoutMs ?? 3e4;
551
+ const warmupPromise = this.warmupProbe(`http://127.0.0.1:${cfg.port}${warmupPath}`, warmupTimeoutMs).then(() => {
552
+ const h = this.managedProcs.get(name);
553
+ if (h) {
554
+ h.warmupPromise = null;
555
+ h.lastRequestAt = Date.now();
556
+ }
557
+ this.log(`Managed process '${name}' ready on 127.0.0.1:${cfg.port}`);
558
+ }).catch((err) => {
559
+ this.log(`Managed process '${name}' warmup failed: ${err.message}`);
560
+ try {
561
+ child.kill("SIGTERM");
562
+ } catch {
563
+ }
564
+ this.managedProcs.delete(name);
565
+ throw err;
566
+ });
567
+ const newHandle = {
568
+ child,
569
+ pid: child.pid ?? 0,
570
+ lastRequestAt: Date.now(),
571
+ warmupPromise
572
+ };
573
+ this.managedProcs.set(name, newHandle);
574
+ this.log(`Managed process '${name}' starting: ${cfg.command} ${(cfg.args ?? []).join(" ")} (port ${cfg.port})`);
575
+ return warmupPromise;
576
+ }
577
+ /** Poll a URL until it returns <500 or the deadline passes. */
578
+ async warmupProbe(url, timeoutMs) {
579
+ const deadline = Date.now() + timeoutMs;
580
+ let lastErr;
581
+ while (Date.now() < deadline) {
582
+ try {
583
+ const ctrl = new AbortController();
584
+ const t = setTimeout(() => ctrl.abort(), 2e3);
585
+ let resp;
586
+ try {
587
+ resp = await fetch(url, {
588
+ method: "GET",
589
+ signal: ctrl.signal,
590
+ redirect: "manual"
591
+ });
592
+ } finally {
593
+ clearTimeout(t);
594
+ }
595
+ if (resp.status < 500) return;
596
+ lastErr = new Error(`status ${resp.status}`);
597
+ } catch (err) {
598
+ lastErr = err;
599
+ }
600
+ await new Promise((r) => setTimeout(r, 500));
601
+ }
602
+ throw new Error(`warmup probe ${url} did not respond within ${timeoutMs}ms (${lastErr?.message || "no response"})`);
603
+ }
604
+ /** Stop a managed process (SIGTERM + SIGKILL after 5s). */
605
+ async stopManagedProcess(name) {
606
+ const h = this.managedProcs.get(name);
607
+ if (!h) return;
608
+ this.managedProcs.delete(name);
609
+ try {
610
+ h.child.kill("SIGTERM");
611
+ } catch {
612
+ }
613
+ const child = h.child;
614
+ await new Promise((resolve) => {
615
+ const t = setTimeout(() => {
616
+ try {
617
+ child.kill("SIGKILL");
618
+ } catch {
619
+ }
620
+ resolve();
621
+ }, 5e3);
622
+ child.once("exit", () => {
623
+ clearTimeout(t);
624
+ resolve();
625
+ });
626
+ });
627
+ this.log(`Managed process '${name}' stopped`);
628
+ }
629
+ /** Idle eviction loop — stops processes that have been idle longer than configured. */
630
+ ensureIdleTimer() {
631
+ if (this.idleTimer) return;
632
+ this.idleTimer = setInterval(() => {
633
+ const now = Date.now();
634
+ for (const [name, mount] of this.mounts) {
635
+ const cfg = mount.process;
636
+ if (!cfg || !cfg.idleTimeoutSec || cfg.idleTimeoutSec <= 0) continue;
637
+ const h = this.managedProcs.get(name);
638
+ if (!h || h.warmupPromise) continue;
639
+ if (now - h.lastRequestAt >= cfg.idleTimeoutSec * 1e3) {
640
+ this.log(`Idle eviction: stopping '${name}' (idle ${Math.round((now - h.lastRequestAt) / 1e3)}s \u2265 ${cfg.idleTimeoutSec}s)`);
641
+ this.stopManagedProcess(name).catch(() => {
642
+ });
643
+ }
644
+ }
645
+ }, 3e4);
646
+ }
447
647
  // ── Internal ─────────────────────────────────────────────────────────
448
648
  /** Get the public URL for a mount (mount-specific subdomain). */
449
649
  getMountUrl(name) {
@@ -485,7 +685,9 @@ class ServeManager {
485
685
  log: (msg) => this.log(`[Caddy] ${msg}`)
486
686
  });
487
687
  for (const mount of this.mounts.values()) {
488
- await this.caddy.addMount(mount.name, mount.directory);
688
+ if (mount.directory) {
689
+ await this.caddy.addMount(mount.name, mount.directory);
690
+ }
489
691
  }
490
692
  await this.caddy.start();
491
693
  this.log(`Caddy file server started on 127.0.0.1:${this.caddyPort}`);
@@ -548,7 +750,7 @@ class ServeManager {
548
750
  return;
549
751
  }
550
752
  }
551
- if (req.method === "PUT" && mount) {
753
+ if (req.method === "PUT" && mount && mount.directory) {
552
754
  const filePath = path.join(mount.directory, basePath);
553
755
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
554
756
  const ws = fs.createWriteStream(filePath);
@@ -563,7 +765,7 @@ class ServeManager {
563
765
  });
564
766
  return;
565
767
  }
566
- if (req.method === "DELETE" && mount) {
768
+ if (req.method === "DELETE" && mount && mount.directory) {
567
769
  const filePath = path.join(mount.directory, basePath);
568
770
  try {
569
771
  fs.unlinkSync(filePath);
@@ -575,6 +777,39 @@ class ServeManager {
575
777
  }
576
778
  return;
577
779
  }
780
+ if (mount && mount.process) {
781
+ const cfg = mount.process;
782
+ if (cfg.wakeOnRequest || !this.managedProcs.has(mount.name)) {
783
+ try {
784
+ await this.ensureManagedRunning(mount.name);
785
+ } catch (err) {
786
+ res.writeHead(503, { "Content-Type": "text/plain" });
787
+ res.end(`Backend not ready: ${err?.message || err}`);
788
+ return;
789
+ }
790
+ }
791
+ const handle = this.managedProcs.get(mount.name);
792
+ if (handle) handle.lastRequestAt = Date.now();
793
+ const targetPath = mountResolvedByHost ? req.url || "/" : (basePath || "/") + (url.search || "");
794
+ const proxyReq2 = http.request({
795
+ hostname: "127.0.0.1",
796
+ port: cfg.port,
797
+ path: targetPath,
798
+ method: req.method,
799
+ headers: req.headers
800
+ }, (proxyRes) => {
801
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
802
+ proxyRes.pipe(res);
803
+ });
804
+ proxyReq2.on("error", (err) => {
805
+ if (!res.headersSent) {
806
+ res.writeHead(502);
807
+ res.end(`Backend error: ${err.message}`);
808
+ }
809
+ });
810
+ req.pipe(proxyReq2);
811
+ return;
812
+ }
578
813
  let proxyPath = req.url || "/";
579
814
  if (mountResolvedByHost && mountName) {
580
815
  const search = url.search || "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.2.46",
3
+ "version": "0.2.47",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",