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.
- package/README.md +7 -1
- package/cli.mjs +459 -10
- 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
|
|
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 =
|
|
2144
|
-
if (telegramEnv.
|
|
2145
|
-
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})`);
|
|
2146
2206
|
} else {
|
|
2147
|
-
addDoctorCheck(
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
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 =
|
|
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" },
|