premanmcp 1.1.17 → 1.1.18

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 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,30 @@ 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
+ `/swarm help` lists every option. Add `--dry-run` to preview a launch without
87
+ starting attempts. Ctrl+C while watching returns to the conversation and leaves
88
+ the swarm running; `/swarm stop <run-id>` cancels it explicitly. The same commands
89
+ work outside a session as `preman swarm list|launch|watch|stop|bank`. These run
90
+ real agents against hosted MCPs; the app's sample swarms remain a separate demo.
91
+
68
92
  A session takes a screen of its own, the way `vim` does: nothing above it is
69
93
  scrollable, and leaving hands your shell back exactly as it was with one line
70
94
  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, clearWatch: false,
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);
package/bin/swarm.js CHANGED
@@ -9,6 +9,8 @@
9
9
  * won when it did not.
10
10
  */
11
11
 
12
+ import { setTimeout as wait } from "node:timers/promises";
13
+
12
14
  import {
13
15
  backendUrl,
14
16
  callBackendJson,
@@ -133,17 +135,21 @@ export function summarize(cells) {
133
135
  };
134
136
  if (cell.status === "running") agent.running += 1;
135
137
  else if (cell.status === "queued") agent.queued += 1;
136
- const unseen = cell.evidence?.tool_surfaced_by_search === false && arm === "deferred";
138
+ const expected = new Set([
139
+ ...(cell.evidence?.expected?.tools || []),
140
+ cell.evidence?.expected?.tool,
141
+ ...(cell.evidence?.expected?.alternates || []),
142
+ ].filter(Boolean));
143
+ const called = cell.evidence?.tools_called || [];
144
+ // Missing search telemetry cannot erase a recorded selection or call to
145
+ // the expected tool. Codex can call it without emitting a tool search.
146
+ const sawExpected = cell.selected === true || called.some((tool) => expected.has(tool));
147
+ const unseen = !sawExpected && cell.evidence?.tool_surfaced_by_search === false && arm === "deferred";
137
148
  if ((cell.selected === true || cell.selected === false) && !unseen) {
138
149
  agent.scored += 1;
139
150
  if (cell.selected) agent.picked += 1;
140
151
  else {
141
- const expected = new Set([
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 || []) {
152
+ for (const tool of called) {
147
153
  if (!expected.has(tool)) agent.lostTo.set(tool, (agent.lostTo.get(tool) || 0) + 1);
148
154
  }
149
155
  }
@@ -226,9 +232,9 @@ async function call(args, token, method, path, options = {}) {
226
232
  return result;
227
233
  }
228
234
 
229
- async function findServer(args, token, ref) {
235
+ async function findServer(request, ref) {
230
236
  if (/^[0-9a-f-]{36}$/i.test(ref)) return { id: ref, name: ref };
231
- const result = await call(args, token, "GET", "/hosted-mcps");
237
+ const result = await request("GET", "/hosted-mcps");
232
238
  const servers = result.hosted_mcps || result.list || [];
233
239
  const match = servers.find((server) => [server.slug, server.name, server.id].includes(ref));
234
240
  if (!match) {
@@ -238,9 +244,9 @@ async function findServer(args, token, ref) {
238
244
  return match;
239
245
  }
240
246
 
241
- async function readSwarm(args, token, id) {
242
- const run = await call(args, token, "GET", `/matrix/runs/${encodeURIComponent(id)}`);
243
- const cells = await call(args, token, "GET", `/matrix/runs/${encodeURIComponent(id)}/cells`);
247
+ async function readSwarm(request, id) {
248
+ const run = await request("GET", `/matrix/runs/${encodeURIComponent(id)}`);
249
+ const cells = await request("GET", `/matrix/runs/${encodeURIComponent(id)}/cells`);
244
250
  const { status_code, ok, ...runBody } = run;
245
251
  return { run: runBody, cells: cells.cells || cells.list || [] };
246
252
  }
@@ -249,25 +255,34 @@ function swarmLink(args, id) {
249
255
  return `${frontendUrl(args)}/workbench/swarms/swarms/${encodeURIComponent(id)}`;
250
256
  }
251
257
 
252
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
253
-
254
- export async function swarmCommand(commandArgs = []) {
258
+ export async function swarmCommand(commandArgs = [], {
259
+ sessionArgs, token: sessionToken, workspaceId, signal,
260
+ write = (text) => process.stdout.write(text), clearWatch = true,
261
+ } = {}) {
255
262
  const [sub = "list", ...rest] = commandArgs;
256
- const args = makeArgs(rest);
263
+ // A session's credential stays with its backend even when slash-command
264
+ // arguments contain transport overrides. First occurrence wins in makeArgs.
265
+ const args = makeArgs(sessionArgs ? [
266
+ "--backend", backendUrl(sessionArgs), "--frontend", frontendUrl(sessionArgs),
267
+ ...(sessionArgs.has("--no-color") || sessionArgs.has("--no-colour") ? ["--no-color"] : []), ...rest,
268
+ ] : rest);
257
269
  const json = args.has("--json");
258
270
  const colour = colourEnabled(args);
259
- const token = requireToken(args);
271
+ const token = sessionToken || requireToken(args);
272
+ const request = (method, path, options = {}) => call(args, token, method, path, {
273
+ ...options, signal, ...(workspaceId ? { headers: { "x-workspace-id": workspaceId } } : {}),
274
+ });
260
275
 
261
276
  if (sub === "list") {
262
- const result = await call(args, token, "GET", "/matrix/runs");
277
+ const result = await request("GET", "/matrix/runs");
263
278
  const runs = result.runs || result.list || [];
264
- if (json) return void process.stdout.write(`${JSON.stringify(runs, null, 2)}\n`);
279
+ if (json) return void write(`${JSON.stringify(runs, null, 2)}\n`);
265
280
  if (!runs.length) {
266
- process.stdout.write(`No swarms yet. Start one with \`${cliInvocation()} swarm launch\`.\n`);
281
+ write(`No swarms yet. Start one with \`${cliInvocation()} swarm launch\`.\n`);
267
282
  return;
268
283
  }
269
284
  for (const run of runs.slice(0, 20)) {
270
- process.stdout.write(
285
+ write(
271
286
  `${String(run.id).slice(0, 8)} ${truncate(run.release_label || "", 32).padEnd(32)} ` +
272
287
  `${truncate(run.hosted_mcp_name || run.hosted_mcp_slug || "", 20).padEnd(20)} ${String(run.status).padEnd(9)} ${headlineFromRollup(run)}\n`
273
288
  );
@@ -281,7 +296,7 @@ export async function swarmCommand(commandArgs = []) {
281
296
  if (!serverRef || !asks.length) {
282
297
  throw new Error(`a swarm needs --server and at least one --ask.${SWARM_HELP}`);
283
298
  }
284
- const server = await findServer(args, token, serverRef);
299
+ const server = await findServer(request, serverRef);
285
300
  const expect = listOf(args.value("--expect"));
286
301
  const payload = {
287
302
  hosted_mcp_id: server.id,
@@ -296,15 +311,15 @@ export async function swarmCommand(commandArgs = []) {
296
311
  const agents = listOf(args.value("--agents"));
297
312
  if (agents.length) payload.harnesses = agents;
298
313
  if (args.has("--dry-run")) payload.dry_run = true;
299
- const result = await call(args, token, "POST", "/matrix/runs", { json: payload });
314
+ const result = await request("POST", "/matrix/runs", { json: payload });
300
315
  const { status_code, ok, ...body } = result;
301
- if (json) return void process.stdout.write(`${JSON.stringify(body, null, 2)}\n`);
316
+ if (json) return void write(`${JSON.stringify(body, null, 2)}\n`);
302
317
  if (payload.dry_run) {
303
- process.stdout.write(`Would send ${body.cell_count ?? "?"} attempts (${body.asks_used ?? asks.length} asks, ${body.replicates ?? 1}× each).\n`);
318
+ write(`Would send ${body.cell_count ?? "?"} attempts (${body.asks_used ?? asks.length} asks, ${body.replicates ?? 1}× each).\n`);
304
319
  return;
305
320
  }
306
- process.stdout.write(`Swarm ${body.id} launched against ${server.name || server.slug}.\n`);
307
- process.stdout.write(` Watch: ${cliInvocation()} swarm watch ${body.id}\n Open: ${swarmLink(args, body.id)}\n`);
321
+ write(`Swarm ${body.id} launched against ${server.name || server.slug}.\n`);
322
+ write(` Watch: ${cliInvocation()} swarm watch ${body.id}\n Open: ${swarmLink(args, body.id)}\n`);
308
323
  return;
309
324
  }
310
325
 
@@ -312,44 +327,44 @@ export async function swarmCommand(commandArgs = []) {
312
327
  const [id] = positionals(rest);
313
328
  if (!id) throw new Error(`which swarm? ${cliInvocation()} swarm watch <id>`);
314
329
  for (;;) {
315
- const { run, cells } = await readSwarm(args, token, id);
330
+ const { run, cells } = await readSwarm(request, id);
316
331
  if (json) {
317
- process.stdout.write(`${JSON.stringify({ run, summary: summarize(cells) }, (k, v) => (v instanceof Map ? Object.fromEntries(v) : v), 2)}\n`);
332
+ write(`${JSON.stringify({ run, summary: summarize(cells) }, (k, v) => (v instanceof Map ? Object.fromEntries(v) : v), 2)}\n`);
318
333
  } else {
319
- if (process.stdout.isTTY && sub === "watch") process.stdout.write("\u001b[2J\u001b[H");
320
- process.stdout.write(renderSwarm(run, cells, { colour }));
334
+ if (clearWatch && process.stdout.isTTY && sub === "watch") write("\u001b[2J\u001b[H");
335
+ write(renderSwarm(run, cells, { colour }));
321
336
  }
322
337
  if (sub === "show" || args.has("--once") || TERMINAL.has(run.status)) {
323
- if (!json) process.stdout.write(`\n${swarmLink(args, id)}\n`);
338
+ if (!json) write(`\n${swarmLink(args, id)}\n`);
324
339
  return;
325
340
  }
326
- await sleep(POLL_MS);
341
+ await wait(POLL_MS, undefined, { signal });
327
342
  }
328
343
  }
329
344
 
330
345
  if (sub === "stop") {
331
346
  const [id] = positionals(rest);
332
347
  if (!id) throw new Error(`which swarm? ${cliInvocation()} swarm stop <id>`);
333
- await call(args, token, "POST", `/matrix/runs/${encodeURIComponent(id)}/cancel`);
334
- process.stdout.write(`Stopped ${id}. Agents that had not finished will not run.\n`);
348
+ await request("POST", `/matrix/runs/${encodeURIComponent(id)}/cancel`);
349
+ write(`Stopped ${id}. Agents that had not finished will not run.\n`);
335
350
  return;
336
351
  }
337
352
 
338
353
  if (sub === "bank") {
339
354
  const serverRef = args.value("--server");
340
- const server = serverRef ? await findServer(args, token, serverRef) : null;
341
- const result = await call(args, token, "GET", "/swarms/behaviors", {
355
+ const server = serverRef ? await findServer(request, serverRef) : null;
356
+ const result = await request("GET", "/swarms/behaviors", {
342
357
  query: server ? { hosted_mcp_id: server.id } : undefined,
343
358
  });
344
359
  const kept = (result.behaviors || []).filter((row) => row.status === "kept");
345
- if (json) return void process.stdout.write(`${JSON.stringify(kept, null, 2)}\n`);
360
+ if (json) return void write(`${JSON.stringify(kept, null, 2)}\n`);
346
361
  if (!kept.length) {
347
- process.stdout.write("Nothing kept yet. After a swarm, keep the patterns PreMan finds in its misses.\n");
362
+ write("Nothing kept yet. After a swarm, keep the patterns PreMan finds in its misses.\n");
348
363
  return;
349
364
  }
350
365
  for (const row of kept) {
351
- process.stdout.write(`${row.hosted_mcp_name || ""} ${row.title} (${row.evidence?.count || 0} attempts)\n`);
352
- for (const ask of (row.seed?.asks || []).slice(0, 3)) process.stdout.write(` “${truncate(ask.prompt, 70)}”\n`);
366
+ write(`${row.hosted_mcp_name || ""} ${row.title} (${row.evidence?.count || 0} attempts)\n`);
367
+ for (const ask of (row.seed?.asks || []).slice(0, 3)) write(` “${truncate(ask.prompt, 70)}”\n`);
353
368
  }
354
369
  return;
355
370
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "1.1.17",
3
+ "version": "1.1.18",
4
4
  "description": "PreMan CLI and stdio proxy for PreMan's hosted MCP server",
5
5
  "type": "module",
6
6
  "bin": {