svamp-cli 0.1.33 → 0.1.35

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.
package/dist/cli.mjs CHANGED
@@ -91,7 +91,7 @@ async function main() {
91
91
  } else if (!subcommand || subcommand === "start") {
92
92
  await handleInteractiveCommand();
93
93
  } else if (subcommand === "--version" || subcommand === "-v") {
94
- const pkg = await import('./package-CMmjP_vD.mjs').catch(() => ({ default: { version: "unknown" } }));
94
+ const pkg = await import('./package-DhEMNA72.mjs').catch(() => ({ default: { version: "unknown" } }));
95
95
  console.log(`svamp version: ${pkg.default.version}`);
96
96
  } else {
97
97
  console.error(`Unknown command: ${subcommand}`);
@@ -100,7 +100,7 @@ async function main() {
100
100
  }
101
101
  }
102
102
  async function handleInteractiveCommand() {
103
- const { runInteractive } = await import('./run-COUdcjR7.mjs');
103
+ const { runInteractive } = await import('./run-CbrQx9o5.mjs');
104
104
  const interactiveArgs = subcommand === "start" ? args.slice(1) : args;
105
105
  let directory = process.cwd();
106
106
  let resumeSessionId;
@@ -1,5 +1,5 @@
1
1
  var name = "svamp-cli";
2
- var version = "0.1.32";
2
+ var version = "0.1.34";
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";
@@ -0,0 +1,57 @@
1
+ var name = "svamp-cli";
2
+ var version = "0.1.35";
3
+ var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
4
+ var author = "Amun AI AB";
5
+ var license = "SEE LICENSE IN LICENSE";
6
+ var type = "module";
7
+ var bin = {
8
+ svamp: "./bin/svamp.mjs"
9
+ };
10
+ var files = [
11
+ "dist",
12
+ "bin"
13
+ ];
14
+ var main = "./dist/index.mjs";
15
+ var exports$1 = {
16
+ ".": "./dist/index.mjs",
17
+ "./cli": "./dist/cli.mjs"
18
+ };
19
+ var scripts = {
20
+ build: "tsc --noEmit && pkgroll",
21
+ typecheck: "tsc --noEmit",
22
+ "test:hypha": "node --no-warnings test/test-hypha-service.mjs",
23
+ dev: "tsx src/cli.ts",
24
+ "dev:daemon": "tsx src/cli.ts daemon start-sync",
25
+ "test:e2e": "node --no-warnings test/e2e-session-tests.mjs"
26
+ };
27
+ var dependencies = {
28
+ "@agentclientprotocol/sdk": "^0.14.1",
29
+ "@modelcontextprotocol/sdk": "^1.25.3",
30
+ "hypha-rpc": "0.21.20",
31
+ zod: "^3.24.4"
32
+ };
33
+ var devDependencies = {
34
+ "@types/node": ">=20",
35
+ pkgroll: "^2.14.2",
36
+ tsx: "^4.20.6",
37
+ typescript: "5.9.3"
38
+ };
39
+ var packageManager = "yarn@1.22.22";
40
+ var _package = {
41
+ name: name,
42
+ version: version,
43
+ description: description,
44
+ author: author,
45
+ license: license,
46
+ type: type,
47
+ bin: bin,
48
+ files: files,
49
+ main: main,
50
+ exports: exports$1,
51
+ scripts: scripts,
52
+ dependencies: dependencies,
53
+ devDependencies: devDependencies,
54
+ packageManager: packageManager
55
+ };
56
+
57
+ export { author, bin, _package as default, dependencies, description, devDependencies, exports$1 as exports, files, license, main, name, packageManager, scripts, type, version };
@@ -224,14 +224,16 @@ async function runLocalMode(opts) {
224
224
  if (opts.claudeSessionId) {
225
225
  args.push("--resume", opts.claudeSessionId);
226
226
  }
227
- args.push("--settings", opts.hookSettingsPath);
227
+ if (opts.hookSettingsPath) {
228
+ args.push("--settings", opts.hookSettingsPath);
229
+ }
228
230
  if (opts.claudeArgs) {
229
231
  args.push(...opts.claudeArgs);
230
232
  }
231
233
  log(`[local] Spawning: claude ${args.join(" ")}`);
232
234
  const claudeBin = findClaudeBinary();
233
235
  if (!claudeBin) {
234
- console.error("Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code");
236
+ process.stderr.write("Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code\n");
235
237
  return { type: "exit", code: 1 };
236
238
  }
237
239
  process.stdin.pause();
@@ -291,14 +293,15 @@ async function runRemoteMode(opts) {
291
293
  } catch {
292
294
  }
293
295
  if (!claudeBin || !existsSync(claudeBin)) {
294
- console.error("Claude Code CLI not found.");
296
+ process.stderr.write("Claude Code CLI not found.\n");
295
297
  return "exit";
296
298
  }
297
- console.log("\n\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m");
298
- console.log("\x1B[36m Remote mode\x1B[0m \u2014 processing message from web app");
299
- console.log("\x1B[90m Press Space Space to switch to local mode\x1B[0m");
300
- console.log("\x1B[90m Press Ctrl-C Ctrl-C to exit\x1B[0m");
301
- console.log("\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m\n");
299
+ const print = (s) => process.stderr.write(s + "\n");
300
+ print("\n\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m");
301
+ print("\x1B[36m Remote mode\x1B[0m \u2014 processing message from web app");
302
+ print("\x1B[90m Press Space Space to switch to local mode\x1B[0m");
303
+ print("\x1B[90m Press Ctrl-C Ctrl-C to exit\x1B[0m");
304
+ print("\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m\n");
302
305
  let exitReason = null;
303
306
  let lastSpace = 0;
304
307
  let lastCtrlC = 0;
@@ -383,7 +386,7 @@ async function runRemoteMode(opts) {
383
386
  continue;
384
387
  }
385
388
  if (!exitReason && !abortController.signal.aborted) {
386
- console.log("\n\x1B[90m Agent idle. Waiting for next message...\x1B[0m");
389
+ process.stderr.write("\n\x1B[90m Agent idle. Waiting for next message...\x1B[0m\n");
387
390
  }
388
391
  }
389
392
  } finally {
@@ -401,9 +404,12 @@ async function runClaudeTurn(opts) {
401
404
  "--verbose",
402
405
  "--input-format",
403
406
  "stream-json",
404
- "--settings",
405
- opts.hookSettingsPath
407
+ "--permission-prompt-tool",
408
+ "stdio"
406
409
  ];
410
+ if (opts.hookSettingsPath) {
411
+ args.push("--settings", opts.hookSettingsPath);
412
+ }
407
413
  if (opts.sessionId) {
408
414
  args.push("--resume", opts.sessionId);
409
415
  }
@@ -662,17 +668,11 @@ async function runInteractive(options) {
662
668
  const token = process.env.HYPHA_TOKEN;
663
669
  if (serverUrl && token) {
664
670
  try {
665
- const origLog = console.log;
666
- const origWarn = console.warn;
667
- console.log = () => {
668
- };
669
- console.warn = () => {
670
- };
671
+ suppressHyphaLogs();
671
672
  server = await connectToHypha({ serverUrl, token, name: "svamp-interactive" });
672
- console.log = origLog;
673
- console.warn = origWarn;
674
673
  log("Connected to Hypha");
675
674
  } catch (err) {
675
+ restoreConsoleLogs();
676
676
  console.error(`\x1B[33mNote:\x1B[0m Could not connect to Hypha (${err.message}). Running in offline mode.`);
677
677
  }
678
678
  } else {
@@ -715,6 +715,7 @@ async function runInteractive(options) {
715
715
  lifecycleState: "running",
716
716
  flavor: "claude"
717
717
  };
718
+ let currentMode = "local";
718
719
  if (server) {
719
720
  const callbacks = {
720
721
  onUserMessage: (content, _meta) => {
@@ -751,6 +752,7 @@ async function runInteractive(options) {
751
752
  );
752
753
  log(`Session service registered: svamp-session-${sessionId}`);
753
754
  } catch (err) {
755
+ restoreConsoleLogs();
754
756
  console.error(`\x1B[33mNote:\x1B[0m Could not register session on Hypha (${err.message}).`);
755
757
  }
756
758
  }
@@ -778,6 +780,7 @@ async function runInteractive(options) {
778
780
  }
779
781
  const cleanup = async () => {
780
782
  log("Cleaning up...");
783
+ restoreConsoleLogs();
781
784
  if (keepAliveInterval) clearInterval(keepAliveInterval);
782
785
  if (sessionService) {
783
786
  sessionService.sendSessionEnd();
@@ -808,12 +811,14 @@ async function runInteractive(options) {
808
811
  if (options.resumeSessionId) ; else if (options.continueSession) {
809
812
  claudeArgs.push("--continue");
810
813
  }
814
+ restoreConsoleLogs();
811
815
  console.log(`\x1B[36mSvamp interactive mode\x1B[0m`);
812
816
  if (server && sessionService) {
813
817
  console.log(`\x1B[90mSession synced to Hypha \u2014 visible in the web app\x1B[0m`);
814
818
  console.log(`\x1B[90mSession ID: ${sessionId.slice(0, 8)}\x1B[0m`);
815
819
  }
816
820
  console.log("");
821
+ if (server) suppressHyphaLogs();
817
822
  try {
818
823
  const exitCode = await loop({
819
824
  cwd,
@@ -824,7 +829,11 @@ async function runInteractive(options) {
824
829
  log,
825
830
  onModeChange: (mode) => {
826
831
  log(`Mode changed: ${mode}`);
832
+ currentMode = mode;
827
833
  if (sessionService) {
834
+ sessionService.updateAgentState({
835
+ controlledByUser: mode === "local"
836
+ });
828
837
  sessionService.updateMetadata({ ...metadata, lifecycleState: "running" });
829
838
  sessionService.sendKeepAlive(false, mode);
830
839
  }
@@ -888,5 +897,68 @@ function readMachineId() {
888
897
  return null;
889
898
  }
890
899
  }
900
+ let _origLog = null;
901
+ let _origWarn = null;
902
+ let _origInfo = null;
903
+ let _origStdoutWrite = null;
904
+ const HYPHA_LOG_PATTERNS = [
905
+ "WebSocket connection",
906
+ "Connection established",
907
+ "reporting services",
908
+ "Successfully registered",
909
+ "Subscribing to",
910
+ "Successfully subscribed",
911
+ "Cleaning up all sessions",
912
+ "Cleaned up session",
913
+ "ClTaned up",
914
+ "Cleaned up ",
915
+ "Handling disconnection",
916
+ "Client ws-user-",
917
+ "WebSocket connection disconnected",
918
+ "disconnected, cleaning up",
919
+ "HYPHA SESSION",
920
+ "Listener registered",
921
+ "local RPC disconnection"
922
+ ];
923
+ function isHyphaLogLine(text) {
924
+ return HYPHA_LOG_PATTERNS.some((p) => text.includes(p));
925
+ }
926
+ function suppressHyphaLogs() {
927
+ if (_origLog) return;
928
+ _origLog = console.log;
929
+ _origWarn = console.warn;
930
+ _origInfo = console.info;
931
+ _origStdoutWrite = process.stdout.write.bind(process.stdout);
932
+ console.log = (...args) => {
933
+ if (DEBUG) _origLog.call(console, "[hypha-log]", ...args);
934
+ };
935
+ console.warn = (...args) => {
936
+ if (DEBUG) _origWarn.call(console, "[hypha-warn]", ...args);
937
+ };
938
+ console.info = (...args) => {
939
+ if (DEBUG) _origInfo.call(console, "[hypha-info]", ...args);
940
+ };
941
+ process.stdout.write = function(chunk, ...rest) {
942
+ const text = typeof chunk === "string" ? chunk : chunk?.toString?.() || "";
943
+ if (isHyphaLogLine(text)) {
944
+ if (DEBUG) _origStdoutWrite(`[hypha-stdout] ${text}`);
945
+ return true;
946
+ }
947
+ return _origStdoutWrite.call(process.stdout, chunk, ...rest);
948
+ };
949
+ }
950
+ function restoreConsoleLogs() {
951
+ if (!_origLog) return;
952
+ console.log = _origLog;
953
+ console.warn = _origWarn;
954
+ console.info = _origInfo;
955
+ if (_origStdoutWrite) {
956
+ process.stdout.write = _origStdoutWrite;
957
+ }
958
+ _origLog = null;
959
+ _origWarn = null;
960
+ _origInfo = null;
961
+ _origStdoutWrite = null;
962
+ }
891
963
 
892
964
  export { runInteractive };
@@ -233,7 +233,7 @@ async function runLocalMode(opts) {
233
233
  log(`[local] Spawning: claude ${args.join(" ")}`);
234
234
  const claudeBin = findClaudeBinary();
235
235
  if (!claudeBin) {
236
- console.error("Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code");
236
+ process.stderr.write("Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code\n");
237
237
  return { type: "exit", code: 1 };
238
238
  }
239
239
  process.stdin.pause();
@@ -293,14 +293,15 @@ async function runRemoteMode(opts) {
293
293
  } catch {
294
294
  }
295
295
  if (!claudeBin || !existsSync(claudeBin)) {
296
- console.error("Claude Code CLI not found.");
296
+ process.stderr.write("Claude Code CLI not found.\n");
297
297
  return "exit";
298
298
  }
299
- console.log("\n\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m");
300
- console.log("\x1B[36m Remote mode\x1B[0m \u2014 processing message from web app");
301
- console.log("\x1B[90m Press Space Space to switch to local mode\x1B[0m");
302
- console.log("\x1B[90m Press Ctrl-C Ctrl-C to exit\x1B[0m");
303
- console.log("\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m\n");
299
+ const print = (s) => process.stderr.write(s + "\n");
300
+ print("\n\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m");
301
+ print("\x1B[36m Remote mode\x1B[0m \u2014 processing message from web app");
302
+ print("\x1B[90m Press Space Space to switch to local mode\x1B[0m");
303
+ print("\x1B[90m Press Ctrl-C Ctrl-C to exit\x1B[0m");
304
+ print("\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m\n");
304
305
  let exitReason = null;
305
306
  let lastSpace = 0;
306
307
  let lastCtrlC = 0;
@@ -385,7 +386,7 @@ async function runRemoteMode(opts) {
385
386
  continue;
386
387
  }
387
388
  if (!exitReason && !abortController.signal.aborted) {
388
- console.log("\n\x1B[90m Agent idle. Waiting for next message...\x1B[0m");
389
+ process.stderr.write("\n\x1B[90m Agent idle. Waiting for next message...\x1B[0m\n");
389
390
  }
390
391
  }
391
392
  } finally {
@@ -667,17 +668,11 @@ async function runInteractive(options) {
667
668
  const token = process.env.HYPHA_TOKEN;
668
669
  if (serverUrl && token) {
669
670
  try {
670
- const origLog = console.log;
671
- const origWarn = console.warn;
672
- console.log = () => {
673
- };
674
- console.warn = () => {
675
- };
671
+ suppressHyphaLogs();
676
672
  server = await connectToHypha({ serverUrl, token, name: "svamp-interactive" });
677
- console.log = origLog;
678
- console.warn = origWarn;
679
673
  log("Connected to Hypha");
680
674
  } catch (err) {
675
+ restoreConsoleLogs();
681
676
  console.error(`\x1B[33mNote:\x1B[0m Could not connect to Hypha (${err.message}). Running in offline mode.`);
682
677
  }
683
678
  } else {
@@ -720,6 +715,7 @@ async function runInteractive(options) {
720
715
  lifecycleState: "running",
721
716
  flavor: "claude"
722
717
  };
718
+ let currentMode = "local";
723
719
  if (server) {
724
720
  const callbacks = {
725
721
  onUserMessage: (content, _meta) => {
@@ -756,6 +752,7 @@ async function runInteractive(options) {
756
752
  );
757
753
  log(`Session service registered: svamp-session-${sessionId}`);
758
754
  } catch (err) {
755
+ restoreConsoleLogs();
759
756
  console.error(`\x1B[33mNote:\x1B[0m Could not register session on Hypha (${err.message}).`);
760
757
  }
761
758
  }
@@ -783,6 +780,7 @@ async function runInteractive(options) {
783
780
  }
784
781
  const cleanup = async () => {
785
782
  log("Cleaning up...");
783
+ restoreConsoleLogs();
786
784
  if (keepAliveInterval) clearInterval(keepAliveInterval);
787
785
  if (sessionService) {
788
786
  sessionService.sendSessionEnd();
@@ -813,12 +811,14 @@ async function runInteractive(options) {
813
811
  if (options.resumeSessionId) ; else if (options.continueSession) {
814
812
  claudeArgs.push("--continue");
815
813
  }
814
+ restoreConsoleLogs();
816
815
  console.log(`\x1B[36mSvamp interactive mode\x1B[0m`);
817
816
  if (server && sessionService) {
818
817
  console.log(`\x1B[90mSession synced to Hypha \u2014 visible in the web app\x1B[0m`);
819
818
  console.log(`\x1B[90mSession ID: ${sessionId.slice(0, 8)}\x1B[0m`);
820
819
  }
821
820
  console.log("");
821
+ if (server) suppressHyphaLogs();
822
822
  try {
823
823
  const exitCode = await loop({
824
824
  cwd,
@@ -829,7 +829,11 @@ async function runInteractive(options) {
829
829
  log,
830
830
  onModeChange: (mode) => {
831
831
  log(`Mode changed: ${mode}`);
832
+ currentMode = mode;
832
833
  if (sessionService) {
834
+ sessionService.updateAgentState({
835
+ controlledByUser: mode === "local"
836
+ });
833
837
  sessionService.updateMetadata({ ...metadata, lifecycleState: "running" });
834
838
  sessionService.sendKeepAlive(false, mode);
835
839
  }
@@ -893,5 +897,32 @@ function readMachineId() {
893
897
  return null;
894
898
  }
895
899
  }
900
+ let _origLog = null;
901
+ let _origWarn = null;
902
+ let _origInfo = null;
903
+ function suppressHyphaLogs() {
904
+ if (_origLog) return;
905
+ _origLog = console.log;
906
+ _origWarn = console.warn;
907
+ _origInfo = console.info;
908
+ console.log = (...args) => {
909
+ if (DEBUG) _origLog.call(console, "[hypha-log]", ...args);
910
+ };
911
+ console.warn = (...args) => {
912
+ if (DEBUG) _origWarn.call(console, "[hypha-warn]", ...args);
913
+ };
914
+ console.info = (...args) => {
915
+ if (DEBUG) _origInfo.call(console, "[hypha-info]", ...args);
916
+ };
917
+ }
918
+ function restoreConsoleLogs() {
919
+ if (!_origLog) return;
920
+ console.log = _origLog;
921
+ console.warn = _origWarn;
922
+ console.info = _origInfo;
923
+ _origLog = null;
924
+ _origWarn = null;
925
+ _origInfo = null;
926
+ }
896
927
 
897
928
  export { runInteractive };
@@ -0,0 +1,964 @@
1
+ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import { randomUUID } from 'node:crypto';
2
+ import os from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { mkdirSync, writeFileSync, existsSync, unlinkSync, readFileSync, watch } from 'node:fs';
5
+ import { c as connectToHypha, a as registerSessionService } from './run-fEuWMTdD.mjs';
6
+ import { createServer } from 'node:http';
7
+ import { spawn } from 'node:child_process';
8
+ import { createInterface } from 'node:readline';
9
+ import 'os';
10
+ import 'fs/promises';
11
+ import 'fs';
12
+ import 'path';
13
+ import 'url';
14
+ import 'child_process';
15
+ import 'crypto';
16
+ import '@agentclientprotocol/sdk';
17
+ import '@modelcontextprotocol/sdk/client/index.js';
18
+ import '@modelcontextprotocol/sdk/client/stdio.js';
19
+ import '@modelcontextprotocol/sdk/types.js';
20
+ import 'zod';
21
+ import 'node:fs/promises';
22
+ import 'node:util';
23
+
24
+ async function startHookServer(onSessionHook, log) {
25
+ return new Promise((resolve, reject) => {
26
+ const server = createServer(async (req, res) => {
27
+ if (req.method === "POST" && req.url === "/hook/session-start") {
28
+ const timeout = setTimeout(() => {
29
+ if (!res.headersSent) res.writeHead(408).end("timeout");
30
+ }, 5e3);
31
+ try {
32
+ const chunks = [];
33
+ for await (const chunk of req) chunks.push(chunk);
34
+ clearTimeout(timeout);
35
+ const body = Buffer.concat(chunks).toString("utf-8");
36
+ log("[hook] Received:", body.slice(0, 200));
37
+ let data = {};
38
+ try {
39
+ data = JSON.parse(body);
40
+ } catch {
41
+ }
42
+ const sessionId = data.session_id || data.sessionId;
43
+ if (sessionId) {
44
+ log(`[hook] Session ID: ${sessionId}`);
45
+ onSessionHook(sessionId);
46
+ }
47
+ res.writeHead(200).end("ok");
48
+ } catch {
49
+ clearTimeout(timeout);
50
+ if (!res.headersSent) res.writeHead(500).end("error");
51
+ }
52
+ return;
53
+ }
54
+ res.writeHead(404).end("not found");
55
+ });
56
+ server.listen(0, "127.0.0.1", () => {
57
+ const addr = server.address();
58
+ if (!addr || typeof addr === "string") {
59
+ reject(new Error("Failed to get server address"));
60
+ return;
61
+ }
62
+ log(`[hook] Listening on port ${addr.port}`);
63
+ resolve({ port: addr.port, stop: () => server.close() });
64
+ });
65
+ server.on("error", reject);
66
+ });
67
+ }
68
+
69
+ const SVAMP_HOME$1 = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
70
+ function generateHookSettings(port) {
71
+ const hooksDir = join(SVAMP_HOME$1, "tmp", "hooks");
72
+ mkdirSync(hooksDir, { recursive: true });
73
+ const forwarderPath = join(hooksDir, `forwarder-${process.pid}.cjs`);
74
+ const forwarderCode = `#!/usr/bin/env node
75
+ const http = require('http');
76
+ const port = parseInt(process.argv[2], 10);
77
+ if (!port || isNaN(port)) process.exit(1);
78
+ const chunks = [];
79
+ process.stdin.on('data', c => chunks.push(c));
80
+ process.stdin.on('end', () => {
81
+ const body = Buffer.concat(chunks);
82
+ const req = http.request({
83
+ host: '127.0.0.1', port, method: 'POST',
84
+ path: '/hook/session-start',
85
+ headers: { 'Content-Type': 'application/json', 'Content-Length': body.length }
86
+ }, res => res.resume());
87
+ req.on('error', () => {});
88
+ req.end(body);
89
+ });
90
+ process.stdin.resume();
91
+ `;
92
+ writeFileSync(forwarderPath, forwarderCode, { mode: 493 });
93
+ const settingsPath = join(hooksDir, `session-hook-${process.pid}.json`);
94
+ const hookCommand = `node "${forwarderPath}" ${port}`;
95
+ const settings = {
96
+ hooks: {
97
+ SessionStart: [
98
+ {
99
+ matcher: "*",
100
+ hooks: [{ type: "command", command: hookCommand }]
101
+ }
102
+ ]
103
+ }
104
+ };
105
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
106
+ const cleanup = () => {
107
+ try {
108
+ if (existsSync(settingsPath)) unlinkSync(settingsPath);
109
+ } catch {
110
+ }
111
+ try {
112
+ if (existsSync(forwarderPath)) unlinkSync(forwarderPath);
113
+ } catch {
114
+ }
115
+ };
116
+ return { settingsPath, cleanup };
117
+ }
118
+
119
+ const INTERNAL_EVENT_TYPES = /* @__PURE__ */ new Set([
120
+ "file-history-snapshot",
121
+ "change",
122
+ "queue-operation"
123
+ ]);
124
+ function getProjectDir(workingDirectory) {
125
+ const projectId = resolve(workingDirectory).replace(/[^a-zA-Z0-9-]/g, "-");
126
+ const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR || join(os.homedir(), ".claude");
127
+ return join(claudeConfigDir, "projects", projectId);
128
+ }
129
+ function createSessionScanner(opts) {
130
+ const { workingDirectory, onMessage, log } = opts;
131
+ const projectDir = getProjectDir(workingDirectory);
132
+ const processedKeys = /* @__PURE__ */ new Set();
133
+ let currentSessionId = null;
134
+ let watcher = null;
135
+ let syncInterval = null;
136
+ let stopped = false;
137
+ function messageKey(msg) {
138
+ if (msg.type === "summary") return `summary:${msg.leafUuid}:${msg.summary}`;
139
+ return msg.uuid || "";
140
+ }
141
+ function readAndSync() {
142
+ if (stopped || !currentSessionId) return;
143
+ const filePath = join(projectDir, `${currentSessionId}.jsonl`);
144
+ if (!existsSync(filePath)) return;
145
+ let content;
146
+ try {
147
+ content = readFileSync(filePath, "utf-8");
148
+ } catch {
149
+ return;
150
+ }
151
+ const lines = content.split("\n");
152
+ for (const line of lines) {
153
+ const trimmed = line.trim();
154
+ if (!trimmed) continue;
155
+ let parsed;
156
+ try {
157
+ parsed = JSON.parse(trimmed);
158
+ } catch {
159
+ continue;
160
+ }
161
+ if (parsed.type && INTERNAL_EVENT_TYPES.has(parsed.type)) continue;
162
+ if (!["user", "assistant", "summary", "system"].includes(parsed.type)) continue;
163
+ const key = messageKey(parsed);
164
+ if (!key || processedKeys.has(key)) continue;
165
+ processedKeys.add(key);
166
+ onMessage(parsed);
167
+ }
168
+ }
169
+ function startWatching(sessionId) {
170
+ stopWatching();
171
+ currentSessionId = sessionId;
172
+ const filePath = join(projectDir, `${sessionId}.jsonl`);
173
+ log(`[scanner] Watching: ${filePath}`);
174
+ readAndSync();
175
+ try {
176
+ watcher = watch(filePath, { persistent: false }, () => {
177
+ readAndSync();
178
+ });
179
+ watcher.on("error", () => {
180
+ });
181
+ } catch {
182
+ }
183
+ syncInterval = setInterval(readAndSync, 2e3);
184
+ }
185
+ function stopWatching() {
186
+ if (watcher) {
187
+ try {
188
+ watcher.close();
189
+ } catch {
190
+ }
191
+ watcher = null;
192
+ }
193
+ if (syncInterval) {
194
+ clearInterval(syncInterval);
195
+ syncInterval = null;
196
+ }
197
+ }
198
+ return {
199
+ onNewSession(sessionId) {
200
+ if (sessionId === currentSessionId) return;
201
+ log(`[scanner] New session: ${sessionId}`);
202
+ startWatching(sessionId);
203
+ },
204
+ sync: readAndSync,
205
+ cleanup() {
206
+ stopped = true;
207
+ stopWatching();
208
+ if (currentSessionId) readAndSync();
209
+ }
210
+ };
211
+ }
212
+
213
+ async function runLocalMode(opts) {
214
+ const { cwd, onSessionFound, onMessage, onThinkingChange, log } = opts;
215
+ const scanner = createSessionScanner({
216
+ workingDirectory: cwd,
217
+ onMessage,
218
+ log
219
+ });
220
+ if (opts.claudeSessionId) {
221
+ scanner.onNewSession(opts.claudeSessionId);
222
+ }
223
+ const args = [];
224
+ if (opts.claudeSessionId) {
225
+ args.push("--resume", opts.claudeSessionId);
226
+ }
227
+ if (opts.hookSettingsPath) {
228
+ args.push("--settings", opts.hookSettingsPath);
229
+ }
230
+ if (opts.claudeArgs) {
231
+ args.push(...opts.claudeArgs);
232
+ }
233
+ log(`[local] Spawning: claude ${args.join(" ")}`);
234
+ const claudeBin = findClaudeBinary();
235
+ if (!claudeBin) {
236
+ process.stderr.write("Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code\n");
237
+ return { type: "exit", code: 1 };
238
+ }
239
+ process.stdin.pause();
240
+ return new Promise((resolve) => {
241
+ const child = spawn(claudeBin, args, {
242
+ stdio: ["inherit", "inherit", "inherit"],
243
+ cwd,
244
+ signal: opts.abort,
245
+ env: process.env
246
+ });
247
+ child.on("error", (err) => {
248
+ log(`[local] Spawn error: ${err.message}`);
249
+ scanner.cleanup();
250
+ process.stdin.resume();
251
+ onThinkingChange(false);
252
+ if (err.code === "ABORT_ERR" || opts.abort.aborted) {
253
+ resolve({ type: "switch" });
254
+ } else {
255
+ resolve({ type: "exit", code: 1 });
256
+ }
257
+ });
258
+ child.on("exit", (code, signal) => {
259
+ log(`[local] Claude exited: code=${code}, signal=${signal}`);
260
+ scanner.cleanup();
261
+ process.stdin.resume();
262
+ onThinkingChange(false);
263
+ if (signal === "SIGTERM" && opts.abort.aborted) {
264
+ resolve({ type: "switch" });
265
+ } else {
266
+ resolve({ type: "exit", code: code ?? 0 });
267
+ }
268
+ });
269
+ if (opts.abort.aborted) {
270
+ try {
271
+ child.kill("SIGTERM");
272
+ } catch {
273
+ }
274
+ }
275
+ });
276
+ }
277
+ function findClaudeBinary() {
278
+ try {
279
+ const { execSync } = require("child_process");
280
+ const path = execSync("which claude 2>/dev/null", { encoding: "utf-8" }).trim();
281
+ if (path && existsSync(path)) return path;
282
+ } catch {
283
+ }
284
+ return null;
285
+ }
286
+
287
+ async function runRemoteMode(opts) {
288
+ const { cwd, log, onThinkingChange, onMessage } = opts;
289
+ let claudeBin = null;
290
+ try {
291
+ const { execSync } = require("child_process");
292
+ claudeBin = execSync("which claude 2>/dev/null", { encoding: "utf-8" }).trim();
293
+ } catch {
294
+ }
295
+ if (!claudeBin || !existsSync(claudeBin)) {
296
+ process.stderr.write("Claude Code CLI not found.\n");
297
+ return "exit";
298
+ }
299
+ const print = (s) => process.stderr.write(s + "\n");
300
+ print("\n\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m");
301
+ print("\x1B[36m Remote mode\x1B[0m \u2014 processing message from web app");
302
+ print("\x1B[90m Press Space Space to switch to local mode\x1B[0m");
303
+ print("\x1B[90m Press Ctrl-C Ctrl-C to exit\x1B[0m");
304
+ print("\x1B[90m" + "\u2550".repeat(50) + "\x1B[0m\n");
305
+ let exitReason = null;
306
+ let lastSpace = 0;
307
+ let lastCtrlC = 0;
308
+ const DOUBLE_TAP_MS = 2e3;
309
+ let spaceHintShown = false;
310
+ let ctrlcHintShown = false;
311
+ const stdinWasRaw = process.stdin.isRaw;
312
+ if (process.stdin.isTTY) {
313
+ process.stdin.setRawMode(true);
314
+ }
315
+ process.stdin.resume();
316
+ process.stdin.setEncoding("utf8");
317
+ const keyHandler = (data) => {
318
+ const now = Date.now();
319
+ if (data === "") {
320
+ if (now - lastCtrlC < DOUBLE_TAP_MS) {
321
+ exitReason = "exit";
322
+ abortController.abort();
323
+ return;
324
+ }
325
+ lastCtrlC = now;
326
+ if (!ctrlcHintShown) {
327
+ ctrlcHintShown = true;
328
+ process.stdout.write("\n\x1B[33m Press Ctrl-C again to exit\x1B[0m\n");
329
+ setTimeout(() => {
330
+ ctrlcHintShown = false;
331
+ }, DOUBLE_TAP_MS);
332
+ }
333
+ return;
334
+ }
335
+ if (data === " ") {
336
+ if (now - lastSpace < DOUBLE_TAP_MS) {
337
+ exitReason = "switch";
338
+ abortController.abort();
339
+ return;
340
+ }
341
+ lastSpace = now;
342
+ if (!spaceHintShown) {
343
+ spaceHintShown = true;
344
+ process.stdout.write("\n\x1B[33m Press Space again to switch to local mode\x1B[0m\n");
345
+ setTimeout(() => {
346
+ spaceHintShown = false;
347
+ }, DOUBLE_TAP_MS);
348
+ }
349
+ return;
350
+ }
351
+ lastSpace = 0;
352
+ lastCtrlC = 0;
353
+ };
354
+ process.stdin.on("data", keyHandler);
355
+ const abortController = new AbortController();
356
+ if (opts.abort.aborted) {
357
+ abortController.abort();
358
+ } else {
359
+ opts.abort.addEventListener("abort", () => abortController.abort(), { once: true });
360
+ }
361
+ try {
362
+ while (!exitReason && !abortController.signal.aborted) {
363
+ const message = await Promise.race([
364
+ opts.nextMessage(),
365
+ new Promise((_, reject) => {
366
+ if (abortController.signal.aborted) reject(new Error("aborted"));
367
+ abortController.signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
368
+ })
369
+ ]).catch(() => null);
370
+ if (!message || exitReason || abortController.signal.aborted) break;
371
+ const turnResult = await runClaudeTurn({
372
+ claudeBin,
373
+ cwd,
374
+ message,
375
+ sessionId: opts.claudeSessionId,
376
+ permissionMode: opts.permissionMode,
377
+ hookSettingsPath: opts.hookSettingsPath,
378
+ claudeArgs: opts.claudeArgs,
379
+ signal: abortController.signal,
380
+ onSessionFound: opts.onSessionFound,
381
+ onThinkingChange,
382
+ onMessage,
383
+ log
384
+ });
385
+ if (turnResult === "error") {
386
+ continue;
387
+ }
388
+ if (!exitReason && !abortController.signal.aborted) {
389
+ process.stderr.write("\n\x1B[90m Agent idle. Waiting for next message...\x1B[0m\n");
390
+ }
391
+ }
392
+ } finally {
393
+ process.stdin.removeListener("data", keyHandler);
394
+ if (process.stdin.isTTY) {
395
+ process.stdin.setRawMode(stdinWasRaw ?? false);
396
+ }
397
+ }
398
+ return exitReason || "exit";
399
+ }
400
+ async function runClaudeTurn(opts) {
401
+ const args = [
402
+ "--output-format",
403
+ "stream-json",
404
+ "--verbose",
405
+ "--input-format",
406
+ "stream-json",
407
+ "--permission-prompt-tool",
408
+ "stdio"
409
+ ];
410
+ if (opts.hookSettingsPath) {
411
+ args.push("--settings", opts.hookSettingsPath);
412
+ }
413
+ if (opts.sessionId) {
414
+ args.push("--resume", opts.sessionId);
415
+ }
416
+ const claudeMode = mapPermissionMode(opts.permissionMode);
417
+ if (claudeMode) {
418
+ args.push("--permission-mode", claudeMode);
419
+ }
420
+ if (opts.claudeArgs) {
421
+ args.push(...opts.claudeArgs);
422
+ }
423
+ opts.log(`[remote] Spawning: claude ${args.join(" ")}`);
424
+ const child = spawn(opts.claudeBin, args, {
425
+ stdio: ["pipe", "pipe", "pipe"],
426
+ cwd: opts.cwd,
427
+ env: process.env
428
+ });
429
+ const userMsg = JSON.stringify({
430
+ type: "user",
431
+ message: { role: "user", content: opts.message }
432
+ });
433
+ child.stdin.write(userMsg + "\n");
434
+ child.stdin.end();
435
+ opts.onThinkingChange(true);
436
+ let currentText = "";
437
+ return new Promise((resolve) => {
438
+ const abortHandler = () => {
439
+ try {
440
+ child.kill("SIGTERM");
441
+ } catch {
442
+ }
443
+ };
444
+ if (opts.signal.aborted) {
445
+ abortHandler();
446
+ } else {
447
+ opts.signal.addEventListener("abort", abortHandler, { once: true });
448
+ }
449
+ const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
450
+ rl.on("line", (line) => {
451
+ const trimmed = line.trim();
452
+ if (!trimmed) return;
453
+ let msg;
454
+ try {
455
+ msg = JSON.parse(trimmed);
456
+ } catch {
457
+ return;
458
+ }
459
+ if (msg.type === "control_request") {
460
+ handleControlRequest(msg, child, opts.log);
461
+ return;
462
+ }
463
+ handleSDKMessage(msg, opts, (text) => {
464
+ process.stdout.write(text);
465
+ currentText += text;
466
+ });
467
+ });
468
+ if (child.stderr) {
469
+ child.stderr.on("data", (data) => {
470
+ opts.log(`[remote:stderr] ${data.toString().trim()}`);
471
+ });
472
+ }
473
+ child.on("error", (err) => {
474
+ opts.log(`[remote] Error: ${err.message}`);
475
+ opts.onThinkingChange(false);
476
+ if (currentText && !currentText.endsWith("\n")) process.stdout.write("\n");
477
+ resolve("error");
478
+ });
479
+ child.on("exit", (code, signal) => {
480
+ opts.log(`[remote] Exit: code=${code}, signal=${signal}`);
481
+ opts.onThinkingChange(false);
482
+ if (currentText && !currentText.endsWith("\n")) process.stdout.write("\n");
483
+ resolve(code === 0 || signal === "SIGTERM" ? "ok" : "error");
484
+ });
485
+ });
486
+ }
487
+ function handleSDKMessage(msg, opts, write) {
488
+ switch (msg.type) {
489
+ case "system": {
490
+ if (msg.subtype === "init" && msg.session_id) {
491
+ opts.onSessionFound(msg.session_id);
492
+ }
493
+ break;
494
+ }
495
+ case "assistant": {
496
+ const content = msg.message?.content;
497
+ if (Array.isArray(content)) {
498
+ for (const block of content) {
499
+ if (block.type === "text" && block.text) {
500
+ write(block.text);
501
+ } else if (block.type === "tool_use") {
502
+ const argsStr = JSON.stringify(block.input || {}).slice(0, 100);
503
+ write(`
504
+ \x1B[33m[tool]\x1B[0m ${block.name}(${argsStr})
505
+ `);
506
+ }
507
+ }
508
+ }
509
+ if (msg.message) {
510
+ opts.onMessage({ type: "assistant", uuid: msg.uuid || msg.message?.id, message: msg.message });
511
+ }
512
+ break;
513
+ }
514
+ case "user": {
515
+ const content = msg.message?.content;
516
+ if (Array.isArray(content)) {
517
+ for (const block of content) {
518
+ if (block.type === "tool_result") {
519
+ const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
520
+ if (text.length > 0) {
521
+ const preview = text.length > 200 ? text.slice(0, 200) + "..." : text;
522
+ write(`\x1B[90m[result]\x1B[0m ${preview}
523
+ `);
524
+ }
525
+ }
526
+ }
527
+ }
528
+ break;
529
+ }
530
+ case "result": {
531
+ if (msg.result) {
532
+ write(`
533
+ \x1B[32m[done]\x1B[0m ${msg.result}
534
+ `);
535
+ }
536
+ break;
537
+ }
538
+ default:
539
+ opts.log(`[remote] Unknown msg type: ${msg.type}`);
540
+ }
541
+ }
542
+ function handleControlRequest(msg, child, log) {
543
+ const request = msg.request;
544
+ const requestId = msg.request_id;
545
+ if (request?.subtype === "can_use_tool") {
546
+ log(`[remote] Auto-approving tool: ${request.tool_name}`);
547
+ const response = JSON.stringify({
548
+ type: "control_response",
549
+ response: {
550
+ subtype: "success",
551
+ request_id: requestId,
552
+ response: {
553
+ behavior: "allow",
554
+ updatedInput: request.input
555
+ }
556
+ }
557
+ });
558
+ try {
559
+ child.stdin.write(response + "\n");
560
+ } catch {
561
+ }
562
+ }
563
+ }
564
+ function mapPermissionMode(mode) {
565
+ const map = {
566
+ "default": "default",
567
+ "acceptEdits": "acceptEdits",
568
+ "bypassPermissions": "bypassPermissions",
569
+ "plan": "plan",
570
+ "auto-approve-all": "bypassPermissions"
571
+ };
572
+ return map[mode] || null;
573
+ }
574
+
575
+ async function loop(opts) {
576
+ const { log } = opts;
577
+ let mode = opts.startingMode;
578
+ let claudeSessionId = null;
579
+ const onSessionFound = (id) => {
580
+ if (id !== claudeSessionId) {
581
+ log(`[loop] Session ID: ${id}`);
582
+ claudeSessionId = id;
583
+ opts.onSessionFound(id);
584
+ }
585
+ };
586
+ while (true) {
587
+ log(`[loop] Mode: ${mode}`);
588
+ switch (mode) {
589
+ case "local": {
590
+ if (opts.hasRemoteMessage()) {
591
+ log("[loop] Pending remote message, switching to remote");
592
+ mode = "remote";
593
+ opts.onModeChange(mode);
594
+ break;
595
+ }
596
+ const abortController = new AbortController();
597
+ let messageWatcher = null;
598
+ messageWatcher = setInterval(() => {
599
+ if (opts.hasRemoteMessage() && !abortController.signal.aborted) {
600
+ log("[loop] Remote message received, switching to remote mode");
601
+ abortController.abort();
602
+ }
603
+ }, 500);
604
+ const result = await runLocalMode({
605
+ cwd: opts.cwd,
606
+ claudeSessionId,
607
+ onSessionFound,
608
+ onMessage: opts.onMessage,
609
+ onThinkingChange: opts.onThinkingChange,
610
+ abort: abortController.signal,
611
+ hookSettingsPath: opts.hookSettingsPath,
612
+ claudeArgs: opts.claudeArgs,
613
+ log
614
+ });
615
+ if (messageWatcher) clearInterval(messageWatcher);
616
+ if (result.type === "switch") {
617
+ mode = "remote";
618
+ opts.onModeChange(mode);
619
+ } else {
620
+ return result.code;
621
+ }
622
+ break;
623
+ }
624
+ case "remote": {
625
+ const abortController = new AbortController();
626
+ const result = await runRemoteMode({
627
+ cwd: opts.cwd,
628
+ claudeSessionId,
629
+ onSessionFound,
630
+ onMessage: opts.onMessage,
631
+ onThinkingChange: opts.onThinkingChange,
632
+ nextMessage: opts.waitForRemoteMessage,
633
+ permissionMode: opts.permissionMode,
634
+ abort: abortController.signal,
635
+ hookSettingsPath: opts.hookSettingsPath,
636
+ claudeArgs: opts.claudeArgs,
637
+ log
638
+ });
639
+ if (result === "switch") {
640
+ mode = "local";
641
+ opts.onModeChange(mode);
642
+ } else {
643
+ return 0;
644
+ }
645
+ break;
646
+ }
647
+ }
648
+ }
649
+ }
650
+
651
+ const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
652
+ const ENV_FILE = join(SVAMP_HOME, ".env");
653
+ const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
654
+ const DEBUG = !!process.env.DEBUG;
655
+ const log = (...args) => {
656
+ if (DEBUG) console.error("[svamp]", ...args);
657
+ };
658
+ async function runInteractive(options) {
659
+ const cwd = options.directory;
660
+ const sessionId = randomUUID();
661
+ const permissionMode = options.permissionMode || "default";
662
+ log(`Starting interactive session: ${sessionId}`);
663
+ log(`Directory: ${cwd}`);
664
+ loadDotEnv();
665
+ let server = null;
666
+ let sessionService = null;
667
+ const serverUrl = process.env.HYPHA_SERVER_URL;
668
+ const token = process.env.HYPHA_TOKEN;
669
+ if (serverUrl && token) {
670
+ try {
671
+ suppressHyphaLogs();
672
+ server = await connectToHypha({ serverUrl, token, name: "svamp-interactive" });
673
+ log("Connected to Hypha");
674
+ } catch (err) {
675
+ restoreConsoleLogs();
676
+ console.error(`\x1B[33mNote:\x1B[0m Could not connect to Hypha (${err.message}). Running in offline mode.`);
677
+ }
678
+ } else {
679
+ console.error("\x1B[33mNote:\x1B[0m No Hypha credentials found. Running in offline mode.");
680
+ console.error(' Run "svamp login <url>" to enable cloud sync.\n');
681
+ }
682
+ const messageQueue = [];
683
+ let messageWaiter = null;
684
+ function enqueueMessage(text) {
685
+ if (messageWaiter) {
686
+ const w = messageWaiter;
687
+ messageWaiter = null;
688
+ w.resolve(text);
689
+ } else {
690
+ messageQueue.push(text);
691
+ }
692
+ }
693
+ function waitForRemoteMessage() {
694
+ if (messageQueue.length > 0) {
695
+ return Promise.resolve(messageQueue.shift());
696
+ }
697
+ return new Promise((resolve) => {
698
+ messageWaiter = { resolve };
699
+ });
700
+ }
701
+ function hasRemoteMessage() {
702
+ return messageQueue.length > 0;
703
+ }
704
+ const machineId = readMachineId();
705
+ const metadata = {
706
+ path: cwd,
707
+ host: os.hostname(),
708
+ os: os.platform(),
709
+ machineId: machineId || void 0,
710
+ homeDir: os.homedir(),
711
+ svampHomeDir: SVAMP_HOME,
712
+ svampLibDir: "",
713
+ svampToolsDir: "",
714
+ startedBy: "terminal",
715
+ lifecycleState: "running",
716
+ flavor: "claude"
717
+ };
718
+ let currentMode = "local";
719
+ if (server) {
720
+ const callbacks = {
721
+ onUserMessage: (content, _meta) => {
722
+ const text = typeof content === "string" ? content : content?.text || content?.content?.text || JSON.stringify(content);
723
+ log(`[hypha] User message received: ${text.slice(0, 80)}`);
724
+ enqueueMessage(text);
725
+ },
726
+ onAbort: () => {
727
+ log("[hypha] Abort requested");
728
+ },
729
+ onPermissionResponse: (_params) => {
730
+ log("[hypha] Permission response");
731
+ },
732
+ onSwitchMode: (mode) => {
733
+ log(`[hypha] Switch mode: ${mode}`);
734
+ },
735
+ onRestartClaude: async () => {
736
+ log("[hypha] Restart requested");
737
+ return { success: false, message: "Restart not supported in interactive mode" };
738
+ },
739
+ onKillSession: () => {
740
+ log("[hypha] Kill requested");
741
+ cleanup();
742
+ process.exit(0);
743
+ }
744
+ };
745
+ try {
746
+ sessionService = await registerSessionService(
747
+ server,
748
+ sessionId,
749
+ metadata,
750
+ { controlledByUser: true },
751
+ callbacks
752
+ );
753
+ log(`Session service registered: svamp-session-${sessionId}`);
754
+ } catch (err) {
755
+ restoreConsoleLogs();
756
+ console.error(`\x1B[33mNote:\x1B[0m Could not register session on Hypha (${err.message}).`);
757
+ }
758
+ }
759
+ let hookServer = null;
760
+ let hookSettings = null;
761
+ let claudeSessionId = options.resumeSessionId || null;
762
+ try {
763
+ hookServer = await startHookServer((id) => {
764
+ claudeSessionId = id;
765
+ log(`Claude session ID from hook: ${id}`);
766
+ if (sessionService) {
767
+ sessionService.updateMetadata({ ...metadata, claudeSessionId: id });
768
+ }
769
+ }, log);
770
+ hookSettings = generateHookSettings(hookServer.port);
771
+ log(`Hook settings: ${hookSettings.settingsPath}`);
772
+ } catch (err) {
773
+ log(`Failed to start hook server: ${err.message}`);
774
+ }
775
+ let keepAliveInterval = null;
776
+ if (sessionService) {
777
+ keepAliveInterval = setInterval(() => {
778
+ sessionService.sendKeepAlive(false);
779
+ }, 3e4);
780
+ }
781
+ const cleanup = async () => {
782
+ log("Cleaning up...");
783
+ if (keepAliveInterval) clearInterval(keepAliveInterval);
784
+ if (sessionService) {
785
+ sessionService.sendSessionEnd();
786
+ await sessionService.disconnect().catch(() => {
787
+ });
788
+ }
789
+ hookSettings?.cleanup();
790
+ hookServer?.stop();
791
+ if (server) {
792
+ await server.disconnect().catch(() => {
793
+ });
794
+ }
795
+ if (messageWaiter) {
796
+ messageWaiter.resolve(null);
797
+ messageWaiter = null;
798
+ }
799
+ restoreConsoleLogs();
800
+ };
801
+ let exiting = false;
802
+ const handleExit = async () => {
803
+ if (exiting) return;
804
+ exiting = true;
805
+ await cleanup();
806
+ process.exit(0);
807
+ };
808
+ process.on("SIGTERM", handleExit);
809
+ process.on("SIGINT", handleExit);
810
+ const claudeArgs = [...options.claudeArgs || []];
811
+ if (options.resumeSessionId) ; else if (options.continueSession) {
812
+ claudeArgs.push("--continue");
813
+ }
814
+ restoreConsoleLogs();
815
+ console.log(`\x1B[36mSvamp interactive mode\x1B[0m`);
816
+ if (server && sessionService) {
817
+ console.log(`\x1B[90mSession synced to Hypha \u2014 visible in the web app\x1B[0m`);
818
+ console.log(`\x1B[90mSession ID: ${sessionId.slice(0, 8)}\x1B[0m`);
819
+ }
820
+ console.log("");
821
+ if (server) suppressHyphaLogs();
822
+ try {
823
+ const exitCode = await loop({
824
+ cwd,
825
+ startingMode: "local",
826
+ hookSettingsPath: hookSettings?.settingsPath || "",
827
+ permissionMode,
828
+ claudeArgs: claudeArgs.length > 0 ? claudeArgs : void 0,
829
+ log,
830
+ onModeChange: (mode) => {
831
+ log(`Mode changed: ${mode}`);
832
+ currentMode = mode;
833
+ if (sessionService) {
834
+ sessionService.updateAgentState({
835
+ controlledByUser: mode === "local"
836
+ });
837
+ sessionService.updateMetadata({ ...metadata, lifecycleState: "running" });
838
+ sessionService.sendKeepAlive(false, mode);
839
+ }
840
+ },
841
+ onSessionFound: (id) => {
842
+ claudeSessionId = id;
843
+ if (sessionService) {
844
+ sessionService.updateMetadata({ ...metadata, claudeSessionId: id });
845
+ }
846
+ },
847
+ onMessage: (msg) => {
848
+ if (!sessionService) return;
849
+ if (msg.type === "assistant" && msg.message) {
850
+ sessionService.pushMessage(msg.message, "agent");
851
+ } else if (msg.type === "user" && msg.message) {
852
+ const text = typeof msg.message.content === "string" ? msg.message.content : msg.message.content?.text || JSON.stringify(msg.message.content);
853
+ sessionService.pushMessage({ type: "text", text }, "user");
854
+ } else if (msg.type === "summary") {
855
+ sessionService.updateMetadata({
856
+ ...metadata,
857
+ claudeSessionId: claudeSessionId || void 0,
858
+ summary: { text: msg.summary || "", updatedAt: Date.now() }
859
+ });
860
+ }
861
+ },
862
+ onThinkingChange: (thinking) => {
863
+ if (sessionService) {
864
+ sessionService.sendKeepAlive(thinking);
865
+ }
866
+ },
867
+ waitForRemoteMessage,
868
+ hasRemoteMessage
869
+ });
870
+ await cleanup();
871
+ process.exit(exitCode);
872
+ } catch (err) {
873
+ log(`Loop error: ${err.message}`);
874
+ await cleanup();
875
+ process.exit(1);
876
+ }
877
+ }
878
+ function loadDotEnv() {
879
+ if (!existsSync(ENV_FILE)) return;
880
+ const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
881
+ for (const line of lines) {
882
+ const trimmed = line.trim();
883
+ if (!trimmed || trimmed.startsWith("#")) continue;
884
+ const eqIdx = trimmed.indexOf("=");
885
+ if (eqIdx === -1) continue;
886
+ const key = trimmed.slice(0, eqIdx).trim();
887
+ const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
888
+ if (!process.env[key]) process.env[key] = value;
889
+ }
890
+ }
891
+ function readMachineId() {
892
+ try {
893
+ if (!existsSync(DAEMON_STATE_FILE)) return null;
894
+ const state = JSON.parse(readFileSync(DAEMON_STATE_FILE, "utf-8"));
895
+ return state.machineId || null;
896
+ } catch {
897
+ return null;
898
+ }
899
+ }
900
+ let _origLog = null;
901
+ let _origWarn = null;
902
+ let _origInfo = null;
903
+ let _origStdoutWrite = null;
904
+ const HYPHA_LOG_PATTERNS = [
905
+ "WebSocket connection",
906
+ "Connection established",
907
+ "reporting services",
908
+ "Successfully registered",
909
+ "Subscribing to",
910
+ "Successfully subscribed",
911
+ "Cleaning up all sessions",
912
+ "Cleaned up session",
913
+ "ClTaned up",
914
+ "Cleaned up ",
915
+ "Handling disconnection",
916
+ "Client ws-user-",
917
+ "WebSocket connection disconnected",
918
+ "disconnected, cleaning up",
919
+ "HYPHA SESSION",
920
+ "Listener registered",
921
+ "local RPC disconnection"
922
+ ];
923
+ function isHyphaLogLine(text) {
924
+ return HYPHA_LOG_PATTERNS.some((p) => text.includes(p));
925
+ }
926
+ function suppressHyphaLogs() {
927
+ if (_origLog) return;
928
+ _origLog = console.log;
929
+ _origWarn = console.warn;
930
+ _origInfo = console.info;
931
+ _origStdoutWrite = process.stdout.write.bind(process.stdout);
932
+ console.log = (...args) => {
933
+ if (DEBUG) _origLog.call(console, "[hypha-log]", ...args);
934
+ };
935
+ console.warn = (...args) => {
936
+ if (DEBUG) _origWarn.call(console, "[hypha-warn]", ...args);
937
+ };
938
+ console.info = (...args) => {
939
+ if (DEBUG) _origInfo.call(console, "[hypha-info]", ...args);
940
+ };
941
+ process.stdout.write = function(chunk, ...rest) {
942
+ const text = typeof chunk === "string" ? chunk : chunk?.toString?.() || "";
943
+ if (isHyphaLogLine(text)) {
944
+ if (DEBUG) _origStdoutWrite(`[hypha-stdout] ${text}`);
945
+ return true;
946
+ }
947
+ return _origStdoutWrite.call(process.stdout, chunk, ...rest);
948
+ };
949
+ }
950
+ function restoreConsoleLogs() {
951
+ if (!_origLog) return;
952
+ console.log = _origLog;
953
+ console.warn = _origWarn;
954
+ console.info = _origInfo;
955
+ if (_origStdoutWrite) {
956
+ process.stdout.write = _origStdoutWrite;
957
+ }
958
+ _origLog = null;
959
+ _origWarn = null;
960
+ _origInfo = null;
961
+ _origStdoutWrite = null;
962
+ }
963
+
964
+ export { runInteractive };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svamp-cli",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
5
5
  "author": "Amun AI AB",
6
6
  "license": "SEE LICENSE IN LICENSE",