metheus-governance-mcp-cli 0.2.49 → 0.2.51

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
@@ -24,21 +24,19 @@ Install creates a local Telegram settings template here:
24
24
 
25
25
  - `~/.metheus/telegram.env`
26
26
 
27
- This file is for local bot secrets only.
27
+ This file is for the local Telegram bot secret only.
28
28
 
29
29
  - Store locally:
30
30
  - `TELEGRAM_BOT_TOKEN`
31
31
  - Server-side Metheus stores project Telegram destination metadata separately:
32
32
  - `chat_id`
33
33
  - label / active state
34
+ - Do not put project `chat_id` in the local Telegram env file.
34
35
 
35
36
  Example template:
36
37
 
37
38
  ```env
38
39
  TELEGRAM_BOT_TOKEN=
39
- TELEGRAM_DEFAULT_CHAT_ID=
40
- TELEGRAM_ALLOWED_CHAT_IDS=
41
- TELEGRAM_DEFAULT_PARSE_MODE=
42
40
  ```
43
41
 
44
42
  ## One command bootstrap (recommended)
@@ -75,7 +73,7 @@ metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctx
75
73
 
76
74
  - `~/.metheus/telegram.env`
77
75
 
78
- Fill only the bot token locally. Project Telegram `chat_id` destinations should be managed on the Metheus server.
76
+ Fill only the bot token locally. Project Telegram `chat_id` destinations should be managed on the Metheus server as project Telegram destinations, not as local env values and not inside Chat Hooks.
79
77
 
80
78
  Gemini CLI note:
81
79
  - `gemini mcp` commands require Gemini auth to be configured first (`GEMINI_API_KEY` or `~/.gemini/settings.json` auth).
@@ -107,13 +105,19 @@ metheus-governance-mcp-cli doctor --project-id <project_uuid> --base-url https:/
107
105
 
108
106
  Checks:
109
107
  - auth token status (+ auto refresh attempt)
110
- - local Telegram env template presence
108
+ - local Telegram env token presence
111
109
  - codex/claude/gemini/antigravity/cursor registration state
112
110
  - gateway `tools/list` reachability
113
111
  - `project.summary` access
114
112
  - ctxpack auto sync status
115
113
  - smoke calls: `workitem.list`, `evidence.list`, `decision.list`
116
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
+
117
121
  ## Use in MCP
118
122
 
119
123
  `setup` auto-registers this server for `codex`, `claude`, `gemini`, `antigravity`, and `cursor` when those CLIs are available.
package/cli.mjs CHANGED
@@ -487,15 +487,10 @@ function telegramEnvTemplate() {
487
487
  return [
488
488
  "# Metheus local Telegram bot settings",
489
489
  "# Keep this file on your machine only. Do not commit it.",
490
- "# Server-side project Telegram destinations store chat_id metadata separately.",
490
+ "# Store only the Telegram bot token locally.",
491
+ "# Project chat_id must be managed on the Metheus server as a project Telegram destination.",
491
492
  "",
492
493
  "TELEGRAM_BOT_TOKEN=",
493
- "# Optional local default for quick tests. Project chat_id is normally resolved from Metheus server.",
494
- "TELEGRAM_DEFAULT_CHAT_ID=",
495
- "# Optional CSV allowlist to avoid accidental sends to the wrong chats.",
496
- "TELEGRAM_ALLOWED_CHAT_IDS=",
497
- "# Optional parse mode: Markdown, MarkdownV2, HTML",
498
- "TELEGRAM_DEFAULT_PARSE_MODE=",
499
494
  "",
500
495
  ].join("\n");
501
496
  }
@@ -514,6 +509,66 @@ function ensureTelegramEnvTemplate() {
514
509
  }
515
510
  }
516
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
+
517
572
  function resolveWorkspaceDir(rawPath) {
518
573
  const input = String(rawPath || "").trim();
519
574
  if (input) {
@@ -2145,16 +2200,28 @@ async function runDoctor(flags) {
2145
2200
  });
2146
2201
  const rows = [];
2147
2202
 
2148
- const telegramEnv = ensureTelegramEnvTemplate();
2149
- if (telegramEnv.error) {
2150
- 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})`);
2151
2206
  } else {
2152
- addDoctorCheck(
2153
- rows,
2154
- telegramEnv.created ? "warn" : "ok",
2155
- "local telegram env",
2156
- `${telegramEnv.created ? "created template" : "ready"} (${telegramEnv.filePath})`,
2157
- );
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
+ }
2158
2225
  }
2159
2226
 
2160
2227
  const resolved = await resolveAccessTokenForCommand(context.baseURL, timeoutSeconds);
@@ -2571,6 +2638,92 @@ function getJSONWithAuth(urlText, timeoutSeconds, token) {
2571
2638
  });
2572
2639
  }
2573
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
+
2574
2727
  function safeObject(value) {
2575
2728
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2576
2729
  return value;
@@ -3797,7 +3950,9 @@ function appendLocalToolToToolsList(responseObj) {
3797
3950
  const tools = ensureArray(result.tools);
3798
3951
  const localSpecs = buildLocalToolSpecs();
3799
3952
  const localNames = new Set(localSpecs.map((spec) => spec.name));
3800
- 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
+ );
3801
3956
  result.tools = injectWorkspaceDirIntoToolSchemas([...localSpecs, ...filtered]);
3802
3957
  responseObj.result = result;
3803
3958
  return responseObj;
@@ -3812,6 +3967,8 @@ function appendProjectHintToInitialize(responseObj, args, options = {}) {
3812
3967
  const ctxpackEnsureTool = displayToolNameForClient("ctxpack.ensure", useSafeToolAliases);
3813
3968
  const ctxpackMergeBriefTool = displayToolNameForClient("ctxpack.merge.brief", useSafeToolAliases);
3814
3969
  const ctxpackMergeExecuteTool = displayToolNameForClient("ctxpack.merge.execute", useSafeToolAliases);
3970
+ const meListBotsTool = displayToolNameForClient("me.list-bots", useSafeToolAliases);
3971
+ const meSendBotMessageTool = displayToolNameForClient("me.send-bot-message", useSafeToolAliases);
3815
3972
  const hintLines = [
3816
3973
  "High-priority project bootstrap rule:",
3817
3974
  ...(useSafeToolAliases
@@ -3825,6 +3982,7 @@ function appendProjectHintToInitialize(responseObj, args, options = {}) {
3825
3982
  `- Never handle a bare Project ID by local file/transcript search before \`${projectSummaryTool}\` succeeds.`,
3826
3983
  `- Run \`${ctxpackEnsureTool}\` only after \`${projectSummaryTool}\`, and only when additional ctxpack refresh/export context is needed.`,
3827
3984
  "- After project summary, use workitem/evidence/decision tools as follow-up.",
3985
+ `- 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.`,
3828
3986
  `- Before any ctxpack merge, call \`${ctxpackMergeBriefTool}\` first, share recommendation to owner, get explicit owner confirmation, then call \`${ctxpackMergeExecuteTool}\`.`,
3829
3987
  "- 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.",
3830
3988
  ];
@@ -3844,6 +4002,233 @@ function appendProjectHintToInitialize(responseObj, args, options = {}) {
3844
4002
  return responseObj;
3845
4003
  }
3846
4004
 
4005
+ function patchServerToolSpecsForLocalRouting(tools) {
4006
+ return ensureArray(tools).map((tool) => {
4007
+ const safeTool = safeObject(tool);
4008
+ const name = String(safeTool.name || "").trim();
4009
+ if (name !== "me.send-bot-message") return safeTool;
4010
+ const inputSchema = {
4011
+ ...safeObject(safeTool.inputSchema),
4012
+ properties: {
4013
+ ...safeObject(safeTool.inputSchema?.properties),
4014
+ project_id: {
4015
+ type: "string",
4016
+ description: "Optional project UUID. Defaults to the current configured project scope.",
4017
+ },
4018
+ destination_id: {
4019
+ type: "string",
4020
+ description: "Optional project chat destination UUID when a project has multiple active destinations.",
4021
+ },
4022
+ destination_label: {
4023
+ type: "string",
4024
+ description: "Optional project chat destination label when a project has multiple active destinations.",
4025
+ },
4026
+ },
4027
+ };
4028
+ return {
4029
+ ...safeTool,
4030
+ description:
4031
+ "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.",
4032
+ inputSchema,
4033
+ };
4034
+ });
4035
+ }
4036
+
4037
+ function sanitizeTelegramAPIURL(rawURL) {
4038
+ const text = String(rawURL || "").trim();
4039
+ if (!text) return "";
4040
+ return text.replace(/\/bot[^/]+/i, "/bot<redacted>");
4041
+ }
4042
+
4043
+ function parseJSONText(rawText) {
4044
+ try {
4045
+ return JSON.parse(String(rawText || ""));
4046
+ } catch {
4047
+ return null;
4048
+ }
4049
+ }
4050
+
4051
+ function buildLocalToolJSONEnvelope(toolName, payload) {
4052
+ const envelope = {
4053
+ tool: String(toolName || "").trim(),
4054
+ ...safeObject(payload),
4055
+ };
4056
+ return JSON.stringify(envelope, null, 2);
4057
+ }
4058
+
4059
+ function buildLocalToolRPCResult(toolName, payload) {
4060
+ const structuredContent = {
4061
+ tool: String(toolName || "").trim(),
4062
+ ...safeObject(payload),
4063
+ };
4064
+ return {
4065
+ content: [
4066
+ {
4067
+ type: "text",
4068
+ text: buildLocalToolJSONEnvelope(toolName, payload),
4069
+ },
4070
+ ],
4071
+ structuredContent,
4072
+ };
4073
+ }
4074
+
4075
+ function normalizeChatDestination(record) {
4076
+ const item = safeObject(record);
4077
+ return {
4078
+ id: String(item.id || "").trim(),
4079
+ provider: String(item.provider || "").trim().toLowerCase(),
4080
+ label: String(item.label || "").trim(),
4081
+ chatID: String(item.chat_id || item.chatId || "").trim(),
4082
+ isActive: item.is_active !== false && item.isActive !== false,
4083
+ };
4084
+ }
4085
+
4086
+ function selectProjectChatDestination(destinations, selectors = {}) {
4087
+ const list = ensureArray(destinations)
4088
+ .map(normalizeChatDestination)
4089
+ .filter((item) => item.provider === "telegram" && item.chatID && item.isActive);
4090
+ const destinationID = String(selectors.destinationID || "").trim();
4091
+ const destinationLabel = String(selectors.destinationLabel || "").trim().toLowerCase();
4092
+
4093
+ if (destinationID) {
4094
+ const match = list.find((item) => item.id === destinationID);
4095
+ if (!match) {
4096
+ throw new Error(`project chat destination ${destinationID} was not found or is inactive`);
4097
+ }
4098
+ return match;
4099
+ }
4100
+ if (destinationLabel) {
4101
+ const match = list.find((item) => item.label.toLowerCase() === destinationLabel);
4102
+ if (!match) {
4103
+ throw new Error(`project chat destination label "${selectors.destinationLabel}" was not found or is inactive`);
4104
+ }
4105
+ return match;
4106
+ }
4107
+ if (list.length === 1) {
4108
+ return list[0];
4109
+ }
4110
+ if (list.length === 0) {
4111
+ throw new Error("no active Telegram chat destination is configured for this project");
4112
+ }
4113
+ const labels = list.map((item) => item.label || item.id).filter(Boolean);
4114
+ throw new Error(
4115
+ `multiple active Telegram chat destinations exist for this project; pass destination_id or destination_label (${labels.join(", ")})`,
4116
+ );
4117
+ }
4118
+
4119
+ async function handleLocalBotMessageToolCall({
4120
+ requestObj,
4121
+ toolArgs,
4122
+ args,
4123
+ token,
4124
+ workspaceDir,
4125
+ }) {
4126
+ const botID = String(toolArgs.bot_id || toolArgs.botId || "").trim();
4127
+ const text = String(toolArgs.text || "").trim();
4128
+ const destinationID = String(toolArgs.destination_id || toolArgs.destinationId || "").trim();
4129
+ const destinationLabel = String(toolArgs.destination_label || toolArgs.destinationLabel || "").trim();
4130
+ const projectID = String(
4131
+ resolveProjectIDForRequest({
4132
+ toolArgs,
4133
+ args,
4134
+ workspaceDir,
4135
+ }),
4136
+ ).trim();
4137
+
4138
+ if (!botID) {
4139
+ throw new Error("bot_id is required");
4140
+ }
4141
+ if (!text) {
4142
+ throw new Error("text is required");
4143
+ }
4144
+ if (!projectID || !isUUID(projectID)) {
4145
+ throw new Error("project_id is required for local bot delivery; set --project-id during setup or include project_id");
4146
+ }
4147
+
4148
+ const telegramEnv = loadTelegramEnvConfig();
4149
+ if (!telegramEnv.ok) {
4150
+ throw new Error(`local telegram env is not ready (${telegramEnv.error})`);
4151
+ }
4152
+
4153
+ const siteBaseURL = normalizeSiteBaseURL(args.baseURL);
4154
+ const encodedBotID = encodeURIComponent(botID);
4155
+ const encodedProjectID = encodeURIComponent(projectID);
4156
+ const botURL = `${siteBaseURL}/api/v1/me/bots/${encodedBotID}`;
4157
+ const destinationURL = `${siteBaseURL}/api/v1/projects/${encodedProjectID}/chat-destinations`;
4158
+
4159
+ const bot = safeObject(await getJSONWithAuth(botURL, args.timeoutSeconds, token));
4160
+ if (!String(bot.id || "").trim()) {
4161
+ throw new Error("bot not found");
4162
+ }
4163
+ if (bot.is_active === false || bot.isActive === false) {
4164
+ throw new Error("bot is inactive");
4165
+ }
4166
+
4167
+ const destinations = ensureArray(await getJSONWithAuth(destinationURL, args.timeoutSeconds, token));
4168
+ const destination = selectProjectChatDestination(destinations, {
4169
+ destinationID,
4170
+ destinationLabel,
4171
+ });
4172
+
4173
+ const disableWebPagePreview = boolFromRaw(
4174
+ Object.prototype.hasOwnProperty.call(toolArgs, "disable_web_page_preview")
4175
+ ? toolArgs.disable_web_page_preview
4176
+ : toolArgs.disableWebPagePreview,
4177
+ true,
4178
+ );
4179
+ const replyToMessageIDRaw = toolArgs.reply_to_message_id ?? toolArgs.replyToMessageID;
4180
+ const replyToMessageID =
4181
+ replyToMessageIDRaw == null || String(replyToMessageIDRaw).trim() === ""
4182
+ ? 0
4183
+ : Number.parseInt(String(replyToMessageIDRaw), 10);
4184
+ if (replyToMessageIDRaw != null && (!Number.isFinite(replyToMessageID) || replyToMessageID <= 0)) {
4185
+ throw new Error("reply_to_message_id must be a positive integer");
4186
+ }
4187
+
4188
+ const telegramURL = `https://api.telegram.org/bot${telegramEnv.token}/sendMessage`;
4189
+ const telegramPayload = {
4190
+ chat_id: destination.chatID,
4191
+ text,
4192
+ disable_web_page_preview: disableWebPagePreview,
4193
+ };
4194
+ if (replyToMessageID > 0) {
4195
+ telegramPayload.reply_to_message_id = replyToMessageID;
4196
+ }
4197
+
4198
+ const response = await postJSONWithoutAuth(telegramURL, args.timeoutSeconds, telegramPayload);
4199
+ const responseJSON = parseJSONText(response.bodyText);
4200
+ const deliveryOK = response.statusCode >= 200 && response.statusCode < 300 && Boolean(responseJSON?.ok ?? true);
4201
+ if (!deliveryOK) {
4202
+ const errorDetail =
4203
+ String(responseJSON?.description || response.bodyText || "").trim() || `telegram api status ${response.statusCode}`;
4204
+ if (response.statusCode === 401 || /unauthorized/i.test(errorDetail)) {
4205
+ throw new Error(
4206
+ `local telegram delivery failed (Unauthorized). Check TELEGRAM_BOT_TOKEN in ${telegramEnv.filePath}`,
4207
+ );
4208
+ }
4209
+ throw new Error(`local telegram delivery failed (${errorDetail})`);
4210
+ }
4211
+
4212
+ return jsonRpcResult(
4213
+ requestObj,
4214
+ buildLocalToolRPCResult("me.send-bot-message", {
4215
+ ok: true,
4216
+ local_delivery: true,
4217
+ provider: "telegram",
4218
+ bot_id: String(bot.id || botID).trim(),
4219
+ bot_name: String(bot.name || "").trim(),
4220
+ bot_role: String(bot.bot_role || bot.botRole || "").trim(),
4221
+ project_id: projectID,
4222
+ destination_id: destination.id,
4223
+ destination_label: destination.label,
4224
+ chat_id: destination.chatID,
4225
+ status: response.statusCode,
4226
+ url: sanitizeTelegramAPIURL(telegramURL),
4227
+ body: responseJSON || response.bodyText,
4228
+ }),
4229
+ );
4230
+ }
4231
+
3847
4232
  async function maybeAutoSyncCtxpackForCall({
3848
4233
  requestObj,
3849
4234
  toolName,
@@ -4746,6 +5131,21 @@ async function runProxy(flags) {
4746
5131
  return;
4747
5132
  }
4748
5133
  }
5134
+ if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "me.send-bot-message") {
5135
+ try {
5136
+ const localSendResult = await handleLocalBotMessageToolCall({
5137
+ requestObj,
5138
+ toolArgs,
5139
+ args,
5140
+ token,
5141
+ workspaceDir: requestWorkspaceDir,
5142
+ });
5143
+ writeProxyJson(localSendResult);
5144
+ } catch (err) {
5145
+ writeProxyJson(jsonRpcError(requestObj, -32001, String(err?.message || err)));
5146
+ }
5147
+ return;
5148
+ }
4749
5149
 
4750
5150
  try {
4751
5151
  const requestWithDefaults = injectCtxpackPushDefaults(requestObj, toolName, requestWorkspaceDir);
@@ -5379,6 +5779,47 @@ function runSelftest(flags = {}) {
5379
5779
  const checks = [];
5380
5780
  const push = (name, ok, detail = "") => checks.push({ name, ok: Boolean(ok), detail: String(detail || "") });
5381
5781
 
5782
+ const parsedEnv = parseSimpleEnvText("\n# comment\nTELEGRAM_BOT_TOKEN=\"abc:123\"\nEMPTY=\n");
5783
+ push(
5784
+ "telegram_env_parse_token",
5785
+ String(parsedEnv.TELEGRAM_BOT_TOKEN || "") === "abc:123",
5786
+ `token=${String(parsedEnv.TELEGRAM_BOT_TOKEN || "(missing)")}`,
5787
+ );
5788
+
5789
+ try {
5790
+ const selected = selectProjectChatDestination(
5791
+ [
5792
+ { id: "dest-1", provider: "telegram", label: "Main Room", chat_id: "-1001", is_active: true },
5793
+ { id: "dest-2", provider: "slack", label: "Slack", chat_id: "C123", is_active: true },
5794
+ ],
5795
+ {},
5796
+ );
5797
+ push(
5798
+ "telegram_destination_select_single_active",
5799
+ selected.id === "dest-1" && selected.chatID === "-1001",
5800
+ `selected=${selected.id || "(none)"}`,
5801
+ );
5802
+ } catch (err) {
5803
+ push("telegram_destination_select_single_active", false, String(err?.message || err));
5804
+ }
5805
+
5806
+ try {
5807
+ selectProjectChatDestination(
5808
+ [
5809
+ { id: "dest-1", provider: "telegram", label: "Room A", chat_id: "-1001", is_active: true },
5810
+ { id: "dest-2", provider: "telegram", label: "Room B", chat_id: "-1002", is_active: true },
5811
+ ],
5812
+ {},
5813
+ );
5814
+ push("telegram_destination_multi_requires_selector", false, "selection unexpectedly succeeded");
5815
+ } catch (err) {
5816
+ push(
5817
+ "telegram_destination_multi_requires_selector",
5818
+ String(err?.message || "").includes("multiple active Telegram chat destinations"),
5819
+ String(err?.message || err),
5820
+ );
5821
+ }
5822
+
5382
5823
  const aliasMaps = buildToolAliasMaps([
5383
5824
  { name: "project.summary" },
5384
5825
  { 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.49",
3
+ "version": "0.2.51",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [
package/postinstall.mjs CHANGED
@@ -16,15 +16,10 @@ function template() {
16
16
  return [
17
17
  "# Metheus local Telegram bot settings",
18
18
  "# Keep this file on your machine only. Do not commit it.",
19
- "# Server-side project Telegram destinations store chat_id metadata separately.",
19
+ "# Store only the Telegram bot token locally.",
20
+ "# Project chat_id must be managed on the Metheus server as a project Telegram destination.",
20
21
  "",
21
22
  "TELEGRAM_BOT_TOKEN=",
22
- "# Optional local default for quick tests. Project chat_id is normally resolved from Metheus server.",
23
- "TELEGRAM_DEFAULT_CHAT_ID=",
24
- "# Optional CSV allowlist to avoid accidental sends to the wrong chats.",
25
- "TELEGRAM_ALLOWED_CHAT_IDS=",
26
- "# Optional parse mode: Markdown, MarkdownV2, HTML",
27
- "TELEGRAM_DEFAULT_PARSE_MODE=",
28
23
  "",
29
24
  ].join("\n");
30
25
  }