premanmcp 0.11.0 → 0.13.0

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
@@ -4,14 +4,24 @@ Turn your APIs into MCP tools that coding agents can discover, call, test, and a
4
4
 
5
5
  PreMan is agent-first API infrastructure. It lets backend teams expose endpoints to AI coding agents through MCP, add an auth layer around those tools, and see exactly which agent called what.
6
6
 
7
- ## Connect
7
+ ## Start
8
+
9
+ ```bash
10
+ npm exec -y premanmcp@latest -- onboard
11
+ ```
12
+
13
+ Create the account, verify the email, and get the PreMan app installed and opened
14
+ already signed in. A bare `preman` in a terminal does the same thing.
15
+
16
+ ## Connect a coding agent
8
17
 
9
18
  ```bash
10
19
  npm exec -y premanmcp@latest -- connect
11
20
  ```
12
21
 
13
- Pick your coding agent from the list — Cursor, Claude Code, or Codex — and PreMan
14
- writes that agent's MCP config for you. No hand-edited config anywhere.
22
+ Optional, and separate from starting out. Pick your coding agent from the list — Cursor,
23
+ Claude Code, or Codex — and PreMan writes that agent's MCP config for you. No hand-edited
24
+ config anywhere.
15
25
 
16
26
  Local development form:
17
27
 
@@ -82,10 +92,18 @@ preman github # or connect it in the dashboard
82
92
  preman status # which of those are done
83
93
  ```
84
94
 
85
- `preman onboard` (or `setup`) is the prompted walk through all of it sign in, coding
86
- agent, endpoints, runner, GitHub, AWS, Slack one question per step, `b` to go back, and a
87
- summary at the end. `connect --guide` runs the old full pass inside connect itself:
88
- discovery, a first test, the runner, the desktop app and the integration prompts.
95
+ `preman onboard` (or `setup`, or a bare `preman` in a terminal) is what someone starting
96
+ out runs: create the account and verify the email, then install the PreMan app and open it
97
+ already signed in, then GitHub, AWS, Slack one question per step, `b` to go back, and a
98
+ summary at the end. Connecting a coding agent is no longer part of starting out; run
99
+ `preman connect` when you actually want one wired into an IDE. `connect --guide` still runs
100
+ the full pass inside connect itself: discovery, a first test, the runner, the desktop app
101
+ and the integration prompts.
102
+
103
+ The app opens signed in because the CLI leaves the key it just minted in
104
+ `~/.preman/desktop-session.json`, which the app reads once and deletes. An app too old to
105
+ look for it, or a machine that is not macOS, falls back to signing in on the app's own
106
+ login screen with the account you just created.
89
107
 
90
108
  Useful flags: `--agent cursor|claude-code|codex` skips the picker, `--project` writes
91
109
  project-local config, `--print` shows the config without writing it, `--no-hook` leaves
package/bin/cli.js CHANGED
@@ -22,7 +22,6 @@ import {
22
22
  CONNECT_HELP,
23
23
  DISPATCH_HELP,
24
24
  connectCommand,
25
- discoverEndpoints,
26
25
  dispatchCommand,
27
26
  resolveAgentForPairing,
28
27
  writeCursorConfig,
@@ -39,7 +38,7 @@ import { STATUS_HELP, statusCommand } from "./status.js";
39
38
  import { HOOK_HELP, hookCommand, scheduleHookRepair } from "./hook.js";
40
39
  import { RUNNER_HELP, runnerCommand } from "./runner.js";
41
40
  import { VERIFY_HELP, verifyCommand } from "./verify.js";
42
- import { DESKTOP_HELP, installDesktopCommand } from "./desktop.js";
41
+ import { DESKTOP_HELP, installDesktopCommand, openDesktopSignedIn } from "./desktop.js";
43
42
  import { ACCOUNT_HELP, doctorCommand, loginBrowser, logoutCommand, watchCommand } from "./account.js";
44
43
  import {
45
44
  CREDENTIALS_FILE,
@@ -57,8 +56,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
57
56
  const ROOT = path.join(__dirname, "..");
58
57
 
59
58
  const args = process.argv.slice(2);
59
+ // A bare `preman` in a terminal is someone starting out, so it runs setup:
60
+ // account first, then the app. Piped or redirected (no TTY) it is an MCP host
61
+ // launching the server, which must stay the behaviour for `start`.
60
62
  const command = args.length === 0 && process.stdin.isTTY
61
- ? "connect"
63
+ ? "onboard"
62
64
  : args[0] === "--help" || args[0] === "-h"
63
65
  ? "help"
64
66
  : args[0] && !args[0].startsWith("-")
@@ -89,8 +91,8 @@ function printHelp() {
89
91
  ["runner start|status|stop", "Run PreMan's queued agent work on this machine"],
90
92
  ["doctor", "Diagnose credentials, backend, target, integrations"],
91
93
  ["install-desktop", "Download and install the PreMan desktop app"],
92
- ["onboard", "Sign in, then agent, endpoints, runner, GitHub, AWS, Slack"],
93
- ["connect [options]", "Pick a coding agent and connect it"],
94
+ ["onboard", "Create an account, install the app signed in, then integrations"],
95
+ ["connect [options]", "Pick a coding agent and connect it (optional)"],
94
96
  ["dispatch [options]", "Let PreMan start agent runs for you"],
95
97
  ["aws | github | slack", "Connect one integration on its own"],
96
98
  ["login [--browser]", "Create/login to PreMan from the terminal"],
@@ -254,14 +256,13 @@ async function main() {
254
256
  } else if (command === "dispatch") {
255
257
  await dispatchCommand(commandArgs);
256
258
  } else if (command === "onboard" || command === "setup") {
257
- // makeArgs/authenticateTerminal/connectCommand are injected rather than
259
+ // makeArgs/authenticateTerminal/the desktop pair are injected rather than
258
260
  // imported there, so integrations.js stays free of a cycle back into the CLI.
259
261
  await onboardCommand(commandArgs, {
260
262
  makeArgs,
261
263
  authenticateTerminal,
262
- connectCommand,
263
- discoverEndpoints,
264
- runnerCommand,
264
+ installDesktop: installDesktopCommand,
265
+ openDesktopSignedIn,
265
266
  });
266
267
  } else if (command === "aws") {
267
268
  await awsCommand(makeArgs(commandArgs));
package/bin/desktop.js CHANGED
@@ -13,11 +13,20 @@
13
13
 
14
14
  import { spawn, spawnSync } from "node:child_process";
15
15
  import { createHash } from "node:crypto";
16
- import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
16
+ import {
17
+ chmodSync,
18
+ existsSync,
19
+ mkdirSync,
20
+ mkdtempSync,
21
+ readFileSync,
22
+ readdirSync,
23
+ rmSync,
24
+ writeFileSync,
25
+ } from "node:fs";
17
26
  import os from "node:os";
18
27
  import path from "node:path";
19
28
 
20
- import { cliInvocation, makeArgs } from "./shared.js";
29
+ import { CREDENTIALS_DIR, cliInvocation, makeArgs } from "./shared.js";
21
30
 
22
31
  export const DESKTOP_HELP = `
23
32
  Install-desktop options:
@@ -32,6 +41,80 @@ const RELEASE_API = "https://api.github.com/repos/PreMan-Inc/PreMan-Desktop/rele
32
41
  export const APP_NAME = "PreMan.app";
33
42
  const DOWNLOAD_TIMEOUT_MS = 300_000;
34
43
 
44
+ /**
45
+ * Where the desktop app looks for a session the CLI just established.
46
+ *
47
+ * The key travels through a file rather than the `preman://` URL because a URL
48
+ * is handed to LaunchServices, which records it; the file sits in the directory
49
+ * that already holds the same key, with the same 0600 mode, so this adds no new
50
+ * class of exposure. The app deletes it on read, which is what makes it
51
+ * single-use -- the CLI cannot know whether an installed app is new enough to
52
+ * consume it, so expiry is enforced on the reading side.
53
+ */
54
+ export const DESKTOP_SESSION_FILE = path.join(CREDENTIALS_DIR, "desktop-session.json");
55
+
56
+ export function installedAppPath(destination = "/Applications") {
57
+ return path.join(destination, APP_NAME);
58
+ }
59
+
60
+ export function desktopAppInstalled(destination = "/Applications") {
61
+ return process.platform === "darwin" && existsSync(installedAppPath(destination));
62
+ }
63
+
64
+ /**
65
+ * Hand the account the CLI just signed in to over to the desktop app.
66
+ *
67
+ * Returns what actually happened rather than a boolean, because the caller has
68
+ * something different to say in each case: an app that is not installed is not
69
+ * a failure, it is the fallback where the customer signs in themselves.
70
+ */
71
+ export function writeDesktopSession(creds) {
72
+ const apiKey = String(creds?.api_key || "").trim();
73
+ if (!apiKey.startsWith("pm_live_")) return { state: "no-key" };
74
+ mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
75
+ writeFileSync(
76
+ DESKTOP_SESSION_FILE,
77
+ `${JSON.stringify({ api_key: apiKey, user_email: creds?.user_email ?? null, created_at: new Date().toISOString() }, null, 2)}\n`,
78
+ { mode: 0o600 }
79
+ );
80
+ return { state: "written", path: DESKTOP_SESSION_FILE };
81
+ }
82
+
83
+ /**
84
+ * Install-or-not aside, get the customer into the app signed in.
85
+ *
86
+ * The session is written before the app is launched so a cold start finds it on
87
+ * first read, rather than racing a window that is already loading.
88
+ *
89
+ * Whether it was taken up is then waited for rather than assumed: the app
90
+ * deletes the file as it reads it, and an app too old to know about the file at
91
+ * all would otherwise be reported as signed in while the customer looks at a
92
+ * login screen. The file is left behind on timeout -- it expires on its own, and
93
+ * a slow first launch can still find it.
94
+ */
95
+ export async function openDesktopSignedIn(
96
+ creds,
97
+ { destination = "/Applications", waitMs = 12_000, sleep = defaultSleep } = {}
98
+ ) {
99
+ if (!desktopAppInstalled(destination)) {
100
+ return { state: "not-installed" };
101
+ }
102
+ const handoff = writeDesktopSession(creds);
103
+ spawn("open", ["-a", "PreMan"], { stdio: "ignore", detached: true }).unref();
104
+ if (handoff.state !== "written") return { state: "opened", handoff: handoff.state };
105
+
106
+ const deadline = Date.now() + waitMs;
107
+ while (Date.now() < deadline) {
108
+ await sleep(500);
109
+ if (!existsSync(DESKTOP_SESSION_FILE)) {
110
+ return { state: "opened-signed-in", handoff: handoff.state };
111
+ }
112
+ }
113
+ return { state: "opened-not-adopted", handoff: handoff.state };
114
+ }
115
+
116
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
117
+
35
118
  /**
36
119
  * Open a Playground session URL in PreMan.app on macOS when it is installed.
37
120
  * Windows/Linux (and Mac without the app) get the website URL printed instead.
@@ -158,6 +241,34 @@ function mountedVolume(hdiutilOutput) {
158
241
  return match[1].trim();
159
242
  }
160
243
 
244
+ /**
245
+ * Attach the disk image somewhere we named, rather than wherever it landed.
246
+ *
247
+ * Asking for the mountpoint is what makes this unambiguous. Reading it back out
248
+ * of `hdiutil info` is not a substitute: that lists every image attached on the
249
+ * machine, so an unrelated volume -- an iOS restore image, someone else's dmg --
250
+ * could be searched for PreMan.app instead of the download.
251
+ */
252
+ function attachDiskImage(dmgPath, mountPoint) {
253
+ mkdirSync(mountPoint, { recursive: true });
254
+ try {
255
+ run("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet", "-readonly", "-mountpoint", mountPoint]);
256
+ return mountPoint;
257
+ } catch {
258
+ // Images that refuse an explicit mountpoint still report where they went, so
259
+ // fall back to this attach's own output -- never to the global image list.
260
+ return mountedVolume(run("hdiutil", ["attach", dmgPath, "-nobrowse", "-readonly"]));
261
+ }
262
+ }
263
+
264
+ /** The bundle to copy out, found by name and then by extension. */
265
+ function appInsideVolume(volume) {
266
+ const named = path.join(volume, APP_NAME);
267
+ if (existsSync(named)) return named;
268
+ const bundle = readdirSync(volume).find((entry) => entry.endsWith(".app"));
269
+ return bundle ? path.join(volume, bundle) : null;
270
+ }
271
+
161
272
  export async function installDesktopCommand(commandArgs = []) {
162
273
  const args = makeArgs(commandArgs);
163
274
 
@@ -212,12 +323,11 @@ export async function installDesktopCommand(commandArgs = []) {
212
323
  process.stdout.write(" checksum unavailable for this release; skipping verification\n");
213
324
  }
214
325
 
215
- const attach = run("hdiutil", ["attach", dmgPath, "-nobrowse", "-quiet", "-readonly"]);
216
- mounted = mountedVolume(attach || run("hdiutil", ["info"]));
326
+ mounted = attachDiskImage(dmgPath, path.join(workDir, "mnt"));
217
327
 
218
- const source = path.join(mounted, APP_NAME);
219
- if (!existsSync(source)) {
220
- throw new Error(`${APP_NAME} not found inside the disk image at ${mounted}`);
328
+ const source = appInsideVolume(mounted);
329
+ if (!source) {
330
+ throw new Error(`no application bundle found inside the disk image at ${mounted}`);
221
331
  }
222
332
  const target = path.join(destination, APP_NAME);
223
333
  if (existsSync(target)) rmSync(target, { recursive: true, force: true });
@@ -225,8 +335,8 @@ export async function installDesktopCommand(commandArgs = []) {
225
335
  chmodSync(target, 0o755);
226
336
 
227
337
  process.stdout.write(
228
- `\nInstalled ${target}\n\nNext: open PreMan and it will pair with this account.\n` +
229
- `Then run \`${cliInvocation()} status\` to see endpoint health.\n`
338
+ `\nInstalled ${target}\n\nNext: open PreMan and sign in with the account you use here.\n` +
339
+ `\`${cliInvocation()} onboard\` installs it and opens it already signed in.\n`
230
340
  );
231
341
  return { state: "installed", version, arch, path: target };
232
342
  } finally {
@@ -337,17 +337,6 @@ export async function slackCommand(args) {
337
337
  // The guided run
338
338
  // ---------------------------------------------------------------------------
339
339
 
340
- /**
341
- * Why a step that needs a connected agent cannot run on its own.
342
- *
343
- * Both of these drive the agent `connect` just linked, so skipping that step
344
- * leaves them without one -- which is a thing to say plainly, with the command
345
- * that does it later, rather than a stack trace about a missing id.
346
- */
347
- function needsAgent(command) {
348
- return `no coding agent connected yet -- run '${cliInvocation()} connect', then '${cliInvocation()} ${command}'`;
349
- }
350
-
351
340
  /** "yes" | "no" | "back" -- back only offered once there is somewhere to go. */
352
341
  async function askStep(question, { assumeYes, canGoBack }) {
353
342
  if (assumeYes) return "yes";
@@ -368,7 +357,7 @@ async function askStep(question, { assumeYes, canGoBack }) {
368
357
  */
369
358
  export async function onboardCommand(
370
359
  commandArgs,
371
- { makeArgs, authenticateTerminal, connectCommand, discoverEndpoints, runnerCommand }
360
+ { makeArgs, authenticateTerminal, installDesktop, openDesktopSignedIn }
372
361
  ) {
373
362
  const args = makeArgs(commandArgs);
374
363
  const assumeYes = args.has("--yes");
@@ -378,47 +367,45 @@ export async function onboardCommand(
378
367
  const creds = await authenticateTerminal(args);
379
368
  connected(`Signed in as ${creds.user_email || "your account"}.`);
380
369
 
381
- // Which agent the endpoints and runner steps drive. `connect` decided it, by
382
- // detection or by asking, and this is the answer rather than a second prompt.
383
- let linked = null;
384
-
385
370
  const steps = [
386
371
  {
387
- name: "coding agent",
388
- question: "Connect your coding agent?",
372
+ name: "PreMan app",
373
+ question: "Install the PreMan app and open it signed in?",
389
374
  run: async () => {
390
- linked = (await connectCommand([...commandArgs, "--skip-login"])) || null;
391
- },
392
- },
393
- {
394
- name: "endpoints",
395
- question: "Map this repository's endpoints?",
396
- run: () => {
397
- if (!linked?.agent) throw new Error(needsAgent("endpoints discover"));
398
- return discoverEndpoints(args, linked.agent, linked.serverName);
399
- },
400
- },
401
- {
402
- name: "runner",
403
- question: "Let PreMan run your agent here when it finds something to fix?",
404
- run: () => {
405
- if (!linked?.agent) throw new Error(needsAgent("runner start --background"));
406
- return runnerCommand(["start", "--background", "--agent", linked.agent.id]);
375
+ const installed = await installDesktop([...commandArgs]);
376
+ if (installed?.state === "unsupported") {
377
+ // Not a failure: the download link has already been printed, and the
378
+ // account this step exists to create is finished either way.
379
+ process.stdout.write("Sign in there with the account you just used.\n");
380
+ return;
381
+ }
382
+ process.stdout.write("Opening PreMan\u2026\n");
383
+ const opened = await openDesktopSignedIn(creds);
384
+ if (opened.state === "opened-signed-in") {
385
+ process.stdout.write("Opened PreMan, signed in as this account.\n");
386
+ } else if (opened.state === "not-installed") {
387
+ process.stdout.write(
388
+ "PreMan is not in /Applications yet \u2014 open it once installed and sign in.\n"
389
+ );
390
+ } else {
391
+ // Either the session could not be handed over or this app is too old to
392
+ // take it up. Say so rather than let the customer wonder why they are
393
+ // looking at a login screen.
394
+ process.stdout.write("Opened PreMan \u2014 sign in with the account you just used.\n");
395
+ }
407
396
  },
408
397
  },
409
- { name: "GitHub", question: "Connect GitHub?", run: () => githubCommand(args) },
410
- { name: "AWS logs", question: "Connect AWS?", run: () => awsCommand(args) },
411
- { name: "Slack", question: "Connect Slack?", run: () => slackCommand(args) },
398
+ // GitHub, AWS and Slack are deliberately not here. Starting out is an account
399
+ // and the app; each integration is its own command for whenever it is wanted.
412
400
  ];
413
401
 
414
402
  // Outcome per step rather than three lists, so revisiting a step replaces its
415
403
  // result instead of recording it twice.
416
404
  const outcome = new Map();
417
405
 
418
- // Indexed rather than for..of so "back" can move the cursor. Re-running a
419
- // step is safe: every one of these is idempotent on the backend -- an AWS
420
- // grant returns the existing link, and an App install that already happened
421
- // is detected rather than duplicated.
406
+ // Indexed rather than for..of so "back" can move the cursor. Re-running a step
407
+ // is safe: an install that already happened overwrites the same bundle rather
408
+ // than duplicating it.
422
409
  let i = 0;
423
410
  while (i < steps.length) {
424
411
  const step = steps[i];
@@ -464,8 +451,8 @@ export async function onboardCommand(
464
451
 
465
452
  export const INTEGRATIONS_HELP = `
466
453
  Setup options:
467
- preman onboard Sign in, then agent, endpoints, runner, GitHub,
468
- AWS and Slack, one prompt per step
454
+ preman onboard Create or sign in to an account, then install the
455
+ PreMan app and open it signed in
469
456
  preman aws Connect an AWS account and stream a log group
470
457
  preman github Install the PreMan GitHub App
471
458
  preman slack Add PreMan to a Slack workspace
package/dist/server.js CHANGED
@@ -1332,6 +1332,90 @@ export function createServer() {
1332
1332
  });
1333
1333
  }
1334
1334
  });
1335
+ // ── Collections test generation ────────────────────────────────────
1336
+ // The dashboard's Generate Tests / Review flagged / Setup fixtures /
1337
+ // Enrich with agent buttons, so an agent can run them without clicking.
1338
+ server.tool("generate_saved_tests", "Generate scheduled test suites for every endpoint already saved in a PreMan workspace. This is the dashboard's Collections 'Generate Tests' button: it clones the workspace's connected GitHub repository, matches the discovered routes onto the saved endpoints by method and path, and writes heuristic cases onto the ones it matches. No model is involved, so nothing invents a body for a DELETE. Write, destructive, low-confidence, and path-parameter routes come back flagged and disabled — clear them with review_generated_tests. This is not generate_tests, which writes cases for a single endpoint you describe.", {
1339
+ action: z
1340
+ .enum(["generate", "status"])
1341
+ .optional()
1342
+ .describe("generate (default) runs the scan; status is read-only counts"),
1343
+ integration_id: z
1344
+ .string()
1345
+ .optional()
1346
+ .describe("Connected repo to scan. Omit to use the one with the most synced endpoints."),
1347
+ workspace_id: z.string().optional().describe("Defaults to the caller's own workspace"),
1348
+ }, async (args) => {
1349
+ try {
1350
+ const result = await callBackend("generate_saved_tests", args);
1351
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
1352
+ }
1353
+ catch (e) {
1354
+ return toolError(e.message, inferErrorCode(e.message), {
1355
+ next_actions: [
1356
+ "Connect a GitHub repository to this workspace in Settings → Integrations, then retry.",
1357
+ ],
1358
+ related_tools: ["review_generated_tests", "setup_test_fixtures", "enrich_generated_tests"],
1359
+ });
1360
+ }
1361
+ });
1362
+ server.tool("review_generated_tests", "List and approve the generated test suites PreMan flagged for human review. This is the dashboard's 'Review flagged' button. generate_saved_tests deliberately leaves mutating and ambiguous routes disabled; this is how a reviewed suite gets turned on. Approving enables the suite's read-only schedule and does not raise the unattended write policy, so a DELETE stays gated at run time. List first and show the user each suite and its reason; only use approve_all when they asked to clear the whole queue.", {
1363
+ action: z
1364
+ .enum(["list", "approve", "approve_all"])
1365
+ .optional()
1366
+ .describe("list (default) | approve one request_id | approve_all (max 50)"),
1367
+ request_id: z.string().optional().describe("Saved request to approve; required for approve"),
1368
+ workspace_id: z.string().optional().describe("Defaults to the caller's own workspace"),
1369
+ }, async (args) => {
1370
+ try {
1371
+ const result = await callBackend("review_generated_tests", args);
1372
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
1373
+ }
1374
+ catch (e) {
1375
+ return toolError(e.message, inferErrorCode(e.message), {
1376
+ next_actions: ["Call review_generated_tests with action='list' to see the queue."],
1377
+ related_tools: ["generate_saved_tests", "enrich_generated_tests"],
1378
+ });
1379
+ }
1380
+ });
1381
+ server.tool("setup_test_fixtures", "Fill the {id} path parameters in saved PreMan endpoints with real record ids. This is the dashboard's 'Setup fixtures' button: a happy-path test for GET /users/{id} needs an id that exists, so PreMan calls the sibling list route read-only, harvests ids from the response, and falls back to the endpoint's schema example. It never mints a fake UUID that would only 404 and never fires a write. Harvested values are stored in workspace settings and returned as a .env snippet. This does not approve anything.", {
1382
+ action: z
1383
+ .enum(["setup", "list"])
1384
+ .optional()
1385
+ .describe("setup (default) harvests ids; list reports what is still missing"),
1386
+ workspace_id: z.string().optional().describe("Defaults to the caller's own workspace"),
1387
+ }, async (args) => {
1388
+ try {
1389
+ const result = await callBackend("setup_test_fixtures", args);
1390
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
1391
+ }
1392
+ catch (e) {
1393
+ return toolError(e.message, inferErrorCode(e.message), {
1394
+ next_actions: ["Save at least one endpoint with a {id} path parameter, then retry."],
1395
+ related_tools: ["generate_saved_tests", "review_generated_tests"],
1396
+ });
1397
+ }
1398
+ });
1399
+ server.tool("enrich_generated_tests", "Add model-written test cases on top of PreMan's heuristic suites. This is the dashboard's 'Enrich with agent' button. generate_saved_tests writes deterministic cases only; this pass asks a model for a few extra edge cases per suite. It touches review-cleared suites only, skips destructive and billing-sensitive requests unless workspace policy already allows that risk, skips suites already enriched, and stops after 40 suites per call so a large collection cannot fan out into hundreds of model calls. Leftovers come back as remaining.", {
1400
+ action: z
1401
+ .enum(["enrich", "list"])
1402
+ .optional()
1403
+ .describe("enrich (default) runs the pass; list returns the eligible suites"),
1404
+ workspace_id: z.string().optional().describe("Defaults to the caller's own workspace"),
1405
+ }, async (args) => {
1406
+ try {
1407
+ const result = await callBackend("enrich_generated_tests", args);
1408
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
1409
+ }
1410
+ catch (e) {
1411
+ return toolError(e.message, inferErrorCode(e.message), {
1412
+ next_actions: [
1413
+ "Run generate_saved_tests first, then clear the review queue with review_generated_tests.",
1414
+ ],
1415
+ related_tools: ["generate_saved_tests", "review_generated_tests"],
1416
+ });
1417
+ }
1418
+ });
1335
1419
  server.tool("share_endpoints_with_ui", "Push discovered or verified endpoints into the PreMan Playground so the user can see them, test them, and convert selected endpoints into hosted MCP tools. Use this after verify_endpoints_live or when the user explicitly asks to stream endpoints to the UI.", {
1336
1420
  endpoints: z.array(z.any()).describe("Endpoints to push. Each item should include method plus path/path_template/url; include schemas when available."),
1337
1421
  upstream_base_url: z.string().optional().describe("Default upstream base URL for testing and MCP generation, e.g. http://127.0.0.1:8000 or https://api.example.com"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,9 +13,10 @@
13
13
  "dev": "tsx src/server.ts",
14
14
  "open-mcp-preview": "node scripts/emit-mcp-preview.mjs",
15
15
  "open-mcp-preview-in-cursor": "node scripts/open-cursor-preview.mjs",
16
- "test": "npm run build && npm run test:connect && npm run test:node",
16
+ "test": "npm run build && npm run test:connect && npm run test:node && npm run test:dmg",
17
17
  "test:connect": "node scripts/smoke-connect.mjs",
18
- "test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.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-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs"
18
+ "test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.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",
19
+ "test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
19
20
  },
20
21
  "dependencies": {
21
22
  "@modelcontextprotocol/ext-apps": "^0.1.0",