metheus-governance-mcp-cli 0.2.11 → 0.2.13

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 +201 -18
  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
 
@@ -30,12 +35,12 @@ function printUsage() {
30
35
  "Metheus Governance MCP CLI",
31
36
  "",
32
37
  "Usage:",
33
- ` ${cmd} [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path>] [--flow <auto|device|callback|manual>]`,
38
+ ` ${cmd} [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--flow <auto|device|callback|manual>]`,
34
39
  ` ${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>]`,
40
+ ` ${cmd} setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--name <server_name>]`,
36
41
  ` ${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>]`,
42
+ ` ${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>]`,
43
+ ` ${cmd} ctxpack pull [--project-id <uuid>] [--base-url <url>] [--workspace-dir <path|auto>] [--paths <csv>] [--timeout-seconds <n>]`,
39
44
  ` ${cmd} auth status`,
40
45
  ` ${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
46
  ` ${cmd} auth set --token <jwt> [--refresh-token <token>] [--base-url <url>]`,
@@ -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) {
@@ -113,6 +285,13 @@ function resolveWorkspaceDir(rawPath) {
113
285
  return path.resolve(process.cwd());
114
286
  }
115
287
 
288
+ function isAutoWorkspaceMode(rawValue) {
289
+ const value = String(rawValue || "")
290
+ .trim()
291
+ .toLowerCase();
292
+ return value === "auto" || value === "dynamic" || value === "client";
293
+ }
294
+
116
295
  function fileURIToLocalPath(rawValue) {
117
296
  const value = String(rawValue || "").trim();
118
297
  if (!value || !/^file:\/\//i.test(value)) return "";
@@ -3832,13 +4011,17 @@ function resolveSetupContext(flags) {
3832
4011
  const ctxpackKey = String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim();
3833
4012
  const baseURL = String(flags["base-url"] || DEFAULT_SITE_URL).trim().replace(/\/+$/, "");
3834
4013
  const workspaceDirRaw = String(flags["workspace-dir"] || "").trim();
3835
- const hasExplicitWorkspaceDir = workspaceDirRaw.length > 0;
3836
- const workspaceDir = resolveWorkspaceDir(hasExplicitWorkspaceDir ? workspaceDirRaw : process.cwd());
4014
+ const workspaceAutoMode = isAutoWorkspaceMode(workspaceDirRaw);
4015
+ const hasWorkspaceDirFlag = workspaceDirRaw.length > 0;
4016
+ const shouldPinWorkspaceDir = !workspaceAutoMode;
4017
+ const workspaceDir = resolveWorkspaceDir(
4018
+ hasWorkspaceDirFlag && !workspaceAutoMode ? workspaceDirRaw : process.cwd(),
4019
+ );
3837
4020
  const serverName = String(flags.name || DEFAULT_SERVER_NAME).trim() || DEFAULT_SERVER_NAME;
3838
4021
  const proxyArgs = ["--base-url", `${baseURL}/governance/mcp`];
3839
- // Default mode: do not pin workspace path in MCP registration.
3840
- // Let client runtime cwd resolve per open folder/workspace.
3841
- if (hasExplicitWorkspaceDir) {
4022
+ // Default mode pins workspace path to where setup/init runs.
4023
+ // Use --workspace-dir auto when runtime auto-detection is preferred.
4024
+ if (shouldPinWorkspaceDir) {
3842
4025
  proxyArgs.push("--workspace-dir", workspaceDir);
3843
4026
  }
3844
4027
  if (projectID) proxyArgs.push("--project-id", projectID);
@@ -3848,7 +4031,8 @@ function resolveSetupContext(flags) {
3848
4031
  ctxpackKey,
3849
4032
  baseURL,
3850
4033
  workspaceDir,
3851
- hasExplicitWorkspaceDir,
4034
+ shouldPinWorkspaceDir,
4035
+ workspaceAutoMode,
3852
4036
  serverName,
3853
4037
  proxyArgs,
3854
4038
  };
@@ -3880,9 +4064,7 @@ function runSetupInternal(flags, options = {}) {
3880
4064
  process.stdout.write(ensureOnly ? "\nEnsure complete.\n" : "\nInstall complete.\n");
3881
4065
  process.stdout.write(`Server: ${context.serverName}\n`);
3882
4066
  process.stdout.write(`Gateway: ${context.baseURL}/governance/mcp\n`);
3883
- process.stdout.write(
3884
- `Workspace: ${context.hasExplicitWorkspaceDir ? context.workspaceDir : "auto (client current folder)"}\n`,
3885
- );
4067
+ process.stdout.write(`Workspace: ${context.shouldPinWorkspaceDir ? context.workspaceDir : "auto (client current folder)"}\n`);
3886
4068
  process.stdout.write(`Project: ${context.projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
3887
4069
  if (context.ctxpackKey) {
3888
4070
  process.stdout.write(`Ctxpack: ${context.ctxpackKey}\n`);
@@ -3923,6 +4105,7 @@ async function runBootstrap(flags) {
3923
4105
  async function main() {
3924
4106
  const [, , rawCommand, ...rest] = process.argv;
3925
4107
  const command = String(rawCommand || "");
4108
+ await maybeAutoUpdate(command, process.argv.slice(2));
3926
4109
 
3927
4110
  if (command === "-v" || command === "--version" || command === "version") {
3928
4111
  printVersion();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [