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 +2 -2
- package/dist/{package-CMmjP_vD.mjs → package-D2n0SOTg.mjs} +1 -1
- package/dist/package-DhEMNA72.mjs +57 -0
- package/dist/{run-DaEcQsPx.mjs → run-4RUIHPcF.mjs} +91 -19
- package/dist/{run-COUdcjR7.mjs → run-CaGe8FtO.mjs} +47 -16
- package/dist/run-CbrQx9o5.mjs +964 -0
- package/package.json +1 -1
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-
|
|
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-
|
|
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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
+
process.stderr.write("Claude Code CLI not found.\n");
|
|
295
297
|
return "exit";
|
|
296
298
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
-
"--
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
+
process.stderr.write("Claude Code CLI not found.\n");
|
|
297
297
|
return "exit";
|
|
298
298
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|