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 +10 -6
- package/cli.mjs +458 -17
- package/package.json +1 -1
- package/postinstall.mjs +2 -7
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
|
|
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
|
|
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
|
-
"#
|
|
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 =
|
|
2149
|
-
if (telegramEnv.
|
|
2150
|
-
addDoctorCheck(rows, "warn", "local telegram env",
|
|
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
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
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 =
|
|
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
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
|
-
"#
|
|
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
|
}
|