premanmcp 0.5.0 → 0.7.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
@@ -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,7 +16,15 @@ 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,
@@ -53,12 +61,16 @@ function printHelp() {
53
61
  process.stdout.write(`PreMan MCP
54
62
 
55
63
  Usage:
64
+ preman onboard Sign in, then connect agent, GitHub, AWS, Slack
56
65
  preman connect [options] Pick a coding agent and connect it
66
+ preman aws | github | slack Connect one integration on its own
57
67
  npm exec -y premanmcp@latest -- login Create/login to PreMan from the terminal
58
68
  npm exec -y premanmcp@latest -- install [options] Install PreMan into Cursor MCP config
59
69
  npm exec -y premanmcp@latest -- Start the PreMan MCP server
60
70
  preman link|tools|run ... Drive a published hosted MCP
61
- ${CONNECT_HELP}
71
+ preman endpoints list|discover|setup ... Discover, list, and set up API endpoints
72
+ preman test <id> [--scenario ...] [--stress] Generate + run tests for an endpoint
73
+ ${INTEGRATIONS_HELP}${CONNECT_HELP}${ENDPOINTS_HELP}${TEST_HELP}
62
74
  Login options:
63
75
  --email <email> Pre-fill the email prompt
64
76
  --backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
@@ -74,7 +86,7 @@ Install options (Cursor only — prefer 'preman connect'):
74
86
  --print Print the config instead of writing it
75
87
 
76
88
  Examples:
77
- npx premanmcp@latest connect
89
+ npm exec -y premanmcp@latest -- connect
78
90
  preman connect --agent claude-code
79
91
  npm exec -y premanmcp@latest -- login
80
92
  npm exec -y premanmcp@latest -- install --project --backend http://127.0.0.1:8000
@@ -167,6 +179,16 @@ async function main() {
167
179
  await loginCommand();
168
180
  } else if (command === "connect") {
169
181
  await connectCommand(commandArgs);
182
+ } else if (command === "onboard" || command === "setup") {
183
+ // makeArgs/authenticateTerminal/connectCommand are injected rather than
184
+ // imported there, so integrations.js stays free of a cycle back into the CLI.
185
+ await onboardCommand(commandArgs, { makeArgs, authenticateTerminal, connectCommand });
186
+ } else if (command === "aws") {
187
+ await awsCommand(makeArgs(commandArgs));
188
+ } else if (command === "github") {
189
+ await githubCommand(makeArgs(commandArgs));
190
+ } else if (command === "slack") {
191
+ await slackCommand(makeArgs(commandArgs));
170
192
  } else if (command === "install") {
171
193
  await installCursorMcp();
172
194
  } else if (command === "link") {
@@ -175,6 +197,10 @@ async function main() {
175
197
  await toolsCommand(commandArgs);
176
198
  } else if (command === "run") {
177
199
  await runCommand(commandArgs);
200
+ } else if (command === "endpoints") {
201
+ await endpointsCommand(commandArgs);
202
+ } else if (command === "test") {
203
+ await testCommand(commandArgs);
178
204
  } else if (command === "help" || command === "--help" || command === "-h") {
179
205
  printHelp();
180
206
  } else if (command === "start") {