@link-assistant/hive-mind 1.74.5 → 1.74.7

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @link-assistant/hive-mind
2
2
 
3
+ ## 1.74.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 8ea7110: Document the issue #1858 case study and add an experimental private Telegram
8
+ `/auth` command for allowlisted chat owners to check or start GitHub, Claude,
9
+ and Codex auth flows.
10
+
11
+ ## 1.74.6
12
+
13
+ ### Patch Changes
14
+
15
+ - e07b243: Split oversized Telegram text messages in the safe reply/edit helper so localized `/help` output cannot exceed Telegram's 4096-character limit.
16
+
3
17
  ## 1.74.5
4
18
 
5
19
  ### Patch Changes
package/README.hi.md CHANGED
@@ -29,14 +29,19 @@ Hive Mind एक **सामान्यवादी AI** (मिनी-AGI) ह
29
29
  | **पूर्व-स्थापित टूलचेन** | 25GB+ तैयार: 10 भाषा रनटाइम, 2 थ्योरम प्रूवर, बिल्ड टूल्स। और इंस्टॉल कर सकते हैं। |
30
30
  | **टोकन दक्षता** | नियमित कार्य कोड में स्वचालित, ताकि AI टोकन रचनात्मक समस्या-समाधान पर केंद्रित रहें। |
31
31
  | **समय की स्वतंत्रता** | जो काम मनुष्यों को 2-8 घंटे लगता है, AI प्रत्येक कार्य सत्र में 10-25 मिनट में पूरा करता है। रिपॉजिटरी में कार्यों का बड़े पैमाने पर निष्पादन संभव है। "सोते समय कोड लिखा जाता है।" |
32
- | **ऑर्केस्ट्रेशन के साथ स्केल** | समानांतर वर्कर डेवलपर्स की एक टीम की तरह महसूस होते हैं, सभी ~$200/माह में। |
32
+ | **ऑर्केस्ट्रेशन के साथ स्केल** | समानांतर वर्कर डेवलपर्स की एक टीम की तरह महसूस होते हैं। Claude MAX और ChatGPT Pro (प्रत्येक $200) को जोड़कर दो स्वतंत्र लगभग असीमित बजट मिलते हैं। |
33
33
  | **मानव नियंत्रण** | AI ड्राफ्ट PR बनाता है - आप तय करते हैं क्या मर्ज होगा। जहाँ मायने रखता है वहाँ गुणवत्ता द्वार। |
34
34
  | **किसी भी डिवाइस से प्रोग्रामिंग** | Telegram बॉट के माध्यम से `/solve` और `/hive` से किसी भी डिवाइस से AI प्रबंधित करें। कोई PC, IDE या लैपटॉप की आवश्यकता नहीं। |
35
35
  | **100% ओपन सोर्स** | Unlicense (पब्लिक डोमेन)। पूर्ण पारदर्शिता, कोई वेंडर लॉक-इन नहीं। |
36
36
 
37
- > _"$200 के Codex की तुलना में, यह समाधान शानदार है।"_ - उपयोगकर्ता प्रतिक्रिया
37
+ **लागत**: Hive Mind दो $200/माह सदस्यताओं को पूर्ण-फीचर वाले लगभग "unlimited" विकल्पों के रूप में समर्थन देता है:
38
38
 
39
- **लागत**: Claude MAX सदस्यता (~$200/माह, वर्तमान में 50% छूट = $400 मूल्य) Hive Mind के लिए लगभग असीमित उपयोग प्रदान करती है - बाजार में सबसे अच्छा मूल्य/गुणवत्ता संतुलन।
39
+ | सदस्यता | `--tool` के साथ | डिफ़ॉल्ट मॉडल | किसके लिए बेहतर है |
40
+ | ---------------------------------------------------------------- | ------------------- | ------------- | ------------------------------------------------- |
41
+ | **Anthropic Claude MAX** (~$200/माह, अक्सर 50% छूट = $400 मूल्य) | `claude` (डिफ़ॉल्ट) | Sonnet/Haiku | उच्चतम रचनात्मकता, मजबूत सामान्य कोड रीजनिंग |
42
+ | **OpenAI ChatGPT Pro** ($200/माह, Codex शामिल) | `codex` | `gpt-5.5` | भरोसेमंद deterministic refactors और तेज iteration |
43
+
44
+ दोनों टूल एक ही hive में साथ उपयोग किए जा सकते हैं। Worker अलग-अलग टूल समानांतर चला सकते हैं, और `/codex` या `/solve --tool codex` कार्यों को ChatGPT Pro पर भेजता है जबकि डिफ़ॉल्ट Claude MAX पर जाता है। किसी एक को चुनना आवश्यक नहीं है: किसी भी एक सदस्यता से संचालन संभव है, और दोनों का उपयोग per-tool/model concurrency mode (#1474) खोलता है।
40
45
 
41
46
  Hive Mind में औसत प्रोग्रामर से अलग न पहचानी जा सकने वाली उच्च रचनात्मकता है। यदि आवश्यकताएँ अस्पष्ट हों तो यह प्रश्न पूछता है, और आप PR टिप्पणियों के माध्यम से चलते-चलते स्पष्ट कर सकते हैं।
42
47
 
@@ -48,7 +53,7 @@ Hive Mind में औसत प्रोग्रामर से अलग
48
53
 
49
54
  इंस्टॉलेशन के लिए Docker का उपयोग करना अनुशंसित है (स्थानीय और सर्वर दोनों पर)। नीचे [Docker इंस्टॉलेशन](#using-docker) अनुभाग देखें।
50
55
 
51
- यह सॉफ़्टवेयर Claude Code के पूर्ण स्वायत्त मोड का उपयोग करता है, जिसका अर्थ है कि यह जो भी उचित समझे वह कमांड निष्पादित करने के लिए स्वतंत्र है।
56
+ यह सॉफ़्टवेयर Claude Code और Codex जैसे समर्थित AI टूल को पूर्ण स्वायत्त मोड में चलाता है, जिसका अर्थ है कि वे जो भी उचित समझें वे कमांड निष्पादित करने के लिए स्वतंत्र हैं।
52
57
 
53
58
  इसका मतलब है कि इससे अप्रत्याशित दुष्प्रभाव हो सकते हैं।
54
59
 
@@ -60,7 +65,7 @@ Hive Mind में औसत प्रोग्रामर से अलग
60
65
 
61
66
  इंटरनेट से जुड़ी वर्चुअल मशीन से टोकन निकालने के असंख्य तरीके हैं। इसमें शामिल हैं लेकिन इन तक सीमित नहीं:
62
67
 
63
- - **Claude MAX टोकन** - AI संचालन के लिए आवश्यक
68
+ - **Claude MAX टोकन** और/या **ChatGPT Pro (Codex) टोकन** - AI संचालन के लिए आवश्यक; आप इनमें से किसी एक या दोनों के साथ चला सकते हैं
64
69
  - **GitHub टोकन** - रिपॉजिटरी एक्सेस के लिए आवश्यक
65
70
  - **API keys और क्रेडेंशियल** - सिस्टम पर कोई भी संवेदनशील डेटा
66
71
 
@@ -161,7 +166,7 @@ docker exec -it hive-mind /bin/bash
161
166
  # Inside the container, authenticate with GitHub
162
167
  gh-setup-git-identity
163
168
 
164
- # Authenticate with Claude
169
+ # Authenticate with Claude (if you have Claude MAX)
165
170
  claude
166
171
 
167
172
  # Optionally set configuration like this:
@@ -174,6 +179,14 @@ claude
174
179
  # Optionally test Claude connection
175
180
  claude -p hi --model haiku
176
181
 
182
+ # Authenticate with Codex (if you have ChatGPT Pro)
183
+ codex login --device-auth
184
+
185
+ # Optionally test Codex connection. codex exec refuses to run unless
186
+ # either cwd is a git repo it trusts or --skip-git-repo-check is passed.
187
+ # It prints the refusal to STDOUT but still exits 0, so do not skip the flag.
188
+ codex exec --skip-git-repo-check --model gpt-5.4-mini "reply with only OK"
189
+
177
190
  # You might need to update hive-mind and agent to latest versions:
178
191
  bun install -g @link-assistant/hive-mind
179
192
  bun install -g @link-assistant/agent
@@ -546,6 +559,7 @@ Shows:
546
559
  - ✅ **Screen सत्र**: कमांड डिटैच्ड screen सत्रों में चलते हैं
547
560
  - ✅ **Live Terminal Watch**: `/terminal_watch` और opt-in auto-start live session logs दिखाते हैं
548
561
  - ✅ **चैट प्रतिबंध**: अनुमत चैट ID की वैकल्पिक सफेद सूची
562
+ - ✅ **Private Auth Check**: allowlisted chat owners के लिए experimental `/auth --status <gh|claude|codex>` और `/auth --login <gh|claude|codex>`
549
563
  - ✅ **डायग्नोस्टिक टूल**: चैट ID और कॉन्फ़िगरेशन जानकारी प्राप्त करें
550
564
 
551
565
  #### Live Terminal Watch
@@ -564,6 +578,8 @@ sessions के लिए अपने आप एक अलग live terminal wat
564
578
 
565
579
  - केवल उन ग्रुप चैट में काम करता है जहाँ बॉट एडमिन है
566
580
  - `TELEGRAM_ALLOWED_CHATS` के माध्यम से वैकल्पिक चैट ID प्रतिबंध
581
+ - private `/auth` तब disabled रहता है जब `TELEGRAM_ALLOWED_CHATS` set नहीं है,
582
+ और इसे केवल listed chats के owners इस्तेमाल कर सकते हैं
567
583
  - बॉट चलाने वाले सिस्टम उपयोगकर्ता के रूप में कमांड चलते हैं
568
584
  - उचित प्रमाणीकरण सुनिश्चित करें (`gh auth login`, `claude-profiles`)
569
585
 
package/README.md CHANGED
@@ -29,14 +29,19 @@ Hive Mind is a **generalist AI** (mini-AGI) capable of working on a wide range o
29
29
  | **Pre-installed Toolchain** | 25GB+ ready: 10 language runtimes, 2 theorem provers, build tools. Can install more. |
30
30
  | **Token Efficiency** | Routine tasks automated in code, so AI tokens focus on creative problem-solving. |
31
31
  | **Time Freedom** | What takes humans 2-8 hours, AI completes each working session in 10-25 minutes. Mass execution of tasks in repository is possible. "The code is written while you sleep." |
32
- | **Scale with Orchestration** | Parallel workers feel like a team of developers, all for ~$200/month. |
32
+ | **Scale with Orchestration** | Parallel workers feel like a team of developers. Pair Claude MAX and ChatGPT Pro ($200 each) for two independent unlimited budgets. |
33
33
  | **Human Control** | AI creates draft PRs - you decide what merges. Quality gates where they matter. |
34
34
  | **Any Device Programming** | Manage AI from any device with `/solve` and `/hive` via Telegram bot. No PC, IDE, or laptop required. |
35
35
  | **100% Open Source** | Unlicense (public domain). Full transparency, no vendor lock-in. |
36
36
 
37
- > _"Compared to Codex for $200, this solution is fire."_ - User feedback
37
+ **Cost**: Hive Mind supports two $200/month subscriptions as full-featured "unlimited" options:
38
38
 
39
- **Cost**: Claude MAX subscription (~$200/month, currently 50% off = $400 value) provides almost unlimited usage for Hive Mind - the best value/quality balance on the market.
39
+ | Subscription | Pairs with `--tool` | Default model | Best for |
40
+ | ------------------------------------------------------------------ | ------------------- | ------------- | ------------------------------------------------------- |
41
+ | **Anthropic Claude MAX** (~$200/month, often 50% off = $400 value) | `claude` (default) | Sonnet/Haiku | Highest creativity, strongest general code reasoning |
42
+ | **OpenAI ChatGPT Pro** ($200/month, includes Codex) | `codex` | `gpt-5.5` | Strong deterministic refactors and fast iteration loops |
43
+
44
+ Both tools can be combined in the same hive. Workers can run different tools in parallel, and `/codex` or `/solve --tool codex` routes tasks to ChatGPT Pro while the default routes to Claude MAX. There is no requirement to pick one: either single subscription is enough to operate, and using both unlocks per-tool/model concurrency mode (#1474).
40
45
 
41
46
  Hive Mind has high creativity indistinguishable from average programmers. It asks questions if requirements are unclear, and you can clarify on the go via PR comments.
42
47
 
@@ -48,7 +53,7 @@ It is UNSAFE to run this software on your developer machine.
48
53
 
49
54
  It is recommended to use Docker for installation (both locally and on servers). See the [Docker installation](#using-docker) section below.
50
55
 
51
- This software uses full autonomous mode of Claude Code, that means it is free to execute any commands it sees fit.
56
+ This software runs supported AI tools such as Claude Code and Codex in full autonomous mode, which means they are free to execute any commands they see fit.
52
57
 
53
58
  That means it can lead to unexpected side effects.
54
59
 
@@ -60,7 +65,7 @@ There is also a known issue of space leakage. So you need to make sure you are a
60
65
 
61
66
  There are infinite ways to extract tokens from a virtual machine connected to the internet. This includes but is not limited to:
62
67
 
63
- - **Claude MAX tokens** - Required for AI operations
68
+ - **Claude MAX tokens** and/or **ChatGPT Pro (Codex) tokens** - Required for AI operations; you can run with either or both
64
69
  - **GitHub tokens** - Required for repository access
65
70
  - **API keys and credentials** - Any sensitive data on the system
66
71
 
@@ -161,7 +166,7 @@ docker exec -it hive-mind /bin/bash
161
166
  # Inside the container, authenticate with GitHub
162
167
  gh-setup-git-identity
163
168
 
164
- # Authenticate with Claude
169
+ # Authenticate with Claude (if you have Claude MAX)
165
170
  claude
166
171
 
167
172
  # Optionally set configuration like this:
@@ -174,6 +179,14 @@ claude
174
179
  # Optionally test Claude connection
175
180
  claude -p hi --model haiku
176
181
 
182
+ # Authenticate with Codex (if you have ChatGPT Pro)
183
+ codex login --device-auth
184
+
185
+ # Optionally test Codex connection. codex exec refuses to run unless
186
+ # either cwd is a git repo it trusts or --skip-git-repo-check is passed.
187
+ # It prints the refusal to STDOUT but still exits 0, so do not skip the flag.
188
+ codex exec --skip-git-repo-check --model gpt-5.4-mini "reply with only OK"
189
+
177
190
  # Verify Playwright MCP is registered for both CLIs in this container image
178
191
  claude mcp list | grep playwright
179
192
  codex mcp list | grep playwright
@@ -567,6 +580,7 @@ Shows:
567
580
  - ✅ **Screen Sessions**: Commands run in detached screen sessions
568
581
  - ✅ **Live Terminal Watch**: `/terminal_watch` and opt-in auto-start show live session logs
569
582
  - ✅ **Chat Restrictions**: Optional whitelist of allowed chat IDs
583
+ - ✅ **Private Auth Check**: Experimental `/auth --status <gh|claude|codex>` and `/auth --login <gh|claude|codex>` for owners of allowlisted chats
570
584
  - ✅ **Diagnostic Tools**: Get chat ID and configuration info
571
585
 
572
586
  #### Live Terminal Watch
@@ -584,6 +598,8 @@ When enabled with `--auto-start-screen-watch-message`, the bot automatically sta
584
598
 
585
599
  - Only works in group chats where the bot is admin
586
600
  - Optional chat ID restrictions via `TELEGRAM_ALLOWED_CHATS`
601
+ - Private `/auth` is disabled unless `TELEGRAM_ALLOWED_CHATS` is set and only
602
+ owners of listed chats can use it
587
603
  - Commands run as the system user running the bot
588
604
  - Ensure proper authentication (`gh auth login`, `claude-profiles`)
589
605
 
package/README.ru.md CHANGED
@@ -21,22 +21,27 @@
21
21
 
22
22
  Hive Mind — это **универсальный ИИ** (мини-AGI), способный работать над широким спектром задач, а не только над программированием. Практически всё, что можно сделать с файлами в репозитории, поддаётся автоматизации.
23
23
 
24
- | Возможность | Что это означает для вас |
25
- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
26
- | **Без постоянного контроля** | Полный автономный режим с правами sudo. ИИ обладает творческой свободой, как настоящий программист. |
27
- | **Изоляция в облаке** | Работает на выделенных виртуальных машинах или в Docker. Легко восстановить в случае поломки. |
28
- | **Полный доступ к интернету + Sudo** | ИИ может устанавливать пакеты, получать документацию и настраивать систему по мере необходимости. |
29
- | **Предустановленный инструментарий** | Более 25 ГБ готово к работе: 10 языковых сред выполнения, 2 средства доказательства теорем, инструменты сборки. Можно установить дополнительные. |
30
- | **Эффективность токенов** | Рутинные задачи автоматизированы в коде, поэтому токены ИИ сосредоточены на творческом решении проблем. |
31
- | **Свобода времени** | То, что занимает у людей 2–8 часов, ИИ выполняет за 10–25 минут за рабочую сессию. Возможно массовое выполнение задач в репозитории. «Код пишется, пока вы спите». |
32
- | **Масштабирование с оркестрацией** | Параллельные воркеры ощущаются как команда разработчиков всё примерно за $200 в месяц. |
33
- | **Контроль со стороны человека** | ИИ создаёт черновые PR — вы решаете, что вливать. Контроль качества там, где это важно. |
34
- | **Программирование с любого устройства** | Управляйте ИИ с любого устройства через `/solve` и `/hive` посредством Telegram-бота. ПК, IDE или ноутбук не нужны. |
35
- | **100% открытый исходный код** | Лицензия Unlicense (общественное достояние). Полная прозрачность, без привязки к поставщику. |
36
-
37
- > _«По сравнению с Codex за $200, это решение огонь.»_ отзыв пользователя
38
-
39
- **Стоимость**: подписка Claude MAX (~$200 в месяц, сейчас со скидкой 50% = ценность $400) обеспечивает практически неограниченное использование для Hive Mind — лучшее соотношение цены и качества на рынке.
24
+ | Возможность | Что это означает для вас |
25
+ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
26
+ | **Без постоянного контроля** | Полный автономный режим с правами sudo. ИИ обладает творческой свободой, как настоящий программист. |
27
+ | **Изоляция в облаке** | Работает на выделенных виртуальных машинах или в Docker. Легко восстановить в случае поломки. |
28
+ | **Полный доступ к интернету + Sudo** | ИИ может устанавливать пакеты, получать документацию и настраивать систему по мере необходимости. |
29
+ | **Предустановленный инструментарий** | Более 25 ГБ готово к работе: 10 языковых сред выполнения, 2 средства доказательства теорем, инструменты сборки. Можно установить дополнительные. |
30
+ | **Эффективность токенов** | Рутинные задачи автоматизированы в коде, поэтому токены ИИ сосредоточены на творческом решении проблем. |
31
+ | **Свобода времени** | То, что занимает у людей 2–8 часов, ИИ выполняет за 10–25 минут за рабочую сессию. Возможно массовое выполнение задач в репозитории. «Код пишется, пока вы спите». |
32
+ | **Масштабирование с оркестрацией** | Параллельные воркеры ощущаются как команда разработчиков. Свяжите Claude MAX и ChatGPT Pro (по $200 каждый), чтобы получить два независимых почти безлимитных бюджета. |
33
+ | **Контроль со стороны человека** | ИИ создаёт черновые PR — вы решаете, что вливать. Контроль качества там, где это важно. |
34
+ | **Программирование с любого устройства** | Управляйте ИИ с любого устройства через `/solve` и `/hive` посредством Telegram-бота. ПК, IDE или ноутбук не нужны. |
35
+ | **100% открытый исходный код** | Лицензия Unlicense (общественное достояние). Полная прозрачность, без привязки к поставщику. |
36
+
37
+ **Стоимость**: Hive Mind поддерживает две подписки по $200 в месяц как полнофункциональные почти «безлимитные» варианты:
38
+
39
+ | Подписка | Используется с `--tool` | Модель по умолчанию | Лучше всего подходит для |
40
+ | ----------------------------------------------------------------- | ----------------------- | ------------------- | ---------------------------------------------------------- |
41
+ | **Anthropic Claude MAX** (~$200 в месяц, часто скидка 50% = $400) | `claude` (по умолчанию) | Sonnet/Haiku | Максимальная креативность и сильное общее кодовое мышление |
42
+ | **OpenAI ChatGPT Pro** ($200 в месяц, включает Codex) | `codex` | `gpt-5.5` | Надёжные детерминированные рефакторинги и быстрые итерации |
43
+
44
+ Оба инструмента можно сочетать в одном hive. Воркеры могут параллельно запускать разные инструменты, а `/codex` или `/solve --tool codex` направляет задачи в ChatGPT Pro, тогда как маршрут по умолчанию идёт в Claude MAX. Выбирать только один вариант не требуется: любой одной подписки достаточно для работы, а использование обеих открывает режим параллелизма по инструментам/моделям (#1474).
40
45
 
41
46
  Hive Mind обладает высоким уровнем творчества, неотличимым от среднего программиста. Он задаёт вопросы, если требования неясны, и вы можете уточнять их на ходу через комментарии к PR.
42
47
 
@@ -48,7 +53,7 @@ Hive Mind обладает высоким уровнем творчества,
48
53
 
49
54
  Рекомендуется использовать Docker для установки (как локально, так и на серверах). Смотрите раздел [Установка через Docker](#использование-docker) ниже.
50
55
 
51
- Это программное обеспечение использует полностью автономный режим Claude Code, а значит, оно может выполнять любые команды по своему усмотрению.
56
+ Это программное обеспечение запускает поддерживаемые ИИ-инструменты, такие как Claude Code и Codex, в полностью автономном режиме, а значит, они могут выполнять любые команды по своему усмотрению.
52
57
 
53
58
  Это может привести к непредвиденным побочным эффектам.
54
59
 
@@ -60,7 +65,7 @@ Hive Mind обладает высоким уровнем творчества,
60
65
 
61
66
  Существует бесконечное множество способов извлечь токены с виртуальной машины, подключённой к интернету. Это включает, но не ограничивается:
62
67
 
63
- - **Токены Claude MAX** — необходимы для работы ИИ
68
+ - **Токены Claude MAX** и/или **токены ChatGPT Pro (Codex)** — необходимы для работы ИИ; можно запускать с любым из них или с обоими
64
69
  - **Токены GitHub** — необходимы для доступа к репозиториям
65
70
  - **API-ключи и учётные данные** — любые конфиденциальные данные в системе
66
71
 
@@ -161,7 +166,7 @@ docker exec -it hive-mind /bin/bash
161
166
  # Inside the container, authenticate with GitHub
162
167
  gh-setup-git-identity
163
168
 
164
- # Authenticate with Claude
169
+ # Authenticate with Claude (if you have Claude MAX)
165
170
  claude
166
171
 
167
172
  # Optionally set configuration like this:
@@ -174,6 +179,14 @@ claude
174
179
  # Optionally test Claude connection
175
180
  claude -p hi --model haiku
176
181
 
182
+ # Authenticate with Codex (if you have ChatGPT Pro)
183
+ codex login --device-auth
184
+
185
+ # Optionally test Codex connection. codex exec refuses to run unless
186
+ # either cwd is a git repo it trusts or --skip-git-repo-check is passed.
187
+ # It prints the refusal to STDOUT but still exits 0, so do not skip the flag.
188
+ codex exec --skip-git-repo-check --model gpt-5.4-mini "reply with only OK"
189
+
177
190
  # You might need to update hive-mind and agent to latest versions:
178
191
  bun install -g @link-assistant/hive-mind
179
192
  bun install -g @link-assistant/agent
@@ -548,6 +561,7 @@ Shows:
548
561
  - ✅ **Screen-сессии**: команды запускаются в отсоединённых screen-сессиях
549
562
  - ✅ **Live Terminal Watch**: `/terminal_watch` и opt-in auto-start показывают live session logs
550
563
  - ✅ **Ограничения по чатам**: опциональный белый список разрешённых ID чатов
564
+ - ✅ **Приватная проверка auth**: экспериментальные `/auth --status <gh|claude|codex>` и `/auth --login <gh|claude|codex>` для владельцев разрешённых чатов
551
565
  - ✅ **Диагностические инструменты**: получение ID чата и информации о конфигурации
552
566
 
553
567
  #### Live Terminal Watch
@@ -566,6 +580,8 @@ Shows:
566
580
 
567
581
  - Работает только в групповых чатах, где бот является администратором
568
582
  - Опциональное ограничение по ID чата через `TELEGRAM_ALLOWED_CHATS`
583
+ - Приватная `/auth` отключена, если `TELEGRAM_ALLOWED_CHATS` не задан, и
584
+ доступна только владельцам перечисленных чатов
569
585
  - Команды выполняются от имени системного пользователя, запустившего бота
570
586
  - Убедитесь в наличии надлежащей аутентификации (`gh auth login`, `claude-profiles`)
571
587
 
package/README.zh.md CHANGED
@@ -29,14 +29,19 @@ Hive Mind 是一款**通用 AI**(迷你 AGI),能够处理广泛的任务
29
29
  | **预装工具链** | 25GB+ 开箱即用:10 种语言运行时、2 个定理证明器、构建工具,还可继续安装更多。 |
30
30
  | **高效利用 Token** | 常规任务通过代码自动化完成,让 AI Token 专注于创造性问题解决。 |
31
31
  | **节省时间** | 人类需要 2~8 小时的工作,AI 每个工作会话仅需 10~25 分钟完成。可批量执行仓库中的任务。"代码在你睡觉时已写好。" |
32
- | **编排式扩展** | 并行工作进程犹如一支开发团队,每月仅需约 $200|
32
+ | **编排式扩展** | 并行工作进程犹如一支开发团队。可将 Claude MAX 和 ChatGPT Pro(各 $200)配对,获得两份独立的近乎无限预算。 |
33
33
  | **人工控制** | AI 创建草稿 PR——由您决定是否合并。在关键节点设置质量把关。 |
34
34
  | **任意设备编程** | 通过 Telegram 机器人使用 `/solve` 和 `/hive` 命令,在任意设备上管理 AI,无需 PC、IDE 或笔记本电脑。 |
35
35
  | **100% 开源** | Unlicense(公共领域)。完全透明,无供应商锁定。 |
36
36
 
37
- > _"与 $200 的 Codex 相比,这个解决方案简直是神器。"_ - 用户反馈
37
+ **费用**:Hive Mind 支持两种 $200/月订阅作为功能完整、近乎“无限”的选项:
38
38
 
39
- **费用**:Claude MAX 订阅(约 $200/月,目前五折优惠 = 价值 $400)可为 Hive Mind 提供几乎无限的使用量——市场上最佳的性价比。
39
+ | 订阅 | 搭配 `--tool` | 默认模型 | 最适合场景 |
40
+ | ---------------------------------------------------------------- | ---------------- | ------------ | ---------------------------- |
41
+ | **Anthropic Claude MAX**(约 $200/月,常有五折优惠 = 价值 $400) | `claude`(默认) | Sonnet/Haiku | 最高创造力、最强通用代码推理 |
42
+ | **OpenAI ChatGPT Pro**($200/月,包含 Codex) | `codex` | `gpt-5.5` | 稳定确定性的重构和快速迭代 |
43
+
44
+ 两个工具可以在同一个 hive 中组合使用。Worker 可以并行运行不同工具,`/codex` 或 `/solve --tool codex` 会将任务路由到 ChatGPT Pro,而默认路由到 Claude MAX。无需二选一:任意单一订阅都足以运行,同时使用两者可解锁按工具/模型划分的并发模式(#1474)。
40
45
 
41
46
  Hive Mind 具备与普通程序员无异的高度创造力。当需求不明确时,它会主动提问,您也可以随时通过 PR 评论进行说明补充。
42
47
 
@@ -48,7 +53,7 @@ Hive Mind 具备与普通程序员无异的高度创造力。当需求不明确
48
53
 
49
54
  建议使用 Docker 进行安装(本地和服务器均适用)。请参阅下方的 [Docker 安装](#使用-docker) 部分。
50
55
 
51
- 本软件使用 Claude Code 的完全自主模式,这意味着它可以自由执行任何它认为合适的命令。
56
+ 本软件会以完全自主模式运行 Claude Code、Codex 等受支持的 AI 工具,这意味着它们可以自由执行任何它们认为合适的命令。
52
57
 
53
58
  这可能导致意想不到的副作用。
54
59
 
@@ -60,7 +65,7 @@ Hive Mind 具备与普通程序员无异的高度创造力。当需求不明确
60
65
 
61
66
  从连接互联网的虚拟机中提取 Token 的方式多种多样,包括但不限于:
62
67
 
63
- - **Claude MAX Token** - AI 操作所必需
68
+ - **Claude MAX Token** 和/或 **ChatGPT Pro (Codex) Token** - AI 操作所必需;您可以使用其中任意一种或同时使用两者
64
69
  - **GitHub Token** - 仓库访问所必需
65
70
  - **API 密钥和凭证** - 系统上的任何敏感数据
66
71
 
@@ -161,7 +166,7 @@ docker exec -it hive-mind /bin/bash
161
166
  # Inside the container, authenticate with GitHub
162
167
  gh-setup-git-identity
163
168
 
164
- # Authenticate with Claude
169
+ # Authenticate with Claude (if you have Claude MAX)
165
170
  claude
166
171
 
167
172
  # Optionally set configuration like this:
@@ -174,6 +179,14 @@ claude
174
179
  # Optionally test Claude connection
175
180
  claude -p hi --model haiku
176
181
 
182
+ # Authenticate with Codex (if you have ChatGPT Pro)
183
+ codex login --device-auth
184
+
185
+ # Optionally test Codex connection. codex exec refuses to run unless
186
+ # either cwd is a git repo it trusts or --skip-git-repo-check is passed.
187
+ # It prints the refusal to STDOUT but still exits 0, so do not skip the flag.
188
+ codex exec --skip-git-repo-check --model gpt-5.4-mini "reply with only OK"
189
+
177
190
  # You might need to update hive-mind and agent to latest versions:
178
191
  bun install -g @link-assistant/hive-mind
179
192
  bun install -g @link-assistant/agent
@@ -542,6 +555,7 @@ Shows:
542
555
  - ✅ **Screen 会话**:命令在后台 Screen 会话中运行
543
556
  - ✅ **Live Terminal Watch**:`/terminal_watch` 和 opt-in auto-start 显示 live session logs
544
557
  - ✅ **聊天限制**:可选配置允许的聊天 ID 白名单
558
+ - ✅ **私聊 Auth 检查**:为白名单聊天所有者提供实验性的 `/auth --status <gh|claude|codex>` 和 `/auth --login <gh|claude|codex>`
545
559
  - ✅ **诊断工具**:获取聊天 ID 和配置信息
546
560
 
547
561
  #### Live Terminal Watch
@@ -560,6 +574,7 @@ Shows:
560
574
 
561
575
  - 仅在机器人为管理员的群聊中有效
562
576
  - 可通过 `TELEGRAM_ALLOWED_CHATS` 配置可选的聊天 ID 限制
577
+ - 如果未设置 `TELEGRAM_ALLOWED_CHATS`,私聊 `/auth` 会被禁用,且只有所列聊天的所有者可以使用
563
578
  - 命令以运行机器人的系统用户身份执行
564
579
  - 请确保已完成正确的身份验证(`gh auth login`、`claude-profiles`)
565
580
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.74.5",
3
+ "version": "1.74.7",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -0,0 +1,298 @@
1
+ import { spawn } from 'child_process';
2
+ import { parseCommandArgs } from './telegram-solve-command.lib.mjs';
3
+
4
+ export const AUTH_PROVIDERS = Object.freeze(['gh', 'claude', 'codex']);
5
+
6
+ const AUTH_PROVIDER_SET = new Set(AUTH_PROVIDERS);
7
+ const AUTH_USAGE = 'Usage: /auth --status <gh|claude|codex> or /auth --login <gh|claude|codex>';
8
+ // eslint-disable-next-line no-control-regex
9
+ const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
10
+ const TOKEN_RE = /\b(?:gh[opsu]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-proj-[A-Za-z0-9_-]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,})\b/g;
11
+ const TOKEN_FIELD_RE = /\b(token|access_token|refresh_token|api[_-]?key|authorization)\s*[:=]\s*["']?[^"'\s,}]+/gi;
12
+
13
+ function trimOutput(text, max = 3500) {
14
+ const value = String(text || '').trim();
15
+ if (value.length <= max) return value;
16
+ return value.slice(0, max) + `\n... truncated ${value.length - max} characters`;
17
+ }
18
+
19
+ function escapeCodeFence(text) {
20
+ return String(text || '').replace(/```/g, '` ` `');
21
+ }
22
+
23
+ function normalizeProvider(provider) {
24
+ return String(provider || '')
25
+ .trim()
26
+ .toLowerCase();
27
+ }
28
+
29
+ function readActionValue(arg) {
30
+ if (arg === '--status') return { action: 'status', provider: null, consumesNext: true };
31
+ if (arg === '--login') return { action: 'login', provider: null, consumesNext: true };
32
+ if (arg.startsWith('--status=')) return { action: 'status', provider: arg.slice('--status='.length), consumesNext: false };
33
+ if (arg.startsWith('--login=')) return { action: 'login', provider: arg.slice('--login='.length), consumesNext: false };
34
+ return null;
35
+ }
36
+
37
+ export function parseAuthRequest(text) {
38
+ const args = parseCommandArgs(text || '');
39
+ let action = null;
40
+ let provider = null;
41
+
42
+ for (let i = 0; i < args.length; i++) {
43
+ const parsed = readActionValue(args[i]);
44
+ if (!parsed) {
45
+ return { action: null, provider: null, error: `Unsupported /auth argument: ${args[i]}\n\n${AUTH_USAGE}` };
46
+ }
47
+ if (action) {
48
+ return { action: null, provider: null, error: `Use exactly one of --status or --login.\n\n${AUTH_USAGE}` };
49
+ }
50
+ action = parsed.action;
51
+ provider = normalizeProvider(parsed.provider);
52
+ if (parsed.consumesNext) {
53
+ const next = args[i + 1];
54
+ if (!next || next.startsWith('--')) {
55
+ return { action: null, provider: null, error: AUTH_USAGE };
56
+ }
57
+ provider = normalizeProvider(next);
58
+ i++;
59
+ }
60
+ }
61
+
62
+ if (!action || !provider) {
63
+ return { action: null, provider: null, error: AUTH_USAGE };
64
+ }
65
+ if (!AUTH_PROVIDER_SET.has(provider)) {
66
+ return { action, provider: null, error: `Unsupported auth provider: ${provider}\n\n${AUTH_USAGE}` };
67
+ }
68
+
69
+ return { action, provider, error: null };
70
+ }
71
+
72
+ export function buildAuthCommand(action, provider) {
73
+ if (action === 'status') {
74
+ if (provider === 'gh') return { command: 'gh', args: ['auth', 'status', '--hostname', 'github.com'] };
75
+ if (provider === 'claude') return { command: 'claude', args: ['auth', 'status'] };
76
+ if (provider === 'codex') return { command: 'codex', args: ['login', 'status'] };
77
+ }
78
+ if (action === 'login') {
79
+ if (provider === 'gh') return { command: 'gh', args: ['auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web'] };
80
+ if (provider === 'claude') return { command: 'claude', args: ['auth', 'login', '--claudeai'] };
81
+ if (provider === 'codex') return { command: 'codex', args: ['login', '--device-auth'] };
82
+ }
83
+ throw new Error(`Unsupported auth command: ${action} ${provider}`);
84
+ }
85
+
86
+ export function redactAuthOutput(output) {
87
+ return String(output || '')
88
+ .replace(ANSI_RE, '')
89
+ .replace(TOKEN_RE, '[REDACTED_TOKEN]')
90
+ .replace(TOKEN_FIELD_RE, (_, name) => `${name}: [REDACTED_TOKEN]`);
91
+ }
92
+
93
+ function collectAuthOutput(result) {
94
+ return redactAuthOutput([result?.stdout, result?.stderr].filter(Boolean).join('\n'));
95
+ }
96
+
97
+ export function extractAuthStartDetails(output) {
98
+ const text = redactAuthOutput(output);
99
+ const urls = [...new Set([...text.matchAll(/https?:\/\/[^\s<>)"']+/g)].map(match => match[0].replace(/[.,;:!?]+$/, '')))];
100
+
101
+ const codePatterns = [/\bone-time code\s*[:=]\s*([A-Z0-9][A-Z0-9-]{3,})/i, /\b(?:user code|verification code|code)\s*[:=]\s*([A-Z0-9][A-Z0-9-]{3,})/i, /\b([A-Z0-9]{4,}-[A-Z0-9-]{4,})\b/];
102
+ let code = null;
103
+ for (const pattern of codePatterns) {
104
+ const match = text.match(pattern);
105
+ if (match) {
106
+ code = match[1].toUpperCase();
107
+ break;
108
+ }
109
+ }
110
+
111
+ return { urls, code };
112
+ }
113
+
114
+ export function formatAuthStatusMessage(provider, result) {
115
+ const code = result?.code;
116
+ const ok = code === 0;
117
+ const output = trimOutput(collectAuthOutput(result)) || '(no output)';
118
+ return `${ok ? 'OK' : 'ERROR'} *${provider} auth status*\n\nExit code: ${code ?? 'unknown'}\n\n\`\`\`\n${escapeCodeFence(output)}\n\`\`\``;
119
+ }
120
+
121
+ export function formatAuthLoginMessage(provider, result) {
122
+ const output = collectAuthOutput(result);
123
+ const details = extractAuthStartDetails(output);
124
+ const lines = [`*${provider} auth login started*`, '', 'The local login command was cancelled locally after capturing the browser step, so this bot command did not replace existing credentials.'];
125
+
126
+ if (details.urls.length > 0) {
127
+ lines.push('', 'Open this URL:');
128
+ for (const url of details.urls) lines.push(url);
129
+ }
130
+ if (details.code) {
131
+ lines.push('', `Code: \`${details.code}\``);
132
+ }
133
+ if (details.urls.length === 0 && !details.code) {
134
+ const shownOutput = trimOutput(output) || '(no output captured)';
135
+ lines.push('', 'Captured output:', '', '```', escapeCodeFence(shownOutput), '```');
136
+ }
137
+ if (result?.cancelled) {
138
+ lines.push('', 'Status: cancelled locally after capture.');
139
+ } else if (typeof result?.code === 'number') {
140
+ lines.push('', `Status: login command exited with code ${result.code}.`);
141
+ }
142
+ lines.push('', 'Continuation by replying with a provider code is not automated yet; this is the first experimental CLI-backed /auth path.');
143
+ return lines.join('\n');
144
+ }
145
+
146
+ export const resolveAllowedAuthChatIds = allowedChats => {
147
+ if (!allowedChats) return [];
148
+ const raw = typeof allowedChats === 'function' ? allowedChats() : allowedChats;
149
+ if (!Array.isArray(raw)) return [];
150
+ return raw.map(value => String(value)).filter(Boolean);
151
+ };
152
+
153
+ export async function isAuthOperator({ telegram, userId, allowedChatIds }) {
154
+ if (!telegram || !userId || !allowedChatIds || allowedChatIds.length === 0) {
155
+ return false;
156
+ }
157
+ for (const chatId of allowedChatIds) {
158
+ if (String(chatId) === String(userId)) return true;
159
+ try {
160
+ const member = await telegram.getChatMember(chatId, userId);
161
+ if (member?.status === 'creator') return true;
162
+ } catch {
163
+ // Try the next configured chat. The bot may no longer be a member.
164
+ }
165
+ }
166
+ return false;
167
+ }
168
+
169
+ export function runAuthCommand(command, args, options = {}) {
170
+ const { mode = 'status', loginCaptureMs = 15000, outputLimit = 20000, env = process.env } = options;
171
+ return new Promise(resolve => {
172
+ const child = spawn(command, args, {
173
+ stdio: ['ignore', 'pipe', 'pipe'],
174
+ env,
175
+ });
176
+ let stdout = '';
177
+ let stderr = '';
178
+ let settled = false;
179
+ let captureTimer = null;
180
+
181
+ const settle = result => {
182
+ if (settled) return;
183
+ settled = true;
184
+ if (captureTimer) clearTimeout(captureTimer);
185
+ resolve({
186
+ stdout: stdout.slice(0, outputLimit),
187
+ stderr: stderr.slice(0, outputLimit),
188
+ ...result,
189
+ });
190
+ };
191
+
192
+ const maybeCancelLogin = () => {
193
+ if (mode !== 'login' || settled) return;
194
+ const details = extractAuthStartDetails(`${stdout}\n${stderr}`);
195
+ if (details.urls.length === 0 && !details.code) return;
196
+ child.kill('SIGTERM');
197
+ settle({ code: null, signal: 'SIGTERM', cancelled: true });
198
+ };
199
+
200
+ child.stdout.on('data', data => {
201
+ stdout += data.toString();
202
+ maybeCancelLogin();
203
+ });
204
+ child.stderr.on('data', data => {
205
+ stderr += data.toString();
206
+ maybeCancelLogin();
207
+ });
208
+ child.on('error', error => {
209
+ settle({ code: null, error: error.message });
210
+ });
211
+ child.on('close', (code, signal) => {
212
+ settle({ code, signal, cancelled: false });
213
+ });
214
+
215
+ if (mode === 'login') {
216
+ captureTimer = setTimeout(() => {
217
+ child.kill('SIGTERM');
218
+ settle({ code: null, signal: 'SIGTERM', cancelled: true });
219
+ }, loginCaptureMs);
220
+ }
221
+ });
222
+ }
223
+
224
+ export function registerAuthCommand(bot, options = {}) {
225
+ const { VERBOSE = false, isOldMessage, isForwardedOrReply, allowedChats, authEnabled = true } = options;
226
+ const execute = options.runCommand || runAuthCommand;
227
+ const reply = options.safeReply || ((ctx, text, replyOptions) => ctx.reply(text, replyOptions));
228
+
229
+ async function handleAuthCommand(ctx) {
230
+ VERBOSE && console.log('[VERBOSE] /auth command received');
231
+
232
+ if (isOldMessage && isOldMessage(ctx)) {
233
+ VERBOSE && console.log('[VERBOSE] /auth ignored: old message');
234
+ return;
235
+ }
236
+ if (isForwardedOrReply && isForwardedOrReply(ctx)) {
237
+ VERBOSE && console.log('[VERBOSE] /auth ignored: forwarded or reply');
238
+ return;
239
+ }
240
+ if (!authEnabled) {
241
+ await reply(ctx, 'The /auth command is disabled on this bot instance.', { reply_to_message_id: ctx.message?.message_id });
242
+ return;
243
+ }
244
+ if (!ctx.chat || !ctx.from || !ctx.message) return;
245
+ if (ctx.chat.type !== 'private') {
246
+ await reply(ctx, 'The /auth command is only available in private messages.', { reply_to_message_id: ctx.message.message_id });
247
+ return;
248
+ }
249
+
250
+ const allowedChatIds = resolveAllowedAuthChatIds(allowedChats);
251
+ if (allowedChatIds.length === 0) {
252
+ await reply(ctx, 'The /auth command is disabled because TELEGRAM_ALLOWED_CHATS is not configured.', { reply_to_message_id: ctx.message.message_id });
253
+ return;
254
+ }
255
+
256
+ const authorized = await isAuthOperator({ telegram: ctx.telegram, userId: ctx.from.id, allowedChatIds });
257
+ if (!authorized) {
258
+ VERBOSE && console.log(`[VERBOSE] /auth denied: user ${ctx.from.id} is not creator of any allowed chat`);
259
+ await reply(ctx, 'The /auth command is only available to owners of allowlisted chats.', { reply_to_message_id: ctx.message.message_id });
260
+ return;
261
+ }
262
+
263
+ const request = parseAuthRequest(ctx.message.text || '');
264
+ if (request.error) {
265
+ await reply(ctx, request.error, { reply_to_message_id: ctx.message.message_id });
266
+ return;
267
+ }
268
+
269
+ const { command, args } = buildAuthCommand(request.action, request.provider);
270
+ let result;
271
+ try {
272
+ result = await execute(command, args, { mode: request.action, provider: request.provider });
273
+ } catch (error) {
274
+ await reply(ctx, `Failed to run ${request.provider} auth ${request.action}: ${error.message || String(error)}`, { reply_to_message_id: ctx.message.message_id });
275
+ return;
276
+ }
277
+
278
+ const message = request.action === 'status' ? formatAuthStatusMessage(request.provider, result) : formatAuthLoginMessage(request.provider, result);
279
+ await reply(ctx, message, { parse_mode: 'Markdown', reply_to_message_id: ctx.message.message_id });
280
+ }
281
+
282
+ bot.command('auth', handleAuthCommand);
283
+ return { handleAuthCommand };
284
+ }
285
+
286
+ export default {
287
+ AUTH_PROVIDERS,
288
+ buildAuthCommand,
289
+ extractAuthStartDetails,
290
+ formatAuthLoginMessage,
291
+ formatAuthStatusMessage,
292
+ isAuthOperator,
293
+ parseAuthRequest,
294
+ redactAuthOutput,
295
+ registerAuthCommand,
296
+ resolveAllowedAuthChatIds,
297
+ runAuthCommand,
298
+ };
@@ -40,7 +40,7 @@ const { applySolveToolAlias, getFirstParsedPositionalArg, getSolveCommandNameFro
40
40
  const { executeStartScreen: executeStartScreenCommand, buildExecuteAndUpdateMessage } = await import('./telegram-command-execution.lib.mjs');
41
41
  const { isChatStopped, getChatStopInfo, getStoppedChatRejectMessage, DEFAULT_STOP_REASON } = await import('./telegram-start-stop-command.lib.mjs');
42
42
  const { isOldMessage: _isOldMessage, isGroupChat: _isGroupChat, isChatAuthorized: _isChatAuthorized, isForwardedOrReply: _isForwardedOrReply, extractCommandFromText, extractGitHubUrl: _extractGitHubUrl } = await import('./telegram-message-filters.lib.mjs');
43
- const { installTelegramFormattingFallback, safeEditMessageText, safeReply } = await import('./telegram-safe-reply.lib.mjs');
43
+ const { installTelegramFormattingFallback, isTelegramFormattingError, isTelegramMessageTooLongError, safeEditMessageText, safeReply, TELEGRAM_TEXT_LIMIT } = await import('./telegram-safe-reply.lib.mjs');
44
44
  const { registerTerminalWatchCommand, startAutoTerminalWatchForSession } = await import('./telegram-terminal-watch-command.lib.mjs');
45
45
  const { launchBotWithRetry } = await import('./telegram-bot-launcher.lib.mjs');
46
46
  const { trackSession, startSessionMonitoring, hasActiveSessionForUrlAsync } = await import('./session-monitor.lib.mjs');
@@ -100,6 +100,11 @@ const config = yargs(hideBin(process.argv))
100
100
  description: 'Enable /task and /split commands (use --no-task to disable)',
101
101
  default: getenv('TELEGRAM_TASK', 'true') !== 'false',
102
102
  })
103
+ .option('auth', {
104
+ type: 'boolean',
105
+ description: 'Enable experimental private /auth command for allowlisted chat owners (use --no-auth to disable)',
106
+ default: getenv('TELEGRAM_AUTH', 'true') !== 'false',
107
+ })
103
108
  .option('dryRun', {
104
109
  type: 'boolean',
105
110
  description: 'Validate configuration and options without starting the bot',
@@ -163,6 +168,7 @@ const hiveOverrides = resolvedHiveOverrides
163
168
  const solveEnabled = config.solve;
164
169
  const hiveEnabled = config.hive;
165
170
  const taskEnabled = config.task;
171
+ const authEnabled = config.auth;
166
172
  // Isolation mode (experimental): uses `$` from start-command with specified backend
167
173
  const ISOLATION_BACKEND = (config.isolation || getenv('TELEGRAM_ISOLATION', '')).trim().toLowerCase();
168
174
  let isolationRunner = null;
@@ -283,7 +289,7 @@ if (config.dryRun) {
283
289
  if (allowedTopics && allowedTopics.length > 0) {
284
290
  console.log(' Allowed topics:', lino.formatLinks(allowedTopics));
285
291
  }
286
- console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled });
292
+ console.log(' Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
287
293
  if (solveOverrides.length > 0) {
288
294
  console.log(' Solve overrides:', lino.format(solveOverrides));
289
295
  }
@@ -606,6 +612,8 @@ const { registerSubscribeCommands } = await import('./telegram-subscribers.lib.m
606
612
  registerSubscribeCommands(bot, sharedCommandOpts);
607
613
  const { registerTaskCommands } = await import('./telegram-task-command.lib.mjs');
608
614
  const { handleTaskCommand, TASK_COMMAND_NAMES } = registerTaskCommands(bot, { ...sharedCommandOpts, taskEnabled, safeReply, executeAndUpdateMessage, resolveLocale: resolveLocaleFromTelegramCtx });
615
+ const { registerAuthCommand } = await import('./telegram-auth-command.lib.mjs');
616
+ const { handleAuthCommand } = registerAuthCommand(bot, { ...sharedCommandOpts, allowedChats, authEnabled, safeReply });
609
617
 
610
618
  // Named handler for /solve command - extracted for reuse by text-based fallback (issue #1207)
611
619
  async function handleSolveCommand(ctx) {
@@ -1170,7 +1178,7 @@ bot.on('message', async (ctx, next) => {
1170
1178
  const solveHandlers = Object.fromEntries(SOLVE_COMMAND_NAMES.map(command => [command, handleSolveCommand]));
1171
1179
  const taskHandlers = Object.fromEntries(TASK_COMMAND_NAMES.map(command => [command, handleTaskCommand]));
1172
1180
  // /queue is the short alias for /solve_queue (issue #1837)
1173
- const handlers = { ...solveHandlers, ...taskHandlers, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand, queue: handleSolveQueueCommand };
1181
+ const handlers = { ...solveHandlers, ...taskHandlers, auth: handleAuthCommand, hive: handleHiveCommand, solve_queue: handleSolveQueueCommand, solvequeue: handleSolveQueueCommand, queue: handleSolveQueueCommand };
1174
1182
 
1175
1183
  const handler = handlers[extracted.command];
1176
1184
  if (!handler) return next();
@@ -1214,15 +1222,17 @@ bot.catch((error, ctx) => {
1214
1222
 
1215
1223
  // Try to notify the user about the error with more details
1216
1224
  if (ctx?.reply) {
1217
- const isTelegramParsingError = 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')));
1225
+ const isTelegramParsingError = isTelegramFormattingError(error);
1226
+ const isTelegramTextLimitError = isTelegramMessageTooLongError(error);
1218
1227
 
1219
1228
  let errorMessage;
1220
1229
 
1221
- if (isTelegramParsingError) {
1230
+ if (isTelegramParsingError || isTelegramTextLimitError) {
1222
1231
  // Issue #1460: Log detailed context for root cause analysis (always logged, not just in verbose mode)
1223
1232
  const userInfo = ctx.from ? { id: ctx.from.id, username: ctx.from.username, first_name: ctx.from.first_name, last_name: ctx.from.last_name } : 'unknown';
1224
- console.error(`[telegram-bot] Parsing error: ${error.message}`);
1225
- console.error(`[telegram-bot] Parsing error context - user: ${JSON.stringify(userInfo)}, command: ${ctx.message?.text?.split(' ')[0] || 'unknown'}`);
1233
+ const errorKind = isTelegramTextLimitError ? 'Message length error' : 'Parsing error';
1234
+ console.error(`[telegram-bot] ${errorKind}: ${error.message}`);
1235
+ console.error(`[telegram-bot] ${errorKind} context - user: ${JSON.stringify(userInfo)}, command: ${ctx.message?.text?.split(' ')[0] || 'unknown'}`);
1226
1236
  console.error(`[telegram-bot] User input text: ${ctx.message?.text || 'none'}`);
1227
1237
  if (ctx.message?.text) {
1228
1238
  const visibleInput = makeSpecialCharsVisible(ctx.message.text, { maxLength: 500 });
@@ -1233,8 +1243,11 @@ bot.catch((error, ctx) => {
1233
1243
  }
1234
1244
  }
1235
1245
 
1236
- // Issue #1460: Show user a simple, non-confusing message — all details are in the logs
1237
- errorMessage = `❌ Failed to send formatted message. Please try your command again.\n\nIf the issue persists, contact support with Update ID: ${ctx.update.update_id}`;
1246
+ if (isTelegramTextLimitError) {
1247
+ errorMessage = `❌ Telegram rejected an oversized bot message.\n\nThe bot splits messages above ${TELEGRAM_TEXT_LIMIT} characters automatically. Please try your command again.\n\nIf the issue persists, contact support with Update ID: ${ctx.update.update_id}`;
1248
+ } else {
1249
+ errorMessage = `❌ Telegram rejected a formatted bot message, and the fallback handler could not recover automatically.\n\nPlease try your command again.\n\nIf the issue persists, contact support with Update ID: ${ctx.update.update_id}`;
1250
+ }
1238
1251
  } else {
1239
1252
  errorMessage = '❌ An error occurred while processing your request.\n\n';
1240
1253
  if (error.message) {
@@ -1251,20 +1264,13 @@ bot.catch((error, ctx) => {
1251
1264
  if (VERBOSE) errorMessage += `\n\n🔍 Debug info: Update ID: ${ctx.update.update_id}`;
1252
1265
  }
1253
1266
 
1254
- // Issue #1460: For parsing errors send plain text; otherwise try Markdown first
1255
- if (isTelegramParsingError) {
1256
- ctx.reply(errorMessage).catch(fallbackError => {
1257
- console.error('Failed to send plain text error message:', fallbackError);
1267
+ safeReply(ctx, errorMessage, { fallbackLocale: resolveLocaleFromTelegramCtx(ctx), verbose: VERBOSE }).catch(replyError => {
1268
+ console.error('Failed to send error message to user:', replyError);
1269
+ const plainMessage = `An error occurred while processing your request. Please try again or contact support.\n\nError: ${error.message || 'Unknown error'}`;
1270
+ ctx.reply(plainMessage).catch(fallbackError => {
1271
+ console.error('Failed to send fallback error message:', fallbackError);
1258
1272
  });
1259
- } else {
1260
- ctx.reply(errorMessage, { parse_mode: 'Markdown' }).catch(replyError => {
1261
- console.error('Failed to send error message to user:', replyError);
1262
- const plainMessage = `An error occurred while processing your request. Please try again or contact support.\n\nError: ${error.message || 'Unknown error'}`;
1263
- ctx.reply(plainMessage).catch(fallbackError => {
1264
- console.error('Failed to send fallback error message:', fallbackError);
1265
- });
1266
- });
1267
- }
1273
+ });
1268
1274
  }
1269
1275
  });
1270
1276
 
@@ -1281,7 +1287,7 @@ if (allowedChats && allowedChats.length > 0) {
1281
1287
  if (allowedTopics && allowedTopics.length > 0) {
1282
1288
  console.log('Allowed topics (lino):', lino.formatLinks(allowedTopics));
1283
1289
  }
1284
- console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled });
1290
+ console.log('Commands enabled:', { solve: solveEnabled, hive: hiveEnabled, task: taskEnabled, auth: authEnabled });
1285
1291
  if (solveOverrides.length > 0) console.log('Solve overrides (lino):', lino.format(solveOverrides));
1286
1292
  if (hiveOverrides.length > 0) console.log('Hive overrides (lino):', lino.format(hiveOverrides));
1287
1293
  if (VERBOSE) {
@@ -2,6 +2,7 @@ import { normalizeLocale, t } from './i18n.lib.mjs';
2
2
 
3
3
  const FORMATTING_FALLBACK_INSTALLED = Symbol.for('hiveMind.telegramFormattingFallbackInstalled');
4
4
  const DEFAULT_FORMATTING_FALLBACK_WARNING = '⚠️ Formatting error detected. Showing plain text fallback.';
5
+ export const TELEGRAM_TEXT_LIMIT = 4096;
5
6
  const FORMATTING_FALLBACK_WARNINGS = {
6
7
  en: DEFAULT_FORMATTING_FALLBACK_WARNING,
7
8
  ru: '⚠️ Обнаружена ошибка форматирования. Показываю обычный текст.',
@@ -18,9 +19,18 @@ function splitOptions(options = {}) {
18
19
  };
19
20
  }
20
21
 
22
+ function getTelegramErrorMessage(error) {
23
+ return error?.description || error?.message || String(error || '');
24
+ }
25
+
21
26
  export function isTelegramFormattingError(error) {
22
- const message = error?.description || error?.message || String(error || '');
23
- return /can't parse entities/i.test(message) || /can't find end of/i.test(message) || /entity.*parse/i.test(message) || (/bad request/i.test(message) && /400|parse|entity/i.test(message));
27
+ const message = getTelegramErrorMessage(error);
28
+ return /can't parse entities/i.test(message) || /can't find end of/i.test(message) || /entity.*parse/i.test(message) || /parse.*entity/i.test(message) || /character .* is reserved/i.test(message) || /unsupported start tag/i.test(message);
29
+ }
30
+
31
+ export function isTelegramMessageTooLongError(error) {
32
+ const message = getTelegramErrorMessage(error);
33
+ return /message is too long/i.test(message) || /message text is too long/i.test(message) || /text is too long/i.test(message) || /message_too_long/i.test(message) || (/bad request/i.test(message) && /too long/i.test(message) && /(message|text|caption)/i.test(message));
24
34
  }
25
35
 
26
36
  export function stripTelegramMarkdown(text) {
@@ -36,6 +46,45 @@ export function stripTelegramMarkdown(text) {
36
46
  .replace(/`([^`]+)`/g, '$1');
37
47
  }
38
48
 
49
+ function findTelegramSplitIndex(text, limit) {
50
+ const minUsefulSplit = Math.floor(limit * 0.45);
51
+ const separators = ['\n\n', '\n', '. ', '; ', ', ', ' '];
52
+
53
+ for (const separator of separators) {
54
+ const searchEnd = Math.max(0, limit - separator.length);
55
+ const index = text.lastIndexOf(separator, searchEnd);
56
+ if (index >= minUsefulSplit) {
57
+ return index + separator.length;
58
+ }
59
+ }
60
+
61
+ return limit;
62
+ }
63
+
64
+ export function splitTelegramMessageText(text, limit = TELEGRAM_TEXT_LIMIT) {
65
+ const source = String(text ?? '');
66
+ if (source.length <= limit) return [source];
67
+
68
+ const chunks = [];
69
+ let remaining = source;
70
+
71
+ while (remaining.length > limit) {
72
+ let splitAt = findTelegramSplitIndex(remaining, limit);
73
+ let chunk = remaining.slice(0, splitAt).trimEnd();
74
+
75
+ if (!chunk) {
76
+ splitAt = limit;
77
+ chunk = remaining.slice(0, splitAt);
78
+ }
79
+
80
+ chunks.push(chunk);
81
+ remaining = remaining.slice(splitAt).trimStart();
82
+ }
83
+
84
+ if (remaining) chunks.push(remaining);
85
+ return chunks;
86
+ }
87
+
39
88
  function getFormattingFallbackWarning(locale) {
40
89
  const key = 'telegram.formatting_fallback_warning';
41
90
  const normalizedLocale = normalizeLocale(locale);
@@ -50,7 +99,7 @@ export function buildTelegramFormattingFallbackText(text, options = {}) {
50
99
  }
51
100
 
52
101
  function logFormattingFailure(scope, error, text, verbose = false, fallbackText = null) {
53
- const message = error?.description || error?.message || String(error || '');
102
+ const message = getTelegramErrorMessage(error);
54
103
  console.error(`[telegram-bot] ${scope}: formatted Telegram message failed: ${message}`);
55
104
  if (verbose) {
56
105
  const originalBytes = Buffer.byteLength(String(text ?? ''), 'utf-8');
@@ -76,54 +125,241 @@ function logFormattingFailure(scope, error, text, verbose = false, fallbackText
76
125
  }
77
126
  }
78
127
 
128
+ function logMessageTooLongFailure(scope, error, text, verbose = false, fallbackText = null) {
129
+ const message = getTelegramErrorMessage(error);
130
+ console.error(`[telegram-bot] ${scope}: Telegram message exceeded ${TELEGRAM_TEXT_LIMIT} character limit: ${message}`);
131
+ if (verbose) {
132
+ const original = String(text ?? '');
133
+ console.error(`[telegram-bot] ${scope}: Oversized message (${original.length} chars, ${Buffer.byteLength(original, 'utf-8')} bytes): ${original}`);
134
+ if (fallbackText !== null) {
135
+ const fallback = String(fallbackText ?? '');
136
+ console.error(`[telegram-bot] ${scope}: Plain text fallback (${fallback.length} chars, ${Buffer.byteLength(fallback, 'utf-8')} bytes): ${fallback}`);
137
+ }
138
+ }
139
+ }
140
+
141
+ function logChunking(scope, text, chunks, verbose = false) {
142
+ if (chunks.length <= 1) return;
143
+ const source = String(text ?? '');
144
+ console.warn(`[telegram-bot] ${scope}: Telegram text is ${source.length} chars (${Buffer.byteLength(source, 'utf-8')} bytes), splitting into ${chunks.length} messages (limit ${TELEGRAM_TEXT_LIMIT}).`);
145
+ if (verbose) {
146
+ chunks.forEach((chunk, index) => {
147
+ console.error(`[telegram-bot] ${scope}: Chunk ${index + 1}/${chunks.length} (${chunk.length} chars, ${Buffer.byteLength(chunk, 'utf-8')} bytes): ${chunk}`);
148
+ });
149
+ }
150
+ }
151
+
152
+ function getPlainTextOptions(telegramOptions) {
153
+ return { ...telegramOptions, parse_mode: undefined, entities: undefined };
154
+ }
155
+
156
+ async function sendPlainTextChunks({ text, telegramOptions, scope, verbose, sendChunk }) {
157
+ const plainOptions = getPlainTextOptions(telegramOptions);
158
+ const chunks = splitTelegramMessageText(text);
159
+ logChunking(`${scope}:plainText`, text, chunks, verbose);
160
+
161
+ let firstResult;
162
+ for (const chunk of chunks) {
163
+ const result = await sendChunk(chunk, plainOptions);
164
+ if (firstResult === undefined) firstResult = result;
165
+ }
166
+ return firstResult;
167
+ }
168
+
169
+ async function sendTelegramTextChunks({ text, telegramOptions, fallbackLocale, verbose, scope, sendChunk }) {
170
+ const chunks = splitTelegramMessageText(text);
171
+ logChunking(scope, text, chunks, verbose);
172
+
173
+ let firstResult;
174
+ for (const chunk of chunks) {
175
+ try {
176
+ const result = await sendChunk(chunk, telegramOptions);
177
+ if (firstResult === undefined) firstResult = result;
178
+ } catch (error) {
179
+ let fallbackText;
180
+ if (isTelegramFormattingError(error)) {
181
+ fallbackText = buildTelegramFormattingFallbackText(chunk, { fallbackLocale });
182
+ logFormattingFailure(scope, error, chunk, verbose, fallbackText);
183
+ } else if (isTelegramMessageTooLongError(error)) {
184
+ fallbackText = stripTelegramMarkdown(chunk);
185
+ logMessageTooLongFailure(scope, error, chunk, verbose, fallbackText);
186
+ } else {
187
+ throw error;
188
+ }
189
+
190
+ const result = await sendPlainTextChunks({
191
+ text: fallbackText,
192
+ telegramOptions,
193
+ scope,
194
+ verbose,
195
+ sendChunk,
196
+ });
197
+ if (firstResult === undefined) firstResult = result;
198
+ }
199
+ }
200
+
201
+ return firstResult;
202
+ }
203
+
204
+ async function sendRemainingEditChunks({ chunks, telegramOptions, fallbackLocale, verbose, scope, sendFollowUpChunk }) {
205
+ if (chunks.length === 0) return undefined;
206
+ if (!sendFollowUpChunk) {
207
+ console.error(`[telegram-bot] ${scope}: cannot send ${chunks.length} remaining chunk(s) after edit because chat_id is unavailable.`);
208
+ return undefined;
209
+ }
210
+
211
+ return await sendTelegramTextChunks({
212
+ text: chunks.join('\n'),
213
+ telegramOptions,
214
+ fallbackLocale,
215
+ verbose,
216
+ scope: `${scope}:followUp`,
217
+ sendChunk: sendFollowUpChunk,
218
+ });
219
+ }
220
+
221
+ async function sendPlainRemainingEditChunks({ chunks, telegramOptions, verbose, scope, sendFollowUpChunk }) {
222
+ if (chunks.length === 0) return undefined;
223
+ if (!sendFollowUpChunk) {
224
+ console.error(`[telegram-bot] ${scope}: cannot send ${chunks.length} plain-text fallback chunk(s) after edit because chat_id is unavailable.`);
225
+ return undefined;
226
+ }
227
+
228
+ return await sendPlainTextChunks({
229
+ text: chunks.join('\n'),
230
+ telegramOptions,
231
+ scope: `${scope}:followUp`,
232
+ verbose,
233
+ sendChunk: sendFollowUpChunk,
234
+ });
235
+ }
236
+
237
+ async function editTelegramTextChunks({ text, telegramOptions, fallbackLocale, verbose, scope, editChunk, sendFollowUpChunk }) {
238
+ const chunks = splitTelegramMessageText(text);
239
+ logChunking(scope, text, chunks, verbose);
240
+
241
+ const [firstChunk, ...remainingChunks] = chunks;
242
+ try {
243
+ const result = await editChunk(firstChunk, telegramOptions);
244
+ await sendRemainingEditChunks({
245
+ chunks: remainingChunks,
246
+ telegramOptions,
247
+ fallbackLocale,
248
+ verbose,
249
+ scope,
250
+ sendFollowUpChunk,
251
+ });
252
+ return result;
253
+ } catch (error) {
254
+ let fallbackText;
255
+ if (isTelegramFormattingError(error)) {
256
+ fallbackText = buildTelegramFormattingFallbackText(firstChunk, { fallbackLocale });
257
+ logFormattingFailure(scope, error, firstChunk, verbose, fallbackText);
258
+ } else if (isTelegramMessageTooLongError(error)) {
259
+ fallbackText = stripTelegramMarkdown(firstChunk);
260
+ logMessageTooLongFailure(scope, error, firstChunk, verbose, fallbackText);
261
+ } else {
262
+ throw error;
263
+ }
264
+
265
+ const plainOptions = getPlainTextOptions(telegramOptions);
266
+ const fallbackChunks = splitTelegramMessageText(fallbackText);
267
+ logChunking(`${scope}:plainText`, fallbackText, fallbackChunks, verbose);
268
+ const [firstFallbackChunk, ...remainingFallbackChunks] = fallbackChunks;
269
+ const result = await editChunk(firstFallbackChunk, plainOptions);
270
+ await sendPlainRemainingEditChunks({
271
+ chunks: [...remainingFallbackChunks, ...remainingChunks.map(stripTelegramMarkdown)],
272
+ telegramOptions,
273
+ verbose,
274
+ scope,
275
+ sendFollowUpChunk,
276
+ });
277
+ return result;
278
+ }
279
+ }
280
+
79
281
  // Issue #1460/#1497/#1788: try Markdown first, fall back to localized plain text on parsing errors.
80
282
  export async function safeReply(ctx, text, options = {}) {
81
283
  const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
82
284
  const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
83
- try {
84
- return await ctx.reply(text, firstOptions);
85
- } catch (error) {
86
- if (!isTelegramFormattingError(error)) throw error;
87
- const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
88
- logFormattingFailure('safeReply', error, text, verbose, fallbackText);
89
- return await ctx.reply(fallbackText, { ...telegramOptions, parse_mode: undefined });
90
- }
285
+ return await sendTelegramTextChunks({
286
+ text,
287
+ telegramOptions: firstOptions,
288
+ fallbackLocale,
289
+ verbose,
290
+ scope: 'safeReply',
291
+ sendChunk: (chunk, chunkOptions) => ctx.reply(chunk, chunkOptions),
292
+ });
91
293
  }
92
294
 
93
295
  export async function safeEditMessageText(telegram, chatId, messageId, inlineMessageId, text, options = {}) {
94
296
  const { telegramOptions, fallbackLocale, verbose } = splitOptions(options);
95
297
  const firstOptions = { parse_mode: 'Markdown', ...telegramOptions };
96
- try {
97
- return await telegram.editMessageText(chatId, messageId, inlineMessageId, text, firstOptions);
98
- } catch (error) {
99
- if (!isTelegramFormattingError(error)) throw error;
100
- const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale });
101
- logFormattingFailure('safeEditMessageText', error, text, verbose, fallbackText);
102
- return await telegram.editMessageText(chatId, messageId, inlineMessageId, fallbackText, { ...telegramOptions, parse_mode: undefined });
103
- }
298
+ return await editTelegramTextChunks({
299
+ text,
300
+ telegramOptions: firstOptions,
301
+ fallbackLocale,
302
+ verbose,
303
+ scope: 'safeEditMessageText',
304
+ editChunk: (chunk, chunkOptions) => telegram.editMessageText(chatId, messageId, inlineMessageId, chunk, chunkOptions),
305
+ sendFollowUpChunk: chatId !== undefined && chatId !== null && typeof telegram.sendMessage === 'function' ? (chunk, chunkOptions) => telegram.sendMessage(chatId, chunk, chunkOptions) : null,
306
+ });
104
307
  }
105
308
 
106
- function wrapTelegramMethod(telegram, methodName, textIndex, optionsIndex, defaults = {}) {
107
- const original = telegram?.[methodName];
309
+ function wrapTelegramSendMessage(telegram, defaults = {}) {
310
+ const original = telegram?.sendMessage;
108
311
  if (typeof original !== 'function') return;
109
312
 
110
- telegram[methodName] = async function wrappedTelegramMessageMethod(...args) {
111
- const text = args[textIndex];
112
- const originalOptions = args[optionsIndex] || {};
313
+ telegram.sendMessage = async function wrappedTelegramSendMessage(...args) {
314
+ const text = args[1];
315
+ const originalOptions = args[2] || {};
113
316
  const { telegramOptions, fallbackLocale, verbose } = splitOptions(originalOptions);
114
- args[optionsIndex] = telegramOptions;
317
+ args[2] = telegramOptions;
115
318
 
116
- try {
117
- return await original.apply(this, args);
118
- } catch (error) {
119
- if (!isTelegramFormattingError(error) || typeof text !== 'string') throw error;
120
- const fallbackText = buildTelegramFormattingFallbackText(text, { fallbackLocale: fallbackLocale || defaults.fallbackLocale });
121
- logFormattingFailure(methodName, error, text, verbose || defaults.verbose, fallbackText);
122
- const retryArgs = [...args];
123
- retryArgs[textIndex] = fallbackText;
124
- retryArgs[optionsIndex] = { ...telegramOptions, parse_mode: undefined };
125
- return await original.apply(this, retryArgs);
126
- }
319
+ if (typeof text !== 'string') return await original.apply(this, args);
320
+
321
+ return await sendTelegramTextChunks({
322
+ text,
323
+ telegramOptions,
324
+ fallbackLocale: fallbackLocale || defaults.fallbackLocale,
325
+ verbose: verbose || defaults.verbose,
326
+ scope: 'sendMessage',
327
+ sendChunk: (chunk, chunkOptions) => {
328
+ const chunkArgs = [...args];
329
+ chunkArgs[1] = chunk;
330
+ chunkArgs[2] = chunkOptions;
331
+ return original.apply(this, chunkArgs);
332
+ },
333
+ });
334
+ };
335
+ }
336
+
337
+ function wrapTelegramEditMessageText(telegram, defaults = {}) {
338
+ const original = telegram?.editMessageText;
339
+ if (typeof original !== 'function') return;
340
+
341
+ telegram.editMessageText = async function wrappedTelegramEditMessageText(...args) {
342
+ const text = args[3];
343
+ const originalOptions = args[4] || {};
344
+ const { telegramOptions, fallbackLocale, verbose } = splitOptions(originalOptions);
345
+ args[4] = telegramOptions;
346
+
347
+ if (typeof text !== 'string') return await original.apply(this, args);
348
+
349
+ return await editTelegramTextChunks({
350
+ text,
351
+ telegramOptions,
352
+ fallbackLocale: fallbackLocale || defaults.fallbackLocale,
353
+ verbose: verbose || defaults.verbose,
354
+ scope: 'editMessageText',
355
+ editChunk: (chunk, chunkOptions) => {
356
+ const chunkArgs = [...args];
357
+ chunkArgs[3] = chunk;
358
+ chunkArgs[4] = chunkOptions;
359
+ return original.apply(this, chunkArgs);
360
+ },
361
+ sendFollowUpChunk: args[0] !== undefined && args[0] !== null && typeof this.sendMessage === 'function' ? (chunk, chunkOptions) => this.sendMessage(args[0], chunk, chunkOptions) : null,
362
+ });
127
363
  };
128
364
  }
129
365
 
@@ -135,8 +371,8 @@ export function installTelegramFormattingFallback(telegram, options = {}) {
135
371
  verbose: Boolean(options.verbose),
136
372
  };
137
373
 
138
- wrapTelegramMethod(telegram, 'sendMessage', 1, 2, defaults);
139
- wrapTelegramMethod(telegram, 'editMessageText', 3, 4, defaults);
374
+ wrapTelegramSendMessage(telegram, defaults);
375
+ wrapTelegramEditMessageText(telegram, defaults);
140
376
  telegram[FORMATTING_FALLBACK_INSTALLED] = true;
141
377
  return telegram;
142
378
  }