metheus-governance-mcp-cli 0.2.4 → 0.2.6

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 +138 -24
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -26,12 +26,12 @@ function printUsage() {
26
26
  "Metheus Governance MCP CLI",
27
27
  "",
28
28
  "Usage:",
29
- " metheus-governance-mcp [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
29
+ " metheus-governance-mcp [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--flow <auto|device|callback|manual>]",
30
30
  " metheus-governance-mcp init [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
31
- " metheus-governance-mcp setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--name <server_name>]",
31
+ " metheus-governance-mcp setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--name <server_name>]",
32
32
  " metheus-governance-mcp doctor [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--timeout-seconds <n>]",
33
- " metheus-governance-mcp proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--include-drafts <true|false>] [--timeout-seconds <n>]",
34
- " metheus-governance-mcp ctxpack pull [--project-id <uuid>] [--base-url <url>] [--timeout-seconds <n>]",
33
+ " metheus-governance-mcp proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--include-drafts <true|false>] [--timeout-seconds <n>]",
34
+ " metheus-governance-mcp ctxpack pull [--project-id <uuid>] [--base-url <url>] [--workspace-dir <path>] [--timeout-seconds <n>]",
35
35
  " metheus-governance-mcp auth status",
36
36
  " metheus-governance-mcp 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>]",
37
37
  " metheus-governance-mcp auth set --token <jwt> [--refresh-token <token>] [--base-url <url>]",
@@ -101,7 +101,15 @@ function authStoreFilePath() {
101
101
  return path.join(home, AUTH_STORE_RELATIVE_PATH);
102
102
  }
103
103
 
104
- function ctxpackCacheRootDir() {
104
+ function resolveWorkspaceDir(rawPath) {
105
+ const input = String(rawPath || "").trim();
106
+ if (input) {
107
+ return path.resolve(input);
108
+ }
109
+ return path.resolve(process.cwd());
110
+ }
111
+
112
+ function homeCtxpackCacheRootDir() {
105
113
  const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
106
114
  if (!home) {
107
115
  return path.resolve(process.cwd(), CTXPACK_CACHE_RELATIVE_DIR);
@@ -109,6 +117,14 @@ function ctxpackCacheRootDir() {
109
117
  return path.join(home, CTXPACK_CACHE_RELATIVE_DIR);
110
118
  }
111
119
 
120
+ function ctxpackCacheRootDir(workspaceDir) {
121
+ const root = resolveWorkspaceDir(workspaceDir);
122
+ if (!root) {
123
+ return homeCtxpackCacheRootDir();
124
+ }
125
+ return path.join(root, CTXPACK_CACHE_RELATIVE_DIR);
126
+ }
127
+
112
128
  function ensureParentDir(filePath) {
113
129
  const parent = path.dirname(filePath);
114
130
  fs.mkdirSync(parent, { recursive: true });
@@ -1241,6 +1257,7 @@ async function resolveAccessTokenForCommand(baseURL, timeoutSeconds) {
1241
1257
  async function runCtxpackPull(flags) {
1242
1258
  const workspaceMeta = loadWorkspaceMeta(process.cwd());
1243
1259
  const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
1260
+ const workspaceDir = resolveWorkspaceDir(flags["workspace-dir"] || process.cwd());
1244
1261
  if (!projectID) {
1245
1262
  process.stderr.write("Missing project_id. Use --project-id <uuid> or run inside a synced ctxpack workspace.\n");
1246
1263
  process.exitCode = 1;
@@ -1271,6 +1288,7 @@ async function runCtxpackPull(flags) {
1271
1288
  timeoutSeconds,
1272
1289
  includeCtxpack: true,
1273
1290
  syncCtxpackLocal: true,
1291
+ workspaceDir,
1274
1292
  });
1275
1293
  process.stdout.write(`${buildProjectSummaryText(summary)}\n`);
1276
1294
 
@@ -1457,6 +1475,7 @@ async function runDoctor(flags) {
1457
1475
  timeoutSeconds,
1458
1476
  includeCtxpack: true,
1459
1477
  syncCtxpackLocal: true,
1478
+ workspaceDir: context.workspaceDir,
1460
1479
  });
1461
1480
  if (String(summary.access || "") !== "granted") {
1462
1481
  addDoctorCheck(
@@ -1832,19 +1851,22 @@ function hasAllCtxpackFiles(baseDir, files) {
1832
1851
  return true;
1833
1852
  }
1834
1853
 
1835
- function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack }) {
1854
+ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack, workspaceDir }) {
1836
1855
  const ctxpackID = String(ctxpack?.ctxpack_id || "").trim();
1837
1856
  const version = String(ctxpack?.version || "").trim();
1838
1857
  const status = String(ctxpack?.status || "").trim() || "draft";
1839
1858
  const files = normalizeCtxpackFiles(ctxpack?.files);
1840
- const cacheDir = path.join(ctxpackCacheRootDir(), projectID);
1859
+ const resolvedWorkspaceDir = resolveWorkspaceDir(workspaceDir);
1860
+ const cacheDir = path.join(ctxpackCacheRootDir(resolvedWorkspaceDir), projectID);
1841
1861
  const metaPath = path.join(cacheDir, CTXPACK_META_FILENAME);
1862
+ const workspaceMetaPath = path.join(resolvedWorkspaceDir, CTXPACK_META_FILENAME);
1842
1863
 
1843
1864
  if (!ctxpackID || !version || files.length === 0) {
1844
1865
  return {
1845
1866
  sync_status: "not_available",
1846
1867
  sync_message: "Ctxpack is missing or has no files on server.",
1847
1868
  local_path: cacheDir,
1869
+ workspace_path: resolvedWorkspaceDir,
1848
1870
  local_file_count: 0,
1849
1871
  };
1850
1872
  }
@@ -1854,12 +1876,28 @@ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack }) {
1854
1876
  const previousSig = previousMeta
1855
1877
  ? `${String(previousMeta.ctxpack_id || "").trim()}|${String(previousMeta.version || "").trim()}|${String(previousMeta.status || "").trim()}|${Number.parseInt(String(previousMeta.files_count || 0), 10) || 0}`
1856
1878
  : "";
1879
+ const payload = {
1880
+ project_id: projectID,
1881
+ ctxpack_id: ctxpackID,
1882
+ version,
1883
+ status,
1884
+ files_count: files.length,
1885
+ files: files.map((f) => ({ path: f.path, doc_type: f.docType || "" })),
1886
+ source: `${String(siteBaseURL || "").replace(/\/+$/, "")}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack`,
1887
+ synced_at: new Date().toISOString(),
1888
+ };
1857
1889
 
1858
1890
  if (previousSig === remoteSig && hasAllCtxpackFiles(cacheDir, files)) {
1891
+ try {
1892
+ fs.writeFileSync(workspaceMetaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1893
+ } catch {
1894
+ // Best-effort metadata refresh for workspace-root auto-detection.
1895
+ }
1859
1896
  return {
1860
1897
  sync_status: "current",
1861
1898
  sync_message: "Local ctxpack cache is up to date.",
1862
1899
  local_path: cacheDir,
1900
+ workspace_path: resolvedWorkspaceDir,
1863
1901
  local_file_count: files.length,
1864
1902
  };
1865
1903
  }
@@ -1874,17 +1912,8 @@ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack }) {
1874
1912
  fs.writeFileSync(target, file.content, "utf8");
1875
1913
  }
1876
1914
 
1877
- const payload = {
1878
- project_id: projectID,
1879
- ctxpack_id: ctxpackID,
1880
- version,
1881
- status,
1882
- files_count: files.length,
1883
- files: files.map((f) => ({ path: f.path, doc_type: f.docType || "" })),
1884
- source: `${String(siteBaseURL || "").replace(/\/+$/, "")}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack`,
1885
- synced_at: new Date().toISOString(),
1886
- };
1887
1915
  fs.writeFileSync(metaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1916
+ fs.writeFileSync(workspaceMetaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1888
1917
 
1889
1918
  return {
1890
1919
  sync_status: previousMeta ? "updated" : "downloaded",
@@ -1892,6 +1921,7 @@ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack }) {
1892
1921
  ? "Ctxpack updated to latest server version."
1893
1922
  : "Ctxpack downloaded to local cache.",
1894
1923
  local_path: cacheDir,
1924
+ workspace_path: resolvedWorkspaceDir,
1895
1925
  local_file_count: files.length,
1896
1926
  };
1897
1927
  } catch (err) {
@@ -1899,6 +1929,7 @@ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack }) {
1899
1929
  sync_status: "error",
1900
1930
  sync_message: String(err?.message || err),
1901
1931
  local_path: cacheDir,
1932
+ workspace_path: resolvedWorkspaceDir,
1902
1933
  local_file_count: 0,
1903
1934
  };
1904
1935
  }
@@ -1911,6 +1942,7 @@ async function loadProjectSummaryForTool({
1911
1942
  timeoutSeconds,
1912
1943
  includeCtxpack,
1913
1944
  syncCtxpackLocal,
1945
+ workspaceDir,
1914
1946
  }) {
1915
1947
  const encodedProjectID = encodeURIComponent(projectID);
1916
1948
  let projectRaw = null;
@@ -1968,6 +2000,7 @@ async function loadProjectSummaryForTool({
1968
2000
  siteBaseURL,
1969
2001
  projectID,
1970
2002
  ctxpack,
2003
+ workspaceDir,
1971
2004
  });
1972
2005
  ctxpackSyncStatus = syncResult.sync_status || "error";
1973
2006
  ctxpackSyncMessage = String(syncResult.sync_message || "").trim();
@@ -2008,6 +2041,7 @@ async function loadProjectSummaryForTool({
2008
2041
  ctxpack_sync_message: ctxpackSyncMessage,
2009
2042
  ctxpack_local_path: ctxpackLocalPath,
2010
2043
  ctxpack_local_file_count: ctxpackLocalFileCount,
2044
+ ctxpack_workspace_path: String(workspaceDir || "").trim(),
2011
2045
  agenda,
2012
2046
  agenda_source: agendaSource,
2013
2047
  };
@@ -2149,6 +2183,7 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
2149
2183
  timeoutSeconds: args.timeoutSeconds,
2150
2184
  includeCtxpack: isEmptyBody,
2151
2185
  syncCtxpackLocal: isEmptyBody,
2186
+ workspaceDir: args.workspaceDir,
2152
2187
  });
2153
2188
  if (String(summary.access || "") === "granted") {
2154
2189
  nextLines.push("Project context:");
@@ -2186,12 +2221,66 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
2186
2221
  return responseObj;
2187
2222
  }
2188
2223
 
2224
+ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs) {
2225
+ const result = safeObject(responseObj.result);
2226
+ const content = ensureArray(result.content);
2227
+ if (!content.length) return responseObj;
2228
+
2229
+ const first = safeObject(content[0]);
2230
+ if (String(first.type || "") !== "text") return responseObj;
2231
+ const text = String(first.text || "").trim();
2232
+ if (!text) return responseObj;
2233
+
2234
+ const envelope = parseToolEnvelopeFromRPCResult(result);
2235
+ if (!envelope) return responseObj;
2236
+ if (String(envelope.tool || "").trim() !== "ctxpack.ensure") return responseObj;
2237
+ if (!Boolean(envelope.ok) || Number(envelope.status || 0) !== 200) return responseObj;
2238
+
2239
+ const body = safeObject(envelope.body);
2240
+ if (!Object.keys(body).length) return responseObj;
2241
+
2242
+ const projectID = String(
2243
+ toolArgs?.project_id || toolArgs?.projectID || envelope.project_id || args.projectID || "",
2244
+ ).trim();
2245
+ if (!projectID || !isUUID(projectID)) return responseObj;
2246
+
2247
+ const syncResult = syncCtxpackToLocalCache({
2248
+ siteBaseURL: normalizeSiteBaseURL(args.baseURL),
2249
+ projectID,
2250
+ ctxpack: body,
2251
+ workspaceDir: args.workspaceDir,
2252
+ });
2253
+
2254
+ const lines = [
2255
+ "",
2256
+ `ctxpack_local_sync_status: ${String(syncResult.sync_status || "error")}`,
2257
+ ];
2258
+ if (syncResult.sync_message) {
2259
+ lines.push(`ctxpack_local_sync_message: ${String(syncResult.sync_message)}`);
2260
+ }
2261
+ if (syncResult.local_path) {
2262
+ lines.push(`ctxpack_local_path: ${String(syncResult.local_path)}`);
2263
+ }
2264
+ if (syncResult.workspace_path) {
2265
+ lines.push(`ctxpack_workspace_path: ${String(syncResult.workspace_path)}`);
2266
+ }
2267
+
2268
+ content[0] = {
2269
+ ...first,
2270
+ text: `${text}\n${lines.join("\n")}`,
2271
+ };
2272
+ result.content = content;
2273
+ responseObj.result = result;
2274
+ return responseObj;
2275
+ }
2276
+
2189
2277
  async function runProxy(flags) {
2190
2278
  const workspaceMeta = loadWorkspaceMeta(process.cwd());
2191
2279
  const args = {
2192
2280
  baseURL: flags["base-url"] || DEFAULT_BASE_URL,
2193
2281
  projectID: String(flags["project-id"] || workspaceMeta.project_id || "").trim(),
2194
2282
  ctxpackKey: String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim(),
2283
+ workspaceDir: resolveWorkspaceDir(flags["workspace-dir"] || process.cwd()),
2195
2284
  includeDrafts: boolFromRaw(flags["include-drafts"], true),
2196
2285
  timeoutSeconds: intFromRaw(flags["timeout-seconds"], 30),
2197
2286
  };
@@ -2289,6 +2378,7 @@ async function runProxy(flags) {
2289
2378
  timeoutSeconds: args.timeoutSeconds,
2290
2379
  includeCtxpack,
2291
2380
  syncCtxpackLocal,
2381
+ workspaceDir: args.workspaceDir,
2292
2382
  });
2293
2383
  const text = buildProjectSummaryText(summary);
2294
2384
  process.stdout.write(
@@ -2324,6 +2414,8 @@ async function runProxy(flags) {
2324
2414
  patched = appendProjectHintToInitialize(patched, args);
2325
2415
  } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "workitem.list") {
2326
2416
  patched = await appendWorkitemListHints(patched, args, toolArgs, token);
2417
+ } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "ctxpack.ensure") {
2418
+ patched = appendCtxpackEnsureSyncHints(patched, args, toolArgs);
2327
2419
  }
2328
2420
  process.stdout.write(`${JSON.stringify(patched)}\n`);
2329
2421
  return;
@@ -2408,11 +2500,27 @@ function resolveSetupContext(flags) {
2408
2500
  const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
2409
2501
  const ctxpackKey = String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim();
2410
2502
  const baseURL = String(flags["base-url"] || DEFAULT_SITE_URL).trim().replace(/\/+$/, "");
2503
+ const workspaceDirRaw = String(flags["workspace-dir"] || "").trim();
2504
+ const hasExplicitWorkspaceDir = workspaceDirRaw.length > 0;
2505
+ const workspaceDir = resolveWorkspaceDir(hasExplicitWorkspaceDir ? workspaceDirRaw : process.cwd());
2411
2506
  const serverName = String(flags.name || DEFAULT_SERVER_NAME).trim() || DEFAULT_SERVER_NAME;
2412
2507
  const proxyArgs = ["--base-url", `${baseURL}/governance/mcp`];
2508
+ // Default mode: do not pin workspace path in MCP registration.
2509
+ // Let client runtime cwd resolve per open folder/workspace.
2510
+ if (hasExplicitWorkspaceDir) {
2511
+ proxyArgs.push("--workspace-dir", workspaceDir);
2512
+ }
2413
2513
  if (projectID) proxyArgs.push("--project-id", projectID);
2414
2514
  if (ctxpackKey) proxyArgs.push("--ctxpack-key", ctxpackKey);
2415
- return { projectID, ctxpackKey, baseURL, serverName, proxyArgs };
2515
+ return {
2516
+ projectID,
2517
+ ctxpackKey,
2518
+ baseURL,
2519
+ workspaceDir,
2520
+ hasExplicitWorkspaceDir,
2521
+ serverName,
2522
+ proxyArgs,
2523
+ };
2416
2524
  }
2417
2525
 
2418
2526
  function runSetupInternal(flags, options = {}) {
@@ -2424,20 +2532,26 @@ function runSetupInternal(flags, options = {}) {
2424
2532
  for (const cliBin of clients) {
2425
2533
  if (!commandExists(cliBin)) continue;
2426
2534
  const alreadyRegistered = isRegistered(cliBin, context.serverName);
2427
- if (ensureOnly && alreadyRegistered) {
2428
- results.push({ cliBin, ok: true, action: "already-registered" });
2429
- continue;
2430
- }
2431
- if (!ensureOnly) {
2535
+ if (!ensureOnly || alreadyRegistered) {
2432
2536
  runRemove(cliBin, context.serverName);
2433
2537
  }
2434
2538
  const ok = tryRegister(cliBin, context.serverName, context.proxyArgs);
2435
- results.push({ cliBin, ok, action: alreadyRegistered ? "re-registered" : "registered" });
2539
+ const action = ensureOnly
2540
+ ? alreadyRegistered
2541
+ ? "updated"
2542
+ : "registered"
2543
+ : alreadyRegistered
2544
+ ? "re-registered"
2545
+ : "registered";
2546
+ results.push({ cliBin, ok, action });
2436
2547
  }
2437
2548
 
2438
2549
  process.stdout.write(ensureOnly ? "\nEnsure complete.\n" : "\nInstall complete.\n");
2439
2550
  process.stdout.write(`Server: ${context.serverName}\n`);
2440
2551
  process.stdout.write(`Gateway: ${context.baseURL}/governance/mcp\n`);
2552
+ process.stdout.write(
2553
+ `Workspace: ${context.hasExplicitWorkspaceDir ? context.workspaceDir : "auto (client current folder)"}\n`,
2554
+ );
2441
2555
  process.stdout.write(`Project: ${context.projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
2442
2556
  if (context.ctxpackKey) {
2443
2557
  process.stdout.write(`Ctxpack: ${context.ctxpackKey}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [