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/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
- const password = await promptPasswordTwice();
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 password = await promptPasswordTwice();
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
- process.stdout.write("This account needs a password. Sending an OTP first.\n");
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
- const password = await promptPasswordTwice();
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
- const login = await callBackendJson(args, "POST", "/auth/login", {
319
- json: { email, password },
320
- });
321
- if (!login.ok && login.status_code === 403 && String(login.detail || "").toLowerCase().includes("not verified")) {
322
- accessToken = await verifyUnconfirmedAccount(args, email);
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
- assertOk(login, "login");
325
- accessToken = String(login.access_token || "");
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 === "string" && val.trim()) {
31
- process.env[key] = val.trim();
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: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
5
5
  "type": "module",
6
6
  "bin": {