premanmcp 0.9.0 → 0.10.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/bin/shared.js CHANGED
@@ -8,7 +8,15 @@
8
8
  */
9
9
 
10
10
  import { spawn, spawnSync } from "node:child_process";
11
- import { chmodSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
+ import {
12
+ chmodSync,
13
+ existsSync,
14
+ readFileSync,
15
+ realpathSync,
16
+ rmSync,
17
+ writeFileSync,
18
+ mkdirSync,
19
+ } from "node:fs";
12
20
  import os from "node:os";
13
21
  import path from "node:path";
14
22
  import { createInterface } from "node:readline/promises";
@@ -29,16 +37,55 @@ export const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
29
37
  */
30
38
  let _invocation = null;
31
39
 
32
- export function cliInvocation() {
33
- if (_invocation) return _invocation;
40
+ /**
41
+ * The package that owns the first `preman` on PATH, or "".
42
+ *
43
+ * `preman` is not ours by name alone: `preman-sdk` publishes a bin called
44
+ * exactly that, and whichever package lost the race still resolves. Assuming it
45
+ * is us writes hints and a git hook that call the other CLI — which answers
46
+ * "Unknown command: verify" and turns every push into a silent
47
+ * "[preman] checks skipped", the worst possible failure for a tool whose whole
48
+ * job is running checks on push.
49
+ */
50
+ export function pathPremanOwner() {
34
51
  const probe = process.platform === "win32" ? "where" : "which";
52
+ let resolved = "";
35
53
  try {
36
54
  const found = spawnSync(probe, ["preman"], { stdio: "pipe", encoding: "utf8" });
37
- _invocation = found.status === 0 && found.stdout.trim() ? "preman" : null;
55
+ if (found.status !== 0) return "";
56
+ resolved = String(found.stdout || "").split("\n")[0].trim();
57
+ } catch {
58
+ return "";
59
+ }
60
+ if (!resolved) return "";
61
+
62
+ // Walk up from the real file, not the symlink: a bin is a link into the
63
+ // package directory, and only that directory's manifest names the owner.
64
+ try {
65
+ let dir = path.dirname(realpathSync(resolved));
66
+ for (let depth = 0; depth < 5; depth += 1) {
67
+ const manifest = path.join(dir, "package.json");
68
+ if (existsSync(manifest)) {
69
+ return String(JSON.parse(readFileSync(manifest, "utf8")).name || "");
70
+ }
71
+ const parent = path.dirname(dir);
72
+ if (parent === dir) break;
73
+ dir = parent;
74
+ }
38
75
  } catch {
39
- _invocation = null;
76
+ return "";
40
77
  }
41
- return (_invocation ||= "npm exec -y premanmcp@latest --");
78
+ return "";
79
+ }
80
+
81
+ export function cliInvocation() {
82
+ return (_invocation ||=
83
+ pathPremanOwner() === "premanmcp" ? "preman" : "npm exec -y premanmcp@latest --");
84
+ }
85
+
86
+ /** Test seam: `cliInvocation` memoizes a PATH probe for the life of the process. */
87
+ export function resetCliInvocation() {
88
+ _invocation = null;
42
89
  }
43
90
 
44
91
  export function makeArgs(commandArgs = []) {
@@ -93,6 +140,18 @@ export function saveStoredCredentials(creds) {
93
140
  writeFileSync(CREDENTIALS_FILE, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
94
141
  }
95
142
 
143
+ /** Forget the stored key. Returns whether there was one. */
144
+ export function clearStoredCredentials() {
145
+ if (!existsSync(CREDENTIALS_FILE)) return false;
146
+ rmSync(CREDENTIALS_FILE, { force: true });
147
+ return true;
148
+ }
149
+
150
+ /** True when the key in play came from a flag or the environment, not our store. */
151
+ export function apiKeyIsExplicit(args) {
152
+ return Boolean(args.value("--api-key", "") || process.env.PREMAN_API_KEY);
153
+ }
154
+
96
155
  export function backendUrl(args) {
97
156
  return args.value("--backend", process.env.PREMAN_BACKEND || DEFAULT_BACKEND).replace(/\/+$/, "");
98
157
  }
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)
@@ -1025,6 +1043,7 @@ export function createServer() {
1025
1043
  backend_url: BACKEND_URL,
1026
1044
  frontend_base_url: FRONTEND_BASE,
1027
1045
  endpoints_page_url: buildAgentDashboardUrl("/endpoints"),
1046
+ config: configSource(),
1028
1047
  message: "Not authenticated. Run `npm exec -y premanmcp@latest -- login`, or use user_auth_* then preman_create_api_key, or run preman_login.",
1029
1048
  }),
1030
1049
  }],
@@ -1040,6 +1059,7 @@ export function createServer() {
1040
1059
  backend_url: BACKEND_URL,
1041
1060
  frontend_base_url: FRONTEND_BASE,
1042
1061
  endpoints_page_url: buildAgentDashboardUrl("/endpoints"),
1062
+ config: configSource(),
1043
1063
  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
1064
  }),
1045
1065
  }],
@@ -1056,6 +1076,7 @@ export function createServer() {
1056
1076
  backend_url: BACKEND_URL,
1057
1077
  frontend_base_url: FRONTEND_BASE,
1058
1078
  endpoints_page_url: buildAgentDashboardUrl("/endpoints"),
1079
+ config: configSource(),
1059
1080
  coding_agent: workbench,
1060
1081
  _agent_hints: {
1061
1082
  next_actions: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "premanmcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Turn APIs into agent-callable MCP tools with auth, testing, and audit logs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,9 +13,9 @@
13
13
  "dev": "tsx src/server.ts",
14
14
  "open-mcp-preview": "node scripts/emit-mcp-preview.mjs",
15
15
  "open-mcp-preview-in-cursor": "node scripts/open-cursor-preview.mjs",
16
- "test": "npm run test:connect && npm run test:node",
16
+ "test": "npm run build && npm run test:connect && npm run test:node",
17
17
  "test:connect": "node scripts/smoke-connect.mjs",
18
- "test:node": "node --test --test-timeout=30000 scripts/smoke-launcher-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.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"
18
+ "test:node": "node --test --test-timeout=30000 scripts/smoke-launcher-config.mjs scripts/smoke-runner.mjs scripts/smoke-repo-config.mjs scripts/smoke-onboard.mjs scripts/smoke-local-detect.mjs scripts/smoke-prepush-hook.mjs scripts/smoke-cli-identity.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"
19
19
  },
20
20
  "dependencies": {
21
21
  "@modelcontextprotocol/ext-apps": "^0.1.0",