metheus-governance-mcp-cli 0.2.69 → 0.2.70

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.
package/README.md CHANGED
@@ -55,7 +55,8 @@ TELEGRAM_BOT_TOKEN=
55
55
 
56
56
  # Preferred named bot mapping
57
57
  TELEGRAM_BOT_MAIN_SERVER_BOT_ID=
58
- TELEGRAM_BOT_MAIN_USERNAME=<bot_username>
58
+ # Optional fallback only when server bot binding is unavailable
59
+ # TELEGRAM_BOT_MAIN_USERNAME=<bot_username>
59
60
  TELEGRAM_BOT_MAIN_TOKEN=
60
61
  TELEGRAM_BOT_MAIN_ROLE_PROFILE=monitor
61
62
  TELEGRAM_BOT_MAIN_AI_CLIENT=codex
@@ -225,9 +226,11 @@ metheus-governance-mcp-cli bot verify --provider telegram --bot-key main
225
226
  Behavior:
226
227
 
227
228
  - `bot setup` asks for `Telegram / Slack / KakaoTalk` first, then prompts with numbered actions.
228
- - `bot add` without flags starts a guided question flow: provider -> bot responsibility -> local bot key -> token -> verify.
229
- - For Telegram, the CLI asks for the server-side bot responsibility first and automatically resolves the matching server bot profile when the role is unique.
230
- - When you choose a Telegram responsibility such as `monitor`, the CLI automatically uses that server role as the local `role_profile` and fills blank AI defaults from your local `bot-runner.json` `role_profiles` mapping.
229
+ - `bot add` without flags now uses the shortest practical guided flow: provider -> token -> verify -> optional save-anyway only when verify fails -> optional username fallback only when verify cannot discover it -> optional default bot.
230
+ - In the normal Telegram path, `bot add` does not ask for a local bot key, a server bot UUID, or an approval / worker / review / monitor choice.
231
+ - For Telegram, the local env key is auto-generated from the matched server bot name or verified username, so you do not have to invent a separate local nickname first.
232
+ - For Telegram, the CLI tries to match the verified bot identity against the server `me/bots` list first. If the server exposes one logical bot name with multiple roles such as `approval`, `worker`, `review`, and `monitor`, the CLI does not ask you to choose one UUID. It binds by server bot name and keeps the local role/AI fields empty so runtime can use the server bot role for each route.
233
+ - When the Telegram username matches exactly one server bot role, the CLI still auto-fills the local `role_profile` and blank AI defaults from your local `bot-runner.json` `role_profiles` mapping.
231
234
  - `bot edit` without flags starts a guided numbered flow: provider -> bot entry -> guided edit -> step-by-step field choices.
232
235
  - In guided `bot edit`, changing the bound Telegram responsibility can also auto-fill blank local AI fields from the selected server role and skip redundant role/AI prompts.
233
236
  - `bot set-default` without flags starts a guided numbered flow: provider -> bot entry -> confirm default change.
@@ -235,7 +238,7 @@ Behavior:
235
238
  - `bot remove` without flags starts a guided numbered flow: provider -> bot entry -> confirm removal.
236
239
  - Telegram supports named local bot entries with:
237
240
  - `SERVER_BOT_ID`
238
- - `USERNAME`
241
+ - `USERNAME` (optional fallback only when server binding is unavailable)
239
242
  - `TOKEN`
240
243
  - `ROLE_PROFILE`
241
244
  - `AI_CLIENT`
@@ -253,7 +256,8 @@ Non-interactive examples:
253
256
 
254
257
  ```bash
255
258
  metheus-governance-mcp-cli bot global --provider telegram --non-interactive true --api-base-url http://127.0.0.1:8999/telegram --auto-clear-webhook false --allowed-updates message,edited_message,channel_post
256
- metheus-governance-mcp-cli bot add --provider telegram --non-interactive true --bot-key main --server-bot-id <server_bot_uuid> --username <telegram_username> --token <telegram_bot_token> --role-profile monitor --ai-client codex --ai-model gpt-5-codex --ai-permission-mode read_only --ai-reasoning-effort low --default true
259
+ metheus-governance-mcp-cli bot add --provider telegram --non-interactive true --server-bot-id <server_bot_uuid> --token <telegram_bot_token> --default true
260
+ metheus-governance-mcp-cli bot add --provider telegram --non-interactive true --server-bot-id <server_bot_uuid> --token <telegram_bot_token> --ai-client codex --ai-model gpt-5-codex --ai-permission-mode read_only --ai-reasoning-effort low
257
261
  metheus-governance-mcp-cli bot edit --provider telegram --bot-key main --non-interactive true --ai-client claude --ai-model claude-sonnet --ai-permission-mode workspace_write --ai-reasoning-effort medium
258
262
  metheus-governance-mcp-cli bot set-default --provider telegram --bot-key main --non-interactive true
259
263
  metheus-governance-mcp-cli bot migrate --provider telegram --bot-key main --server-bot-id <server_bot_uuid> --bot-name <telegram_username> --role-profile monitor --ai-client codex --ai-permission-mode read_only --ai-reasoning-effort low --non-interactive true
@@ -261,6 +265,8 @@ metheus-governance-mcp-cli bot remove --provider telegram --bot-key main --non-i
261
265
  metheus-governance-mcp-cli bot verify --provider telegram --bot-key main --json true
262
266
  ```
263
267
 
268
+ For direct Telegram adds, the CLI can derive the local entry key from the matched server bot name. You no longer need `--bot-key` or `--username` in the normal server-bound path. Treat `--bot-key` as an advanced override only when you intentionally want a different local suffix.
269
+
264
270
  Current support status:
265
271
 
266
272
  - Telegram: full local bot entry management, token verification, bot-to-AI binding, inbound runner support
package/cli.mjs CHANGED
@@ -748,7 +748,7 @@ function providerEnvTemplate(provider) {
748
748
  "# Preferred v2 multi-bot mapping:",
749
749
  "# TELEGRAM_DEFAULT_BOT_KEY=main",
750
750
  "# TELEGRAM_BOT_MAIN_SERVER_BOT_ID=<server bot uuid>",
751
- "# TELEGRAM_BOT_MAIN_USERNAME=<bot username>",
751
+ "# TELEGRAM_BOT_MAIN_USERNAME=<optional fallback username when server binding is unavailable>",
752
752
  "# TELEGRAM_BOT_MAIN_TOKEN=<bot token>",
753
753
  "# TELEGRAM_BOT_MAIN_ROLE_PROFILE=monitor",
754
754
  "# TELEGRAM_BOT_MAIN_AI_CLIENT=codex",
@@ -766,7 +766,6 @@ function providerEnvTemplate(provider) {
766
766
  "",
767
767
  "# Preferred named bot entry",
768
768
  "TELEGRAM_BOT_MAIN_SERVER_BOT_ID=",
769
- "TELEGRAM_BOT_MAIN_USERNAME=",
770
769
  "TELEGRAM_BOT_MAIN_TOKEN=",
771
770
  "TELEGRAM_BOT_MAIN_ROLE_PROFILE=",
772
771
  "TELEGRAM_BOT_MAIN_AI_CLIENT=",
@@ -2730,7 +2729,9 @@ function resolveTelegramEnvConfig(parsedEnv, filePath, config, selectors = {}) {
2730
2729
  selected = entries.find((entry) => entry.serverBotID === desiredBotID) || null;
2731
2730
  }
2732
2731
  if (!selected && desiredUsername) {
2733
- selected = entries.find((entry) => entry.username === desiredUsername) || null;
2732
+ selected = entries.find(
2733
+ (entry) => entry.username === desiredUsername || normalizeTelegramBotUsername(entry.key) === desiredUsername,
2734
+ ) || null;
2734
2735
  }
2735
2736
  if (!selected && desiredBotKey) {
2736
2737
  selected = entries.find((entry) => entry.key === desiredBotKey) || null;
@@ -3697,8 +3698,9 @@ async function runDoctor(flags) {
3697
3698
  }
3698
3699
  const detailParts = [];
3699
3700
  if (envConfig.botKey) detailParts.push(`bot=${envConfig.botKey}`);
3700
- if (envConfig.botUsername) detailParts.push(`@${envConfig.botUsername}`);
3701
- if (envConfig.serverBotID) detailParts.push(`server_bot_id=${envConfig.serverBotID}`);
3701
+ if (envConfig.botUsername) detailParts.push(`@${envConfig.botUsername}`);
3702
+ if (envConfig.serverBotID) detailParts.push(`server_bot_id=${envConfig.serverBotID}`);
3703
+ else if (envConfig.botKey) detailParts.push(`server_name=${envConfig.botKey}`);
3702
3704
  if (envConfig.client) detailParts.push(`ai=${envConfig.client}`);
3703
3705
  addDoctorCheck(
3704
3706
  rows,
@@ -3715,12 +3717,28 @@ async function runDoctor(flags) {
3715
3717
  );
3716
3718
  }
3717
3719
  if (route.botID && !envConfig.serverBotID) {
3718
- addDoctorCheck(
3719
- rows,
3720
- strictMode ? "fail" : "warn",
3721
- `runner route ${routeLabel} server_bot_id`,
3722
- `route bot_id ${route.botID} is set, but local env entry does not carry SERVER_BOT_ID`,
3720
+ const serverMatch = ensureArray(serverBots).find((bot) => String(bot.id || "").trim() === String(route.botID || "").trim());
3721
+ const routeBotName = normalizeTelegramBotUsername(
3722
+ firstNonEmptyString([route.botName, route.bot_name, serverMatch?.name]),
3723
3723
  );
3724
+ const envBotName = normalizeTelegramBotUsername(envConfig.botUsername || envConfig.botKey);
3725
+ if (routeBotName && envBotName && routeBotName === envBotName) {
3726
+ addDoctorCheck(
3727
+ rows,
3728
+ "ok",
3729
+ `runner route ${routeLabel} server bot`,
3730
+ envConfig.botUsername
3731
+ ? `resolved by bot username @${envConfig.botUsername}`
3732
+ : `resolved by server bot name ${envConfig.botKey}`,
3733
+ );
3734
+ } else {
3735
+ addDoctorCheck(
3736
+ rows,
3737
+ strictMode ? "fail" : "warn",
3738
+ `runner route ${routeLabel} server_bot_id`,
3739
+ `route bot_id ${route.botID} is set, but local env entry does not carry SERVER_BOT_ID`,
3740
+ );
3741
+ }
3724
3742
  }
3725
3743
  if (serverBots && route.botID) {
3726
3744
  const match = ensureArray(serverBots).find((bot) => String(bot.id || "").trim() === String(route.botID || "").trim());
@@ -43,6 +43,35 @@ function firstNonEmptyString(values) {
43
43
  return "";
44
44
  }
45
45
 
46
+ function uniqueNormalizedValues(values, deps, fallback = "") {
47
+ const normalizeBotKey = requireDependency(deps, "normalizeTelegramBotEnvKey");
48
+ const seen = new Set();
49
+ const ordered = [];
50
+ ensureArray(values).forEach((value) => {
51
+ const normalized = normalizeBotKey(value || "", "");
52
+ if (!normalized || seen.has(normalized)) return;
53
+ seen.add(normalized);
54
+ ordered.push(normalized);
55
+ });
56
+ if (ordered.length) return ordered;
57
+ const fallbackKey = normalizeBotKey(fallback || "", "");
58
+ return fallbackKey ? [fallbackKey] : [];
59
+ }
60
+
61
+ function normalizeServerBotIdentityText(rawValue) {
62
+ return String(rawValue || "").trim().replace(/^@+/, "").toLowerCase();
63
+ }
64
+
65
+ function summarizeServerBotRoles(bots) {
66
+ return Array.from(
67
+ new Set(
68
+ ensureArray(bots)
69
+ .map((bot) => String(bot?.role || "").trim())
70
+ .filter(Boolean),
71
+ ),
72
+ ).sort((left, right) => left.localeCompare(right));
73
+ }
74
+
46
75
  function hasOwnFlag(flags, key) {
47
76
  return Object.prototype.hasOwnProperty.call(safeObject(flags), key);
48
77
  }
@@ -105,7 +134,7 @@ function printBotUsage(deps) {
105
134
  ` ${cliName} bot setup`,
106
135
  ` ${cliName} bot list [--provider <telegram|slack|kakaotalk>] [--json <true|false>]`,
107
136
  ` ${cliName} bot show [--provider <telegram|slack|kakaotalk>] [--bot-key <key>] [--bot-id <uuid>] [--bot-name <name>] [--json <true|false>]`,
108
- ` ${cliName} bot add [--provider <telegram|slack|kakaotalk>] [--base-url <url>] [--timeout-seconds <n>] [--non-interactive <true|false>] [--bot-key <key>] [--server-bot-id <uuid>] [--username <name>] [--token <token>] [--role-profile <name>] [--ai-client <name>] [--ai-model <name>] [--ai-permission-mode <mode>] [--ai-reasoning-effort <level>] [--default <true|false>] [--verify <true|false>]`,
137
+ ` ${cliName} bot add [--provider <telegram|slack|kakaotalk>] [--base-url <url>] [--timeout-seconds <n>] [--non-interactive <true|false>] [--server-bot-id <uuid>] [--token <token>] [--default <true|false>] [--verify <true|false>] [--ai-client <name>] [--ai-model <name>] [--ai-permission-mode <mode>] [--ai-reasoning-effort <level>] [--bot-key <advanced_key>] [--username <fallback_name>] [--role-profile <name>]`,
109
138
  ` ${cliName} bot edit [--provider <telegram|slack|kakaotalk>] [--base-url <url>] [--timeout-seconds <n>] [--non-interactive <true|false>] [--bot-key <key>] [--server-bot-id <uuid>] [--username <name>] [--token <token>] [--role-profile <name>] [--ai-client <name>] [--ai-model <name>] [--ai-permission-mode <mode>] [--ai-reasoning-effort <level>]`,
110
139
  ` ${cliName} bot remove [--provider <telegram|slack|kakaotalk>] [--non-interactive <true|false>] [--bot-key <key>]`,
111
140
  ` ${cliName} bot set-default --provider telegram [--bot-key <key>] [--non-interactive <true|false>]`,
@@ -113,6 +142,14 @@ function printBotUsage(deps) {
113
142
  ` ${cliName} bot verify [--provider <telegram|slack|kakaotalk>] [--bot-key <key>] [--timeout-seconds <n>] [--json <true|false>]`,
114
143
  ` ${cliName} bot global --provider telegram [--non-interactive <true|false>] [--api-base-url <url>] [--auto-clear-webhook <true|false>] [--allowed-updates <csv>] [--default-bot-key <key>]`,
115
144
  "",
145
+ `Behavior:`,
146
+ ` - bot setup asks for provider first, then shows a numbered action menu.`,
147
+ ` - bot add without flags uses the shortest practical guided flow: provider -> token -> verify -> optional username fallback -> optional default bot.`,
148
+ ` - in the normal Telegram path, bot add does not ask for a local bot key or a server role/profile choice.`,
149
+ ` - server bot name/UUID is the source of truth; local key is auto-derived unless you intentionally override it with --bot-key.`,
150
+ ` - bot edit without flags asks for provider, existing entry, then a numbered edit flow.`,
151
+ ` - bot set-default / bot verify / bot remove also support guided numbered selection when run without flags.`,
152
+ "",
116
153
  ].join("\n"),
117
154
  );
118
155
  }
@@ -277,6 +314,13 @@ function telegramEntryEnvKeys(botKey) {
277
314
  };
278
315
  }
279
316
 
317
+ function persistTelegramUsername(entry) {
318
+ const current = safeObject(entry);
319
+ if (current.__preferServerIdentity) return "";
320
+ if (String(current.serverBotID || "").trim()) return "";
321
+ return normalizeServerBotIdentityText(current.username || "");
322
+ }
323
+
280
324
  function removeTelegramEntry(parsedEnv, botKey) {
281
325
  const parsed = { ...safeObject(parsedEnv) };
282
326
  const keys = telegramEntryEnvKeys(botKey);
@@ -295,7 +339,7 @@ function upsertTelegramEntry(parsedEnv, entry) {
295
339
  const keys = telegramEntryEnvKeys(entry.key);
296
340
  const next = { ...parsed };
297
341
  next[keys.serverBotID] = String(entry.serverBotID || "").trim();
298
- next[keys.username] = String(entry.username || "").trim();
342
+ next[keys.username] = persistTelegramUsername(entry);
299
343
  next[keys.token] = String(entry.token || "").trim();
300
344
  next[keys.roleProfile] = String(entry.roleProfile || "").trim();
301
345
  next[keys.client] = String(entry.client || "").trim();
@@ -363,10 +407,14 @@ function renderTelegramEnv(parsedEnv, deps) {
363
407
  } else {
364
408
  entries.forEach((entry) => {
365
409
  const keys = telegramEntryEnvKeys(entry.key);
366
- lines.push(
410
+ const entryLines = [
367
411
  `# Telegram bot entry: ${entry.key}`,
368
412
  `${keys.serverBotID}=${formatEnvValue(entry.serverBotID || "")}`,
369
- `${keys.username}=${formatEnvValue(entry.username || "")}`,
413
+ ];
414
+ if (String(entry.username || "").trim()) {
415
+ entryLines.push(`${keys.username}=${formatEnvValue(entry.username || "")}`);
416
+ }
417
+ entryLines.push(
370
418
  `${keys.token}=${formatEnvValue(entry.token || "")}`,
371
419
  `${keys.roleProfile}=${formatEnvValue(entry.roleProfile || "")}`,
372
420
  `${keys.client}=${formatEnvValue(entry.client || "")}`,
@@ -375,6 +423,7 @@ function renderTelegramEnv(parsedEnv, deps) {
375
423
  `${keys.reasoningEffort}=${formatEnvValue(entry.reasoningEffort || "")}`,
376
424
  "",
377
425
  );
426
+ lines.push(...entryLines);
378
427
  });
379
428
  }
380
429
  const knownKeys = telegramKnownKeys(parsed, deps);
@@ -453,6 +502,23 @@ function telegramEntriesForDisplay(parsedEnv, deps) {
453
502
  .sort((left, right) => String(left.key || "").localeCompare(String(right.key || "")));
454
503
  }
455
504
 
505
+ function telegramEntryDisplayName(entry) {
506
+ const current = safeObject(entry);
507
+ const username = String(current.username || "").trim();
508
+ if (username) return `@${username}`;
509
+ return String(current.key || "").trim() || "(unnamed)";
510
+ }
511
+
512
+ function telegramEntryDisplayDescription(entry) {
513
+ const current = safeObject(entry);
514
+ const detail = [
515
+ current.key ? `key:${current.key}` : "",
516
+ current.serverBotID ? `server:${current.serverBotID}` : "",
517
+ current.client ? `AI:${current.client}` : "",
518
+ ].filter(Boolean);
519
+ return detail.join(" ");
520
+ }
521
+
456
522
  function findTelegramEntryByFlags(parsedEnv, flags, deps) {
457
523
  const entries = telegramEntriesForDisplay(parsedEnv, deps);
458
524
  const requestedKey = String(flags["bot-key"] || "").trim();
@@ -473,9 +539,11 @@ function findTelegramEntryByFlags(parsedEnv, flags, deps) {
473
539
  return match;
474
540
  }
475
541
  if (requestedName) {
476
- const match = entries.find((entry) => entry.username === requestedName);
542
+ const match = entries.find(
543
+ (entry) => entry.username === requestedName || normalizeServerBotIdentityText(entry.key) === requestedName,
544
+ );
477
545
  if (!match) {
478
- throw new Error(`Telegram bot entry with username "${requestedName}" was not found`);
546
+ throw new Error(`Telegram bot entry with server bot name "${requestedName}" was not found`);
479
547
  }
480
548
  return match;
481
549
  }
@@ -496,8 +564,44 @@ function saveTelegramBotEdit(parsed, selected, current, deps) {
496
564
  return filePath;
497
565
  }
498
566
 
567
+ function buildDerivedTelegramBotKey(parsedEnv, deps, preferredValues, { excludeKeys = [] } = {}) {
568
+ const normalizeBotKey = requireDependency(deps, "normalizeTelegramBotEnvKey");
569
+ const existingKeys = new Set(
570
+ telegramEntriesForDisplay(parsedEnv, deps)
571
+ .map((entry) => entry.key)
572
+ .filter((key) => !ensureArray(excludeKeys).includes(key)),
573
+ );
574
+ const candidates = uniqueNormalizedValues(
575
+ [
576
+ ...ensureArray(preferredValues),
577
+ "main",
578
+ ],
579
+ deps,
580
+ "main",
581
+ );
582
+ const baseKey = candidates[0] || normalizeBotKey("main", "main") || "main";
583
+ for (const candidate of candidates) {
584
+ if (!existingKeys.has(candidate)) {
585
+ return candidate;
586
+ }
587
+ }
588
+ let suffix = 2;
589
+ while (existingKeys.has(`${baseKey}_${suffix}`)) {
590
+ suffix += 1;
591
+ }
592
+ return `${baseKey}_${suffix}`;
593
+ }
594
+
499
595
  async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps) {
500
596
  let serverRoleAutoResolved = "";
597
+ const aiFlagOverrides = hasOwnFlag(flags, "client")
598
+ || hasOwnFlag(flags, "ai-client")
599
+ || hasOwnFlag(flags, "model")
600
+ || hasOwnFlag(flags, "ai-model")
601
+ || hasOwnFlag(flags, "permission-mode")
602
+ || hasOwnFlag(flags, "ai-permission-mode")
603
+ || hasOwnFlag(flags, "reasoning-effort")
604
+ || hasOwnFlag(flags, "ai-reasoning-effort");
501
605
  const bindingAction = await promptKeepChangeClear(ui, "Server bot binding", {
502
606
  allowClear: true,
503
607
  defaultValue: current.serverBotID ? "keep" : "change",
@@ -509,15 +613,39 @@ async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps)
509
613
  flags["base-url"] || deps.defaultSiteURL,
510
614
  intFromRaw(flags["timeout-seconds"], 15) || 15,
511
615
  deps,
616
+ {
617
+ preferredUsername: current.username,
618
+ preferredName: current.username,
619
+ },
512
620
  );
513
- current.serverBotID = serverBot.botID;
514
- if (!current.roleProfile && serverBot.role) {
515
- current.roleProfile = serverBot.role;
621
+ if (serverBot.matchMode === "group") {
622
+ current.serverBotID = "";
623
+ current.__preferServerIdentity = true;
624
+ if (!String(flags["role-profile"] || "").trim()) {
625
+ current.roleProfile = "";
626
+ }
627
+ if (!aiFlagOverrides) {
628
+ current.client = "";
629
+ current.model = "";
630
+ current.permissionMode = "";
631
+ current.reasoningEffort = "";
632
+ }
633
+ serverRoleAutoResolved = "__server_role_group__";
634
+ process.stdout.write(
635
+ `Matched server Telegram bot "${serverBot.name || current.username || current.key}" with roles: ${ensureArray(serverBot.roles).join(", ")}. Runtime will use the server role, so local role/AI fields can stay empty.\n`,
636
+ );
637
+ } else {
638
+ current.serverBotID = serverBot.botID;
639
+ current.__preferServerIdentity = true;
640
+ if (!current.roleProfile && serverBot.role) {
641
+ current.roleProfile = serverBot.role;
642
+ }
643
+ serverRoleAutoResolved = String(serverBot.role || current.roleProfile || "").trim();
644
+ applyRoleProfileDefaults(current, resolveRoleProfileDefaults(current.roleProfile || serverBot.role, deps));
516
645
  }
517
- serverRoleAutoResolved = String(serverBot.role || current.roleProfile || "").trim();
518
- applyRoleProfileDefaults(current, resolveRoleProfileDefaults(current.roleProfile || serverBot.role, deps));
519
646
  } else if (bindingAction === "clear") {
520
647
  current.serverBotID = "";
648
+ current.__preferServerIdentity = false;
521
649
  }
522
650
 
523
651
  const usernameAction = await promptKeepChangeClear(ui, "Telegram username", {
@@ -754,7 +882,8 @@ function printBotList(provider, state, deps) {
754
882
  entries.forEach((entry) => {
755
883
  process.stdout.write(
756
884
  [
757
- ` - ${entry.key}${entry.isDefault ? " [default]" : ""}`,
885
+ ` - ${telegramEntryDisplayName(entry)}${entry.isDefault ? " [default]" : ""}`,
886
+ ` key: ${entry.key || "-"}`,
758
887
  ` server_bot_id: ${entry.serverBotID || "-"}`,
759
888
  ` username: ${entry.username ? `@${entry.username}` : "-"}`,
760
889
  ` token: ${entry.token ? maskSecret(entry.token) : "-"}`,
@@ -778,6 +907,7 @@ function printBotShow(provider, state, entry, deps, extras = {}) {
778
907
  const selectedEntry = entry || {};
779
908
  process.stdout.write(`${providerLabel(provider, deps)} bot\n`);
780
909
  process.stdout.write(` file: ${state.filePath}\n`);
910
+ process.stdout.write(` name: ${telegramEntryDisplayName(selectedEntry)}\n`);
781
911
  process.stdout.write(` key: ${selectedEntry.key || "-"}\n`);
782
912
  process.stdout.write(` default: ${selectedEntry.isDefault ? "yes" : "no"}\n`);
783
913
  process.stdout.write(` server_bot_id: ${selectedEntry.serverBotID || "-"}\n`);
@@ -810,8 +940,8 @@ async function chooseTelegramEntry(ui, parsedEnv, deps, title = "Select Telegram
810
940
  title,
811
941
  entries.map((entry) => ({
812
942
  value: entry.key,
813
- label: `${entry.key}${entry.isDefault ? " [default]" : ""}`,
814
- description: `${entry.serverBotID || "-"} ${entry.username ? `@${entry.username}` : ""} ${entry.client ? `AI:${entry.client}` : ""}`.trim(),
943
+ label: `${telegramEntryDisplayName(entry)}${entry.isDefault ? " [default]" : ""}`,
944
+ description: telegramEntryDisplayDescription(entry),
815
945
  })),
816
946
  { defaultIndex: 0 },
817
947
  );
@@ -844,7 +974,7 @@ async function resolveTelegramEntryForShow(ui, parsedEnv, flags, deps) {
844
974
  return chooseTelegramEntry(ui, parsedEnv, deps, "Select Telegram bot entry");
845
975
  }
846
976
 
847
- async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps) {
977
+ async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps, options = {}) {
848
978
  const lookup = requireDependency(deps, "listServerBots");
849
979
  const result = await lookup({ provider, baseURL, timeoutSeconds });
850
980
  const bots = ensureArray(result?.bots).map((bot) => ({
@@ -857,11 +987,40 @@ async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps) {
857
987
  process.stdout.write(`Server bot profiles unavailable: ${result.error}\n`);
858
988
  }
859
989
  const manualID = await promptLine(ui, "Server bot UUID (blank to leave empty)", "");
860
- return {
861
- botID: manualID,
862
- role: "",
863
- name: "",
864
- };
990
+ return {
991
+ botID: manualID,
992
+ role: "",
993
+ name: "",
994
+ roles: [],
995
+ matchMode: manualID ? "manual" : "blank",
996
+ };
997
+ }
998
+ const preferredIdentity = normalizeServerBotIdentityText(
999
+ firstNonEmptyString([options.preferredUsername, options.preferredName]),
1000
+ );
1001
+ if (preferredIdentity) {
1002
+ const matchedByName = bots.filter(
1003
+ (bot) => normalizeServerBotIdentityText(bot.name) === preferredIdentity,
1004
+ );
1005
+ if (matchedByName.length === 1) {
1006
+ const match = matchedByName[0] || {};
1007
+ return {
1008
+ botID: String(match.id || "").trim(),
1009
+ role: String(match.role || "").trim(),
1010
+ name: String(match.name || "").trim(),
1011
+ roles: summarizeServerBotRoles([match]),
1012
+ matchMode: "exact",
1013
+ };
1014
+ }
1015
+ if (matchedByName.length > 1) {
1016
+ return {
1017
+ botID: "",
1018
+ role: "",
1019
+ name: String(matchedByName[0]?.name || "").trim(),
1020
+ roles: summarizeServerBotRoles(matchedByName),
1021
+ matchMode: "group",
1022
+ };
1023
+ }
865
1024
  }
866
1025
  if (bots.length === 1) {
867
1026
  const match = bots[0] || {};
@@ -869,8 +1028,30 @@ async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps) {
869
1028
  botID: String(match.id || "").trim(),
870
1029
  role: String(match.role || "").trim(),
871
1030
  name: String(match.name || "").trim(),
1031
+ roles: summarizeServerBotRoles([match]),
1032
+ matchMode: "exact",
872
1033
  };
873
1034
  }
1035
+ const groupedByName = new Map();
1036
+ bots.forEach((bot) => {
1037
+ const nameKey = normalizeServerBotIdentityText(bot.name) || bot.id;
1038
+ if (!groupedByName.has(nameKey)) {
1039
+ groupedByName.set(nameKey, []);
1040
+ }
1041
+ groupedByName.get(nameKey).push(bot);
1042
+ });
1043
+ if (groupedByName.size === 1) {
1044
+ const groupedBots = ensureArray(groupedByName.values().next().value);
1045
+ if (groupedBots.length > 1) {
1046
+ return {
1047
+ botID: "",
1048
+ role: "",
1049
+ name: String(groupedBots[0]?.name || "").trim(),
1050
+ roles: summarizeServerBotRoles(groupedBots),
1051
+ matchMode: "group",
1052
+ };
1053
+ }
1054
+ }
874
1055
  const preferredRoleOrder = ["monitor", "review", "worker", "approval"];
875
1056
  const roleGroups = new Map();
876
1057
  bots.forEach((bot) => {
@@ -921,7 +1102,7 @@ async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps) {
921
1102
  { defaultIndex: 0 },
922
1103
  );
923
1104
  if (!roleChoice || roleChoice.value === "__blank__") {
924
- return { botID: "", role: "", name: "" };
1105
+ return { botID: "", role: "", name: "", roles: [], matchMode: "blank" };
925
1106
  }
926
1107
  if (roleChoice.value === "__manual__") {
927
1108
  const manualID = await promptLine(ui, "Server bot UUID", "");
@@ -929,6 +1110,8 @@ async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps) {
929
1110
  botID: manualID,
930
1111
  role: "",
931
1112
  name: "",
1113
+ roles: [],
1114
+ matchMode: manualID ? "manual" : "blank",
932
1115
  };
933
1116
  }
934
1117
  if (String(roleChoice.value || "").startsWith("role:")) {
@@ -938,6 +1121,8 @@ async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps) {
938
1121
  botID: String(match.id || "").trim(),
939
1122
  role: String(match.role || "").trim(),
940
1123
  name: String(match.name || "").trim(),
1124
+ roles: summarizeServerBotRoles([match]),
1125
+ matchMode: "exact",
941
1126
  };
942
1127
  }
943
1128
  const selectedRole = String(roleChoice.value || "").slice(6);
@@ -965,7 +1150,7 @@ async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps) {
965
1150
  { defaultIndex: 0 },
966
1151
  );
967
1152
  if (!choice || choice.value === "__blank__") {
968
- return { botID: "", role: "", name: "" };
1153
+ return { botID: "", role: "", name: "", roles: [], matchMode: "blank" };
969
1154
  }
970
1155
  if (choice.value === "__manual__") {
971
1156
  const manualID = await promptLine(ui, "Server bot UUID", "");
@@ -973,6 +1158,8 @@ async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps) {
973
1158
  botID: manualID,
974
1159
  role: selectedRole,
975
1160
  name: "",
1161
+ roles: selectedRole ? [selectedRole] : [],
1162
+ matchMode: manualID ? "manual" : "blank",
976
1163
  };
977
1164
  }
978
1165
  const match = matches.find((bot) => bot.id === choice.value) || {};
@@ -980,6 +1167,72 @@ async function chooseServerBot(ui, provider, baseURL, timeoutSeconds, deps) {
980
1167
  botID: String(match.id || "").trim(),
981
1168
  role: String(match.role || "").trim(),
982
1169
  name: String(match.name || "").trim(),
1170
+ roles: summarizeServerBotRoles([match]),
1171
+ matchMode: "exact",
1172
+ };
1173
+ }
1174
+
1175
+ async function resolveServerBotForNonInteractive(provider, flags, deps, options = {}) {
1176
+ const requestedBotID = String(getServerBotIDFlag(flags) || "").trim();
1177
+ const requestedName = normalizeServerBotIdentityText(
1178
+ firstNonEmptyString([
1179
+ flags["bot-name"],
1180
+ flags.username,
1181
+ options.preferredUsername,
1182
+ options.preferredName,
1183
+ ]),
1184
+ );
1185
+ if (!requestedBotID && !requestedName) {
1186
+ return { botID: "", role: "", name: "", roles: [], matchMode: "blank" };
1187
+ }
1188
+ const lookup = await requireDependency(deps, "listServerBots")({
1189
+ provider,
1190
+ baseURL: flags["base-url"] || deps.defaultSiteURL,
1191
+ timeoutSeconds: intFromRaw(flags["timeout-seconds"], 15) || 15,
1192
+ });
1193
+ const bots = ensureArray(lookup?.bots).map((bot) => ({
1194
+ id: String(bot?.id || "").trim(),
1195
+ role: String(bot?.role || bot?.bot_role || "").trim(),
1196
+ name: String(bot?.name || "").trim(),
1197
+ })).filter((bot) => bot.id);
1198
+ if (!lookup?.ok) {
1199
+ throw new Error(lookup?.error || "server bot lookup unavailable");
1200
+ }
1201
+ if (requestedBotID) {
1202
+ const match = bots.find((bot) => bot.id === requestedBotID);
1203
+ if (!match) {
1204
+ throw new Error(`server bot ${requestedBotID} not found`);
1205
+ }
1206
+ return {
1207
+ botID: match.id,
1208
+ role: match.role,
1209
+ name: match.name,
1210
+ roles: summarizeServerBotRoles([match]),
1211
+ matchMode: "exact",
1212
+ };
1213
+ }
1214
+ const matchedByName = bots.filter(
1215
+ (bot) => normalizeServerBotIdentityText(bot.name) === requestedName,
1216
+ );
1217
+ if (!matchedByName.length) {
1218
+ throw new Error(`no server bot matched ${requestedName}`);
1219
+ }
1220
+ if (matchedByName.length === 1) {
1221
+ const match = matchedByName[0] || {};
1222
+ return {
1223
+ botID: String(match.id || "").trim(),
1224
+ role: String(match.role || "").trim(),
1225
+ name: String(match.name || "").trim(),
1226
+ roles: summarizeServerBotRoles([match]),
1227
+ matchMode: "exact",
1228
+ };
1229
+ }
1230
+ return {
1231
+ botID: "",
1232
+ role: "",
1233
+ name: String(matchedByName[0]?.name || "").trim(),
1234
+ roles: summarizeServerBotRoles(matchedByName),
1235
+ matchMode: "group",
983
1236
  };
984
1237
  }
985
1238
 
@@ -1090,35 +1343,9 @@ async function addTelegramBot(ui, flags, deps) {
1090
1343
  const state = loadProviderEnvState("telegram", deps);
1091
1344
  const parsed = { ...state.parsed };
1092
1345
  const nonInteractive = boolFromRaw(flags["non-interactive"] ?? flags.yes, false);
1093
- const serverBot = nonInteractive
1094
- ? {
1095
- botID: getServerBotIDFlag(flags),
1096
- role: String(flags.role || "").trim(),
1097
- name: "",
1098
- }
1099
- : await chooseServerBot(
1100
- ui,
1101
- "telegram",
1102
- flags["base-url"] || deps.defaultSiteURL,
1103
- intFromRaw(flags["timeout-seconds"], 15) || 15,
1104
- deps,
1105
- );
1106
1346
  const normalizeBotKey = requireDependency(deps, "normalizeTelegramBotEnvKey");
1107
1347
  const existingKeys = new Set(telegramEntriesForDisplay(parsed, deps).map((entry) => entry.key));
1108
- const defaultKeyHint = normalizeBotKey(flags["bot-key"] || serverBot.role || serverBot.name || "main", "main");
1109
- let botKey = normalizeBotKey(
1110
- nonInteractive
1111
- ? String(flags["bot-key"] || defaultKeyHint).trim()
1112
- : await promptRequiredLine(ui, "Local Telegram bot key", defaultKeyHint),
1113
- defaultKeyHint,
1114
- );
1115
- if (nonInteractive && existingKeys.has(botKey)) {
1116
- throw new Error(`Telegram bot key "${botKey}" already exists`);
1117
- }
1118
- while (!nonInteractive && existingKeys.has(botKey)) {
1119
- process.stdout.write(`Telegram bot key "${botKey}" already exists.\n`);
1120
- botKey = normalizeBotKey(await promptRequiredLine(ui, "Local Telegram bot key", `${botKey}_2`), `${botKey}_2`);
1121
- }
1348
+ const defaultKeyHint = normalizeBotKey(flags["bot-key"] || "main", "main");
1122
1349
 
1123
1350
  const token = nonInteractive
1124
1351
  ? String(flags.token || "").trim()
@@ -1159,12 +1386,69 @@ async function addTelegramBot(ui, flags, deps) {
1159
1386
  ? String(flags.username || usernameDefault).trim()
1160
1387
  : (usernameDefault || await promptLine(ui, "Telegram username (without @)", usernameDefault)),
1161
1388
  );
1389
+ const serverBot = nonInteractive
1390
+ ? await resolveServerBotForNonInteractive("telegram", flags, deps, {
1391
+ preferredUsername: username,
1392
+ preferredName: username,
1393
+ })
1394
+ : await chooseServerBot(
1395
+ ui,
1396
+ "telegram",
1397
+ flags["base-url"] || deps.defaultSiteURL,
1398
+ intFromRaw(flags["timeout-seconds"], 15) || 15,
1399
+ deps,
1400
+ {
1401
+ preferredUsername: username,
1402
+ preferredName: username,
1403
+ },
1404
+ );
1405
+ let botKey = normalizeBotKey(
1406
+ nonInteractive
1407
+ ? String(
1408
+ flags["bot-key"]
1409
+ || buildDerivedTelegramBotKey(
1410
+ parsed,
1411
+ deps,
1412
+ [
1413
+ serverBot.name,
1414
+ username,
1415
+ flags["bot-name"],
1416
+ flags.username,
1417
+ defaultKeyHint,
1418
+ ],
1419
+ ),
1420
+ ).trim()
1421
+ : buildDerivedTelegramBotKey(
1422
+ parsed,
1423
+ deps,
1424
+ [
1425
+ flags["bot-key"],
1426
+ serverBot.name,
1427
+ username,
1428
+ flags["bot-name"],
1429
+ flags.username,
1430
+ defaultKeyHint,
1431
+ ],
1432
+ ),
1433
+ defaultKeyHint,
1434
+ );
1435
+ if (nonInteractive && existingKeys.has(botKey)) {
1436
+ throw new Error(`Telegram bot key "${botKey}" already exists`);
1437
+ }
1438
+ const serverGroupMatched = !nonInteractive && serverBot.matchMode === "group";
1439
+ if (serverGroupMatched) {
1440
+ process.stdout.write(
1441
+ `Matched server Telegram bot "${serverBot.name || username || botKey}" with roles: ${ensureArray(serverBot.roles).join(", ")}. Runtime will use the server role, so local role/AI fields can stay empty.\n`,
1442
+ );
1443
+ }
1162
1444
  const defaultRoleProfile = firstNonEmptyString([flags["role-profile"], serverBot.role, ""]);
1163
1445
  const roleProfileAutoResolved = !nonInteractive && !String(flags["role-profile"] || "").trim() && Boolean(defaultRoleProfile);
1164
1446
  const roleProfile = requireDependency(deps, "normalizeRunnerRoleProfileName")(
1165
1447
  nonInteractive
1166
1448
  ? String(flags["role-profile"] || defaultRoleProfile).trim()
1167
- : (roleProfileAutoResolved ? defaultRoleProfile : await promptTelegramRoleProfile(ui, deps, defaultRoleProfile)),
1449
+ : (serverGroupMatched && !String(flags["role-profile"] || "").trim()
1450
+ ? ""
1451
+ : (roleProfileAutoResolved ? defaultRoleProfile : await promptTelegramRoleProfile(ui, deps, defaultRoleProfile))),
1168
1452
  );
1169
1453
  const roleProfileDefaults = resolveRoleProfileDefaults(roleProfile, deps);
1170
1454
  const aiFlagOverrides = hasOwnFlag(flags, "client")
@@ -1176,33 +1460,45 @@ async function addTelegramBot(ui, flags, deps) {
1176
1460
  || hasOwnFlag(flags, "reasoning-effort")
1177
1461
  || hasOwnFlag(flags, "ai-reasoning-effort");
1178
1462
  const autoApplyRoleDefaults = !nonInteractive && roleProfileAutoResolved && !aiFlagOverrides;
1463
+ const autoUseServerRoleRouting = !nonInteractive
1464
+ && serverGroupMatched
1465
+ && !String(flags["role-profile"] || "").trim()
1466
+ && !aiFlagOverrides;
1179
1467
  const client = requireDependency(deps, "normalizeLocalAIClientName")(
1180
1468
  nonInteractive
1181
1469
  ? firstNonEmptyString([getAIClientFlag(flags), roleProfileDefaults.client])
1182
- : (autoApplyRoleDefaults
1183
- ? firstNonEmptyString([getAIClientFlag(flags), roleProfileDefaults.client])
1184
- : await promptAIClient(ui, deps, firstNonEmptyString([getAIClientFlag(flags), roleProfileDefaults.client]))),
1470
+ : (autoUseServerRoleRouting
1471
+ ? getAIClientFlag(flags)
1472
+ : (autoApplyRoleDefaults
1473
+ ? firstNonEmptyString([getAIClientFlag(flags), roleProfileDefaults.client])
1474
+ : await promptAIClient(ui, deps, firstNonEmptyString([getAIClientFlag(flags), roleProfileDefaults.client])))),
1185
1475
  "",
1186
1476
  );
1187
1477
  const model = nonInteractive
1188
1478
  ? firstNonEmptyString([getAIModelFlag(flags), roleProfileDefaults.model])
1189
- : (autoApplyRoleDefaults
1190
- ? firstNonEmptyString([getAIModelFlag(flags), roleProfileDefaults.model])
1191
- : await promptLine(ui, "AI model", firstNonEmptyString([getAIModelFlag(flags), roleProfileDefaults.model])));
1479
+ : (autoUseServerRoleRouting
1480
+ ? getAIModelFlag(flags)
1481
+ : (autoApplyRoleDefaults
1482
+ ? firstNonEmptyString([getAIModelFlag(flags), roleProfileDefaults.model])
1483
+ : await promptLine(ui, "AI model", firstNonEmptyString([getAIModelFlag(flags), roleProfileDefaults.model]))));
1192
1484
  const permissionMode = requireDependency(deps, "normalizeLocalAIPermissionMode")(
1193
1485
  nonInteractive
1194
1486
  ? firstNonEmptyString([getAIPermissionModeFlag(flags), roleProfileDefaults.permissionMode])
1195
- : (autoApplyRoleDefaults
1196
- ? firstNonEmptyString([getAIPermissionModeFlag(flags), roleProfileDefaults.permissionMode])
1197
- : await promptPermissionMode(ui, firstNonEmptyString([getAIPermissionModeFlag(flags), roleProfileDefaults.permissionMode]))),
1487
+ : (autoUseServerRoleRouting
1488
+ ? getAIPermissionModeFlag(flags)
1489
+ : (autoApplyRoleDefaults
1490
+ ? firstNonEmptyString([getAIPermissionModeFlag(flags), roleProfileDefaults.permissionMode])
1491
+ : await promptPermissionMode(ui, firstNonEmptyString([getAIPermissionModeFlag(flags), roleProfileDefaults.permissionMode])))),
1198
1492
  "",
1199
1493
  );
1200
1494
  const reasoningEffort = requireDependency(deps, "normalizeLocalAIReasoningEffort")(
1201
1495
  nonInteractive
1202
1496
  ? firstNonEmptyString([getAIReasoningEffortFlag(flags), roleProfileDefaults.reasoningEffort])
1203
- : (autoApplyRoleDefaults
1204
- ? firstNonEmptyString([getAIReasoningEffortFlag(flags), roleProfileDefaults.reasoningEffort])
1205
- : await promptReasoningEffort(ui, firstNonEmptyString([getAIReasoningEffortFlag(flags), roleProfileDefaults.reasoningEffort]))),
1497
+ : (autoUseServerRoleRouting
1498
+ ? getAIReasoningEffortFlag(flags)
1499
+ : (autoApplyRoleDefaults
1500
+ ? firstNonEmptyString([getAIReasoningEffortFlag(flags), roleProfileDefaults.reasoningEffort])
1501
+ : await promptReasoningEffort(ui, firstNonEmptyString([getAIReasoningEffortFlag(flags), roleProfileDefaults.reasoningEffort])))),
1206
1502
  "",
1207
1503
  );
1208
1504
 
@@ -1210,6 +1506,7 @@ async function addTelegramBot(ui, flags, deps) {
1210
1506
  key: botKey,
1211
1507
  serverBotID: String(getServerBotIDFlag(flags) || serverBot.botID || "").trim(),
1212
1508
  username,
1509
+ __preferServerIdentity: serverBot.matchMode === "group" || Boolean(String(getServerBotIDFlag(flags) || serverBot.botID || "").trim()),
1213
1510
  token,
1214
1511
  roleProfile,
1215
1512
  client,
@@ -1218,9 +1515,11 @@ async function addTelegramBot(ui, flags, deps) {
1218
1515
  reasoningEffort,
1219
1516
  });
1220
1517
 
1221
- const hasCurrentDefault = Boolean(String(parsed.TELEGRAM_DEFAULT_BOT_KEY || "").trim());
1518
+ const currentDefaultKey = normalizeBotKey(parsed.TELEGRAM_DEFAULT_BOT_KEY || "", "");
1519
+ const hasCurrentDefault = telegramEntriesForDisplay(parsed, deps).some((entry) => entry.key === currentDefaultKey);
1222
1520
  if (
1223
1521
  boolFromRaw(flags.default, false)
1522
+ || (nonInteractive && !hasCurrentDefault)
1224
1523
  || (!nonInteractive && !hasCurrentDefault && await promptYesNo(ui, "Set this bot as TELEGRAM_DEFAULT_BOT_KEY?", true))
1225
1524
  ) {
1226
1525
  nextParsed.TELEGRAM_DEFAULT_BOT_KEY = botKey;
@@ -1296,6 +1595,7 @@ async function editTelegramBot(ui, flags, deps) {
1296
1595
  if (nonInteractive) {
1297
1596
  if (getServerBotIDFlag(flags)) {
1298
1597
  current.serverBotID = getServerBotIDFlag(flags);
1598
+ current.__preferServerIdentity = true;
1299
1599
  }
1300
1600
  if (Object.prototype.hasOwnProperty.call(flags, "username")) {
1301
1601
  current.username = requireDependency(deps, "normalizeTelegramBotUsername")(flags.username || "");
@@ -1368,10 +1668,21 @@ async function editTelegramBot(ui, flags, deps) {
1368
1668
  flags["base-url"] || deps.defaultSiteURL,
1369
1669
  intFromRaw(flags["timeout-seconds"], 15) || 15,
1370
1670
  deps,
1671
+ {
1672
+ preferredUsername: current.username,
1673
+ preferredName: current.username,
1674
+ },
1371
1675
  );
1372
- current.serverBotID = serverBot.botID;
1373
- if (!current.roleProfile && serverBot.role) {
1374
- current.roleProfile = serverBot.role;
1676
+ if (serverBot.matchMode === "group") {
1677
+ current.serverBotID = "";
1678
+ process.stdout.write(
1679
+ `Matched server Telegram bot "${serverBot.name || current.username || current.key}" with roles: ${ensureArray(serverBot.roles).join(", ")}. Keep role/AI fields empty if you want runtime role-based resolution.\n`,
1680
+ );
1681
+ } else {
1682
+ current.serverBotID = serverBot.botID;
1683
+ if (!current.roleProfile && serverBot.role) {
1684
+ current.roleProfile = serverBot.role;
1685
+ }
1375
1686
  }
1376
1687
  } else if (choice.value === "username") {
1377
1688
  current.username = requireDependency(deps, "normalizeTelegramBotUsername")(
@@ -1490,7 +1801,7 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
1490
1801
  );
1491
1802
  let serverBinding = null;
1492
1803
  let overallOK = Boolean(result.ok);
1493
- if (provider === "telegram" && String(envConfig.serverBotID || "").trim()) {
1804
+ if (provider === "telegram" && (String(envConfig.serverBotID || "").trim() || String(envConfig.botUsername || "").trim() || String(envConfig.botKey || "").trim())) {
1494
1805
  const lookup = await requireDependency(deps, "listServerBots")({
1495
1806
  provider,
1496
1807
  baseURL: flags["base-url"] || deps.defaultSiteURL,
@@ -1503,18 +1814,37 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
1503
1814
  };
1504
1815
  overallOK = false;
1505
1816
  } else {
1506
- const match = ensureArray(lookup.bots).find((bot) => String(bot.id || "").trim() === String(envConfig.serverBotID || "").trim());
1507
- if (!match) {
1508
- serverBinding = {
1509
- ok: false,
1510
- detail: `server bot ${envConfig.serverBotID} not found`,
1511
- };
1512
- overallOK = false;
1817
+ const normalizedServerIdentity = normalizeServerBotIdentityText(envConfig.botUsername || envConfig.botKey);
1818
+ if (String(envConfig.serverBotID || "").trim()) {
1819
+ const match = ensureArray(lookup.bots).find((bot) => String(bot.id || "").trim() === String(envConfig.serverBotID || "").trim());
1820
+ if (!match) {
1821
+ serverBinding = {
1822
+ ok: false,
1823
+ detail: `server bot ${envConfig.serverBotID} not found`,
1824
+ };
1825
+ overallOK = false;
1826
+ } else {
1827
+ serverBinding = {
1828
+ ok: true,
1829
+ detail: `${String(match.name || "").trim() || "(unnamed)"} [${String(match.role || "").trim() || "-"}]`,
1830
+ };
1831
+ }
1513
1832
  } else {
1514
- serverBinding = {
1515
- ok: true,
1516
- detail: `${String(match.name || "").trim() || "(unnamed)"} [${String(match.role || "").trim() || "-"}]`,
1517
- };
1833
+ const matches = ensureArray(lookup.bots).filter(
1834
+ (bot) => normalizeServerBotIdentityText(bot?.name) === normalizedServerIdentity,
1835
+ );
1836
+ if (!matches.length) {
1837
+ serverBinding = {
1838
+ ok: false,
1839
+ detail: `no server bot matched ${envConfig.botUsername ? `@${envConfig.botUsername}` : envConfig.botKey}`,
1840
+ };
1841
+ overallOK = false;
1842
+ } else {
1843
+ serverBinding = {
1844
+ ok: true,
1845
+ detail: `${String(matches[0]?.name || "").trim() || "(unnamed)"} roles: ${summarizeServerBotRoles(matches).join(", ") || "-"}`,
1846
+ };
1847
+ }
1518
1848
  }
1519
1849
  }
1520
1850
  }
@@ -84,20 +84,26 @@ function readTrailingJSON(rawText) {
84
84
  return readJSON(text.slice(start));
85
85
  }
86
86
 
87
- function createMockServer() {
88
- const serverBots = [
89
- {
90
- id: "11111111-2222-3333-4444-555555555555",
91
- name: "MonitorSelftestBot",
92
- provider: "telegram",
93
- bot_role: "monitor",
94
- is_active: true,
95
- },
96
- ];
97
- const telegramUsersByToken = new Map([
98
- ["selftest-main-token", "MonitorSelftestBot"],
99
- ["selftest-edited-token", "MonitorSelftestBot"],
100
- ]);
87
+ function createMockServer(options = {}) {
88
+ const serverBots = ensureArray(options.serverBots).length
89
+ ? ensureArray(options.serverBots)
90
+ : [
91
+ {
92
+ id: "11111111-2222-3333-4444-555555555555",
93
+ name: "MonitorSelftestBot",
94
+ provider: "telegram",
95
+ bot_role: "monitor",
96
+ is_active: true,
97
+ },
98
+ ];
99
+ const telegramUsersByToken = new Map(
100
+ ensureArray(options.telegramUsersByToken).length
101
+ ? ensureArray(options.telegramUsersByToken)
102
+ : [
103
+ ["selftest-main-token", "MonitorSelftestBot"],
104
+ ["selftest-edited-token", "MonitorSelftestBot"],
105
+ ],
106
+ );
101
107
 
102
108
  const server = http.createServer((req, res) => {
103
109
  const url = new URL(req.url || "/", "http://127.0.0.1");
@@ -271,7 +277,6 @@ export async function runSelftestBotCommands(push, deps) {
271
277
  ...env,
272
278
  METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
273
279
  "1", // provider: telegram
274
- "main_test",
275
280
  "selftest-main-token",
276
281
  "y", // verify now
277
282
  ]),
@@ -280,22 +285,82 @@ export async function runSelftestBotCommands(push, deps) {
280
285
  const addState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
281
286
  push(
282
287
  "bot_add_guided_creates_named_telegram_entry",
283
- String(addState.TELEGRAM_BOT_MAIN_TEST_SERVER_BOT_ID || "") === mock.bots[0].id
284
- && String(addState.TELEGRAM_BOT_MAIN_TEST_USERNAME || "").trim().toLowerCase() === "monitorselftestbot"
285
- && String(addState.TELEGRAM_BOT_MAIN_TEST_ROLE_PROFILE || "") === "monitor"
286
- && String(addState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "") === "codex"
287
- && String(addState.TELEGRAM_BOT_MAIN_TEST_AI_PERMISSION_MODE || "") === "read_only"
288
- && String(addState.TELEGRAM_BOT_MAIN_TEST_AI_REASONING_EFFORT || "") === "low"
289
- && String(addState.TELEGRAM_DEFAULT_BOT_KEY || "") === "main",
290
- `default=${String(addState.TELEGRAM_DEFAULT_BOT_KEY || "")} role=${String(addState.TELEGRAM_BOT_MAIN_TEST_ROLE_PROFILE || "")} client=${String(addState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "")}`,
288
+ String(addState.TELEGRAM_BOT_MONITORSELFTESTBOT_SERVER_BOT_ID || "") === mock.bots[0].id
289
+ && String(addState.TELEGRAM_BOT_MONITORSELFTESTBOT_USERNAME || "").trim() === ""
290
+ && String(addState.TELEGRAM_BOT_MONITORSELFTESTBOT_ROLE_PROFILE || "") === "monitor"
291
+ && String(addState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_CLIENT || "") === "codex"
292
+ && String(addState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_PERMISSION_MODE || "") === "read_only"
293
+ && String(addState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_REASONING_EFFORT || "") === "low"
294
+ && String(addState.TELEGRAM_DEFAULT_BOT_KEY || "") === "monitorselftestbot",
295
+ `default=${String(addState.TELEGRAM_DEFAULT_BOT_KEY || "")} role=${String(addState.TELEGRAM_BOT_MONITORSELFTESTBOT_ROLE_PROFILE || "")} client=${String(addState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_CLIENT || "")}`,
291
296
  );
292
297
 
298
+ const groupedMock = await createMockServer({
299
+ serverBots: [
300
+ { id: "977ef999-c40b-4cf6-a142-ade246f1b9cf", name: "RyoAI_bot", provider: "telegram", bot_role: "approval", is_active: true },
301
+ { id: "d74b57de-a635-4bac-a20f-f8ada188cf97", name: "RyoAI_bot", provider: "telegram", bot_role: "worker", is_active: true },
302
+ { id: "bbae9450-4236-487c-9a05-61dc4a626215", name: "RyoAI_bot", provider: "telegram", bot_role: "review", is_active: true },
303
+ { id: "353896c5-3b6b-422f-9eeb-5dc9354f77b3", name: "RyoAI_bot", provider: "telegram", bot_role: "monitor", is_active: true },
304
+ ],
305
+ telegramUsersByToken: [["selftest-group-token", "RyoAI_bot"]],
306
+ }).listen();
307
+ try {
308
+ await runCLI({
309
+ cliPath,
310
+ args: [
311
+ "bot", "global",
312
+ "--provider", "telegram",
313
+ "--non-interactive", "true",
314
+ "--api-base-url", `http://127.0.0.1:${groupedMock.port}/telegram`,
315
+ ],
316
+ env,
317
+ });
318
+ await runCLI({
319
+ cliPath,
320
+ args: [
321
+ "bot", "add",
322
+ "--base-url", `http://127.0.0.1:${groupedMock.port}`,
323
+ "--timeout-seconds", "5",
324
+ ],
325
+ env: {
326
+ ...env,
327
+ METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
328
+ "1",
329
+ "selftest-group-token",
330
+ "y",
331
+ ]),
332
+ },
333
+ });
334
+ const groupedState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
335
+ push(
336
+ "bot_add_guided_autoresolves_server_bot_group_without_role_prompt",
337
+ String(groupedState.TELEGRAM_BOT_RYOAI_BOT_USERNAME || "").trim() === ""
338
+ && String(groupedState.TELEGRAM_BOT_RYOAI_BOT_SERVER_BOT_ID || "") === ""
339
+ && String(groupedState.TELEGRAM_BOT_RYOAI_BOT_ROLE_PROFILE || "") === ""
340
+ && String(groupedState.TELEGRAM_BOT_RYOAI_BOT_AI_CLIENT || "") === ""
341
+ && String(groupedState.TELEGRAM_BOT_RYOAI_BOT_AI_PERMISSION_MODE || "") === "",
342
+ `username=${String(groupedState.TELEGRAM_BOT_RYOAI_BOT_USERNAME || "")} server_bot_id=${String(groupedState.TELEGRAM_BOT_RYOAI_BOT_SERVER_BOT_ID || "")} role=${String(groupedState.TELEGRAM_BOT_RYOAI_BOT_ROLE_PROFILE || "")}`,
343
+ );
344
+ } finally {
345
+ await groupedMock.close();
346
+ await runCLI({
347
+ cliPath,
348
+ args: [
349
+ "bot", "global",
350
+ "--provider", "telegram",
351
+ "--non-interactive", "true",
352
+ "--api-base-url", telegramApiBaseURL,
353
+ ],
354
+ env,
355
+ });
356
+ }
357
+
293
358
  const showResult = await runCLI({
294
359
  cliPath,
295
360
  args: [
296
361
  "bot", "show",
297
362
  "--provider", "telegram",
298
- "--bot-key", "main_test",
363
+ "--bot-key", "monitorselftestbot",
299
364
  "--json", "true",
300
365
  ],
301
366
  env,
@@ -303,7 +368,7 @@ export async function runSelftestBotCommands(push, deps) {
303
368
  const showPayload = readJSON(showResult.stdout);
304
369
  push(
305
370
  "bot_show_returns_selected_telegram_entry",
306
- safeObject(showPayload.entry).key === "main_test"
371
+ safeObject(showPayload.entry).key === "monitorselftestbot"
307
372
  && safeObject(showPayload.entry).client === "codex",
308
373
  `key=${String(safeObject(showPayload.entry).key || "")} client=${String(safeObject(showPayload.entry).client || "")}`,
309
374
  );
@@ -315,7 +380,7 @@ export async function runSelftestBotCommands(push, deps) {
315
380
  ...env,
316
381
  METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
317
382
  "1", // provider: telegram
318
- "1", // bot entry: main_test
383
+ "1", // bot entry: @monitorselftestbot
319
384
  "1", // edit mode: guided
320
385
  "1", // keep server bot binding
321
386
  "1", // keep username
@@ -338,11 +403,11 @@ export async function runSelftestBotCommands(push, deps) {
338
403
  const guidedState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
339
404
  push(
340
405
  "bot_edit_guided_prompts_update_ai_binding_fields",
341
- String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "") === "gemini"
342
- && String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_MODEL || "") === "guided-gemini-pro"
343
- && String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_PERMISSION_MODE || "") === "workspace_write"
344
- && String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_REASONING_EFFORT || "") === "medium",
345
- `client=${String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "")} model=${String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_MODEL || "")}`,
406
+ String(guidedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_CLIENT || "") === "gemini"
407
+ && String(guidedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_MODEL || "") === "guided-gemini-pro"
408
+ && String(guidedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_PERMISSION_MODE || "") === "workspace_write"
409
+ && String(guidedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_REASONING_EFFORT || "") === "medium",
410
+ `client=${String(guidedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_CLIENT || "")} model=${String(guidedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_MODEL || "")}`,
346
411
  );
347
412
 
348
413
  await runCLI({
@@ -350,7 +415,7 @@ export async function runSelftestBotCommands(push, deps) {
350
415
  args: [
351
416
  "bot", "edit",
352
417
  "--provider", "telegram",
353
- "--bot-key", "main_test",
418
+ "--bot-key", "monitorselftestbot",
354
419
  "--non-interactive", "true",
355
420
  "--token", "selftest-edited-token",
356
421
  "--client", "claude",
@@ -363,12 +428,12 @@ export async function runSelftestBotCommands(push, deps) {
363
428
  const editedState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
364
429
  push(
365
430
  "bot_edit_updates_ai_binding_fields",
366
- String(editedState.TELEGRAM_BOT_MAIN_TEST_TOKEN || "") === "selftest-edited-token"
367
- && String(editedState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "") === "claude"
368
- && String(editedState.TELEGRAM_BOT_MAIN_TEST_AI_MODEL || "") === "claude-3.7-sonnet"
369
- && String(editedState.TELEGRAM_BOT_MAIN_TEST_AI_PERMISSION_MODE || "") === "workspace_write"
370
- && String(editedState.TELEGRAM_BOT_MAIN_TEST_AI_REASONING_EFFORT || "") === "medium",
371
- `client=${String(editedState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "")} model=${String(editedState.TELEGRAM_BOT_MAIN_TEST_AI_MODEL || "")}`,
431
+ String(editedState.TELEGRAM_BOT_MONITORSELFTESTBOT_TOKEN || "") === "selftest-edited-token"
432
+ && String(editedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_CLIENT || "") === "claude"
433
+ && String(editedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_MODEL || "") === "claude-3.7-sonnet"
434
+ && String(editedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_PERMISSION_MODE || "") === "workspace_write"
435
+ && String(editedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_REASONING_EFFORT || "") === "medium",
436
+ `client=${String(editedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_CLIENT || "")} model=${String(editedState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_MODEL || "")}`,
372
437
  );
373
438
 
374
439
  await runCLI({
@@ -378,7 +443,7 @@ export async function runSelftestBotCommands(push, deps) {
378
443
  ...env,
379
444
  METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
380
445
  "1", // provider: telegram
381
- "1", // bot entry: main_test
446
+ "1", // bot entry: @monitorselftestbot
382
447
  "1", // confirm set default
383
448
  ]),
384
449
  },
@@ -386,7 +451,7 @@ export async function runSelftestBotCommands(push, deps) {
386
451
  const guidedDefaultState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
387
452
  push(
388
453
  "bot_set_default_guided_selects_entry",
389
- String(guidedDefaultState.TELEGRAM_DEFAULT_BOT_KEY || "") === "main_test",
454
+ String(guidedDefaultState.TELEGRAM_DEFAULT_BOT_KEY || "") === "monitorselftestbot",
390
455
  `default=${String(guidedDefaultState.TELEGRAM_DEFAULT_BOT_KEY || "")}`,
391
456
  );
392
457
 
@@ -401,7 +466,7 @@ export async function runSelftestBotCommands(push, deps) {
401
466
  ...env,
402
467
  METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
403
468
  "1", // provider: telegram
404
- "1", // bot entry: main_test
469
+ "1", // bot entry: @monitorselftestbot
405
470
  "2", // output format: json
406
471
  ]),
407
472
  },
@@ -420,7 +485,7 @@ export async function runSelftestBotCommands(push, deps) {
420
485
  args: [
421
486
  "bot", "set-default",
422
487
  "--provider", "telegram",
423
- "--bot-key", "main_test",
488
+ "--bot-key", "monitorselftestbot",
424
489
  "--non-interactive", "true",
425
490
  ],
426
491
  env,
@@ -428,7 +493,7 @@ export async function runSelftestBotCommands(push, deps) {
428
493
  const defaultState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
429
494
  push(
430
495
  "bot_set_default_updates_default_bot_key",
431
- String(defaultState.TELEGRAM_DEFAULT_BOT_KEY || "") === "main_test",
496
+ String(defaultState.TELEGRAM_DEFAULT_BOT_KEY || "") === "monitorselftestbot",
432
497
  `default=${String(defaultState.TELEGRAM_DEFAULT_BOT_KEY || "")}`,
433
498
  );
434
499
 
@@ -437,7 +502,7 @@ export async function runSelftestBotCommands(push, deps) {
437
502
  args: [
438
503
  "bot", "verify",
439
504
  "--provider", "telegram",
440
- "--bot-key", "main_test",
505
+ "--bot-key", "monitorselftestbot",
441
506
  "--base-url", baseURL,
442
507
  "--timeout-seconds", "5",
443
508
  "--json", "true",
@@ -458,7 +523,7 @@ export async function runSelftestBotCommands(push, deps) {
458
523
  args: [
459
524
  "bot", "remove",
460
525
  "--provider", "telegram",
461
- "--bot-key", "main_test",
526
+ "--bot-key", "monitorselftestbot",
462
527
  "--non-interactive", "true",
463
528
  ],
464
529
  env,
@@ -476,7 +541,7 @@ export async function runSelftestBotCommands(push, deps) {
476
541
  const telegramEntry = safeObject(listPayload[0]);
477
542
  push(
478
543
  "bot_remove_deletes_named_telegram_entry",
479
- ensureArray(telegramEntry.entries).length === 0,
544
+ !ensureArray(telegramEntry.entries).some((entry) => String(safeObject(entry).key || "") === "monitorselftestbot"),
480
545
  `entries=${String(ensureArray(telegramEntry.entries).length)}`,
481
546
  );
482
547
 
@@ -488,11 +553,8 @@ export async function runSelftestBotCommands(push, deps) {
488
553
  "--non-interactive", "true",
489
554
  "--base-url", baseURL,
490
555
  "--timeout-seconds", "5",
491
- "--bot-key", "explicit_test",
492
556
  "--server-bot-id", mock.bots[0].id,
493
- "--username", "MonitorSelftestBot",
494
557
  "--token", "selftest-main-token",
495
- "--role-profile", "monitor",
496
558
  "--ai-client", "codex",
497
559
  "--ai-model", "gpt-5-codex",
498
560
  "--ai-permission-mode", "read_only",
@@ -504,12 +566,13 @@ export async function runSelftestBotCommands(push, deps) {
504
566
  const aliasAddState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
505
567
  push(
506
568
  "bot_add_accepts_ai_prefixed_option_aliases",
507
- String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_SERVER_BOT_ID || "") === mock.bots[0].id
508
- && String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_CLIENT || "") === "codex"
509
- && String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_MODEL || "") === "gpt-5-codex"
510
- && String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_PERMISSION_MODE || "") === "read_only"
511
- && String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_REASONING_EFFORT || "") === "low",
512
- `client=${String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_CLIENT || "")} model=${String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_MODEL || "")}`,
569
+ String(aliasAddState.TELEGRAM_BOT_MONITORSELFTESTBOT_SERVER_BOT_ID || "") === mock.bots[0].id
570
+ && String(aliasAddState.TELEGRAM_BOT_MONITORSELFTESTBOT_USERNAME || "") === ""
571
+ && String(aliasAddState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_CLIENT || "") === "codex"
572
+ && String(aliasAddState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_MODEL || "") === "gpt-5-codex"
573
+ && String(aliasAddState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_PERMISSION_MODE || "") === "read_only"
574
+ && String(aliasAddState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_REASONING_EFFORT || "") === "low",
575
+ `client=${String(aliasAddState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_CLIENT || "")} model=${String(aliasAddState.TELEGRAM_BOT_MONITORSELFTESTBOT_AI_MODEL || "")}`,
513
576
  );
514
577
 
515
578
  const guidedRemoveBeforeList = await runCLI({
@@ -524,7 +587,7 @@ export async function runSelftestBotCommands(push, deps) {
524
587
  const guidedRemoveBeforePayload = ensureArray(readJSON(guidedRemoveBeforeList.stdout));
525
588
  const guidedRemoveBeforeEntry = safeObject(guidedRemoveBeforePayload[0]);
526
589
  const guidedRemoveEntries = ensureArray(guidedRemoveBeforeEntry.entries);
527
- const explicitEntryIndex = guidedRemoveEntries.findIndex((entry) => String(safeObject(entry).key || "") === "explicit_test");
590
+ const explicitEntryIndex = guidedRemoveEntries.findIndex((entry) => String(safeObject(entry).key || "") === "monitorselftestbot");
528
591
 
529
592
  await runCLI({
530
593
  cliPath,
@@ -552,7 +615,7 @@ export async function runSelftestBotCommands(push, deps) {
552
615
  push(
553
616
  "bot_remove_guided_deletes_selected_entry",
554
617
  explicitEntryIndex >= 0
555
- && ensureArray(guidedRemoveEntry.entries).length === 0,
618
+ && !ensureArray(guidedRemoveEntry.entries).some((entry) => String(safeObject(entry).key || "") === "explicit_test"),
556
619
  `entries=${String(ensureArray(guidedRemoveEntry.entries).length)} selected=${String(explicitEntryIndex + 1)}`,
557
620
  );
558
621
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.69",
3
+ "version": "0.2.70",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [