premanmcp 1.1.18 → 1.1.19
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/README.md +4 -2
- package/bin/agent.js +1 -1
- package/bin/swarm-progress.js +127 -0
- package/bin/swarm.js +119 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -83,8 +83,10 @@ account, backend, and workspace:
|
|
|
83
83
|
/swarm stop <run-id>
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
Launch follows automatically, showing queue and running counts, live tool
|
|
87
|
+
actions, and the final result. `/swarm help` lists every option. Add `--dry-run`
|
|
88
|
+
to preview a launch without starting attempts, or `--no-watch` to return
|
|
89
|
+
immediately. `--json` returns one JSON response. Ctrl+C while watching returns to the conversation and leaves
|
|
88
90
|
the swarm running; `/swarm stop <run-id>` cancels it explicitly. The same commands
|
|
89
91
|
work outside a session as `preman swarm list|launch|watch|stop|bank`. These run
|
|
90
92
|
real agents against hosted MCPs; the app's sample swarms remain a separate demo.
|
package/bin/agent.js
CHANGED
|
@@ -2086,7 +2086,7 @@ export async function runSwarm(rest, { args, token, workspaceId, say, paint, reg
|
|
|
2086
2086
|
}
|
|
2087
2087
|
await swarmCommand(argv, {
|
|
2088
2088
|
sessionArgs: args, token, workspaceId, signal: controller.signal,
|
|
2089
|
-
write: say,
|
|
2089
|
+
write: say,
|
|
2090
2090
|
});
|
|
2091
2091
|
say("\n");
|
|
2092
2092
|
} catch (error) {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
|
|
3
|
+
const FINISHED = new Set(["done", "failed", "cancelled"]);
|
|
4
|
+
const MAX_LINES = 24;
|
|
5
|
+
const MAX_BUFFER = 65536;
|
|
6
|
+
|
|
7
|
+
// Only action names and outcomes enter the transcript, never tool arguments,
|
|
8
|
+
// results, credentials, or the agent's private reasoning.
|
|
9
|
+
export function cleanLabel(value, limit = 120) {
|
|
10
|
+
return stripVTControlCharacters(String(value ?? ""))
|
|
11
|
+
.replace(/[\x00-\x1f\x7f-\x9f]/g, " ")
|
|
12
|
+
.replace(/(?:pm_live_|sk-)[A-Za-z0-9_-]+/g, "[redacted]")
|
|
13
|
+
.slice(0, limit);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class SwarmProgress {
|
|
17
|
+
constructor({ write, colour = false }) {
|
|
18
|
+
this.write = write;
|
|
19
|
+
this.colour = colour;
|
|
20
|
+
this.cells = new Map();
|
|
21
|
+
this.lines = [];
|
|
22
|
+
this.omitted = 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
state(cell) {
|
|
26
|
+
if (!this.cells.has(cell.id)) {
|
|
27
|
+
this.cells.set(cell.id, { seen: new Set(), offset: 0, pending: "", tools: new Map() });
|
|
28
|
+
}
|
|
29
|
+
return this.cells.get(cell.id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
note(cell, key, text, outcome = "") {
|
|
33
|
+
const state = this.state(cell);
|
|
34
|
+
if (state.seen.has(key)) return;
|
|
35
|
+
// Tool logs are untrusted and a single agent can emit indefinitely.
|
|
36
|
+
if (state.seen.size >= 500) return;
|
|
37
|
+
state.seen.add(key);
|
|
38
|
+
const mark = outcome === "failed" ? "✗" : outcome === "done" ? "✓" : "·";
|
|
39
|
+
const coloured = this.colour && outcome ? `\x1b[${outcome === "failed" ? 31 : 32}m${mark}\x1b[0m` : mark;
|
|
40
|
+
if (this.lines.length < MAX_LINES) this.lines.push(` ${coloured} ${cleanLabel(cell.harness, 24)} ${cleanLabel(cell.id, 8)} ${text}`);
|
|
41
|
+
else this.omitted += 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
observe(cells, { results = false } = {}) {
|
|
45
|
+
for (const cell of cells) {
|
|
46
|
+
const evidence = cell.evidence || {};
|
|
47
|
+
if (!results) {
|
|
48
|
+
if (cell.status === "running") this.note(cell, "running", "Agent starting");
|
|
49
|
+
if (evidence.container_id) this.note(cell, "container", "Container started", "done");
|
|
50
|
+
if (cell.reached === true) this.note(cell, "connected", "Connected to the MCP", "done");
|
|
51
|
+
if (evidence.tools_listed === true) this.note(cell, "listed", "Tools loaded", "done");
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const tool of (evidence.tools_called || []).slice(0, 100)) {
|
|
55
|
+
this.note(cell, `called:${tool}`, `Called ${cleanLabel(tool)}`, "done");
|
|
56
|
+
}
|
|
57
|
+
if (FINISHED.has(cell.status)) {
|
|
58
|
+
const failed = cell.status !== "done" || cell.selected === false;
|
|
59
|
+
const result = cell.selected === true ? "Expected tool selected" : cell.selected === false ? "Expected tool not selected" : "Not scored";
|
|
60
|
+
const reason = cell.verdict_reason ? ` · ${cleanLabel(cell.verdict_reason).replaceAll("_", " ")}` : "";
|
|
61
|
+
this.note(cell, `finished:${cell.status}`, `${cell.status === "cancelled" ? "Cancelled" : "Finished"} — ${result}${reason}`, failed ? "failed" : "done");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console(cell, chunk) {
|
|
67
|
+
const state = this.state(cell);
|
|
68
|
+
if (chunk.truncated) state.pending = "";
|
|
69
|
+
state.offset = Number.isFinite(chunk.total) ? chunk.total : state.offset;
|
|
70
|
+
let text = state.pending + String(chunk.text || "");
|
|
71
|
+
if (text.length > MAX_BUFFER) {
|
|
72
|
+
text = text.slice(-MAX_BUFFER);
|
|
73
|
+
text = text.slice(text.indexOf("\n") + 1);
|
|
74
|
+
}
|
|
75
|
+
const lines = text.split("\n");
|
|
76
|
+
state.pending = lines.pop() || "";
|
|
77
|
+
if (FINISHED.has(cell.status) && state.pending) {
|
|
78
|
+
lines.push(state.pending);
|
|
79
|
+
state.pending = "";
|
|
80
|
+
}
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
let event;
|
|
83
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
84
|
+
if (!event || typeof event !== "object") continue;
|
|
85
|
+
const item = event.item;
|
|
86
|
+
if (item?.type === "mcp_tool_call") {
|
|
87
|
+
const name = cleanLabel(item.tool || item.name || "MCP tool");
|
|
88
|
+
const finished = event.type === "item.completed";
|
|
89
|
+
const failed = Boolean(item.error) || item.status === "failed";
|
|
90
|
+
this.note(cell, `event:${item.id}:${name}:${event.type}`, `${finished ? failed ? "Tool failed" : "Tool completed" : "Calling"}: ${name}`, finished ? failed ? "failed" : "done" : "");
|
|
91
|
+
if (finished && !failed && state.seen.size < 500) state.seen.add(`called:${item.tool || item.name}`);
|
|
92
|
+
} else if (item?.type === "command_execution" && event.type === "item.started") {
|
|
93
|
+
this.note(cell, `command:${item.id}`, "Running a shell command");
|
|
94
|
+
}
|
|
95
|
+
const content = event.message?.content || event.content;
|
|
96
|
+
if (Array.isArray(content)) for (const block of content) {
|
|
97
|
+
if (block?.type === "tool_use") {
|
|
98
|
+
const name = cleanLabel(block.name || "tool");
|
|
99
|
+
if (state.tools.size < 100) state.tools.set(block.id, name);
|
|
100
|
+
this.note(cell, `use:${block.id}`, `Calling: ${name}`);
|
|
101
|
+
} else if (block?.type === "tool_result") {
|
|
102
|
+
const name = state.tools.get(block.tool_use_id) || "tool";
|
|
103
|
+
this.note(cell, `result:${block.tool_use_id}`, `Tool ${block.is_error ? "failed" : "completed"}: ${name}`, block.is_error ? "failed" : "done");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (event.type === "turn.started") this.note(cell, "turn", "Agent working");
|
|
107
|
+
if (event.type === "turn.failed") this.note(cell, "turn-failed", "Agent turn failed", "failed");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
flush() {
|
|
112
|
+
if (this.lines.length) this.write(`${this.lines.slice(0, MAX_LINES).join("\n")}\n`);
|
|
113
|
+
if (this.omitted) this.write(` … ${this.omitted} more updates; full details in the swarm page.\n`);
|
|
114
|
+
this.lines = [];
|
|
115
|
+
this.omitted = 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function progressLine(run, cells, started) {
|
|
120
|
+
const counts = {};
|
|
121
|
+
for (const cell of cells) counts[cell.status] = (counts[cell.status] || 0) + 1;
|
|
122
|
+
const done = (counts.done || 0) + (counts.failed || 0) + (counts.cancelled || 0);
|
|
123
|
+
const elapsed = Math.max(0, Math.floor((Date.now() - started) / 1000));
|
|
124
|
+
const age = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
|
|
125
|
+
const state = run.status === "queued" ? "Waiting for a worker" : FINISHED.has(run.status) ? cleanLabel(run.status) : "Running";
|
|
126
|
+
return `${state} · ${counts.queued || 0} queued · ${counts.running || 0} running · ${done}/${cells.length} finished · watching ${age}\n`;
|
|
127
|
+
}
|
package/bin/swarm.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { setTimeout as wait } from "node:timers/promises";
|
|
13
|
+
import { SwarmProgress, cleanLabel, progressLine } from "./swarm-progress.js";
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
backendUrl,
|
|
@@ -27,8 +28,9 @@ Swarm options:
|
|
|
27
28
|
preman swarm list Recent swarms and what each found
|
|
28
29
|
preman swarm launch --server <slug|id> --ask "<what a developer asks>" [--ask ...]
|
|
29
30
|
[--expect tool,tool] [--agents claude_code,codex,...] [--rivals 0,5]
|
|
30
|
-
[--size 60] [--name <label>] [--dry-run]
|
|
31
|
-
|
|
31
|
+
[--size 60] [--name <label>] [--dry-run] [--no-watch]
|
|
32
|
+
Launch and follow live actions until done
|
|
33
|
+
--no-watch queues and returns immediately
|
|
32
34
|
preman swarm watch <id> [--once] Follow a swarm until it finishes
|
|
33
35
|
preman swarm stop <id> Stop a swarm; unfinished agents do not run
|
|
34
36
|
preman swarm bank [--server <slug|id>] Behaviors PreMan learned and you kept
|
|
@@ -51,7 +53,7 @@ const AGENT_NAMES = {
|
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
const TERMINAL = new Set(["done", "failed", "cancelled"]);
|
|
54
|
-
const POLL_MS =
|
|
56
|
+
const POLL_MS = 2000;
|
|
55
57
|
|
|
56
58
|
function environmentOf(harness) {
|
|
57
59
|
return ENVIRONMENTS.find((env) => env.harnesses.includes(harness))?.id || "other";
|
|
@@ -89,7 +91,7 @@ export function positionals(raw) {
|
|
|
89
91
|
return out;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
const BARE_FLAGS = new Set(["--json", "--once", "--dry-run", "--no-color"]);
|
|
94
|
+
const BARE_FLAGS = new Set(["--json", "--once", "--dry-run", "--no-color", "--no-watch"]);
|
|
93
95
|
|
|
94
96
|
function listOf(value) {
|
|
95
97
|
return String(value || "")
|
|
@@ -179,7 +181,7 @@ export function renderSwarm(run, cells, { colour = false } = {}) {
|
|
|
179
181
|
const done = (cells || []).filter((cell) => cell.status === "done" || cell.status === "failed").length;
|
|
180
182
|
const lines = [];
|
|
181
183
|
lines.push(
|
|
182
|
-
`${paint.bold(run.release_label || run.id)} ${paint.dim(run.hosted_mcp_name || run.hosted_mcp_slug || "")} ${run.status}`
|
|
184
|
+
`${paint.bold(cleanLabel(run.release_label || run.id))} ${paint.dim(cleanLabel(run.hosted_mcp_name || run.hosted_mcp_slug || ""))} ${cleanLabel(run.status)}`
|
|
183
185
|
);
|
|
184
186
|
if (summary.scored) {
|
|
185
187
|
const range = likelyRange(summary.picked, summary.scored);
|
|
@@ -198,8 +200,8 @@ export function renderSwarm(run, cells, { colour = false } = {}) {
|
|
|
198
200
|
const rate = agent.rate === null ? paint.dim("—") : agent.rate >= 0.75 ? paint.green(pct(agent.rate)) : agent.rate >= 0.4 ? paint.yellow(pct(agent.rate)) : paint.red(pct(agent.rate));
|
|
199
201
|
const working = agent.running ? paint.dim(` · ${agent.running} working`) : "";
|
|
200
202
|
const rival = [...agent.lostTo.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
201
|
-
const lost = rival ? paint.dim(` · lost to ${rival[0]} ×${rival[1]}`) : "";
|
|
202
|
-
lines.push(` ${truncate(agent.name, 30).padEnd(30)} ${String(rate).padStart(colour ? 13 : 4)} ${paint.dim(`${agent.picked}/${agent.scored}`)}${working}${lost}`);
|
|
203
|
+
const lost = rival ? paint.dim(` · lost to ${cleanLabel(rival[0])} ×${rival[1]}`) : "";
|
|
204
|
+
lines.push(` ${truncate(cleanLabel(agent.name), 30).padEnd(30)} ${String(rate).padStart(colour ? 13 : 4)} ${paint.dim(`${agent.picked}/${agent.scored}`)}${working}${lost}`);
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
return `${lines.join("\n")}\n`;
|
|
@@ -227,7 +229,9 @@ function requireToken(args) {
|
|
|
227
229
|
async function call(args, token, method, path, options = {}) {
|
|
228
230
|
const result = await callBackendJson(args, method, path, { token, ...options });
|
|
229
231
|
if (!result.ok) {
|
|
230
|
-
|
|
232
|
+
const error = new Error(`${method} ${path} on ${backendUrl(args)}: ${result.status_code} ${describeFailure(result, "request failed")}`);
|
|
233
|
+
error.status = result.status_code;
|
|
234
|
+
throw error;
|
|
231
235
|
}
|
|
232
236
|
return result;
|
|
233
237
|
}
|
|
@@ -255,9 +259,92 @@ function swarmLink(args, id) {
|
|
|
255
259
|
return `${frontendUrl(args)}/workbench/swarms/swarms/${encodeURIComponent(id)}`;
|
|
256
260
|
}
|
|
257
261
|
|
|
262
|
+
async function followSwarm(id, { args, request, write, colour, signal, pollMs, once = false, json = false }) {
|
|
263
|
+
const controller = new AbortController();
|
|
264
|
+
const interrupt = () => controller.abort();
|
|
265
|
+
if (signal?.aborted) controller.abort();
|
|
266
|
+
signal?.addEventListener("abort", interrupt, { once: true });
|
|
267
|
+
if (!signal) process.on("SIGINT", interrupt);
|
|
268
|
+
const read = (method, path, options) => request(method, path, { ...options, signal: controller.signal });
|
|
269
|
+
const resume = `${cliInvocation()} swarm watch ${id}`;
|
|
270
|
+
const progress = new SwarmProgress({ write, colour });
|
|
271
|
+
const started = Date.now();
|
|
272
|
+
let cursor = 0;
|
|
273
|
+
let failures = 0;
|
|
274
|
+
let consoleWarned = false;
|
|
275
|
+
if (!once && !json) write("Following live actions. Ctrl+C stops watching; the swarm keeps running.\n");
|
|
276
|
+
try {
|
|
277
|
+
for (;;) {
|
|
278
|
+
let snapshot;
|
|
279
|
+
try {
|
|
280
|
+
snapshot = await readSwarm(read, id);
|
|
281
|
+
failures = 0;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (controller.signal.aborted) throw error;
|
|
284
|
+
failures += 1;
|
|
285
|
+
const retryable = !error.status || error.status === 429 || error.status >= 500;
|
|
286
|
+
if (once || json || !retryable || failures >= 3) throw error;
|
|
287
|
+
write(`Connection interrupted; retrying status (${failures}/3). Swarm ${id} remains on the server.\n`);
|
|
288
|
+
await wait(pollMs, undefined, { signal: controller.signal });
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const { run, cells } = snapshot;
|
|
292
|
+
if (json) {
|
|
293
|
+
write(`${JSON.stringify({ run, summary: summarize(cells) }, (k, v) => (v instanceof Map ? Object.fromEntries(v) : v), 2)}\n`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (once) {
|
|
297
|
+
write(renderSwarm(run, cells, { colour }));
|
|
298
|
+
write(`\n${swarmLink(args, id)}\n`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
write(progressLine(run, cells, started));
|
|
302
|
+
progress.observe(cells);
|
|
303
|
+
// At most four console reads per poll, rotated across larger swarms.
|
|
304
|
+
// Keep finished cells eligible for their final output after status changes.
|
|
305
|
+
const available = cells.filter((cell) => cell.id && cell.status !== "queued" && !progress.state(cell).consoleFinished);
|
|
306
|
+
const selected = available.length ? Array.from({ length: Math.min(4, available.length) }, (_, i) => available[(cursor + i) % available.length]) : [];
|
|
307
|
+
cursor += selected.length;
|
|
308
|
+
const logs = await Promise.allSettled(selected.map((cell) => read("GET", `/matrix/cells/${encodeURIComponent(cell.id)}/console`, { query: { after: progress.state(cell).offset } })));
|
|
309
|
+
for (let i = 0; i < logs.length; i += 1) {
|
|
310
|
+
const log = logs[i];
|
|
311
|
+
if (log.status === "fulfilled") {
|
|
312
|
+
progress.console(selected[i], log.value);
|
|
313
|
+
if (TERMINAL.has(selected[i].status)) progress.state(selected[i]).consoleFinished = true;
|
|
314
|
+
} else if (!controller.signal.aborted && !consoleWarned) {
|
|
315
|
+
write(" Live action output is unavailable; continuing to follow run status.\n");
|
|
316
|
+
consoleWarned = true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (controller.signal.aborted) throw new Error("Interrupted");
|
|
320
|
+
progress.observe(cells, { results: true });
|
|
321
|
+
progress.flush();
|
|
322
|
+
if (once || TERMINAL.has(run.status)) {
|
|
323
|
+
const eligible = cells.filter((cell) => cell.id && cell.status !== "queued");
|
|
324
|
+
const shown = eligible.filter((cell) => progress.state(cell).consoleFinished).length;
|
|
325
|
+
if (shown < eligible.length) write(`Action logs shown for ${shown} of ${eligible.length} attempts; open the swarm page for all logs.\n`);
|
|
326
|
+
write(renderSwarm(run, cells, { colour }));
|
|
327
|
+
if (run.stop_reason) write(`Stopped: ${cleanLabel(run.stop_reason)}\n`);
|
|
328
|
+
write(`\n${swarmLink(args, id)}\n`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
await wait(pollMs, undefined, { signal: controller.signal });
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (controller.signal.aborted) {
|
|
335
|
+
write(`\nStopped watching. The swarm was not cancelled.\nResume: ${resume}\n`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
throw new Error(`Could not follow swarm ${id}: ${cleanLabel(error.message, 240)}\nResume: ${resume}`);
|
|
339
|
+
} finally {
|
|
340
|
+
signal?.removeEventListener("abort", interrupt);
|
|
341
|
+
if (!signal) process.off("SIGINT", interrupt);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
258
345
|
export async function swarmCommand(commandArgs = [], {
|
|
259
346
|
sessionArgs, token: sessionToken, workspaceId, signal,
|
|
260
|
-
write = (text) => process.stdout.write(text),
|
|
347
|
+
write = (text) => process.stdout.write(text), pollMs = POLL_MS,
|
|
261
348
|
} = {}) {
|
|
262
349
|
const [sub = "list", ...rest] = commandArgs;
|
|
263
350
|
// A session's credential stays with its backend even when slash-command
|
|
@@ -269,9 +356,23 @@ export async function swarmCommand(commandArgs = [], {
|
|
|
269
356
|
const json = args.has("--json");
|
|
270
357
|
const colour = colourEnabled(args);
|
|
271
358
|
const token = sessionToken || requireToken(args);
|
|
272
|
-
const request = (method, path, options = {}) =>
|
|
273
|
-
|
|
274
|
-
|
|
359
|
+
const request = async (method, path, options = {}) => {
|
|
360
|
+
const parent = options.signal || signal;
|
|
361
|
+
const controller = new AbortController();
|
|
362
|
+
const abort = () => controller.abort();
|
|
363
|
+
if (parent?.aborted) abort();
|
|
364
|
+
parent?.addEventListener("abort", abort, { once: true });
|
|
365
|
+
const timeout = setTimeout(abort, 15000);
|
|
366
|
+
try {
|
|
367
|
+
return await call(args, token, method, path, {
|
|
368
|
+
...options, signal: controller.signal,
|
|
369
|
+
...(workspaceId ? { headers: { "x-workspace-id": workspaceId } } : {}),
|
|
370
|
+
});
|
|
371
|
+
} finally {
|
|
372
|
+
clearTimeout(timeout);
|
|
373
|
+
parent?.removeEventListener("abort", abort);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
275
376
|
|
|
276
377
|
if (sub === "list") {
|
|
277
378
|
const result = await request("GET", "/matrix/runs");
|
|
@@ -296,6 +397,7 @@ export async function swarmCommand(commandArgs = [], {
|
|
|
296
397
|
if (!serverRef || !asks.length) {
|
|
297
398
|
throw new Error(`a swarm needs --server and at least one --ask.${SWARM_HELP}`);
|
|
298
399
|
}
|
|
400
|
+
if (!json && !args.has("--dry-run")) write(`Resolving ${cleanLabel(serverRef)}…\n`);
|
|
299
401
|
const server = await findServer(request, serverRef);
|
|
300
402
|
const expect = listOf(args.value("--expect"));
|
|
301
403
|
const payload = {
|
|
@@ -311,6 +413,7 @@ export async function swarmCommand(commandArgs = [], {
|
|
|
311
413
|
const agents = listOf(args.value("--agents"));
|
|
312
414
|
if (agents.length) payload.harnesses = agents;
|
|
313
415
|
if (args.has("--dry-run")) payload.dry_run = true;
|
|
416
|
+
if (!json && !payload.dry_run) write("Queueing swarm…\n");
|
|
314
417
|
const result = await request("POST", "/matrix/runs", { json: payload });
|
|
315
418
|
const { status_code, ok, ...body } = result;
|
|
316
419
|
if (json) return void write(`${JSON.stringify(body, null, 2)}\n`);
|
|
@@ -318,28 +421,17 @@ export async function swarmCommand(commandArgs = [], {
|
|
|
318
421
|
write(`Would send ${body.cell_count ?? "?"} attempts (${body.asks_used ?? asks.length} asks, ${body.replicates ?? 1}× each).\n`);
|
|
319
422
|
return;
|
|
320
423
|
}
|
|
321
|
-
write(`Swarm ${body.id} launched against ${server.name || server.slug}.\n`);
|
|
424
|
+
write(`Swarm ${cleanLabel(body.id)} launched against ${cleanLabel(server.name || server.slug)}.\n`);
|
|
322
425
|
write(` Watch: ${cliInvocation()} swarm watch ${body.id}\n Open: ${swarmLink(args, body.id)}\n`);
|
|
426
|
+
if (!args.has("--no-watch")) await followSwarm(body.id, { args, request, write, colour, signal, pollMs });
|
|
323
427
|
return;
|
|
324
428
|
}
|
|
325
429
|
|
|
326
430
|
if (sub === "watch" || sub === "show") {
|
|
327
431
|
const [id] = positionals(rest);
|
|
328
432
|
if (!id) throw new Error(`which swarm? ${cliInvocation()} swarm watch <id>`);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
if (json) {
|
|
332
|
-
write(`${JSON.stringify({ run, summary: summarize(cells) }, (k, v) => (v instanceof Map ? Object.fromEntries(v) : v), 2)}\n`);
|
|
333
|
-
} else {
|
|
334
|
-
if (clearWatch && process.stdout.isTTY && sub === "watch") write("\u001b[2J\u001b[H");
|
|
335
|
-
write(renderSwarm(run, cells, { colour }));
|
|
336
|
-
}
|
|
337
|
-
if (sub === "show" || args.has("--once") || TERMINAL.has(run.status)) {
|
|
338
|
-
if (!json) write(`\n${swarmLink(args, id)}\n`);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
await wait(POLL_MS, undefined, { signal });
|
|
342
|
-
}
|
|
433
|
+
await followSwarm(id, { args, request, write, colour, signal, pollMs, json, once: sub === "show" || args.has("--once") || json });
|
|
434
|
+
return;
|
|
343
435
|
}
|
|
344
436
|
|
|
345
437
|
if (sub === "stop") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premanmcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19",
|
|
4
4
|
"description": "PreMan CLI and stdio proxy for PreMan's hosted MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"test:proxy": "node --test scripts/smoke-proxy.mjs",
|
|
17
17
|
"test:connect": "node scripts/smoke-connect.mjs",
|
|
18
18
|
"test:connect-heartbeat": "node scripts/smoke-connect-heartbeat.mjs",
|
|
19
|
-
"test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-agent-session.mjs scripts/smoke-agent-dock.mjs scripts/smoke-terminal-lifecycle.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-cli-update.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-eval.mjs scripts/smoke-eval-start.mjs scripts/smoke-eval-run-watch.mjs scripts/smoke-eval-behaviors.mjs scripts/smoke-eval-runtime.mjs scripts/smoke-eval-entry.mjs scripts/smoke-eval-entry-placeholder.mjs scripts/smoke-eval-discover.mjs scripts/smoke-eval-templates.mjs scripts/smoke-eval-decorated-capture.mjs scripts/smoke-eval-decorate-approval.mjs scripts/smoke-eval-approval-record.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-agent-target.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs scripts/smoke-prompts.mjs scripts/smoke-process-title.mjs scripts/smoke-predict.mjs scripts/smoke-swarm.mjs",
|
|
19
|
+
"test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-agent-session.mjs scripts/smoke-agent-dock.mjs scripts/smoke-terminal-lifecycle.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-cli-update.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-eval.mjs scripts/smoke-eval-start.mjs scripts/smoke-eval-run-watch.mjs scripts/smoke-eval-behaviors.mjs scripts/smoke-eval-runtime.mjs scripts/smoke-eval-entry.mjs scripts/smoke-eval-entry-placeholder.mjs scripts/smoke-eval-discover.mjs scripts/smoke-eval-templates.mjs scripts/smoke-eval-decorated-capture.mjs scripts/smoke-eval-decorate-approval.mjs scripts/smoke-eval-approval-record.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-agent-target.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs scripts/smoke-prompts.mjs scripts/smoke-process-title.mjs scripts/smoke-predict.mjs scripts/smoke-swarm.mjs scripts/smoke-swarm-follow.mjs",
|
|
20
20
|
"test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|