premanmcp 1.1.10 → 1.1.12
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/account.js +48 -0
- package/bin/cli.js +73 -14
- package/bin/connect/configs.js +20 -6
- package/bin/desktop.js +557 -79
- package/bin/eval.js +325 -50
- package/bin/eval_generation_contract.py +72 -0
- package/bin/eval_generation_debug.py +279 -0
- package/bin/eval_generation_status.py +284 -0
- package/bin/eval_harness.py +166 -3
- package/bin/eval_start.js +203 -0
- package/bin/hook.js +21 -1
- package/bin/integrations.js +20 -4
- package/bin/probe.js +323 -0
- package/bin/runner.js +23 -0
- package/bin/shared.js +331 -14
- package/package.json +2 -2
package/bin/account.js
CHANGED
|
@@ -9,18 +9,23 @@
|
|
|
9
9
|
|
|
10
10
|
import { existsSync, rmSync } from "node:fs";
|
|
11
11
|
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
12
14
|
|
|
13
15
|
import { detectCandidates } from "./detect.js";
|
|
14
16
|
import { DESKTOP_SESSION_FILE } from "./desktop.js";
|
|
15
17
|
import { hookStatus } from "./hook.js";
|
|
16
18
|
import {
|
|
17
19
|
CREDENTIALS_FILE,
|
|
20
|
+
MIN_NODE_MAJOR,
|
|
18
21
|
backendUrl,
|
|
19
22
|
callBackendJson,
|
|
20
23
|
cliInvocation,
|
|
21
24
|
frontendUrl,
|
|
22
25
|
makeArgs,
|
|
26
|
+
nodeTooOld,
|
|
23
27
|
openUrl,
|
|
28
|
+
pathPremanOwner,
|
|
24
29
|
readStoredCredentials,
|
|
25
30
|
resolveApiKey,
|
|
26
31
|
saveStoredCredentials,
|
|
@@ -110,6 +115,49 @@ export async function doctorCommand(commandArgs = []) {
|
|
|
110
115
|
const results = [];
|
|
111
116
|
let failures = 0;
|
|
112
117
|
|
|
118
|
+
// The install itself, first, because everything below assumes it works.
|
|
119
|
+
// Doctor checked the account, the backend, the repo and the hook -- five
|
|
120
|
+
// things on the far side of a CLI that can run -- and nothing about the CLI.
|
|
121
|
+
// So the two failures that make every other line meaningless, a Node too old
|
|
122
|
+
// for fetch and a `preman` on PATH that belongs to something else, were the
|
|
123
|
+
// two it could not report.
|
|
124
|
+
const oldNode = nodeTooOld();
|
|
125
|
+
results.push(
|
|
126
|
+
line(
|
|
127
|
+
"node",
|
|
128
|
+
!oldNode,
|
|
129
|
+
oldNode ? `${process.versions.node} — needs ${MIN_NODE_MAJOR}+` : process.versions.node
|
|
130
|
+
)
|
|
131
|
+
);
|
|
132
|
+
if (oldNode) failures += 1;
|
|
133
|
+
|
|
134
|
+
// `preman` resolving to something other than this package is why a hook
|
|
135
|
+
// installed under one Node reports "Unknown command: verify" after an nvm
|
|
136
|
+
// switch, and why the printed instructions fall back to re-resolving the
|
|
137
|
+
// package from the network on every push.
|
|
138
|
+
const owner = pathPremanOwner();
|
|
139
|
+
results.push(
|
|
140
|
+
line(
|
|
141
|
+
"preman on PATH",
|
|
142
|
+
owner === "premanmcp",
|
|
143
|
+
owner === "premanmcp"
|
|
144
|
+
? "premanmcp"
|
|
145
|
+
: owner
|
|
146
|
+
? `owned by ${owner}, not premanmcp — commands will use \`${cliInvocation()}\``
|
|
147
|
+
: `not on PATH — commands will use \`${cliInvocation()}\``
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// The MCP server is compiled, and `npm exec` fetches the published package
|
|
152
|
+
// rather than this checkout, so a missing dist is invisible until a host
|
|
153
|
+
// launches the server and gets nothing.
|
|
154
|
+
const serverPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "dist", "server.js");
|
|
155
|
+
const built = existsSync(serverPath);
|
|
156
|
+
results.push(
|
|
157
|
+
line("mcp server", built, built ? "dist/server.js" : "dist/server.js missing — run `npm run build`")
|
|
158
|
+
);
|
|
159
|
+
if (!built) failures += 1;
|
|
160
|
+
|
|
113
161
|
const stored = readStoredCredentials();
|
|
114
162
|
const token = resolveApiKey(args);
|
|
115
163
|
results.push(
|
package/bin/cli.js
CHANGED
|
@@ -40,6 +40,7 @@ import { HOOK_HELP, hookCommand, installHook, scheduleHookRepair } from "./hook.
|
|
|
40
40
|
import { EVAL_HELP } from "./eval.js";
|
|
41
41
|
import { TARGET_HELP, targetCommand } from "./target.js";
|
|
42
42
|
import { RUNNER_HELP, runnerCommand } from "./runner.js";
|
|
43
|
+
import { PROBE_HELP, probeCommand } from "./probe.js";
|
|
43
44
|
import { VERIFY_HELP, verifyCommand } from "./verify.js";
|
|
44
45
|
import {
|
|
45
46
|
DESKTOP_HELP,
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
buildServerConfig,
|
|
60
61
|
hasKeyAvailable,
|
|
61
62
|
makeArgs,
|
|
63
|
+
nodeTooOld,
|
|
62
64
|
readStoredCredentials,
|
|
63
65
|
} from "./shared.js";
|
|
64
66
|
|
|
@@ -73,6 +75,16 @@ const ROOT = path.join(__dirname, "..");
|
|
|
73
75
|
// argument parsing is still attributable.
|
|
74
76
|
process.title = "preman";
|
|
75
77
|
|
|
78
|
+
// Before anything else that could need it. `start` is exempt: an MCP host
|
|
79
|
+
// launched it, the host is the only thing reading this stream, and a line of
|
|
80
|
+
// prose on stdout is a protocol violation -- the preflight inside `connect`
|
|
81
|
+
// already refuses that case in a way the host can render.
|
|
82
|
+
const tooOld = nodeTooOld();
|
|
83
|
+
if (tooOld && !(process.argv[2] === "start" || !process.stdin.isTTY)) {
|
|
84
|
+
process.stderr.write(tooOld);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
const args = process.argv.slice(2);
|
|
77
89
|
// A bare `preman` in a terminal opens the agent session: that is what the
|
|
78
90
|
// product is in a terminal, and it signs the caller in on the way if they are
|
|
@@ -85,9 +97,25 @@ const args = process.argv.slice(2);
|
|
|
85
97
|
// explicit way to ask for the server from a terminal.
|
|
86
98
|
const bare = args.length === 0;
|
|
87
99
|
const leadingFlag = Boolean(args[0]) && args[0].startsWith("-");
|
|
88
|
-
|
|
100
|
+
// Asking for help anywhere in the line is asking for help. Only the leading
|
|
101
|
+
// position was recognised, so `preman install --help` was dispatched to
|
|
102
|
+
// `install` with `--help` as an argument -- and a command whose job is to
|
|
103
|
+
// explain itself instead went and did the thing it was being asked about.
|
|
104
|
+
// Only the first two positions, because `run` and `dispatch` forward arguments
|
|
105
|
+
// to a tool and one of those may legitimately be asking that tool for help.
|
|
106
|
+
const wantsHelp = args.slice(0, 2).some((arg) => arg === "--help" || arg === "-h");
|
|
107
|
+
// Leading position only, unlike help: `update --version 1.1.9` is how somebody
|
|
108
|
+
// pins a version to install, so reading `--version` anywhere would answer that
|
|
109
|
+
// with a number and never run the update. Asking a CLI its version is also the
|
|
110
|
+
// one question it must not answer by starting something -- and it did: the flag
|
|
111
|
+
// fell through to the session, so `preman --version` opened a chat, and with a
|
|
112
|
+
// pipe on stdin it started the MCP server and said nothing at all.
|
|
113
|
+
const wantsVersion = args[0] === "--version" || args[0] === "-v";
|
|
114
|
+
const command = wantsHelp
|
|
89
115
|
? "help"
|
|
90
|
-
:
|
|
116
|
+
: wantsVersion
|
|
117
|
+
? "version"
|
|
118
|
+
: bare || leadingFlag
|
|
91
119
|
? (process.stdin.isTTY ? "agent" : "start")
|
|
92
120
|
: args[0];
|
|
93
121
|
// A session or the server started this way owns every argument; a named
|
|
@@ -115,9 +143,12 @@ function printHelp() {
|
|
|
115
143
|
["chat [message] [--continue]", "The same session, named explicitly"],
|
|
116
144
|
["status", "Healthy / failing / recently fixed endpoints"],
|
|
117
145
|
["verify [options]", "Test endpoints against your local app"],
|
|
146
|
+
["probe [options]", "Fire a population of simulated users at your local app"],
|
|
118
147
|
["hook install|uninstall|status", "Manage the git pre-push hook"],
|
|
119
148
|
["runner start|status|stop", "Run PreMan's queued agent work on this machine"],
|
|
120
|
-
["
|
|
149
|
+
["runner work", "Take one queued eval and run it here, then exit"],
|
|
150
|
+
["eval", "Measure your agent: priced and confirmed before it starts"],
|
|
151
|
+
["eval target|doctor", "Find the agent to measure; check this can run"],
|
|
121
152
|
["doctor", "Diagnose credentials, backend, target, integrations"],
|
|
122
153
|
["install-desktop", "Download and install the PreMan desktop app"],
|
|
123
154
|
["predict [options]", "Local dev: bring the stack up and measure the forecaster"],
|
|
@@ -134,7 +165,7 @@ function printHelp() {
|
|
|
134
165
|
["start", "Start the PreMan MCP server (what an MCP host runs)"],
|
|
135
166
|
["link|tools|run ...", "Drive a published hosted MCP"],
|
|
136
167
|
["endpoints list|discover|setup ...", "Discover, list, and set up API endpoints"],
|
|
137
|
-
["test --agent [--only <id>]", "
|
|
168
|
+
["test --agent [--only <id>]", "Older spelling of `eval`; still works"],
|
|
138
169
|
["test <id> [--scenario ...] [--stress]", "Generate + run tests for an endpoint"],
|
|
139
170
|
["tests generate|review|setup|enrich", "Collections: generate, review, fixtures, enrich"],
|
|
140
171
|
];
|
|
@@ -147,7 +178,7 @@ function printHelp() {
|
|
|
147
178
|
|
|
148
179
|
Usage:
|
|
149
180
|
${usageLines}
|
|
150
|
-
${AGENT_HELP}${INTEGRATIONS_HELP}${CONNECT_HELP}${DISPATCH_HELP}${STATUS_HELP}${VERIFY_HELP}${HOOK_HELP}${RUNNER_HELP}${EVAL_HELP}${TARGET_HELP}${ACCOUNT_HELP}${DESKTOP_HELP}${PREDICT_HELP}${ENDPOINTS_HELP}${TEST_HELP}${TESTS_HELP}
|
|
181
|
+
${AGENT_HELP}${INTEGRATIONS_HELP}${CONNECT_HELP}${DISPATCH_HELP}${STATUS_HELP}${VERIFY_HELP}${PROBE_HELP}${HOOK_HELP}${RUNNER_HELP}${EVAL_HELP}${TARGET_HELP}${ACCOUNT_HELP}${DESKTOP_HELP}${PREDICT_HELP}${ENDPOINTS_HELP}${TEST_HELP}${TESTS_HELP}
|
|
151
182
|
Login options:
|
|
152
183
|
--email <email> Pre-fill the email prompt
|
|
153
184
|
--backend <url> PreMan backend URL. Defaults to ${DEFAULT_BACKEND}
|
|
@@ -233,6 +264,7 @@ async function updateCommand(commandArgs = []) {
|
|
|
233
264
|
|
|
234
265
|
process.stdout.write(`Updating ${PACKAGE_NAME} (have ${before})…\n`);
|
|
235
266
|
const done = spawnSync("npm", ["install", "-g", `${PACKAGE_NAME}@${wanted}`], {
|
|
267
|
+
shell: process.platform === "win32",
|
|
236
268
|
stdio: "inherit",
|
|
237
269
|
});
|
|
238
270
|
if (done.error || done.status !== 0) {
|
|
@@ -247,7 +279,10 @@ async function updateCommand(commandArgs = []) {
|
|
|
247
279
|
}
|
|
248
280
|
// Read from the freshly installed copy rather than this process, which is
|
|
249
281
|
// still running the version we just replaced.
|
|
250
|
-
const now = spawnSync("npm", ["view", PACKAGE_NAME, "version"], {
|
|
282
|
+
const now = spawnSync("npm", ["view", PACKAGE_NAME, "version"], {
|
|
283
|
+
encoding: "utf8",
|
|
284
|
+
shell: process.platform === "win32",
|
|
285
|
+
});
|
|
251
286
|
const after = String(now.stdout || "").trim();
|
|
252
287
|
process.stdout.write(
|
|
253
288
|
after && after !== before
|
|
@@ -330,7 +365,9 @@ async function main() {
|
|
|
330
365
|
// would only race with the one the user is watching.
|
|
331
366
|
if (!["hook", "verify", "connect"].includes(command)) scheduleHookRepair();
|
|
332
367
|
|
|
333
|
-
if (command === "
|
|
368
|
+
if (command === "version") {
|
|
369
|
+
process.stdout.write(`${packageVersion() || "unknown"}\n`);
|
|
370
|
+
} else if (command === "agent" || command === "chat") {
|
|
334
371
|
await agentCommand(commandArgs);
|
|
335
372
|
} else if (command === "target") {
|
|
336
373
|
await targetCommand(commandArgs, { makeArgs });
|
|
@@ -340,6 +377,8 @@ async function main() {
|
|
|
340
377
|
await statusCommand(commandArgs);
|
|
341
378
|
} else if (command === "verify") {
|
|
342
379
|
await verifyCommand(commandArgs);
|
|
380
|
+
} else if (command === "probe") {
|
|
381
|
+
await probeCommand(commandArgs);
|
|
343
382
|
} else if (command === "hook") {
|
|
344
383
|
await hookCommand(commandArgs);
|
|
345
384
|
} else if (command === "runner") {
|
|
@@ -353,7 +392,11 @@ async function main() {
|
|
|
353
392
|
// Passing them in keeps the dependency one-directional.
|
|
354
393
|
const { evalCommand } = await import("./eval.js");
|
|
355
394
|
const runner = await import("./runner.js");
|
|
356
|
-
|
|
395
|
+
// `preman eval && deploy` has to be able to tell "the agent behaved" from
|
|
396
|
+
// "we never found out", so the code the command worked out is the code the
|
|
397
|
+
// shell sees. Left alone when there is none: a path that did not measure
|
|
398
|
+
// anything has no verdict to report, and forcing one would invent it.
|
|
399
|
+
const outcome = await evalCommand(commandArgs, {
|
|
357
400
|
makeArgs,
|
|
358
401
|
registerRunner: runner.registerRunner,
|
|
359
402
|
readRunnerState: runner.readRunnerState,
|
|
@@ -363,6 +406,7 @@ async function main() {
|
|
|
363
406
|
pairingIsLive: runner.pairingIsLive,
|
|
364
407
|
resolveAgent: resolveAgentForPairing,
|
|
365
408
|
});
|
|
409
|
+
if (Number.isInteger(outcome?.exitCode)) process.exitCode = outcome.exitCode;
|
|
366
410
|
} else if (command === "logout") {
|
|
367
411
|
await logoutCommand();
|
|
368
412
|
} else if (command === "doctor") {
|
|
@@ -399,12 +443,18 @@ async function main() {
|
|
|
399
443
|
} else if (command === "update" || command === "upgrade") {
|
|
400
444
|
await updateCommand(commandArgs);
|
|
401
445
|
} else if (command === "install") {
|
|
402
|
-
// `install` meant "write Cursor's MCP config", and
|
|
403
|
-
// it
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
|
|
446
|
+
// `install` meant "write Cursor's MCP config", and bare `install` never had
|
|
447
|
+
// a use, so it became what people actually type it expecting: set me up.
|
|
448
|
+
//
|
|
449
|
+
// That fork used to be "were there any arguments at all", which routed on
|
|
450
|
+
// the presence of a flag rather than on what the flag asked for. So
|
|
451
|
+
// `install --yes` -- the documented way to run an unattended setup -- wrote
|
|
452
|
+
// a Cursor config instead, and `install --help` did too, which is a command
|
|
453
|
+
// that cannot create an account answering by creating one. Cursor intent is
|
|
454
|
+
// now named. `--backend` is deliberately absent: every command takes it, so
|
|
455
|
+
// it says which deployment, never which program.
|
|
456
|
+
const CURSOR_INTENT = ["--cursor", "--name", "--project", "--print", "--skip-login", "--api-key"];
|
|
457
|
+
if (CURSOR_INTENT.some((flag) => commandArgs.includes(flag))) {
|
|
408
458
|
await installCursorMcp();
|
|
409
459
|
} else {
|
|
410
460
|
await onboardCommand(commandArgs, {
|
|
@@ -436,6 +486,15 @@ async function main() {
|
|
|
436
486
|
// differs. Routed here rather than inside testCommand because the two share
|
|
437
487
|
// no arguments beyond the word `test`, and matched in first position only so
|
|
438
488
|
// that an endpoint id is never overtaken by a flag further along the line.
|
|
489
|
+
// Signposted rather than removed. This spelling is in circulation, in
|
|
490
|
+
// scripts and in people's shell history, and breaking it to make a point
|
|
491
|
+
// about naming would cost more than the confusion it fixes.
|
|
492
|
+
const invocation = cliInvocation();
|
|
493
|
+
process.stderr.write(
|
|
494
|
+
`\`${invocation} test --agent\` is now \`${invocation} eval\`, which ` +
|
|
495
|
+
`checks the agent can be found and prices the run before starting it. ` +
|
|
496
|
+
`Running it for you.\n\n`
|
|
497
|
+
);
|
|
439
498
|
const { agentTestCommand } = await import("./eval.js");
|
|
440
499
|
const runner = await import("./runner.js");
|
|
441
500
|
await agentTestCommand(commandArgs, {
|
package/bin/connect/configs.js
CHANGED
|
@@ -11,7 +11,7 @@ import os from "node:os";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
13
13
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
-
import { LAUNCHER_ARGS, LAUNCHER_COMMAND, backendUrl, frontendUrl, onPath, readJsonFile, writeJsonFile } from "../shared.js";
|
|
14
|
+
import { LAUNCHER_ARGS, LAUNCHER_COMMAND, backendUrl, frontendUrl, launcherFor, onPath, readJsonFile, writeJsonFile } from "../shared.js";
|
|
15
15
|
import { AGENTS } from "./agents.js";
|
|
16
16
|
|
|
17
17
|
// ── Config writers ──────────────────────────────────────────────────────
|
|
@@ -42,6 +42,10 @@ export function writeCursorConfig({ serverName, serverConfig, projectInstall })
|
|
|
42
42
|
execFileSync("cursor-agent", cursorMcpEnableArgs(serverName), {
|
|
43
43
|
stdio: "pipe",
|
|
44
44
|
timeout: 15000,
|
|
45
|
+
// Both agent CLIs install as `.cmd` shims on Windows, and execFile
|
|
46
|
+
// cannot run a batch file: it fails with EINVAL, which reads as the
|
|
47
|
+
// agent being broken rather than as us spawning it wrongly.
|
|
48
|
+
shell: process.platform === "win32",
|
|
45
49
|
});
|
|
46
50
|
return { path: configPath, how: "wrote and enabled" };
|
|
47
51
|
} catch (error) {
|
|
@@ -79,7 +83,7 @@ export function writeClaudeConfig({ serverName, serverConfig, projectInstall })
|
|
|
79
83
|
if (onPath("claude")) {
|
|
80
84
|
const args = claudeMcpAddArgs(serverName, serverConfig, projectInstall);
|
|
81
85
|
try {
|
|
82
|
-
execFileSync("claude", args, { stdio: "pipe" });
|
|
86
|
+
execFileSync("claude", args, { stdio: "pipe", shell: process.platform === "win32" });
|
|
83
87
|
return { path: projectInstall ? ".mcp.json" : "Claude Code user config", how: "registered via claude mcp add" };
|
|
84
88
|
} catch (error) {
|
|
85
89
|
const detail = error?.stderr?.toString().trim() || error.message;
|
|
@@ -171,6 +175,17 @@ export const WRITERS = {
|
|
|
171
175
|
|
|
172
176
|
/** Quote only what a shell would otherwise mangle, so the line stays readable. */
|
|
173
177
|
export function shellQuote(value) {
|
|
178
|
+
// cmd.exe does not read single quotes as quoting at all, so the POSIX form
|
|
179
|
+
// was handed to Windows users as something to paste and arrived at the agent
|
|
180
|
+
// with the quotes still in it -- an API key wrapped in apostrophes is a
|
|
181
|
+
// different API key. A value with a double quote in it cannot be expressed
|
|
182
|
+
// safely on cmd.exe, and saying so beats emitting something that will run
|
|
183
|
+
// and do the wrong thing.
|
|
184
|
+
if (process.platform === "win32") {
|
|
185
|
+
if (/^[A-Za-z0-9_@%+=:,./\\-]+$/.test(value)) return value;
|
|
186
|
+
if (value.includes('"')) return `<value containing a double quote: set it by hand>`;
|
|
187
|
+
return `"${value}"`;
|
|
188
|
+
}
|
|
174
189
|
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
175
190
|
}
|
|
176
191
|
|
|
@@ -193,8 +208,7 @@ export function renderAllAgentSnippets(args, serverName, { projectInstall = fals
|
|
|
193
208
|
PREMAN_FRONTEND: frontendUrl(args),
|
|
194
209
|
};
|
|
195
210
|
const serverConfig = {
|
|
196
|
-
|
|
197
|
-
args: [...LAUNCHER_ARGS],
|
|
211
|
+
...launcherFor(),
|
|
198
212
|
env,
|
|
199
213
|
};
|
|
200
214
|
|
|
@@ -243,7 +257,7 @@ export function verifyWrittenConfig(agent, { serverName, written }) {
|
|
|
243
257
|
if (!block) {
|
|
244
258
|
return { status: "mismatch", detail: `no [mcp_servers.${serverName}] block in ${written.path}` };
|
|
245
259
|
}
|
|
246
|
-
if (!block.some((line) => line.trim() === `command = "${
|
|
260
|
+
if (!block.some((line) => line.trim() === `command = "${launcherFor().command}"`)) {
|
|
247
261
|
return {
|
|
248
262
|
status: "mismatch",
|
|
249
263
|
detail: `${written.path} does not launch ${LAUNCHER_COMMAND}`,
|
|
@@ -263,7 +277,7 @@ export function verifyWrittenConfig(agent, { serverName, written }) {
|
|
|
263
277
|
|
|
264
278
|
const entry = readJsonFile(written.path).mcpServers?.[serverName];
|
|
265
279
|
if (!entry) return { status: "mismatch", detail: `${serverName} is missing from ${written.path}` };
|
|
266
|
-
if (entry.command !==
|
|
280
|
+
if (entry.command !== launcherFor().command) {
|
|
267
281
|
return {
|
|
268
282
|
status: "mismatch",
|
|
269
283
|
detail: `${written.path} does not launch ${LAUNCHER_COMMAND}`,
|