metheus-governance-mcp-cli 0.2.72 → 0.2.74

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
@@ -247,8 +247,8 @@ Behavior:
247
247
  - `AI_PERMISSION_MODE`
248
248
  - `AI_REASONING_EFFORT`
249
249
  - Slack and KakaoTalk currently use a single local token entry per provider in this command flow.
250
- - `bot verify` checks the configured local token and prints the current AI binding summary.
251
- - `bot show` prints one local bot entry in detail.
250
+ - `bot verify` checks the configured local token, cross-checks the server bot binding, prints the effective runtime role profile summary that the runner will use, and shows which local runner routes currently point at this bot entry.
251
+ - `bot show` prints one local bot entry in detail, including grouped role summaries when one server bot name expands to multiple roles and any linked local runner routes.
252
252
  - `bot global` edits Telegram-wide local settings such as API base URL, allowed updates, and default bot key.
253
253
  - `bot set-default` updates `TELEGRAM_DEFAULT_BOT_KEY`.
254
254
  - `bot migrate` moves legacy `TELEGRAM_BOT_TOKEN` into a named Telegram bot entry.
@@ -259,7 +259,7 @@ Non-interactive examples:
259
259
  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
260
260
  metheus-governance-mcp-cli bot add --provider telegram --non-interactive true --server-bot-id <server_bot_uuid> --token <telegram_bot_token> --default true
261
261
  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
262
- 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
262
+ metheus-governance-mcp-cli bot edit --provider telegram --bot-key ryoai_bot --non-interactive true --ai-client claude --ai-model claude-sonnet-4 --ai-permission-mode danger_full_access --ai-reasoning-effort high
263
263
  metheus-governance-mcp-cli bot set-default --provider telegram --bot-key main --non-interactive true
264
264
  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
265
265
  metheus-governance-mcp-cli bot remove --provider telegram --bot-key main --non-interactive true
@@ -268,6 +268,8 @@ metheus-governance-mcp-cli bot verify --provider telegram --bot-key main --json
268
268
 
269
269
  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.
270
270
 
271
+ For direct Telegram edits, `--bot-key` still identifies which saved local entry to update. If one server bot name expands to multiple roles such as `approval / worker / review / monitor`, prefer the guided `bot edit` flow so you can keep the current grouped settings, edit one role only, or walk every role in sequence instead of forcing one entry-level AI override.
272
+
271
273
  Current support status:
272
274
 
273
275
  - Telegram: full local bot entry management, token verification, bot-to-AI binding, inbound runner support
@@ -809,6 +809,16 @@ function renderBotListPayload(provider, state, deps) {
809
809
  };
810
810
  }
811
811
 
812
+ function formatRoleProfileOutputLine(profile) {
813
+ const current = safeObject(profile);
814
+ return [
815
+ current.client ? `client=${current.client}` : "client=(blank)",
816
+ current.model ? `model=${current.model}` : "model=(blank)",
817
+ current.permissionMode ? `permission=${current.permissionMode}` : "permission=(blank)",
818
+ current.reasoningEffort ? `reasoning=${current.reasoningEffort}` : "reasoning=(blank)",
819
+ ].join(" | ");
820
+ }
821
+
812
822
  function buildBotShowPayload(provider, state, entry, deps, extras = {}) {
813
823
  if (provider === "telegram") {
814
824
  const selectedEntry = entry || null;
@@ -829,6 +839,8 @@ function buildBotShowPayload(provider, state, entry, deps, extras = {}) {
829
839
  permissionMode: selectedEntry.permissionMode,
830
840
  reasoningEffort: selectedEntry.reasoningEffort,
831
841
  } : null,
842
+ serverBinding: safeObject(extras.serverBinding),
843
+ routeLinks: safeObject(extras.routeLinks),
832
844
  ...safeObject(extras),
833
845
  };
834
846
  }
@@ -846,6 +858,60 @@ function buildBotShowPayload(provider, state, entry, deps, extras = {}) {
846
858
  };
847
859
  }
848
860
 
861
+ function describeRouteRuntimeProfile(routeRole, serverBinding) {
862
+ const binding = safeObject(serverBinding);
863
+ if (binding.mode === "group") {
864
+ return safeObject(safeObject(binding.effectiveRoleProfiles)[String(routeRole || "").trim()]);
865
+ }
866
+ return safeObject(binding.effectiveRoleProfile);
867
+ }
868
+
869
+ function summarizeTelegramRouteLinks(parsedEnv, entry, serverBinding, deps) {
870
+ const selectedEntry = safeObject(entry);
871
+ const binding = safeObject(serverBinding);
872
+ const config = safeObject(requireDependency(deps, "loadBotRunnerConfig")({ persistIfNeeded: true }));
873
+ const routes = ensureArray(config.routes);
874
+ const entryKey = String(selectedEntry.key || "").trim();
875
+ const defaultBotKey = String(safeObject(parsedEnv).TELEGRAM_DEFAULT_BOT_KEY || "").trim();
876
+ const serverBotID = String(binding.serverBotID || selectedEntry.serverBotID || "").trim();
877
+ const normalizedNames = new Set(
878
+ [
879
+ normalizeServerBotIdentityText(selectedEntry.username),
880
+ normalizeServerBotIdentityText(binding.name),
881
+ normalizeServerBotIdentityText(entryKey),
882
+ ].filter(Boolean),
883
+ );
884
+ const linkedRoutes = [];
885
+ routes.forEach((rawRoute, index) => {
886
+ const route = safeObject(rawRoute);
887
+ if (route.enabled === false) return;
888
+ if (String(route.provider || "").trim().toLowerCase() !== "telegram") return;
889
+ const routeName = String(route.name || route.route_name || `telegram-route-${index + 1}`).trim();
890
+ const routeBotID = String(route.bot_id || route.botID || "").trim();
891
+ const routeBotName = normalizeServerBotIdentityText(route.bot_name || route.botName || "");
892
+ const routeRole = String(route.role_profile || route.roleProfile || route.role || "").trim();
893
+ let matchedBy = "";
894
+ if (serverBotID && routeBotID === serverBotID) {
895
+ matchedBy = "bot_id";
896
+ } else if (routeBotName && normalizedNames.has(routeBotName)) {
897
+ matchedBy = "bot_name";
898
+ } else if (!routeBotID && !routeBotName && entryKey && defaultBotKey && entryKey === defaultBotKey) {
899
+ matchedBy = "default_bot";
900
+ }
901
+ if (!matchedBy) return;
902
+ linkedRoutes.push({
903
+ routeName,
904
+ matchedBy,
905
+ routeRole,
906
+ runtimeRoleProfile: describeRouteRuntimeProfile(routeRole, binding),
907
+ });
908
+ });
909
+ return {
910
+ total: linkedRoutes.length,
911
+ routes: linkedRoutes,
912
+ };
913
+ }
914
+
849
915
  function printBotList(provider, state, deps) {
850
916
  process.stdout.write(`${providerLabel(provider, deps)}\n`);
851
917
  process.stdout.write(` file: ${state.filePath}\n`);
@@ -883,6 +949,8 @@ function printBotList(provider, state, deps) {
883
949
  function printBotShow(provider, state, entry, deps, extras = {}) {
884
950
  if (provider === "telegram") {
885
951
  const selectedEntry = entry || {};
952
+ const serverBinding = safeObject(extras.serverBinding);
953
+ const routeLinks = safeObject(extras.routeLinks);
886
954
  process.stdout.write(`${providerLabel(provider, deps)} bot\n`);
887
955
  process.stdout.write(` file: ${state.filePath}\n`);
888
956
  process.stdout.write(` name: ${telegramEntryDisplayName(selectedEntry)}\n`);
@@ -896,9 +964,32 @@ function printBotShow(provider, state, entry, deps, extras = {}) {
896
964
  process.stdout.write(` ai_model: ${selectedEntry.model || "-"}\n`);
897
965
  process.stdout.write(` permission_mode: ${selectedEntry.permissionMode || "-"}\n`);
898
966
  process.stdout.write(` reasoning_effort: ${selectedEntry.reasoningEffort || "-"}\n`);
899
- if (extras.serverBinding) {
900
- process.stdout.write(` server_binding: ${extras.serverBinding.ok ? "OK" : "FAIL"}${extras.serverBinding.detail ? ` (${extras.serverBinding.detail})` : ""}\n`);
967
+ if (Object.keys(serverBinding).length) {
968
+ process.stdout.write(` server_binding: ${serverBinding.ok ? "OK" : "FAIL"}${serverBinding.detail ? ` (${serverBinding.detail})` : ""}\n`);
969
+ process.stdout.write(` server_binding_mode: ${serverBinding.mode || "-"}\n`);
970
+ process.stdout.write(` server_bot_name: ${serverBinding.name || "-"}\n`);
971
+ process.stdout.write(` server_bot_id: ${serverBinding.serverBotID || selectedEntry.serverBotID || "-" }\n`);
972
+ if (serverBinding.mode === "group") {
973
+ process.stdout.write(` server_roles: ${ensureArray(serverBinding.roles).join(", ") || "-"}\n`);
974
+ const groupedProfiles = safeObject(serverBinding.effectiveRoleProfiles);
975
+ Object.keys(groupedProfiles).forEach((role) => {
976
+ process.stdout.write(` runtime_role_profile[${role}]: ${formatRoleProfileOutputLine(groupedProfiles[role])}\n`);
977
+ });
978
+ } else if (serverBinding.effectiveRoleProfile) {
979
+ process.stdout.write(` server_role: ${serverBinding.role || "-"}\n`);
980
+ process.stdout.write(` runtime_role_profile: ${formatRoleProfileOutputLine(serverBinding.effectiveRoleProfile)}\n`);
981
+ }
901
982
  }
983
+ process.stdout.write(` route_links: ${intFromRaw(routeLinks.total, 0)}\n`);
984
+ ensureArray(routeLinks.routes).forEach((route) => {
985
+ const routeRuntime = safeObject(route.runtimeRoleProfile);
986
+ const routeDetail = [
987
+ `matched_by=${String(route.matchedBy || "-")}`,
988
+ route.routeRole ? `route_role=${String(route.routeRole || "").trim()}` : "",
989
+ Object.keys(routeRuntime).length ? `runtime=${formatRoleProfileOutputLine(routeRuntime)}` : "",
990
+ ].filter(Boolean).join(" ");
991
+ process.stdout.write(` linked_route[${String(route.routeName || "-")}]: ${routeDetail || "-"}\n`);
992
+ });
902
993
  return;
903
994
  }
904
995
  const tokenKey = providerTokenKey(provider, deps);
@@ -1348,6 +1439,31 @@ function formatRoleProfileSummary(profile) {
1348
1439
  ].join(" | ");
1349
1440
  }
1350
1441
 
1442
+ function serializeRoleProfile(profile) {
1443
+ const current = safeObject(profile);
1444
+ return {
1445
+ role: String(current.role || "").trim(),
1446
+ client: String(current.client || "").trim(),
1447
+ model: String(current.model || "").trim(),
1448
+ permissionMode: String(current.permissionMode || "").trim(),
1449
+ reasoningEffort: String(current.reasoningEffort || "").trim(),
1450
+ };
1451
+ }
1452
+
1453
+ function buildRoleProfileOutput(roleName, deps) {
1454
+ return serializeRoleProfile(currentRoleProfileState(roleName, deps));
1455
+ }
1456
+
1457
+ function buildGroupedRoleProfileOutput(roles, deps) {
1458
+ const output = {};
1459
+ preferredRoleSort(roles).forEach((role) => {
1460
+ const normalizedRole = String(role || "").trim();
1461
+ if (!normalizedRole) return;
1462
+ output[normalizedRole] = buildRoleProfileOutput(normalizedRole, deps);
1463
+ });
1464
+ return output;
1465
+ }
1466
+
1351
1467
  function persistRoleProfileState(profile, deps) {
1352
1468
  const current = safeObject(profile);
1353
1469
  const role = requireDependency(deps, "normalizeRunnerRoleProfileName")(current.role || "");
@@ -1416,9 +1532,13 @@ async function maybePromptGroupedServerRoleProfiles(ui, serverBot, deps) {
1416
1532
  if (roles.length <= 1) {
1417
1533
  return false;
1418
1534
  }
1535
+ process.stdout.write(`Current grouped role execution profiles for "${String(serverBot?.name || "").trim() || "telegram"}":\n`);
1536
+ roles.forEach((role) => {
1537
+ process.stdout.write(` - ${role}: ${formatRoleProfileSummary(currentRoleProfileState(role, deps))}\n`);
1538
+ });
1419
1539
  const editChoice = await promptChoice(
1420
1540
  ui,
1421
- `Server bot "${String(serverBot?.name || "").trim() || "telegram"}" uses multiple roles. What should happen to the local execution settings?`,
1541
+ "Grouped role settings",
1422
1542
  [
1423
1543
  {
1424
1544
  value: "keep",
@@ -1426,22 +1546,173 @@ async function maybePromptGroupedServerRoleProfiles(ui, serverBot, deps) {
1426
1546
  description: "use the existing role_profiles defaults as-is",
1427
1547
  },
1428
1548
  {
1429
- value: "review",
1430
- label: "Review role settings",
1431
- description: "check each role and change AI client/model/permission/reasoning when needed",
1549
+ value: "one",
1550
+ label: "Edit one role",
1551
+ description: "change only the role that needs an update",
1552
+ },
1553
+ {
1554
+ value: "all",
1555
+ label: "Edit all roles",
1556
+ description: "review each role in sequence",
1432
1557
  },
1433
1558
  ],
1434
1559
  { defaultIndex: 0 },
1435
1560
  );
1436
- if (editChoice?.value !== "review") {
1561
+ if (editChoice?.value === "keep") {
1437
1562
  return false;
1438
1563
  }
1439
- for (const role of roles) {
1440
- await promptRoleExecutionProfile(ui, role, deps);
1564
+ if (editChoice?.value === "all") {
1565
+ for (const role of roles) {
1566
+ await promptRoleExecutionProfile(ui, role, deps);
1567
+ }
1568
+ return true;
1569
+ }
1570
+ const remaining = new Set(roles);
1571
+ while (remaining.size) {
1572
+ const roleChoice = await promptChoice(
1573
+ ui,
1574
+ "Select role to edit",
1575
+ [
1576
+ ...Array.from(remaining).map((role) => ({
1577
+ value: role,
1578
+ label: role,
1579
+ description: formatRoleProfileSummary(currentRoleProfileState(role, deps)),
1580
+ })),
1581
+ {
1582
+ value: "__done__",
1583
+ label: "Done",
1584
+ description: "finish grouped role editing",
1585
+ },
1586
+ ],
1587
+ { defaultIndex: 0 },
1588
+ );
1589
+ if (!roleChoice || roleChoice.value === "__done__") {
1590
+ break;
1591
+ }
1592
+ await promptRoleExecutionProfile(ui, roleChoice.value, deps);
1593
+ remaining.delete(roleChoice.value);
1594
+ if (!remaining.size) {
1595
+ break;
1596
+ }
1597
+ if (!await promptYesNo(ui, "Edit another role?", false)) {
1598
+ break;
1599
+ }
1441
1600
  }
1442
1601
  return true;
1443
1602
  }
1444
1603
 
1604
+ async function resolveTelegramServerBindingDetails(envConfig, flags, deps) {
1605
+ const current = safeObject(envConfig);
1606
+ if (!String(current.serverBotID || "").trim() && !String(current.botUsername || "").trim() && !String(current.botKey || "").trim()) {
1607
+ return null;
1608
+ }
1609
+ const lookup = await requireDependency(deps, "listServerBots")({
1610
+ provider: "telegram",
1611
+ baseURL: flags["base-url"] || deps.defaultSiteURL,
1612
+ timeoutSeconds: intFromRaw(flags["timeout-seconds"], 15) || 15,
1613
+ });
1614
+ if (!lookup?.ok) {
1615
+ return {
1616
+ ok: false,
1617
+ mode: "lookup_error",
1618
+ matchedBy: "",
1619
+ name: "",
1620
+ role: "",
1621
+ roles: [],
1622
+ effectiveRoleProfile: null,
1623
+ effectiveRoleProfiles: {},
1624
+ detail: `server bot lookup unavailable: ${lookup?.error || "unknown error"}`,
1625
+ };
1626
+ }
1627
+ const bots = ensureArray(lookup.bots).map((bot) => ({
1628
+ id: String(bot?.id || "").trim(),
1629
+ role: String(bot?.role || bot?.bot_role || "").trim(),
1630
+ name: String(bot?.name || "").trim(),
1631
+ })).filter((bot) => bot.id);
1632
+ const resolveGroupedPayload = (matches, matchedBy) => {
1633
+ const roles = preferredRoleSort(summarizeServerBotRoles(matches));
1634
+ return {
1635
+ ok: true,
1636
+ mode: "group",
1637
+ matchedBy,
1638
+ name: String(matches[0]?.name || "").trim(),
1639
+ role: "",
1640
+ roles,
1641
+ serverBotID: String(current.serverBotID || "").trim(),
1642
+ effectiveRoleProfile: null,
1643
+ effectiveRoleProfiles: buildGroupedRoleProfileOutput(roles, deps),
1644
+ detail: `${String(matches[0]?.name || "").trim() || "(unnamed)"} roles: ${roles.join(", ") || "-"}`,
1645
+ };
1646
+ };
1647
+ if (String(current.serverBotID || "").trim()) {
1648
+ const match = bots.find((bot) => bot.id === String(current.serverBotID || "").trim());
1649
+ if (!match) {
1650
+ return {
1651
+ ok: false,
1652
+ mode: "missing",
1653
+ matchedBy: "server_bot_id",
1654
+ name: "",
1655
+ role: "",
1656
+ roles: [],
1657
+ serverBotID: String(current.serverBotID || "").trim(),
1658
+ effectiveRoleProfile: null,
1659
+ effectiveRoleProfiles: {},
1660
+ detail: `server bot ${current.serverBotID} not found`,
1661
+ };
1662
+ }
1663
+ const sameNameMatches = bots.filter((bot) => normalizeServerBotIdentityText(bot.name) === normalizeServerBotIdentityText(match.name));
1664
+ if (sameNameMatches.length > 1) {
1665
+ return resolveGroupedPayload(sameNameMatches, "server_bot_id");
1666
+ }
1667
+ const effectiveRoleProfile = buildRoleProfileOutput(match.role || current.roleProfile || "", deps);
1668
+ return {
1669
+ ok: true,
1670
+ mode: "single",
1671
+ matchedBy: "server_bot_id",
1672
+ name: match.name,
1673
+ role: match.role,
1674
+ roles: summarizeServerBotRoles([match]),
1675
+ serverBotID: match.id,
1676
+ effectiveRoleProfile,
1677
+ effectiveRoleProfiles: {},
1678
+ detail: `${String(match.name || "").trim() || "(unnamed)"} [${String(match.role || "").trim() || "-"}]`,
1679
+ };
1680
+ }
1681
+ const normalizedServerIdentity = normalizeServerBotIdentityText(current.botUsername || current.botKey);
1682
+ const matches = bots.filter((bot) => normalizeServerBotIdentityText(bot.name) === normalizedServerIdentity);
1683
+ if (!matches.length) {
1684
+ return {
1685
+ ok: false,
1686
+ mode: "missing",
1687
+ matchedBy: "server_bot_name",
1688
+ name: "",
1689
+ role: "",
1690
+ roles: [],
1691
+ serverBotID: "",
1692
+ effectiveRoleProfile: null,
1693
+ effectiveRoleProfiles: {},
1694
+ detail: `no server bot matched ${current.botUsername ? `@${current.botUsername}` : current.botKey}`,
1695
+ };
1696
+ }
1697
+ if (matches.length > 1) {
1698
+ return resolveGroupedPayload(matches, "server_bot_name");
1699
+ }
1700
+ const match = matches[0] || {};
1701
+ const effectiveRoleProfile = buildRoleProfileOutput(match.role || current.roleProfile || "", deps);
1702
+ return {
1703
+ ok: true,
1704
+ mode: "single",
1705
+ matchedBy: "server_bot_name",
1706
+ name: String(match.name || "").trim(),
1707
+ role: String(match.role || "").trim(),
1708
+ roles: summarizeServerBotRoles([match]),
1709
+ serverBotID: String(match.id || "").trim(),
1710
+ effectiveRoleProfile,
1711
+ effectiveRoleProfiles: {},
1712
+ detail: `${String(match.name || "").trim() || "(unnamed)"} [${String(match.role || "").trim() || "-"}]`,
1713
+ };
1714
+ }
1715
+
1445
1716
  function buildTemporaryTelegramEnvConfig({ token, apiBaseURL }) {
1446
1717
  return {
1447
1718
  ok: true,
@@ -1840,54 +2111,19 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
1840
2111
  envConfig,
1841
2112
  intFromRaw(flags["timeout-seconds"], 15) || 15,
1842
2113
  );
1843
- let serverBinding = null;
1844
2114
  let overallOK = Boolean(result.ok);
1845
- if (provider === "telegram" && (String(envConfig.serverBotID || "").trim() || String(envConfig.botUsername || "").trim() || String(envConfig.botKey || "").trim())) {
1846
- const lookup = await requireDependency(deps, "listServerBots")({
1847
- provider,
1848
- baseURL: flags["base-url"] || deps.defaultSiteURL,
1849
- timeoutSeconds: intFromRaw(flags["timeout-seconds"], 15) || 15,
1850
- });
1851
- if (!lookup?.ok) {
1852
- serverBinding = {
1853
- ok: false,
1854
- detail: `server bot lookup unavailable: ${lookup?.error || "unknown error"}`,
1855
- };
2115
+ let serverBinding = null;
2116
+ let routeLinks = null;
2117
+ if (provider === "telegram") {
2118
+ serverBinding = await resolveTelegramServerBindingDetails(envConfig, flags, deps);
2119
+ if (serverBinding && !serverBinding.ok) {
1856
2120
  overallOK = false;
1857
- } else {
1858
- const normalizedServerIdentity = normalizeServerBotIdentityText(envConfig.botUsername || envConfig.botKey);
1859
- if (String(envConfig.serverBotID || "").trim()) {
1860
- const match = ensureArray(lookup.bots).find((bot) => String(bot.id || "").trim() === String(envConfig.serverBotID || "").trim());
1861
- if (!match) {
1862
- serverBinding = {
1863
- ok: false,
1864
- detail: `server bot ${envConfig.serverBotID} not found`,
1865
- };
1866
- overallOK = false;
1867
- } else {
1868
- serverBinding = {
1869
- ok: true,
1870
- detail: `${String(match.name || "").trim() || "(unnamed)"} [${String(match.role || "").trim() || "-"}]`,
1871
- };
1872
- }
1873
- } else {
1874
- const matches = ensureArray(lookup.bots).filter(
1875
- (bot) => normalizeServerBotIdentityText(bot?.name) === normalizedServerIdentity,
1876
- );
1877
- if (!matches.length) {
1878
- serverBinding = {
1879
- ok: false,
1880
- detail: `no server bot matched ${envConfig.botUsername ? `@${envConfig.botUsername}` : envConfig.botKey}`,
1881
- };
1882
- overallOK = false;
1883
- } else {
1884
- serverBinding = {
1885
- ok: true,
1886
- detail: `${String(matches[0]?.name || "").trim() || "(unnamed)"} roles: ${summarizeServerBotRoles(matches).join(", ") || "-"}`,
1887
- };
1888
- }
1889
- }
1890
2121
  }
2122
+ routeLinks = summarizeTelegramRouteLinks(state.parsed, {
2123
+ key: envConfig.botKey,
2124
+ serverBotID: envConfig.serverBotID,
2125
+ username: envConfig.botUsername,
2126
+ }, serverBinding, deps);
1891
2127
  }
1892
2128
  const interactiveJsonChoice = (
1893
2129
  !boolFromRaw(flags.json, false)
@@ -1920,24 +2156,49 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
1920
2156
  permissionMode: envConfig.permissionMode || "",
1921
2157
  reasoningEffort: envConfig.reasoningEffort || "",
1922
2158
  serverBinding,
2159
+ routeLinks,
1923
2160
  }, null, 2)}\n`,
1924
2161
  );
1925
2162
  } else {
1926
- process.stdout.write(
1927
- [
1928
- `${providerLabel(provider, deps)} verify: ${overallOK ? "OK" : "FAIL"}`,
1929
- `file: ${envConfig.filePath}`,
1930
- provider === "telegram" ? `bot_key: ${envConfig.botKey || "-"}` : "",
1931
- provider === "telegram" ? `server_bot_id: ${envConfig.serverBotID || "-"}` : "",
1932
- provider === "telegram" ? `role_profile: ${envConfig.roleProfile || "-"}` : "",
1933
- provider === "telegram" ? `ai_client: ${envConfig.client || "-"}` : "",
1934
- provider === "telegram" ? `ai_model: ${envConfig.model || "-"}` : "",
1935
- provider === "telegram" ? `permission_mode: ${envConfig.permissionMode || "-"}` : "",
1936
- provider === "telegram" ? `reasoning_effort: ${envConfig.reasoningEffort || "-"}` : "",
1937
- `detail: ${result.detail || "-"}`,
1938
- serverBinding ? `server_binding: ${serverBinding.ok ? "OK" : "FAIL"}${serverBinding.detail ? ` (${serverBinding.detail})` : ""}` : "",
1939
- ].filter(Boolean).join("\n") + "\n",
1940
- );
2163
+ const lines = [
2164
+ `${providerLabel(provider, deps)} verify: ${overallOK ? "OK" : "FAIL"}`,
2165
+ `file: ${envConfig.filePath}`,
2166
+ provider === "telegram" ? `bot_key: ${envConfig.botKey || "-"}` : "",
2167
+ provider === "telegram" ? `server_bot_id: ${envConfig.serverBotID || "-"}` : "",
2168
+ provider === "telegram" ? `role_profile: ${envConfig.roleProfile || "-"}` : "",
2169
+ provider === "telegram" ? `ai_client: ${envConfig.client || "-"}` : "",
2170
+ provider === "telegram" ? `ai_model: ${envConfig.model || "-"}` : "",
2171
+ provider === "telegram" ? `permission_mode: ${envConfig.permissionMode || "-"}` : "",
2172
+ provider === "telegram" ? `reasoning_effort: ${envConfig.reasoningEffort || "-"}` : "",
2173
+ `detail: ${result.detail || "-"}`,
2174
+ serverBinding ? `server_binding: ${serverBinding.ok ? "OK" : "FAIL"}${serverBinding.detail ? ` (${serverBinding.detail})` : ""}` : "",
2175
+ routeLinks ? `route_links: ${intFromRaw(routeLinks.total, 0)}` : "",
2176
+ ].filter(Boolean);
2177
+ if (provider === "telegram" && serverBinding) {
2178
+ lines.push(`server_binding_mode: ${serverBinding.mode || "-"}`);
2179
+ lines.push(`server_bot_name: ${serverBinding.name || "-"}`);
2180
+ lines.push(`server_roles: ${ensureArray(serverBinding.roles).join(", ") || "-"}`);
2181
+ if (serverBinding.mode === "group") {
2182
+ const groupedProfiles = safeObject(serverBinding.effectiveRoleProfiles);
2183
+ Object.keys(groupedProfiles).forEach((role) => {
2184
+ lines.push(`runtime_role_profile[${role}]: ${formatRoleProfileOutputLine(groupedProfiles[role])}`);
2185
+ });
2186
+ } else if (serverBinding.effectiveRoleProfile) {
2187
+ lines.push(`runtime_role_profile: ${formatRoleProfileOutputLine(serverBinding.effectiveRoleProfile)}`);
2188
+ }
2189
+ }
2190
+ if (provider === "telegram" && routeLinks) {
2191
+ ensureArray(routeLinks.routes).forEach((route) => {
2192
+ const routeRuntime = safeObject(route.runtimeRoleProfile);
2193
+ const routeDetail = [
2194
+ `matched_by=${String(route.matchedBy || "-")}`,
2195
+ route.routeRole ? `route_role=${String(route.routeRole || "").trim()}` : "",
2196
+ Object.keys(routeRuntime).length ? `runtime=${formatRoleProfileOutputLine(routeRuntime)}` : "",
2197
+ ].filter(Boolean).join(" ");
2198
+ lines.push(`linked_route[${String(route.routeName || "-")}]: ${routeDetail || "-"}`);
2199
+ });
2200
+ }
2201
+ process.stdout.write(`${lines.join("\n")}\n`);
1941
2202
  }
1942
2203
  if (!overallOK) {
1943
2204
  process.exitCode = 1;
@@ -2096,12 +2357,22 @@ async function runBotShow(ui, flags, deps, explicitProvider = "") {
2096
2357
  const state = loadProviderEnvState(provider, deps);
2097
2358
  if (provider === "telegram") {
2098
2359
  const entry = await resolveTelegramEntryForShow(ui, state.parsed, flags, deps);
2099
- const payload = buildBotShowPayload(provider, state, entry, deps);
2360
+ const serverBinding = await resolveTelegramServerBindingDetails(
2361
+ requireDependency(deps, "loadProviderEnvConfig")(provider, {
2362
+ botKey: entry.key,
2363
+ botID: entry.serverBotID,
2364
+ botName: entry.username || entry.key,
2365
+ }),
2366
+ flags,
2367
+ deps,
2368
+ );
2369
+ const routeLinks = summarizeTelegramRouteLinks(state.parsed, entry, serverBinding, deps);
2370
+ const payload = buildBotShowPayload(provider, state, entry, deps, { serverBinding, routeLinks });
2100
2371
  if (boolFromRaw(flags.json, false)) {
2101
2372
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
2102
2373
  return;
2103
2374
  }
2104
- printBotShow(provider, state, entry, deps);
2375
+ printBotShow(provider, state, entry, deps, { serverBinding, routeLinks });
2105
2376
  return;
2106
2377
  }
2107
2378
  const payload = buildBotShowPayload(provider, state, null, deps);
@@ -84,6 +84,11 @@ function readTrailingJSON(rawText) {
84
84
  return readJSON(text.slice(start));
85
85
  }
86
86
 
87
+ function intFromRaw(rawValue, fallback = 0) {
88
+ const parsed = Number.parseInt(String(rawValue ?? "").trim(), 10);
89
+ return Number.isFinite(parsed) ? parsed : fallback;
90
+ }
91
+
87
92
  function createMockServer(options = {}) {
88
93
  const serverBots = ensureArray(options.serverBots).length
89
94
  ? ensureArray(options.serverBots)
@@ -356,19 +361,21 @@ export async function runSelftestBotCommands(push, deps) {
356
361
  "2", // bot entry: ryoai_bot
357
362
  "1", // keep username
358
363
  "1", // keep token
359
- "2", // review grouped server role settings
360
- "1", // monitor: keep current role profile settings
361
- "1", // review: keep current role profile settings
364
+ "2", // grouped role settings: edit one role
365
+ "3", // select role to edit: worker
362
366
  "2", // worker: edit settings
363
367
  "3", // worker AI client: claude
364
368
  "worker-sonnet-4",
365
369
  "4", // worker permission: danger_full_access
366
370
  "4", // worker reasoning: high
371
+ "y", // edit another role
372
+ "3", // select role to edit: approval
367
373
  "2", // approval: edit settings
368
374
  "4", // approval AI client: gemini
369
375
  "approval-pro-2",
370
376
  "4", // approval permission: danger_full_access
371
377
  "4", // approval reasoning: high
378
+ "n", // stop editing roles
372
379
  "1", // keep current default setting
373
380
  "y", // save
374
381
  ]),
@@ -386,6 +393,26 @@ export async function runSelftestBotCommands(push, deps) {
386
393
  && String(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).approval).model || "") === "approval-pro-2",
387
394
  `worker=${JSON.stringify(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).worker))} approval=${JSON.stringify(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).approval))}`,
388
395
  );
396
+
397
+ const groupedShowResult = await runCLI({
398
+ cliPath,
399
+ args: [
400
+ "bot", "show",
401
+ "--provider", "telegram",
402
+ "--bot-key", "ryoai_bot",
403
+ "--base-url", `http://127.0.0.1:${groupedMock.port}`,
404
+ "--json", "true",
405
+ ],
406
+ env,
407
+ });
408
+ const groupedShowPayload = readJSON(groupedShowResult.stdout);
409
+ push(
410
+ "bot_show_reports_grouped_server_roles",
411
+ safeObject(groupedShowPayload.serverBinding).mode === "group"
412
+ && safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).worker?.client === "claude"
413
+ && safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).approval?.client === "gemini",
414
+ `mode=${String(safeObject(groupedShowPayload.serverBinding).mode || "")} worker=${String(safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).worker?.client || "")} approval=${String(safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).approval?.client || "")}`,
415
+ );
389
416
  } finally {
390
417
  await groupedMock.close();
391
418
  await runCLI({
@@ -406,16 +433,58 @@ export async function runSelftestBotCommands(push, deps) {
406
433
  "bot", "show",
407
434
  "--provider", "telegram",
408
435
  "--bot-key", "monitorselftestbot",
436
+ "--base-url", baseURL,
409
437
  "--json", "true",
410
438
  ],
411
439
  env,
412
440
  });
413
441
  const showPayload = readJSON(showResult.stdout);
442
+ const runnerConfigPath = path.join(tempHome, ".metheus", "bot-runner.json");
443
+ const runnerConfig = readJSON(fs.readFileSync(runnerConfigPath, "utf8"));
444
+ runnerConfig.routes = [
445
+ {
446
+ name: "telegram-monitor",
447
+ enabled: true,
448
+ project_id: "03c586a2-006d-4051-83b4-f353a5813176",
449
+ provider: "telegram",
450
+ bot_id: mock.bots[0].id,
451
+ role_profile: "monitor",
452
+ },
453
+ {
454
+ name: "telegram-default",
455
+ enabled: true,
456
+ project_id: "03c586a2-006d-4051-83b4-f353a5813176",
457
+ provider: "telegram",
458
+ },
459
+ ];
460
+ fs.writeFileSync(runnerConfigPath, `${JSON.stringify(runnerConfig, null, 2)}\n`, "utf8");
461
+ const showWithRoutesResult = await runCLI({
462
+ cliPath,
463
+ args: [
464
+ "bot", "show",
465
+ "--provider", "telegram",
466
+ "--bot-key", "monitorselftestbot",
467
+ "--base-url", baseURL,
468
+ "--json", "true",
469
+ ],
470
+ env,
471
+ });
472
+ const showWithRoutesPayload = readJSON(showWithRoutesResult.stdout);
414
473
  push(
415
474
  "bot_show_returns_selected_telegram_entry",
416
475
  safeObject(showPayload.entry).key === "monitorselftestbot"
417
- && safeObject(showPayload.entry).client === "codex",
418
- `key=${String(safeObject(showPayload.entry).key || "")} client=${String(safeObject(showPayload.entry).client || "")}`,
476
+ && safeObject(showPayload.entry).client === "codex"
477
+ && safeObject(showPayload.serverBinding).mode === "single",
478
+ `key=${String(safeObject(showPayload.entry).key || "")} client=${String(safeObject(showPayload.entry).client || "")} mode=${String(safeObject(showPayload.serverBinding).mode || "")}`,
479
+ );
480
+ push(
481
+ "bot_show_reports_linked_runner_routes",
482
+ intFromRaw(safeObject(showWithRoutesPayload.routeLinks).total, 0) >= 1
483
+ && ensureArray(safeObject(showWithRoutesPayload.routeLinks).routes).some((route) => (
484
+ String(safeObject(route).routeName || "") === "telegram-monitor"
485
+ && String(safeObject(route).matchedBy || "") === "bot_id"
486
+ )),
487
+ `routes=${JSON.stringify(safeObject(showWithRoutesPayload.routeLinks))}`,
419
488
  );
420
489
 
421
490
  await runCLI({
@@ -436,7 +505,6 @@ export async function runSelftestBotCommands(push, deps) {
436
505
  "3", // workspace_write
437
506
  "2", // change reasoning effort
438
507
  "3", // medium
439
- "1", // keep bot key
440
508
  "1", // keep default setting
441
509
  "y", // save
442
510
  ]),
@@ -518,8 +586,9 @@ export async function runSelftestBotCommands(push, deps) {
518
586
  "bot_verify_guided_can_emit_json",
519
587
  guidedVerifyPayload.ok === true
520
588
  && safeObject(guidedVerifyPayload.serverBinding).ok === true
589
+ && safeObject(guidedVerifyPayload.serverBinding).mode === "single"
521
590
  && String(guidedVerifyPayload.client || "") === "claude",
522
- `verify=${String(guidedVerifyPayload.ok)} server=${String(safeObject(guidedVerifyPayload.serverBinding).detail || "")}`,
591
+ `verify=${String(guidedVerifyPayload.ok)} mode=${String(safeObject(guidedVerifyPayload.serverBinding).mode || "")} server=${String(safeObject(guidedVerifyPayload.serverBinding).detail || "")}`,
523
592
  );
524
593
 
525
594
  await runCLI({
@@ -556,8 +625,17 @@ export async function runSelftestBotCommands(push, deps) {
556
625
  "bot_verify_cross_checks_server_bot_binding",
557
626
  verifyPayload.ok === true
558
627
  && safeObject(verifyPayload.serverBinding).ok === true
628
+ && safeObject(verifyPayload.serverBinding).mode === "single"
559
629
  && String(verifyPayload.client || "") === "claude",
560
- `verify=${String(verifyPayload.ok)} server=${String(safeObject(verifyPayload.serverBinding).detail || "")}`,
630
+ `verify=${String(verifyPayload.ok)} mode=${String(safeObject(verifyPayload.serverBinding).mode || "")} server=${String(safeObject(verifyPayload.serverBinding).detail || "")}`,
631
+ );
632
+ push(
633
+ "bot_verify_reports_linked_runner_routes",
634
+ intFromRaw(safeObject(verifyPayload.routeLinks).total, 0) >= 1
635
+ && ensureArray(safeObject(verifyPayload.routeLinks).routes).some((route) => (
636
+ String(safeObject(route).routeName || "") === "telegram-monitor"
637
+ )),
638
+ `routes=${JSON.stringify(safeObject(verifyPayload.routeLinks))}`,
561
639
  );
562
640
 
563
641
  await runCLI({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.72",
3
+ "version": "0.2.74",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [