metheus-governance-mcp-cli 0.2.10 → 0.2.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.
Files changed (2) hide show
  1. package/cli.mjs +216 -33
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -14,12 +14,17 @@ const DEFAULT_SITE_URL = "https://metheus.gesiaplatform.com";
14
14
  const DEFAULT_BASE_URL = `${DEFAULT_SITE_URL}/governance/mcp`;
15
15
  const DEFAULT_SERVER_NAME = "metheus-governance-mcp";
16
16
  const AUTH_STORE_RELATIVE_PATH = path.join(".metheus", "governance-mcp-auth.json");
17
+ const SELF_UPDATE_STATE_RELATIVE_PATH = path.join(".metheus", "governance-mcp-cli-update.json");
17
18
  const CTXPACK_CACHE_RELATIVE_DIR = path.join(".metheus", "ctxpack-cache");
18
19
  const CTXPACK_META_FILENAME = ".metheus_ctxpack_sync.json";
19
20
  const CTXPACK_PUSH_TOOL_NAMES = ["ctxpack.push", "ctxpack.update", "ctxpack.save"];
20
21
  const CLI_META = loadCLIMeta();
21
22
  const CLI_NAME = CLI_META.name || "metheus-governance-mcp-cli";
22
23
  const CLI_VERSION = CLI_META.version || "0.0.0";
24
+ const SELF_UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
25
+ const SELF_UPDATE_TIMEOUT_SECONDS = 6;
26
+ const SELF_UPDATE_ENV_KEY = "METHEUS_CLI_AUTO_UPDATE";
27
+ const SELF_UPDATE_GUARD_ENV_KEY = "METHEUS_CLI_SELF_UPDATE_ATTEMPTED";
23
28
  const AUTO_CTXPACK_SYNC_INTERVAL_MS = 60 * 1000;
24
29
  const autoCtxpackSyncTracker = new Map();
25
30
 
@@ -44,6 +49,7 @@ function printUsage() {
44
49
  "",
45
50
  "Environment:",
46
51
  " METHEUS_TOKEN or MCP_AUTH_TOKEN is used first for proxy requests.",
52
+ ` ${SELF_UPDATE_ENV_KEY}=0 to disable startup auto-update check.`,
47
53
  " If env is missing, stored token file is used:",
48
54
  ` ${AUTH_STORE_RELATIVE_PATH}`,
49
55
  "",
@@ -69,6 +75,176 @@ function printVersion() {
69
75
  process.stdout.write(`${CLI_NAME} ${CLI_VERSION}\n`);
70
76
  }
71
77
 
78
+ function resolveHomeFilePath(relativePath) {
79
+ const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
80
+ if (!home) {
81
+ return path.resolve(process.cwd(), relativePath);
82
+ }
83
+ return path.join(home, relativePath);
84
+ }
85
+
86
+ function updateStateFilePath() {
87
+ return resolveHomeFilePath(SELF_UPDATE_STATE_RELATIVE_PATH);
88
+ }
89
+
90
+ function loadSelfUpdateState() {
91
+ const filePath = updateStateFilePath();
92
+ try {
93
+ const raw = fs.readFileSync(filePath, "utf8");
94
+ const parsed = JSON.parse(raw);
95
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
96
+ return { filePath, checkedAt: "", latestVersion: "", installedVersion: "", updatedAt: "" };
97
+ }
98
+ return {
99
+ filePath,
100
+ checkedAt: String(parsed.checked_at || "").trim(),
101
+ latestVersion: String(parsed.latest_version || "").trim(),
102
+ installedVersion: String(parsed.installed_version || "").trim(),
103
+ updatedAt: String(parsed.updated_at || "").trim(),
104
+ };
105
+ } catch {
106
+ return { filePath, checkedAt: "", latestVersion: "", installedVersion: "", updatedAt: "" };
107
+ }
108
+ }
109
+
110
+ function saveSelfUpdateState(nextState) {
111
+ const filePath = updateStateFilePath();
112
+ const payload = {
113
+ package_name: CLI_NAME,
114
+ checked_at: String(nextState?.checkedAt || "").trim() || new Date().toISOString(),
115
+ latest_version: String(nextState?.latestVersion || "").trim(),
116
+ installed_version: String(nextState?.installedVersion || "").trim(),
117
+ updated_at: String(nextState?.updatedAt || "").trim(),
118
+ };
119
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
120
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
121
+ }
122
+
123
+ function parseSemver(rawVersion) {
124
+ const match = String(rawVersion || "")
125
+ .trim()
126
+ .match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
127
+ if (!match) return null;
128
+ return {
129
+ major: Number.parseInt(match[1], 10),
130
+ minor: Number.parseInt(match[2], 10),
131
+ patch: Number.parseInt(match[3], 10),
132
+ prerelease: String(match[4] || "").trim(),
133
+ };
134
+ }
135
+
136
+ function compareSemver(leftRaw, rightRaw) {
137
+ const left = parseSemver(leftRaw);
138
+ const right = parseSemver(rightRaw);
139
+ if (!left || !right) {
140
+ return String(leftRaw || "").localeCompare(String(rightRaw || ""));
141
+ }
142
+ if (left.major !== right.major) return left.major - right.major;
143
+ if (left.minor !== right.minor) return left.minor - right.minor;
144
+ if (left.patch !== right.patch) return left.patch - right.patch;
145
+ const leftPre = left.prerelease;
146
+ const rightPre = right.prerelease;
147
+ if (leftPre === rightPre) return 0;
148
+ if (!leftPre) return 1;
149
+ if (!rightPre) return -1;
150
+ return leftPre.localeCompare(rightPre);
151
+ }
152
+
153
+ function hasNoUpdateFlag(argv) {
154
+ return argv.some((arg) => String(arg || "").trim().toLowerCase() === "--no-update");
155
+ }
156
+
157
+ function parseIsoMs(raw) {
158
+ const parsed = Date.parse(String(raw || "").trim());
159
+ if (!Number.isFinite(parsed) || parsed <= 0) return 0;
160
+ return parsed;
161
+ }
162
+
163
+ async function fetchLatestCLIVersion(timeoutSeconds = SELF_UPDATE_TIMEOUT_SECONDS) {
164
+ const url = `https://registry.npmjs.org/${encodeURIComponent(CLI_NAME)}/latest`;
165
+ const body = await getJSON(url, timeoutSeconds);
166
+ return String(body?.version || "").trim();
167
+ }
168
+
169
+ function applyRerunExitResult(runResult) {
170
+ if (runResult?.error) {
171
+ process.stderr.write(`[${CLI_NAME}] restart failed: ${String(runResult.error.message || runResult.error)}\n`);
172
+ return;
173
+ }
174
+ if (typeof runResult?.status === "number") {
175
+ process.exit(runResult.status);
176
+ return;
177
+ }
178
+ if (runResult?.signal) {
179
+ process.kill(process.pid, runResult.signal);
180
+ return;
181
+ }
182
+ process.exit(0);
183
+ }
184
+
185
+ async function maybeAutoUpdate(commandToken, argv) {
186
+ const command = String(commandToken || "").trim().toLowerCase();
187
+ if (command === "proxy") return;
188
+ if (command === "-v" || command === "--version" || command === "version") return;
189
+ if (command === "-h" || command === "--help") return;
190
+ if (String(process.env[SELF_UPDATE_GUARD_ENV_KEY] || "").trim() === "1") return;
191
+ if (hasNoUpdateFlag(argv)) return;
192
+ if (!boolFromRaw(process.env[SELF_UPDATE_ENV_KEY], true)) return;
193
+
194
+ const nowIso = new Date().toISOString();
195
+ const cached = loadSelfUpdateState();
196
+ let latestVersion = "";
197
+ const checkedMs = parseIsoMs(cached.checkedAt);
198
+ if (checkedMs > 0 && Date.now() - checkedMs < SELF_UPDATE_CHECK_INTERVAL_MS) {
199
+ latestVersion = cached.latestVersion;
200
+ }
201
+ if (!latestVersion) {
202
+ try {
203
+ latestVersion = await fetchLatestCLIVersion(SELF_UPDATE_TIMEOUT_SECONDS);
204
+ } catch {
205
+ latestVersion = "";
206
+ }
207
+ saveSelfUpdateState({
208
+ checkedAt: nowIso,
209
+ latestVersion,
210
+ installedVersion: CLI_VERSION,
211
+ updatedAt: cached.updatedAt,
212
+ });
213
+ }
214
+
215
+ if (!latestVersion || compareSemver(latestVersion, CLI_VERSION) <= 0) {
216
+ return;
217
+ }
218
+
219
+ process.stderr.write(`[${CLI_NAME}] update available (${CLI_VERSION} -> ${latestVersion}). Installing...\n`);
220
+ const run = runCLICommand("npm", ["install", "-g", `${CLI_NAME}@latest`], {
221
+ stdio: "inherit",
222
+ });
223
+ if (run.status !== 0) {
224
+ process.stderr.write(`[${CLI_NAME}] auto-update failed. Continue with current version ${CLI_VERSION}.\n`);
225
+ return;
226
+ }
227
+
228
+ const updatedAt = new Date().toISOString();
229
+ saveSelfUpdateState({
230
+ checkedAt: updatedAt,
231
+ latestVersion,
232
+ installedVersion: latestVersion,
233
+ updatedAt,
234
+ });
235
+ process.stderr.write(`[${CLI_NAME}] updated to ${latestVersion}. Restarting...\n`);
236
+
237
+ const selfPath = fileURLToPath(import.meta.url);
238
+ const rerun = spawnSync(process.execPath, [selfPath, ...argv], {
239
+ stdio: "inherit",
240
+ env: {
241
+ ...process.env,
242
+ [SELF_UPDATE_GUARD_ENV_KEY]: "1",
243
+ },
244
+ });
245
+ applyRerunExitResult(rerun);
246
+ }
247
+
72
248
  function parseArgs(argv) {
73
249
  const out = {};
74
250
  for (let i = 0; i < argv.length; i += 1) {
@@ -98,11 +274,7 @@ function normalizeToken(raw) {
98
274
  }
99
275
 
100
276
  function authStoreFilePath() {
101
- const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
102
- if (!home) {
103
- return path.resolve(process.cwd(), AUTH_STORE_RELATIVE_PATH);
104
- }
105
- return path.join(home, AUTH_STORE_RELATIVE_PATH);
277
+ return resolveHomeFilePath(AUTH_STORE_RELATIVE_PATH);
106
278
  }
107
279
 
108
280
  function resolveWorkspaceDir(rawPath) {
@@ -140,12 +312,33 @@ function firstNonEmptyString(values) {
140
312
  return "";
141
313
  }
142
314
 
315
+ function isEditorInstallDirectory(candidatePath) {
316
+ const normalized = String(candidatePath || "").replace(/\//g, "\\").toLowerCase();
317
+ if (!normalized) return false;
318
+ // Guard against runtime cwd/env resolving to editor installation directory
319
+ // instead of the actual opened workspace.
320
+ if (normalized.includes("\\appdata\\local\\programs\\microsoft vs code")) return true;
321
+ if (normalized.includes("\\program files\\microsoft vs code")) return true;
322
+ if (normalized.includes("\\program files (x86)\\microsoft vs code")) return true;
323
+ return false;
324
+ }
325
+
326
+ function sanitizeWorkspaceCandidate(rawCandidate) {
327
+ const fileCandidate = fileURIToLocalPath(rawCandidate);
328
+ const candidate = firstNonEmptyString([fileCandidate, rawCandidate]);
329
+ if (!candidate) return "";
330
+ const resolved = resolveWorkspaceDir(candidate);
331
+ if (!resolved) return "";
332
+ if (isEditorInstallDirectory(resolved)) return "";
333
+ return resolved;
334
+ }
335
+
143
336
  function extractWorkspaceCandidateFromFolders(rawFolders) {
144
337
  if (!Array.isArray(rawFolders)) return "";
145
338
  for (const folder of rawFolders) {
146
339
  if (typeof folder === "string") {
147
- const direct = firstNonEmptyString([fileURIToLocalPath(folder), folder]);
148
- if (direct) return resolveWorkspaceDir(direct);
340
+ const direct = sanitizeWorkspaceCandidate(folder);
341
+ if (direct) return direct;
149
342
  continue;
150
343
  }
151
344
  if (!folder || typeof folder !== "object" || Array.isArray(folder)) continue;
@@ -157,8 +350,8 @@ function extractWorkspaceCandidateFromFolders(rawFolders) {
157
350
  folder.root_path,
158
351
  folder.rootPath,
159
352
  ]);
160
- const candidate = firstNonEmptyString([fileURIToLocalPath(uriValue), uriValue]);
161
- if (candidate) return resolveWorkspaceDir(candidate);
353
+ const candidate = sanitizeWorkspaceCandidate(uriValue);
354
+ if (candidate) return candidate;
162
355
  }
163
356
  return "";
164
357
  }
@@ -192,10 +385,7 @@ function extractWorkspaceCandidateFromRequest(requestObj, toolArgs) {
192
385
  meta.root_uri,
193
386
  meta.rootUri,
194
387
  ]);
195
- const fileCandidate = fileURIToLocalPath(rawCandidate);
196
- const candidate = firstNonEmptyString([fileCandidate, rawCandidate]);
197
- if (!candidate) return "";
198
- return resolveWorkspaceDir(candidate);
388
+ return sanitizeWorkspaceCandidate(rawCandidate);
199
389
  }
200
390
 
201
391
  function extractWorkspaceCandidateFromEnv() {
@@ -208,26 +398,22 @@ function extractWorkspaceCandidateFromEnv() {
208
398
  process.env.WORKSPACE_DIR,
209
399
  process.env.WORKSPACE_FOLDER,
210
400
  process.env.VSCODE_WORKSPACE_FOLDER,
211
- process.env.VSCODE_CWD,
212
401
  process.env.PWD,
213
402
  process.env.INIT_CWD,
403
+ process.env.VSCODE_CWD,
214
404
  ]);
215
- const fileCandidate = fileURIToLocalPath(rawCandidate);
216
- const candidate = firstNonEmptyString([fileCandidate, rawCandidate]);
217
- if (!candidate) return "";
218
- return resolveWorkspaceDir(candidate);
405
+ return sanitizeWorkspaceCandidate(rawCandidate);
219
406
  }
220
407
 
221
408
  function resolveWorkspaceDirForRequest(defaultWorkspaceDir, requestObj, toolArgs) {
222
409
  const requestCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
223
410
  const envCandidate = extractWorkspaceCandidateFromEnv();
224
- const candidate = firstNonEmptyString([
225
- requestCandidate,
226
- envCandidate,
227
- defaultWorkspaceDir,
228
- process.cwd(),
229
- ]);
230
- return resolveWorkspaceDir(candidate);
411
+ const homeCandidate = firstNonEmptyString([process.env.USERPROFILE, process.env.HOME]);
412
+ for (const rawCandidate of [requestCandidate, envCandidate, defaultWorkspaceDir, process.cwd(), homeCandidate]) {
413
+ const resolved = sanitizeWorkspaceCandidate(rawCandidate);
414
+ if (resolved) return resolved;
415
+ }
416
+ return resolveWorkspaceDir(process.cwd());
231
417
  }
232
418
 
233
419
  function resolveProjectIDForRequest({
@@ -3508,14 +3694,10 @@ async function runProxy(flags) {
3508
3694
  } else if (envWorkspaceCandidate) {
3509
3695
  sessionWorkspaceDir = envWorkspaceCandidate;
3510
3696
  }
3511
- const requestWorkspaceDir = resolveWorkspaceDir(
3512
- firstNonEmptyString([
3513
- requestWorkspaceCandidate,
3514
- envWorkspaceCandidate,
3515
- sessionWorkspaceDir,
3516
- args.workspaceDir,
3517
- process.cwd(),
3518
- ]),
3697
+ const requestWorkspaceDir = resolveWorkspaceDirForRequest(
3698
+ firstNonEmptyString([sessionWorkspaceDir, args.workspaceDir, process.cwd()]),
3699
+ requestObj,
3700
+ toolArgs,
3519
3701
  );
3520
3702
  let autoSyncSummary = null;
3521
3703
  if (isJsonRpcMethod(requestObj, "tools/call")) {
@@ -3913,6 +4095,7 @@ async function runBootstrap(flags) {
3913
4095
  async function main() {
3914
4096
  const [, , rawCommand, ...rest] = process.argv;
3915
4097
  const command = String(rawCommand || "");
4098
+ await maybeAutoUpdate(command, process.argv.slice(2));
3916
4099
 
3917
4100
  if (command === "-v" || command === "--version" || command === "version") {
3918
4101
  printVersion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [