metheus-governance-mcp-cli 0.2.100 → 0.2.102

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
@@ -58,8 +58,11 @@ TELEGRAM_BOT_TOKEN=
58
58
 
59
59
  ```env
60
60
  # ~/.metheus/telegram-bots/ryoai_bot.env
61
- TELEGRAM_BOT_NAME=ryoai_bot
62
- TELEGRAM_BOT_SERVER_BOT_ID=
61
+ TELEGRAM_BOT_SERVER_BOT_ID=<server_bot_uuid>
62
+ TELEGRAM_BOT_SERVER_NAME=RyoAI_bot
63
+ TELEGRAM_BOT_SERVER_ROLES=monitor,review,worker,approval
64
+ # Optional fallback only when server bot binding is unavailable
65
+ # TELEGRAM_BOT_USERNAME=ryoai_bot
63
66
  TELEGRAM_BOT_TOKEN=
64
67
  TELEGRAM_BOT_ROLE_PROFILE=
65
68
  TELEGRAM_BOT_AI_CLIENT=
@@ -223,15 +226,15 @@ Direct commands:
223
226
 
224
227
  ```bash
225
228
  metheus-governance-mcp-cli bot list
226
- metheus-governance-mcp-cli bot show --provider telegram --bot-key ryoai_bot
229
+ metheus-governance-mcp-cli bot show --provider telegram --bot-name RyoAI_bot
227
230
  metheus-governance-mcp-cli bot add --provider telegram
228
231
  metheus-governance-mcp-cli bot edit
229
232
  metheus-governance-mcp-cli bot edit --provider telegram
230
233
  metheus-governance-mcp-cli bot remove --provider telegram
231
- metheus-governance-mcp-cli bot set-default --provider telegram --bot-key ryoai_bot
232
- metheus-governance-mcp-cli bot migrate --provider telegram --bot-key ryoai_bot
234
+ metheus-governance-mcp-cli bot set-default --provider telegram --bot-name RyoAI_bot
235
+ metheus-governance-mcp-cli bot migrate --provider telegram --bot-name RyoAI_bot
233
236
  metheus-governance-mcp-cli bot global --provider telegram
234
- metheus-governance-mcp-cli bot verify --provider telegram --bot-key ryoai_bot
237
+ metheus-governance-mcp-cli bot verify --provider telegram --bot-name RyoAI_bot
235
238
  ```
236
239
 
237
240
  Behavior:
@@ -259,8 +262,10 @@ Behavior:
259
262
  - `bot verify` without flags starts a guided numbered flow: provider -> bot entry -> output format.
260
263
  - `bot remove` without flags starts a guided numbered flow: provider -> bot entry -> confirm removal.
261
264
  - Telegram stores one bot file per entry under `~/.metheus/telegram-bots/<server-bot-name>.env` with generic fields:
262
- - `TELEGRAM_BOT_NAME`
263
265
  - `TELEGRAM_BOT_SERVER_BOT_ID`
266
+ - `TELEGRAM_BOT_SERVER_NAME`
267
+ - `TELEGRAM_BOT_SERVER_ROLES`
268
+ - `TELEGRAM_BOT_USERNAME` only as a fallback when server bot binding is unavailable
264
269
  - `TELEGRAM_BOT_TOKEN`
265
270
  - `TELEGRAM_BOT_ROLE_PROFILE`
266
271
  - `TELEGRAM_BOT_AI_CLIENT`
@@ -270,6 +275,7 @@ Behavior:
270
275
  - Slack and KakaoTalk currently use a single local token entry per provider in this command flow.
271
276
  - `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.
272
277
  - `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.
278
+ - In `bot show` and `bot verify`, `stored_*` fields come from the local env file and `live_server_*` fields come from the current server `me/bots` response.
273
279
  - `bot global` edits Telegram-wide local settings such as API base URL, allowed updates, and default bot key.
274
280
  - `bot set-default` updates `TELEGRAM_DEFAULT_BOT_KEY`.
275
281
  - `bot migrate` moves legacy `TELEGRAM_BOT_TOKEN` into a named Telegram bot entry.
@@ -280,16 +286,16 @@ Non-interactive examples:
280
286
  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
281
287
  metheus-governance-mcp-cli bot add --provider telegram --non-interactive true --server-bot-id <server_bot_uuid> --token <telegram_bot_token> --default true
282
288
  metheus-governance-mcp-cli bot add --provider telegram --non-interactive true --server-bot-id <server_bot_uuid> --token <telegram_bot_token> --ai-client gpt --ai-model "gpt-5.4" --ai-permission-mode read_only --ai-reasoning-effort low
283
- metheus-governance-mcp-cli bot edit --provider telegram --bot-key ryoai_bot --non-interactive true --ai-client claude --ai-model "Sonnet 4.6r" --ai-permission-mode danger_full_access --ai-reasoning-effort high
284
- metheus-governance-mcp-cli bot set-default --provider telegram --bot-key ryoai_bot --non-interactive true
285
- metheus-governance-mcp-cli bot migrate --provider telegram --bot-key ryoai_bot --server-bot-id <server_bot_uuid> --bot-name <telegram_username> --role-profile monitor --ai-client gpt --ai-permission-mode read_only --ai-reasoning-effort low --non-interactive true
286
- metheus-governance-mcp-cli bot remove --provider telegram --bot-key ryoai_bot --non-interactive true
287
- metheus-governance-mcp-cli bot verify --provider telegram --bot-key ryoai_bot --json true
289
+ metheus-governance-mcp-cli bot edit --provider telegram --bot-name RyoAI_bot --non-interactive true --ai-client claude --ai-model "Sonnet 4.6r" --ai-permission-mode danger_full_access --ai-reasoning-effort high
290
+ metheus-governance-mcp-cli bot set-default --provider telegram --bot-name RyoAI_bot --non-interactive true
291
+ metheus-governance-mcp-cli bot migrate --provider telegram --bot-name RyoAI_bot --server-bot-id <server_bot_uuid> --role-profile monitor --ai-client gpt --ai-permission-mode read_only --ai-reasoning-effort low --non-interactive true
292
+ metheus-governance-mcp-cli bot remove --provider telegram --bot-name RyoAI_bot --non-interactive true
293
+ metheus-governance-mcp-cli bot verify --provider telegram --bot-name RyoAI_bot --json true
288
294
  ```
289
295
 
290
- 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.
296
+ For direct Telegram adds, the CLI derives the local file name from the matched server bot name. You normally do not need `--bot-key` or `--username`. Treat `--bot-key` as an advanced compatibility selector only when you intentionally need a different local file stem.
291
297
 
292
- 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.
298
+ For direct Telegram edits, prefer `--bot-name` or `--bot-id` because server bot identity is the source of truth. Use `--bot-key` only when you intentionally need the advanced local selector. 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.
293
299
 
294
300
  Current support status:
295
301
 
package/cli.mjs CHANGED
@@ -241,13 +241,13 @@ function printUsage() {
241
241
  ` ${cmd} setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--workspace-fallback-dir <path>] [--name <server_name>]`,
242
242
  ` ${cmd} bot setup [--provider <telegram|slack|kakaotalk>] [--base-url <url>] [--timeout-seconds <n>]`,
243
243
  ` ${cmd} bot list [--provider <telegram|slack|kakaotalk>] [--json <true|false>]`,
244
- ` ${cmd} bot show [--provider <telegram|slack|kakaotalk>] [--bot-key <key>] [--bot-id <uuid>] [--bot-name <name>] [--json <true|false>]`,
244
+ ` ${cmd} bot show [--provider <telegram|slack|kakaotalk>] [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>] [--json <true|false>]`,
245
245
  ` ${cmd} bot add [--provider <telegram|slack|kakaotalk>] [--base-url <url>] [--timeout-seconds <n>]`,
246
246
  ` ${cmd} bot edit [--provider <telegram|slack|kakaotalk>] [--base-url <url>] [--timeout-seconds <n>]`,
247
247
  ` ${cmd} bot remove [--provider <telegram|slack|kakaotalk>]`,
248
- ` ${cmd} bot set-default --provider telegram [--bot-key <key>]`,
249
- ` ${cmd} bot migrate --provider telegram [--bot-key <key>] [--bot-id <uuid>] [--bot-name <name>]`,
250
- ` ${cmd} bot verify [--provider <telegram|slack|kakaotalk>] [--bot-key <key>] [--timeout-seconds <n>] [--json <true|false>]`,
248
+ ` ${cmd} bot set-default --provider telegram [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>]`,
249
+ ` ${cmd} bot migrate --provider telegram [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>]`,
250
+ ` ${cmd} bot verify [--provider <telegram|slack|kakaotalk>] [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>] [--timeout-seconds <n>] [--json <true|false>]`,
251
251
  ` ${cmd} bot global --provider telegram [--api-base-url <url>] [--auto-clear-webhook <true|false>] [--allowed-updates <csv>] [--default-bot-key <key>]`,
252
252
  ` ${cmd} doctor [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--timeout-seconds <n>] [--strict <true|false>]`,
253
253
  ` ${cmd} proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--include-drafts <true|false>] [--auto-pull-on-conflict <true|false>] [--timeout-seconds <n>]`,
@@ -759,7 +759,7 @@ function providerEnvTemplate(provider) {
759
759
  "# Keep this file on your machine only. Do not commit it.",
760
760
  "# Store Telegram-wide settings here.",
761
761
  "# This global file now lives in ~/.metheus/telegram-bots/_global.env.",
762
- "# Per-bot secrets and AI settings live in ~/.metheus/telegram-bots/<bot-key>.env.",
762
+ "# Per-bot secrets and AI settings live in ~/.metheus/telegram-bots/<server-bot-name>.env.",
763
763
  "# Project chat destinations must be managed on the Metheus server.",
764
764
  "",
765
765
  "TELEGRAM_API_BASE_URL=",
@@ -2684,12 +2684,35 @@ function parseTelegramAllowedUpdates(rawValue) {
2684
2684
  return Array.from(new Set(values));
2685
2685
  }
2686
2686
 
2687
+ function preferredTelegramServerRoleSort(roles) {
2688
+ const order = ["monitor", "review", "worker", "approval"];
2689
+ return ensureArray(roles).slice().sort((left, right) => {
2690
+ const leftText = String(left || "").trim();
2691
+ const rightText = String(right || "").trim();
2692
+ const leftIndex = order.indexOf(leftText);
2693
+ const rightIndex = order.indexOf(rightText);
2694
+ if (leftIndex >= 0 && rightIndex >= 0) return leftIndex - rightIndex;
2695
+ if (leftIndex >= 0) return -1;
2696
+ if (rightIndex >= 0) return 1;
2697
+ return leftText.localeCompare(rightText);
2698
+ });
2699
+ }
2700
+
2701
+ function parseTelegramServerRoles(rawValue) {
2702
+ const values = String(rawValue || "")
2703
+ .split(",")
2704
+ .map((value) => String(value || "").trim())
2705
+ .filter(Boolean);
2706
+ if (!values.length) return [];
2707
+ return Array.from(new Set(preferredTelegramServerRoleSort(values)));
2708
+ }
2709
+
2687
2710
  function collectTelegramEnvBotEntries(parsedEnv) {
2688
2711
  const parsed = safeObject(parsedEnv);
2689
2712
  const entries = new Map();
2690
2713
  for (const [rawKey, rawValue] of Object.entries(parsed)) {
2691
2714
  const match = String(rawKey || "").trim().match(
2692
- /^TELEGRAM_BOT_([A-Z0-9_]+)_(TOKEN|USERNAME|SERVER_BOT_ID|ROLE_PROFILE|AI_CLIENT|AI_MODEL|AI_PERMISSION_MODE|AI_REASONING_EFFORT)$/i,
2715
+ /^TELEGRAM_BOT_([A-Z0-9_]+)_(TOKEN|USERNAME|SERVER_BOT_ID|SERVER_NAME|SERVER_ROLES|ROLE_PROFILE|AI_CLIENT|AI_MODEL|AI_PERMISSION_MODE|AI_REASONING_EFFORT)$/i,
2693
2716
  );
2694
2717
  if (!match) continue;
2695
2718
  const botKey = normalizeTelegramBotEnvKey(match[1], "");
@@ -2700,6 +2723,8 @@ function collectTelegramEnvBotEntries(parsedEnv) {
2700
2723
  token: "",
2701
2724
  username: "",
2702
2725
  serverBotID: "",
2726
+ serverBotName: "",
2727
+ serverRoles: [],
2703
2728
  roleProfile: "",
2704
2729
  client: "",
2705
2730
  model: "",
@@ -2713,6 +2738,10 @@ function collectTelegramEnvBotEntries(parsedEnv) {
2713
2738
  entry.username = normalizeTelegramBotUsername(textValue);
2714
2739
  } else if (field === "SERVER_BOT_ID") {
2715
2740
  entry.serverBotID = textValue;
2741
+ } else if (field === "SERVER_NAME") {
2742
+ entry.serverBotName = textValue;
2743
+ } else if (field === "SERVER_ROLES") {
2744
+ entry.serverRoles = parseTelegramServerRoles(textValue);
2716
2745
  } else if (field === "ROLE_PROFILE") {
2717
2746
  entry.roleProfile = normalizeRunnerRoleProfileName(textValue);
2718
2747
  } else if (field === "AI_CLIENT") {
@@ -2738,6 +2767,8 @@ function collectTelegramEnvBotEntries(parsedEnv) {
2738
2767
 
2739
2768
  function telegramBotEntryDisplayNameForState(entry) {
2740
2769
  const current = safeObject(entry);
2770
+ const serverBotName = String(current.serverBotName || "").trim();
2771
+ if (serverBotName) return serverBotName;
2741
2772
  const username = normalizeTelegramBotUsername(current.username || "");
2742
2773
  if (username) return username;
2743
2774
  return normalizeTelegramBotEnvKey(current.key || "", "telegram_bot");
@@ -2747,11 +2778,16 @@ function parseTelegramBotEntryFile(filePath) {
2747
2778
  const raw = fs.readFileSync(filePath, "utf8");
2748
2779
  const parsed = parseSimpleEnvText(raw);
2749
2780
  const keyFromFile = normalizeTelegramBotEnvKey(path.basename(filePath, path.extname(filePath)), "telegram_bot");
2750
- const keyFromName = normalizeTelegramBotEnvKey(parsed.TELEGRAM_BOT_NAME || parsed.TELEGRAM_BOT_USERNAME || "", "");
2781
+ const keyFromName = normalizeTelegramBotEnvKey(
2782
+ parsed.TELEGRAM_BOT_USERNAME || parsed.TELEGRAM_BOT_NAME || parsed.TELEGRAM_BOT_SERVER_NAME || "",
2783
+ "",
2784
+ );
2751
2785
  return {
2752
- key: keyFromName || keyFromFile,
2753
- username: normalizeTelegramBotUsername(parsed.TELEGRAM_BOT_NAME || parsed.TELEGRAM_BOT_USERNAME || ""),
2786
+ key: keyFromFile || keyFromName,
2787
+ username: normalizeTelegramBotUsername(parsed.TELEGRAM_BOT_USERNAME || parsed.TELEGRAM_BOT_NAME || ""),
2754
2788
  serverBotID: String(parsed.TELEGRAM_BOT_SERVER_BOT_ID || "").trim(),
2789
+ serverBotName: String(parsed.TELEGRAM_BOT_SERVER_NAME || "").trim(),
2790
+ serverRoles: parseTelegramServerRoles(parsed.TELEGRAM_BOT_SERVER_ROLES || ""),
2755
2791
  token: String(parsed.TELEGRAM_BOT_TOKEN || "").trim(),
2756
2792
  roleProfile: normalizeRunnerRoleProfileName(parsed.TELEGRAM_BOT_ROLE_PROFILE || ""),
2757
2793
  client: normalizeLocalAIClientName(parsed.TELEGRAM_BOT_AI_CLIENT || "", ""),
@@ -2813,6 +2849,8 @@ function buildMergedTelegramEnvParsed(globalParsed, entries) {
2813
2849
  const upper = String(entry.key || "").trim().toUpperCase();
2814
2850
  if (!upper) return;
2815
2851
  merged[`TELEGRAM_BOT_${upper}_SERVER_BOT_ID`] = String(entry.serverBotID || "").trim();
2852
+ merged[`TELEGRAM_BOT_${upper}_SERVER_NAME`] = String(entry.serverBotName || "").trim();
2853
+ merged[`TELEGRAM_BOT_${upper}_SERVER_ROLES`] = ensureArray(entry.serverRoles).join(",");
2816
2854
  merged[`TELEGRAM_BOT_${upper}_USERNAME`] = normalizeTelegramBotUsername(entry.username || "");
2817
2855
  merged[`TELEGRAM_BOT_${upper}_TOKEN`] = String(entry.token || "").trim();
2818
2856
  merged[`TELEGRAM_BOT_${upper}_ROLE_PROFILE`] = String(entry.roleProfile || "").trim();
@@ -2874,12 +2912,14 @@ function readTelegramEnvState() {
2874
2912
  }
2875
2913
 
2876
2914
  function isTelegramEntryEnvKey(rawKey) {
2877
- return /^TELEGRAM_BOT_([A-Z0-9_]+)_(TOKEN|USERNAME|SERVER_BOT_ID|ROLE_PROFILE|AI_CLIENT|AI_MODEL|AI_PERMISSION_MODE|AI_REASONING_EFFORT)$/i
2915
+ return /^TELEGRAM_BOT_([A-Z0-9_]+)_(TOKEN|USERNAME|SERVER_BOT_ID|SERVER_NAME|SERVER_ROLES|ROLE_PROFILE|AI_CLIENT|AI_MODEL|AI_PERMISSION_MODE|AI_REASONING_EFFORT)$/i
2878
2916
  .test(String(rawKey || "").trim());
2879
2917
  }
2880
2918
 
2881
2919
  function telegramEntryCommentNameForEnv(entry) {
2882
2920
  const current = safeObject(entry);
2921
+ const serverBotName = String(current.serverBotName || "").trim();
2922
+ if (serverBotName) return serverBotName;
2883
2923
  const username = normalizeTelegramBotUsername(current.username || "");
2884
2924
  if (username) return username;
2885
2925
  return normalizeTelegramBotEnvKey(current.key || "", "telegram_bot");
@@ -2896,7 +2936,7 @@ function renderNormalizedTelegramEnv(parsedEnv) {
2896
2936
  "# Keep this file on your machine only. Do not commit it.",
2897
2937
  "# Store Telegram-wide settings here.",
2898
2938
  "# This global file lives in ~/.metheus/telegram-bots/_global.env.",
2899
- "# Per-bot secrets and AI settings live in ~/.metheus/telegram-bots/<bot-key>.env.",
2939
+ "# Per-bot secrets and AI settings live in ~/.metheus/telegram-bots/<server-bot-name>.env.",
2900
2940
  "",
2901
2941
  `TELEGRAM_API_BASE_URL=${formatProviderEnvValue(parsed.TELEGRAM_API_BASE_URL || "")}`,
2902
2942
  `TELEGRAM_AUTO_CLEAR_WEBHOOK=${boolFromRaw(parsed.TELEGRAM_AUTO_CLEAR_WEBHOOK, true) ? "true" : "false"}`,
@@ -2930,12 +2970,22 @@ function renderNormalizedTelegramEnv(parsedEnv) {
2930
2970
  function renderTelegramBotEntryEnv(entryRaw) {
2931
2971
  const entry = safeObject(entryRaw);
2932
2972
  const botName = telegramEntryCommentNameForEnv(entry);
2973
+ const serverRoles = parseTelegramServerRoles(entry.serverRoles || "");
2933
2974
  const lines = [
2934
2975
  "# Metheus local Telegram bot entry",
2935
- `# Bot: ${botName}`,
2976
+ `# Server bot: ${botName}`,
2936
2977
  "",
2937
- `TELEGRAM_BOT_NAME=${formatProviderEnvValue(botName)}`,
2938
2978
  `TELEGRAM_BOT_SERVER_BOT_ID=${formatProviderEnvValue(entry.serverBotID || "")}`,
2979
+ `TELEGRAM_BOT_SERVER_NAME=${formatProviderEnvValue(entry.serverBotName || "")}`,
2980
+ `TELEGRAM_BOT_SERVER_ROLES=${formatProviderEnvValue(serverRoles.join(","))}`,
2981
+ ];
2982
+ if (!String(entry.serverBotID || "").trim() && !String(entry.serverBotName || "").trim() && String(entry.username || "").trim()) {
2983
+ lines.push(
2984
+ "# Fallback only when server bot binding is unavailable",
2985
+ `TELEGRAM_BOT_USERNAME=${formatProviderEnvValue(entry.username || "")}`,
2986
+ );
2987
+ }
2988
+ lines.push(
2939
2989
  `TELEGRAM_BOT_TOKEN=${formatProviderEnvValue(entry.token || "")}`,
2940
2990
  `TELEGRAM_BOT_ROLE_PROFILE=${formatProviderEnvValue(entry.roleProfile || "")}`,
2941
2991
  `TELEGRAM_BOT_AI_CLIENT=${formatProviderEnvValue(entry.client || "")}`,
@@ -2943,7 +2993,7 @@ function renderTelegramBotEntryEnv(entryRaw) {
2943
2993
  `TELEGRAM_BOT_AI_PERMISSION_MODE=${formatProviderEnvValue(entry.permissionMode || "")}`,
2944
2994
  `TELEGRAM_BOT_AI_REASONING_EFFORT=${formatProviderEnvValue(entry.reasoningEffort || "")}`,
2945
2995
  "",
2946
- ];
2996
+ );
2947
2997
  return `${lines.join("\n").trimEnd()}\n`;
2948
2998
  }
2949
2999
 
@@ -2954,6 +3004,8 @@ function writeTelegramEnvState(parsedEnv) {
2954
3004
  entry.token
2955
3005
  || entry.username
2956
3006
  || entry.serverBotID
3007
+ || entry.serverBotName
3008
+ || ensureArray(entry.serverRoles).length
2957
3009
  || entry.roleProfile
2958
3010
  || entry.client
2959
3011
  || entry.model
@@ -3009,7 +3061,13 @@ function normalizeExistingProviderEnvFile(provider, filePath) {
3009
3061
  function resolveTelegramEnvConfig(parsedEnv, filePath, config, selectors = {}) {
3010
3062
  const parsed = safeObject(parsedEnv);
3011
3063
  const legacyToken = String(parsed.TELEGRAM_BOT_TOKEN || "").trim();
3012
- const entries = collectTelegramEnvBotEntries(parsed).filter((entry) => entry.token || entry.username || entry.serverBotID);
3064
+ const entries = collectTelegramEnvBotEntries(parsed).filter((entry) => (
3065
+ entry.token
3066
+ || entry.username
3067
+ || entry.serverBotID
3068
+ || entry.serverBotName
3069
+ || ensureArray(entry.serverRoles).length
3070
+ ));
3013
3071
  const desiredBotID = firstNonEmptyString([
3014
3072
  selectors.serverBotID,
3015
3073
  selectors.botID,
@@ -3037,7 +3095,11 @@ function resolveTelegramEnvConfig(parsedEnv, filePath, config, selectors = {}) {
3037
3095
  }
3038
3096
  if (!selected && desiredUsername) {
3039
3097
  selected = entries.find(
3040
- (entry) => entry.username === desiredUsername || normalizeTelegramBotUsername(entry.key) === desiredUsername,
3098
+ (entry) => (
3099
+ entry.username === desiredUsername
3100
+ || normalizeTelegramBotUsername(entry.serverBotName) === desiredUsername
3101
+ || normalizeTelegramBotUsername(entry.key) === desiredUsername
3102
+ ),
3041
3103
  ) || null;
3042
3104
  }
3043
3105
  if (!selected && desiredBotKey) {
@@ -3071,6 +3133,8 @@ function resolveTelegramEnvConfig(parsedEnv, filePath, config, selectors = {}) {
3071
3133
  botKey: selected.key,
3072
3134
  botUsername: selected.username,
3073
3135
  serverBotID: selected.serverBotID,
3136
+ serverBotName: selected.serverBotName,
3137
+ serverRoles: ensureArray(selected.serverRoles),
3074
3138
  roleProfile: selected.roleProfile,
3075
3139
  client: selected.client,
3076
3140
  model: selected.model,
@@ -3095,6 +3159,8 @@ function resolveTelegramEnvConfig(parsedEnv, filePath, config, selectors = {}) {
3095
3159
  botKey: "",
3096
3160
  botUsername: "",
3097
3161
  serverBotID: "",
3162
+ serverBotName: "",
3163
+ serverRoles: [],
3098
3164
  roleProfile: "",
3099
3165
  client: "",
3100
3166
  model: "",
@@ -5467,8 +5533,8 @@ TELEGRAM_BOT_REVIEW_TOKEN=review-token
5467
5533
  normalizedGlobalText.includes("TELEGRAM_DEFAULT_BOT_KEY=ryoai_bot")
5468
5534
  && !normalizedGlobalText.includes("TELEGRAM_BOT_RYOAI_BOT_TOKEN")
5469
5535
  && !normalizedGlobalText.includes("TELEGRAM_BOT_RYOAI_TOKEN")
5470
- && normalizedBotText.includes("TELEGRAM_BOT_NAME=ryoai_bot")
5471
5536
  && normalizedBotText.includes("TELEGRAM_BOT_SERVER_BOT_ID=bot-ryoai")
5537
+ && normalizedBotText.includes("TELEGRAM_BOT_SERVER_NAME=")
5472
5538
  && normalizedBotText.includes("TELEGRAM_BOT_TOKEN=ryoai-token"),
5473
5539
  `${normalizedGlobalText}\n---\n${normalizedBotText}`,
5474
5540
  );
@@ -216,20 +216,21 @@ function printBotUsage(deps) {
216
216
  "",
217
217
  ` ${cliName} bot setup`,
218
218
  ` ${cliName} bot list [--provider <telegram|slack|kakaotalk>] [--json <true|false>]`,
219
- ` ${cliName} bot show [--provider <telegram|slack|kakaotalk>] [--bot-key <key>] [--bot-id <uuid>] [--bot-name <name>] [--json <true|false>]`,
219
+ ` ${cliName} bot show [--provider <telegram|slack|kakaotalk>] [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>] [--json <true|false>]`,
220
220
  ` ${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>]`,
221
- ` ${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>]`,
222
- ` ${cliName} bot remove [--provider <telegram|slack|kakaotalk>] [--non-interactive <true|false>] [--bot-key <key>]`,
223
- ` ${cliName} bot set-default --provider telegram [--bot-key <key>] [--non-interactive <true|false>]`,
224
- ` ${cliName} bot migrate --provider telegram [--bot-key <key>] [--bot-id <uuid>] [--bot-name <name>] [--keep-legacy-token <true|false>] [--non-interactive <true|false>]`,
225
- ` ${cliName} bot verify [--provider <telegram|slack|kakaotalk>] [--bot-key <key>] [--timeout-seconds <n>] [--json <true|false>]`,
226
- ` ${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>]`,
221
+ ` ${cliName} bot edit [--provider <telegram|slack|kakaotalk>] [--base-url <url>] [--timeout-seconds <n>] [--non-interactive <true|false>] [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>] [--username <fallback_name>] [--token <token>] [--role-profile <name>] [--ai-client <name>] [--ai-model <name>] [--ai-permission-mode <mode>] [--ai-reasoning-effort <level>]`,
222
+ ` ${cliName} bot remove [--provider <telegram|slack|kakaotalk>] [--non-interactive <true|false>] [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>]`,
223
+ ` ${cliName} bot set-default --provider telegram [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>] [--non-interactive <true|false>]`,
224
+ ` ${cliName} bot migrate --provider telegram [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>] [--keep-legacy-token <true|false>] [--non-interactive <true|false>]`,
225
+ ` ${cliName} bot verify [--provider <telegram|slack|kakaotalk>] [--bot-name <server_name>] [--bot-id <uuid>] [--bot-key <advanced_key>] [--timeout-seconds <n>] [--json <true|false>]`,
226
+ ` ${cliName} bot global --provider telegram [--non-interactive <true|false>] [--api-base-url <url>] [--auto-clear-webhook <true|false>] [--allowed-updates <csv>] [--default-bot-key <advanced_key>]`,
227
227
  "",
228
228
  `Behavior:`,
229
229
  ` - bot setup asks for provider first, then shows a numbered action menu.`,
230
230
  ` - bot add without flags uses the shortest practical guided flow: provider -> token -> verify -> optional username fallback -> optional default bot.`,
231
231
  ` - in the normal Telegram path, bot add does not ask for a local bot key or a server role/profile choice.`,
232
- ` - server bot name/UUID is the source of truth; local key is auto-derived unless you intentionally override it with --bot-key.`,
232
+ ` - server bot name/UUID is the source of truth; --bot-key is an advanced local selector only.`,
233
+ ` - runner commands use --route-name, not bot name. Use bot verify/show first, then run the linked route.`,
233
234
  ` - bot edit without flags asks for provider, existing entry, then walks field-by-field in a numbered flow.`,
234
235
  ` - in the normal Telegram edit path, the CLI keeps or re-resolves the server bot binding automatically instead of asking you to pick a server role/profile UUID first.`,
235
236
  ` - bot set-default / bot verify / bot remove also support guided numbered selection when run without flags.`,
@@ -442,6 +443,8 @@ function telegramEntryEnvKeys(botKey) {
442
443
  const upper = String(botKey || "").trim().toUpperCase();
443
444
  return {
444
445
  serverBotID: `TELEGRAM_BOT_${upper}_SERVER_BOT_ID`,
446
+ serverBotName: `TELEGRAM_BOT_${upper}_SERVER_NAME`,
447
+ serverRoles: `TELEGRAM_BOT_${upper}_SERVER_ROLES`,
445
448
  username: `TELEGRAM_BOT_${upper}_USERNAME`,
446
449
  token: `TELEGRAM_BOT_${upper}_TOKEN`,
447
450
  roleProfile: `TELEGRAM_BOT_${upper}_ROLE_PROFILE`,
@@ -454,6 +457,8 @@ function telegramEntryEnvKeys(botKey) {
454
457
 
455
458
  function telegramEntryCommentName(entry) {
456
459
  const current = safeObject(entry);
460
+ const serverBotName = String(current.serverBotName || "").trim();
461
+ if (serverBotName) return serverBotName;
457
462
  const username = String(current.username || "").trim();
458
463
  if (username) return username;
459
464
  const key = String(current.key || "").trim();
@@ -465,6 +470,8 @@ function persistTelegramUsername(entry) {
465
470
  const current = safeObject(entry);
466
471
  if (current.__preferServerIdentity) return "";
467
472
  if (String(current.serverBotID || "").trim()) return "";
473
+ if (String(current.serverBotName || "").trim()) return "";
474
+ if (ensureArray(current.serverRoles).length) return "";
468
475
  return normalizeServerBotIdentityText(current.username || "");
469
476
  }
470
477
 
@@ -486,6 +493,8 @@ function upsertTelegramEntry(parsedEnv, entry) {
486
493
  const keys = telegramEntryEnvKeys(entry.key);
487
494
  const next = { ...parsed };
488
495
  next[keys.serverBotID] = String(entry.serverBotID || "").trim();
496
+ next[keys.serverBotName] = String(entry.serverBotName || "").trim();
497
+ next[keys.serverRoles] = ensureArray(entry.serverRoles).join(",");
489
498
  next[keys.username] = persistTelegramUsername(entry);
490
499
  next[keys.token] = String(entry.token || "").trim();
491
500
  next[keys.roleProfile] = String(entry.roleProfile || "").trim();
@@ -512,7 +521,7 @@ function telegramKnownKeys(parsedEnv, deps) {
512
521
  }
513
522
 
514
523
  function isTelegramEntryEnvKey(rawKey) {
515
- return /^TELEGRAM_BOT_([A-Z0-9_]+)_(TOKEN|USERNAME|SERVER_BOT_ID|ROLE_PROFILE|AI_CLIENT|AI_MODEL|AI_PERMISSION_MODE|AI_REASONING_EFFORT)$/i
524
+ return /^TELEGRAM_BOT_([A-Z0-9_]+)_(TOKEN|USERNAME|SERVER_BOT_ID|SERVER_NAME|SERVER_ROLES|ROLE_PROFILE|AI_CLIENT|AI_MODEL|AI_PERMISSION_MODE|AI_REASONING_EFFORT)$/i
516
525
  .test(String(rawKey || "").trim());
517
526
  }
518
527
 
@@ -525,6 +534,8 @@ function renderTelegramEnv(parsedEnv, deps) {
525
534
  entry.token
526
535
  || entry.username
527
536
  || entry.serverBotID
537
+ || entry.serverBotName
538
+ || ensureArray(entry.serverRoles).length
528
539
  || entry.roleProfile
529
540
  || entry.client
530
541
  || entry.model
@@ -562,6 +573,8 @@ function renderTelegramEnv(parsedEnv, deps) {
562
573
  const entryLines = [
563
574
  `# Telegram bot entry: ${telegramEntryCommentName(entry)}`,
564
575
  `${keys.serverBotID}=${formatEnvValue(entry.serverBotID || "")}`,
576
+ `${keys.serverBotName}=${formatEnvValue(entry.serverBotName || "")}`,
577
+ `${keys.serverRoles}=${formatEnvValue(ensureArray(entry.serverRoles).join(","))}`,
565
578
  ];
566
579
  if (String(entry.username || "").trim()) {
567
580
  entryLines.push(`${keys.username}=${formatEnvValue(entry.username || "")}`);
@@ -615,6 +628,46 @@ function renderTokenOnlyProviderEnv(provider, parsedEnv, deps) {
615
628
  return `${lines.join("\n").trimEnd()}\n`;
616
629
  }
617
630
 
631
+ function renderTelegramBotEntryFile(entryRaw) {
632
+ const entry = safeObject(entryRaw);
633
+ const botName = telegramEntryCommentName(entry);
634
+ const serverRoles = ensureArray(entry.serverRoles).join(",");
635
+ const lines = [
636
+ "# Metheus local Telegram bot entry",
637
+ `# Server bot: ${botName}`,
638
+ "",
639
+ `TELEGRAM_BOT_SERVER_BOT_ID=${formatEnvValue(entry.serverBotID || "")}`,
640
+ `TELEGRAM_BOT_SERVER_NAME=${formatEnvValue(entry.serverBotName || "")}`,
641
+ `TELEGRAM_BOT_SERVER_ROLES=${formatEnvValue(serverRoles)}`,
642
+ ];
643
+ if (!String(entry.serverBotID || "").trim() && !String(entry.serverBotName || "").trim() && String(entry.username || "").trim()) {
644
+ lines.push(
645
+ "# Fallback only when server bot binding is unavailable",
646
+ `TELEGRAM_BOT_USERNAME=${formatEnvValue(entry.username || "")}`,
647
+ );
648
+ }
649
+ lines.push(
650
+ `TELEGRAM_BOT_TOKEN=${formatEnvValue(entry.token || "")}`,
651
+ `TELEGRAM_BOT_ROLE_PROFILE=${formatEnvValue(entry.roleProfile || "")}`,
652
+ `TELEGRAM_BOT_AI_CLIENT=${formatEnvValue(entry.client || "")}`,
653
+ `TELEGRAM_BOT_AI_MODEL=${formatEnvValue(entry.model || "")}`,
654
+ `TELEGRAM_BOT_AI_PERMISSION_MODE=${formatEnvValue(entry.permissionMode || "")}`,
655
+ `TELEGRAM_BOT_AI_REASONING_EFFORT=${formatEnvValue(entry.reasoningEffort || "")}`,
656
+ "",
657
+ );
658
+ return `${lines.join("\n").trimEnd()}\n`;
659
+ }
660
+
661
+ function rewriteTelegramEntryFile(entry, deps) {
662
+ const entryFilePath = String(requireDependency(deps, "telegramBotEntryFilePath")(entry.key) || "").trim();
663
+ if (!entryFilePath) return;
664
+ fs.mkdirSync(path.dirname(entryFilePath), { recursive: true });
665
+ fs.writeFileSync(entryFilePath, renderTelegramBotEntryFile(entry), {
666
+ encoding: "utf8",
667
+ mode: 0o600,
668
+ });
669
+ }
670
+
618
671
  function writeProviderEnvState(provider, parsedEnv, deps) {
619
672
  if (provider === "telegram" && typeof deps?.writeTelegramEnvState === "function") {
620
673
  return deps.writeTelegramEnvState(parsedEnv);
@@ -644,6 +697,8 @@ function telegramEntriesForDisplay(parsedEnv, deps) {
644
697
  entry.token
645
698
  || entry.username
646
699
  || entry.serverBotID
700
+ || entry.serverBotName
701
+ || ensureArray(entry.serverRoles).length
647
702
  || entry.roleProfile
648
703
  || entry.client
649
704
  || entry.model
@@ -659,6 +714,8 @@ function telegramEntriesForDisplay(parsedEnv, deps) {
659
714
 
660
715
  function telegramEntryDisplayName(entry) {
661
716
  const current = safeObject(entry);
717
+ const serverBotName = String(current.serverBotName || "").trim();
718
+ if (serverBotName) return serverBotName;
662
719
  const username = String(current.username || "").trim();
663
720
  if (username) return `@${username}`;
664
721
  return String(current.key || "").trim() || "(unnamed)";
@@ -694,9 +751,9 @@ function formatRuntimeReasoningField(clientName, reasoningEffort, style = "equal
694
751
  function telegramEntryDisplayDescription(entry) {
695
752
  const current = safeObject(entry);
696
753
  const detail = [
697
- current.key ? `key:${current.key}` : "",
698
- current.serverBotID ? `server:${current.serverBotID}` : "",
699
- current.client ? `AI:${displayLocalAIClientName(current.client)}` : "",
754
+ current.key ? `local_selector:${current.key}` : "",
755
+ current.serverBotID ? `stored_server_bot_id:${current.serverBotID}` : "",
756
+ current.client ? `ai_client:${displayLocalAIClientName(current.client)}` : "",
700
757
  ].filter(Boolean);
701
758
  return detail.join(" ");
702
759
  }
@@ -705,7 +762,9 @@ function findTelegramEntryByFlags(parsedEnv, flags, deps) {
705
762
  const entries = telegramEntriesForDisplay(parsedEnv, deps);
706
763
  const requestedKey = String(flags["bot-key"] || "").trim();
707
764
  const requestedBotID = String(flags["bot-id"] || flags["server-bot-id"] || "").trim();
708
- const requestedName = requireDependency(deps, "normalizeTelegramBotUsername")(flags["bot-name"] || flags.username || "");
765
+ const requestedNameRaw = String(flags["bot-name"] || flags.username || "").trim();
766
+ const requestedUsername = requireDependency(deps, "normalizeTelegramBotUsername")(requestedNameRaw);
767
+ const requestedIdentity = normalizeServerBotIdentityText(requestedNameRaw);
709
768
  if (requestedKey) {
710
769
  const match = entries.find((entry) => entry.key === requestedKey);
711
770
  if (!match) {
@@ -720,12 +779,16 @@ function findTelegramEntryByFlags(parsedEnv, flags, deps) {
720
779
  }
721
780
  return match;
722
781
  }
723
- if (requestedName) {
782
+ if (requestedNameRaw) {
724
783
  const match = entries.find(
725
- (entry) => entry.username === requestedName || normalizeServerBotIdentityText(entry.key) === requestedName,
784
+ (entry) => (
785
+ entry.username === requestedUsername
786
+ || normalizeServerBotIdentityText(entry.serverBotName) === requestedIdentity
787
+ || normalizeServerBotIdentityText(entry.key) === requestedIdentity
788
+ ),
726
789
  );
727
790
  if (!match) {
728
- throw new Error(`Telegram bot entry with server bot name "${requestedName}" was not found`);
791
+ throw new Error(`Telegram bot entry with server bot name "${requestedNameRaw}" was not found`);
729
792
  }
730
793
  return match;
731
794
  }
@@ -783,6 +846,7 @@ async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps)
783
846
  const initialBinding = await resolveTelegramServerBindingDetails(
784
847
  {
785
848
  serverBotID: current.serverBotID,
849
+ serverBotName: current.serverBotName,
786
850
  botUsername: current.username,
787
851
  botKey: current.key,
788
852
  roleProfile: current.roleProfile,
@@ -795,8 +859,13 @@ async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps)
795
859
  current.__preferServerIdentity = true;
796
860
  if (initialBinding.mode === "single") {
797
861
  current.serverBotID = String(initialBinding.serverBotID || "").trim();
862
+ current.serverBotName = String(initialBinding.name || "").trim();
863
+ current.serverRoles = ensureArray(initialBinding.roles);
798
864
  current.roleProfile = String(initialBinding.role || current.roleProfile || "").trim();
799
865
  applyRoleProfileDefaults(current, resolveRoleProfileDefaults(current.roleProfile, deps));
866
+ } else if (initialBinding.mode === "group") {
867
+ current.serverBotName = String(initialBinding.name || "").trim();
868
+ current.serverRoles = ensureArray(initialBinding.roles);
800
869
  }
801
870
  }
802
871
  }
@@ -847,6 +916,8 @@ async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps)
847
916
  const serverBot = await autoResolveTelegramServerBot(current, flags, deps);
848
917
  if (serverBot.matchMode === "group") {
849
918
  current.serverBotID = "";
919
+ current.serverBotName = String(serverBot.name || "").trim();
920
+ current.serverRoles = preferredRoleSort(serverBot.roles);
850
921
  current.__preferServerIdentity = true;
851
922
  current.roleProfile = "";
852
923
  current.client = "";
@@ -861,6 +932,8 @@ async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps)
861
932
  );
862
933
  } else if (String(serverBot.botID || "").trim()) {
863
934
  current.serverBotID = String(serverBot.botID || "").trim();
935
+ current.serverBotName = String(serverBot.name || "").trim();
936
+ current.serverRoles = ensureArray(serverBot.roles);
864
937
  current.__preferServerIdentity = true;
865
938
  current.roleProfile = String(serverBot.role || current.roleProfile || "").trim();
866
939
  serverRoleAutoResolved = String(serverBot.role || current.roleProfile || "").trim() || "__server_binding__";
@@ -995,6 +1068,8 @@ function renderBotListPayload(provider, state, deps) {
995
1068
  key: entry.key,
996
1069
  isDefault: entry.isDefault,
997
1070
  serverBotID: entry.serverBotID,
1071
+ serverBotName: entry.serverBotName,
1072
+ serverRoles: ensureArray(entry.serverRoles),
998
1073
  username: entry.username,
999
1074
  tokenConfigured: Boolean(entry.token),
1000
1075
  roleProfile: entry.roleProfile,
@@ -1037,6 +1112,8 @@ function buildBotShowPayload(provider, state, entry, deps, extras = {}) {
1037
1112
  key: selectedEntry.key,
1038
1113
  isDefault: selectedEntry.isDefault,
1039
1114
  serverBotID: selectedEntry.serverBotID,
1115
+ serverBotName: selectedEntry.serverBotName,
1116
+ serverRoles: ensureArray(selectedEntry.serverRoles),
1040
1117
  username: selectedEntry.username,
1041
1118
  tokenConfigured: Boolean(selectedEntry.token),
1042
1119
  roleProfile: selectedEntry.roleProfile,
@@ -1133,9 +1210,9 @@ function printBotList(provider, state, deps) {
1133
1210
  process.stdout.write(
1134
1211
  [
1135
1212
  ` - ${telegramEntryDisplayName(entry)}${entry.isDefault ? " [default]" : ""}`,
1136
- ` key: ${entry.key || "-"}`,
1213
+ ` local_selector: ${entry.key || "-"}${entry.key ? " (advanced)" : ""}`,
1137
1214
  ` server_bot_id: ${entry.serverBotID || "-"}`,
1138
- ` username: ${entry.username ? `@${entry.username}` : "-"}`,
1215
+ ` fallback_username: ${entry.username ? `@${entry.username}` : "-"}`,
1139
1216
  ` token: ${entry.token ? maskSecret(entry.token) : "-"}`,
1140
1217
  ` role_profile: ${entry.roleProfile || "-"}`,
1141
1218
  ` ai_client: ${entry.client ? displayLocalAIClientName(entry.client) : "-"}`,
@@ -1159,11 +1236,13 @@ function printBotShow(provider, state, entry, deps, extras = {}) {
1159
1236
  const routeLinks = safeObject(extras.routeLinks);
1160
1237
  process.stdout.write(`${providerLabel(provider, deps)} bot\n`);
1161
1238
  process.stdout.write(` file: ${state.filePath}\n`);
1162
- process.stdout.write(` name: ${telegramEntryDisplayName(selectedEntry)}\n`);
1163
- process.stdout.write(` key: ${selectedEntry.key || "-"}\n`);
1239
+ process.stdout.write(` server_name: ${telegramEntryDisplayName(selectedEntry)}\n`);
1240
+ process.stdout.write(` local_selector: ${selectedEntry.key || "-"}${selectedEntry.key ? " (advanced)" : ""}\n`);
1164
1241
  process.stdout.write(` default: ${selectedEntry.isDefault ? "yes" : "no"}\n`);
1165
- process.stdout.write(` server_bot_id: ${selectedEntry.serverBotID || "-"}\n`);
1166
- process.stdout.write(` username: ${selectedEntry.username ? `@${selectedEntry.username}` : "-"}\n`);
1242
+ process.stdout.write(` stored_server_bot_id: ${selectedEntry.serverBotID || "-"}\n`);
1243
+ process.stdout.write(` stored_server_name: ${selectedEntry.serverBotName || "-"}\n`);
1244
+ process.stdout.write(` stored_server_roles: ${ensureArray(selectedEntry.serverRoles).join(", ") || "-" }\n`);
1245
+ process.stdout.write(` fallback_username: ${selectedEntry.username ? `@${selectedEntry.username}` : "-"}\n`);
1167
1246
  process.stdout.write(` token: ${selectedEntry.token ? maskSecret(selectedEntry.token) : "(not configured)"}\n`);
1168
1247
  process.stdout.write(` role_profile: ${selectedEntry.roleProfile || "-"}\n`);
1169
1248
  process.stdout.write(` ai_client: ${selectedEntry.client ? displayLocalAIClientName(selectedEntry.client) : "-"}\n`);
@@ -1172,17 +1251,17 @@ function printBotShow(provider, state, entry, deps, extras = {}) {
1172
1251
  process.stdout.write(` reasoning_effort: ${selectedEntry.reasoningEffort || "-"}\n`);
1173
1252
  if (Object.keys(serverBinding).length) {
1174
1253
  process.stdout.write(` server_binding: ${serverBinding.ok ? "OK" : "FAIL"}${serverBinding.detail ? ` (${serverBinding.detail})` : ""}\n`);
1175
- process.stdout.write(` server_binding_mode: ${serverBinding.mode || "-"}\n`);
1176
- process.stdout.write(` server_bot_name: ${serverBinding.name || "-"}\n`);
1177
- process.stdout.write(` server_bot_id: ${serverBinding.serverBotID || selectedEntry.serverBotID || "-" }\n`);
1254
+ process.stdout.write(` binding_mode: ${serverBinding.mode || "-"}\n`);
1255
+ process.stdout.write(` live_server_name: ${serverBinding.name || "-"}\n`);
1256
+ process.stdout.write(` live_server_bot_id: ${serverBinding.serverBotID || selectedEntry.serverBotID || "-" }\n`);
1178
1257
  if (serverBinding.mode === "group") {
1179
- process.stdout.write(` server_roles: ${ensureArray(serverBinding.roles).join(", ") || "-"}\n`);
1258
+ process.stdout.write(` live_server_roles: ${ensureArray(serverBinding.roles).join(", ") || "-"}\n`);
1180
1259
  const groupedProfiles = safeObject(serverBinding.effectiveRoleProfiles);
1181
1260
  Object.keys(groupedProfiles).forEach((role) => {
1182
1261
  process.stdout.write(` runtime_role_profile[${role}]: ${formatRoleProfileOutputLine(groupedProfiles[role])}\n`);
1183
1262
  });
1184
1263
  } else if (serverBinding.effectiveRoleProfile) {
1185
- process.stdout.write(` server_role: ${serverBinding.role || "-"}\n`);
1264
+ process.stdout.write(` live_server_role: ${serverBinding.role || "-"}\n`);
1186
1265
  process.stdout.write(` runtime_role_profile: ${formatRoleProfileOutputLine(serverBinding.effectiveRoleProfile)}\n`);
1187
1266
  }
1188
1267
  }
@@ -1877,7 +1956,12 @@ async function maybePromptGroupedServerRoleProfiles(ui, serverBot, deps) {
1877
1956
 
1878
1957
  function resolveTelegramServerBindingDetailsFromBots(envConfig, bots, deps) {
1879
1958
  const current = safeObject(envConfig);
1880
- if (!String(current.serverBotID || "").trim() && !String(current.botUsername || "").trim() && !String(current.botKey || "").trim()) {
1959
+ if (
1960
+ !String(current.serverBotID || "").trim()
1961
+ && !String(current.serverBotName || "").trim()
1962
+ && !String(current.botUsername || "").trim()
1963
+ && !String(current.botKey || "").trim()
1964
+ ) {
1881
1965
  return null;
1882
1966
  }
1883
1967
  const resolveGroupedPayload = (matches, matchedBy) => {
@@ -1929,7 +2013,7 @@ function resolveTelegramServerBindingDetailsFromBots(envConfig, bots, deps) {
1929
2013
  detail: `${String(match.name || "").trim() || "(unnamed)"} [${String(match.role || "").trim() || "-"}]`,
1930
2014
  };
1931
2015
  }
1932
- const normalizedServerIdentity = normalizeServerBotIdentityText(current.botUsername || current.botKey);
2016
+ const normalizedServerIdentity = normalizeServerBotIdentityText(current.serverBotName || current.botUsername || current.botKey);
1933
2017
  const matches = bots.filter((bot) => normalizeServerBotIdentityText(bot.name) === normalizedServerIdentity);
1934
2018
  if (!matches.length) {
1935
2019
  return {
@@ -2014,6 +2098,7 @@ async function buildTelegramEntrySelectionRows(entries, flags, deps) {
2014
2098
  ? resolveTelegramServerBindingDetailsFromBots(
2015
2099
  {
2016
2100
  serverBotID: current.serverBotID,
2101
+ serverBotName: current.serverBotName,
2017
2102
  botUsername: current.username,
2018
2103
  botKey: current.key,
2019
2104
  roleProfile: current.roleProfile,
@@ -2034,9 +2119,9 @@ async function buildTelegramEntrySelectionRows(entries, flags, deps) {
2034
2119
  label = serverName;
2035
2120
  }
2036
2121
  const descriptionParts = [
2037
- current.key ? `local:${current.key}` : "",
2038
- current.serverBotID ? `server:${current.serverBotID}` : "",
2039
- current.client ? `AI:${displayLocalAIClientName(current.client)}` : "",
2122
+ current.key ? `local_selector:${current.key} (advanced)` : "",
2123
+ current.serverBotID ? `stored_server_bot_id:${current.serverBotID}` : "",
2124
+ current.client ? `ai_client:${displayLocalAIClientName(current.client)}` : "",
2040
2125
  ].filter(Boolean);
2041
2126
  return {
2042
2127
  value: current.key,
@@ -2062,6 +2147,7 @@ async function verifyTelegramTokenCandidate(provider, token, apiBaseURL, timeout
2062
2147
  async function autoResolveTelegramServerBot(current, flags, deps) {
2063
2148
  const existingBotID = String(current?.serverBotID || "").trim();
2064
2149
  const preferredIdentity = firstNonEmptyString([
2150
+ current?.serverBotName,
2065
2151
  current?.username,
2066
2152
  current?.key,
2067
2153
  ]);
@@ -2275,6 +2361,8 @@ async function addTelegramBot(ui, flags, deps) {
2275
2361
  const nextParsed = upsertTelegramEntry(parsed, {
2276
2362
  key: botKey,
2277
2363
  serverBotID: String(getServerBotIDFlag(flags) || serverBot.botID || "").trim(),
2364
+ serverBotName: String(serverBot.name || "").trim(),
2365
+ serverRoles: preferredRoleSort(serverBot.roles),
2278
2366
  username,
2279
2367
  __preferServerIdentity: serverBot.matchMode === "group" || Boolean(String(getServerBotIDFlag(flags) || serverBot.botID || "").trim()),
2280
2368
  token,
@@ -2391,6 +2479,23 @@ async function editTelegramBot(ui, flags, deps) {
2391
2479
  if (boolFromRaw(flags.default, false)) {
2392
2480
  parsed.TELEGRAM_DEFAULT_BOT_KEY = current.key;
2393
2481
  }
2482
+ if (current.serverBotID || current.username || current.serverBotName) {
2483
+ const resolvedServerBot = await autoResolveTelegramServerBot(current, flags, deps);
2484
+ if (resolvedServerBot.matchMode === "group") {
2485
+ current.serverBotID = String(getServerBotIDFlag(flags) || current.serverBotID || "").trim();
2486
+ current.serverBotName = String(resolvedServerBot.name || current.serverBotName || "").trim();
2487
+ current.serverRoles = preferredRoleSort(resolvedServerBot.roles);
2488
+ if (!hasOwnFlag(flags, "role-profile")) current.roleProfile = "";
2489
+ if (!(hasOwnFlag(flags, "client") || hasOwnFlag(flags, "ai-client"))) current.client = "";
2490
+ if (!(hasOwnFlag(flags, "model") || hasOwnFlag(flags, "ai-model"))) current.model = "";
2491
+ if (!(hasOwnFlag(flags, "permission-mode") || hasOwnFlag(flags, "ai-permission-mode"))) current.permissionMode = "";
2492
+ if (!(hasOwnFlag(flags, "reasoning-effort") || hasOwnFlag(flags, "ai-reasoning-effort"))) current.reasoningEffort = "";
2493
+ } else if (String(resolvedServerBot.botID || "").trim()) {
2494
+ current.serverBotID = String(resolvedServerBot.botID || "").trim();
2495
+ current.serverBotName = String(resolvedServerBot.name || "").trim();
2496
+ current.serverRoles = ensureArray(resolvedServerBot.roles);
2497
+ }
2498
+ }
2394
2499
  saveTelegramBotEdit(parsed, selected, current, deps);
2395
2500
  return;
2396
2501
  }
@@ -2462,6 +2567,62 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
2462
2567
  if (serverBinding && !serverBinding.ok) {
2463
2568
  overallOK = false;
2464
2569
  }
2570
+ if (serverBinding?.ok && envConfig.botKey) {
2571
+ const selectedEntry = findTelegramEntryByFlags(
2572
+ state.parsed,
2573
+ { "bot-key": envConfig.botKey },
2574
+ deps,
2575
+ );
2576
+ if (selectedEntry) {
2577
+ const desiredServerBotName = String(serverBinding.name || "").trim();
2578
+ const desiredServerRoles = ensureArray(serverBinding.roles);
2579
+ const currentServerRoles = ensureArray(selectedEntry.serverRoles);
2580
+ const currentServerRolesJoined = currentServerRoles.join(",");
2581
+ const desiredServerRolesJoined = desiredServerRoles.join(",");
2582
+ const desiredServerBotID = (
2583
+ String(selectedEntry.serverBotID || "").trim()
2584
+ || (serverBinding.mode === "single" ? String(serverBinding.serverBotID || "").trim() : "")
2585
+ );
2586
+ const needsMetadataBackfill = (
2587
+ String(selectedEntry.serverBotName || "").trim() !== desiredServerBotName
2588
+ || currentServerRolesJoined !== desiredServerRolesJoined
2589
+ || String(selectedEntry.serverBotID || "").trim() !== desiredServerBotID
2590
+ );
2591
+ if (needsMetadataBackfill) {
2592
+ const nextEntry = {
2593
+ ...selectedEntry,
2594
+ serverBotID: desiredServerBotID,
2595
+ serverBotName: desiredServerBotName,
2596
+ serverRoles: desiredServerRoles,
2597
+ };
2598
+ const nextParsed = upsertTelegramEntry(state.parsed, nextEntry);
2599
+ writeProviderEnvState("telegram", nextParsed, deps);
2600
+ rewriteTelegramEntryFile(nextEntry, deps);
2601
+ envConfig.serverBotID = desiredServerBotID;
2602
+ envConfig.serverBotName = desiredServerBotName;
2603
+ envConfig.serverRoles = desiredServerRoles;
2604
+ }
2605
+ const rewriteEntry = needsMetadataBackfill
2606
+ ? {
2607
+ ...selectedEntry,
2608
+ serverBotID: desiredServerBotID,
2609
+ serverBotName: desiredServerBotName,
2610
+ serverRoles: desiredServerRoles,
2611
+ }
2612
+ : selectedEntry;
2613
+ const currentEntryFilePath = String(
2614
+ rewriteEntry.entryFilePath
2615
+ || requireDependency(deps, "telegramBotEntryFilePath")(rewriteEntry.key),
2616
+ ).trim();
2617
+ const expectedEntryText = renderTelegramBotEntryFile(rewriteEntry);
2618
+ const currentEntryText = currentEntryFilePath && fs.existsSync(currentEntryFilePath)
2619
+ ? fs.readFileSync(currentEntryFilePath, "utf8")
2620
+ : "";
2621
+ if (currentEntryText !== expectedEntryText) {
2622
+ rewriteTelegramEntryFile(rewriteEntry, deps);
2623
+ }
2624
+ }
2625
+ }
2465
2626
  routeLinks = summarizeTelegramRouteLinks(state.parsed, {
2466
2627
  key: envConfig.botKey,
2467
2628
  serverBotID: envConfig.serverBotID,
@@ -2492,6 +2653,8 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
2492
2653
  filePath: envConfig.filePath,
2493
2654
  botKey: envConfig.botKey || "",
2494
2655
  serverBotID: envConfig.serverBotID || "",
2656
+ serverBotName: envConfig.serverBotName || "",
2657
+ serverRoles: ensureArray(envConfig.serverRoles),
2495
2658
  detail: result.detail || "",
2496
2659
  roleProfile: envConfig.roleProfile || "",
2497
2660
  client: envConfig.client || "",
@@ -2506,8 +2669,10 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
2506
2669
  const lines = [
2507
2670
  `${providerLabel(provider, deps)} verify: ${overallOK ? "OK" : "FAIL"}`,
2508
2671
  `file: ${envConfig.filePath}`,
2509
- provider === "telegram" ? `bot_key: ${envConfig.botKey || "-"}` : "",
2510
- provider === "telegram" ? `server_bot_id: ${envConfig.serverBotID || "-"}` : "",
2672
+ provider === "telegram" ? `local_selector: ${envConfig.botKey || "-"}${envConfig.botKey ? " (advanced)" : ""}` : "",
2673
+ provider === "telegram" ? `stored_server_bot_id: ${envConfig.serverBotID || "-"}` : "",
2674
+ provider === "telegram" ? `stored_server_name: ${envConfig.serverBotName || "-"}` : "",
2675
+ provider === "telegram" ? `stored_server_roles: ${ensureArray(envConfig.serverRoles).join(", ") || "-"}` : "",
2511
2676
  provider === "telegram" ? `role_profile: ${envConfig.roleProfile || "-"}` : "",
2512
2677
  provider === "telegram" ? `ai_client: ${envConfig.client ? displayLocalAIClientName(envConfig.client) : "-"}` : "",
2513
2678
  provider === "telegram" ? `ai_model: ${envConfig.model || "-"}` : "",
@@ -2518,9 +2683,9 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
2518
2683
  routeLinks ? `route_links: ${intFromRaw(routeLinks.total, 0)}` : "",
2519
2684
  ].filter(Boolean);
2520
2685
  if (provider === "telegram" && serverBinding) {
2521
- lines.push(`server_binding_mode: ${serverBinding.mode || "-"}`);
2522
- lines.push(`server_bot_name: ${serverBinding.name || "-"}`);
2523
- lines.push(`server_roles: ${ensureArray(serverBinding.roles).join(", ") || "-"}`);
2686
+ lines.push(`binding_mode: ${serverBinding.mode || "-"}`);
2687
+ lines.push(`live_server_name: ${serverBinding.name || "-"}`);
2688
+ lines.push(`live_server_roles: ${ensureArray(serverBinding.roles).join(", ") || "-"}`);
2524
2689
  if (serverBinding.mode === "group") {
2525
2690
  const groupedProfiles = safeObject(serverBinding.effectiveRoleProfiles);
2526
2691
  Object.keys(groupedProfiles).forEach((role) => {
@@ -352,14 +352,15 @@ export async function runSelftestBotCommands(push, deps) {
352
352
  const addGlobals = readTelegramGlobals();
353
353
  push(
354
354
  "bot_add_guided_creates_named_telegram_entry",
355
- String(addState.TELEGRAM_BOT_NAME || "").toLowerCase().includes("monitorselftestbot")
355
+ String(addState.TELEGRAM_BOT_SERVER_NAME || "") === "MonitorSelftestBot"
356
+ && String(addState.TELEGRAM_BOT_SERVER_ROLES || "") === "monitor"
356
357
  && String(addState.TELEGRAM_BOT_TOKEN || "") === "selftest-main-token"
357
358
  && String(addState.TELEGRAM_BOT_ROLE_PROFILE || "") === "monitor"
358
359
  && String(addState.TELEGRAM_BOT_AI_CLIENT || "") === "codex"
359
360
  && String(addState.TELEGRAM_BOT_AI_PERMISSION_MODE || "") === "read_only"
360
361
  && String(addState.TELEGRAM_BOT_AI_REASONING_EFFORT || "") === "low"
361
362
  && String(addGlobals.TELEGRAM_DEFAULT_BOT_KEY || "") === "monitorselftestbot",
362
- `default=${String(addGlobals.TELEGRAM_DEFAULT_BOT_KEY || "")} token=${String(addState.TELEGRAM_BOT_TOKEN || "")} role=${String(addState.TELEGRAM_BOT_ROLE_PROFILE || "")} client=${String(addState.TELEGRAM_BOT_AI_CLIENT || "")}`,
363
+ `default=${String(addGlobals.TELEGRAM_DEFAULT_BOT_KEY || "")} token=${String(addState.TELEGRAM_BOT_TOKEN || "")} server_name=${String(addState.TELEGRAM_BOT_SERVER_NAME || "")} roles=${String(addState.TELEGRAM_BOT_SERVER_ROLES || "")} role=${String(addState.TELEGRAM_BOT_ROLE_PROFILE || "")} client=${String(addState.TELEGRAM_BOT_AI_CLIENT || "")}`,
363
364
  );
364
365
 
365
366
  const groupedMock = await createMockServer({
@@ -401,12 +402,13 @@ export async function runSelftestBotCommands(push, deps) {
401
402
  const groupedState = readTelegramBotEntry("ryoai_bot");
402
403
  push(
403
404
  "bot_add_guided_autoresolves_server_bot_group_without_role_prompt",
404
- String(groupedState.TELEGRAM_BOT_NAME || "").toLowerCase() === "ryoai_bot"
405
- && String(groupedState.TELEGRAM_BOT_SERVER_BOT_ID || "") === ""
405
+ String(groupedState.TELEGRAM_BOT_SERVER_BOT_ID || "") === ""
406
+ && String(groupedState.TELEGRAM_BOT_SERVER_NAME || "") === "RyoAI_bot"
407
+ && String(groupedState.TELEGRAM_BOT_SERVER_ROLES || "") === "monitor,review,worker,approval"
406
408
  && String(groupedState.TELEGRAM_BOT_ROLE_PROFILE || "") === ""
407
409
  && String(groupedState.TELEGRAM_BOT_AI_CLIENT || "") === ""
408
410
  && String(groupedState.TELEGRAM_BOT_AI_PERMISSION_MODE || "") === "",
409
- `name=${String(groupedState.TELEGRAM_BOT_NAME || "")} server_bot_id=${String(groupedState.TELEGRAM_BOT_SERVER_BOT_ID || "")} role=${String(groupedState.TELEGRAM_BOT_ROLE_PROFILE || "")}`,
411
+ `server_name=${String(groupedState.TELEGRAM_BOT_SERVER_NAME || "")} roles=${String(groupedState.TELEGRAM_BOT_SERVER_ROLES || "")} server_bot_id=${String(groupedState.TELEGRAM_BOT_SERVER_BOT_ID || "")} role=${String(groupedState.TELEGRAM_BOT_ROLE_PROFILE || "")}`,
410
412
  );
411
413
 
412
414
  const groupedEditResult = await runCLI({
@@ -758,11 +760,13 @@ export async function runSelftestBotCommands(push, deps) {
758
760
  push(
759
761
  "bot_add_accepts_ai_prefixed_option_aliases",
760
762
  String(aliasAddState.TELEGRAM_BOT_SERVER_BOT_ID || "") === mock.bots[0].id
763
+ && String(aliasAddState.TELEGRAM_BOT_SERVER_NAME || "") === "MonitorSelftestBot"
764
+ && String(aliasAddState.TELEGRAM_BOT_SERVER_ROLES || "") === "monitor"
761
765
  && String(aliasAddState.TELEGRAM_BOT_AI_CLIENT || "") === "codex"
762
766
  && String(aliasAddState.TELEGRAM_BOT_AI_MODEL || "") === "gpt-5.4"
763
767
  && String(aliasAddState.TELEGRAM_BOT_AI_PERMISSION_MODE || "") === "read_only"
764
768
  && String(aliasAddState.TELEGRAM_BOT_AI_REASONING_EFFORT || "") === "low",
765
- `client=${String(aliasAddState.TELEGRAM_BOT_AI_CLIENT || "")} model=${String(aliasAddState.TELEGRAM_BOT_AI_MODEL || "")}`,
769
+ `server_name=${String(aliasAddState.TELEGRAM_BOT_SERVER_NAME || "")} roles=${String(aliasAddState.TELEGRAM_BOT_SERVER_ROLES || "")} client=${String(aliasAddState.TELEGRAM_BOT_AI_CLIENT || "")} model=${String(aliasAddState.TELEGRAM_BOT_AI_MODEL || "")}`,
766
770
  );
767
771
 
768
772
  const guidedRemoveBeforeList = await runCLI({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.100",
3
+ "version": "0.2.102",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [