svamp-cli 0.1.43 → 0.1.46
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 +87 -13
- package/dist/commands-8Xn02pQg.mjs +1217 -0
- package/dist/commands-BD_NjWJL.mjs +1217 -0
- package/dist/commands-BIFQZZGw.mjs +1375 -0
- package/dist/commands-BLmRIMdf.mjs +1217 -0
- package/dist/commands-BQ_347V_.mjs +1374 -0
- package/dist/commands-BVjcCbWS.mjs +1375 -0
- package/dist/commands-CFv6lO0D.mjs +1217 -0
- package/dist/commands-CJ2n5jS2.mjs +1375 -0
- package/dist/commands-CKEKQ_5B.mjs +1217 -0
- package/dist/commands-CRZbJjqN.mjs +1375 -0
- package/dist/commands-CWsfciHn.mjs +1217 -0
- package/dist/commands-Cc73uUnP.mjs +1375 -0
- package/dist/commands-CorUNLRF.mjs +1375 -0
- package/dist/commands-DLoe6FyK.mjs +1375 -0
- package/dist/commands-DVygnMsh.mjs +1217 -0
- package/dist/commands-DZfaDmsk.mjs +1374 -0
- package/dist/index.mjs +1 -1
- package/dist/package-BufekbY1.mjs +57 -0
- package/dist/package-CmIBOZtY.mjs +57 -0
- package/dist/package-CmVt1kdw.mjs +58 -0
- package/dist/package-DRO1LpXW.mjs +58 -0
- package/dist/package-UwLIU765.mjs +58 -0
- package/dist/package-rasGC9_z.mjs +58 -0
- package/dist/run-B2zRMxE0.mjs +5508 -0
- package/dist/run-B31biy0V.mjs +1050 -0
- package/dist/run-B7V-xXM7.mjs +5775 -0
- package/dist/run-BREPr7Yc.mjs +5508 -0
- package/dist/run-BTwshVk1.mjs +5728 -0
- package/dist/run-BWsDPiNe.mjs +1050 -0
- package/dist/run-BXYfq8mK.mjs +5836 -0
- package/dist/run-BY12Ataq.mjs +5732 -0
- package/dist/run-BieEN0Pg.mjs +5761 -0
- package/dist/run-BlEFlhfn.mjs +5510 -0
- package/dist/run-C1lS3SwN.mjs +5733 -0
- package/dist/run-C9Hrqjy_.mjs +1050 -0
- package/dist/run-CCcW4asS.mjs +1050 -0
- package/dist/run-CHyN5U0t.mjs +1050 -0
- package/dist/run-COWb9ovq.mjs +1050 -0
- package/dist/run-CSk7i0Hq.mjs +1050 -0
- package/dist/run-C_8iOjO1.mjs +5892 -0
- package/dist/run-CxGAa9MH.mjs +1050 -0
- package/dist/run-D39C7Ta3.mjs +1050 -0
- package/dist/run-D3bhRCCb.mjs +1051 -0
- package/dist/run-DGsXW19O.mjs +5541 -0
- package/dist/run-DHrF2xpW.mjs +5776 -0
- package/dist/run-DJ4k0WzZ.mjs +1051 -0
- package/dist/run-DNX3djCI.mjs +1050 -0
- package/dist/run-DTkldU6a.mjs +1050 -0
- package/dist/run-DU10B3gK.mjs +5728 -0
- package/dist/run-DVZGKdKO.mjs +1050 -0
- package/dist/run-DXJ2M19k.mjs +1050 -0
- package/dist/run-DZOeccNu.mjs +5484 -0
- package/dist/run-De-wkVl3.mjs +5487 -0
- package/dist/run-Dfl3Ze2L.mjs +5541 -0
- package/dist/run-Dge2K7h1.mjs +1050 -0
- package/package.json +3 -2
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-Dfl3Ze2L.mjs';
|
|
5
|
+
import 'os';
|
|
6
|
+
import 'fs/promises';
|
|
7
|
+
import 'fs';
|
|
8
|
+
import 'path';
|
|
9
|
+
import 'url';
|
|
10
|
+
import 'child_process';
|
|
11
|
+
import 'crypto';
|
|
12
|
+
import 'node:crypto';
|
|
13
|
+
import 'node:child_process';
|
|
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 toMarkdownInline(value) {
|
|
23
|
+
const escaped = value.replace(/`/g, "\\`");
|
|
24
|
+
return `\`${escaped}\``;
|
|
25
|
+
}
|
|
26
|
+
function formatSessionStatus(data) {
|
|
27
|
+
const lines = [
|
|
28
|
+
"## Session Status",
|
|
29
|
+
"",
|
|
30
|
+
`- Session ID: ${toMarkdownInline(data.sessionId)}`,
|
|
31
|
+
`- Agent: ${data.flavor}`
|
|
32
|
+
];
|
|
33
|
+
if (data.name) lines.push(`- Name: ${data.name}`);
|
|
34
|
+
if (data.summary) lines.push(`- Summary: ${data.summary}`);
|
|
35
|
+
if (data.path) lines.push(`- Path: ${data.path}`);
|
|
36
|
+
if (data.host) lines.push(`- Host: ${data.host}`);
|
|
37
|
+
if (data.lifecycleState) lines.push(`- Lifecycle: ${data.lifecycleState}`);
|
|
38
|
+
lines.push(`- Active: ${data.active ? "yes" : "no"}`);
|
|
39
|
+
lines.push(`- Thinking: ${data.thinking ? "yes" : "no"}`);
|
|
40
|
+
lines.push(`- Agent Status: ${data.active ? "busy" : "idle"}`);
|
|
41
|
+
if (data.startedBy) lines.push(`- Started By: ${data.startedBy}`);
|
|
42
|
+
if (data.claudeSessionId) lines.push(`- Claude Session: ${data.claudeSessionId}`);
|
|
43
|
+
if (data.sessionLink) lines.push(`- Link: ${data.sessionLink}`);
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
46
|
+
function formatJson(data) {
|
|
47
|
+
return JSON.stringify(data, null, 2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
|
|
51
|
+
const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
|
|
52
|
+
const ENV_FILE = join(SVAMP_HOME, ".env");
|
|
53
|
+
function loadDotEnv() {
|
|
54
|
+
if (!existsSync(ENV_FILE)) return;
|
|
55
|
+
const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
59
|
+
const eqIdx = trimmed.indexOf("=");
|
|
60
|
+
if (eqIdx === -1) continue;
|
|
61
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
62
|
+
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
63
|
+
if (!process.env[key]) {
|
|
64
|
+
process.env[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function readDaemonState() {
|
|
69
|
+
if (!existsSync(DAEMON_STATE_FILE)) return null;
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(readFileSync(DAEMON_STATE_FILE, "utf-8"));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function isDaemonAlive(state) {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(state.pid, 0);
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function connectAndGetMachine(machineId) {
|
|
85
|
+
loadDotEnv();
|
|
86
|
+
const state = readDaemonState();
|
|
87
|
+
if (!state || !isDaemonAlive(state)) {
|
|
88
|
+
console.error('Daemon is not running. Start it with "svamp daemon start".');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
|
|
92
|
+
const token = process.env.HYPHA_TOKEN;
|
|
93
|
+
if (!serverUrl) {
|
|
94
|
+
console.error('No Hypha server URL. Run "svamp login <url>" first.');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
const origLog = console.log;
|
|
98
|
+
const origWarn = console.warn;
|
|
99
|
+
const origInfo = console.info;
|
|
100
|
+
const origError = console.error;
|
|
101
|
+
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
102
|
+
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
103
|
+
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"));
|
|
104
|
+
console.log = () => {
|
|
105
|
+
};
|
|
106
|
+
console.warn = () => {
|
|
107
|
+
};
|
|
108
|
+
console.info = () => {
|
|
109
|
+
};
|
|
110
|
+
console.error = (...args) => {
|
|
111
|
+
if (args.some((a) => isHyphaLog(a))) return;
|
|
112
|
+
origError(...args);
|
|
113
|
+
};
|
|
114
|
+
process.stdout.write = (chunk, ...args) => {
|
|
115
|
+
if (isHyphaLog(chunk)) return true;
|
|
116
|
+
return stdoutWrite(chunk, ...args);
|
|
117
|
+
};
|
|
118
|
+
process.stderr.write = (chunk, ...args) => {
|
|
119
|
+
if (isHyphaLog(chunk)) return true;
|
|
120
|
+
return stderrWrite(chunk, ...args);
|
|
121
|
+
};
|
|
122
|
+
const restoreConsole = () => {
|
|
123
|
+
console.log = origLog;
|
|
124
|
+
console.warn = origWarn;
|
|
125
|
+
console.info = origInfo;
|
|
126
|
+
console.error = origError;
|
|
127
|
+
};
|
|
128
|
+
let server;
|
|
129
|
+
try {
|
|
130
|
+
server = await connectToHypha({
|
|
131
|
+
serverUrl,
|
|
132
|
+
token,
|
|
133
|
+
name: "svamp-session-cli"
|
|
134
|
+
});
|
|
135
|
+
} catch (err) {
|
|
136
|
+
restoreConsole();
|
|
137
|
+
console.error(`Failed to connect to Hypha: ${err.message}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
let machine;
|
|
141
|
+
try {
|
|
142
|
+
const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
|
|
143
|
+
if (services.length === 0) {
|
|
144
|
+
restoreConsole();
|
|
145
|
+
console.error("No machine service found. Is the daemon registered on Hypha?");
|
|
146
|
+
await server.disconnect();
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
let selectedService;
|
|
150
|
+
if (machineId) {
|
|
151
|
+
const exact = services.find((s) => (s.id || s.name) === machineId);
|
|
152
|
+
if (exact) {
|
|
153
|
+
selectedService = exact;
|
|
154
|
+
} else {
|
|
155
|
+
const prefixMatches = services.filter((s) => {
|
|
156
|
+
const id = s.id || s.name;
|
|
157
|
+
return id.startsWith(machineId);
|
|
158
|
+
});
|
|
159
|
+
if (prefixMatches.length === 1) {
|
|
160
|
+
selectedService = prefixMatches[0];
|
|
161
|
+
} else if (prefixMatches.length === 0) {
|
|
162
|
+
const substringMatches = services.filter((s) => {
|
|
163
|
+
const id = s.id || s.name || "";
|
|
164
|
+
return id.includes(machineId);
|
|
165
|
+
});
|
|
166
|
+
if (substringMatches.length === 1) {
|
|
167
|
+
selectedService = substringMatches[0];
|
|
168
|
+
} else {
|
|
169
|
+
restoreConsole();
|
|
170
|
+
console.error(`No machine found matching: ${machineId}`);
|
|
171
|
+
console.error("Available machines:");
|
|
172
|
+
for (const s of services) {
|
|
173
|
+
console.error(` ${s.id || s.name}`);
|
|
174
|
+
}
|
|
175
|
+
await server.disconnect();
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
restoreConsole();
|
|
180
|
+
console.error(`Ambiguous machine ID "${machineId}". Matches:`);
|
|
181
|
+
for (const s of prefixMatches) {
|
|
182
|
+
console.error(` ${s.id || s.name}`);
|
|
183
|
+
}
|
|
184
|
+
await server.disconnect();
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
if (state.hyphaClientId) {
|
|
190
|
+
const localMatch = services.find((s) => {
|
|
191
|
+
const id = s.id || s.name || "";
|
|
192
|
+
return id.includes(state.hyphaClientId);
|
|
193
|
+
});
|
|
194
|
+
selectedService = localMatch || services[0];
|
|
195
|
+
} else if (state.machineId) {
|
|
196
|
+
const localMatch = services.find((s) => {
|
|
197
|
+
const id = s.id || s.name || "";
|
|
198
|
+
return id.includes(state.machineId);
|
|
199
|
+
});
|
|
200
|
+
selectedService = localMatch || services[0];
|
|
201
|
+
} else {
|
|
202
|
+
selectedService = services[0];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const svcId = selectedService.id || selectedService.name;
|
|
206
|
+
machine = await server.getService(svcId);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
restoreConsole();
|
|
209
|
+
console.error(`Failed to discover machine service: ${err.message}`);
|
|
210
|
+
await server.disconnect();
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
restoreConsole();
|
|
214
|
+
return { server, machine };
|
|
215
|
+
}
|
|
216
|
+
async function connectAndGetAllMachines() {
|
|
217
|
+
loadDotEnv();
|
|
218
|
+
const state = readDaemonState();
|
|
219
|
+
if (!state || !isDaemonAlive(state)) {
|
|
220
|
+
console.error('Daemon is not running. Start it with "svamp daemon start".');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
|
|
224
|
+
const token = process.env.HYPHA_TOKEN;
|
|
225
|
+
if (!serverUrl) {
|
|
226
|
+
console.error('No Hypha server URL. Run "svamp login <url>" first.');
|
|
227
|
+
process.exit(1);
|
|
228
|
+
}
|
|
229
|
+
const origLog = console.log;
|
|
230
|
+
const origWarn = console.warn;
|
|
231
|
+
const origInfo = console.info;
|
|
232
|
+
const origError = console.error;
|
|
233
|
+
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
234
|
+
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
235
|
+
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"));
|
|
236
|
+
console.log = () => {
|
|
237
|
+
};
|
|
238
|
+
console.warn = () => {
|
|
239
|
+
};
|
|
240
|
+
console.info = () => {
|
|
241
|
+
};
|
|
242
|
+
console.error = (...args) => {
|
|
243
|
+
if (!args.some((a) => isHyphaLog(a))) origError(...args);
|
|
244
|
+
};
|
|
245
|
+
process.stdout.write = (chunk, ...args) => {
|
|
246
|
+
if (isHyphaLog(chunk)) return true;
|
|
247
|
+
return stdoutWrite(chunk, ...args);
|
|
248
|
+
};
|
|
249
|
+
process.stderr.write = (chunk, ...args) => {
|
|
250
|
+
if (isHyphaLog(chunk)) return true;
|
|
251
|
+
return stderrWrite(chunk, ...args);
|
|
252
|
+
};
|
|
253
|
+
const restoreConsole = () => {
|
|
254
|
+
console.log = origLog;
|
|
255
|
+
console.warn = origWarn;
|
|
256
|
+
console.info = origInfo;
|
|
257
|
+
console.error = origError;
|
|
258
|
+
};
|
|
259
|
+
let server;
|
|
260
|
+
try {
|
|
261
|
+
server = await connectToHypha({ serverUrl, token, name: "svamp-session-cli" });
|
|
262
|
+
} catch (err) {
|
|
263
|
+
restoreConsole();
|
|
264
|
+
console.error(`Failed to connect to Hypha: ${err.message}`);
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
const machines = [];
|
|
268
|
+
try {
|
|
269
|
+
const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
|
|
270
|
+
for (const svc of services) {
|
|
271
|
+
try {
|
|
272
|
+
const svcId = svc.id || svc.name;
|
|
273
|
+
machines.push(await server.getService(svcId));
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
restoreConsole();
|
|
279
|
+
console.error(`Failed to discover machine services: ${err.message}`);
|
|
280
|
+
await server.disconnect();
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
restoreConsole();
|
|
284
|
+
if (machines.length === 0) {
|
|
285
|
+
console.error("No machine service found. Is the daemon registered on Hypha?");
|
|
286
|
+
await server.disconnect();
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
return { server, machines };
|
|
290
|
+
}
|
|
291
|
+
async function sessionMachines() {
|
|
292
|
+
loadDotEnv();
|
|
293
|
+
const state = readDaemonState();
|
|
294
|
+
if (!state || !isDaemonAlive(state)) {
|
|
295
|
+
console.error('Daemon is not running. Start it with "svamp daemon start".');
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
|
|
299
|
+
const token = process.env.HYPHA_TOKEN;
|
|
300
|
+
if (!serverUrl) {
|
|
301
|
+
console.error('No Hypha server URL. Run "svamp login <url>" first.');
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
const origLog = console.log;
|
|
305
|
+
const origWarn = console.warn;
|
|
306
|
+
const origInfo = console.info;
|
|
307
|
+
const origError = console.error;
|
|
308
|
+
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
309
|
+
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
310
|
+
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"));
|
|
311
|
+
console.log = () => {
|
|
312
|
+
};
|
|
313
|
+
console.warn = () => {
|
|
314
|
+
};
|
|
315
|
+
console.info = () => {
|
|
316
|
+
};
|
|
317
|
+
console.error = (...args) => {
|
|
318
|
+
if (args.some((a) => isHyphaLog(a))) return;
|
|
319
|
+
origError(...args);
|
|
320
|
+
};
|
|
321
|
+
process.stdout.write = (chunk, ...args) => {
|
|
322
|
+
if (isHyphaLog(chunk)) return true;
|
|
323
|
+
return stdoutWrite(chunk, ...args);
|
|
324
|
+
};
|
|
325
|
+
process.stderr.write = (chunk, ...args) => {
|
|
326
|
+
if (isHyphaLog(chunk)) return true;
|
|
327
|
+
return stderrWrite(chunk, ...args);
|
|
328
|
+
};
|
|
329
|
+
const restoreConsole = () => {
|
|
330
|
+
console.log = origLog;
|
|
331
|
+
console.warn = origWarn;
|
|
332
|
+
console.info = origInfo;
|
|
333
|
+
console.error = origError;
|
|
334
|
+
};
|
|
335
|
+
let server;
|
|
336
|
+
try {
|
|
337
|
+
server = await connectToHypha({
|
|
338
|
+
serverUrl,
|
|
339
|
+
token,
|
|
340
|
+
name: "svamp-session-cli"
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
restoreConsole();
|
|
344
|
+
console.error(`Failed to connect to Hypha: ${err.message}`);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
|
|
349
|
+
restoreConsole();
|
|
350
|
+
if (services.length === 0) {
|
|
351
|
+
console.log("No machines found.");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const machines = [];
|
|
355
|
+
for (const svc of services) {
|
|
356
|
+
const svcId = svc.id || svc.name;
|
|
357
|
+
try {
|
|
358
|
+
const machineSvc = await server.getService(svcId);
|
|
359
|
+
const info = await machineSvc.getMachineInfo();
|
|
360
|
+
const sessions = await machineSvc.listSessions();
|
|
361
|
+
machines.push({
|
|
362
|
+
serviceId: svcId,
|
|
363
|
+
machineId: info.machineId || svcId,
|
|
364
|
+
displayName: info.metadata?.displayName || info.metadata?.host || "-",
|
|
365
|
+
platform: info.metadata?.platform || "-",
|
|
366
|
+
host: info.metadata?.host || "-",
|
|
367
|
+
sessions: sessions.length,
|
|
368
|
+
status: info.daemonState?.status || "unknown"
|
|
369
|
+
});
|
|
370
|
+
} catch {
|
|
371
|
+
machines.push({
|
|
372
|
+
serviceId: svcId,
|
|
373
|
+
machineId: svcId,
|
|
374
|
+
displayName: "-",
|
|
375
|
+
platform: "-",
|
|
376
|
+
host: "-",
|
|
377
|
+
sessions: -1,
|
|
378
|
+
status: "unreachable"
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const header = `${"MACHINE ID".padEnd(20)} ${"NAME".padEnd(20)} ${"PLATFORM".padEnd(12)} ${"HOST".padEnd(25)} ${"SESSIONS".padEnd(10)} ${"STATUS"}`;
|
|
383
|
+
console.log(header);
|
|
384
|
+
console.log("-".repeat(header.length));
|
|
385
|
+
for (const m of machines) {
|
|
386
|
+
const id = truncate(m.machineId, 18).padEnd(20);
|
|
387
|
+
const name = truncate(m.displayName, 18).padEnd(20);
|
|
388
|
+
const platform = m.platform.padEnd(12);
|
|
389
|
+
const host = truncate(m.host, 23).padEnd(25);
|
|
390
|
+
const sessions = m.sessions >= 0 ? String(m.sessions).padEnd(10) : "-".padEnd(10);
|
|
391
|
+
const status = m.status === "running" ? `\x1B[32m${m.status}\x1B[0m` : m.status === "unreachable" ? `\x1B[31m${m.status}\x1B[0m` : m.status;
|
|
392
|
+
console.log(`${id} ${name} ${platform} ${host} ${sessions} ${status}`);
|
|
393
|
+
}
|
|
394
|
+
console.log(`
|
|
395
|
+
${machines.length} machine(s) found.`);
|
|
396
|
+
console.log("Use --machine <id> to target a specific machine.");
|
|
397
|
+
} finally {
|
|
398
|
+
await server.disconnect();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function resolveSessionId(sessions, partial) {
|
|
402
|
+
const exact = sessions.find((s) => s.sessionId === partial);
|
|
403
|
+
if (exact) return exact;
|
|
404
|
+
const matches = sessions.filter((s) => s.sessionId.startsWith(partial));
|
|
405
|
+
if (matches.length === 1) return matches[0];
|
|
406
|
+
if (matches.length === 0) {
|
|
407
|
+
console.error(`No session found matching: ${partial}`);
|
|
408
|
+
console.error('Run "svamp session list" to see active sessions.');
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
console.error(`Ambiguous session ID "${partial}". Matches:`);
|
|
412
|
+
for (const s of matches) {
|
|
413
|
+
console.error(` ${s.sessionId}`);
|
|
414
|
+
}
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
function truncate(str, max) {
|
|
418
|
+
if (str.length <= max) return str;
|
|
419
|
+
return "..." + str.slice(str.length - max + 3);
|
|
420
|
+
}
|
|
421
|
+
function renderMessage(msg) {
|
|
422
|
+
const content = msg.content;
|
|
423
|
+
if (!content) return;
|
|
424
|
+
const role = content.role;
|
|
425
|
+
if (role === "user") {
|
|
426
|
+
const data = content.content;
|
|
427
|
+
let text;
|
|
428
|
+
if (typeof data === "string") {
|
|
429
|
+
try {
|
|
430
|
+
const parsed = JSON.parse(data);
|
|
431
|
+
text = parsed?.text || parsed?.content?.text || data;
|
|
432
|
+
} catch {
|
|
433
|
+
text = data;
|
|
434
|
+
}
|
|
435
|
+
} else if (data?.text) {
|
|
436
|
+
text = data.text;
|
|
437
|
+
} else if (data?.type === "text") {
|
|
438
|
+
text = data.text || "";
|
|
439
|
+
} else {
|
|
440
|
+
text = typeof data === "object" ? JSON.stringify(data) : String(data || "");
|
|
441
|
+
}
|
|
442
|
+
console.log(`\x1B[36m[user]\x1B[0m ${text}`);
|
|
443
|
+
} else if (role === "agent" || role === "assistant") {
|
|
444
|
+
const data = content.content?.data || content.content;
|
|
445
|
+
if (!data) return;
|
|
446
|
+
if (data.type === "assistant" && Array.isArray(data.content)) {
|
|
447
|
+
for (const block of data.content) {
|
|
448
|
+
if (block.type === "text" && block.text) {
|
|
449
|
+
process.stdout.write(block.text);
|
|
450
|
+
if (!block.text.endsWith("\n")) process.stdout.write("\n");
|
|
451
|
+
} else if (block.type === "tool_use") {
|
|
452
|
+
const argsStr = JSON.stringify(block.input || {}).slice(0, 120);
|
|
453
|
+
console.log(`\x1B[33m[tool]\x1B[0m ${block.name}(${argsStr})`);
|
|
454
|
+
} else if (block.type === "tool_result") {
|
|
455
|
+
const resultStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
|
|
456
|
+
console.log(`\x1B[90m[result]\x1B[0m ${resultStr.slice(0, 200)}${resultStr.length > 200 ? "..." : ""}`);
|
|
457
|
+
} else if (block.type === "thinking") {
|
|
458
|
+
const text = block.thinking || block.text || "";
|
|
459
|
+
if (text) console.log(`\x1B[90m[thinking] ${text.slice(0, 200)}\x1B[0m`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
} else if (data.type === "result") {
|
|
463
|
+
if (data.result) console.log(`\x1B[32m[done]\x1B[0m ${data.result}`);
|
|
464
|
+
} else if (data.type === "output") {
|
|
465
|
+
const inner = data.data;
|
|
466
|
+
if (inner?.type === "assistant" && Array.isArray(inner.content)) {
|
|
467
|
+
for (const block of inner.content) {
|
|
468
|
+
if (block.type === "text" && block.text) {
|
|
469
|
+
process.stdout.write(block.text);
|
|
470
|
+
if (!block.text.endsWith("\n")) process.stdout.write("\n");
|
|
471
|
+
} else if (block.type === "tool_use") {
|
|
472
|
+
const argsStr = JSON.stringify(block.input || {}).slice(0, 120);
|
|
473
|
+
console.log(`\x1B[33m[tool]\x1B[0m ${block.name}(${argsStr})`);
|
|
474
|
+
} else if (block.type === "tool_result") {
|
|
475
|
+
const resultStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
|
|
476
|
+
console.log(`\x1B[90m[result]\x1B[0m ${resultStr.slice(0, 200)}${resultStr.length > 200 ? "..." : ""}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} else if (inner?.type === "result") {
|
|
480
|
+
if (inner.result) console.log(`\x1B[32m[done]\x1B[0m ${inner.result}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} else if (role === "session") {
|
|
484
|
+
const data = content.content?.data;
|
|
485
|
+
if (data?.type === "system" && data?.subtype === "init") {
|
|
486
|
+
console.log(`\x1B[90m[session init]\x1B[0m`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function extractMessageText(msg) {
|
|
491
|
+
const content = msg.content;
|
|
492
|
+
if (!content) return null;
|
|
493
|
+
const role = content.role || "unknown";
|
|
494
|
+
let text = "";
|
|
495
|
+
if (role === "user") {
|
|
496
|
+
const data = content.content;
|
|
497
|
+
if (typeof data === "string") {
|
|
498
|
+
try {
|
|
499
|
+
const parsed = JSON.parse(data);
|
|
500
|
+
text = parsed?.text || parsed?.content?.text || data;
|
|
501
|
+
} catch {
|
|
502
|
+
text = data;
|
|
503
|
+
}
|
|
504
|
+
} else if (data?.text) {
|
|
505
|
+
text = data.text;
|
|
506
|
+
} else if (data?.type === "text") {
|
|
507
|
+
text = data.text || "";
|
|
508
|
+
} else {
|
|
509
|
+
text = typeof data === "object" ? JSON.stringify(data) : String(data || "");
|
|
510
|
+
}
|
|
511
|
+
} else if (role === "agent" || role === "assistant") {
|
|
512
|
+
const data = content.content?.data || content.content;
|
|
513
|
+
if (!data) return null;
|
|
514
|
+
if (data.type === "assistant" && Array.isArray(data.content)) {
|
|
515
|
+
const parts = [];
|
|
516
|
+
for (const block of data.content) {
|
|
517
|
+
if (block.type === "text" && block.text) {
|
|
518
|
+
parts.push(block.text);
|
|
519
|
+
} else if (block.type === "tool_use") {
|
|
520
|
+
parts.push(`[tool: ${block.name}]`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
text = parts.join("\n");
|
|
524
|
+
} else if (data.type === "result") {
|
|
525
|
+
text = data.result || "";
|
|
526
|
+
} else if (data.type === "output") {
|
|
527
|
+
const inner = data.data;
|
|
528
|
+
if (inner?.type === "assistant" && Array.isArray(inner.content)) {
|
|
529
|
+
const parts = [];
|
|
530
|
+
for (const block of inner.content) {
|
|
531
|
+
if (block.type === "text" && block.text) {
|
|
532
|
+
parts.push(block.text);
|
|
533
|
+
} else if (block.type === "tool_use") {
|
|
534
|
+
parts.push(`[tool: ${block.name}]`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
text = parts.join("\n");
|
|
538
|
+
} else if (inner?.type === "result") {
|
|
539
|
+
text = inner.result || "";
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
} else if (role === "session") {
|
|
543
|
+
text = "[session event]";
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
id: msg.id || "",
|
|
547
|
+
seq: msg.seq || 0,
|
|
548
|
+
role,
|
|
549
|
+
text,
|
|
550
|
+
createdAt: msg.createdAt || 0
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async function waitForIdle(server, sessionId, timeoutMs) {
|
|
554
|
+
const svc = await server.getService(`svamp-session-${sessionId}`);
|
|
555
|
+
const pollInterval = 2e3;
|
|
556
|
+
const deadline = Date.now() + timeoutMs;
|
|
557
|
+
while (Date.now() < deadline) {
|
|
558
|
+
const activity = await svc.getActivityState();
|
|
559
|
+
if (activity && !activity.thinking) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
563
|
+
}
|
|
564
|
+
throw new Error("Timeout waiting for agent to become idle");
|
|
565
|
+
}
|
|
566
|
+
async function waitForBusyThenIdle(server, sessionId, timeoutMs = 3e5, busyTimeoutMs = 1e4) {
|
|
567
|
+
const svc = await server.getService(`svamp-session-${sessionId}`);
|
|
568
|
+
const pollInterval = 2e3;
|
|
569
|
+
const deadline = Date.now() + timeoutMs;
|
|
570
|
+
const busyDeadline = Date.now() + busyTimeoutMs;
|
|
571
|
+
let sawBusy = false;
|
|
572
|
+
while (Date.now() < deadline) {
|
|
573
|
+
const activity = await svc.getActivityState();
|
|
574
|
+
const isBusy = activity?.thinking === true;
|
|
575
|
+
if (isBusy) {
|
|
576
|
+
sawBusy = true;
|
|
577
|
+
}
|
|
578
|
+
if (activity && !activity.active) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (sawBusy && !isBusy) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (!sawBusy && Date.now() > busyDeadline) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
588
|
+
}
|
|
589
|
+
throw new Error("Timeout waiting for agent to become idle");
|
|
590
|
+
}
|
|
591
|
+
async function sessionList(machineId, opts) {
|
|
592
|
+
if (machineId) {
|
|
593
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
594
|
+
try {
|
|
595
|
+
await listSessionsFromMachines(server, [machine], opts);
|
|
596
|
+
} finally {
|
|
597
|
+
await server.disconnect();
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
const { server, machines } = await connectAndGetAllMachines();
|
|
601
|
+
try {
|
|
602
|
+
await listSessionsFromMachines(server, machines, opts);
|
|
603
|
+
} finally {
|
|
604
|
+
await server.disconnect();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async function listSessionsFromMachines(server, machines, opts) {
|
|
609
|
+
const allSessions = [];
|
|
610
|
+
for (const machine of machines) {
|
|
611
|
+
try {
|
|
612
|
+
const info = await machine.getMachineInfo();
|
|
613
|
+
const sessions = await machine.listSessions();
|
|
614
|
+
for (const s of sessions) {
|
|
615
|
+
s.machineHost = info.metadata?.displayName || info.metadata?.host || info.machineId;
|
|
616
|
+
}
|
|
617
|
+
allSessions.push(...sessions);
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const filtered = opts?.active ? allSessions.filter((s) => s.active) : allSessions;
|
|
622
|
+
if (filtered.length === 0) {
|
|
623
|
+
if (opts?.json) {
|
|
624
|
+
console.log(formatJson([]));
|
|
625
|
+
} else {
|
|
626
|
+
console.log("No active sessions.");
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const enriched = [];
|
|
631
|
+
for (const s of filtered) {
|
|
632
|
+
let flavor = "claude";
|
|
633
|
+
let name = "";
|
|
634
|
+
let path = s.directory || "";
|
|
635
|
+
let host = s.machineHost || "";
|
|
636
|
+
if (s.metadata) {
|
|
637
|
+
flavor = s.metadata.flavor || "claude";
|
|
638
|
+
name = s.metadata.name || "";
|
|
639
|
+
}
|
|
640
|
+
if (s.active) {
|
|
641
|
+
try {
|
|
642
|
+
const svc = await server.getService(`svamp-session-${s.sessionId}`);
|
|
643
|
+
const { metadata } = await svc.getMetadata();
|
|
644
|
+
flavor = metadata?.flavor || flavor;
|
|
645
|
+
name = metadata?.name || name;
|
|
646
|
+
path = metadata?.path || path;
|
|
647
|
+
host = metadata?.host || host;
|
|
648
|
+
} catch {
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
enriched.push({ ...s, flavor, name, path, host });
|
|
652
|
+
}
|
|
653
|
+
if (opts?.json) {
|
|
654
|
+
console.log(formatJson(enriched.map((s) => ({
|
|
655
|
+
sessionId: s.sessionId,
|
|
656
|
+
agent: s.flavor,
|
|
657
|
+
name: s.name,
|
|
658
|
+
path: s.path,
|
|
659
|
+
host: s.host,
|
|
660
|
+
active: s.active,
|
|
661
|
+
directory: s.directory
|
|
662
|
+
}))));
|
|
663
|
+
} else {
|
|
664
|
+
const header = `${"ID".padEnd(10)} ${"AGENT".padEnd(10)} ${"STATUS".padEnd(9)} ${"NAME".padEnd(25)} ${"MACHINE".padEnd(18)} ${"DIRECTORY".padEnd(35)}`;
|
|
665
|
+
console.log(header);
|
|
666
|
+
console.log("-".repeat(header.length));
|
|
667
|
+
for (const s of enriched) {
|
|
668
|
+
const id = s.sessionId.slice(0, 8);
|
|
669
|
+
const agent = (s.flavor || "claude").padEnd(10);
|
|
670
|
+
const status = s.active ? "\x1B[32mactive\x1B[0m " : "\x1B[90minactive\x1B[0m";
|
|
671
|
+
const name = truncate(s.name || "-", 25).padEnd(25);
|
|
672
|
+
const machine = truncate(s.host || "-", 16).padEnd(18);
|
|
673
|
+
const dir = truncate(s.directory || "-", 33).padEnd(35);
|
|
674
|
+
console.log(`${id.padEnd(10)} ${agent} ${status} ${name} ${machine} ${dir}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function parseShareArg(arg) {
|
|
679
|
+
const parts = arg.split(":");
|
|
680
|
+
const email = parts[0];
|
|
681
|
+
const role = parts[1] || "interact";
|
|
682
|
+
if (!["view", "interact", "admin"].includes(role)) {
|
|
683
|
+
throw new Error(`Invalid role "${role}" in --share ${arg}. Must be view, interact, or admin.`);
|
|
684
|
+
}
|
|
685
|
+
return { email, role };
|
|
686
|
+
}
|
|
687
|
+
async function sessionSpawn(agent, directory, machineId, opts) {
|
|
688
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
689
|
+
try {
|
|
690
|
+
let sharing;
|
|
691
|
+
if (opts?.share?.length) {
|
|
692
|
+
sharing = {
|
|
693
|
+
enabled: true,
|
|
694
|
+
owner: "",
|
|
695
|
+
// will be auto-set by machine service from Hypha context
|
|
696
|
+
allowedUsers: opts.share.map((s) => ({
|
|
697
|
+
email: s.email,
|
|
698
|
+
role: s.role || "interact",
|
|
699
|
+
addedAt: Date.now(),
|
|
700
|
+
addedBy: "cli"
|
|
701
|
+
}))
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
let securityContext;
|
|
705
|
+
if (opts?.securityContextPath) {
|
|
706
|
+
const configPath = resolve(opts.securityContextPath);
|
|
707
|
+
const config = loadSecurityContextConfig(configPath);
|
|
708
|
+
securityContext = resolveSecurityContext(config, void 0);
|
|
709
|
+
}
|
|
710
|
+
if (opts?.denyRead?.length || opts?.allowWrite?.length || opts?.denyNetwork || opts?.allowDomain?.length) {
|
|
711
|
+
const flagCtx = buildSecurityContextFromFlags({
|
|
712
|
+
denyRead: opts.denyRead,
|
|
713
|
+
allowWrite: opts.allowWrite,
|
|
714
|
+
denyNetwork: opts.denyNetwork,
|
|
715
|
+
allowDomain: opts.allowDomain
|
|
716
|
+
});
|
|
717
|
+
securityContext = mergeSecurityContexts(securityContext, flagCtx);
|
|
718
|
+
}
|
|
719
|
+
const forceIsolation = opts?.isolate || !!securityContext;
|
|
720
|
+
if (securityContext?.filesystem?.allowWrite?.length) {
|
|
721
|
+
const absDir = resolve(directory);
|
|
722
|
+
const writePaths = securityContext.filesystem.allowWrite;
|
|
723
|
+
const dirCovered = writePaths.some((p) => absDir.startsWith(resolve(p)) || resolve(p).startsWith(absDir));
|
|
724
|
+
if (!dirCovered) {
|
|
725
|
+
console.warn(`Warning: Working directory ${absDir} is not covered by allowWrite paths: ${writePaths.join(", ")}`);
|
|
726
|
+
console.warn(` The agent may not be able to write files in the working directory.`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
console.log(`Spawning ${agent} session in ${directory}...`);
|
|
730
|
+
if (forceIsolation) {
|
|
731
|
+
console.log(`Isolation: enabled (workspace: ${resolve(directory)})`);
|
|
732
|
+
}
|
|
733
|
+
if (securityContext) {
|
|
734
|
+
console.log(`Security context: ${JSON.stringify(securityContext, null, 2)}`);
|
|
735
|
+
}
|
|
736
|
+
if (sharing) {
|
|
737
|
+
console.log(`Sharing with: ${sharing.allowedUsers.map((u) => `${u.email} (${u.role})`).join(", ")}`);
|
|
738
|
+
}
|
|
739
|
+
const result = await machine.spawnSession({
|
|
740
|
+
directory,
|
|
741
|
+
agent,
|
|
742
|
+
sharing,
|
|
743
|
+
securityContext,
|
|
744
|
+
forceIsolation
|
|
745
|
+
});
|
|
746
|
+
if (result.type === "success") {
|
|
747
|
+
console.log(`Session started: ${result.sessionId}`);
|
|
748
|
+
if (result.message) console.log(` ${result.message}`);
|
|
749
|
+
if (opts?.message && result.sessionId) {
|
|
750
|
+
const svc = await server.getService(`svamp-session-${result.sessionId}`);
|
|
751
|
+
const sendResult = await svc.sendMessage(
|
|
752
|
+
JSON.stringify({
|
|
753
|
+
role: "user",
|
|
754
|
+
content: { type: "text", text: opts.message },
|
|
755
|
+
meta: { sentFrom: "svamp-cli" }
|
|
756
|
+
})
|
|
757
|
+
);
|
|
758
|
+
console.log(`Message sent (seq: ${sendResult.seq})`);
|
|
759
|
+
if (opts.wait) {
|
|
760
|
+
console.log("Waiting for agent to become idle...");
|
|
761
|
+
await waitForBusyThenIdle(server, result.sessionId);
|
|
762
|
+
console.log("Agent is idle.");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
} else if (result.type === "requestToApproveDirectoryCreation") {
|
|
766
|
+
console.error(`Directory ${result.directory} does not exist. Create it first or use an existing directory.`);
|
|
767
|
+
process.exit(1);
|
|
768
|
+
} else {
|
|
769
|
+
console.error(`Failed: ${result.errorMessage || "Unknown error"}`);
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
} finally {
|
|
773
|
+
await server.disconnect();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async function sessionStop(sessionId, machineId) {
|
|
777
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
778
|
+
try {
|
|
779
|
+
const sessions = await machine.listSessions();
|
|
780
|
+
const match = resolveSessionId(sessions, sessionId);
|
|
781
|
+
const success = await machine.stopSession(match.sessionId);
|
|
782
|
+
if (success) {
|
|
783
|
+
console.log(`Session ${match.sessionId.slice(0, 8)} stopped.`);
|
|
784
|
+
} else {
|
|
785
|
+
console.error("Failed to stop session (not found on daemon).");
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
} finally {
|
|
789
|
+
await server.disconnect();
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
async function sessionInfo(sessionId, machineId, opts) {
|
|
793
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
794
|
+
try {
|
|
795
|
+
const sessions = await machine.listSessions();
|
|
796
|
+
const match = resolveSessionId(sessions, sessionId);
|
|
797
|
+
const fullId = match.sessionId;
|
|
798
|
+
let metadata = {};
|
|
799
|
+
let activity = {};
|
|
800
|
+
try {
|
|
801
|
+
const svc = await server.getService(`svamp-session-${fullId}`);
|
|
802
|
+
const metaResult = await svc.getMetadata();
|
|
803
|
+
metadata = metaResult.metadata || {};
|
|
804
|
+
activity = await svc.getActivityState();
|
|
805
|
+
} catch {
|
|
806
|
+
}
|
|
807
|
+
const statusData = {
|
|
808
|
+
sessionId: fullId,
|
|
809
|
+
flavor: metadata.flavor || "claude",
|
|
810
|
+
name: metadata.name || "",
|
|
811
|
+
path: metadata.path || match.directory || "",
|
|
812
|
+
host: metadata.host || "",
|
|
813
|
+
lifecycleState: metadata.lifecycleState || "unknown",
|
|
814
|
+
active: activity.active ?? false,
|
|
815
|
+
thinking: activity.thinking ?? false,
|
|
816
|
+
startedBy: metadata.startedBy || match.startedBy || "",
|
|
817
|
+
summary: metadata.summary?.text || void 0,
|
|
818
|
+
claudeSessionId: metadata.claudeSessionId || void 0,
|
|
819
|
+
sessionLink: metadata.sessionLink?.url || void 0
|
|
820
|
+
};
|
|
821
|
+
if (opts?.json) {
|
|
822
|
+
console.log(formatJson(statusData));
|
|
823
|
+
} else {
|
|
824
|
+
console.log(formatSessionStatus(statusData));
|
|
825
|
+
}
|
|
826
|
+
} finally {
|
|
827
|
+
await server.disconnect();
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
async function sessionMessages(sessionId, machineId, opts) {
|
|
831
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
832
|
+
try {
|
|
833
|
+
const sessions = await machine.listSessions();
|
|
834
|
+
const match = resolveSessionId(sessions, sessionId);
|
|
835
|
+
const fullId = match.sessionId;
|
|
836
|
+
const svc = await server.getService(`svamp-session-${fullId}`);
|
|
837
|
+
const afterSeq = opts?.after ?? 0;
|
|
838
|
+
const apiLimit = opts?.limit ?? 1e3;
|
|
839
|
+
const { messages } = await svc.getMessages(afterSeq, apiLimit);
|
|
840
|
+
const toShow = opts?.last ? messages.slice(-opts.last) : messages;
|
|
841
|
+
if (toShow.length === 0) {
|
|
842
|
+
if (opts?.json) {
|
|
843
|
+
console.log(formatJson([]));
|
|
844
|
+
} else {
|
|
845
|
+
console.log("No messages yet.");
|
|
846
|
+
}
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (opts?.json) {
|
|
850
|
+
const formatted = [];
|
|
851
|
+
for (const msg of toShow) {
|
|
852
|
+
const extracted = extractMessageText(msg);
|
|
853
|
+
if (extracted) {
|
|
854
|
+
formatted.push(extracted);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
formatted.sort((a, b) => a.createdAt - b.createdAt);
|
|
858
|
+
console.log(formatJson(formatted));
|
|
859
|
+
} else {
|
|
860
|
+
for (const msg of toShow) {
|
|
861
|
+
renderMessage(msg);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} finally {
|
|
865
|
+
await server.disconnect();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async function sessionAttach(sessionId, machineId) {
|
|
869
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
870
|
+
const sessions = await machine.listSessions();
|
|
871
|
+
const match = resolveSessionId(sessions, sessionId);
|
|
872
|
+
const fullId = match.sessionId;
|
|
873
|
+
let svc;
|
|
874
|
+
try {
|
|
875
|
+
svc = await server.getService(`svamp-session-${fullId}`);
|
|
876
|
+
} catch (err) {
|
|
877
|
+
console.error(`Could not find session service: ${err.message}`);
|
|
878
|
+
await server.disconnect();
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
const { metadata } = await svc.getMetadata();
|
|
882
|
+
const flavor = metadata?.flavor || "claude";
|
|
883
|
+
const name = metadata?.name || fullId.slice(0, 8);
|
|
884
|
+
console.log(`Attached to ${flavor} session "${name}". Commands: /quit /abort /kill
|
|
885
|
+
`);
|
|
886
|
+
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
887
|
+
let replayDone = false;
|
|
888
|
+
await svc.registerListener({
|
|
889
|
+
onUpdate: (update) => {
|
|
890
|
+
if (update.type === "new-message") {
|
|
891
|
+
const msg = update.message;
|
|
892
|
+
if (!msg?.id) return;
|
|
893
|
+
if (seenMessageIds.has(msg.id)) return;
|
|
894
|
+
seenMessageIds.add(msg.id);
|
|
895
|
+
if (!replayDone) return;
|
|
896
|
+
renderMessage(msg);
|
|
897
|
+
} else if (update.type === "activity") {
|
|
898
|
+
if (!replayDone) return;
|
|
899
|
+
if (update.thinking) {
|
|
900
|
+
process.stdout.write("\x1B[90m[thinking...]\x1B[0m\r");
|
|
901
|
+
} else if (!update.thinking) {
|
|
902
|
+
process.stdout.write("\n> ");
|
|
903
|
+
}
|
|
904
|
+
} else if (update.type === "update-session") ;
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
908
|
+
replayDone = true;
|
|
909
|
+
console.log(`\x1B[90m(${seenMessageIds.size} messages in history)\x1B[0m`);
|
|
910
|
+
process.stdout.write("> ");
|
|
911
|
+
const readline = await import('readline');
|
|
912
|
+
const rl = readline.createInterface({
|
|
913
|
+
input: process.stdin,
|
|
914
|
+
output: process.stdout,
|
|
915
|
+
terminal: true
|
|
916
|
+
});
|
|
917
|
+
rl.on("line", async (line) => {
|
|
918
|
+
const trimmed = line.trim();
|
|
919
|
+
if (!trimmed) {
|
|
920
|
+
process.stdout.write("> ");
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (trimmed === "/quit" || trimmed === "/detach") {
|
|
924
|
+
console.log("Detaching (session continues running)...");
|
|
925
|
+
rl.close();
|
|
926
|
+
await server.disconnect();
|
|
927
|
+
process.exit(0);
|
|
928
|
+
}
|
|
929
|
+
if (trimmed === "/abort" || trimmed === "/cancel") {
|
|
930
|
+
try {
|
|
931
|
+
await svc.abort();
|
|
932
|
+
console.log("Abort sent.");
|
|
933
|
+
} catch (err) {
|
|
934
|
+
console.error(`Abort failed: ${err.message}`);
|
|
935
|
+
}
|
|
936
|
+
process.stdout.write("> ");
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (trimmed === "/kill") {
|
|
940
|
+
try {
|
|
941
|
+
await svc.killSession();
|
|
942
|
+
console.log("Session killed.");
|
|
943
|
+
} catch (err) {
|
|
944
|
+
console.error(`Kill failed: ${err.message}`);
|
|
945
|
+
}
|
|
946
|
+
rl.close();
|
|
947
|
+
await server.disconnect();
|
|
948
|
+
process.exit(0);
|
|
949
|
+
}
|
|
950
|
+
if (trimmed === "/info") {
|
|
951
|
+
try {
|
|
952
|
+
const { metadata: m } = await svc.getMetadata();
|
|
953
|
+
const act = await svc.getActivityState();
|
|
954
|
+
console.log(` Agent: ${m?.flavor || "claude"}, State: ${m?.lifecycleState || "?"}, Active: ${act?.active}, Thinking: ${act?.thinking}`);
|
|
955
|
+
} catch (err) {
|
|
956
|
+
console.error(`Info failed: ${err.message}`);
|
|
957
|
+
}
|
|
958
|
+
process.stdout.write("> ");
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
try {
|
|
962
|
+
await svc.sendMessage(
|
|
963
|
+
JSON.stringify({
|
|
964
|
+
role: "user",
|
|
965
|
+
content: { type: "text", text: trimmed }
|
|
966
|
+
})
|
|
967
|
+
);
|
|
968
|
+
} catch (err) {
|
|
969
|
+
console.error(`Send failed: ${err.message}`);
|
|
970
|
+
process.stdout.write("> ");
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
rl.on("close", async () => {
|
|
974
|
+
await server.disconnect();
|
|
975
|
+
process.exit(0);
|
|
976
|
+
});
|
|
977
|
+
process.on("SIGINT", async () => {
|
|
978
|
+
console.log("\nDetaching (session continues running)...");
|
|
979
|
+
rl.close();
|
|
980
|
+
await server.disconnect();
|
|
981
|
+
process.exit(0);
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
async function sessionSend(sessionId, message, machineId, opts) {
|
|
985
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
986
|
+
try {
|
|
987
|
+
const sessions = await machine.listSessions();
|
|
988
|
+
const match = resolveSessionId(sessions, sessionId);
|
|
989
|
+
const fullId = match.sessionId;
|
|
990
|
+
const svc = await server.getService(`svamp-session-${fullId}`);
|
|
991
|
+
const result = await svc.sendMessage(
|
|
992
|
+
JSON.stringify({
|
|
993
|
+
role: "user",
|
|
994
|
+
content: { type: "text", text: message },
|
|
995
|
+
meta: { sentFrom: "svamp-cli" }
|
|
996
|
+
})
|
|
997
|
+
);
|
|
998
|
+
if (opts?.wait) {
|
|
999
|
+
const timeoutMs = (opts.timeout || 300) * 1e3;
|
|
1000
|
+
await waitForBusyThenIdle(server, fullId, timeoutMs);
|
|
1001
|
+
}
|
|
1002
|
+
if (opts?.json) {
|
|
1003
|
+
console.log(formatJson({
|
|
1004
|
+
sessionId: fullId,
|
|
1005
|
+
message,
|
|
1006
|
+
sent: true,
|
|
1007
|
+
seq: result.seq,
|
|
1008
|
+
waited: !!opts.wait
|
|
1009
|
+
}));
|
|
1010
|
+
} else {
|
|
1011
|
+
console.log(`Message sent to session ${fullId.slice(0, 8)} (seq: ${result.seq})`);
|
|
1012
|
+
if (opts?.wait) {
|
|
1013
|
+
console.log("Agent is idle.");
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
} finally {
|
|
1017
|
+
await server.disconnect();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
async function sessionWait(sessionId, machineId, opts) {
|
|
1021
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1022
|
+
try {
|
|
1023
|
+
const sessions = await machine.listSessions();
|
|
1024
|
+
const match = resolveSessionId(sessions, sessionId);
|
|
1025
|
+
const fullId = match.sessionId;
|
|
1026
|
+
const timeoutMs = (opts?.timeout || 300) * 1e3;
|
|
1027
|
+
await waitForIdle(server, fullId, timeoutMs);
|
|
1028
|
+
console.log(`Session ${fullId.slice(0, 8)} is idle.`);
|
|
1029
|
+
} catch (err) {
|
|
1030
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1031
|
+
console.error(msg);
|
|
1032
|
+
process.exitCode = 1;
|
|
1033
|
+
} finally {
|
|
1034
|
+
await server.disconnect();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
async function sessionShare(sessionIdPartial, machineId, opts) {
|
|
1038
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1039
|
+
try {
|
|
1040
|
+
const sessions = await machine.listSessions();
|
|
1041
|
+
const match = resolveSessionId(sessions, sessionIdPartial);
|
|
1042
|
+
const fullId = match.sessionId;
|
|
1043
|
+
const svc = await server.getService(`svamp-session-${fullId}`);
|
|
1044
|
+
if (opts.list) {
|
|
1045
|
+
const metaResult = await svc.getMetadata();
|
|
1046
|
+
const sharing = metaResult.metadata?.sharing;
|
|
1047
|
+
if (!sharing || !sharing.enabled) {
|
|
1048
|
+
console.log("Sharing is not enabled for this session.");
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
console.log(`Owner: ${sharing.owner}`);
|
|
1052
|
+
if (sharing.allowedUsers.length === 0) {
|
|
1053
|
+
console.log("No shared users.");
|
|
1054
|
+
} else {
|
|
1055
|
+
console.log("Shared users:");
|
|
1056
|
+
for (const u of sharing.allowedUsers) {
|
|
1057
|
+
console.log(` ${u.email.padEnd(30)} ${u.role.padEnd(10)} (added ${new Date(u.addedAt).toISOString().slice(0, 10)})`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const secCtx = metaResult.metadata?.securityContext;
|
|
1061
|
+
if (secCtx) {
|
|
1062
|
+
console.log(`
|
|
1063
|
+
Security context:`);
|
|
1064
|
+
console.log(JSON.stringify(secCtx, null, 2));
|
|
1065
|
+
}
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (opts.add) {
|
|
1069
|
+
const { email, role } = parseShareArg(opts.add);
|
|
1070
|
+
const metaResult = await svc.getMetadata();
|
|
1071
|
+
let sharing = metaResult.metadata?.sharing || {
|
|
1072
|
+
enabled: true,
|
|
1073
|
+
owner: "",
|
|
1074
|
+
allowedUsers: []
|
|
1075
|
+
};
|
|
1076
|
+
sharing.enabled = true;
|
|
1077
|
+
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
1078
|
+
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
1079
|
+
);
|
|
1080
|
+
sharing.allowedUsers.push({
|
|
1081
|
+
email,
|
|
1082
|
+
role,
|
|
1083
|
+
addedAt: Date.now(),
|
|
1084
|
+
addedBy: "cli"
|
|
1085
|
+
});
|
|
1086
|
+
await svc.updateSharing(sharing);
|
|
1087
|
+
console.log(`Shared session ${fullId.slice(0, 8)} with ${email} (${role}).`);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (opts.remove) {
|
|
1091
|
+
const email = opts.remove;
|
|
1092
|
+
const metaResult = await svc.getMetadata();
|
|
1093
|
+
const sharing = metaResult.metadata?.sharing;
|
|
1094
|
+
if (!sharing) {
|
|
1095
|
+
console.error("Sharing is not enabled for this session.");
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
const before = sharing.allowedUsers.length;
|
|
1099
|
+
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
1100
|
+
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
1101
|
+
);
|
|
1102
|
+
if (sharing.allowedUsers.length === before) {
|
|
1103
|
+
console.error(`User ${email} is not in the shared users list.`);
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
1106
|
+
await svc.updateSharing(sharing);
|
|
1107
|
+
console.log(`Removed ${email} from session ${fullId.slice(0, 8)}.`);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
console.error("Usage: svamp session share <id> --add <email>[:<role>] | --remove <email> | --list");
|
|
1111
|
+
process.exit(1);
|
|
1112
|
+
} finally {
|
|
1113
|
+
await server.disconnect();
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
async function machineShare(machineId, opts) {
|
|
1117
|
+
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1118
|
+
try {
|
|
1119
|
+
if (opts.list) {
|
|
1120
|
+
const info = await machine.getMachineInfo();
|
|
1121
|
+
const sharing = info.metadata?.sharing;
|
|
1122
|
+
if (!sharing || !sharing.enabled) {
|
|
1123
|
+
console.log("Sharing is not enabled for this machine.");
|
|
1124
|
+
} else {
|
|
1125
|
+
console.log(`Owner: ${sharing.owner}`);
|
|
1126
|
+
if (sharing.allowedUsers.length === 0) {
|
|
1127
|
+
console.log("No shared users.");
|
|
1128
|
+
} else {
|
|
1129
|
+
console.log("Shared users:");
|
|
1130
|
+
for (const u of sharing.allowedUsers) {
|
|
1131
|
+
console.log(` ${u.email.padEnd(30)} ${u.role.padEnd(10)} (added ${new Date(u.addedAt).toISOString().slice(0, 10)})`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
const iso = info.metadata?.isolationCapabilities;
|
|
1136
|
+
if (iso) {
|
|
1137
|
+
console.log(`
|
|
1138
|
+
Isolation: ${iso.available.length > 0 ? iso.available.join(", ") : "none available"} (preferred: ${iso.preferred || "none"})`);
|
|
1139
|
+
}
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
if (opts.showConfig) {
|
|
1143
|
+
const result = await machine.getSecurityContextConfig();
|
|
1144
|
+
const config = result?.securityContextConfig;
|
|
1145
|
+
if (!config) {
|
|
1146
|
+
console.log("No security context config set for this machine.");
|
|
1147
|
+
} else {
|
|
1148
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
if (opts.configPath) {
|
|
1153
|
+
const configPath = resolve(opts.configPath);
|
|
1154
|
+
const config = loadSecurityContextConfig(configPath);
|
|
1155
|
+
await machine.updateSecurityContextConfig(config);
|
|
1156
|
+
const userCount = config.users ? Object.keys(config.users).length : 0;
|
|
1157
|
+
console.log(`Security context config applied to machine.`);
|
|
1158
|
+
console.log(` Default context: ${config.default ? "yes" : "none"}`);
|
|
1159
|
+
console.log(` User-specific entries: ${userCount}`);
|
|
1160
|
+
if (config.users) {
|
|
1161
|
+
for (const email of Object.keys(config.users)) {
|
|
1162
|
+
const ctx = config.users[email];
|
|
1163
|
+
console.log(` ${email}: role=${ctx.role || "default"}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (opts.add) {
|
|
1169
|
+
const { email, role } = parseShareArg(opts.add);
|
|
1170
|
+
const info = await machine.getMachineInfo();
|
|
1171
|
+
let sharing = info.metadata?.sharing || {
|
|
1172
|
+
enabled: true,
|
|
1173
|
+
owner: "",
|
|
1174
|
+
allowedUsers: []
|
|
1175
|
+
};
|
|
1176
|
+
sharing.enabled = true;
|
|
1177
|
+
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
1178
|
+
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
1179
|
+
);
|
|
1180
|
+
sharing.allowedUsers.push({
|
|
1181
|
+
email,
|
|
1182
|
+
role,
|
|
1183
|
+
addedAt: Date.now(),
|
|
1184
|
+
addedBy: "cli"
|
|
1185
|
+
});
|
|
1186
|
+
await machine.updateSharing(sharing);
|
|
1187
|
+
console.log(`Shared machine with ${email} (${role}).`);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (opts.remove) {
|
|
1191
|
+
const email = opts.remove;
|
|
1192
|
+
const info = await machine.getMachineInfo();
|
|
1193
|
+
const sharing = info.metadata?.sharing;
|
|
1194
|
+
if (!sharing) {
|
|
1195
|
+
console.error("Sharing is not enabled for this machine.");
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
}
|
|
1198
|
+
const before = sharing.allowedUsers.length;
|
|
1199
|
+
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
1200
|
+
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
1201
|
+
);
|
|
1202
|
+
if (sharing.allowedUsers.length === before) {
|
|
1203
|
+
console.error(`User ${email} is not in the shared users list.`);
|
|
1204
|
+
process.exit(1);
|
|
1205
|
+
}
|
|
1206
|
+
await machine.updateSharing(sharing);
|
|
1207
|
+
console.log(`Removed ${email} from machine sharing.`);
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
console.error("Usage: svamp machine share --add <email>[:<role>] | --remove <email> | --list | --config <path> | --show-config");
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
} finally {
|
|
1213
|
+
await server.disconnect();
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
export { connectAndGetMachine, machineShare, parseShareArg, renderMessage, resolveSessionId, sessionAttach, sessionInfo, sessionList, sessionMachines, sessionMessages, sessionSend, sessionShare, sessionSpawn, sessionStop, sessionWait };
|