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