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 +34 -16
- package/bin/api_tools.js +215 -0
- package/bin/cli.js +54 -13
- package/bin/connect.js +321 -27
- package/bin/integrations.js +405 -0
- package/bin/shared.js +34 -2
- package/dist/server.js +121 -1
- package/package.json +3 -2
package/bin/connect.js
CHANGED
|
@@ -15,12 +15,15 @@ import { chmodSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "n
|
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
|
|
18
|
+
import { callTool as callPremanTool, printTestSummary } from "./api_tools.js";
|
|
18
19
|
import {
|
|
19
20
|
assertOk,
|
|
20
21
|
authenticateTerminal,
|
|
21
22
|
backendUrl,
|
|
22
23
|
buildServerConfig,
|
|
23
24
|
callBackendJson,
|
|
25
|
+
cliInvocation,
|
|
26
|
+
frontendUrl,
|
|
24
27
|
hasKeyAvailable,
|
|
25
28
|
makeArgs,
|
|
26
29
|
promptSecret,
|
|
@@ -49,18 +52,24 @@ const AGENTS = [
|
|
|
49
52
|
label: "Cursor",
|
|
50
53
|
aliases: ["cursor"],
|
|
51
54
|
dispatch: { credential: "Cursor API key", needsRoutine: false },
|
|
55
|
+
snippetHint: "merge into ~/.cursor/mcp.json",
|
|
56
|
+
restartHint: 'Fully quit and reopen Cursor, then Settings → MCP → toggle "preman" off and on.',
|
|
52
57
|
},
|
|
53
58
|
{
|
|
54
59
|
id: "claude_code",
|
|
55
60
|
label: "Claude Code",
|
|
56
61
|
aliases: ["claude", "claude-code", "claude_code", "claudecode"],
|
|
57
62
|
dispatch: { credential: "Claude Code routine token", needsRoutine: true },
|
|
63
|
+
snippetHint: "run:",
|
|
64
|
+
restartHint: 'Start a new Claude Code session and run `claude mcp list` — "preman" should be listed.',
|
|
58
65
|
},
|
|
59
66
|
{
|
|
60
67
|
id: "codex",
|
|
61
68
|
label: "Codex",
|
|
62
69
|
aliases: ["codex", "openai-codex", "openai_codex"],
|
|
63
70
|
dispatch: null, // No public fire API; stays on the copy-paste path.
|
|
71
|
+
snippetHint: "append to ~/.codex/config.toml",
|
|
72
|
+
restartHint: "Restart Codex so it re-reads its config.toml.",
|
|
64
73
|
},
|
|
65
74
|
];
|
|
66
75
|
|
|
@@ -125,6 +134,22 @@ export function writeCursorConfig({ serverName, serverConfig, projectInstall })
|
|
|
125
134
|
return { path: configPath, how: "wrote" };
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
/** The `claude mcp add` invocation, shared by the writer and the printed fallback. */
|
|
138
|
+
export function claudeMcpAddArgs(serverName, serverConfig, projectInstall) {
|
|
139
|
+
const envArgs = Object.entries(serverConfig.env).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
|
|
140
|
+
return [
|
|
141
|
+
"mcp",
|
|
142
|
+
"add",
|
|
143
|
+
serverName,
|
|
144
|
+
"--scope",
|
|
145
|
+
projectInstall ? "project" : "user",
|
|
146
|
+
...envArgs,
|
|
147
|
+
"--",
|
|
148
|
+
serverConfig.command,
|
|
149
|
+
...serverConfig.args,
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
128
153
|
/**
|
|
129
154
|
* Claude Code owns ~/.claude.json, so prefer its own CLI. Fall back to writing
|
|
130
155
|
* the config directly when `claude` is not installed — a user can connect before
|
|
@@ -132,18 +157,7 @@ export function writeCursorConfig({ serverName, serverConfig, projectInstall })
|
|
|
132
157
|
*/
|
|
133
158
|
export function writeClaudeConfig({ serverName, serverConfig, projectInstall }) {
|
|
134
159
|
if (onPath("claude")) {
|
|
135
|
-
const
|
|
136
|
-
const args = [
|
|
137
|
-
"mcp",
|
|
138
|
-
"add",
|
|
139
|
-
serverName,
|
|
140
|
-
"--scope",
|
|
141
|
-
projectInstall ? "project" : "user",
|
|
142
|
-
...envArgs,
|
|
143
|
-
"--",
|
|
144
|
-
serverConfig.command,
|
|
145
|
-
...serverConfig.args,
|
|
146
|
-
];
|
|
160
|
+
const args = claudeMcpAddArgs(serverName, serverConfig, projectInstall);
|
|
147
161
|
try {
|
|
148
162
|
execFileSync("claude", args, { stdio: "pipe" });
|
|
149
163
|
return { path: projectInstall ? ".mcp.json" : "Claude Code user config", how: "registered via claude mcp add" };
|
|
@@ -233,6 +247,104 @@ const WRITERS = {
|
|
|
233
247
|
codex: writeCodexConfig,
|
|
234
248
|
};
|
|
235
249
|
|
|
250
|
+
// ── Copy-paste fallbacks ────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/** Quote only what a shell would otherwise mangle, so the line stays readable. */
|
|
253
|
+
function shellQuote(value) {
|
|
254
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** What a user would paste by hand to get exactly what the writer would have written. */
|
|
258
|
+
export function renderAgentSnippet(agentId, serverName, serverConfig, { projectInstall = false } = {}) {
|
|
259
|
+
if (agentId === "codex") return renderCodexToml(serverName, serverConfig);
|
|
260
|
+
if (agentId === "claude_code") {
|
|
261
|
+
const args = claudeMcpAddArgs(serverName, serverConfig, projectInstall);
|
|
262
|
+
return `claude ${args.map(shellQuote).join(" ")}\n`;
|
|
263
|
+
}
|
|
264
|
+
return `${JSON.stringify({ mcpServers: { [serverName]: serverConfig } }, null, 2)}\n`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Every agent's snippet, for when we could not pick one for the user. */
|
|
268
|
+
export function renderAllAgentSnippets(args, serverName, { projectInstall = false } = {}) {
|
|
269
|
+
// Build config WITHOUT resolved API key to avoid leaking secrets in CI logs.
|
|
270
|
+
// Users must supply --api-key or set PREMAN_API_KEY separately.
|
|
271
|
+
const env = {
|
|
272
|
+
PREMAN_BACKEND: backendUrl(args),
|
|
273
|
+
PREMAN_FRONTEND: frontendUrl(args),
|
|
274
|
+
};
|
|
275
|
+
const serverConfig = { command: "npx", args: ["-y", "premanmcp@latest"], env };
|
|
276
|
+
|
|
277
|
+
return AGENTS.map((agent) => {
|
|
278
|
+
// Adjust hint based on projectInstall, matching verifyWrittenConfig logic
|
|
279
|
+
let hint = agent.snippetHint;
|
|
280
|
+
if (agent.id === "cursor" && projectInstall) {
|
|
281
|
+
hint = "merge into .cursor/mcp.json";
|
|
282
|
+
} else if (agent.id === "claude_code") {
|
|
283
|
+
hint = projectInstall ? "run (with --scope project):" : "run:";
|
|
284
|
+
}
|
|
285
|
+
return `# ${agent.label} — ${hint}\n` + renderAgentSnippet(agent.id, serverName, serverConfig, { projectInstall });
|
|
286
|
+
}).join("\n");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Post-write validation ───────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
/** The lines belonging to our `[mcp_servers.<name>]` block, or null if absent. */
|
|
292
|
+
function codexBlockLines(text, serverName) {
|
|
293
|
+
const lines = text.split("\n");
|
|
294
|
+
const start = lines.findIndex((line) => line.trim() === `[mcp_servers.${serverName}]`);
|
|
295
|
+
if (start === -1) return null;
|
|
296
|
+
const block = [];
|
|
297
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
298
|
+
const line = lines[i];
|
|
299
|
+
if (/^\[/.test(line) && !line.startsWith(`[mcp_servers.${serverName}.`)) break;
|
|
300
|
+
block.push(line);
|
|
301
|
+
}
|
|
302
|
+
return block;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Read back what we just wrote, so a silently-failed write is not reported as a
|
|
307
|
+
* success. Proves the entry is on disk (or that Claude Code knows about it) —
|
|
308
|
+
* whether the agent has actually loaded it is only ever proven by a check-in.
|
|
309
|
+
*
|
|
310
|
+
* Never throws: a verification that cannot run must not fail the connect. An
|
|
311
|
+
* unreadable state reports "unknown" and stays quiet, because a wrong warning
|
|
312
|
+
* costs more trust than a missing one.
|
|
313
|
+
*/
|
|
314
|
+
export function verifyWrittenConfig(agent, { serverName, written }) {
|
|
315
|
+
try {
|
|
316
|
+
if (agent.id === "codex") {
|
|
317
|
+
const text = written.path && existsSync(written.path) ? readFileSync(written.path, "utf8") : "";
|
|
318
|
+
const block = codexBlockLines(text, serverName);
|
|
319
|
+
if (!block) {
|
|
320
|
+
return { status: "mismatch", detail: `no [mcp_servers.${serverName}] block in ${written.path}` };
|
|
321
|
+
}
|
|
322
|
+
if (!block.some((line) => line.trim() === 'command = "npx"')) {
|
|
323
|
+
return { status: "mismatch", detail: `${written.path} does not launch npx` };
|
|
324
|
+
}
|
|
325
|
+
return { status: "verified" };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Claude Code registered the server itself — ask its CLI what it ended up with.
|
|
329
|
+
if (agent.id === "claude_code" && written.how !== "wrote") {
|
|
330
|
+
const probe = spawnSync("claude", ["mcp", "get", serverName], { stdio: "ignore" });
|
|
331
|
+
if (probe.error) return { status: "unknown", detail: "could not run claude" };
|
|
332
|
+
return probe.status === 0
|
|
333
|
+
? { status: "verified" }
|
|
334
|
+
: { status: "mismatch", detail: `claude mcp get ${serverName} did not find the server` };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const entry = readJsonFile(written.path).mcpServers?.[serverName];
|
|
338
|
+
if (!entry) return { status: "mismatch", detail: `${serverName} is missing from ${written.path}` };
|
|
339
|
+
if (entry.command !== "npx") {
|
|
340
|
+
return { status: "mismatch", detail: `${written.path} does not launch npx` };
|
|
341
|
+
}
|
|
342
|
+
return { status: "verified" };
|
|
343
|
+
} catch (error) {
|
|
344
|
+
return { status: "unknown", detail: error.message };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
236
348
|
// ── Pairing ─────────────────────────────────────────────────────────────
|
|
237
349
|
|
|
238
350
|
async function startPairing(args, agent, apiKey) {
|
|
@@ -250,7 +362,14 @@ async function startPairing(args, agent, apiKey) {
|
|
|
250
362
|
return String(result.pair_code || "");
|
|
251
363
|
}
|
|
252
364
|
|
|
253
|
-
async function waitForConnection(
|
|
365
|
+
export async function waitForConnection(
|
|
366
|
+
args,
|
|
367
|
+
apiKey,
|
|
368
|
+
{
|
|
369
|
+
intervalMs = Number(process.env.PREMAN_CONNECT_POLL_MS) || 3000,
|
|
370
|
+
timeoutMs = Number(process.env.PREMAN_CONNECT_WAIT_MS) || 300000,
|
|
371
|
+
} = {}
|
|
372
|
+
) {
|
|
254
373
|
const deadline = Date.now() + timeoutMs;
|
|
255
374
|
let interrupted = false;
|
|
256
375
|
const onInterrupt = () => {
|
|
@@ -283,11 +402,21 @@ async function captureDispatchCredential(args, agent, apiKey) {
|
|
|
283
402
|
|
|
284
403
|
if (!secret) {
|
|
285
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.
|
|
286
409
|
process.stdout.write(
|
|
287
|
-
`\
|
|
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`
|
|
288
412
|
);
|
|
289
|
-
secret = await promptSecret(
|
|
290
|
-
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
|
+
}
|
|
291
420
|
if (agent.dispatch.needsRoutine && !routineId) {
|
|
292
421
|
routineId = await promptText("Routine id or URL: ");
|
|
293
422
|
}
|
|
@@ -323,6 +452,135 @@ export function extractRoutineId(value) {
|
|
|
323
452
|
return match ? match[0] : raw;
|
|
324
453
|
}
|
|
325
454
|
|
|
455
|
+
// ── Guided first run ────────────────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
const MANUAL_STEPS =
|
|
458
|
+
" preman endpoints discover # brief for your agent → endpoints.json\n" +
|
|
459
|
+
" preman endpoints setup --file endpoints.json # register runnable requests\n" +
|
|
460
|
+
" preman test <request-id> # generate + run your first scenarios\n";
|
|
461
|
+
|
|
462
|
+
function nextStepsBlock(agent) {
|
|
463
|
+
return (
|
|
464
|
+
"\nNext steps:\n" +
|
|
465
|
+
` 1. Restart ${agent.label}, then ask it: "run preman_status" to finish linking.\n` +
|
|
466
|
+
" 2. preman endpoints discover # brief for your agent → endpoints.json\n" +
|
|
467
|
+
" 3. preman endpoints setup --file endpoints.json # register runnable requests\n" +
|
|
468
|
+
" 4. preman test <request-id> # generate + run your first scenarios\n"
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function confirm(question) {
|
|
473
|
+
const answer = (await promptText(`${question} [Y/n]: `)).toLowerCase();
|
|
474
|
+
return answer === "" || answer.startsWith("y");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Carry a freshly linked agent to its first passing test.
|
|
479
|
+
*
|
|
480
|
+
* Discovery itself belongs to the coding agent — the backend hands back a brief
|
|
481
|
+
* for it to execute — so this either runs a test against what the account
|
|
482
|
+
* already has, or prints that brief and the two commands that follow it.
|
|
483
|
+
*
|
|
484
|
+
* Never throws: onboarding help must not turn a successful connect into a failure.
|
|
485
|
+
*/
|
|
486
|
+
async function guidedFirstRun(args, agent) {
|
|
487
|
+
try {
|
|
488
|
+
const inventory = await callPremanTool(args, "get_endpoints", {
|
|
489
|
+
include_workbench: true,
|
|
490
|
+
limit: 50,
|
|
491
|
+
});
|
|
492
|
+
const runnable = (inventory.workbench_requests || [])[0];
|
|
493
|
+
const registered = (inventory.endpoints || []).length;
|
|
494
|
+
|
|
495
|
+
if (runnable) {
|
|
496
|
+
const label = `${runnable.method || "GET"} ${runnable.url || ""}`.trim();
|
|
497
|
+
if (!(await confirm(`\nRun a first test against ${label}?`))) {
|
|
498
|
+
process.stdout.write(`Whenever you are ready: preman test ${runnable.id}\n`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
process.stdout.write("Generating scenarios…\n");
|
|
502
|
+
const result = await callPremanTool(args, "generate_endpoint_tests", {
|
|
503
|
+
target: runnable.id,
|
|
504
|
+
run: true,
|
|
505
|
+
allow_writes: false,
|
|
506
|
+
max_cases: 10,
|
|
507
|
+
});
|
|
508
|
+
printTestSummary(result);
|
|
509
|
+
process.stdout.write(`\nAdd your own: preman test ${runnable.id} --scenario "..."\n`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (registered) {
|
|
514
|
+
process.stdout.write(
|
|
515
|
+
`\nYou have ${registered} registered endpoint(s), but none are runnable yet:\n` +
|
|
516
|
+
" preman endpoints setup --ids <id1,id2> # make them runnable\n" +
|
|
517
|
+
" preman test <request-id> # generate + run your first scenarios\n"
|
|
518
|
+
);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (!(await confirm("\nNo endpoints in PreMan yet. Print the discovery brief for your agent?"))) {
|
|
523
|
+
process.stdout.write(`\nWhen you are ready:\n${MANUAL_STEPS}`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const brief = await callPremanTool(args, "discover_endpoints_from_codebase", { base_path: "." });
|
|
528
|
+
for (const line of brief.instructions || []) process.stdout.write(`${line}\n`);
|
|
529
|
+
process.stdout.write(
|
|
530
|
+
`\nHand this brief to ${agent.label}, then run:\n` +
|
|
531
|
+
" preman endpoints setup --file endpoints.json\n" +
|
|
532
|
+
" preman test <request-id>\n"
|
|
533
|
+
);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
process.stdout.write(
|
|
536
|
+
`Note: ${error.message}. Run \`preman endpoints list\` when you are ready.\n`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ── Preflight ───────────────────────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
const MIN_NODE_MAJOR = 18;
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Verify the machine can actually run the config we are about to write.
|
|
547
|
+
* Hard-fails only on a Node that cannot run the server; everything else is a
|
|
548
|
+
* warning — connect must keep working offline and behind odd shells.
|
|
549
|
+
*/
|
|
550
|
+
export async function preflight(args) {
|
|
551
|
+
const problems = [];
|
|
552
|
+
const notes = [];
|
|
553
|
+
|
|
554
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
555
|
+
if (Number.isFinite(major) && major < MIN_NODE_MAJOR) {
|
|
556
|
+
problems.push(
|
|
557
|
+
`Node ${process.versions.node} is too old — the PreMan MCP server needs Node ${MIN_NODE_MAJOR}+.`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (!onPath("npx")) {
|
|
562
|
+
notes.push(
|
|
563
|
+
"npx was not found on PATH; the written config launches PreMan via npx, so make sure your agent's environment has it."
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const resp = await fetch(new URL("health", `${backendUrl(args)}/`), {
|
|
569
|
+
signal: AbortSignal.timeout(4000),
|
|
570
|
+
});
|
|
571
|
+
if (!resp.ok) {
|
|
572
|
+
notes.push(`PreMan backend ${backendUrl(args)} answered ${resp.status}; connect will continue but calls may fail.`);
|
|
573
|
+
}
|
|
574
|
+
} catch {
|
|
575
|
+
notes.push(`Could not reach ${backendUrl(args)}; connect will continue but pairing and logins need it.`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for (const note of notes) process.stdout.write(`Note: ${note}\n`);
|
|
579
|
+
if (problems.length) {
|
|
580
|
+
throw new ConnectError(problems.join("\n"), EXIT_USAGE);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
326
584
|
// ── Command ─────────────────────────────────────────────────────────────
|
|
327
585
|
|
|
328
586
|
export const CONNECT_HELP = `
|
|
@@ -340,6 +598,7 @@ Connect options:
|
|
|
340
598
|
--skip-login Write config without interactive terminal auth
|
|
341
599
|
--no-pair Do not mint a pair code
|
|
342
600
|
--no-wait Do not wait for the agent to check in
|
|
601
|
+
--no-guide Skip the guided first run after connecting
|
|
343
602
|
--print Print the config instead of writing it
|
|
344
603
|
`;
|
|
345
604
|
|
|
@@ -360,15 +619,28 @@ export async function connectCommand(commandArgs) {
|
|
|
360
619
|
|
|
361
620
|
if (!agent) {
|
|
362
621
|
if (!interactive) {
|
|
622
|
+
// Nothing to prompt on, so leave behind everything a CI log needs to
|
|
623
|
+
// finish the setup by hand rather than just the reason it stopped.
|
|
624
|
+
process.stdout.write(
|
|
625
|
+
`preman connect needs a terminal to pick an agent. Copy-paste setup instead:\n\n${renderAllAgentSnippets(args, serverName, { projectInstall })}\n` +
|
|
626
|
+
'Then restart your agent and ask it: "run preman_status".\n' +
|
|
627
|
+
"Or rerun: preman connect --agent <cursor|claude-code|codex> --api-key pm_live_…\n"
|
|
628
|
+
);
|
|
363
629
|
throw new ConnectError(
|
|
364
630
|
"preman connect needs a terminal. In CI pass --agent <cursor|claude-code|codex> " +
|
|
365
|
-
"and --api-key pm_live_… (or --print).",
|
|
631
|
+
"and --api-key pm_live_… (or --print), or use one of the snippets above.",
|
|
366
632
|
EXIT_USAGE
|
|
367
633
|
);
|
|
368
634
|
}
|
|
369
635
|
agent = await promptAgentChoice(detectAgents());
|
|
370
636
|
}
|
|
371
637
|
|
|
638
|
+
if (!printOnly) {
|
|
639
|
+
// Verify the machine can run what we are about to write — before any
|
|
640
|
+
// config edits or logins, so failures leave nothing half-done.
|
|
641
|
+
await preflight(args);
|
|
642
|
+
}
|
|
643
|
+
|
|
372
644
|
if (printOnly) {
|
|
373
645
|
const serverConfig = buildServerConfig(args);
|
|
374
646
|
if (agent.id === "codex") {
|
|
@@ -407,14 +679,28 @@ export async function connectCommand(commandArgs) {
|
|
|
407
679
|
`Backend: ${serverConfig.env.PREMAN_BACKEND}\n`
|
|
408
680
|
);
|
|
409
681
|
|
|
682
|
+
const verification = verifyWrittenConfig(agent, { serverName, written });
|
|
683
|
+
if (verification.status === "mismatch") {
|
|
684
|
+
// Generate hint that matches the actual install location
|
|
685
|
+
let hint = agent.snippetHint;
|
|
686
|
+
if (agent.id === "cursor" && projectInstall) {
|
|
687
|
+
hint = "merge into .cursor/mcp.json";
|
|
688
|
+
} else if (agent.id === "claude_code") {
|
|
689
|
+
hint = projectInstall ? "run (with --scope project):" : "run:";
|
|
690
|
+
}
|
|
691
|
+
process.stdout.write(
|
|
692
|
+
`\nWarning: could not confirm the ${agent.label} config (${verification.detail}).\n` +
|
|
693
|
+
`Apply it by hand — ${hint}\n\n` +
|
|
694
|
+
renderAgentSnippet(agent.id, serverName, serverConfig, { projectInstall })
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
410
698
|
// Not gated on TTY: --dispatch-credential is the non-interactive path, and the
|
|
411
699
|
// prompt inside only runs when there is a terminal to prompt on.
|
|
412
700
|
await captureDispatchCredential(args, agent, apiKey);
|
|
413
701
|
|
|
414
702
|
if (!pairCode || args.has("--no-wait") || !interactive) {
|
|
415
|
-
process.stdout.write(
|
|
416
|
-
`\nRestart ${agent.label}, then ask it: "run preman_status" to finish linking.\n`
|
|
417
|
-
);
|
|
703
|
+
process.stdout.write(nextStepsBlock(agent));
|
|
418
704
|
return;
|
|
419
705
|
}
|
|
420
706
|
|
|
@@ -422,11 +708,19 @@ export async function connectCommand(commandArgs) {
|
|
|
422
708
|
`\nRestart ${agent.label} and ask it: "run preman_status"\n` +
|
|
423
709
|
"Waiting for your agent to check in… (Ctrl+C to stop waiting)\n"
|
|
424
710
|
);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
711
|
+
|
|
712
|
+
if (!(await waitForConnection(args, apiKey))) {
|
|
713
|
+
process.stdout.write(
|
|
714
|
+
"No check-in yet. Troubleshooting:\n" +
|
|
715
|
+
` - ${agent.restartHint}\n` +
|
|
716
|
+
` - Config written to: ${written.path}\n` +
|
|
717
|
+
` - Then ask ${agent.label} to "run preman_status" — it links on its first PreMan call.\n`
|
|
718
|
+
);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
process.stdout.write(`Connected as ${agent.label}.\n`);
|
|
723
|
+
if (!args.has("--no-guide")) {
|
|
724
|
+
await guidedFirstRun(args, agent);
|
|
725
|
+
}
|
|
432
726
|
}
|