svamp-cli 0.1.33 → 0.1.34
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-
|
|
94
|
+
const pkg = await import('./package-D2n0SOTg.mjs').catch(() => ({ default: { version: "unknown" } }));
|
|
95
95
|
console.log(`svamp version: ${pkg.default.version}`);
|
|
96
96
|
} else {
|
|
97
97
|
console.error(`Unknown command: ${subcommand}`);
|
package/package.json
CHANGED
package/dist/run-DaEcQsPx.mjs
DELETED
|
@@ -1,892 +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-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
|
-
args.push("--settings", opts.hookSettingsPath);
|
|
228
|
-
if (opts.claudeArgs) {
|
|
229
|
-
args.push(...opts.claudeArgs);
|
|
230
|
-
}
|
|
231
|
-
log(`[local] Spawning: claude ${args.join(" ")}`);
|
|
232
|
-
const claudeBin = findClaudeBinary();
|
|
233
|
-
if (!claudeBin) {
|
|
234
|
-
console.error("Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code");
|
|
235
|
-
return { type: "exit", code: 1 };
|
|
236
|
-
}
|
|
237
|
-
process.stdin.pause();
|
|
238
|
-
return new Promise((resolve) => {
|
|
239
|
-
const child = spawn(claudeBin, args, {
|
|
240
|
-
stdio: ["inherit", "inherit", "inherit"],
|
|
241
|
-
cwd,
|
|
242
|
-
signal: opts.abort,
|
|
243
|
-
env: process.env
|
|
244
|
-
});
|
|
245
|
-
child.on("error", (err) => {
|
|
246
|
-
log(`[local] Spawn error: ${err.message}`);
|
|
247
|
-
scanner.cleanup();
|
|
248
|
-
process.stdin.resume();
|
|
249
|
-
onThinkingChange(false);
|
|
250
|
-
if (err.code === "ABORT_ERR" || opts.abort.aborted) {
|
|
251
|
-
resolve({ type: "switch" });
|
|
252
|
-
} else {
|
|
253
|
-
resolve({ type: "exit", code: 1 });
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
child.on("exit", (code, signal) => {
|
|
257
|
-
log(`[local] Claude exited: code=${code}, signal=${signal}`);
|
|
258
|
-
scanner.cleanup();
|
|
259
|
-
process.stdin.resume();
|
|
260
|
-
onThinkingChange(false);
|
|
261
|
-
if (signal === "SIGTERM" && opts.abort.aborted) {
|
|
262
|
-
resolve({ type: "switch" });
|
|
263
|
-
} else {
|
|
264
|
-
resolve({ type: "exit", code: code ?? 0 });
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
if (opts.abort.aborted) {
|
|
268
|
-
try {
|
|
269
|
-
child.kill("SIGTERM");
|
|
270
|
-
} catch {
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
function findClaudeBinary() {
|
|
276
|
-
try {
|
|
277
|
-
const { execSync } = require("child_process");
|
|
278
|
-
const path = execSync("which claude 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
279
|
-
if (path && existsSync(path)) return path;
|
|
280
|
-
} catch {
|
|
281
|
-
}
|
|
282
|
-
return null;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async function runRemoteMode(opts) {
|
|
286
|
-
const { cwd, log, onThinkingChange, onMessage } = opts;
|
|
287
|
-
let claudeBin = null;
|
|
288
|
-
try {
|
|
289
|
-
const { execSync } = require("child_process");
|
|
290
|
-
claudeBin = execSync("which claude 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
291
|
-
} catch {
|
|
292
|
-
}
|
|
293
|
-
if (!claudeBin || !existsSync(claudeBin)) {
|
|
294
|
-
console.error("Claude Code CLI not found.");
|
|
295
|
-
return "exit";
|
|
296
|
-
}
|
|
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");
|
|
302
|
-
let exitReason = null;
|
|
303
|
-
let lastSpace = 0;
|
|
304
|
-
let lastCtrlC = 0;
|
|
305
|
-
const DOUBLE_TAP_MS = 2e3;
|
|
306
|
-
let spaceHintShown = false;
|
|
307
|
-
let ctrlcHintShown = false;
|
|
308
|
-
const stdinWasRaw = process.stdin.isRaw;
|
|
309
|
-
if (process.stdin.isTTY) {
|
|
310
|
-
process.stdin.setRawMode(true);
|
|
311
|
-
}
|
|
312
|
-
process.stdin.resume();
|
|
313
|
-
process.stdin.setEncoding("utf8");
|
|
314
|
-
const keyHandler = (data) => {
|
|
315
|
-
const now = Date.now();
|
|
316
|
-
if (data === "") {
|
|
317
|
-
if (now - lastCtrlC < DOUBLE_TAP_MS) {
|
|
318
|
-
exitReason = "exit";
|
|
319
|
-
abortController.abort();
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
lastCtrlC = now;
|
|
323
|
-
if (!ctrlcHintShown) {
|
|
324
|
-
ctrlcHintShown = true;
|
|
325
|
-
process.stdout.write("\n\x1B[33m Press Ctrl-C again to exit\x1B[0m\n");
|
|
326
|
-
setTimeout(() => {
|
|
327
|
-
ctrlcHintShown = false;
|
|
328
|
-
}, DOUBLE_TAP_MS);
|
|
329
|
-
}
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
if (data === " ") {
|
|
333
|
-
if (now - lastSpace < DOUBLE_TAP_MS) {
|
|
334
|
-
exitReason = "switch";
|
|
335
|
-
abortController.abort();
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
lastSpace = now;
|
|
339
|
-
if (!spaceHintShown) {
|
|
340
|
-
spaceHintShown = true;
|
|
341
|
-
process.stdout.write("\n\x1B[33m Press Space again to switch to local mode\x1B[0m\n");
|
|
342
|
-
setTimeout(() => {
|
|
343
|
-
spaceHintShown = false;
|
|
344
|
-
}, DOUBLE_TAP_MS);
|
|
345
|
-
}
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
lastSpace = 0;
|
|
349
|
-
lastCtrlC = 0;
|
|
350
|
-
};
|
|
351
|
-
process.stdin.on("data", keyHandler);
|
|
352
|
-
const abortController = new AbortController();
|
|
353
|
-
if (opts.abort.aborted) {
|
|
354
|
-
abortController.abort();
|
|
355
|
-
} else {
|
|
356
|
-
opts.abort.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
357
|
-
}
|
|
358
|
-
try {
|
|
359
|
-
while (!exitReason && !abortController.signal.aborted) {
|
|
360
|
-
const message = await Promise.race([
|
|
361
|
-
opts.nextMessage(),
|
|
362
|
-
new Promise((_, reject) => {
|
|
363
|
-
if (abortController.signal.aborted) reject(new Error("aborted"));
|
|
364
|
-
abortController.signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
365
|
-
})
|
|
366
|
-
]).catch(() => null);
|
|
367
|
-
if (!message || exitReason || abortController.signal.aborted) break;
|
|
368
|
-
const turnResult = await runClaudeTurn({
|
|
369
|
-
claudeBin,
|
|
370
|
-
cwd,
|
|
371
|
-
message,
|
|
372
|
-
sessionId: opts.claudeSessionId,
|
|
373
|
-
permissionMode: opts.permissionMode,
|
|
374
|
-
hookSettingsPath: opts.hookSettingsPath,
|
|
375
|
-
claudeArgs: opts.claudeArgs,
|
|
376
|
-
signal: abortController.signal,
|
|
377
|
-
onSessionFound: opts.onSessionFound,
|
|
378
|
-
onThinkingChange,
|
|
379
|
-
onMessage,
|
|
380
|
-
log
|
|
381
|
-
});
|
|
382
|
-
if (turnResult === "error") {
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
if (!exitReason && !abortController.signal.aborted) {
|
|
386
|
-
console.log("\n\x1B[90m Agent idle. Waiting for next message...\x1B[0m");
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
} finally {
|
|
390
|
-
process.stdin.removeListener("data", keyHandler);
|
|
391
|
-
if (process.stdin.isTTY) {
|
|
392
|
-
process.stdin.setRawMode(stdinWasRaw ?? false);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return exitReason || "exit";
|
|
396
|
-
}
|
|
397
|
-
async function runClaudeTurn(opts) {
|
|
398
|
-
const args = [
|
|
399
|
-
"--output-format",
|
|
400
|
-
"stream-json",
|
|
401
|
-
"--verbose",
|
|
402
|
-
"--input-format",
|
|
403
|
-
"stream-json",
|
|
404
|
-
"--settings",
|
|
405
|
-
opts.hookSettingsPath
|
|
406
|
-
];
|
|
407
|
-
if (opts.sessionId) {
|
|
408
|
-
args.push("--resume", opts.sessionId);
|
|
409
|
-
}
|
|
410
|
-
const claudeMode = mapPermissionMode(opts.permissionMode);
|
|
411
|
-
if (claudeMode) {
|
|
412
|
-
args.push("--permission-mode", claudeMode);
|
|
413
|
-
}
|
|
414
|
-
if (opts.claudeArgs) {
|
|
415
|
-
args.push(...opts.claudeArgs);
|
|
416
|
-
}
|
|
417
|
-
opts.log(`[remote] Spawning: claude ${args.join(" ")}`);
|
|
418
|
-
const child = spawn(opts.claudeBin, args, {
|
|
419
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
420
|
-
cwd: opts.cwd,
|
|
421
|
-
env: process.env
|
|
422
|
-
});
|
|
423
|
-
const userMsg = JSON.stringify({
|
|
424
|
-
type: "user",
|
|
425
|
-
message: { role: "user", content: opts.message }
|
|
426
|
-
});
|
|
427
|
-
child.stdin.write(userMsg + "\n");
|
|
428
|
-
child.stdin.end();
|
|
429
|
-
opts.onThinkingChange(true);
|
|
430
|
-
let currentText = "";
|
|
431
|
-
return new Promise((resolve) => {
|
|
432
|
-
const abortHandler = () => {
|
|
433
|
-
try {
|
|
434
|
-
child.kill("SIGTERM");
|
|
435
|
-
} catch {
|
|
436
|
-
}
|
|
437
|
-
};
|
|
438
|
-
if (opts.signal.aborted) {
|
|
439
|
-
abortHandler();
|
|
440
|
-
} else {
|
|
441
|
-
opts.signal.addEventListener("abort", abortHandler, { once: true });
|
|
442
|
-
}
|
|
443
|
-
const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
444
|
-
rl.on("line", (line) => {
|
|
445
|
-
const trimmed = line.trim();
|
|
446
|
-
if (!trimmed) return;
|
|
447
|
-
let msg;
|
|
448
|
-
try {
|
|
449
|
-
msg = JSON.parse(trimmed);
|
|
450
|
-
} catch {
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
if (msg.type === "control_request") {
|
|
454
|
-
handleControlRequest(msg, child, opts.log);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
handleSDKMessage(msg, opts, (text) => {
|
|
458
|
-
process.stdout.write(text);
|
|
459
|
-
currentText += text;
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
if (child.stderr) {
|
|
463
|
-
child.stderr.on("data", (data) => {
|
|
464
|
-
opts.log(`[remote:stderr] ${data.toString().trim()}`);
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
child.on("error", (err) => {
|
|
468
|
-
opts.log(`[remote] Error: ${err.message}`);
|
|
469
|
-
opts.onThinkingChange(false);
|
|
470
|
-
if (currentText && !currentText.endsWith("\n")) process.stdout.write("\n");
|
|
471
|
-
resolve("error");
|
|
472
|
-
});
|
|
473
|
-
child.on("exit", (code, signal) => {
|
|
474
|
-
opts.log(`[remote] Exit: code=${code}, signal=${signal}`);
|
|
475
|
-
opts.onThinkingChange(false);
|
|
476
|
-
if (currentText && !currentText.endsWith("\n")) process.stdout.write("\n");
|
|
477
|
-
resolve(code === 0 || signal === "SIGTERM" ? "ok" : "error");
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
function handleSDKMessage(msg, opts, write) {
|
|
482
|
-
switch (msg.type) {
|
|
483
|
-
case "system": {
|
|
484
|
-
if (msg.subtype === "init" && msg.session_id) {
|
|
485
|
-
opts.onSessionFound(msg.session_id);
|
|
486
|
-
}
|
|
487
|
-
break;
|
|
488
|
-
}
|
|
489
|
-
case "assistant": {
|
|
490
|
-
const content = msg.message?.content;
|
|
491
|
-
if (Array.isArray(content)) {
|
|
492
|
-
for (const block of content) {
|
|
493
|
-
if (block.type === "text" && block.text) {
|
|
494
|
-
write(block.text);
|
|
495
|
-
} else if (block.type === "tool_use") {
|
|
496
|
-
const argsStr = JSON.stringify(block.input || {}).slice(0, 100);
|
|
497
|
-
write(`
|
|
498
|
-
\x1B[33m[tool]\x1B[0m ${block.name}(${argsStr})
|
|
499
|
-
`);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
if (msg.message) {
|
|
504
|
-
opts.onMessage({ type: "assistant", uuid: msg.uuid || msg.message?.id, message: msg.message });
|
|
505
|
-
}
|
|
506
|
-
break;
|
|
507
|
-
}
|
|
508
|
-
case "user": {
|
|
509
|
-
const content = msg.message?.content;
|
|
510
|
-
if (Array.isArray(content)) {
|
|
511
|
-
for (const block of content) {
|
|
512
|
-
if (block.type === "tool_result") {
|
|
513
|
-
const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
|
|
514
|
-
if (text.length > 0) {
|
|
515
|
-
const preview = text.length > 200 ? text.slice(0, 200) + "..." : text;
|
|
516
|
-
write(`\x1B[90m[result]\x1B[0m ${preview}
|
|
517
|
-
`);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
case "result": {
|
|
525
|
-
if (msg.result) {
|
|
526
|
-
write(`
|
|
527
|
-
\x1B[32m[done]\x1B[0m ${msg.result}
|
|
528
|
-
`);
|
|
529
|
-
}
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
|
-
default:
|
|
533
|
-
opts.log(`[remote] Unknown msg type: ${msg.type}`);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
function handleControlRequest(msg, child, log) {
|
|
537
|
-
const request = msg.request;
|
|
538
|
-
const requestId = msg.request_id;
|
|
539
|
-
if (request?.subtype === "can_use_tool") {
|
|
540
|
-
log(`[remote] Auto-approving tool: ${request.tool_name}`);
|
|
541
|
-
const response = JSON.stringify({
|
|
542
|
-
type: "control_response",
|
|
543
|
-
response: {
|
|
544
|
-
subtype: "success",
|
|
545
|
-
request_id: requestId,
|
|
546
|
-
response: {
|
|
547
|
-
behavior: "allow",
|
|
548
|
-
updatedInput: request.input
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
try {
|
|
553
|
-
child.stdin.write(response + "\n");
|
|
554
|
-
} catch {
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
function mapPermissionMode(mode) {
|
|
559
|
-
const map = {
|
|
560
|
-
"default": "default",
|
|
561
|
-
"acceptEdits": "acceptEdits",
|
|
562
|
-
"bypassPermissions": "bypassPermissions",
|
|
563
|
-
"plan": "plan",
|
|
564
|
-
"auto-approve-all": "bypassPermissions"
|
|
565
|
-
};
|
|
566
|
-
return map[mode] || null;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
async function loop(opts) {
|
|
570
|
-
const { log } = opts;
|
|
571
|
-
let mode = opts.startingMode;
|
|
572
|
-
let claudeSessionId = null;
|
|
573
|
-
const onSessionFound = (id) => {
|
|
574
|
-
if (id !== claudeSessionId) {
|
|
575
|
-
log(`[loop] Session ID: ${id}`);
|
|
576
|
-
claudeSessionId = id;
|
|
577
|
-
opts.onSessionFound(id);
|
|
578
|
-
}
|
|
579
|
-
};
|
|
580
|
-
while (true) {
|
|
581
|
-
log(`[loop] Mode: ${mode}`);
|
|
582
|
-
switch (mode) {
|
|
583
|
-
case "local": {
|
|
584
|
-
if (opts.hasRemoteMessage()) {
|
|
585
|
-
log("[loop] Pending remote message, switching to remote");
|
|
586
|
-
mode = "remote";
|
|
587
|
-
opts.onModeChange(mode);
|
|
588
|
-
break;
|
|
589
|
-
}
|
|
590
|
-
const abortController = new AbortController();
|
|
591
|
-
let messageWatcher = null;
|
|
592
|
-
messageWatcher = setInterval(() => {
|
|
593
|
-
if (opts.hasRemoteMessage() && !abortController.signal.aborted) {
|
|
594
|
-
log("[loop] Remote message received, switching to remote mode");
|
|
595
|
-
abortController.abort();
|
|
596
|
-
}
|
|
597
|
-
}, 500);
|
|
598
|
-
const result = await runLocalMode({
|
|
599
|
-
cwd: opts.cwd,
|
|
600
|
-
claudeSessionId,
|
|
601
|
-
onSessionFound,
|
|
602
|
-
onMessage: opts.onMessage,
|
|
603
|
-
onThinkingChange: opts.onThinkingChange,
|
|
604
|
-
abort: abortController.signal,
|
|
605
|
-
hookSettingsPath: opts.hookSettingsPath,
|
|
606
|
-
claudeArgs: opts.claudeArgs,
|
|
607
|
-
log
|
|
608
|
-
});
|
|
609
|
-
if (messageWatcher) clearInterval(messageWatcher);
|
|
610
|
-
if (result.type === "switch") {
|
|
611
|
-
mode = "remote";
|
|
612
|
-
opts.onModeChange(mode);
|
|
613
|
-
} else {
|
|
614
|
-
return result.code;
|
|
615
|
-
}
|
|
616
|
-
break;
|
|
617
|
-
}
|
|
618
|
-
case "remote": {
|
|
619
|
-
const abortController = new AbortController();
|
|
620
|
-
const result = await runRemoteMode({
|
|
621
|
-
cwd: opts.cwd,
|
|
622
|
-
claudeSessionId,
|
|
623
|
-
onSessionFound,
|
|
624
|
-
onMessage: opts.onMessage,
|
|
625
|
-
onThinkingChange: opts.onThinkingChange,
|
|
626
|
-
nextMessage: opts.waitForRemoteMessage,
|
|
627
|
-
permissionMode: opts.permissionMode,
|
|
628
|
-
abort: abortController.signal,
|
|
629
|
-
hookSettingsPath: opts.hookSettingsPath,
|
|
630
|
-
claudeArgs: opts.claudeArgs,
|
|
631
|
-
log
|
|
632
|
-
});
|
|
633
|
-
if (result === "switch") {
|
|
634
|
-
mode = "local";
|
|
635
|
-
opts.onModeChange(mode);
|
|
636
|
-
} else {
|
|
637
|
-
return 0;
|
|
638
|
-
}
|
|
639
|
-
break;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
|
|
646
|
-
const ENV_FILE = join(SVAMP_HOME, ".env");
|
|
647
|
-
const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
|
|
648
|
-
const DEBUG = !!process.env.DEBUG;
|
|
649
|
-
const log = (...args) => {
|
|
650
|
-
if (DEBUG) console.error("[svamp]", ...args);
|
|
651
|
-
};
|
|
652
|
-
async function runInteractive(options) {
|
|
653
|
-
const cwd = options.directory;
|
|
654
|
-
const sessionId = randomUUID();
|
|
655
|
-
const permissionMode = options.permissionMode || "default";
|
|
656
|
-
log(`Starting interactive session: ${sessionId}`);
|
|
657
|
-
log(`Directory: ${cwd}`);
|
|
658
|
-
loadDotEnv();
|
|
659
|
-
let server = null;
|
|
660
|
-
let sessionService = null;
|
|
661
|
-
const serverUrl = process.env.HYPHA_SERVER_URL;
|
|
662
|
-
const token = process.env.HYPHA_TOKEN;
|
|
663
|
-
if (serverUrl && token) {
|
|
664
|
-
try {
|
|
665
|
-
const origLog = console.log;
|
|
666
|
-
const origWarn = console.warn;
|
|
667
|
-
console.log = () => {
|
|
668
|
-
};
|
|
669
|
-
console.warn = () => {
|
|
670
|
-
};
|
|
671
|
-
server = await connectToHypha({ serverUrl, token, name: "svamp-interactive" });
|
|
672
|
-
console.log = origLog;
|
|
673
|
-
console.warn = origWarn;
|
|
674
|
-
log("Connected to Hypha");
|
|
675
|
-
} catch (err) {
|
|
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
|
-
if (server) {
|
|
719
|
-
const callbacks = {
|
|
720
|
-
onUserMessage: (content, _meta) => {
|
|
721
|
-
const text = typeof content === "string" ? content : content?.text || content?.content?.text || JSON.stringify(content);
|
|
722
|
-
log(`[hypha] User message received: ${text.slice(0, 80)}`);
|
|
723
|
-
enqueueMessage(text);
|
|
724
|
-
},
|
|
725
|
-
onAbort: () => {
|
|
726
|
-
log("[hypha] Abort requested");
|
|
727
|
-
},
|
|
728
|
-
onPermissionResponse: (_params) => {
|
|
729
|
-
log("[hypha] Permission response");
|
|
730
|
-
},
|
|
731
|
-
onSwitchMode: (mode) => {
|
|
732
|
-
log(`[hypha] Switch mode: ${mode}`);
|
|
733
|
-
},
|
|
734
|
-
onRestartClaude: async () => {
|
|
735
|
-
log("[hypha] Restart requested");
|
|
736
|
-
return { success: false, message: "Restart not supported in interactive mode" };
|
|
737
|
-
},
|
|
738
|
-
onKillSession: () => {
|
|
739
|
-
log("[hypha] Kill requested");
|
|
740
|
-
cleanup();
|
|
741
|
-
process.exit(0);
|
|
742
|
-
}
|
|
743
|
-
};
|
|
744
|
-
try {
|
|
745
|
-
sessionService = await registerSessionService(
|
|
746
|
-
server,
|
|
747
|
-
sessionId,
|
|
748
|
-
metadata,
|
|
749
|
-
{ controlledByUser: true },
|
|
750
|
-
callbacks
|
|
751
|
-
);
|
|
752
|
-
log(`Session service registered: svamp-session-${sessionId}`);
|
|
753
|
-
} catch (err) {
|
|
754
|
-
console.error(`\x1B[33mNote:\x1B[0m Could not register session on Hypha (${err.message}).`);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
let hookServer = null;
|
|
758
|
-
let hookSettings = null;
|
|
759
|
-
let claudeSessionId = options.resumeSessionId || null;
|
|
760
|
-
try {
|
|
761
|
-
hookServer = await startHookServer((id) => {
|
|
762
|
-
claudeSessionId = id;
|
|
763
|
-
log(`Claude session ID from hook: ${id}`);
|
|
764
|
-
if (sessionService) {
|
|
765
|
-
sessionService.updateMetadata({ ...metadata, claudeSessionId: id });
|
|
766
|
-
}
|
|
767
|
-
}, log);
|
|
768
|
-
hookSettings = generateHookSettings(hookServer.port);
|
|
769
|
-
log(`Hook settings: ${hookSettings.settingsPath}`);
|
|
770
|
-
} catch (err) {
|
|
771
|
-
log(`Failed to start hook server: ${err.message}`);
|
|
772
|
-
}
|
|
773
|
-
let keepAliveInterval = null;
|
|
774
|
-
if (sessionService) {
|
|
775
|
-
keepAliveInterval = setInterval(() => {
|
|
776
|
-
sessionService.sendKeepAlive(false);
|
|
777
|
-
}, 3e4);
|
|
778
|
-
}
|
|
779
|
-
const cleanup = async () => {
|
|
780
|
-
log("Cleaning up...");
|
|
781
|
-
if (keepAliveInterval) clearInterval(keepAliveInterval);
|
|
782
|
-
if (sessionService) {
|
|
783
|
-
sessionService.sendSessionEnd();
|
|
784
|
-
await sessionService.disconnect().catch(() => {
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
hookSettings?.cleanup();
|
|
788
|
-
hookServer?.stop();
|
|
789
|
-
if (server) {
|
|
790
|
-
await server.disconnect().catch(() => {
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
if (messageWaiter) {
|
|
794
|
-
messageWaiter.resolve(null);
|
|
795
|
-
messageWaiter = null;
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
let exiting = false;
|
|
799
|
-
const handleExit = async () => {
|
|
800
|
-
if (exiting) return;
|
|
801
|
-
exiting = true;
|
|
802
|
-
await cleanup();
|
|
803
|
-
process.exit(0);
|
|
804
|
-
};
|
|
805
|
-
process.on("SIGTERM", handleExit);
|
|
806
|
-
process.on("SIGINT", handleExit);
|
|
807
|
-
const claudeArgs = [...options.claudeArgs || []];
|
|
808
|
-
if (options.resumeSessionId) ; else if (options.continueSession) {
|
|
809
|
-
claudeArgs.push("--continue");
|
|
810
|
-
}
|
|
811
|
-
console.log(`\x1B[36mSvamp interactive mode\x1B[0m`);
|
|
812
|
-
if (server && sessionService) {
|
|
813
|
-
console.log(`\x1B[90mSession synced to Hypha \u2014 visible in the web app\x1B[0m`);
|
|
814
|
-
console.log(`\x1B[90mSession ID: ${sessionId.slice(0, 8)}\x1B[0m`);
|
|
815
|
-
}
|
|
816
|
-
console.log("");
|
|
817
|
-
try {
|
|
818
|
-
const exitCode = await loop({
|
|
819
|
-
cwd,
|
|
820
|
-
startingMode: "local",
|
|
821
|
-
hookSettingsPath: hookSettings?.settingsPath || "",
|
|
822
|
-
permissionMode,
|
|
823
|
-
claudeArgs: claudeArgs.length > 0 ? claudeArgs : void 0,
|
|
824
|
-
log,
|
|
825
|
-
onModeChange: (mode) => {
|
|
826
|
-
log(`Mode changed: ${mode}`);
|
|
827
|
-
if (sessionService) {
|
|
828
|
-
sessionService.updateMetadata({ ...metadata, lifecycleState: "running" });
|
|
829
|
-
sessionService.sendKeepAlive(false, mode);
|
|
830
|
-
}
|
|
831
|
-
},
|
|
832
|
-
onSessionFound: (id) => {
|
|
833
|
-
claudeSessionId = id;
|
|
834
|
-
if (sessionService) {
|
|
835
|
-
sessionService.updateMetadata({ ...metadata, claudeSessionId: id });
|
|
836
|
-
}
|
|
837
|
-
},
|
|
838
|
-
onMessage: (msg) => {
|
|
839
|
-
if (!sessionService) return;
|
|
840
|
-
if (msg.type === "assistant" && msg.message) {
|
|
841
|
-
sessionService.pushMessage(msg.message, "agent");
|
|
842
|
-
} else if (msg.type === "user" && msg.message) {
|
|
843
|
-
const text = typeof msg.message.content === "string" ? msg.message.content : msg.message.content?.text || JSON.stringify(msg.message.content);
|
|
844
|
-
sessionService.pushMessage({ type: "text", text }, "user");
|
|
845
|
-
} else if (msg.type === "summary") {
|
|
846
|
-
sessionService.updateMetadata({
|
|
847
|
-
...metadata,
|
|
848
|
-
claudeSessionId: claudeSessionId || void 0,
|
|
849
|
-
summary: { text: msg.summary || "", updatedAt: Date.now() }
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
},
|
|
853
|
-
onThinkingChange: (thinking) => {
|
|
854
|
-
if (sessionService) {
|
|
855
|
-
sessionService.sendKeepAlive(thinking);
|
|
856
|
-
}
|
|
857
|
-
},
|
|
858
|
-
waitForRemoteMessage,
|
|
859
|
-
hasRemoteMessage
|
|
860
|
-
});
|
|
861
|
-
await cleanup();
|
|
862
|
-
process.exit(exitCode);
|
|
863
|
-
} catch (err) {
|
|
864
|
-
log(`Loop error: ${err.message}`);
|
|
865
|
-
await cleanup();
|
|
866
|
-
process.exit(1);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
function loadDotEnv() {
|
|
870
|
-
if (!existsSync(ENV_FILE)) return;
|
|
871
|
-
const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
|
|
872
|
-
for (const line of lines) {
|
|
873
|
-
const trimmed = line.trim();
|
|
874
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
875
|
-
const eqIdx = trimmed.indexOf("=");
|
|
876
|
-
if (eqIdx === -1) continue;
|
|
877
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
878
|
-
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
879
|
-
if (!process.env[key]) process.env[key] = value;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
function readMachineId() {
|
|
883
|
-
try {
|
|
884
|
-
if (!existsSync(DAEMON_STATE_FILE)) return null;
|
|
885
|
-
const state = JSON.parse(readFileSync(DAEMON_STATE_FILE, "utf-8"));
|
|
886
|
-
return state.machineId || null;
|
|
887
|
-
} catch {
|
|
888
|
-
return null;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
export { runInteractive };
|