svamp-cli 0.2.224 → 0.2.225
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/dist/agentCommands-BignK4hn.mjs +0 -526
- package/dist/auth-tP-DHfsI.mjs +0 -83
- package/dist/caddy-CuTbE3NY.mjs +0 -322
- package/dist/cli.mjs +0 -2338
- package/dist/commands-1IupY9qh.mjs +0 -265
- package/dist/commands-Bpq8_9OU.mjs +0 -610
- package/dist/commands-C1VIwqOH.mjs +0 -428
- package/dist/commands-CA8AMvq-.mjs +0 -493
- package/dist/commands-DMoXgLUh.mjs +0 -196
- package/dist/commands-DqvzIDMK.mjs +0 -2762
- package/dist/commands-ez1xdcH5.mjs +0 -643
- package/dist/fleet-B6Rh4mje.mjs +0 -356
- package/dist/frpc-vpwKk0kV.mjs +0 -681
- package/dist/headlessCli-C51w8i7T.mjs +0 -333
- package/dist/httpServer-B1KVQJfm.mjs +0 -276
- package/dist/index.mjs +0 -22
- package/dist/package-CzzAMi8h.mjs +0 -63
- package/dist/pinnedClaudeCode-VIupR1NK.mjs +0 -58
- package/dist/rpc-CcQQrbBO.mjs +0 -92
- package/dist/rpc-D7QFTk_g.mjs +0 -194
- package/dist/run-BZrPFG_w.mjs +0 -1086
- package/dist/run-C-9ZtEi4.mjs +0 -16050
- package/dist/runStore-CtptN7US.mjs +0 -168
- package/dist/sandboxDetect-DNTcbgWD.mjs +0 -12
- package/dist/scheduler-jlX6Mkxf.mjs +0 -102
- package/dist/serveCommands-Dhrv-HAD.mjs +0 -310
- package/dist/serveManager-DtBN8E9D.mjs +0 -781
- package/dist/serviceManager-hlOVxkhW.mjs +0 -78
- package/dist/sideband-DxH9bMhv.mjs +0 -80
- package/dist/store-BTs0H_y0.mjs +0 -148
- package/dist/supervisorLock-DmfzJx7B.mjs +0 -159
|
@@ -1,2762 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
|
-
import { basename, resolve, join, isAbsolute } from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import { Q as formatHandle, T as normalizeAllowedUser, U as loadSecurityContextConfig, V as resolveSecurityContext, W as buildSecurityContextFromFlags, X as mergeSecurityContexts, c as connectToHypha, Y as buildSessionShareUrl, Z as computeOutboundHop, A as shortId, _ as registerAwaitingReply, $ as buildMachineShareUrl, a0 as parseHandle, a1 as handleMatchesMetadata } from './run-C-9ZtEi4.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 'util';
|
|
15
|
-
import 'node:events';
|
|
16
|
-
import '@agentclientprotocol/sdk';
|
|
17
|
-
import '@modelcontextprotocol/sdk/client/index.js';
|
|
18
|
-
import '@modelcontextprotocol/sdk/client/stdio.js';
|
|
19
|
-
import '@modelcontextprotocol/sdk/types.js';
|
|
20
|
-
import 'zod';
|
|
21
|
-
import 'node:fs/promises';
|
|
22
|
-
import 'node:util';
|
|
23
|
-
|
|
24
|
-
function formatTime(ts) {
|
|
25
|
-
if (!ts) return "-";
|
|
26
|
-
const date = new Date(ts);
|
|
27
|
-
const now = /* @__PURE__ */ new Date();
|
|
28
|
-
const diffMs = now.getTime() - date.getTime();
|
|
29
|
-
const diffMin = Math.floor(diffMs / 6e4);
|
|
30
|
-
if (diffMin < 1) return "just now";
|
|
31
|
-
if (diffMin < 60) return `${diffMin}m ago`;
|
|
32
|
-
const diffHr = Math.floor(diffMin / 60);
|
|
33
|
-
if (diffHr < 24) return `${diffHr}h ago`;
|
|
34
|
-
const diffDay = Math.floor(diffHr / 24);
|
|
35
|
-
return `${diffDay}d ago`;
|
|
36
|
-
}
|
|
37
|
-
function resolveDisplayName(meta, directory) {
|
|
38
|
-
const m = meta || {};
|
|
39
|
-
const customTitle = typeof m.customTitle === "string" ? m.customTitle.trim() : "";
|
|
40
|
-
const summary = typeof m.summary?.text === "string" ? m.summary.text.trim() : "";
|
|
41
|
-
const name = typeof m.name === "string" ? m.name.trim() : "";
|
|
42
|
-
const dir = (directory || m.path || "").replace(/[/\\]+$/, "");
|
|
43
|
-
const dirName = dir ? basename(dir) : "";
|
|
44
|
-
return customTitle || summary || name || dirName || "untitled";
|
|
45
|
-
}
|
|
46
|
-
function resolveHandle(meta) {
|
|
47
|
-
const m = meta || {};
|
|
48
|
-
return formatHandle(m.projectName, m.friendlyName);
|
|
49
|
-
}
|
|
50
|
-
function toMarkdownInline(value) {
|
|
51
|
-
const escaped = value.replace(/`/g, "\\`");
|
|
52
|
-
return `\`${escaped}\``;
|
|
53
|
-
}
|
|
54
|
-
function formatSessionStatus(data) {
|
|
55
|
-
const lines = [
|
|
56
|
-
"## Session Status",
|
|
57
|
-
"",
|
|
58
|
-
`- Session ID: ${toMarkdownInline(data.sessionId)}`,
|
|
59
|
-
...data.handle ? [`- Handle: ${toMarkdownInline(data.handle)}`] : [],
|
|
60
|
-
`- Agent: ${data.flavor}`
|
|
61
|
-
];
|
|
62
|
-
if (data.name) lines.push(`- Title: ${data.name}`);
|
|
63
|
-
if (data.summary && data.summary !== data.name) lines.push(`- Topic: ${data.summary}`);
|
|
64
|
-
if (data.projectName) lines.push(`- Project: ${data.projectName}`);
|
|
65
|
-
if (data.projectDescription) lines.push(`- Project Description: ${data.projectDescription}`);
|
|
66
|
-
if (data.path) lines.push(`- Path: ${data.path}`);
|
|
67
|
-
if (data.host) lines.push(`- Host: ${data.host}`);
|
|
68
|
-
if (data.lifecycleState) lines.push(`- Lifecycle: ${data.lifecycleState}`);
|
|
69
|
-
lines.push(`- Active: ${data.active ? "yes" : "no"}`);
|
|
70
|
-
lines.push(`- Thinking: ${data.thinking ? "yes" : "no"}`);
|
|
71
|
-
lines.push(`- Agent Status: ${data.active ? "busy" : "idle"}`);
|
|
72
|
-
if (data.startedBy) lines.push(`- Started By: ${data.startedBy}`);
|
|
73
|
-
if (data.claudeSessionId) lines.push(`- Claude Session: ${data.claudeSessionId}`);
|
|
74
|
-
if (data.sessionLink) lines.push(`- Link: ${data.sessionLink}`);
|
|
75
|
-
if (data.pendingPermissions && data.pendingPermissions.length > 0) {
|
|
76
|
-
lines.push("");
|
|
77
|
-
lines.push("### Pending Permissions");
|
|
78
|
-
for (const p of data.pendingPermissions) {
|
|
79
|
-
const argsStr = JSON.stringify(p.arguments || {}).slice(0, 120);
|
|
80
|
-
lines.push(` [${p.id.slice(0, 8)}] ${p.tool}(${argsStr}) ${formatTime(p.createdAt)}`);
|
|
81
|
-
}
|
|
82
|
-
lines.push("");
|
|
83
|
-
lines.push(`Use: svamp session approve ${data.sessionId.slice(0, 8)}`);
|
|
84
|
-
}
|
|
85
|
-
return lines.join("\n");
|
|
86
|
-
}
|
|
87
|
-
function formatJson(data) {
|
|
88
|
-
return JSON.stringify(data, null, 2);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const SESSION_RPC_PARAMS = {
|
|
92
|
-
getMessages: ["afterSeq", "limit"],
|
|
93
|
-
getMessageCount: [],
|
|
94
|
-
getLatestMessages: ["beforeSeq", "limit"],
|
|
95
|
-
sendMessage: ["content", "localId", "meta"],
|
|
96
|
-
btw: ["question"],
|
|
97
|
-
editMessage: ["messageId", "newText"],
|
|
98
|
-
refineLastReply: ["instruction"],
|
|
99
|
-
undoLastEdit: [],
|
|
100
|
-
getMetadata: [],
|
|
101
|
-
updateMetadata: ["newMetadata", "expectedVersion"],
|
|
102
|
-
updateConfig: ["patch"],
|
|
103
|
-
getAgentState: [],
|
|
104
|
-
updateAgentState: ["newState", "expectedVersion"],
|
|
105
|
-
abort: [],
|
|
106
|
-
permissionResponse: ["params"],
|
|
107
|
-
switchMode: ["mode"],
|
|
108
|
-
restartClaude: [],
|
|
109
|
-
archiveSession: [],
|
|
110
|
-
regenerateSummary: [],
|
|
111
|
-
keepAlive: ["thinking", "mode"],
|
|
112
|
-
sessionEnd: [],
|
|
113
|
-
getActivityState: [],
|
|
114
|
-
readFile: ["path"],
|
|
115
|
-
writeFile: ["path", "content"],
|
|
116
|
-
listDirectory: ["path"],
|
|
117
|
-
bash: ["command", "cwd", "timeout"],
|
|
118
|
-
ripgrep: ["args", "cwd"],
|
|
119
|
-
getDirectoryTree: ["path", "maxDepth"],
|
|
120
|
-
getSharing: [],
|
|
121
|
-
getEffectiveRole: [],
|
|
122
|
-
updateSharing: ["newSharing"],
|
|
123
|
-
updateSecurityContext: ["newSecurityContext"],
|
|
124
|
-
setClaudeChrome: ["enabled"],
|
|
125
|
-
applySystemPrompt: ["prompt"],
|
|
126
|
-
sendInboxMessage: ["message"],
|
|
127
|
-
getInbox: ["opts"],
|
|
128
|
-
markInboxRead: ["messageId"],
|
|
129
|
-
clearInbox: ["opts"],
|
|
130
|
-
registerListener: ["callback"]
|
|
131
|
-
};
|
|
132
|
-
function getSessionProxy(machine, sessionId) {
|
|
133
|
-
return new Proxy({}, {
|
|
134
|
-
get(_target, method) {
|
|
135
|
-
return (...args) => {
|
|
136
|
-
const paramNames = SESSION_RPC_PARAMS[method];
|
|
137
|
-
if (paramNames) {
|
|
138
|
-
const kwargs = {};
|
|
139
|
-
for (let i = 0; i < paramNames.length && i < args.length; i++) {
|
|
140
|
-
if (args[i] !== void 0) {
|
|
141
|
-
kwargs[paramNames[i]] = args[i];
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return machine.sessionRPC(sessionId, method, kwargs);
|
|
145
|
-
}
|
|
146
|
-
return machine.sessionRPC(sessionId, method, {});
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
const SVAMP_HOME = process.env.SVAMP_HOME || join(os.homedir(), ".svamp");
|
|
152
|
-
const DAEMON_STATE_FILE = join(SVAMP_HOME, "daemon.state.json");
|
|
153
|
-
const ENV_FILE = join(SVAMP_HOME, ".env");
|
|
154
|
-
function loadDotEnv() {
|
|
155
|
-
if (!existsSync(ENV_FILE)) return;
|
|
156
|
-
const lines = readFileSync(ENV_FILE, "utf-8").split("\n");
|
|
157
|
-
for (const line of lines) {
|
|
158
|
-
const trimmed = line.trim();
|
|
159
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
160
|
-
const eqIdx = trimmed.indexOf("=");
|
|
161
|
-
if (eqIdx === -1) continue;
|
|
162
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
163
|
-
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
164
|
-
if (!process.env[key]) {
|
|
165
|
-
process.env[key] = value;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function readDaemonState() {
|
|
170
|
-
if (!existsSync(DAEMON_STATE_FILE)) return null;
|
|
171
|
-
try {
|
|
172
|
-
return JSON.parse(readFileSync(DAEMON_STATE_FILE, "utf-8"));
|
|
173
|
-
} catch {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
function isDaemonAlive(state) {
|
|
178
|
-
try {
|
|
179
|
-
process.kill(state.pid, 0);
|
|
180
|
-
return true;
|
|
181
|
-
} catch {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
async function connectAndGetMachine(machineId) {
|
|
186
|
-
loadDotEnv();
|
|
187
|
-
const state = readDaemonState();
|
|
188
|
-
if (!state || !isDaemonAlive(state)) {
|
|
189
|
-
console.error('Daemon is not running. Start it with "svamp daemon start".');
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
192
|
-
const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
|
|
193
|
-
const token = process.env.HYPHA_TOKEN;
|
|
194
|
-
if (!serverUrl) {
|
|
195
|
-
console.error('No Hypha server URL. Run "svamp login <url>" first.');
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
const origLog = console.log;
|
|
199
|
-
const origWarn = console.warn;
|
|
200
|
-
const origInfo = console.info;
|
|
201
|
-
const origError = console.error;
|
|
202
|
-
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
203
|
-
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
204
|
-
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"));
|
|
205
|
-
console.log = () => {
|
|
206
|
-
};
|
|
207
|
-
console.warn = () => {
|
|
208
|
-
};
|
|
209
|
-
console.info = () => {
|
|
210
|
-
};
|
|
211
|
-
console.error = (...args) => {
|
|
212
|
-
if (args.some((a) => isHyphaLog(a))) return;
|
|
213
|
-
origError(...args);
|
|
214
|
-
};
|
|
215
|
-
process.stdout.write = (chunk, ...args) => {
|
|
216
|
-
if (isHyphaLog(chunk)) return true;
|
|
217
|
-
return stdoutWrite(chunk, ...args);
|
|
218
|
-
};
|
|
219
|
-
process.stderr.write = (chunk, ...args) => {
|
|
220
|
-
if (isHyphaLog(chunk)) return true;
|
|
221
|
-
return stderrWrite(chunk, ...args);
|
|
222
|
-
};
|
|
223
|
-
const restoreConsole = () => {
|
|
224
|
-
console.log = origLog;
|
|
225
|
-
console.warn = origWarn;
|
|
226
|
-
console.info = origInfo;
|
|
227
|
-
console.error = origError;
|
|
228
|
-
};
|
|
229
|
-
let server;
|
|
230
|
-
try {
|
|
231
|
-
server = await connectToHypha({
|
|
232
|
-
serverUrl,
|
|
233
|
-
token,
|
|
234
|
-
name: "svamp-session-cli"
|
|
235
|
-
});
|
|
236
|
-
} catch (err) {
|
|
237
|
-
restoreConsole();
|
|
238
|
-
console.error(`Failed to connect to Hypha: ${err.message}`);
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
241
|
-
let machine;
|
|
242
|
-
try {
|
|
243
|
-
const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
|
|
244
|
-
if (services.length === 0) {
|
|
245
|
-
restoreConsole();
|
|
246
|
-
console.error("No machine service found. Is the daemon registered on Hypha?");
|
|
247
|
-
await server.disconnect();
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
let selectedService;
|
|
251
|
-
if (machineId) {
|
|
252
|
-
const exact = services.find((s) => (s.id || s.name) === machineId);
|
|
253
|
-
if (exact) {
|
|
254
|
-
selectedService = exact;
|
|
255
|
-
} else {
|
|
256
|
-
const prefixMatches = services.filter((s) => {
|
|
257
|
-
const id = s.id || s.name;
|
|
258
|
-
return id.startsWith(machineId);
|
|
259
|
-
});
|
|
260
|
-
if (prefixMatches.length === 1) {
|
|
261
|
-
selectedService = prefixMatches[0];
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (!selectedService) {
|
|
265
|
-
const query = machineId.toLowerCase();
|
|
266
|
-
const resolved = [];
|
|
267
|
-
await Promise.all(services.map(async (svc) => {
|
|
268
|
-
const svcId2 = svc.id || svc.name;
|
|
269
|
-
try {
|
|
270
|
-
const machineSvc = await server.getService(svcId2);
|
|
271
|
-
const info = await machineSvc.getMachineInfo();
|
|
272
|
-
resolved.push({
|
|
273
|
-
svc,
|
|
274
|
-
machineId: (info.machineId || svcId2).toLowerCase(),
|
|
275
|
-
host: (info.metadata?.host || "").toLowerCase(),
|
|
276
|
-
displayName: (info.metadata?.displayName || "").toLowerCase()
|
|
277
|
-
});
|
|
278
|
-
} catch {
|
|
279
|
-
}
|
|
280
|
-
}));
|
|
281
|
-
let matches = resolved.filter(
|
|
282
|
-
(r) => r.machineId === query || r.host === query || r.displayName === query
|
|
283
|
-
);
|
|
284
|
-
if (matches.length === 0) {
|
|
285
|
-
matches = resolved.filter(
|
|
286
|
-
(r) => r.machineId.startsWith(query) || r.host.startsWith(query) || r.displayName.startsWith(query)
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
if (matches.length === 0) {
|
|
290
|
-
matches = resolved.filter(
|
|
291
|
-
(r) => r.machineId.includes(query) || r.host.includes(query) || r.displayName.includes(query)
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
if (matches.length === 1) {
|
|
295
|
-
selectedService = matches[0].svc;
|
|
296
|
-
} else if (matches.length > 1) {
|
|
297
|
-
restoreConsole();
|
|
298
|
-
console.error(`Ambiguous machine "${machineId}". Matches:`);
|
|
299
|
-
for (const m of matches) {
|
|
300
|
-
console.error(` ${m.machineId} (${m.host})`);
|
|
301
|
-
}
|
|
302
|
-
await server.disconnect();
|
|
303
|
-
process.exit(1);
|
|
304
|
-
} else {
|
|
305
|
-
restoreConsole();
|
|
306
|
-
console.error(`No machine found matching: ${machineId}`);
|
|
307
|
-
if (resolved.length > 0) {
|
|
308
|
-
console.error("Available machines:");
|
|
309
|
-
for (const r of resolved) {
|
|
310
|
-
console.error(` ${r.machineId} (${r.host})`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
await server.disconnect();
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
} else {
|
|
318
|
-
if (state.hyphaClientId) {
|
|
319
|
-
const localMatch = services.find((s) => {
|
|
320
|
-
const id = s.id || s.name || "";
|
|
321
|
-
return id.includes(state.hyphaClientId);
|
|
322
|
-
});
|
|
323
|
-
selectedService = localMatch || services[0];
|
|
324
|
-
} else if (state.machineId) {
|
|
325
|
-
const localMatch = services.find((s) => {
|
|
326
|
-
const id = s.id || s.name || "";
|
|
327
|
-
return id.includes(state.machineId);
|
|
328
|
-
});
|
|
329
|
-
selectedService = localMatch || services[0];
|
|
330
|
-
} else {
|
|
331
|
-
selectedService = services[0];
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
const svcId = selectedService.id || selectedService.name;
|
|
335
|
-
machine = await server.getService(svcId);
|
|
336
|
-
} catch (err) {
|
|
337
|
-
restoreConsole();
|
|
338
|
-
console.error(`Failed to discover machine service: ${err.message}`);
|
|
339
|
-
await server.disconnect();
|
|
340
|
-
process.exit(1);
|
|
341
|
-
}
|
|
342
|
-
restoreConsole();
|
|
343
|
-
return { server, machine };
|
|
344
|
-
}
|
|
345
|
-
async function connectAndGetAllMachines() {
|
|
346
|
-
loadDotEnv();
|
|
347
|
-
const state = readDaemonState();
|
|
348
|
-
if (!state || !isDaemonAlive(state)) {
|
|
349
|
-
console.error('Daemon is not running. Start it with "svamp daemon start".');
|
|
350
|
-
process.exit(1);
|
|
351
|
-
}
|
|
352
|
-
const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
|
|
353
|
-
const token = process.env.HYPHA_TOKEN;
|
|
354
|
-
if (!serverUrl) {
|
|
355
|
-
console.error('No Hypha server URL. Run "svamp login <url>" first.');
|
|
356
|
-
process.exit(1);
|
|
357
|
-
}
|
|
358
|
-
const origLog = console.log;
|
|
359
|
-
const origWarn = console.warn;
|
|
360
|
-
const origInfo = console.info;
|
|
361
|
-
const origError = console.error;
|
|
362
|
-
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
363
|
-
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
364
|
-
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"));
|
|
365
|
-
console.log = () => {
|
|
366
|
-
};
|
|
367
|
-
console.warn = () => {
|
|
368
|
-
};
|
|
369
|
-
console.info = () => {
|
|
370
|
-
};
|
|
371
|
-
console.error = (...args) => {
|
|
372
|
-
if (!args.some((a) => isHyphaLog(a))) origError(...args);
|
|
373
|
-
};
|
|
374
|
-
process.stdout.write = (chunk, ...args) => {
|
|
375
|
-
if (isHyphaLog(chunk)) return true;
|
|
376
|
-
return stdoutWrite(chunk, ...args);
|
|
377
|
-
};
|
|
378
|
-
process.stderr.write = (chunk, ...args) => {
|
|
379
|
-
if (isHyphaLog(chunk)) return true;
|
|
380
|
-
return stderrWrite(chunk, ...args);
|
|
381
|
-
};
|
|
382
|
-
const restoreConsole = () => {
|
|
383
|
-
console.log = origLog;
|
|
384
|
-
console.warn = origWarn;
|
|
385
|
-
console.info = origInfo;
|
|
386
|
-
console.error = origError;
|
|
387
|
-
};
|
|
388
|
-
let server;
|
|
389
|
-
try {
|
|
390
|
-
server = await connectToHypha({ serverUrl, token, name: "svamp-session-cli" });
|
|
391
|
-
} catch (err) {
|
|
392
|
-
restoreConsole();
|
|
393
|
-
console.error(`Failed to connect to Hypha: ${err.message}`);
|
|
394
|
-
process.exit(1);
|
|
395
|
-
}
|
|
396
|
-
const machines = [];
|
|
397
|
-
try {
|
|
398
|
-
const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
|
|
399
|
-
const results = await Promise.allSettled(
|
|
400
|
-
services.map((svc) => server.getService(svc.id || svc.name))
|
|
401
|
-
);
|
|
402
|
-
for (const r of results) {
|
|
403
|
-
if (r.status === "fulfilled") machines.push(r.value);
|
|
404
|
-
}
|
|
405
|
-
} catch (err) {
|
|
406
|
-
restoreConsole();
|
|
407
|
-
console.error(`Failed to discover machine services: ${err.message}`);
|
|
408
|
-
await server.disconnect();
|
|
409
|
-
process.exit(1);
|
|
410
|
-
}
|
|
411
|
-
restoreConsole();
|
|
412
|
-
if (machines.length === 0) {
|
|
413
|
-
console.error("No machine service found. Is the daemon registered on Hypha?");
|
|
414
|
-
await server.disconnect();
|
|
415
|
-
process.exit(1);
|
|
416
|
-
}
|
|
417
|
-
return { server, machines };
|
|
418
|
-
}
|
|
419
|
-
async function sessionMachines() {
|
|
420
|
-
loadDotEnv();
|
|
421
|
-
const state = readDaemonState();
|
|
422
|
-
if (!state || !isDaemonAlive(state)) {
|
|
423
|
-
console.error('Daemon is not running. Start it with "svamp daemon start".');
|
|
424
|
-
process.exit(1);
|
|
425
|
-
}
|
|
426
|
-
const serverUrl = process.env.HYPHA_SERVER_URL || state.hyphaServerUrl;
|
|
427
|
-
const token = process.env.HYPHA_TOKEN;
|
|
428
|
-
if (!serverUrl) {
|
|
429
|
-
console.error('No Hypha server URL. Run "svamp login <url>" first.');
|
|
430
|
-
process.exit(1);
|
|
431
|
-
}
|
|
432
|
-
const origLog = console.log;
|
|
433
|
-
const origWarn = console.warn;
|
|
434
|
-
const origInfo = console.info;
|
|
435
|
-
const origError = console.error;
|
|
436
|
-
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
437
|
-
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
438
|
-
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"));
|
|
439
|
-
console.log = () => {
|
|
440
|
-
};
|
|
441
|
-
console.warn = () => {
|
|
442
|
-
};
|
|
443
|
-
console.info = () => {
|
|
444
|
-
};
|
|
445
|
-
console.error = (...args) => {
|
|
446
|
-
if (args.some((a) => isHyphaLog(a))) return;
|
|
447
|
-
origError(...args);
|
|
448
|
-
};
|
|
449
|
-
process.stdout.write = (chunk, ...args) => {
|
|
450
|
-
if (isHyphaLog(chunk)) return true;
|
|
451
|
-
return stdoutWrite(chunk, ...args);
|
|
452
|
-
};
|
|
453
|
-
process.stderr.write = (chunk, ...args) => {
|
|
454
|
-
if (isHyphaLog(chunk)) return true;
|
|
455
|
-
return stderrWrite(chunk, ...args);
|
|
456
|
-
};
|
|
457
|
-
const restoreConsole = () => {
|
|
458
|
-
console.log = origLog;
|
|
459
|
-
console.warn = origWarn;
|
|
460
|
-
console.info = origInfo;
|
|
461
|
-
console.error = origError;
|
|
462
|
-
};
|
|
463
|
-
let server;
|
|
464
|
-
try {
|
|
465
|
-
server = await connectToHypha({
|
|
466
|
-
serverUrl,
|
|
467
|
-
token,
|
|
468
|
-
name: "svamp-session-cli"
|
|
469
|
-
});
|
|
470
|
-
} catch (err) {
|
|
471
|
-
restoreConsole();
|
|
472
|
-
console.error(`Failed to connect to Hypha: ${err.message}`);
|
|
473
|
-
process.exit(1);
|
|
474
|
-
}
|
|
475
|
-
try {
|
|
476
|
-
const services = await server.listServices({ query: { type: "svamp-machine" }, include_unlisted: true, _rkwargs: true });
|
|
477
|
-
restoreConsole();
|
|
478
|
-
if (services.length === 0) {
|
|
479
|
-
console.log("No machines found.");
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
const machines = await Promise.all(
|
|
483
|
-
services.map(async (svc) => {
|
|
484
|
-
const svcId = svc.id || svc.name;
|
|
485
|
-
try {
|
|
486
|
-
const machineSvc = await server.getService(svcId);
|
|
487
|
-
const [info, sessions] = await Promise.all([
|
|
488
|
-
machineSvc.getMachineInfo(),
|
|
489
|
-
machineSvc.listSessions()
|
|
490
|
-
]);
|
|
491
|
-
return {
|
|
492
|
-
serviceId: svcId,
|
|
493
|
-
machineId: info.machineId || svcId,
|
|
494
|
-
displayName: info.metadata?.displayName || info.metadata?.host || "-",
|
|
495
|
-
platform: info.metadata?.platform || "-",
|
|
496
|
-
host: info.metadata?.host || "-",
|
|
497
|
-
sessions: sessions.length,
|
|
498
|
-
status: info.daemonState?.status || "unknown"
|
|
499
|
-
};
|
|
500
|
-
} catch {
|
|
501
|
-
return {
|
|
502
|
-
serviceId: svcId,
|
|
503
|
-
machineId: svcId,
|
|
504
|
-
displayName: "-",
|
|
505
|
-
platform: "-",
|
|
506
|
-
host: "-",
|
|
507
|
-
sessions: -1,
|
|
508
|
-
status: "unreachable"
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
})
|
|
512
|
-
);
|
|
513
|
-
const header = `${"MACHINE ID".padEnd(20)} ${"NAME".padEnd(20)} ${"PLATFORM".padEnd(12)} ${"HOST".padEnd(25)} ${"SESSIONS".padEnd(10)} ${"STATUS"}`;
|
|
514
|
-
console.log(header);
|
|
515
|
-
console.log("-".repeat(header.length));
|
|
516
|
-
for (const m of machines) {
|
|
517
|
-
const id = truncate(m.machineId, 18).padEnd(20);
|
|
518
|
-
const name = truncate(m.displayName, 18).padEnd(20);
|
|
519
|
-
const platform = m.platform.padEnd(12);
|
|
520
|
-
const host = truncate(m.host, 23).padEnd(25);
|
|
521
|
-
const sessions = m.sessions >= 0 ? String(m.sessions).padEnd(10) : "-".padEnd(10);
|
|
522
|
-
const status = m.status === "running" ? `\x1B[32m${m.status}\x1B[0m` : m.status === "unreachable" ? `\x1B[31m${m.status}\x1B[0m` : m.status;
|
|
523
|
-
console.log(`${id} ${name} ${platform} ${host} ${sessions} ${status}`);
|
|
524
|
-
}
|
|
525
|
-
console.log(`
|
|
526
|
-
${machines.length} machine(s) found.`);
|
|
527
|
-
console.log("Use --machine <id> to target a specific machine.");
|
|
528
|
-
} finally {
|
|
529
|
-
await server.disconnect();
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
function matchByHandle(sessions, partial) {
|
|
533
|
-
const parsed = parseHandle(partial);
|
|
534
|
-
if (!parsed) return [];
|
|
535
|
-
return sessions.filter((s) => handleMatchesMetadata(parsed, s.metadata || {}));
|
|
536
|
-
}
|
|
537
|
-
function findSessionMatch(sessions, partial) {
|
|
538
|
-
const exact = sessions.find((s) => s.sessionId === partial);
|
|
539
|
-
if (exact) return exact;
|
|
540
|
-
const byHandle = matchByHandle(sessions, partial);
|
|
541
|
-
if (byHandle.length === 1) return byHandle[0];
|
|
542
|
-
const matches = sessions.filter((s) => s.sessionId.startsWith(partial));
|
|
543
|
-
if (matches.length === 1) return matches[0];
|
|
544
|
-
return void 0;
|
|
545
|
-
}
|
|
546
|
-
function resolveSessionId(sessions, partial) {
|
|
547
|
-
const exact = sessions.find((s) => s.sessionId === partial);
|
|
548
|
-
if (exact) return exact;
|
|
549
|
-
const byHandle = matchByHandle(sessions, partial);
|
|
550
|
-
if (byHandle.length === 1) return byHandle[0];
|
|
551
|
-
if (byHandle.length > 1) {
|
|
552
|
-
console.error(`Ambiguous handle "${partial}". Matches:`);
|
|
553
|
-
for (const s of byHandle) {
|
|
554
|
-
console.error(` ${resolveHandle(s.metadata || {}) || s.sessionId} (${s.sessionId})`);
|
|
555
|
-
}
|
|
556
|
-
process.exit(1);
|
|
557
|
-
}
|
|
558
|
-
const matches = sessions.filter((s) => s.sessionId.startsWith(partial));
|
|
559
|
-
if (matches.length === 1) return matches[0];
|
|
560
|
-
if (matches.length === 0) {
|
|
561
|
-
console.error(`No session found matching: ${partial}`);
|
|
562
|
-
console.error('Run "svamp session list" to see active sessions.');
|
|
563
|
-
process.exit(1);
|
|
564
|
-
}
|
|
565
|
-
console.error(`Ambiguous session ID "${partial}". Matches:`);
|
|
566
|
-
for (const s of matches) {
|
|
567
|
-
console.error(` ${s.sessionId}`);
|
|
568
|
-
}
|
|
569
|
-
process.exit(1);
|
|
570
|
-
}
|
|
571
|
-
async function connectAndResolveSession(sessionId, machineId) {
|
|
572
|
-
if (sessionId === ".") {
|
|
573
|
-
const self = process.env.SVAMP_SESSION_ID;
|
|
574
|
-
if (!self) {
|
|
575
|
-
console.error("'.' means the current session, but SVAMP_SESSION_ID is not set here.");
|
|
576
|
-
console.error('In a workflow step, give the workflow an owner session (so the scheduler can resolve "."), or use an explicit session id/handle.');
|
|
577
|
-
process.exit(1);
|
|
578
|
-
}
|
|
579
|
-
sessionId = self;
|
|
580
|
-
}
|
|
581
|
-
let { server, machine } = await connectAndGetMachine(machineId);
|
|
582
|
-
const sessions = await machine.listSessions();
|
|
583
|
-
const localMatch = findSessionMatch(sessions, sessionId);
|
|
584
|
-
if (localMatch) {
|
|
585
|
-
return { server, machine, fullId: localMatch.sessionId };
|
|
586
|
-
}
|
|
587
|
-
if (!machineId) {
|
|
588
|
-
try {
|
|
589
|
-
const { server: allServer, machines } = await connectAndGetAllMachines();
|
|
590
|
-
for (const m of machines) {
|
|
591
|
-
try {
|
|
592
|
-
const mSessions = await m.listSessions();
|
|
593
|
-
const mMatch = findSessionMatch(mSessions, sessionId);
|
|
594
|
-
if (mMatch) {
|
|
595
|
-
await server.disconnect();
|
|
596
|
-
return { server: allServer, machine: m, fullId: mMatch.sessionId };
|
|
597
|
-
}
|
|
598
|
-
} catch {
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
await allServer.disconnect();
|
|
602
|
-
} catch {
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
resolveSessionId(sessions, sessionId);
|
|
606
|
-
process.exit(1);
|
|
607
|
-
}
|
|
608
|
-
function truncate(str, max) {
|
|
609
|
-
if (str.length <= max) return str;
|
|
610
|
-
return "..." + str.slice(str.length - max + 3);
|
|
611
|
-
}
|
|
612
|
-
function renderMessage(msg) {
|
|
613
|
-
const content = msg.content;
|
|
614
|
-
if (!content) return;
|
|
615
|
-
const role = content.role;
|
|
616
|
-
if (role === "user") {
|
|
617
|
-
const data = content.content;
|
|
618
|
-
let text;
|
|
619
|
-
if (typeof data === "string") {
|
|
620
|
-
try {
|
|
621
|
-
const parsed = JSON.parse(data);
|
|
622
|
-
text = parsed?.text || parsed?.content?.text || data;
|
|
623
|
-
} catch {
|
|
624
|
-
text = data;
|
|
625
|
-
}
|
|
626
|
-
} else if (data?.text) {
|
|
627
|
-
text = data.text;
|
|
628
|
-
} else if (data?.type === "text") {
|
|
629
|
-
text = data.text || "";
|
|
630
|
-
} else {
|
|
631
|
-
text = typeof data === "object" ? JSON.stringify(data) : String(data || "");
|
|
632
|
-
}
|
|
633
|
-
console.log(`\x1B[36m[user]\x1B[0m ${text}`);
|
|
634
|
-
} else if (role === "agent" || role === "assistant") {
|
|
635
|
-
const data = content.content?.data || content.content;
|
|
636
|
-
if (!data) return;
|
|
637
|
-
const getBlocks = (d) => {
|
|
638
|
-
if (d.type === "assistant") {
|
|
639
|
-
const blocks2 = d.message?.content || d.content;
|
|
640
|
-
return Array.isArray(blocks2) ? blocks2 : null;
|
|
641
|
-
}
|
|
642
|
-
return null;
|
|
643
|
-
};
|
|
644
|
-
const renderBlocks = (blocks2) => {
|
|
645
|
-
for (const block of blocks2) {
|
|
646
|
-
if (block.type === "text" && block.text) {
|
|
647
|
-
process.stdout.write(block.text);
|
|
648
|
-
if (!block.text.endsWith("\n")) process.stdout.write("\n");
|
|
649
|
-
} else if (block.type === "tool_use") {
|
|
650
|
-
const argsStr = JSON.stringify(block.input || {}).slice(0, 120);
|
|
651
|
-
console.log(`\x1B[33m[tool]\x1B[0m ${block.name}(${argsStr})`);
|
|
652
|
-
} else if (block.type === "tool_result") {
|
|
653
|
-
const resultStr = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
|
|
654
|
-
console.log(`\x1B[90m[result]\x1B[0m ${resultStr.slice(0, 200)}${resultStr.length > 200 ? "..." : ""}`);
|
|
655
|
-
} else if (block.type === "thinking") {
|
|
656
|
-
const text = block.thinking || block.text || "";
|
|
657
|
-
if (text) console.log(`\x1B[90m[thinking] ${text.slice(0, 200)}\x1B[0m`);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
const blocks = getBlocks(data);
|
|
662
|
-
if (blocks) {
|
|
663
|
-
renderBlocks(blocks);
|
|
664
|
-
} else if (data.type === "result") {
|
|
665
|
-
if (data.result) console.log(`\x1B[32m[done]\x1B[0m ${data.result}`);
|
|
666
|
-
} else if (data.type === "output") {
|
|
667
|
-
const inner = data.data;
|
|
668
|
-
const innerBlocks = inner ? getBlocks(inner) : null;
|
|
669
|
-
if (innerBlocks) {
|
|
670
|
-
renderBlocks(innerBlocks);
|
|
671
|
-
} else if (inner?.type === "result") {
|
|
672
|
-
if (inner.result) console.log(`\x1B[32m[done]\x1B[0m ${inner.result}`);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
} else if (role === "session") {
|
|
676
|
-
const data = content.content?.data;
|
|
677
|
-
if (data?.type === "system" && data?.subtype === "init") {
|
|
678
|
-
console.log(`\x1B[90m[session init]\x1B[0m`);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
function extractContentBlocks(blocks) {
|
|
683
|
-
const parts = [];
|
|
684
|
-
for (const block of blocks) {
|
|
685
|
-
if (block.type === "text" && block.text) {
|
|
686
|
-
parts.push(block.text);
|
|
687
|
-
} else if (block.type === "tool_use") {
|
|
688
|
-
const argsStr = JSON.stringify(block.input || {}).slice(0, 200);
|
|
689
|
-
parts.push(`[tool: ${block.name}](${argsStr})`);
|
|
690
|
-
} else if (block.type === "tool_result") {
|
|
691
|
-
const r = typeof block.content === "string" ? block.content : JSON.stringify(block.content || "");
|
|
692
|
-
parts.push(`[result: ${r.slice(0, 200)}]`);
|
|
693
|
-
} else if (block.type === "thinking") {
|
|
694
|
-
const t = block.thinking || block.text || "";
|
|
695
|
-
if (t) parts.push(`[thinking: ${t.slice(0, 200)}]`);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
return parts;
|
|
699
|
-
}
|
|
700
|
-
function extractMessageText(msg) {
|
|
701
|
-
const content = msg.content;
|
|
702
|
-
if (!content) return null;
|
|
703
|
-
const role = content.role || "unknown";
|
|
704
|
-
let text = "";
|
|
705
|
-
if (role === "user") {
|
|
706
|
-
const data = content.content;
|
|
707
|
-
if (typeof data === "string") {
|
|
708
|
-
try {
|
|
709
|
-
const parsed = JSON.parse(data);
|
|
710
|
-
text = parsed?.text || parsed?.content?.text || data;
|
|
711
|
-
} catch {
|
|
712
|
-
text = data;
|
|
713
|
-
}
|
|
714
|
-
} else if (data?.text) {
|
|
715
|
-
text = data.text;
|
|
716
|
-
} else if (data?.type === "text") {
|
|
717
|
-
text = data.text || "";
|
|
718
|
-
} else {
|
|
719
|
-
text = typeof data === "object" ? JSON.stringify(data) : String(data || "");
|
|
720
|
-
}
|
|
721
|
-
} else if (role === "agent" || role === "assistant") {
|
|
722
|
-
const data = content.content?.data || content.content;
|
|
723
|
-
if (!data) return null;
|
|
724
|
-
const getBlocks = (d) => {
|
|
725
|
-
if (d.type === "assistant") {
|
|
726
|
-
const blocks2 = d.message?.content || d.content;
|
|
727
|
-
return Array.isArray(blocks2) ? blocks2 : null;
|
|
728
|
-
}
|
|
729
|
-
return null;
|
|
730
|
-
};
|
|
731
|
-
const blocks = getBlocks(data);
|
|
732
|
-
if (blocks) {
|
|
733
|
-
text = extractContentBlocks(blocks).join("\n");
|
|
734
|
-
} else if (data.type === "result") {
|
|
735
|
-
text = data.result || "";
|
|
736
|
-
} else if (data.type === "output") {
|
|
737
|
-
const inner = data.data;
|
|
738
|
-
const innerBlocks = inner ? getBlocks(inner) : null;
|
|
739
|
-
if (innerBlocks) {
|
|
740
|
-
text = extractContentBlocks(innerBlocks).join("\n");
|
|
741
|
-
} else if (inner?.type === "result") {
|
|
742
|
-
text = inner.result || "";
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
} else if (role === "session") {
|
|
746
|
-
text = "[session event]";
|
|
747
|
-
}
|
|
748
|
-
return {
|
|
749
|
-
id: msg.id || "",
|
|
750
|
-
seq: msg.seq || 0,
|
|
751
|
-
role,
|
|
752
|
-
text,
|
|
753
|
-
createdAt: msg.createdAt || 0
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
async function waitForIdle(machine, sessionId, timeoutMs) {
|
|
757
|
-
const pollInterval = 2e3;
|
|
758
|
-
const deadline = Date.now() + timeoutMs;
|
|
759
|
-
while (Date.now() < deadline) {
|
|
760
|
-
const activity = await machine.sessionRPC(sessionId, "getActivityState", {});
|
|
761
|
-
if (activity?.pendingPermissions?.length > 0) {
|
|
762
|
-
return { idle: false, pendingPermissions: activity.pendingPermissions };
|
|
763
|
-
}
|
|
764
|
-
if (activity && !activity.thinking) {
|
|
765
|
-
return { idle: true };
|
|
766
|
-
}
|
|
767
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
768
|
-
}
|
|
769
|
-
throw new Error("Timeout waiting for agent to become idle");
|
|
770
|
-
}
|
|
771
|
-
async function waitForBusyThenIdle(machine, sessionId, timeoutMs = 3e5, busyTimeoutMs = 1e4) {
|
|
772
|
-
const pollInterval = 2e3;
|
|
773
|
-
const deadline = Date.now() + timeoutMs;
|
|
774
|
-
const busyDeadline = Date.now() + busyTimeoutMs;
|
|
775
|
-
let sawBusy = false;
|
|
776
|
-
while (Date.now() < deadline) {
|
|
777
|
-
const activity = await machine.sessionRPC(sessionId, "getActivityState", {});
|
|
778
|
-
if (activity?.pendingPermissions?.length > 0) {
|
|
779
|
-
return { idle: false, pendingPermissions: activity.pendingPermissions };
|
|
780
|
-
}
|
|
781
|
-
const isBusy = activity?.thinking === true;
|
|
782
|
-
if (isBusy) {
|
|
783
|
-
sawBusy = true;
|
|
784
|
-
}
|
|
785
|
-
if (activity && !activity.active) {
|
|
786
|
-
return { idle: true };
|
|
787
|
-
}
|
|
788
|
-
if (sawBusy && !isBusy) {
|
|
789
|
-
return { idle: true };
|
|
790
|
-
}
|
|
791
|
-
if (!sawBusy && Date.now() > busyDeadline) {
|
|
792
|
-
return { idle: true };
|
|
793
|
-
}
|
|
794
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
795
|
-
}
|
|
796
|
-
throw new Error("Timeout waiting for agent to become idle");
|
|
797
|
-
}
|
|
798
|
-
async function sessionList(machineId, opts) {
|
|
799
|
-
if (machineId) {
|
|
800
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
801
|
-
try {
|
|
802
|
-
await listSessionsFromMachines(server, [machine], opts);
|
|
803
|
-
} finally {
|
|
804
|
-
await server.disconnect();
|
|
805
|
-
}
|
|
806
|
-
} else {
|
|
807
|
-
const { server, machines } = await connectAndGetAllMachines();
|
|
808
|
-
try {
|
|
809
|
-
await listSessionsFromMachines(server, machines, opts);
|
|
810
|
-
} finally {
|
|
811
|
-
await server.disconnect();
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
function formatPeerLine(s) {
|
|
816
|
-
const m = s.metadata || {};
|
|
817
|
-
const title = resolveDisplayName(m, s.directory);
|
|
818
|
-
const handle = resolveHandle(m);
|
|
819
|
-
const agent = m.flavor || "claude";
|
|
820
|
-
const status = s.active ? "\x1B[32mactive\x1B[0m" : "\x1B[90midle\x1B[0m";
|
|
821
|
-
const dir = m.path || s.directory || "-";
|
|
822
|
-
const label = handle ? `${handle} \xB7 ${truncate(title, 28)}` : truncate(title, 32);
|
|
823
|
-
return ` [${s.sessionId.slice(0, 8)}] ${label} \u2014 ${status} \xB7 ${agent} \xB7 ${dir}`;
|
|
824
|
-
}
|
|
825
|
-
async function sessionWhoami(opts) {
|
|
826
|
-
const currentSessionId = process.env.SVAMP_SESSION_ID;
|
|
827
|
-
const { server, machines } = await connectAndGetAllMachines();
|
|
828
|
-
try {
|
|
829
|
-
const perMachine = await Promise.allSettled(
|
|
830
|
-
machines.map(async (machine) => {
|
|
831
|
-
const [info, sessions] = await Promise.all([
|
|
832
|
-
machine.getMachineInfo(),
|
|
833
|
-
machine.listSessions()
|
|
834
|
-
]);
|
|
835
|
-
return { info, sessions };
|
|
836
|
-
})
|
|
837
|
-
);
|
|
838
|
-
const gathered = perMachine.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
839
|
-
const machineLabel = (info) => info?.metadata?.displayName || info?.metadata?.host || info?.metadata?.hostname || info?.machineId || "unknown";
|
|
840
|
-
let current = null;
|
|
841
|
-
let currentSession = null;
|
|
842
|
-
if (currentSessionId) {
|
|
843
|
-
for (const g of gathered) {
|
|
844
|
-
const match = g.sessions.find((s) => s.sessionId === currentSessionId || s.sessionId.startsWith(currentSessionId));
|
|
845
|
-
if (match) {
|
|
846
|
-
current = g;
|
|
847
|
-
currentSession = match;
|
|
848
|
-
break;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
if (opts?.json) {
|
|
853
|
-
console.log(formatJson({
|
|
854
|
-
operator: server.config?.user?.email || gathered.find((g) => g.info?.metadata?.sharing?.owner)?.info?.metadata?.sharing?.owner || null,
|
|
855
|
-
workspace: server.config?.workspace || null,
|
|
856
|
-
currentSessionId: currentSession?.sessionId || currentSessionId || null,
|
|
857
|
-
currentMachine: current ? machineLabel(current.info) : null,
|
|
858
|
-
totalSessions: gathered.reduce((n, g) => n + g.sessions.length, 0),
|
|
859
|
-
machines: gathered.map((g) => ({
|
|
860
|
-
sessionCount: g.sessions.length,
|
|
861
|
-
activeCount: g.sessions.filter((s) => s.active).length,
|
|
862
|
-
machine: machineLabel(g.info),
|
|
863
|
-
isCurrent: g === current,
|
|
864
|
-
sessions: g.sessions.map((s) => ({
|
|
865
|
-
sessionId: s.sessionId,
|
|
866
|
-
handle: resolveHandle(s.metadata || {}) || null,
|
|
867
|
-
title: resolveDisplayName(s.metadata || {}, s.directory),
|
|
868
|
-
agent: s.metadata?.flavor || "claude",
|
|
869
|
-
active: s.active,
|
|
870
|
-
directory: s.metadata?.path || s.directory || null,
|
|
871
|
-
isSelf: s.sessionId === currentSession?.sessionId
|
|
872
|
-
}))
|
|
873
|
-
}))
|
|
874
|
-
}));
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
const operatorUser = server.config?.user;
|
|
878
|
-
const operatorEmail = operatorUser?.email || gathered.find((g) => g.info?.metadata?.sharing?.owner)?.info?.metadata?.sharing?.owner;
|
|
879
|
-
const workspace = server.config?.workspace;
|
|
880
|
-
const lines = ["# Your environment", ""];
|
|
881
|
-
if (operatorEmail || workspace) {
|
|
882
|
-
lines.push(`Operator: ${operatorEmail || "unknown"}${workspace ? ` \xB7 workspace ${workspace}` : ""}`);
|
|
883
|
-
lines.push("");
|
|
884
|
-
}
|
|
885
|
-
if (currentSession && current) {
|
|
886
|
-
const m = currentSession.metadata || {};
|
|
887
|
-
lines.push("## This session");
|
|
888
|
-
lines.push(`- id: ${currentSession.sessionId}`);
|
|
889
|
-
{
|
|
890
|
-
const h = resolveHandle(m);
|
|
891
|
-
if (h) lines.push(`- handle: ${h}`);
|
|
892
|
-
}
|
|
893
|
-
lines.push(`- topic: ${resolveDisplayName(m, currentSession.directory)}`);
|
|
894
|
-
lines.push(`- agent: ${m.flavor || "claude"}`);
|
|
895
|
-
lines.push(`- dir: ${m.path || currentSession.directory || "-"}`);
|
|
896
|
-
lines.push(`- machine: ${machineLabel(current.info)}`);
|
|
897
|
-
if (m.projectName || m.projectDescription) {
|
|
898
|
-
lines.push(`- project: ${m.projectName || "-"}${m.projectDescription ? ` \u2014 ${m.projectDescription}` : ""}`);
|
|
899
|
-
}
|
|
900
|
-
} else if (currentSessionId) {
|
|
901
|
-
lines.push("## This session");
|
|
902
|
-
lines.push(`- id: ${currentSessionId} (not yet visible in the machine registry)`);
|
|
903
|
-
} else {
|
|
904
|
-
lines.push("_Not running inside a Svamp session (SVAMP_SESSION_ID unset). Showing all reachable sessions._");
|
|
905
|
-
}
|
|
906
|
-
lines.push("");
|
|
907
|
-
const byActive = (a, b) => (b.active ? 1 : 0) - (a.active ? 1 : 0);
|
|
908
|
-
const activeOf = (arr) => arr.filter((s) => s.active).length;
|
|
909
|
-
const grandTotal = gathered.reduce((n, g) => n + g.sessions.length, 0);
|
|
910
|
-
const orderedMachines = [...gathered].sort((a, b) => {
|
|
911
|
-
if (a === current) return -1;
|
|
912
|
-
if (b === current) return 1;
|
|
913
|
-
return b.sessions.length - a.sessions.length;
|
|
914
|
-
});
|
|
915
|
-
lines.push(`## Machines (same user) \u2014 ${grandTotal} session${grandTotal === 1 ? "" : "s"} across ${gathered.length} machine${gathered.length === 1 ? "" : "s"}`);
|
|
916
|
-
for (const g of orderedMachines) {
|
|
917
|
-
const ms = [...g.sessions].sort(byActive);
|
|
918
|
-
const here = g === current ? " (this machine)" : "";
|
|
919
|
-
lines.push(`- ${machineLabel(g.info)}${here}: ${ms.length} session${ms.length === 1 ? "" : "s"} (${activeOf(ms)} active)`);
|
|
920
|
-
if (opts?.all) {
|
|
921
|
-
const peers = g === current ? ms.filter((s) => s.sessionId !== currentSession?.sessionId) : ms;
|
|
922
|
-
for (const s of peers) lines.push(formatPeerLine(s));
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
lines.push("- List sessions: `svamp session list` (add `--active`, or `-m <machine>` for one).");
|
|
926
|
-
if (!opts?.all && grandTotal > 1) {
|
|
927
|
-
lines.push("- Or `svamp session whoami --all` to list every session inline.");
|
|
928
|
-
}
|
|
929
|
-
lines.push("");
|
|
930
|
-
lines.push("## Reach a peer");
|
|
931
|
-
lines.push("- `svamp session list` \u2014 find a peer's handle (project:name) or id, then:");
|
|
932
|
-
lines.push("- `svamp session info <handle|id>` / `svamp session messages <handle|id>` \u2014 inspect before reaching out");
|
|
933
|
-
lines.push('- `svamp session send <handle|id> "<msg>"` \u2014 drop a message into a peer\'s inbox (e.g. `send hypha-cloud:brave-otter "\u2026"`)');
|
|
934
|
-
lines.push('- Reply to an inbox message: `svamp session inbox reply <message-id> "<body>"`');
|
|
935
|
-
lines.push("- Keep it purposeful: only initiate with a concrete reason, avoid ping-pong loops.");
|
|
936
|
-
console.log(lines.join("\n"));
|
|
937
|
-
} finally {
|
|
938
|
-
await server.disconnect();
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
async function listSessionsFromMachines(_server, machines, opts) {
|
|
942
|
-
const allSessions = [];
|
|
943
|
-
const machineResults = await Promise.allSettled(
|
|
944
|
-
machines.map(async (machine) => {
|
|
945
|
-
const [info, sessions] = await Promise.all([
|
|
946
|
-
machine.getMachineInfo(),
|
|
947
|
-
machine.listSessions()
|
|
948
|
-
]);
|
|
949
|
-
const host = info.metadata?.displayName || info.metadata?.host || info.machineId;
|
|
950
|
-
for (const s of sessions) {
|
|
951
|
-
s.machineHost = host;
|
|
952
|
-
}
|
|
953
|
-
return sessions;
|
|
954
|
-
})
|
|
955
|
-
);
|
|
956
|
-
for (const r of machineResults) {
|
|
957
|
-
if (r.status === "fulfilled") allSessions.push(...r.value);
|
|
958
|
-
}
|
|
959
|
-
const filtered = opts?.active ? allSessions.filter((s) => s.active) : allSessions;
|
|
960
|
-
if (filtered.length === 0) {
|
|
961
|
-
if (opts?.json) {
|
|
962
|
-
console.log(formatJson([]));
|
|
963
|
-
} else {
|
|
964
|
-
console.log("No active sessions.");
|
|
965
|
-
}
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
const enriched = filtered.map((s) => {
|
|
969
|
-
const m = s.metadata || {};
|
|
970
|
-
return {
|
|
971
|
-
...s,
|
|
972
|
-
flavor: m.flavor || "claude",
|
|
973
|
-
name: resolveDisplayName(m, s.directory),
|
|
974
|
-
handle: resolveHandle(m),
|
|
975
|
-
path: m.path || s.directory || "",
|
|
976
|
-
host: m.host || s.machineHost || ""
|
|
977
|
-
};
|
|
978
|
-
});
|
|
979
|
-
if (opts?.json) {
|
|
980
|
-
console.log(formatJson(enriched.map((s) => ({
|
|
981
|
-
sessionId: s.sessionId,
|
|
982
|
-
handle: s.handle,
|
|
983
|
-
agent: s.flavor,
|
|
984
|
-
name: s.name,
|
|
985
|
-
path: s.path,
|
|
986
|
-
host: s.host,
|
|
987
|
-
active: s.active,
|
|
988
|
-
directory: s.directory
|
|
989
|
-
}))));
|
|
990
|
-
} else {
|
|
991
|
-
const header = `${"SESSION".padEnd(30)} ${"AGENT".padEnd(8)} ${"STATUS".padEnd(9)} ${"TITLE".padEnd(22)} ${"MACHINE".padEnd(16)} ${"DIRECTORY".padEnd(30)}`;
|
|
992
|
-
console.log(header);
|
|
993
|
-
console.log("-".repeat(header.length));
|
|
994
|
-
for (const s of enriched) {
|
|
995
|
-
const ref = truncate(s.handle || s.sessionId, 30).padEnd(30);
|
|
996
|
-
const agent = (s.flavor || "claude").padEnd(8);
|
|
997
|
-
const status = s.active ? "\x1B[32mactive\x1B[0m " : "\x1B[90minactive\x1B[0m";
|
|
998
|
-
const name = truncate(s.name || "-", 22).padEnd(22);
|
|
999
|
-
const machine = truncate(s.host || "-", 14).padEnd(16);
|
|
1000
|
-
const dir = truncate(s.directory || "-", 28).padEnd(30);
|
|
1001
|
-
console.log(`${ref} ${agent} ${status} ${name} ${machine} ${dir}`);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
const WORKTREE_ADJECTIVES = [
|
|
1006
|
-
"clever",
|
|
1007
|
-
"happy",
|
|
1008
|
-
"swift",
|
|
1009
|
-
"bright",
|
|
1010
|
-
"calm",
|
|
1011
|
-
"bold",
|
|
1012
|
-
"quiet",
|
|
1013
|
-
"brave",
|
|
1014
|
-
"wise",
|
|
1015
|
-
"eager",
|
|
1016
|
-
"gentle",
|
|
1017
|
-
"quick",
|
|
1018
|
-
"sharp",
|
|
1019
|
-
"smooth",
|
|
1020
|
-
"fresh"
|
|
1021
|
-
];
|
|
1022
|
-
const WORKTREE_NOUNS = [
|
|
1023
|
-
"ocean",
|
|
1024
|
-
"forest",
|
|
1025
|
-
"cloud",
|
|
1026
|
-
"star",
|
|
1027
|
-
"river",
|
|
1028
|
-
"mountain",
|
|
1029
|
-
"valley",
|
|
1030
|
-
"bridge",
|
|
1031
|
-
"beacon",
|
|
1032
|
-
"harbor",
|
|
1033
|
-
"garden",
|
|
1034
|
-
"meadow",
|
|
1035
|
-
"canyon",
|
|
1036
|
-
"island",
|
|
1037
|
-
"desert"
|
|
1038
|
-
];
|
|
1039
|
-
function generateWorktreeName() {
|
|
1040
|
-
const adj = WORKTREE_ADJECTIVES[Math.floor(Math.random() * WORKTREE_ADJECTIVES.length)];
|
|
1041
|
-
const noun = WORKTREE_NOUNS[Math.floor(Math.random() * WORKTREE_NOUNS.length)];
|
|
1042
|
-
return `${adj}-${noun}`;
|
|
1043
|
-
}
|
|
1044
|
-
function ensureWorktreeExcludes(projectRoot) {
|
|
1045
|
-
try {
|
|
1046
|
-
const common = execSync("git rev-parse --git-common-dir", { cwd: projectRoot, stdio: "pipe" }).toString().trim();
|
|
1047
|
-
const commonAbs = isAbsolute(common) ? common : join(projectRoot, common);
|
|
1048
|
-
const excludeFile = join(commonAbs, "info", "exclude");
|
|
1049
|
-
const existing = existsSync(excludeFile) ? readFileSync(excludeFile, "utf-8") : "";
|
|
1050
|
-
const lines = existing.split("\n");
|
|
1051
|
-
const want = [".svamp/", ".dev/"];
|
|
1052
|
-
const missing = want.filter((w) => !lines.some((l) => l.trim() === w));
|
|
1053
|
-
if (missing.length) {
|
|
1054
|
-
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
1055
|
-
writeFileSync(excludeFile, existing + prefix + missing.join("\n") + "\n");
|
|
1056
|
-
}
|
|
1057
|
-
} catch {
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
function createWorktree(baseDir) {
|
|
1061
|
-
const absBase = resolve(baseDir);
|
|
1062
|
-
const marker = "/.dev/worktree/";
|
|
1063
|
-
const idx = absBase.indexOf(marker);
|
|
1064
|
-
const projectRoot = idx !== -1 ? absBase.substring(0, idx) : absBase;
|
|
1065
|
-
try {
|
|
1066
|
-
execSync("git rev-parse --git-dir", { cwd: projectRoot, stdio: "pipe" });
|
|
1067
|
-
} catch {
|
|
1068
|
-
throw new Error(`Not a git repository: ${projectRoot}`);
|
|
1069
|
-
}
|
|
1070
|
-
ensureWorktreeExcludes(projectRoot);
|
|
1071
|
-
const name = generateWorktreeName();
|
|
1072
|
-
const relPath = `.dev/worktree/${name}`;
|
|
1073
|
-
try {
|
|
1074
|
-
execSync(`git worktree add -b ${name} ${relPath}`, { cwd: projectRoot, stdio: "pipe" });
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
for (let i = 2; i <= 4; i++) {
|
|
1077
|
-
const suffixed = `${name}-${i}`;
|
|
1078
|
-
const suffixedPath = `.dev/worktree/${suffixed}`;
|
|
1079
|
-
try {
|
|
1080
|
-
execSync(`git worktree add -b ${suffixed} ${suffixedPath}`, { cwd: projectRoot, stdio: "pipe" });
|
|
1081
|
-
return { path: join(projectRoot, suffixedPath), branch: suffixed };
|
|
1082
|
-
} catch {
|
|
1083
|
-
continue;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
throw new Error(`Failed to create worktree: ${err.stderr?.toString() || err.message}`);
|
|
1087
|
-
}
|
|
1088
|
-
return { path: join(projectRoot, relPath), branch: name };
|
|
1089
|
-
}
|
|
1090
|
-
function parseShareArg(arg) {
|
|
1091
|
-
const parts = arg.split(":");
|
|
1092
|
-
const email = parts[0];
|
|
1093
|
-
const suppliedRole = parts[1];
|
|
1094
|
-
if (suppliedRole) {
|
|
1095
|
-
if (!["view", "interact", "admin"].includes(suppliedRole)) {
|
|
1096
|
-
throw new Error(`Invalid role "${suppliedRole}" in --share ${arg}. Per-user roles are deprecated; omit the :role suffix.`);
|
|
1097
|
-
}
|
|
1098
|
-
if (suppliedRole !== "admin") {
|
|
1099
|
-
console.warn(`Note: per-user role "${suppliedRole}" is deprecated. Added users have full (owner-equivalent) access.`);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
return { email, role: "admin" };
|
|
1103
|
-
}
|
|
1104
|
-
async function sessionSpawn(agent, directory, machineId, opts) {
|
|
1105
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
1106
|
-
try {
|
|
1107
|
-
let sharing;
|
|
1108
|
-
if (opts?.share?.length) {
|
|
1109
|
-
sharing = {
|
|
1110
|
-
enabled: true,
|
|
1111
|
-
owner: "",
|
|
1112
|
-
// will be auto-set by machine service from Hypha context
|
|
1113
|
-
allowedUsers: opts.share.map(
|
|
1114
|
-
(s) => normalizeAllowedUser({ email: s.email, addedBy: "cli" }, "cli")
|
|
1115
|
-
)
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
let securityContext;
|
|
1119
|
-
if (opts?.securityContextPath) {
|
|
1120
|
-
const configPath = resolve(opts.securityContextPath);
|
|
1121
|
-
const config = loadSecurityContextConfig(configPath);
|
|
1122
|
-
securityContext = resolveSecurityContext(config, void 0);
|
|
1123
|
-
}
|
|
1124
|
-
if (opts?.denyRead?.length || opts?.allowWrite?.length || opts?.denyNetwork || opts?.allowDomain?.length) {
|
|
1125
|
-
const flagCtx = buildSecurityContextFromFlags({
|
|
1126
|
-
denyRead: opts.denyRead,
|
|
1127
|
-
allowWrite: opts.allowWrite,
|
|
1128
|
-
denyNetwork: opts.denyNetwork,
|
|
1129
|
-
allowDomain: opts.allowDomain
|
|
1130
|
-
});
|
|
1131
|
-
securityContext = mergeSecurityContexts(securityContext, flagCtx);
|
|
1132
|
-
}
|
|
1133
|
-
const forceIsolation = opts?.isolate || !!securityContext;
|
|
1134
|
-
if (securityContext?.filesystem?.allowWrite?.length) {
|
|
1135
|
-
const absDir = resolve(directory);
|
|
1136
|
-
const writePaths = securityContext.filesystem.allowWrite;
|
|
1137
|
-
const dirCovered = writePaths.some((p) => absDir.startsWith(resolve(p)) || resolve(p).startsWith(absDir));
|
|
1138
|
-
if (!dirCovered) {
|
|
1139
|
-
console.warn(`Warning: Working directory ${absDir} is not covered by allowWrite paths: ${writePaths.join(", ")}`);
|
|
1140
|
-
console.warn(` The agent may not be able to write files in the working directory.`);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
if (opts?.worktree) {
|
|
1144
|
-
try {
|
|
1145
|
-
const wt = createWorktree(directory);
|
|
1146
|
-
console.log(`Created worktree: ${wt.branch} \u2192 ${wt.path}`);
|
|
1147
|
-
directory = wt.path;
|
|
1148
|
-
} catch (err) {
|
|
1149
|
-
console.error(`Failed to create worktree: ${err.message}`);
|
|
1150
|
-
process.exit(1);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
let effectiveParentSessionId = opts?.parentSessionId || process.env.SVAMP_SESSION_ID || void 0;
|
|
1154
|
-
if (effectiveParentSessionId) {
|
|
1155
|
-
try {
|
|
1156
|
-
const sessions = await machine.listSessions();
|
|
1157
|
-
if (opts?.parentSessionId) {
|
|
1158
|
-
const match = resolveSessionId(sessions, effectiveParentSessionId);
|
|
1159
|
-
effectiveParentSessionId = match.sessionId;
|
|
1160
|
-
} else {
|
|
1161
|
-
const found = sessions.find((s) => s.sessionId === effectiveParentSessionId);
|
|
1162
|
-
if (!found) throw new Error("not found");
|
|
1163
|
-
}
|
|
1164
|
-
} catch (err) {
|
|
1165
|
-
const source = opts?.parentSessionId ? "--parent" : "SVAMP_SESSION_ID";
|
|
1166
|
-
console.error(`Error: Parent session ${effectiveParentSessionId} not found (from ${source}).`);
|
|
1167
|
-
process.exit(1);
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
console.log(`Spawning ${agent} session in ${directory}...`);
|
|
1171
|
-
if (effectiveParentSessionId) {
|
|
1172
|
-
console.log(`Parent session: ${effectiveParentSessionId}${!opts?.parentSessionId ? " (auto-detected from SVAMP_SESSION_ID)" : ""}`);
|
|
1173
|
-
}
|
|
1174
|
-
if (opts?.tags?.length) {
|
|
1175
|
-
console.log(`Tags: ${opts.tags.join(", ")}`);
|
|
1176
|
-
}
|
|
1177
|
-
if (forceIsolation) {
|
|
1178
|
-
console.log(`Isolation: enabled (workspace: ${resolve(directory)})`);
|
|
1179
|
-
}
|
|
1180
|
-
if (securityContext) {
|
|
1181
|
-
console.log(`Security context: ${JSON.stringify(securityContext, null, 2)}`);
|
|
1182
|
-
}
|
|
1183
|
-
if (sharing) {
|
|
1184
|
-
console.log(`Sharing with: ${sharing.allowedUsers.map((u) => u.email).join(", ")} (full access)`);
|
|
1185
|
-
}
|
|
1186
|
-
const result = await machine.spawnSession({
|
|
1187
|
-
directory,
|
|
1188
|
-
agent,
|
|
1189
|
-
sharing,
|
|
1190
|
-
securityContext,
|
|
1191
|
-
forceIsolation,
|
|
1192
|
-
tags: opts?.tags,
|
|
1193
|
-
parentSessionId: effectiveParentSessionId,
|
|
1194
|
-
permissionMode: opts?.permissionMode
|
|
1195
|
-
});
|
|
1196
|
-
if (result.type === "success") {
|
|
1197
|
-
console.log(`Session started: ${result.sessionId}`);
|
|
1198
|
-
if (result.message) console.log(` ${result.message}`);
|
|
1199
|
-
const explicitPermMode = opts?.permissionMode;
|
|
1200
|
-
if (explicitPermMode && explicitPermMode !== "bypassPermissions") {
|
|
1201
|
-
console.log(`\x1B[33m\u26A0 Permission mode: ${explicitPermMode}. Agent will pause for tool approval.\x1B[0m`);
|
|
1202
|
-
console.log(`\x1B[33m Use \`svamp session approve <id>\` to unblock, or drop the flag to run autonomously.\x1B[0m`);
|
|
1203
|
-
}
|
|
1204
|
-
if (opts?.message && result.sessionId) {
|
|
1205
|
-
const svc = getSessionProxy(machine, result.sessionId);
|
|
1206
|
-
const sendResult = await svc.sendMessage(
|
|
1207
|
-
JSON.stringify({
|
|
1208
|
-
role: "user",
|
|
1209
|
-
content: { type: "text", text: opts.message },
|
|
1210
|
-
meta: { sentFrom: "svamp-cli" }
|
|
1211
|
-
})
|
|
1212
|
-
);
|
|
1213
|
-
console.log(`Message sent (seq: ${sendResult.seq})`);
|
|
1214
|
-
if (opts.wait) {
|
|
1215
|
-
console.log("Waiting for agent to become idle...");
|
|
1216
|
-
await waitForBusyThenIdle(machine, result.sessionId);
|
|
1217
|
-
console.log("Agent is idle.");
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
} else if (result.type === "requestToApproveDirectoryCreation") {
|
|
1221
|
-
console.error(`Directory ${result.directory} does not exist. Create it first or use an existing directory.`);
|
|
1222
|
-
process.exit(1);
|
|
1223
|
-
} else {
|
|
1224
|
-
console.error(`Failed: ${result.errorMessage || "Unknown error"}`);
|
|
1225
|
-
process.exit(1);
|
|
1226
|
-
}
|
|
1227
|
-
} finally {
|
|
1228
|
-
await server.disconnect();
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
async function sessionArchive(sessionId, machineId) {
|
|
1232
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1233
|
-
try {
|
|
1234
|
-
const success = await machine.archiveSession(fullId);
|
|
1235
|
-
if (success) {
|
|
1236
|
-
console.log(`Session ${fullId.slice(0, 8)} archived. Run \`svamp session resume ${fullId.slice(0, 8)}\` to bring it back.`);
|
|
1237
|
-
} else {
|
|
1238
|
-
console.error("Failed to archive session (not found on daemon).");
|
|
1239
|
-
process.exit(1);
|
|
1240
|
-
}
|
|
1241
|
-
} finally {
|
|
1242
|
-
await server.disconnect();
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
async function sessionResume(sessionId, machineId) {
|
|
1246
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1247
|
-
try {
|
|
1248
|
-
const result = await machine.resumeSession(fullId);
|
|
1249
|
-
if (result?.success) {
|
|
1250
|
-
console.log(`Session ${fullId.slice(0, 8)} resumed.`);
|
|
1251
|
-
} else {
|
|
1252
|
-
console.error(`Failed to resume session: ${result?.message || "unknown error"}`);
|
|
1253
|
-
process.exit(1);
|
|
1254
|
-
}
|
|
1255
|
-
} finally {
|
|
1256
|
-
await server.disconnect();
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
async function sessionDelete(sessionId, machineId) {
|
|
1260
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1261
|
-
try {
|
|
1262
|
-
const success = await machine.deleteSession(fullId);
|
|
1263
|
-
if (success) {
|
|
1264
|
-
console.log(`Session ${fullId.slice(0, 8)} deleted.`);
|
|
1265
|
-
} else {
|
|
1266
|
-
console.error("Failed to delete session.");
|
|
1267
|
-
process.exit(1);
|
|
1268
|
-
}
|
|
1269
|
-
} finally {
|
|
1270
|
-
await server.disconnect();
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
async function sessionInfo(sessionId, machineId, opts) {
|
|
1274
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1275
|
-
try {
|
|
1276
|
-
let metadata = {};
|
|
1277
|
-
let activity = {};
|
|
1278
|
-
try {
|
|
1279
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1280
|
-
const metaResult = await svc.getMetadata();
|
|
1281
|
-
metadata = metaResult.metadata || {};
|
|
1282
|
-
activity = await svc.getActivityState();
|
|
1283
|
-
} catch {
|
|
1284
|
-
}
|
|
1285
|
-
const pendingPermissions = activity.pendingPermissions || [];
|
|
1286
|
-
const statusData = {
|
|
1287
|
-
sessionId: fullId,
|
|
1288
|
-
flavor: metadata.flavor || "claude",
|
|
1289
|
-
name: resolveDisplayName(metadata, metadata.path || void 0),
|
|
1290
|
-
handle: resolveHandle(metadata),
|
|
1291
|
-
path: metadata.path || "",
|
|
1292
|
-
host: metadata.host || "",
|
|
1293
|
-
lifecycleState: metadata.lifecycleState || "unknown",
|
|
1294
|
-
active: activity.active ?? false,
|
|
1295
|
-
thinking: activity.thinking ?? false,
|
|
1296
|
-
startedBy: metadata.startedBy || "",
|
|
1297
|
-
summary: metadata.summary?.text || void 0,
|
|
1298
|
-
projectName: metadata.projectName || void 0,
|
|
1299
|
-
projectDescription: metadata.projectDescription || void 0,
|
|
1300
|
-
claudeSessionId: metadata.claudeSessionId || void 0,
|
|
1301
|
-
sessionLink: metadata.sessionLink?.url || void 0,
|
|
1302
|
-
tags: metadata.tags?.length ? metadata.tags : void 0,
|
|
1303
|
-
parentSessionId: metadata.parentSessionId || void 0,
|
|
1304
|
-
pendingPermissions: pendingPermissions.length > 0 ? pendingPermissions : void 0
|
|
1305
|
-
};
|
|
1306
|
-
if (opts?.json) {
|
|
1307
|
-
console.log(formatJson(statusData));
|
|
1308
|
-
} else {
|
|
1309
|
-
console.log(formatSessionStatus(statusData));
|
|
1310
|
-
}
|
|
1311
|
-
} finally {
|
|
1312
|
-
await server.disconnect();
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
async function sessionMessages(sessionId, machineId, opts) {
|
|
1316
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1317
|
-
try {
|
|
1318
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1319
|
-
let messages;
|
|
1320
|
-
if (opts?.after !== void 0) {
|
|
1321
|
-
const apiLimit = opts?.limit ?? 500;
|
|
1322
|
-
({ messages } = await svc.getMessages(opts.after, apiLimit));
|
|
1323
|
-
} else {
|
|
1324
|
-
const tailCount = Math.min(opts?.last ?? opts?.limit ?? 50, 500);
|
|
1325
|
-
({ messages } = await svc.getLatestMessages(void 0, tailCount));
|
|
1326
|
-
}
|
|
1327
|
-
const toShow = opts?.last ? messages.slice(-opts.last) : messages;
|
|
1328
|
-
if (toShow.length === 0) {
|
|
1329
|
-
if (opts?.json) {
|
|
1330
|
-
console.log(formatJson([]));
|
|
1331
|
-
} else {
|
|
1332
|
-
console.log("No messages yet.");
|
|
1333
|
-
}
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
if (opts?.json && opts?.raw) {
|
|
1337
|
-
console.log(formatJson(toShow));
|
|
1338
|
-
} else if (opts?.json) {
|
|
1339
|
-
const formatted = [];
|
|
1340
|
-
for (const msg of toShow) {
|
|
1341
|
-
const extracted = extractMessageText(msg);
|
|
1342
|
-
if (extracted) {
|
|
1343
|
-
formatted.push(extracted);
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
formatted.sort((a, b) => a.createdAt - b.createdAt);
|
|
1347
|
-
console.log(formatJson(formatted));
|
|
1348
|
-
} else {
|
|
1349
|
-
for (const msg of toShow) {
|
|
1350
|
-
renderMessage(msg);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
} finally {
|
|
1354
|
-
await server.disconnect();
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
async function sessionEditMessage(sessionId, messageId, newText, machineId) {
|
|
1358
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1359
|
-
try {
|
|
1360
|
-
const res = await machine.sessionRPC(fullId, "editMessage", { messageId, newText });
|
|
1361
|
-
if (res?.success) {
|
|
1362
|
-
console.log("\u2713 Message edited; Claude restarted with the rewritten history.");
|
|
1363
|
-
} else {
|
|
1364
|
-
console.error(`\u2717 Edit failed: ${res?.message || "unknown error"}`);
|
|
1365
|
-
process.exitCode = 1;
|
|
1366
|
-
}
|
|
1367
|
-
} finally {
|
|
1368
|
-
await server.disconnect();
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
async function sessionRefineLastReply(sessionId, instruction, machineId) {
|
|
1372
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1373
|
-
try {
|
|
1374
|
-
const res = await machine.sessionRPC(fullId, "refineLastReply", { instruction });
|
|
1375
|
-
if (res?.success) {
|
|
1376
|
-
console.log("\u2713 Latest reply refined; Claude restarted with the rewritten history.");
|
|
1377
|
-
} else {
|
|
1378
|
-
console.error(`\u2717 Refine failed: ${res?.message || "unknown error"}`);
|
|
1379
|
-
process.exitCode = 1;
|
|
1380
|
-
}
|
|
1381
|
-
} finally {
|
|
1382
|
-
await server.disconnect();
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
async function sessionUndoEdit(sessionId, machineId) {
|
|
1386
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1387
|
-
try {
|
|
1388
|
-
const res = await machine.sessionRPC(fullId, "undoLastEdit", {});
|
|
1389
|
-
if (res?.success) {
|
|
1390
|
-
console.log("\u2713 Last edit reverted; Claude restarted with the pre-edit history.");
|
|
1391
|
-
} else {
|
|
1392
|
-
console.error(`\u2717 Undo failed: ${res?.message || "unknown error"}`);
|
|
1393
|
-
process.exitCode = 1;
|
|
1394
|
-
}
|
|
1395
|
-
} finally {
|
|
1396
|
-
await server.disconnect();
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
async function sessionApprove(sessionId, requestId, machineId, opts) {
|
|
1400
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1401
|
-
try {
|
|
1402
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1403
|
-
if (requestId) {
|
|
1404
|
-
const activity = await svc.getActivityState();
|
|
1405
|
-
const pending = activity.pendingPermissions || [];
|
|
1406
|
-
const matched = pending.find((p) => p.id === requestId || p.id.startsWith(requestId));
|
|
1407
|
-
if (!matched) {
|
|
1408
|
-
console.error(`No pending permission matching "${requestId}".`);
|
|
1409
|
-
process.exitCode = 1;
|
|
1410
|
-
return;
|
|
1411
|
-
}
|
|
1412
|
-
await svc.permissionResponse({ id: matched.id, approved: true });
|
|
1413
|
-
if (opts?.json) {
|
|
1414
|
-
console.log(formatJson({ sessionId: fullId, approved: 1, requestIds: [matched.id] }));
|
|
1415
|
-
} else {
|
|
1416
|
-
console.log(`Approved: ${matched.tool} [${matched.id.slice(0, 8)}]`);
|
|
1417
|
-
}
|
|
1418
|
-
} else {
|
|
1419
|
-
const activity = await svc.getActivityState();
|
|
1420
|
-
const pending = activity.pendingPermissions || [];
|
|
1421
|
-
if (pending.length === 0) {
|
|
1422
|
-
if (opts?.json) {
|
|
1423
|
-
console.log(formatJson({ sessionId: fullId, approved: 0, requestIds: [] }));
|
|
1424
|
-
} else {
|
|
1425
|
-
console.error("No pending permission requests.");
|
|
1426
|
-
}
|
|
1427
|
-
process.exitCode = 1;
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
const approvedIds = [];
|
|
1431
|
-
for (const p of pending) {
|
|
1432
|
-
await svc.permissionResponse({ id: p.id, approved: true });
|
|
1433
|
-
approvedIds.push(p.id);
|
|
1434
|
-
}
|
|
1435
|
-
if (opts?.json) {
|
|
1436
|
-
console.log(formatJson({ sessionId: fullId, approved: approvedIds.length, requestIds: approvedIds }));
|
|
1437
|
-
} else {
|
|
1438
|
-
console.log(`Approved ${approvedIds.length} permission(s).`);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
} finally {
|
|
1442
|
-
await server.disconnect();
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
async function sessionDeny(sessionId, requestId, machineId, opts) {
|
|
1446
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1447
|
-
try {
|
|
1448
|
-
const svc = getSessionProxy(machine, fullId);
|
|
1449
|
-
if (requestId) {
|
|
1450
|
-
const activity = await svc.getActivityState();
|
|
1451
|
-
const pending = activity.pendingPermissions || [];
|
|
1452
|
-
const matched = pending.find((p) => p.id === requestId || p.id.startsWith(requestId));
|
|
1453
|
-
if (!matched) {
|
|
1454
|
-
console.error(`No pending permission matching "${requestId}".`);
|
|
1455
|
-
process.exitCode = 1;
|
|
1456
|
-
return;
|
|
1457
|
-
}
|
|
1458
|
-
await svc.permissionResponse({ id: matched.id, approved: false });
|
|
1459
|
-
if (opts?.json) {
|
|
1460
|
-
console.log(formatJson({ sessionId: fullId, denied: 1, requestIds: [matched.id] }));
|
|
1461
|
-
} else {
|
|
1462
|
-
console.log(`Denied: ${matched.tool} [${matched.id.slice(0, 8)}]`);
|
|
1463
|
-
}
|
|
1464
|
-
} else {
|
|
1465
|
-
const activity = await svc.getActivityState();
|
|
1466
|
-
const pending = activity.pendingPermissions || [];
|
|
1467
|
-
if (pending.length === 0) {
|
|
1468
|
-
if (opts?.json) {
|
|
1469
|
-
console.log(formatJson({ sessionId: fullId, denied: 0, requestIds: [] }));
|
|
1470
|
-
} else {
|
|
1471
|
-
console.error("No pending permission requests.");
|
|
1472
|
-
}
|
|
1473
|
-
process.exitCode = 1;
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
const deniedIds = [];
|
|
1477
|
-
for (const p of pending) {
|
|
1478
|
-
await svc.permissionResponse({ id: p.id, approved: false });
|
|
1479
|
-
deniedIds.push(p.id);
|
|
1480
|
-
}
|
|
1481
|
-
if (opts?.json) {
|
|
1482
|
-
console.log(formatJson({ sessionId: fullId, denied: deniedIds.length, requestIds: deniedIds }));
|
|
1483
|
-
} else {
|
|
1484
|
-
console.log(`Denied ${deniedIds.length} permission(s).`);
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
} finally {
|
|
1488
|
-
await server.disconnect();
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
async function sessionAttach(sessionId, machineId) {
|
|
1492
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1493
|
-
let svc;
|
|
1494
|
-
try {
|
|
1495
|
-
svc = getSessionProxy(machine, fullId);
|
|
1496
|
-
} catch (err) {
|
|
1497
|
-
console.error(`Could not find session service: ${err.message}`);
|
|
1498
|
-
await server.disconnect();
|
|
1499
|
-
process.exit(1);
|
|
1500
|
-
}
|
|
1501
|
-
const { metadata } = await svc.getMetadata();
|
|
1502
|
-
const flavor = metadata?.flavor || "claude";
|
|
1503
|
-
const name = metadata?.name || fullId.slice(0, 8);
|
|
1504
|
-
console.log(`Attached to ${flavor} session "${name}". Commands: /quit /abort /kill
|
|
1505
|
-
`);
|
|
1506
|
-
const seenMessageIds = /* @__PURE__ */ new Set();
|
|
1507
|
-
let replayDone = false;
|
|
1508
|
-
await svc.registerListener({
|
|
1509
|
-
onUpdate: (update) => {
|
|
1510
|
-
if (update.type === "new-message") {
|
|
1511
|
-
const msg = update.message;
|
|
1512
|
-
if (!msg?.id) return;
|
|
1513
|
-
if (seenMessageIds.has(msg.id)) return;
|
|
1514
|
-
seenMessageIds.add(msg.id);
|
|
1515
|
-
if (!replayDone) return;
|
|
1516
|
-
renderMessage(msg);
|
|
1517
|
-
} else if (update.type === "activity") {
|
|
1518
|
-
if (!replayDone) return;
|
|
1519
|
-
if (update.thinking) {
|
|
1520
|
-
process.stdout.write("\x1B[90m[thinking...]\x1B[0m\r");
|
|
1521
|
-
} else if (!update.thinking) {
|
|
1522
|
-
process.stdout.write("\n> ");
|
|
1523
|
-
}
|
|
1524
|
-
} else if (update.type === "update-session") ;
|
|
1525
|
-
}
|
|
1526
|
-
});
|
|
1527
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
1528
|
-
replayDone = true;
|
|
1529
|
-
console.log(`\x1B[90m(${seenMessageIds.size} messages in history)\x1B[0m`);
|
|
1530
|
-
process.stdout.write("> ");
|
|
1531
|
-
const readline = await import('readline');
|
|
1532
|
-
const rl = readline.createInterface({
|
|
1533
|
-
input: process.stdin,
|
|
1534
|
-
output: process.stdout,
|
|
1535
|
-
terminal: true
|
|
1536
|
-
});
|
|
1537
|
-
rl.on("line", async (line) => {
|
|
1538
|
-
const trimmed = line.trim();
|
|
1539
|
-
if (!trimmed) {
|
|
1540
|
-
process.stdout.write("> ");
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
if (trimmed === "/quit" || trimmed === "/detach") {
|
|
1544
|
-
console.log("Detaching (session continues running)...");
|
|
1545
|
-
rl.close();
|
|
1546
|
-
await server.disconnect();
|
|
1547
|
-
process.exit(0);
|
|
1548
|
-
}
|
|
1549
|
-
if (trimmed === "/abort" || trimmed === "/cancel") {
|
|
1550
|
-
try {
|
|
1551
|
-
await svc.abort();
|
|
1552
|
-
console.log("Abort sent.");
|
|
1553
|
-
} catch (err) {
|
|
1554
|
-
console.error(`Abort failed: ${err.message}`);
|
|
1555
|
-
}
|
|
1556
|
-
process.stdout.write("> ");
|
|
1557
|
-
return;
|
|
1558
|
-
}
|
|
1559
|
-
if (trimmed === "/archive") {
|
|
1560
|
-
try {
|
|
1561
|
-
await svc.archiveSession();
|
|
1562
|
-
console.log("Session archived. Run `svamp session resume <id>` to bring it back.");
|
|
1563
|
-
} catch (err) {
|
|
1564
|
-
console.error(`Archive failed: ${err.message}`);
|
|
1565
|
-
}
|
|
1566
|
-
rl.close();
|
|
1567
|
-
await server.disconnect();
|
|
1568
|
-
process.exit(0);
|
|
1569
|
-
}
|
|
1570
|
-
if (trimmed === "/info") {
|
|
1571
|
-
try {
|
|
1572
|
-
const { metadata: m } = await svc.getMetadata();
|
|
1573
|
-
const act = await svc.getActivityState();
|
|
1574
|
-
console.log(` Agent: ${m?.flavor || "claude"}, State: ${m?.lifecycleState || "?"}, Active: ${act?.active}, Thinking: ${act?.thinking}`);
|
|
1575
|
-
} catch (err) {
|
|
1576
|
-
console.error(`Info failed: ${err.message}`);
|
|
1577
|
-
}
|
|
1578
|
-
process.stdout.write("> ");
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
try {
|
|
1582
|
-
await svc.sendMessage(
|
|
1583
|
-
JSON.stringify({
|
|
1584
|
-
role: "user",
|
|
1585
|
-
content: { type: "text", text: trimmed }
|
|
1586
|
-
})
|
|
1587
|
-
);
|
|
1588
|
-
} catch (err) {
|
|
1589
|
-
console.error(`Send failed: ${err.message}`);
|
|
1590
|
-
process.stdout.write("> ");
|
|
1591
|
-
}
|
|
1592
|
-
});
|
|
1593
|
-
rl.on("close", async () => {
|
|
1594
|
-
await server.disconnect();
|
|
1595
|
-
process.exit(0);
|
|
1596
|
-
});
|
|
1597
|
-
process.on("SIGINT", async () => {
|
|
1598
|
-
console.log("\nDetaching (session continues running)...");
|
|
1599
|
-
rl.close();
|
|
1600
|
-
await server.disconnect();
|
|
1601
|
-
process.exit(0);
|
|
1602
|
-
});
|
|
1603
|
-
}
|
|
1604
|
-
async function snapshotLatestSeq(machine, fullId) {
|
|
1605
|
-
try {
|
|
1606
|
-
const r = await machine.sessionRPC(fullId, "getMessageCount", {});
|
|
1607
|
-
return Math.max(0, Number(r?.latestSeq ?? 0));
|
|
1608
|
-
} catch {
|
|
1609
|
-
try {
|
|
1610
|
-
const r = await machine.sessionRPC(fullId, "getLatestMessages", { limit: 1 });
|
|
1611
|
-
const msgs = r?.messages || [];
|
|
1612
|
-
return msgs.length > 0 ? Number(msgs[msgs.length - 1].seq || 0) : 0;
|
|
1613
|
-
} catch {
|
|
1614
|
-
return 0;
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
async function collectAssistantResponse(machine, fullId, afterSeq) {
|
|
1619
|
-
const pageLimit = 500;
|
|
1620
|
-
let cursor = afterSeq;
|
|
1621
|
-
const collected = [];
|
|
1622
|
-
const seqs = [];
|
|
1623
|
-
const resultText = [];
|
|
1624
|
-
const resultSeqs = [];
|
|
1625
|
-
for (let i = 0; i < 20; i++) {
|
|
1626
|
-
const { messages, hasMore } = await machine.sessionRPC(
|
|
1627
|
-
fullId,
|
|
1628
|
-
"getMessages",
|
|
1629
|
-
{ afterSeq: cursor, limit: pageLimit }
|
|
1630
|
-
);
|
|
1631
|
-
if (!messages || messages.length === 0) break;
|
|
1632
|
-
for (const msg of messages) {
|
|
1633
|
-
const role = msg?.content?.role;
|
|
1634
|
-
if (role === "agent" || role === "assistant") {
|
|
1635
|
-
const data = msg.content?.content?.data || msg.content?.content;
|
|
1636
|
-
if (!data) continue;
|
|
1637
|
-
let blocks = null;
|
|
1638
|
-
if (data.type === "assistant") {
|
|
1639
|
-
const b = data.message?.content || data.content;
|
|
1640
|
-
blocks = Array.isArray(b) ? b : null;
|
|
1641
|
-
} else if (data.type === "output" && data.data?.type === "assistant") {
|
|
1642
|
-
const b = data.data.message?.content || data.data.content;
|
|
1643
|
-
blocks = Array.isArray(b) ? b : null;
|
|
1644
|
-
}
|
|
1645
|
-
if (blocks) {
|
|
1646
|
-
for (const block of blocks) {
|
|
1647
|
-
if (block.type === "text" && typeof block.text === "string" && block.text) {
|
|
1648
|
-
collected.push(block.text);
|
|
1649
|
-
if (!seqs.includes(msg.seq)) seqs.push(msg.seq);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
} else if (data.type === "result" && typeof data.result === "string" && data.result) {
|
|
1653
|
-
resultText.push(data.result);
|
|
1654
|
-
if (!resultSeqs.includes(msg.seq)) resultSeqs.push(msg.seq);
|
|
1655
|
-
} else if (data.type === "output" && data.data?.type === "result" && typeof data.data.result === "string" && data.data.result) {
|
|
1656
|
-
resultText.push(data.data.result);
|
|
1657
|
-
if (!resultSeqs.includes(msg.seq)) resultSeqs.push(msg.seq);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
cursor = Math.max(cursor, Number(msg.seq || 0));
|
|
1661
|
-
}
|
|
1662
|
-
if (!hasMore) break;
|
|
1663
|
-
}
|
|
1664
|
-
if (collected.length) return { text: collected.join("\n").trim(), messageSeqs: seqs };
|
|
1665
|
-
return { text: resultText.join("\n").trim(), messageSeqs: resultSeqs };
|
|
1666
|
-
}
|
|
1667
|
-
function isSelfBtw(fullId, btw, selfSessionId) {
|
|
1668
|
-
return !!btw && !!selfSessionId && fullId === selfSessionId;
|
|
1669
|
-
}
|
|
1670
|
-
function validateSendOptions(opts) {
|
|
1671
|
-
if (opts?.plain) {
|
|
1672
|
-
const bad = [];
|
|
1673
|
-
if (opts.btw) bad.push("--btw");
|
|
1674
|
-
if (opts.model) bad.push("--model");
|
|
1675
|
-
if (bad.length) return bad.map((f) => `${f} (incompatible with --plain)`);
|
|
1676
|
-
}
|
|
1677
|
-
if (!opts?.btw) return [];
|
|
1678
|
-
const incompatible = [];
|
|
1679
|
-
if (opts.subject) incompatible.push("--subject");
|
|
1680
|
-
if (opts.urgency) incompatible.push("--urgency");
|
|
1681
|
-
if (opts.wait) incompatible.push("--wait");
|
|
1682
|
-
if (opts.response) incompatible.push("--response");
|
|
1683
|
-
if (opts.model) incompatible.push("--model");
|
|
1684
|
-
if (opts.plain) incompatible.push("--plain");
|
|
1685
|
-
return incompatible;
|
|
1686
|
-
}
|
|
1687
|
-
async function sendCore(machine, fullId, message, opts) {
|
|
1688
|
-
if (opts?.btw) {
|
|
1689
|
-
const btwResult = await machine.sessionRPC(fullId, "btw", { question: message });
|
|
1690
|
-
if (!btwResult?.success) {
|
|
1691
|
-
return {
|
|
1692
|
-
sessionId: fullId,
|
|
1693
|
-
mode: "btw",
|
|
1694
|
-
sent: false,
|
|
1695
|
-
waited: false,
|
|
1696
|
-
status: "error",
|
|
1697
|
-
error: btwResult?.error || "btw failed"
|
|
1698
|
-
};
|
|
1699
|
-
}
|
|
1700
|
-
return {
|
|
1701
|
-
sessionId: fullId,
|
|
1702
|
-
mode: "btw",
|
|
1703
|
-
sent: true,
|
|
1704
|
-
waited: true,
|
|
1705
|
-
status: "idle",
|
|
1706
|
-
response: String(btwResult.answer || "")
|
|
1707
|
-
};
|
|
1708
|
-
}
|
|
1709
|
-
if (opts?.asIssue) {
|
|
1710
|
-
const issue = await machine.sessionRPC(fullId, "issue", {
|
|
1711
|
-
op: "add",
|
|
1712
|
-
body: message,
|
|
1713
|
-
session: fullId,
|
|
1714
|
-
rekick: true,
|
|
1715
|
-
...opts.issueTitle ? { title: opts.issueTitle } : {}
|
|
1716
|
-
});
|
|
1717
|
-
return {
|
|
1718
|
-
sessionId: fullId,
|
|
1719
|
-
mode: "send",
|
|
1720
|
-
sent: true,
|
|
1721
|
-
waited: false,
|
|
1722
|
-
status: "sent",
|
|
1723
|
-
messageId: issue?.id,
|
|
1724
|
-
response: `Created issue #${issue?.id}: ${issue?.title || ""}`.trim()
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
|
-
const wantResponse = !!opts?.response;
|
|
1728
|
-
const shouldWait = !!opts?.wait || wantResponse;
|
|
1729
|
-
if (opts?.plain) {
|
|
1730
|
-
const { randomUUID: ruuid } = await import('node:crypto');
|
|
1731
|
-
if (shouldWait && !opts?.requireApproval) {
|
|
1732
|
-
try {
|
|
1733
|
-
await machine.sessionRPC(fullId, "switchMode", { mode: "bypassPermissions" });
|
|
1734
|
-
} catch {
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
const preSeq2 = wantResponse ? await snapshotLatestSeq(machine, fullId) : 0;
|
|
1738
|
-
const meta = { sentFrom: "svamp-cli-plain" };
|
|
1739
|
-
const contentStr = JSON.stringify({ role: "user", content: { type: "text", text: message }, meta });
|
|
1740
|
-
const sendRes = await machine.sessionRPC(fullId, "sendMessage", { content: contentStr, localId: ruuid(), meta });
|
|
1741
|
-
let pWait;
|
|
1742
|
-
if (shouldWait) pWait = await waitForBusyThenIdle(machine, fullId, (opts?.timeout || 300) * 1e3);
|
|
1743
|
-
if (pWait?.pendingPermissions?.length) {
|
|
1744
|
-
return { sessionId: fullId, mode: "send", sent: true, messageId: sendRes?.id, waited: true, status: "permission-pending", pendingPermissions: pWait.pendingPermissions };
|
|
1745
|
-
}
|
|
1746
|
-
let pResp;
|
|
1747
|
-
if (wantResponse) pResp = await collectAssistantResponse(machine, fullId, preSeq2);
|
|
1748
|
-
return {
|
|
1749
|
-
sessionId: fullId,
|
|
1750
|
-
mode: "send",
|
|
1751
|
-
sent: true,
|
|
1752
|
-
messageId: sendRes?.id,
|
|
1753
|
-
waited: !!shouldWait,
|
|
1754
|
-
status: shouldWait ? "idle" : "sent",
|
|
1755
|
-
...wantResponse && pResp ? { response: pResp.text, responseSeqs: pResp.messageSeqs } : {}
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
if (opts?.model) {
|
|
1759
|
-
const { randomUUID: ruuid } = await import('node:crypto');
|
|
1760
|
-
if (shouldWait && !opts?.requireApproval) {
|
|
1761
|
-
try {
|
|
1762
|
-
await machine.sessionRPC(fullId, "switchMode", { mode: "bypassPermissions" });
|
|
1763
|
-
} catch {
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
try {
|
|
1767
|
-
await machine.sessionRPC(fullId, "abort", {});
|
|
1768
|
-
} catch {
|
|
1769
|
-
}
|
|
1770
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
1771
|
-
const meta = { model: opts.model, sentFrom: "svamp-cli" };
|
|
1772
|
-
const contentStr = JSON.stringify({ role: "user", content: { type: "text", text: message }, meta });
|
|
1773
|
-
const sendRes = await machine.sessionRPC(fullId, "sendMessage", {
|
|
1774
|
-
content: contentStr,
|
|
1775
|
-
localId: ruuid(),
|
|
1776
|
-
meta
|
|
1777
|
-
});
|
|
1778
|
-
let mWait;
|
|
1779
|
-
if (shouldWait) {
|
|
1780
|
-
mWait = await waitForBusyThenIdle(machine, fullId, (opts?.timeout || 300) * 1e3);
|
|
1781
|
-
}
|
|
1782
|
-
if (mWait?.pendingPermissions?.length) {
|
|
1783
|
-
return {
|
|
1784
|
-
sessionId: fullId,
|
|
1785
|
-
mode: "send",
|
|
1786
|
-
sent: true,
|
|
1787
|
-
messageId: sendRes?.id,
|
|
1788
|
-
waited: true,
|
|
1789
|
-
status: "permission-pending",
|
|
1790
|
-
pendingPermissions: mWait.pendingPermissions
|
|
1791
|
-
};
|
|
1792
|
-
}
|
|
1793
|
-
let mResponse;
|
|
1794
|
-
if (wantResponse) {
|
|
1795
|
-
mResponse = await collectAssistantResponse(machine, fullId, 0);
|
|
1796
|
-
}
|
|
1797
|
-
return {
|
|
1798
|
-
sessionId: fullId,
|
|
1799
|
-
mode: "send",
|
|
1800
|
-
sent: true,
|
|
1801
|
-
messageId: sendRes?.id,
|
|
1802
|
-
waited: shouldWait,
|
|
1803
|
-
status: shouldWait ? "idle" : "sent",
|
|
1804
|
-
...wantResponse && mResponse ? { response: mResponse.text, responseSeqs: mResponse.messageSeqs } : {}
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1807
|
-
let bypassEnsured = false;
|
|
1808
|
-
if (shouldWait && !opts?.requireApproval) {
|
|
1809
|
-
try {
|
|
1810
|
-
await machine.sessionRPC(fullId, "switchMode", { mode: "bypassPermissions" });
|
|
1811
|
-
bypassEnsured = true;
|
|
1812
|
-
} catch {
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
const preSeq = wantResponse ? await snapshotLatestSeq(machine, fullId) : 0;
|
|
1816
|
-
const callerSessionId = process.env.SVAMP_SESSION_ID;
|
|
1817
|
-
const hop = computeOutboundHop(callerSessionId);
|
|
1818
|
-
let urgency = opts?.urgency ? opts.urgency : shouldWait ? "urgent" : "normal";
|
|
1819
|
-
if (hop.fromInboxTurn) urgency = "normal";
|
|
1820
|
-
const inboxMessage = {
|
|
1821
|
-
messageId: shortId(),
|
|
1822
|
-
body: message,
|
|
1823
|
-
timestamp: Date.now(),
|
|
1824
|
-
read: false,
|
|
1825
|
-
// Prefer the sender session's friendly handle (project:name) so the receiver —
|
|
1826
|
-
// and a human watching the exchange — sees who's talking, not an opaque id.
|
|
1827
|
-
// `fromSession` still carries the routable id for reply delivery.
|
|
1828
|
-
from: callerSessionId ? process.env.SVAMP_SESSION_HANDLE || `agent:${callerSessionId}` : `cli:${os.userInfo().username}`,
|
|
1829
|
-
...callerSessionId ? { fromSession: callerSessionId } : {},
|
|
1830
|
-
to: fullId,
|
|
1831
|
-
subject: opts?.subject,
|
|
1832
|
-
urgency,
|
|
1833
|
-
hopCount: hop.hopCount,
|
|
1834
|
-
...hop.threadId ? { threadId: hop.threadId } : {}
|
|
1835
|
-
};
|
|
1836
|
-
const result = await machine.sessionRPC(fullId, "sendInboxMessage", { message: inboxMessage });
|
|
1837
|
-
if (callerSessionId) {
|
|
1838
|
-
try {
|
|
1839
|
-
registerAwaitingReply(callerSessionId, hop.threadId || inboxMessage.messageId);
|
|
1840
|
-
} catch {
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
let waitResult;
|
|
1844
|
-
if (shouldWait) {
|
|
1845
|
-
const timeoutMs = (opts?.timeout || 300) * 1e3;
|
|
1846
|
-
waitResult = await waitForBusyThenIdle(machine, fullId, timeoutMs);
|
|
1847
|
-
}
|
|
1848
|
-
if (waitResult?.pendingPermissions?.length) {
|
|
1849
|
-
return {
|
|
1850
|
-
sessionId: fullId,
|
|
1851
|
-
mode: "send",
|
|
1852
|
-
sent: true,
|
|
1853
|
-
messageId: result.messageId,
|
|
1854
|
-
waited: true,
|
|
1855
|
-
status: "permission-pending",
|
|
1856
|
-
pendingPermissions: waitResult.pendingPermissions,
|
|
1857
|
-
bypassEnsured
|
|
1858
|
-
};
|
|
1859
|
-
}
|
|
1860
|
-
let response;
|
|
1861
|
-
if (wantResponse) {
|
|
1862
|
-
response = await collectAssistantResponse(machine, fullId, preSeq);
|
|
1863
|
-
}
|
|
1864
|
-
return {
|
|
1865
|
-
sessionId: fullId,
|
|
1866
|
-
mode: "send",
|
|
1867
|
-
sent: true,
|
|
1868
|
-
messageId: result.messageId,
|
|
1869
|
-
waited: shouldWait,
|
|
1870
|
-
status: shouldWait ? "idle" : "sent",
|
|
1871
|
-
bypassEnsured,
|
|
1872
|
-
...wantResponse && response ? { response: response.text, responseSeqs: response.messageSeqs } : {}
|
|
1873
|
-
};
|
|
1874
|
-
}
|
|
1875
|
-
async function sessionSend(sessionId, message, machineId, opts) {
|
|
1876
|
-
const incompatible = validateSendOptions(opts);
|
|
1877
|
-
if (incompatible.length) {
|
|
1878
|
-
console.error(`Error: --btw is incompatible with: ${incompatible.join(", ")}`);
|
|
1879
|
-
console.error(" --btw runs as a one-shot forked query that returns the answer directly.");
|
|
1880
|
-
process.exit(1);
|
|
1881
|
-
}
|
|
1882
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
1883
|
-
if (isSelfBtw(fullId, opts?.btw, process.env.SVAMP_SESSION_ID)) {
|
|
1884
|
-
console.error("Refusing to --btw your own session: a /btw forks a query that resumes this session, which would deadlock the active turn (and the watchdog would kill it). You are already inside this session \u2014 just answer/think directly, or send to a DIFFERENT session.");
|
|
1885
|
-
try {
|
|
1886
|
-
await server.disconnect();
|
|
1887
|
-
} catch {
|
|
1888
|
-
}
|
|
1889
|
-
process.exit(1);
|
|
1890
|
-
}
|
|
1891
|
-
try {
|
|
1892
|
-
const r = await sendCore(machine, fullId, message, opts);
|
|
1893
|
-
if (r.mode === "btw") {
|
|
1894
|
-
if (r.status === "error") {
|
|
1895
|
-
if (opts?.json) {
|
|
1896
|
-
console.log(formatJson({
|
|
1897
|
-
sessionId: r.sessionId,
|
|
1898
|
-
mode: "btw",
|
|
1899
|
-
message,
|
|
1900
|
-
success: false,
|
|
1901
|
-
error: r.error
|
|
1902
|
-
}));
|
|
1903
|
-
} else {
|
|
1904
|
-
console.error(`btw failed: ${r.error || "unknown error"}`);
|
|
1905
|
-
}
|
|
1906
|
-
process.exitCode = 1;
|
|
1907
|
-
return;
|
|
1908
|
-
}
|
|
1909
|
-
if (opts?.json) {
|
|
1910
|
-
console.log(formatJson({
|
|
1911
|
-
sessionId: r.sessionId,
|
|
1912
|
-
mode: "btw",
|
|
1913
|
-
message,
|
|
1914
|
-
success: true,
|
|
1915
|
-
response: r.response || ""
|
|
1916
|
-
}));
|
|
1917
|
-
} else if (r.response) {
|
|
1918
|
-
console.log(r.response);
|
|
1919
|
-
}
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
if (r.status === "permission-pending") {
|
|
1923
|
-
if (opts?.json) {
|
|
1924
|
-
console.log(formatJson({
|
|
1925
|
-
sessionId: r.sessionId,
|
|
1926
|
-
message,
|
|
1927
|
-
sent: true,
|
|
1928
|
-
messageId: r.messageId,
|
|
1929
|
-
status: "permission-pending",
|
|
1930
|
-
pendingPermissions: r.pendingPermissions
|
|
1931
|
-
}));
|
|
1932
|
-
} else {
|
|
1933
|
-
console.log(`Message sent to session ${r.sessionId.slice(0, 8)} (id: ${(r.messageId || "").slice(0, 8)})`);
|
|
1934
|
-
console.log("Agent is waiting for permission approval:");
|
|
1935
|
-
for (const p of r.pendingPermissions || []) {
|
|
1936
|
-
const argsStr = JSON.stringify(p.arguments || {}).slice(0, 120);
|
|
1937
|
-
console.log(` [${p.id.slice(0, 8)}] ${p.tool}(${argsStr})`);
|
|
1938
|
-
}
|
|
1939
|
-
console.log(`
|
|
1940
|
-
Use: svamp session approve ${r.sessionId.slice(0, 8)}`);
|
|
1941
|
-
}
|
|
1942
|
-
process.exitCode = 2;
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
if (opts?.json) {
|
|
1946
|
-
console.log(formatJson({
|
|
1947
|
-
sessionId: r.sessionId,
|
|
1948
|
-
message,
|
|
1949
|
-
sent: true,
|
|
1950
|
-
messageId: r.messageId,
|
|
1951
|
-
waited: r.waited,
|
|
1952
|
-
status: r.status,
|
|
1953
|
-
...r.bypassEnsured ? { bypassEnsured: true } : {},
|
|
1954
|
-
...opts.response ? { response: r.response || "", responseSeqs: r.responseSeqs || [] } : {}
|
|
1955
|
-
}));
|
|
1956
|
-
} else {
|
|
1957
|
-
console.log(`Message sent to session ${r.sessionId.slice(0, 8)} (id: ${(r.messageId || "").slice(0, 8)})`);
|
|
1958
|
-
if (r.waited) {
|
|
1959
|
-
console.log("Agent is idle.");
|
|
1960
|
-
}
|
|
1961
|
-
if (opts?.response) {
|
|
1962
|
-
if (r.response) {
|
|
1963
|
-
console.log("");
|
|
1964
|
-
console.log(r.response);
|
|
1965
|
-
} else {
|
|
1966
|
-
console.log("(no text response captured)");
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
} finally {
|
|
1971
|
-
await server.disconnect();
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
async function queryCore(machine, directory, prompt, opts) {
|
|
1975
|
-
const absDir = resolve(directory);
|
|
1976
|
-
const spawnOpts = {
|
|
1977
|
-
directory: absDir,
|
|
1978
|
-
agent: "claude",
|
|
1979
|
-
permissionMode: opts?.permissionMode || "bypassPermissions",
|
|
1980
|
-
tags: opts?.tag ? [opts.tag] : ["svamp-query"]
|
|
1981
|
-
};
|
|
1982
|
-
const spawn = await machine.spawnSession(spawnOpts);
|
|
1983
|
-
if (spawn.type !== "success" || !spawn.sessionId) {
|
|
1984
|
-
return {
|
|
1985
|
-
sessionId: "",
|
|
1986
|
-
mode: "query",
|
|
1987
|
-
sent: false,
|
|
1988
|
-
waited: false,
|
|
1989
|
-
status: "error",
|
|
1990
|
-
error: spawn.errorMessage || `spawn returned type=${spawn.type}`,
|
|
1991
|
-
directory: absDir
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
|
-
const spawnedId = spawn.sessionId;
|
|
1995
|
-
const svc = getSessionProxy(machine, spawnedId);
|
|
1996
|
-
await svc.sendMessage(
|
|
1997
|
-
JSON.stringify({
|
|
1998
|
-
role: "user",
|
|
1999
|
-
content: { type: "text", text: prompt },
|
|
2000
|
-
meta: { sentFrom: "svamp-cli-query" }
|
|
2001
|
-
})
|
|
2002
|
-
);
|
|
2003
|
-
const timeoutMs = (opts?.timeout || 300) * 1e3;
|
|
2004
|
-
const waitResult = await waitForBusyThenIdle(machine, spawnedId, timeoutMs);
|
|
2005
|
-
if (waitResult.pendingPermissions?.length) {
|
|
2006
|
-
return {
|
|
2007
|
-
sessionId: spawnedId,
|
|
2008
|
-
mode: "query",
|
|
2009
|
-
sent: true,
|
|
2010
|
-
waited: true,
|
|
2011
|
-
status: "permission-pending",
|
|
2012
|
-
pendingPermissions: waitResult.pendingPermissions,
|
|
2013
|
-
error: `Agent paused for tool approval (${waitResult.pendingPermissions.length} pending). Pass --permission-mode bypassPermissions or approve the request manually.`,
|
|
2014
|
-
directory: absDir
|
|
2015
|
-
};
|
|
2016
|
-
}
|
|
2017
|
-
const response = await collectAssistantResponse(machine, spawnedId, 0);
|
|
2018
|
-
return {
|
|
2019
|
-
sessionId: spawnedId,
|
|
2020
|
-
mode: "query",
|
|
2021
|
-
sent: true,
|
|
2022
|
-
waited: true,
|
|
2023
|
-
status: "idle",
|
|
2024
|
-
response: response.text,
|
|
2025
|
-
responseSeqs: response.messageSeqs,
|
|
2026
|
-
directory: absDir
|
|
2027
|
-
};
|
|
2028
|
-
}
|
|
2029
|
-
async function sessionQuery(directory, prompt, machineId, opts) {
|
|
2030
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2031
|
-
let spawnedId;
|
|
2032
|
-
try {
|
|
2033
|
-
const r = await queryCore(machine, directory, prompt, opts);
|
|
2034
|
-
spawnedId = r.sessionId || void 0;
|
|
2035
|
-
if (r.status === "error") {
|
|
2036
|
-
if (opts?.json) {
|
|
2037
|
-
console.log(formatJson({
|
|
2038
|
-
success: false,
|
|
2039
|
-
error: r.error,
|
|
2040
|
-
directory: r.directory
|
|
2041
|
-
}));
|
|
2042
|
-
} else {
|
|
2043
|
-
console.error(`Failed to spawn query session: ${r.error}`);
|
|
2044
|
-
}
|
|
2045
|
-
process.exitCode = 1;
|
|
2046
|
-
return;
|
|
2047
|
-
}
|
|
2048
|
-
if (r.status === "permission-pending") {
|
|
2049
|
-
if (opts?.json) {
|
|
2050
|
-
console.log(formatJson({
|
|
2051
|
-
success: false,
|
|
2052
|
-
sessionId: r.sessionId,
|
|
2053
|
-
directory: r.directory,
|
|
2054
|
-
error: r.error,
|
|
2055
|
-
pendingPermissions: r.pendingPermissions
|
|
2056
|
-
}));
|
|
2057
|
-
} else {
|
|
2058
|
-
console.error(r.error);
|
|
2059
|
-
}
|
|
2060
|
-
process.exitCode = 2;
|
|
2061
|
-
return;
|
|
2062
|
-
}
|
|
2063
|
-
if (opts?.json) {
|
|
2064
|
-
console.log(formatJson({
|
|
2065
|
-
success: true,
|
|
2066
|
-
sessionId: r.sessionId,
|
|
2067
|
-
directory: r.directory,
|
|
2068
|
-
prompt,
|
|
2069
|
-
response: r.response || "",
|
|
2070
|
-
responseSeqs: r.responseSeqs || []
|
|
2071
|
-
}));
|
|
2072
|
-
} else {
|
|
2073
|
-
if (r.response) {
|
|
2074
|
-
console.log(r.response);
|
|
2075
|
-
} else {
|
|
2076
|
-
console.log("(no text response captured)");
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
} finally {
|
|
2080
|
-
if (spawnedId && !opts?.keep) {
|
|
2081
|
-
try {
|
|
2082
|
-
await machine.deleteSession(spawnedId);
|
|
2083
|
-
} catch (err) {
|
|
2084
|
-
console.error(`Warning: failed to delete query session ${spawnedId}: ${err?.message || err}`);
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
await server.disconnect();
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
async function sessionWait(sessionId, machineId, opts) {
|
|
2091
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionId, machineId);
|
|
2092
|
-
try {
|
|
2093
|
-
const timeoutMs = (opts?.timeout || 300) * 1e3;
|
|
2094
|
-
const result = await waitForIdle(machine, fullId, timeoutMs);
|
|
2095
|
-
if (result.pendingPermissions?.length) {
|
|
2096
|
-
if (opts?.json) {
|
|
2097
|
-
console.log(formatJson({
|
|
2098
|
-
sessionId: fullId,
|
|
2099
|
-
status: "permission-pending",
|
|
2100
|
-
pendingPermissions: result.pendingPermissions
|
|
2101
|
-
}));
|
|
2102
|
-
} else {
|
|
2103
|
-
console.log(`Session ${fullId.slice(0, 8)} is waiting for permission approval:`);
|
|
2104
|
-
for (const p of result.pendingPermissions) {
|
|
2105
|
-
const argsStr = JSON.stringify(p.arguments || {}).slice(0, 120);
|
|
2106
|
-
console.log(` [${p.id.slice(0, 8)}] ${p.tool}(${argsStr})`);
|
|
2107
|
-
}
|
|
2108
|
-
console.log(`
|
|
2109
|
-
Use: svamp session approve ${fullId.slice(0, 8)}`);
|
|
2110
|
-
}
|
|
2111
|
-
process.exitCode = 2;
|
|
2112
|
-
} else {
|
|
2113
|
-
if (opts?.json) {
|
|
2114
|
-
console.log(formatJson({ sessionId: fullId, status: "idle" }));
|
|
2115
|
-
} else {
|
|
2116
|
-
console.log(`Session ${fullId.slice(0, 8)} is idle.`);
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
} catch (err) {
|
|
2120
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2121
|
-
console.error(msg);
|
|
2122
|
-
process.exitCode = 1;
|
|
2123
|
-
} finally {
|
|
2124
|
-
await server.disconnect();
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
async function sessionShare(sessionIdPartial, machineId, opts) {
|
|
2128
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
|
|
2129
|
-
try {
|
|
2130
|
-
const svc = getSessionProxy(machine, fullId);
|
|
2131
|
-
if (opts.list) {
|
|
2132
|
-
const metaResult = await svc.getMetadata();
|
|
2133
|
-
const sharing = metaResult.metadata?.sharing;
|
|
2134
|
-
if (!sharing || !sharing.enabled) {
|
|
2135
|
-
console.log("Sharing is not enabled for this session.");
|
|
2136
|
-
return;
|
|
2137
|
-
}
|
|
2138
|
-
console.log(`Owner: ${sharing.owner}`);
|
|
2139
|
-
if (sharing.publicAccess) {
|
|
2140
|
-
console.log(`Public access: ${sharing.publicAccess} (anyone with the link)`);
|
|
2141
|
-
}
|
|
2142
|
-
if (sharing.allowedUsers.length === 0) {
|
|
2143
|
-
console.log("No shared users.");
|
|
2144
|
-
} else {
|
|
2145
|
-
console.log("Shared users (each has full access):");
|
|
2146
|
-
for (const u of sharing.allowedUsers) {
|
|
2147
|
-
console.log(` ${u.email.padEnd(30)} (added ${new Date(u.addedAt).toISOString().slice(0, 10)})`);
|
|
2148
|
-
}
|
|
2149
|
-
}
|
|
2150
|
-
const secCtx = metaResult.metadata?.securityContext;
|
|
2151
|
-
if (secCtx) {
|
|
2152
|
-
console.log(`
|
|
2153
|
-
Security context:`);
|
|
2154
|
-
console.log(JSON.stringify(secCtx, null, 2));
|
|
2155
|
-
}
|
|
2156
|
-
console.log(`
|
|
2157
|
-
Share link: ${buildSessionShareUrl({
|
|
2158
|
-
sessionId: fullId,
|
|
2159
|
-
workspace: server.config?.workspace,
|
|
2160
|
-
machineServiceId: machine.id || `${server.config?.workspace}/${server.config?.client_id}:default`
|
|
2161
|
-
})}`);
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
if (opts.add) {
|
|
2165
|
-
const { email } = parseShareArg(opts.add);
|
|
2166
|
-
const metaResult = await svc.getMetadata();
|
|
2167
|
-
let sharing = metaResult.metadata?.sharing || {
|
|
2168
|
-
enabled: true,
|
|
2169
|
-
owner: "",
|
|
2170
|
-
allowedUsers: []
|
|
2171
|
-
};
|
|
2172
|
-
sharing.enabled = true;
|
|
2173
|
-
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
2174
|
-
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
2175
|
-
);
|
|
2176
|
-
sharing.allowedUsers.push(normalizeAllowedUser({ email, addedBy: "cli" }, "cli"));
|
|
2177
|
-
await svc.updateSharing(sharing);
|
|
2178
|
-
const shareUrl = buildSessionShareUrl({
|
|
2179
|
-
sessionId: fullId,
|
|
2180
|
-
workspace: server.config?.workspace,
|
|
2181
|
-
machineServiceId: machine.id || `${server.config?.workspace}/${server.config?.client_id}:default`
|
|
2182
|
-
});
|
|
2183
|
-
console.log(`Shared session ${fullId.slice(0, 8)} with ${email} (full access).`);
|
|
2184
|
-
console.log(`Share link: ${shareUrl}`);
|
|
2185
|
-
return;
|
|
2186
|
-
}
|
|
2187
|
-
if (opts.remove) {
|
|
2188
|
-
const email = opts.remove;
|
|
2189
|
-
const metaResult = await svc.getMetadata();
|
|
2190
|
-
const sharing = metaResult.metadata?.sharing;
|
|
2191
|
-
if (!sharing) {
|
|
2192
|
-
console.error("Sharing is not enabled for this session.");
|
|
2193
|
-
process.exit(1);
|
|
2194
|
-
}
|
|
2195
|
-
const before = sharing.allowedUsers.length;
|
|
2196
|
-
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
2197
|
-
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
2198
|
-
);
|
|
2199
|
-
if (sharing.allowedUsers.length === before) {
|
|
2200
|
-
console.error(`User ${email} is not in the shared users list.`);
|
|
2201
|
-
process.exit(1);
|
|
2202
|
-
}
|
|
2203
|
-
await svc.updateSharing(sharing);
|
|
2204
|
-
console.log(`Removed ${email} from session ${fullId.slice(0, 8)}.`);
|
|
2205
|
-
return;
|
|
2206
|
-
}
|
|
2207
|
-
if (opts.public !== void 0) {
|
|
2208
|
-
const value = opts.public.toLowerCase();
|
|
2209
|
-
if (value === "off" || value === "none" || value === "no") {
|
|
2210
|
-
const metaResult = await svc.getMetadata();
|
|
2211
|
-
const sharing = metaResult.metadata?.sharing || {
|
|
2212
|
-
enabled: true,
|
|
2213
|
-
owner: "",
|
|
2214
|
-
allowedUsers: []
|
|
2215
|
-
};
|
|
2216
|
-
sharing.publicAccess = null;
|
|
2217
|
-
await svc.updateSharing(sharing);
|
|
2218
|
-
console.log(`Public access disabled for session ${fullId.slice(0, 8)}.`);
|
|
2219
|
-
} else if (value === "view" || value === "interact") {
|
|
2220
|
-
const metaResult = await svc.getMetadata();
|
|
2221
|
-
const sharing = metaResult.metadata?.sharing || {
|
|
2222
|
-
enabled: true,
|
|
2223
|
-
owner: "",
|
|
2224
|
-
allowedUsers: []
|
|
2225
|
-
};
|
|
2226
|
-
sharing.enabled = true;
|
|
2227
|
-
sharing.publicAccess = value;
|
|
2228
|
-
await svc.updateSharing(sharing);
|
|
2229
|
-
console.log(`Public access set to '${value}' for session ${fullId.slice(0, 8)}.`);
|
|
2230
|
-
} else {
|
|
2231
|
-
console.error("Invalid --public value. Use: view, interact, or off");
|
|
2232
|
-
process.exit(1);
|
|
2233
|
-
}
|
|
2234
|
-
return;
|
|
2235
|
-
}
|
|
2236
|
-
console.error("Usage: svamp session share <id> --add <email>[:<role>] | --remove <email> | --list | --public <view|interact|off>");
|
|
2237
|
-
process.exit(1);
|
|
2238
|
-
} finally {
|
|
2239
|
-
await server.disconnect();
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2242
|
-
async function machineShare(machineId, opts) {
|
|
2243
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2244
|
-
try {
|
|
2245
|
-
if (opts.list) {
|
|
2246
|
-
const info = await machine.getMachineInfo();
|
|
2247
|
-
const sharing = info.metadata?.sharing;
|
|
2248
|
-
if (!sharing || !sharing.enabled) {
|
|
2249
|
-
console.log("Sharing is not enabled for this machine.");
|
|
2250
|
-
} else {
|
|
2251
|
-
console.log(`Owner: ${sharing.owner}`);
|
|
2252
|
-
if (sharing.allowedUsers.length === 0) {
|
|
2253
|
-
console.log("No shared users.");
|
|
2254
|
-
} else {
|
|
2255
|
-
console.log("Shared users (each has full machine access):");
|
|
2256
|
-
for (const u of sharing.allowedUsers) {
|
|
2257
|
-
console.log(` ${u.email.padEnd(30)} (added ${new Date(u.addedAt).toISOString().slice(0, 10)})`);
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
console.log(`
|
|
2261
|
-
Share link: ${buildMachineShareUrl({
|
|
2262
|
-
machineId: info.machineId,
|
|
2263
|
-
workspace: server.config?.workspace
|
|
2264
|
-
})}`);
|
|
2265
|
-
}
|
|
2266
|
-
const iso = info.metadata?.isolationCapabilities;
|
|
2267
|
-
if (iso) {
|
|
2268
|
-
console.log(`
|
|
2269
|
-
Isolation: ${iso.available.length > 0 ? iso.available.join(", ") : "none available"} (preferred: ${iso.preferred || "none"})`);
|
|
2270
|
-
}
|
|
2271
|
-
return;
|
|
2272
|
-
}
|
|
2273
|
-
if (opts.showConfig) {
|
|
2274
|
-
const result = await machine.getSecurityContextConfig();
|
|
2275
|
-
const config = result?.securityContextConfig;
|
|
2276
|
-
if (!config) {
|
|
2277
|
-
console.log("No security context config set for this machine.");
|
|
2278
|
-
} else {
|
|
2279
|
-
console.log(JSON.stringify(config, null, 2));
|
|
2280
|
-
}
|
|
2281
|
-
return;
|
|
2282
|
-
}
|
|
2283
|
-
if (opts.configPath) {
|
|
2284
|
-
const configPath = resolve(opts.configPath);
|
|
2285
|
-
const config = loadSecurityContextConfig(configPath);
|
|
2286
|
-
await machine.updateSecurityContextConfig(config);
|
|
2287
|
-
const userCount = config.users ? Object.keys(config.users).length : 0;
|
|
2288
|
-
console.log(`Security context config applied to machine.`);
|
|
2289
|
-
console.log(` Default context: ${config.default ? "yes" : "none"}`);
|
|
2290
|
-
console.log(` User-specific entries: ${userCount}`);
|
|
2291
|
-
if (config.users) {
|
|
2292
|
-
for (const email of Object.keys(config.users)) {
|
|
2293
|
-
const ctx = config.users[email];
|
|
2294
|
-
console.log(` ${email}: role=${ctx.role || "default"}`);
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
return;
|
|
2298
|
-
}
|
|
2299
|
-
if (opts.add) {
|
|
2300
|
-
const { email } = parseShareArg(opts.add);
|
|
2301
|
-
const info = await machine.getMachineInfo();
|
|
2302
|
-
let sharing = info.metadata?.sharing || {
|
|
2303
|
-
enabled: true,
|
|
2304
|
-
owner: "",
|
|
2305
|
-
allowedUsers: []
|
|
2306
|
-
};
|
|
2307
|
-
sharing.enabled = true;
|
|
2308
|
-
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
2309
|
-
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
2310
|
-
);
|
|
2311
|
-
sharing.allowedUsers.push(normalizeAllowedUser({ email, addedBy: "cli" }, "cli"));
|
|
2312
|
-
await machine.updateSharing(sharing);
|
|
2313
|
-
const shareUrl = buildMachineShareUrl({
|
|
2314
|
-
machineId: info.machineId,
|
|
2315
|
-
workspace: server.config?.workspace
|
|
2316
|
-
});
|
|
2317
|
-
console.log(`Shared machine with ${email} (full access).`);
|
|
2318
|
-
console.log(`Share link: ${shareUrl}`);
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
if (opts.remove) {
|
|
2322
|
-
const email = opts.remove;
|
|
2323
|
-
const info = await machine.getMachineInfo();
|
|
2324
|
-
const sharing = info.metadata?.sharing;
|
|
2325
|
-
if (!sharing) {
|
|
2326
|
-
console.error("Sharing is not enabled for this machine.");
|
|
2327
|
-
process.exit(1);
|
|
2328
|
-
}
|
|
2329
|
-
const before = sharing.allowedUsers.length;
|
|
2330
|
-
sharing.allowedUsers = sharing.allowedUsers.filter(
|
|
2331
|
-
(u) => u.email.toLowerCase() !== email.toLowerCase()
|
|
2332
|
-
);
|
|
2333
|
-
if (sharing.allowedUsers.length === before) {
|
|
2334
|
-
console.error(`User ${email} is not in the shared users list.`);
|
|
2335
|
-
process.exit(1);
|
|
2336
|
-
}
|
|
2337
|
-
await machine.updateSharing(sharing);
|
|
2338
|
-
console.log(`Removed ${email} from machine sharing.`);
|
|
2339
|
-
return;
|
|
2340
|
-
}
|
|
2341
|
-
console.error("Usage: svamp machine share --add <email>[:<role>] | --remove <email> | --list | --config <path> | --show-config");
|
|
2342
|
-
process.exit(1);
|
|
2343
|
-
} finally {
|
|
2344
|
-
await server.disconnect();
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
async function machineExec(machineId, command, cwd) {
|
|
2348
|
-
if (!command) {
|
|
2349
|
-
console.error("Usage: svamp machine exec <command> [--cwd <path>]");
|
|
2350
|
-
process.exit(1);
|
|
2351
|
-
}
|
|
2352
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2353
|
-
try {
|
|
2354
|
-
const result = await machine.bash(command, cwd || void 0);
|
|
2355
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
2356
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
2357
|
-
process.exit(result.exitCode);
|
|
2358
|
-
} finally {
|
|
2359
|
-
await server.disconnect();
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
async function wiseAskCli(machineId, message, sessionId, opts) {
|
|
2363
|
-
if (!message) {
|
|
2364
|
-
console.error('Usage: svamp wise ask "<message>" [--session <id>] [-m <machine>]');
|
|
2365
|
-
process.exit(1);
|
|
2366
|
-
}
|
|
2367
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2368
|
-
try {
|
|
2369
|
-
const res = await machine.wiseAsk({ message, sessionId });
|
|
2370
|
-
if (opts?.json) {
|
|
2371
|
-
console.log(JSON.stringify(res, null, 2));
|
|
2372
|
-
process.exit(res?.success ? 0 : 1);
|
|
2373
|
-
}
|
|
2374
|
-
if (res?.error) {
|
|
2375
|
-
console.error(`\u2717 ${res.error}`);
|
|
2376
|
-
process.exit(1);
|
|
2377
|
-
}
|
|
2378
|
-
console.log(res?.reply || `(${res?.status || "no reply"})`);
|
|
2379
|
-
process.exit(0);
|
|
2380
|
-
} finally {
|
|
2381
|
-
await server.disconnect();
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
async function wiseJoinMeetingCli(machineId, url, sessionId, opts) {
|
|
2385
|
-
if (!url) {
|
|
2386
|
-
console.error('Usage: svamp wise join-meeting <zoom|meet|teams url> [--session <id>|--global] [--mode listen|text|voice] [--mission "<prompt>"] [-m <machine>]');
|
|
2387
|
-
process.exit(1);
|
|
2388
|
-
}
|
|
2389
|
-
const mode = opts?.mode ? String(opts.mode).toLowerCase() : void 0;
|
|
2390
|
-
if (mode && !["listen", "text", "voice"].includes(mode)) {
|
|
2391
|
-
console.error(`\u2717 invalid --mode "${mode}" (use listen | text | voice)`);
|
|
2392
|
-
process.exit(1);
|
|
2393
|
-
}
|
|
2394
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2395
|
-
try {
|
|
2396
|
-
const res = await machine.wiseJoinMeeting({ meetingUrl: url, sessionId, mode, mission: opts?.mission });
|
|
2397
|
-
if (opts?.json) {
|
|
2398
|
-
console.log(JSON.stringify(res, null, 2));
|
|
2399
|
-
process.exit(res?.success ? 0 : 1);
|
|
2400
|
-
}
|
|
2401
|
-
if (res?.error) {
|
|
2402
|
-
console.error(`\u2717 ${res.error}`);
|
|
2403
|
-
process.exit(1);
|
|
2404
|
-
}
|
|
2405
|
-
console.log(`\u2713 WISE joining ${res.platform} meeting ${res.nativeId} (${res.target}, ${res.mode || "voice"} mode)${res.meetingId ? ` \u2014 meeting #${res.meetingId}` : ""}`);
|
|
2406
|
-
process.exit(0);
|
|
2407
|
-
} finally {
|
|
2408
|
-
await server.disconnect();
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
async function wiseLeaveMeetingCli(machineId, sessionId, opts) {
|
|
2412
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2413
|
-
try {
|
|
2414
|
-
const res = await machine.wiseLeaveMeeting({ sessionId });
|
|
2415
|
-
if (opts?.json) {
|
|
2416
|
-
console.log(JSON.stringify(res, null, 2));
|
|
2417
|
-
process.exit(res?.success ? 0 : 1);
|
|
2418
|
-
}
|
|
2419
|
-
if (res?.error) {
|
|
2420
|
-
console.error(`\u2717 ${res.error}`);
|
|
2421
|
-
process.exit(1);
|
|
2422
|
-
}
|
|
2423
|
-
console.log("\u2713 WISE left the meeting");
|
|
2424
|
-
process.exit(0);
|
|
2425
|
-
} finally {
|
|
2426
|
-
await server.disconnect();
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
async function wiseAnnounceCli(machineId, text, sessionId, opts) {
|
|
2430
|
-
if (!text) {
|
|
2431
|
-
console.error('Usage: svamp wise announce "<text>" [--session <id>] [-m <machine>]');
|
|
2432
|
-
process.exit(1);
|
|
2433
|
-
}
|
|
2434
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2435
|
-
try {
|
|
2436
|
-
const res = await machine.wiseAnnounce({ text, sessionId });
|
|
2437
|
-
if (opts?.json) {
|
|
2438
|
-
console.log(JSON.stringify(res, null, 2));
|
|
2439
|
-
process.exit(res?.success ? 0 : 1);
|
|
2440
|
-
}
|
|
2441
|
-
if (res?.error) {
|
|
2442
|
-
console.error(`\u2717 ${res.error}`);
|
|
2443
|
-
process.exit(1);
|
|
2444
|
-
}
|
|
2445
|
-
console.log("\u2713 Announced to the meeting");
|
|
2446
|
-
process.exit(0);
|
|
2447
|
-
} finally {
|
|
2448
|
-
await server.disconnect();
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
async function wiseMeetingsCli(machineId, opts) {
|
|
2452
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2453
|
-
try {
|
|
2454
|
-
const res = await machine.wiseMeetingStatus({});
|
|
2455
|
-
if (opts?.json) {
|
|
2456
|
-
console.log(JSON.stringify(res, null, 2));
|
|
2457
|
-
process.exit(0);
|
|
2458
|
-
}
|
|
2459
|
-
const ms = res?.meetings || [];
|
|
2460
|
-
if (!ms.length) console.log("No active meeting agents.");
|
|
2461
|
-
else for (const m of ms) console.log(`\u2022 ${m.platform} ${m.nativeId} \u2014 ${m.target}${m.meetingId ? ` (#${m.meetingId})` : ""}`);
|
|
2462
|
-
process.exit(0);
|
|
2463
|
-
} finally {
|
|
2464
|
-
await server.disconnect();
|
|
2465
|
-
}
|
|
2466
|
-
}
|
|
2467
|
-
async function machineInfo(machineId) {
|
|
2468
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2469
|
-
try {
|
|
2470
|
-
const info = await machine.getMachineInfo();
|
|
2471
|
-
const meta = info.metadata || {};
|
|
2472
|
-
const ds = info.daemonState || {};
|
|
2473
|
-
console.log(`Machine: ${info.machineId}`);
|
|
2474
|
-
if (meta.hostname) console.log(`Host: ${meta.hostname}`);
|
|
2475
|
-
if (meta.platform) console.log(`OS: ${meta.platform} ${meta.arch || ""}`);
|
|
2476
|
-
console.log(`Status: ${ds.status || "unknown"}`);
|
|
2477
|
-
const iso = meta.isolationCapabilities;
|
|
2478
|
-
if (iso) {
|
|
2479
|
-
console.log(`Isolation: ${iso.available?.length > 0 ? iso.available.join(", ") : "none"} (preferred: ${iso.preferred || "none"})`);
|
|
2480
|
-
}
|
|
2481
|
-
const sessions = await machine.listSessions();
|
|
2482
|
-
console.log(`Sessions: ${Array.isArray(sessions) ? sessions.length : 0} active`);
|
|
2483
|
-
const sharing = meta.sharing;
|
|
2484
|
-
if (sharing?.enabled) {
|
|
2485
|
-
console.log(`Sharing: enabled (owner: ${sharing.owner}, ${sharing.allowedUsers?.length || 0} users)`);
|
|
2486
|
-
} else {
|
|
2487
|
-
console.log(`Sharing: disabled`);
|
|
2488
|
-
}
|
|
2489
|
-
} finally {
|
|
2490
|
-
await server.disconnect();
|
|
2491
|
-
}
|
|
2492
|
-
}
|
|
2493
|
-
async function machineLs(machineId, path, showHidden) {
|
|
2494
|
-
const { server, machine } = await connectAndGetMachine(machineId);
|
|
2495
|
-
try {
|
|
2496
|
-
const result = await machine.listDirectory(path || "", { showHidden: showHidden || false });
|
|
2497
|
-
if (!result.success) {
|
|
2498
|
-
console.error(`Error: ${result.error || "Failed to list directory"}`);
|
|
2499
|
-
process.exit(1);
|
|
2500
|
-
}
|
|
2501
|
-
console.log(`${result.path}/`);
|
|
2502
|
-
for (const entry of result.entries || []) {
|
|
2503
|
-
const prefix = entry.type === "directory" ? "d " : " ";
|
|
2504
|
-
console.log(`${prefix}${entry.name}${entry.type === "directory" ? "/" : ""}`);
|
|
2505
|
-
}
|
|
2506
|
-
} finally {
|
|
2507
|
-
await server.disconnect();
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
async function sessionLoopStart(sessionIdPartial, task, machineId, opts) {
|
|
2511
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
|
|
2512
|
-
try {
|
|
2513
|
-
const svc = getSessionProxy(machine, fullId);
|
|
2514
|
-
const until = opts?.until || opts?.criteria;
|
|
2515
|
-
const isStart = !!(task || until);
|
|
2516
|
-
const maxIterations = opts?.maxIterations ?? (isStart ? 1e4 : void 0);
|
|
2517
|
-
const evaluator = opts?.agent ? true : opts?.evaluator !== false;
|
|
2518
|
-
const budget = opts?.maxTokensPerHour || opts?.maxRuntimeSec ? {
|
|
2519
|
-
...opts?.maxTokensPerHour ? { max_tokens_per_hour: opts.maxTokensPerHour } : {},
|
|
2520
|
-
...opts?.maxRuntimeSec ? { max_runtime_sec: opts.maxRuntimeSec } : {}
|
|
2521
|
-
} : void 0;
|
|
2522
|
-
await svc.updateConfig({
|
|
2523
|
-
loop: {
|
|
2524
|
-
...task ? { task } : {},
|
|
2525
|
-
...until ? { until } : {},
|
|
2526
|
-
...opts?.oracle ? { oracle: opts.oracle } : {},
|
|
2527
|
-
...opts?.parent ? { parent: opts.parent } : {},
|
|
2528
|
-
...maxIterations != null ? { max_iterations: maxIterations } : {},
|
|
2529
|
-
...budget ? { budget } : {},
|
|
2530
|
-
...opts?.engine ? { engine: opts.engine } : {},
|
|
2531
|
-
evaluator
|
|
2532
|
-
}
|
|
2533
|
-
});
|
|
2534
|
-
if (!isStart) {
|
|
2535
|
-
console.log(`\u{1F501} Loop limit updated on session ${fullId.slice(0, 8)}${maxIterations != null ? ` \u2192 max ${maxIterations} iterations` : ""} (resumes the loop if it had stopped).`);
|
|
2536
|
-
} else {
|
|
2537
|
-
console.log(`\u{1F501} Loop ${task ? "started" : "attached"} on session ${fullId.slice(0, 8)}`);
|
|
2538
|
-
if (task) console.log(` Task: ${task.slice(0, 100)}${task.length > 100 ? "..." : ""}`);
|
|
2539
|
-
if (until) console.log(` Until: ${until.slice(0, 100)}${until.length > 100 ? "..." : ""}`);
|
|
2540
|
-
console.log(` Oracle: ${opts?.oracle || "(none)"}`);
|
|
2541
|
-
console.log(` Evaluator: ${evaluator ? "on" : "off"}`);
|
|
2542
|
-
if (opts?.parent) console.log(` Parent review: ${opts.parent.slice(0, 8)}`);
|
|
2543
|
-
if (maxIterations != null) console.log(` Max iterations: ${maxIterations}`);
|
|
2544
|
-
if (!task) console.log(` (hot-plug \u2014 gating the session's current work)`);
|
|
2545
|
-
}
|
|
2546
|
-
} finally {
|
|
2547
|
-
await server.disconnect();
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
async function sessionLoopCancel(sessionIdPartial, machineId) {
|
|
2551
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
|
|
2552
|
-
try {
|
|
2553
|
-
const svc = getSessionProxy(machine, fullId);
|
|
2554
|
-
await svc.updateConfig({ loop: null });
|
|
2555
|
-
console.log(`Loop cancelled on session ${fullId.slice(0, 8)}`);
|
|
2556
|
-
} finally {
|
|
2557
|
-
await server.disconnect();
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
async function sessionLoopStatus(sessionIdPartial, machineId) {
|
|
2561
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
|
|
2562
|
-
try {
|
|
2563
|
-
const svc = getSessionProxy(machine, fullId);
|
|
2564
|
-
const readJsonFile = async (relPath) => {
|
|
2565
|
-
try {
|
|
2566
|
-
const raw = await svc.readFile(relPath);
|
|
2567
|
-
const content = typeof raw === "string" ? raw : raw?.content;
|
|
2568
|
-
if (content) return JSON.parse(Buffer.from(content, "base64").toString("utf-8"));
|
|
2569
|
-
} catch {
|
|
2570
|
-
}
|
|
2571
|
-
return null;
|
|
2572
|
-
};
|
|
2573
|
-
const loopRel = `.svamp/${fullId}/loop`;
|
|
2574
|
-
let base = loopRel;
|
|
2575
|
-
let state = await readJsonFile(`${loopRel}/loop-state.json`);
|
|
2576
|
-
if (!state) {
|
|
2577
|
-
base = ".claude/loop";
|
|
2578
|
-
state = await readJsonFile(`${base}/loop-state.json`);
|
|
2579
|
-
}
|
|
2580
|
-
if (!state) {
|
|
2581
|
-
console.log("No loop configured on this session.");
|
|
2582
|
-
return;
|
|
2583
|
-
}
|
|
2584
|
-
const config = await readJsonFile(`${base}/loop.config.json`);
|
|
2585
|
-
const verdict = await readJsonFile(`${base}/evaluator-verdict.json`);
|
|
2586
|
-
console.log(`Loop on session ${fullId.slice(0, 8)}:`);
|
|
2587
|
-
const maxN = config?.max_iterations;
|
|
2588
|
-
console.log(` Phase: ${state.phase || (state.active === false ? "inactive" : "running")}`);
|
|
2589
|
-
console.log(` Iteration: ${state.iteration ?? 0}${maxN ? ` / ${maxN}` : ""}`);
|
|
2590
|
-
if (config?.oracle?.command) console.log(` Oracle: ${config.oracle.command}`);
|
|
2591
|
-
if (state.last_oracle) console.log(` Last oracle: ${String(state.last_oracle).split("\n")[0]}`);
|
|
2592
|
-
if (config && config.evaluator?.enabled !== false) {
|
|
2593
|
-
console.log(` Evaluator: ${verdict ? `${verdict.verdict}${verdict.reason ? ` \u2014 ${verdict.reason}` : ""}` : "on (no verdict yet)"}`);
|
|
2594
|
-
}
|
|
2595
|
-
if (state.gave_up_reason) console.log(` Gave up: ${state.gave_up_reason}`);
|
|
2596
|
-
if (state.started_at) console.log(` Started at: ${state.started_at}`);
|
|
2597
|
-
} finally {
|
|
2598
|
-
await server.disconnect();
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
async function sessionInboxSend(sessionIdPartial, body, machineId, opts) {
|
|
2602
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
|
|
2603
|
-
try {
|
|
2604
|
-
const callerSessionId = process.env.SVAMP_SESSION_ID;
|
|
2605
|
-
const hop = computeOutboundHop(callerSessionId);
|
|
2606
|
-
let urgency = opts?.urgency || "normal";
|
|
2607
|
-
if (hop.fromInboxTurn) urgency = "normal";
|
|
2608
|
-
const message = {
|
|
2609
|
-
messageId: shortId(),
|
|
2610
|
-
body,
|
|
2611
|
-
timestamp: Date.now(),
|
|
2612
|
-
read: false,
|
|
2613
|
-
from: callerSessionId ? `agent:${callerSessionId}` : `cli:${os.userInfo().username}`,
|
|
2614
|
-
...callerSessionId ? { fromSession: callerSessionId } : {},
|
|
2615
|
-
to: fullId,
|
|
2616
|
-
subject: opts?.subject,
|
|
2617
|
-
urgency,
|
|
2618
|
-
hopCount: hop.hopCount,
|
|
2619
|
-
replyTo: opts?.replyTo,
|
|
2620
|
-
threadId: opts?.threadId || hop.threadId
|
|
2621
|
-
};
|
|
2622
|
-
const result = await machine.sessionRPC(fullId, "sendInboxMessage", { message });
|
|
2623
|
-
if (callerSessionId) {
|
|
2624
|
-
try {
|
|
2625
|
-
registerAwaitingReply(callerSessionId, opts?.threadId || hop.threadId || message.messageId);
|
|
2626
|
-
} catch {
|
|
2627
|
-
}
|
|
2628
|
-
}
|
|
2629
|
-
if (opts?.json) {
|
|
2630
|
-
console.log(formatJson({ sessionId: fullId, messageId: result.messageId, sent: true }));
|
|
2631
|
-
} else {
|
|
2632
|
-
console.log(`Inbox message sent to session ${fullId.slice(0, 8)} (id: ${result.messageId.slice(0, 8)})`);
|
|
2633
|
-
}
|
|
2634
|
-
} finally {
|
|
2635
|
-
await server.disconnect();
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
async function sessionInboxList(sessionIdPartial, machineId, opts) {
|
|
2639
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
|
|
2640
|
-
try {
|
|
2641
|
-
const result = await machine.sessionRPC(fullId, "getInbox", { opts: { unread: opts?.unread, limit: opts?.limit } });
|
|
2642
|
-
const messages = result.messages;
|
|
2643
|
-
if (opts?.json) {
|
|
2644
|
-
console.log(formatJson({ sessionId: fullId, messages }));
|
|
2645
|
-
return;
|
|
2646
|
-
}
|
|
2647
|
-
if (messages.length === 0) {
|
|
2648
|
-
console.log(`Inbox for session ${fullId.slice(0, 8)} is empty.`);
|
|
2649
|
-
return;
|
|
2650
|
-
}
|
|
2651
|
-
const unreadCount = messages.filter((m) => !m.read).length;
|
|
2652
|
-
console.log(`Inbox for session ${fullId.slice(0, 8)} (${messages.length} message(s), ${unreadCount} unread):
|
|
2653
|
-
`);
|
|
2654
|
-
for (const msg of messages) {
|
|
2655
|
-
const status = msg.read ? " " : "\u25CF";
|
|
2656
|
-
const from = msg.from ? ` from ${msg.from}` : "";
|
|
2657
|
-
const subject = msg.subject ? ` \u2014 ${msg.subject}` : "";
|
|
2658
|
-
const urgencyTag = msg.urgency === "urgent" ? " [URGENT]" : "";
|
|
2659
|
-
const date = new Date(msg.timestamp).toLocaleString();
|
|
2660
|
-
const preview = msg.body.length > 100 ? msg.body.slice(0, 97) + "..." : msg.body;
|
|
2661
|
-
console.log(` ${status} ${msg.messageId.slice(0, 8)}${urgencyTag}${from}${subject}`);
|
|
2662
|
-
console.log(` ${date}`);
|
|
2663
|
-
console.log(` ${preview}
|
|
2664
|
-
`);
|
|
2665
|
-
}
|
|
2666
|
-
} finally {
|
|
2667
|
-
await server.disconnect();
|
|
2668
|
-
}
|
|
2669
|
-
}
|
|
2670
|
-
async function sessionInboxRead(sessionIdPartial, messageId, machineId) {
|
|
2671
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
|
|
2672
|
-
try {
|
|
2673
|
-
const result = await machine.sessionRPC(fullId, "getInbox", {});
|
|
2674
|
-
const msg = result.messages.find((m) => m.messageId === messageId || m.messageId.startsWith(messageId));
|
|
2675
|
-
if (!msg) {
|
|
2676
|
-
console.error(`Message ${messageId} not found in inbox.`);
|
|
2677
|
-
process.exit(1);
|
|
2678
|
-
}
|
|
2679
|
-
if (!msg.read) {
|
|
2680
|
-
await machine.sessionRPC(fullId, "markInboxRead", { messageId: msg.messageId });
|
|
2681
|
-
}
|
|
2682
|
-
console.log(`From: ${msg.from || "(unknown)"}`);
|
|
2683
|
-
if (msg.subject) console.log(`Subject: ${msg.subject}`);
|
|
2684
|
-
console.log(`Date: ${new Date(msg.timestamp).toLocaleString()}`);
|
|
2685
|
-
if (msg.urgency === "urgent") console.log("Urgency: URGENT");
|
|
2686
|
-
if (msg.replyTo) console.log(`Reply-To: ${msg.replyTo.slice(0, 8)}`);
|
|
2687
|
-
if (msg.threadId) console.log(`Thread: ${msg.threadId.slice(0, 8)}`);
|
|
2688
|
-
console.log(`
|
|
2689
|
-
${msg.body}`);
|
|
2690
|
-
} finally {
|
|
2691
|
-
await server.disconnect();
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
async function sessionInboxReply(sessionIdPartial, messageId, body, machineId) {
|
|
2695
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
|
|
2696
|
-
try {
|
|
2697
|
-
const result = await machine.sessionRPC(fullId, "getInbox", {});
|
|
2698
|
-
const original = result.messages.find((m) => m.messageId === messageId || m.messageId.startsWith(messageId));
|
|
2699
|
-
if (!original) {
|
|
2700
|
-
console.error(`Message ${messageId} not found in inbox.`);
|
|
2701
|
-
process.exit(1);
|
|
2702
|
-
}
|
|
2703
|
-
let channelDelivered = false;
|
|
2704
|
-
if (original.channelId && original.correlationId) {
|
|
2705
|
-
const rr = await machine.sessionRPC(fullId, "channelReply", { params: { channel: original.channelId, correlationId: original.correlationId, to: original.from, body } });
|
|
2706
|
-
if (rr?.error) {
|
|
2707
|
-
if (!original.fromSession) {
|
|
2708
|
-
console.error(`Channel reply failed: ${rr.error}`);
|
|
2709
|
-
process.exit(1);
|
|
2710
|
-
}
|
|
2711
|
-
} else {
|
|
2712
|
-
channelDelivered = true;
|
|
2713
|
-
console.log(`Reply queued to "${original.from}" on channel ${original.channelId} (correlation ${original.correlationId}).`);
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
if (original.fromSession) {
|
|
2717
|
-
try {
|
|
2718
|
-
const hop = computeOutboundHop(process.env.SVAMP_SESSION_ID || fullId);
|
|
2719
|
-
const reply = {
|
|
2720
|
-
messageId: shortId(),
|
|
2721
|
-
body,
|
|
2722
|
-
timestamp: Date.now(),
|
|
2723
|
-
read: false,
|
|
2724
|
-
from: process.env.SVAMP_SESSION_HANDLE || `session:${fullId}`,
|
|
2725
|
-
fromSession: fullId,
|
|
2726
|
-
to: original.fromSession,
|
|
2727
|
-
subject: original.subject ? `Re: ${original.subject}` : void 0,
|
|
2728
|
-
urgency: "normal",
|
|
2729
|
-
hopCount: hop.hopCount,
|
|
2730
|
-
replyTo: original.messageId,
|
|
2731
|
-
threadId: original.threadId || original.messageId
|
|
2732
|
-
};
|
|
2733
|
-
const sendResult = await machine.sessionRPC(original.fromSession, "sendInboxMessage", { message: reply });
|
|
2734
|
-
console.log(`Reply sent to session ${original.fromSession.slice(0, 8)} (id: ${sendResult.messageId.slice(0, 8)})`);
|
|
2735
|
-
} catch (err) {
|
|
2736
|
-
if (!channelDelivered) {
|
|
2737
|
-
console.error(`Reply failed: ${err?.message || err}`);
|
|
2738
|
-
process.exit(1);
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
return;
|
|
2742
|
-
}
|
|
2743
|
-
if (!channelDelivered) {
|
|
2744
|
-
console.error("Cannot reply: original message has no fromSession (and not a channel message).");
|
|
2745
|
-
process.exit(1);
|
|
2746
|
-
}
|
|
2747
|
-
} finally {
|
|
2748
|
-
await server.disconnect();
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
|
-
async function sessionInboxClear(sessionIdPartial, machineId, opts) {
|
|
2752
|
-
const { server, machine, fullId } = await connectAndResolveSession(sessionIdPartial, machineId);
|
|
2753
|
-
try {
|
|
2754
|
-
const result = await machine.sessionRPC(fullId, "clearInbox", { opts: { all: opts?.all, messageId: opts?.messageId } });
|
|
2755
|
-
const what = opts?.messageId ? `message ${opts.messageId}` : opts?.all ? "all messages" : "read messages";
|
|
2756
|
-
console.log(`Cleared ${what} on session ${fullId.slice(0, 8)} (removed ${result.removed ?? "?"}, ${result.remaining} remaining)`);
|
|
2757
|
-
} finally {
|
|
2758
|
-
await server.disconnect();
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2762
|
-
export { collectAssistantResponse, connectAndGetMachine, connectAndResolveSession, createWorktree, generateWorktreeName, isSelfBtw, machineExec, machineInfo, machineLs, machineShare, parseShareArg, queryCore, renderMessage, resolveSessionId, sendCore, sessionApprove, sessionArchive, sessionAttach, sessionDelete, sessionDeny, sessionEditMessage, sessionInboxClear, sessionInboxList, sessionInboxRead, sessionInboxReply, sessionInboxSend, sessionInfo, sessionList, sessionLoopCancel, sessionLoopStart, sessionLoopStatus, sessionMachines, sessionMessages, sessionQuery, sessionRefineLastReply, sessionResume, sessionSend, sessionShare, sessionSpawn, sessionUndoEdit, sessionWait, sessionWhoami, snapshotLatestSeq, validateSendOptions, wiseAnnounceCli, wiseAskCli, wiseJoinMeetingCli, wiseLeaveMeetingCli, wiseMeetingsCli };
|