premanmcp 0.4.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/bin/cli.js CHANGED
@@ -11,20 +11,34 @@
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 { ENDPOINTS_HELP, TEST_HELP, endpointsCommand, testCommand } from "./api_tools.js";
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";
29
+ import {
30
+ CREDENTIALS_FILE,
31
+ DEFAULT_BACKEND,
32
+ DEFAULT_FRONTEND,
33
+ authenticateTerminal,
34
+ buildServerConfig,
35
+ hasKeyAvailable,
36
+ makeArgs,
37
+ readStoredCredentials,
38
+ } from "./shared.js";
21
39
 
22
40
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
41
  const ROOT = path.join(__dirname, "..");
24
- const DEFAULT_BACKEND = "https://api.preman.live";
25
- const DEFAULT_FRONTEND = "https://app.preman.live";
26
- const CREDENTIALS_DIR = path.join(os.homedir(), ".preman");
27
- const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
28
42
 
29
43
  const args = process.argv.slice(2);
30
44
  const command = args[0] === "--help" || args[0] === "-h"
@@ -33,31 +47,35 @@ const command = args[0] === "--help" || args[0] === "-h"
33
47
  ? args[0]
34
48
  : "start";
35
49
  const commandArgs = command === "start" ? args : args.slice(1);
50
+ const cliArgs = makeArgs(commandArgs);
36
51
 
37
52
  function argValue(name, fallback = "") {
38
- const index = commandArgs.indexOf(name);
39
- if (index === -1) return fallback;
40
- return commandArgs[index + 1] || fallback;
53
+ return cliArgs.value(name, fallback);
41
54
  }
42
55
 
43
56
  function hasFlag(name) {
44
- return commandArgs.includes(name);
57
+ return cliArgs.has(name);
45
58
  }
46
59
 
47
60
  function printHelp() {
48
61
  process.stdout.write(`PreMan MCP
49
62
 
50
63
  Usage:
64
+ preman onboard Sign in, then connect agent, GitHub, AWS, Slack
65
+ preman connect [options] Pick a coding agent and connect it
66
+ preman aws | github | slack Connect one integration on its own
51
67
  npm exec -y premanmcp@latest -- login Create/login to PreMan from the terminal
52
68
  npm exec -y premanmcp@latest -- install [options] Install PreMan into Cursor MCP config
53
69
  npm exec -y premanmcp@latest -- Start the PreMan MCP server
54
70
  preman link|tools|run ... Drive a published hosted MCP
55
-
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}
56
74
  Login options:
57
75
  --email <email> Pre-fill the email prompt
58
76
  --backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
59
77
 
60
- Install options:
78
+ Install options (Cursor only — prefer 'preman connect'):
61
79
  --api-key <key> PreMan API key. If omitted, stored CLI credentials are used
62
80
  --backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
63
81
  --frontend <url> PreMan frontend URL. Defaults to ${DEFAULT_FRONTEND}
@@ -68,296 +86,15 @@ Install options:
68
86
  --print Print the config instead of writing it
69
87
 
70
88
  Examples:
89
+ npm exec -y premanmcp@latest -- connect
90
+ preman connect --agent claude-code
71
91
  npm exec -y premanmcp@latest -- login
72
- npm exec -y premanmcp@latest -- install
73
- npm exec -y premanmcp@latest -- install --api-key pm_live_xxx
74
92
  npm exec -y premanmcp@latest -- install --project --backend http://127.0.0.1:8000
75
93
  ${HOSTED_HELP}`);
76
94
  }
77
95
 
78
- function readJsonFile(filePath) {
79
- if (!existsSync(filePath)) return {};
80
- const raw = readFileSync(filePath, "utf8").trim();
81
- if (!raw) return {};
82
- try {
83
- return JSON.parse(raw);
84
- } catch (error) {
85
- throw new Error(`Could not parse ${filePath}: ${error.message}`);
86
- }
87
- }
88
-
89
- function readStoredCredentials() {
90
- try {
91
- const raw = readFileSync(CREDENTIALS_FILE, "utf8").trim();
92
- if (!raw) return null;
93
- const creds = JSON.parse(raw);
94
- if (creds && typeof creds.api_key === "string" && creds.api_key.startsWith("pm_live_")) {
95
- return creds;
96
- }
97
- } catch {
98
- // No stored credentials yet.
99
- }
100
- return null;
101
- }
102
-
103
- function saveStoredCredentials(creds) {
104
- mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
105
- writeFileSync(CREDENTIALS_FILE, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
106
- }
107
-
108
- function backendUrl() {
109
- return argValue("--backend", process.env.PREMAN_BACKEND || DEFAULT_BACKEND).replace(/\/+$/, "");
110
- }
111
-
112
- async function promptText(question) {
113
- const rl = createInterface({ input: process.stdin, output: process.stdout });
114
- try {
115
- return (await rl.question(question)).trim();
116
- } finally {
117
- rl.close();
118
- }
119
- }
120
-
121
- async function promptSecret(question) {
122
- if (!process.stdin.isTTY || !process.stdin.setRawMode) {
123
- return promptText(question);
124
- }
125
-
126
- return new Promise((resolve) => {
127
- const stdin = process.stdin;
128
- const stdout = process.stdout;
129
- const wasRaw = stdin.isRaw;
130
- let value = "";
131
-
132
- function cleanup() {
133
- stdin.off("data", onData);
134
- stdin.setRawMode(Boolean(wasRaw));
135
- stdin.pause();
136
- }
137
-
138
- function onData(chunk) {
139
- const text = String(chunk);
140
- if (text === "\u0003") {
141
- stdout.write("\n");
142
- cleanup();
143
- process.exit(130);
144
- }
145
- if (text === "\r" || text === "\n" || text === "\u0004") {
146
- stdout.write("\n");
147
- cleanup();
148
- resolve(value);
149
- return;
150
- }
151
- if (text === "\u007f" || text === "\b") {
152
- if (value.length) {
153
- value = value.slice(0, -1);
154
- stdout.write("\b \b");
155
- }
156
- return;
157
- }
158
- value += text;
159
- stdout.write("*");
160
- }
161
-
162
- stdout.write(question);
163
- stdin.setRawMode(true);
164
- stdin.resume();
165
- stdin.setEncoding("utf8");
166
- stdin.on("data", onData);
167
- });
168
- }
169
-
170
- async function promptPasswordTwice() {
171
- const password = await promptSecret("Create password: ");
172
- if (!password || password.length < 6) {
173
- throw new Error("Password must be at least 6 characters.");
174
- }
175
- const confirm = await promptSecret("Confirm password: ");
176
- if (password !== confirm) {
177
- throw new Error("Passwords do not match.");
178
- }
179
- return password;
180
- }
181
-
182
- async function callBackendJson(method, routePath, { json, token, query } = {}) {
183
- const url = new URL(routePath.replace(/^\/+/, ""), `${backendUrl()}/`);
184
- if (query) {
185
- for (const [key, value] of Object.entries(query)) {
186
- if (value != null && value !== "") url.searchParams.set(key, String(value));
187
- }
188
- }
189
-
190
- const headers = { Accept: "application/json" };
191
- const hasBody = json !== undefined && json !== null;
192
- if (hasBody) headers["Content-Type"] = "application/json";
193
- if (token) headers.Authorization = `Bearer ${token}`;
194
-
195
- const resp = await fetch(url, {
196
- method,
197
- headers,
198
- body: hasBody ? JSON.stringify(json) : undefined,
199
- });
200
- const text = await resp.text();
201
- let body = {};
202
- try {
203
- body = text ? JSON.parse(text) : {};
204
- } catch {
205
- body = { raw: text };
206
- }
207
- return {
208
- status_code: resp.status,
209
- ok: resp.ok,
210
- ...body,
211
- };
212
- }
213
-
214
- function assertOk(result, action) {
215
- if (result.ok) return;
216
- const detail = result.detail || result.message || result.raw || `${action} failed`;
217
- throw new Error(`${action} failed: ${result.status_code} ${detail}`);
218
- }
219
-
220
- async function verifyUnconfirmedAccount(email) {
221
- process.stdout.write("This email exists but is not verified. Sending a new OTP.\n");
222
- const resend = await callBackendJson("POST", "/auth/resend-otp", { json: { email } });
223
- assertOk(resend, "resend OTP");
224
- const otp = await promptText("Verification code: ");
225
- const verified = await callBackendJson("POST", "/auth/verify-otp", { json: { email, otp } });
226
- assertOk(verified, "verify OTP");
227
- return verified.access_token;
228
- }
229
-
230
- async function createAccountFromTerminal(email) {
231
- const signup = await callBackendJson("POST", "/auth/start-signup", {
232
- json: { email },
233
- });
234
-
235
- if (signup.ok) {
236
- process.stdout.write("Verification code sent. Check your email.\n");
237
- const otp = await promptText("Verification code: ");
238
- const password = await promptPasswordTwice();
239
- const setPassword = await callBackendJson("POST", "/auth/set-password", {
240
- json: { email, otp, new_password: password },
241
- });
242
- assertOk(setPassword, "set password");
243
- return String(setPassword.access_token || "");
244
- }
245
-
246
- if (signup.status_code !== 404) {
247
- assertOk(signup, "start signup");
248
- }
249
-
250
- process.stdout.write(
251
- "This PreMan backend uses the password-first signup flow. Create your password now, then enter the email code.\n"
252
- );
253
- const password = await promptPasswordTwice();
254
- const legacySignup = await callBackendJson("POST", "/auth/signup", {
255
- json: { email, password },
256
- });
257
- assertOk(legacySignup, "signup");
258
- process.stdout.write("Verification code sent. Check your email.\n");
259
- const otp = await promptText("Verification code: ");
260
- const verified = await callBackendJson("POST", "/auth/verify-otp", {
261
- json: { email, otp },
262
- });
263
- assertOk(verified, "verify OTP");
264
- return String(verified.access_token || "");
265
- }
266
-
267
- async function authenticateTerminal() {
268
- const explicitKey = argValue("--api-key", process.env.PREMAN_API_KEY || "");
269
- if (explicitKey && explicitKey.startsWith("pm_live_")) {
270
- const creds = {
271
- api_key: explicitKey,
272
- backend_url: backendUrl(),
273
- user_email: argValue("--email", ""),
274
- device_name: os.hostname(),
275
- created_at: new Date().toISOString(),
276
- };
277
- saveStoredCredentials(creds);
278
- return creds;
279
- }
280
-
281
- const email = (argValue("--email", "") || await promptText("Email: ")).trim().toLowerCase();
282
- if (!email) throw new Error("Email is required.");
283
-
284
- process.stdout.write(`Checking PreMan account for ${email}...\n`);
285
- const account = await callBackendJson("GET", "/auth/needs-password", { query: { email } });
286
- assertOk(account, "check account");
287
-
288
- let accessToken = "";
289
-
290
- if (!account.exists) {
291
- process.stdout.write("No PreMan account found. Creating one now.\n");
292
- accessToken = await createAccountFromTerminal(email);
293
- } else if (account.needs_password) {
294
- process.stdout.write("This account needs a password. Sending an OTP first.\n");
295
- const resend = await callBackendJson("POST", "/auth/resend-otp", { json: { email } });
296
- assertOk(resend, "resend OTP");
297
- const otp = await promptText("Verification code: ");
298
- const password = await promptPasswordTwice();
299
- const setPassword = await callBackendJson("POST", "/auth/set-password", {
300
- json: { email, otp, new_password: password },
301
- });
302
- assertOk(setPassword, "set password");
303
- accessToken = String(setPassword.access_token || "");
304
- } else {
305
- const password = await promptSecret("Password: ");
306
- const login = await callBackendJson("POST", "/auth/login", {
307
- json: { email, password },
308
- });
309
- if (!login.ok && login.status_code === 403 && String(login.detail || "").toLowerCase().includes("not verified")) {
310
- accessToken = await verifyUnconfirmedAccount(email);
311
- } else {
312
- assertOk(login, "login");
313
- accessToken = String(login.access_token || "");
314
- }
315
- }
316
-
317
- if (!accessToken) throw new Error("PreMan did not return an access token.");
318
-
319
- const keyName = `PreMan MCP CLI (${os.hostname()})`;
320
- const key = await callBackendJson("POST", "/api-keys", {
321
- token: accessToken,
322
- json: { name: keyName },
323
- });
324
- assertOk(key, "create API key");
325
- if (!key.key || !String(key.key).startsWith("pm_live_")) {
326
- throw new Error("PreMan did not return a valid API key.");
327
- }
328
-
329
- const creds = {
330
- api_key: String(key.key),
331
- backend_url: backendUrl(),
332
- user_email: email,
333
- device_name: os.hostname(),
334
- created_at: new Date().toISOString(),
335
- };
336
- saveStoredCredentials(creds);
337
- return creds;
338
- }
339
-
340
- function buildServerConfig() {
341
- const apiKey = argValue("--api-key", process.env.PREMAN_API_KEY || "");
342
- const backend = argValue("--backend", process.env.PREMAN_BACKEND || DEFAULT_BACKEND);
343
- const frontend = argValue("--frontend", process.env.PREMAN_FRONTEND || DEFAULT_FRONTEND);
344
- const env = {
345
- PREMAN_BACKEND: backend,
346
- PREMAN_FRONTEND: frontend,
347
- };
348
- if (apiKey) {
349
- env.PREMAN_API_KEY = apiKey;
350
- }
351
-
352
- return {
353
- command: "npm",
354
- args: ["exec", "-y", "--package", "premanmcp@latest", "--", "premanmcp"],
355
- env,
356
- };
357
- }
358
-
359
96
  async function loginCommand() {
360
- const creds = await authenticateTerminal();
97
+ const creds = await authenticateTerminal(cliArgs);
361
98
  process.stdout.write(`PreMan account ready.
362
99
 
363
100
  Email: ${creds.user_email || "unknown"}
@@ -366,61 +103,47 @@ API key: ${creds.api_key}
366
103
  Saved to: ${CREDENTIALS_FILE}
367
104
 
368
105
  You can now run:
369
- npm exec -y premanmcp@latest -- install
106
+ preman connect
370
107
  `);
371
108
  }
372
109
 
110
+ /**
111
+ * Cursor-only installer, kept for the documented `install` flow. `connect` is
112
+ * the same write plus agent choice, pairing, and dispatch setup.
113
+ */
373
114
  async function installCursorMcp() {
374
115
  const serverName = argValue("--name", "preman");
375
116
  const projectInstall = hasFlag("--project");
376
117
 
377
- if (
378
- !hasFlag("--print") &&
379
- !hasFlag("--skip-login") &&
380
- !argValue("--api-key", "") &&
381
- !process.env.PREMAN_API_KEY &&
382
- !readStoredCredentials()
383
- ) {
118
+ if (!hasFlag("--print") && !hasFlag("--skip-login") && !hasKeyAvailable(cliArgs)) {
384
119
  process.stdout.write("First, let's create or connect your PreMan account.\n");
385
- await authenticateTerminal();
120
+ await authenticateTerminal(cliArgs);
386
121
  process.stdout.write("\n");
387
122
  }
388
123
 
389
- const serverConfig = buildServerConfig();
124
+ const serverConfig = buildServerConfig(cliArgs);
390
125
 
391
126
  if (hasFlag("--print")) {
392
127
  process.stdout.write(`${JSON.stringify({ mcpServers: { [serverName]: serverConfig } }, null, 2)}\n`);
393
128
  return;
394
129
  }
395
130
 
396
- const configPath = projectInstall
397
- ? path.join(process.cwd(), ".cursor", "mcp.json")
398
- : path.join(os.homedir(), ".cursor", "mcp.json");
399
-
400
- const config = {
401
- ...readJsonFile(configPath),
402
- };
403
- config.mcpServers = {
404
- ...(config.mcpServers || {}),
405
- [serverName]: serverConfig,
406
- };
131
+ const written = writeCursorConfig({ serverName, serverConfig, projectInstall });
407
132
 
408
- const rendered = `${JSON.stringify(config, null, 2)}\n`;
409
- mkdirSync(path.dirname(configPath), { recursive: true });
410
- writeFileSync(configPath, rendered, { mode: 0o600 });
411
-
412
- const hasInlineKey = Boolean(config.mcpServers[serverName].env.PREMAN_API_KEY);
133
+ const hasInlineKey = Boolean(serverConfig.env.PREMAN_API_KEY);
413
134
  const hasStoredKey = Boolean(readStoredCredentials());
414
- process.stdout.write(`PreMan MCP installed in ${configPath}
135
+ process.stdout.write(`PreMan MCP installed in ${written.path}
415
136
 
416
137
  Server name: ${serverName}
417
- Backend: ${config.mcpServers[serverName].env.PREMAN_BACKEND}
138
+ Backend: ${serverConfig.env.PREMAN_BACKEND}
418
139
 
419
140
  Next steps:
420
141
  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."}
421
142
  2. Restart Cursor or toggle the PreMan MCP server off/on.
422
143
  3. In your API repo, ask your coding agent:
423
144
  "Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
145
+
146
+ Tip: 'preman connect' also supports Claude Code and Codex, and links the agent to your account.
424
147
  `);
425
148
  }
426
149
 
@@ -454,6 +177,18 @@ function startServer() {
454
177
  async function main() {
455
178
  if (command === "login") {
456
179
  await loginCommand();
180
+ } else if (command === "connect") {
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));
457
192
  } else if (command === "install") {
458
193
  await installCursorMcp();
459
194
  } else if (command === "link") {
@@ -462,6 +197,10 @@ async function main() {
462
197
  await toolsCommand(commandArgs);
463
198
  } else if (command === "run") {
464
199
  await runCommand(commandArgs);
200
+ } else if (command === "endpoints") {
201
+ await endpointsCommand(commandArgs);
202
+ } else if (command === "test") {
203
+ await testCommand(commandArgs);
465
204
  } else if (command === "help" || command === "--help" || command === "-h") {
466
205
  printHelp();
467
206
  } else if (command === "start") {