metheus-governance-mcp-cli 0.2.50 → 0.2.52

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.
Files changed (3) hide show
  1. package/README.md +7 -1
  2. package/cli.mjs +459 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -105,13 +105,19 @@ metheus-governance-mcp-cli doctor --project-id <project_uuid> --base-url https:/
105
105
 
106
106
  Checks:
107
107
  - auth token status (+ auto refresh attempt)
108
- - local Telegram env template presence
108
+ - local Telegram env token presence
109
109
  - codex/claude/gemini/antigravity/cursor registration state
110
110
  - gateway `tools/list` reachability
111
111
  - `project.summary` access
112
112
  - ctxpack auto sync status
113
113
  - smoke calls: `workitem.list`, `evidence.list`, `decision.list`
114
114
 
115
+ Direct bot posting:
116
+ - `me.send-bot-message` uses the local `TELEGRAM_BOT_TOKEN` from `~/.metheus/telegram.env`
117
+ - it does not use a server-stored bot token
118
+ - the destination `chat_id` is resolved from the current project's saved Chat Destinations on the Metheus server
119
+ - if multiple active Telegram destinations exist for the project, pass `destination_id` or `destination_label`
120
+
115
121
  ## Use in MCP
116
122
 
117
123
  `setup` auto-registers this server for `codex`, `claude`, `gemini`, `antigravity`, and `cursor` when those CLIs are available.
package/cli.mjs CHANGED
@@ -509,6 +509,66 @@ function ensureTelegramEnvTemplate() {
509
509
  }
510
510
  }
511
511
 
512
+ function parseSimpleEnvText(rawText) {
513
+ const out = {};
514
+ const lines = String(rawText || "").split(/\r?\n/);
515
+ for (const rawLine of lines) {
516
+ const line = String(rawLine || "").trim();
517
+ if (!line || line.startsWith("#")) continue;
518
+ const eqIndex = line.indexOf("=");
519
+ if (eqIndex <= 0) continue;
520
+ const key = String(line.slice(0, eqIndex) || "").trim();
521
+ if (!key) continue;
522
+ let value = String(line.slice(eqIndex + 1) || "").trim();
523
+ if (
524
+ (value.startsWith('"') && value.endsWith('"'))
525
+ || (value.startsWith("'") && value.endsWith("'"))
526
+ ) {
527
+ value = value.slice(1, -1);
528
+ }
529
+ out[key] = value;
530
+ }
531
+ return out;
532
+ }
533
+
534
+ function loadTelegramEnvConfig() {
535
+ const ensured = ensureTelegramEnvTemplate();
536
+ const filePath = ensured.filePath;
537
+ if (ensured.error) {
538
+ return {
539
+ ok: false,
540
+ filePath,
541
+ error: ensured.error,
542
+ token: "",
543
+ };
544
+ }
545
+ try {
546
+ const raw = fs.readFileSync(filePath, "utf8");
547
+ const parsed = parseSimpleEnvText(raw);
548
+ const token = String(parsed.TELEGRAM_BOT_TOKEN || "").trim();
549
+ if (!token) {
550
+ return {
551
+ ok: false,
552
+ filePath,
553
+ error: `TELEGRAM_BOT_TOKEN is missing in ${filePath}`,
554
+ token: "",
555
+ };
556
+ }
557
+ return {
558
+ ok: true,
559
+ filePath,
560
+ token,
561
+ };
562
+ } catch (err) {
563
+ return {
564
+ ok: false,
565
+ filePath,
566
+ error: String(err?.message || err),
567
+ token: "",
568
+ };
569
+ }
570
+ }
571
+
512
572
  function resolveWorkspaceDir(rawPath) {
513
573
  const input = String(rawPath || "").trim();
514
574
  if (input) {
@@ -2140,16 +2200,28 @@ async function runDoctor(flags) {
2140
2200
  });
2141
2201
  const rows = [];
2142
2202
 
2143
- const telegramEnv = ensureTelegramEnvTemplate();
2144
- if (telegramEnv.error) {
2145
- addDoctorCheck(rows, "warn", "local telegram env", `unavailable (${telegramEnv.error})`);
2203
+ const telegramEnv = loadTelegramEnvConfig();
2204
+ if (!telegramEnv.ok) {
2205
+ addDoctorCheck(rows, "warn", "local telegram env", `${telegramEnv.error} (${telegramEnv.filePath})`);
2146
2206
  } else {
2147
- addDoctorCheck(
2148
- rows,
2149
- telegramEnv.created ? "warn" : "ok",
2150
- "local telegram env",
2151
- `${telegramEnv.created ? "created template" : "ready"} (${telegramEnv.filePath})`,
2152
- );
2207
+ addDoctorCheck(rows, "ok", "local telegram env", `token configured (${telegramEnv.filePath})`);
2208
+ try {
2209
+ const me = safeObject(
2210
+ await getJSONWithoutAuth(`https://api.telegram.org/bot${telegramEnv.token}/getMe`, timeoutSeconds),
2211
+ );
2212
+ if (Boolean(me.ok) && safeObject(me.result).id) {
2213
+ addDoctorCheck(
2214
+ rows,
2215
+ "ok",
2216
+ "local telegram bot",
2217
+ `reachable as @${String(safeObject(me.result).username || "").trim() || "-"}`,
2218
+ );
2219
+ } else {
2220
+ addDoctorCheck(rows, "warn", "local telegram bot", "getMe returned an unexpected payload");
2221
+ }
2222
+ } catch (err) {
2223
+ addDoctorCheck(rows, "warn", "local telegram bot", String(err?.message || err));
2224
+ }
2153
2225
  }
2154
2226
 
2155
2227
  const resolved = await resolveAccessTokenForCommand(context.baseURL, timeoutSeconds);
@@ -2566,6 +2638,92 @@ function getJSONWithAuth(urlText, timeoutSeconds, token) {
2566
2638
  });
2567
2639
  }
2568
2640
 
2641
+ function getJSONWithoutAuth(urlText, timeoutSeconds) {
2642
+ return new Promise((resolve, reject) => {
2643
+ const url = new URL(urlText);
2644
+ const transport = url.protocol === "http:" ? http : https;
2645
+ const req = transport.request(
2646
+ {
2647
+ protocol: url.protocol,
2648
+ hostname: url.hostname,
2649
+ port: url.port || (url.protocol === "http:" ? 80 : 443),
2650
+ path: `${url.pathname}${url.search}`,
2651
+ method: "GET",
2652
+ headers: {
2653
+ accept: "application/json",
2654
+ },
2655
+ timeout: Math.max(3, timeoutSeconds) * 1000,
2656
+ },
2657
+ (res) => {
2658
+ const chunks = [];
2659
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
2660
+ res.on("end", () => {
2661
+ const text = Buffer.concat(chunks).toString("utf8");
2662
+ const statusCode = Number(res.statusCode || 0);
2663
+ if (statusCode >= 200 && statusCode < 300) {
2664
+ try {
2665
+ resolve(JSON.parse(text));
2666
+ } catch {
2667
+ reject(new Error("invalid json response"));
2668
+ }
2669
+ return;
2670
+ }
2671
+ const err = new Error(text.trim() || `http ${statusCode}`);
2672
+ err.statusCode = statusCode;
2673
+ err.responseBody = text;
2674
+ reject(err);
2675
+ });
2676
+ },
2677
+ );
2678
+ req.on("timeout", () => {
2679
+ req.destroy(new Error("http timeout"));
2680
+ });
2681
+ req.on("error", reject);
2682
+ req.end();
2683
+ });
2684
+ }
2685
+
2686
+ function postJSONWithoutAuth(urlText, timeoutSeconds, payload, extraHeaders = {}) {
2687
+ return new Promise((resolve, reject) => {
2688
+ const url = new URL(urlText);
2689
+ const body = Buffer.from(JSON.stringify(payload));
2690
+ const transport = url.protocol === "http:" ? http : https;
2691
+ const req = transport.request(
2692
+ {
2693
+ protocol: url.protocol,
2694
+ hostname: url.hostname,
2695
+ port: url.port || (url.protocol === "http:" ? 80 : 443),
2696
+ path: `${url.pathname}${url.search}`,
2697
+ method: "POST",
2698
+ headers: {
2699
+ "content-type": "application/json",
2700
+ accept: "application/json",
2701
+ "content-length": String(body.length),
2702
+ ...extraHeaders,
2703
+ },
2704
+ timeout: Math.max(3, timeoutSeconds) * 1000,
2705
+ },
2706
+ (res) => {
2707
+ const chunks = [];
2708
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
2709
+ res.on("end", () => {
2710
+ const text = Buffer.concat(chunks).toString("utf8");
2711
+ resolve({
2712
+ statusCode: Number(res.statusCode || 0),
2713
+ bodyText: text,
2714
+ });
2715
+ });
2716
+ },
2717
+ );
2718
+ req.on("timeout", () => {
2719
+ req.destroy(new Error("http timeout"));
2720
+ });
2721
+ req.on("error", reject);
2722
+ req.write(body);
2723
+ req.end();
2724
+ });
2725
+ }
2726
+
2569
2727
  function safeObject(value) {
2570
2728
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2571
2729
  return value;
@@ -3792,7 +3950,9 @@ function appendLocalToolToToolsList(responseObj) {
3792
3950
  const tools = ensureArray(result.tools);
3793
3951
  const localSpecs = buildLocalToolSpecs();
3794
3952
  const localNames = new Set(localSpecs.map((spec) => spec.name));
3795
- const filtered = tools.filter((tool) => !localNames.has(String(tool?.name || "").trim()));
3953
+ const filtered = patchServerToolSpecsForLocalRouting(
3954
+ tools.filter((tool) => !localNames.has(String(tool?.name || "").trim())),
3955
+ );
3796
3956
  result.tools = injectWorkspaceDirIntoToolSchemas([...localSpecs, ...filtered]);
3797
3957
  responseObj.result = result;
3798
3958
  return responseObj;
@@ -3807,6 +3967,9 @@ function appendProjectHintToInitialize(responseObj, args, options = {}) {
3807
3967
  const ctxpackEnsureTool = displayToolNameForClient("ctxpack.ensure", useSafeToolAliases);
3808
3968
  const ctxpackMergeBriefTool = displayToolNameForClient("ctxpack.merge.brief", useSafeToolAliases);
3809
3969
  const ctxpackMergeExecuteTool = displayToolNameForClient("ctxpack.merge.execute", useSafeToolAliases);
3970
+ const projectListDestinationsTool = displayToolNameForClient("project.list-telegram-destinations", useSafeToolAliases);
3971
+ const meListBotsTool = displayToolNameForClient("me.list-bots", useSafeToolAliases);
3972
+ const meSendBotMessageTool = displayToolNameForClient("me.send-bot-message", useSafeToolAliases);
3810
3973
  const hintLines = [
3811
3974
  "High-priority project bootstrap rule:",
3812
3975
  ...(useSafeToolAliases
@@ -3818,8 +3981,11 @@ function appendProjectHintToInitialize(responseObj, args, options = {}) {
3818
3981
  `- If user enters a Project ID in text (e.g., "Project ID <uuid>"), pass that exact UUID as \`project_id\` argument when calling \`${projectSummaryTool}\`.`,
3819
3982
  `- \`${projectDescribeTool}\` and \`${projectGetTool}\` are aliases of \`${projectSummaryTool}\`.`,
3820
3983
  `- Never handle a bare Project ID by local file/transcript search before \`${projectSummaryTool}\` succeeds.`,
3984
+ `- For project room/provider checks, call \`${projectListDestinationsTool}\` first. Treat those chat destinations as the normal routing source.`,
3985
+ "- Do not surface webhook target URLs for normal room routing unless the user explicitly asks about advanced relays or webhook debugging.",
3821
3986
  `- Run \`${ctxpackEnsureTool}\` only after \`${projectSummaryTool}\`, and only when additional ctxpack refresh/export context is needed.`,
3822
3987
  "- After project summary, use workitem/evidence/decision tools as follow-up.",
3988
+ `- For direct bot posting, call \`${meListBotsTool}\` first, then use \`${meSendBotMessageTool}\`. The proxy delivers with your local provider token config and the current project's saved chat destination.`,
3823
3989
  `- Before any ctxpack merge, call \`${ctxpackMergeBriefTool}\` first, share recommendation to owner, get explicit owner confirmation, then call \`${ctxpackMergeExecuteTool}\`.`,
3824
3990
  "- IMPORTANT: When calling ANY tool, always include your current working directory / project folder path as the `workspace_dir` argument. This enables ctxpack files to be synced to the correct workspace location.",
3825
3991
  ];
@@ -3839,6 +4005,233 @@ function appendProjectHintToInitialize(responseObj, args, options = {}) {
3839
4005
  return responseObj;
3840
4006
  }
3841
4007
 
4008
+ function patchServerToolSpecsForLocalRouting(tools) {
4009
+ return ensureArray(tools).map((tool) => {
4010
+ const safeTool = safeObject(tool);
4011
+ const name = String(safeTool.name || "").trim();
4012
+ if (name !== "me.send-bot-message") return safeTool;
4013
+ const inputSchema = {
4014
+ ...safeObject(safeTool.inputSchema),
4015
+ properties: {
4016
+ ...safeObject(safeTool.inputSchema?.properties),
4017
+ project_id: {
4018
+ type: "string",
4019
+ description: "Optional project UUID. Defaults to the current configured project scope.",
4020
+ },
4021
+ destination_id: {
4022
+ type: "string",
4023
+ description: "Optional project chat destination UUID when a project has multiple active destinations.",
4024
+ },
4025
+ destination_label: {
4026
+ type: "string",
4027
+ description: "Optional project chat destination label when a project has multiple active destinations.",
4028
+ },
4029
+ },
4030
+ };
4031
+ return {
4032
+ ...safeTool,
4033
+ description:
4034
+ "Send a direct message through one bot profile using your local provider token config and the current project's saved chat destination. Call me.list-bots first to choose the correct bot_id.",
4035
+ inputSchema,
4036
+ };
4037
+ });
4038
+ }
4039
+
4040
+ function sanitizeTelegramAPIURL(rawURL) {
4041
+ const text = String(rawURL || "").trim();
4042
+ if (!text) return "";
4043
+ return text.replace(/\/bot[^/]+/i, "/bot<redacted>");
4044
+ }
4045
+
4046
+ function parseJSONText(rawText) {
4047
+ try {
4048
+ return JSON.parse(String(rawText || ""));
4049
+ } catch {
4050
+ return null;
4051
+ }
4052
+ }
4053
+
4054
+ function buildLocalToolJSONEnvelope(toolName, payload) {
4055
+ const envelope = {
4056
+ tool: String(toolName || "").trim(),
4057
+ ...safeObject(payload),
4058
+ };
4059
+ return JSON.stringify(envelope, null, 2);
4060
+ }
4061
+
4062
+ function buildLocalToolRPCResult(toolName, payload) {
4063
+ const structuredContent = {
4064
+ tool: String(toolName || "").trim(),
4065
+ ...safeObject(payload),
4066
+ };
4067
+ return {
4068
+ content: [
4069
+ {
4070
+ type: "text",
4071
+ text: buildLocalToolJSONEnvelope(toolName, payload),
4072
+ },
4073
+ ],
4074
+ structuredContent,
4075
+ };
4076
+ }
4077
+
4078
+ function normalizeChatDestination(record) {
4079
+ const item = safeObject(record);
4080
+ return {
4081
+ id: String(item.id || "").trim(),
4082
+ provider: String(item.provider || "").trim().toLowerCase(),
4083
+ label: String(item.label || "").trim(),
4084
+ chatID: String(item.chat_id || item.chatId || "").trim(),
4085
+ isActive: item.is_active !== false && item.isActive !== false,
4086
+ };
4087
+ }
4088
+
4089
+ function selectProjectChatDestination(destinations, selectors = {}) {
4090
+ const list = ensureArray(destinations)
4091
+ .map(normalizeChatDestination)
4092
+ .filter((item) => item.provider === "telegram" && item.chatID && item.isActive);
4093
+ const destinationID = String(selectors.destinationID || "").trim();
4094
+ const destinationLabel = String(selectors.destinationLabel || "").trim().toLowerCase();
4095
+
4096
+ if (destinationID) {
4097
+ const match = list.find((item) => item.id === destinationID);
4098
+ if (!match) {
4099
+ throw new Error(`project chat destination ${destinationID} was not found or is inactive`);
4100
+ }
4101
+ return match;
4102
+ }
4103
+ if (destinationLabel) {
4104
+ const match = list.find((item) => item.label.toLowerCase() === destinationLabel);
4105
+ if (!match) {
4106
+ throw new Error(`project chat destination label "${selectors.destinationLabel}" was not found or is inactive`);
4107
+ }
4108
+ return match;
4109
+ }
4110
+ if (list.length === 1) {
4111
+ return list[0];
4112
+ }
4113
+ if (list.length === 0) {
4114
+ throw new Error("no active Telegram chat destination is configured for this project");
4115
+ }
4116
+ const labels = list.map((item) => item.label || item.id).filter(Boolean);
4117
+ throw new Error(
4118
+ `multiple active Telegram chat destinations exist for this project; pass destination_id or destination_label (${labels.join(", ")})`,
4119
+ );
4120
+ }
4121
+
4122
+ async function handleLocalBotMessageToolCall({
4123
+ requestObj,
4124
+ toolArgs,
4125
+ args,
4126
+ token,
4127
+ workspaceDir,
4128
+ }) {
4129
+ const botID = String(toolArgs.bot_id || toolArgs.botId || "").trim();
4130
+ const text = String(toolArgs.text || "").trim();
4131
+ const destinationID = String(toolArgs.destination_id || toolArgs.destinationId || "").trim();
4132
+ const destinationLabel = String(toolArgs.destination_label || toolArgs.destinationLabel || "").trim();
4133
+ const projectID = String(
4134
+ resolveProjectIDForRequest({
4135
+ toolArgs,
4136
+ args,
4137
+ workspaceDir,
4138
+ }),
4139
+ ).trim();
4140
+
4141
+ if (!botID) {
4142
+ throw new Error("bot_id is required");
4143
+ }
4144
+ if (!text) {
4145
+ throw new Error("text is required");
4146
+ }
4147
+ if (!projectID || !isUUID(projectID)) {
4148
+ throw new Error("project_id is required for local bot delivery; set --project-id during setup or include project_id");
4149
+ }
4150
+
4151
+ const telegramEnv = loadTelegramEnvConfig();
4152
+ if (!telegramEnv.ok) {
4153
+ throw new Error(`local telegram env is not ready (${telegramEnv.error})`);
4154
+ }
4155
+
4156
+ const siteBaseURL = normalizeSiteBaseURL(args.baseURL);
4157
+ const encodedBotID = encodeURIComponent(botID);
4158
+ const encodedProjectID = encodeURIComponent(projectID);
4159
+ const botURL = `${siteBaseURL}/api/v1/me/bots/${encodedBotID}`;
4160
+ const destinationURL = `${siteBaseURL}/api/v1/projects/${encodedProjectID}/chat-destinations`;
4161
+
4162
+ const bot = safeObject(await getJSONWithAuth(botURL, args.timeoutSeconds, token));
4163
+ if (!String(bot.id || "").trim()) {
4164
+ throw new Error("bot not found");
4165
+ }
4166
+ if (bot.is_active === false || bot.isActive === false) {
4167
+ throw new Error("bot is inactive");
4168
+ }
4169
+
4170
+ const destinations = ensureArray(await getJSONWithAuth(destinationURL, args.timeoutSeconds, token));
4171
+ const destination = selectProjectChatDestination(destinations, {
4172
+ destinationID,
4173
+ destinationLabel,
4174
+ });
4175
+
4176
+ const disableWebPagePreview = boolFromRaw(
4177
+ Object.prototype.hasOwnProperty.call(toolArgs, "disable_web_page_preview")
4178
+ ? toolArgs.disable_web_page_preview
4179
+ : toolArgs.disableWebPagePreview,
4180
+ true,
4181
+ );
4182
+ const replyToMessageIDRaw = toolArgs.reply_to_message_id ?? toolArgs.replyToMessageID;
4183
+ const replyToMessageID =
4184
+ replyToMessageIDRaw == null || String(replyToMessageIDRaw).trim() === ""
4185
+ ? 0
4186
+ : Number.parseInt(String(replyToMessageIDRaw), 10);
4187
+ if (replyToMessageIDRaw != null && (!Number.isFinite(replyToMessageID) || replyToMessageID <= 0)) {
4188
+ throw new Error("reply_to_message_id must be a positive integer");
4189
+ }
4190
+
4191
+ const telegramURL = `https://api.telegram.org/bot${telegramEnv.token}/sendMessage`;
4192
+ const telegramPayload = {
4193
+ chat_id: destination.chatID,
4194
+ text,
4195
+ disable_web_page_preview: disableWebPagePreview,
4196
+ };
4197
+ if (replyToMessageID > 0) {
4198
+ telegramPayload.reply_to_message_id = replyToMessageID;
4199
+ }
4200
+
4201
+ const response = await postJSONWithoutAuth(telegramURL, args.timeoutSeconds, telegramPayload);
4202
+ const responseJSON = parseJSONText(response.bodyText);
4203
+ const deliveryOK = response.statusCode >= 200 && response.statusCode < 300 && Boolean(responseJSON?.ok ?? true);
4204
+ if (!deliveryOK) {
4205
+ const errorDetail =
4206
+ String(responseJSON?.description || response.bodyText || "").trim() || `telegram api status ${response.statusCode}`;
4207
+ if (response.statusCode === 401 || /unauthorized/i.test(errorDetail)) {
4208
+ throw new Error(
4209
+ `local telegram delivery failed (Unauthorized). Check TELEGRAM_BOT_TOKEN in ${telegramEnv.filePath}`,
4210
+ );
4211
+ }
4212
+ throw new Error(`local telegram delivery failed (${errorDetail})`);
4213
+ }
4214
+
4215
+ return jsonRpcResult(
4216
+ requestObj,
4217
+ buildLocalToolRPCResult("me.send-bot-message", {
4218
+ ok: true,
4219
+ local_delivery: true,
4220
+ provider: "telegram",
4221
+ bot_id: String(bot.id || botID).trim(),
4222
+ bot_name: String(bot.name || "").trim(),
4223
+ bot_role: String(bot.bot_role || bot.botRole || "").trim(),
4224
+ project_id: projectID,
4225
+ destination_id: destination.id,
4226
+ destination_label: destination.label,
4227
+ chat_id: destination.chatID,
4228
+ status: response.statusCode,
4229
+ url: sanitizeTelegramAPIURL(telegramURL),
4230
+ body: responseJSON || response.bodyText,
4231
+ }),
4232
+ );
4233
+ }
4234
+
3842
4235
  async function maybeAutoSyncCtxpackForCall({
3843
4236
  requestObj,
3844
4237
  toolName,
@@ -4741,6 +5134,21 @@ async function runProxy(flags) {
4741
5134
  return;
4742
5135
  }
4743
5136
  }
5137
+ if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "me.send-bot-message") {
5138
+ try {
5139
+ const localSendResult = await handleLocalBotMessageToolCall({
5140
+ requestObj,
5141
+ toolArgs,
5142
+ args,
5143
+ token,
5144
+ workspaceDir: requestWorkspaceDir,
5145
+ });
5146
+ writeProxyJson(localSendResult);
5147
+ } catch (err) {
5148
+ writeProxyJson(jsonRpcError(requestObj, -32001, String(err?.message || err)));
5149
+ }
5150
+ return;
5151
+ }
4744
5152
 
4745
5153
  try {
4746
5154
  const requestWithDefaults = injectCtxpackPushDefaults(requestObj, toolName, requestWorkspaceDir);
@@ -5374,6 +5782,47 @@ function runSelftest(flags = {}) {
5374
5782
  const checks = [];
5375
5783
  const push = (name, ok, detail = "") => checks.push({ name, ok: Boolean(ok), detail: String(detail || "") });
5376
5784
 
5785
+ const parsedEnv = parseSimpleEnvText("\n# comment\nTELEGRAM_BOT_TOKEN=\"abc:123\"\nEMPTY=\n");
5786
+ push(
5787
+ "telegram_env_parse_token",
5788
+ String(parsedEnv.TELEGRAM_BOT_TOKEN || "") === "abc:123",
5789
+ `token=${String(parsedEnv.TELEGRAM_BOT_TOKEN || "(missing)")}`,
5790
+ );
5791
+
5792
+ try {
5793
+ const selected = selectProjectChatDestination(
5794
+ [
5795
+ { id: "dest-1", provider: "telegram", label: "Main Room", chat_id: "-1001", is_active: true },
5796
+ { id: "dest-2", provider: "slack", label: "Slack", chat_id: "C123", is_active: true },
5797
+ ],
5798
+ {},
5799
+ );
5800
+ push(
5801
+ "telegram_destination_select_single_active",
5802
+ selected.id === "dest-1" && selected.chatID === "-1001",
5803
+ `selected=${selected.id || "(none)"}`,
5804
+ );
5805
+ } catch (err) {
5806
+ push("telegram_destination_select_single_active", false, String(err?.message || err));
5807
+ }
5808
+
5809
+ try {
5810
+ selectProjectChatDestination(
5811
+ [
5812
+ { id: "dest-1", provider: "telegram", label: "Room A", chat_id: "-1001", is_active: true },
5813
+ { id: "dest-2", provider: "telegram", label: "Room B", chat_id: "-1002", is_active: true },
5814
+ ],
5815
+ {},
5816
+ );
5817
+ push("telegram_destination_multi_requires_selector", false, "selection unexpectedly succeeded");
5818
+ } catch (err) {
5819
+ push(
5820
+ "telegram_destination_multi_requires_selector",
5821
+ String(err?.message || "").includes("multiple active Telegram chat destinations"),
5822
+ String(err?.message || err),
5823
+ );
5824
+ }
5825
+
5377
5826
  const aliasMaps = buildToolAliasMaps([
5378
5827
  { name: "project.summary" },
5379
5828
  { name: "ctxpack.merge.brief" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.50",
3
+ "version": "0.2.52",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [