premanmcp 0.5.0 → 0.7.1

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
@@ -7,7 +7,7 @@ PreMan is agent-first API infrastructure. It lets backend teams expose endpoints
7
7
  ## Connect
8
8
 
9
9
  ```bash
10
- npx premanmcp@latest connect
10
+ npm exec -y premanmcp@latest -- connect
11
11
  ```
12
12
 
13
13
  Pick your coding agent from the list — Cursor, Claude Code, or Codex — and PreMan
@@ -29,8 +29,8 @@ agent actually reads (`~/.cursor/mcp.json`, Claude Code's MCP config, or
29
29
  {
30
30
  "mcpServers": {
31
31
  "preman": {
32
- "command": "npx",
33
- "args": ["-y", "premanmcp@latest"],
32
+ "command": "npm",
33
+ "args": ["exec", "-y", "premanmcp@latest", "--"],
34
34
  "env": {
35
35
  "PREMAN_BACKEND": "https://api.preman.live",
36
36
  "PREMAN_FRONTEND": "https://app.preman.live"
@@ -42,8 +42,21 @@ agent actually reads (`~/.cursor/mcp.json`, Claude Code's MCP config, or
42
42
 
43
43
  Restart your agent afterwards, then ask it to `run preman_status` to finish linking.
44
44
 
45
+ `connect` reads the config back after writing it, and prints a copy-paste snippet if it
46
+ cannot confirm the entry landed. It then waits for your agent to check in — that
47
+ check-in is the only real proof the agent loaded the server.
48
+
49
+ Once the agent has checked in, `connect` walks you into your first test: it runs one
50
+ against an endpoint you already have, or prints the discovery brief to hand your agent,
51
+ followed by `preman endpoints setup` and `preman test`. Pass `--no-guide` to skip it.
52
+
45
53
  Useful flags: `--agent cursor|claude-code|codex` skips the picker, `--project` writes
46
- project-local config, `--print` shows the config without writing it.
54
+ project-local config, `--print` shows the config without writing it, `--no-guide` skips
55
+ the guided first run.
56
+
57
+ In CI or any non-interactive shell, run `connect --agent <name> --api-key pm_live_…`.
58
+ Without `--agent` there is nothing to prompt on, so `connect` prints ready-to-paste
59
+ setup blocks for all three agents and exits 2.
47
60
 
48
61
  `preman install` still exists and does the Cursor-only half of this.
49
62
 
@@ -63,13 +76,13 @@ npm exec -y premanmcp@latest -- login
63
76
  You can also pass the key directly:
64
77
 
65
78
  ```bash
66
- npx premanmcp@latest connect --api-key pm_live_xxx
79
+ npm exec -y premanmcp@latest -- connect --api-key pm_live_xxx
67
80
  ```
68
81
 
69
82
  For project-local config:
70
83
 
71
84
  ```bash
72
- npx premanmcp@latest connect --project
85
+ npm exec -y premanmcp@latest -- connect --project
73
86
  ```
74
87
 
75
88
  ## What It Does
@@ -82,6 +95,7 @@ npx premanmcp@latest connect --project
82
95
  - Supports hosted MCPs with consumer tokens for customer-facing agent access.
83
96
  - Records per-call observability so teams can audit which agent did what.
84
97
  - Hands failing-endpoint alerts to your agent as fix tasks (`preman_get_fix_task` → repro curl → `preman_complete_fix_task`).
98
+ - Connects your production logs from the terminal (`connect_logs`): your agent asks where the logs live, then deploys a read-only CloudFormation role for AWS, or wires a shipper to the ingest endpoint for everything else.
85
99
 
86
100
  ## Common Agent Commands
87
101
 
@@ -111,10 +125,14 @@ Show me the audit log for this hosted MCP.
111
125
  Pull my pending PreMan fix tasks and fix the failing endpoint.
112
126
  ```
113
127
 
128
+ ```text
129
+ Connect my production logs to PreMan.
130
+ ```
131
+
114
132
  ## Cursor
115
133
 
116
134
  ```bash
117
- npx premanmcp@latest connect --agent cursor
135
+ npm exec -y premanmcp@latest -- connect --agent cursor
118
136
  ```
119
137
 
120
138
  Manual Cursor config, if you would rather write it yourself:
@@ -123,8 +141,8 @@ Manual Cursor config, if you would rather write it yourself:
123
141
  {
124
142
  "mcpServers": {
125
143
  "preman": {
126
- "command": "npx",
127
- "args": ["-y", "premanmcp@latest"]
144
+ "command": "npm",
145
+ "args": ["exec", "-y", "premanmcp@latest", "--"]
128
146
  }
129
147
  }
130
148
  }
@@ -133,19 +151,19 @@ Manual Cursor config, if you would rather write it yourself:
133
151
  ## Claude Code
134
152
 
135
153
  ```bash
136
- npx premanmcp@latest connect --agent claude-code
154
+ npm exec -y premanmcp@latest -- connect --agent claude-code
137
155
  ```
138
156
 
139
157
  Equivalent manual command:
140
158
 
141
159
  ```bash
142
- claude mcp add preman -- npx -y premanmcp@latest
160
+ claude mcp add preman -- npm exec -y premanmcp@latest --
143
161
  ```
144
162
 
145
163
  ## Codex
146
164
 
147
165
  ```bash
148
- npx premanmcp@latest connect --agent codex
166
+ npm exec -y premanmcp@latest -- connect --agent codex
149
167
  ```
150
168
 
151
169
  Writes an `[mcp_servers.preman]` block into `~/.codex/config.toml`.
@@ -153,10 +171,10 @@ Writes an `[mcp_servers.preman]` block into `~/.codex/config.toml`.
153
171
  ## CLI
154
172
 
155
173
  ```bash
156
- npx premanmcp@latest connect # Pick an agent and connect it
157
- npx premanmcp@latest connect --agent codex # Skip the picker
158
- npx premanmcp@latest connect --project # Write project-local config
159
- npx premanmcp@latest connect --print # Print config without writing
174
+ npm exec -y premanmcp@latest -- connect # Pick an agent and connect it
175
+ npm exec -y premanmcp@latest -- connect --agent codex # Skip the picker
176
+ npm exec -y premanmcp@latest -- connect --project # Write project-local config
177
+ npm exec -y premanmcp@latest -- connect --print # Print config without writing
160
178
  npm exec -y premanmcp@latest -- # Start the MCP server
161
179
  npm exec -y premanmcp@latest -- login # Create/login and generate a PreMan API key
162
180
  npm exec -y premanmcp@latest -- install # Cursor-only installer (legacy)
@@ -0,0 +1,215 @@
1
+ /**
2
+ * `preman endpoints` and `preman test` — the API-testing surface of the CLI.
3
+ *
4
+ * Both are thin proxies over `POST /mcp/call-tool`, exactly like the MCP
5
+ * server's tools, so the CLI and an agent calling the same tool see identical
6
+ * behaviour. Requires a pm_live_ key (flag, env, or stored credentials).
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+
11
+ import { callBackendJson, makeArgs, resolveApiKey } from "./shared.js";
12
+
13
+ export const ENDPOINTS_HELP = `
14
+ Endpoints:
15
+ preman endpoints list [--status s] [--method m] [--workbench] [--json-out]
16
+ List endpoints saved in PreMan (add --workbench for runnable requests).
17
+ preman endpoints discover [--framework hint]
18
+ Print the codebase-discovery brief for your coding agent to execute.
19
+ preman endpoints setup (--file endpoints.json | --ids id1,id2) [--base-url url] [--no-workbench]
20
+ Register discovered endpoints and set them up as runnable requests.
21
+ `;
22
+
23
+ export const TEST_HELP = `
24
+ Testing:
25
+ preman test <request-or-endpoint-id> [options]
26
+ Generate scenario tests for one endpoint and run them.
27
+ --scenario "..." Add a described scenario (repeatable)
28
+ --no-run Generate only, don't execute
29
+ --allow-writes Permit mutating cases / write endpoints
30
+ --code Emit unit-test files (prints filename + content)
31
+ --framework pytest|jest Framework for --code (default pytest)
32
+ --stress Run a bounded load test instead
33
+ --duration n --rps n --concurrency n Stress parameters
34
+ --json-out Print the raw JSON result
35
+ `;
36
+
37
+ class CliError extends Error {
38
+ constructor(message, exitCode = 1) {
39
+ super(message);
40
+ this.exitCode = exitCode;
41
+ }
42
+ }
43
+
44
+ export async function callTool(args, tool, toolArguments) {
45
+ const token = resolveApiKey(args);
46
+ if (!token) {
47
+ throw new CliError(
48
+ "No PreMan API key. Run `preman login` or pass --api-key pm_live_...",
49
+ 2,
50
+ );
51
+ }
52
+ const result = await callBackendJson(args, "POST", "/mcp/call-tool", {
53
+ json: { tool, arguments: toolArguments },
54
+ token,
55
+ });
56
+ if (!result.ok) {
57
+ const detail = result.detail || result.message || result.raw || "backend error";
58
+ throw new CliError(`${tool} failed: ${result.status_code} ${detail}`);
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function printJson(value) {
64
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
65
+ }
66
+
67
+ function collectRepeated(commandArgs, flag) {
68
+ const out = [];
69
+ for (let i = 0; i < commandArgs.length; i += 1) {
70
+ if (commandArgs[i] === flag && commandArgs[i + 1]) out.push(commandArgs[i + 1]);
71
+ }
72
+ return out;
73
+ }
74
+
75
+ export async function endpointsCommand(commandArgs) {
76
+ const sub = commandArgs[0] && !commandArgs[0].startsWith("-") ? commandArgs[0] : "list";
77
+ const rest = sub === commandArgs[0] ? commandArgs.slice(1) : commandArgs;
78
+ const args = makeArgs(rest);
79
+
80
+ if (sub === "list") {
81
+ const toolArguments = {
82
+ status: args.value("--status", "") || undefined,
83
+ method: args.value("--method", "") || undefined,
84
+ include_workbench: args.has("--workbench"),
85
+ limit: Number(args.value("--limit", "50")),
86
+ };
87
+ const result = await callTool(args, "get_endpoints", toolArguments);
88
+ if (args.has("--json-out")) return printJson(result);
89
+ const rows = result.endpoints || [];
90
+ process.stdout.write(`${rows.length} registry endpoint(s)\n`);
91
+ for (const ep of rows) {
92
+ const status = ep.status ? ` [${ep.status}]` : "";
93
+ process.stdout.write(` ${ep.method || "GET"} ${ep.path_template || ep.url || ""}${status} ${ep.id || ""}\n`);
94
+ }
95
+ const workbench = result.workbench_requests || [];
96
+ if (workbench.length) {
97
+ process.stdout.write(`${workbench.length} runnable workbench request(s)\n`);
98
+ for (const r of workbench) {
99
+ process.stdout.write(` ${r.method} ${r.url} ${r.id}\n`);
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ if (sub === "discover") {
106
+ const result = await callTool(args, "discover_endpoints_from_codebase", {
107
+ base_path: args.value("--path", "."),
108
+ framework_hint: args.value("--framework", "") || undefined,
109
+ });
110
+ if (args.has("--json-out")) return printJson(result);
111
+ for (const line of result.instructions || []) process.stdout.write(`${line}\n`);
112
+ process.stdout.write(
113
+ "\nHand this brief to your coding agent, then run `preman endpoints setup --file endpoints.json`.\n",
114
+ );
115
+ return undefined;
116
+ }
117
+
118
+ if (sub === "setup") {
119
+ const file = args.value("--file", "");
120
+ const ids = args.value("--ids", "");
121
+ const toolArguments = {
122
+ base_url: args.value("--base-url", "") || undefined,
123
+ setup_workbench: !args.has("--no-workbench"),
124
+ };
125
+ if (file) {
126
+ let parsed;
127
+ try {
128
+ parsed = JSON.parse(readFileSync(file, "utf8"));
129
+ } catch (error) {
130
+ throw new CliError(`could not read ${file}: ${error.message}`, 2);
131
+ }
132
+ toolArguments.endpoints = Array.isArray(parsed) ? parsed : parsed.endpoints;
133
+ } else if (ids) {
134
+ toolArguments.endpoint_ids = ids.split(",").map((s) => s.trim()).filter(Boolean);
135
+ } else {
136
+ throw new CliError("pass --file endpoints.json or --ids id1,id2", 2);
137
+ }
138
+ const result = await callTool(args, "register_discovered_endpoints", toolArguments);
139
+ if (args.has("--json-out")) return printJson(result);
140
+ process.stdout.write(
141
+ `registered ${result.registered || 0} endpoint(s), skipped ${result.skipped || 0}\n`,
142
+ );
143
+ const requests = result.workbench_request_ids || [];
144
+ if (requests.length) {
145
+ process.stdout.write(`runnable as: ${requests.join(", ")}\n`);
146
+ }
147
+ return undefined;
148
+ }
149
+
150
+ throw new CliError(`unknown endpoints subcommand: ${sub}${ENDPOINTS_HELP}`, 2);
151
+ }
152
+
153
+ /** Print the scenario list and run outcome from generate_endpoint_tests. */
154
+ export function printTestSummary(result) {
155
+ process.stdout.write(`${result.count || 0} scenario(s) for ${result.request_id}\n`);
156
+ for (const c of result.cases || []) {
157
+ process.stdout.write(` - ${c.name} (${c.kind})\n`);
158
+ }
159
+ const run = result.run;
160
+ if (!run) return;
161
+ process.stdout.write(
162
+ `run ${run.status}: ${run.passed}/${run.total} passed, ${run.skipped} skipped` +
163
+ `${run.aborted_reason ? `, aborted: ${run.aborted_reason}` : ""}\n`,
164
+ );
165
+ for (const o of run.outcomes || []) {
166
+ if (o.status !== "passed") {
167
+ process.stdout.write(` FAIL ${o.case_name}: ${o.response_status ?? o.error ?? ""}\n`);
168
+ }
169
+ }
170
+ }
171
+
172
+ export async function testCommand(commandArgs) {
173
+ const target = commandArgs[0] && !commandArgs[0].startsWith("-") ? commandArgs[0] : "";
174
+ const rest = target ? commandArgs.slice(1) : commandArgs;
175
+ const args = makeArgs(rest);
176
+ if (!target) {
177
+ throw new CliError(`pass a request or endpoint id${TEST_HELP}`, 2);
178
+ }
179
+
180
+ if (args.has("--stress")) {
181
+ const result = await callTool(args, "run_stress_test", {
182
+ target,
183
+ duration_seconds: Number(args.value("--duration", "15")),
184
+ rps: Number(args.value("--rps", "5")),
185
+ concurrency: Number(args.value("--concurrency", "5")),
186
+ allow_writes: args.has("--allow-writes"),
187
+ });
188
+ if (args.has("--json-out")) return printJson(result);
189
+ process.stdout.write(
190
+ `stress ${result.status}: ${result.total_requests} requests, ` +
191
+ `${Math.round((result.error_rate || 0) * 1000) / 10}% errors, ` +
192
+ `p50 ${result.p50_ms ?? "n/a"}ms p95 ${result.p95_ms ?? "n/a"}ms p99 ${result.p99_ms ?? "n/a"}ms, ` +
193
+ `${Math.round((result.throughput_rps || 0) * 10) / 10} rps\n`,
194
+ );
195
+ if (result.aborted_reason) process.stdout.write(`aborted: ${result.aborted_reason}\n`);
196
+ return undefined;
197
+ }
198
+
199
+ const result = await callTool(args, "generate_endpoint_tests", {
200
+ target,
201
+ scenarios: collectRepeated(rest, "--scenario"),
202
+ run: !args.has("--no-run"),
203
+ allow_writes: args.has("--allow-writes"),
204
+ max_cases: Number(args.value("--max-cases", "10")),
205
+ include_code: args.has("--code"),
206
+ test_framework: args.value("--framework", "pytest"),
207
+ });
208
+ if (args.has("--json-out")) return printJson(result);
209
+
210
+ printTestSummary(result);
211
+ for (const artifact of result.code_artifacts || []) {
212
+ process.stdout.write(`\n===== ${artifact.filename} =====\n${artifact.content}\n`);
213
+ }
214
+ return undefined;
215
+ }
package/bin/cli.js CHANGED
@@ -16,10 +16,19 @@ import os from "node:os";
16
16
  import path from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
 
19
+ import { ENDPOINTS_HELP, TEST_HELP, endpointsCommand, testCommand } from "./api_tools.js";
19
20
  import { CONNECT_HELP, connectCommand, writeCursorConfig } from "./connect.js";
21
+ import {
22
+ INTEGRATIONS_HELP,
23
+ awsCommand,
24
+ githubCommand,
25
+ onboardCommand,
26
+ slackCommand,
27
+ } from "./integrations.js";
20
28
  import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
21
29
  import {
22
30
  CREDENTIALS_FILE,
31
+ cliInvocation,
23
32
  DEFAULT_BACKEND,
24
33
  DEFAULT_FRONTEND,
25
34
  authenticateTerminal,
@@ -50,20 +59,38 @@ function hasFlag(name) {
50
59
  }
51
60
 
52
61
  function printHelp() {
62
+ // Say "preman x" only to people who can actually run it; everyone else gets
63
+ // the form that works from a bare npm install.
64
+ const cli = cliInvocation();
65
+ // Padded here rather than hand-aligned: the invocation prefix is 6 characters
66
+ // for a global install and 38 for npm exec, so a fixed layout is wrong for
67
+ // one of them.
68
+ const usage = [
69
+ ["onboard", "Sign in, then connect agent, GitHub, AWS, Slack"],
70
+ ["connect [options]", "Pick a coding agent and connect it"],
71
+ ["aws | github | slack", "Connect one integration on its own"],
72
+ ["login", "Create/login to PreMan from the terminal"],
73
+ ["install [options]", "Install PreMan into Cursor MCP config"],
74
+ ["", "Start the PreMan MCP server"],
75
+ ["link|tools|run ...", "Drive a published hosted MCP"],
76
+ ["endpoints list|discover|setup ...", "Discover, list, and set up API endpoints"],
77
+ ["test <id> [--scenario ...] [--stress]", "Generate + run tests for an endpoint"],
78
+ ];
79
+ const width = Math.max(...usage.map(([command]) => `${cli} ${command}`.trimEnd().length));
80
+ const usageLines = usage
81
+ .map(([command, blurb]) => ` ${`${cli} ${command}`.trimEnd().padEnd(width)} ${blurb}`)
82
+ .join("\n");
83
+
53
84
  process.stdout.write(`PreMan MCP
54
85
 
55
86
  Usage:
56
- preman connect [options] Pick a coding agent and connect it
57
- npm exec -y premanmcp@latest -- login Create/login to PreMan from the terminal
58
- npm exec -y premanmcp@latest -- install [options] Install PreMan into Cursor MCP config
59
- npm exec -y premanmcp@latest -- Start the PreMan MCP server
60
- preman link|tools|run ... Drive a published hosted MCP
61
- ${CONNECT_HELP}
87
+ ${usageLines}
88
+ ${INTEGRATIONS_HELP}${CONNECT_HELP}${ENDPOINTS_HELP}${TEST_HELP}
62
89
  Login options:
63
90
  --email <email> Pre-fill the email prompt
64
91
  --backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
65
92
 
66
- Install options (Cursor only — prefer 'preman connect'):
93
+ Install options (Cursor only — prefer '${cli} connect'):
67
94
  --api-key <key> PreMan API key. If omitted, stored CLI credentials are used
68
95
  --backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
69
96
  --frontend <url> PreMan frontend URL. Defaults to ${DEFAULT_FRONTEND}
@@ -74,9 +101,9 @@ Install options (Cursor only — prefer 'preman connect'):
74
101
  --print Print the config instead of writing it
75
102
 
76
103
  Examples:
77
- npx premanmcp@latest connect
78
- preman connect --agent claude-code
79
- npm exec -y premanmcp@latest -- login
104
+ npm exec -y premanmcp@latest -- connect
105
+ ${cli} connect --agent claude-code
106
+ ${cli} login
80
107
  npm exec -y premanmcp@latest -- install --project --backend http://127.0.0.1:8000
81
108
  ${HOSTED_HELP}`);
82
109
  }
@@ -91,7 +118,7 @@ API key: ${creds.api_key}
91
118
  Saved to: ${CREDENTIALS_FILE}
92
119
 
93
120
  You can now run:
94
- preman connect
121
+ ${cli} connect
95
122
  `);
96
123
  }
97
124
 
@@ -126,12 +153,12 @@ Server name: ${serverName}
126
153
  Backend: ${serverConfig.env.PREMAN_BACKEND}
127
154
 
128
155
  Next steps:
129
- 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 npm exec -y premanmcp@latest -- login to create/connect your account and generate an API key."}
156
+ 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."}
130
157
  2. Restart Cursor or toggle the PreMan MCP server off/on.
131
158
  3. In your API repo, ask your coding agent:
132
159
  "Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
133
160
 
134
- Tip: 'preman connect' also supports Claude Code and Codex, and links the agent to your account.
161
+ Tip: '${cli} connect' also supports Claude Code and Codex, and links the agent to your account.
135
162
  `);
136
163
  }
137
164
 
@@ -167,6 +194,16 @@ async function main() {
167
194
  await loginCommand();
168
195
  } else if (command === "connect") {
169
196
  await connectCommand(commandArgs);
197
+ } else if (command === "onboard" || command === "setup") {
198
+ // makeArgs/authenticateTerminal/connectCommand are injected rather than
199
+ // imported there, so integrations.js stays free of a cycle back into the CLI.
200
+ await onboardCommand(commandArgs, { makeArgs, authenticateTerminal, connectCommand });
201
+ } else if (command === "aws") {
202
+ await awsCommand(makeArgs(commandArgs));
203
+ } else if (command === "github") {
204
+ await githubCommand(makeArgs(commandArgs));
205
+ } else if (command === "slack") {
206
+ await slackCommand(makeArgs(commandArgs));
170
207
  } else if (command === "install") {
171
208
  await installCursorMcp();
172
209
  } else if (command === "link") {
@@ -175,6 +212,10 @@ async function main() {
175
212
  await toolsCommand(commandArgs);
176
213
  } else if (command === "run") {
177
214
  await runCommand(commandArgs);
215
+ } else if (command === "endpoints") {
216
+ await endpointsCommand(commandArgs);
217
+ } else if (command === "test") {
218
+ await testCommand(commandArgs);
178
219
  } else if (command === "help" || command === "--help" || command === "-h") {
179
220
  printHelp();
180
221
  } else if (command === "start") {