@link-assistant/hive-mind 1.56.19 → 1.57.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/CHANGELOG.md +17 -0
- package/README.hi.md +43 -2
- package/README.md +40 -2
- package/README.ru.md +45 -2
- package/README.zh.md +39 -2
- package/package.json +2 -2
- package/src/telegram-bot.mjs +18 -24
- package/src/telegram-safe-reply.lib.mjs +19 -0
- package/src/telegram-terminal-watch-command.lib.mjs +412 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.57.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 272a2d4: Add live terminal watch support for hive-telegram-bot
|
|
8
|
+
|
|
9
|
+
This feature adds `/terminal_watch` plus the experimental `--auto-start-screen-watch-message` option. The command watches the log reported by `$ --status <uuid>` and updates a separate Telegram message with a terminal-sized text snapshot.
|
|
10
|
+
|
|
11
|
+
Key features:
|
|
12
|
+
- Manual `/terminal_watch <uuid>` command, including reply-based usage
|
|
13
|
+
- Configurable terminal snapshot size with `--size`, `--width`, and `--height`
|
|
14
|
+
- Auto-freezes the watch message and attaches the full log when the session ends
|
|
15
|
+
- Public repository logs can update in chat; private/unknown visibility uses DM for manual watches
|
|
16
|
+
- Auto-start remains off by default and never starts for private or unknown-visibility repositories
|
|
17
|
+
|
|
18
|
+
Based on the proof-of-concept from konard/telegram-terminal-bot.
|
|
19
|
+
|
|
3
20
|
## 1.56.19
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.hi.md
CHANGED
|
@@ -431,9 +431,21 @@ Hive Mind को क्रिया में देखना चाहते
|
|
|
431
431
|
hive-telegram-bot 2>&1 | tee -a "logs/bot-$(date +%Y%m%d-%H%M%S).log"
|
|
432
432
|
```
|
|
433
433
|
|
|
434
|
+
**Experimental: live terminal watch**
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
hive-telegram-bot --auto-start-screen-watch-message
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
यह opt-in flag सार्वजनिक `/solve` sessions के लिए एक अलग live terminal
|
|
441
|
+
message शुरू करता है। Private या unknown-visibility repositories के लिए watch
|
|
442
|
+
message अपने आप शुरू नहीं होता।
|
|
443
|
+
|
|
434
444
|
### बॉट कमांड
|
|
435
445
|
|
|
436
|
-
|
|
446
|
+
अधिकांश operational commands केवल **ग्रुप चैट में** काम करते हैं (बॉट के साथ
|
|
447
|
+
निजी संदेशों में नहीं)। `/terminal_watch` जैसे commands, जो जानबूझकर private
|
|
448
|
+
updates भेजते हैं, direct messages में भी उपयोग किए जा सकते हैं:
|
|
437
449
|
|
|
438
450
|
#### `/solve` - GitHub इश्यू हल करें
|
|
439
451
|
|
|
@@ -494,6 +506,22 @@ Shows:
|
|
|
494
506
|
- Claude usage limits (session and weekly)
|
|
495
507
|
```
|
|
496
508
|
|
|
509
|
+
#### `/terminal_watch` - Live Session Log
|
|
510
|
+
|
|
511
|
+
```
|
|
512
|
+
/terminal_watch <uuid> [--size 120x25]
|
|
513
|
+
|
|
514
|
+
Examples:
|
|
515
|
+
/terminal_watch 4d934f71-4cdb-4b8c-b474-582116d12c12
|
|
516
|
+
/terminal_watch 4d934f71-4cdb-4b8c-b474-582116d12c12 --width 100 --height 20
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
आप bot session message पर `/terminal_watch` के साथ reply भी कर सकते हैं। यह
|
|
520
|
+
command `$ --status <uuid>` द्वारा report किए गए session log की latest lines के
|
|
521
|
+
साथ एक अलग Telegram message update करता है और session खत्म होने पर पूरी log file
|
|
522
|
+
attach करता है। Public repository logs chat में watch किए जा सकते हैं; private या
|
|
523
|
+
unknown-visibility repository logs केवल direct message से भेजे जाते हैं।
|
|
524
|
+
|
|
497
525
|
#### `/help` - सहायता और डायग्नोस्टिक जानकारी प्राप्त करें
|
|
498
526
|
|
|
499
527
|
```
|
|
@@ -508,12 +536,25 @@ Shows:
|
|
|
508
536
|
|
|
509
537
|
### विशेषताएँ
|
|
510
538
|
|
|
511
|
-
- ✅
|
|
539
|
+
- ✅ **Group Chat Execution**: `/solve` और `/hive` workflows authorized group chats से चलते हैं
|
|
512
540
|
- ✅ **पूर्ण विकल्प सपोर्ट**: सभी कमांड-लाइन विकल्प Telegram में काम करते हैं
|
|
513
541
|
- ✅ **Screen सत्र**: कमांड डिटैच्ड screen सत्रों में चलते हैं
|
|
542
|
+
- ✅ **Live Terminal Watch**: `/terminal_watch` और opt-in auto-start live session logs दिखाते हैं
|
|
514
543
|
- ✅ **चैट प्रतिबंध**: अनुमत चैट ID की वैकल्पिक सफेद सूची
|
|
515
544
|
- ✅ **डायग्नोस्टिक टूल**: चैट ID और कॉन्फ़िगरेशन जानकारी प्राप्त करें
|
|
516
545
|
|
|
546
|
+
#### Live Terminal Watch
|
|
547
|
+
|
|
548
|
+
`--auto-start-screen-watch-message` से enabled होने पर, bot public `/solve`
|
|
549
|
+
sessions के लिए अपने आप एक अलग live terminal watch message शुरू करता है:
|
|
550
|
+
|
|
551
|
+
- **Manual Watch**: `/terminal_watch <uuid>` या `/terminal_watch` के साथ reply
|
|
552
|
+
- **Real-time Updates**: Commands execute होते समय live session log output देखें
|
|
553
|
+
- **Auto-freeze**: Command पूरा होने पर message freeze हो जाता है
|
|
554
|
+
- **Log Attachment**: Session खत्म होने पर full logs अपने आप attach होते हैं
|
|
555
|
+
- **Security**: Private या unknown-visibility repositories के लिए auto-start disabled है
|
|
556
|
+
- **Smart Updates**: केवल वास्तविक बदलाव मिलने पर update करता है (API limits से बचने के लिए rate-limited)
|
|
557
|
+
|
|
517
558
|
### सुरक्षा नोट
|
|
518
559
|
|
|
519
560
|
- केवल उन ग्रुप चैट में काम करता है जहाँ बॉट एडमिन है
|
package/README.md
CHANGED
|
@@ -440,9 +440,19 @@ Want to see the Hive Mind in action? Request a free demo or get faster support b
|
|
|
440
440
|
hive-telegram-bot 2>&1 | tee -a "logs/bot-$(date +%Y%m%d-%H%M%S).log"
|
|
441
441
|
```
|
|
442
442
|
|
|
443
|
+
**Experimental: live terminal watch**
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
hive-telegram-bot --auto-start-screen-watch-message
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
This opt-in flag starts a separate live terminal message for public `/solve`
|
|
450
|
+
sessions. Private or unknown-visibility repositories never auto-start a
|
|
451
|
+
watch message.
|
|
452
|
+
|
|
443
453
|
### Bot Commands
|
|
444
454
|
|
|
445
|
-
|
|
455
|
+
Most operational commands work in **group chats only** (not in private messages with the bot). Commands that intentionally deliver private updates, such as `/terminal_watch`, may also be used in direct messages:
|
|
446
456
|
|
|
447
457
|
#### `/solve` - Solve GitHub Issues
|
|
448
458
|
|
|
@@ -514,6 +524,22 @@ Shows:
|
|
|
514
524
|
- Claude usage limits (session and weekly)
|
|
515
525
|
```
|
|
516
526
|
|
|
527
|
+
#### `/terminal_watch` - Live Session Log
|
|
528
|
+
|
|
529
|
+
```
|
|
530
|
+
/terminal_watch <uuid> [--size 120x25]
|
|
531
|
+
|
|
532
|
+
Examples:
|
|
533
|
+
/terminal_watch 4d934f71-4cdb-4b8c-b474-582116d12c12
|
|
534
|
+
/terminal_watch 4d934f71-4cdb-4b8c-b474-582116d12c12 --width 100 --height 20
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
You can also reply to a bot session message with `/terminal_watch`. The command
|
|
538
|
+
updates a separate Telegram message with the latest lines from the session log
|
|
539
|
+
reported by `$ --status <uuid>` and attaches the full log file when the session
|
|
540
|
+
finishes. Public repository logs can be watched in the chat; private or
|
|
541
|
+
unknown-visibility repository logs are delivered by direct message only.
|
|
542
|
+
|
|
517
543
|
#### `/help` - Get Help and Diagnostic Info
|
|
518
544
|
|
|
519
545
|
```
|
|
@@ -528,12 +554,24 @@ Shows:
|
|
|
528
554
|
|
|
529
555
|
### Features
|
|
530
556
|
|
|
531
|
-
- ✅ **Group Chat
|
|
557
|
+
- ✅ **Group Chat Execution**: `/solve` and `/hive` workflows run from authorized group chats
|
|
532
558
|
- ✅ **Full Options Support**: All command-line options work in Telegram
|
|
533
559
|
- ✅ **Screen Sessions**: Commands run in detached screen sessions
|
|
560
|
+
- ✅ **Live Terminal Watch**: `/terminal_watch` and opt-in auto-start show live session logs
|
|
534
561
|
- ✅ **Chat Restrictions**: Optional whitelist of allowed chat IDs
|
|
535
562
|
- ✅ **Diagnostic Tools**: Get chat ID and configuration info
|
|
536
563
|
|
|
564
|
+
#### Live Terminal Watch
|
|
565
|
+
|
|
566
|
+
When enabled with `--auto-start-screen-watch-message`, the bot automatically starts a separate live terminal watch message for public `/solve` sessions:
|
|
567
|
+
|
|
568
|
+
- **Manual Watch**: `/terminal_watch <uuid>` or reply with `/terminal_watch`
|
|
569
|
+
- **Real-time Updates**: See live session log output as commands execute
|
|
570
|
+
- **Auto-freeze**: Message freezes when command completes
|
|
571
|
+
- **Log Attachment**: Full logs attached automatically when session ends
|
|
572
|
+
- **Security**: Auto-start is disabled for private or unknown-visibility repositories
|
|
573
|
+
- **Smart Updates**: Only updates when actual changes detected (rate-limited to avoid API limits)
|
|
574
|
+
|
|
537
575
|
### Security Notes
|
|
538
576
|
|
|
539
577
|
- Only works in group chats where the bot is admin
|
package/README.ru.md
CHANGED
|
@@ -431,9 +431,22 @@ Hive Mind включает интерфейс Telegram-бота (SwarmMindBot)
|
|
|
431
431
|
hive-telegram-bot 2>&1 | tee -a "logs/bot-$(date +%Y%m%d-%H%M%S).log"
|
|
432
432
|
```
|
|
433
433
|
|
|
434
|
+
**Экспериментально: live terminal watch**
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
hive-telegram-bot --auto-start-screen-watch-message
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Этот opt-in флаг запускает отдельное live terminal сообщение для публичных
|
|
441
|
+
сессий `/solve`. Для приватных репозиториев или репозиториев с неизвестной
|
|
442
|
+
видимостью watch-сообщение автоматически не запускается.
|
|
443
|
+
|
|
434
444
|
### Команды бота
|
|
435
445
|
|
|
436
|
-
|
|
446
|
+
Большинство операционных команд работают **только в групповых чатах** (не в
|
|
447
|
+
личных сообщениях боту). Команды, которые намеренно доставляют приватные
|
|
448
|
+
обновления, например `/terminal_watch`, также можно использовать в личных
|
|
449
|
+
сообщениях:
|
|
437
450
|
|
|
438
451
|
#### `/solve` — Решение задач GitHub
|
|
439
452
|
|
|
@@ -494,6 +507,23 @@ Shows:
|
|
|
494
507
|
- Claude usage limits (session and weekly)
|
|
495
508
|
```
|
|
496
509
|
|
|
510
|
+
#### `/terminal_watch` — Live Session Log
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
/terminal_watch <uuid> [--size 120x25]
|
|
514
|
+
|
|
515
|
+
Examples:
|
|
516
|
+
/terminal_watch 4d934f71-4cdb-4b8c-b474-582116d12c12
|
|
517
|
+
/terminal_watch 4d934f71-4cdb-4b8c-b474-582116d12c12 --width 100 --height 20
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Также можно ответить на сообщение сессии бота командой `/terminal_watch`. Команда
|
|
521
|
+
обновляет отдельное сообщение Telegram последними строками лога сессии,
|
|
522
|
+
полученного через `$ --status <uuid>`, и прикрепляет полный файл лога после
|
|
523
|
+
завершения сессии. Логи публичных репозиториев можно смотреть в чате; логи
|
|
524
|
+
приватных репозиториев или репозиториев с неизвестной видимостью доставляются
|
|
525
|
+
только личным сообщением.
|
|
526
|
+
|
|
497
527
|
#### `/help` — Получить справку и диагностическую информацию
|
|
498
528
|
|
|
499
529
|
```
|
|
@@ -508,12 +538,25 @@ Shows:
|
|
|
508
538
|
|
|
509
539
|
### Возможности
|
|
510
540
|
|
|
511
|
-
- ✅
|
|
541
|
+
- ✅ **Запуск из групповых чатов**: workflows `/solve` и `/hive` запускаются из авторизованных групповых чатов
|
|
512
542
|
- ✅ **Полная поддержка параметров**: все параметры командной строки работают в Telegram
|
|
513
543
|
- ✅ **Screen-сессии**: команды запускаются в отсоединённых screen-сессиях
|
|
544
|
+
- ✅ **Live Terminal Watch**: `/terminal_watch` и opt-in auto-start показывают live session logs
|
|
514
545
|
- ✅ **Ограничения по чатам**: опциональный белый список разрешённых ID чатов
|
|
515
546
|
- ✅ **Диагностические инструменты**: получение ID чата и информации о конфигурации
|
|
516
547
|
|
|
548
|
+
#### Live Terminal Watch
|
|
549
|
+
|
|
550
|
+
Если включить `--auto-start-screen-watch-message`, бот автоматически запускает
|
|
551
|
+
отдельное live terminal watch сообщение для публичных сессий `/solve`:
|
|
552
|
+
|
|
553
|
+
- **Manual Watch**: `/terminal_watch <uuid>` или ответ командой `/terminal_watch`
|
|
554
|
+
- **Real-time Updates**: смотрите live session log output во время выполнения команд
|
|
555
|
+
- **Auto-freeze**: сообщение замораживается после завершения команды
|
|
556
|
+
- **Log Attachment**: полные логи автоматически прикрепляются после завершения сессии
|
|
557
|
+
- **Security**: auto-start отключён для приватных репозиториев и репозиториев с неизвестной видимостью
|
|
558
|
+
- **Smart Updates**: обновляет сообщение только при реальных изменениях (rate-limited для защиты от API limits)
|
|
559
|
+
|
|
517
560
|
### Замечания по безопасности
|
|
518
561
|
|
|
519
562
|
- Работает только в групповых чатах, где бот является администратором
|
package/README.zh.md
CHANGED
|
@@ -431,9 +431,18 @@ Hive Mind 内置 Telegram 机器人接口(SwarmMindBot),支持远程命令
|
|
|
431
431
|
hive-telegram-bot 2>&1 | tee -a "logs/bot-$(date +%Y%m%d-%H%M%S).log"
|
|
432
432
|
```
|
|
433
433
|
|
|
434
|
+
**实验性:live terminal watch**
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
hive-telegram-bot --auto-start-screen-watch-message
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
这个 opt-in 标志会为公开仓库的 `/solve` 会话启动一条单独的 live terminal
|
|
441
|
+
消息。私有仓库或可见性未知的仓库不会自动启动 watch 消息。
|
|
442
|
+
|
|
434
443
|
### 机器人命令
|
|
435
444
|
|
|
436
|
-
|
|
445
|
+
大多数操作类命令仅在**群聊中**有效(不支持与机器人的私聊)。有意发送私密更新的命令(例如 `/terminal_watch`)也可以在私聊中使用:
|
|
437
446
|
|
|
438
447
|
#### `/solve` - 解决 GitHub Issue
|
|
439
448
|
|
|
@@ -494,6 +503,21 @@ Shows:
|
|
|
494
503
|
- Claude usage limits (session and weekly)
|
|
495
504
|
```
|
|
496
505
|
|
|
506
|
+
#### `/terminal_watch` - Live Session Log
|
|
507
|
+
|
|
508
|
+
```
|
|
509
|
+
/terminal_watch <uuid> [--size 120x25]
|
|
510
|
+
|
|
511
|
+
Examples:
|
|
512
|
+
/terminal_watch 4d934f71-4cdb-4b8c-b474-582116d12c12
|
|
513
|
+
/terminal_watch 4d934f71-4cdb-4b8c-b474-582116d12c12 --width 100 --height 20
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
您也可以回复机器人会话消息并发送 `/terminal_watch`。该命令会用
|
|
517
|
+
`$ --status <uuid>` 报告的会话日志最新内容更新一条单独的 Telegram
|
|
518
|
+
消息,并在会话结束时附加完整日志文件。公开仓库日志可以在群聊中 watch;
|
|
519
|
+
私有仓库或可见性未知仓库的日志只会通过私聊发送。
|
|
520
|
+
|
|
497
521
|
#### `/help` - 获取帮助和诊断信息
|
|
498
522
|
|
|
499
523
|
```
|
|
@@ -508,12 +532,25 @@ Shows:
|
|
|
508
532
|
|
|
509
533
|
### 功能特性
|
|
510
534
|
|
|
511
|
-
- ✅
|
|
535
|
+
- ✅ **群聊执行**:`/solve` 和 `/hive` workflow 从已授权群聊中运行
|
|
512
536
|
- ✅ **完整选项支持**:所有命令行选项均可在 Telegram 中使用
|
|
513
537
|
- ✅ **Screen 会话**:命令在后台 Screen 会话中运行
|
|
538
|
+
- ✅ **Live Terminal Watch**:`/terminal_watch` 和 opt-in auto-start 显示 live session logs
|
|
514
539
|
- ✅ **聊天限制**:可选配置允许的聊天 ID 白名单
|
|
515
540
|
- ✅ **诊断工具**:获取聊天 ID 和配置信息
|
|
516
541
|
|
|
542
|
+
#### Live Terminal Watch
|
|
543
|
+
|
|
544
|
+
启用 `--auto-start-screen-watch-message` 后,机器人会为公开仓库的 `/solve`
|
|
545
|
+
会话自动启动一条单独的 live terminal watch 消息:
|
|
546
|
+
|
|
547
|
+
- **Manual Watch**:`/terminal_watch <uuid>` 或回复 `/terminal_watch`
|
|
548
|
+
- **Real-time Updates**:在命令执行时查看 live session log output
|
|
549
|
+
- **Auto-freeze**:命令完成时消息会被冻结
|
|
550
|
+
- **Log Attachment**:会话结束时自动附加完整日志
|
|
551
|
+
- **Security**:私有仓库或可见性未知的仓库禁用 auto-start
|
|
552
|
+
- **Smart Updates**:仅在实际内容变化时更新(rate-limited 以避免 API limits)
|
|
553
|
+
|
|
517
554
|
### 安全注意事项
|
|
518
555
|
|
|
519
556
|
- 仅在机器人为管理员的群聊中有效
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/hive-mind",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.0",
|
|
4
4
|
"description": "AI-powered issue solver and hive mind for collaborative problem solving",
|
|
5
5
|
"main": "src/hive.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hive-telegram-bot": "./src/telegram-bot.mjs"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-log-upload-output-1678.mjs && node tests/test-log-upload-output-1682.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-issue-1670-screen-status-monitoring.mjs && node tests/test-issue-1680-session-monitoring.mjs && node tests/test-issue-1684-message-formatting.mjs && node tests/test-issue-1686-log-command.mjs && node tests/test-issue-1688-subscribe-and-pr-link.mjs && node tests/test-issue-1694-stabilized-defaults.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
18
|
+
"test": "node tests/solve-queue.test.mjs && node tests/limits-display.test.mjs && node tests/test-usage-limit.mjs && node tests/test-codex-support.mjs && node tests/test-build-cost-info-string.mjs && node tests/test-claude-code-install-method.mjs && node tests/test-claude-quiet-config.mjs && node tests/test-configure-claude-bin.mjs && node tests/test-docker-release-order.mjs && node tests/test-docker-box-migration.mjs && node tests/test-hive-screens.mjs && node tests/test-issue-1616-pr-issue-link-preservation.mjs && node tests/test-pre-pr-failure-notifier-1640.mjs && node tests/test-ready-to-merge-pagination-1645.mjs && node tests/test-require-gh-paginate-rule.mjs && node tests/test-auto-restart-limits-1664.mjs && node tests/test-log-upload-output-1678.mjs && node tests/test-log-upload-output-1682.mjs && node tests/test-telegram-message-filters.mjs && node tests/test-telegram-bot-command-aliases.mjs && node tests/test-telegram-options-before-url.mjs && node tests/test-telegram-bot-configuration-isolation-links-notation.mjs && node tests/test-extract-isolation-from-args.mjs && node tests/test-solve-queue-command.mjs && node tests/test-queue-display-1267.mjs && node tests/test-issue-467-terminal-watch.mjs && node tests/test-issue-1670-screen-status-monitoring.mjs && node tests/test-issue-1680-session-monitoring.mjs && node tests/test-issue-1684-message-formatting.mjs && node tests/test-issue-1686-log-command.mjs && node tests/test-issue-1688-subscribe-and-pr-link.mjs && node tests/test-issue-1694-stabilized-defaults.mjs && node tests/test-telegram-bot-launcher.mjs",
|
|
19
19
|
"test:queue": "node tests/solve-queue.test.mjs",
|
|
20
20
|
"test:limits-display": "node tests/limits-display.test.mjs",
|
|
21
21
|
"test:usage-limit": "node tests/test-usage-limit.mjs",
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -49,6 +49,8 @@ const { getSolveQueue, createQueueExecuteCallback } = await import('./telegram-s
|
|
|
49
49
|
const { applySolveToolAlias, getFirstParsedPositionalArg, getSolveCommandNameFromText, getSolveToolAliasFromText, moveArgumentToFront, parseArgsWithYargs, parseCommandArgs, SOLVE_COMMAND_NAMES } = await import('./telegram-solve-command.lib.mjs');
|
|
50
50
|
const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
|
|
51
51
|
const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
|
|
52
|
+
const { safeReply } = await import('./telegram-safe-reply.lib.mjs');
|
|
53
|
+
const { registerTerminalWatchCommand, startAutoTerminalWatchForSession } = await import('./telegram-terminal-watch-command.lib.mjs');
|
|
52
54
|
const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
|
|
53
55
|
const { trackSession, startSessionMonitoring, hasActiveSessionForUrlAsync } = await import('./session-monitor.lib.mjs');
|
|
54
56
|
const { formatExecutingWorkSessionMessage, formatStartingWorkSessionMessage } = await import('./work-session-formatting.lib.mjs');
|
|
@@ -113,6 +115,7 @@ const config = yargs(hideBin(process.argv))
|
|
|
113
115
|
alias: 'v',
|
|
114
116
|
default: getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true',
|
|
115
117
|
})
|
|
118
|
+
.option('autoStartScreenWatchMessage', { type: 'boolean', description: 'Experimental: auto-start separate /terminal_watch messages for public /solve sessions', alias: 'auto-start-screen-watch-message', default: getenv('TELEGRAM_AUTO_START_SCREEN_WATCH_MESSAGE', getenv('TELEGRAM_AUTO_WATCH_MESSAGE', 'false')) === 'true' })
|
|
116
119
|
.option('isolation', { type: 'string', description: "Isolation backend (screen/tmux/docker). Defaults to 'screen' so Telegram-bot work sessions survive bot restarts; pass --isolation '' (or set TELEGRAM_ISOLATION='') to disable.", default: getenv('TELEGRAM_ISOLATION', 'screen') })
|
|
117
120
|
.help('h')
|
|
118
121
|
.alias('h', 'help')
|
|
@@ -130,6 +133,7 @@ if (config.configuration) {
|
|
|
130
133
|
|
|
131
134
|
const BOT_TOKEN = config.token || getenv('TELEGRAM_BOT_TOKEN', '');
|
|
132
135
|
const VERBOSE = config.verbose || getenv('TELEGRAM_BOT_VERBOSE', 'false') === 'true';
|
|
136
|
+
const AUTO_WATCH_MESSAGE = config.autoStartScreenWatchMessage === true;
|
|
133
137
|
if (!BOT_TOKEN) {
|
|
134
138
|
console.error('Error: TELEGRAM_BOT_TOKEN not set. Use --token or TELEGRAM_BOT_TOKEN env var.');
|
|
135
139
|
process.exit(1);
|
|
@@ -532,25 +536,6 @@ async function validateGitHubUrl(args, options = {}) {
|
|
|
532
536
|
return { valid: true, parsed, normalizedUrl: url };
|
|
533
537
|
}
|
|
534
538
|
|
|
535
|
-
// Issue #1460/#1497: safeReply - try Markdown first, fall back to plain text on parsing errors
|
|
536
|
-
async function safeReply(ctx, text, options = {}) {
|
|
537
|
-
try {
|
|
538
|
-
return await ctx.reply(text, { parse_mode: 'Markdown', ...options });
|
|
539
|
-
} catch (error) {
|
|
540
|
-
const isParsingError = error.message && (error.message.includes("can't parse entities") || error.message.includes("Can't parse entities") || error.message.includes("can't find end of") || (error.message.includes('Bad Request') && error.message.includes('400')));
|
|
541
|
-
if (!isParsingError) throw error;
|
|
542
|
-
console.error(`[telegram-bot] safeReply: Markdown parsing failed: ${error.message}`);
|
|
543
|
-
console.error(`[telegram-bot] safeReply: Failing message (${Buffer.byteLength(text, 'utf-8')} bytes): ${text}`);
|
|
544
|
-
const plainText = text
|
|
545
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
|
|
546
|
-
.replace(/\\_/g, '_')
|
|
547
|
-
.replace(/\\\*/g, '*')
|
|
548
|
-
.replace(/\*([^*]+)\*/g, '$1')
|
|
549
|
-
.replace(/`([^`]+)`/g, '$1');
|
|
550
|
-
return await ctx.reply(plainText, { ...options, parse_mode: undefined });
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
539
|
async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock, perCommandIsolation = null, tool = 'claude', urlContext = null) {
|
|
555
540
|
const { chat, message_id: msgId } = startingMessage;
|
|
556
541
|
const safeEdit = async text => {
|
|
@@ -562,18 +547,24 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
|
|
|
562
547
|
};
|
|
563
548
|
const requesterUserId = ctx.from?.id ?? null; // Issue #1688: suppress duplicate /subscribe DM
|
|
564
549
|
const iso = await resolveIsolation(perCommandIsolation, ISOLATION_BACKEND, isolationRunner, VERBOSE);
|
|
565
|
-
let result, session;
|
|
550
|
+
let result, session, sessionInfo;
|
|
566
551
|
if (iso) {
|
|
567
552
|
session = iso.runner.generateSessionId();
|
|
568
553
|
VERBOSE && console.log(`[VERBOSE] Using isolation (${iso.backend}), session: ${session}`);
|
|
569
554
|
result = await iso.runner.executeWithIsolation(commandName, args, { backend: iso.backend, sessionId: session, verbose: VERBOSE });
|
|
570
|
-
if (result.success)
|
|
555
|
+
if (result.success) {
|
|
556
|
+
sessionInfo = { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, isolationBackend: iso.backend, sessionId: session, tool, infoBlock, urlContext, requesterUserId };
|
|
557
|
+
trackSession(session, sessionInfo, VERBOSE);
|
|
558
|
+
}
|
|
571
559
|
} else {
|
|
572
560
|
result = await executeStartScreen(commandName, args);
|
|
573
561
|
const match = result.success && (result.output.match(/session:\s*(\S+)/i) || result.output.match(/screen -R\s+(\S+)/));
|
|
574
562
|
session = match ? match[1] : 'unknown';
|
|
575
563
|
// Issue #1586: Non-isolation sessions auto-expire after 10 min — screen stays alive via `exec bash` so completion can't be detected reliably; this still blocks duplicate commands in the timeout window.
|
|
576
|
-
if (result.success && session !== 'unknown')
|
|
564
|
+
if (result.success && session !== 'unknown') {
|
|
565
|
+
sessionInfo = { chatId: ctx.chat.id, messageId: msgId, startTime: new Date(), url: args[0], command: commandName, tool, infoBlock, urlContext, requesterUserId };
|
|
566
|
+
trackSession(session, sessionInfo, VERBOSE);
|
|
567
|
+
}
|
|
577
568
|
}
|
|
578
569
|
if (result.warning) return safeEdit(`⚠️ ${result.warning}`);
|
|
579
570
|
if (result.success) {
|
|
@@ -584,6 +575,7 @@ async function executeAndUpdateMessage(ctx, startingMessage, commandName, args,
|
|
|
584
575
|
infoBlock,
|
|
585
576
|
})
|
|
586
577
|
);
|
|
578
|
+
if (AUTO_WATCH_MESSAGE && commandName === 'solve' && sessionInfo?.isolationBackend) await startAutoTerminalWatchForSession({ bot, ctx, sessionId: session, sessionInfo, verbose: VERBOSE });
|
|
587
579
|
} else await safeEdit(`❌ Error executing ${commandName} command:\n\n\`\`\`\n${result.error || result.output}\n\`\`\`\n\n${infoBlock}`);
|
|
588
580
|
}
|
|
589
581
|
|
|
@@ -663,11 +655,12 @@ bot.command('help', async ctx => {
|
|
|
663
655
|
message += '*/subscribe* / */unsubscribe* - 🔔 Get private DM forward of /solve completion (experimental, #1688)\n';
|
|
664
656
|
message += '*/help* - Show this help message\n';
|
|
665
657
|
message += '*/stop* / */start* - Stop or resume accepting new tasks (owner only)\n';
|
|
666
|
-
message += '*/log* - Fetch isolation session log (owner only). Usage: `/log <uuid>` or reply with `/log`\n
|
|
658
|
+
message += '*/log* - Fetch isolation session log (owner only). Usage: `/log <uuid>` or reply with `/log`\n';
|
|
659
|
+
message += '*/terminal\\_watch* - Live-update an isolation session log (owner only). Usage: `/terminal_watch <uuid>` or reply with `/terminal_watch`\n\n';
|
|
667
660
|
message += '🔔 *Session Notifications:* Completion notifications are automatic; use /subscribe for private DM forwards.\n';
|
|
668
661
|
if (ISOLATION_BACKEND) message += `🔒 *Isolation Mode:* \`${ISOLATION_BACKEND}\` (experimental)\n`;
|
|
669
662
|
message += '\n';
|
|
670
|
-
message += '⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /subscribe and /unsubscribe work in private and group chats.\n\n';
|
|
663
|
+
message += '⚠️ *Note:* /solve, /do, /continue, /claude, /codex, /opencode, /agent, /hive, /solve\\_queue, /limits, /version, /accept\\_invites, /merge, /stop and /start commands only work in group chats. /terminal\\_watch, /subscribe and /unsubscribe work in private and group chats.\n\n';
|
|
671
664
|
message += '🔧 *Common Options:*\n';
|
|
672
665
|
message += `• \`--model <model>\` or \`-m\` - ${buildModelOptionDescription()}\n`;
|
|
673
666
|
message += '• `--base-branch <branch>` or `-b` - Target branch for PR (default: repo default branch)\n';
|
|
@@ -1194,6 +1187,7 @@ const { registerLogCommand } = await import('./telegram-log-command.lib.mjs');
|
|
|
1194
1187
|
registerTopCommand(bot, sharedCommandOpts);
|
|
1195
1188
|
registerStartStopCommands(bot, sharedCommandOpts);
|
|
1196
1189
|
await registerLogCommand(bot, sharedCommandOpts);
|
|
1190
|
+
await registerTerminalWatchCommand(bot, sharedCommandOpts);
|
|
1197
1191
|
|
|
1198
1192
|
// Add message listener for verbose debugging
|
|
1199
1193
|
if (VERBOSE) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Issue #1460/#1497: safeReply - try Markdown first, fall back to plain text on parsing errors.
|
|
2
|
+
export async function safeReply(ctx, text, options = {}) {
|
|
3
|
+
try {
|
|
4
|
+
return await ctx.reply(text, { parse_mode: 'Markdown', ...options });
|
|
5
|
+
} catch (error) {
|
|
6
|
+
const message = error?.message || '';
|
|
7
|
+
const isParsingError = message.includes("can't parse entities") || message.includes("Can't parse entities") || message.includes("can't find end of") || (message.includes('Bad Request') && message.includes('400'));
|
|
8
|
+
if (!isParsingError) throw error;
|
|
9
|
+
console.error(`[telegram-bot] safeReply: Markdown parsing failed: ${message}`);
|
|
10
|
+
console.error(`[telegram-bot] safeReply: Failing message (${Buffer.byteLength(text, 'utf-8')} bytes): ${text}`);
|
|
11
|
+
const plainText = text
|
|
12
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
|
|
13
|
+
.replace(/\\_/g, '_')
|
|
14
|
+
.replace(/\\\*/g, '*')
|
|
15
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
16
|
+
.replace(/`([^`]+)`/g, '$1');
|
|
17
|
+
return await ctx.reply(plainText, { ...options, parse_mode: undefined });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram /terminal_watch command.
|
|
3
|
+
*
|
|
4
|
+
* Watches the text log reported by `$ --status <uuid>` and edits a separate
|
|
5
|
+
* Telegram message with the latest terminal-sized snapshot.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { constants as fsConstants } from 'fs';
|
|
11
|
+
import { extractSessionIdFromText, decideLogDestination, resolveLogPath } from './telegram-log-command.lib.mjs';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_WIDTH = 120;
|
|
14
|
+
const DEFAULT_HEIGHT = 25;
|
|
15
|
+
const DEFAULT_INTERVAL_MS = 2500;
|
|
16
|
+
const DEFAULT_MAX_CHARS = 3400;
|
|
17
|
+
const TELEGRAM_DOCUMENT_MAX_BYTES = 50 * 1024 * 1024;
|
|
18
|
+
const GITHUB_URL_RE = /https:\/\/github\.com\/[^\s"'`<>]+/i;
|
|
19
|
+
const activeWatches = new Map();
|
|
20
|
+
|
|
21
|
+
function splitCommandArgs(text) {
|
|
22
|
+
const body = String(text || '')
|
|
23
|
+
.replace(/^\/terminal_watch(?:@\w+)?\b/i, '')
|
|
24
|
+
.trim();
|
|
25
|
+
return body.match(/"[^"]*"|'[^']*'|\S+/g)?.map(token => token.replace(/^(['"])(.*)\1$/, '$2')) || [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readOptionValue(tokens, index, inlineValue, optionName, errors) {
|
|
29
|
+
if (inlineValue !== null) return { value: inlineValue, nextIndex: index };
|
|
30
|
+
const next = tokens[index + 1];
|
|
31
|
+
if (!next || next.startsWith('--')) {
|
|
32
|
+
errors.push(`${optionName} requires a value`);
|
|
33
|
+
return { value: null, nextIndex: index };
|
|
34
|
+
}
|
|
35
|
+
return { value: next, nextIndex: index + 1 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseIntegerOption(value, optionName, errors, { min = 1, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
39
|
+
const parsed = Number.parseInt(value, 10);
|
|
40
|
+
if (!Number.isFinite(parsed) || String(parsed) !== String(value).trim() || parsed < min || parsed > max) {
|
|
41
|
+
errors.push(`${optionName} must be an integer from ${min} to ${max}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseTerminalWatchArgs(text) {
|
|
48
|
+
const tokens = splitCommandArgs(text);
|
|
49
|
+
const options = { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, intervalMs: DEFAULT_INTERVAL_MS, maxChars: DEFAULT_MAX_CHARS };
|
|
50
|
+
const errors = [];
|
|
51
|
+
let sessionId = extractSessionIdFromText(text);
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
54
|
+
const token = tokens[i];
|
|
55
|
+
if (extractSessionIdFromText(token)) {
|
|
56
|
+
sessionId ||= extractSessionIdFromText(token);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!token.startsWith('--')) {
|
|
60
|
+
errors.push(`Unexpected argument: ${token}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const eq = token.indexOf('=');
|
|
65
|
+
const name = eq === -1 ? token : token.slice(0, eq);
|
|
66
|
+
const inlineValue = eq === -1 ? null : token.slice(eq + 1);
|
|
67
|
+
const read = () => {
|
|
68
|
+
const result = readOptionValue(tokens, i, inlineValue, name, errors);
|
|
69
|
+
i = result.nextIndex;
|
|
70
|
+
return result.value;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (['--width', '--columns', '--cols', '--terminal-width'].includes(name)) {
|
|
74
|
+
const value = read();
|
|
75
|
+
if (value !== null) options.width = parseIntegerOption(value, name, errors, { min: 20, max: 240 }) || options.width;
|
|
76
|
+
} else if (['--height', '--lines', '--rows', '--terminal-height'].includes(name)) {
|
|
77
|
+
const value = read();
|
|
78
|
+
if (value !== null) options.height = parseIntegerOption(value, name, errors, { min: 5, max: 80 }) || options.height;
|
|
79
|
+
} else if (['--interval', '--interval-ms'].includes(name)) {
|
|
80
|
+
const value = read();
|
|
81
|
+
if (value !== null) options.intervalMs = parseIntegerOption(value, name, errors, { min: 1000, max: 60000 }) || options.intervalMs;
|
|
82
|
+
} else if (name === '--max-chars') {
|
|
83
|
+
const value = read();
|
|
84
|
+
if (value !== null) options.maxChars = parseIntegerOption(value, name, errors, { min: 500, max: 3800 }) || options.maxChars;
|
|
85
|
+
} else if (name === '--size') {
|
|
86
|
+
const value = read();
|
|
87
|
+
const match = value?.match(/^(\d+)x(\d+)$/i);
|
|
88
|
+
if (!match) errors.push('--size must use WIDTHxHEIGHT format, for example --size 120x25');
|
|
89
|
+
else {
|
|
90
|
+
options.width = parseIntegerOption(match[1], '--size width', errors, { min: 20, max: 240 }) || options.width;
|
|
91
|
+
options.height = parseIntegerOption(match[2], '--size height', errors, { min: 5, max: 80 }) || options.height;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
errors.push(`Unknown option: ${name}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { sessionId, options, errors };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function tailTextForTerminal(text, { width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT, maxChars = DEFAULT_MAX_CHARS } = {}) {
|
|
102
|
+
const normalized = String(text || '')
|
|
103
|
+
.replace(/\r\n/g, '\n')
|
|
104
|
+
.replace(/\r/g, '\n');
|
|
105
|
+
const lines = normalized.split('\n');
|
|
106
|
+
const visibleLines = lines.slice(-height).map(line => {
|
|
107
|
+
const expanded = line.replace(/\t/g, ' ');
|
|
108
|
+
return expanded.length > width ? `...${expanded.slice(-(width - 3))}` : expanded;
|
|
109
|
+
});
|
|
110
|
+
let result = visibleLines.join('\n').trimEnd();
|
|
111
|
+
if (!result) return '(no log output yet)';
|
|
112
|
+
if (result.length > maxChars) {
|
|
113
|
+
result = result.slice(-maxChars);
|
|
114
|
+
const firstNewline = result.indexOf('\n');
|
|
115
|
+
if (firstNewline > 0) result = result.slice(firstNewline + 1);
|
|
116
|
+
result = `...[truncated]\n${result}`;
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sanitizeCodeBlock(text) {
|
|
122
|
+
return String(text || '').replace(/```/g, "'''");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function formatTerminalWatchMessage({ sessionId, statusResult = null, logText = '', options = {}, updateCount = 0, completed = false, repoDescription = null }) {
|
|
126
|
+
const status = statusResult?.status || 'unknown';
|
|
127
|
+
const width = options.width || DEFAULT_WIDTH;
|
|
128
|
+
const height = options.height || DEFAULT_HEIGHT;
|
|
129
|
+
const snapshot = sanitizeCodeBlock(tailTextForTerminal(logText, options));
|
|
130
|
+
const title = completed ? '✅ Terminal watch complete' : '🔄 Live terminal watch';
|
|
131
|
+
const lines = [title, `Session: \`${sessionId}\``, `Status: \`${status}\``, `Terminal: \`${width}x${height}\``];
|
|
132
|
+
if (repoDescription) lines.push(`Repo: \`${repoDescription}\``);
|
|
133
|
+
if (!completed) lines.push(`Updates: ${updateCount}`);
|
|
134
|
+
lines.push('', '```', snapshot, '```');
|
|
135
|
+
return lines.join('\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function fileExists(filePath) {
|
|
139
|
+
try {
|
|
140
|
+
await fs.access(filePath, fsConstants.R_OK);
|
|
141
|
+
return true;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function fileSize(filePath) {
|
|
148
|
+
try {
|
|
149
|
+
return (await fs.stat(filePath)).size;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function readLogFile(logPath) {
|
|
156
|
+
try {
|
|
157
|
+
return await fs.readFile(logPath, 'utf8');
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (error?.code === 'ENOENT') return '';
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function extractGitHubUrlFromStatus(statusResult) {
|
|
165
|
+
const match = String(statusResult?.command || '').match(GITHUB_URL_RE);
|
|
166
|
+
return match ? match[0].replace(/[),.;]+$/, '') : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function resolveTerminalWatchRepository({ sessionInfo = null, statusResult = null, parseGitHubUrl, detectRepositoryVisibility }) {
|
|
170
|
+
const url = sessionInfo?.url || extractGitHubUrlFromStatus(statusResult);
|
|
171
|
+
if (!url || !parseGitHubUrl || !detectRepositoryVisibility) return { repoVisibility: null, repoDescription: null };
|
|
172
|
+
const parsed = parseGitHubUrl(url);
|
|
173
|
+
if (!parsed?.valid || !parsed.owner || !parsed.repo) return { repoVisibility: null, repoDescription: null };
|
|
174
|
+
try {
|
|
175
|
+
return {
|
|
176
|
+
repoVisibility: await detectRepositoryVisibility(parsed.owner, parsed.repo),
|
|
177
|
+
repoDescription: `${parsed.owner}/${parsed.repo}`,
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('[ERROR] /terminal_watch: detectRepositoryVisibility failed:', error);
|
|
181
|
+
return { repoVisibility: null, repoDescription: `${parsed.owner}/${parsed.repo}` };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function sendLogDocument({ bot, chatId, logPath, sessionId, statusResult }) {
|
|
186
|
+
if (!(await fileExists(logPath))) return;
|
|
187
|
+
const size = await fileSize(logPath);
|
|
188
|
+
if (size !== null && size > TELEGRAM_DOCUMENT_MAX_BYTES) {
|
|
189
|
+
await bot.telegram.sendMessage(chatId, `⚠️ Full log for \`${sessionId}\` is ${(size / (1024 * 1024)).toFixed(1)} MB, above Telegram's 50 MB upload limit.`, { parse_mode: 'Markdown' });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await bot.telegram.sendDocument(chatId, { source: logPath, filename: path.basename(logPath) }, { caption: `📄 Full log for session \`${sessionId}\`${statusResult?.status ? `\nStatus: \`${statusResult.status}\`` : ''}`, parse_mode: 'Markdown' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function querySessionStatusWithRetry(querySessionStatus, sessionId, verbose, attempts = 3) {
|
|
196
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
197
|
+
const statusResult = await querySessionStatus(sessionId, verbose);
|
|
198
|
+
if (statusResult?.exists || attempt === attempts) return statusResult;
|
|
199
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function watchTerminalLogSession({ bot, chatId, messageId, sessionId, logPath, querySessionStatus, isTerminalSessionStatus, options = {}, repoDescription = null, verbose = false, attachLogOnComplete = true }) {
|
|
205
|
+
const key = `${chatId}:${messageId}:${sessionId}`;
|
|
206
|
+
activeWatches.get(key)?.stop();
|
|
207
|
+
|
|
208
|
+
let stopped = false;
|
|
209
|
+
let lastMessage = '';
|
|
210
|
+
let updateCount = 0;
|
|
211
|
+
let timer = null;
|
|
212
|
+
const intervalMs = options.intervalMs || DEFAULT_INTERVAL_MS;
|
|
213
|
+
|
|
214
|
+
const tick = async () => {
|
|
215
|
+
if (stopped) return;
|
|
216
|
+
try {
|
|
217
|
+
const statusResult = await querySessionStatus(sessionId, verbose);
|
|
218
|
+
const completed = !!statusResult?.status && isTerminalSessionStatus(statusResult.status);
|
|
219
|
+
const logText = await readLogFile(logPath);
|
|
220
|
+
const message = formatTerminalWatchMessage({ sessionId, statusResult, logText, options, updateCount: ++updateCount, completed, repoDescription });
|
|
221
|
+
if (message !== lastMessage) {
|
|
222
|
+
await bot.telegram.editMessageText(chatId, messageId, undefined, message, { parse_mode: 'Markdown' });
|
|
223
|
+
lastMessage = message;
|
|
224
|
+
}
|
|
225
|
+
if (completed) {
|
|
226
|
+
stopped = true;
|
|
227
|
+
activeWatches.delete(key);
|
|
228
|
+
if (attachLogOnComplete) await sendLogDocument({ bot, chatId, logPath, sessionId, statusResult });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
console.error(`[terminal-watch] Error while watching ${sessionId}:`, error);
|
|
233
|
+
}
|
|
234
|
+
if (!stopped) timer = setTimeout(tick, intervalMs);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const control = {
|
|
238
|
+
stop: () => {
|
|
239
|
+
stopped = true;
|
|
240
|
+
if (timer) clearTimeout(timer);
|
|
241
|
+
activeWatches.delete(key);
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
activeWatches.set(key, control);
|
|
245
|
+
timer = setTimeout(tick, 0);
|
|
246
|
+
return control;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildUsage() {
|
|
250
|
+
return 'Usage:\n• `/terminal_watch <UUID>`\n• Reply to a session message with `/terminal_watch`\n\nOptions: `--size 120x25`, `--width 120`, `--height 25`, `--interval-ms 2500`, `--max-chars 3400`';
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function createWatchMessage({ ctx, targetChatId, replyToMessageId, text }) {
|
|
254
|
+
if (targetChatId === ctx.chat.id) {
|
|
255
|
+
return await ctx.reply(text, { parse_mode: 'Markdown', reply_to_message_id: replyToMessageId });
|
|
256
|
+
}
|
|
257
|
+
return await ctx.telegram.sendMessage(targetChatId, text, replyToMessageId ? { parse_mode: 'Markdown', reply_to_message_id: replyToMessageId } : { parse_mode: 'Markdown' });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function forwardOrCopyToDm(ctx, sourceMessage) {
|
|
261
|
+
const userId = ctx.from?.id;
|
|
262
|
+
if (!userId || !sourceMessage) return null;
|
|
263
|
+
try {
|
|
264
|
+
const forwarded = await ctx.telegram.forwardMessage(userId, ctx.chat.id, sourceMessage.message_id);
|
|
265
|
+
return forwarded?.message_id || null;
|
|
266
|
+
} catch (forwardError) {
|
|
267
|
+
try {
|
|
268
|
+
const copied = await ctx.telegram.copyMessage(userId, ctx.chat.id, sourceMessage.message_id);
|
|
269
|
+
return copied?.message_id || null;
|
|
270
|
+
} catch (copyError) {
|
|
271
|
+
console.error('[ERROR] /terminal_watch: forward/copyMessage to DM failed:', forwardError, copyError);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function startWatchFromResolvedSession({ bot, ctx, sessionId, statusResult, sessionInfo, decision, logPath, watchOptions, querySessionStatus, isTerminalSessionStatus, repoDescription, auto = false, verbose = false }) {
|
|
278
|
+
if (auto && decision.destination !== 'chat') {
|
|
279
|
+
verbose && console.log(`[VERBOSE] Auto terminal watch skipped for ${sessionId}: ${decision.reason}`);
|
|
280
|
+
return { started: false, reason: decision.reason };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const targetChatId = decision.destination === 'chat' ? ctx.chat.id : ctx.from?.id;
|
|
284
|
+
if (!targetChatId) return { started: false, reason: 'Missing target chat id' };
|
|
285
|
+
|
|
286
|
+
const initialLogText = await readLogFile(logPath);
|
|
287
|
+
const initialText = formatTerminalWatchMessage({ sessionId, statusResult, logText: initialLogText, options: watchOptions, repoDescription });
|
|
288
|
+
let replyToMessageId = ctx.message?.message_id || undefined;
|
|
289
|
+
if (decision.destination === 'dm' && ctx.chat.type !== 'private') {
|
|
290
|
+
replyToMessageId = await forwardOrCopyToDm(ctx, ctx.message?.reply_to_message || ctx.message);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const watchMessage = await createWatchMessage({ ctx, targetChatId, replyToMessageId, text: initialText });
|
|
294
|
+
watchTerminalLogSession({ bot, chatId: targetChatId, messageId: watchMessage.message_id, sessionId, logPath, querySessionStatus, isTerminalSessionStatus, options: watchOptions, repoDescription, verbose });
|
|
295
|
+
|
|
296
|
+
if (!auto && decision.destination === 'dm' && ctx.chat.type !== 'private') {
|
|
297
|
+
await ctx.reply(`📬 Started terminal watch for \`${sessionId}\` in your direct messages.`, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
|
|
298
|
+
}
|
|
299
|
+
return { started: true, messageId: watchMessage.message_id, sessionInfo };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export async function startAutoTerminalWatchForSession({ bot, ctx, sessionId, sessionInfo, verbose = false, options = {} }) {
|
|
303
|
+
try {
|
|
304
|
+
const runner = await import('./isolation-runner.lib.mjs');
|
|
305
|
+
const { parseGitHubUrl, detectRepositoryVisibility } = await import('./github.lib.mjs');
|
|
306
|
+
const statusResult = await querySessionStatusWithRetry(runner.querySessionStatus, sessionId, verbose);
|
|
307
|
+
if (!statusResult?.exists) return { started: false, reason: 'Unknown session id' };
|
|
308
|
+
const { repoVisibility, repoDescription } = await resolveTerminalWatchRepository({ sessionInfo, statusResult, parseGitHubUrl, detectRepositoryVisibility });
|
|
309
|
+
const decision = decideLogDestination({ statusResult, sessionInfo, repoVisibility, chatType: ctx.chat?.type });
|
|
310
|
+
if (decision.destination !== 'chat') return { started: false, reason: decision.reason };
|
|
311
|
+
const logPath = resolveLogPath({ statusResult, isolationBackend: decision.isolationBackend });
|
|
312
|
+
if (!logPath) return { started: false, reason: 'Missing log path' };
|
|
313
|
+
return await startWatchFromResolvedSession({ bot, ctx, sessionId, statusResult, sessionInfo, decision, logPath, watchOptions: { width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, intervalMs: DEFAULT_INTERVAL_MS, maxChars: DEFAULT_MAX_CHARS, ...options }, querySessionStatus: runner.querySessionStatus, isTerminalSessionStatus: runner.isTerminalSessionStatus, repoDescription, auto: true, verbose });
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error('[terminal-watch] Auto-start failed:', error);
|
|
316
|
+
return { started: false, reason: error.message || String(error) };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function registerTerminalWatchCommand(bot, options) {
|
|
321
|
+
const { VERBOSE = false, isOldMessage, isChatAuthorized, isTopicAuthorized, buildAuthErrorMessage } = options;
|
|
322
|
+
const runner = await import('./isolation-runner.lib.mjs');
|
|
323
|
+
const getTrackedSessionInfo = options.getTrackedSessionInfo || (await import('./session-monitor.lib.mjs')).getTrackedSessionInfo;
|
|
324
|
+
const detectRepositoryVisibility = options.detectRepositoryVisibility || (await import('./github.lib.mjs')).detectRepositoryVisibility;
|
|
325
|
+
const parseGitHubUrl = options.parseGitHubUrl || (await import('./github.lib.mjs')).parseGitHubUrl;
|
|
326
|
+
|
|
327
|
+
bot.command('terminal_watch', async ctx => {
|
|
328
|
+
VERBOSE && console.log('[VERBOSE] /terminal_watch command received');
|
|
329
|
+
if (isOldMessage && isOldMessage(ctx)) return;
|
|
330
|
+
|
|
331
|
+
const chat = ctx.chat;
|
|
332
|
+
const message = ctx.message;
|
|
333
|
+
if (!chat || !message) return;
|
|
334
|
+
|
|
335
|
+
const parsedArgs = parseTerminalWatchArgs(message.text || '');
|
|
336
|
+
if (parsedArgs.errors.length > 0) {
|
|
337
|
+
await ctx.reply(`❌ Invalid /terminal_watch options:\n${parsedArgs.errors.map(e => `• ${e}`).join('\n')}\n\n${buildUsage()}`, { parse_mode: 'Markdown', reply_to_message_id: message.message_id });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const sessionId = parsedArgs.sessionId || extractSessionIdFromText(message.reply_to_message?.text || message.reply_to_message?.caption || '');
|
|
342
|
+
if (!sessionId) {
|
|
343
|
+
await ctx.reply(`❌ /terminal_watch requires a session id.\n\n${buildUsage()}`, { parse_mode: 'Markdown', reply_to_message_id: message.message_id });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (chat.type !== 'private') {
|
|
348
|
+
try {
|
|
349
|
+
const member = await ctx.telegram.getChatMember(chat.id, ctx.from.id);
|
|
350
|
+
if (!member || member.status !== 'creator') {
|
|
351
|
+
await ctx.reply('❌ /terminal_watch is only available to the chat owner.', { reply_to_message_id: message.message_id });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error('[ERROR] /terminal_watch: getChatMember failed:', error);
|
|
356
|
+
await ctx.reply('❌ Failed to verify permissions for /terminal_watch.', { reply_to_message_id: message.message_id });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (isChatAuthorized && !isChatAuthorized(chat.id) && (!isTopicAuthorized || !isTopicAuthorized(ctx))) {
|
|
362
|
+
const errMsg = buildAuthErrorMessage ? buildAuthErrorMessage(ctx) : `❌ This chat (ID: ${chat.id}) is not authorized.`;
|
|
363
|
+
await ctx.reply(errMsg, { reply_to_message_id: message.message_id });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let statusResult;
|
|
368
|
+
try {
|
|
369
|
+
statusResult = await runner.querySessionStatus(sessionId, VERBOSE);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error('[ERROR] /terminal_watch: querySessionStatus failed:', error);
|
|
372
|
+
await ctx.reply(`❌ Failed to query session status: ${error.message || String(error)}`, { reply_to_message_id: message.message_id });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!statusResult?.exists) {
|
|
377
|
+
await ctx.reply(`❌ Session \`${sessionId}\` is not known to start-command.`, { parse_mode: 'Markdown', reply_to_message_id: message.message_id });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const sessionInfo = getTrackedSessionInfo ? getTrackedSessionInfo(sessionId) : null;
|
|
382
|
+
const { repoVisibility, repoDescription } = await resolveTerminalWatchRepository({ sessionInfo, statusResult, parseGitHubUrl, detectRepositoryVisibility });
|
|
383
|
+
const decision = decideLogDestination({ statusResult, sessionInfo, repoVisibility, chatType: chat.type });
|
|
384
|
+
if (decision.destination === 'reject') {
|
|
385
|
+
await ctx.reply(`❌ ${decision.reason}`, { reply_to_message_id: message.message_id });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const logPath = resolveLogPath({ statusResult, isolationBackend: decision.isolationBackend });
|
|
390
|
+
if (!logPath) {
|
|
391
|
+
await ctx.reply('❌ Could not determine the log file path for this session.', { reply_to_message_id: message.message_id });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
await startWatchFromResolvedSession({ bot, ctx, sessionId, statusResult, sessionInfo, decision, logPath, watchOptions: parsedArgs.options, querySessionStatus: runner.querySessionStatus, isTerminalSessionStatus: runner.isTerminalSessionStatus, repoDescription, verbose: VERBOSE });
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error('[ERROR] /terminal_watch: failed to start watch:', error);
|
|
399
|
+
const friendly = error?.code === 403 || /chat not found|bot can't initiate conversation/i.test(error?.message || '') ? 'I could not send you a DM. Please open a private chat with me and send /start, then try again.' : `Failed to start terminal watch: ${error.message || String(error)}`;
|
|
400
|
+
await ctx.reply(`❌ ${friendly}`, { reply_to_message_id: message.message_id });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export const __INTERNAL_FOR_TESTS__ = {
|
|
406
|
+
DEFAULT_WIDTH,
|
|
407
|
+
DEFAULT_HEIGHT,
|
|
408
|
+
DEFAULT_INTERVAL_MS,
|
|
409
|
+
DEFAULT_MAX_CHARS,
|
|
410
|
+
TELEGRAM_DOCUMENT_MAX_BYTES,
|
|
411
|
+
GITHUB_URL_RE,
|
|
412
|
+
};
|