premanmcp 1.1.17 → 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 +27 -1
- package/bin/agent.js +150 -0
- package/bin/swarm-progress.js +127 -0
- package/bin/swarm.js +163 -56
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ preman chat -p "list my endpoints" # one turn, printed, then exit
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Inside a session: `/help`, `/new`, `/conversations`, `/resume <id>`, `/app`,
|
|
56
|
-
`/eval`, `/clear`, `/exit`. `/eval list` shows what your agent can be measured
|
|
56
|
+
`/swarm`, `/eval`, `/clear`, `/exit`. `/eval list` shows what your agent can be measured
|
|
57
57
|
on, numbered; `/eval on 3 12` or `/eval on sycophancy` switches those on (a
|
|
58
58
|
number, a `3-7` range, `all`, an id, or enough of one to be unambiguous), `/eval
|
|
59
59
|
off` the same in reverse, and `/eval` runs what is on. `/eval defaults` reads
|
|
@@ -65,6 +65,32 @@ link underneath opens the PreMan desktop app when it is installed (a build older
|
|
|
65
65
|
than 0.4.0 comes forward on whatever it was showing rather than on the run), and
|
|
66
66
|
the run's dashboard URL is always printed with it.
|
|
67
67
|
|
|
68
|
+
Ask the agent to launch a hosted-MCP swarm in plain language, just as in the
|
|
69
|
+
app: “Launch a 12-attempt Codex swarm against my tracker server; ask it to find
|
|
70
|
+
open issues and expect search_issues.” The terminal uses the same conversation
|
|
71
|
+
service and approval rules. A proposed launch shows its summary in the terminal;
|
|
72
|
+
choose “Approve exact action” to launch it, “Decline” to reject it, or Enter to
|
|
73
|
+
leave it unanswered. A swarm result prints its label, status, run ID, and link.
|
|
74
|
+
Piped sessions leave approvals unanswered for review in the app.
|
|
75
|
+
|
|
76
|
+
For explicit commands inside the session, `/swarm` uses the current session's
|
|
77
|
+
account, backend, and workspace:
|
|
78
|
+
|
|
79
|
+
```text
|
|
80
|
+
/swarm list
|
|
81
|
+
/swarm launch --server tracker --ask "Find open issues" --expect search_issues --agents codex --size 12 --rivals 0
|
|
82
|
+
/swarm watch <run-id>
|
|
83
|
+
/swarm stop <run-id>
|
|
84
|
+
```
|
|
85
|
+
|
|
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
|
|
90
|
+
the swarm running; `/swarm stop <run-id>` cancels it explicitly. The same commands
|
|
91
|
+
work outside a session as `preman swarm list|launch|watch|stop|bank`. These run
|
|
92
|
+
real agents against hosted MCPs; the app's sample swarms remain a separate demo.
|
|
93
|
+
|
|
68
94
|
A session takes a screen of its own, the way `vim` does: nothing above it is
|
|
69
95
|
scrollable, and leaving hands your shell back exactly as it was with one line
|
|
70
96
|
saying how to resume. Ctrl+C stops the current turn (PreMan keeps working on it)
|
package/bin/agent.js
CHANGED
|
@@ -25,6 +25,7 @@ import { basename, join } from "node:path";
|
|
|
25
25
|
import { createInterface } from "node:readline/promises";
|
|
26
26
|
|
|
27
27
|
import { createSseParser } from "./runner.js";
|
|
28
|
+
import { SWARM_HELP, swarmCommand } from "./swarm.js";
|
|
28
29
|
import {
|
|
29
30
|
authenticateTerminal,
|
|
30
31
|
backendUrl,
|
|
@@ -59,6 +60,10 @@ const SLASH_HELP = `Commands:
|
|
|
59
60
|
/eval on|off <what> Switch behaviours on or off, by number or by id
|
|
60
61
|
/eval defaults Pick a few that suit this project, and switch them on
|
|
61
62
|
/eval doctor Check this machine can run evals before starting one
|
|
63
|
+
/swarm list List your hosted-MCP swarms
|
|
64
|
+
/swarm launch ... Launch with the same flags as preman swarm launch
|
|
65
|
+
/swarm watch <id> Follow a swarm; Ctrl+C returns to the conversation
|
|
66
|
+
/swarm help Show launch, watch, stop and behavior-bank options
|
|
62
67
|
/app Open this conversation in the PreMan app
|
|
63
68
|
/clear Clear the screen
|
|
64
69
|
/exit Leave (Ctrl+D does the same)
|
|
@@ -212,6 +217,13 @@ export function artifactLine(artifact) {
|
|
|
212
217
|
if (!artifact || typeof artifact !== "object") return "";
|
|
213
218
|
const type = String(artifact.type || "");
|
|
214
219
|
if (!type || SILENT_ARTIFACTS.has(type)) return "";
|
|
220
|
+
if (type === "swarm_run" && artifact.run?.id) {
|
|
221
|
+
const run = artifact.run;
|
|
222
|
+
const name = truncate(run.release_label || artifact.title || "Swarm", 60);
|
|
223
|
+
const line = [name, run.status, run.id].filter(Boolean).join(" · ");
|
|
224
|
+
const url = artifactUrl(artifact);
|
|
225
|
+
return url ? `${line} — ${url}` : line;
|
|
226
|
+
}
|
|
215
227
|
const label =
|
|
216
228
|
String(artifact.title || "").trim() || ARTIFACT_LABELS[type] || type.replace(/_/g, " ");
|
|
217
229
|
const url = artifactUrl(artifact);
|
|
@@ -757,10 +769,82 @@ async function runTurn({
|
|
|
757
769
|
ask,
|
|
758
770
|
say,
|
|
759
771
|
});
|
|
772
|
+
await askActionProposals({
|
|
773
|
+
args, token, workspaceId, artifacts: done.turn.artifacts, paint, ask, say, register,
|
|
774
|
+
});
|
|
760
775
|
}
|
|
761
776
|
return done;
|
|
762
777
|
}
|
|
763
778
|
|
|
779
|
+
/** Approve only a stored action on this session's backend; no tool-supplied URL is executed. */
|
|
780
|
+
async function askActionProposals({ args, token, workspaceId, artifacts, paint, ask, say, register }) {
|
|
781
|
+
if (!process.stdin.isTTY) return;
|
|
782
|
+
const seen = new Set();
|
|
783
|
+
const proposals = (Array.isArray(artifacts) ? artifacts : []).filter((item) => {
|
|
784
|
+
const proposal = item?.type === "action_proposal" ? item.proposal : null;
|
|
785
|
+
const id = proposal?.id;
|
|
786
|
+
if (typeof id !== "string" || !/^[\w-]{1,128}$/.test(id) || seen.has(id)) return false;
|
|
787
|
+
if ((proposal.status || "pending") !== "pending") return false;
|
|
788
|
+
if (proposal.expires_at && Date.parse(proposal.expires_at) <= Date.now()) return false;
|
|
789
|
+
seen.add(id);
|
|
790
|
+
return true;
|
|
791
|
+
});
|
|
792
|
+
if (!proposals.length) return;
|
|
793
|
+
const write = say || ((text) => process.stdout.write(text));
|
|
794
|
+
const borrowed = typeof ask === "function";
|
|
795
|
+
const rl = borrowed ? null : createInterface({ input: process.stdin, output: process.stdout });
|
|
796
|
+
const readAnswer = borrowed ? ask : (text) => rl.question(text);
|
|
797
|
+
try {
|
|
798
|
+
for (const artifact of proposals.slice(0, 10)) {
|
|
799
|
+
const proposal = artifact.proposal;
|
|
800
|
+
const title = truncate(String(proposal.title || artifact.title || "Confirm action"), 240);
|
|
801
|
+
write(`\n${paint.yellow("PreMan needs a decision")}\n ${title}\n`);
|
|
802
|
+
const summary = String(proposal.summary || artifact.summary || "").trim();
|
|
803
|
+
if (summary) write(` ${truncate(summary, 1000)}\n`);
|
|
804
|
+
write(` 1. Approve exact action\n 2. Decline\n${paint.dim(" Enter to skip\n")}`);
|
|
805
|
+
const answer = String((await readAnswer(" > ")) || "").trim().toLowerCase();
|
|
806
|
+
const action = ["1", "y", "yes", "approve"].includes(answer) ? "approve"
|
|
807
|
+
: ["2", "n", "no", "decline"].includes(answer) ? "decline" : null;
|
|
808
|
+
if (!action) {
|
|
809
|
+
write(paint.dim(" Left unanswered.\n"));
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (proposal.expires_at && Date.parse(proposal.expires_at) <= Date.now()) {
|
|
813
|
+
write(paint.dim(" This action expired. Ask PreMan to regenerate it.\n"));
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
const controller = new AbortController();
|
|
817
|
+
register?.({ abort: () => controller.abort() });
|
|
818
|
+
try {
|
|
819
|
+
const response = await backendCall(args, "POST",
|
|
820
|
+
`/workbench/action-proposals/${encodeURIComponent(proposal.id)}/${action}`, {
|
|
821
|
+
token, signal: controller.signal,
|
|
822
|
+
headers: workspaceId ? { "x-workspace-id": workspaceId } : undefined,
|
|
823
|
+
});
|
|
824
|
+
const result = response.result;
|
|
825
|
+
const status = response.proposal?.status || "unknown";
|
|
826
|
+
if (result?.ok === false || result?.error) {
|
|
827
|
+
write(` ${paint.red(describeFailure(result, "The action did not complete."))}\n`);
|
|
828
|
+
} else if (status === "completed" || status === "declined") {
|
|
829
|
+
write(` ${paint.green("✓")} ${status === "completed" ? "Completed" : "Declined"}: ${title}\n`);
|
|
830
|
+
if (result?.run?.id) write(` ${artifactLine({ type: "swarm_run", title, run: result.run, url: result.url })}\n`);
|
|
831
|
+
} else {
|
|
832
|
+
write(` Action ${String(status).replaceAll("_", " ")}. Use /app to check its result.\n`);
|
|
833
|
+
}
|
|
834
|
+
} catch (error) {
|
|
835
|
+
write(controller.signal.aborted
|
|
836
|
+
? " Stopped waiting for the action result. Check /app before trying again.\n"
|
|
837
|
+
: ` ${paint.red(error?.message || String(error))}\n`);
|
|
838
|
+
} finally {
|
|
839
|
+
register?.(null);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (proposals.length > 10) write(" More actions are awaiting a decision in /app.\n");
|
|
843
|
+
} finally {
|
|
844
|
+
if (rl) rl.close();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
764
848
|
/**
|
|
765
849
|
* Put a permission confirmation to the person at the keyboard.
|
|
766
850
|
*
|
|
@@ -1956,6 +2040,64 @@ export async function runEval(rest, { args, token, dock, rl, paint, say, running
|
|
|
1956
2040
|
}
|
|
1957
2041
|
}
|
|
1958
2042
|
|
|
2043
|
+
/** Quoted flag values for a session command, without shell evaluation. */
|
|
2044
|
+
export function swarmArguments(rest) {
|
|
2045
|
+
const words = [];
|
|
2046
|
+
let word = "";
|
|
2047
|
+
let quote = "";
|
|
2048
|
+
let escaped = false;
|
|
2049
|
+
let active = false;
|
|
2050
|
+
for (const character of String(rest || "")) {
|
|
2051
|
+
if (escaped) {
|
|
2052
|
+
word += character;
|
|
2053
|
+
escaped = false;
|
|
2054
|
+
} else if (character === "\\" && quote !== "'") {
|
|
2055
|
+
escaped = true;
|
|
2056
|
+
active = true;
|
|
2057
|
+
} else if (quote) {
|
|
2058
|
+
if (character === quote) quote = "";
|
|
2059
|
+
else word += character;
|
|
2060
|
+
} else if (character === "'" || character === '"') {
|
|
2061
|
+
quote = character;
|
|
2062
|
+
active = true;
|
|
2063
|
+
} else if (/\s/.test(character)) {
|
|
2064
|
+
if (active) words.push(word);
|
|
2065
|
+
word = "";
|
|
2066
|
+
active = false;
|
|
2067
|
+
} else {
|
|
2068
|
+
word += character;
|
|
2069
|
+
active = true;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (quote || escaped) throw new Error("Finish the quoted value or escaped character in /swarm.");
|
|
2073
|
+
if (active) words.push(word);
|
|
2074
|
+
return words;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
/** Reuse the CLI command inside the current authenticated conversation. */
|
|
2078
|
+
export async function runSwarm(rest, { args, token, workspaceId, say, paint, register }) {
|
|
2079
|
+
const controller = new AbortController();
|
|
2080
|
+
register?.({ abort: () => controller.abort() });
|
|
2081
|
+
try {
|
|
2082
|
+
const argv = swarmArguments(rest);
|
|
2083
|
+
if (["help", "--help", "-h"].includes(argv[0])) {
|
|
2084
|
+
say(`${SWARM_HELP}\n`);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
await swarmCommand(argv, {
|
|
2088
|
+
sessionArgs: args, token, workspaceId, signal: controller.signal,
|
|
2089
|
+
write: say,
|
|
2090
|
+
});
|
|
2091
|
+
say("\n");
|
|
2092
|
+
} catch (error) {
|
|
2093
|
+
say(controller.signal.aborted
|
|
2094
|
+
? "Stopped waiting. The swarm may still be running; /swarm list to check.\n\n"
|
|
2095
|
+
: `${paint.red(error?.message || String(error))}\n\n`);
|
|
2096
|
+
} finally {
|
|
2097
|
+
register?.(null);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
1959
2101
|
export async function agentCommand(commandArgs = [], { authenticate = authenticateTerminal } = {}) {
|
|
1960
2102
|
const args = makeArgs(commandArgs);
|
|
1961
2103
|
if (args.has("--help") || args.has("-h")) {
|
|
@@ -2067,6 +2209,7 @@ export async function agentCommand(commandArgs = [], { authenticate = authentica
|
|
|
2067
2209
|
// Only reaches anything when a run is open, and the import is already
|
|
2068
2210
|
// resolved by then -- the module was loaded to start the run.
|
|
2069
2211
|
if (evaluating) void import("./eval.js").then(({ cancelEvalRuns }) => cancelEvalRuns());
|
|
2212
|
+
turn?.abort();
|
|
2070
2213
|
});
|
|
2071
2214
|
hold.onTeardown(() => process.stdout.off("resize", onResize));
|
|
2072
2215
|
hold.onTeardown(() => dock.disable());
|
|
@@ -2286,6 +2429,13 @@ export async function agentCommand(commandArgs = [], { authenticate = authentica
|
|
|
2286
2429
|
});
|
|
2287
2430
|
continue;
|
|
2288
2431
|
}
|
|
2432
|
+
if (slash.name === "swarm") {
|
|
2433
|
+
await runSwarm(slash.rest, {
|
|
2434
|
+
args, token, workspaceId: workspace.id, say, paint,
|
|
2435
|
+
register: (handle) => { turn = handle; },
|
|
2436
|
+
});
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2289
2439
|
if (slash.name === "app") {
|
|
2290
2440
|
const url = `${frontendUrl(args)}/workbench/${conversation.id}`;
|
|
2291
2441
|
openUrl(url);
|
|
@@ -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
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* won when it did not.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { setTimeout as wait } from "node:timers/promises";
|
|
13
|
+
import { SwarmProgress, cleanLabel, progressLine } from "./swarm-progress.js";
|
|
14
|
+
|
|
12
15
|
import {
|
|
13
16
|
backendUrl,
|
|
14
17
|
callBackendJson,
|
|
@@ -25,8 +28,9 @@ Swarm options:
|
|
|
25
28
|
preman swarm list Recent swarms and what each found
|
|
26
29
|
preman swarm launch --server <slug|id> --ask "<what a developer asks>" [--ask ...]
|
|
27
30
|
[--expect tool,tool] [--agents claude_code,codex,...] [--rivals 0,5]
|
|
28
|
-
[--size 60] [--name <label>] [--dry-run]
|
|
29
|
-
|
|
31
|
+
[--size 60] [--name <label>] [--dry-run] [--no-watch]
|
|
32
|
+
Launch and follow live actions until done
|
|
33
|
+
--no-watch queues and returns immediately
|
|
30
34
|
preman swarm watch <id> [--once] Follow a swarm until it finishes
|
|
31
35
|
preman swarm stop <id> Stop a swarm; unfinished agents do not run
|
|
32
36
|
preman swarm bank [--server <slug|id>] Behaviors PreMan learned and you kept
|
|
@@ -49,7 +53,7 @@ const AGENT_NAMES = {
|
|
|
49
53
|
};
|
|
50
54
|
|
|
51
55
|
const TERMINAL = new Set(["done", "failed", "cancelled"]);
|
|
52
|
-
const POLL_MS =
|
|
56
|
+
const POLL_MS = 2000;
|
|
53
57
|
|
|
54
58
|
function environmentOf(harness) {
|
|
55
59
|
return ENVIRONMENTS.find((env) => env.harnesses.includes(harness))?.id || "other";
|
|
@@ -87,7 +91,7 @@ export function positionals(raw) {
|
|
|
87
91
|
return out;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
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"]);
|
|
91
95
|
|
|
92
96
|
function listOf(value) {
|
|
93
97
|
return String(value || "")
|
|
@@ -133,17 +137,21 @@ export function summarize(cells) {
|
|
|
133
137
|
};
|
|
134
138
|
if (cell.status === "running") agent.running += 1;
|
|
135
139
|
else if (cell.status === "queued") agent.queued += 1;
|
|
136
|
-
const
|
|
140
|
+
const expected = new Set([
|
|
141
|
+
...(cell.evidence?.expected?.tools || []),
|
|
142
|
+
cell.evidence?.expected?.tool,
|
|
143
|
+
...(cell.evidence?.expected?.alternates || []),
|
|
144
|
+
].filter(Boolean));
|
|
145
|
+
const called = cell.evidence?.tools_called || [];
|
|
146
|
+
// Missing search telemetry cannot erase a recorded selection or call to
|
|
147
|
+
// the expected tool. Codex can call it without emitting a tool search.
|
|
148
|
+
const sawExpected = cell.selected === true || called.some((tool) => expected.has(tool));
|
|
149
|
+
const unseen = !sawExpected && cell.evidence?.tool_surfaced_by_search === false && arm === "deferred";
|
|
137
150
|
if ((cell.selected === true || cell.selected === false) && !unseen) {
|
|
138
151
|
agent.scored += 1;
|
|
139
152
|
if (cell.selected) agent.picked += 1;
|
|
140
153
|
else {
|
|
141
|
-
const
|
|
142
|
-
...(cell.evidence?.expected?.tools || []),
|
|
143
|
-
cell.evidence?.expected?.tool,
|
|
144
|
-
...(cell.evidence?.expected?.alternates || []),
|
|
145
|
-
].filter(Boolean));
|
|
146
|
-
for (const tool of cell.evidence?.tools_called || []) {
|
|
154
|
+
for (const tool of called) {
|
|
147
155
|
if (!expected.has(tool)) agent.lostTo.set(tool, (agent.lostTo.get(tool) || 0) + 1);
|
|
148
156
|
}
|
|
149
157
|
}
|
|
@@ -173,7 +181,7 @@ export function renderSwarm(run, cells, { colour = false } = {}) {
|
|
|
173
181
|
const done = (cells || []).filter((cell) => cell.status === "done" || cell.status === "failed").length;
|
|
174
182
|
const lines = [];
|
|
175
183
|
lines.push(
|
|
176
|
-
`${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)}`
|
|
177
185
|
);
|
|
178
186
|
if (summary.scored) {
|
|
179
187
|
const range = likelyRange(summary.picked, summary.scored);
|
|
@@ -192,8 +200,8 @@ export function renderSwarm(run, cells, { colour = false } = {}) {
|
|
|
192
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));
|
|
193
201
|
const working = agent.running ? paint.dim(` · ${agent.running} working`) : "";
|
|
194
202
|
const rival = [...agent.lostTo.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
195
|
-
const lost = rival ? paint.dim(` · lost to ${rival[0]} ×${rival[1]}`) : "";
|
|
196
|
-
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}`);
|
|
197
205
|
}
|
|
198
206
|
}
|
|
199
207
|
return `${lines.join("\n")}\n`;
|
|
@@ -221,14 +229,16 @@ function requireToken(args) {
|
|
|
221
229
|
async function call(args, token, method, path, options = {}) {
|
|
222
230
|
const result = await callBackendJson(args, method, path, { token, ...options });
|
|
223
231
|
if (!result.ok) {
|
|
224
|
-
|
|
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;
|
|
225
235
|
}
|
|
226
236
|
return result;
|
|
227
237
|
}
|
|
228
238
|
|
|
229
|
-
async function findServer(
|
|
239
|
+
async function findServer(request, ref) {
|
|
230
240
|
if (/^[0-9a-f-]{36}$/i.test(ref)) return { id: ref, name: ref };
|
|
231
|
-
const result = await
|
|
241
|
+
const result = await request("GET", "/hosted-mcps");
|
|
232
242
|
const servers = result.hosted_mcps || result.list || [];
|
|
233
243
|
const match = servers.find((server) => [server.slug, server.name, server.id].includes(ref));
|
|
234
244
|
if (!match) {
|
|
@@ -238,9 +248,9 @@ async function findServer(args, token, ref) {
|
|
|
238
248
|
return match;
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
async function readSwarm(
|
|
242
|
-
const run = await
|
|
243
|
-
const cells = await
|
|
251
|
+
async function readSwarm(request, id) {
|
|
252
|
+
const run = await request("GET", `/matrix/runs/${encodeURIComponent(id)}`);
|
|
253
|
+
const cells = await request("GET", `/matrix/runs/${encodeURIComponent(id)}/cells`);
|
|
244
254
|
const { status_code, ok, ...runBody } = run;
|
|
245
255
|
return { run: runBody, cells: cells.cells || cells.list || [] };
|
|
246
256
|
}
|
|
@@ -249,25 +259,131 @@ function swarmLink(args, id) {
|
|
|
249
259
|
return `${frontendUrl(args)}/workbench/swarms/swarms/${encodeURIComponent(id)}`;
|
|
250
260
|
}
|
|
251
261
|
|
|
252
|
-
|
|
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
|
+
}
|
|
253
344
|
|
|
254
|
-
export async function swarmCommand(commandArgs = []
|
|
345
|
+
export async function swarmCommand(commandArgs = [], {
|
|
346
|
+
sessionArgs, token: sessionToken, workspaceId, signal,
|
|
347
|
+
write = (text) => process.stdout.write(text), pollMs = POLL_MS,
|
|
348
|
+
} = {}) {
|
|
255
349
|
const [sub = "list", ...rest] = commandArgs;
|
|
256
|
-
|
|
350
|
+
// A session's credential stays with its backend even when slash-command
|
|
351
|
+
// arguments contain transport overrides. First occurrence wins in makeArgs.
|
|
352
|
+
const args = makeArgs(sessionArgs ? [
|
|
353
|
+
"--backend", backendUrl(sessionArgs), "--frontend", frontendUrl(sessionArgs),
|
|
354
|
+
...(sessionArgs.has("--no-color") || sessionArgs.has("--no-colour") ? ["--no-color"] : []), ...rest,
|
|
355
|
+
] : rest);
|
|
257
356
|
const json = args.has("--json");
|
|
258
357
|
const colour = colourEnabled(args);
|
|
259
|
-
const token = requireToken(args);
|
|
358
|
+
const token = sessionToken || requireToken(args);
|
|
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
|
+
};
|
|
260
376
|
|
|
261
377
|
if (sub === "list") {
|
|
262
|
-
const result = await
|
|
378
|
+
const result = await request("GET", "/matrix/runs");
|
|
263
379
|
const runs = result.runs || result.list || [];
|
|
264
|
-
if (json) return void
|
|
380
|
+
if (json) return void write(`${JSON.stringify(runs, null, 2)}\n`);
|
|
265
381
|
if (!runs.length) {
|
|
266
|
-
|
|
382
|
+
write(`No swarms yet. Start one with \`${cliInvocation()} swarm launch\`.\n`);
|
|
267
383
|
return;
|
|
268
384
|
}
|
|
269
385
|
for (const run of runs.slice(0, 20)) {
|
|
270
|
-
|
|
386
|
+
write(
|
|
271
387
|
`${String(run.id).slice(0, 8)} ${truncate(run.release_label || "", 32).padEnd(32)} ` +
|
|
272
388
|
`${truncate(run.hosted_mcp_name || run.hosted_mcp_slug || "", 20).padEnd(20)} ${String(run.status).padEnd(9)} ${headlineFromRollup(run)}\n`
|
|
273
389
|
);
|
|
@@ -281,7 +397,8 @@ export async function swarmCommand(commandArgs = []) {
|
|
|
281
397
|
if (!serverRef || !asks.length) {
|
|
282
398
|
throw new Error(`a swarm needs --server and at least one --ask.${SWARM_HELP}`);
|
|
283
399
|
}
|
|
284
|
-
|
|
400
|
+
if (!json && !args.has("--dry-run")) write(`Resolving ${cleanLabel(serverRef)}…\n`);
|
|
401
|
+
const server = await findServer(request, serverRef);
|
|
285
402
|
const expect = listOf(args.value("--expect"));
|
|
286
403
|
const payload = {
|
|
287
404
|
hosted_mcp_id: server.id,
|
|
@@ -296,60 +413,50 @@ export async function swarmCommand(commandArgs = []) {
|
|
|
296
413
|
const agents = listOf(args.value("--agents"));
|
|
297
414
|
if (agents.length) payload.harnesses = agents;
|
|
298
415
|
if (args.has("--dry-run")) payload.dry_run = true;
|
|
299
|
-
|
|
416
|
+
if (!json && !payload.dry_run) write("Queueing swarm…\n");
|
|
417
|
+
const result = await request("POST", "/matrix/runs", { json: payload });
|
|
300
418
|
const { status_code, ok, ...body } = result;
|
|
301
|
-
if (json) return void
|
|
419
|
+
if (json) return void write(`${JSON.stringify(body, null, 2)}\n`);
|
|
302
420
|
if (payload.dry_run) {
|
|
303
|
-
|
|
421
|
+
write(`Would send ${body.cell_count ?? "?"} attempts (${body.asks_used ?? asks.length} asks, ${body.replicates ?? 1}× each).\n`);
|
|
304
422
|
return;
|
|
305
423
|
}
|
|
306
|
-
|
|
307
|
-
|
|
424
|
+
write(`Swarm ${cleanLabel(body.id)} launched against ${cleanLabel(server.name || server.slug)}.\n`);
|
|
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 });
|
|
308
427
|
return;
|
|
309
428
|
}
|
|
310
429
|
|
|
311
430
|
if (sub === "watch" || sub === "show") {
|
|
312
431
|
const [id] = positionals(rest);
|
|
313
432
|
if (!id) throw new Error(`which swarm? ${cliInvocation()} swarm watch <id>`);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (json) {
|
|
317
|
-
process.stdout.write(`${JSON.stringify({ run, summary: summarize(cells) }, (k, v) => (v instanceof Map ? Object.fromEntries(v) : v), 2)}\n`);
|
|
318
|
-
} else {
|
|
319
|
-
if (process.stdout.isTTY && sub === "watch") process.stdout.write("\u001b[2J\u001b[H");
|
|
320
|
-
process.stdout.write(renderSwarm(run, cells, { colour }));
|
|
321
|
-
}
|
|
322
|
-
if (sub === "show" || args.has("--once") || TERMINAL.has(run.status)) {
|
|
323
|
-
if (!json) process.stdout.write(`\n${swarmLink(args, id)}\n`);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
await sleep(POLL_MS);
|
|
327
|
-
}
|
|
433
|
+
await followSwarm(id, { args, request, write, colour, signal, pollMs, json, once: sub === "show" || args.has("--once") || json });
|
|
434
|
+
return;
|
|
328
435
|
}
|
|
329
436
|
|
|
330
437
|
if (sub === "stop") {
|
|
331
438
|
const [id] = positionals(rest);
|
|
332
439
|
if (!id) throw new Error(`which swarm? ${cliInvocation()} swarm stop <id>`);
|
|
333
|
-
await
|
|
334
|
-
|
|
440
|
+
await request("POST", `/matrix/runs/${encodeURIComponent(id)}/cancel`);
|
|
441
|
+
write(`Stopped ${id}. Agents that had not finished will not run.\n`);
|
|
335
442
|
return;
|
|
336
443
|
}
|
|
337
444
|
|
|
338
445
|
if (sub === "bank") {
|
|
339
446
|
const serverRef = args.value("--server");
|
|
340
|
-
const server = serverRef ? await findServer(
|
|
341
|
-
const result = await
|
|
447
|
+
const server = serverRef ? await findServer(request, serverRef) : null;
|
|
448
|
+
const result = await request("GET", "/swarms/behaviors", {
|
|
342
449
|
query: server ? { hosted_mcp_id: server.id } : undefined,
|
|
343
450
|
});
|
|
344
451
|
const kept = (result.behaviors || []).filter((row) => row.status === "kept");
|
|
345
|
-
if (json) return void
|
|
452
|
+
if (json) return void write(`${JSON.stringify(kept, null, 2)}\n`);
|
|
346
453
|
if (!kept.length) {
|
|
347
|
-
|
|
454
|
+
write("Nothing kept yet. After a swarm, keep the patterns PreMan finds in its misses.\n");
|
|
348
455
|
return;
|
|
349
456
|
}
|
|
350
457
|
for (const row of kept) {
|
|
351
|
-
|
|
352
|
-
for (const ask of (row.seed?.asks || []).slice(0, 3))
|
|
458
|
+
write(`${row.hosted_mcp_name || ""} ${row.title} (${row.evidence?.count || 0} attempts)\n`);
|
|
459
|
+
for (const ask of (row.seed?.asks || []).slice(0, 3)) write(` “${truncate(ask.prompt, 70)}”\n`);
|
|
353
460
|
}
|
|
354
461
|
return;
|
|
355
462
|
}
|
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": {
|