metheus-governance-mcp-cli 0.2.57 → 0.2.59
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 +431 -18
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import readline from "node:readline";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
11
|
import http from "node:http";
|
|
12
12
|
import https from "node:https";
|
|
@@ -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) {
|
|
@@ -1135,7 +1363,7 @@ function buildRunnerShellInvocation(command) {
|
|
|
1135
1363
|
};
|
|
1136
1364
|
}
|
|
1137
1365
|
|
|
1138
|
-
function runLocalAICommand({ command, inputPayload, route, destination }) {
|
|
1366
|
+
async function runLocalAICommand({ command, inputPayload, route, destination }) {
|
|
1139
1367
|
const invocation = buildRunnerShellInvocation(command);
|
|
1140
1368
|
const stdinText = `${JSON.stringify(inputPayload, null, 2)}\n`;
|
|
1141
1369
|
const env = {
|
|
@@ -1146,16 +1374,72 @@ function runLocalAICommand({ command, inputPayload, route, destination }) {
|
|
|
1146
1374
|
METHEUS_RUNNER_CHAT_ID: String(destination.chatID || "").trim(),
|
|
1147
1375
|
METHEUS_RUNNER_DESTINATION_LABEL: String(destination.label || "").trim(),
|
|
1148
1376
|
};
|
|
1149
|
-
const result =
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1377
|
+
const result = await new Promise((resolve, reject) => {
|
|
1378
|
+
const child = spawn(invocation.file, invocation.args, {
|
|
1379
|
+
env,
|
|
1380
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1381
|
+
});
|
|
1382
|
+
let stdoutText = "";
|
|
1383
|
+
let stderrText = "";
|
|
1384
|
+
let stdoutBytes = 0;
|
|
1385
|
+
let stderrBytes = 0;
|
|
1386
|
+
const maxBufferBytes = 4 * 1024 * 1024;
|
|
1387
|
+
let settled = false;
|
|
1388
|
+
|
|
1389
|
+
function finishWithError(error) {
|
|
1390
|
+
if (settled) return;
|
|
1391
|
+
settled = true;
|
|
1392
|
+
try {
|
|
1393
|
+
child.kill();
|
|
1394
|
+
} catch {}
|
|
1395
|
+
reject(error);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
function appendOutput(chunk, target) {
|
|
1399
|
+
const text = String(chunk || "");
|
|
1400
|
+
if (!text) return;
|
|
1401
|
+
const bytes = Buffer.byteLength(text);
|
|
1402
|
+
if (target === "stdout") {
|
|
1403
|
+
stdoutBytes += bytes;
|
|
1404
|
+
if (stdoutBytes > maxBufferBytes) {
|
|
1405
|
+
finishWithError(new Error("runner command exceeded stdout buffer limit"));
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
stdoutText += text;
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
stderrBytes += bytes;
|
|
1412
|
+
if (stderrBytes > maxBufferBytes) {
|
|
1413
|
+
finishWithError(new Error("runner command exceeded stderr buffer limit"));
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
stderrText += text;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
child.on("error", (error) => {
|
|
1420
|
+
if (settled) return;
|
|
1421
|
+
settled = true;
|
|
1422
|
+
reject(error);
|
|
1423
|
+
});
|
|
1424
|
+
child.stdout.on("data", (chunk) => appendOutput(chunk, "stdout"));
|
|
1425
|
+
child.stderr.on("data", (chunk) => appendOutput(chunk, "stderr"));
|
|
1426
|
+
child.on("close", (status, signal) => {
|
|
1427
|
+
if (settled) return;
|
|
1428
|
+
settled = true;
|
|
1429
|
+
resolve({
|
|
1430
|
+
status,
|
|
1431
|
+
signal,
|
|
1432
|
+
stdoutText: stdoutText.trim(),
|
|
1433
|
+
stderrText: stderrText.trim(),
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
child.stdin.on("error", () => {});
|
|
1437
|
+
child.stdin.end(stdinText);
|
|
1154
1438
|
});
|
|
1155
|
-
const stdoutText = String(result.
|
|
1156
|
-
const stderrText = String(result.
|
|
1157
|
-
if (result.
|
|
1158
|
-
throw new Error(
|
|
1439
|
+
const stdoutText = String(result.stdoutText || "").trim();
|
|
1440
|
+
const stderrText = String(result.stderrText || "").trim();
|
|
1441
|
+
if (result.signal) {
|
|
1442
|
+
throw new Error(stderrText || stdoutText || `runner command terminated by signal ${result.signal}`);
|
|
1159
1443
|
}
|
|
1160
1444
|
if (result.status !== 0) {
|
|
1161
1445
|
throw new Error(stderrText || stdoutText || `runner command exited with status ${result.status}`);
|
|
@@ -1244,9 +1528,11 @@ function buildRunnerRouteStateFromComment(record, patch = {}) {
|
|
|
1244
1528
|
|
|
1245
1529
|
function saveRunnerRouteState(routeKey, routeState) {
|
|
1246
1530
|
const current = loadBotRunnerState();
|
|
1531
|
+
const previous = safeObject(current.routes[routeKey]);
|
|
1247
1532
|
const nextRoutes = {
|
|
1248
1533
|
...safeObject(current.routes),
|
|
1249
1534
|
[routeKey]: {
|
|
1535
|
+
...previous,
|
|
1250
1536
|
...safeObject(routeState),
|
|
1251
1537
|
updated_at: new Date().toISOString(),
|
|
1252
1538
|
},
|
|
@@ -1462,6 +1748,20 @@ async function processRunnerRouteOnce(route, runtime, mode) {
|
|
|
1462
1748
|
archiveThreadID: normalizedRoute.archiveThreadID,
|
|
1463
1749
|
archiveWorkItemID: normalizedRoute.archiveWorkItemID,
|
|
1464
1750
|
});
|
|
1751
|
+
const importOutcome = await archiveLocalTelegramMessagesForRoute({
|
|
1752
|
+
routeKey,
|
|
1753
|
+
routeState: currentState,
|
|
1754
|
+
runtime,
|
|
1755
|
+
destination,
|
|
1756
|
+
archiveThread,
|
|
1757
|
+
});
|
|
1758
|
+
saveRunnerRouteState(routeKey, {
|
|
1759
|
+
local_receive_mode: "telegram_get_updates",
|
|
1760
|
+
local_telegram_polling_ready: true,
|
|
1761
|
+
last_provider_update_id: intFromRawAllowZero(importOutcome.lastUpdateID, 0),
|
|
1762
|
+
last_local_poll_at: new Date().toISOString(),
|
|
1763
|
+
});
|
|
1764
|
+
const refreshedState = safeObject(loadBotRunnerState().routes[routeKey]);
|
|
1465
1765
|
const comments = await listThreadComments({
|
|
1466
1766
|
siteBaseURL: runtime.baseURL,
|
|
1467
1767
|
threadID: archiveThread.threadID,
|
|
@@ -1470,7 +1770,22 @@ async function processRunnerRouteOnce(route, runtime, mode) {
|
|
|
1470
1770
|
actorUserID: runtime.actor.user_id,
|
|
1471
1771
|
limit: Math.max(50, normalizedRoute.contextComments * 6),
|
|
1472
1772
|
});
|
|
1473
|
-
const
|
|
1773
|
+
const orderedComments = ensureArray(comments)
|
|
1774
|
+
.map(normalizeArchiveCommentRecord)
|
|
1775
|
+
.filter((record) => record.id && record.parsedArchive)
|
|
1776
|
+
.sort(compareArchiveCommentRecords);
|
|
1777
|
+
const inboundComments = orderedComments.filter((record) => isInboundArchiveKind(record.parsedArchive.kind));
|
|
1778
|
+
const importedRecords = ensureArray(importOutcome.importedCommentIDs)
|
|
1779
|
+
.map((commentID) => orderedComments.find((record) => record.id === commentID))
|
|
1780
|
+
.filter(Boolean);
|
|
1781
|
+
const pending = importedRecords.length > 0
|
|
1782
|
+
? {
|
|
1783
|
+
ordered: orderedComments,
|
|
1784
|
+
latest: inboundComments.length ? inboundComments[inboundComments.length - 1] : null,
|
|
1785
|
+
shouldPrime: false,
|
|
1786
|
+
pending: importedRecords,
|
|
1787
|
+
}
|
|
1788
|
+
: selectPendingArchiveComments(comments, refreshedState, mode);
|
|
1474
1789
|
if (pending.shouldPrime && pending.latest) {
|
|
1475
1790
|
saveRunnerRouteState(routeKey, buildRunnerRouteStateFromComment(pending.latest, { primed: true }));
|
|
1476
1791
|
return {
|
|
@@ -1487,7 +1802,9 @@ async function processRunnerRouteOnce(route, runtime, mode) {
|
|
|
1487
1802
|
route_key: routeKey,
|
|
1488
1803
|
route_name: normalizedRoute.name,
|
|
1489
1804
|
outcome: "idle",
|
|
1490
|
-
detail:
|
|
1805
|
+
detail: importOutcome.importedCount > 0
|
|
1806
|
+
? "local telegram updates imported but no pending archive comments were selected"
|
|
1807
|
+
: "no new local telegram messages or archived inbound messages",
|
|
1491
1808
|
thread_id: archiveThread.threadID,
|
|
1492
1809
|
};
|
|
1493
1810
|
}
|
|
@@ -1507,12 +1824,41 @@ async function processRunnerRouteOnce(route, runtime, mode) {
|
|
|
1507
1824
|
selectedRecord,
|
|
1508
1825
|
contextWindow,
|
|
1509
1826
|
});
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
inputPayload: aiPayload,
|
|
1513
|
-
route: normalizedRoute,
|
|
1827
|
+
await maybeSendRunnerChatAction({
|
|
1828
|
+
provider: normalizedRoute.provider,
|
|
1514
1829
|
destination,
|
|
1830
|
+
timeoutSeconds: runtime.timeoutSeconds,
|
|
1831
|
+
action: "typing",
|
|
1515
1832
|
});
|
|
1833
|
+
let typingRequestInFlight = false;
|
|
1834
|
+
const typingInterval = setInterval(() => {
|
|
1835
|
+
if (typingRequestInFlight) return;
|
|
1836
|
+
typingRequestInFlight = true;
|
|
1837
|
+
maybeSendRunnerChatAction({
|
|
1838
|
+
provider: normalizedRoute.provider,
|
|
1839
|
+
destination,
|
|
1840
|
+
timeoutSeconds: runtime.timeoutSeconds,
|
|
1841
|
+
action: "typing",
|
|
1842
|
+
})
|
|
1843
|
+
.catch(() => {})
|
|
1844
|
+
.finally(() => {
|
|
1845
|
+
typingRequestInFlight = false;
|
|
1846
|
+
});
|
|
1847
|
+
}, 4000);
|
|
1848
|
+
if (typeof typingInterval.unref === "function") {
|
|
1849
|
+
typingInterval.unref();
|
|
1850
|
+
}
|
|
1851
|
+
let aiResult;
|
|
1852
|
+
try {
|
|
1853
|
+
aiResult = await runLocalAICommand({
|
|
1854
|
+
command: normalizedRoute.command,
|
|
1855
|
+
inputPayload: aiPayload,
|
|
1856
|
+
route: normalizedRoute,
|
|
1857
|
+
destination,
|
|
1858
|
+
});
|
|
1859
|
+
} finally {
|
|
1860
|
+
clearInterval(typingInterval);
|
|
1861
|
+
}
|
|
1516
1862
|
if (aiResult.skip) {
|
|
1517
1863
|
saveRunnerRouteState(
|
|
1518
1864
|
routeKey,
|
|
@@ -1596,7 +1942,6 @@ async function runRunnerOnce(flags) {
|
|
|
1596
1942
|
}
|
|
1597
1943
|
|
|
1598
1944
|
async function runRunnerStart(flags) {
|
|
1599
|
-
const runtime = await resolveRunnerContext(flags);
|
|
1600
1945
|
const jsonMode = boolFromRaw(flags.json, false);
|
|
1601
1946
|
const routes = resolveRunnerRoutes(flags, "start");
|
|
1602
1947
|
const schedules = new Map();
|
|
@@ -1621,6 +1966,7 @@ async function runRunnerStart(flags) {
|
|
|
1621
1966
|
continue;
|
|
1622
1967
|
}
|
|
1623
1968
|
try {
|
|
1969
|
+
const runtime = await resolveRunnerContext(flags);
|
|
1624
1970
|
const result = await processRunnerRouteOnce(normalizedRoute, runtime, "start");
|
|
1625
1971
|
printRunnerResult("start", result, jsonMode);
|
|
1626
1972
|
} catch (err) {
|
|
@@ -1916,6 +2262,73 @@ async function deliverLocalProviderMessage({
|
|
|
1916
2262
|
throw new Error(`${providerEnvConfig(normalizedProvider).label} local delivery is not implemented yet`);
|
|
1917
2263
|
}
|
|
1918
2264
|
|
|
2265
|
+
async function sendLocalProviderChatAction({
|
|
2266
|
+
provider,
|
|
2267
|
+
token,
|
|
2268
|
+
destination,
|
|
2269
|
+
action,
|
|
2270
|
+
timeoutSeconds,
|
|
2271
|
+
}) {
|
|
2272
|
+
const normalizedProvider = normalizeBotProvider(provider);
|
|
2273
|
+
if (normalizedProvider === "telegram") {
|
|
2274
|
+
const requestURL = `https://api.telegram.org/bot${token}/sendChatAction`;
|
|
2275
|
+
const payload = {
|
|
2276
|
+
chat_id: destination.chatID,
|
|
2277
|
+
action: String(action || "typing").trim() || "typing",
|
|
2278
|
+
};
|
|
2279
|
+
const response = await postJSONWithoutAuth(requestURL, timeoutSeconds, payload);
|
|
2280
|
+
const responseJSON = parseJSONText(response.bodyText);
|
|
2281
|
+
return {
|
|
2282
|
+
statusCode: response.statusCode,
|
|
2283
|
+
body: responseJSON || response.bodyText,
|
|
2284
|
+
ok: response.statusCode >= 200 && response.statusCode < 300 && Boolean(responseJSON?.ok ?? true),
|
|
2285
|
+
url: sanitizeTelegramAPIURL(requestURL),
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
return {
|
|
2289
|
+
statusCode: 204,
|
|
2290
|
+
body: { ok: true, skipped: true },
|
|
2291
|
+
ok: true,
|
|
2292
|
+
url: "",
|
|
2293
|
+
};
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
async function maybeSendRunnerChatAction({
|
|
2297
|
+
provider,
|
|
2298
|
+
destination,
|
|
2299
|
+
timeoutSeconds,
|
|
2300
|
+
action = "typing",
|
|
2301
|
+
}) {
|
|
2302
|
+
const providerEnv = loadProviderEnvConfig(provider);
|
|
2303
|
+
if (!providerEnv.ok) {
|
|
2304
|
+
return {
|
|
2305
|
+
ok: false,
|
|
2306
|
+
skipped: true,
|
|
2307
|
+
detail: providerEnv.error,
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
try {
|
|
2311
|
+
const result = await sendLocalProviderChatAction({
|
|
2312
|
+
provider,
|
|
2313
|
+
token: providerEnv.token,
|
|
2314
|
+
destination,
|
|
2315
|
+
action,
|
|
2316
|
+
timeoutSeconds,
|
|
2317
|
+
});
|
|
2318
|
+
return {
|
|
2319
|
+
ok: Boolean(result.ok),
|
|
2320
|
+
skipped: false,
|
|
2321
|
+
detail: result.ok ? "" : String(safeObject(result.body).description || safeObject(result.body).error || "").trim(),
|
|
2322
|
+
};
|
|
2323
|
+
} catch (err) {
|
|
2324
|
+
return {
|
|
2325
|
+
ok: false,
|
|
2326
|
+
skipped: false,
|
|
2327
|
+
detail: String(err?.message || err),
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
1919
2332
|
function resolveWorkspaceDir(rawPath) {
|
|
1920
2333
|
const input = String(rawPath || "").trim();
|
|
1921
2334
|
if (input) {
|