metheus-governance-mcp-cli 0.2.55 → 0.2.58

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/cli.mjs CHANGED
@@ -897,6 +897,80 @@ function parseArchivedChatComment(rawBody) {
897
897
  };
898
898
  }
899
899
 
900
+ function joinTextParts(parts, separator = " ") {
901
+ return ensureArray(parts)
902
+ .map((value) => String(value || "").trim())
903
+ .filter(Boolean)
904
+ .join(separator);
905
+ }
906
+
907
+ function toISOStringFromUnix(rawValue) {
908
+ const numeric = Number(rawValue || 0);
909
+ if (!Number.isFinite(numeric) || numeric <= 0) {
910
+ return new Date().toISOString();
911
+ }
912
+ return new Date(numeric * 1000).toISOString();
913
+ }
914
+
915
+ function collectTelegramUpdateText(message) {
916
+ return firstNonEmptyString([message?.text, message?.caption]);
917
+ }
918
+
919
+ function normalizeLocalTelegramUpdate(rawUpdate) {
920
+ const update = safeObject(rawUpdate);
921
+ const message = safeObject(update.message || update.edited_message);
922
+ if (!message || Object.keys(message).length === 0) {
923
+ return null;
924
+ }
925
+ const chat = safeObject(message.chat);
926
+ const from = safeObject(message.from);
927
+ const replyTo = safeObject(message.reply_to_message);
928
+ const text = collectTelegramUpdateText(message);
929
+ if (!text) {
930
+ return null;
931
+ }
932
+ return {
933
+ eventName: update.edited_message ? "telegram.message.updated" : "telegram.message.created",
934
+ updateID: intFromRawAllowZero(update.update_id, 0),
935
+ messageID: intFromRawAllowZero(message.message_id, 0),
936
+ chatID: String(chat.id || "").trim(),
937
+ chatType: String(chat.type || "").trim(),
938
+ chatTitle: firstNonEmptyString([chat.title, chat.username, joinTextParts([chat.first_name, chat.last_name]), chat.id]),
939
+ fromID: String(from.id || "").trim(),
940
+ fromName: firstNonEmptyString([joinTextParts([from.first_name, from.last_name]), from.username, from.id]),
941
+ fromUsername: String(from.username || "").trim(),
942
+ fromIsBot: Boolean(from.is_bot),
943
+ text,
944
+ occurredAt: toISOStringFromUnix(message.edit_date || message.date),
945
+ messageThreadID: String(message.message_thread_id || "").trim(),
946
+ replyToMessageID: intFromRawAllowZero(replyTo.message_id, 0),
947
+ };
948
+ }
949
+
950
+ function buildArchivedInboundMessageKey(chatID, messageID) {
951
+ return `${String(chatID || "").trim()}:${intFromRawAllowZero(messageID, 0)}`;
952
+ }
953
+
954
+ function formatTelegramInboundArchiveComment(normalized) {
955
+ const headerLines = [
956
+ `[Telegram ${normalized.eventName === "telegram.message.updated" ? "edited" : "message"}]`,
957
+ `chat_id: ${normalized.chatID || "<missing>"}`,
958
+ `message_id: ${normalized.messageID || "<missing>"}`,
959
+ `occurred_at: ${normalized.occurredAt || new Date().toISOString()}`,
960
+ `sender: ${normalized.fromName || normalized.fromUsername || normalized.fromID || "unknown"}`,
961
+ ];
962
+ if (normalized.fromUsername) {
963
+ headerLines.push(`telegram_username: @${normalized.fromUsername.replace(/^@+/, "")}`);
964
+ }
965
+ if (normalized.messageThreadID) {
966
+ headerLines.push(`telegram_topic_id: ${normalized.messageThreadID}`);
967
+ }
968
+ if (normalized.replyToMessageID > 0) {
969
+ headerLines.push(`reply_to_message_id: ${normalized.replyToMessageID}`);
970
+ }
971
+ return `${headerLines.join("\n")}\n\n${String(normalized.text || "").trim()}`;
972
+ }
973
+
900
974
  function formatBotReplyArchiveComment({
901
975
  provider,
902
976
  bot,
@@ -1026,6 +1100,65 @@ async function createThreadComment({
1026
1100
  return safeObject(parseJSONText(responseText));
1027
1101
  }
1028
1102
 
1103
+ async function getTelegramAPIJSON(token, methodName, timeoutSeconds, query = {}) {
1104
+ const requestURL = new URL(`https://api.telegram.org/bot${token}/${methodName}`);
1105
+ Object.entries(safeObject(query)).forEach(([key, value]) => {
1106
+ if (value == null || value === "") return;
1107
+ requestURL.searchParams.set(key, String(value));
1108
+ });
1109
+ const parsed = safeObject(await getJSONWithoutAuth(requestURL.toString(), timeoutSeconds));
1110
+ if (parsed.ok === false) {
1111
+ throw new Error(String(parsed.description || `${methodName} failed`).trim());
1112
+ }
1113
+ return parsed;
1114
+ }
1115
+
1116
+ async function postTelegramAPIJSON(token, methodName, timeoutSeconds, payload = {}) {
1117
+ const requestURL = `https://api.telegram.org/bot${token}/${methodName}`;
1118
+ const response = await postJSONWithoutAuth(requestURL, timeoutSeconds, payload);
1119
+ const parsed = safeObject(parseJSONText(response.bodyText));
1120
+ if (!(response.statusCode >= 200 && response.statusCode < 300) || parsed.ok === false) {
1121
+ const error = new Error(String(parsed.description || response.bodyText || `${methodName} failed`).trim() || `${methodName} failed`);
1122
+ error.statusCode = response.statusCode;
1123
+ error.responseBody = response.bodyText;
1124
+ throw error;
1125
+ }
1126
+ return parsed;
1127
+ }
1128
+
1129
+ async function getTelegramWebhookInfo(token, timeoutSeconds) {
1130
+ return safeObject(await getTelegramAPIJSON(token, "getWebhookInfo", timeoutSeconds));
1131
+ }
1132
+
1133
+ async function deleteTelegramWebhook(token, timeoutSeconds) {
1134
+ return safeObject(await postTelegramAPIJSON(token, "deleteWebhook", timeoutSeconds, {
1135
+ drop_pending_updates: false,
1136
+ }));
1137
+ }
1138
+
1139
+ async function getTelegramUpdates(token, timeoutSeconds, offset = 0) {
1140
+ try {
1141
+ return safeObject(await getTelegramAPIJSON(token, "getUpdates", timeoutSeconds, {
1142
+ timeout: 0,
1143
+ offset: offset > 0 ? offset : "",
1144
+ allowed_updates: JSON.stringify(["message", "edited_message"]),
1145
+ }));
1146
+ } catch (err) {
1147
+ const statusCode = Number(err?.statusCode || 0);
1148
+ const errorText = String(err?.responseBody || err?.message || "").toLowerCase();
1149
+ const webhookConflict = statusCode === 409 || errorText.includes("webhook") || errorText.includes("getupdates");
1150
+ if (!webhookConflict) {
1151
+ throw err;
1152
+ }
1153
+ await deleteTelegramWebhook(token, timeoutSeconds);
1154
+ return safeObject(await getTelegramAPIJSON(token, "getUpdates", timeoutSeconds, {
1155
+ timeout: 0,
1156
+ offset: offset > 0 ? offset : "",
1157
+ allowed_updates: JSON.stringify(["message", "edited_message"]),
1158
+ }));
1159
+ }
1160
+ }
1161
+
1029
1162
  function looksLikeArchiveWorkItemTitle(title, provider) {
1030
1163
  const text = String(title || "").trim().toLowerCase();
1031
1164
  if (!text) return false;
@@ -1118,6 +1251,101 @@ async function discoverArchiveThreadForDestination({
1118
1251
  );
1119
1252
  }
1120
1253
 
1254
+ async function archiveLocalTelegramMessagesForRoute({
1255
+ routeKey,
1256
+ routeState,
1257
+ runtime,
1258
+ destination,
1259
+ archiveThread,
1260
+ }) {
1261
+ const envConfig = loadProviderEnvConfig("telegram");
1262
+ if (!envConfig.ok) {
1263
+ throw new Error(envConfig.error || "TELEGRAM_BOT_TOKEN is not configured");
1264
+ }
1265
+
1266
+ const state = safeObject(routeState);
1267
+ let webhookClearedURL = "";
1268
+ if (!boolFromRaw(state.local_telegram_polling_ready, false)) {
1269
+ const webhookInfo = safeObject(await getTelegramWebhookInfo(envConfig.token, runtime.timeoutSeconds).catch(() => ({})));
1270
+ const currentWebhookURL = String(safeObject(webhookInfo.result).url || "").trim();
1271
+ if (currentWebhookURL) {
1272
+ await deleteTelegramWebhook(envConfig.token, runtime.timeoutSeconds);
1273
+ webhookClearedURL = currentWebhookURL;
1274
+ }
1275
+ saveRunnerRouteState(routeKey, {
1276
+ local_receive_mode: "telegram_get_updates",
1277
+ local_telegram_polling_ready: true,
1278
+ local_telegram_webhook_cleared_url: webhookClearedURL,
1279
+ local_telegram_webhook_cleared_at: webhookClearedURL ? new Date().toISOString() : state.local_telegram_webhook_cleared_at,
1280
+ });
1281
+ }
1282
+
1283
+ const lastUpdateID = intFromRawAllowZero(state.last_provider_update_id ?? state.last_telegram_update_id, 0);
1284
+ const updatesEnvelope = await getTelegramUpdates(envConfig.token, runtime.timeoutSeconds, lastUpdateID > 0 ? lastUpdateID + 1 : 0);
1285
+ const updates = ensureArray(updatesEnvelope.result)
1286
+ .map(normalizeLocalTelegramUpdate)
1287
+ .filter(Boolean);
1288
+ const highestUpdateID = updates.reduce((max, item) => Math.max(max, intFromRawAllowZero(item.updateID, 0)), lastUpdateID);
1289
+
1290
+ saveRunnerRouteState(routeKey, {
1291
+ local_receive_mode: "telegram_get_updates",
1292
+ local_telegram_polling_ready: true,
1293
+ last_provider_update_id: highestUpdateID,
1294
+ last_local_poll_at: new Date().toISOString(),
1295
+ });
1296
+
1297
+ if (!updates.length) {
1298
+ return {
1299
+ importedCommentIDs: [],
1300
+ importedCount: 0,
1301
+ lastUpdateID: highestUpdateID,
1302
+ };
1303
+ }
1304
+
1305
+ const existingComments = await listThreadComments({
1306
+ siteBaseURL: runtime.baseURL,
1307
+ threadID: archiveThread.threadID,
1308
+ token: runtime.token,
1309
+ timeoutSeconds: runtime.timeoutSeconds,
1310
+ limit: 200,
1311
+ actorUserID: runtime.actor.user_id,
1312
+ });
1313
+ const existingKeys = new Set(
1314
+ existingComments
1315
+ .map(normalizeArchiveCommentRecord)
1316
+ .map((record) => record.parsedArchive)
1317
+ .filter((parsed) => parsed && isInboundArchiveKind(parsed.kind) && parsed.chatID)
1318
+ .map((parsed) => buildArchivedInboundMessageKey(parsed.chatID, parsed.messageID)),
1319
+ );
1320
+
1321
+ const importedCommentIDs = [];
1322
+ for (const update of updates) {
1323
+ if (String(update.chatID || "").trim() !== String(destination.chatID || "").trim()) continue;
1324
+ if (update.fromIsBot) continue;
1325
+ if (!String(update.text || "").trim()) continue;
1326
+ const dedupeKey = buildArchivedInboundMessageKey(update.chatID, update.messageID);
1327
+ if (existingKeys.has(dedupeKey)) continue;
1328
+ const createdComment = await createThreadComment({
1329
+ siteBaseURL: runtime.baseURL,
1330
+ token: runtime.token,
1331
+ timeoutSeconds: runtime.timeoutSeconds,
1332
+ threadID: archiveThread.threadID,
1333
+ actorUserID: runtime.actor.user_id,
1334
+ body: formatTelegramInboundArchiveComment(update),
1335
+ });
1336
+ if (String(createdComment.id || "").trim()) {
1337
+ importedCommentIDs.push(String(createdComment.id || "").trim());
1338
+ }
1339
+ existingKeys.add(dedupeKey);
1340
+ }
1341
+
1342
+ return {
1343
+ importedCommentIDs,
1344
+ importedCount: importedCommentIDs.length,
1345
+ lastUpdateID: highestUpdateID,
1346
+ };
1347
+ }
1348
+
1121
1349
  function buildRunnerShellInvocation(command) {
1122
1350
  const text = String(command || "").trim();
1123
1351
  if (!text) {
@@ -1244,9 +1472,11 @@ function buildRunnerRouteStateFromComment(record, patch = {}) {
1244
1472
 
1245
1473
  function saveRunnerRouteState(routeKey, routeState) {
1246
1474
  const current = loadBotRunnerState();
1475
+ const previous = safeObject(current.routes[routeKey]);
1247
1476
  const nextRoutes = {
1248
1477
  ...safeObject(current.routes),
1249
1478
  [routeKey]: {
1479
+ ...previous,
1250
1480
  ...safeObject(routeState),
1251
1481
  updated_at: new Date().toISOString(),
1252
1482
  },
@@ -1462,6 +1692,20 @@ async function processRunnerRouteOnce(route, runtime, mode) {
1462
1692
  archiveThreadID: normalizedRoute.archiveThreadID,
1463
1693
  archiveWorkItemID: normalizedRoute.archiveWorkItemID,
1464
1694
  });
1695
+ const importOutcome = await archiveLocalTelegramMessagesForRoute({
1696
+ routeKey,
1697
+ routeState: currentState,
1698
+ runtime,
1699
+ destination,
1700
+ archiveThread,
1701
+ });
1702
+ saveRunnerRouteState(routeKey, {
1703
+ local_receive_mode: "telegram_get_updates",
1704
+ local_telegram_polling_ready: true,
1705
+ last_provider_update_id: intFromRawAllowZero(importOutcome.lastUpdateID, 0),
1706
+ last_local_poll_at: new Date().toISOString(),
1707
+ });
1708
+ const refreshedState = safeObject(loadBotRunnerState().routes[routeKey]);
1465
1709
  const comments = await listThreadComments({
1466
1710
  siteBaseURL: runtime.baseURL,
1467
1711
  threadID: archiveThread.threadID,
@@ -1470,7 +1714,22 @@ async function processRunnerRouteOnce(route, runtime, mode) {
1470
1714
  actorUserID: runtime.actor.user_id,
1471
1715
  limit: Math.max(50, normalizedRoute.contextComments * 6),
1472
1716
  });
1473
- const pending = selectPendingArchiveComments(comments, currentState, mode);
1717
+ const orderedComments = ensureArray(comments)
1718
+ .map(normalizeArchiveCommentRecord)
1719
+ .filter((record) => record.id && record.parsedArchive)
1720
+ .sort(compareArchiveCommentRecords);
1721
+ const inboundComments = orderedComments.filter((record) => isInboundArchiveKind(record.parsedArchive.kind));
1722
+ const importedRecords = ensureArray(importOutcome.importedCommentIDs)
1723
+ .map((commentID) => orderedComments.find((record) => record.id === commentID))
1724
+ .filter(Boolean);
1725
+ const pending = importedRecords.length > 0
1726
+ ? {
1727
+ ordered: orderedComments,
1728
+ latest: inboundComments.length ? inboundComments[inboundComments.length - 1] : null,
1729
+ shouldPrime: false,
1730
+ pending: importedRecords,
1731
+ }
1732
+ : selectPendingArchiveComments(comments, refreshedState, mode);
1474
1733
  if (pending.shouldPrime && pending.latest) {
1475
1734
  saveRunnerRouteState(routeKey, buildRunnerRouteStateFromComment(pending.latest, { primed: true }));
1476
1735
  return {
@@ -1487,7 +1746,9 @@ async function processRunnerRouteOnce(route, runtime, mode) {
1487
1746
  route_key: routeKey,
1488
1747
  route_name: normalizedRoute.name,
1489
1748
  outcome: "idle",
1490
- detail: "no new archived inbound messages",
1749
+ detail: importOutcome.importedCount > 0
1750
+ ? "local telegram updates imported but no pending archive comments were selected"
1751
+ : "no new local telegram messages or archived inbound messages",
1491
1752
  thread_id: archiveThread.threadID,
1492
1753
  };
1493
1754
  }
@@ -1596,7 +1857,6 @@ async function runRunnerOnce(flags) {
1596
1857
  }
1597
1858
 
1598
1859
  async function runRunnerStart(flags) {
1599
- const runtime = await resolveRunnerContext(flags);
1600
1860
  const jsonMode = boolFromRaw(flags.json, false);
1601
1861
  const routes = resolveRunnerRoutes(flags, "start");
1602
1862
  const schedules = new Map();
@@ -1621,6 +1881,7 @@ async function runRunnerStart(flags) {
1621
1881
  continue;
1622
1882
  }
1623
1883
  try {
1884
+ const runtime = await resolveRunnerContext(flags);
1624
1885
  const result = await processRunnerRouteOnce(normalizedRoute, runtime, "start");
1625
1886
  printRunnerResult("start", result, jsonMode);
1626
1887
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.55",
3
+ "version": "0.2.58",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [
@@ -56,6 +56,50 @@ function resolveWorkspaceDir(rawValue) {
56
56
  return process.cwd();
57
57
  }
58
58
 
59
+ function resolveLocalCliCommand(commandName) {
60
+ const name = String(commandName || "").trim();
61
+ if (!name) {
62
+ throw new Error("command name is required");
63
+ }
64
+ if (process.platform === "win32") {
65
+ const candidates = [
66
+ path.join(process.env.APPDATA || "", "npm", `${name}.cmd`),
67
+ path.join(process.env.APPDATA || "", "npm", `${name}.ps1`),
68
+ path.join(process.env.APPDATA || "", "npm", name),
69
+ ].filter(Boolean);
70
+ for (const candidate of candidates) {
71
+ if (candidate && fs.existsSync(candidate)) {
72
+ return candidate;
73
+ }
74
+ }
75
+ }
76
+ return name;
77
+ }
78
+
79
+ function quoteWindowsShellArg(value) {
80
+ const text = String(value ?? "");
81
+ if (!text) return '""';
82
+ if (!/[\s"&|<>^()]/.test(text)) return text;
83
+ return `"${text.replace(/"/g, '""')}"`;
84
+ }
85
+
86
+ function spawnCli(commandPath, args, options) {
87
+ const executable = String(commandPath || "").trim();
88
+ if (!executable) {
89
+ throw new Error("command path is required");
90
+ }
91
+ const normalizedArgs = Array.isArray(args) ? args.map((item) => String(item)) : [];
92
+ if (process.platform === "win32" && /\.(cmd|bat)$/i.test(executable)) {
93
+ const commandLine = [quoteWindowsShellArg(executable), ...normalizedArgs.map(quoteWindowsShellArg)].join(" ");
94
+ return spawnSync(
95
+ process.env.ComSpec || "cmd.exe",
96
+ ["/d", "/s", "/c", commandLine],
97
+ options,
98
+ );
99
+ }
100
+ return spawnSync(executable, normalizedArgs, options);
101
+ }
102
+
59
103
  function buildPrompt(payload, { terse = true } = {}) {
60
104
  const safePayload = payload && typeof payload === "object" ? payload : {};
61
105
  const trigger = safePayload.trigger && typeof safePayload.trigger === "object" ? safePayload.trigger : {};
@@ -117,8 +161,9 @@ function normalizeCliResponse(rawText) {
117
161
 
118
162
  function runCodex(promptText, workspaceDir) {
119
163
  const outputPath = path.join(os.tmpdir(), `metheus-runner-codex-${Date.now()}.txt`);
120
- const result = spawnSync(
121
- "codex",
164
+ const codexCommand = resolveLocalCliCommand("codex");
165
+ const result = spawnCli(
166
+ codexCommand,
122
167
  [
123
168
  "exec",
124
169
  "-",
@@ -148,8 +193,9 @@ function runCodex(promptText, workspaceDir) {
148
193
  }
149
194
 
150
195
  function runClaude(promptText) {
151
- const result = spawnSync(
152
- "claude",
196
+ const claudeCommand = resolveLocalCliCommand("claude");
197
+ const result = spawnCli(
198
+ claudeCommand,
153
199
  [
154
200
  "-p",
155
201
  promptText,
@@ -175,8 +221,9 @@ function runClaude(promptText) {
175
221
  }
176
222
 
177
223
  function runGemini(promptText) {
178
- const result = spawnSync(
179
- "gemini",
224
+ const geminiCommand = resolveLocalCliCommand("gemini");
225
+ const result = spawnCli(
226
+ geminiCommand,
180
227
  [
181
228
  "-p",
182
229
  promptText,