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.
- package/package.json +1 -1
- package/dist/agentCommands-BrImgZsc.mjs +0 -263
- package/dist/api-BRbsyqJ4.mjs +0 -147
- package/dist/cli.mjs +0 -1535
- package/dist/commands-2Hgmysua.mjs +0 -593
- package/dist/commands-BYbuedOK.mjs +0 -469
- package/dist/commands-DJo7kCIf.mjs +0 -1899
- package/dist/commands-DJoYOM_1.mjs +0 -531
- package/dist/index.mjs +0 -20
- package/dist/package-BXisDMWj.mjs +0 -62
- package/dist/run-D0PoM5_v.mjs +0 -8260
- package/dist/run-Ds7dVmnU.mjs +0 -1103
- package/dist/staticServer-CWcmMF5V.mjs +0 -477
- package/dist/tunnel-BDKdemh0.mjs +0 -383
|
@@ -1,1899 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { resolve, join } from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-D0PoM5_v.mjs';
|
|
6
|
-
import 'os';
|
|
7
|
-
import 'fs/promises';
|
|
8
|
-
import 'fs';
|
|
9
|
-
import 'path';
|
|
10
|
-
import 'url';
|
|
11
|
-
import 'child_process';
|
|
12
|
-
import 'crypto';
|
|
13
|
-
import 'node:crypto';
|
|
14
|
-
import '@agentclientprotocol/sdk';
|
|
15
|
-
import '@modelcontextprotocol/sdk/client/index.js';
|
|
16
|
-
import '@modelcontextprotocol/sdk/client/stdio.js';
|
|
17
|
-
import '@modelcontextprotocol/sdk/types.js';
|
|
18
|
-
import 'zod';
|
|
19
|
-
import 'node:fs/promises';
|
|
20
|
-
import 'node:util';
|
|
21
|
-
|
|
22
|
-
function formatTime(ts) {
|
|
23
|
-
if (!ts) return "-";
|
|
24
|
-
const date = new Date(ts);
|
|
25
|
-
const now = /* @__PURE__ */ new Date();
|
|
26
|
-
const diffMs = now.getTime() - date.getTime();
|
|
27
|
-
const diffMin = Math.floor(diffMs / 6e4);
|
|
28
|
-
if (diffMin < 1) return "just now";
|
|
29
|
-
if (diffMin < 60) return `${diffMin}m ago`;
|
|
30
|
-
const diffHr = Math.floor(diffMin / 60);
|
|
31
|
-
if (diffHr < 24) return `${diffHr}h ago`;
|
|
32
|
-
const diffDay = Math.floor(diffHr / 24);
|
|
33
|
-
return `${diffDay}d ago`;
|
|
34
|
-
}
|
|
35
|
-
function toMarkdownInline(value) {
|
|
36
|
-
const escaped = value.replace(/`/g, "\\`");
|
|
37
|
-
return `\`${escaped}\``;
|
|
38
|
-
}
|
|
39
|
-
function formatSessionStatus(data) {
|
|
40
|
-
const lines = [
|
|
41
|
-
"## Session Status",
|
|
42
|
-
"",
|
|
43
|
-
`- Session ID: ${toMarkdownInline(data.sessionId)}`,
|
|
44
|
-
`- Agent: ${data.flavor}`
|
|
45
|
-
];
|
|
46
|
-
if (data.name) lines.push(`- Name: ${data.name}`);
|
|
47
|
-
if (data.summary) lines.push(`- Summary: ${data.summary}`);
|
|
48
|
-
if (data.path) lines.push(`- Path: ${data.path}`);
|
|
49
|
-
if (data.host) lines.push(`- Host: ${data.host}`);
|
|
50
|
-
if (data.lifecycleState) lines.push(`- Lifecycle: ${data.lifecycleState}`);
|
|
51
|
-
lines.push(`- Active: ${data.active ? "yes" : "no"}`);
|
|
52
|
-
lines.push(`- Thinking: ${data.thinking ? "yes" : "no"}`);
|
|
53
|
-
lines.push(`- Agent Status: ${data.active ? "busy" : "idle"}`);
|
|
54
|
-
if (data.startedBy) lines.push(`- Started By: ${data.startedBy}`);
|
|
55
|
-
if (data.claudeSessionId) lines.push(`- Claude Session: ${data.claudeSessionId}`);
|
|
56
|
-
if (data.sessionLink) lines.push(`- Link: ${data.sessionLink}`);
|
|
57
|
-
if (data.pendingPermissions && data.pendingPermissions.length > 0) {
|
|
58
|
-
lines.push("");
|
|
59
|
-
lines.push("### Pending Permissions");
|
|
60
|
-
for (const p of data.pendingPermissions) {
|
|
61
|
-
const argsStr = JSON.stringify(p.arguments || {}).slice(0, 120);
|
|
62
|
-
lines.push(` [${p.id.slice(0, 8)}] ${p.tool}(${argsStr}) ${formatTime(p.createdAt)}`);
|
|
63
|
-
}
|
|
64
|
-
lines.push("");
|
|
65
|
-
lines.push(`Use: svamp session approve ${data.sessionId.slice(0, 8)}`);
|
|
66
|
-
}
|
|
67
|
-
return lines.join("\n");
|
|
68
|
-
}
|
|
69
|
-
function formatJson(data) {
|
|
70
|
-
return JSON.stringify(data, null, 2);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const SESSION_RPC_PARAMS = {
|
|
74
|
-
getMessages: ["afterSeq", "limit"],
|
|
75
|
-
sendMessage: ["content", "localId", "meta"],
|
|
76
|
-
getMetadata: [],
|
|
77
|
-
updateMetadata: ["newMetadata", "expectedVersion"],
|
|
78
|
-
updateConfig: ["patch"],
|
|
79
|
-
getAgentState: [],
|
|
80
|
-
updateAgentState: ["newState", "expectedVersion"],
|
|
81
|
-
abort: [],
|
|
82
|
-
permissionResponse: ["params"],
|
|
83
|
-
switchMode: ["mode"],
|
|
84
|
-
restartClaude: [],
|
|
85
|
-
killSession: [],
|
|
86
|
-
keepAlive: ["thinking", "mode"],
|
|
87
|
-
sessionEnd: [],
|
|
88
|
-
getActivityState: [],
|
|
89
|
-
readFile: ["path"],
|
|
90
|
-
writeFile: ["path", "content"],
|
|
91
|
-
listDirectory: ["path"],
|
|
92
|
-
bash: ["command", "cwd", "timeout"],
|
|
93
|
-
ripgrep: ["args", "cwd"],
|
|
94
|
-
getDirectoryTree: ["path", "maxDepth"],
|
|
95
|
-
getSharing: [],
|
|
96
|
-
getEffectiveRole: [],
|
|
97
|
-
updateSharing: ["newSharing"],
|
|
98
|
-
updateSecurityContext: ["newSecurityContext"],
|
|
99
|
-
applySystemPrompt: ["prompt"],
|
|
100
|
-
sendInboxMessage: ["message"],
|
|
101
|
-
getInbox: ["opts"],
|
|
102
|
-
markInboxRead: ["messageId"],
|
|
103
|
-
clearInbox: ["opts"],
|
|
104
|
-
registerListener: ["callback"]
|
|
105
|
-
};
|
|
106
|
-
function getSessionProxy(machine, sessionId) {
|
|
107
|
-
return new Proxy({}, {
|
|
108
|
-
get(_target, method) {
|
|
109
|
-
return (...args) => {
|
|
110
|
-
const paramNames = SESSION_RPC_PARAMS[method];
|
|
111
|
-
if (paramNames) {
|
|
112
|
-
const kwargs = {};
|
|
113
|
-
for (let i = 0; i < paramNames.length && i < args.length; i++) {
|
|
114
|
-
if (args[i] !== void 0) {
|
|
115
|
-
kwargs[paramNames[i]] = args[i];
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return machine.sessionRPC(sessionId, method, kwargs);
|
|
119
|
-
}
|
|
120
|
-
return machine.sessionRPC(sessionId, method, {});
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
|
|
126
|
-
const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
|
|
127
|
-
const ENV_FILE = join(SVAMP_HOME, ".env");
|
|
128
|
-
function loadDotEnv() {
|
|
129
|
-
if (!existsSync(ENV_FILE)) return;
|
|
130
|
-
const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
|
|
131
|
-
for (const line of lines) {
|
|
132
|
-
const trimmed = line.trim();
|
|
133
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
134
|
-
const eqIdx = trimmed.indexOf("=");
|
|
135
|
-
if (eqIdx === -1) continue;
|
|
136
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
137
|
-
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
138
|
-
if (!process.env[key]) {
|
|
139
|
-
process.env[key] = value;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
function readDaemonState() {
|
|
144
|
-
if (!existsSync(DAEMON_STATE_FILE)) return null;
|
|
145
|
-
try {
|
|
146
|
-
return JSON.parse(readFileSync(DAEMON_STATE_FILE, "utf-8"));
|
|
147
|
-
} catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
function isDaemonAlive(state) {
|
|
152
|
-
try {
|
|
153
|
-
process.kill(state.pid, 0);
|
|
154
|
-
return true;
|
|
155
|
-
} catch {
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
async function connectAndGetMachine(machineId) {
|
|
160
|
-
loadDotEnv();
|
|
161
|
-
const state = readDaemonState();
|
|
162
|
-
if (!state || !isDaemonAlive(state)) {
|
|
163
|
-
console.error('Daemon is not running. Start it with "svamp daemon start".');
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
|
|
167
|
-
const token = process.env.HYPHA_TOKEN;
|
|
168
|
-
if (!serverUrl) {
|
|
169
|
-
console.error('No Hypha server URL. Run "svamp login <url>" first.');
|
|
170
|
-
process.exit(1);
|
|
171
|
-
}
|
|
172
|
-
const origLog = console.log;
|
|
173
|
-
const origWarn = console.warn;
|
|
174
|
-
const origInfo = console.info;
|
|
175
|
-
const origError = console.error;
|
|
176
|
-
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
177
|
-
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
178
|
-
const isHyphaLog = (chunk) => typeof chunk === "string" && (chunk.includes("WebSocket connection") || chunk.includes("Connection established") || chunk.includes("registering service built-in") || chunk.includes("registered service") || chunk.includes("registered all") || chunk.includes("Subscribing to client_") || chunk.includes("subscribed to client_") || chunk.includes("subscribe to client_") || chunk.includes("Cleaning up all sessions") || chunk.includes("WebSocket connection disconnected") || chunk.includes("local RPC disconnection") || chunk.includes("Timeout registering service") || chunk.includes("Failed to subscribe to client_disconnected") || chunk.includes("Timeout subscribing to client_disconnected"));
|
|
179
|
-
console.log = () => {
|
|
180
|
-
};
|
|
181
|
-
console.warn = () => {
|
|
182
|
-
};
|
|
183
|
-
console.info = () => {
|
|
184
|
-
};
|
|
185
|
-
console.error = (...args) => {
|
|
186
|
-
if (args.some((a) => isHyphaLog(a))) return;
|
|
187
|
-
origError(...args);
|
|
188
|
-
};
|
|
189
|
-
process.stdout.write = (chunk, ...args) => {
|
|
190
|
-
if (isHyphaLog(chunk)) return true;
|
|
191
|
-
return stdoutWrite(chunk, ...args);
|
|
192
|
-
};
|
|
193
|
-
process.stderr.write = (chunk, ...args) => {
|
|
194
|
-
if (isHyphaLog(chunk)) return true;
|
|
195
|
-
return stderrWrite(chunk, ...args);
|
|
196
|
-
};
|
|
197
|
-
const restoreConsole = () => {
|
|
198
|
-
console.log = origLog;
|
|
199
|
-
console.warn = origWarn;
|
|
200
|
-
console.info = origInfo;
|
|
201
|
-
console.error = origError;
|
|
202
|
-
};
|
|
203
|
-
let server;
|
|
204
|
-
try {
|
|
205
|
-
server = await connectToHypha({
|
|
206
|
-
serverUrl,
|
|
207
|
-
token,
|
|
208
|
-
name: "svamp-session-cli"
|
|
209
|
-
});
|
|
210
|
-
} catch (err) {
|
|
211
|
-
restoreConsole();
|
|
212
|
-
console.error(`Failed to connect to Hypha: ${err.message}`);
|
|
213
|
-
process.exit(1);
|
|
214
|
-
}
|
|
215
|
-
let machine;
|
|
216
|
-
try {
|
|
217
|
-
const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
|
|
218
|
-
if (services.length === 0) {
|
|
219
|
-
restoreConsole();
|
|
220
|
-
console.error("No machine service found. Is the daemon registered on Hypha?");
|
|
221
|
-
await server.disconnect();
|
|
222
|
-
process.exit(1);
|
|
223
|
-
}
|
|
224
|
-
let selectedService;
|
|
225
|
-
if (machineId) {
|
|
226
|
-
const exact = services.find((s) => (s.id || s.name) === machineId);
|
|
227
|
-
if (exact) {
|
|
228
|
-
selectedService = exact;
|
|
229
|
-
} else {
|
|
230
|
-
const prefixMatches = services.filter((s) => {
|
|
231
|
-
const id = s.id || s.name;
|
|
232
|
-
return id.startsWith(machineId);
|
|
233
|
-
});
|
|
234
|
-
if (prefixMatches.length === 1) {
|
|
235
|
-
selectedService = prefixMatches[0];
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (!selectedService) {
|
|
239
|
-
const query = machineId.toLowerCase();
|
|
240
|
-
const resolved = [];
|
|
241
|
-
await Promise.all(services.map(async (svc) => {
|
|
242
|
-
const svcId2 = svc.id || svc.name;
|
|
243
|
-
try {
|
|
244
|
-
const machineSvc = await server.getService(svcId2);
|
|
245
|
-
const info = await machineSvc.getMachineInfo();
|
|
246
|
-
resolved.push({
|
|
247
|
-
svc,
|
|
248
|
-
machineId: (info.machineId || svcId2).toLowerCase(),
|
|
249
|
-
host: (info.metadata?.host || "").toLowerCase(),
|
|
250
|
-
displayName: (info.metadata?.displayName || "").toLowerCase()
|
|
251
|
-
});
|
|
252
|
-
} catch {
|
|
253
|
-
}
|
|
254
|
-
}));
|
|
255
|
-
let matches = resolved.filter(
|
|
256
|
-
(r) => r.machineId === query || r.host === query || r.displayName === query
|
|
257
|
-
);
|
|
258
|
-
if (matches.length === 0) {
|
|
259
|
-
matches = resolved.filter(
|
|
260
|
-
(r) => r.machineId.startsWith(query) || r.host.startsWith(query) || r.displayName.startsWith(query)
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
if (matches.length === 0) {
|
|
264
|
-
matches = resolved.filter(
|
|
265
|
-
(r) => r.machineId.includes(query) || r.host.includes(query) || r.displayName.includes(query)
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
if (matches.length === 1) {
|
|
269
|
-
selectedService = matches[0].svc;
|
|
270
|
-
} else if (matches.length > 1) {
|
|
271
|
-
restoreConsole();
|
|
272
|
-
console.error(`Ambiguous machine "${machineId}". Matches:`);
|
|
273
|
-
for (const m of matches) {
|
|
274
|
-
console.error(` ${m.machineId} (${m.host})`);
|
|
275
|
-
}
|
|
276
|
-
await server.disconnect();
|
|
277
|
-
process.exit(1);
|
|
278
|
-
} else {
|
|
279
|
-
restoreConsole();
|
|
280
|
-
console.error(`No machine found matching: ${machineId}`);
|
|
281
|
-
if (resolved.length > 0) {
|
|
282
|
-
console.error("Available machines:");
|
|
283
|
-
for (const r of resolved) {
|
|
284
|
-
console.error(` ${r.machineId} (${r.host})`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
await server.disconnect();
|
|
288
|
-
process.exit(1);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
if (state.hyphaClientId) {
|
|
293
|
-
const localMatch = services.find((s) => {
|
|
294
|
-
const id = s.id || s.name || "";
|
|
295
|
-
return id.includes(state.hyphaClientId);
|
|
296
|
-
});
|
|
297
|
-
selectedService = localMatch || services[0];
|
|
298
|
-
} else if (state.machineId) {
|
|
299
|
-
const localMatch = services.find((s) => {
|
|
300
|
-
const id = s.id || s.name || "";
|
|
301
|
-
return id.includes(state.machineId);
|
|
302
|
-
});
|
|
303
|
-
selectedService = localMatch || services[0];
|
|
304
|
-
} else {
|
|
305
|
-
selectedService = services[0];
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
const svcId = selectedService.id || selectedService.name;
|
|
309
|
-
machine = await server.getService(svcId);
|
|
310
|
-
} catch (err) {
|
|
311
|
-
restoreConsole();
|
|
312
|
-
console.error(`Failed to discover machine service: ${err.message}`);
|
|
313
|
-
await server.disconnect();
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
restoreConsole();
|
|
317
|
-
return { server, machine };
|
|
318
|
-
}
|
|
319
|
-
async function connectAndGetAllMachines() {
|
|
320
|
-
loadDotEnv();
|
|
321
|
-
const state = readDaemonState();
|
|
322
|
-
if (!state || !isDaemonAlive(state)) {
|
|
323
|
-
console.error('Daemon is not running. Start it with "svamp daemon start".');
|
|
324
|
-
process.exit(1);
|
|
325
|
-
}
|
|
326
|
-
const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
|
|
327
|
-
const token = process.env.HYPHA_TOKEN;
|
|
328
|
-
if (!serverUrl) {
|
|
329
|
-
console.error('No Hypha server URL. Run "svamp login <url>" first.');
|
|
330
|
-
process.exit(1);
|
|
331
|
-
}
|
|
332
|
-
const origLog = console.log;
|
|
333
|
-
const origWarn = console.warn;
|
|
334
|
-
const origInfo = console.info;
|
|
335
|
-
const origError = console.error;
|
|
336
|
-
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
337
|
-
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
338
|
-
const isHyphaLog = (chunk) => typeof chunk === "string" && (chunk.includes("WebSocket connection") || chunk.includes("Connection established") || chunk.includes("registering service built-in") || chunk.includes("registered service") || chunk.includes("registered all") || chunk.includes("Subscribing to client_") || chunk.includes("subscribed to client_") || chunk.includes("subscribe to client_") || chunk.includes("Cleaning up all sessions") || chunk.includes("WebSocket connection disconnected") || chunk.includes("local RPC disconnection") || chunk.includes("Timeout registering service") || chunk.includes("Failed to subscribe to client_disconnected") || chunk.includes("Timeout subscribing to client_disconnected"));
|
|
339
|
-
console.log = () => {
|
|
340
|
-
};
|
|
341
|
-
console.warn = () => {
|
|
342
|
-
};
|
|
343
|
-
console.info = () => {
|
|
344
|
-
};
|
|
345
|
-
console.error = (...args) => {
|
|
346
|
-
if (!args.some((a) => isHyphaLog(a))) origError(...args);
|
|
347
|
-
};
|
|
348
|
-
process.stdout.write = (chunk, ...args) => {
|
|
349
|
-
if (isHyphaLog(chunk)) return true;
|
|
350
|
-
return stdoutWrite(chunk, ...args);
|
|
351
|
-
};
|
|
352
|
-
process.stderr.write = (chunk, ...args) => {
|
|
353
|
-
if (isHyphaLog(chunk)) return true;
|
|
354
|
-
return stderrWrite(chunk, ...args);
|
|
355
|
-
};
|
|
356
|
-
const restoreConsole = () => {
|
|
357
|
-
console.log = origLog;
|
|
358
|
-
console.warn = origWarn;
|
|
359
|
-
console.info = origInfo;
|
|
360
|
-
console.error = origError;
|
|
361
|
-
};
|
|
362
|
-
let server;
|
|
363
|
-
try {
|
|
364
|
-
server = await connectToHypha({ serverUrl, token, name: "svamp-session-cli" });
|
|
365
|
-
} catch (err) {
|
|
366
|
-
restoreConsole();
|
|
367
|
-
console.error(`Failed to connect to Hypha: ${err.message}`);
|
|
368
|
-
process.exit(1);
|
|
369
|
-
}
|
|
370
|
-
const machines = [];
|
|
371
|
-
try {
|
|
372
|
-
const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
|
|
373
|
-
const results = await Promise.allSettled(
|
|
374
|
-
services.map((svc) => server.getService(svc.id || svc.name))
|
|
375
|
-
);
|
|
376
|
-
for (const r of results) {
|
|
377
|
-
if (r.status === "fulfilled") machines.push(r.value);
|
|
378
|
-
}
|
|
379
|
-
} catch (err) {
|
|
380
|
-
restoreConsole();
|
|
381
|
-
console.error(`Failed to discover machine services: ${err.message}`);
|
|
382
|
-
await server.disconnect();
|
|
383
|
-
process.exit(1);
|
|
384
|
-
}
|
|
385
|
-
restoreConsole();
|
|
386
|
-
if (machines.length === 0) {
|
|
387
|
-
console.error("No machine service found. Is the daemon registered on Hypha?");
|
|
388
|
-
await server.disconnect();
|
|
389
|
-
process.exit(1);
|
|
390
|
-
}
|
|
391
|
-
return { server, machines };
|
|
392
|
-
}
|
|
393
|
-
async function sessionMachines() {
|
|
394
|
-
loadDotEnv();
|
|
395
|
-
const state = readDaemonState();
|
|
396
|
-
if (!state || !isDaemonAlive(state)) {
|
|
397
|
-
console.error('Daemon is not running. Start it with "svamp daemon start".');
|
|
398
|
-
process.exit(1);
|
|
399
|
-
}
|
|
400
|
-
const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
|
|
401
|
-
const token = process.env.HYPHA_TOKEN;
|
|
402
|
-
if (!serverUrl) {
|
|
403
|
-
console.error('No Hypha server URL. Run "svamp login <url>" first.');
|
|
404
|
-
process.exit(1);
|
|
405
|
-
}
|
|
406
|
-
const origLog = console.log;
|
|
407
|
-
const origWarn = console.warn;
|
|
408
|
-
const origInfo = console.info;
|
|
409
|
-
const origError = console.error;
|
|
410
|
-
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
411
|
-
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
412
|
-
const isHyphaLog = (chunk) => typeof chunk === "string" && (chunk.includes("WebSocket connection") || chunk.includes("Connection established") || chunk.includes("registering service built-in") || chunk.includes("registered service") || chunk.includes("registered all") || chunk.includes("Subscribing to client_") || chunk.includes("subscribed to client_") || chunk.includes("subscribe to client_") || chunk.includes("Cleaning up all sessions") || chunk.includes("WebSocket connection disconnected") || chunk.includes("local RPC disconnection") || chunk.includes("Timeout registering service") || chunk.includes("Failed to subscribe to client_disconnected") || chunk.includes("Timeout subscribing to client_disconnected"));
|
|
413
|
-
console.log = () => {
|
|
414
|
-
};
|
|
415
|
-
console.warn = () => {
|
|
416
|
-
};
|
|
417
|
-
console.info = () => {
|
|
418
|
-
};
|
|
419
|
-
console.error = (...args) => {
|
|
420
|
-
if (args.some((a) => isHyphaLog(a))) return;
|
|
421
|
-
origError(...args);
|
|
422
|
-
};
|
|
423
|
-
process.stdout.write = (chunk, ...args) => {
|
|
424
|
-
if (isHyphaLog(chunk)) return true;
|
|
425
|
-
return stdoutWrite(chunk, ...args);
|
|
426
|
-
};
|
|
427
|
-
process.stderr.write = (chunk, ...args) => {
|
|
428
|
-
if (isHyphaLog(chunk)) return true;
|
|
429
|
-
return stderrWrite(chunk, ...args);
|
|
430
|
-
};
|
|
431
|
-
const restoreConsole = () => {
|
|
432
|
-
console.log = origLog;
|
|
433
|
-
console.warn = origWarn;
|
|
434
|
-
console.info = origInfo;
|
|
435
|
-
console.error = origError;
|
|
436
|
-
};
|
|
437
|
-
let server;
|
|
438
|
-
try {
|
|
439
|
-
server = await connectToHypha({
|
|
440
|
-
serverUrl,
|
|
441
|
-
token,
|
|
442
|
-
name: "svamp-session-cli"
|
|
443
|
-
});
|
|
444
|
-
} catch (err) {
|
|
445
|
-
restoreConsole();
|
|
446
|
-
console.error(`Failed to connect to Hypha: ${err.message}`);
|
|
447
|
-
process.exit(1);
|
|
448
|
-
}
|
|
449
|
-
try {
|
|
450
|
-
const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
|
|
451
|
-
restoreConsole();
|
|
452
|
-
if (services.length === 0) {
|
|
453
|
-
console.log("No machines found.");
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
const machines = await Promise.all(
|
|
457
|
-
services.map(async (svc) => {
|
|
458
|
-
const svcId = svc.id || svc.name;
|
|
459
|
-
try {
|
|
460
|
-
const machineSvc = await server.getService(svcId);
|
|
461
|
-
const [info, sessions] = await Promise.all([
|
|
462
|
-
machineSvc.getMachineInfo(),
|
|
463
|
-
machineSvc.listSessions()
|
|
464
|
-
]);
|
|
465
|
-
return {
|
|
466
|
-
serviceId: svcId,
|
|
467
|
-
machineId: info.machineId || svcId,
|
|
468
|
-
displayName: info.metadata?.displayName || info.metadata?.host || "-",
|
|
469
|
-
platform: info.metadata?.platform || "-",
|
|
470
|
-
host: info.metadata?.host || "-",
|
|
471
|
-
sessions: sessions.length,
|
|
472
|
-
status: info.daemonState?.status || "unknown"
|
|
473
|
-
};
|
|
474
|
-
} catch {
|
|
475
|
-
return {
|
|
476
|
-
serviceId: svcId,
|
|
477
|
-
machineId: svcId,
|
|
478
|
-
displayName: "-",
|
|
479
|
-
platform: "-",
|
|
480
|
-
host: "-",
|
|
481
|
-
sessions: -1,
|
|
482
|
-
status: "unreachable"
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
})
|
|
486
|
-
);
|
|
487
|
-
const header = `${"MACHINE ID".padEnd(20)} ${"NAME".padEnd(20)} ${"PLATFORM".padEnd(12)} ${"HOST".padEnd(25)} ${"SESSIONS".padEnd(10)} ${"STATUS"}`;
|
|
488
|
-
console.log(header);
|
|
489
|
-
console.log("-".repeat(header.length));
|
|
490
|
-
for (const m of machines) {
|
|
491
|
-
const id = truncate(m.machineId, 18).padEnd(20);
|
|
492
|
-
const name = truncate(m.displayName, 18).padEnd(20);
|
|
493
|
-
const platform = m.platform.padEnd(12);
|
|
494
|
-
const host = truncate(m.host, 23).padEnd(25);
|
|
495
|
-
const sessions = m.sessions >= 0 ? String(m.sessions).padEnd(10) : "-".padEnd(10);
|
|
496
|
-
const status = m.status === "running" ? `\x1B[32m${m.status}\x1B[0m` : m.status === "unreachable" ? `\x1B[31m${m.status}\x1B[0m` : m.status;
|
|
497
|
-
console.log(`${id} ${name} ${platform} ${host} ${sessions} ${status}`);
|
|
498
|
-
}
|
|
499
|
-
console.log(`
|
|
500
|
-
${machines.length} machine(s) found.`);
|
|
501
|
-
console.log("Use --machine <id> to target a specific machine.");
|
|
502
|
-
} finally {
|
|
503
|
-
await server.disconnect();
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
function resolveSessionId(sessions, partial) {
|
|
507
|
-
const exact = sessions.find((s) => s.sessionId === partial);
|
|
508
|
-
if (exact) return exact;
|
|
509
|
-
const matches = sessions.filter((s) => s.sessionId.startsWith(partial));
|
|
510
|
-
if (matches.length === 1) return matches[0];
|
|
511
|
-
if (matches.length === 0) {
|
|
512
|
-
console.error(`No session found matching: ${partial}`);
|
|
513
|
-
console.error('Run "svamp session list" to see active sessions.');
|
|
514
|
-
process.exit(1);
|
|
515
|
-
}
|
|
516
|
-
console.error(`Ambiguous session ID "${partial}". Matches:`);
|
|
517
|
-
for (const s of matches) {
|
|
518
|
-
console.error(` ${s.sessionId}`);
|
|
519
|
-
}
|
|
520
|
-
process.exit(1);
|
|
521
|
-
}
|
|
522
|
-
function truncate(str, max) {
|
|
523
|
-
if (str.length <= max) return str;
|
|
524
|
-
return "..." + str.slice(str.length - max + 3);
|
|
525
|
-
}
|
|
526
|
-
function renderMessage(msg) {
|
|
527
|
-
const content = msg.content;
|
|
528
|
-
if (!content) return;
|
|
529
|
-
const role = content.role;
|
|
530
|
-
if (role === "user") {
|
|
531
|
-
const data = content.content;
|
|
532
|
-
let text;
|
|
533
|
-
if (typeof data === "string") {
|
|
534
|
-
try {
|
|
535
|
-
const parsed = JSON.parse(data);
|
|
536
|
-
text = parsed?.text || parsed?.content?.text || data;
|
|
537
|
-
} catch {
|
|
538
|
-
text = data;
|
|
539
|
-
}
|
|
540
|
-
} else if (data?.text) {
|
|
541
|
-
text = data.text;
|
|
542
|
-
} else if (data?.type === "text") {
|
|
543
|
-
text = data.text || "";
|
|
544
|
-
} else {
|
|
545
|
-
text = typeof data === "object" ? JSON.stringify(data) : String(data || "");
|
|
546
|
-
}
|
|
547
|
-
console.log(`\x1B[36m[user]\x1B[0m ${text}`);
|
|
548
|
-
} else if (role === "agent" || role === "assistant") {
|
|
549
|
-
const data = content.content?.data || content.content;
|
|
550
|
-
if (!data) return;
|
|
551
|
-
const getBlocks = (d) => {
|
|
552
|
-
if (d.type === "assistant") {
|
|
553
|
-
const blocks2 = d.message?.content || d.content;
|
|
554
|
-
return Array.isArray(blocks2) ? blocks2 : null;
|
|
555
|
-
}
|
|
556
|
-
return null;
|
|
557
|
-
};
|
|
558
|
-
const renderBlocks = (blocks2) => {
|
|
559
|
-
for (const block of blocks2) {
|
|
560
|
-
if (block.type === "text" && block.text) {
|
|
561
|
-
process.stdout.write(block.text);
|
|
562
|
-
if (!block.text.endsWith("\n")) process.stdout.write("\n");
|
|
563
|
-
} else if (block.type === "tool_use") {
|
|
564
|
-
const argsStr = JSON.stringify(block.input || {}).slice(0, 120);
|
|
565
|
-
console.log(`\x1B[33m[tool]\x1B[0m ${block.name}(${argsStr})`);
|
|
566
|
-
} else if (block.type === "tool_result") {
|
|
567
|
-
const resultStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
|
|
568
|
-
console.log(`\x1B[90m[result]\x1B[0m ${resultStr.slice(0, 200)}${resultStr.length > 200 ? "..." : ""}`);
|
|
569
|
-
} else if (block.type === "thinking") {
|
|
570
|
-
const text = block.thinking || block.text || "";
|
|
571
|
-
if (text) console.log(`\x1B[90m[thinking] ${text.slice(0, 200)}\x1B[0m`);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
const blocks = getBlocks(data);
|
|
576
|
-
if (blocks) {
|
|
577
|
-
renderBlocks(blocks);
|
|
578
|
-
} else if (data.type === "result") {
|
|
579
|
-
if (data.result) console.log(`\x1B[32m[done]\x1B[0m ${data.result}`);
|
|
580
|
-
} else if (data.type === "output") {
|
|
581
|
-
const inner = data.data;
|
|
582
|
-
const innerBlocks = inner ? getBlocks(inner) : null;
|
|
583
|
-
if (innerBlocks) {
|
|
584
|
-
renderBlocks(innerBlocks);
|
|
585
|
-
} else if (inner?.type === "result") {
|
|
586
|
-
if (inner.result) console.log(`\x1B[32m[done]\x1B[0m ${inner.result}`);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
} else if (role === "session") {
|
|
590
|
-
const data = content.content?.data;
|
|
591
|
-
if (data?.type === "system" && data?.subtype === "init") {
|
|
592
|
-
console.log(`\x1B[90m[session init]\x1B[0m`);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
function extractContentBlocks(blocks) {
|
|
597
|
-
const parts = [];
|
|
598
|
-
for (const block of blocks) {
|
|
599
|
-
if (block.type === "text" && block.text) {
|
|
600
|
-
parts.push(block.text);
|
|
601
|
-
} else if (block.type === "tool_use") {
|
|
602
|
-
const argsStr = JSON.stringify(block.input || {}).slice(0, 200);
|
|
603
|
-
parts.push(`[tool: ${block.name}](${argsStr})`);
|
|
604
|
-
} else if (block.type === "tool_result") {
|
|
605
|
-
const r = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
|
|
606
|
-
parts.push(`[result: ${r.slice(0, 200)}]`);
|
|
607
|
-
} else if (block.type === "thinking") {
|
|
608
|
-
const t = block.thinking || block.text || "";
|
|
609
|
-
if (t) parts.push(`[thinking: ${t.slice(0, 200)}]`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return parts;
|
|
613
|
-
}
|
|
614
|
-
function extractMessageText(msg) {
|
|
615
|
-
const content = msg.content;
|
|
616
|
-
if (!content) return null;
|
|
617
|
-
const role = content.role || "unknown";
|
|
618
|
-
let text = "";
|
|
619
|
-
if (role === "user") {
|
|
620
|
-
const data = content.content;
|
|
621
|
-
if (typeof data === "string") {
|
|
622
|
-
try {
|
|
623
|
-
const parsed = JSON.parse(data);
|
|
624
|
-
text = parsed?.text || parsed?.content?.text || data;
|
|
625
|
-
} catch {
|
|
626
|
-
text = data;
|
|
627
|
-
}
|
|
628
|
-
} else if (data?.text) {
|
|
629
|
-
text = data.text;
|
|
630
|
-
} else if (data?.type === "text") {
|
|
631
|
-
text = data.text || "";
|
|
632
|
-
} else {
|
|
633
|
-
text = typeof data === "object" ? JSON.stringify(data) : String(data || "");
|
|
634
|
-
}
|
|
635
|
-
} else if (role === "agent" || role === "assistant") {
|
|
636
|
-
const data = content.content?.data || content.content;
|
|
637
|
-
if (!data) return null;
|
|
638
|
-
const getBlocks = (d) => {
|
|
639
|
-
if (d.type === "assistant") {
|
|
640
|
-
const blocks2 = d.message?.content || d.content;
|
|
641
|
-
return Array.isArray(blocks2) ? blocks2 : null;
|
|
642
|
-
}
|
|
643
|
-
return null;
|
|
644
|
-
};
|
|
645
|
-
const blocks = getBlocks(data);
|
|
646
|
-
if (blocks) {
|
|
647
|
-
text = extractContentBlocks(blocks).join("\n");
|
|
648
|
-
} else if (data.type === "result") {
|
|
649
|
-
text = data.result || "";
|
|
650
|
-
} else if (data.type === "output") {
|
|
651
|
-
const inner = data.data;
|
|
652
|
-
const innerBlocks = inner ? getBlocks(inner) : null;
|
|
653
|
-
if (innerBlocks) {
|
|
654
|
-
text = extractContentBlocks(innerBlocks).join("\n");
|
|
655
|
-
} else if (inner?.type === "result") {
|
|
656
|
-
text = inner.result || "";
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
} else if (role === "session") {
|
|
660
|
-
text = "[session event]";
|
|
661
|
-
}
|
|
662
|
-
return {
|
|
663
|
-
id: msg.id || "",
|
|
664
|
-
seq: msg.seq || 0,
|
|
665
|
-
role,
|
|
666
|
-
text,
|
|
667
|
-
createdAt: msg.createdAt || 0
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
async function waitForIdle(machine, sessionId, timeoutMs) {
|
|
671
|
-
const pollInterval = 2e3;
|
|
672
|
-
const deadline = Date.now() + timeoutMs;
|
|
673
|
-
while (Date.now() < deadline) {
|
|
674
|
-
const activity = await machine.sessionRPC(sessionId, "getActivityState", {});
|
|
675
|
-
if (activity?.pendingPermissions?.length > 0) {
|
|
676
|
-
return { idle: false, pendingPermissions: activity.pendingPermissions };
|
|
677
|
-
}
|
|
678
|
-
if (activity && !activity.thinking) {
|
|
679
|
-
return { idle: true };
|
|
680
|
-
}
|
|
681
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
682
|
-
}
|
|
683
|
-
throw new Error("Timeout waiting for agent to become idle");
|
|
684
|
-
}
|
|
685
|
-
async function waitForBusyThenIdle(machine, sessionId, timeoutMs = 3e5, busyTimeoutMs = 1e4) {
|
|
686
|
-
const pollInterval = 2e3;
|
|
687
|
-
const deadline = Date.now() + timeoutMs;
|
|
688
|
-
const busyDeadline = Date.now() + busyTimeoutMs;
|
|
689
|
-
let sawBusy = false;
|
|
690
|
-
while (Date.now() < deadline) {
|
|
691
|
-
const activity = await machine.sessionRPC(sessionId, "getActivityState", {});
|
|
692
|
-
if (activity?.pendingPermissions?.length > 0) {
|
|
693
|
-
return { idle: false, pendingPermissions: activity.pendingPermissions };
|
|
694
|
-
}
|
|
695
|
-
const isBusy = activity?.thinking === true;
|
|
696
|
-
if (isBusy) {
|
|
697
|
-
sawBusy = true;
|
|
698
|
-
}
|
|
699
|
-
if (activity && !activity.active) {
|
|
700
|
-
return { idle: true };
|
|
701
|
-
}
|
|
702
|
-
if (sawBusy && !isBusy) {
|
|
703
|
-
return { idle: true };
|
|
704
|
-
}
|
|
705
|
-
if (!sawBusy && Date.now() > busyDeadline) {
|
|
706
|
-
return { idle: true };
|
|
707
|
-
}
|
|
708
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
709
|
-
}
|
|
710
|
-
throw new Error("Timeout waiting for agent to become idle");
|
|
711
|
-
}
|
|
712
|
-
async function sessionList(machineId, opts) {
|
|
713
|
-
if (machineId) {
|
|
714
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
715
|
-
try {
|
|
716
|
-
await listSessionsFromMachines(server, [machine], opts);
|
|
717
|
-
} finally {
|
|
718
|
-
await server.disconnect();
|
|
719
|
-
}
|
|
720
|
-
} else {
|
|
721
|
-
const { server, machines } = await connectAndGetAllMachines();
|
|
722
|
-
try {
|
|
723
|
-
await listSessionsFromMachines(server, machines, opts);
|
|
724
|
-
} finally {
|
|
725
|
-
await server.disconnect();
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
async function listSessionsFromMachines(_server, machines, opts) {
|
|
730
|
-
const allSessions = [];
|
|
731
|
-
const machineResults = await Promise.allSettled(
|
|
732
|
-
machines.map(async (machine) => {
|
|
733
|
-
const [info, sessions] = await Promise.all([
|
|
734
|
-
machine.getMachineInfo(),
|
|
735
|
-
machine.listSessions()
|
|
736
|
-
]);
|
|
737
|
-
const host = info.metadata?.displayName || info.metadata?.host || info.machineId;
|
|
738
|
-
for (const s of sessions) {
|
|
739
|
-
s.machineHost = host;
|
|
740
|
-
}
|
|
741
|
-
return sessions;
|
|
742
|
-
})
|
|
743
|
-
);
|
|
744
|
-
for (const r of machineResults) {
|
|
745
|
-
if (r.status === "fulfilled") allSessions.push(...r.value);
|
|
746
|
-
}
|
|
747
|
-
const filtered = opts?.active ? allSessions.filter((s) => s.active) : allSessions;
|
|
748
|
-
if (filtered.length === 0) {
|
|
749
|
-
if (opts?.json) {
|
|
750
|
-
console.log(formatJson([]));
|
|
751
|
-
} else {
|
|
752
|
-
console.log("No active sessions.");
|
|
753
|
-
}
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
const enriched = filtered.map((s) => {
|
|
757
|
-
const m = s.metadata || {};
|
|
758
|
-
return {
|
|
759
|
-
...s,
|
|
760
|
-
flavor: m.flavor || "claude",
|
|
761
|
-
name: m.name || "",
|
|
762
|
-
path: m.path || s.directory || "",
|
|
763
|
-
host: m.host || s.machineHost || ""
|
|
764
|
-
};
|
|
765
|
-
});
|
|
766
|
-
if (opts?.json) {
|
|
767
|
-
console.log(formatJson(enriched.map((s) => ({
|
|
768
|
-
sessionId: s.sessionId,
|
|
769
|
-
agent: s.flavor,
|
|
770
|
-
name: s.name,
|
|
771
|
-
path: s.path,
|
|
772
|
-
host: s.host,
|
|
773
|
-
active: s.active,
|
|
774
|
-
directory: s.directory
|
|
775
|
-
}))));
|
|
776
|
-
} else {
|
|
777
|
-
const header = `${"ID".padEnd(10)} ${"AGENT".padEnd(10)} ${"STATUS".padEnd(9)} ${"NAME".padEnd(25)} ${"MACHINE".padEnd(18)} ${"DIRECTORY".padEnd(35)}`;
|
|
778
|
-
console.log(header);
|
|
779
|
-
console.log("-".repeat(header.length));
|
|
780
|
-
for (const s of enriched) {
|
|
781
|
-
const id = s.sessionId.slice(0, 8);
|
|
782
|
-
const agent = (s.flavor || "claude").padEnd(10);
|
|
783
|
-
const status = s.active ? "\x1B[32mactive\x1B[0m " : "\x1B[90minactive\x1B[0m";
|
|
784
|
-
const name = truncate(s.name || "-", 25).padEnd(25);
|
|
785
|
-
const machine = truncate(s.host || "-", 16).padEnd(18);
|
|
786
|
-
const dir = truncate(s.directory || "-", 33).padEnd(35);
|
|
787
|
-
console.log(`${id.padEnd(10)} ${agent} ${status} ${name} ${machine} ${dir}`);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
const WORKTREE_ADJECTIVES = [
|
|
792
|
-
"clever",
|
|
793
|
-
"happy",
|
|
794
|
-
"swift",
|
|
795
|
-
"bright",
|
|
796
|
-
"calm",
|
|
797
|
-
"bold",
|
|
798
|
-
"quiet",
|
|
799
|
-
"brave",
|
|
800
|
-
"wise",
|
|
801
|
-
"eager",
|
|
802
|
-
"gentle",
|
|
803
|
-
"quick",
|
|
804
|
-
"sharp",
|
|
805
|
-
"smooth",
|
|
806
|
-
"fresh"
|
|
807
|
-
];
|
|
808
|
-
const WORKTREE_NOUNS = [
|
|
809
|
-
"ocean",
|
|
810
|
-
"forest",
|
|
811
|
-
"cloud",
|
|
812
|
-
"star",
|
|
813
|
-
"river",
|
|
814
|
-
"mountain",
|
|
815
|
-
"valley",
|
|
816
|
-
"bridge",
|
|
817
|
-
"beacon",
|
|
818
|
-
"harbor",
|
|
819
|
-
"garden",
|
|
820
|
-
"meadow",
|
|
821
|
-
"canyon",
|
|
822
|
-
"island",
|
|
823
|
-
"desert"
|
|
824
|
-
];
|
|
825
|
-
function generateWorktreeName() {
|
|
826
|
-
const adj = WORKTREE_ADJECTIVES[Math.floor(Math.random() * WORKTREE_ADJECTIVES.length)];
|
|
827
|
-
const noun = WORKTREE_NOUNS[Math.floor(Math.random() * WORKTREE_NOUNS.length)];
|
|
828
|
-
return `${adj}-${noun}`;
|
|
829
|
-
}
|
|
830
|
-
function createWorktree(baseDir) {
|
|
831
|
-
const absBase = resolve(baseDir);
|
|
832
|
-
const marker = "/.dev/worktree/";
|
|
833
|
-
const idx = absBase.indexOf(marker);
|
|
834
|
-
const projectRoot = idx !== -1 ? absBase.substring(0, idx) : absBase;
|
|
835
|
-
try {
|
|
836
|
-
execSync("git rev-parse --git-dir", { cwd: projectRoot, stdio: "pipe" });
|
|
837
|
-
} catch {
|
|
838
|
-
throw new Error(`Not a git repository: ${projectRoot}`);
|
|
839
|
-
}
|
|
840
|
-
const name = generateWorktreeName();
|
|
841
|
-
const relPath = `.dev/worktree/${name}`;
|
|
842
|
-
try {
|
|
843
|
-
execSync(`git worktree add -b ${name} ${relPath}`, { cwd: projectRoot, stdio: "pipe" });
|
|
844
|
-
} catch (err) {
|
|
845
|
-
for (let i = 2; i <= 4; i++) {
|
|
846
|
-
const suffixed = `${name}-${i}`;
|
|
847
|
-
const suffixedPath = `.dev/worktree/${suffixed}`;
|
|
848
|
-
try {
|
|
849
|
-
execSync(`git worktree add -b ${suffixed} ${suffixedPath}`, { cwd: projectRoot, stdio: "pipe" });
|
|
850
|
-
return { path: join(projectRoot, suffixedPath), branch: suffixed };
|
|
851
|
-
} catch {
|
|
852
|
-
continue;
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
throw new Error(`Failed to create worktree: ${err.stderr?.toString() || err.message}`);
|
|
856
|
-
}
|
|
857
|
-
return { path: join(projectRoot, relPath), branch: name };
|
|
858
|
-
}
|
|
859
|
-
function parseShareArg(arg) {
|
|
860
|
-
const parts = arg.split(":");
|
|
861
|
-
const email = parts[0];
|
|
862
|
-
const role = parts[1] || "interact";
|
|
863
|
-
if (!["view", "interact", "admin"].includes(role)) {
|
|
864
|
-
throw new Error(`Invalid role "${role}" in --share ${arg}. Must be view, interact, or admin.`);
|
|
865
|
-
}
|
|
866
|
-
return { email, role };
|
|
867
|
-
}
|
|
868
|
-
async function sessionSpawn(agent, directory, machineId, opts) {
|
|
869
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
870
|
-
try {
|
|
871
|
-
let sharing;
|
|
872
|
-
if (opts?.share?.length) {
|
|
873
|
-
sharing = {
|
|
874
|
-
enabled: true,
|
|
875
|
-
owner: "",
|
|
876
|
-
// will be auto-set by machine service from Hypha context
|
|
877
|
-
allowedUsers: opts.share.map((s) => ({
|
|
878
|
-
email: s.email,
|
|
879
|
-
role: s.role || "interact",
|
|
880
|
-
addedAt: Date.now(),
|
|
881
|
-
addedBy: "cli"
|
|
882
|
-
}))
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
let securityContext;
|
|
886
|
-
if (opts?.securityContextPath) {
|
|
887
|
-
const configPath = resolve(opts.securityContextPath);
|
|
888
|
-
const config = loadSecurityContextConfig(configPath);
|
|
889
|
-
securityContext = resolveSecurityContext(config, void 0);
|
|
890
|
-
}
|
|
891
|
-
if (opts?.denyRead?.length || opts?.allowWrite?.length || opts?.denyNetwork || opts?.allowDomain?.length) {
|
|
892
|
-
const flagCtx = buildSecurityContextFromFlags({
|
|
893
|
-
denyRead: opts.denyRead,
|
|
894
|
-
allowWrite: opts.allowWrite,
|
|
895
|
-
denyNetwork: opts.denyNetwork,
|
|
896
|
-
allowDomain: opts.allowDomain
|
|
897
|
-
});
|
|
898
|
-
securityContext = mergeSecurityContexts(securityContext, flagCtx);
|
|
899
|
-
}
|
|
900
|
-
const forceIsolation = opts?.isolate || !!securityContext;
|
|
901
|
-
if (securityContext?.filesystem?.allowWrite?.length) {
|
|
902
|
-
const absDir = resolve(directory);
|
|
903
|
-
const writePaths = securityContext.filesystem.allowWrite;
|
|
904
|
-
const dirCovered = writePaths.some((p) => absDir.startsWith(resolve(p)) || resolve(p).startsWith(absDir));
|
|
905
|
-
if (!dirCovered) {
|
|
906
|
-
console.warn(`Warning: Working directory ${absDir} is not covered by allowWrite paths: ${writePaths.join(", ")}`);
|
|
907
|
-
console.warn(` The agent may not be able to write files in the working directory.`);
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
if (opts?.worktree) {
|
|
911
|
-
try {
|
|
912
|
-
const wt = createWorktree(directory);
|
|
913
|
-
console.log(`Created worktree: ${wt.branch} \u2192 ${wt.path}`);
|
|
914
|
-
directory = wt.path;
|
|
915
|
-
} catch (err) {
|
|
916
|
-
console.error(`Failed to create worktree: ${err.message}`);
|
|
917
|
-
process.exit(1);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
let effectiveParentSessionId = opts?.parentSessionId || process.env.SVAMP_SESSION_ID || void 0;
|
|
921
|
-
if (effectiveParentSessionId) {
|
|
922
|
-
try {
|
|
923
|
-
const sessions = await machine.listSessions();
|
|
924
|
-
if (opts?.parentSessionId) {
|
|
925
|
-
const match = resolveSessionId(sessions, effectiveParentSessionId);
|
|
926
|
-
effectiveParentSessionId = match.sessionId;
|
|
927
|
-
} else {
|
|
928
|
-
const found = sessions.find((s) => s.sessionId === effectiveParentSessionId);
|
|
929
|
-
if (!found) throw new Error("not found");
|
|
930
|
-
}
|
|
931
|
-
} catch (err) {
|
|
932
|
-
const source = opts?.parentSessionId ? "--parent" : "SVAMP_SESSION_ID";
|
|
933
|
-
console.error(`Error: Parent session ${effectiveParentSessionId} not found (from ${source}).`);
|
|
934
|
-
process.exit(1);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
console.log(`Spawning ${agent} session in ${directory}...`);
|
|
938
|
-
if (effectiveParentSessionId) {
|
|
939
|
-
console.log(`Parent session: ${effectiveParentSessionId}${!opts?.parentSessionId ? " (auto-detected from SVAMP_SESSION_ID)" : ""}`);
|
|
940
|
-
}
|
|
941
|
-
if (opts?.tags?.length) {
|
|
942
|
-
console.log(`Tags: ${opts.tags.join(", ")}`);
|
|
943
|
-
}
|
|
944
|
-
if (forceIsolation) {
|
|
945
|
-
console.log(`Isolation: enabled (workspace: ${resolve(directory)})`);
|
|
946
|
-
}
|
|
947
|
-
if (securityContext) {
|
|
948
|
-
console.log(`Security context: ${JSON.stringify(securityContext, null, 2)}`);
|
|
949
|
-
}
|
|
950
|
-
if (sharing) {
|
|
951
|
-
console.log(`Sharing with: ${sharing.allowedUsers.map((u) => `${u.email} (${u.role})`).join(", ")}`);
|
|
952
|
-
}
|
|
953
|
-
const result = await machine.spawnSession({
|
|
954
|
-
directory,
|
|
955
|
-
agent,
|
|
956
|
-
sharing,
|
|
957
|
-
securityContext,
|
|
958
|
-
forceIsolation,
|
|
959
|
-
tags: opts?.tags,
|
|
960
|
-
parentSessionId: effectiveParentSessionId,
|
|
961
|
-
permissionMode: opts?.permissionMode
|
|
962
|
-
});
|
|
963
|
-
if (result.type === "success") {
|
|
964
|
-
console.log(`Session started: ${result.sessionId}`);
|
|
965
|
-
if (result.message) console.log(` ${result.message}`);
|
|
966
|
-
const effectivePermMode = opts?.permissionMode || "default";
|
|
967
|
-
if (effectivePermMode !== "bypassPermissions") {
|
|
968
|
-
console.log(`\x1B[33m\u26A0 Permission mode: ${effectivePermMode}. Agent may pause for tool approval.\x1B[0m`);
|
|
969
|
-
console.log(`\x1B[33m Use -p bypassPermissions to run without approval prompts.\x1B[0m`);
|
|
970
|
-
}
|
|
971
|
-
if (opts?.message && result.sessionId) {
|
|
972
|
-
const svc = getSessionProxy(machine, result.sessionId);
|
|
973
|
-
const sendResult = await svc.sendMessage(
|
|
974
|
-
JSON.stringify({
|
|
975
|
-
role: "user",
|
|
976
|
-
content: { type: "text", text: opts.message },
|
|
977
|
-
meta: { sentFrom: "svamp-cli" }
|
|
978
|
-
})
|
|
979
|
-
);
|
|
980
|
-
console.log(`Message sent (seq: ${sendResult.seq})`);
|
|
981
|
-
if (opts.wait) {
|
|
982
|
-
console.log("Waiting for agent to become idle...");
|
|
983
|
-
await waitForBusyThenIdle(machine, result.sessionId);
|
|
984
|
-
console.log("Agent is idle.");
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
} else if (result.type === "requestToApproveDirectoryCreation") {
|
|
988
|
-
console.error(`Directory ${result.directory} does not exist. Create it first or use an existing directory.`);
|
|
989
|
-
process.exit(1);
|
|
990
|
-
} else {
|
|
991
|
-
console.error(`Failed: ${result.errorMessage || "Unknown error"}`);
|
|
992
|
-
process.exit(1);
|
|
993
|
-
}
|
|
994
|
-
} finally {
|
|
995
|
-
await server.disconnect();
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
async function sessionStop(sessionId, machineId) {
|
|
999
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1000
|
-
try {
|
|
1001
|
-
const sessions = await machine.listSessions();
|
|
1002
|
-
const match = resolveSessionId(sessions, sessionId);
|
|
1003
|
-
const success = await machine.stopSession(match.sessionId);
|
|
1004
|
-
if (success) {
|
|
1005
|
-
console.log(`Session ${match.sessionId.slice(0, 8)} stopped.`);
|
|
1006
|
-
} else {
|
|
1007
|
-
console.error("Failed to stop session (not found on daemon).");
|
|
1008
|
-
process.exit(1);
|
|
1009
|
-
}
|
|
1010
|
-
} finally {
|
|
1011
|
-
await server.disconnect();
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
async function sessionInfo(sessionId, machineId, opts) {
|
|
1015
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1016
|
-
try {
|
|
1017
|
-
const sessions = await machine.listSessions();
|
|
1018
|
-
const match = resolveSessionId(sessions, sessionId);
|
|
1019
|
-
const fullId = match.sessionId;
|
|
1020
|
-
let metadata = {};
|
|
1021
|
-
let activity = {};
|
|
1022
|
-
try {
|
|
1023
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1024
|
-
const metaResult = await svc.getMetadata();
|
|
1025
|
-
metadata = metaResult.metadata || {};
|
|
1026
|
-
activity = await svc.getActivityState();
|
|
1027
|
-
} catch {
|
|
1028
|
-
}
|
|
1029
|
-
const pendingPermissions = activity.pendingPermissions || [];
|
|
1030
|
-
const statusData = {
|
|
1031
|
-
sessionId: fullId,
|
|
1032
|
-
flavor: metadata.flavor || "claude",
|
|
1033
|
-
name: metadata.name || "",
|
|
1034
|
-
path: metadata.path || match.directory || "",
|
|
1035
|
-
host: metadata.host || "",
|
|
1036
|
-
lifecycleState: metadata.lifecycleState || "unknown",
|
|
1037
|
-
active: activity.active ?? false,
|
|
1038
|
-
thinking: activity.thinking ?? false,
|
|
1039
|
-
startedBy: metadata.startedBy || match.startedBy || "",
|
|
1040
|
-
summary: metadata.summary?.text || void 0,
|
|
1041
|
-
claudeSessionId: metadata.claudeSessionId || void 0,
|
|
1042
|
-
sessionLink: metadata.sessionLink?.url || void 0,
|
|
1043
|
-
tags: metadata.tags?.length ? metadata.tags : void 0,
|
|
1044
|
-
parentSessionId: metadata.parentSessionId || void 0,
|
|
1045
|
-
pendingPermissions: pendingPermissions.length > 0 ? pendingPermissions : void 0,
|
|
1046
|
-
ralphLoop: metadata.ralphLoop || void 0
|
|
1047
|
-
};
|
|
1048
|
-
if (opts?.json) {
|
|
1049
|
-
console.log(formatJson(statusData));
|
|
1050
|
-
} else {
|
|
1051
|
-
console.log(formatSessionStatus(statusData));
|
|
1052
|
-
}
|
|
1053
|
-
} finally {
|
|
1054
|
-
await server.disconnect();
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
async function sessionMessages(sessionId, machineId, opts) {
|
|
1058
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1059
|
-
try {
|
|
1060
|
-
const sessions = await machine.listSessions();
|
|
1061
|
-
const match = resolveSessionId(sessions, sessionId);
|
|
1062
|
-
const fullId = match.sessionId;
|
|
1063
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1064
|
-
const afterSeq = opts?.after ?? 0;
|
|
1065
|
-
const apiLimit = opts?.limit ?? 1e3;
|
|
1066
|
-
const { messages } = await svc.getMessages(afterSeq, apiLimit);
|
|
1067
|
-
const toShow = opts?.last ? messages.slice(-opts.last) : messages;
|
|
1068
|
-
if (toShow.length === 0) {
|
|
1069
|
-
if (opts?.json) {
|
|
1070
|
-
console.log(formatJson([]));
|
|
1071
|
-
} else {
|
|
1072
|
-
console.log("No messages yet.");
|
|
1073
|
-
}
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
if (opts?.json && opts?.raw) {
|
|
1077
|
-
console.log(formatJson(toShow));
|
|
1078
|
-
} else if (opts?.json) {
|
|
1079
|
-
const formatted = [];
|
|
1080
|
-
for (const msg of toShow) {
|
|
1081
|
-
const extracted = extractMessageText(msg);
|
|
1082
|
-
if (extracted) {
|
|
1083
|
-
formatted.push(extracted);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
formatted.sort((a, b) => a.createdAt - b.createdAt);
|
|
1087
|
-
console.log(formatJson(formatted));
|
|
1088
|
-
} else {
|
|
1089
|
-
for (const msg of toShow) {
|
|
1090
|
-
renderMessage(msg);
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
} finally {
|
|
1094
|
-
await server.disconnect();
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
async function sessionApprove(sessionId, requestId, machineId, opts) {
|
|
1098
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1099
|
-
try {
|
|
1100
|
-
const sessions = await machine.listSessions();
|
|
1101
|
-
const match = resolveSessionId(sessions, sessionId);
|
|
1102
|
-
const fullId = match.sessionId;
|
|
1103
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1104
|
-
if (requestId) {
|
|
1105
|
-
const activity = await svc.getActivityState();
|
|
1106
|
-
const pending = activity.pendingPermissions || [];
|
|
1107
|
-
const matched = pending.find((p) => p.id === requestId || p.id.startsWith(requestId));
|
|
1108
|
-
if (!matched) {
|
|
1109
|
-
console.error(`No pending permission matching "${requestId}".`);
|
|
1110
|
-
process.exitCode = 1;
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
await svc.permissionResponse({ id: matched.id, approved: true });
|
|
1114
|
-
if (opts?.json) {
|
|
1115
|
-
console.log(formatJson({ sessionId: fullId, approved: 1, requestIds: [matched.id] }));
|
|
1116
|
-
} else {
|
|
1117
|
-
console.log(`Approved: ${matched.tool} [${matched.id.slice(0, 8)}]`);
|
|
1118
|
-
}
|
|
1119
|
-
} else {
|
|
1120
|
-
const activity = await svc.getActivityState();
|
|
1121
|
-
const pending = activity.pendingPermissions || [];
|
|
1122
|
-
if (pending.length === 0) {
|
|
1123
|
-
if (opts?.json) {
|
|
1124
|
-
console.log(formatJson({ sessionId: fullId, approved: 0, requestIds: [] }));
|
|
1125
|
-
} else {
|
|
1126
|
-
console.error("No pending permission requests.");
|
|
1127
|
-
}
|
|
1128
|
-
process.exitCode = 1;
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
const approvedIds = [];
|
|
1132
|
-
for (const p of pending) {
|
|
1133
|
-
await svc.permissionResponse({ id: p.id, approved: true });
|
|
1134
|
-
approvedIds.push(p.id);
|
|
1135
|
-
}
|
|
1136
|
-
if (opts?.json) {
|
|
1137
|
-
console.log(formatJson({ sessionId: fullId, approved: approvedIds.length, requestIds: approvedIds }));
|
|
1138
|
-
} else {
|
|
1139
|
-
console.log(`Approved ${approvedIds.length} permission(s).`);
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
} finally {
|
|
1143
|
-
await server.disconnect();
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
async function sessionDeny(sessionId, requestId, machineId, opts) {
|
|
1147
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1148
|
-
try {
|
|
1149
|
-
const sessions = await machine.listSessions();
|
|
1150
|
-
const match = resolveSessionId(sessions, sessionId);
|
|
1151
|
-
const fullId = match.sessionId;
|
|
1152
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1153
|
-
if (requestId) {
|
|
1154
|
-
const activity = await svc.getActivityState();
|
|
1155
|
-
const pending = activity.pendingPermissions || [];
|
|
1156
|
-
const matched = pending.find((p) => p.id === requestId || p.id.startsWith(requestId));
|
|
1157
|
-
if (!matched) {
|
|
1158
|
-
console.error(`No pending permission matching "${requestId}".`);
|
|
1159
|
-
process.exitCode = 1;
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
await svc.permissionResponse({ id: matched.id, approved: false });
|
|
1163
|
-
if (opts?.json) {
|
|
1164
|
-
console.log(formatJson({ sessionId: fullId, denied: 1, requestIds: [matched.id] }));
|
|
1165
|
-
} else {
|
|
1166
|
-
console.log(`Denied: ${matched.tool} [${matched.id.slice(0, 8)}]`);
|
|
1167
|
-
}
|
|
1168
|
-
} else {
|
|
1169
|
-
const activity = await svc.getActivityState();
|
|
1170
|
-
const pending = activity.pendingPermissions || [];
|
|
1171
|
-
if (pending.length === 0) {
|
|
1172
|
-
if (opts?.json) {
|
|
1173
|
-
console.log(formatJson({ sessionId: fullId, denied: 0, requestIds: [] }));
|
|
1174
|
-
} else {
|
|
1175
|
-
console.error("No pending permission requests.");
|
|
1176
|
-
}
|
|
1177
|
-
process.exitCode = 1;
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
const deniedIds = [];
|
|
1181
|
-
for (const p of pending) {
|
|
1182
|
-
await svc.permissionResponse({ id: p.id, approved: false });
|
|
1183
|
-
deniedIds.push(p.id);
|
|
1184
|
-
}
|
|
1185
|
-
if (opts?.json) {
|
|
1186
|
-
console.log(formatJson({ sessionId: fullId, denied: deniedIds.length, requestIds: deniedIds }));
|
|
1187
|
-
} else {
|
|
1188
|
-
console.log(`Denied ${deniedIds.length} permission(s).`);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
} finally {
|
|
1192
|
-
await server.disconnect();
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
async function sessionAttach(sessionId, machineId) {
|
|
1196
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1197
|
-
const sessions = await machine.listSessions();
|
|
1198
|
-
const match = resolveSessionId(sessions, sessionId);
|
|
1199
|
-
const fullId = match.sessionId;
|
|
1200
|
-
let svc;
|
|
1201
|
-
try {
|
|
1202
|
-
svc = getSessionProxy(machine, fullId);
|
|
1203
|
-
} catch (err) {
|
|
1204
|
-
console.error(`Could not find session service: ${err.message}`);
|
|
1205
|
-
await server.disconnect();
|
|
1206
|
-
process.exit(1);
|
|
1207
|
-
}
|
|
1208
|
-
const { metadata } = await svc.getMetadata();
|
|
1209
|
-
const flavor = metadata?.flavor || "claude";
|
|
1210
|
-
const name = metadata?.name || fullId.slice(0, 8);
|
|
1211
|
-
console.log(`Attached to ${flavor} session "${name}". Commands: /quit /abort /kill
|
|
1212
|
-
`);
|
|
1213
|
-
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
1214
|
-
let replayDone = false;
|
|
1215
|
-
await svc.registerListener({
|
|
1216
|
-
onUpdate: (update) => {
|
|
1217
|
-
if (update.type === "new-message") {
|
|
1218
|
-
const msg = update.message;
|
|
1219
|
-
if (!msg?.id) return;
|
|
1220
|
-
if (seenMessageIds.has(msg.id)) return;
|
|
1221
|
-
seenMessageIds.add(msg.id);
|
|
1222
|
-
if (!replayDone) return;
|
|
1223
|
-
renderMessage(msg);
|
|
1224
|
-
} else if (update.type === "activity") {
|
|
1225
|
-
if (!replayDone) return;
|
|
1226
|
-
if (update.thinking) {
|
|
1227
|
-
process.stdout.write("\x1B[90m[thinking...]\x1B[0m\r");
|
|
1228
|
-
} else if (!update.thinking) {
|
|
1229
|
-
process.stdout.write("\n> ");
|
|
1230
|
-
}
|
|
1231
|
-
} else if (update.type === "update-session") ;
|
|
1232
|
-
}
|
|
1233
|
-
});
|
|
1234
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1235
|
-
replayDone = true;
|
|
1236
|
-
console.log(`\x1B[90m(${seenMessageIds.size} messages in history)\x1B[0m`);
|
|
1237
|
-
process.stdout.write("> ");
|
|
1238
|
-
const readline = await import('readline');
|
|
1239
|
-
const rl = readline.createInterface({
|
|
1240
|
-
input: process.stdin,
|
|
1241
|
-
output: process.stdout,
|
|
1242
|
-
terminal: true
|
|
1243
|
-
});
|
|
1244
|
-
rl.on("line", async (line) => {
|
|
1245
|
-
const trimmed = line.trim();
|
|
1246
|
-
if (!trimmed) {
|
|
1247
|
-
process.stdout.write("> ");
|
|
1248
|
-
return;
|
|
1249
|
-
}
|
|
1250
|
-
if (trimmed === "/quit" || trimmed === "/detach") {
|
|
1251
|
-
console.log("Detaching (session continues running)...");
|
|
1252
|
-
rl.close();
|
|
1253
|
-
await server.disconnect();
|
|
1254
|
-
process.exit(0);
|
|
1255
|
-
}
|
|
1256
|
-
if (trimmed === "/abort" || trimmed === "/cancel") {
|
|
1257
|
-
try {
|
|
1258
|
-
await svc.abort();
|
|
1259
|
-
console.log("Abort sent.");
|
|
1260
|
-
} catch (err) {
|
|
1261
|
-
console.error(`Abort failed: ${err.message}`);
|
|
1262
|
-
}
|
|
1263
|
-
process.stdout.write("> ");
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
if (trimmed === "/kill") {
|
|
1267
|
-
try {
|
|
1268
|
-
await svc.killSession();
|
|
1269
|
-
console.log("Session killed.");
|
|
1270
|
-
} catch (err) {
|
|
1271
|
-
console.error(`Kill failed: ${err.message}`);
|
|
1272
|
-
}
|
|
1273
|
-
rl.close();
|
|
1274
|
-
await server.disconnect();
|
|
1275
|
-
process.exit(0);
|
|
1276
|
-
}
|
|
1277
|
-
if (trimmed === "/info") {
|
|
1278
|
-
try {
|
|
1279
|
-
const { metadata: m } = await svc.getMetadata();
|
|
1280
|
-
const act = await svc.getActivityState();
|
|
1281
|
-
console.log(` Agent: ${m?.flavor || "claude"}, State: ${m?.lifecycleState || "?"}, Active: ${act?.active}, Thinking: ${act?.thinking}`);
|
|
1282
|
-
} catch (err) {
|
|
1283
|
-
console.error(`Info failed: ${err.message}`);
|
|
1284
|
-
}
|
|
1285
|
-
process.stdout.write("> ");
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
try {
|
|
1289
|
-
await svc.sendMessage(
|
|
1290
|
-
JSON.stringify({
|
|
1291
|
-
role: "user",
|
|
1292
|
-
content: { type: "text", text: trimmed }
|
|
1293
|
-
})
|
|
1294
|
-
);
|
|
1295
|
-
} catch (err) {
|
|
1296
|
-
console.error(`Send failed: ${err.message}`);
|
|
1297
|
-
process.stdout.write("> ");
|
|
1298
|
-
}
|
|
1299
|
-
});
|
|
1300
|
-
rl.on("close", async () => {
|
|
1301
|
-
await server.disconnect();
|
|
1302
|
-
process.exit(0);
|
|
1303
|
-
});
|
|
1304
|
-
process.on("SIGINT", async () => {
|
|
1305
|
-
console.log("\nDetaching (session continues running)...");
|
|
1306
|
-
rl.close();
|
|
1307
|
-
await server.disconnect();
|
|
1308
|
-
process.exit(0);
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
async function sessionSend(sessionId, message, machineId, opts) {
|
|
1312
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1313
|
-
try {
|
|
1314
|
-
const sessions = await machine.listSessions();
|
|
1315
|
-
const match = resolveSessionId(sessions, sessionId);
|
|
1316
|
-
const fullId = match.sessionId;
|
|
1317
|
-
const { randomUUID } = await import('node:crypto');
|
|
1318
|
-
const inboxMessage = {
|
|
1319
|
-
messageId: randomUUID(),
|
|
1320
|
-
body: message,
|
|
1321
|
-
timestamp: Date.now(),
|
|
1322
|
-
read: false,
|
|
1323
|
-
from: `cli:${os.userInfo().username}`,
|
|
1324
|
-
to: fullId,
|
|
1325
|
-
subject: opts?.subject,
|
|
1326
|
-
urgency: opts?.urgency || "urgent"
|
|
1327
|
-
};
|
|
1328
|
-
const result = await machine.sessionRPC(fullId, "sendInboxMessage", { message: inboxMessage });
|
|
1329
|
-
let waitResult;
|
|
1330
|
-
if (opts?.wait) {
|
|
1331
|
-
const timeoutMs = (opts.timeout || 300) * 1e3;
|
|
1332
|
-
waitResult = await waitForBusyThenIdle(machine, fullId, timeoutMs);
|
|
1333
|
-
}
|
|
1334
|
-
if (waitResult?.pendingPermissions?.length) {
|
|
1335
|
-
if (opts?.json) {
|
|
1336
|
-
console.log(formatJson({
|
|
1337
|
-
sessionId: fullId,
|
|
1338
|
-
message,
|
|
1339
|
-
sent: true,
|
|
1340
|
-
messageId: result.messageId,
|
|
1341
|
-
status: "permission-pending",
|
|
1342
|
-
pendingPermissions: waitResult.pendingPermissions
|
|
1343
|
-
}));
|
|
1344
|
-
} else {
|
|
1345
|
-
console.log(`Message sent to session ${fullId.slice(0, 8)} (id: ${result.messageId.slice(0, 8)})`);
|
|
1346
|
-
console.log("Agent is waiting for permission approval:");
|
|
1347
|
-
for (const p of waitResult.pendingPermissions) {
|
|
1348
|
-
const argsStr = JSON.stringify(p.arguments || {}).slice(0, 120);
|
|
1349
|
-
console.log(` [${p.id.slice(0, 8)}] ${p.tool}(${argsStr})`);
|
|
1350
|
-
}
|
|
1351
|
-
console.log(`
|
|
1352
|
-
Use: svamp session approve ${fullId.slice(0, 8)}`);
|
|
1353
|
-
}
|
|
1354
|
-
process.exitCode = 2;
|
|
1355
|
-
} else if (opts?.json) {
|
|
1356
|
-
console.log(formatJson({
|
|
1357
|
-
sessionId: fullId,
|
|
1358
|
-
message,
|
|
1359
|
-
sent: true,
|
|
1360
|
-
messageId: result.messageId,
|
|
1361
|
-
waited: !!opts.wait,
|
|
1362
|
-
status: opts.wait ? "idle" : "sent"
|
|
1363
|
-
}));
|
|
1364
|
-
} else {
|
|
1365
|
-
console.log(`Message sent to session ${fullId.slice(0, 8)} (id: ${result.messageId.slice(0, 8)})`);
|
|
1366
|
-
if (opts?.wait) {
|
|
1367
|
-
console.log("Agent is idle.");
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
} finally {
|
|
1371
|
-
await server.disconnect();
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
async function sessionWait(sessionId, machineId, opts) {
|
|
1375
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1376
|
-
try {
|
|
1377
|
-
const sessions = await machine.listSessions();
|
|
1378
|
-
const match = resolveSessionId(sessions, sessionId);
|
|
1379
|
-
const fullId = match.sessionId;
|
|
1380
|
-
const timeoutMs = (opts?.timeout || 300) * 1e3;
|
|
1381
|
-
const result = await waitForIdle(machine, fullId, timeoutMs);
|
|
1382
|
-
if (result.pendingPermissions?.length) {
|
|
1383
|
-
if (opts?.json) {
|
|
1384
|
-
console.log(formatJson({
|
|
1385
|
-
sessionId: fullId,
|
|
1386
|
-
status: "permission-pending",
|
|
1387
|
-
pendingPermissions: result.pendingPermissions
|
|
1388
|
-
}));
|
|
1389
|
-
} else {
|
|
1390
|
-
console.log(`Session ${fullId.slice(0, 8)} is waiting for permission approval:`);
|
|
1391
|
-
for (const p of result.pendingPermissions) {
|
|
1392
|
-
const argsStr = JSON.stringify(p.arguments || {}).slice(0, 120);
|
|
1393
|
-
console.log(` [${p.id.slice(0, 8)}] ${p.tool}(${argsStr})`);
|
|
1394
|
-
}
|
|
1395
|
-
console.log(`
|
|
1396
|
-
Use: svamp session approve ${fullId.slice(0, 8)}`);
|
|
1397
|
-
}
|
|
1398
|
-
process.exitCode = 2;
|
|
1399
|
-
} else {
|
|
1400
|
-
if (opts?.json) {
|
|
1401
|
-
console.log(formatJson({ sessionId: fullId, status: "idle" }));
|
|
1402
|
-
} else {
|
|
1403
|
-
console.log(`Session ${fullId.slice(0, 8)} is idle.`);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
} catch (err) {
|
|
1407
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1408
|
-
console.error(msg);
|
|
1409
|
-
process.exitCode = 1;
|
|
1410
|
-
} finally {
|
|
1411
|
-
await server.disconnect();
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
async function sessionShare(sessionIdPartial, machineId, opts) {
|
|
1415
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1416
|
-
try {
|
|
1417
|
-
const sessions = await machine.listSessions();
|
|
1418
|
-
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1419
|
-
const fullId = match.sessionId;
|
|
1420
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1421
|
-
if (opts.list) {
|
|
1422
|
-
const metaResult = await svc.getMetadata();
|
|
1423
|
-
const sharing = metaResult.metadata?.sharing;
|
|
1424
|
-
if (!sharing || !sharing.enabled) {
|
|
1425
|
-
console.log("Sharing is not enabled for this session.");
|
|
1426
|
-
return;
|
|
1427
|
-
}
|
|
1428
|
-
console.log(`Owner: ${sharing.owner}`);
|
|
1429
|
-
if (sharing.publicAccess) {
|
|
1430
|
-
console.log(`Public access: ${sharing.publicAccess} (anyone with the link)`);
|
|
1431
|
-
}
|
|
1432
|
-
if (sharing.allowedUsers.length === 0) {
|
|
1433
|
-
console.log("No shared users.");
|
|
1434
|
-
} else {
|
|
1435
|
-
console.log("Shared users:");
|
|
1436
|
-
for (const u of sharing.allowedUsers) {
|
|
1437
|
-
console.log(` ${u.email.padEnd(30)} ${u.role.padEnd(10)} (added ${new Date(u.addedAt).toISOString().slice(0, 10)})`);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
const secCtx = metaResult.metadata?.securityContext;
|
|
1441
|
-
if (secCtx) {
|
|
1442
|
-
console.log(`
|
|
1443
|
-
Security context:`);
|
|
1444
|
-
console.log(JSON.stringify(secCtx, null, 2));
|
|
1445
|
-
}
|
|
1446
|
-
return;
|
|
1447
|
-
}
|
|
1448
|
-
if (opts.add) {
|
|
1449
|
-
const { email, role } = parseShareArg(opts.add);
|
|
1450
|
-
const metaResult = await svc.getMetadata();
|
|
1451
|
-
let sharing = metaResult.metadata?.sharing || {
|
|
1452
|
-
enabled: true,
|
|
1453
|
-
owner: "",
|
|
1454
|
-
allowedUsers: []
|
|
1455
|
-
};
|
|
1456
|
-
sharing.enabled = true;
|
|
1457
|
-
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
1458
|
-
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
1459
|
-
);
|
|
1460
|
-
sharing.allowedUsers.push({
|
|
1461
|
-
email,
|
|
1462
|
-
role,
|
|
1463
|
-
addedAt: Date.now(),
|
|
1464
|
-
addedBy: "cli"
|
|
1465
|
-
});
|
|
1466
|
-
await svc.updateSharing(sharing);
|
|
1467
|
-
console.log(`Shared session ${fullId.slice(0, 8)} with ${email} (${role}).`);
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
if (opts.remove) {
|
|
1471
|
-
const email = opts.remove;
|
|
1472
|
-
const metaResult = await svc.getMetadata();
|
|
1473
|
-
const sharing = metaResult.metadata?.sharing;
|
|
1474
|
-
if (!sharing) {
|
|
1475
|
-
console.error("Sharing is not enabled for this session.");
|
|
1476
|
-
process.exit(1);
|
|
1477
|
-
}
|
|
1478
|
-
const before = sharing.allowedUsers.length;
|
|
1479
|
-
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
1480
|
-
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
1481
|
-
);
|
|
1482
|
-
if (sharing.allowedUsers.length === before) {
|
|
1483
|
-
console.error(`User ${email} is not in the shared users list.`);
|
|
1484
|
-
process.exit(1);
|
|
1485
|
-
}
|
|
1486
|
-
await svc.updateSharing(sharing);
|
|
1487
|
-
console.log(`Removed ${email} from session ${fullId.slice(0, 8)}.`);
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
if (opts.public !== void 0) {
|
|
1491
|
-
const value = opts.public.toLowerCase();
|
|
1492
|
-
if (value === "off" || value === "none" || value === "no") {
|
|
1493
|
-
const metaResult = await svc.getMetadata();
|
|
1494
|
-
const sharing = metaResult.metadata?.sharing || {
|
|
1495
|
-
enabled: true,
|
|
1496
|
-
owner: "",
|
|
1497
|
-
allowedUsers: []
|
|
1498
|
-
};
|
|
1499
|
-
sharing.publicAccess = null;
|
|
1500
|
-
await svc.updateSharing(sharing);
|
|
1501
|
-
console.log(`Public access disabled for session ${fullId.slice(0, 8)}.`);
|
|
1502
|
-
} else if (value === "view" || value === "interact") {
|
|
1503
|
-
const metaResult = await svc.getMetadata();
|
|
1504
|
-
const sharing = metaResult.metadata?.sharing || {
|
|
1505
|
-
enabled: true,
|
|
1506
|
-
owner: "",
|
|
1507
|
-
allowedUsers: []
|
|
1508
|
-
};
|
|
1509
|
-
sharing.enabled = true;
|
|
1510
|
-
sharing.publicAccess = value;
|
|
1511
|
-
await svc.updateSharing(sharing);
|
|
1512
|
-
console.log(`Public access set to '${value}' for session ${fullId.slice(0, 8)}.`);
|
|
1513
|
-
} else {
|
|
1514
|
-
console.error("Invalid --public value. Use: view, interact, or off");
|
|
1515
|
-
process.exit(1);
|
|
1516
|
-
}
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
console.error("Usage: svamp session share <id> --add <email>[:<role>] | --remove <email> | --list | --public <view|interact|off>");
|
|
1520
|
-
process.exit(1);
|
|
1521
|
-
} finally {
|
|
1522
|
-
await server.disconnect();
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
async function machineShare(machineId, opts) {
|
|
1526
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1527
|
-
try {
|
|
1528
|
-
if (opts.list) {
|
|
1529
|
-
const info = await machine.getMachineInfo();
|
|
1530
|
-
const sharing = info.metadata?.sharing;
|
|
1531
|
-
if (!sharing || !sharing.enabled) {
|
|
1532
|
-
console.log("Sharing is not enabled for this machine.");
|
|
1533
|
-
} else {
|
|
1534
|
-
console.log(`Owner: ${sharing.owner}`);
|
|
1535
|
-
if (sharing.allowedUsers.length === 0) {
|
|
1536
|
-
console.log("No shared users.");
|
|
1537
|
-
} else {
|
|
1538
|
-
console.log("Shared users:");
|
|
1539
|
-
for (const u of sharing.allowedUsers) {
|
|
1540
|
-
console.log(` ${u.email.padEnd(30)} ${u.role.padEnd(10)} (added ${new Date(u.addedAt).toISOString().slice(0, 10)})`);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
const iso = info.metadata?.isolationCapabilities;
|
|
1545
|
-
if (iso) {
|
|
1546
|
-
console.log(`
|
|
1547
|
-
Isolation: ${iso.available.length > 0 ? iso.available.join(", ") : "none available"} (preferred: ${iso.preferred || "none"})`);
|
|
1548
|
-
}
|
|
1549
|
-
return;
|
|
1550
|
-
}
|
|
1551
|
-
if (opts.showConfig) {
|
|
1552
|
-
const result = await machine.getSecurityContextConfig();
|
|
1553
|
-
const config = result?.securityContextConfig;
|
|
1554
|
-
if (!config) {
|
|
1555
|
-
console.log("No security context config set for this machine.");
|
|
1556
|
-
} else {
|
|
1557
|
-
console.log(JSON.stringify(config, null, 2));
|
|
1558
|
-
}
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
if (opts.configPath) {
|
|
1562
|
-
const configPath = resolve(opts.configPath);
|
|
1563
|
-
const config = loadSecurityContextConfig(configPath);
|
|
1564
|
-
await machine.updateSecurityContextConfig(config);
|
|
1565
|
-
const userCount = config.users ? Object.keys(config.users).length : 0;
|
|
1566
|
-
console.log(`Security context config applied to machine.`);
|
|
1567
|
-
console.log(` Default context: ${config.default ? "yes" : "none"}`);
|
|
1568
|
-
console.log(` User-specific entries: ${userCount}`);
|
|
1569
|
-
if (config.users) {
|
|
1570
|
-
for (const email of Object.keys(config.users)) {
|
|
1571
|
-
const ctx = config.users[email];
|
|
1572
|
-
console.log(` ${email}: role=${ctx.role || "default"}`);
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
if (opts.add) {
|
|
1578
|
-
const { email, role } = parseShareArg(opts.add);
|
|
1579
|
-
const info = await machine.getMachineInfo();
|
|
1580
|
-
let sharing = info.metadata?.sharing || {
|
|
1581
|
-
enabled: true,
|
|
1582
|
-
owner: "",
|
|
1583
|
-
allowedUsers: []
|
|
1584
|
-
};
|
|
1585
|
-
sharing.enabled = true;
|
|
1586
|
-
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
1587
|
-
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
1588
|
-
);
|
|
1589
|
-
sharing.allowedUsers.push({
|
|
1590
|
-
email,
|
|
1591
|
-
role,
|
|
1592
|
-
addedAt: Date.now(),
|
|
1593
|
-
addedBy: "cli"
|
|
1594
|
-
});
|
|
1595
|
-
await machine.updateSharing(sharing);
|
|
1596
|
-
console.log(`Shared machine with ${email} (${role}).`);
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
if (opts.remove) {
|
|
1600
|
-
const email = opts.remove;
|
|
1601
|
-
const info = await machine.getMachineInfo();
|
|
1602
|
-
const sharing = info.metadata?.sharing;
|
|
1603
|
-
if (!sharing) {
|
|
1604
|
-
console.error("Sharing is not enabled for this machine.");
|
|
1605
|
-
process.exit(1);
|
|
1606
|
-
}
|
|
1607
|
-
const before = sharing.allowedUsers.length;
|
|
1608
|
-
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
1609
|
-
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
1610
|
-
);
|
|
1611
|
-
if (sharing.allowedUsers.length === before) {
|
|
1612
|
-
console.error(`User ${email} is not in the shared users list.`);
|
|
1613
|
-
process.exit(1);
|
|
1614
|
-
}
|
|
1615
|
-
await machine.updateSharing(sharing);
|
|
1616
|
-
console.log(`Removed ${email} from machine sharing.`);
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
console.error("Usage: svamp machine share --add <email>[:<role>] | --remove <email> | --list | --config <path> | --show-config");
|
|
1620
|
-
process.exit(1);
|
|
1621
|
-
} finally {
|
|
1622
|
-
await server.disconnect();
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
async function machineExec(machineId, command, cwd) {
|
|
1626
|
-
if (!command) {
|
|
1627
|
-
console.error("Usage: svamp machine exec <command> [--cwd <path>]");
|
|
1628
|
-
process.exit(1);
|
|
1629
|
-
}
|
|
1630
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1631
|
-
try {
|
|
1632
|
-
const result = await machine.bash(command, cwd || void 0);
|
|
1633
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
1634
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
1635
|
-
process.exit(result.exitCode);
|
|
1636
|
-
} finally {
|
|
1637
|
-
await server.disconnect();
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
async function machineInfo(machineId) {
|
|
1641
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1642
|
-
try {
|
|
1643
|
-
const info = await machine.getMachineInfo();
|
|
1644
|
-
const meta = info.metadata || {};
|
|
1645
|
-
const ds = info.daemonState || {};
|
|
1646
|
-
console.log(`Machine: ${info.machineId}`);
|
|
1647
|
-
if (meta.hostname) console.log(`Host: ${meta.hostname}`);
|
|
1648
|
-
if (meta.platform) console.log(`OS: ${meta.platform} ${meta.arch || ""}`);
|
|
1649
|
-
console.log(`Status: ${ds.status || "unknown"}`);
|
|
1650
|
-
const iso = meta.isolationCapabilities;
|
|
1651
|
-
if (iso) {
|
|
1652
|
-
console.log(`Isolation: ${iso.available?.length > 0 ? iso.available.join(", ") : "none"} (preferred: ${iso.preferred || "none"})`);
|
|
1653
|
-
}
|
|
1654
|
-
const sessions = await machine.listSessions();
|
|
1655
|
-
console.log(`Sessions: ${Array.isArray(sessions) ? sessions.length : 0} active`);
|
|
1656
|
-
const sharing = meta.sharing;
|
|
1657
|
-
if (sharing?.enabled) {
|
|
1658
|
-
console.log(`Sharing: enabled (owner: ${sharing.owner}, ${sharing.allowedUsers?.length || 0} users)`);
|
|
1659
|
-
} else {
|
|
1660
|
-
console.log(`Sharing: disabled`);
|
|
1661
|
-
}
|
|
1662
|
-
} finally {
|
|
1663
|
-
await server.disconnect();
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
async function machineLs(machineId, path, showHidden) {
|
|
1667
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1668
|
-
try {
|
|
1669
|
-
const result = await machine.listDirectory(path || "", { showHidden: showHidden || false });
|
|
1670
|
-
if (!result.success) {
|
|
1671
|
-
console.error(`Error: ${result.error || "Failed to list directory"}`);
|
|
1672
|
-
process.exit(1);
|
|
1673
|
-
}
|
|
1674
|
-
console.log(`${result.path}/`);
|
|
1675
|
-
for (const entry of result.entries || []) {
|
|
1676
|
-
const prefix = entry.type === "directory" ? "d " : " ";
|
|
1677
|
-
console.log(`${prefix}${entry.name}${entry.type === "directory" ? "/" : ""}`);
|
|
1678
|
-
}
|
|
1679
|
-
} finally {
|
|
1680
|
-
await server.disconnect();
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
async function sessionRalphStart(sessionIdPartial, task, machineId, opts) {
|
|
1684
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1685
|
-
try {
|
|
1686
|
-
const sessions = await machine.listSessions();
|
|
1687
|
-
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1688
|
-
const fullId = match.sessionId;
|
|
1689
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1690
|
-
const { metadata } = await svc.getMetadata();
|
|
1691
|
-
if (metadata?.ralphLoop?.active) {
|
|
1692
|
-
console.error(`Ralph loop is already active (iteration ${metadata.ralphLoop.currentIteration}). Cancel it first with: svamp session ralph-cancel ${sessionIdPartial}`);
|
|
1693
|
-
process.exit(1);
|
|
1694
|
-
}
|
|
1695
|
-
const completionPromise = opts?.completionPromise || "DONE";
|
|
1696
|
-
const maxIterations = opts?.maxIterations ?? 0;
|
|
1697
|
-
const cooldownSeconds = opts?.cooldownSeconds ?? 1;
|
|
1698
|
-
const contextMode = opts?.contextMode || void 0;
|
|
1699
|
-
await svc.updateConfig({
|
|
1700
|
-
ralph_loop: {
|
|
1701
|
-
task,
|
|
1702
|
-
completion_promise: completionPromise,
|
|
1703
|
-
max_iterations: maxIterations,
|
|
1704
|
-
cooldown_seconds: cooldownSeconds,
|
|
1705
|
-
...contextMode ? { context_mode: contextMode } : {}
|
|
1706
|
-
}
|
|
1707
|
-
});
|
|
1708
|
-
console.log(`Ralph loop started on session ${fullId.slice(0, 8)}`);
|
|
1709
|
-
console.log(` Task: ${task.slice(0, 100)}${task.length > 100 ? "..." : ""}`);
|
|
1710
|
-
console.log(` Completion promise: ${completionPromise}`);
|
|
1711
|
-
console.log(` Max iterations: ${maxIterations > 0 ? maxIterations : "unlimited"}`);
|
|
1712
|
-
if (contextMode) console.log(` Context mode: ${contextMode}`);
|
|
1713
|
-
} finally {
|
|
1714
|
-
await server.disconnect();
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
async function sessionRalphCancel(sessionIdPartial, machineId) {
|
|
1718
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1719
|
-
try {
|
|
1720
|
-
const sessions = await machine.listSessions();
|
|
1721
|
-
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1722
|
-
const fullId = match.sessionId;
|
|
1723
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1724
|
-
const { metadata } = await svc.getMetadata();
|
|
1725
|
-
if (!metadata?.ralphLoop?.active) {
|
|
1726
|
-
console.log("No active Ralph loop on this session.");
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
const iteration = metadata.ralphLoop.currentIteration;
|
|
1730
|
-
await svc.updateConfig({ ralph_loop: null });
|
|
1731
|
-
console.log(`Ralph loop cancelled on session ${fullId.slice(0, 8)} (was at iteration ${iteration})`);
|
|
1732
|
-
} finally {
|
|
1733
|
-
await server.disconnect();
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
async function sessionRalphStatus(sessionIdPartial, machineId) {
|
|
1737
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1738
|
-
try {
|
|
1739
|
-
const sessions = await machine.listSessions();
|
|
1740
|
-
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1741
|
-
const fullId = match.sessionId;
|
|
1742
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1743
|
-
const { metadata } = await svc.getMetadata();
|
|
1744
|
-
const ralph = metadata?.ralphLoop;
|
|
1745
|
-
if (!ralph || !ralph.active) {
|
|
1746
|
-
console.log("No Ralph loop configured on this session.");
|
|
1747
|
-
return;
|
|
1748
|
-
}
|
|
1749
|
-
console.log(`Ralph loop on session ${fullId.slice(0, 8)}:`);
|
|
1750
|
-
console.log(` Active: ${ralph.active}`);
|
|
1751
|
-
console.log(` Task: ${ralph.task?.slice(0, 100)}${ralph.task?.length > 100 ? "..." : ""}`);
|
|
1752
|
-
console.log(` Completion promise: ${ralph.completionPromise}`);
|
|
1753
|
-
console.log(` Iteration: ${ralph.currentIteration}${ralph.maxIterations > 0 ? `/${ralph.maxIterations}` : " (unlimited)"}`);
|
|
1754
|
-
console.log(` Started at: ${ralph.startedAt}`);
|
|
1755
|
-
} finally {
|
|
1756
|
-
await server.disconnect();
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
async function sessionInboxSend(sessionIdPartial, body, machineId, opts) {
|
|
1760
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1761
|
-
try {
|
|
1762
|
-
const sessions = await machine.listSessions();
|
|
1763
|
-
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1764
|
-
const fullId = match.sessionId;
|
|
1765
|
-
const { randomUUID } = await import('node:crypto');
|
|
1766
|
-
const message = {
|
|
1767
|
-
messageId: randomUUID(),
|
|
1768
|
-
body,
|
|
1769
|
-
timestamp: Date.now(),
|
|
1770
|
-
read: false,
|
|
1771
|
-
from: `cli:${os.userInfo().username}`,
|
|
1772
|
-
to: fullId,
|
|
1773
|
-
subject: opts?.subject,
|
|
1774
|
-
urgency: opts?.urgency || "normal",
|
|
1775
|
-
replyTo: opts?.replyTo,
|
|
1776
|
-
threadId: opts?.threadId
|
|
1777
|
-
};
|
|
1778
|
-
const result = await machine.sessionRPC(fullId, "sendInboxMessage", { message });
|
|
1779
|
-
if (opts?.json) {
|
|
1780
|
-
console.log(formatJson({ sessionId: fullId, messageId: result.messageId, sent: true }));
|
|
1781
|
-
} else {
|
|
1782
|
-
console.log(`Inbox message sent to session ${fullId.slice(0, 8)} (id: ${result.messageId.slice(0, 8)})`);
|
|
1783
|
-
}
|
|
1784
|
-
} finally {
|
|
1785
|
-
await server.disconnect();
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
async function sessionInboxList(sessionIdPartial, machineId, opts) {
|
|
1789
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1790
|
-
try {
|
|
1791
|
-
const sessions = await machine.listSessions();
|
|
1792
|
-
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1793
|
-
const fullId = match.sessionId;
|
|
1794
|
-
const result = await machine.sessionRPC(fullId, "getInbox", { opts: { unread: opts?.unread, limit: opts?.limit } });
|
|
1795
|
-
const messages = result.messages;
|
|
1796
|
-
if (opts?.json) {
|
|
1797
|
-
console.log(formatJson({ sessionId: fullId, messages }));
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
if (messages.length === 0) {
|
|
1801
|
-
console.log(`Inbox for session ${fullId.slice(0, 8)} is empty.`);
|
|
1802
|
-
return;
|
|
1803
|
-
}
|
|
1804
|
-
const unreadCount = messages.filter((m) => !m.read).length;
|
|
1805
|
-
console.log(`Inbox for session ${fullId.slice(0, 8)} (${messages.length} message(s), ${unreadCount} unread):
|
|
1806
|
-
`);
|
|
1807
|
-
for (const msg of messages) {
|
|
1808
|
-
const status = msg.read ? " " : "\u25CF";
|
|
1809
|
-
const from = msg.from ? ` from ${msg.from}` : "";
|
|
1810
|
-
const subject = msg.subject ? ` \u2014 ${msg.subject}` : "";
|
|
1811
|
-
const urgencyTag = msg.urgency === "urgent" ? " [URGENT]" : "";
|
|
1812
|
-
const date = new Date(msg.timestamp).toLocaleString();
|
|
1813
|
-
const preview = msg.body.length > 100 ? msg.body.slice(0, 97) + "..." : msg.body;
|
|
1814
|
-
console.log(` ${status} ${msg.messageId.slice(0, 8)}${urgencyTag}${from}${subject}`);
|
|
1815
|
-
console.log(` ${date}`);
|
|
1816
|
-
console.log(` ${preview}
|
|
1817
|
-
`);
|
|
1818
|
-
}
|
|
1819
|
-
} finally {
|
|
1820
|
-
await server.disconnect();
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
async function sessionInboxRead(sessionIdPartial, messageId, machineId) {
|
|
1824
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1825
|
-
try {
|
|
1826
|
-
const sessions = await machine.listSessions();
|
|
1827
|
-
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1828
|
-
const fullId = match.sessionId;
|
|
1829
|
-
const result = await machine.sessionRPC(fullId, "getInbox", {});
|
|
1830
|
-
const msg = result.messages.find((m) => m.messageId === messageId || m.messageId.startsWith(messageId));
|
|
1831
|
-
if (!msg) {
|
|
1832
|
-
console.error(`Message ${messageId} not found in inbox.`);
|
|
1833
|
-
process.exit(1);
|
|
1834
|
-
}
|
|
1835
|
-
if (!msg.read) {
|
|
1836
|
-
await machine.sessionRPC(fullId, "markInboxRead", { messageId: msg.messageId });
|
|
1837
|
-
}
|
|
1838
|
-
console.log(`From: ${msg.from || "(unknown)"}`);
|
|
1839
|
-
if (msg.subject) console.log(`Subject: ${msg.subject}`);
|
|
1840
|
-
console.log(`Date: ${new Date(msg.timestamp).toLocaleString()}`);
|
|
1841
|
-
if (msg.urgency === "urgent") console.log("Urgency: URGENT");
|
|
1842
|
-
if (msg.replyTo) console.log(`Reply-To: ${msg.replyTo.slice(0, 8)}`);
|
|
1843
|
-
if (msg.threadId) console.log(`Thread: ${msg.threadId.slice(0, 8)}`);
|
|
1844
|
-
console.log(`
|
|
1845
|
-
${msg.body}`);
|
|
1846
|
-
} finally {
|
|
1847
|
-
await server.disconnect();
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
async function sessionInboxReply(sessionIdPartial, messageId, body, machineId) {
|
|
1851
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1852
|
-
try {
|
|
1853
|
-
const sessions = await machine.listSessions();
|
|
1854
|
-
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1855
|
-
const fullId = match.sessionId;
|
|
1856
|
-
const result = await machine.sessionRPC(fullId, "getInbox", {});
|
|
1857
|
-
const original = result.messages.find((m) => m.messageId === messageId || m.messageId.startsWith(messageId));
|
|
1858
|
-
if (!original) {
|
|
1859
|
-
console.error(`Message ${messageId} not found in inbox.`);
|
|
1860
|
-
process.exit(1);
|
|
1861
|
-
}
|
|
1862
|
-
if (!original.fromSession) {
|
|
1863
|
-
console.error("Cannot reply: original message has no fromSession.");
|
|
1864
|
-
process.exit(1);
|
|
1865
|
-
}
|
|
1866
|
-
const { randomUUID } = await import('node:crypto');
|
|
1867
|
-
const reply = {
|
|
1868
|
-
messageId: randomUUID(),
|
|
1869
|
-
body,
|
|
1870
|
-
timestamp: Date.now(),
|
|
1871
|
-
read: false,
|
|
1872
|
-
from: `session:${fullId}`,
|
|
1873
|
-
fromSession: fullId,
|
|
1874
|
-
to: original.fromSession,
|
|
1875
|
-
subject: original.subject ? `Re: ${original.subject}` : void 0,
|
|
1876
|
-
urgency: "normal",
|
|
1877
|
-
replyTo: original.messageId,
|
|
1878
|
-
threadId: original.threadId || original.messageId
|
|
1879
|
-
};
|
|
1880
|
-
const sendResult = await machine.sessionRPC(original.fromSession, "sendInboxMessage", { message: reply });
|
|
1881
|
-
console.log(`Reply sent to session ${original.fromSession.slice(0, 8)} (id: ${sendResult.messageId.slice(0, 8)})`);
|
|
1882
|
-
} finally {
|
|
1883
|
-
await server.disconnect();
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
async function sessionInboxClear(sessionIdPartial, machineId, opts) {
|
|
1887
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1888
|
-
try {
|
|
1889
|
-
const sessions = await machine.listSessions();
|
|
1890
|
-
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1891
|
-
const fullId = match.sessionId;
|
|
1892
|
-
const result = await machine.sessionRPC(fullId, "clearInbox", { opts: { all: opts?.all } });
|
|
1893
|
-
console.log(`Cleared inbox on session ${fullId.slice(0, 8)} (${result.remaining} remaining)`);
|
|
1894
|
-
} finally {
|
|
1895
|
-
await server.disconnect();
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
export { connectAndGetMachine, createWorktree, generateWorktreeName, machineExec, machineInfo, machineLs, machineShare, parseShareArg, renderMessage, resolveSessionId, sessionApprove, sessionAttach, sessionDeny, sessionInboxClear, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxSend, sessionInfo, sessionList, sessionMachines, sessionMessages, sessionRalphCancel, sessionRalphStart, sessionRalphStatus, sessionSend, sessionShare, sessionSpawn, sessionStop, sessionWait };
|