metheus-governance-mcp-cli 0.2.37 → 0.2.38

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 (3) hide show
  1. package/README.md +28 -0
  2. package/cli.mjs +138 -3
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -103,6 +103,11 @@ Antigravity note:
103
103
  Cursor note:
104
104
  - this CLI manages MCP registration via Cursor global MCP config (`~/.cursor/mcp.json`).
105
105
 
106
+ Tool naming compatibility:
107
+ - Claude/Codex/Gemini keep canonical MCP tool names (`project.summary`, `ctxpack.merge.brief`, ...).
108
+ - Cursor/Antigravity sessions may receive safe aliases (`project_summary`, `ctxpack_merge_brief`, ...).
109
+ - Proxy maps alias calls back to canonical names automatically.
110
+
106
111
  Local bootstrap tools exposed by proxy:
107
112
 
108
113
  - `project.summary`
@@ -239,6 +244,7 @@ npm publish --access public
239
244
 
240
245
  ```bash
241
246
  npm run check
247
+ npm run test:compat
242
248
  npm run publish:dry
243
249
  ```
244
250
 
@@ -255,3 +261,25 @@ If npm account uses 2FA, pass OTP:
255
261
  ```bash
256
262
  node release.mjs --otp <6-digit-code>
257
263
  ```
264
+
265
+ ## Regression and smoke checks
266
+
267
+ Local compatibility selftest (no network):
268
+
269
+ ```bash
270
+ npm run test:compat
271
+ ```
272
+
273
+ Proxy smoke test (initialize -> tools/list -> project summary -> ctxpack local sync):
274
+
275
+ ```bash
276
+ npm run smoke:proxy -- --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --workspace-dir <workspace_path>
277
+ ```
278
+
279
+ Optional:
280
+ - `--client cursor-vscode` (default) or `--client antigravity`
281
+ - `--base-url https://metheus.gesiaplatform.com/governance/mcp`
282
+
283
+ Workspace fallback guardrail:
284
+ - If `METHEUS_WORKSPACE_DIR` is accidentally set to a non-existing boolean suffix path like `C:\code_test\true`,
285
+ proxy now recovers to the parent workspace (`C:\code_test`) instead of pinning to the bad path.
package/cli.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import fs from "node:fs";
4
+ import os from "node:os";
4
5
  import path from "node:path";
5
6
  import process from "node:process";
6
7
  import readline from "node:readline";
@@ -42,6 +43,7 @@ function printUsage() {
42
43
  ` ${cmd} setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--workspace-fallback-dir <path>] [--name <server_name>]`,
43
44
  ` ${cmd} doctor [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--timeout-seconds <n>]`,
44
45
  ` ${cmd} proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--include-drafts <true|false>] [--auto-pull-on-conflict <true|false>] [--timeout-seconds <n>]`,
46
+ ` ${cmd} selftest [--json <true|false>]`,
45
47
  ` ${cmd} ctxpack pull [--project-id <uuid>] [--base-url <url>] [--workspace-dir <path|auto>] [--paths <csv>] [--timeout-seconds <n>]`,
46
48
  ` ${cmd} auth status`,
47
49
  ` ${cmd} auth login [--base-url <url>] [--flow <auto|device|callback|manual>] [--keycloak-url <url>] [--realm <name>] [--client-id <id>] [--open-browser <true|false>] [--callback-port <n>] [--timeout-seconds <n>] [--manual <true|false>]`,
@@ -604,7 +606,7 @@ function extractWeakWorkspaceCandidateFromRequest(requestObj) {
604
606
  }
605
607
 
606
608
  function extractStrongWorkspaceCandidateFromEnv() {
607
- const rawCandidate = firstNonEmptyString([
609
+ const rawCandidates = [
608
610
  process.env.METHEUS_WORKSPACE_DIR,
609
611
  process.env.METHEUS_WORKSPACE_URI,
610
612
  process.env.CODEX_WORKSPACE_DIR,
@@ -612,8 +614,12 @@ function extractStrongWorkspaceCandidateFromEnv() {
612
614
  process.env.CLAUDE_WORKSPACE_DIR,
613
615
  process.env.CLAUDE_WORKSPACE_URI,
614
616
  process.env.CLAUDE_PROJECT_DIR,
615
- ]);
616
- return sanitizeWorkspaceCandidate(rawCandidate);
617
+ ];
618
+ for (const rawCandidate of rawCandidates) {
619
+ const normalized = sanitizeWorkspaceFallbackEnvCandidate(rawCandidate);
620
+ if (normalized) return normalized;
621
+ }
622
+ return "";
617
623
  }
618
624
 
619
625
  function extractWorkspaceCandidateFromEnv() {
@@ -635,6 +641,35 @@ function extractWeakWorkspaceCandidateFromEnv() {
635
641
  return sanitizeWorkspaceCandidate(rawCandidate);
636
642
  }
637
643
 
644
+ function sanitizeWorkspaceFallbackEnvCandidate(rawCandidate) {
645
+ const input = String(rawCandidate || "").trim();
646
+ if (!input) return "";
647
+
648
+ const direct = sanitizeWorkspaceCandidate(input);
649
+ if (direct && fs.existsSync(direct)) {
650
+ return direct;
651
+ }
652
+
653
+ const fileCandidate = fileURIToLocalPath(input);
654
+ const candidate = firstNonEmptyString([fileCandidate, input]);
655
+ if (!candidate) return direct;
656
+
657
+ const resolved = resolveWorkspaceDir(candidate);
658
+ if (!resolved || isEditorInstallDirectory(resolved)) {
659
+ return direct;
660
+ }
661
+
662
+ const leaf = path.basename(resolved).trim().toLowerCase();
663
+ if ((leaf === "true" || leaf === "false") && !fs.existsSync(resolved)) {
664
+ const parent = sanitizeWorkspaceCandidate(path.dirname(resolved));
665
+ if (parent && fs.existsSync(parent)) {
666
+ return parent;
667
+ }
668
+ }
669
+
670
+ return direct;
671
+ }
672
+
638
673
  function resolveWorkspaceDirForRequest(defaultWorkspaceDir, requestObj, toolArgs) {
639
674
  const strongRequestCandidate = extractStrongWorkspaceCandidateFromRequest(requestObj, toolArgs);
640
675
  const strongEnvCandidate = extractStrongWorkspaceCandidateFromEnv();
@@ -5099,6 +5134,102 @@ function runSetup(flags) {
5099
5134
  runSetupInternal(flags, { ensureOnly: false });
5100
5135
  }
5101
5136
 
5137
+ function runSelftest(flags = {}) {
5138
+ const jsonMode = boolFromRaw(flags.json, false);
5139
+ const checks = [];
5140
+ const push = (name, ok, detail = "") => checks.push({ name, ok: Boolean(ok), detail: String(detail || "") });
5141
+
5142
+ const aliasMaps = buildToolAliasMaps([
5143
+ { name: "project.summary" },
5144
+ { name: "ctxpack.merge.brief" },
5145
+ { name: "workitem.list" },
5146
+ ]);
5147
+ const summaryAlias = String(aliasMaps.canonicalToAlias.get("project.summary") || "");
5148
+ const roundTrip = rewriteAliasedToolCallToCanonical(
5149
+ {
5150
+ jsonrpc: "2.0",
5151
+ id: 1,
5152
+ method: "tools/call",
5153
+ params: {
5154
+ name: summaryAlias,
5155
+ arguments: {},
5156
+ },
5157
+ },
5158
+ aliasMaps.aliasToCanonical,
5159
+ );
5160
+ const roundTripOk = summaryAlias === "project_summary" && String(roundTrip?.params?.name || "") === "project.summary";
5161
+ push(
5162
+ "alias_roundtrip_project_summary",
5163
+ roundTripOk,
5164
+ `alias=${summaryAlias || "(none)"} canonical=${String(roundTrip?.params?.name || "(none)")}`,
5165
+ );
5166
+
5167
+ const collisionMaps = buildToolAliasMaps([{ name: "a.b" }, { name: "a_b" }]);
5168
+ const aliasDot = String(collisionMaps.canonicalToAlias.get("a.b") || "");
5169
+ const aliasUnderscore = String(collisionMaps.canonicalToAlias.get("a_b") || "");
5170
+ const collisionOk = aliasDot === "a_b_2" && aliasUnderscore === "";
5171
+ push(
5172
+ "alias_collision_safe_suffix",
5173
+ collisionOk,
5174
+ `a.b->${aliasDot || "(none)"} a_b->${aliasUnderscore || "(none)"}`,
5175
+ );
5176
+
5177
+ let tempRoot = "";
5178
+ try {
5179
+ tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-selftest-"));
5180
+ const parent = path.join(tempRoot, "workspace");
5181
+ fs.mkdirSync(parent, { recursive: true });
5182
+ const suspicious = path.join(parent, "true");
5183
+ const recovered = sanitizeWorkspaceFallbackEnvCandidate(suspicious);
5184
+ const recoveredOk = normalizedPathForCompare(recovered) === normalizedPathForCompare(parent);
5185
+ push(
5186
+ "workspace_env_true_suffix_recovery",
5187
+ recoveredOk,
5188
+ `input=${suspicious} recovered=${recovered || "(none)"}`,
5189
+ );
5190
+
5191
+ fs.mkdirSync(suspicious, { recursive: true });
5192
+ const keptExisting = sanitizeWorkspaceFallbackEnvCandidate(suspicious);
5193
+ const keptExistingOk = normalizedPathForCompare(keptExisting) === normalizedPathForCompare(suspicious);
5194
+ push(
5195
+ "workspace_env_existing_true_dir_kept",
5196
+ keptExistingOk,
5197
+ `input=${suspicious} recovered=${keptExisting || "(none)"}`,
5198
+ );
5199
+ } catch (err) {
5200
+ push("workspace_env_guard_test_setup", false, String(err?.message || err));
5201
+ } finally {
5202
+ if (tempRoot) {
5203
+ try {
5204
+ fs.rmSync(tempRoot, { recursive: true, force: true });
5205
+ } catch {
5206
+ // ignore selftest cleanup error
5207
+ }
5208
+ }
5209
+ }
5210
+
5211
+ const failed = checks.filter((row) => !row.ok);
5212
+ const payload = {
5213
+ ok: failed.length === 0,
5214
+ pass_count: checks.length - failed.length,
5215
+ fail_count: failed.length,
5216
+ checks,
5217
+ };
5218
+
5219
+ if (jsonMode) {
5220
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
5221
+ } else {
5222
+ process.stdout.write(`Selftest: ${payload.ok ? "PASS" : "FAIL"} (${payload.pass_count}/${checks.length})\n`);
5223
+ for (const row of checks) {
5224
+ process.stdout.write(`- ${row.ok ? "PASS" : "FAIL"} ${row.name}${row.detail ? ` :: ${row.detail}` : ""}\n`);
5225
+ }
5226
+ }
5227
+
5228
+ if (!payload.ok) {
5229
+ process.exitCode = 1;
5230
+ }
5231
+ }
5232
+
5102
5233
  async function runBootstrap(flags) {
5103
5234
  process.stdout.write("Bootstrap start.\n");
5104
5235
  let resolved = resolveCurrentAccessToken();
@@ -5159,6 +5290,10 @@ async function main() {
5159
5290
  await runDoctor(flags);
5160
5291
  return;
5161
5292
  }
5293
+ if (command === "selftest") {
5294
+ runSelftest(flags);
5295
+ return;
5296
+ }
5162
5297
  if (command === "auth") {
5163
5298
  await runAuth(rest);
5164
5299
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.37",
3
+ "version": "0.2.38",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [
@@ -11,6 +11,8 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "check": "node --check cli.mjs && node --check release.mjs",
14
+ "test:compat": "node cli.mjs selftest --json",
15
+ "smoke:proxy": "node scripts/smoke-proxy.mjs",
14
16
  "pack:dry": "npm pack --dry-run",
15
17
  "publish:dry": "node release.mjs --dry-run",
16
18
  "publish:public": "node release.mjs"