metheus-governance-mcp-cli 0.2.73 → 0.2.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -247,8 +247,8 @@ Behavior:
247
247
  - `AI_PERMISSION_MODE`
248
248
  - `AI_REASONING_EFFORT`
249
249
  - Slack and KakaoTalk currently use a single local token entry per provider in this command flow.
250
- - `bot verify` checks the configured local token, 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.
250
+ - `bot verify` checks the configured local token, cross-checks the server bot binding, prints the effective runtime role profile summary that the runner will use, and shows which local runner routes currently point at this bot entry.
251
+ - `bot show` prints one local bot entry in detail, including grouped role summaries when one server bot name expands to multiple roles and any linked local runner routes.
252
252
  - `bot global` edits Telegram-wide local settings such as API base URL, allowed updates, and default bot key.
253
253
  - `bot set-default` updates `TELEGRAM_DEFAULT_BOT_KEY`.
254
254
  - `bot migrate` moves legacy `TELEGRAM_BOT_TOKEN` into a named Telegram bot entry.
@@ -108,6 +108,70 @@ function maskSecret(rawValue) {
108
108
  return `${text.slice(0, 4)}...${text.slice(-4)}`;
109
109
  }
110
110
 
111
+ function supportsANSIColors() {
112
+ if (process.env.NO_COLOR) return false;
113
+ if (!process.stdout?.isTTY) return false;
114
+ const term = String(process.env.TERM || "").trim().toLowerCase();
115
+ return term !== "dumb";
116
+ }
117
+
118
+ function colorText(text, colorCode) {
119
+ const value = String(text || "");
120
+ if (!value || !supportsANSIColors()) return value;
121
+ return `\u001b[${colorCode}m${value}\u001b[0m`;
122
+ }
123
+
124
+ function padRight(text, width) {
125
+ const value = String(text || "");
126
+ if (value.length >= width) return value.slice(0, width);
127
+ return `${value}${" ".repeat(width - value.length)}`;
128
+ }
129
+
130
+ function wrapAsciiLine(text, width) {
131
+ const value = String(text || "");
132
+ if (!value) return [""];
133
+ const words = value.split(/\s+/).filter(Boolean);
134
+ if (!words.length) return [""];
135
+ const lines = [];
136
+ let current = "";
137
+ words.forEach((word) => {
138
+ const candidate = current ? `${current} ${word}` : word;
139
+ if (candidate.length <= width) {
140
+ current = candidate;
141
+ return;
142
+ }
143
+ if (current) {
144
+ lines.push(current);
145
+ current = word;
146
+ return;
147
+ }
148
+ let remaining = word;
149
+ while (remaining.length > width) {
150
+ lines.push(remaining.slice(0, width));
151
+ remaining = remaining.slice(width);
152
+ }
153
+ current = remaining;
154
+ });
155
+ if (current) lines.push(current);
156
+ return lines.length ? lines : [""];
157
+ }
158
+
159
+ function formatAsciiBlock(title, lines = [], { accentColor = "36" } = {}) {
160
+ const contentWidth = 62;
161
+ const blockLines = [
162
+ title ? String(title || "").trim() : "",
163
+ ...ensureArray(lines).flatMap((line) => wrapAsciiLine(line, contentWidth)),
164
+ ].filter((line, index, all) => !(index > 0 && !line && !all[index - 1]));
165
+ const border = `+${"-".repeat(contentWidth + 2)}+`;
166
+ const formatted = [colorText(border, accentColor)];
167
+ blockLines.forEach((line, index) => {
168
+ const rendered = `| ${padRight(line, contentWidth)} |`;
169
+ formatted.push(index === 0 ? colorText(rendered, accentColor) : rendered);
170
+ });
171
+ formatted.push(colorText(border, accentColor));
172
+ return `${formatted.join("\n")}\n`;
173
+ }
174
+
111
175
  function formatEnvValue(rawValue) {
112
176
  const text = String(rawValue ?? "");
113
177
  if (!text) return "";
@@ -125,6 +189,13 @@ function providerTokenKey(provider, deps) {
125
189
  return safeObject(requireDependency(deps, "providerEnvConfig")(provider)).tokenKey || "";
126
190
  }
127
191
 
192
+ function shouldRenderPromptChrome(flags) {
193
+ const parsedFlags = safeObject(flags);
194
+ if (boolFromRaw(parsedFlags["non-interactive"] ?? parsedFlags.yes, false)) return false;
195
+ if (boolFromRaw(parsedFlags.json, false)) return false;
196
+ return true;
197
+ }
198
+
128
199
  function printBotUsage(deps) {
129
200
  const cliName = String(deps?.cliName || "metheus-governance-mcp-cli").trim() || "metheus-governance-mcp-cli";
130
201
  process.stdout.write(
@@ -156,6 +227,24 @@ function printBotUsage(deps) {
156
227
  }
157
228
 
158
229
  function createPrompter() {
230
+ let flowTitle = "";
231
+ let flowSubtitle = "";
232
+ let stepIndex = 0;
233
+ function write(text) {
234
+ process.stdout.write(String(text || ""));
235
+ }
236
+ function printFlowBanner() {
237
+ if (!flowTitle) return;
238
+ const lines = flowSubtitle ? [flowSubtitle] : [];
239
+ write(`\n${formatAsciiBlock(flowTitle, lines, { accentColor: "35" })}`);
240
+ }
241
+ function beginPrompt(title, lines = []) {
242
+ stepIndex += 1;
243
+ const bannerTitle = flowTitle
244
+ ? `${flowTitle} | STEP ${stepIndex}`
245
+ : `STEP ${stepIndex}`;
246
+ write(`\n${formatAsciiBlock(bannerTitle, [title, ...ensureArray(lines)], { accentColor: "36" })}`);
247
+ }
159
248
  const scriptedPromptAnswersRaw = String(process.env.METHEUS_SCRIPTED_PROMPT_ANSWERS || "").trim();
160
249
  if (scriptedPromptAnswersRaw) {
161
250
  let scriptedAnswers = [];
@@ -167,17 +256,31 @@ function createPrompter() {
167
256
  }
168
257
  let answerIndex = 0;
169
258
  return {
259
+ setFlow(title, subtitle = "") {
260
+ flowTitle = String(title || "").trim();
261
+ flowSubtitle = String(subtitle || "").trim();
262
+ stepIndex = 0;
263
+ printFlowBanner();
264
+ },
265
+ beginPrompt,
170
266
  ask(promptText) {
171
- process.stdout.write(promptText);
267
+ write(promptText);
172
268
  const answer = answerIndex < scriptedAnswers.length ? scriptedAnswers[answerIndex] : "";
173
269
  answerIndex += 1;
174
- process.stdout.write(`${answer}\n`);
270
+ write(`${answer}\n`);
175
271
  return Promise.resolve(String(answer || ""));
176
272
  },
177
273
  close() {},
178
274
  };
179
275
  }
180
276
  return {
277
+ setFlow(title, subtitle = "") {
278
+ flowTitle = String(title || "").trim();
279
+ flowSubtitle = String(subtitle || "").trim();
280
+ stepIndex = 0;
281
+ printFlowBanner();
282
+ },
283
+ beginPrompt,
181
284
  ask(promptText) {
182
285
  return new Promise((resolve) => {
183
286
  const rl = readline.createInterface({
@@ -195,8 +298,13 @@ function createPrompter() {
195
298
  }
196
299
 
197
300
  async function promptLine(ui, promptText, defaultValue = "") {
301
+ ui.beginPrompt(promptText, [
302
+ String(defaultValue || "").trim()
303
+ ? `Press Enter to keep the current value: ${defaultValue}`
304
+ : "Type a value and press Enter.",
305
+ ]);
198
306
  const suffix = String(defaultValue || "").trim() ? ` [${defaultValue}]` : "";
199
- const answer = await ui.ask(`${promptText}${suffix}: `);
307
+ const answer = await ui.ask(`${colorText(">", "32")} ${promptText}${suffix}: `);
200
308
  const text = String(answer || "").trim();
201
309
  return text || String(defaultValue || "").trim();
202
310
  }
@@ -205,18 +313,19 @@ async function promptRequiredLine(ui, promptText, defaultValue = "") {
205
313
  while (true) {
206
314
  const answer = await promptLine(ui, promptText, defaultValue);
207
315
  if (answer) return answer;
208
- process.stdout.write("Value is required.\n");
316
+ process.stdout.write(`${colorText("Value is required.\n", "31")}`);
209
317
  }
210
318
  }
211
319
 
212
320
  async function promptYesNo(ui, promptText, defaultValue = true) {
321
+ ui.beginPrompt(promptText, ["Choose y or n, then press Enter."]);
213
322
  const hint = defaultValue ? "Y/n" : "y/N";
214
323
  while (true) {
215
- const answer = String(await ui.ask(`${promptText} [${hint}]: `) || "").trim().toLowerCase();
324
+ const answer = String(await ui.ask(`${colorText(">", "32")} ${promptText} [${hint}]: `) || "").trim().toLowerCase();
216
325
  if (!answer) return defaultValue;
217
326
  if (["y", "yes", "1", "true"].includes(answer)) return true;
218
327
  if (["n", "no", "0", "false"].includes(answer)) return false;
219
- process.stdout.write("Choose y or n.\n");
328
+ process.stdout.write(`${colorText("Choose y or n.\n", "31")}`);
220
329
  }
221
330
  }
222
331
 
@@ -249,16 +358,20 @@ async function promptChoice(ui, title, options, { defaultIndex = 0, allowCancel
249
358
  if (!list.length) {
250
359
  throw new Error(`no options available for ${title}`);
251
360
  }
361
+ const guidance = [
362
+ "Choose one option by number and press Enter.",
363
+ allowCancel ? "Enter 0 to cancel." : "",
364
+ ].filter(Boolean);
365
+ ui.beginPrompt(title, guidance);
252
366
  while (true) {
253
- process.stdout.write(`${title}\n`);
254
367
  list.forEach((option, index) => {
255
- process.stdout.write(` ${index + 1}. ${formatChoiceLabel(option)}\n`);
368
+ process.stdout.write(` ${colorText(`[${index + 1}]`, "33")} ${formatChoiceLabel(option)}\n`);
256
369
  });
257
370
  if (allowCancel) {
258
- process.stdout.write(" 0. Cancel\n");
371
+ process.stdout.write(` ${colorText("[0]", "33")} Cancel\n`);
259
372
  }
260
373
  const defaultLabel = defaultIndex >= 0 && defaultIndex < list.length ? String(defaultIndex + 1) : "";
261
- const answer = String(await ui.ask(`> ${defaultLabel ? `[${defaultLabel}] ` : ""}`) || "").trim();
374
+ const answer = String(await ui.ask(`${colorText(">", "32")} ${defaultLabel ? `[${defaultLabel}] ` : ""}`) || "").trim();
262
375
  if (!answer && defaultLabel) {
263
376
  return list[defaultIndex];
264
377
  }
@@ -267,7 +380,7 @@ async function promptChoice(ui, title, options, { defaultIndex = 0, allowCancel
267
380
  if (Number.isFinite(numeric) && numeric >= 1 && numeric <= list.length) {
268
381
  return list[numeric - 1];
269
382
  }
270
- process.stdout.write("Select a valid number.\n");
383
+ process.stdout.write(`${colorText("Select a valid number.\n", "31")}`);
271
384
  }
272
385
  }
273
386
 
@@ -840,6 +953,7 @@ function buildBotShowPayload(provider, state, entry, deps, extras = {}) {
840
953
  reasoningEffort: selectedEntry.reasoningEffort,
841
954
  } : null,
842
955
  serverBinding: safeObject(extras.serverBinding),
956
+ routeLinks: safeObject(extras.routeLinks),
843
957
  ...safeObject(extras),
844
958
  };
845
959
  }
@@ -857,6 +971,60 @@ function buildBotShowPayload(provider, state, entry, deps, extras = {}) {
857
971
  };
858
972
  }
859
973
 
974
+ function describeRouteRuntimeProfile(routeRole, serverBinding) {
975
+ const binding = safeObject(serverBinding);
976
+ if (binding.mode === "group") {
977
+ return safeObject(safeObject(binding.effectiveRoleProfiles)[String(routeRole || "").trim()]);
978
+ }
979
+ return safeObject(binding.effectiveRoleProfile);
980
+ }
981
+
982
+ function summarizeTelegramRouteLinks(parsedEnv, entry, serverBinding, deps) {
983
+ const selectedEntry = safeObject(entry);
984
+ const binding = safeObject(serverBinding);
985
+ const config = safeObject(requireDependency(deps, "loadBotRunnerConfig")({ persistIfNeeded: true }));
986
+ const routes = ensureArray(config.routes);
987
+ const entryKey = String(selectedEntry.key || "").trim();
988
+ const defaultBotKey = String(safeObject(parsedEnv).TELEGRAM_DEFAULT_BOT_KEY || "").trim();
989
+ const serverBotID = String(binding.serverBotID || selectedEntry.serverBotID || "").trim();
990
+ const normalizedNames = new Set(
991
+ [
992
+ normalizeServerBotIdentityText(selectedEntry.username),
993
+ normalizeServerBotIdentityText(binding.name),
994
+ normalizeServerBotIdentityText(entryKey),
995
+ ].filter(Boolean),
996
+ );
997
+ const linkedRoutes = [];
998
+ routes.forEach((rawRoute, index) => {
999
+ const route = safeObject(rawRoute);
1000
+ if (route.enabled === false) return;
1001
+ if (String(route.provider || "").trim().toLowerCase() !== "telegram") return;
1002
+ const routeName = String(route.name || route.route_name || `telegram-route-${index + 1}`).trim();
1003
+ const routeBotID = String(route.bot_id || route.botID || "").trim();
1004
+ const routeBotName = normalizeServerBotIdentityText(route.bot_name || route.botName || "");
1005
+ const routeRole = String(route.role_profile || route.roleProfile || route.role || "").trim();
1006
+ let matchedBy = "";
1007
+ if (serverBotID && routeBotID === serverBotID) {
1008
+ matchedBy = "bot_id";
1009
+ } else if (routeBotName && normalizedNames.has(routeBotName)) {
1010
+ matchedBy = "bot_name";
1011
+ } else if (!routeBotID && !routeBotName && entryKey && defaultBotKey && entryKey === defaultBotKey) {
1012
+ matchedBy = "default_bot";
1013
+ }
1014
+ if (!matchedBy) return;
1015
+ linkedRoutes.push({
1016
+ routeName,
1017
+ matchedBy,
1018
+ routeRole,
1019
+ runtimeRoleProfile: describeRouteRuntimeProfile(routeRole, binding),
1020
+ });
1021
+ });
1022
+ return {
1023
+ total: linkedRoutes.length,
1024
+ routes: linkedRoutes,
1025
+ };
1026
+ }
1027
+
860
1028
  function printBotList(provider, state, deps) {
861
1029
  process.stdout.write(`${providerLabel(provider, deps)}\n`);
862
1030
  process.stdout.write(` file: ${state.filePath}\n`);
@@ -895,6 +1063,7 @@ function printBotShow(provider, state, entry, deps, extras = {}) {
895
1063
  if (provider === "telegram") {
896
1064
  const selectedEntry = entry || {};
897
1065
  const serverBinding = safeObject(extras.serverBinding);
1066
+ const routeLinks = safeObject(extras.routeLinks);
898
1067
  process.stdout.write(`${providerLabel(provider, deps)} bot\n`);
899
1068
  process.stdout.write(` file: ${state.filePath}\n`);
900
1069
  process.stdout.write(` name: ${telegramEntryDisplayName(selectedEntry)}\n`);
@@ -924,6 +1093,16 @@ function printBotShow(provider, state, entry, deps, extras = {}) {
924
1093
  process.stdout.write(` runtime_role_profile: ${formatRoleProfileOutputLine(serverBinding.effectiveRoleProfile)}\n`);
925
1094
  }
926
1095
  }
1096
+ process.stdout.write(` route_links: ${intFromRaw(routeLinks.total, 0)}\n`);
1097
+ ensureArray(routeLinks.routes).forEach((route) => {
1098
+ const routeRuntime = safeObject(route.runtimeRoleProfile);
1099
+ const routeDetail = [
1100
+ `matched_by=${String(route.matchedBy || "-")}`,
1101
+ route.routeRole ? `route_role=${String(route.routeRole || "").trim()}` : "",
1102
+ Object.keys(routeRuntime).length ? `runtime=${formatRoleProfileOutputLine(routeRuntime)}` : "",
1103
+ ].filter(Boolean).join(" ");
1104
+ process.stdout.write(` linked_route[${String(route.routeName || "-")}]: ${routeDetail || "-"}\n`);
1105
+ });
927
1106
  return;
928
1107
  }
929
1108
  const tokenKey = providerTokenKey(provider, deps);
@@ -2047,11 +2226,17 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
2047
2226
  );
2048
2227
  let overallOK = Boolean(result.ok);
2049
2228
  let serverBinding = null;
2229
+ let routeLinks = null;
2050
2230
  if (provider === "telegram") {
2051
2231
  serverBinding = await resolveTelegramServerBindingDetails(envConfig, flags, deps);
2052
2232
  if (serverBinding && !serverBinding.ok) {
2053
2233
  overallOK = false;
2054
2234
  }
2235
+ routeLinks = summarizeTelegramRouteLinks(state.parsed, {
2236
+ key: envConfig.botKey,
2237
+ serverBotID: envConfig.serverBotID,
2238
+ username: envConfig.botUsername,
2239
+ }, serverBinding, deps);
2055
2240
  }
2056
2241
  const interactiveJsonChoice = (
2057
2242
  !boolFromRaw(flags.json, false)
@@ -2084,6 +2269,7 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
2084
2269
  permissionMode: envConfig.permissionMode || "",
2085
2270
  reasoningEffort: envConfig.reasoningEffort || "",
2086
2271
  serverBinding,
2272
+ routeLinks,
2087
2273
  }, null, 2)}\n`,
2088
2274
  );
2089
2275
  } else {
@@ -2099,6 +2285,7 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
2099
2285
  provider === "telegram" ? `reasoning_effort: ${envConfig.reasoningEffort || "-"}` : "",
2100
2286
  `detail: ${result.detail || "-"}`,
2101
2287
  serverBinding ? `server_binding: ${serverBinding.ok ? "OK" : "FAIL"}${serverBinding.detail ? ` (${serverBinding.detail})` : ""}` : "",
2288
+ routeLinks ? `route_links: ${intFromRaw(routeLinks.total, 0)}` : "",
2102
2289
  ].filter(Boolean);
2103
2290
  if (provider === "telegram" && serverBinding) {
2104
2291
  lines.push(`server_binding_mode: ${serverBinding.mode || "-"}`);
@@ -2113,6 +2300,17 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
2113
2300
  lines.push(`runtime_role_profile: ${formatRoleProfileOutputLine(serverBinding.effectiveRoleProfile)}`);
2114
2301
  }
2115
2302
  }
2303
+ if (provider === "telegram" && routeLinks) {
2304
+ ensureArray(routeLinks.routes).forEach((route) => {
2305
+ const routeRuntime = safeObject(route.runtimeRoleProfile);
2306
+ const routeDetail = [
2307
+ `matched_by=${String(route.matchedBy || "-")}`,
2308
+ route.routeRole ? `route_role=${String(route.routeRole || "").trim()}` : "",
2309
+ Object.keys(routeRuntime).length ? `runtime=${formatRoleProfileOutputLine(routeRuntime)}` : "",
2310
+ ].filter(Boolean).join(" ");
2311
+ lines.push(`linked_route[${String(route.routeName || "-")}]: ${routeDetail || "-"}`);
2312
+ });
2313
+ }
2116
2314
  process.stdout.write(`${lines.join("\n")}\n`);
2117
2315
  }
2118
2316
  if (!overallOK) {
@@ -2169,6 +2367,9 @@ async function removeTokenOnlyProvider(ui, provider, flags, deps) {
2169
2367
  }
2170
2368
 
2171
2369
  async function runBotSetup(ui, flags, deps) {
2370
+ if (shouldRenderPromptChrome(flags)) {
2371
+ ui.setFlow("BOT SETUP", "Guided local bot management");
2372
+ }
2172
2373
  const provider = await selectProvider(ui, flags.provider, deps);
2173
2374
  const telegramActions = [
2174
2375
  { value: "list", label: "List bots" },
@@ -2281,12 +2482,13 @@ async function runBotShow(ui, flags, deps, explicitProvider = "") {
2281
2482
  flags,
2282
2483
  deps,
2283
2484
  );
2284
- const payload = buildBotShowPayload(provider, state, entry, deps, { serverBinding });
2485
+ const routeLinks = summarizeTelegramRouteLinks(state.parsed, entry, serverBinding, deps);
2486
+ const payload = buildBotShowPayload(provider, state, entry, deps, { serverBinding, routeLinks });
2285
2487
  if (boolFromRaw(flags.json, false)) {
2286
2488
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
2287
2489
  return;
2288
2490
  }
2289
- printBotShow(provider, state, entry, deps, { serverBinding });
2491
+ printBotShow(provider, state, entry, deps, { serverBinding, routeLinks });
2290
2492
  return;
2291
2493
  }
2292
2494
  const payload = buildBotShowPayload(provider, state, null, deps);
@@ -2298,6 +2500,9 @@ async function runBotShow(ui, flags, deps, explicitProvider = "") {
2298
2500
  }
2299
2501
 
2300
2502
  async function runBotAdd(ui, flags, deps) {
2503
+ if (shouldRenderPromptChrome(flags)) {
2504
+ ui.setFlow("BOT ADD", "Create a local bot entry from server bot identity and local token");
2505
+ }
2301
2506
  const provider = await selectProvider(ui, flags.provider, deps);
2302
2507
  if (provider === "telegram") {
2303
2508
  await addTelegramBot(ui, flags, deps);
@@ -2307,6 +2512,9 @@ async function runBotAdd(ui, flags, deps) {
2307
2512
  }
2308
2513
 
2309
2514
  async function runBotEdit(ui, flags, deps) {
2515
+ if (shouldRenderPromptChrome(flags)) {
2516
+ ui.setFlow("BOT EDIT", "Update an existing local bot entry");
2517
+ }
2310
2518
  const provider = await selectProvider(ui, flags.provider, deps);
2311
2519
  if (provider === "telegram") {
2312
2520
  await editTelegramBot(ui, flags, deps);
@@ -2316,6 +2524,9 @@ async function runBotEdit(ui, flags, deps) {
2316
2524
  }
2317
2525
 
2318
2526
  async function runBotRemove(ui, flags, deps) {
2527
+ if (shouldRenderPromptChrome(flags)) {
2528
+ ui.setFlow("BOT REMOVE", "Delete a local bot entry");
2529
+ }
2319
2530
  const provider = await selectProvider(ui, flags.provider, deps);
2320
2531
  if (provider === "telegram") {
2321
2532
  const nonInteractive = boolFromRaw(flags["non-interactive"] ?? flags.yes, false);
@@ -2341,11 +2552,17 @@ async function runBotRemove(ui, flags, deps) {
2341
2552
  }
2342
2553
 
2343
2554
  async function runBotVerify(ui, flags, deps) {
2555
+ if (shouldRenderPromptChrome(flags)) {
2556
+ ui.setFlow("BOT VERIFY", "Check local token, server binding, and linked routes");
2557
+ }
2344
2558
  const provider = await selectProvider(ui, flags.provider, deps);
2345
2559
  await verifyProviderEntry(ui, provider, flags, deps);
2346
2560
  }
2347
2561
 
2348
2562
  async function runBotSetDefault(ui, flags, deps) {
2563
+ if (shouldRenderPromptChrome(flags)) {
2564
+ ui.setFlow("BOT SET-DEFAULT", "Choose the default Telegram local entry");
2565
+ }
2349
2566
  const provider = await selectProvider(ui, flags.provider, deps);
2350
2567
  if (provider !== "telegram") {
2351
2568
  throw new Error("bot set-default currently supports only --provider telegram");
@@ -2375,6 +2592,9 @@ async function runBotSetDefault(ui, flags, deps) {
2375
2592
  }
2376
2593
 
2377
2594
  async function runBotMigrate(ui, flags, deps) {
2595
+ if (shouldRenderPromptChrome(flags)) {
2596
+ ui.setFlow("BOT MIGRATE", "Convert legacy Telegram token config into a named entry");
2597
+ }
2378
2598
  const provider = String(flags.provider || "").trim()
2379
2599
  ? requireDependency(deps, "normalizeBotProvider")(flags.provider)
2380
2600
  : "telegram";
@@ -2425,6 +2645,9 @@ async function runBotMigrate(ui, flags, deps) {
2425
2645
  }
2426
2646
 
2427
2647
  async function runBotGlobal(ui, flags, deps) {
2648
+ if (shouldRenderPromptChrome(flags)) {
2649
+ ui.setFlow("BOT GLOBAL", "Edit Telegram global local settings");
2650
+ }
2428
2651
  const provider = String(flags.provider || "").trim()
2429
2652
  ? requireDependency(deps, "normalizeBotProvider")(flags.provider)
2430
2653
  : "telegram";
@@ -84,6 +84,11 @@ function readTrailingJSON(rawText) {
84
84
  return readJSON(text.slice(start));
85
85
  }
86
86
 
87
+ function intFromRaw(rawValue, fallback = 0) {
88
+ const parsed = Number.parseInt(String(rawValue ?? "").trim(), 10);
89
+ return Number.isFinite(parsed) ? parsed : fallback;
90
+ }
91
+
87
92
  function createMockServer(options = {}) {
88
93
  const serverBots = ensureArray(options.serverBots).length
89
94
  ? ensureArray(options.serverBots)
@@ -434,6 +439,37 @@ export async function runSelftestBotCommands(push, deps) {
434
439
  env,
435
440
  });
436
441
  const showPayload = readJSON(showResult.stdout);
442
+ const runnerConfigPath = path.join(tempHome, ".metheus", "bot-runner.json");
443
+ const runnerConfig = readJSON(fs.readFileSync(runnerConfigPath, "utf8"));
444
+ runnerConfig.routes = [
445
+ {
446
+ name: "telegram-monitor",
447
+ enabled: true,
448
+ project_id: "03c586a2-006d-4051-83b4-f353a5813176",
449
+ provider: "telegram",
450
+ bot_id: mock.bots[0].id,
451
+ role_profile: "monitor",
452
+ },
453
+ {
454
+ name: "telegram-default",
455
+ enabled: true,
456
+ project_id: "03c586a2-006d-4051-83b4-f353a5813176",
457
+ provider: "telegram",
458
+ },
459
+ ];
460
+ fs.writeFileSync(runnerConfigPath, `${JSON.stringify(runnerConfig, null, 2)}\n`, "utf8");
461
+ const showWithRoutesResult = await runCLI({
462
+ cliPath,
463
+ args: [
464
+ "bot", "show",
465
+ "--provider", "telegram",
466
+ "--bot-key", "monitorselftestbot",
467
+ "--base-url", baseURL,
468
+ "--json", "true",
469
+ ],
470
+ env,
471
+ });
472
+ const showWithRoutesPayload = readJSON(showWithRoutesResult.stdout);
437
473
  push(
438
474
  "bot_show_returns_selected_telegram_entry",
439
475
  safeObject(showPayload.entry).key === "monitorselftestbot"
@@ -441,6 +477,15 @@ export async function runSelftestBotCommands(push, deps) {
441
477
  && safeObject(showPayload.serverBinding).mode === "single",
442
478
  `key=${String(safeObject(showPayload.entry).key || "")} client=${String(safeObject(showPayload.entry).client || "")} mode=${String(safeObject(showPayload.serverBinding).mode || "")}`,
443
479
  );
480
+ push(
481
+ "bot_show_reports_linked_runner_routes",
482
+ intFromRaw(safeObject(showWithRoutesPayload.routeLinks).total, 0) >= 1
483
+ && ensureArray(safeObject(showWithRoutesPayload.routeLinks).routes).some((route) => (
484
+ String(safeObject(route).routeName || "") === "telegram-monitor"
485
+ && String(safeObject(route).matchedBy || "") === "bot_id"
486
+ )),
487
+ `routes=${JSON.stringify(safeObject(showWithRoutesPayload.routeLinks))}`,
488
+ );
444
489
 
445
490
  await runCLI({
446
491
  cliPath,
@@ -584,6 +629,14 @@ export async function runSelftestBotCommands(push, deps) {
584
629
  && String(verifyPayload.client || "") === "claude",
585
630
  `verify=${String(verifyPayload.ok)} mode=${String(safeObject(verifyPayload.serverBinding).mode || "")} server=${String(safeObject(verifyPayload.serverBinding).detail || "")}`,
586
631
  );
632
+ push(
633
+ "bot_verify_reports_linked_runner_routes",
634
+ intFromRaw(safeObject(verifyPayload.routeLinks).total, 0) >= 1
635
+ && ensureArray(safeObject(verifyPayload.routeLinks).routes).some((route) => (
636
+ String(safeObject(route).routeName || "") === "telegram-monitor"
637
+ )),
638
+ `routes=${JSON.stringify(safeObject(verifyPayload.routeLinks))}`,
639
+ );
587
640
 
588
641
  await runCLI({
589
642
  cliPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.73",
3
+ "version": "0.2.75",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [