premanmcp 1.1.11 → 1.1.13
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 +328 -50
- package/bin/eval_entry.js +9 -2
- 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 +198 -4
- package/bin/eval_start.js +203 -0
- package/bin/eval_target.js +20 -1
- package/bin/eval_templates.js +447 -21
- 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/shared.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readFileSync,
|
|
15
15
|
realpathSync,
|
|
16
16
|
rmSync,
|
|
17
|
+
statSync,
|
|
17
18
|
writeFileSync,
|
|
18
19
|
mkdirSync,
|
|
19
20
|
} from "node:fs";
|
|
@@ -107,6 +108,19 @@ export function pathPremanOwner() {
|
|
|
107
108
|
if (isEphemeralBin(realPath)) continue;
|
|
108
109
|
const owner = owningPackage(realPath);
|
|
109
110
|
if (owner) return owner;
|
|
111
|
+
// Only a symlink resolves to the file inside the package. npm on Windows
|
|
112
|
+
// installs `preman.cmd`, and pnpm installs a launcher shim on every
|
|
113
|
+
// platform, both of which are real files that merely *name* the target --
|
|
114
|
+
// so realpath ends at the shim, no manifest is found above it, and a
|
|
115
|
+
// perfectly good global install was reported as not owning `preman`. The
|
|
116
|
+
// cost of that wrong answer is the hook and every printed instruction
|
|
117
|
+
// switching to `npm exec -y premanmcp@latest --`, which re-resolves the
|
|
118
|
+
// package from the network on every git push.
|
|
119
|
+
const named = shimTarget(realPath);
|
|
120
|
+
if (named) {
|
|
121
|
+
const shimOwner = owningPackage(named);
|
|
122
|
+
if (shimOwner) return shimOwner;
|
|
123
|
+
}
|
|
110
124
|
} catch {
|
|
111
125
|
// A dangling link or an unreadable manifest: try the next match.
|
|
112
126
|
}
|
|
@@ -114,6 +128,24 @@ export function pathPremanOwner() {
|
|
|
114
128
|
return "";
|
|
115
129
|
}
|
|
116
130
|
|
|
131
|
+
/** The path a launcher shim points at, or "" if this is not one. */
|
|
132
|
+
function shimTarget(shimPath) {
|
|
133
|
+
try {
|
|
134
|
+
// A real bundle is megabytes; a shim is a few hundred bytes. The cap is
|
|
135
|
+
// what keeps this from reading an executable into memory to look for paths.
|
|
136
|
+
if (statSync(shimPath).size > 16 * 1024) return "";
|
|
137
|
+
const body = readFileSync(shimPath, "utf8");
|
|
138
|
+
// Covers npm's .cmd and its shell wrapper, and pnpm's, all of which name
|
|
139
|
+
// the entry point relative to the shim or absolutely.
|
|
140
|
+
const match = body.match(/["']?([^"'\s]*node_modules[/\\][^"'\s]+\.(?:js|mjs|cjs))["']?/);
|
|
141
|
+
if (!match) return "";
|
|
142
|
+
const target = match[1].replace(/^%~dp0/, "").replace(/^\$basedir/, "");
|
|
143
|
+
return realpathSync(path.resolve(path.dirname(shimPath), target));
|
|
144
|
+
} catch {
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
117
149
|
export function cliInvocation() {
|
|
118
150
|
return (_invocation ||=
|
|
119
151
|
pathPremanOwner() === "premanmcp" ? "preman" : "npm exec -y premanmcp@latest --");
|
|
@@ -208,6 +240,26 @@ export function readStoredCredentials() {
|
|
|
208
240
|
export function saveStoredCredentials(creds) {
|
|
209
241
|
mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
210
242
|
writeFileSync(CREDENTIALS_FILE, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
|
|
243
|
+
warnIfModesAreDecorative();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Say so where 0600 does not mean 0600.
|
|
248
|
+
*
|
|
249
|
+
* Windows ignores the POSIX mode bits entirely, so the file holding a live
|
|
250
|
+
* pm_live_ key inherits whatever the parent directory grants -- which under a
|
|
251
|
+
* roaming or shared profile can be other accounts on the machine. The mode is
|
|
252
|
+
* still passed, because it costs nothing and is correct everywhere else; what
|
|
253
|
+
* was missing is anyone being told that on this platform it is decorative.
|
|
254
|
+
*/
|
|
255
|
+
let modeWarningShown = false;
|
|
256
|
+
function warnIfModesAreDecorative() {
|
|
257
|
+
if (process.platform !== "win32" || modeWarningShown) return;
|
|
258
|
+
modeWarningShown = true;
|
|
259
|
+
process.stdout.write(
|
|
260
|
+
`Note: ${CREDENTIALS_FILE} holds an API key, and Windows does not apply the\n` +
|
|
261
|
+
` owner-only permissions PreMan asks for. Keep this profile to yourself.\n`
|
|
262
|
+
);
|
|
211
263
|
}
|
|
212
264
|
|
|
213
265
|
/** Forget the stored key. Returns whether there was one. */
|
|
@@ -371,15 +423,124 @@ export function openUrl(url, { app = "" } = {}) {
|
|
|
371
423
|
detached: true,
|
|
372
424
|
shell: process.platform === "win32",
|
|
373
425
|
});
|
|
426
|
+
// spawn reports a missing opener through an asynchronous 'error' event, not
|
|
427
|
+
// a throw, so this try/catch never saw it and an EventEmitter with no
|
|
428
|
+
// 'error' listener rethrows: a Linux box without `xdg-open` took the whole
|
|
429
|
+
// process down from `login --browser`, from the integration flows, and from
|
|
430
|
+
// /app inside a session. Headless boxes and locked-down shells are normal,
|
|
431
|
+
// so this is the expected case rather than a fault.
|
|
432
|
+
child.once("error", () => {});
|
|
374
433
|
child.unref();
|
|
375
434
|
return true;
|
|
376
435
|
} catch {
|
|
377
|
-
// Headless boxes and locked-down shells are normal.
|
|
378
436
|
return false;
|
|
379
437
|
}
|
|
380
438
|
}
|
|
381
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Piped answers, read as a stream and kept in order.
|
|
442
|
+
*
|
|
443
|
+
* A readline interface built per prompt consumed the line it was handed and
|
|
444
|
+
* then threw away everything readline had already buffered behind it, so piped
|
|
445
|
+
* input survived exactly one question. `printf 'sure\nn\n' | preman onboard`
|
|
446
|
+
* spent "sure" on the first prompt, lost the "n" with the interface, and every
|
|
447
|
+
* later prompt fell through to its default -- and the defaults are yes. A
|
|
448
|
+
* script written to decline things accepted them instead, the git hook
|
|
449
|
+
* included, and it did so silently.
|
|
450
|
+
*
|
|
451
|
+
* Lines are therefore consumed from stdin directly and queued. stdin is only
|
|
452
|
+
* referenced while a prompt is actually outstanding: holding it open would
|
|
453
|
+
* leave a finished run waiting on a writer that has nothing left to say, and
|
|
454
|
+
* unreferencing it permanently would let the process exit mid-walk on a
|
|
455
|
+
* question it had already drawn.
|
|
456
|
+
*/
|
|
457
|
+
let pipedAnswers = null;
|
|
458
|
+
|
|
459
|
+
function pipedReader() {
|
|
460
|
+
if (pipedAnswers) return pipedAnswers;
|
|
461
|
+
|
|
462
|
+
const ready = [];
|
|
463
|
+
const waiting = [];
|
|
464
|
+
const state = { ended: false, held: false };
|
|
465
|
+
let buffer = "";
|
|
466
|
+
|
|
467
|
+
const hold = (on) => {
|
|
468
|
+
if (state.held === on) return;
|
|
469
|
+
const fn = on ? process.stdin.ref : process.stdin.unref;
|
|
470
|
+
if (typeof fn === "function") {
|
|
471
|
+
fn.call(process.stdin);
|
|
472
|
+
state.held = on;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const deliver = (value) => {
|
|
477
|
+
const next = waiting.shift();
|
|
478
|
+
if (!next) {
|
|
479
|
+
ready.push(value);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
next(value);
|
|
483
|
+
if (!waiting.length) hold(false);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const end = () => {
|
|
487
|
+
if (state.ended) return;
|
|
488
|
+
state.ended = true;
|
|
489
|
+
if (buffer.length) deliver(buffer);
|
|
490
|
+
buffer = "";
|
|
491
|
+
while (waiting.length) waiting.shift()("");
|
|
492
|
+
hold(false);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// Nothing read stdin before now, so an already-ended stream will not emit
|
|
496
|
+
// 'end' again and every take() would wait on a line that cannot arrive.
|
|
497
|
+
if (process.stdin.readableEnded || process.stdin.destroyed) state.ended = true;
|
|
498
|
+
process.stdin.setEncoding("utf8");
|
|
499
|
+
process.stdin.on("data", (chunk) => {
|
|
500
|
+
buffer += chunk;
|
|
501
|
+
let cut = buffer.indexOf("\n");
|
|
502
|
+
while (cut !== -1) {
|
|
503
|
+
deliver(buffer.slice(0, cut).replace(/\r$/, ""));
|
|
504
|
+
buffer = buffer.slice(cut + 1);
|
|
505
|
+
cut = buffer.indexOf("\n");
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
process.stdin.once("end", end);
|
|
509
|
+
process.stdin.once("error", end);
|
|
510
|
+
hold(false);
|
|
511
|
+
|
|
512
|
+
pipedAnswers = {
|
|
513
|
+
take() {
|
|
514
|
+
if (ready.length) return Promise.resolve(ready.shift());
|
|
515
|
+
if (state.ended) return Promise.resolve("");
|
|
516
|
+
hold(true);
|
|
517
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
return pipedAnswers;
|
|
521
|
+
}
|
|
522
|
+
|
|
382
523
|
export async function promptText(question) {
|
|
524
|
+
// Piped input is answered from the queue and nowhere else. Testing
|
|
525
|
+
// `readableEnded` first looked like the same thing and was not: a pipe
|
|
526
|
+
// delivers every answer in one write and then closes, so by the second
|
|
527
|
+
// question the stream has ended while its remaining lines are still sitting
|
|
528
|
+
// in the queue unread -- and this returned "" on top of them. It only showed
|
|
529
|
+
// up where something slow sat between two prompts, which is why the walk hit
|
|
530
|
+
// it and a tight loop of prompts did not: the account check is an HTTP round
|
|
531
|
+
// trip, and during it stdin ends. The prompt after it was the confirmation
|
|
532
|
+
// before creating an account, so a piped "n" was read as the default, and
|
|
533
|
+
// the default is yes.
|
|
534
|
+
if (!process.stdin.isTTY) {
|
|
535
|
+
process.stdout.write(question);
|
|
536
|
+
const answer = await pipedReader().take();
|
|
537
|
+
// A piped answer is never echoed back, so without this the next thing
|
|
538
|
+
// written lands on the prompt line and reads as one broken sentence:
|
|
539
|
+
// `Email: [preman] Email is required.`
|
|
540
|
+
process.stdout.write(`${answer}\n`);
|
|
541
|
+
return answer.trim();
|
|
542
|
+
}
|
|
543
|
+
|
|
383
544
|
// A stream that has already ended will never deliver a line, and readline does
|
|
384
545
|
// not emit 'close' for an 'end' that happened before the interface existed --
|
|
385
546
|
// so the second prompt after stdin ran out resolved nothing, kept nothing
|
|
@@ -390,7 +551,17 @@ export async function promptText(question) {
|
|
|
390
551
|
process.stdout.write(`${question}\n`);
|
|
391
552
|
return "";
|
|
392
553
|
}
|
|
554
|
+
|
|
393
555
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
556
|
+
// Readline owns raw mode while it has a question open, so ^C never reaches
|
|
557
|
+
// Node as a signal: the prompt resolved empty, empty is the step's default,
|
|
558
|
+
// and the default is yes. Interrupting a question used to accept it.
|
|
559
|
+
const interrupt = () => {
|
|
560
|
+
process.stdout.write("\n");
|
|
561
|
+
rl.close();
|
|
562
|
+
process.exit(130);
|
|
563
|
+
};
|
|
564
|
+
rl.once("SIGINT", interrupt);
|
|
394
565
|
try {
|
|
395
566
|
// An exhausted or closed stdin never answers, and rl.question waits for an
|
|
396
567
|
// answer that cannot come. That is how a run given fewer answers than it had
|
|
@@ -403,6 +574,7 @@ export async function promptText(question) {
|
|
|
403
574
|
]);
|
|
404
575
|
return answer.trim();
|
|
405
576
|
} finally {
|
|
577
|
+
rl.off("SIGINT", interrupt);
|
|
406
578
|
rl.close();
|
|
407
579
|
}
|
|
408
580
|
}
|
|
@@ -645,9 +817,10 @@ function authSessionFrom(result, email) {
|
|
|
645
817
|
}
|
|
646
818
|
|
|
647
819
|
async function verifyUnconfirmedAccount(args, email) {
|
|
648
|
-
process.stdout.write("This
|
|
820
|
+
process.stdout.write("This account still needs its email verified. Sending a new code.\n");
|
|
649
821
|
const resend = await callBackendJson(args, "POST", "/auth/resend-otp", { json: { email } });
|
|
650
822
|
assertOk(resend, "resend OTP");
|
|
823
|
+
if (!reportCodeSent(resend, "New code sent. Check your email.")) throw undeliveredCode();
|
|
651
824
|
const otp = await promptText("Verification code: ");
|
|
652
825
|
const verified = await callBackendJson(args, "POST", "/auth/verify-otp", { json: { email, otp } });
|
|
653
826
|
assertOk(verified, "verify OTP");
|
|
@@ -703,7 +876,7 @@ async function createAccountFromTerminal(args, email) {
|
|
|
703
876
|
});
|
|
704
877
|
|
|
705
878
|
if (signup.ok) {
|
|
706
|
-
|
|
879
|
+
if (!reportCodeSent(signup)) throw undeliveredCode();
|
|
707
880
|
const otp = await promptText("Verification code: ");
|
|
708
881
|
return completeWithCode(args, email, otp, password);
|
|
709
882
|
}
|
|
@@ -722,7 +895,7 @@ async function createAccountFromTerminal(args, email) {
|
|
|
722
895
|
json: { email, password: legacyPassword },
|
|
723
896
|
});
|
|
724
897
|
assertOk(legacySignup, "signup");
|
|
725
|
-
|
|
898
|
+
if (!reportCodeSent(legacySignup)) throw undeliveredCode();
|
|
726
899
|
const otp = await promptText("Verification code: ");
|
|
727
900
|
const verified = await callBackendJson(args, "POST", "/auth/verify-otp", {
|
|
728
901
|
json: { email, otp },
|
|
@@ -731,6 +904,79 @@ async function createAccountFromTerminal(args, email) {
|
|
|
731
904
|
return authSessionFrom(verified, email);
|
|
732
905
|
}
|
|
733
906
|
|
|
907
|
+
/** Read the address back before an account exists on it. */
|
|
908
|
+
async function confirmNewAccount(email) {
|
|
909
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
910
|
+
const answer = (await promptText(`No PreMan account for ${email}. Create one? [Y/n]: `))
|
|
911
|
+
.trim()
|
|
912
|
+
.toLowerCase();
|
|
913
|
+
if (answer === "" || answer === "y" || answer === "yes") return true;
|
|
914
|
+
if (answer === "n" || answer === "no") return false;
|
|
915
|
+
process.stdout.write(" Please answer y or n.\n");
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* The stored key, if it is for this backend and the backend still honours it.
|
|
922
|
+
*
|
|
923
|
+
* Revoked and stale keys have to fall through to a real sign-in rather than
|
|
924
|
+
* being handed back, or the walk would carry on to the steps that need a
|
|
925
|
+
* working key and fail at each of them in turn.
|
|
926
|
+
*/
|
|
927
|
+
async function reusableCredentials(args) {
|
|
928
|
+
const stored = readStoredCredentials();
|
|
929
|
+
if (!stored?.api_key) return null;
|
|
930
|
+
if (stored.backend_url && stored.backend_url !== backendUrl(args)) return null;
|
|
931
|
+
const wanted = args.value("--email", "").trim().toLowerCase();
|
|
932
|
+
if (wanted && String(stored.user_email || "").toLowerCase() !== wanted) return null;
|
|
933
|
+
if (args.has("--relogin")) return null;
|
|
934
|
+
|
|
935
|
+
try {
|
|
936
|
+
const status = await callBackendJson(args, "GET", "/cli/status", { token: stored.api_key });
|
|
937
|
+
return status.ok ? stored : null;
|
|
938
|
+
} catch {
|
|
939
|
+
// Offline or a backend that is down is not evidence the key is bad, but it
|
|
940
|
+
// is also not a sign-in, and prompting for an email we cannot check is
|
|
941
|
+
// worse than saying what actually happened.
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Say whether a code is actually on its way.
|
|
948
|
+
*
|
|
949
|
+
* The backend reports delivery in `email_sent` and answers 200 either way, so
|
|
950
|
+
* every one of these lines used to send somebody to an inbox that was never
|
|
951
|
+
* going to receive anything -- including on any deployment with no sender
|
|
952
|
+
* configured, where the code only ever reached the server log. The dashboard
|
|
953
|
+
* has read this field since it existed; the terminal never did.
|
|
954
|
+
*/
|
|
955
|
+
function reportCodeSent(result, sent = "Verification code sent. Check your email.") {
|
|
956
|
+
// Only ever present on a deployment that has deliberately switched it on,
|
|
957
|
+
// which is how dev stays usable without a mail sender.
|
|
958
|
+
if (result?.dev_otp) {
|
|
959
|
+
process.stdout.write(`Verification code (dev): ${result.dev_otp}\n`);
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
if (result?.email_sent === false) {
|
|
963
|
+
process.stdout.write(
|
|
964
|
+
"PreMan could not send the code — the email service rejected it.\n" +
|
|
965
|
+
" Nothing is wrong with your account. Try again shortly, or contact support.\n"
|
|
966
|
+
);
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
process.stdout.write(`${sent}\n`);
|
|
970
|
+
return true;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** The reason a code never went out, phrased as something to do about it. */
|
|
974
|
+
function undeliveredCode() {
|
|
975
|
+
return new Error(
|
|
976
|
+
"PreMan could not email a verification code, so there is nothing to enter. Try again shortly."
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
734
980
|
/**
|
|
735
981
|
* Get a usable pm_live_ key: an explicit one, or by logging in / signing up
|
|
736
982
|
* right here in the terminal. Always persists to ~/.preman/credentials.json.
|
|
@@ -758,8 +1004,26 @@ export async function authenticateTerminal(args) {
|
|
|
758
1004
|
return creds;
|
|
759
1005
|
}
|
|
760
1006
|
|
|
761
|
-
|
|
1007
|
+
// A key is minted per call, so a second `onboard` used to leave a second
|
|
1008
|
+
// `PreMan MCP CLI (host)` key behind and ask for an email and a code to do
|
|
1009
|
+
// it. Re-running setup is ordinary -- it is how anyone re-checks their own
|
|
1010
|
+
// machine -- so a credential that still answers is the answer.
|
|
1011
|
+
const reusable = await reusableCredentials(args);
|
|
1012
|
+
if (reusable) {
|
|
1013
|
+
process.stdout.write(`Already signed in as ${reusable.user_email}.\n`);
|
|
1014
|
+
return reusable;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const supplied = args.value("--email", "");
|
|
1018
|
+
const email = (supplied || (await promptText("Email: "))).trim().toLowerCase();
|
|
762
1019
|
if (!email) throw new Error("Email is required.");
|
|
1020
|
+
// Nothing downstream validates this and the backend takes what it is given,
|
|
1021
|
+
// so a mistyped address became an account with a code sent to somebody
|
|
1022
|
+
// else's inbox. Deliberately permissive: the only job here is to catch the
|
|
1023
|
+
// shapes that cannot be an address at all.
|
|
1024
|
+
if (!/^[^\s@]+@[^\s@.]+\.[^\s@]+$/.test(email)) {
|
|
1025
|
+
throw new Error(`${email} does not look like an email address.`);
|
|
1026
|
+
}
|
|
763
1027
|
|
|
764
1028
|
process.stdout.write(`Checking PreMan account for ${email}...\n`);
|
|
765
1029
|
const account = await callBackendJson(args, "GET", "/auth/needs-password", { query: { email } });
|
|
@@ -768,14 +1032,22 @@ export async function authenticateTerminal(args) {
|
|
|
768
1032
|
let session = { access_token: "", user: { email } };
|
|
769
1033
|
|
|
770
1034
|
if (!account.exists) {
|
|
771
|
-
|
|
1035
|
+
// Creating the account was the silent half of a fork the customer never
|
|
1036
|
+
// chose: one typo in the domain and the next prompt asked for a code that
|
|
1037
|
+
// had gone to an address they do not own, with an account left behind on
|
|
1038
|
+
// it. The address is worth reading back before any of that happens.
|
|
1039
|
+
if (!args.has("--yes") && !supplied && !(await confirmNewAccount(email))) {
|
|
1040
|
+
throw new Error("Stopped before creating an account. Run it again with the right address.");
|
|
1041
|
+
}
|
|
1042
|
+
process.stdout.write(`No PreMan account found for ${email}. Creating one now.\n`);
|
|
772
1043
|
session = await createAccountFromTerminal(args, email);
|
|
773
1044
|
} else if (account.needs_password) {
|
|
774
1045
|
// No password on this account, so a code is the only way in.
|
|
775
1046
|
const password = await optionalPassword(args);
|
|
776
|
-
process.stdout.write("
|
|
1047
|
+
process.stdout.write("This account signs in with an email code.\n");
|
|
777
1048
|
const resend = await callBackendJson(args, "POST", "/auth/resend-otp", { json: { email } });
|
|
778
1049
|
assertOk(resend, "resend OTP");
|
|
1050
|
+
if (!reportCodeSent(resend, "Sign-in code sent. Check your email.")) throw undeliveredCode();
|
|
779
1051
|
const otp = await promptText("Verification code: ");
|
|
780
1052
|
session = await completeWithCode(args, email, otp, password);
|
|
781
1053
|
} else {
|
|
@@ -783,14 +1055,20 @@ export async function authenticateTerminal(args) {
|
|
|
783
1055
|
if (!password) {
|
|
784
1056
|
const resend = await callBackendJson(args, "POST", "/auth/resend-otp", { json: { email } });
|
|
785
1057
|
assertOk(resend, "resend OTP");
|
|
786
|
-
|
|
1058
|
+
if (!reportCodeSent(resend, "Sign-in code sent. Check your email.")) throw undeliveredCode();
|
|
787
1059
|
const otp = await promptText("Verification code: ");
|
|
788
1060
|
session = await completeWithCode(args, email, otp, "");
|
|
789
1061
|
} else {
|
|
790
1062
|
const login = await callBackendJson(args, "POST", "/auth/login", {
|
|
791
1063
|
json: { email, password },
|
|
792
1064
|
});
|
|
793
|
-
|
|
1065
|
+
// The backend now names this case in a `code` field. The prose test stays
|
|
1066
|
+
// as the fallback, because a customer on an older deployment still has to
|
|
1067
|
+
// be able to get past an unverified account.
|
|
1068
|
+
const unverified =
|
|
1069
|
+
login.code === "email_not_verified" ||
|
|
1070
|
+
/not verified/i.test(describeFailure(login, ""));
|
|
1071
|
+
if (!login.ok && login.status_code === 403 && unverified) {
|
|
794
1072
|
session = await verifyUnconfirmedAccount(args, email);
|
|
795
1073
|
} else {
|
|
796
1074
|
assertOk(login, "login");
|
|
@@ -882,6 +1160,49 @@ export function resolveApiKey(args) {
|
|
|
882
1160
|
export const LAUNCHER_COMMAND = "npm";
|
|
883
1161
|
export const LAUNCHER_ARGS = ["exec", "-y", "premanmcp@latest", "--"];
|
|
884
1162
|
|
|
1163
|
+
/**
|
|
1164
|
+
* How this platform has to be told to start the server.
|
|
1165
|
+
*
|
|
1166
|
+
* `npm` on Windows is `npm.cmd`, and a batch file is not an executable: a host
|
|
1167
|
+
* that spawns the command directly -- which is what every MCP host does, none
|
|
1168
|
+
* of them go through a shell -- fails with EINVAL before any of this code runs.
|
|
1169
|
+
* The config therefore has to name the interpreter, and `cmd /c` is the form
|
|
1170
|
+
* every Windows host accepts. Nothing changes anywhere else, so the constants
|
|
1171
|
+
* stay the plain answer they have always been.
|
|
1172
|
+
*
|
|
1173
|
+
* The verifier reads this too. It used to compare the written command against
|
|
1174
|
+
* LAUNCHER_COMMAND, which on Windows would have meant warning about a config
|
|
1175
|
+
* that was right and accepting one that could not start.
|
|
1176
|
+
*/
|
|
1177
|
+
export function launcherFor(platform = process.platform) {
|
|
1178
|
+
if (platform === "win32") {
|
|
1179
|
+
return { command: "cmd", args: ["/c", LAUNCHER_COMMAND, ...LAUNCHER_ARGS] };
|
|
1180
|
+
}
|
|
1181
|
+
return { command: LAUNCHER_COMMAND, args: [...LAUNCHER_ARGS] };
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* The oldest Node that can run this CLI, checked wherever it is about to matter.
|
|
1186
|
+
*
|
|
1187
|
+
* `engines` in package.json is advice: npm prints a warning and installs
|
|
1188
|
+
* anyway, so it stops nobody. The real floor is `fetch`, which arrived in Node
|
|
1189
|
+
* 18 and which every backend call goes through -- on Node 16 the first of them
|
|
1190
|
+
* dies with `fetch is not defined`, a message that names nothing a customer
|
|
1191
|
+
* could act on and appears several steps into a walk rather than at the start.
|
|
1192
|
+
* Only the MCP server preflight checked this; every other command did not.
|
|
1193
|
+
*/
|
|
1194
|
+
export const MIN_NODE_MAJOR = 18;
|
|
1195
|
+
|
|
1196
|
+
export function nodeTooOld() {
|
|
1197
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
1198
|
+
if (!Number.isFinite(major) || major >= MIN_NODE_MAJOR) return "";
|
|
1199
|
+
return (
|
|
1200
|
+
`PreMan needs Node ${MIN_NODE_MAJOR} or newer. This is Node ${process.versions.node}.\n` +
|
|
1201
|
+
` Upgrade Node, then run the same command again.\n` +
|
|
1202
|
+
` With nvm: nvm install ${MIN_NODE_MAJOR} && nvm use ${MIN_NODE_MAJOR}\n`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
885
1206
|
export function buildServerConfig(args, { pairCode = "", codingAgent = "" } = {}) {
|
|
886
1207
|
const env = {
|
|
887
1208
|
PREMAN_BACKEND: backendUrl(args),
|
|
@@ -892,9 +1213,5 @@ export function buildServerConfig(args, { pairCode = "", codingAgent = "" } = {}
|
|
|
892
1213
|
if (pairCode) env.PREMAN_PAIR_CODE = pairCode;
|
|
893
1214
|
if (codingAgent) env.PREMAN_CODING_AGENT = codingAgent;
|
|
894
1215
|
|
|
895
|
-
return {
|
|
896
|
-
command: LAUNCHER_COMMAND,
|
|
897
|
-
args: [...LAUNCHER_ARGS],
|
|
898
|
-
env,
|
|
899
|
-
};
|
|
1216
|
+
return { ...launcherFor(), env };
|
|
900
1217
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "premanmcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.13",
|
|
4
4
|
"description": "PreMan CLI and stdio proxy for PreMan's hosted MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"test:proxy": "node --test scripts/smoke-proxy.mjs",
|
|
17
17
|
"test:connect": "node scripts/smoke-connect.mjs",
|
|
18
18
|
"test:connect-heartbeat": "node scripts/smoke-connect-heartbeat.mjs",
|
|
19
|
-
"test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-agent-session.mjs scripts/smoke-agent-dock.mjs scripts/smoke-terminal-lifecycle.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-cli-update.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-eval.mjs scripts/smoke-eval-run-watch.mjs scripts/smoke-eval-behaviors.mjs scripts/smoke-eval-runtime.mjs scripts/smoke-eval-entry.mjs scripts/smoke-eval-entry-placeholder.mjs scripts/smoke-eval-discover.mjs scripts/smoke-eval-templates.mjs scripts/smoke-eval-approval-record.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-agent-target.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs scripts/smoke-process-title.mjs scripts/smoke-predict.mjs",
|
|
19
|
+
"test:node": "node --test --test-timeout=90000 scripts/smoke-account.mjs scripts/smoke-agent-session.mjs scripts/smoke-agent-dock.mjs scripts/smoke-terminal-lifecycle.mjs scripts/smoke-cli-entrypoint.mjs scripts/smoke-cli-update.mjs scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-eval.mjs scripts/smoke-eval-start.mjs scripts/smoke-eval-run-watch.mjs scripts/smoke-eval-behaviors.mjs scripts/smoke-eval-runtime.mjs scripts/smoke-eval-entry.mjs scripts/smoke-eval-entry-placeholder.mjs scripts/smoke-eval-discover.mjs scripts/smoke-eval-templates.mjs scripts/smoke-eval-approval-record.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-onboard-opening.mjs scripts/smoke-local-detect.mjs scripts/smoke-agent-target.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.mjs scripts/smoke-runner-heartbeat.mjs scripts/smoke-verify-prepush.mjs scripts/smoke-push-diff.mjs scripts/smoke-progress-reporter.mjs scripts/smoke-verify-plan.mjs scripts/smoke-install-desktop.mjs scripts/smoke-desktop-session.mjs scripts/smoke-api-tools.mjs scripts/smoke-tests-workbench.mjs scripts/smoke-bin-scope.mjs scripts/smoke-shared-errors.mjs scripts/smoke-prompts.mjs scripts/smoke-process-title.mjs scripts/smoke-predict.mjs",
|
|
20
20
|
"test:dmg": "node --test --test-timeout=300000 scripts/smoke-install-desktop-volume.mjs"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|