premanmcp 0.7.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/bin/cli.js +30 -15
- package/bin/connect.js +14 -3
- package/bin/integrations.js +59 -21
- package/bin/shared.js +29 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { HOSTED_HELP, linkCommand, runCommand, toolsCommand } from "./hosted.js";
|
|
29
29
|
import {
|
|
30
30
|
CREDENTIALS_FILE,
|
|
31
|
+
cliInvocation,
|
|
31
32
|
DEFAULT_BACKEND,
|
|
32
33
|
DEFAULT_FRONTEND,
|
|
33
34
|
authenticateTerminal,
|
|
@@ -58,24 +59,38 @@ function hasFlag(name) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
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
|
+
|
|
61
84
|
process.stdout.write(`PreMan MCP
|
|
62
85
|
|
|
63
86
|
Usage:
|
|
64
|
-
|
|
65
|
-
preman connect [options] Pick a coding agent and connect it
|
|
66
|
-
preman aws | github | slack Connect one integration on its own
|
|
67
|
-
npm exec -y premanmcp@latest -- login Create/login to PreMan from the terminal
|
|
68
|
-
npm exec -y premanmcp@latest -- install [options] Install PreMan into Cursor MCP config
|
|
69
|
-
npm exec -y premanmcp@latest -- Start the PreMan MCP server
|
|
70
|
-
preman link|tools|run ... Drive a published hosted MCP
|
|
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
|
|
87
|
+
${usageLines}
|
|
73
88
|
${INTEGRATIONS_HELP}${CONNECT_HELP}${ENDPOINTS_HELP}${TEST_HELP}
|
|
74
89
|
Login options:
|
|
75
90
|
--email <email> Pre-fill the email prompt
|
|
76
91
|
--backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
|
|
77
92
|
|
|
78
|
-
Install options (Cursor only — prefer '
|
|
93
|
+
Install options (Cursor only — prefer '${cli} connect'):
|
|
79
94
|
--api-key <key> PreMan API key. If omitted, stored CLI credentials are used
|
|
80
95
|
--backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
|
|
81
96
|
--frontend <url> PreMan frontend URL. Defaults to ${DEFAULT_FRONTEND}
|
|
@@ -87,8 +102,8 @@ Install options (Cursor only — prefer 'preman connect'):
|
|
|
87
102
|
|
|
88
103
|
Examples:
|
|
89
104
|
npm exec -y premanmcp@latest -- connect
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
${cli} connect --agent claude-code
|
|
106
|
+
${cli} login
|
|
92
107
|
npm exec -y premanmcp@latest -- install --project --backend http://127.0.0.1:8000
|
|
93
108
|
${HOSTED_HELP}`);
|
|
94
109
|
}
|
|
@@ -103,7 +118,7 @@ API key: ${creds.api_key}
|
|
|
103
118
|
Saved to: ${CREDENTIALS_FILE}
|
|
104
119
|
|
|
105
120
|
You can now run:
|
|
106
|
-
|
|
121
|
+
${cli} connect
|
|
107
122
|
`);
|
|
108
123
|
}
|
|
109
124
|
|
|
@@ -138,12 +153,12 @@ Server name: ${serverName}
|
|
|
138
153
|
Backend: ${serverConfig.env.PREMAN_BACKEND}
|
|
139
154
|
|
|
140
155
|
Next steps:
|
|
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
|
|
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."}
|
|
142
157
|
2. Restart Cursor or toggle the PreMan MCP server off/on.
|
|
143
158
|
3. In your API repo, ask your coding agent:
|
|
144
159
|
"Use PreMan to convert the endpoints I choose into a hosted MCP server, then give me the Cursor/Claude install snippet."
|
|
145
160
|
|
|
146
|
-
Tip: '
|
|
161
|
+
Tip: '${cli} connect' also supports Claude Code and Codex, and links the agent to your account.
|
|
147
162
|
`);
|
|
148
163
|
}
|
|
149
164
|
|
package/bin/connect.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
backendUrl,
|
|
23
23
|
buildServerConfig,
|
|
24
24
|
callBackendJson,
|
|
25
|
+
cliInvocation,
|
|
25
26
|
frontendUrl,
|
|
26
27
|
hasKeyAvailable,
|
|
27
28
|
makeArgs,
|
|
@@ -401,11 +402,21 @@ async function captureDispatchCredential(args, agent, apiKey) {
|
|
|
401
402
|
|
|
402
403
|
if (!secret) {
|
|
403
404
|
if (!process.stdin.isTTY) return;
|
|
405
|
+
// Framed as the expected step rather than an optional aside. Without it
|
|
406
|
+
// PreMan can only suggest fixes; with it, it can run them. Presenting it as
|
|
407
|
+
// "optional, press Enter to skip" meant almost everyone skipped the thing
|
|
408
|
+
// that makes the product act rather than advise.
|
|
404
409
|
process.stdout.write(
|
|
405
|
-
`\
|
|
410
|
+
`\nLet PreMan start ${agent.label} runs for you — it can then apply fixes and\n` +
|
|
411
|
+
`run checks on a schedule instead of only telling you what to do.\n`
|
|
406
412
|
);
|
|
407
|
-
secret = await promptSecret(
|
|
408
|
-
if (!secret)
|
|
413
|
+
secret = await promptSecret(`Paste your ${agent.dispatch.credential} (Enter to set up later): `);
|
|
414
|
+
if (!secret) {
|
|
415
|
+
process.stdout.write(
|
|
416
|
+
`Skipped. Run '${cliInvocation()} connect --agent ${agent.id.replace("_", "-")}' when you have the token.\n`
|
|
417
|
+
);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
409
420
|
if (agent.dispatch.needsRoutine && !routineId) {
|
|
410
421
|
routineId = await promptText("Routine id or URL: ");
|
|
411
422
|
}
|
package/bin/integrations.js
CHANGED
|
@@ -224,8 +224,13 @@ export async function awsCommand(args) {
|
|
|
224
224
|
export async function githubCommand(args) {
|
|
225
225
|
const token = requireKey(args);
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
const
|
|
227
|
+
// This route answers with a bare array; callBackendJson exposes it as `list`.
|
|
228
|
+
const listRepos = async () => {
|
|
229
|
+
const resp = await callBackendJson(args, "GET", "/integrations/github", { token });
|
|
230
|
+
return resp.list || resp.integrations || resp.repos || [];
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const seen = new Set((await listRepos()).map((r) => r.id));
|
|
229
234
|
|
|
230
235
|
const started = await callBackendJson(args, "POST", "/integrations/github/app/install", {
|
|
231
236
|
token,
|
|
@@ -239,11 +244,17 @@ export async function githubCommand(args) {
|
|
|
239
244
|
process.stdout.write("Pick the repositories PreMan may read.\n");
|
|
240
245
|
|
|
241
246
|
const done = await waitFor("the installation", async () => {
|
|
242
|
-
|
|
243
|
-
|
|
247
|
+
// Installing the App and having repositories appear are two events: the
|
|
248
|
+
// callback records the installation, and a refresh materialises the repos.
|
|
249
|
+
// Polling the repo list alone waits for something that may never arrive on
|
|
250
|
+
// its own.
|
|
251
|
+
await callBackendJson(args, "POST", "/integrations/github/app/refresh", {
|
|
252
|
+
token,
|
|
253
|
+
json: {},
|
|
254
|
+
});
|
|
244
255
|
// Compare against what existed before, so a user who already had repos
|
|
245
256
|
// connected is not told they are done the moment polling starts.
|
|
246
|
-
const fresh =
|
|
257
|
+
const fresh = (await listRepos()).filter((r) => !seen.has(r.id));
|
|
247
258
|
return fresh.length ? fresh : null;
|
|
248
259
|
});
|
|
249
260
|
|
|
@@ -289,10 +300,14 @@ export async function slackCommand(args) {
|
|
|
289
300
|
// The guided run
|
|
290
301
|
// ---------------------------------------------------------------------------
|
|
291
302
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
303
|
+
/** "yes" | "no" | "back" -- back only offered once there is somewhere to go. */
|
|
304
|
+
async function askStep(question, { assumeYes, canGoBack }) {
|
|
305
|
+
if (assumeYes) return "yes";
|
|
306
|
+
const hint = canGoBack ? "[Y/n/b]" : "[Y/n]";
|
|
307
|
+
const answer = (await promptText(`${question} ${hint}: `)).trim().toLowerCase();
|
|
308
|
+
if (canGoBack && (answer === "b" || answer === "back")) return "back";
|
|
309
|
+
if (answer === "" || answer === "y" || answer === "yes") return "yes";
|
|
310
|
+
return "no";
|
|
296
311
|
}
|
|
297
312
|
|
|
298
313
|
/**
|
|
@@ -323,32 +338,54 @@ export async function onboardCommand(commandArgs, { makeArgs, authenticateTermin
|
|
|
323
338
|
{ name: "Slack", question: "Connect Slack?", run: () => slackCommand(args) },
|
|
324
339
|
];
|
|
325
340
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
341
|
+
// Outcome per step rather than three lists, so revisiting a step replaces its
|
|
342
|
+
// result instead of recording it twice.
|
|
343
|
+
const outcome = new Map();
|
|
344
|
+
|
|
345
|
+
// Indexed rather than for..of so "back" can move the cursor. Re-running a
|
|
346
|
+
// step is safe: every one of these is idempotent on the backend -- an AWS
|
|
347
|
+
// grant returns the existing link, and an App install that already happened
|
|
348
|
+
// is detected rather than duplicated.
|
|
349
|
+
let i = 0;
|
|
350
|
+
while (i < steps.length) {
|
|
351
|
+
const step = steps[i];
|
|
331
352
|
process.stdout.write(`\n── ${step.name} ──\n`);
|
|
332
|
-
|
|
333
|
-
|
|
353
|
+
|
|
354
|
+
const choice = await askStep(step.question, { assumeYes, canGoBack: i > 0 });
|
|
355
|
+
if (choice === "back") {
|
|
356
|
+
// Drop the result we are about to redo, or the summary would report the
|
|
357
|
+
// stale outcome of a step the customer chose to revisit.
|
|
358
|
+
outcome.delete(steps[i - 1].name);
|
|
359
|
+
i -= 1;
|
|
334
360
|
continue;
|
|
335
361
|
}
|
|
362
|
+
if (choice === "no") {
|
|
363
|
+
outcome.set(step.name, { state: "skipped" });
|
|
364
|
+
i += 1;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
336
368
|
try {
|
|
337
369
|
await step.run();
|
|
338
|
-
|
|
370
|
+
outcome.set(step.name, { state: "done" });
|
|
339
371
|
} catch (err) {
|
|
340
372
|
// Report and carry on: a failed Slack install must not cost the customer
|
|
341
373
|
// the AWS connection they just finished.
|
|
342
|
-
|
|
374
|
+
outcome.set(step.name, { state: "failed", detail: err.message });
|
|
343
375
|
process.stdout.write(`Could not finish ${step.name}: ${err.message}\n`);
|
|
344
376
|
}
|
|
377
|
+
i += 1;
|
|
345
378
|
}
|
|
346
379
|
|
|
347
380
|
process.stdout.write("\n── done ──\n");
|
|
348
381
|
// One line per step, marked, so the outcome is scannable rather than prose.
|
|
349
|
-
for (const
|
|
350
|
-
|
|
351
|
-
|
|
382
|
+
for (const step of steps) {
|
|
383
|
+
const result = outcome.get(step.name);
|
|
384
|
+
if (!result) continue;
|
|
385
|
+
if (result.state === "done") process.stdout.write(`${MARK.ok()} ${step.name}\n`);
|
|
386
|
+
else if (result.state === "skipped") process.stdout.write(`${MARK.skip()} ${step.name} (skipped)\n`);
|
|
387
|
+
else process.stdout.write(`${MARK.fail()} ${step.name}: ${result.detail}\n`);
|
|
388
|
+
}
|
|
352
389
|
process.stdout.write(`\nOpen ${frontendUrl(args)} to see your logs and endpoints.\n`);
|
|
353
390
|
}
|
|
354
391
|
|
|
@@ -360,6 +397,7 @@ Setup options:
|
|
|
360
397
|
preman slack Add PreMan to a Slack workspace
|
|
361
398
|
|
|
362
399
|
--yes Accept every step without prompting (onboard)
|
|
400
|
+
b at any onboard prompt Go back to the previous step
|
|
363
401
|
--account <id> AWS account id, skips the prompt
|
|
364
402
|
--region <region> AWS region for log groups. Defaults to us-east-1
|
|
365
403
|
--project-id <id> PreMan project to attach the log connector to
|
package/bin/shared.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* over a module-level argv, so a command can pass its own slice.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
10
11
|
import { chmodSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
11
12
|
import os from "node:os";
|
|
12
13
|
import path from "node:path";
|
|
@@ -18,6 +19,28 @@ export const CREDENTIALS_DIR = path.join(os.homedir(), ".preman");
|
|
|
18
19
|
export const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
19
20
|
|
|
20
21
|
/** Wrap a raw argv slice in the positional lookup the CLI has always used. */
|
|
22
|
+
/**
|
|
23
|
+
* How to tell this user to invoke us.
|
|
24
|
+
*
|
|
25
|
+
* The help used to say "preman connect" unconditionally, which is only true
|
|
26
|
+
* after a global install. Anyone reading it straight out of
|
|
27
|
+
* `npm exec premanmcp -- --help` -- the way the docs tell them to run it --
|
|
28
|
+
* copied a command that answers "command not found".
|
|
29
|
+
*/
|
|
30
|
+
let _invocation = null;
|
|
31
|
+
|
|
32
|
+
export function cliInvocation() {
|
|
33
|
+
if (_invocation) return _invocation;
|
|
34
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
35
|
+
try {
|
|
36
|
+
const found = spawnSync(probe, ["preman"], { stdio: "pipe", encoding: "utf8" });
|
|
37
|
+
_invocation = found.status === 0 && found.stdout.trim() ? "preman" : null;
|
|
38
|
+
} catch {
|
|
39
|
+
_invocation = null;
|
|
40
|
+
}
|
|
41
|
+
return (_invocation ||= "npm exec -y premanmcp@latest --");
|
|
42
|
+
}
|
|
43
|
+
|
|
21
44
|
export function makeArgs(commandArgs = []) {
|
|
22
45
|
return {
|
|
23
46
|
raw: commandArgs,
|
|
@@ -173,6 +196,12 @@ export async function callBackendJson(args, method, routePath, { json, token, qu
|
|
|
173
196
|
} catch {
|
|
174
197
|
body = { raw: text };
|
|
175
198
|
}
|
|
199
|
+
// Some routes answer with a bare array. Spreading one into this object turns
|
|
200
|
+
// it into {0:…,1:…} and quietly loses the list, so it gets its own key.
|
|
201
|
+
if (Array.isArray(body)) {
|
|
202
|
+
return { status_code: resp.status, ok: resp.ok, list: body };
|
|
203
|
+
}
|
|
204
|
+
|
|
176
205
|
return {
|
|
177
206
|
status_code: resp.status,
|
|
178
207
|
ok: resp.ok,
|