metheus-governance-mcp-cli 0.2.64 → 0.2.66

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
@@ -213,6 +213,7 @@ Direct commands:
213
213
  metheus-governance-mcp-cli bot list
214
214
  metheus-governance-mcp-cli bot show --provider telegram --bot-key main
215
215
  metheus-governance-mcp-cli bot add --provider telegram
216
+ metheus-governance-mcp-cli bot edit
216
217
  metheus-governance-mcp-cli bot edit --provider telegram
217
218
  metheus-governance-mcp-cli bot remove --provider telegram
218
219
  metheus-governance-mcp-cli bot set-default --provider telegram --bot-key main
@@ -224,6 +225,11 @@ metheus-governance-mcp-cli bot verify --provider telegram --bot-key main
224
225
  Behavior:
225
226
 
226
227
  - `bot setup` asks for `Telegram / Slack / KakaoTalk` first, then prompts with numbered actions.
228
+ - `bot add` without flags starts a guided question flow: provider -> server bot -> local bot key -> token -> verify -> role/AI binding -> default bot choice.
229
+ - `bot edit` without flags starts a guided numbered flow: provider -> bot entry -> guided edit -> step-by-step field choices.
230
+ - `bot set-default` without flags starts a guided numbered flow: provider -> bot entry -> confirm default change.
231
+ - `bot verify` without flags starts a guided numbered flow: provider -> bot entry -> output format.
232
+ - `bot remove` without flags starts a guided numbered flow: provider -> bot entry -> confirm removal.
227
233
  - Telegram supports named local bot entries with:
228
234
  - `SERVER_BOT_ID`
229
235
  - `USERNAME`
@@ -244,10 +250,10 @@ Non-interactive examples:
244
250
 
245
251
  ```bash
246
252
  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
247
- metheus-governance-mcp-cli bot add --provider telegram --non-interactive true --bot-key main --bot-id <server_bot_uuid> --token <telegram_bot_token> --role-profile monitor --client codex --permission-mode read_only --reasoning-effort low --default true
248
- metheus-governance-mcp-cli bot edit --provider telegram --bot-key main --non-interactive true --client claude --model claude-3.7-sonnet --permission-mode workspace_write --reasoning-effort medium
253
+ metheus-governance-mcp-cli bot add --provider telegram --non-interactive true --bot-key main --server-bot-id <server_bot_uuid> --username <telegram_username> --token <telegram_bot_token> --role-profile monitor --ai-client codex --ai-model gpt-5-codex --ai-permission-mode read_only --ai-reasoning-effort low --default true
254
+ 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
249
255
  metheus-governance-mcp-cli bot set-default --provider telegram --bot-key main --non-interactive true
250
- metheus-governance-mcp-cli bot migrate --provider telegram --bot-key main --bot-id <server_bot_uuid> --bot-name <telegram_username> --role-profile monitor --client codex --permission-mode read_only --reasoning-effort low --non-interactive true
256
+ 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
251
257
  metheus-governance-mcp-cli bot remove --provider telegram --bot-key main --non-interactive true
252
258
  metheus-governance-mcp-cli bot verify --provider telegram --bot-key main --json true
253
259
  ```
@@ -43,6 +43,35 @@ function firstNonEmptyString(values) {
43
43
  return "";
44
44
  }
45
45
 
46
+ function hasOwnFlag(flags, key) {
47
+ return Object.prototype.hasOwnProperty.call(safeObject(flags), key);
48
+ }
49
+
50
+ function getServerBotIDFlag(flags) {
51
+ const parsedFlags = safeObject(flags);
52
+ return firstNonEmptyString([parsedFlags["server-bot-id"], parsedFlags["bot-id"]]);
53
+ }
54
+
55
+ function getAIClientFlag(flags) {
56
+ const parsedFlags = safeObject(flags);
57
+ return firstNonEmptyString([parsedFlags["ai-client"], parsedFlags.client]);
58
+ }
59
+
60
+ function getAIModelFlag(flags) {
61
+ const parsedFlags = safeObject(flags);
62
+ return firstNonEmptyString([parsedFlags["ai-model"], parsedFlags.model]);
63
+ }
64
+
65
+ function getAIPermissionModeFlag(flags) {
66
+ const parsedFlags = safeObject(flags);
67
+ return firstNonEmptyString([parsedFlags["ai-permission-mode"], parsedFlags["permission-mode"]]);
68
+ }
69
+
70
+ function getAIReasoningEffortFlag(flags) {
71
+ const parsedFlags = safeObject(flags);
72
+ return firstNonEmptyString([parsedFlags["ai-reasoning-effort"], parsedFlags["reasoning-effort"]]);
73
+ }
74
+
46
75
  function maskSecret(rawValue) {
47
76
  const text = String(rawValue || "").trim();
48
77
  if (!text) return "";
@@ -76,8 +105,8 @@ function printBotUsage(deps) {
76
105
  ` ${cliName} bot setup`,
77
106
  ` ${cliName} bot list [--provider <telegram|slack|kakaotalk>] [--json <true|false>]`,
78
107
  ` ${cliName} bot show [--provider <telegram|slack|kakaotalk>] [--bot-key <key>] [--bot-id <uuid>] [--bot-name <name>] [--json <true|false>]`,
79
- ` ${cliName} bot add [--provider <telegram|slack|kakaotalk>] [--non-interactive <true|false>] [--yes <true|false>]`,
80
- ` ${cliName} bot edit [--provider <telegram|slack|kakaotalk>] [--non-interactive <true|false>] [--bot-key <key>]`,
108
+ ` ${cliName} bot add [--provider <telegram|slack|kakaotalk>] [--base-url <url>] [--timeout-seconds <n>] [--non-interactive <true|false>] [--bot-key <key>] [--server-bot-id <uuid>] [--username <name>] [--token <token>] [--role-profile <name>] [--ai-client <name>] [--ai-model <name>] [--ai-permission-mode <mode>] [--ai-reasoning-effort <level>] [--default <true|false>] [--verify <true|false>]`,
109
+ ` ${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>]`,
81
110
  ` ${cliName} bot remove [--provider <telegram|slack|kakaotalk>] [--non-interactive <true|false>] [--bot-key <key>]`,
82
111
  ` ${cliName} bot set-default --provider telegram [--bot-key <key>] [--non-interactive <true|false>]`,
83
112
  ` ${cliName} bot migrate --provider telegram [--bot-key <key>] [--bot-id <uuid>] [--bot-name <name>] [--keep-legacy-token <true|false>] [--non-interactive <true|false>]`,
@@ -89,6 +118,27 @@ function printBotUsage(deps) {
89
118
  }
90
119
 
91
120
  function createPrompter() {
121
+ const scriptedPromptAnswersRaw = String(process.env.METHEUS_SCRIPTED_PROMPT_ANSWERS || "").trim();
122
+ if (scriptedPromptAnswersRaw) {
123
+ let scriptedAnswers = [];
124
+ try {
125
+ const parsed = JSON.parse(scriptedPromptAnswersRaw);
126
+ scriptedAnswers = ensureArray(parsed).map((value) => String(value ?? ""));
127
+ } catch {
128
+ scriptedAnswers = scriptedPromptAnswersRaw.split(/\r?\n/).map((value) => String(value ?? ""));
129
+ }
130
+ let answerIndex = 0;
131
+ return {
132
+ ask(promptText) {
133
+ process.stdout.write(promptText);
134
+ const answer = answerIndex < scriptedAnswers.length ? scriptedAnswers[answerIndex] : "";
135
+ answerIndex += 1;
136
+ process.stdout.write(`${answer}\n`);
137
+ return Promise.resolve(String(answer || ""));
138
+ },
139
+ close() {},
140
+ };
141
+ }
92
142
  return {
93
143
  ask(promptText) {
94
144
  return new Promise((resolve) => {
@@ -132,6 +182,26 @@ async function promptYesNo(ui, promptText, defaultValue = true) {
132
182
  }
133
183
  }
134
184
 
185
+ async function promptConfirmChoice(
186
+ ui,
187
+ title,
188
+ {
189
+ confirmLabel = "Confirm",
190
+ confirmDescription = "",
191
+ cancelLabel = "Cancel",
192
+ cancelDescription = "",
193
+ defaultValue = "confirm",
194
+ } = {},
195
+ ) {
196
+ const options = [
197
+ { value: "confirm", label: confirmLabel, description: confirmDescription },
198
+ { value: "cancel", label: cancelLabel, description: cancelDescription },
199
+ ];
200
+ const defaultIndex = defaultValue === "cancel" ? 1 : 0;
201
+ const selected = await promptChoice(ui, title, options, { defaultIndex });
202
+ return selected?.value === "confirm";
203
+ }
204
+
135
205
  function formatChoiceLabel(option) {
136
206
  return `${String(option.label || option.value || "").trim()}${option.description ? ` - ${option.description}` : ""}`;
137
207
  }
@@ -163,6 +233,19 @@ async function promptChoice(ui, title, options, { defaultIndex = 0, allowCancel
163
233
  }
164
234
  }
165
235
 
236
+ async function promptKeepChangeClear(ui, title, { allowClear = true, defaultValue = "keep" } = {}) {
237
+ const options = [
238
+ { value: "keep", label: "Keep current value" },
239
+ { value: "change", label: "Change value" },
240
+ ];
241
+ if (allowClear) {
242
+ options.push({ value: "clear", label: "Clear value" });
243
+ }
244
+ const defaultIndex = Math.max(0, options.findIndex((option) => option.value === defaultValue));
245
+ const selected = await promptChoice(ui, title, options, { defaultIndex });
246
+ return selected?.value || "keep";
247
+ }
248
+
166
249
  function loadProviderEnvState(provider, deps) {
167
250
  const ensureTemplate = requireDependency(deps, "ensureProviderEnvTemplate");
168
251
  const filePathResolver = requireDependency(deps, "providerEnvFilePath");
@@ -399,6 +482,175 @@ function findTelegramEntryByFlags(parsedEnv, flags, deps) {
399
482
  return null;
400
483
  }
401
484
 
485
+ function saveTelegramBotEdit(parsed, selected, current, deps) {
486
+ let nextParsed = parsed;
487
+ if (current.key !== selected.key) {
488
+ nextParsed = removeTelegramEntry(nextParsed, selected.key);
489
+ }
490
+ nextParsed = upsertTelegramEntry(nextParsed, current);
491
+ if (String(parsed.TELEGRAM_DEFAULT_BOT_KEY || "").trim() === selected.key) {
492
+ nextParsed.TELEGRAM_DEFAULT_BOT_KEY = current.key;
493
+ }
494
+ const filePath = writeProviderEnvState("telegram", nextParsed, deps);
495
+ process.stdout.write(`Saved Telegram bot entry "${current.key}" to ${filePath}\n`);
496
+ return filePath;
497
+ }
498
+
499
+ async function editTelegramBotGuided(ui, parsed, selected, current, flags, deps) {
500
+ const bindingAction = await promptKeepChangeClear(ui, "Server bot binding", {
501
+ allowClear: true,
502
+ defaultValue: current.serverBotID ? "keep" : "change",
503
+ });
504
+ if (bindingAction === "change") {
505
+ const serverBot = await chooseServerBot(
506
+ ui,
507
+ "telegram",
508
+ flags["base-url"] || deps.defaultSiteURL,
509
+ intFromRaw(flags["timeout-seconds"], 15) || 15,
510
+ deps,
511
+ );
512
+ current.serverBotID = serverBot.botID;
513
+ if (!current.roleProfile && serverBot.role) {
514
+ current.roleProfile = serverBot.role;
515
+ }
516
+ } else if (bindingAction === "clear") {
517
+ current.serverBotID = "";
518
+ }
519
+
520
+ const usernameAction = await promptKeepChangeClear(ui, "Telegram username", {
521
+ allowClear: true,
522
+ defaultValue: current.username ? "keep" : "change",
523
+ });
524
+ if (usernameAction === "change") {
525
+ current.username = requireDependency(deps, "normalizeTelegramBotUsername")(
526
+ await promptRequiredLine(ui, "Telegram username (without @)", current.username),
527
+ "",
528
+ );
529
+ } else if (usernameAction === "clear") {
530
+ current.username = "";
531
+ }
532
+
533
+ const tokenAction = await promptKeepChangeClear(ui, "Telegram token", {
534
+ allowClear: false,
535
+ defaultValue: "keep",
536
+ });
537
+ if (tokenAction === "change") {
538
+ current.token = await promptRequiredLine(ui, "Telegram bot token", current.token);
539
+ if (await promptYesNo(ui, "Verify updated token now?", true)) {
540
+ const verifyResult = await verifyTelegramTokenCandidate(
541
+ "telegram",
542
+ current.token,
543
+ String(parsed.TELEGRAM_API_BASE_URL || "").trim(),
544
+ intFromRaw(flags["timeout-seconds"], 15) || 15,
545
+ deps,
546
+ );
547
+ process.stdout.write(`Verify: ${verifyResult.ok ? "OK" : "FAIL"}${verifyResult.detail ? ` - ${verifyResult.detail}` : ""}\n`);
548
+ const maybeUsername = extractVerifiedTelegramUsername(verifyResult.detail);
549
+ if (verifyResult.ok && maybeUsername && !current.username) {
550
+ current.username = maybeUsername;
551
+ }
552
+ }
553
+ }
554
+
555
+ const roleAction = await promptKeepChangeClear(ui, "Role profile", {
556
+ allowClear: true,
557
+ defaultValue: current.roleProfile ? "keep" : "change",
558
+ });
559
+ if (roleAction === "change") {
560
+ current.roleProfile = requireDependency(deps, "normalizeRunnerRoleProfileName")(
561
+ await promptTelegramRoleProfile(ui, deps, current.roleProfile),
562
+ );
563
+ } else if (roleAction === "clear") {
564
+ current.roleProfile = "";
565
+ }
566
+
567
+ const clientAction = await promptKeepChangeClear(ui, "AI client", {
568
+ allowClear: true,
569
+ defaultValue: current.client ? "keep" : "change",
570
+ });
571
+ if (clientAction === "change") {
572
+ current.client = requireDependency(deps, "normalizeLocalAIClientName")(
573
+ await promptAIClient(ui, deps, current.client),
574
+ "",
575
+ );
576
+ } else if (clientAction === "clear") {
577
+ current.client = "";
578
+ }
579
+
580
+ const modelAction = await promptKeepChangeClear(ui, "AI model", {
581
+ allowClear: true,
582
+ defaultValue: current.model ? "keep" : "change",
583
+ });
584
+ if (modelAction === "change") {
585
+ current.model = await promptLine(ui, "AI model", current.model);
586
+ } else if (modelAction === "clear") {
587
+ current.model = "";
588
+ }
589
+
590
+ const permissionAction = await promptKeepChangeClear(ui, "AI permission mode", {
591
+ allowClear: true,
592
+ defaultValue: current.permissionMode ? "keep" : "change",
593
+ });
594
+ if (permissionAction === "change") {
595
+ current.permissionMode = requireDependency(deps, "normalizeLocalAIPermissionMode")(
596
+ await promptPermissionMode(ui, current.permissionMode),
597
+ "",
598
+ );
599
+ } else if (permissionAction === "clear") {
600
+ current.permissionMode = "";
601
+ }
602
+
603
+ const reasoningAction = await promptKeepChangeClear(ui, "AI reasoning effort", {
604
+ allowClear: true,
605
+ defaultValue: current.reasoningEffort ? "keep" : "change",
606
+ });
607
+ if (reasoningAction === "change") {
608
+ current.reasoningEffort = requireDependency(deps, "normalizeLocalAIReasoningEffort")(
609
+ await promptReasoningEffort(ui, current.reasoningEffort),
610
+ "",
611
+ );
612
+ } else if (reasoningAction === "clear") {
613
+ current.reasoningEffort = "";
614
+ }
615
+
616
+ const botKeyAction = await promptKeepChangeClear(ui, "Local bot key", {
617
+ allowClear: false,
618
+ defaultValue: "keep",
619
+ });
620
+ if (botKeyAction === "change") {
621
+ const normalizeBotKey = requireDependency(deps, "normalizeTelegramBotEnvKey");
622
+ let nextKey = normalizeBotKey(await promptRequiredLine(ui, "Local bot key", current.key), current.key);
623
+ const existing = new Set(telegramEntriesForDisplay(parsed, deps).map((entry) => entry.key).filter((key) => key !== current.key));
624
+ while (existing.has(nextKey)) {
625
+ process.stdout.write(`Telegram bot key "${nextKey}" already exists.\n`);
626
+ nextKey = normalizeBotKey(await promptRequiredLine(ui, "Local bot key", `${nextKey}_2`), `${nextKey}_2`);
627
+ }
628
+ current.key = nextKey;
629
+ }
630
+
631
+ const defaultChoice = await promptChoice(
632
+ ui,
633
+ "Default Telegram bot setting",
634
+ [
635
+ { value: "keep", label: "Keep current default setting", description: String(parsed.TELEGRAM_DEFAULT_BOT_KEY || "").trim() === selected.key ? "currently default" : "not default" },
636
+ { value: "set", label: "Set this bot as default" },
637
+ { value: "unset", label: "Do not make this bot default" },
638
+ ],
639
+ { defaultIndex: 0 },
640
+ );
641
+ if (defaultChoice?.value === "set") {
642
+ parsed.TELEGRAM_DEFAULT_BOT_KEY = current.key;
643
+ } else if (defaultChoice?.value === "unset" && String(parsed.TELEGRAM_DEFAULT_BOT_KEY || "").trim() === selected.key) {
644
+ parsed.TELEGRAM_DEFAULT_BOT_KEY = "";
645
+ }
646
+
647
+ if (!await promptYesNo(ui, `Save changes for "${current.key}" now?`, true)) {
648
+ process.stdout.write("Cancelled.\n");
649
+ return;
650
+ }
651
+ saveTelegramBotEdit(parsed, selected, current, deps);
652
+ }
653
+
402
654
  async function selectProvider(ui, initialProvider, deps) {
403
655
  const normalizeProvider = requireDependency(deps, "normalizeBotProvider");
404
656
  const provider = String(initialProvider || "").trim();
@@ -708,7 +960,7 @@ async function addTelegramBot(ui, flags, deps) {
708
960
  const nonInteractive = boolFromRaw(flags["non-interactive"] ?? flags.yes, false);
709
961
  const serverBot = nonInteractive
710
962
  ? {
711
- botID: String(flags["server-bot-id"] || flags["bot-id"] || "").trim(),
963
+ botID: getServerBotIDFlag(flags),
712
964
  role: String(flags.role || "").trim(),
713
965
  name: "",
714
966
  }
@@ -783,27 +1035,27 @@ async function addTelegramBot(ui, flags, deps) {
783
1035
  );
784
1036
  const client = requireDependency(deps, "normalizeLocalAIClientName")(
785
1037
  nonInteractive
786
- ? String(flags.client || "").trim()
787
- : await promptAIClient(ui, deps, flags.client || ""),
1038
+ ? getAIClientFlag(flags)
1039
+ : await promptAIClient(ui, deps, getAIClientFlag(flags)),
788
1040
  "",
789
1041
  );
790
- const model = nonInteractive ? String(flags.model || "").trim() : await promptLine(ui, "AI model", String(flags.model || "").trim());
1042
+ const model = nonInteractive ? getAIModelFlag(flags) : await promptLine(ui, "AI model", getAIModelFlag(flags));
791
1043
  const permissionMode = requireDependency(deps, "normalizeLocalAIPermissionMode")(
792
1044
  nonInteractive
793
- ? String(flags["permission-mode"] || "").trim()
794
- : await promptPermissionMode(ui, String(flags["permission-mode"] || "").trim()),
1045
+ ? getAIPermissionModeFlag(flags)
1046
+ : await promptPermissionMode(ui, getAIPermissionModeFlag(flags)),
795
1047
  "",
796
1048
  );
797
1049
  const reasoningEffort = requireDependency(deps, "normalizeLocalAIReasoningEffort")(
798
1050
  nonInteractive
799
- ? String(flags["reasoning-effort"] || "").trim()
800
- : await promptReasoningEffort(ui, String(flags["reasoning-effort"] || "").trim()),
1051
+ ? getAIReasoningEffortFlag(flags)
1052
+ : await promptReasoningEffort(ui, getAIReasoningEffortFlag(flags)),
801
1053
  "",
802
1054
  );
803
1055
 
804
1056
  const nextParsed = upsertTelegramEntry(parsed, {
805
1057
  key: botKey,
806
- serverBotID: String(flags["server-bot-id"] || serverBot.botID || "").trim(),
1058
+ serverBotID: String(getServerBotIDFlag(flags) || serverBot.botID || "").trim(),
807
1059
  username,
808
1060
  token,
809
1061
  roleProfile,
@@ -888,8 +1140,8 @@ async function editTelegramBot(ui, flags, deps) {
888
1140
  }
889
1141
  let current = { ...selected };
890
1142
  if (nonInteractive) {
891
- if (flags["server-bot-id"] || flags["bot-id"]) {
892
- current.serverBotID = String(flags["server-bot-id"] || flags["bot-id"] || "").trim();
1143
+ if (getServerBotIDFlag(flags)) {
1144
+ current.serverBotID = getServerBotIDFlag(flags);
893
1145
  }
894
1146
  if (Object.prototype.hasOwnProperty.call(flags, "username")) {
895
1147
  current.username = requireDependency(deps, "normalizeTelegramBotUsername")(flags.username || "");
@@ -900,25 +1152,35 @@ async function editTelegramBot(ui, flags, deps) {
900
1152
  if (Object.prototype.hasOwnProperty.call(flags, "role-profile")) {
901
1153
  current.roleProfile = requireDependency(deps, "normalizeRunnerRoleProfileName")(flags["role-profile"] || "");
902
1154
  }
903
- if (Object.prototype.hasOwnProperty.call(flags, "client")) {
904
- current.client = requireDependency(deps, "normalizeLocalAIClientName")(flags.client || "", "");
1155
+ if (hasOwnFlag(flags, "client") || hasOwnFlag(flags, "ai-client")) {
1156
+ current.client = requireDependency(deps, "normalizeLocalAIClientName")(getAIClientFlag(flags), "");
905
1157
  }
906
- if (Object.prototype.hasOwnProperty.call(flags, "model")) {
907
- current.model = String(flags.model || "").trim();
1158
+ if (hasOwnFlag(flags, "model") || hasOwnFlag(flags, "ai-model")) {
1159
+ current.model = getAIModelFlag(flags);
908
1160
  }
909
- if (Object.prototype.hasOwnProperty.call(flags, "permission-mode")) {
910
- current.permissionMode = requireDependency(deps, "normalizeLocalAIPermissionMode")(flags["permission-mode"] || "", "");
1161
+ if (hasOwnFlag(flags, "permission-mode") || hasOwnFlag(flags, "ai-permission-mode")) {
1162
+ current.permissionMode = requireDependency(deps, "normalizeLocalAIPermissionMode")(getAIPermissionModeFlag(flags), "");
911
1163
  }
912
- if (Object.prototype.hasOwnProperty.call(flags, "reasoning-effort")) {
913
- current.reasoningEffort = requireDependency(deps, "normalizeLocalAIReasoningEffort")(flags["reasoning-effort"] || "", "");
1164
+ if (hasOwnFlag(flags, "reasoning-effort") || hasOwnFlag(flags, "ai-reasoning-effort")) {
1165
+ current.reasoningEffort = requireDependency(deps, "normalizeLocalAIReasoningEffort")(getAIReasoningEffortFlag(flags), "");
914
1166
  }
915
1167
  if (boolFromRaw(flags.default, false)) {
916
1168
  parsed.TELEGRAM_DEFAULT_BOT_KEY = current.key;
917
1169
  }
918
- let nextParsed = parsed;
919
- nextParsed = upsertTelegramEntry(nextParsed, current);
920
- const filePath = writeProviderEnvState("telegram", nextParsed, deps);
921
- process.stdout.write(`Saved Telegram bot entry "${current.key}" to ${filePath}\n`);
1170
+ saveTelegramBotEdit(parsed, selected, current, deps);
1171
+ return;
1172
+ }
1173
+ const editMode = await promptChoice(
1174
+ ui,
1175
+ `How do you want to edit Telegram bot "${current.key}"?`,
1176
+ [
1177
+ { value: "guided", label: "Guided edit (Recommended)", description: "Step-by-step prompts with numbered choices" },
1178
+ { value: "field_menu", label: "Field menu", description: "Choose one field at a time until save" },
1179
+ ],
1180
+ { defaultIndex: 0 },
1181
+ );
1182
+ if (editMode?.value === "guided") {
1183
+ await editTelegramBotGuided(ui, parsed, selected, current, flags, deps);
922
1184
  return;
923
1185
  }
924
1186
  while (true) {
@@ -942,16 +1204,7 @@ async function editTelegramBot(ui, flags, deps) {
942
1204
  );
943
1205
  if (!choice) return;
944
1206
  if (choice.value === "save") {
945
- let nextParsed = parsed;
946
- if (current.key !== selected.key) {
947
- nextParsed = removeTelegramEntry(nextParsed, selected.key);
948
- }
949
- nextParsed = upsertTelegramEntry(nextParsed, current);
950
- if (String(parsed.TELEGRAM_DEFAULT_BOT_KEY || "").trim() === selected.key) {
951
- nextParsed.TELEGRAM_DEFAULT_BOT_KEY = current.key;
952
- }
953
- const filePath = writeProviderEnvState("telegram", nextParsed, deps);
954
- process.stdout.write(`Saved Telegram bot entry "${current.key}" to ${filePath}\n`);
1207
+ saveTelegramBotEdit(parsed, selected, current, deps);
955
1208
  return;
956
1209
  }
957
1210
  if (choice.value === "server_bot_id") {
@@ -1029,7 +1282,13 @@ async function removeTelegramBot(ui, deps) {
1029
1282
  const parsed = { ...state.parsed };
1030
1283
  const selected = await chooseTelegramEntry(ui, parsed, deps, "Select Telegram bot entry to remove");
1031
1284
  if (!selected) return;
1032
- if (!await promptYesNo(ui, `Remove Telegram bot "${selected.key}"?`, false)) {
1285
+ if (!await promptConfirmChoice(ui, `Remove Telegram bot "${selected.key}"?`, {
1286
+ confirmLabel: "Remove bot",
1287
+ confirmDescription: "delete this local Telegram bot entry",
1288
+ cancelLabel: "Cancel",
1289
+ cancelDescription: "keep the current local bot entry",
1290
+ defaultValue: "cancel",
1291
+ })) {
1033
1292
  process.stdout.write("Cancelled.\n");
1034
1293
  return;
1035
1294
  }
@@ -1105,7 +1364,23 @@ async function verifyProviderEntry(ui, provider, flags, deps) {
1105
1364
  }
1106
1365
  }
1107
1366
  }
1108
- if (boolFromRaw(flags.json, false)) {
1367
+ const interactiveJsonChoice = (
1368
+ !boolFromRaw(flags.json, false)
1369
+ && !boolFromRaw(flags["non-interactive"] ?? flags.yes, false)
1370
+ )
1371
+ ? await promptChoice(
1372
+ ui,
1373
+ "Verification output format",
1374
+ [
1375
+ { value: "text", label: "Text output (Recommended)", description: "human-readable summary" },
1376
+ { value: "json", label: "JSON output", description: "machine-readable verification payload" },
1377
+ ],
1378
+ { defaultIndex: 0 },
1379
+ )
1380
+ : null;
1381
+ const outputAsJson = boolFromRaw(flags.json, false) || interactiveJsonChoice?.value === "json";
1382
+
1383
+ if (outputAsJson) {
1109
1384
  process.stdout.write(
1110
1385
  `${JSON.stringify({
1111
1386
  ok: overallOK,
@@ -1361,9 +1636,7 @@ async function runBotVerify(ui, flags, deps) {
1361
1636
  }
1362
1637
 
1363
1638
  async function runBotSetDefault(ui, flags, deps) {
1364
- const provider = String(flags.provider || "").trim()
1365
- ? requireDependency(deps, "normalizeBotProvider")(flags.provider)
1366
- : "telegram";
1639
+ const provider = await selectProvider(ui, flags.provider, deps);
1367
1640
  if (provider !== "telegram") {
1368
1641
  throw new Error("bot set-default currently supports only --provider telegram");
1369
1642
  }
@@ -1376,6 +1649,16 @@ async function runBotSetDefault(ui, flags, deps) {
1376
1649
  if (!selected) {
1377
1650
  throw new Error("Telegram bot selector is required for set-default");
1378
1651
  }
1652
+ if (!nonInteractive && !await promptConfirmChoice(ui, `Set "${selected.key}" as TELEGRAM_DEFAULT_BOT_KEY?`, {
1653
+ confirmLabel: "Set default bot",
1654
+ confirmDescription: "make this the default local Telegram bot entry",
1655
+ cancelLabel: "Cancel",
1656
+ cancelDescription: "leave the current default unchanged",
1657
+ defaultValue: "confirm",
1658
+ })) {
1659
+ process.stdout.write("Cancelled.\n");
1660
+ return;
1661
+ }
1379
1662
  parsed.TELEGRAM_DEFAULT_BOT_KEY = selected.key;
1380
1663
  const filePath = writeProviderEnvState("telegram", parsed, deps);
1381
1664
  process.stdout.write(`Set TELEGRAM_DEFAULT_BOT_KEY=${selected.key} in ${filePath}\n`);
@@ -1410,14 +1693,14 @@ async function runBotMigrate(ui, flags, deps) {
1410
1693
  }
1411
1694
  const nextParsed = upsertTelegramEntry(parsed, {
1412
1695
  key: botKey,
1413
- serverBotID: String(flags["server-bot-id"] || flags["bot-id"] || "").trim(),
1696
+ serverBotID: getServerBotIDFlag(flags),
1414
1697
  username: requireDependency(deps, "normalizeTelegramBotUsername")(flags["bot-name"] || flags.username || ""),
1415
1698
  token: legacyToken,
1416
1699
  roleProfile: requireDependency(deps, "normalizeRunnerRoleProfileName")(flags["role-profile"] || ""),
1417
- client: requireDependency(deps, "normalizeLocalAIClientName")(flags.client || "", ""),
1418
- model: String(flags.model || "").trim(),
1419
- permissionMode: requireDependency(deps, "normalizeLocalAIPermissionMode")(flags["permission-mode"] || "", ""),
1420
- reasoningEffort: requireDependency(deps, "normalizeLocalAIReasoningEffort")(flags["reasoning-effort"] || "", ""),
1700
+ client: requireDependency(deps, "normalizeLocalAIClientName")(getAIClientFlag(flags), ""),
1701
+ model: getAIModelFlag(flags),
1702
+ permissionMode: requireDependency(deps, "normalizeLocalAIPermissionMode")(getAIPermissionModeFlag(flags), ""),
1703
+ reasoningEffort: requireDependency(deps, "normalizeLocalAIReasoningEffort")(getAIReasoningEffortFlag(flags), ""),
1421
1704
  });
1422
1705
  if (!hasNamedTelegramEntry(parsed, deps) || !String(nextParsed.TELEGRAM_DEFAULT_BOT_KEY || "").trim()) {
1423
1706
  nextParsed.TELEGRAM_DEFAULT_BOT_KEY = botKey;
@@ -45,6 +45,45 @@ function readJSON(rawText) {
45
45
  return JSON.parse(String(rawText || "").trim() || "{}");
46
46
  }
47
47
 
48
+ function readTrailingJSON(rawText) {
49
+ const text = String(rawText || "");
50
+ const start = text.indexOf("{");
51
+ if (start < 0) {
52
+ return readJSON(text);
53
+ }
54
+ let depth = 0;
55
+ let inString = false;
56
+ let escaping = false;
57
+ for (let index = start; index < text.length; index += 1) {
58
+ const char = text[index];
59
+ if (inString) {
60
+ if (escaping) {
61
+ escaping = false;
62
+ } else if (char === "\\") {
63
+ escaping = true;
64
+ } else if (char === "\"") {
65
+ inString = false;
66
+ }
67
+ continue;
68
+ }
69
+ if (char === "\"") {
70
+ inString = true;
71
+ continue;
72
+ }
73
+ if (char === "{") {
74
+ depth += 1;
75
+ continue;
76
+ }
77
+ if (char === "}") {
78
+ depth -= 1;
79
+ if (depth === 0) {
80
+ return readJSON(text.slice(start, index + 1));
81
+ }
82
+ }
83
+ }
84
+ return readJSON(text.slice(start));
85
+ }
86
+
48
87
  function createMockServer() {
49
88
  const serverBots = [
50
89
  {
@@ -225,29 +264,36 @@ export async function runSelftestBotCommands(push, deps) {
225
264
  cliPath,
226
265
  args: [
227
266
  "bot", "add",
228
- "--provider", "telegram",
229
- "--non-interactive", "true",
230
267
  "--base-url", baseURL,
231
268
  "--timeout-seconds", "5",
232
- "--bot-key", "main_test",
233
- "--bot-id", mock.bots[0].id,
234
- "--token", "selftest-main-token",
235
- "--role-profile", "monitor",
236
- "--client", "codex",
237
- "--permission-mode", "read_only",
238
- "--reasoning-effort", "low",
239
- "--default", "true",
240
- "--verify", "true",
241
269
  ],
242
- env,
270
+ env: {
271
+ ...env,
272
+ METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
273
+ "1", // provider: telegram
274
+ "1", // server bot: first listed bot
275
+ "main_test",
276
+ "selftest-main-token",
277
+ "y", // verify now
278
+ "", // keep verified username
279
+ "3", // role profile: monitor
280
+ "2", // ai client: codex
281
+ "gpt-5-codex",
282
+ "2", // permission: read_only
283
+ "2", // reasoning: low
284
+ "y", // set as default
285
+ ]),
286
+ },
243
287
  });
244
288
  const addState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
245
289
  push(
246
- "bot_add_creates_named_telegram_entry",
290
+ "bot_add_guided_creates_named_telegram_entry",
247
291
  String(addState.TELEGRAM_BOT_MAIN_TEST_SERVER_BOT_ID || "") === mock.bots[0].id
292
+ && String(addState.TELEGRAM_BOT_MAIN_TEST_USERNAME || "").trim().toLowerCase() === "monitorselftestbot"
248
293
  && String(addState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "") === "codex"
294
+ && String(addState.TELEGRAM_BOT_MAIN_TEST_AI_MODEL || "") === "gpt-5-codex"
249
295
  && String(addState.TELEGRAM_DEFAULT_BOT_KEY || "") === "main_test",
250
- `default=${String(addState.TELEGRAM_DEFAULT_BOT_KEY || "")} client=${String(addState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "")}`,
296
+ `default=${String(addState.TELEGRAM_DEFAULT_BOT_KEY || "")} client=${String(addState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "")} model=${String(addState.TELEGRAM_BOT_MAIN_TEST_AI_MODEL || "")}`,
251
297
  );
252
298
 
253
299
  const showResult = await runCLI({
@@ -268,6 +314,43 @@ export async function runSelftestBotCommands(push, deps) {
268
314
  `key=${String(safeObject(showPayload.entry).key || "")} client=${String(safeObject(showPayload.entry).client || "")}`,
269
315
  );
270
316
 
317
+ await runCLI({
318
+ cliPath,
319
+ args: ["bot", "edit"],
320
+ env: {
321
+ ...env,
322
+ METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
323
+ "1", // provider: telegram
324
+ "1", // bot entry: main_test
325
+ "1", // edit mode: guided
326
+ "1", // keep server bot binding
327
+ "1", // keep username
328
+ "1", // keep token
329
+ "1", // keep role profile
330
+ "2", // change AI client
331
+ "4", // gemini
332
+ "2", // change AI model
333
+ "guided-gemini-pro",
334
+ "2", // change permission mode
335
+ "3", // workspace_write
336
+ "2", // change reasoning effort
337
+ "3", // medium
338
+ "1", // keep bot key
339
+ "1", // keep default setting
340
+ "y", // save
341
+ ]),
342
+ },
343
+ });
344
+ const guidedState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
345
+ push(
346
+ "bot_edit_guided_prompts_update_ai_binding_fields",
347
+ String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "") === "gemini"
348
+ && String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_MODEL || "") === "guided-gemini-pro"
349
+ && String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_PERMISSION_MODE || "") === "workspace_write"
350
+ && String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_REASONING_EFFORT || "") === "medium",
351
+ `client=${String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "")} model=${String(guidedState.TELEGRAM_BOT_MAIN_TEST_AI_MODEL || "")}`,
352
+ );
353
+
271
354
  await runCLI({
272
355
  cliPath,
273
356
  args: [
@@ -294,6 +377,50 @@ export async function runSelftestBotCommands(push, deps) {
294
377
  `client=${String(editedState.TELEGRAM_BOT_MAIN_TEST_AI_CLIENT || "")} model=${String(editedState.TELEGRAM_BOT_MAIN_TEST_AI_MODEL || "")}`,
295
378
  );
296
379
 
380
+ await runCLI({
381
+ cliPath,
382
+ args: ["bot", "set-default"],
383
+ env: {
384
+ ...env,
385
+ METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
386
+ "1", // provider: telegram
387
+ "1", // bot entry: main_test
388
+ "1", // confirm set default
389
+ ]),
390
+ },
391
+ });
392
+ const guidedDefaultState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
393
+ push(
394
+ "bot_set_default_guided_selects_entry",
395
+ String(guidedDefaultState.TELEGRAM_DEFAULT_BOT_KEY || "") === "main_test",
396
+ `default=${String(guidedDefaultState.TELEGRAM_DEFAULT_BOT_KEY || "")}`,
397
+ );
398
+
399
+ const guidedVerifyResult = await runCLI({
400
+ cliPath,
401
+ args: [
402
+ "bot", "verify",
403
+ "--base-url", baseURL,
404
+ "--timeout-seconds", "5",
405
+ ],
406
+ env: {
407
+ ...env,
408
+ METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
409
+ "1", // provider: telegram
410
+ "1", // bot entry: main_test
411
+ "2", // output format: json
412
+ ]),
413
+ },
414
+ });
415
+ const guidedVerifyPayload = readTrailingJSON(guidedVerifyResult.stdout);
416
+ push(
417
+ "bot_verify_guided_can_emit_json",
418
+ guidedVerifyPayload.ok === true
419
+ && safeObject(guidedVerifyPayload.serverBinding).ok === true
420
+ && String(guidedVerifyPayload.client || "") === "claude",
421
+ `verify=${String(guidedVerifyPayload.ok)} server=${String(safeObject(guidedVerifyPayload.serverBinding).detail || "")}`,
422
+ );
423
+
297
424
  await runCLI({
298
425
  cliPath,
299
426
  args: [
@@ -359,6 +486,82 @@ export async function runSelftestBotCommands(push, deps) {
359
486
  `entries=${String(ensureArray(telegramEntry.entries).length)}`,
360
487
  );
361
488
 
489
+ await runCLI({
490
+ cliPath,
491
+ args: [
492
+ "bot", "add",
493
+ "--provider", "telegram",
494
+ "--non-interactive", "true",
495
+ "--base-url", baseURL,
496
+ "--timeout-seconds", "5",
497
+ "--bot-key", "explicit_test",
498
+ "--server-bot-id", mock.bots[0].id,
499
+ "--username", "MonitorSelftestBot",
500
+ "--token", "selftest-main-token",
501
+ "--role-profile", "monitor",
502
+ "--ai-client", "codex",
503
+ "--ai-model", "gpt-5-codex",
504
+ "--ai-permission-mode", "read_only",
505
+ "--ai-reasoning-effort", "low",
506
+ "--verify", "true",
507
+ ],
508
+ env,
509
+ });
510
+ const aliasAddState = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
511
+ push(
512
+ "bot_add_accepts_ai_prefixed_option_aliases",
513
+ String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_SERVER_BOT_ID || "") === mock.bots[0].id
514
+ && String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_CLIENT || "") === "codex"
515
+ && String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_MODEL || "") === "gpt-5-codex"
516
+ && String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_PERMISSION_MODE || "") === "read_only"
517
+ && String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_REASONING_EFFORT || "") === "low",
518
+ `client=${String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_CLIENT || "")} model=${String(aliasAddState.TELEGRAM_BOT_EXPLICIT_TEST_AI_MODEL || "")}`,
519
+ );
520
+
521
+ const guidedRemoveBeforeList = await runCLI({
522
+ cliPath,
523
+ args: [
524
+ "bot", "list",
525
+ "--provider", "telegram",
526
+ "--json", "true",
527
+ ],
528
+ env,
529
+ });
530
+ const guidedRemoveBeforePayload = ensureArray(readJSON(guidedRemoveBeforeList.stdout));
531
+ const guidedRemoveBeforeEntry = safeObject(guidedRemoveBeforePayload[0]);
532
+ const guidedRemoveEntries = ensureArray(guidedRemoveBeforeEntry.entries);
533
+ const explicitEntryIndex = guidedRemoveEntries.findIndex((entry) => String(safeObject(entry).key || "") === "explicit_test");
534
+
535
+ await runCLI({
536
+ cliPath,
537
+ args: ["bot", "remove"],
538
+ env: {
539
+ ...env,
540
+ METHEUS_SCRIPTED_PROMPT_ANSWERS: JSON.stringify([
541
+ "1", // provider: telegram
542
+ String(explicitEntryIndex + 1), // bot entry: explicit_test
543
+ "1", // confirm remove
544
+ ]),
545
+ },
546
+ });
547
+ const guidedRemoveList = await runCLI({
548
+ cliPath,
549
+ args: [
550
+ "bot", "list",
551
+ "--provider", "telegram",
552
+ "--json", "true",
553
+ ],
554
+ env,
555
+ });
556
+ const guidedRemovePayload = ensureArray(readJSON(guidedRemoveList.stdout));
557
+ const guidedRemoveEntry = safeObject(guidedRemovePayload[0]);
558
+ push(
559
+ "bot_remove_guided_deletes_selected_entry",
560
+ explicitEntryIndex >= 0
561
+ && ensureArray(guidedRemoveEntry.entries).length === 0,
562
+ `entries=${String(ensureArray(guidedRemoveEntry.entries).length)} selected=${String(explicitEntryIndex + 1)}`,
563
+ );
564
+
362
565
  const migratedEnv = parseSimpleEnvText(fs.readFileSync(telegramEnvPath, "utf8"));
363
566
  migratedEnv.TELEGRAM_BOT_TOKEN = "legacy-selftest-token";
364
567
  fs.writeFileSync(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.64",
3
+ "version": "0.2.66",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [