metheus-governance-mcp-cli 0.2.5 → 0.2.7

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 +21 -14
  2. package/cli.mjs +533 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  Metheus Governance MCP helper CLI.
4
4
 
5
- - `metheus-governance-mcp` (no args): bootstrap mode
5
+ Compatibility note: legacy command alias `metheus-governance-mcp` is still supported.
6
+
7
+ - `metheus-governance-mcp-cli` (no args): bootstrap mode
6
8
  - checks auth token
7
9
  - if token is missing/expired, starts `auth login`
8
10
  - checks Codex/Claude MCP registration
@@ -23,7 +25,7 @@ npm install -g metheus-governance-mcp-cli
23
25
  Run in your project folder:
24
26
 
25
27
  ```bash
26
- metheus-governance-mcp --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
28
+ metheus-governance-mcp-cli --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
27
29
  ```
28
30
 
29
31
  If auth is not ready, login starts automatically, then MCP registration is completed.
@@ -31,7 +33,7 @@ If auth is not ready, login starts automatically, then MCP registration is compl
31
33
  ## Setup
32
34
 
33
35
  ```bash
34
- metheus-governance-mcp setup --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
36
+ metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
35
37
  ```
36
38
 
37
39
  `project-id` can be omitted if your current folder (or parent) has `.metheus_ctxpack_sync.json`.
@@ -39,7 +41,7 @@ metheus-governance-mcp setup --project-id <project_uuid> --ctxpack-key "<ctxpack
39
41
  ## Doctor
40
42
 
41
43
  ```bash
42
- metheus-governance-mcp doctor --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
44
+ metheus-governance-mcp-cli doctor --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
43
45
  ```
44
46
 
45
47
  Checks:
@@ -70,30 +72,35 @@ These tools accept `project_id` and return:
70
72
  - missing local cache -> download
71
73
  - same version -> keep current
72
74
  - newer server version -> update local cache
75
+ - workspace path -> follows active client workspace/root when provided by Codex/Claude
73
76
 
74
77
  Manual ctxpack pull/update:
75
78
 
76
79
  ```bash
77
- metheus-governance-mcp ctxpack pull --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
80
+ metheus-governance-mcp-cli ctxpack pull --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
78
81
  ```
79
82
 
80
83
  When `workitem.list` returns empty, proxy appends a hint to call `project.summary` first.
81
84
 
85
+ For `ctxpack` push conflicts (`ctxpack_conflict` / stale baseline), proxy behavior:
86
+ - standard conflict message with pull/merge/retry guidance
87
+ - auto ctxpack pull-to-local on conflict by default (`--auto-pull-on-conflict=true`)
88
+
82
89
  ## Auth flow (recommended)
83
90
 
84
91
  1. Auto login and save token once (default flow: `device -> callback -> manual hint`):
85
92
 
86
93
  ```bash
87
- metheus-governance-mcp auth login --base-url https://metheus.gesiaplatform.com
94
+ metheus-governance-mcp-cli auth login --base-url https://metheus.gesiaplatform.com
88
95
  ```
89
96
 
90
97
  Optional flags:
91
98
 
92
99
  ```bash
93
- metheus-governance-mcp auth login --callback-port 43819 --open-browser true
94
- metheus-governance-mcp auth login --flow device
95
- metheus-governance-mcp auth login --flow callback --callback-port 43819
96
- metheus-governance-mcp auth login --client-id metheus-cli --realm master --keycloak-url https://oauth3.gesia.io
100
+ metheus-governance-mcp-cli auth login --callback-port 43819 --open-browser true
101
+ metheus-governance-mcp-cli auth login --flow device
102
+ metheus-governance-mcp-cli auth login --flow callback --callback-port 43819
103
+ metheus-governance-mcp-cli auth login --client-id metheus-cli --realm master --keycloak-url https://oauth3.gesia.io
97
104
  ```
98
105
 
99
106
  Keycloak client requirement for auto login:
@@ -106,25 +113,25 @@ Keycloak client requirement for auto login:
106
113
  Manual fallback:
107
114
 
108
115
  ```bash
109
- metheus-governance-mcp auth login --manual true --base-url https://metheus.gesiaplatform.com
116
+ metheus-governance-mcp-cli auth login --manual true --base-url https://metheus.gesiaplatform.com
110
117
  ```
111
118
 
112
119
  2. Check token status:
113
120
 
114
121
  ```bash
115
- metheus-governance-mcp auth status
122
+ metheus-governance-mcp-cli auth status
116
123
  ```
117
124
 
118
125
  3. If token is rotated, set directly:
119
126
 
120
127
  ```bash
121
- metheus-governance-mcp auth set --token "<access_token>" --refresh-token "<refresh_token>"
128
+ metheus-governance-mcp-cli auth set --token "<access_token>" --refresh-token "<refresh_token>"
122
129
  ```
123
130
 
124
131
  4. Remove local token:
125
132
 
126
133
  ```bash
127
- metheus-governance-mcp auth clear
134
+ metheus-governance-mcp-cli auth clear
128
135
  ```
129
136
 
130
137
  `proxy` priority:
package/cli.mjs CHANGED
@@ -16,27 +16,31 @@ const DEFAULT_SERVER_NAME = "metheus-governance-mcp";
16
16
  const AUTH_STORE_RELATIVE_PATH = path.join(".metheus", "governance-mcp-auth.json");
17
17
  const CTXPACK_CACHE_RELATIVE_DIR = path.join(".metheus", "ctxpack-cache");
18
18
  const CTXPACK_META_FILENAME = ".metheus_ctxpack_sync.json";
19
+ const CTXPACK_PUSH_TOOL_NAMES = ["ctxpack.push", "ctxpack.update", "ctxpack.save"];
19
20
  const CLI_META = loadCLIMeta();
20
21
  const CLI_NAME = CLI_META.name || "metheus-governance-mcp-cli";
21
22
  const CLI_VERSION = CLI_META.version || "0.0.0";
23
+ const AUTO_CTXPACK_SYNC_INTERVAL_MS = 60 * 1000;
24
+ const autoCtxpackSyncTracker = new Map();
22
25
 
23
26
  function printUsage() {
27
+ const cmd = CLI_NAME;
24
28
  process.stderr.write(
25
29
  [
26
30
  "Metheus Governance MCP CLI",
27
31
  "",
28
32
  "Usage:",
29
- " metheus-governance-mcp [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--flow <auto|device|callback|manual>]",
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>] [--workspace-dir <path>] [--name <server_name>]",
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>] [--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
- " metheus-governance-mcp auth status",
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
- " metheus-governance-mcp auth set --token <jwt> [--refresh-token <token>] [--base-url <url>]",
38
- " metheus-governance-mcp auth clear",
39
- " metheus-governance-mcp -v | --version",
33
+ ` ${cmd} [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--flow <auto|device|callback|manual>]`,
34
+ ` ${cmd} init [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]`,
35
+ ` ${cmd} setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--name <server_name>]`,
36
+ ` ${cmd} doctor [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--timeout-seconds <n>]`,
37
+ ` ${cmd} proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--include-drafts <true|false>] [--auto-pull-on-conflict <true|false>] [--timeout-seconds <n>]`,
38
+ ` ${cmd} ctxpack pull [--project-id <uuid>] [--base-url <url>] [--workspace-dir <path>] [--paths <csv>] [--timeout-seconds <n>]`,
39
+ ` ${cmd} auth status`,
40
+ ` ${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>]`,
41
+ ` ${cmd} auth set --token <jwt> [--refresh-token <token>] [--base-url <url>]`,
42
+ ` ${cmd} auth clear`,
43
+ ` ${cmd} -v | --version`,
40
44
  "",
41
45
  "Environment:",
42
46
  " METHEUS_TOKEN or MCP_AUTH_TOKEN is used first for proxy requests.",
@@ -109,6 +113,67 @@ function resolveWorkspaceDir(rawPath) {
109
113
  return path.resolve(process.cwd());
110
114
  }
111
115
 
116
+ function fileURIToLocalPath(rawValue) {
117
+ const value = String(rawValue || "").trim();
118
+ if (!value || !/^file:\/\//i.test(value)) return "";
119
+ try {
120
+ const parsed = new URL(value);
121
+ if (parsed.protocol !== "file:") return "";
122
+ const pathname = decodeURIComponent(parsed.pathname || "");
123
+ if (!pathname) return "";
124
+ if (process.platform === "win32") {
125
+ // file:///C:/repo -> C:\repo
126
+ const winPath = pathname.replace(/^\/+([a-zA-Z]:\/)/, "$1").replace(/\//g, "\\");
127
+ return winPath;
128
+ }
129
+ return pathname;
130
+ } catch {
131
+ return "";
132
+ }
133
+ }
134
+
135
+ function firstNonEmptyString(values) {
136
+ for (const value of values) {
137
+ const text = String(value || "").trim();
138
+ if (text) return text;
139
+ }
140
+ return "";
141
+ }
142
+
143
+ function extractWorkspaceCandidateFromRequest(requestObj, toolArgs) {
144
+ const params = safeObject(requestObj?.params);
145
+ const meta = safeObject(params._meta);
146
+ const args = safeObject(toolArgs);
147
+ const rawCandidate = firstNonEmptyString([
148
+ args.workspace_dir,
149
+ args.workspaceDir,
150
+ params.workspace_dir,
151
+ params.workspaceDir,
152
+ params.cwd,
153
+ params.root_path,
154
+ params.rootPath,
155
+ params.root_uri,
156
+ params.rootUri,
157
+ meta.workspace_dir,
158
+ meta.workspaceDir,
159
+ meta.cwd,
160
+ meta.root_path,
161
+ meta.rootPath,
162
+ meta.root_uri,
163
+ meta.rootUri,
164
+ ]);
165
+ const fileCandidate = fileURIToLocalPath(rawCandidate);
166
+ const candidate = firstNonEmptyString([fileCandidate, rawCandidate]);
167
+ if (!candidate) return "";
168
+ return resolveWorkspaceDir(candidate);
169
+ }
170
+
171
+ function resolveWorkspaceDirForRequest(defaultWorkspaceDir, requestObj, toolArgs) {
172
+ const requestCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
173
+ const candidate = firstNonEmptyString([requestCandidate, defaultWorkspaceDir, process.cwd()]);
174
+ return resolveWorkspaceDir(candidate);
175
+ }
176
+
112
177
  function homeCtxpackCacheRootDir() {
113
178
  const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
114
179
  if (!home) {
@@ -1258,6 +1323,7 @@ async function runCtxpackPull(flags) {
1258
1323
  const workspaceMeta = loadWorkspaceMeta(process.cwd());
1259
1324
  const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
1260
1325
  const workspaceDir = resolveWorkspaceDir(flags["workspace-dir"] || process.cwd());
1326
+ const pathsCSV = String(flags.paths || "").trim();
1261
1327
  if (!projectID) {
1262
1328
  process.stderr.write("Missing project_id. Use --project-id <uuid> or run inside a synced ctxpack workspace.\n");
1263
1329
  process.exitCode = 1;
@@ -1289,6 +1355,7 @@ async function runCtxpackPull(flags) {
1289
1355
  includeCtxpack: true,
1290
1356
  syncCtxpackLocal: true,
1291
1357
  workspaceDir,
1358
+ ctxpackPaths: pathsCSV,
1292
1359
  });
1293
1360
  process.stdout.write(`${buildProjectSummaryText(summary)}\n`);
1294
1361
 
@@ -1509,6 +1576,110 @@ async function runDoctor(flags) {
1509
1576
  addDoctorCheck(rows, "fail", "ctxpack sync", summary.ctxpack_sync_message || "ctxpack sync failed");
1510
1577
  }
1511
1578
 
1579
+ const localMeta = loadWorkspaceMeta(context.workspaceDir);
1580
+ const localBaseVersionID = firstNonEmptyString([localMeta.base_version_id, localMeta.version_id]);
1581
+ const serverCurrentVersionID = firstNonEmptyString([summary.current_version_id, summary.ctxpack_version_id]);
1582
+ if (summary.ctxpack_id) {
1583
+ if (!serverCurrentVersionID) {
1584
+ addDoctorCheck(rows, "warn", "stale baseline", "server current_version_id is missing");
1585
+ } else if (!localBaseVersionID) {
1586
+ addDoctorCheck(
1587
+ rows,
1588
+ "warn",
1589
+ "stale baseline",
1590
+ "local base_version_id is missing. Run: metheus-governance-mcp ctxpack pull --project-id <project_id>",
1591
+ );
1592
+ } else if (localBaseVersionID !== serverCurrentVersionID) {
1593
+ addDoctorCheck(
1594
+ rows,
1595
+ "fail",
1596
+ "stale baseline",
1597
+ `local ${localBaseVersionID} != server ${serverCurrentVersionID}. Run ctxpack pull before safe push.`,
1598
+ );
1599
+ } else {
1600
+ addDoctorCheck(rows, "ok", "stale baseline", `baseline is current (${serverCurrentVersionID})`);
1601
+ }
1602
+ } else {
1603
+ addDoctorCheck(rows, "warn", "stale baseline", "ctxpack is not available for this project");
1604
+ }
1605
+
1606
+ if (summary.ctxpack_id) {
1607
+ try {
1608
+ const encodedProjectID = encodeURIComponent(context.projectID);
1609
+ const statsRaw = await getJSONWithAuth(
1610
+ `${context.baseURL}/api/v1/projects/${encodedProjectID}/ctxpack/stats`,
1611
+ timeoutSeconds,
1612
+ token,
1613
+ );
1614
+ const stats = safeObject(statsRaw);
1615
+ const strictConflict = Boolean(stats.strict_conflict);
1616
+ const preflightMode = String(stats.preflight_mode || "").trim().toLowerCase() || "off";
1617
+ const directPushMode = String(stats.direct_push_mode || "").trim().toLowerCase() || "unknown";
1618
+ const allowLegacyPush = Boolean(stats.allow_legacy_push);
1619
+ const forceAdminOnly = Boolean(stats.force_admin_only);
1620
+ const readOnlyMode = Boolean(stats.read_only_mode);
1621
+ const mergeMinApprovalsRaw = Number.parseInt(String(stats.merge_min_approvals ?? "1"), 10);
1622
+ const mergeMinApprovals = Number.isFinite(mergeMinApprovalsRaw) ? mergeMinApprovalsRaw : 1;
1623
+
1624
+ if (strictConflict) {
1625
+ addDoctorCheck(rows, "ok", "server policy strict_conflict", "enabled");
1626
+ } else {
1627
+ addDoctorCheck(rows, "warn", "server policy strict_conflict", "disabled (stale overwrite risk)");
1628
+ }
1629
+
1630
+ if (preflightMode === "enforce") {
1631
+ addDoctorCheck(rows, "ok", "server policy preflight_mode", "enforce");
1632
+ } else if (preflightMode === "warn") {
1633
+ addDoctorCheck(rows, "warn", "server policy preflight_mode", "warn (blocking not enabled yet)");
1634
+ } else {
1635
+ addDoctorCheck(rows, "warn", "server policy preflight_mode", `${preflightMode || "off"} (recommended: enforce)`);
1636
+ }
1637
+
1638
+ if (directPushMode === "disabled") {
1639
+ addDoctorCheck(rows, "ok", "server policy direct_push_mode", "disabled (merge-request workflow)");
1640
+ } else if (directPushMode === "admin_only") {
1641
+ addDoctorCheck(rows, "ok", "server policy direct_push_mode", "admin_only (merge-first default)");
1642
+ } else if (directPushMode === "enabled") {
1643
+ addDoctorCheck(rows, "warn", "server policy direct_push_mode", "enabled (writer direct push is open)");
1644
+ } else {
1645
+ addDoctorCheck(rows, "warn", "server policy direct_push_mode", directPushMode || "unknown");
1646
+ }
1647
+
1648
+ if (mergeMinApprovals >= 2) {
1649
+ addDoctorCheck(rows, "ok", "server policy merge_min_approvals", String(mergeMinApprovals));
1650
+ } else if (mergeMinApprovals === 1) {
1651
+ addDoctorCheck(rows, "warn", "server policy merge_min_approvals", "1 (recommended: >=2 for team review)");
1652
+ } else {
1653
+ addDoctorCheck(rows, "fail", "server policy merge_min_approvals", String(mergeMinApprovals));
1654
+ }
1655
+
1656
+ if (allowLegacyPush) {
1657
+ addDoctorCheck(rows, "warn", "server policy allow_legacy_push", "enabled (recommended: false)");
1658
+ } else {
1659
+ addDoctorCheck(rows, "ok", "server policy allow_legacy_push", "disabled");
1660
+ }
1661
+
1662
+ if (forceAdminOnly) {
1663
+ addDoctorCheck(rows, "ok", "server policy force_admin_only", "enabled");
1664
+ } else {
1665
+ addDoctorCheck(rows, "warn", "server policy force_admin_only", "disabled (writers may force/rollback)");
1666
+ }
1667
+
1668
+ if (readOnlyMode) {
1669
+ addDoctorCheck(rows, "warn", "server mode", "ctxpack read-only mode is enabled");
1670
+ } else {
1671
+ addDoctorCheck(rows, "ok", "server mode", "ctxpack writable");
1672
+ }
1673
+ } catch (err) {
1674
+ addDoctorCheck(
1675
+ rows,
1676
+ "warn",
1677
+ "server ctxpack policy",
1678
+ `unable to read /ctxpack/stats (${String(err?.message || err)})`,
1679
+ );
1680
+ }
1681
+ }
1682
+
1512
1683
  const smokeCalls = [
1513
1684
  {
1514
1685
  tool: "workitem.list",
@@ -1854,6 +2025,7 @@ function hasAllCtxpackFiles(baseDir, files) {
1854
2025
  function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack, workspaceDir }) {
1855
2026
  const ctxpackID = String(ctxpack?.ctxpack_id || "").trim();
1856
2027
  const version = String(ctxpack?.version || "").trim();
2028
+ const versionID = String(ctxpack?.version_id || ctxpack?.current_version_id || "").trim();
1857
2029
  const status = String(ctxpack?.status || "").trim() || "draft";
1858
2030
  const files = normalizeCtxpackFiles(ctxpack?.files);
1859
2031
  const resolvedWorkspaceDir = resolveWorkspaceDir(workspaceDir);
@@ -1871,13 +2043,29 @@ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack, workspaceDir
1871
2043
  };
1872
2044
  }
1873
2045
 
1874
- const remoteSig = `${ctxpackID}|${version}|${status}|${files.length}`;
2046
+ const remoteSig = `${ctxpackID}|${versionID}|${version}|${status}|${files.length}`;
1875
2047
  const previousMeta = loadCtxpackMeta(metaPath);
1876
2048
  const previousSig = previousMeta
1877
- ? `${String(previousMeta.ctxpack_id || "").trim()}|${String(previousMeta.version || "").trim()}|${String(previousMeta.status || "").trim()}|${Number.parseInt(String(previousMeta.files_count || 0), 10) || 0}`
2049
+ ? `${String(previousMeta.ctxpack_id || "").trim()}|${String(previousMeta.version_id || "").trim()}|${String(previousMeta.version || "").trim()}|${String(previousMeta.status || "").trim()}|${Number.parseInt(String(previousMeta.files_count || 0), 10) || 0}`
1878
2050
  : "";
2051
+ const payload = {
2052
+ project_id: projectID,
2053
+ ctxpack_id: ctxpackID,
2054
+ version_id: versionID,
2055
+ version,
2056
+ status,
2057
+ files_count: files.length,
2058
+ files: files.map((f) => ({ path: f.path, doc_type: f.docType || "" })),
2059
+ source: `${String(siteBaseURL || "").replace(/\/+$/, "")}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack`,
2060
+ synced_at: new Date().toISOString(),
2061
+ };
1879
2062
 
1880
2063
  if (previousSig === remoteSig && hasAllCtxpackFiles(cacheDir, files)) {
2064
+ try {
2065
+ fs.writeFileSync(workspaceMetaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
2066
+ } catch {
2067
+ // Best-effort metadata refresh for workspace-root auto-detection.
2068
+ }
1881
2069
  return {
1882
2070
  sync_status: "current",
1883
2071
  sync_message: "Local ctxpack cache is up to date.",
@@ -1897,16 +2085,6 @@ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack, workspaceDir
1897
2085
  fs.writeFileSync(target, file.content, "utf8");
1898
2086
  }
1899
2087
 
1900
- const payload = {
1901
- project_id: projectID,
1902
- ctxpack_id: ctxpackID,
1903
- version,
1904
- status,
1905
- files_count: files.length,
1906
- files: files.map((f) => ({ path: f.path, doc_type: f.docType || "" })),
1907
- source: `${String(siteBaseURL || "").replace(/\/+$/, "")}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack`,
1908
- synced_at: new Date().toISOString(),
1909
- };
1910
2088
  fs.writeFileSync(metaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1911
2089
  fs.writeFileSync(workspaceMetaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1912
2090
 
@@ -1938,6 +2116,7 @@ async function loadProjectSummaryForTool({
1938
2116
  includeCtxpack,
1939
2117
  syncCtxpackLocal,
1940
2118
  workspaceDir,
2119
+ ctxpackPaths,
1941
2120
  }) {
1942
2121
  const encodedProjectID = encodeURIComponent(projectID);
1943
2122
  let projectRaw = null;
@@ -1975,6 +2154,7 @@ async function loadProjectSummaryForTool({
1975
2154
  let agendaSource = "";
1976
2155
  let ctxpackID = "";
1977
2156
  let ctxpackVersion = "";
2157
+ let ctxpackVersionID = "";
1978
2158
  let ctxpackFileCount = 0;
1979
2159
  let ctxpackSyncStatus = includeCtxpack && syncCtxpackLocal ? "not_attempted" : "disabled";
1980
2160
  let ctxpackSyncMessage = "";
@@ -1982,7 +2162,12 @@ async function loadProjectSummaryForTool({
1982
2162
  let ctxpackLocalFileCount = 0;
1983
2163
  if (includeCtxpack) {
1984
2164
  try {
1985
- const ctxpackRaw = await getJSONWithAuth(`${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack`, timeoutSeconds, token);
2165
+ const pathQuery = String(ctxpackPaths || "").trim();
2166
+ const ctxpackURL =
2167
+ pathQuery.length > 0
2168
+ ? `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack?paths=${encodeURIComponent(pathQuery)}`
2169
+ : `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack`;
2170
+ const ctxpackRaw = await getJSONWithAuth(ctxpackURL, timeoutSeconds, token);
1986
2171
  const ctxpack = safeObject(ctxpackRaw);
1987
2172
  const extracted = extractAgendaFromFiles(ctxpack.files);
1988
2173
  agenda = extracted.agenda;
@@ -1990,6 +2175,7 @@ async function loadProjectSummaryForTool({
1990
2175
  ctxpackFileCount = extracted.ctxpackFileCount;
1991
2176
  ctxpackID = String(ctxpack.ctxpack_id || "").trim();
1992
2177
  ctxpackVersion = String(ctxpack.version || "").trim();
2178
+ ctxpackVersionID = String(ctxpack.version_id || "").trim();
1993
2179
  if (syncCtxpackLocal) {
1994
2180
  const syncResult = syncCtxpackToLocalCache({
1995
2181
  siteBaseURL,
@@ -2030,7 +2216,10 @@ async function loadProjectSummaryForTool({
2030
2216
  member_count: Number(project.member_count || 0),
2031
2217
  created_at: String(project.created_at || "").trim(),
2032
2218
  ctxpack_id: ctxpackID,
2219
+ ctxpack_version_id: ctxpackVersionID,
2033
2220
  ctxpack_version: ctxpackVersion,
2221
+ base_version_id: ctxpackVersionID,
2222
+ current_version_id: ctxpackVersionID,
2034
2223
  ctxpack_file_count: ctxpackFileCount,
2035
2224
  ctxpack_sync_status: ctxpackSyncStatus,
2036
2225
  ctxpack_sync_message: ctxpackSyncMessage,
@@ -2125,6 +2314,75 @@ function appendProjectHintToInitialize(responseObj, args) {
2125
2314
  return responseObj;
2126
2315
  }
2127
2316
 
2317
+ async function maybeAutoSyncCtxpackForCall({
2318
+ requestObj,
2319
+ toolName,
2320
+ toolArgs,
2321
+ args,
2322
+ token,
2323
+ workspaceDir,
2324
+ }) {
2325
+ if (!isJsonRpcMethod(requestObj, "tools/call")) return null;
2326
+ if (!token) return null;
2327
+ if (LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) return null;
2328
+ if (String(toolName || "").trim().toLowerCase() === "ctxpack.ensure") return null;
2329
+
2330
+ const projectID = firstNonEmptyString([
2331
+ toolArgs?.project_id,
2332
+ toolArgs?.projectID,
2333
+ args.projectID,
2334
+ ]);
2335
+ if (!isUUID(projectID)) return null;
2336
+
2337
+ const workspacePath = resolveWorkspaceDir(workspaceDir || process.cwd());
2338
+ const trackerKey = `${workspacePath}::${projectID}`;
2339
+ const now = Date.now();
2340
+ const lastAt = Number(autoCtxpackSyncTracker.get(trackerKey) || 0);
2341
+ if (lastAt > 0 && now-lastAt < AUTO_CTXPACK_SYNC_INTERVAL_MS) {
2342
+ return null;
2343
+ }
2344
+ autoCtxpackSyncTracker.set(trackerKey, now);
2345
+
2346
+ try {
2347
+ return await loadProjectSummaryForTool({
2348
+ siteBaseURL: normalizeSiteBaseURL(args.baseURL),
2349
+ projectID,
2350
+ token,
2351
+ timeoutSeconds: args.timeoutSeconds,
2352
+ includeCtxpack: true,
2353
+ syncCtxpackLocal: true,
2354
+ workspaceDir: workspacePath,
2355
+ });
2356
+ } catch {
2357
+ return null;
2358
+ }
2359
+ }
2360
+
2361
+ function appendAutoCtxpackSyncHint(responseObj, summary) {
2362
+ const status = String(summary?.ctxpack_sync_status || "").trim();
2363
+ if (!status || status === "disabled" || status === "not_attempted" || status === "current") {
2364
+ return responseObj;
2365
+ }
2366
+ const result = safeObject(responseObj.result);
2367
+ const content = ensureArray(result.content);
2368
+ if (!content.length) return responseObj;
2369
+ const first = safeObject(content[0]);
2370
+ if (String(first.type || "").trim() !== "text") return responseObj;
2371
+ const text = String(first.text || "");
2372
+ if (!text.trim() || text.includes("ctxpack_auto_sync:")) return responseObj;
2373
+
2374
+ let suffix = `ctxpack_auto_sync: ${status}`;
2375
+ const localPath = String(summary?.ctxpack_local_path || "").trim();
2376
+ if (localPath) suffix += ` (${localPath})`;
2377
+ const message = String(summary?.ctxpack_sync_message || "").trim();
2378
+ if (message) suffix += ` - ${message}`;
2379
+
2380
+ content[0] = { ...first, text: `${text}\n\n${suffix}` };
2381
+ result.content = content;
2382
+ responseObj.result = result;
2383
+ return responseObj;
2384
+ }
2385
+
2128
2386
  async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
2129
2387
  const result = safeObject(responseObj.result);
2130
2388
  const content = ensureArray(result.content);
@@ -2216,7 +2474,7 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
2216
2474
  return responseObj;
2217
2475
  }
2218
2476
 
2219
- function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs) {
2477
+ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs, requestObj) {
2220
2478
  const result = safeObject(responseObj.result);
2221
2479
  const content = ensureArray(result.content);
2222
2480
  if (!content.length) return responseObj;
@@ -2238,13 +2496,16 @@ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs) {
2238
2496
  toolArgs?.project_id || toolArgs?.projectID || envelope.project_id || args.projectID || "",
2239
2497
  ).trim();
2240
2498
  if (!projectID || !isUUID(projectID)) return responseObj;
2499
+ const workspaceDir = resolveWorkspaceDirForRequest(args.workspaceDir || process.cwd(), requestObj, toolArgs);
2241
2500
 
2242
2501
  const syncResult = syncCtxpackToLocalCache({
2243
2502
  siteBaseURL: normalizeSiteBaseURL(args.baseURL),
2244
2503
  projectID,
2245
2504
  ctxpack: body,
2246
- workspaceDir: args.workspaceDir,
2505
+ workspaceDir,
2247
2506
  });
2507
+ const baseVersionID = firstNonEmptyString([body.base_version_id, body.version_id, body.current_version_id]);
2508
+ const currentVersionID = firstNonEmptyString([body.current_version_id, body.version_id, body.base_version_id]);
2248
2509
 
2249
2510
  const lines = [
2250
2511
  "",
@@ -2259,16 +2520,189 @@ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs) {
2259
2520
  if (syncResult.workspace_path) {
2260
2521
  lines.push(`ctxpack_workspace_path: ${String(syncResult.workspace_path)}`);
2261
2522
  }
2523
+ if (baseVersionID) {
2524
+ lines.push(`ctxpack_base_version_id: ${baseVersionID}`);
2525
+ }
2526
+ if (currentVersionID) {
2527
+ lines.push(`ctxpack_current_version_id: ${currentVersionID}`);
2528
+ }
2262
2529
 
2263
2530
  content[0] = {
2264
2531
  ...first,
2265
2532
  text: `${text}\n${lines.join("\n")}`,
2266
2533
  };
2267
2534
  result.content = content;
2535
+ const structured = safeObject(result.structuredContent);
2536
+ result.structuredContent = {
2537
+ ...structured,
2538
+ sync_status: String(syncResult.sync_status || "error"),
2539
+ base_version_id: baseVersionID,
2540
+ current_version_id: currentVersionID,
2541
+ local_workspace_path: String(syncResult.workspace_path || workspaceDir),
2542
+ local_ctxpack_path: String(syncResult.local_path || ""),
2543
+ };
2268
2544
  responseObj.result = result;
2269
2545
  return responseObj;
2270
2546
  }
2271
2547
 
2548
+ function injectCtxpackPushDefaults(requestObj, toolName, workspaceDir) {
2549
+ if (!isJsonRpcMethod(requestObj, "tools/call")) return requestObj;
2550
+ const normalizedTool = String(toolName || "").trim().toLowerCase();
2551
+ if (!CTXPACK_PUSH_TOOL_NAMES.includes(normalizedTool)) {
2552
+ return requestObj;
2553
+ }
2554
+
2555
+ const params = safeObject(requestObj.params);
2556
+ const rawArgs = safeObject(params.arguments ?? params.args);
2557
+ if (String(rawArgs.base_version_id || "").trim() && String(rawArgs.push_mode || "").trim()) {
2558
+ return requestObj;
2559
+ }
2560
+
2561
+ const workspaceMeta = loadWorkspaceMeta(workspaceDir);
2562
+ const baseVersionID = firstNonEmptyString([
2563
+ rawArgs.base_version_id,
2564
+ workspaceMeta.base_version_id,
2565
+ workspaceMeta.version_id,
2566
+ ]);
2567
+ const nextArgs = { ...rawArgs };
2568
+ if (!String(nextArgs.base_version_id || "").trim() && isUUID(baseVersionID)) {
2569
+ nextArgs.base_version_id = baseVersionID;
2570
+ }
2571
+ if (!String(nextArgs.push_mode || "").trim()) {
2572
+ nextArgs.push_mode = "safe";
2573
+ }
2574
+
2575
+ const nextParams = { ...params };
2576
+ nextParams.arguments = nextArgs;
2577
+ const patched = { ...requestObj, params: nextParams };
2578
+ return patched;
2579
+ }
2580
+
2581
+ function collectChangedPathsForPreflight(rawArgs) {
2582
+ const args = safeObject(rawArgs);
2583
+ const explicit = ensureArray(args.changed_paths ?? args.changedPaths);
2584
+ const raw = explicit.length > 0 ? explicit : ensureArray(args.files).map((file) => safeObject(file).path);
2585
+ const dedup = new Set();
2586
+ const out = [];
2587
+ for (const value of raw) {
2588
+ const text = String(value || "").trim();
2589
+ if (!text || dedup.has(text)) continue;
2590
+ dedup.add(text);
2591
+ out.push(text);
2592
+ }
2593
+ return out;
2594
+ }
2595
+
2596
+ async function injectCtxpackPreflightToken(requestObj, toolName, toolArgs, args, token, workspaceDir) {
2597
+ if (!isJsonRpcMethod(requestObj, "tools/call")) return requestObj;
2598
+ if (!token) return requestObj;
2599
+
2600
+ const normalizedTool = String(toolName || "").trim().toLowerCase();
2601
+ if (!CTXPACK_PUSH_TOOL_NAMES.includes(normalizedTool)) {
2602
+ return requestObj;
2603
+ }
2604
+
2605
+ const params = safeObject(requestObj.params);
2606
+ const rawArgs = safeObject(params.arguments ?? params.args);
2607
+ if (String(rawArgs.preflight_token || rawArgs.preflightToken || "").trim()) {
2608
+ return requestObj;
2609
+ }
2610
+
2611
+ const projectID = firstNonEmptyString([
2612
+ rawArgs.project_id,
2613
+ rawArgs.projectID,
2614
+ toolArgs?.project_id,
2615
+ toolArgs?.projectID,
2616
+ args.projectID,
2617
+ ]);
2618
+ if (!isUUID(projectID)) {
2619
+ return requestObj;
2620
+ }
2621
+
2622
+ const workspaceMeta = loadWorkspaceMeta(workspaceDir);
2623
+ const baseVersionID = firstNonEmptyString([
2624
+ rawArgs.base_version_id,
2625
+ rawArgs.baseVersionID,
2626
+ workspaceMeta.base_version_id,
2627
+ workspaceMeta.version_id,
2628
+ ]);
2629
+ if (!isUUID(baseVersionID)) {
2630
+ return requestObj;
2631
+ }
2632
+
2633
+ const changedPaths = collectChangedPathsForPreflight(rawArgs);
2634
+ const payload = {
2635
+ base_version_id: baseVersionID,
2636
+ };
2637
+ if (changedPaths.length > 0) {
2638
+ payload.changed_paths = changedPaths;
2639
+ }
2640
+
2641
+ try {
2642
+ const baseURL = normalizeSiteBaseURL(args.baseURL);
2643
+ const preflightURL = `${baseURL}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack/preflight`;
2644
+ const responseText = await postJSON(preflightURL, args.timeoutSeconds, token, payload);
2645
+ const responseBody = safeObject(tryJsonParse(responseText));
2646
+ const preflightID = firstNonEmptyString([responseBody.preflight_id, responseBody.preflightID, responseBody.id]);
2647
+ if (!preflightID) return requestObj;
2648
+
2649
+ const nextArgs = { ...rawArgs, preflight_token: preflightID };
2650
+ if (!ensureArray(rawArgs.changed_paths ?? rawArgs.changedPaths).length && changedPaths.length > 0) {
2651
+ nextArgs.changed_paths = changedPaths;
2652
+ }
2653
+ const nextParams = { ...params, arguments: nextArgs };
2654
+ return { ...requestObj, params: nextParams };
2655
+ } catch {
2656
+ // Best-effort: if preflight auto-issue fails, server-side validation still enforces policy.
2657
+ return requestObj;
2658
+ }
2659
+ }
2660
+
2661
+ async function appendCtxpackConflictHintToErrorResponse(responseObj, args, toolName, toolArgs, token, workspaceDir) {
2662
+ const out = safeObject(responseObj);
2663
+ const errObj = safeObject(out.error);
2664
+ const message = String(errObj.message || "").trim();
2665
+ if (!message) return responseObj;
2666
+ const normalizedTool = String(toolName || "").trim().toLowerCase();
2667
+ const normalizedMessage = message.toLowerCase();
2668
+ const isCtxpackConflict = normalizedMessage.includes("ctxpack_conflict") || normalizedMessage.includes("ctxpack conflict");
2669
+ if (!isCtxpackConflict) return responseObj;
2670
+ if (!normalizedTool.startsWith("ctxpack.")) return responseObj;
2671
+
2672
+ const projectIDHint = firstNonEmptyString([
2673
+ toolArgs?.project_id,
2674
+ toolArgs?.projectID,
2675
+ args.projectID,
2676
+ "<project_id>",
2677
+ ]);
2678
+ const baseURL = normalizeSiteBaseURL(args.baseURL);
2679
+ let autoPullHint = "";
2680
+ if (args.autoPullOnConflict && token && isUUID(projectIDHint)) {
2681
+ try {
2682
+ const encodedProjectID = encodeURIComponent(projectIDHint);
2683
+ const ctxpackRaw = await getJSONWithAuth(
2684
+ `${baseURL}/api/v1/projects/${encodedProjectID}/ctxpack`,
2685
+ args.timeoutSeconds,
2686
+ token,
2687
+ );
2688
+ const syncResult = syncCtxpackToLocalCache({
2689
+ siteBaseURL: baseURL,
2690
+ projectID: projectIDHint,
2691
+ ctxpack: safeObject(ctxpackRaw),
2692
+ workspaceDir,
2693
+ });
2694
+ autoPullHint = ` auto-pull: ${String(syncResult.sync_status || "error")}${
2695
+ syncResult.local_path ? ` (${String(syncResult.local_path)})` : ""
2696
+ }`;
2697
+ } catch (err) {
2698
+ autoPullHint = ` auto-pull failed: ${String(err?.message || err)}`;
2699
+ }
2700
+ }
2701
+ const hint = `stale baseline detected. run 'metheus-governance-mcp ctxpack pull --project-id ${projectIDHint} --base-url ${baseURL}', merge local edits, then retry safe push.${autoPullHint}`;
2702
+ const nextError = { ...errObj, message: `${message}; ${hint}` };
2703
+ return { ...out, error: nextError };
2704
+ }
2705
+
2272
2706
  async function runProxy(flags) {
2273
2707
  const workspaceMeta = loadWorkspaceMeta(process.cwd());
2274
2708
  const args = {
@@ -2277,11 +2711,13 @@ async function runProxy(flags) {
2277
2711
  ctxpackKey: String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim(),
2278
2712
  workspaceDir: resolveWorkspaceDir(flags["workspace-dir"] || process.cwd()),
2279
2713
  includeDrafts: boolFromRaw(flags["include-drafts"], true),
2714
+ autoPullOnConflict: boolFromRaw(flags["auto-pull-on-conflict"], true),
2280
2715
  timeoutSeconds: intFromRaw(flags["timeout-seconds"], 30),
2281
2716
  };
2282
2717
  const gatewayURL = buildGatewayURL(args);
2283
2718
  let lastRefreshAttemptAtMs = 0;
2284
2719
  let lastRefreshError = "";
2720
+ let sessionWorkspaceDir = "";
2285
2721
 
2286
2722
  const rl = readline.createInterface({
2287
2723
  input: process.stdin,
@@ -2339,6 +2775,29 @@ async function runProxy(flags) {
2339
2775
  }
2340
2776
 
2341
2777
  const { name: toolName, args: toolArgs } = extractToolCall(requestObj);
2778
+ const requestWorkspaceCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
2779
+ if (requestWorkspaceCandidate) {
2780
+ sessionWorkspaceDir = requestWorkspaceCandidate;
2781
+ }
2782
+ const requestWorkspaceDir = resolveWorkspaceDir(
2783
+ firstNonEmptyString([
2784
+ requestWorkspaceCandidate,
2785
+ sessionWorkspaceDir,
2786
+ args.workspaceDir,
2787
+ process.cwd(),
2788
+ ]),
2789
+ );
2790
+ let autoSyncSummary = null;
2791
+ if (isJsonRpcMethod(requestObj, "tools/call")) {
2792
+ autoSyncSummary = await maybeAutoSyncCtxpackForCall({
2793
+ requestObj,
2794
+ toolName,
2795
+ toolArgs,
2796
+ args,
2797
+ token,
2798
+ workspaceDir: requestWorkspaceDir,
2799
+ });
2800
+ }
2342
2801
  if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) {
2343
2802
  const projectID = String(toolArgs.project_id || toolArgs.projectID || args.projectID || "").trim();
2344
2803
  if (!projectID) {
@@ -2373,7 +2832,7 @@ async function runProxy(flags) {
2373
2832
  timeoutSeconds: args.timeoutSeconds,
2374
2833
  includeCtxpack,
2375
2834
  syncCtxpackLocal,
2376
- workspaceDir: args.workspaceDir,
2835
+ workspaceDir: requestWorkspaceDir,
2377
2836
  });
2378
2837
  const text = buildProjectSummaryText(summary);
2379
2838
  process.stdout.write(
@@ -2398,11 +2857,32 @@ async function runProxy(flags) {
2398
2857
  }
2399
2858
 
2400
2859
  try {
2401
- const responseText = await postJSON(gatewayURL, args.timeoutSeconds, token, requestObj);
2860
+ const requestWithDefaults = injectCtxpackPushDefaults(requestObj, toolName, requestWorkspaceDir);
2861
+ const outboundRequestObj = await injectCtxpackPreflightToken(
2862
+ requestWithDefaults,
2863
+ toolName,
2864
+ toolArgs,
2865
+ args,
2866
+ token,
2867
+ requestWorkspaceDir,
2868
+ );
2869
+ const responseText = await postJSON(gatewayURL, args.timeoutSeconds, token, outboundRequestObj);
2402
2870
  if (responseText) {
2403
2871
  const parsed = parseGatewayResponseText(responseText);
2404
- if (parsed && !parsed.error) {
2872
+ if (parsed) {
2405
2873
  let patched = parsed;
2874
+ if (parsed.error) {
2875
+ patched = await appendCtxpackConflictHintToErrorResponse(
2876
+ patched,
2877
+ args,
2878
+ toolName,
2879
+ toolArgs,
2880
+ token,
2881
+ requestWorkspaceDir,
2882
+ );
2883
+ process.stdout.write(`${JSON.stringify(patched)}\n`);
2884
+ return;
2885
+ }
2406
2886
  if (isJsonRpcMethod(requestObj, "tools/list")) {
2407
2887
  patched = appendLocalToolToToolsList(patched);
2408
2888
  } else if (isJsonRpcMethod(requestObj, "initialize")) {
@@ -2410,7 +2890,10 @@ async function runProxy(flags) {
2410
2890
  } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "workitem.list") {
2411
2891
  patched = await appendWorkitemListHints(patched, args, toolArgs, token);
2412
2892
  } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "ctxpack.ensure") {
2413
- patched = appendCtxpackEnsureSyncHints(patched, args, toolArgs);
2893
+ patched = appendCtxpackEnsureSyncHints(patched, args, toolArgs, requestObj);
2894
+ }
2895
+ if (autoSyncSummary) {
2896
+ patched = appendAutoCtxpackSyncHint(patched, autoSyncSummary);
2414
2897
  }
2415
2898
  process.stdout.write(`${JSON.stringify(patched)}\n`);
2416
2899
  return;
@@ -2495,13 +2978,27 @@ function resolveSetupContext(flags) {
2495
2978
  const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
2496
2979
  const ctxpackKey = String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim();
2497
2980
  const baseURL = String(flags["base-url"] || DEFAULT_SITE_URL).trim().replace(/\/+$/, "");
2498
- const workspaceDir = resolveWorkspaceDir(flags["workspace-dir"] || process.cwd());
2981
+ const workspaceDirRaw = String(flags["workspace-dir"] || "").trim();
2982
+ const hasExplicitWorkspaceDir = workspaceDirRaw.length > 0;
2983
+ const workspaceDir = resolveWorkspaceDir(hasExplicitWorkspaceDir ? workspaceDirRaw : process.cwd());
2499
2984
  const serverName = String(flags.name || DEFAULT_SERVER_NAME).trim() || DEFAULT_SERVER_NAME;
2500
2985
  const proxyArgs = ["--base-url", `${baseURL}/governance/mcp`];
2501
- proxyArgs.push("--workspace-dir", workspaceDir);
2986
+ // Default mode: do not pin workspace path in MCP registration.
2987
+ // Let client runtime cwd resolve per open folder/workspace.
2988
+ if (hasExplicitWorkspaceDir) {
2989
+ proxyArgs.push("--workspace-dir", workspaceDir);
2990
+ }
2502
2991
  if (projectID) proxyArgs.push("--project-id", projectID);
2503
2992
  if (ctxpackKey) proxyArgs.push("--ctxpack-key", ctxpackKey);
2504
- return { projectID, ctxpackKey, baseURL, workspaceDir, serverName, proxyArgs };
2993
+ return {
2994
+ projectID,
2995
+ ctxpackKey,
2996
+ baseURL,
2997
+ workspaceDir,
2998
+ hasExplicitWorkspaceDir,
2999
+ serverName,
3000
+ proxyArgs,
3001
+ };
2505
3002
  }
2506
3003
 
2507
3004
  function runSetupInternal(flags, options = {}) {
@@ -2530,7 +3027,9 @@ function runSetupInternal(flags, options = {}) {
2530
3027
  process.stdout.write(ensureOnly ? "\nEnsure complete.\n" : "\nInstall complete.\n");
2531
3028
  process.stdout.write(`Server: ${context.serverName}\n`);
2532
3029
  process.stdout.write(`Gateway: ${context.baseURL}/governance/mcp\n`);
2533
- process.stdout.write(`Workspace: ${context.workspaceDir}\n`);
3030
+ process.stdout.write(
3031
+ `Workspace: ${context.hasExplicitWorkspaceDir ? context.workspaceDir : "auto (client current folder)"}\n`,
3032
+ );
2534
3033
  process.stdout.write(`Project: ${context.projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
2535
3034
  if (context.ctxpackKey) {
2536
3035
  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.5",
3
+ "version": "0.2.7",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [