premanmcp 0.8.0 → 0.10.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/README.md +63 -19
- package/bin/account.js +2 -13
- package/bin/cli.js +15 -2
- package/bin/connect.js +891 -106
- package/bin/integrations.js +2 -22
- package/bin/runner.js +880 -0
- package/bin/shared.js +112 -25
- package/dist/server.js +29 -9
- package/package.json +1 -1
package/bin/shared.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
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";
|
|
11
|
-
import { chmodSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
11
|
+
import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline/promises";
|
|
@@ -93,6 +93,18 @@ export function saveStoredCredentials(creds) {
|
|
|
93
93
|
writeFileSync(CREDENTIALS_FILE, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** Forget the stored key. Returns whether there was one. */
|
|
97
|
+
export function clearStoredCredentials() {
|
|
98
|
+
if (!existsSync(CREDENTIALS_FILE)) return false;
|
|
99
|
+
rmSync(CREDENTIALS_FILE, { force: true });
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** True when the key in play came from a flag or the environment, not our store. */
|
|
104
|
+
export function apiKeyIsExplicit(args) {
|
|
105
|
+
return Boolean(args.value("--api-key", "") || process.env.PREMAN_API_KEY);
|
|
106
|
+
}
|
|
107
|
+
|
|
96
108
|
export function backendUrl(args) {
|
|
97
109
|
return args.value("--backend", process.env.PREMAN_BACKEND || DEFAULT_BACKEND).replace(/\/+$/, "");
|
|
98
110
|
}
|
|
@@ -101,6 +113,36 @@ export function frontendUrl(args) {
|
|
|
101
113
|
return args.value("--frontend", process.env.PREMAN_FRONTEND || DEFAULT_FRONTEND).replace(/\/+$/, "");
|
|
102
114
|
}
|
|
103
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Open a URL in the user's default browser, best effort.
|
|
118
|
+
*
|
|
119
|
+
* Returns false without spawning anything when nobody is there to look at it:
|
|
120
|
+
* `PREMAN_NO_BROWSER` for an explicit no, and a non-TTY stdout for CI, piped
|
|
121
|
+
* output, and test runs. Every caller prints the URL as well, so this is only
|
|
122
|
+
* ever a convenience — a suite that exercises these flows should not be able to
|
|
123
|
+
* throw tabs at whoever is running it.
|
|
124
|
+
*/
|
|
125
|
+
export function openUrl(url) {
|
|
126
|
+
const optOut = (process.env.PREMAN_NO_BROWSER || "").trim().toLowerCase();
|
|
127
|
+
if (optOut && !["0", "false", "no"].includes(optOut)) return false;
|
|
128
|
+
if (!process.stdout.isTTY) return false;
|
|
129
|
+
|
|
130
|
+
const opener =
|
|
131
|
+
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
132
|
+
try {
|
|
133
|
+
const child = spawn(opener, [url], {
|
|
134
|
+
stdio: "ignore",
|
|
135
|
+
detached: true,
|
|
136
|
+
shell: process.platform === "win32",
|
|
137
|
+
});
|
|
138
|
+
child.unref();
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
// Headless boxes and locked-down shells are normal.
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
104
146
|
export async function promptText(question) {
|
|
105
147
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
106
148
|
try {
|
|
@@ -235,7 +277,50 @@ async function verifyUnconfirmedAccount(args, email) {
|
|
|
235
277
|
return verified.access_token;
|
|
236
278
|
}
|
|
237
279
|
|
|
280
|
+
/**
|
|
281
|
+
* A password only if the caller asked for one.
|
|
282
|
+
*
|
|
283
|
+
* The terminal's durable credential is the pm_live_ key, so signup does not
|
|
284
|
+
* need a password and no longer asks for one. `--password` is for anyone who
|
|
285
|
+
* wants to be able to sign in to the dashboard immediately; `--password <value>`
|
|
286
|
+
* skips the prompt for scripted runs.
|
|
287
|
+
*/
|
|
288
|
+
async function optionalPassword(args) {
|
|
289
|
+
if (!args.has("--password")) return "";
|
|
290
|
+
const supplied = args.value("--password", "");
|
|
291
|
+
// Bare `--password` followed by another flag means "prompt me", not "my
|
|
292
|
+
// password is --backend".
|
|
293
|
+
if (supplied && !supplied.startsWith("--")) {
|
|
294
|
+
if (supplied.length < 6) throw new Error("Password must be at least 6 characters.");
|
|
295
|
+
return supplied;
|
|
296
|
+
}
|
|
297
|
+
return promptPasswordTwice();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Turn a verification code into a session token.
|
|
302
|
+
*
|
|
303
|
+
* Without a password this is one call: verify-otp signs the user in and the
|
|
304
|
+
* backend keeps the account passwordless, so the dashboard knows to offer a
|
|
305
|
+
* code rather than a password field.
|
|
306
|
+
*/
|
|
307
|
+
async function completeWithCode(args, email, otp, password) {
|
|
308
|
+
if (password) {
|
|
309
|
+
const setPassword = await callBackendJson(args, "POST", "/auth/set-password", {
|
|
310
|
+
json: { email, otp, new_password: password },
|
|
311
|
+
});
|
|
312
|
+
assertOk(setPassword, "set password");
|
|
313
|
+
return String(setPassword.access_token || "");
|
|
314
|
+
}
|
|
315
|
+
const verified = await callBackendJson(args, "POST", "/auth/verify-otp", {
|
|
316
|
+
json: { email, otp },
|
|
317
|
+
});
|
|
318
|
+
assertOk(verified, "verify OTP");
|
|
319
|
+
return String(verified.access_token || "");
|
|
320
|
+
}
|
|
321
|
+
|
|
238
322
|
async function createAccountFromTerminal(args, email) {
|
|
323
|
+
const password = await optionalPassword(args);
|
|
239
324
|
const signup = await callBackendJson(args, "POST", "/auth/start-signup", {
|
|
240
325
|
json: { email },
|
|
241
326
|
});
|
|
@@ -243,24 +328,21 @@ async function createAccountFromTerminal(args, email) {
|
|
|
243
328
|
if (signup.ok) {
|
|
244
329
|
process.stdout.write("Verification code sent. Check your email.\n");
|
|
245
330
|
const otp = await promptText("Verification code: ");
|
|
246
|
-
|
|
247
|
-
const setPassword = await callBackendJson(args, "POST", "/auth/set-password", {
|
|
248
|
-
json: { email, otp, new_password: password },
|
|
249
|
-
});
|
|
250
|
-
assertOk(setPassword, "set password");
|
|
251
|
-
return String(setPassword.access_token || "");
|
|
331
|
+
return completeWithCode(args, email, otp, password);
|
|
252
332
|
}
|
|
253
333
|
|
|
254
334
|
if (signup.status_code !== 404) {
|
|
255
335
|
assertOk(signup, "start signup");
|
|
256
336
|
}
|
|
257
337
|
|
|
338
|
+
// Backends predating start-signup have no passwordless route in, so this one
|
|
339
|
+
// path still has to ask.
|
|
258
340
|
process.stdout.write(
|
|
259
341
|
"This PreMan backend uses the password-first signup flow. Create your password now, then enter the email code.\n"
|
|
260
342
|
);
|
|
261
|
-
const
|
|
343
|
+
const legacyPassword = password || (await promptPasswordTwice());
|
|
262
344
|
const legacySignup = await callBackendJson(args, "POST", "/auth/signup", {
|
|
263
|
-
json: { email, password },
|
|
345
|
+
json: { email, password: legacyPassword },
|
|
264
346
|
});
|
|
265
347
|
assertOk(legacySignup, "signup");
|
|
266
348
|
process.stdout.write("Verification code sent. Check your email.\n");
|
|
@@ -303,26 +385,31 @@ export async function authenticateTerminal(args) {
|
|
|
303
385
|
process.stdout.write("No PreMan account found. Creating one now.\n");
|
|
304
386
|
accessToken = await createAccountFromTerminal(args, email);
|
|
305
387
|
} else if (account.needs_password) {
|
|
306
|
-
|
|
388
|
+
// No password on this account, so a code is the only way in.
|
|
389
|
+
const password = await optionalPassword(args);
|
|
390
|
+
process.stdout.write("Sending a sign-in code to your email.\n");
|
|
307
391
|
const resend = await callBackendJson(args, "POST", "/auth/resend-otp", { json: { email } });
|
|
308
392
|
assertOk(resend, "resend OTP");
|
|
309
393
|
const otp = await promptText("Verification code: ");
|
|
310
|
-
|
|
311
|
-
const setPassword = await callBackendJson(args, "POST", "/auth/set-password", {
|
|
312
|
-
json: { email, otp, new_password: password },
|
|
313
|
-
});
|
|
314
|
-
assertOk(setPassword, "set password");
|
|
315
|
-
accessToken = String(setPassword.access_token || "");
|
|
394
|
+
accessToken = await completeWithCode(args, email, otp, password);
|
|
316
395
|
} else {
|
|
317
|
-
const password = await promptSecret("Password: ");
|
|
318
|
-
|
|
319
|
-
json: { email
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
396
|
+
const password = await promptSecret("Password (Enter to get a code by email): ");
|
|
397
|
+
if (!password) {
|
|
398
|
+
const resend = await callBackendJson(args, "POST", "/auth/resend-otp", { json: { email } });
|
|
399
|
+
assertOk(resend, "resend OTP");
|
|
400
|
+
process.stdout.write("Sign-in code sent. Check your email.\n");
|
|
401
|
+
const otp = await promptText("Verification code: ");
|
|
402
|
+
accessToken = await completeWithCode(args, email, otp, "");
|
|
323
403
|
} else {
|
|
324
|
-
|
|
325
|
-
|
|
404
|
+
const login = await callBackendJson(args, "POST", "/auth/login", {
|
|
405
|
+
json: { email, password },
|
|
406
|
+
});
|
|
407
|
+
if (!login.ok && login.status_code === 403 && String(login.detail || "").toLowerCase().includes("not verified")) {
|
|
408
|
+
accessToken = await verifyUnconfirmedAccount(args, email);
|
|
409
|
+
} else {
|
|
410
|
+
assertOk(login, "login");
|
|
411
|
+
accessToken = String(login.access_token || "");
|
|
412
|
+
}
|
|
326
413
|
}
|
|
327
414
|
}
|
|
328
415
|
|
package/dist/server.js
CHANGED
|
@@ -9,11 +9,7 @@ import path from "node:path";
|
|
|
9
9
|
import os from "node:os";
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
11
|
import { MCP_PREVIEW_RESOURCE_URI, RESOURCE_URI_META_KEY, buildConversionPanelHtml, writeMcpPreviewFile, } from "./mcp-preview-panel.js";
|
|
12
|
-
|
|
13
|
-
* Load ``PREMAN_*`` from repo JSON so project URLs win over stale **global** Cursor MCP env
|
|
14
|
-
* (which often sets the wrong ``PREMAN_BACKEND``, e.g. app port instead of API port).
|
|
15
|
-
* First match wins: ``.cursor/preman-mcp.config.json``, then ``preman-mcp.config.json`` (cwd).
|
|
16
|
-
*/
|
|
12
|
+
let REPO_CONFIG = null;
|
|
17
13
|
function applyRepoPremanConfig() {
|
|
18
14
|
const candidates = [
|
|
19
15
|
path.join(process.cwd(), ".cursor", "preman-mcp.config.json"),
|
|
@@ -24,13 +20,19 @@ function applyRepoPremanConfig() {
|
|
|
24
20
|
if (!existsSync(p))
|
|
25
21
|
continue;
|
|
26
22
|
const j = JSON.parse(readFileSync(p, "utf8"));
|
|
23
|
+
const override = j.PREMAN_CONFIG_OVERRIDE === true || j.PREMAN_CONFIG_OVERRIDE === "true";
|
|
24
|
+
const applied = [];
|
|
27
25
|
for (const [key, val] of Object.entries(j)) {
|
|
28
|
-
if (!key.startsWith("PREMAN_"))
|
|
26
|
+
if (!key.startsWith("PREMAN_") || key === "PREMAN_CONFIG_OVERRIDE")
|
|
29
27
|
continue;
|
|
30
|
-
if (typeof val
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
if (typeof val !== "string" || !val.trim())
|
|
29
|
+
continue;
|
|
30
|
+
if (!override && process.env[key])
|
|
31
|
+
continue;
|
|
32
|
+
process.env[key] = val.trim();
|
|
33
|
+
applied.push(key);
|
|
33
34
|
}
|
|
35
|
+
REPO_CONFIG = { path: p, override, applied };
|
|
34
36
|
return;
|
|
35
37
|
}
|
|
36
38
|
catch {
|
|
@@ -42,6 +44,22 @@ applyRepoPremanConfig();
|
|
|
42
44
|
const BACKEND_URL = process.env.PREMAN_BACKEND || "https://api.preman.live";
|
|
43
45
|
const FRONTEND_URL = process.env.PREMAN_FRONTEND || "https://app.preman.live";
|
|
44
46
|
let API_KEY = process.env.PREMAN_API_KEY || "";
|
|
47
|
+
/**
|
|
48
|
+
* Where the URLs this process is using came from.
|
|
49
|
+
*
|
|
50
|
+
* Reported by `preman_status` because the most confusing failure in setup is a
|
|
51
|
+
* server that works perfectly against the wrong backend: the key is fine, the
|
|
52
|
+
* config on disk is fine, and every call still 401s. Naming the file that
|
|
53
|
+
* redirected it turns that into a one-line diagnosis.
|
|
54
|
+
*/
|
|
55
|
+
function configSource() {
|
|
56
|
+
return {
|
|
57
|
+
backend_url: BACKEND_URL,
|
|
58
|
+
repo_config: REPO_CONFIG
|
|
59
|
+
? { path: REPO_CONFIG.path, override: REPO_CONFIG.override, applied: REPO_CONFIG.applied }
|
|
60
|
+
: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
45
63
|
function detectCodingAgent() {
|
|
46
64
|
const forced = (process.env.PREMAN_CODING_AGENT || "").trim().toLowerCase();
|
|
47
65
|
if (forced)
|
|
@@ -1040,6 +1058,7 @@ export function createServer() {
|
|
|
1040
1058
|
backend_url: BACKEND_URL,
|
|
1041
1059
|
frontend_base_url: FRONTEND_BASE,
|
|
1042
1060
|
endpoints_page_url: buildAgentDashboardUrl("/endpoints"),
|
|
1061
|
+
config: configSource(),
|
|
1043
1062
|
message: "Stored API key is no longer valid. Run `npm exec -y premanmcp@latest -- login`, or use user_auth_* then preman_create_api_key, or run preman_login.",
|
|
1044
1063
|
}),
|
|
1045
1064
|
}],
|
|
@@ -1056,6 +1075,7 @@ export function createServer() {
|
|
|
1056
1075
|
backend_url: BACKEND_URL,
|
|
1057
1076
|
frontend_base_url: FRONTEND_BASE,
|
|
1058
1077
|
endpoints_page_url: buildAgentDashboardUrl("/endpoints"),
|
|
1078
|
+
config: configSource(),
|
|
1059
1079
|
coding_agent: workbench,
|
|
1060
1080
|
_agent_hints: {
|
|
1061
1081
|
next_actions: [
|