metheus-governance-mcp-cli 0.2.0 → 0.2.1

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 +16 -1
  2. package/cli.mjs +256 -32
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  Metheus Governance MCP helper CLI.
4
4
 
5
+ - `metheus-governance-mcp` (no args): bootstrap mode
6
+ - checks auth token
7
+ - if token is missing/expired, starts `auth login`
8
+ - checks Codex/Claude MCP registration
9
+ - registers only missing clients
5
10
  - `setup`: register `metheus-governance-mcp` into Codex/Claude (if installed)
6
11
  - `proxy`: stdio MCP bridge to Metheus HTTPS gateway
7
12
  - `auth`: save/check/clear local Metheus token used by proxy
@@ -12,6 +17,16 @@ Metheus Governance MCP helper CLI.
12
17
  npm install -g metheus-governance-mcp-cli
13
18
  ```
14
19
 
20
+ ## One command bootstrap (recommended)
21
+
22
+ Run in your project folder:
23
+
24
+ ```bash
25
+ metheus-governance-mcp --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
26
+ ```
27
+
28
+ If auth is not ready, login starts automatically, then MCP registration is completed.
29
+
15
30
  ## Setup
16
31
 
17
32
  ```bash
@@ -63,7 +78,7 @@ metheus-governance-mcp auth status
63
78
  3. If token is rotated, set directly:
64
79
 
65
80
  ```bash
66
- metheus-governance-mcp auth set --token "<access_token>"
81
+ metheus-governance-mcp auth set --token "<access_token>" --refresh-token "<refresh_token>"
67
82
  ```
68
83
 
69
84
  4. Remove local token:
package/cli.mjs CHANGED
@@ -14,6 +14,9 @@ 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 CLI_META = loadCLIMeta();
18
+ const CLI_NAME = CLI_META.name || "metheus-governance-mcp-cli";
19
+ const CLI_VERSION = CLI_META.version || "0.0.0";
17
20
 
18
21
  function printUsage() {
19
22
  process.stderr.write(
@@ -21,12 +24,15 @@ function printUsage() {
21
24
  "Metheus Governance MCP CLI",
22
25
  "",
23
26
  "Usage:",
27
+ " metheus-governance-mcp [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
28
+ " metheus-governance-mcp init [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
24
29
  " metheus-governance-mcp setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--name <server_name>]",
25
30
  " metheus-governance-mcp proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--include-drafts <true|false>] [--timeout-seconds <n>]",
26
31
  " metheus-governance-mcp auth status",
27
32
  " 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>]",
28
- " metheus-governance-mcp auth set --token <jwt> [--base-url <url>]",
33
+ " metheus-governance-mcp auth set --token <jwt> [--refresh-token <token>] [--base-url <url>]",
29
34
  " metheus-governance-mcp auth clear",
35
+ " metheus-governance-mcp -v | --version",
30
36
  "",
31
37
  "Environment:",
32
38
  " METHEUS_TOKEN or MCP_AUTH_TOKEN is used first for proxy requests.",
@@ -37,6 +43,24 @@ function printUsage() {
37
43
  );
38
44
  }
39
45
 
46
+ function loadCLIMeta() {
47
+ try {
48
+ const dir = path.dirname(fileURLToPath(import.meta.url));
49
+ const raw = fs.readFileSync(path.join(dir, "package.json"), "utf8");
50
+ const parsed = JSON.parse(raw);
51
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
52
+ return {};
53
+ }
54
+ return parsed;
55
+ } catch {
56
+ return {};
57
+ }
58
+ }
59
+
60
+ function printVersion() {
61
+ process.stdout.write(`${CLI_NAME} ${CLI_VERSION}\n`);
62
+ }
63
+
40
64
  function parseArgs(argv) {
41
65
  const out = {};
42
66
  for (let i = 0; i < argv.length; i += 1) {
@@ -82,29 +106,31 @@ function loadStoredAuth() {
82
106
  const filePath = authStoreFilePath();
83
107
  try {
84
108
  if (!fs.existsSync(filePath)) {
85
- return { filePath, token: "", baseURL: "", updatedAt: "" };
109
+ return { filePath, token: "", refreshToken: "", baseURL: "", updatedAt: "" };
86
110
  }
87
111
  const raw = fs.readFileSync(filePath, "utf8");
88
112
  const parsed = JSON.parse(raw);
89
113
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
90
- return { filePath, token: "", baseURL: "", updatedAt: "" };
114
+ return { filePath, token: "", refreshToken: "", baseURL: "", updatedAt: "" };
91
115
  }
92
116
  return {
93
117
  filePath,
94
118
  token: normalizeToken(parsed.token),
119
+ refreshToken: normalizeToken(parsed.refresh_token),
95
120
  baseURL: String(parsed.base_url || "").trim(),
96
121
  updatedAt: String(parsed.updated_at || "").trim(),
97
122
  };
98
123
  } catch {
99
- return { filePath, token: "", baseURL: "", updatedAt: "" };
124
+ return { filePath, token: "", refreshToken: "", baseURL: "", updatedAt: "" };
100
125
  }
101
126
  }
102
127
 
103
- function saveStoredAuth({ token, baseURL }) {
128
+ function saveStoredAuth({ token, refreshToken, baseURL }) {
104
129
  const filePath = authStoreFilePath();
105
130
  ensureParentDir(filePath);
106
131
  const payload = {
107
132
  token: normalizeToken(token),
133
+ refresh_token: normalizeToken(refreshToken),
108
134
  base_url: String(baseURL || "").trim(),
109
135
  updated_at: new Date().toISOString(),
110
136
  };
@@ -512,6 +538,45 @@ async function exchangeAuthorizationCodeForToken({
512
538
  return parsed;
513
539
  }
514
540
 
541
+ async function exchangeRefreshTokenForToken({
542
+ tokenEndpoint,
543
+ clientID,
544
+ refreshToken,
545
+ timeoutSeconds,
546
+ }) {
547
+ const params = new URLSearchParams({
548
+ grant_type: "refresh_token",
549
+ client_id: String(clientID || "").trim(),
550
+ refresh_token: String(refreshToken || "").trim(),
551
+ });
552
+
553
+ const response = await fetch(tokenEndpoint, {
554
+ method: "POST",
555
+ headers: {
556
+ "content-type": "application/x-www-form-urlencoded",
557
+ accept: "application/json",
558
+ },
559
+ body: params.toString(),
560
+ redirect: "follow",
561
+ signal: AbortSignal.timeout(Math.max(5, intFromRaw(timeoutSeconds, 20)) * 1000),
562
+ });
563
+
564
+ const body = await response.text();
565
+ const parsed = tryJsonParse(body) || {};
566
+ if (!response.ok) {
567
+ const reason =
568
+ String(parsed.error_description || parsed.error || "").trim() ||
569
+ body.slice(0, 250) ||
570
+ `${response.status} ${response.statusText}`;
571
+ throw new Error(`refresh token exchange failed: ${reason}`);
572
+ }
573
+ const accessToken = normalizeToken(parsed.access_token);
574
+ if (!accessToken) {
575
+ throw new Error("refresh token exchange succeeded but access_token is missing");
576
+ }
577
+ return parsed;
578
+ }
579
+
515
580
  async function requestDeviceAuthorization({
516
581
  deviceAuthorizationEndpoint,
517
582
  clientID,
@@ -658,6 +723,48 @@ function resolveCurrentAccessToken() {
658
723
  };
659
724
  }
660
725
 
726
+ async function tryRefreshStoredAccessToken({ baseURL, timeoutSeconds }) {
727
+ const stored = loadStoredAuth();
728
+ const refreshCandidates = [
729
+ { source: "METHEUS_REFRESH_TOKEN", token: normalizeToken(process.env.METHEUS_REFRESH_TOKEN) },
730
+ { source: "MCP_AUTH_REFRESH_TOKEN", token: normalizeToken(process.env.MCP_AUTH_REFRESH_TOKEN) },
731
+ { source: `${stored.filePath}:refresh_token`, token: normalizeToken(stored.refreshToken) },
732
+ ];
733
+ const refreshRow = refreshCandidates.find((row) => Boolean(row.token));
734
+ if (!refreshRow) {
735
+ return { ok: false, error: "no refresh token configured" };
736
+ }
737
+
738
+ const siteBaseURL = normalizeSiteBaseURL(baseURL || stored.baseURL || DEFAULT_SITE_URL);
739
+ try {
740
+ const oidc = await discoverOIDCConfig({ siteBaseURL });
741
+ const payload = await exchangeRefreshTokenForToken({
742
+ tokenEndpoint: oidc.tokenEndpoint,
743
+ clientID: oidc.clientID,
744
+ refreshToken: refreshRow.token,
745
+ timeoutSeconds: intFromRaw(timeoutSeconds, 20),
746
+ });
747
+
748
+ const accessToken = normalizeToken(payload.access_token);
749
+ const nextRefreshToken = normalizeToken(payload.refresh_token) || refreshRow.token;
750
+ const filePath = saveStoredAuth({
751
+ token: accessToken,
752
+ refreshToken: nextRefreshToken,
753
+ baseURL: siteBaseURL,
754
+ });
755
+
756
+ return {
757
+ ok: true,
758
+ token: accessToken,
759
+ source: filePath,
760
+ refreshSource: refreshRow.source,
761
+ expires: tokenExpiryIso(accessToken),
762
+ };
763
+ } catch (err) {
764
+ return { ok: false, error: String(err?.message || err), refreshSource: refreshRow.source };
765
+ }
766
+ }
767
+
661
768
  function currentAccessToken() {
662
769
  return resolveCurrentAccessToken().token;
663
770
  }
@@ -755,6 +862,10 @@ async function runAuthStatus() {
755
862
  const token = resolved.token;
756
863
  const source = resolved.source;
757
864
  const stored = resolved.stored;
865
+ const refreshToken =
866
+ normalizeToken(process.env.METHEUS_REFRESH_TOKEN) ||
867
+ normalizeToken(process.env.MCP_AUTH_REFRESH_TOKEN) ||
868
+ normalizeToken(stored.refreshToken);
758
869
  if (!token) {
759
870
  process.stdout.write("Auth token: missing\n");
760
871
  if (resolved.expired?.source) {
@@ -767,6 +878,7 @@ async function runAuthStatus() {
767
878
  process.stdout.write(
768
879
  "Run: metheus-governance-mcp auth login --base-url https://metheus.gesiaplatform.com\n",
769
880
  );
881
+ process.stdout.write(`Refresh token: ${refreshToken ? "configured" : "missing"}\n`);
770
882
  process.exitCode = 1;
771
883
  return;
772
884
  }
@@ -782,10 +894,18 @@ async function runAuthStatus() {
782
894
  process.stdout.write(`Stored at: ${stored.updatedAt}\n`);
783
895
  }
784
896
  }
897
+ process.stdout.write(`Refresh token: ${refreshToken ? "configured" : "missing"}\n`);
785
898
  }
786
899
 
787
900
  async function runAuthSet(flags) {
901
+ const stored = loadStoredAuth();
788
902
  const rawToken = normalizeToken(flags.token || flags["access-token"] || "");
903
+ const rawRefreshToken = normalizeToken(
904
+ flags["refresh-token"] ||
905
+ process.env.METHEUS_REFRESH_TOKEN ||
906
+ process.env.MCP_AUTH_REFRESH_TOKEN ||
907
+ "",
908
+ );
789
909
  let token = rawToken;
790
910
  if (!token && process.stdin.isTTY) {
791
911
  token = normalizeToken(await promptLine("Paste Metheus access token (JWT): "));
@@ -796,15 +916,20 @@ async function runAuthSet(flags) {
796
916
  return;
797
917
  }
798
918
  const baseURL = normalizeSiteBaseURL(flags["base-url"] || DEFAULT_SITE_URL);
799
- const filePath = saveStoredAuth({ token, baseURL });
919
+ const refreshToken = rawRefreshToken || normalizeToken(stored.refreshToken);
920
+ const filePath = saveStoredAuth({ token, refreshToken, baseURL });
800
921
  const exp = tokenExpiryIso(token);
801
922
  process.stdout.write(`Token saved: ${filePath}\n`);
802
923
  if (exp) {
803
924
  process.stdout.write(`Token expires: ${exp}\n`);
804
925
  }
926
+ if (refreshToken) {
927
+ process.stdout.write("Refresh token saved.\n");
928
+ }
805
929
  }
806
930
 
807
931
  async function runAuthLoginManual(flags) {
932
+ const stored = loadStoredAuth();
808
933
  const baseURL = normalizeSiteBaseURL(flags["base-url"] || DEFAULT_SITE_URL);
809
934
  const loginURL = authLoginURL(baseURL);
810
935
  const shouldOpen = boolFromRaw(flags["open-browser"], true);
@@ -822,7 +947,11 @@ async function runAuthLoginManual(flags) {
822
947
  process.exitCode = 1;
823
948
  return;
824
949
  }
825
- const filePath = saveStoredAuth({ token, baseURL });
950
+ const filePath = saveStoredAuth({
951
+ token,
952
+ refreshToken: normalizeToken(stored.refreshToken),
953
+ baseURL,
954
+ });
826
955
  const exp = tokenExpiryIso(token);
827
956
  process.stdout.write(`Token saved: ${filePath}\n`);
828
957
  if (exp) {
@@ -899,12 +1028,16 @@ async function runAuthLoginCallback(flags, baseURL, oidc) {
899
1028
  }
900
1029
 
901
1030
  const token = normalizeToken(tokenPayload.access_token);
902
- const filePath = saveStoredAuth({ token, baseURL });
1031
+ const refreshToken = normalizeToken(tokenPayload.refresh_token);
1032
+ const filePath = saveStoredAuth({ token, refreshToken, baseURL });
903
1033
  const exp = tokenExpiryIso(token);
904
1034
  process.stdout.write(`Token saved: ${filePath}\n`);
905
1035
  if (exp) {
906
1036
  process.stdout.write(`Token expires: ${exp}\n`);
907
1037
  }
1038
+ if (refreshToken) {
1039
+ process.stdout.write("Refresh token saved.\n");
1040
+ }
908
1041
  process.stdout.write("Auth login complete (callback flow).\n");
909
1042
  process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
910
1043
  }
@@ -962,12 +1095,16 @@ async function runAuthLoginDevice(flags, baseURL, oidc) {
962
1095
  });
963
1096
 
964
1097
  const token = normalizeToken(tokenPayload.access_token);
965
- const filePath = saveStoredAuth({ token, baseURL });
1098
+ const refreshToken = normalizeToken(tokenPayload.refresh_token);
1099
+ const filePath = saveStoredAuth({ token, refreshToken, baseURL });
966
1100
  const exp = tokenExpiryIso(token);
967
1101
  process.stdout.write(`Token saved: ${filePath}\n`);
968
1102
  if (exp) {
969
1103
  process.stdout.write(`Token expires: ${exp}\n`);
970
1104
  }
1105
+ if (refreshToken) {
1106
+ process.stdout.write("Refresh token saved.\n");
1107
+ }
971
1108
  process.stdout.write("Auth login complete (device flow).\n");
972
1109
  process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
973
1110
  }
@@ -1132,6 +1269,8 @@ async function runProxy(flags) {
1132
1269
  timeoutSeconds: intFromRaw(flags["timeout-seconds"], 30),
1133
1270
  };
1134
1271
  const gatewayURL = buildGatewayURL(args);
1272
+ let lastRefreshAttemptAtMs = 0;
1273
+ let lastRefreshError = "";
1135
1274
 
1136
1275
  const rl = readline.createInterface({
1137
1276
  input: process.stdin,
@@ -1148,7 +1287,24 @@ async function runProxy(flags) {
1148
1287
  return;
1149
1288
  }
1150
1289
 
1151
- const resolved = resolveCurrentAccessToken();
1290
+ let resolved = resolveCurrentAccessToken();
1291
+ if (!resolved.token) {
1292
+ const nowMs = Date.now();
1293
+ // Throttle refresh attempts to avoid hammering token endpoint on repeated requests.
1294
+ if (nowMs - lastRefreshAttemptAtMs > 10000) {
1295
+ lastRefreshAttemptAtMs = nowMs;
1296
+ const refreshed = await tryRefreshStoredAccessToken({
1297
+ baseURL: args.baseURL,
1298
+ timeoutSeconds: args.timeoutSeconds,
1299
+ });
1300
+ if (refreshed.ok) {
1301
+ resolved = resolveCurrentAccessToken();
1302
+ lastRefreshError = "";
1303
+ } else {
1304
+ lastRefreshError = String(refreshed.error || "").trim();
1305
+ }
1306
+ }
1307
+ }
1152
1308
  const token = resolved.token;
1153
1309
  if (!token) {
1154
1310
  const helpURL = authLoginURL(args.baseURL);
@@ -1156,6 +1312,7 @@ async function runProxy(flags) {
1156
1312
  resolved.expired?.source
1157
1313
  ? `; expired token ignored: ${resolved.expired.source}${resolved.expired.expires ? ` (${resolved.expired.expires})` : ""}`
1158
1314
  : "";
1315
+ const refreshHint = lastRefreshError ? `; refresh failed: ${lastRefreshError}` : "";
1159
1316
  process.stdout.write(
1160
1317
  `${JSON.stringify(
1161
1318
  jsonRpcError(
@@ -1163,7 +1320,7 @@ async function runProxy(flags) {
1163
1320
  -32001,
1164
1321
  `missing auth token (set METHEUS_TOKEN or run 'metheus-governance-mcp auth login --base-url ${normalizeSiteBaseURL(
1165
1322
  args.baseURL,
1166
- )}'; login: ${helpURL}${expiredHint})`,
1323
+ )}'; login: ${helpURL}${expiredHint}${refreshHint})`,
1167
1324
  ),
1168
1325
  )}\n`,
1169
1326
  );
@@ -1233,10 +1390,10 @@ function tryRegister(cliBin, serverName, proxyArgs) {
1233
1390
 
1234
1391
  function runRemove(cliBin, serverName) {
1235
1392
  if (cliBin === "claude") {
1236
- runCLICommand(cliBin, ["mcp", "remove", serverName, "--scope", "user"], {
1393
+ runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "user"], {
1237
1394
  stdio: "ignore",
1238
1395
  });
1239
- runCLICommand(cliBin, ["mcp", "remove", serverName, "--scope", "local"], {
1396
+ runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "local"], {
1240
1397
  stdio: "ignore",
1241
1398
  });
1242
1399
  return;
@@ -1244,51 +1401,118 @@ function runRemove(cliBin, serverName) {
1244
1401
  runCLICommand(cliBin, ["mcp", "remove", serverName], { stdio: "ignore" });
1245
1402
  }
1246
1403
 
1247
- function runSetup(flags) {
1404
+ function isRegistered(cliBin, serverName) {
1405
+ const args =
1406
+ cliBin === "claude"
1407
+ ? ["mcp", "get", serverName]
1408
+ : ["mcp", "get", serverName, "--json"];
1409
+ const run = runCLICommand(cliBin, args, { stdio: "ignore" });
1410
+ return run.status === 0;
1411
+ }
1412
+
1413
+ function resolveSetupContext(flags) {
1248
1414
  const workspaceMeta = loadWorkspaceMeta(process.cwd());
1249
1415
  const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
1250
1416
  const ctxpackKey = String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim();
1251
- const baseURL = String(flags["base-url"] || "https://metheus.gesiaplatform.com").trim().replace(/\/+$/, "");
1417
+ const baseURL = String(flags["base-url"] || DEFAULT_SITE_URL).trim().replace(/\/+$/, "");
1252
1418
  const serverName = String(flags.name || DEFAULT_SERVER_NAME).trim() || DEFAULT_SERVER_NAME;
1253
-
1254
1419
  const proxyArgs = ["--base-url", `${baseURL}/governance/mcp`];
1255
1420
  if (projectID) proxyArgs.push("--project-id", projectID);
1256
1421
  if (ctxpackKey) proxyArgs.push("--ctxpack-key", ctxpackKey);
1422
+ return { projectID, ctxpackKey, baseURL, serverName, proxyArgs };
1423
+ }
1257
1424
 
1425
+ function runSetupInternal(flags, options = {}) {
1426
+ const ensureOnly = Boolean(options.ensureOnly);
1427
+ const context = resolveSetupContext(flags);
1258
1428
  const clients = ["codex", "claude"];
1259
1429
  const results = [];
1430
+
1260
1431
  for (const cliBin of clients) {
1261
1432
  if (!commandExists(cliBin)) continue;
1262
- runRemove(cliBin, serverName);
1263
- const ok = tryRegister(cliBin, serverName, proxyArgs);
1264
- results.push({ cliBin, ok });
1433
+ const alreadyRegistered = isRegistered(cliBin, context.serverName);
1434
+ if (ensureOnly && alreadyRegistered) {
1435
+ results.push({ cliBin, ok: true, action: "already-registered" });
1436
+ continue;
1437
+ }
1438
+ if (!ensureOnly) {
1439
+ runRemove(cliBin, context.serverName);
1440
+ }
1441
+ const ok = tryRegister(cliBin, context.serverName, context.proxyArgs);
1442
+ results.push({ cliBin, ok, action: alreadyRegistered ? "re-registered" : "registered" });
1265
1443
  }
1266
1444
 
1267
- process.stdout.write("\nInstall complete.\n");
1268
- process.stdout.write(`Server: ${serverName}\n`);
1269
- process.stdout.write(`Gateway: ${baseURL}/governance/mcp\n`);
1270
- process.stdout.write(`Project: ${projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
1271
- if (ctxpackKey) {
1272
- process.stdout.write(`Ctxpack: ${ctxpackKey}\n`);
1445
+ process.stdout.write(ensureOnly ? "\nEnsure complete.\n" : "\nInstall complete.\n");
1446
+ process.stdout.write(`Server: ${context.serverName}\n`);
1447
+ process.stdout.write(`Gateway: ${context.baseURL}/governance/mcp\n`);
1448
+ process.stdout.write(`Project: ${context.projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
1449
+ if (context.ctxpackKey) {
1450
+ process.stdout.write(`Ctxpack: ${context.ctxpackKey}\n`);
1273
1451
  }
1274
1452
  if (results.length === 0) {
1275
1453
  process.stdout.write("No codex/claude CLI found. Registration skipped.\n");
1276
- process.stdout.write("Set METHEUS_TOKEN, then register manually if needed.\n");
1277
- } else {
1278
- for (const row of results) {
1279
- process.stdout.write(`${row.cliBin}: ${row.ok ? "registered" : "registration failed"}\n`);
1454
+ return;
1455
+ }
1456
+ for (const row of results) {
1457
+ process.stdout.write(`${row.cliBin}: ${row.ok ? row.action : "registration failed"}\n`);
1458
+ }
1459
+ }
1460
+
1461
+ function runSetup(flags) {
1462
+ runSetupInternal(flags, { ensureOnly: false });
1463
+ }
1464
+
1465
+ async function runBootstrap(flags) {
1466
+ process.stdout.write("Bootstrap start.\n");
1467
+ let resolved = resolveCurrentAccessToken();
1468
+ if (!resolved.token) {
1469
+ process.stdout.write("Auth token missing/expired. Starting login flow...\n");
1470
+ await runAuthLogin(flags);
1471
+ resolved = resolveCurrentAccessToken();
1472
+ if (!resolved.token) {
1473
+ process.stderr.write("Auth login finished but token is still unavailable.\n");
1474
+ process.exitCode = 1;
1475
+ return;
1280
1476
  }
1477
+ } else {
1478
+ process.stdout.write(`Auth token OK (${resolved.source}).\n`);
1281
1479
  }
1480
+
1481
+ runSetupInternal(flags, { ensureOnly: true });
1482
+ process.stdout.write("Bootstrap done.\n");
1282
1483
  }
1283
1484
 
1284
1485
  async function main() {
1285
- const [, , command, ...rest] = process.argv;
1286
- if (!command || command === "-h" || command === "--help") {
1486
+ const [, , rawCommand, ...rest] = process.argv;
1487
+ const command = String(rawCommand || "");
1488
+
1489
+ if (command === "-v" || command === "--version" || command === "version") {
1490
+ printVersion();
1491
+ return;
1492
+ }
1493
+
1494
+ if (!command) {
1495
+ await runBootstrap({});
1496
+ return;
1497
+ }
1498
+
1499
+ if (command === "-h" || command === "--help") {
1287
1500
  printUsage();
1288
- process.exit(command ? 0 : 1);
1501
+ process.exit(0);
1289
1502
  return;
1290
1503
  }
1504
+
1505
+ if (command.startsWith("--")) {
1506
+ const flags = parseArgs([command, ...rest]);
1507
+ await runBootstrap(flags);
1508
+ return;
1509
+ }
1510
+
1291
1511
  const flags = parseArgs(rest);
1512
+ if (command === "init") {
1513
+ await runBootstrap(flags);
1514
+ return;
1515
+ }
1292
1516
  if (command === "setup") {
1293
1517
  runSetup(flags);
1294
1518
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [
@@ -16,7 +16,8 @@
16
16
  "publish:public": "node release.mjs"
17
17
  },
18
18
  "bin": {
19
- "metheus-governance-mcp": "bin/metheus-governance-mcp.js"
19
+ "metheus-governance-mcp": "bin/metheus-governance-mcp.js",
20
+ "metheus-governance-mcp-cli": "bin/metheus-governance-mcp.js"
20
21
  },
21
22
  "engines": {
22
23
  "node": ">=18.0.0"