@grinev/opencode-telegram-bot 0.18.0 → 0.19.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 +22 -1
- package/README.md +20 -5
- package/dist/app/start-bot-app.js +4 -0
- package/dist/attach/service.js +6 -1
- package/dist/bot/commands/definitions.js +1 -0
- package/dist/bot/commands/mcps.js +394 -0
- package/dist/bot/commands/opencode-start.js +2 -10
- package/dist/bot/index.js +6 -2
- package/dist/config.js +25 -6
- package/dist/i18n/de.js +19 -0
- package/dist/i18n/en.js +19 -0
- package/dist/i18n/es.js +19 -0
- package/dist/i18n/fr.js +19 -0
- package/dist/i18n/ru.js +19 -0
- package/dist/i18n/zh.js +19 -0
- package/dist/opencode/auto-restart.js +92 -0
- package/dist/opencode/process.js +18 -1
- package/dist/pinned/manager.js +21 -0
- package/dist/summary/aggregator.js +35 -0
- package/dist/summary/formatter.js +2 -2
- package/dist/summary/markdown-to-telegram-v2.js +99 -0
- package/dist/summary/subagent-formatter.js +23 -2
- package/dist/telegram/render/inline-renderer.js +18 -0
- package/dist/tts/client.js +94 -19
- package/package.json +2 -2
package/dist/i18n/de.js
CHANGED
|
@@ -10,6 +10,7 @@ export const de = {
|
|
|
10
10
|
"cmd.description.tasklist": "Geplante Aufgaben anzeigen",
|
|
11
11
|
"cmd.description.commands": "Benutzerdefinierte Befehle",
|
|
12
12
|
"cmd.description.skills": "Skill-Katalog",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "OpenCode-Server starten",
|
|
14
15
|
"cmd.description.opencode_stop": "OpenCode-Server stoppen",
|
|
15
16
|
"cmd.description.help": "Hilfe",
|
|
@@ -364,6 +365,24 @@ export const de = {
|
|
|
364
365
|
"skills.button.next_page": "Weiter ➡️",
|
|
365
366
|
"skills.page_empty_callback": "Keine Skills auf dieser Seite",
|
|
366
367
|
"skills.page_load_error_callback": "Diese Seite konnte nicht geladen werden. Bitte versuche es erneut.",
|
|
368
|
+
"mcps.select": "MCP servers:",
|
|
369
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
370
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
371
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
372
|
+
"mcps.enabling": "Enabling...",
|
|
373
|
+
"mcps.disabling": "Disabling...",
|
|
374
|
+
"mcps.status.connected": "🟢 Connected",
|
|
375
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
376
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
377
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
378
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
379
|
+
"mcps.detail.title": "Server: {name}",
|
|
380
|
+
"mcps.detail.status": "Status: {status}",
|
|
381
|
+
"mcps.detail.error": "Error: {error}",
|
|
382
|
+
"mcps.button.enable": "🟢 Enable",
|
|
383
|
+
"mcps.button.disable": "🔴 Disable",
|
|
384
|
+
"mcps.button.back": "⬅️ Back",
|
|
385
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
386
|
"cmd.description.rename": "Aktuelle Sitzung umbenennen",
|
|
368
387
|
"legacy.models.fetch_error": "🔴 Modellliste konnte nicht geladen werden. Prüfe den Serverstatus mit /status.",
|
|
369
388
|
"legacy.models.empty": "📋 Keine verfügbaren Modelle. Konfiguriere Provider in OpenCode.",
|
package/dist/i18n/en.js
CHANGED
|
@@ -10,6 +10,7 @@ export const en = {
|
|
|
10
10
|
"cmd.description.tasklist": "List scheduled tasks",
|
|
11
11
|
"cmd.description.commands": "Custom commands",
|
|
12
12
|
"cmd.description.skills": "Skills catalog",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "Start OpenCode server",
|
|
14
15
|
"cmd.description.opencode_stop": "Stop OpenCode server",
|
|
15
16
|
"cmd.description.help": "Help",
|
|
@@ -364,6 +365,24 @@ export const en = {
|
|
|
364
365
|
"skills.button.next_page": "Next ➡️",
|
|
365
366
|
"skills.page_empty_callback": "No skills on this page",
|
|
366
367
|
"skills.page_load_error_callback": "Cannot load this page. Please try again.",
|
|
368
|
+
"mcps.select": "MCP servers:",
|
|
369
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
370
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
371
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
372
|
+
"mcps.enabling": "Enabling...",
|
|
373
|
+
"mcps.disabling": "Disabling...",
|
|
374
|
+
"mcps.status.connected": "🟢 Connected",
|
|
375
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
376
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
377
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
378
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
379
|
+
"mcps.detail.title": "Server: {name}",
|
|
380
|
+
"mcps.detail.status": "Status: {status}",
|
|
381
|
+
"mcps.detail.error": "Error: {error}",
|
|
382
|
+
"mcps.button.enable": "🟢 Enable",
|
|
383
|
+
"mcps.button.disable": "🔴 Disable",
|
|
384
|
+
"mcps.button.back": "⬅️ Back",
|
|
385
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
386
|
"cmd.description.rename": "Rename current session",
|
|
368
387
|
"legacy.models.fetch_error": "🔴 Failed to get models list. Check server status with /status.",
|
|
369
388
|
"legacy.models.empty": "📋 No available models. Configure providers in OpenCode.",
|
package/dist/i18n/es.js
CHANGED
|
@@ -10,6 +10,7 @@ export const es = {
|
|
|
10
10
|
"cmd.description.tasklist": "Ver tareas programadas",
|
|
11
11
|
"cmd.description.commands": "Comandos personalizados",
|
|
12
12
|
"cmd.description.skills": "Catálogo de skills",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "Iniciar servidor OpenCode",
|
|
14
15
|
"cmd.description.opencode_stop": "Detener servidor OpenCode",
|
|
15
16
|
"cmd.description.help": "Ayuda",
|
|
@@ -364,6 +365,24 @@ export const es = {
|
|
|
364
365
|
"skills.button.next_page": "Siguiente ➡️",
|
|
365
366
|
"skills.page_empty_callback": "No hay skills en esta página",
|
|
366
367
|
"skills.page_load_error_callback": "No se pudo cargar esta página. Por favor, inténtalo de nuevo.",
|
|
368
|
+
"mcps.select": "MCP servers:",
|
|
369
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
370
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
371
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
372
|
+
"mcps.enabling": "Enabling...",
|
|
373
|
+
"mcps.disabling": "Disabling...",
|
|
374
|
+
"mcps.status.connected": "🟢 Connected",
|
|
375
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
376
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
377
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
378
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
379
|
+
"mcps.detail.title": "Server: {name}",
|
|
380
|
+
"mcps.detail.status": "Status: {status}",
|
|
381
|
+
"mcps.detail.error": "Error: {error}",
|
|
382
|
+
"mcps.button.enable": "🟢 Enable",
|
|
383
|
+
"mcps.button.disable": "🔴 Disable",
|
|
384
|
+
"mcps.button.back": "⬅️ Back",
|
|
385
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
386
|
"cmd.description.rename": "Renombrar la sesión actual",
|
|
368
387
|
"legacy.models.fetch_error": "🔴 No se pudo obtener la lista de modelos. Revisa el estado del servidor con /status.",
|
|
369
388
|
"legacy.models.empty": "📋 No hay modelos disponibles. Configura los proveedores en OpenCode.",
|
package/dist/i18n/fr.js
CHANGED
|
@@ -10,6 +10,7 @@ export const fr = {
|
|
|
10
10
|
"cmd.description.tasklist": "Afficher les tâches planifiées",
|
|
11
11
|
"cmd.description.commands": "Commandes personnalisées",
|
|
12
12
|
"cmd.description.skills": "Catalogue de skills",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "Démarrer le serveur OpenCode",
|
|
14
15
|
"cmd.description.opencode_stop": "Arrêter le serveur OpenCode",
|
|
15
16
|
"cmd.description.help": "Aide",
|
|
@@ -364,6 +365,24 @@ export const fr = {
|
|
|
364
365
|
"skills.button.next_page": "Suivant ➡️",
|
|
365
366
|
"skills.page_empty_callback": "Aucun skill sur cette page",
|
|
366
367
|
"skills.page_load_error_callback": "Impossible de charger cette page. Veuillez réessayer.",
|
|
368
|
+
"mcps.select": "MCP servers:",
|
|
369
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
370
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
371
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
372
|
+
"mcps.enabling": "Enabling...",
|
|
373
|
+
"mcps.disabling": "Disabling...",
|
|
374
|
+
"mcps.status.connected": "🟢 Connected",
|
|
375
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
376
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
377
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
378
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
379
|
+
"mcps.detail.title": "Server: {name}",
|
|
380
|
+
"mcps.detail.status": "Status: {status}",
|
|
381
|
+
"mcps.detail.error": "Error: {error}",
|
|
382
|
+
"mcps.button.enable": "🟢 Enable",
|
|
383
|
+
"mcps.button.disable": "🔴 Disable",
|
|
384
|
+
"mcps.button.back": "⬅️ Back",
|
|
385
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
386
|
"cmd.description.rename": "Renommer la session actuelle",
|
|
368
387
|
"legacy.models.fetch_error": "🔴 Impossible de récupérer la liste des modèles. Vérifiez l'état du serveur avec /status.",
|
|
369
388
|
"legacy.models.empty": "📋 Aucun modèle disponible. Configurez les fournisseurs dans OpenCode.",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -10,6 +10,7 @@ export const ru = {
|
|
|
10
10
|
"cmd.description.tasklist": "Список задач по расписанию",
|
|
11
11
|
"cmd.description.commands": "Пользовательские команды",
|
|
12
12
|
"cmd.description.skills": "Каталог скиллов",
|
|
13
|
+
"cmd.description.mcps": "MCP серверы",
|
|
13
14
|
"cmd.description.opencode_start": "Запустить OpenCode сервер",
|
|
14
15
|
"cmd.description.opencode_stop": "Остановить OpenCode сервер",
|
|
15
16
|
"cmd.description.help": "Справка",
|
|
@@ -364,6 +365,24 @@ export const ru = {
|
|
|
364
365
|
"skills.button.next_page": "Вперёд ➡️",
|
|
365
366
|
"skills.page_empty_callback": "На этой странице нет скиллов",
|
|
366
367
|
"skills.page_load_error_callback": "Не удалось загрузить эту страницу. Пожалуйста, попробуйте снова.",
|
|
368
|
+
"mcps.select": "MCP серверы:",
|
|
369
|
+
"mcps.empty": "📭 MCP серверы не настроены.",
|
|
370
|
+
"mcps.fetch_error": "🔴 Не удалось загрузить MCP серверы.",
|
|
371
|
+
"mcps.toggle_error": "🔴 Не удалось переключить MCP сервер.",
|
|
372
|
+
"mcps.enabling": "Включаю...",
|
|
373
|
+
"mcps.disabling": "Выключаю...",
|
|
374
|
+
"mcps.status.connected": "🟢 Подключен",
|
|
375
|
+
"mcps.status.disabled": "🔴 Отключен",
|
|
376
|
+
"mcps.status.failed": "⚠️ Ошибка",
|
|
377
|
+
"mcps.status.needs_auth": "🔒 Требуется авторизация",
|
|
378
|
+
"mcps.status.needs_client_registration": "🔒 Требуется регистрация",
|
|
379
|
+
"mcps.detail.title": "Сервер: {name}",
|
|
380
|
+
"mcps.detail.status": "Статус: {status}",
|
|
381
|
+
"mcps.detail.error": "Ошибка: {error}",
|
|
382
|
+
"mcps.button.enable": "🟢 Включить",
|
|
383
|
+
"mcps.button.disable": "🔴 Выключить",
|
|
384
|
+
"mcps.button.back": "⬅️ Назад",
|
|
385
|
+
"mcps.auth_required": "Этот сервер требует авторизации и не может быть включен из бота.",
|
|
367
386
|
"cmd.description.rename": "Переименовать текущую сессию",
|
|
368
387
|
"legacy.models.fetch_error": "🔴 Не удалось получить список моделей. Проверьте статус сервера /status.",
|
|
369
388
|
"legacy.models.empty": "📋 Нет доступных моделей. Настройте провайдеры через OpenCode.",
|
package/dist/i18n/zh.js
CHANGED
|
@@ -10,6 +10,7 @@ export const zh = {
|
|
|
10
10
|
"cmd.description.tasklist": "查看定时任务",
|
|
11
11
|
"cmd.description.commands": "自定义命令",
|
|
12
12
|
"cmd.description.skills": "技能目录",
|
|
13
|
+
"cmd.description.mcps": "MCP servers",
|
|
13
14
|
"cmd.description.opencode_start": "启动 OpenCode 服务器",
|
|
14
15
|
"cmd.description.opencode_stop": "停止 OpenCode 服务器",
|
|
15
16
|
"cmd.description.help": "帮助",
|
|
@@ -364,6 +365,24 @@ export const zh = {
|
|
|
364
365
|
"skills.button.next_page": "下一页 ➡️",
|
|
365
366
|
"skills.page_empty_callback": "这一页没有技能",
|
|
366
367
|
"skills.page_load_error_callback": "无法加载此页面。请重试。",
|
|
368
|
+
"mcps.select": "MCP servers:",
|
|
369
|
+
"mcps.empty": "📭 No MCP servers configured.",
|
|
370
|
+
"mcps.fetch_error": "🔴 Failed to load MCP servers.",
|
|
371
|
+
"mcps.toggle_error": "🔴 Failed to toggle MCP server.",
|
|
372
|
+
"mcps.enabling": "Enabling...",
|
|
373
|
+
"mcps.disabling": "Disabling...",
|
|
374
|
+
"mcps.status.connected": "🟢 Connected",
|
|
375
|
+
"mcps.status.disabled": "🔴 Disabled",
|
|
376
|
+
"mcps.status.failed": "⚠️ Failed",
|
|
377
|
+
"mcps.status.needs_auth": "🔒 Needs auth",
|
|
378
|
+
"mcps.status.needs_client_registration": "🔒 Needs registration",
|
|
379
|
+
"mcps.detail.title": "Server: {name}",
|
|
380
|
+
"mcps.detail.status": "Status: {status}",
|
|
381
|
+
"mcps.detail.error": "Error: {error}",
|
|
382
|
+
"mcps.button.enable": "🟢 Enable",
|
|
383
|
+
"mcps.button.disable": "🔴 Disable",
|
|
384
|
+
"mcps.button.back": "⬅️ Back",
|
|
385
|
+
"mcps.auth_required": "This server requires authorization and cannot be enabled from the bot.",
|
|
367
386
|
"cmd.description.rename": "重命名当前会话",
|
|
368
387
|
"legacy.models.fetch_error": "🔴 获取模型列表失败。请使用 /status 检查服务器状态。",
|
|
369
388
|
"legacy.models.empty": "📋 没有可用模型。请在 OpenCode 中配置 providers。",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { opencodeClient } from "./client.js";
|
|
4
|
+
import { resolveLocalOpencodeTarget, startLocalOpencodeServer, } from "./process.js";
|
|
5
|
+
const SERVER_READY_TIMEOUT_MS = 10000;
|
|
6
|
+
const SERVER_READY_POLL_INTERVAL_MS = 500;
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
async function isOpencodeServerHealthy() {
|
|
11
|
+
try {
|
|
12
|
+
const { data, error } = await opencodeClient.global.health();
|
|
13
|
+
return !error && data?.healthy === true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function waitForOpencodeServerReady(timeoutMs) {
|
|
20
|
+
const startedAt = Date.now();
|
|
21
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
22
|
+
if (await isOpencodeServerHealthy()) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
await sleep(SERVER_READY_POLL_INTERVAL_MS);
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
export class OpencodeAutoRestartService {
|
|
30
|
+
timer = null;
|
|
31
|
+
localTarget = null;
|
|
32
|
+
started = false;
|
|
33
|
+
checkInProgress = false;
|
|
34
|
+
async start() {
|
|
35
|
+
if (this.started || !config.opencode.autoRestartEnabled) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const localTarget = resolveLocalOpencodeTarget(config.opencode.apiUrl);
|
|
39
|
+
if (!localTarget) {
|
|
40
|
+
logger.warn(`[OpenCodeAutoRestart] Disabled because OPENCODE_API_URL is not local: ${config.opencode.apiUrl}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
this.started = true;
|
|
44
|
+
this.localTarget = localTarget;
|
|
45
|
+
logger.info(`[OpenCodeAutoRestart] Enabled: port=${localTarget.port}, intervalSec=${config.opencode.monitorIntervalSec}`);
|
|
46
|
+
await this.checkAndRestart("startup");
|
|
47
|
+
this.timer = setInterval(() => {
|
|
48
|
+
void this.checkAndRestart("interval");
|
|
49
|
+
}, config.opencode.monitorIntervalSec * 1000);
|
|
50
|
+
this.timer.unref?.();
|
|
51
|
+
}
|
|
52
|
+
stop() {
|
|
53
|
+
if (this.timer) {
|
|
54
|
+
clearInterval(this.timer);
|
|
55
|
+
this.timer = null;
|
|
56
|
+
}
|
|
57
|
+
this.started = false;
|
|
58
|
+
this.localTarget = null;
|
|
59
|
+
}
|
|
60
|
+
async checkAndRestart(reason) {
|
|
61
|
+
if (this.checkInProgress || !this.localTarget) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.checkInProgress = true;
|
|
65
|
+
try {
|
|
66
|
+
if (await isOpencodeServerHealthy()) {
|
|
67
|
+
logger.debug(`[OpenCodeAutoRestart] Health-check succeeded: reason=${reason}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
logger.warn(`[OpenCodeAutoRestart] OpenCode server is unavailable, starting local server: reason=${reason}, port=${this.localTarget.port}`);
|
|
71
|
+
const childProcess = startLocalOpencodeServer(this.localTarget);
|
|
72
|
+
childProcess.once("error", (error) => {
|
|
73
|
+
logger.error("[OpenCodeAutoRestart] OpenCode server process failed to start", error);
|
|
74
|
+
});
|
|
75
|
+
const pid = childProcess.pid;
|
|
76
|
+
childProcess.unref();
|
|
77
|
+
const ready = await waitForOpencodeServerReady(SERVER_READY_TIMEOUT_MS);
|
|
78
|
+
if (!ready) {
|
|
79
|
+
logger.warn(`[OpenCodeAutoRestart] OpenCode server was started but did not become ready: pid=${pid ?? "unknown"}, port=${this.localTarget.port}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
logger.info(`[OpenCodeAutoRestart] OpenCode server recovered: pid=${pid ?? "unknown"}, port=${this.localTarget.port}`);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
logger.error("[OpenCodeAutoRestart] Failed to check or restart OpenCode server", error);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
this.checkInProgress = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export const opencodeAutoRestartService = new OpencodeAutoRestartService();
|
package/dist/opencode/process.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { exec } from "node:child_process";
|
|
1
|
+
import { exec, spawn } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
const execAsync = promisify(exec);
|
|
4
4
|
const DEFAULT_OPENCODE_PORT = 4096;
|
|
@@ -25,6 +25,23 @@ export function resolveLocalOpencodeTarget(apiUrl) {
|
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
export function createOpencodeServeSpawnCommand(target) {
|
|
29
|
+
const isWindows = process.platform === "win32";
|
|
30
|
+
const port = target.port.toString();
|
|
31
|
+
return {
|
|
32
|
+
command: isWindows ? "cmd.exe" : "opencode",
|
|
33
|
+
args: isWindows ? ["/c", "opencode", "serve", "--port", port] : ["serve", "--port", port],
|
|
34
|
+
windowsHide: isWindows,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function startLocalOpencodeServer(target) {
|
|
38
|
+
const spawnCommand = createOpencodeServeSpawnCommand(target);
|
|
39
|
+
return spawn(spawnCommand.command, spawnCommand.args, {
|
|
40
|
+
detached: true,
|
|
41
|
+
stdio: "ignore",
|
|
42
|
+
windowsHide: spawnCommand.windowsHide,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
28
45
|
function parsePid(value) {
|
|
29
46
|
const pid = Number.parseInt(value.trim(), 10);
|
|
30
47
|
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
package/dist/pinned/manager.js
CHANGED
|
@@ -77,6 +77,27 @@ class PinnedMessageManager {
|
|
|
77
77
|
// Load existing diffs from API (for session restoration)
|
|
78
78
|
await this.loadDiffsFromApi(sessionId);
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Restore in-memory state for a persisted pinned message without creating a new Telegram message.
|
|
82
|
+
*/
|
|
83
|
+
async restoreExistingSession(sessionId, sessionTitle) {
|
|
84
|
+
logger.info(`[PinnedManager] Restoring existing pinned message for session: ${sessionId}`);
|
|
85
|
+
this.state.sessionId = sessionId;
|
|
86
|
+
this.state.sessionTitle = sessionTitle || t("pinned.default_session_title");
|
|
87
|
+
this.state.attachActive = false;
|
|
88
|
+
this.state.attachBusy = false;
|
|
89
|
+
this.state.changedFiles = [];
|
|
90
|
+
this.lastRenderedMessageText = null;
|
|
91
|
+
this.pendingUpdate = false;
|
|
92
|
+
this.pendingForceUpdate = false;
|
|
93
|
+
await this.refreshProjectMetadata();
|
|
94
|
+
await this.fetchContextLimit();
|
|
95
|
+
if (this.onKeyboardUpdateCallback && this.state.tokensLimit > 0) {
|
|
96
|
+
this.onKeyboardUpdateCallback(this.state.tokensUsed, this.state.tokensLimit);
|
|
97
|
+
}
|
|
98
|
+
await this.updatePinnedMessage(true);
|
|
99
|
+
await this.loadDiffsFromApi(sessionId);
|
|
100
|
+
}
|
|
80
101
|
/**
|
|
81
102
|
* Called when session title is updated (after first message)
|
|
82
103
|
*/
|
|
@@ -24,6 +24,17 @@ function countDiffChangesFromText(text) {
|
|
|
24
24
|
}
|
|
25
25
|
return { additions, deletions };
|
|
26
26
|
}
|
|
27
|
+
function normalizeSnapshotValue(value) {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
return value.map((item) => normalizeSnapshotValue(item));
|
|
30
|
+
}
|
|
31
|
+
if (value && typeof value === "object") {
|
|
32
|
+
return Object.fromEntries(Object.entries(value)
|
|
33
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
34
|
+
.map(([key, entryValue]) => [key, normalizeSnapshotValue(entryValue)]));
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
27
38
|
class SummaryAggregator {
|
|
28
39
|
currentSessionId = null;
|
|
29
40
|
textMessageStates = new Map();
|
|
@@ -65,6 +76,7 @@ class SummaryAggregator {
|
|
|
65
76
|
pendingSubagentCardIdsByParent = new Map();
|
|
66
77
|
pendingChildSessionIdsByParent = new Map();
|
|
67
78
|
fallbackSubagentCardIdsByParent = new Map();
|
|
79
|
+
lastSubagentSnapshot = "";
|
|
68
80
|
setBotAndChatId(bot, chatId) {
|
|
69
81
|
this.bot = bot;
|
|
70
82
|
this.chatId = chatId;
|
|
@@ -239,6 +251,7 @@ class SummaryAggregator {
|
|
|
239
251
|
this.pendingSubagentCardIdsByParent.clear();
|
|
240
252
|
this.pendingChildSessionIdsByParent.clear();
|
|
241
253
|
this.fallbackSubagentCardIdsByParent.clear();
|
|
254
|
+
this.lastSubagentSnapshot = "";
|
|
242
255
|
this.messageCount = 0;
|
|
243
256
|
this.lastUpdated = 0;
|
|
244
257
|
if (this.onClearedCallback) {
|
|
@@ -312,6 +325,28 @@ class SummaryAggregator {
|
|
|
312
325
|
terminalMessage: state.terminalMessage,
|
|
313
326
|
updatedAt: state.updatedAt,
|
|
314
327
|
}));
|
|
328
|
+
const snapshot = JSON.stringify(subagents.map((subagent) => ({
|
|
329
|
+
cardId: subagent.cardId,
|
|
330
|
+
sessionId: subagent.sessionId,
|
|
331
|
+
parentSessionId: subagent.parentSessionId,
|
|
332
|
+
agent: subagent.agent,
|
|
333
|
+
description: subagent.description,
|
|
334
|
+
prompt: subagent.prompt,
|
|
335
|
+
command: subagent.command,
|
|
336
|
+
status: subagent.status,
|
|
337
|
+
providerID: subagent.providerID,
|
|
338
|
+
modelID: subagent.modelID,
|
|
339
|
+
tokens: subagent.tokens,
|
|
340
|
+
cost: subagent.cost,
|
|
341
|
+
currentTool: subagent.currentTool,
|
|
342
|
+
currentToolInput: normalizeSnapshotValue(subagent.currentToolInput),
|
|
343
|
+
currentToolTitle: subagent.currentToolTitle,
|
|
344
|
+
terminalMessage: subagent.terminalMessage,
|
|
345
|
+
})));
|
|
346
|
+
if (snapshot === this.lastSubagentSnapshot) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.lastSubagentSnapshot = snapshot;
|
|
315
350
|
this.onSubagentCallback(this.currentSessionId, subagents);
|
|
316
351
|
}
|
|
317
352
|
createSubagentState(parentSessionId, sessionId, cardId = `subagent-${parentSessionId}-${Date.now()}-${this.subagentOrder.length}`) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
-
import { convert } from "telegram-markdown-v2";
|
|
3
2
|
import { config } from "../config.js";
|
|
4
3
|
import { logger } from "../utils/logger.js";
|
|
5
4
|
import { t } from "../i18n/index.js";
|
|
6
5
|
import { getCurrentProject } from "../settings/manager.js";
|
|
6
|
+
import { convertToTelegramMarkdownV2 } from "./markdown-to-telegram-v2.js";
|
|
7
7
|
import { normalizeMarkdownForTelegramRendering } from "../telegram/render/markdown-normalizer.js";
|
|
8
8
|
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
9
9
|
const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
|
|
@@ -90,7 +90,7 @@ export function escapePlainTextForTelegramMarkdownV2(text) {
|
|
|
90
90
|
function formatMarkdownForTelegram(text) {
|
|
91
91
|
try {
|
|
92
92
|
const preprocessed = normalizeMarkdownForTelegramRendering(text);
|
|
93
|
-
return escapeMarkdownV2PipesOutsideCode(
|
|
93
|
+
return escapeMarkdownV2PipesOutsideCode(convertToTelegramMarkdownV2(preprocessed));
|
|
94
94
|
}
|
|
95
95
|
catch (error) {
|
|
96
96
|
logger.warn("[Formatter] Failed to convert markdown summary, falling back to raw text", error);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { parseTelegramBlocks } from "../telegram/render/block-parser.js";
|
|
2
|
+
import { normalizeMarkdownForTelegramRendering } from "../telegram/render/markdown-normalizer.js";
|
|
3
|
+
/**
|
|
4
|
+
* Escapes characters reserved in Telegram MarkdownV2.
|
|
5
|
+
* These must be escaped everywhere except when used as syntactic delimiters
|
|
6
|
+
* we intentionally emit (e.g. the surrounding * of bold).
|
|
7
|
+
*/
|
|
8
|
+
function escapeMarkdownV2(text) {
|
|
9
|
+
return text.replace(/([_*[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
|
|
10
|
+
}
|
|
11
|
+
function escapeMarkdownV2Code(text) {
|
|
12
|
+
return text.replace(/([`\\])/g, "\\$1");
|
|
13
|
+
}
|
|
14
|
+
function escapeMarkdownV2LinkUrl(text) {
|
|
15
|
+
return text.replace(/([)\\])/g, "\\$1");
|
|
16
|
+
}
|
|
17
|
+
function renderInlineNodes(nodes) {
|
|
18
|
+
return nodes
|
|
19
|
+
.map((node) => {
|
|
20
|
+
switch (node.type) {
|
|
21
|
+
case "text":
|
|
22
|
+
return escapeMarkdownV2(node.text);
|
|
23
|
+
case "bold":
|
|
24
|
+
return `*${renderInlineNodes(node.children)}*`;
|
|
25
|
+
case "italic":
|
|
26
|
+
return `_${renderInlineNodes(node.children)}_`;
|
|
27
|
+
case "strike":
|
|
28
|
+
return `~${renderInlineNodes(node.children)}~`;
|
|
29
|
+
case "code":
|
|
30
|
+
return `\`${escapeMarkdownV2Code(node.text)}\``;
|
|
31
|
+
case "link":
|
|
32
|
+
return `[${renderInlineNodes(node.text)}](${escapeMarkdownV2LinkUrl(node.url)})`;
|
|
33
|
+
case "underline":
|
|
34
|
+
return `__${renderInlineNodes(node.children)}__`;
|
|
35
|
+
case "spoiler":
|
|
36
|
+
return `||${renderInlineNodes(node.children)}||`;
|
|
37
|
+
default:
|
|
38
|
+
return escapeMarkdownV2(String(node));
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
.join("");
|
|
42
|
+
}
|
|
43
|
+
function renderBlock(block) {
|
|
44
|
+
switch (block.type) {
|
|
45
|
+
case "paragraph":
|
|
46
|
+
return renderInlineNodes(block.inlines);
|
|
47
|
+
case "heading": {
|
|
48
|
+
const content = renderInlineNodes(block.inlines);
|
|
49
|
+
return `*${content}*`;
|
|
50
|
+
}
|
|
51
|
+
case "blockquote":
|
|
52
|
+
return block.lines
|
|
53
|
+
.map((line) => renderInlineNodes(line)
|
|
54
|
+
.split("\n")
|
|
55
|
+
.map((part) => `> ${part}`)
|
|
56
|
+
.join("\n"))
|
|
57
|
+
.join("\n");
|
|
58
|
+
case "list":
|
|
59
|
+
return block.items
|
|
60
|
+
.map((item, index) => {
|
|
61
|
+
const prefix = block.ordered ? `${index + 1}\\. ` : "\\- ";
|
|
62
|
+
const body = renderInlineNodes(item);
|
|
63
|
+
return `${prefix}${body}`;
|
|
64
|
+
})
|
|
65
|
+
.join("\n");
|
|
66
|
+
case "code":
|
|
67
|
+
return `\`\`\`${escapeMarkdownV2Code(block.language ?? "")}\n${escapeMarkdownV2Code(block.text)}\n\`\`\``;
|
|
68
|
+
case "table": {
|
|
69
|
+
if (block.rows.length === 0) {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
const header = `\\| ${block.rows[0].map(escapeMarkdownV2).join(" \\| ")} \\|`;
|
|
73
|
+
const separator = `\\| ${block.rows[0].map(() => "\\-\\-\\-").join(" \\| ")} \\|`;
|
|
74
|
+
const body = block.rows
|
|
75
|
+
.slice(1)
|
|
76
|
+
.map((row) => `\\| ${row.map(escapeMarkdownV2).join(" \\| ")} \\|`);
|
|
77
|
+
return [header, separator, ...body].join("\n");
|
|
78
|
+
}
|
|
79
|
+
case "rule":
|
|
80
|
+
return "---";
|
|
81
|
+
case "plain":
|
|
82
|
+
return escapeMarkdownV2(block.text);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Converts standard Markdown into Telegram MarkdownV2.
|
|
87
|
+
*
|
|
88
|
+
* Uses the project's existing block parser (unified/remark) so that
|
|
89
|
+
* headings, lists, tables, code blocks, etc. are handled consistently
|
|
90
|
+
* with the entity-based Telegram renderer.
|
|
91
|
+
*/
|
|
92
|
+
export function convertToTelegramMarkdownV2(markdown) {
|
|
93
|
+
const normalized = normalizeMarkdownForTelegramRendering(markdown);
|
|
94
|
+
const blocks = parseTelegramBlocks(normalized);
|
|
95
|
+
if (blocks.length === 0) {
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
return blocks.map(renderBlock).join("\n\n");
|
|
99
|
+
}
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import { formatModelDisplayName } from "../pinned/format.js";
|
|
2
2
|
import { t } from "../i18n/index.js";
|
|
3
3
|
import { formatCompactToolInfo } from "./formatter.js";
|
|
4
|
+
function shouldPreferInputDetails(tool, input) {
|
|
5
|
+
if (!input) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
switch (tool) {
|
|
9
|
+
case "read":
|
|
10
|
+
case "edit":
|
|
11
|
+
case "write":
|
|
12
|
+
case "apply_patch":
|
|
13
|
+
return typeof input.path === "string" || typeof input.filePath === "string";
|
|
14
|
+
case "bash":
|
|
15
|
+
return typeof input.command === "string";
|
|
16
|
+
case "grep":
|
|
17
|
+
case "glob":
|
|
18
|
+
return typeof input.pattern === "string";
|
|
19
|
+
}
|
|
20
|
+
return ["query", "url", "name", "prompt", "text"].some((field) => typeof input[field] === "string");
|
|
21
|
+
}
|
|
4
22
|
function formatToolStep(subagent) {
|
|
5
23
|
if (!subagent.currentTool) {
|
|
6
24
|
return "";
|
|
7
25
|
}
|
|
26
|
+
const toolTitle = shouldPreferInputDetails(subagent.currentTool, subagent.currentToolInput)
|
|
27
|
+
? undefined
|
|
28
|
+
: subagent.currentToolTitle;
|
|
8
29
|
const toolInfo = {
|
|
9
30
|
sessionId: subagent.sessionId ?? subagent.parentSessionId,
|
|
10
31
|
messageId: subagent.cardId,
|
|
@@ -13,12 +34,12 @@ function formatToolStep(subagent) {
|
|
|
13
34
|
state: {
|
|
14
35
|
status: "running",
|
|
15
36
|
input: subagent.currentToolInput ?? {},
|
|
16
|
-
title:
|
|
37
|
+
title: toolTitle,
|
|
17
38
|
metadata: {},
|
|
18
39
|
time: { start: subagent.updatedAt },
|
|
19
40
|
},
|
|
20
41
|
input: subagent.currentToolInput,
|
|
21
|
-
title:
|
|
42
|
+
title: toolTitle,
|
|
22
43
|
metadata: {},
|
|
23
44
|
hasFileAttachment: false,
|
|
24
45
|
};
|
|
@@ -32,6 +32,18 @@ function pushEntity(state, entity) {
|
|
|
32
32
|
}
|
|
33
33
|
state.entities.push(entity);
|
|
34
34
|
}
|
|
35
|
+
function isTelegramTextLinkUrl(url) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(url);
|
|
38
|
+
return ["http:", "https:", "tg:", "mailto:"].includes(parsed.protocol);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isLocalReferenceUrl(url) {
|
|
45
|
+
return url.startsWith("#") || url.startsWith("/") || url.startsWith("./") || url.startsWith("../");
|
|
46
|
+
}
|
|
35
47
|
function renderIntoState(state, nodes) {
|
|
36
48
|
for (const node of nodes) {
|
|
37
49
|
switch (node.type) {
|
|
@@ -81,6 +93,12 @@ function renderIntoState(state, nodes) {
|
|
|
81
93
|
case "link": {
|
|
82
94
|
const offset = state.text.length;
|
|
83
95
|
renderIntoState(state, node.text);
|
|
96
|
+
if (!isTelegramTextLinkUrl(node.url)) {
|
|
97
|
+
if (isLocalReferenceUrl(node.url)) {
|
|
98
|
+
appendText(state, ` (${node.url})`);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
84
102
|
pushEntity(state, {
|
|
85
103
|
type: "text_link",
|
|
86
104
|
offset,
|