metheus-governance-mcp-cli 0.2.71 → 0.2.73

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
@@ -231,7 +231,8 @@ Behavior:
231
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
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
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.
234
- - `bot edit` without flags now uses the same sequential flow every time: provider -> bot entry -> username/token review -> AI field choices -> default choice -> save.
234
+ - `bot edit` without flags now uses the same sequential flow every time: provider -> bot entry -> username/token review -> grouped server-role review when needed -> AI field choices -> default choice -> save.
235
+ - if one server bot name maps to multiple server roles, `bot edit` keeps the Telegram env entry bound to the server identity and lets you review the local `role_profiles` for each detected role instead of forcing one role/profile UUID choice up front.
235
236
  - In the normal Telegram edit path, the CLI keeps or re-resolves the server bot binding automatically. It no longer asks you to pick `approval / worker / review / monitor` or a server bot UUID first.
236
237
  - `bot set-default` without flags starts a guided numbered flow: provider -> bot entry -> confirm default change.
237
238
  - `bot verify` without flags starts a guided numbered flow: provider -> bot entry -> output format.
@@ -246,8 +247,8 @@ Behavior:
246
247
  - `AI_PERMISSION_MODE`
247
248
  - `AI_REASONING_EFFORT`
248
249
  - Slack and KakaoTalk currently use a single local token entry per provider in this command flow.
249
- - `bot verify` checks the configured local token and prints the current AI binding summary.
250
- - `bot show` prints one local bot entry in detail.
250
+ - `bot verify` checks the configured local token, cross-checks the server bot binding, and prints the effective runtime role profile summary that the runner will use.
251
+ - `bot show` prints one local bot entry in detail, including grouped role summaries when one server bot name expands to multiple roles.
251
252
  - `bot global` edits Telegram-wide local settings such as API base URL, allowed updates, and default bot key.
252
253
  - `bot set-default` updates `TELEGRAM_DEFAULT_BOT_KEY`.
253
254
  - `bot migrate` moves legacy `TELEGRAM_BOT_TOKEN` into a named Telegram bot entry.
@@ -258,7 +259,7 @@ Non-interactive examples:
258
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
259
260
  metheus-governance-mcp-cli bot add --provider telegram --non-interactive true --server-bot-id <server_bot_uuid> --token <telegram_bot_token> --default true
260
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
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
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
262
263
  metheus-governance-mcp-cli bot set-default --provider telegram --bot-key main --non-interactive true
263
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
264
265
  metheus-governance-mcp-cli bot remove --provider telegram --bot-key main --non-interactive true
@@ -267,6 +268,8 @@ metheus-governance-mcp-cli bot verify --provider telegram --bot-key main --json
267
268
 
268
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.
269
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
+
270
273
  Current support status:
271
274
 
272
275
  - Telegram: full local bot entry management, token verification, bot-to-AI binding, inbound runner support
package/cli.mjs CHANGED
@@ -3362,6 +3362,7 @@ function buildBotCommandDeps() {
3362
3362
  loadProviderEnvConfig,
3363
3363
  verifyLocalProviderToken,
3364
3364
  loadBotRunnerConfig,
3365
+ saveBotRunnerConfig,
3365
3366
  listServerBots: async ({ provider, baseURL, timeoutSeconds }) => {
3366
3367
  const authFlowDeps = buildAuthFlowDeps();
3367
3368
  const resolved = resolveCurrentAccessToken(authFlowDeps);
@@ -595,6 +595,8 @@ function buildDerivedTelegramBotKey(parsedEnv, deps, preferredValues, { excludeK
595
595
 
596
596
  async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps) {
597
597
  let serverRoleAutoResolved = "";
598
+ let groupedServerRoles = [];
599
+ let groupedServerName = "";
598
600
  if (current.serverBotID || current.__preferServerIdentity) {
599
601
  current.__preferServerIdentity = true;
600
602
  serverRoleAutoResolved = String(current.roleProfile || "").trim() || "__server_binding__";
@@ -641,9 +643,15 @@ async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps)
641
643
  current.serverBotID = "";
642
644
  current.__preferServerIdentity = true;
643
645
  current.roleProfile = "";
646
+ current.client = "";
647
+ current.model = "";
648
+ current.permissionMode = "";
649
+ current.reasoningEffort = "";
644
650
  serverRoleAutoResolved = "__server_role_group__";
651
+ groupedServerRoles = preferredRoleSort(serverBot.roles);
652
+ groupedServerName = String(serverBot.name || current.username || current.key).trim();
645
653
  process.stdout.write(
646
- `Using server Telegram bot "${serverBot.name || current.username || current.key}" with roles: ${ensureArray(serverBot.roles).join(", ")}. Runtime will use the server role automatically.\n`,
654
+ `Using server Telegram bot "${groupedServerName}" with roles: ${groupedServerRoles.join(", ")}. Runtime will use the server role automatically.\n`,
647
655
  );
648
656
  } else if (String(serverBot.botID || "").trim()) {
649
657
  current.serverBotID = String(serverBot.botID || "").trim();
@@ -672,53 +680,64 @@ async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps)
672
680
  }
673
681
  }
674
682
 
675
- const clientAction = await promptKeepChangeClear(ui, "AI client", {
676
- allowClear: true,
677
- defaultValue: current.client ? "keep" : "change",
678
- });
679
- if (clientAction === "change") {
680
- current.client = requireDependency(deps, "normalizeLocalAIClientName")(
681
- await promptAIClient(ui, deps, current.client),
682
- "",
683
+ if (serverRoleAutoResolved === "__server_role_group__") {
684
+ await maybePromptGroupedServerRoleProfiles(
685
+ ui,
686
+ {
687
+ name: groupedServerName,
688
+ roles: groupedServerRoles,
689
+ },
690
+ deps,
683
691
  );
684
- } else if (clientAction === "clear") {
685
- current.client = "";
686
- }
692
+ } else {
693
+ const clientAction = await promptKeepChangeClear(ui, "AI client", {
694
+ allowClear: true,
695
+ defaultValue: current.client ? "keep" : "change",
696
+ });
697
+ if (clientAction === "change") {
698
+ current.client = requireDependency(deps, "normalizeLocalAIClientName")(
699
+ await promptAIClient(ui, deps, current.client),
700
+ "",
701
+ );
702
+ } else if (clientAction === "clear") {
703
+ current.client = "";
704
+ }
687
705
 
688
- const modelAction = await promptKeepChangeClear(ui, "AI model", {
689
- allowClear: true,
690
- defaultValue: current.model ? "keep" : "change",
691
- });
692
- if (modelAction === "change") {
693
- current.model = await promptLine(ui, "AI model", current.model);
694
- } else if (modelAction === "clear") {
695
- current.model = "";
696
- }
706
+ const modelAction = await promptKeepChangeClear(ui, "AI model", {
707
+ allowClear: true,
708
+ defaultValue: current.model ? "keep" : "change",
709
+ });
710
+ if (modelAction === "change") {
711
+ current.model = await promptLine(ui, "AI model", current.model);
712
+ } else if (modelAction === "clear") {
713
+ current.model = "";
714
+ }
697
715
 
698
- const permissionAction = await promptKeepChangeClear(ui, "AI permission mode", {
699
- allowClear: true,
700
- defaultValue: current.permissionMode ? "keep" : "change",
701
- });
702
- if (permissionAction === "change") {
703
- current.permissionMode = requireDependency(deps, "normalizeLocalAIPermissionMode")(
704
- await promptPermissionMode(ui, current.permissionMode),
705
- "",
706
- );
707
- } else if (permissionAction === "clear") {
708
- current.permissionMode = "";
709
- }
716
+ const permissionAction = await promptKeepChangeClear(ui, "AI permission mode", {
717
+ allowClear: true,
718
+ defaultValue: current.permissionMode ? "keep" : "change",
719
+ });
720
+ if (permissionAction === "change") {
721
+ current.permissionMode = requireDependency(deps, "normalizeLocalAIPermissionMode")(
722
+ await promptPermissionMode(ui, current.permissionMode),
723
+ "",
724
+ );
725
+ } else if (permissionAction === "clear") {
726
+ current.permissionMode = "";
727
+ }
710
728
 
711
- const reasoningAction = await promptKeepChangeClear(ui, "AI reasoning effort", {
712
- allowClear: true,
713
- defaultValue: current.reasoningEffort ? "keep" : "change",
714
- });
715
- if (reasoningAction === "change") {
716
- current.reasoningEffort = requireDependency(deps, "normalizeLocalAIReasoningEffort")(
717
- await promptReasoningEffort(ui, current.reasoningEffort),
718
- "",
719
- );
720
- } else if (reasoningAction === "clear") {
721
- current.reasoningEffort = "";
729
+ const reasoningAction = await promptKeepChangeClear(ui, "AI reasoning effort", {
730
+ allowClear: true,
731
+ defaultValue: current.reasoningEffort ? "keep" : "change",
732
+ });
733
+ if (reasoningAction === "change") {
734
+ current.reasoningEffort = requireDependency(deps, "normalizeLocalAIReasoningEffort")(
735
+ await promptReasoningEffort(ui, current.reasoningEffort),
736
+ "",
737
+ );
738
+ } else if (reasoningAction === "clear") {
739
+ current.reasoningEffort = "";
740
+ }
722
741
  }
723
742
 
724
743
  const defaultChoice = await promptChoice(
@@ -790,6 +809,16 @@ function renderBotListPayload(provider, state, deps) {
790
809
  };
791
810
  }
792
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
+
793
822
  function buildBotShowPayload(provider, state, entry, deps, extras = {}) {
794
823
  if (provider === "telegram") {
795
824
  const selectedEntry = entry || null;
@@ -810,6 +839,7 @@ function buildBotShowPayload(provider, state, entry, deps, extras = {}) {
810
839
  permissionMode: selectedEntry.permissionMode,
811
840
  reasoningEffort: selectedEntry.reasoningEffort,
812
841
  } : null,
842
+ serverBinding: safeObject(extras.serverBinding),
813
843
  ...safeObject(extras),
814
844
  };
815
845
  }
@@ -864,6 +894,7 @@ function printBotList(provider, state, deps) {
864
894
  function printBotShow(provider, state, entry, deps, extras = {}) {
865
895
  if (provider === "telegram") {
866
896
  const selectedEntry = entry || {};
897
+ const serverBinding = safeObject(extras.serverBinding);
867
898
  process.stdout.write(`${providerLabel(provider, deps)} bot\n`);
868
899
  process.stdout.write(` file: ${state.filePath}\n`);
869
900
  process.stdout.write(` name: ${telegramEntryDisplayName(selectedEntry)}\n`);
@@ -877,8 +908,21 @@ function printBotShow(provider, state, entry, deps, extras = {}) {
877
908
  process.stdout.write(` ai_model: ${selectedEntry.model || "-"}\n`);
878
909
  process.stdout.write(` permission_mode: ${selectedEntry.permissionMode || "-"}\n`);
879
910
  process.stdout.write(` reasoning_effort: ${selectedEntry.reasoningEffort || "-"}\n`);
880
- if (extras.serverBinding) {
881
- process.stdout.write(` server_binding: ${extras.serverBinding.ok ? "OK" : "FAIL"}${extras.serverBinding.detail ? ` (${extras.serverBinding.detail})` : ""}\n`);
911
+ if (Object.keys(serverBinding).length) {
912
+ process.stdout.write(` server_binding: ${serverBinding.ok ? "OK" : "FAIL"}${serverBinding.detail ? ` (${serverBinding.detail})` : ""}\n`);
913
+ process.stdout.write(` server_binding_mode: ${serverBinding.mode || "-"}\n`);
914
+ process.stdout.write(` server_bot_name: ${serverBinding.name || "-"}\n`);
915
+ process.stdout.write(` server_bot_id: ${serverBinding.serverBotID || selectedEntry.serverBotID || "-" }\n`);
916
+ if (serverBinding.mode === "group") {
917
+ process.stdout.write(` server_roles: ${ensureArray(serverBinding.roles).join(", ") || "-"}\n`);
918
+ const groupedProfiles = safeObject(serverBinding.effectiveRoleProfiles);
919
+ Object.keys(groupedProfiles).forEach((role) => {
920
+ process.stdout.write(` runtime_role_profile[${role}]: ${formatRoleProfileOutputLine(groupedProfiles[role])}\n`);
921
+ });
922
+ } else if (serverBinding.effectiveRoleProfile) {
923
+ process.stdout.write(` server_role: ${serverBinding.role || "-"}\n`);
924
+ process.stdout.write(` runtime_role_profile: ${formatRoleProfileOutputLine(serverBinding.effectiveRoleProfile)}\n`);
925
+ }
882
926
  }
883
927
  return;
884
928
  }
@@ -1285,6 +1329,324 @@ function applyRoleProfileDefaults(entry, defaults, { overwrite = false } = {}) {
1285
1329
  }
1286
1330
  }
1287
1331
 
1332
+ function preferredRoleSort(roles) {
1333
+ const order = ["monitor", "review", "worker", "approval"];
1334
+ return ensureArray(roles).slice().sort((left, right) => {
1335
+ const leftText = String(left || "").trim();
1336
+ const rightText = String(right || "").trim();
1337
+ const leftIndex = order.indexOf(leftText);
1338
+ const rightIndex = order.indexOf(rightText);
1339
+ if (leftIndex >= 0 && rightIndex >= 0) return leftIndex - rightIndex;
1340
+ if (leftIndex >= 0) return -1;
1341
+ if (rightIndex >= 0) return 1;
1342
+ return leftText.localeCompare(rightText);
1343
+ });
1344
+ }
1345
+
1346
+ function currentRoleProfileState(roleName, deps) {
1347
+ const normalizedRole = requireDependency(deps, "normalizeRunnerRoleProfileName")(roleName || "");
1348
+ const defaults = resolveRoleProfileDefaults(normalizedRole, deps);
1349
+ const config = requireDependency(deps, "loadBotRunnerConfig")({ persistIfNeeded: true });
1350
+ const profile = safeObject(safeObject(config.roleProfiles || {})[normalizedRole]);
1351
+ return {
1352
+ role: normalizedRole,
1353
+ client: requireDependency(deps, "normalizeLocalAIClientName")(profile.client || defaults.client || "", ""),
1354
+ model: String(profile.model || defaults.model || "").trim(),
1355
+ permissionMode: requireDependency(deps, "normalizeLocalAIPermissionMode")(
1356
+ profile.permission_mode || profile.permissionMode || defaults.permissionMode || "",
1357
+ "",
1358
+ ),
1359
+ reasoningEffort: requireDependency(deps, "normalizeLocalAIReasoningEffort")(
1360
+ profile.reasoning_effort || profile.reasoningEffort || defaults.reasoningEffort || "",
1361
+ "",
1362
+ ),
1363
+ };
1364
+ }
1365
+
1366
+ function formatRoleProfileSummary(profile) {
1367
+ const current = safeObject(profile);
1368
+ return [
1369
+ current.client ? `client:${current.client}` : "client:(blank)",
1370
+ current.model ? `model:${current.model}` : "model:(blank)",
1371
+ current.permissionMode ? `permission:${current.permissionMode}` : "permission:(blank)",
1372
+ current.reasoningEffort ? `reasoning:${current.reasoningEffort}` : "reasoning:(blank)",
1373
+ ].join(" | ");
1374
+ }
1375
+
1376
+ function serializeRoleProfile(profile) {
1377
+ const current = safeObject(profile);
1378
+ return {
1379
+ role: String(current.role || "").trim(),
1380
+ client: String(current.client || "").trim(),
1381
+ model: String(current.model || "").trim(),
1382
+ permissionMode: String(current.permissionMode || "").trim(),
1383
+ reasoningEffort: String(current.reasoningEffort || "").trim(),
1384
+ };
1385
+ }
1386
+
1387
+ function buildRoleProfileOutput(roleName, deps) {
1388
+ return serializeRoleProfile(currentRoleProfileState(roleName, deps));
1389
+ }
1390
+
1391
+ function buildGroupedRoleProfileOutput(roles, deps) {
1392
+ const output = {};
1393
+ preferredRoleSort(roles).forEach((role) => {
1394
+ const normalizedRole = String(role || "").trim();
1395
+ if (!normalizedRole) return;
1396
+ output[normalizedRole] = buildRoleProfileOutput(normalizedRole, deps);
1397
+ });
1398
+ return output;
1399
+ }
1400
+
1401
+ function persistRoleProfileState(profile, deps) {
1402
+ const current = safeObject(profile);
1403
+ const role = requireDependency(deps, "normalizeRunnerRoleProfileName")(current.role || "");
1404
+ if (!role) return null;
1405
+ const config = requireDependency(deps, "loadBotRunnerConfig")({ persistIfNeeded: true });
1406
+ const nextRoleProfiles = {
1407
+ ...safeObject(config.roleProfiles || {}),
1408
+ [role]: {
1409
+ client: String(current.client || "").trim(),
1410
+ model: String(current.model || "").trim(),
1411
+ permission_mode: String(current.permissionMode || "").trim(),
1412
+ reasoning_effort: String(current.reasoningEffort || "").trim(),
1413
+ },
1414
+ };
1415
+ const nextConfig = {
1416
+ ...config,
1417
+ roleProfiles: nextRoleProfiles,
1418
+ };
1419
+ return requireDependency(deps, "saveBotRunnerConfig")(nextConfig, config.filePath);
1420
+ }
1421
+
1422
+ async function promptRoleExecutionProfile(ui, roleName, deps) {
1423
+ const current = currentRoleProfileState(roleName, deps);
1424
+ const action = await promptChoice(
1425
+ ui,
1426
+ `Role "${current.role}" local execution profile`,
1427
+ [
1428
+ {
1429
+ value: "keep",
1430
+ label: "Keep current settings",
1431
+ description: formatRoleProfileSummary(current),
1432
+ },
1433
+ {
1434
+ value: "edit",
1435
+ label: "Edit settings",
1436
+ description: "change AI client, model, permission, and reasoning",
1437
+ },
1438
+ ],
1439
+ { defaultIndex: 0 },
1440
+ );
1441
+ if (action?.value !== "edit") {
1442
+ return { changed: false, filePath: "" };
1443
+ }
1444
+ current.client = requireDependency(deps, "normalizeLocalAIClientName")(
1445
+ await promptAIClient(ui, deps, current.client),
1446
+ "",
1447
+ );
1448
+ current.model = await promptLine(ui, `AI model for role "${current.role}"`, current.model);
1449
+ current.permissionMode = requireDependency(deps, "normalizeLocalAIPermissionMode")(
1450
+ await promptPermissionMode(ui, current.permissionMode),
1451
+ "",
1452
+ );
1453
+ current.reasoningEffort = requireDependency(deps, "normalizeLocalAIReasoningEffort")(
1454
+ await promptReasoningEffort(ui, current.reasoningEffort),
1455
+ "",
1456
+ );
1457
+ const filePath = persistRoleProfileState(current, deps);
1458
+ if (filePath) {
1459
+ process.stdout.write(`Saved local execution profile for role "${current.role}" to ${filePath}\n`);
1460
+ }
1461
+ return { changed: true, filePath };
1462
+ }
1463
+
1464
+ async function maybePromptGroupedServerRoleProfiles(ui, serverBot, deps) {
1465
+ const roles = preferredRoleSort(ensureArray(serverBot?.roles).filter(Boolean));
1466
+ if (roles.length <= 1) {
1467
+ return false;
1468
+ }
1469
+ process.stdout.write(`Current grouped role execution profiles for "${String(serverBot?.name || "").trim() || "telegram"}":\n`);
1470
+ roles.forEach((role) => {
1471
+ process.stdout.write(` - ${role}: ${formatRoleProfileSummary(currentRoleProfileState(role, deps))}\n`);
1472
+ });
1473
+ const editChoice = await promptChoice(
1474
+ ui,
1475
+ "Grouped role settings",
1476
+ [
1477
+ {
1478
+ value: "keep",
1479
+ label: "Keep current role settings",
1480
+ description: "use the existing role_profiles defaults as-is",
1481
+ },
1482
+ {
1483
+ value: "one",
1484
+ label: "Edit one role",
1485
+ description: "change only the role that needs an update",
1486
+ },
1487
+ {
1488
+ value: "all",
1489
+ label: "Edit all roles",
1490
+ description: "review each role in sequence",
1491
+ },
1492
+ ],
1493
+ { defaultIndex: 0 },
1494
+ );
1495
+ if (editChoice?.value === "keep") {
1496
+ return false;
1497
+ }
1498
+ if (editChoice?.value === "all") {
1499
+ for (const role of roles) {
1500
+ await promptRoleExecutionProfile(ui, role, deps);
1501
+ }
1502
+ return true;
1503
+ }
1504
+ const remaining = new Set(roles);
1505
+ while (remaining.size) {
1506
+ const roleChoice = await promptChoice(
1507
+ ui,
1508
+ "Select role to edit",
1509
+ [
1510
+ ...Array.from(remaining).map((role) => ({
1511
+ value: role,
1512
+ label: role,
1513
+ description: formatRoleProfileSummary(currentRoleProfileState(role, deps)),
1514
+ })),
1515
+ {
1516
+ value: "__done__",
1517
+ label: "Done",
1518
+ description: "finish grouped role editing",
1519
+ },
1520
+ ],
1521
+ { defaultIndex: 0 },
1522
+ );
1523
+ if (!roleChoice || roleChoice.value === "__done__") {
1524
+ break;
1525
+ }
1526
+ await promptRoleExecutionProfile(ui, roleChoice.value, deps);
1527
+ remaining.delete(roleChoice.value);
1528
+ if (!remaining.size) {
1529
+ break;
1530
+ }
1531
+ if (!await promptYesNo(ui, "Edit another role?", false)) {
1532
+ break;
1533
+ }
1534
+ }
1535
+ return true;
1536
+ }
1537
+
1538
+ async function resolveTelegramServerBindingDetails(envConfig, flags, deps) {
1539
+ const current = safeObject(envConfig);
1540
+ if (!String(current.serverBotID || "").trim() && !String(current.botUsername || "").trim() && !String(current.botKey || "").trim()) {
1541
+ return null;
1542
+ }
1543
+ const lookup = await requireDependency(deps, "listServerBots")({
1544
+ provider: "telegram",
1545
+ baseURL: flags["base-url"] || deps.defaultSiteURL,
1546
+ timeoutSeconds: intFromRaw(flags["timeout-seconds"], 15) || 15,
1547
+ });
1548
+ if (!lookup?.ok) {
1549
+ return {
1550
+ ok: false,
1551
+ mode: "lookup_error",
1552
+ matchedBy: "",
1553
+ name: "",
1554
+ role: "",
1555
+ roles: [],
1556
+ effectiveRoleProfile: null,
1557
+ effectiveRoleProfiles: {},
1558
+ detail: `server bot lookup unavailable: ${lookup?.error || "unknown error"}`,
1559
+ };
1560
+ }
1561
+ const bots = ensureArray(lookup.bots).map((bot) => ({
1562
+ id: String(bot?.id || "").trim(),
1563
+ role: String(bot?.role || bot?.bot_role || "").trim(),
1564
+ name: String(bot?.name || "").trim(),
1565
+ })).filter((bot) => bot.id);
1566
+ const resolveGroupedPayload = (matches, matchedBy) => {
1567
+ const roles = preferredRoleSort(summarizeServerBotRoles(matches));
1568
+ return {
1569
+ ok: true,
1570
+ mode: "group",
1571
+ matchedBy,
1572
+ name: String(matches[0]?.name || "").trim(),
1573
+ role: "",
1574
+ roles,
1575
+ serverBotID: String(current.serverBotID || "").trim(),
1576
+ effectiveRoleProfile: null,
1577
+ effectiveRoleProfiles: buildGroupedRoleProfileOutput(roles, deps),
1578
+ detail: `${String(matches[0]?.name || "").trim() || "(unnamed)"} roles: ${roles.join(", ") || "-"}`,
1579
+ };
1580
+ };
1581
+ if (String(current.serverBotID || "").trim()) {
1582
+ const match = bots.find((bot) => bot.id === String(current.serverBotID || "").trim());
1583
+ if (!match) {
1584
+ return {
1585
+ ok: false,
1586
+ mode: "missing",
1587
+ matchedBy: "server_bot_id",
1588
+ name: "",
1589
+ role: "",
1590
+ roles: [],
1591
+ serverBotID: String(current.serverBotID || "").trim(),
1592
+ effectiveRoleProfile: null,
1593
+ effectiveRoleProfiles: {},
1594
+ detail: `server bot ${current.serverBotID} not found`,
1595
+ };
1596
+ }
1597
+ const sameNameMatches = bots.filter((bot) => normalizeServerBotIdentityText(bot.name) === normalizeServerBotIdentityText(match.name));
1598
+ if (sameNameMatches.length > 1) {
1599
+ return resolveGroupedPayload(sameNameMatches, "server_bot_id");
1600
+ }
1601
+ const effectiveRoleProfile = buildRoleProfileOutput(match.role || current.roleProfile || "", deps);
1602
+ return {
1603
+ ok: true,
1604
+ mode: "single",
1605
+ matchedBy: "server_bot_id",
1606
+ name: match.name,
1607
+ role: match.role,
1608
+ roles: summarizeServerBotRoles([match]),
1609
+ serverBotID: match.id,
1610
+ effectiveRoleProfile,
1611
+ effectiveRoleProfiles: {},
1612
+ detail: `${String(match.name || "").trim() || "(unnamed)"} [${String(match.role || "").trim() || "-"}]`,
1613
+ };
1614
+ }
1615
+ const normalizedServerIdentity = normalizeServerBotIdentityText(current.botUsername || current.botKey);
1616
+ const matches = bots.filter((bot) => normalizeServerBotIdentityText(bot.name) === normalizedServerIdentity);
1617
+ if (!matches.length) {
1618
+ return {
1619
+ ok: false,
1620
+ mode: "missing",
1621
+ matchedBy: "server_bot_name",
1622
+ name: "",
1623
+ role: "",
1624
+ roles: [],
1625
+ serverBotID: "",
1626
+ effectiveRoleProfile: null,
1627
+ effectiveRoleProfiles: {},
1628
+ detail: `no server bot matched ${current.botUsername ? `@${current.botUsername}` : current.botKey}`,
1629
+ };
1630
+ }
1631
+ if (matches.length > 1) {
1632
+ return resolveGroupedPayload(matches, "server_bot_name");
1633
+ }
1634
+ const match = matches[0] || {};
1635
+ const effectiveRoleProfile = buildRoleProfileOutput(match.role || current.roleProfile || "", deps);
1636
+ return {
1637
+ ok: true,
1638
+ mode: "single",
1639
+ matchedBy: "server_bot_name",
1640
+ name: String(match.name || "").trim(),
1641
+ role: String(match.role || "").trim(),
1642
+ roles: summarizeServerBotRoles([match]),
1643
+ serverBotID: String(match.id || "").trim(),
1644
+ effectiveRoleProfile,
1645
+ effectiveRoleProfiles: {},
1646
+ detail: `${String(match.name || "").trim() || "(unnamed)"} [${String(match.role || "").trim() || "-"}]`,
1647
+ };
1648
+ }
1649
+
1288
1650
  function buildTemporaryTelegramEnvConfig({ token, apiBaseURL }) {
1289
1651
  return {
1290
1652
  ok: true,
@@ -1300,8 +1662,11 @@ async function verifyTelegramTokenCandidate(provider, token, apiBaseURL, timeout
1300
1662
 
1301
1663
  async function autoResolveTelegramServerBot(current, flags, deps) {
1302
1664
  const existingBotID = String(current?.serverBotID || "").trim();
1303
- const preferredUsername = String(current?.username || "").trim();
1304
- if (!preferredUsername) {
1665
+ const preferredIdentity = firstNonEmptyString([
1666
+ current?.username,
1667
+ current?.key,
1668
+ ]);
1669
+ if (!preferredIdentity) {
1305
1670
  return {
1306
1671
  botID: existingBotID,
1307
1672
  role: String(current?.roleProfile || "").trim(),
@@ -1314,14 +1679,14 @@ async function autoResolveTelegramServerBot(current, flags, deps) {
1314
1679
  return await resolveServerBotForNonInteractive(
1315
1680
  "telegram",
1316
1681
  {
1317
- "bot-name": preferredUsername,
1682
+ "bot-name": preferredIdentity,
1318
1683
  "base-url": flags["base-url"] || deps.defaultSiteURL,
1319
1684
  "timeout-seconds": intFromRaw(flags["timeout-seconds"], 15) || 15,
1320
1685
  },
1321
1686
  deps,
1322
1687
  {
1323
- preferredUsername,
1324
- preferredName: preferredUsername,
1688
+ preferredUsername: preferredIdentity,
1689
+ preferredName: preferredIdentity,
1325
1690
  },
1326
1691
  );
1327
1692
  } catch {
@@ -1680,53 +2045,12 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
1680
2045
  envConfig,
1681
2046
  intFromRaw(flags["timeout-seconds"], 15) || 15,
1682
2047
  );
1683
- let serverBinding = null;
1684
2048
  let overallOK = Boolean(result.ok);
1685
- if (provider === "telegram" && (String(envConfig.serverBotID || "").trim() || String(envConfig.botUsername || "").trim() || String(envConfig.botKey || "").trim())) {
1686
- const lookup = await requireDependency(deps, "listServerBots")({
1687
- provider,
1688
- baseURL: flags["base-url"] || deps.defaultSiteURL,
1689
- timeoutSeconds: intFromRaw(flags["timeout-seconds"], 15) || 15,
1690
- });
1691
- if (!lookup?.ok) {
1692
- serverBinding = {
1693
- ok: false,
1694
- detail: `server bot lookup unavailable: ${lookup?.error || "unknown error"}`,
1695
- };
2049
+ let serverBinding = null;
2050
+ if (provider === "telegram") {
2051
+ serverBinding = await resolveTelegramServerBindingDetails(envConfig, flags, deps);
2052
+ if (serverBinding && !serverBinding.ok) {
1696
2053
  overallOK = false;
1697
- } else {
1698
- const normalizedServerIdentity = normalizeServerBotIdentityText(envConfig.botUsername || envConfig.botKey);
1699
- if (String(envConfig.serverBotID || "").trim()) {
1700
- const match = ensureArray(lookup.bots).find((bot) => String(bot.id || "").trim() === String(envConfig.serverBotID || "").trim());
1701
- if (!match) {
1702
- serverBinding = {
1703
- ok: false,
1704
- detail: `server bot ${envConfig.serverBotID} not found`,
1705
- };
1706
- overallOK = false;
1707
- } else {
1708
- serverBinding = {
1709
- ok: true,
1710
- detail: `${String(match.name || "").trim() || "(unnamed)"} [${String(match.role || "").trim() || "-"}]`,
1711
- };
1712
- }
1713
- } else {
1714
- const matches = ensureArray(lookup.bots).filter(
1715
- (bot) => normalizeServerBotIdentityText(bot?.name) === normalizedServerIdentity,
1716
- );
1717
- if (!matches.length) {
1718
- serverBinding = {
1719
- ok: false,
1720
- detail: `no server bot matched ${envConfig.botUsername ? `@${envConfig.botUsername}` : envConfig.botKey}`,
1721
- };
1722
- overallOK = false;
1723
- } else {
1724
- serverBinding = {
1725
- ok: true,
1726
- detail: `${String(matches[0]?.name || "").trim() || "(unnamed)"} roles: ${summarizeServerBotRoles(matches).join(", ") || "-"}`,
1727
- };
1728
- }
1729
- }
1730
2054
  }
1731
2055
  }
1732
2056
  const interactiveJsonChoice = (
@@ -1763,21 +2087,33 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
1763
2087
  }, null, 2)}\n`,
1764
2088
  );
1765
2089
  } else {
1766
- process.stdout.write(
1767
- [
1768
- `${providerLabel(provider, deps)} verify: ${overallOK ? "OK" : "FAIL"}`,
1769
- `file: ${envConfig.filePath}`,
1770
- provider === "telegram" ? `bot_key: ${envConfig.botKey || "-"}` : "",
1771
- provider === "telegram" ? `server_bot_id: ${envConfig.serverBotID || "-"}` : "",
1772
- provider === "telegram" ? `role_profile: ${envConfig.roleProfile || "-"}` : "",
1773
- provider === "telegram" ? `ai_client: ${envConfig.client || "-"}` : "",
1774
- provider === "telegram" ? `ai_model: ${envConfig.model || "-"}` : "",
1775
- provider === "telegram" ? `permission_mode: ${envConfig.permissionMode || "-"}` : "",
1776
- provider === "telegram" ? `reasoning_effort: ${envConfig.reasoningEffort || "-"}` : "",
1777
- `detail: ${result.detail || "-"}`,
1778
- serverBinding ? `server_binding: ${serverBinding.ok ? "OK" : "FAIL"}${serverBinding.detail ? ` (${serverBinding.detail})` : ""}` : "",
1779
- ].filter(Boolean).join("\n") + "\n",
1780
- );
2090
+ const lines = [
2091
+ `${providerLabel(provider, deps)} verify: ${overallOK ? "OK" : "FAIL"}`,
2092
+ `file: ${envConfig.filePath}`,
2093
+ provider === "telegram" ? `bot_key: ${envConfig.botKey || "-"}` : "",
2094
+ provider === "telegram" ? `server_bot_id: ${envConfig.serverBotID || "-"}` : "",
2095
+ provider === "telegram" ? `role_profile: ${envConfig.roleProfile || "-"}` : "",
2096
+ provider === "telegram" ? `ai_client: ${envConfig.client || "-"}` : "",
2097
+ provider === "telegram" ? `ai_model: ${envConfig.model || "-"}` : "",
2098
+ provider === "telegram" ? `permission_mode: ${envConfig.permissionMode || "-"}` : "",
2099
+ provider === "telegram" ? `reasoning_effort: ${envConfig.reasoningEffort || "-"}` : "",
2100
+ `detail: ${result.detail || "-"}`,
2101
+ serverBinding ? `server_binding: ${serverBinding.ok ? "OK" : "FAIL"}${serverBinding.detail ? ` (${serverBinding.detail})` : ""}` : "",
2102
+ ].filter(Boolean);
2103
+ if (provider === "telegram" && serverBinding) {
2104
+ lines.push(`server_binding_mode: ${serverBinding.mode || "-"}`);
2105
+ lines.push(`server_bot_name: ${serverBinding.name || "-"}`);
2106
+ lines.push(`server_roles: ${ensureArray(serverBinding.roles).join(", ") || "-"}`);
2107
+ if (serverBinding.mode === "group") {
2108
+ const groupedProfiles = safeObject(serverBinding.effectiveRoleProfiles);
2109
+ Object.keys(groupedProfiles).forEach((role) => {
2110
+ lines.push(`runtime_role_profile[${role}]: ${formatRoleProfileOutputLine(groupedProfiles[role])}`);
2111
+ });
2112
+ } else if (serverBinding.effectiveRoleProfile) {
2113
+ lines.push(`runtime_role_profile: ${formatRoleProfileOutputLine(serverBinding.effectiveRoleProfile)}`);
2114
+ }
2115
+ }
2116
+ process.stdout.write(`${lines.join("\n")}\n`);
1781
2117
  }
1782
2118
  if (!overallOK) {
1783
2119
  process.exitCode = 1;
@@ -1936,12 +2272,21 @@ async function runBotShow(ui, flags, deps, explicitProvider = "") {
1936
2272
  const state = loadProviderEnvState(provider, deps);
1937
2273
  if (provider === "telegram") {
1938
2274
  const entry = await resolveTelegramEntryForShow(ui, state.parsed, flags, deps);
1939
- const payload = buildBotShowPayload(provider, state, entry, deps);
2275
+ const serverBinding = await resolveTelegramServerBindingDetails(
2276
+ requireDependency(deps, "loadProviderEnvConfig")(provider, {
2277
+ botKey: entry.key,
2278
+ botID: entry.serverBotID,
2279
+ botName: entry.username || entry.key,
2280
+ }),
2281
+ flags,
2282
+ deps,
2283
+ );
2284
+ const payload = buildBotShowPayload(provider, state, entry, deps, { serverBinding });
1940
2285
  if (boolFromRaw(flags.json, false)) {
1941
2286
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
1942
2287
  return;
1943
2288
  }
1944
- printBotShow(provider, state, entry, deps);
2289
+ printBotShow(provider, state, entry, deps, { serverBinding });
1945
2290
  return;
1946
2291
  }
1947
2292
  const payload = buildBotShowPayload(provider, state, null, deps);
@@ -341,6 +341,73 @@ export async function runSelftestBotCommands(push, deps) {
341
341
  && String(groupedState.TELEGRAM_BOT_RYOAI_BOT_AI_PERMISSION_MODE || "") === "",
342
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
343
  );
344
+
345
+ await runCLI({
346
+ cliPath,
347
+ args: [
348
+ "bot", "edit",
349
+ "--base-url", `http://127.0.0.1:${groupedMock.port}`,
350
+ "--timeout-seconds", "5",
351
+ ],
352
+ env: {
353
+ ...env,
354
+ METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
355
+ "1", // provider: telegram
356
+ "2", // bot entry: ryoai_bot
357
+ "1", // keep username
358
+ "1", // keep token
359
+ "2", // grouped role settings: edit one role
360
+ "3", // select role to edit: worker
361
+ "2", // worker: edit settings
362
+ "3", // worker AI client: claude
363
+ "worker-sonnet-4",
364
+ "4", // worker permission: danger_full_access
365
+ "4", // worker reasoning: high
366
+ "y", // edit another role
367
+ "3", // select role to edit: approval
368
+ "2", // approval: edit settings
369
+ "4", // approval AI client: gemini
370
+ "approval-pro-2",
371
+ "4", // approval permission: danger_full_access
372
+ "4", // approval reasoning: high
373
+ "n", // stop editing roles
374
+ "1", // keep current default setting
375
+ "y", // save
376
+ ]),
377
+ },
378
+ });
379
+ const groupedRunnerConfigPath = path.join(tempHome, ".metheus", "bot-runner.json");
380
+ const groupedRunnerConfig = readJSON(fs.readFileSync(groupedRunnerConfigPath, "utf8"));
381
+ push(
382
+ "bot_edit_grouped_server_roles_updates_role_profiles",
383
+ String(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).worker).client || "") === "claude"
384
+ && String(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).worker).model || "") === "worker-sonnet-4"
385
+ && String(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).worker).permission_mode || "") === "danger_full_access"
386
+ && String(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).worker).reasoning_effort || "") === "high"
387
+ && String(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).approval).client || "") === "gemini"
388
+ && String(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).approval).model || "") === "approval-pro-2",
389
+ `worker=${JSON.stringify(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).worker))} approval=${JSON.stringify(safeObject(safeObject(groupedRunnerConfig.role_profiles || {}).approval))}`,
390
+ );
391
+
392
+ const groupedShowResult = await runCLI({
393
+ cliPath,
394
+ args: [
395
+ "bot", "show",
396
+ "--provider", "telegram",
397
+ "--bot-key", "ryoai_bot",
398
+ "--base-url", `http://127.0.0.1:${groupedMock.port}`,
399
+ "--json", "true",
400
+ ],
401
+ env,
402
+ });
403
+ const groupedShowPayload = readJSON(groupedShowResult.stdout);
404
+ push(
405
+ "bot_show_reports_grouped_server_roles",
406
+ safeObject(groupedShowPayload.serverBinding).mode === "group"
407
+ && safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).worker?.client === "claude"
408
+ && safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).approval?.client === "gemini",
409
+ `mode=${String(safeObject(groupedShowPayload.serverBinding).mode || "")} worker=${String(safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).worker?.client || "")} approval=${String(safeObject(safeObject(groupedShowPayload.serverBinding).effectiveRoleProfiles).approval?.client || "")}`,
410
+ );
344
411
  } finally {
345
412
  await groupedMock.close();
346
413
  await runCLI({
@@ -361,6 +428,7 @@ export async function runSelftestBotCommands(push, deps) {
361
428
  "bot", "show",
362
429
  "--provider", "telegram",
363
430
  "--bot-key", "monitorselftestbot",
431
+ "--base-url", baseURL,
364
432
  "--json", "true",
365
433
  ],
366
434
  env,
@@ -369,8 +437,9 @@ export async function runSelftestBotCommands(push, deps) {
369
437
  push(
370
438
  "bot_show_returns_selected_telegram_entry",
371
439
  safeObject(showPayload.entry).key === "monitorselftestbot"
372
- && safeObject(showPayload.entry).client === "codex",
373
- `key=${String(safeObject(showPayload.entry).key || "")} client=${String(safeObject(showPayload.entry).client || "")}`,
440
+ && safeObject(showPayload.entry).client === "codex"
441
+ && safeObject(showPayload.serverBinding).mode === "single",
442
+ `key=${String(safeObject(showPayload.entry).key || "")} client=${String(safeObject(showPayload.entry).client || "")} mode=${String(safeObject(showPayload.serverBinding).mode || "")}`,
374
443
  );
375
444
 
376
445
  await runCLI({
@@ -391,7 +460,6 @@ export async function runSelftestBotCommands(push, deps) {
391
460
  "3", // workspace_write
392
461
  "2", // change reasoning effort
393
462
  "3", // medium
394
- "1", // keep bot key
395
463
  "1", // keep default setting
396
464
  "y", // save
397
465
  ]),
@@ -473,8 +541,9 @@ export async function runSelftestBotCommands(push, deps) {
473
541
  "bot_verify_guided_can_emit_json",
474
542
  guidedVerifyPayload.ok === true
475
543
  && safeObject(guidedVerifyPayload.serverBinding).ok === true
544
+ && safeObject(guidedVerifyPayload.serverBinding).mode === "single"
476
545
  && String(guidedVerifyPayload.client || "") === "claude",
477
- `verify=${String(guidedVerifyPayload.ok)} server=${String(safeObject(guidedVerifyPayload.serverBinding).detail || "")}`,
546
+ `verify=${String(guidedVerifyPayload.ok)} mode=${String(safeObject(guidedVerifyPayload.serverBinding).mode || "")} server=${String(safeObject(guidedVerifyPayload.serverBinding).detail || "")}`,
478
547
  );
479
548
 
480
549
  await runCLI({
@@ -511,8 +580,9 @@ export async function runSelftestBotCommands(push, deps) {
511
580
  "bot_verify_cross_checks_server_bot_binding",
512
581
  verifyPayload.ok === true
513
582
  && safeObject(verifyPayload.serverBinding).ok === true
583
+ && safeObject(verifyPayload.serverBinding).mode === "single"
514
584
  && String(verifyPayload.client || "") === "claude",
515
- `verify=${String(verifyPayload.ok)} server=${String(safeObject(verifyPayload.serverBinding).detail || "")}`,
585
+ `verify=${String(verifyPayload.ok)} mode=${String(safeObject(verifyPayload.serverBinding).mode || "")} server=${String(safeObject(verifyPayload.serverBinding).detail || "")}`,
516
586
  );
517
587
 
518
588
  await runCLI({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.71",
3
+ "version": "0.2.73",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [