premanmcp 0.10.5 → 0.10.7

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
@@ -48,11 +48,13 @@ to go restart anything, in cheapest-first order:
48
48
  1. **Self-test.** It starts the MCP server exactly as your agent will and calls
49
49
  `preman_status` over stdio. That both completes the link and proves the whole chain —
50
50
  launcher, package, key, backend. `--no-self-test` turns it off.
51
- 2. **Agent run**, which also proves your agent can load what was written. It opens your
52
- agent interactively in a new terminal window the session you go on to use — and falls
53
- back to a headless run (`claude -p`, `cursor-agent -p`, `codex exec`) where no window can
54
- be opened, such as CI or SSH. `--no-auto-checkin` turns it off, `PREMAN_NO_TERMINAL=1`
55
- keeps it headless.
51
+ 2. **Agent run**, which also proves your agent can load what was written. For Cursor it
52
+ runs `cursor-agent mcp enable` after writing `mcp.json` (new servers stay off the CLI
53
+ approved list otherwise) and launches the agent with `--approve-mcps` so the check-in
54
+ can actually call `preman_status`. It opens your agent interactively in a new terminal
55
+ window the session you go on to use — and falls back to a headless run (`claude -p`,
56
+ `cursor-agent --approve-mcps -p`, `codex exec`) where no window can be opened, such as
57
+ CI or SSH. `--no-auto-checkin` turns it off, `PREMAN_NO_TERMINAL=1` keeps it headless.
56
58
  3. **Wait**, if neither is possible: restart your agent and it links on its first call.
57
59
 
58
60
  A self-test that answers from an unexpected backend is reported with the file that
@@ -71,6 +73,10 @@ up a link that already worked:
71
73
 
72
74
  ```bash
73
75
  preman endpoints discover # map this repo's endpoints
76
+ preman tests generate # heuristic suites on saved Collections requests
77
+ preman tests review # list flagged suites; --approve <id> enables schedule
78
+ preman tests setup # harvest path-param fixture IDs into a .env snippet
79
+ preman tests enrich # optional LLM cases on remaining heuristic suites
74
80
  preman runner start --background # let PreMan apply fixes on this machine
75
81
  preman github # or connect it in the dashboard
76
82
  preman status # which of those are done
@@ -133,6 +139,7 @@ npm exec -y premanmcp@latest -- connect --project
133
139
  ## What It Does
134
140
 
135
141
  - Converts API endpoints into agent-callable MCP tools.
142
+ - Migrates a Postman collection in one call (`migrate_from_postman`): keeps the assertions your `pm.test` blocks declared, splits the environment into shared variables and encrypted secrets, and schedules a monitored test suite per request.
136
143
  - Creates/connects a PreMan account from the terminal or IDE agent.
137
144
  - Lets agents test real backend endpoints from the IDE.
138
145
  - Syncs endpoint inventory across backend and frontend workflows.
@@ -166,6 +173,10 @@ Convert these endpoints into an MCP.
166
173
  Show me the audit log for this hosted MCP.
167
174
  ```
168
175
 
176
+ ```text
177
+ Move my Postman collection over to PreMan.
178
+ ```
179
+
169
180
  ```text
170
181
  Pull my pending PreMan fix tasks and fix the failing endpoint.
171
182
  ```
@@ -230,6 +241,7 @@ npm exec -y premanmcp@latest -- install # Cursor-only installer (legacy)
230
241
  ```bash
231
242
  preman hook install # Write the pre-push hook (connect does this too)
232
243
  preman hook status # Installed? And does the command in it still answer?
244
+ preman hook repair # Rewrite it if it stopped working (usually automatic)
233
245
  preman hook uninstall # Remove it, restoring any hook it replaced
234
246
  ```
235
247
 
@@ -242,17 +254,33 @@ than `@latest`, so upgrading us never changes what your pushes run; re-run `hook
242
254
  to move it. Set `PREMAN_HOOK_INVOCATION` to write a command of your own instead, and
243
255
  `PREMAN_SKIP_HOOK=1` to silence the hook for a push.
244
256
 
257
+ You should not have to run `repair`. A hook of ours that stops answering is rewritten in the
258
+ background by anything that proves PreMan runs here — any command, or your agent starting the
259
+ MCP server — because nothing re-runs `connect` after an upgrade to notice, and the broken
260
+ state is silent. It is narrow on purpose: a hook that still works keeps the version it pins, a
261
+ hook we did not write is never touched, and no hook is installed where you never asked for
262
+ one. The answer is remembered for an hour per directory, and `PREMAN_NO_HOOK_REPAIR=1` turns
263
+ it off.
264
+
245
265
  ### Runner
246
266
 
247
- `connect --guide` sets this up for you; these are for managing it afterwards.
267
+ `preman onboard` and `connect --guide` set this up for you; these are for managing it
268
+ afterwards.
248
269
 
249
270
  ```bash
250
271
  preman runner status # Paired? Running?
251
272
  preman runner start --background # Hold the job stream, log to ~/.preman/runner.log
252
273
  preman runner stop # Stop it and report offline
253
- preman runner register --agent claude-code # Re-pair (e.g. a different project)
274
+ preman runner register --agent claude-code # Pair without starting
254
275
  ```
255
276
 
277
+ `start` pairs this machine itself when it is not paired yet, and re-pairs it when the
278
+ backend has revoked the token it held, so neither is a command you have to be told to run.
279
+ It works out which agent to pair as from the session it is running in, then from the agent
280
+ PreMan is already configured in, then from the only one installed — and asks only when
281
+ those disagree. Pass `--agent cursor|claude-code|codex` to settle it yourself, which is
282
+ also what a machine with no terminal to ask in needs.
283
+
256
284
  The runner holds one outbound connection to PreMan and runs the work PreMan queues for
257
285
  this machine — a failing endpoint becomes an agent run in your own repo instead of a
258
286
  prompt you have to paste. It is bound to the agent and directory it was registered with
package/bin/account.js CHANGED
@@ -29,7 +29,7 @@ export const ACCOUNT_HELP = `
29
29
  Account options:
30
30
  login --browser Approve sign-in in the browser instead of typing a password
31
31
  logout Delete stored CLI credentials
32
- doctor Check backend, credentials, target, and integrations
32
+ doctor [--port n] Check backend, credentials, target, and integrations
33
33
  watch <run-id> Follow a push simulation as it runs
34
34
  `;
35
35
 
@@ -140,7 +140,10 @@ export async function doctorCommand(commandArgs = []) {
140
140
  if (!status.ok) failures += 1;
141
141
  }
142
142
 
143
- const candidates = detectCandidates(process.cwd());
143
+ const explicitPort = args.value("--port", "");
144
+ const candidates = detectCandidates(process.cwd(), {
145
+ extraPorts: explicitPort ? [explicitPort] : [],
146
+ });
144
147
  results.push(
145
148
  line(
146
149
  "local target",
@@ -150,6 +153,7 @@ export async function doctorCommand(commandArgs = []) {
150
153
  : "no signals in this directory"
151
154
  )
152
155
  );
156
+ if (!candidates.length) failures += 1;
153
157
 
154
158
  // An installed hook whose command no longer answers is the one failure here
155
159
  // that reports itself as success everywhere else: `git push` prints a single
@@ -200,7 +204,18 @@ export async function doctorCommand(commandArgs = []) {
200
204
  return { failures };
201
205
  }
202
206
 
203
- const TERMINAL_STATUSES = new Set(["complete", "completed", "failed", "error", "cancelled"]);
207
+ const TERMINAL_STATUSES = new Set([
208
+ "complete",
209
+ "completed",
210
+ "succeeded",
211
+ "failed",
212
+ "error",
213
+ "cancelled",
214
+ ]);
215
+
216
+ export function isTerminalSimulationStatus(value) {
217
+ return TERMINAL_STATUSES.has(String(value || "").toLowerCase());
218
+ }
204
219
 
205
220
  export async function watchCommand(commandArgs = []) {
206
221
  const args = makeArgs(commandArgs);
@@ -235,7 +250,7 @@ export async function watchCommand(commandArgs = []) {
235
250
  process.stdout.write(` ${mark} ${String(step.key || "").padEnd(14)} ${step.message || ""}\n`);
236
251
  }
237
252
 
238
- if (TERMINAL_STATUSES.has(String(result.status || "").toLowerCase())) {
253
+ if (isTerminalSimulationStatus(result.status)) {
239
254
  const verdict = result.summary?.verdict || result.status;
240
255
  process.stdout.write(`\nRun ${result.status}: ${verdict}\n`);
241
256
  return result;
package/bin/api_tools.js CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { readFileSync } from "node:fs";
10
10
 
11
- import { callBackendJson, makeArgs, resolveApiKey } from "./shared.js";
11
+ import { callBackendJson, cliInvocation, makeArgs, resolveApiKey } from "./shared.js";
12
12
 
13
13
  export const ENDPOINTS_HELP = `
14
14
  Endpoints:
package/bin/cli.js CHANGED
@@ -17,12 +17,14 @@ import path from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
 
19
19
  import { ENDPOINTS_HELP, TEST_HELP, endpointsCommand, testCommand } from "./api_tools.js";
20
+ import { TESTS_HELP, testsCommand } from "./tests.js";
20
21
  import {
21
22
  CONNECT_HELP,
22
23
  DISPATCH_HELP,
23
24
  connectCommand,
24
25
  discoverEndpoints,
25
26
  dispatchCommand,
27
+ resolveAgentForPairing,
26
28
  writeCursorConfig,
27
29
  } from "./connect.js";
28
30
  import {
@@ -34,7 +36,7 @@ import {
34
36
  } from "./integrations.js";
35
37
  import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
36
38
  import { STATUS_HELP, statusCommand } from "./status.js";
37
- import { HOOK_HELP, hookCommand } from "./hook.js";
39
+ import { HOOK_HELP, hookCommand, scheduleHookRepair } from "./hook.js";
38
40
  import { RUNNER_HELP, runnerCommand } from "./runner.js";
39
41
  import { VERIFY_HELP, verifyCommand } from "./verify.js";
40
42
  import { DESKTOP_HELP, installDesktopCommand } from "./desktop.js";
@@ -55,7 +57,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
55
57
  const ROOT = path.join(__dirname, "..");
56
58
 
57
59
  const args = process.argv.slice(2);
58
- const command = args[0] === "--help" || args[0] === "-h"
60
+ const command = args.length === 0 && process.stdin.isTTY
61
+ ? "connect"
62
+ : args[0] === "--help" || args[0] === "-h"
59
63
  ? "help"
60
64
  : args[0] && !args[0].startsWith("-")
61
65
  ? args[0]
@@ -97,6 +101,7 @@ function printHelp() {
97
101
  ["link|tools|run ...", "Drive a published hosted MCP"],
98
102
  ["endpoints list|discover|setup ...", "Discover, list, and set up API endpoints"],
99
103
  ["test <id> [--scenario ...] [--stress]", "Generate + run tests for an endpoint"],
104
+ ["tests generate|review|setup|enrich", "Collections: generate, review, fixtures, enrich"],
100
105
  ];
101
106
  const width = Math.max(...usage.map(([command]) => `${cli} ${command}`.trimEnd().length));
102
107
  const usageLines = usage
@@ -107,7 +112,7 @@ function printHelp() {
107
112
 
108
113
  Usage:
109
114
  ${usageLines}
110
- ${INTEGRATIONS_HELP}${CONNECT_HELP}${DISPATCH_HELP}${STATUS_HELP}${VERIFY_HELP}${HOOK_HELP}${RUNNER_HELP}${ACCOUNT_HELP}${DESKTOP_HELP}${ENDPOINTS_HELP}${TEST_HELP}
115
+ ${INTEGRATIONS_HELP}${CONNECT_HELP}${DISPATCH_HELP}${STATUS_HELP}${VERIFY_HELP}${HOOK_HELP}${RUNNER_HELP}${ACCOUNT_HELP}${DESKTOP_HELP}${ENDPOINTS_HELP}${TEST_HELP}${TESTS_HELP}
111
116
  Login options:
112
117
  --email <email> Pre-fill the email prompt
113
118
  --backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
@@ -135,6 +140,7 @@ async function loginCommand() {
135
140
  await loginBrowser(cliArgs);
136
141
  return;
137
142
  }
143
+ const cli = cliInvocation();
138
144
  const creds = await authenticateTerminal(cliArgs);
139
145
  process.stdout.write(`PreMan account ready.
140
146
 
@@ -153,6 +159,7 @@ You can now run:
153
159
  * the same write plus agent choice, pairing, and dispatch setup.
154
160
  */
155
161
  async function installCursorMcp() {
162
+ const cli = cliInvocation();
156
163
  const serverName = argValue("--name", "preman");
157
164
  const projectInstall = hasFlag("--project");
158
165
 
@@ -179,7 +186,7 @@ Server name: ${serverName}
179
186
  Backend: ${serverConfig.env.PREMAN_BACKEND}
180
187
 
181
188
  Next steps:
182
- 1. ${hasInlineKey ? "Your PreMan API key was written to the MCP config." : hasStoredKey ? `Your PreMan API key is saved in ${CREDENTIALS_FILE}; the MCP server will load it automatically.` : "Run ${cli} login to create/connect your account and generate an API key."}
189
+ 1. ${hasInlineKey ? "Your PreMan API key was written to the MCP config." : hasStoredKey ? `Your PreMan API key is saved in ${CREDENTIALS_FILE}; the MCP server will load it automatically.` : `Run ${cli} login to create/connect your account and generate an API key.`}
183
190
  2. Restart Cursor or toggle the PreMan MCP server off/on.
184
191
  3. In your API repo, ask your coding agent:
185
192
  "Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
@@ -216,6 +223,11 @@ function startServer() {
216
223
  }
217
224
 
218
225
  async function main() {
226
+ // Running at all is the signal. `hook` and `verify` own the hook themselves,
227
+ // and connect reinstalls it in the foreground, so a background rewrite there
228
+ // would only race with the one the user is watching.
229
+ if (!["hook", "verify", "connect"].includes(command)) scheduleHookRepair();
230
+
219
231
  if (command === "login") {
220
232
  await loginCommand();
221
233
  } else if (command === "status") {
@@ -225,7 +237,10 @@ async function main() {
225
237
  } else if (command === "hook") {
226
238
  await hookCommand(commandArgs);
227
239
  } else if (command === "runner") {
228
- await runnerCommand(commandArgs);
240
+ // Injected rather than imported inside runner.js, which connect.js already
241
+ // depends on: the agent picker lives there, and importing it back would
242
+ // close the cycle.
243
+ await runnerCommand(commandArgs, { resolveAgent: resolveAgentForPairing });
229
244
  } else if (command === "logout") {
230
245
  await logoutCommand();
231
246
  } else if (command === "doctor") {
@@ -266,6 +281,8 @@ async function main() {
266
281
  await endpointsCommand(commandArgs);
267
282
  } else if (command === "test") {
268
283
  await testCommand(commandArgs);
284
+ } else if (command === "tests") {
285
+ await testsCommand(commandArgs);
269
286
  } else if (command === "help" || command === "--help" || command === "-h") {
270
287
  printHelp();
271
288
  } else if (command === "start") {
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Which coding agent this machine should use, and how it is asked.
3
+ *
4
+ * The registry is the shared vocabulary: `id` matches the backend's
5
+ * normalize_agent(), so a link record and this CLI cannot disagree about who is
6
+ * connected. Resolution is ordered by how much each signal actually knows --
7
+ * the session we are inside, the agent PreMan is already written into, the only
8
+ * one installed -- because asking is the step people resent most.
9
+ */
10
+
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { spawnSync } from "node:child_process";
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { onPath, promptText, readJsonFile } from "../shared.js";
16
+ import { ConnectError, EXIT_USAGE } from "./errors.js";
17
+
18
+ /**
19
+ * Supported agents. `id` matches the backend's normalize_agent() vocabulary so
20
+ * the link record and this CLI cannot disagree about who is connected.
21
+ */
22
+ export const AGENTS = [
23
+ {
24
+ id: "cursor",
25
+ label: "Cursor",
26
+ aliases: ["cursor"],
27
+ dispatch: {
28
+ credential: "Cursor API key",
29
+ needsRoutine: false,
30
+ source: "cursor.com/dashboard → Integrations → API Keys",
31
+ },
32
+ snippetHint: "merge into ~/.cursor/mcp.json, then run: cursor-agent mcp enable preman",
33
+ restartHint:
34
+ 'Fully quit and reopen Cursor, then Settings → MCP → toggle "preman" off and on. For the CLI, run `cursor-agent mcp enable preman`.',
35
+ },
36
+ {
37
+ id: "claude_code",
38
+ label: "Claude Code",
39
+ aliases: ["claude", "claude-code", "claude_code", "claudecode"],
40
+ dispatch: {
41
+ credential: "Claude Code routine token",
42
+ needsRoutine: true,
43
+ // Named explicitly because "Claude Code routine token" reads like an
44
+ // Anthropic API key, which is a different credential from a different page.
45
+ source: "claude.ai/code/routines → your routine → Add API trigger",
46
+ },
47
+ snippetHint: "run:",
48
+ restartHint: 'Start a new Claude Code session and run `claude mcp list` — "preman" should be listed.',
49
+ },
50
+ {
51
+ id: "codex",
52
+ label: "Codex",
53
+ aliases: ["codex", "openai-codex", "openai_codex"],
54
+ dispatch: null, // No public fire API; stays on the copy-paste path.
55
+ snippetHint: "append to ~/.codex/config.toml",
56
+ restartHint: "Restart Codex so it re-reads its config.toml.",
57
+ },
58
+ ];
59
+
60
+ export function findAgent(value) {
61
+ const raw = String(value || "").trim().toLowerCase().replace(/\s+/g, "-");
62
+ return AGENTS.find((a) => a.id === raw || a.aliases.includes(raw)) || null;
63
+ }
64
+
65
+
66
+ /**
67
+ * Why this agent's CLI cannot run unattended right now, or "".
68
+ *
69
+ * Installed is not the same as usable. `cursor-agent` sits on PATH and exits 1
70
+ * on every `-p` run until someone signs in, which surfaced as "Cursor did not
71
+ * register any endpoints" and sent people looking at PreMan for an hour. Only
72
+ * Cursor is probed because only its CLI has a cheap non-interactive status
73
+ * subcommand; the others are diagnosed from their own output when they fail.
74
+ */
75
+ export function agentBlocker(agentId) {
76
+ if (agentId !== "cursor") return "";
77
+ const probe = spawnSync("cursor-agent", ["status"], {
78
+ encoding: "utf8",
79
+ timeout: 15000,
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ });
82
+ const text = `${probe.stdout || ""}${probe.stderr || ""}`;
83
+ if (/not logged in|not authenticated|no active session/i.test(text)) {
84
+ return "cursor-agent is installed but not signed in — run `cursor-agent login`";
85
+ }
86
+ return "";
87
+ }
88
+
89
+ /** Best guess at which agent this machine actually uses, for the default pick. */
90
+ export function detectAgents() {
91
+ const home = os.homedir();
92
+ return {
93
+ cursor: existsSync(path.join(home, ".cursor")) || Boolean(process.env.CURSOR_TRACE_ID),
94
+ claude_code: onPath("claude") || existsSync(path.join(home, ".claude.json")),
95
+ codex: onPath("codex") || existsSync(process.env.CODEX_HOME || path.join(home, ".codex")),
96
+ };
97
+ }
98
+
99
+ /**
100
+ * The agent whose session this command is running inside, if any.
101
+ *
102
+ * A better default than "first one installed": someone who types this into an
103
+ * agent's terminal almost always means that agent, and on a machine with all
104
+ * three installed the detected-order default is usually wrong.
105
+ */
106
+ export function runningInside(env = process.env) {
107
+ if (env.CLAUDECODE || env.CLAUDE_CODE) return "claude_code";
108
+ if (env.CURSOR_TRACE_ID) return "cursor";
109
+ return "";
110
+ }
111
+
112
+ /** Is PreMan's MCP server already written into this agent's config? */
113
+ function agentIsLinked(agentId, serverName = "preman") {
114
+ try {
115
+ const home = os.homedir();
116
+ if (agentId === "cursor") {
117
+ return Boolean(readJsonFile(path.join(home, ".cursor", "mcp.json")).mcpServers?.[serverName]);
118
+ }
119
+ if (agentId === "claude_code") {
120
+ return Boolean(readJsonFile(path.join(home, ".claude.json")).mcpServers?.[serverName]);
121
+ }
122
+ if (agentId === "codex") {
123
+ const file = path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "config.toml");
124
+ return existsSync(file) && readFileSync(file, "utf8").includes(`[mcp_servers.${serverName}]`);
125
+ }
126
+ } catch {
127
+ // An unreadable config is not an answer; fall through to the other signals.
128
+ }
129
+ return false;
130
+ }
131
+
132
+ /**
133
+ * Which agent this machine should pair as, or "" when only a person can say.
134
+ *
135
+ * Pairing needs an agent id, and demanding `--agent` for it turned "start the
136
+ * runner" into a command someone had to be told twice. The signals are ordered
137
+ * by how much they actually know: the session we are inside, then the agent
138
+ * PreMan is already configured in, then the only one installed. Asking is the
139
+ * last resort rather than the first, and a machine with no terminal to ask in
140
+ * gets "" so the caller can fail with a sentence instead of hanging on a prompt.
141
+ */
142
+ export async function resolveAgentForPairing({ interactive = Boolean(process.stdin.isTTY) } = {}) {
143
+ const inside = runningInside();
144
+ if (inside) return inside;
145
+
146
+ const linked = AGENTS.filter((agent) => agentIsLinked(agent.id));
147
+ if (linked.length === 1) return linked[0].id;
148
+
149
+ const detected = detectAgents();
150
+ const present = AGENTS.filter((agent) => detected[agent.id]);
151
+ if (present.length === 1) return present[0].id;
152
+
153
+ if (!interactive) return "";
154
+ const picked = await promptAgentChoice(detected);
155
+ return picked?.id || "";
156
+ }
157
+
158
+ /**
159
+ * The agent a directly-invoked command should drive, as an object, or null.
160
+ *
161
+ * Same signals as pairing, resolved to the record `discoverEndpoints` needs,
162
+ * with an explicit `--agent` taking precedence so the command stays scriptable
163
+ * on a machine that has all three installed. Commands the user typed on purpose
164
+ * get to run the agent; the caller falls back to printing the brief on null,
165
+ * which is also what a non-interactive run with no clear answer gets.
166
+ */
167
+ export async function resolveAgentToDrive({ agent = "", ...options } = {}) {
168
+ return findAgent(agent) || findAgent(await resolveAgentForPairing(options));
169
+ }
170
+
171
+ export async function promptAgentChoice(detected, insideId = runningInside()) {
172
+ process.stdout.write("Which coding agent?\n");
173
+ AGENTS.forEach((agent, index) => {
174
+ let mark = "";
175
+ if (agent.id === insideId) mark = " (this session)";
176
+ else if (detected[agent.id]) mark = " (detected)";
177
+ process.stdout.write(` ${index + 1}. ${agent.label}${mark}\n`);
178
+ });
179
+
180
+ const inside = AGENTS.findIndex((a) => a.id === insideId);
181
+ const defaultIndex =
182
+ inside >= 0
183
+ ? inside
184
+ : Math.max(
185
+ 0,
186
+ AGENTS.findIndex((a) => detected[a.id])
187
+ );
188
+ const answer = await promptText(`Pick [${defaultIndex + 1}]: `);
189
+ if (!answer) return AGENTS[defaultIndex];
190
+
191
+ const byNumber = Number.parseInt(answer, 10);
192
+ if (Number.isInteger(byNumber) && byNumber >= 1 && byNumber <= AGENTS.length) {
193
+ return AGENTS[byNumber - 1];
194
+ }
195
+ const byName = findAgent(answer);
196
+ if (byName) return byName;
197
+ throw new ConnectError(`Not a valid choice: ${answer}`, EXIT_USAGE);
198
+ }