premanmcp 0.3.5 → 0.5.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,26 +4,33 @@ 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
- ## Install
7
+ ## Connect
8
8
 
9
9
  ```bash
10
- npm exec -y premanmcp@latest -- install
10
+ npx premanmcp@latest connect
11
11
  ```
12
12
 
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.
15
+
13
16
  Local development form:
14
17
 
15
18
  ```bash
16
- node bin/cli.js install
19
+ node bin/cli.js connect
17
20
  ```
18
21
 
19
- First-time users are prompted for email, OTP, and password directly in the terminal. PreMan creates or connects the account, generates an API key, saves it to `~/.preman/credentials.json`, then writes a `preman` MCP server into `~/.cursor/mcp.json`:
22
+ First-time users are prompted for email, OTP, and password directly in the terminal.
23
+ PreMan creates or connects the account, generates an API key, saves it to
24
+ `~/.preman/credentials.json`, then writes a `preman` MCP server into the config your
25
+ agent actually reads (`~/.cursor/mcp.json`, Claude Code's MCP config, or
26
+ `~/.codex/config.toml`):
20
27
 
21
28
  ```json
22
29
  {
23
30
  "mcpServers": {
24
31
  "preman": {
25
- "command": "npm",
26
- "args": ["exec", "-y", "--package", "premanmcp@latest", "--", "premanmcp"],
32
+ "command": "npx",
33
+ "args": ["-y", "premanmcp@latest"],
27
34
  "env": {
28
35
  "PREMAN_BACKEND": "https://api.preman.live",
29
36
  "PREMAN_FRONTEND": "https://app.preman.live"
@@ -33,9 +40,21 @@ First-time users are prompted for email, OTP, and password directly in the termi
33
40
  }
34
41
  ```
35
42
 
36
- Restart Cursor or toggle the MCP server off/on after install. The MCP server loads the saved key automatically.
43
+ Restart your agent afterwards, then ask it to `run preman_status` to finish linking.
44
+
45
+ Useful flags: `--agent cursor|claude-code|codex` skips the picker, `--project` writes
46
+ project-local config, `--print` shows the config without writing it.
47
+
48
+ `preman install` still exists and does the Cursor-only half of this.
49
+
50
+ ### Cloud dispatch (optional)
37
51
 
38
- You can also create or connect your account before install:
52
+ `connect` offers to store a coding-agent credential a Cursor API key, or a Claude
53
+ Code routine token and id. With one saved, PreMan can start an agent run for you when
54
+ it finds a failing endpoint instead of handing back a prompt to paste. Press Enter to
55
+ skip; everything else still works.
56
+
57
+ You can also create or connect your account first:
39
58
 
40
59
  ```bash
41
60
  npm exec -y premanmcp@latest -- login
@@ -44,13 +63,13 @@ npm exec -y premanmcp@latest -- login
44
63
  You can also pass the key directly:
45
64
 
46
65
  ```bash
47
- npm exec -y premanmcp@latest -- install --api-key pm_live_xxx
66
+ npx premanmcp@latest connect --api-key pm_live_xxx
48
67
  ```
49
68
 
50
- For project-local installation:
69
+ For project-local config:
51
70
 
52
71
  ```bash
53
- npm exec -y premanmcp@latest -- install --project
72
+ npx premanmcp@latest connect --project
54
73
  ```
55
74
 
56
75
  ## What It Does
@@ -62,13 +81,14 @@ npm exec -y premanmcp@latest -- install --project
62
81
  - Adds API-key auth for PreMan MCP access.
63
82
  - Supports hosted MCPs with consumer tokens for customer-facing agent access.
64
83
  - Records per-call observability so teams can audit which agent did what.
84
+ - Hands failing-endpoint alerts to your agent as fix tasks (`preman_get_fix_task` → repro curl → `preman_complete_fix_task`).
65
85
 
66
86
  ## Common Agent Commands
67
87
 
68
88
  After installing, ask your coding agent:
69
89
 
70
90
  ```text
71
- Use the PreMan MCP to scan this codebase, deploy a hosted MCP, and give me the install command.
91
+ Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet.
72
92
  ```
73
93
 
74
94
  ```text
@@ -87,22 +107,24 @@ Convert these endpoints into an MCP.
87
107
  Show me the audit log for this hosted MCP.
88
108
  ```
89
109
 
90
- ## Cursor
110
+ ```text
111
+ Pull my pending PreMan fix tasks and fix the failing endpoint.
112
+ ```
91
113
 
92
- The installer targets Cursor by default:
114
+ ## Cursor
93
115
 
94
116
  ```bash
95
- npm exec -y premanmcp@latest -- install
117
+ npx premanmcp@latest connect --agent cursor
96
118
  ```
97
119
 
98
- Manual Cursor config:
120
+ Manual Cursor config, if you would rather write it yourself:
99
121
 
100
122
  ```json
101
123
  {
102
124
  "mcpServers": {
103
125
  "preman": {
104
- "command": "npm",
105
- "args": ["exec", "-y", "--package", "premanmcp@latest", "--", "premanmcp"]
126
+ "command": "npx",
127
+ "args": ["-y", "premanmcp@latest"]
106
128
  }
107
129
  }
108
130
  }
@@ -111,18 +133,33 @@ Manual Cursor config:
111
133
  ## Claude Code
112
134
 
113
135
  ```bash
114
- claude mcp add preman -- npm exec -y --package premanmcp@latest -- premanmcp
136
+ npx premanmcp@latest connect --agent claude-code
115
137
  ```
116
138
 
139
+ Equivalent manual command:
140
+
141
+ ```bash
142
+ claude mcp add preman -- npx -y premanmcp@latest
143
+ ```
144
+
145
+ ## Codex
146
+
147
+ ```bash
148
+ npx premanmcp@latest connect --agent codex
149
+ ```
150
+
151
+ Writes an `[mcp_servers.preman]` block into `~/.codex/config.toml`.
152
+
117
153
  ## CLI
118
154
 
119
155
  ```bash
120
- npm exec -y premanmcp@latest -- # Start the MCP server
121
- npm exec -y premanmcp@latest -- login # Create/login and generate a PreMan API key
122
- npm exec -y premanmcp@latest -- install # Install Cursor MCP config
123
- npm exec -y premanmcp@latest -- install --project # Write .cursor/mcp.json in current repo
124
- npm exec -y premanmcp@latest -- install --print # Print config without writing
125
- npm exec -y premanmcp@latest -- install --api-key KEY # Install with your PreMan API key
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
160
+ npm exec -y premanmcp@latest -- # Start the MCP server
161
+ npm exec -y premanmcp@latest -- login # Create/login and generate a PreMan API key
162
+ npm exec -y premanmcp@latest -- install # Cursor-only installer (legacy)
126
163
  ```
127
164
 
128
165
  Options:
@@ -135,11 +172,43 @@ Options:
135
172
  - `--skip-login`: Install config without interactive terminal auth.
136
173
  - `--print`: Print the generated MCP config without writing it.
137
174
 
175
+ ## Run a hosted MCP from the terminal
176
+
177
+ Installing the package also gives you a `preman` command. Once a selection is published as a
178
+ hosted MCP, you can call its tools straight from a shell — same published selection, same
179
+ consumer token, and same audit trail as an agent calling `POST /h/<id>/mcp`. Updating the
180
+ selection updates the terminal; there is nothing to re-install.
181
+
182
+ ```bash
183
+ preman link https://api.preman.live/h/<id>/mcp --env staging --token pm_hmcp_xxx
184
+ preman tools --env staging
185
+ preman run post_users_id_orders --env staging --arg id=42 --json '{"body":{"sku":"A1"}}'
186
+ ```
187
+
188
+ `link` takes the URL straight from the deploy install snippet (a bare hosted MCP id works too,
189
+ with `--backend`). Profiles are stored in `~/.preman/cli.json` (mode 0600); `--env <name>`
190
+ picks one. Tool names are the ones the runtime publishes — run `preman tools` to see them.
191
+
192
+ Run options:
193
+
194
+ - `--env <name>`: Profile to use. Falls back to `PREMAN_MCP_URL`, then the default profile.
195
+ - `--arg key=value`: String argument (repeatable). `--arg key:=json` sends a JSON-typed value.
196
+ - `--json '{...}'`: Full arguments object. `--json -` reads it from stdin.
197
+ - `--timeout <seconds>`: Client-side timeout. Defaults to 60.
198
+ - `--json-out`: Print the raw JSON-RPC result.
199
+ - `<tool>` may be dotted — `preman run staging.get_orders` selects the profile inline.
200
+
201
+ Exit codes: `0` success, `1` the tool returned an error, `2` usage, `3` authentication,
202
+ `4` JSON-RPC error (e.g. unknown tool), `5` network failure.
203
+
138
204
  ## Environment Variables
139
205
 
140
206
  - `PREMAN_API_KEY`: PreMan API key.
141
207
  - `PREMAN_BACKEND`: PreMan backend URL.
142
208
  - `PREMAN_FRONTEND`: PreMan frontend URL.
209
+ - `PREMAN_MCP_URL`: Hosted MCP endpoint (`…/h/<id>/mcp`) for `preman run`/`preman tools` when
210
+ `--env` is omitted — useful in CI, where no profile file exists.
211
+ - `PREMAN_MCP_TOKEN`: Consumer token for `PREMAN_MCP_URL`.
143
212
 
144
213
  If `PREMAN_API_KEY` is omitted, the MCP server loads credentials from `~/.preman/credentials.json`.
145
214
 
package/bin/cli.js CHANGED
@@ -11,18 +11,26 @@
11
11
  */
12
12
 
13
13
  import { spawn } from "node:child_process";
14
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
14
+ import { existsSync } from "node:fs";
15
15
  import os from "node:os";
16
16
  import path from "node:path";
17
- import { createInterface } from "node:readline/promises";
18
17
  import { fileURLToPath } from "node:url";
19
18
 
19
+ import { CONNECT_HELP, connectCommand, writeCursorConfig } from "./connect.js";
20
+ import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
21
+ import {
22
+ CREDENTIALS_FILE,
23
+ DEFAULT_BACKEND,
24
+ DEFAULT_FRONTEND,
25
+ authenticateTerminal,
26
+ buildServerConfig,
27
+ hasKeyAvailable,
28
+ makeArgs,
29
+ readStoredCredentials,
30
+ } from "./shared.js";
31
+
20
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
33
  const ROOT = path.join(__dirname, "..");
22
- const DEFAULT_BACKEND = "https://api.preman.live";
23
- const DEFAULT_FRONTEND = "https://app.preman.live";
24
- const CREDENTIALS_DIR = path.join(os.homedir(), ".preman");
25
- const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
26
34
 
27
35
  const args = process.argv.slice(2);
28
36
  const command = args[0] === "--help" || args[0] === "-h"
@@ -31,30 +39,31 @@ const command = args[0] === "--help" || args[0] === "-h"
31
39
  ? args[0]
32
40
  : "start";
33
41
  const commandArgs = command === "start" ? args : args.slice(1);
42
+ const cliArgs = makeArgs(commandArgs);
34
43
 
35
44
  function argValue(name, fallback = "") {
36
- const index = commandArgs.indexOf(name);
37
- if (index === -1) return fallback;
38
- return commandArgs[index + 1] || fallback;
45
+ return cliArgs.value(name, fallback);
39
46
  }
40
47
 
41
48
  function hasFlag(name) {
42
- return commandArgs.includes(name);
49
+ return cliArgs.has(name);
43
50
  }
44
51
 
45
52
  function printHelp() {
46
53
  process.stdout.write(`PreMan MCP
47
54
 
48
55
  Usage:
56
+ preman connect [options] Pick a coding agent and connect it
49
57
  npm exec -y premanmcp@latest -- login Create/login to PreMan from the terminal
50
58
  npm exec -y premanmcp@latest -- install [options] Install PreMan into Cursor MCP config
51
59
  npm exec -y premanmcp@latest -- Start the PreMan MCP server
52
-
60
+ preman link|tools|run ... Drive a published hosted MCP
61
+ ${CONNECT_HELP}
53
62
  Login options:
54
63
  --email <email> Pre-fill the email prompt
55
64
  --backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
56
65
 
57
- Install options:
66
+ Install options (Cursor only — prefer 'preman connect'):
58
67
  --api-key <key> PreMan API key. If omitted, stored CLI credentials are used
59
68
  --backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
60
69
  --frontend <url> PreMan frontend URL. Defaults to ${DEFAULT_FRONTEND}
@@ -65,270 +74,15 @@ Install options:
65
74
  --print Print the config instead of writing it
66
75
 
67
76
  Examples:
77
+ npx premanmcp@latest connect
78
+ preman connect --agent claude-code
68
79
  npm exec -y premanmcp@latest -- login
69
- npm exec -y premanmcp@latest -- install
70
- npm exec -y premanmcp@latest -- install --api-key pm_live_xxx
71
80
  npm exec -y premanmcp@latest -- install --project --backend http://127.0.0.1:8000
72
- `);
73
- }
74
-
75
- function readJsonFile(filePath) {
76
- if (!existsSync(filePath)) return {};
77
- const raw = readFileSync(filePath, "utf8").trim();
78
- if (!raw) return {};
79
- try {
80
- return JSON.parse(raw);
81
- } catch (error) {
82
- throw new Error(`Could not parse ${filePath}: ${error.message}`);
83
- }
84
- }
85
-
86
- function readStoredCredentials() {
87
- try {
88
- const raw = readFileSync(CREDENTIALS_FILE, "utf8").trim();
89
- if (!raw) return null;
90
- const creds = JSON.parse(raw);
91
- if (creds && typeof creds.api_key === "string" && creds.api_key.startsWith("pm_live_")) {
92
- return creds;
93
- }
94
- } catch {
95
- // No stored credentials yet.
96
- }
97
- return null;
98
- }
99
-
100
- function saveStoredCredentials(creds) {
101
- mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
102
- writeFileSync(CREDENTIALS_FILE, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
103
- }
104
-
105
- function backendUrl() {
106
- return argValue("--backend", process.env.PREMAN_BACKEND || DEFAULT_BACKEND).replace(/\/+$/, "");
107
- }
108
-
109
- async function promptText(question) {
110
- const rl = createInterface({ input: process.stdin, output: process.stdout });
111
- try {
112
- return (await rl.question(question)).trim();
113
- } finally {
114
- rl.close();
115
- }
116
- }
117
-
118
- async function promptSecret(question) {
119
- if (!process.stdin.isTTY || !process.stdin.setRawMode) {
120
- return promptText(question);
121
- }
122
-
123
- return new Promise((resolve) => {
124
- const stdin = process.stdin;
125
- const stdout = process.stdout;
126
- const wasRaw = stdin.isRaw;
127
- let value = "";
128
-
129
- function cleanup() {
130
- stdin.off("data", onData);
131
- stdin.setRawMode(Boolean(wasRaw));
132
- stdin.pause();
133
- }
134
-
135
- function onData(chunk) {
136
- const text = String(chunk);
137
- if (text === "\u0003") {
138
- stdout.write("\n");
139
- cleanup();
140
- process.exit(130);
141
- }
142
- if (text === "\r" || text === "\n" || text === "\u0004") {
143
- stdout.write("\n");
144
- cleanup();
145
- resolve(value);
146
- return;
147
- }
148
- if (text === "\u007f" || text === "\b") {
149
- if (value.length) {
150
- value = value.slice(0, -1);
151
- stdout.write("\b \b");
152
- }
153
- return;
154
- }
155
- value += text;
156
- stdout.write("*");
157
- }
158
-
159
- stdout.write(question);
160
- stdin.setRawMode(true);
161
- stdin.resume();
162
- stdin.setEncoding("utf8");
163
- stdin.on("data", onData);
164
- });
165
- }
166
-
167
- async function promptPasswordTwice() {
168
- const password = await promptSecret("Create password: ");
169
- if (!password || password.length < 6) {
170
- throw new Error("Password must be at least 6 characters.");
171
- }
172
- const confirm = await promptSecret("Confirm password: ");
173
- if (password !== confirm) {
174
- throw new Error("Passwords do not match.");
175
- }
176
- return password;
177
- }
178
-
179
- async function callBackendJson(method, routePath, { json, token, query } = {}) {
180
- const url = new URL(routePath.replace(/^\/+/, ""), `${backendUrl()}/`);
181
- if (query) {
182
- for (const [key, value] of Object.entries(query)) {
183
- if (value != null && value !== "") url.searchParams.set(key, String(value));
184
- }
185
- }
186
-
187
- const headers = { Accept: "application/json" };
188
- const hasBody = json !== undefined && json !== null;
189
- if (hasBody) headers["Content-Type"] = "application/json";
190
- if (token) headers.Authorization = `Bearer ${token}`;
191
-
192
- const resp = await fetch(url, {
193
- method,
194
- headers,
195
- body: hasBody ? JSON.stringify(json) : undefined,
196
- });
197
- const text = await resp.text();
198
- let body = {};
199
- try {
200
- body = text ? JSON.parse(text) : {};
201
- } catch {
202
- body = { raw: text };
203
- }
204
- return {
205
- status_code: resp.status,
206
- ok: resp.ok,
207
- ...body,
208
- };
209
- }
210
-
211
- function assertOk(result, action) {
212
- if (result.ok) return;
213
- const detail = result.detail || result.message || result.raw || `${action} failed`;
214
- throw new Error(`${action} failed: ${result.status_code} ${detail}`);
215
- }
216
-
217
- async function verifyUnconfirmedAccount(email) {
218
- process.stdout.write("This email exists but is not verified. Sending a new OTP.\n");
219
- const resend = await callBackendJson("POST", "/auth/resend-otp", { json: { email } });
220
- assertOk(resend, "resend OTP");
221
- const otp = await promptText("Verification code: ");
222
- const verified = await callBackendJson("POST", "/auth/verify-otp", { json: { email, otp } });
223
- assertOk(verified, "verify OTP");
224
- return verified.access_token;
225
- }
226
-
227
- async function authenticateTerminal() {
228
- const explicitKey = argValue("--api-key", process.env.PREMAN_API_KEY || "");
229
- if (explicitKey && explicitKey.startsWith("pm_live_")) {
230
- const creds = {
231
- api_key: explicitKey,
232
- backend_url: backendUrl(),
233
- user_email: argValue("--email", ""),
234
- device_name: os.hostname(),
235
- created_at: new Date().toISOString(),
236
- };
237
- saveStoredCredentials(creds);
238
- return creds;
239
- }
240
-
241
- const email = (argValue("--email", "") || await promptText("Email: ")).trim().toLowerCase();
242
- if (!email) throw new Error("Email is required.");
243
-
244
- process.stdout.write(`Checking PreMan account for ${email}...\n`);
245
- const account = await callBackendJson("GET", "/auth/needs-password", { query: { email } });
246
- assertOk(account, "check account");
247
-
248
- let accessToken = "";
249
-
250
- if (!account.exists) {
251
- process.stdout.write("No PreMan account found. Creating one now.\n");
252
- const signup = await callBackendJson("POST", "/auth/start-signup", {
253
- json: { email },
254
- });
255
- assertOk(signup, "start signup");
256
- process.stdout.write("Verification code sent. Check your email.\n");
257
- const otp = await promptText("Verification code: ");
258
- const password = await promptPasswordTwice();
259
- const setPassword = await callBackendJson("POST", "/auth/set-password", {
260
- json: { email, otp, new_password: password },
261
- });
262
- assertOk(setPassword, "set password");
263
- accessToken = String(setPassword.access_token || "");
264
- } else if (account.needs_password) {
265
- process.stdout.write("This account needs a password. Sending an OTP first.\n");
266
- const resend = await callBackendJson("POST", "/auth/resend-otp", { json: { email } });
267
- assertOk(resend, "resend OTP");
268
- const otp = await promptText("Verification code: ");
269
- const password = await promptPasswordTwice();
270
- const setPassword = await callBackendJson("POST", "/auth/set-password", {
271
- json: { email, otp, new_password: password },
272
- });
273
- assertOk(setPassword, "set password");
274
- accessToken = String(setPassword.access_token || "");
275
- } else {
276
- const password = await promptSecret("Password: ");
277
- const login = await callBackendJson("POST", "/auth/login", {
278
- json: { email, password },
279
- });
280
- if (!login.ok && login.status_code === 403 && String(login.detail || "").toLowerCase().includes("not verified")) {
281
- accessToken = await verifyUnconfirmedAccount(email);
282
- } else {
283
- assertOk(login, "login");
284
- accessToken = String(login.access_token || "");
285
- }
286
- }
287
-
288
- if (!accessToken) throw new Error("PreMan did not return an access token.");
289
-
290
- const keyName = `PreMan MCP CLI (${os.hostname()})`;
291
- const key = await callBackendJson("POST", "/api-keys", {
292
- token: accessToken,
293
- json: { name: keyName },
294
- });
295
- assertOk(key, "create API key");
296
- if (!key.key || !String(key.key).startsWith("pm_live_")) {
297
- throw new Error("PreMan did not return a valid API key.");
298
- }
299
-
300
- const creds = {
301
- api_key: String(key.key),
302
- backend_url: backendUrl(),
303
- user_email: email,
304
- device_name: os.hostname(),
305
- created_at: new Date().toISOString(),
306
- };
307
- saveStoredCredentials(creds);
308
- return creds;
309
- }
310
-
311
- function buildServerConfig() {
312
- const apiKey = argValue("--api-key", process.env.PREMAN_API_KEY || "");
313
- const backend = argValue("--backend", process.env.PREMAN_BACKEND || DEFAULT_BACKEND);
314
- const frontend = argValue("--frontend", process.env.PREMAN_FRONTEND || DEFAULT_FRONTEND);
315
- const env = {
316
- PREMAN_BACKEND: backend,
317
- PREMAN_FRONTEND: frontend,
318
- };
319
- if (apiKey) {
320
- env.PREMAN_API_KEY = apiKey;
321
- }
322
-
323
- return {
324
- command: "npm",
325
- args: ["exec", "-y", "--package", "premanmcp@latest", "--", "premanmcp"],
326
- env,
327
- };
81
+ ${HOSTED_HELP}`);
328
82
  }
329
83
 
330
84
  async function loginCommand() {
331
- const creds = await authenticateTerminal();
85
+ const creds = await authenticateTerminal(cliArgs);
332
86
  process.stdout.write(`PreMan account ready.
333
87
 
334
88
  Email: ${creds.user_email || "unknown"}
@@ -337,60 +91,47 @@ API key: ${creds.api_key}
337
91
  Saved to: ${CREDENTIALS_FILE}
338
92
 
339
93
  You can now run:
340
- npm exec -y premanmcp@latest -- install
94
+ preman connect
341
95
  `);
342
96
  }
343
97
 
98
+ /**
99
+ * Cursor-only installer, kept for the documented `install` flow. `connect` is
100
+ * the same write plus agent choice, pairing, and dispatch setup.
101
+ */
344
102
  async function installCursorMcp() {
345
103
  const serverName = argValue("--name", "preman");
346
104
  const projectInstall = hasFlag("--project");
347
105
 
348
- if (
349
- !hasFlag("--print") &&
350
- !hasFlag("--skip-login") &&
351
- !argValue("--api-key", "") &&
352
- !process.env.PREMAN_API_KEY &&
353
- !readStoredCredentials()
354
- ) {
106
+ if (!hasFlag("--print") && !hasFlag("--skip-login") && !hasKeyAvailable(cliArgs)) {
355
107
  process.stdout.write("First, let's create or connect your PreMan account.\n");
356
- await authenticateTerminal();
108
+ await authenticateTerminal(cliArgs);
357
109
  process.stdout.write("\n");
358
110
  }
359
111
 
360
- const serverConfig = buildServerConfig();
112
+ const serverConfig = buildServerConfig(cliArgs);
361
113
 
362
114
  if (hasFlag("--print")) {
363
115
  process.stdout.write(`${JSON.stringify({ mcpServers: { [serverName]: serverConfig } }, null, 2)}\n`);
364
116
  return;
365
117
  }
366
118
 
367
- const configPath = projectInstall
368
- ? path.join(process.cwd(), ".cursor", "mcp.json")
369
- : path.join(os.homedir(), ".cursor", "mcp.json");
119
+ const written = writeCursorConfig({ serverName, serverConfig, projectInstall });
370
120
 
371
- const config = {
372
- ...readJsonFile(configPath),
373
- };
374
- config.mcpServers = {
375
- ...(config.mcpServers || {}),
376
- [serverName]: serverConfig,
377
- };
378
-
379
- const rendered = `${JSON.stringify(config, null, 2)}\n`;
380
- mkdirSync(path.dirname(configPath), { recursive: true });
381
- writeFileSync(configPath, rendered, { mode: 0o600 });
382
-
383
- const hasInlineKey = Boolean(config.mcpServers[serverName].env.PREMAN_API_KEY);
121
+ const hasInlineKey = Boolean(serverConfig.env.PREMAN_API_KEY);
384
122
  const hasStoredKey = Boolean(readStoredCredentials());
385
- process.stdout.write(`PreMan MCP installed in ${configPath}
123
+ process.stdout.write(`PreMan MCP installed in ${written.path}
386
124
 
387
125
  Server name: ${serverName}
388
- Backend: ${config.mcpServers[serverName].env.PREMAN_BACKEND}
126
+ Backend: ${serverConfig.env.PREMAN_BACKEND}
389
127
 
390
128
  Next steps:
391
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."}
392
130
  2. Restart Cursor or toggle the PreMan MCP server off/on.
393
- 3. Ask your agent: "Use the PreMan MCP to scan this codebase, deploy a hosted MCP, and give me the install command."
131
+ 3. In your API repo, ask your coding agent:
132
+ "Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
133
+
134
+ Tip: 'preman connect' also supports Claude Code and Codex, and links the agent to your account.
394
135
  `);
395
136
  }
396
137
 
@@ -424,8 +165,16 @@ function startServer() {
424
165
  async function main() {
425
166
  if (command === "login") {
426
167
  await loginCommand();
168
+ } else if (command === "connect") {
169
+ await connectCommand(commandArgs);
427
170
  } else if (command === "install") {
428
171
  await installCursorMcp();
172
+ } else if (command === "link") {
173
+ await linkCommand(commandArgs);
174
+ } else if (command === "tools") {
175
+ await toolsCommand(commandArgs);
176
+ } else if (command === "run") {
177
+ await runCommand(commandArgs);
429
178
  } else if (command === "help" || command === "--help" || command === "-h") {
430
179
  printHelp();
431
180
  } else if (command === "start") {
@@ -440,5 +189,5 @@ async function main() {
440
189
  main().catch((error) => {
441
190
  const message = error instanceof Error ? error.message : String(error);
442
191
  process.stderr.write(`[preman] ${message}\n`);
443
- process.exit(1);
192
+ process.exit(error?.exitCode || 1);
444
193
  });