@grinev/opencode-telegram-bot 0.2.0 → 0.3.0
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/.env.example +8 -0
- package/README.md +1 -0
- package/dist/bot/handlers/permission.js +5 -2
- package/dist/bot/handlers/question.js +5 -2
- package/dist/bot/index.js +61 -14
- package/dist/config.js +1 -0
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/ru.js +1 -0
- package/dist/opencode/events.js +78 -17
- package/dist/utils/error-format.js +29 -0
- package/package.json +4 -2
package/.env.example
CHANGED
|
@@ -4,6 +4,14 @@ TELEGRAM_BOT_TOKEN=
|
|
|
4
4
|
# Allowed Telegram User ID (from @userinfobot)
|
|
5
5
|
TELEGRAM_ALLOWED_USER_ID=
|
|
6
6
|
|
|
7
|
+
# Telegram Proxy URL (optional)
|
|
8
|
+
# Supports socks5://, socks4://, http://, https:// protocols
|
|
9
|
+
# Examples:
|
|
10
|
+
# TELEGRAM_PROXY_URL=socks5://proxy.example.com:1080
|
|
11
|
+
# TELEGRAM_PROXY_URL=socks5://user:password@proxy.example.com:1080
|
|
12
|
+
# TELEGRAM_PROXY_URL=http://proxy.example.com:8080
|
|
13
|
+
# TELEGRAM_PROXY_URL=
|
|
14
|
+
|
|
7
15
|
# OpenCode API URL (optional, default: http://localhost:4096)
|
|
8
16
|
# OPENCODE_API_URL=http://localhost:4096
|
|
9
17
|
|
package/README.md
CHANGED
|
@@ -117,6 +117,7 @@ When installed via npm, the configuration wizard handles the initial setup. The
|
|
|
117
117
|
| -------------------------- | -------------------------------------------- | :------: | ----------------------- |
|
|
118
118
|
| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | Yes | — |
|
|
119
119
|
| `TELEGRAM_ALLOWED_USER_ID` | Your numeric Telegram user ID | Yes | — |
|
|
120
|
+
| `TELEGRAM_PROXY_URL` | Proxy URL for Telegram API (SOCKS5/HTTP) | No | — |
|
|
120
121
|
| `OPENCODE_API_URL` | OpenCode server URL | No | `http://localhost:4096` |
|
|
121
122
|
| `OPENCODE_SERVER_USERNAME` | Server auth username | No | `opencode` |
|
|
122
123
|
| `OPENCODE_SERVER_PASSWORD` | Server auth password | No | — |
|
|
@@ -2,6 +2,7 @@ import { InlineKeyboard } from "grammy";
|
|
|
2
2
|
import { permissionManager } from "../../permission/manager.js";
|
|
3
3
|
import { opencodeClient } from "../../opencode/client.js";
|
|
4
4
|
import { getCurrentProject } from "../../settings/manager.js";
|
|
5
|
+
import { getCurrentSession } from "../../session/manager.js";
|
|
5
6
|
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
6
7
|
import { logger } from "../../utils/logger.js";
|
|
7
8
|
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
@@ -69,8 +70,10 @@ export async function handlePermissionCallback(ctx) {
|
|
|
69
70
|
async function handlePermissionReply(ctx, reply) {
|
|
70
71
|
const requestID = permissionManager.getRequestID();
|
|
71
72
|
const currentProject = getCurrentProject();
|
|
73
|
+
const currentSession = getCurrentSession();
|
|
72
74
|
const chatId = ctx.chat?.id;
|
|
73
|
-
|
|
75
|
+
const directory = currentSession?.directory ?? currentProject?.worktree;
|
|
76
|
+
if (!requestID || !directory || !chatId) {
|
|
74
77
|
await ctx.answerCallbackQuery({
|
|
75
78
|
text: t("permission.no_active_request_callback"),
|
|
76
79
|
show_alert: true,
|
|
@@ -94,7 +97,7 @@ async function handlePermissionReply(ctx, reply) {
|
|
|
94
97
|
taskName: "permission.reply",
|
|
95
98
|
task: () => opencodeClient.permission.reply({
|
|
96
99
|
requestID,
|
|
97
|
-
directory
|
|
100
|
+
directory,
|
|
98
101
|
reply,
|
|
99
102
|
}),
|
|
100
103
|
onSuccess: ({ error }) => {
|
|
@@ -2,6 +2,7 @@ import { InlineKeyboard } from "grammy";
|
|
|
2
2
|
import { questionManager } from "../../question/manager.js";
|
|
3
3
|
import { opencodeClient } from "../../opencode/client.js";
|
|
4
4
|
import { getCurrentProject } from "../../settings/manager.js";
|
|
5
|
+
import { getCurrentSession } from "../../session/manager.js";
|
|
5
6
|
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
6
7
|
import { logger } from "../../utils/logger.js";
|
|
7
8
|
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
@@ -194,9 +195,11 @@ async function showPollSummary(bot, chatId) {
|
|
|
194
195
|
}
|
|
195
196
|
async function sendAllAnswersToAgent(bot, chatId) {
|
|
196
197
|
const currentProject = getCurrentProject();
|
|
198
|
+
const currentSession = getCurrentSession();
|
|
197
199
|
const requestID = questionManager.getRequestID();
|
|
198
200
|
const totalQuestions = questionManager.getTotalQuestions();
|
|
199
|
-
|
|
201
|
+
const directory = currentSession?.directory ?? currentProject?.worktree;
|
|
202
|
+
if (!directory) {
|
|
200
203
|
logger.error("[QuestionHandler] No project for sending answers");
|
|
201
204
|
await bot.sendMessage(chatId, t("question.no_active_project"));
|
|
202
205
|
return;
|
|
@@ -233,7 +236,7 @@ async function sendAllAnswersToAgent(bot, chatId) {
|
|
|
233
236
|
taskName: "question.reply",
|
|
234
237
|
task: () => opencodeClient.question.reply({
|
|
235
238
|
requestID,
|
|
236
|
-
directory
|
|
239
|
+
directory,
|
|
237
240
|
answers: allAnswers,
|
|
238
241
|
}),
|
|
239
242
|
onSuccess: ({ error }) => {
|
package/dist/bot/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import { Bot, InputFile } from "grammy";
|
|
|
2
2
|
import { promises as fs } from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
+
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
6
|
+
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
5
7
|
import { config } from "../config.js";
|
|
6
8
|
import { authMiddleware } from "./middleware/auth.js";
|
|
7
9
|
import { BOT_COMMANDS } from "./commands/definitions.js";
|
|
@@ -24,12 +26,13 @@ import { handleModelSelect, showModelSelectionMenu } from "./handlers/model.js";
|
|
|
24
26
|
import { handleVariantSelect, showVariantSelectionMenu } from "./handlers/variant.js";
|
|
25
27
|
import { handleContextButtonPress, handleCompactConfirm, handleCompactCancel, } from "./handlers/context.js";
|
|
26
28
|
import { questionManager } from "../question/manager.js";
|
|
29
|
+
import { permissionManager } from "../permission/manager.js";
|
|
27
30
|
import { keyboardManager } from "../keyboard/manager.js";
|
|
28
|
-
import { subscribeToEvents } from "../opencode/events.js";
|
|
31
|
+
import { stopEventListening, subscribeToEvents } from "../opencode/events.js";
|
|
29
32
|
import { summaryAggregator } from "../summary/aggregator.js";
|
|
30
33
|
import { formatSummary, formatToolInfo } from "../summary/formatter.js";
|
|
31
34
|
import { opencodeClient } from "../opencode/client.js";
|
|
32
|
-
import { getCurrentSession, setCurrentSession } from "../session/manager.js";
|
|
35
|
+
import { clearSession, getCurrentSession, setCurrentSession } from "../session/manager.js";
|
|
33
36
|
import { ingestSessionInfoForCache } from "../session/cache-manager.js";
|
|
34
37
|
import { getCurrentProject } from "../settings/manager.js";
|
|
35
38
|
import { getStoredAgent } from "../agent/manager.js";
|
|
@@ -38,6 +41,7 @@ import { formatVariantForButton } from "../variant/manager.js";
|
|
|
38
41
|
import { createMainKeyboard } from "./utils/keyboard.js";
|
|
39
42
|
import { logger } from "../utils/logger.js";
|
|
40
43
|
import { safeBackgroundTask } from "../utils/safe-background-task.js";
|
|
44
|
+
import { formatErrorDetails } from "../utils/error-format.js";
|
|
41
45
|
import { pinnedMessageManager } from "../pinned/manager.js";
|
|
42
46
|
import { t } from "../i18n/index.js";
|
|
43
47
|
let botInstance = null;
|
|
@@ -68,10 +72,9 @@ async function ensureCommandsInitialized(ctx, next) {
|
|
|
68
72
|
}
|
|
69
73
|
await next();
|
|
70
74
|
}
|
|
71
|
-
async function ensureEventSubscription() {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
logger.error("No current project found for event subscription");
|
|
75
|
+
async function ensureEventSubscription(directory) {
|
|
76
|
+
if (!directory) {
|
|
77
|
+
logger.error("No directory found for event subscription");
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
80
|
summaryAggregator.setOnComplete(async (sessionId, messageText) => {
|
|
@@ -256,8 +259,8 @@ async function ensureEventSubscription() {
|
|
|
256
259
|
logger.error("[Bot] Error updating keyboard context:", err);
|
|
257
260
|
}
|
|
258
261
|
});
|
|
259
|
-
logger.info(`[Bot] Subscribing to OpenCode events for project: ${
|
|
260
|
-
subscribeToEvents(
|
|
262
|
+
logger.info(`[Bot] Subscribing to OpenCode events for project: ${directory}`);
|
|
263
|
+
subscribeToEvents(directory, (event) => {
|
|
261
264
|
if (event.type === "session.created" || event.type === "session.updated") {
|
|
262
265
|
const info = event.properties.info;
|
|
263
266
|
if (info?.directory) {
|
|
@@ -291,8 +294,44 @@ async function isSessionBusy(sessionId, directory) {
|
|
|
291
294
|
return false;
|
|
292
295
|
}
|
|
293
296
|
}
|
|
297
|
+
async function resetMismatchedSessionContext() {
|
|
298
|
+
stopEventListening();
|
|
299
|
+
summaryAggregator.clear();
|
|
300
|
+
questionManager.clear();
|
|
301
|
+
permissionManager.clear();
|
|
302
|
+
clearSession();
|
|
303
|
+
keyboardManager.clearContext();
|
|
304
|
+
if (!pinnedMessageManager.isInitialized()) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
await pinnedMessageManager.clear();
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
logger.error("[Bot] Failed to clear pinned message during session reset:", err);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
294
314
|
export function createBot() {
|
|
295
|
-
const
|
|
315
|
+
const botOptions = {};
|
|
316
|
+
if (config.telegram.proxyUrl) {
|
|
317
|
+
const proxyUrl = config.telegram.proxyUrl;
|
|
318
|
+
let agent;
|
|
319
|
+
if (proxyUrl.startsWith("socks")) {
|
|
320
|
+
agent = new SocksProxyAgent(proxyUrl);
|
|
321
|
+
logger.info(`[Bot] Using SOCKS proxy: ${proxyUrl.replace(/\/\/.*@/, "//***@")}`);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
agent = new HttpsProxyAgent(proxyUrl);
|
|
325
|
+
logger.info(`[Bot] Using HTTP/HTTPS proxy: ${proxyUrl.replace(/\/\/.*@/, "//***@")}`);
|
|
326
|
+
}
|
|
327
|
+
botOptions.client = {
|
|
328
|
+
baseFetchConfig: {
|
|
329
|
+
agent,
|
|
330
|
+
compress: true,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const bot = new Bot(config.telegram.token, botOptions);
|
|
296
335
|
// Heartbeat for diagnostics: verify the event loop is not blocked
|
|
297
336
|
let heartbeatCounter = 0;
|
|
298
337
|
setInterval(() => {
|
|
@@ -462,7 +501,6 @@ export function createBot() {
|
|
|
462
501
|
await ctx.reply(t("bot.project_not_selected"));
|
|
463
502
|
return;
|
|
464
503
|
}
|
|
465
|
-
await ensureEventSubscription();
|
|
466
504
|
botInstance = bot;
|
|
467
505
|
chatIdInstance = ctx.chat.id;
|
|
468
506
|
// Initialize pinned message manager if not already
|
|
@@ -472,6 +510,12 @@ export function createBot() {
|
|
|
472
510
|
// Initialize keyboard manager if not already
|
|
473
511
|
keyboardManager.initialize(bot.api, ctx.chat.id);
|
|
474
512
|
let currentSession = getCurrentSession();
|
|
513
|
+
if (currentSession && currentSession.directory !== currentProject.worktree) {
|
|
514
|
+
logger.warn(`[Bot] Session/project mismatch detected. sessionDirectory=${currentSession.directory}, projectDirectory=${currentProject.worktree}. Resetting session context.`);
|
|
515
|
+
await resetMismatchedSessionContext();
|
|
516
|
+
await ctx.reply(t("bot.session_reset_project_mismatch"));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
475
519
|
if (!currentSession) {
|
|
476
520
|
await ctx.reply(t("bot.creating_session"));
|
|
477
521
|
const { data: session, error } = await opencodeClient.session.create({
|
|
@@ -517,6 +561,7 @@ export function createBot() {
|
|
|
517
561
|
}
|
|
518
562
|
}
|
|
519
563
|
}
|
|
564
|
+
await ensureEventSubscription(currentSession.directory);
|
|
520
565
|
summaryAggregator.setSession(currentSession.id);
|
|
521
566
|
summaryAggregator.setBotAndChatId(bot, ctx.chat.id);
|
|
522
567
|
const sessionIsBusy = await isSessionBusy(currentSession.id, currentSession.directory);
|
|
@@ -530,7 +575,7 @@ export function createBot() {
|
|
|
530
575
|
const storedModel = getStoredModel();
|
|
531
576
|
const promptOptions = {
|
|
532
577
|
sessionID: currentSession.id,
|
|
533
|
-
directory:
|
|
578
|
+
directory: currentSession.directory,
|
|
534
579
|
parts: [{ type: "text", text }],
|
|
535
580
|
agent: currentAgent,
|
|
536
581
|
};
|
|
@@ -555,18 +600,20 @@ export function createBot() {
|
|
|
555
600
|
task: () => opencodeClient.session.prompt(promptOptions),
|
|
556
601
|
onSuccess: ({ error }) => {
|
|
557
602
|
if (error) {
|
|
558
|
-
|
|
603
|
+
const details = formatErrorDetails(error);
|
|
604
|
+
logger.error("OpenCode API error:", error);
|
|
559
605
|
// Send the error via API directly because ctx is no longer available
|
|
560
606
|
void bot.api
|
|
561
607
|
.sendMessage(ctx.chat.id, t("bot.prompt_send_error_detailed", {
|
|
562
|
-
details
|
|
608
|
+
details,
|
|
563
609
|
}))
|
|
564
610
|
.catch(() => { });
|
|
565
611
|
return;
|
|
566
612
|
}
|
|
567
613
|
logger.info("[Bot] session.prompt completed");
|
|
568
614
|
},
|
|
569
|
-
onError: () => {
|
|
615
|
+
onError: (error) => {
|
|
616
|
+
logger.error("[Bot] session.prompt background task failed:", error);
|
|
570
617
|
void bot.api.sendMessage(ctx.chat.id, t("bot.prompt_send_error")).catch(() => { });
|
|
571
618
|
},
|
|
572
619
|
});
|
package/dist/config.js
CHANGED
|
@@ -38,6 +38,7 @@ export const config = {
|
|
|
38
38
|
telegram: {
|
|
39
39
|
token: getEnvVar("TELEGRAM_BOT_TOKEN"),
|
|
40
40
|
allowedUserId: parseInt(getEnvVar("TELEGRAM_ALLOWED_USER_ID"), 10),
|
|
41
|
+
proxyUrl: getEnvVar("TELEGRAM_PROXY_URL", false),
|
|
41
42
|
},
|
|
42
43
|
opencode: {
|
|
43
44
|
apiUrl: getEnvVar("OPENCODE_API_URL", false) || "http://localhost:4096",
|
package/dist/i18n/en.js
CHANGED
|
@@ -26,6 +26,7 @@ export const en = {
|
|
|
26
26
|
"bot.create_session_error": "🔴 Failed to create session. Try /new or check server status with /status.",
|
|
27
27
|
"bot.session_created": "✅ Session created: {title}",
|
|
28
28
|
"bot.session_busy": "⏳ Agent is already running a task. Wait for completion or use /stop to interrupt current run.",
|
|
29
|
+
"bot.session_reset_project_mismatch": "⚠️ Active session does not match the selected project, so it was reset. Use /sessions to pick one or /new to create a new session.",
|
|
29
30
|
"bot.prompt_send_error_detailed": "🔴 Failed to send request.\n\nDetails: {details}",
|
|
30
31
|
"bot.prompt_send_error": "🔴 An error occurred while sending request to OpenCode.",
|
|
31
32
|
"status.header_running": "🟢 **OpenCode Server is running**",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -26,6 +26,7 @@ export const ru = {
|
|
|
26
26
|
"bot.create_session_error": "🔴 Не удалось создать сессию. Попробуйте команду /new или проверьте статус сервера /status.",
|
|
27
27
|
"bot.session_created": "✅ Сессия создана: {title}",
|
|
28
28
|
"bot.session_busy": "⏳ Агент уже выполняет задачу. Дождитесь завершения или используйте /stop, чтобы прервать текущий запуск.",
|
|
29
|
+
"bot.session_reset_project_mismatch": "⚠️ Активная сессия не соответствует выбранному проекту, поэтому была сброшена. Используйте /sessions для выбора или /new для создания новой сессии.",
|
|
29
30
|
"bot.prompt_send_error_detailed": "🔴 Ошибка при отправке запроса.\n\nДетали: {details}",
|
|
30
31
|
"bot.prompt_send_error": "🔴 Произошла ошибка при отправке запроса в OpenCode.",
|
|
31
32
|
"status.header_running": "🟢 **OpenCode Server запущен**",
|
package/dist/opencode/events.js
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import { opencodeClient } from "./client.js";
|
|
2
2
|
import { logger } from "../utils/logger.js";
|
|
3
|
+
const RECONNECT_BASE_DELAY_MS = 1000;
|
|
4
|
+
const RECONNECT_MAX_DELAY_MS = 15000;
|
|
5
|
+
const FATAL_NO_STREAM_ERROR = "No stream returned from event subscription";
|
|
3
6
|
let eventStream = null;
|
|
4
7
|
let eventCallback = null;
|
|
5
8
|
let isListening = false;
|
|
6
9
|
let activeDirectory = null;
|
|
7
10
|
let streamAbortController = null;
|
|
11
|
+
function getReconnectDelayMs(attempt) {
|
|
12
|
+
const exponentialDelay = RECONNECT_BASE_DELAY_MS * Math.pow(2, Math.max(0, attempt - 1));
|
|
13
|
+
return Math.min(exponentialDelay, RECONNECT_MAX_DELAY_MS);
|
|
14
|
+
}
|
|
15
|
+
function waitWithAbort(ms, signal) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
if (signal.aborted) {
|
|
18
|
+
resolve(false);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const onAbort = () => {
|
|
22
|
+
clearTimeout(timeout);
|
|
23
|
+
signal.removeEventListener("abort", onAbort);
|
|
24
|
+
resolve(false);
|
|
25
|
+
};
|
|
26
|
+
const timeout = setTimeout(() => {
|
|
27
|
+
signal.removeEventListener("abort", onAbort);
|
|
28
|
+
resolve(true);
|
|
29
|
+
}, ms);
|
|
30
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
8
33
|
export async function subscribeToEvents(directory, callback) {
|
|
9
34
|
if (isListening && activeDirectory === directory) {
|
|
35
|
+
eventCallback = callback;
|
|
10
36
|
logger.debug(`Event listener already running for ${directory}`);
|
|
11
37
|
return;
|
|
12
38
|
}
|
|
@@ -23,24 +49,59 @@ export async function subscribeToEvents(directory, callback) {
|
|
|
23
49
|
isListening = true;
|
|
24
50
|
streamAbortController = controller;
|
|
25
51
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
let reconnectAttempt = 0;
|
|
53
|
+
while (isListening && activeDirectory === directory && !controller.signal.aborted) {
|
|
54
|
+
try {
|
|
55
|
+
const result = await opencodeClient.event.subscribe({ directory }, { signal: controller.signal });
|
|
56
|
+
if (!result.stream) {
|
|
57
|
+
throw new Error(FATAL_NO_STREAM_ERROR);
|
|
58
|
+
}
|
|
59
|
+
reconnectAttempt = 0;
|
|
60
|
+
eventStream = result.stream;
|
|
61
|
+
for await (const event of eventStream) {
|
|
62
|
+
if (!isListening || activeDirectory !== directory || controller.signal.aborted) {
|
|
63
|
+
logger.debug(`Event listener stopped or changed directory, breaking loop`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
// CRITICAL: Explicitly yield to the event loop BEFORE processing the event
|
|
67
|
+
// This allows grammY to handle getUpdates between SSE events
|
|
68
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
69
|
+
if (eventCallback) {
|
|
70
|
+
// Use setImmediate to avoid blocking the event loop
|
|
71
|
+
// and let grammY process incoming Telegram updates
|
|
72
|
+
const callbackSnapshot = eventCallback;
|
|
73
|
+
setImmediate(() => callbackSnapshot(event));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
eventStream = null;
|
|
77
|
+
if (!isListening || activeDirectory !== directory || controller.signal.aborted) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
reconnectAttempt++;
|
|
81
|
+
const reconnectDelay = getReconnectDelayMs(reconnectAttempt);
|
|
82
|
+
logger.warn(`Event stream ended for ${directory}, reconnecting in ${reconnectDelay}ms (attempt=${reconnectAttempt})`);
|
|
83
|
+
const shouldContinue = await waitWithAbort(reconnectDelay, controller.signal);
|
|
84
|
+
if (!shouldContinue) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
35
87
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
88
|
+
catch (error) {
|
|
89
|
+
eventStream = null;
|
|
90
|
+
if (controller.signal.aborted || !isListening || activeDirectory !== directory) {
|
|
91
|
+
logger.info("Event listener aborted");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (error instanceof Error && error.message === FATAL_NO_STREAM_ERROR) {
|
|
95
|
+
logger.error("Event stream fatal error:", error);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
reconnectAttempt++;
|
|
99
|
+
const reconnectDelay = getReconnectDelayMs(reconnectAttempt);
|
|
100
|
+
logger.error(`Event stream error for ${directory}, reconnecting in ${reconnectDelay}ms (attempt=${reconnectAttempt})`, error);
|
|
101
|
+
const shouldContinue = await waitWithAbort(reconnectDelay, controller.signal);
|
|
102
|
+
if (!shouldContinue) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
44
105
|
}
|
|
45
106
|
}
|
|
46
107
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const DEFAULT_MAX_ERROR_DETAILS_LENGTH = 1500;
|
|
2
|
+
function clipText(value, maxLength) {
|
|
3
|
+
if (value.length <= maxLength) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
7
|
+
}
|
|
8
|
+
export function formatErrorDetails(error, maxLength = DEFAULT_MAX_ERROR_DETAILS_LENGTH) {
|
|
9
|
+
let details = "";
|
|
10
|
+
if (error instanceof Error) {
|
|
11
|
+
details = error.stack ?? `${error.name}: ${error.message}`;
|
|
12
|
+
}
|
|
13
|
+
else if (typeof error === "string") {
|
|
14
|
+
details = error;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
try {
|
|
18
|
+
details = JSON.stringify(error, null, 2);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
details = String(error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const normalized = details.trim();
|
|
25
|
+
if (!normalized || normalized === "{}" || normalized === "[object Object]") {
|
|
26
|
+
return "unknown error";
|
|
27
|
+
}
|
|
28
|
+
return clipText(normalized, maxLength);
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grinev/opencode-telegram-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,7 +55,9 @@
|
|
|
55
55
|
"@opencode-ai/sdk": "^1.1.21",
|
|
56
56
|
"better-sqlite3": "^12.6.2",
|
|
57
57
|
"dotenv": "^17.2.3",
|
|
58
|
-
"grammy": "^1.39.2"
|
|
58
|
+
"grammy": "^1.39.2",
|
|
59
|
+
"https-proxy-agent": "^7.0.6",
|
|
60
|
+
"socks-proxy-agent": "^8.0.5"
|
|
59
61
|
},
|
|
60
62
|
"devDependencies": {
|
|
61
63
|
"@types/better-sqlite3": "^7.6.13",
|