svamp-cli 0.1.85 → 0.1.87

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.
@@ -1,1103 +0,0 @@
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-D0PoM5_v.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
- stopWatching();
207
- if (currentSessionId) readAndSync();
208
- stopped = true;
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
- "--print",
406
- opts.message
407
- ];
408
- if (opts.hookSettingsPath) {
409
- args.push("--settings", opts.hookSettingsPath);
410
- }
411
- if (opts.sessionId) {
412
- args.push("--resume", opts.sessionId);
413
- }
414
- const claudeMode = mapPermissionMode(opts.permissionMode) || "bypassPermissions";
415
- args.push("--permission-mode", claudeMode);
416
- if (opts.claudeArgs) {
417
- args.push(...opts.claudeArgs);
418
- }
419
- opts.log(`[remote] Spawning: claude ${args.join(" ")}`);
420
- process.stderr.write(`
421
- \x1B[34m \u25B6 User:\x1B[0m ${opts.message}
422
-
423
- `);
424
- const spawnEnv = { ...process.env, CLAUDE_CODE_ENTRYPOINT: "sdk-ts" };
425
- delete spawnEnv.CLAUDECODE;
426
- const child = spawn(opts.claudeBin, args, {
427
- stdio: ["pipe", "pipe", "pipe"],
428
- cwd: opts.cwd,
429
- env: spawnEnv
430
- });
431
- child.stdin.end();
432
- opts.onThinkingChange(true);
433
- let currentText = "";
434
- return new Promise((resolve) => {
435
- let killTimer;
436
- const abortHandler = () => {
437
- try {
438
- child.kill("SIGTERM");
439
- } catch {
440
- }
441
- killTimer = setTimeout(() => {
442
- try {
443
- child.kill("SIGKILL");
444
- } catch {
445
- }
446
- }, 5e3);
447
- };
448
- if (opts.signal.aborted) {
449
- abortHandler();
450
- } else {
451
- opts.signal.addEventListener("abort", abortHandler, { once: true });
452
- }
453
- const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
454
- rl.on("line", (line) => {
455
- const trimmed = line.trim();
456
- if (!trimmed) return;
457
- let msg;
458
- try {
459
- msg = JSON.parse(trimmed);
460
- } catch {
461
- return;
462
- }
463
- handleSDKMessage(msg, opts, (text) => {
464
- process.stdout.write(text);
465
- currentText += text;
466
- });
467
- });
468
- rl.on("error", (err) => {
469
- opts.log(`[remote] readline error: ${err.message}`);
470
- });
471
- if (child.stderr) {
472
- child.stderr.on("data", (data) => {
473
- opts.log(`[remote:stderr] ${data.toString().trim()}`);
474
- });
475
- }
476
- child.on("error", (err) => {
477
- if (killTimer) {
478
- clearTimeout(killTimer);
479
- killTimer = void 0;
480
- }
481
- rl.close();
482
- opts.log(`[remote] Error: ${err.message}`);
483
- opts.onThinkingChange(false);
484
- if (currentText && !currentText.endsWith("\n")) process.stdout.write("\n");
485
- resolve("error");
486
- });
487
- child.on("exit", (code, signal) => {
488
- if (killTimer) {
489
- clearTimeout(killTimer);
490
- killTimer = void 0;
491
- }
492
- rl.close();
493
- opts.log(`[remote] Exit: code=${code}, signal=${signal}`);
494
- opts.onThinkingChange(false);
495
- if (currentText && !currentText.endsWith("\n")) process.stdout.write("\n");
496
- resolve(code === 0 || signal === "SIGTERM" ? "ok" : "error");
497
- });
498
- });
499
- }
500
- function handleSDKMessage(msg, opts, write) {
501
- switch (msg.type) {
502
- case "system": {
503
- if (msg.subtype === "init" && msg.session_id) {
504
- opts.onSessionFound(msg.session_id);
505
- }
506
- break;
507
- }
508
- case "assistant": {
509
- const content = msg.message?.content;
510
- if (Array.isArray(content)) {
511
- for (const block of content) {
512
- if (block.type === "text" && block.text) {
513
- write(block.text);
514
- } else if (block.type === "tool_use") {
515
- const argsStr = JSON.stringify(block.input || {}).slice(0, 100);
516
- write(`
517
- \x1B[33m[tool]\x1B[0m ${block.name}(${argsStr})
518
- `);
519
- }
520
- }
521
- }
522
- if (msg.message) {
523
- opts.onMessage({ type: "assistant", uuid: msg.uuid || msg.message?.id, message: msg.message });
524
- }
525
- break;
526
- }
527
- case "user": {
528
- const content = msg.message?.content;
529
- if (Array.isArray(content)) {
530
- for (const block of content) {
531
- if (block.type === "tool_result") {
532
- const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
533
- if (text.length > 0) {
534
- const preview = text.length > 200 ? text.slice(0, 200) + "..." : text;
535
- write(`\x1B[90m[result]\x1B[0m ${preview}
536
- `);
537
- }
538
- }
539
- }
540
- }
541
- break;
542
- }
543
- case "result": {
544
- write(`
545
- \x1B[32m[done]\x1B[0m
546
- `);
547
- break;
548
- }
549
- default:
550
- opts.log(`[remote] Unknown msg type: ${msg.type}`);
551
- }
552
- }
553
- function mapPermissionMode(mode) {
554
- const map = {
555
- "default": "default",
556
- "acceptEdits": "acceptEdits",
557
- "bypassPermissions": "bypassPermissions",
558
- "plan": "plan",
559
- "auto-approve-all": "bypassPermissions"
560
- };
561
- return map[mode] || null;
562
- }
563
-
564
- async function loop(opts) {
565
- const { log } = opts;
566
- let mode = opts.startingMode;
567
- let claudeSessionId = null;
568
- const onSessionFound = (id) => {
569
- if (id !== claudeSessionId) {
570
- log(`[loop] Session ID: ${id}`);
571
- claudeSessionId = id;
572
- opts.onSessionFound(id);
573
- }
574
- };
575
- while (true) {
576
- log(`[loop] Mode: ${mode}`);
577
- switch (mode) {
578
- case "local": {
579
- if (opts.hasRemoteMessage()) {
580
- log("[loop] Pending remote message, switching to remote");
581
- mode = "remote";
582
- opts.onModeChange(mode);
583
- break;
584
- }
585
- const abortController = new AbortController();
586
- let messageWatcher = null;
587
- messageWatcher = setInterval(() => {
588
- if (opts.hasRemoteMessage() && !abortController.signal.aborted) {
589
- log("[loop] Remote message received, switching to remote mode");
590
- abortController.abort();
591
- }
592
- }, 500);
593
- let result;
594
- try {
595
- result = await runLocalMode({
596
- cwd: opts.cwd,
597
- claudeSessionId,
598
- onSessionFound,
599
- onMessage: opts.onMessage,
600
- onThinkingChange: opts.onThinkingChange,
601
- abort: abortController.signal,
602
- hookSettingsPath: opts.hookSettingsPath,
603
- claudeArgs: opts.claudeArgs,
604
- log
605
- });
606
- } finally {
607
- if (messageWatcher) {
608
- clearInterval(messageWatcher);
609
- messageWatcher = null;
610
- }
611
- }
612
- if (result.type === "switch") {
613
- mode = "remote";
614
- opts.onModeChange(mode);
615
- } else {
616
- return result.code;
617
- }
618
- break;
619
- }
620
- case "remote": {
621
- const abortController = new AbortController();
622
- const result = await runRemoteMode({
623
- cwd: opts.cwd,
624
- claudeSessionId,
625
- onSessionFound,
626
- onMessage: opts.onMessage,
627
- onThinkingChange: opts.onThinkingChange,
628
- nextMessage: opts.waitForRemoteMessage,
629
- permissionMode: opts.permissionMode,
630
- abort: abortController.signal,
631
- hookSettingsPath: opts.hookSettingsPath,
632
- claudeArgs: opts.claudeArgs,
633
- log
634
- });
635
- if (result === "switch") {
636
- mode = "local";
637
- opts.onModeChange(mode);
638
- } else {
639
- return 0;
640
- }
641
- break;
642
- }
643
- }
644
- }
645
- }
646
-
647
- const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
648
- const ENV_FILE = join(SVAMP_HOME, ".env");
649
- const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
650
- const DEBUG = !!process.env.DEBUG;
651
- const log = (...args) => {
652
- if (DEBUG) console.error("[svamp]", ...args);
653
- };
654
- async function runInteractive(options) {
655
- const cwd = options.directory;
656
- const sessionId = randomUUID();
657
- const permissionMode = options.permissionMode || "default";
658
- log(`Starting interactive session: ${sessionId}`);
659
- log(`Directory: ${cwd}`);
660
- process.env.SVAMP_SESSION_ID = sessionId;
661
- loadDotEnv();
662
- let server = null;
663
- let sessionService = null;
664
- const serverUrl = process.env.HYPHA_SERVER_URL;
665
- const token = process.env.HYPHA_TOKEN;
666
- if (serverUrl && token) {
667
- try {
668
- suppressHyphaLogs();
669
- server = await connectToHypha({ serverUrl, token, name: "svamp-interactive", transport: "http" });
670
- log("Connected to Hypha");
671
- } catch (err) {
672
- restoreConsoleLogs();
673
- console.error(`\x1B[33mNote:\x1B[0m Could not connect to Hypha (${err.message}). Running in offline mode.`);
674
- }
675
- } else {
676
- console.error("\x1B[33mNote:\x1B[0m No Hypha credentials found. Running in offline mode.");
677
- console.error(' Run "svamp login <url>" to enable cloud sync.\n');
678
- }
679
- const messageQueue = [];
680
- let messageWaiter = null;
681
- function enqueueMessage(text) {
682
- if (messageWaiter) {
683
- const w = messageWaiter;
684
- messageWaiter = null;
685
- w.resolve(text);
686
- } else {
687
- messageQueue.push(text);
688
- }
689
- }
690
- function waitForRemoteMessage() {
691
- if (messageQueue.length > 0) {
692
- return Promise.resolve(messageQueue.shift());
693
- }
694
- return new Promise((resolve2) => {
695
- messageWaiter = { resolve: resolve2 };
696
- });
697
- }
698
- function hasRemoteMessage() {
699
- return messageQueue.length > 0;
700
- }
701
- const machineId = readMachineId();
702
- const metadata = {
703
- path: cwd,
704
- host: os.hostname(),
705
- os: os.platform(),
706
- machineId: machineId || void 0,
707
- homeDir: os.homedir(),
708
- svampHomeDir: SVAMP_HOME,
709
- svampLibDir: "",
710
- svampToolsDir: "",
711
- startedBy: "terminal",
712
- lifecycleState: "running",
713
- flavor: "claude"
714
- };
715
- let currentMode = "local";
716
- if (server) {
717
- const callbacks = {
718
- onUserMessage: (content, _meta) => {
719
- const text = typeof content === "string" ? content : content?.text || content?.content?.text || JSON.stringify(content);
720
- log(`[hypha] User message received: ${text.slice(0, 80)}`);
721
- enqueueMessage(text);
722
- },
723
- onAbort: () => {
724
- log("[hypha] Abort requested");
725
- },
726
- onPermissionResponse: (_params) => {
727
- log("[hypha] Permission response");
728
- },
729
- onSwitchMode: (mode) => {
730
- log(`[hypha] Switch mode: ${mode}`);
731
- },
732
- onRestartClaude: async () => {
733
- log("[hypha] Restart requested");
734
- return { success: false, message: "Restart not supported in interactive mode" };
735
- },
736
- onKillSession: async () => {
737
- log("[hypha] Kill requested");
738
- await cleanup();
739
- process.exit(0);
740
- },
741
- // File system operations — run locally since we have direct access
742
- // All callbacks use try/catch to prevent errors from leaking
743
- // through hypha-rpc's error handler to the terminal
744
- onBash: async (command, execCwd) => {
745
- try {
746
- const { execSync } = await import('child_process');
747
- const result = execSync(command, {
748
- cwd: execCwd || cwd,
749
- encoding: "utf-8",
750
- timeout: 3e4,
751
- maxBuffer: 1024 * 1024
752
- });
753
- return { output: result };
754
- } catch (err) {
755
- return { output: err.stdout || "", error: err.message, exitCode: err.status || 1 };
756
- }
757
- },
758
- onReadFile: async (filePath) => {
759
- const resolvedPath = resolve(cwd, filePath);
760
- if (resolvedPath !== resolve(cwd) && !resolvedPath.startsWith(resolve(cwd) + "/")) {
761
- throw new Error("Path outside working directory");
762
- }
763
- try {
764
- const { readFileSync: readFileSync2 } = await import('fs');
765
- const buf = readFileSync2(resolvedPath);
766
- return buf.toString("base64");
767
- } catch (err) {
768
- throw new Error(`Cannot read file: ${err.message}`);
769
- }
770
- },
771
- onWriteFile: async (filePath, content) => {
772
- const resolvedPath = resolve(cwd, filePath);
773
- if (resolvedPath !== resolve(cwd) && !resolvedPath.startsWith(resolve(cwd) + "/")) {
774
- throw new Error("Path outside working directory");
775
- }
776
- try {
777
- const { writeFileSync, mkdirSync } = await import('fs');
778
- const { dirname: dir } = await import('path');
779
- mkdirSync(dir(resolvedPath), { recursive: true });
780
- writeFileSync(resolvedPath, Buffer.from(content, "base64"));
781
- } catch (err) {
782
- throw new Error(`Cannot write file: ${err.message}`);
783
- }
784
- },
785
- onListDirectory: async (dirPath) => {
786
- const resolvedDir = resolve(cwd, dirPath || ".");
787
- if (resolvedDir !== resolve(cwd) && !resolvedDir.startsWith(resolve(cwd) + "/")) {
788
- throw new Error("Path outside working directory");
789
- }
790
- const { readdirSync, statSync } = await import('fs');
791
- const { join: joinPath } = await import('path');
792
- return readdirSync(resolvedDir).map((name) => {
793
- try {
794
- const st = statSync(joinPath(resolvedDir, name));
795
- return { name, type: st.isDirectory() ? "directory" : "file", size: st.size };
796
- } catch {
797
- return { name, type: "unknown", size: 0 };
798
- }
799
- });
800
- },
801
- onRipgrep: async (rgArgs, execCwd) => {
802
- try {
803
- const { execSync } = await import('child_process');
804
- return execSync(`rg ${rgArgs}`, {
805
- cwd: execCwd || cwd,
806
- encoding: "utf-8",
807
- timeout: 15e3,
808
- maxBuffer: 1024 * 1024
809
- });
810
- } catch (err) {
811
- return err.stdout || "";
812
- }
813
- }
814
- };
815
- try {
816
- sessionService = await registerSessionService(
817
- server,
818
- sessionId,
819
- metadata,
820
- { controlledByUser: true },
821
- callbacks
822
- );
823
- log(`Session service registered: svamp-session-${sessionId}`);
824
- } catch (err) {
825
- restoreConsoleLogs();
826
- console.error(`\x1B[33mNote:\x1B[0m Could not register session on Hypha (${err.message}).`);
827
- }
828
- }
829
- let hookServer = null;
830
- let hookSettings = null;
831
- let claudeSessionId = options.resumeSessionId || null;
832
- try {
833
- hookServer = await startHookServer((id) => {
834
- claudeSessionId = id;
835
- log(`Claude session ID from hook: ${id}`);
836
- if (sessionService) {
837
- sessionService.updateMetadata({ ...metadata, claudeSessionId: id });
838
- }
839
- }, log);
840
- hookSettings = generateHookSettings(hookServer.port);
841
- log(`Hook settings: ${hookSettings.settingsPath}`);
842
- } catch (err) {
843
- log(`Failed to start hook server: ${err.message}`);
844
- }
845
- let keepAliveInterval = null;
846
- if (sessionService) {
847
- keepAliveInterval = setInterval(() => {
848
- try {
849
- sessionService.sendKeepAlive(false);
850
- } catch {
851
- }
852
- }, 3e4);
853
- }
854
- const cleanup = async () => {
855
- log("Cleaning up...");
856
- if (keepAliveInterval) clearInterval(keepAliveInterval);
857
- if (sessionService) {
858
- sessionService.sendSessionEnd();
859
- await sessionService.disconnect().catch(() => {
860
- });
861
- }
862
- hookSettings?.cleanup();
863
- hookServer?.stop();
864
- if (server) {
865
- await server.disconnect().catch(() => {
866
- });
867
- }
868
- if (messageWaiter) {
869
- messageWaiter.resolve(null);
870
- messageWaiter = null;
871
- }
872
- restoreConsoleLogs();
873
- };
874
- let exiting = false;
875
- const handleExit = async () => {
876
- if (exiting) return;
877
- exiting = true;
878
- await cleanup();
879
- process.exit(0);
880
- };
881
- process.on("SIGTERM", handleExit);
882
- process.on("SIGINT", handleExit);
883
- const claudeArgs = [...options.claudeArgs || []];
884
- if (options.resumeSessionId) ; else if (options.continueSession) {
885
- claudeArgs.push("--continue");
886
- }
887
- restoreConsoleLogs();
888
- console.log(`\x1B[36mSvamp interactive mode\x1B[0m`);
889
- if (server && sessionService) {
890
- const serviceId = sessionService.serviceInfo?.id || `svamp-session-${sessionId}`;
891
- const slashIdx = serviceId.indexOf("/");
892
- const colonIdx = serviceId.indexOf(":");
893
- let serviceUrl = "";
894
- if (slashIdx > 0 && colonIdx > slashIdx) {
895
- const workspace = serviceId.slice(0, slashIdx);
896
- const clientAndService = serviceId.slice(slashIdx + 1);
897
- serviceUrl = `${serverUrl}/${workspace}/services/${clientAndService}`;
898
- }
899
- console.log(`\x1B[90mSession synced to Hypha \u2014 visible in the web app\x1B[0m`);
900
- if (serviceUrl) {
901
- console.log(`\x1B[90mService: ${serviceUrl}\x1B[0m`);
902
- }
903
- console.log(`\x1B[90mSession ID: ${sessionId.slice(0, 8)}\x1B[0m`);
904
- }
905
- console.log("");
906
- if (server) suppressHyphaLogs();
907
- try {
908
- const exitCode = await loop({
909
- cwd,
910
- startingMode: "local",
911
- hookSettingsPath: hookSettings?.settingsPath || "",
912
- permissionMode,
913
- claudeArgs: claudeArgs.length > 0 ? claudeArgs : void 0,
914
- log,
915
- onModeChange: (mode) => {
916
- log(`Mode changed: ${mode}`);
917
- currentMode = mode;
918
- if (mode === "local" && messageWaiter) {
919
- messageWaiter = null;
920
- }
921
- if (sessionService) {
922
- sessionService.updateAgentState({
923
- controlledByUser: mode === "local"
924
- });
925
- sessionService.updateMetadata({
926
- ...metadata,
927
- claudeSessionId: claudeSessionId || void 0,
928
- lifecycleState: "running"
929
- });
930
- sessionService.sendKeepAlive(false, mode);
931
- }
932
- },
933
- onSessionFound: (id) => {
934
- claudeSessionId = id;
935
- if (sessionService) {
936
- sessionService.updateMetadata({ ...metadata, claudeSessionId: id });
937
- }
938
- },
939
- onMessage: (msg) => {
940
- if (!sessionService) return;
941
- if (msg.type === "assistant" && msg.message) {
942
- sessionService.pushMessage({
943
- type: "assistant",
944
- message: msg.message
945
- }, "agent");
946
- } else if (msg.type === "user" && msg.message) {
947
- const text = typeof msg.message.content === "string" ? msg.message.content : msg.message.content?.text || JSON.stringify(msg.message.content);
948
- sessionService.pushMessage({ type: "text", text }, "user");
949
- } else if (msg.type === "summary") {
950
- sessionService.updateMetadata({
951
- ...metadata,
952
- claudeSessionId: claudeSessionId || void 0,
953
- summary: { text: msg.summary || "", updatedAt: Date.now() }
954
- });
955
- }
956
- },
957
- onThinkingChange: (thinking) => {
958
- if (sessionService) {
959
- sessionService.sendKeepAlive(thinking);
960
- }
961
- },
962
- waitForRemoteMessage,
963
- hasRemoteMessage
964
- });
965
- await cleanup();
966
- process.exit(exitCode);
967
- } catch (err) {
968
- log(`Loop error: ${err.message}`);
969
- await cleanup();
970
- process.exit(1);
971
- }
972
- }
973
- function loadDotEnv() {
974
- if (!existsSync(ENV_FILE)) return;
975
- const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
976
- for (const line of lines) {
977
- const trimmed = line.trim();
978
- if (!trimmed || trimmed.startsWith("#")) continue;
979
- const eqIdx = trimmed.indexOf("=");
980
- if (eqIdx === -1) continue;
981
- const key = trimmed.slice(0, eqIdx).trim();
982
- const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
983
- if (!process.env[key]) process.env[key] = value;
984
- }
985
- }
986
- function readMachineId() {
987
- try {
988
- if (!existsSync(DAEMON_STATE_FILE)) return null;
989
- const state = JSON.parse(readFileSync(DAEMON_STATE_FILE, "utf-8"));
990
- return state.machineId || null;
991
- } catch {
992
- return null;
993
- }
994
- }
995
- let _origLog = null;
996
- let _origWarn = null;
997
- let _origInfo = null;
998
- let _origDebug = null;
999
- let _origError = null;
1000
- let _origStdoutWrite = null;
1001
- let _origStderrWrite = null;
1002
- const HYPHA_LOG_PATTERNS = [
1003
- "WebSocket connection",
1004
- "Connection established",
1005
- "reporting services",
1006
- "Successfully registered",
1007
- "Subscribing to",
1008
- "Successfully subscribed",
1009
- "Cleaning up all sessions",
1010
- "Cleaned up session",
1011
- "ClTaned up",
1012
- "Cleaned up ",
1013
- "Handling disconnection",
1014
- "Client ws-user-",
1015
- "WebSocket connection disconnected",
1016
- "disconnected, cleaning up",
1017
- "HYPHA SESSION",
1018
- "Listener registered",
1019
- "local RPC disconnection",
1020
- "not found for method",
1021
- "likely cleaned up",
1022
- "Promise method",
1023
- "not available (detected by session type)",
1024
- "Session ",
1025
- // catch-all for "Session X not found" etc.
1026
- "Failed to find method",
1027
- "Method not found",
1028
- "Error during calling method",
1029
- "Timeout subscribing",
1030
- "Manager does not support subscribe"
1031
- ];
1032
- function isHyphaLogLine(text) {
1033
- return HYPHA_LOG_PATTERNS.some((p) => text.includes(p));
1034
- }
1035
- function suppressHyphaLogs() {
1036
- if (_origLog) return;
1037
- _origLog = console.log;
1038
- _origWarn = console.warn;
1039
- _origInfo = console.info;
1040
- _origDebug = console.debug;
1041
- _origError = console.error;
1042
- _origStdoutWrite = process.stdout.write.bind(process.stdout);
1043
- _origStderrWrite = process.stderr.write.bind(process.stderr);
1044
- console.log = (...args) => {
1045
- if (DEBUG) _origLog.call(console, "[hypha-log]", ...args);
1046
- };
1047
- console.warn = (...args) => {
1048
- if (DEBUG) _origWarn.call(console, "[hypha-warn]", ...args);
1049
- };
1050
- console.info = (...args) => {
1051
- if (DEBUG) _origInfo.call(console, "[hypha-info]", ...args);
1052
- };
1053
- console.debug = (...args) => {
1054
- if (DEBUG) _origDebug.call(console, "[hypha-debug]", ...args);
1055
- };
1056
- console.error = (...args) => {
1057
- const text = args.map((a) => typeof a === "string" ? a : String(a)).join(" ");
1058
- if (isHyphaLogLine(text)) {
1059
- if (DEBUG) _origError.call(console, "[hypha-err]", ...args);
1060
- return;
1061
- }
1062
- _origError.call(console, ...args);
1063
- };
1064
- process.stdout.write = function(chunk, ...rest) {
1065
- const text = typeof chunk === "string" ? chunk : chunk?.toString?.() || "";
1066
- if (isHyphaLogLine(text)) {
1067
- if (DEBUG) _origStdoutWrite(`[hypha-stdout] ${text}`);
1068
- return true;
1069
- }
1070
- return _origStdoutWrite.call(process.stdout, chunk, ...rest);
1071
- };
1072
- process.stderr.write = function(chunk, ...rest) {
1073
- const text = typeof chunk === "string" ? chunk : chunk?.toString?.() || "";
1074
- if (isHyphaLogLine(text)) {
1075
- if (DEBUG) _origStderrWrite(`[hypha-stderr] ${text}`);
1076
- return true;
1077
- }
1078
- return _origStderrWrite.call(process.stderr, chunk, ...rest);
1079
- };
1080
- }
1081
- function restoreConsoleLogs() {
1082
- if (!_origLog) return;
1083
- console.log = _origLog;
1084
- console.warn = _origWarn;
1085
- console.info = _origInfo;
1086
- console.debug = _origDebug;
1087
- console.error = _origError;
1088
- if (_origStdoutWrite) {
1089
- process.stdout.write = _origStdoutWrite;
1090
- }
1091
- if (_origStderrWrite) {
1092
- process.stderr.write = _origStderrWrite;
1093
- }
1094
- _origLog = null;
1095
- _origWarn = null;
1096
- _origInfo = null;
1097
- _origDebug = null;
1098
- _origError = null;
1099
- _origStdoutWrite = null;
1100
- _origStderrWrite = null;
1101
- }
1102
-
1103
- export { runInteractive };