@grinev/opencode-telegram-bot 0.21.1 → 0.22.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.
Files changed (193) hide show
  1. package/.env.example +10 -15
  2. package/README.md +33 -13
  3. package/dist/app/{start-bot-app.js → bootstrap/start-bot-app.js} +16 -15
  4. package/dist/{bot/utils/assistant-run-footer.js → app/formatters/assistant-run-footer-formatter.js} +1 -1
  5. package/dist/{summary → app/formatters}/subagent-formatter.js +8 -3
  6. package/dist/{summary/formatter.js → app/formatters/summary-formatter.js} +20 -132
  7. package/dist/{summary → app/formatters}/tool-message-batcher.js +1 -1
  8. package/dist/{bot/assistant-run-state.js → app/managers/assistant-run-state-manager.js} +1 -1
  9. package/dist/{attach/manager.js → app/managers/attach-manager.js} +1 -1
  10. package/dist/{background-session/tracker.js → app/managers/background-session-manager.js} +2 -2
  11. package/dist/{scheduled-task/foreground-state.js → app/managers/foreground-session-state-manager.js} +1 -1
  12. package/dist/{interaction/manager.js → app/managers/interaction-manager.js} +31 -1
  13. package/dist/{permission/manager.js → app/managers/permission-manager.js} +1 -1
  14. package/dist/{question/manager.js → app/managers/question-manager.js} +1 -1
  15. package/dist/{rename/manager.js → app/managers/rename-manager.js} +1 -1
  16. package/dist/{scheduled-task/creation-manager.js → app/managers/scheduled-task-creation-manager.js} +2 -2
  17. package/dist/{summary/aggregator.js → app/managers/summary-aggregation-manager.js} +177 -13
  18. package/dist/{agent/manager.js → app/services/agent-selection-service.js} +4 -5
  19. package/dist/{attach/service.js → app/services/attach-service.js} +23 -44
  20. package/dist/{bot/utils/busy-reconciliation.js → app/services/busy-reconciliation-service.js} +12 -7
  21. package/dist/app/services/command-catalog-service.js +21 -0
  22. package/dist/{bot/utils/external-user-input.js → app/services/external-user-input-service.js} +4 -19
  23. package/dist/app/services/file-browser-service.js +265 -0
  24. package/dist/{bot/utils/file-download.js → app/services/file-download-service.js} +14 -28
  25. package/dist/app/services/mcp-catalog-service.js +79 -0
  26. package/dist/app/services/message-history-service.js +81 -0
  27. package/dist/{model/capabilities.js → app/services/model-capabilities-service.js} +2 -2
  28. package/dist/{model/context-limit.js → app/services/model-context-limit-service.js} +4 -4
  29. package/dist/{model/manager.js → app/services/model-selection-service.js} +4 -4
  30. package/dist/{project/manager.js → app/services/project-service.js} +3 -3
  31. package/dist/app/services/project-switch-service.js +39 -0
  32. package/dist/{bot/utils/busy-guard.js → app/services/run-control-service.js} +3 -14
  33. package/dist/{scheduled-task/executor.js → app/services/scheduled-task-executor-service.js} +27 -12
  34. package/dist/{scheduled-task/runtime.js → app/services/scheduled-task-runtime-service.js} +17 -56
  35. package/dist/{scheduled-task/schedule-parser.js → app/services/scheduled-task-schedule-parser-service.js} +3 -3
  36. package/dist/{scheduled-task/session-ignore.js → app/services/scheduled-task-session-ignore-service.js} +1 -1
  37. package/dist/{session/cache-manager.js → app/services/session-cache-service.js} +3 -3
  38. package/dist/{session/manager.js → app/services/session-service.js} +1 -1
  39. package/dist/app/services/skills-catalog-service.js +21 -0
  40. package/dist/{stt/client.js → app/services/stt-service.js} +2 -2
  41. package/dist/{tts/client.js → app/services/tts-service.js} +68 -5
  42. package/dist/{variant/manager.js → app/services/variant-selection-service.js} +3 -3
  43. package/dist/{scheduled-task/store.js → app/stores/scheduled-task-store.js} +3 -3
  44. package/dist/{settings/manager.js → app/stores/settings-store.js} +49 -7
  45. package/dist/app/types/worktree.js +1 -0
  46. package/dist/bot/{handlers/agent.js → callbacks/agent-selection-callback-handler.js} +8 -57
  47. package/dist/bot/callbacks/callback-router.js +113 -0
  48. package/dist/bot/{commands/commands.js → callbacks/command-catalog-callback-handler.js} +17 -197
  49. package/dist/bot/{handlers/context.js → callbacks/context-control-callback-handler.js} +3 -32
  50. package/dist/bot/callbacks/file-browser-callback-handler.js +219 -0
  51. package/dist/bot/callbacks/inline-menu-cancel-callback-handler.js +32 -0
  52. package/dist/bot/{commands/mcps.js → callbacks/mcp-catalog-callback-handler.js} +7 -172
  53. package/dist/bot/{commands/messages.js → callbacks/message-history-callback-handler.js} +17 -248
  54. package/dist/bot/{handlers/model.js → callbacks/model-selection-callback-handler.js} +10 -71
  55. package/dist/bot/callbacks/permission-callback-handler.js +114 -0
  56. package/dist/bot/callbacks/project-callback-handler.js +79 -0
  57. package/dist/bot/callbacks/question-callback-handler.js +159 -0
  58. package/dist/bot/{commands/rename.js → callbacks/rename-callback-handler.js} +6 -34
  59. package/dist/bot/{commands/tasklist.js → callbacks/scheduled-task-callback-handler.js} +83 -68
  60. package/dist/bot/{commands/sessions.js → callbacks/session-callback-handler.js} +19 -158
  61. package/dist/bot/callbacks/settings-callback-handler.js +105 -0
  62. package/dist/bot/{commands/skills.js → callbacks/skills-catalog-callback-handler.js} +8 -188
  63. package/dist/bot/{handlers/variant.js → callbacks/variant-selection-callback-handler.js} +7 -69
  64. package/dist/bot/callbacks/worktree-callback-handler.js +97 -0
  65. package/dist/bot/commands/{abort.js → abort-command.js} +6 -6
  66. package/dist/bot/commands/command-catalog-command.js +42 -0
  67. package/dist/bot/commands/definitions.js +1 -1
  68. package/dist/bot/commands/{detach.js → detach-command.js} +8 -8
  69. package/dist/bot/commands/ls-command.js +48 -0
  70. package/dist/bot/commands/mcp-catalog-command.js +39 -0
  71. package/dist/bot/commands/messages-command.js +59 -0
  72. package/dist/bot/commands/{new.js → new-command.js} +12 -11
  73. package/dist/bot/commands/open-command.js +45 -0
  74. package/dist/bot/commands/{opencode-start.js → opencode-start-command.js} +1 -1
  75. package/dist/bot/commands/{opencode-stop.js → opencode-stop-command.js} +1 -1
  76. package/dist/bot/commands/projects-command.js +32 -0
  77. package/dist/bot/commands/rename-command.js +33 -0
  78. package/dist/bot/commands/sessions-command.js +42 -0
  79. package/dist/bot/commands/settings-command.js +10 -0
  80. package/dist/bot/commands/skills-catalog-command.js +42 -0
  81. package/dist/bot/commands/{start.js → start-command.js} +12 -12
  82. package/dist/bot/commands/{status.js → status-command.js} +15 -10
  83. package/dist/bot/commands/{task.js → task-command.js} +9 -81
  84. package/dist/bot/commands/tasklist-command.js +40 -0
  85. package/dist/bot/commands/worktree-command.js +47 -0
  86. package/dist/bot/handlers/{document.js → document-handler.js} +3 -3
  87. package/dist/bot/handlers/{media-group.js → media-group-handler.js} +3 -3
  88. package/dist/bot/handlers/photo-handler.js +44 -0
  89. package/dist/bot/handlers/prompt.js +17 -17
  90. package/dist/bot/handlers/text-message-handler.js +61 -0
  91. package/dist/bot/handlers/tts-response-handler.js +29 -0
  92. package/dist/bot/handlers/{voice.js → voice-handler.js} +6 -3
  93. package/dist/bot/index.js +27 -1179
  94. package/dist/{keyboard/manager.js → bot/keyboards/keyboard-manager.js} +6 -6
  95. package/dist/bot/keyboards/keyboard-types.js +1 -0
  96. package/dist/bot/{utils/keyboard.js → keyboards/main-reply-keyboard.js} +2 -2
  97. package/dist/bot/menus/agent-selection-menu.js +54 -0
  98. package/dist/bot/menus/command-catalog-menu.js +101 -0
  99. package/dist/bot/menus/context-control-menu.js +33 -0
  100. package/dist/bot/menus/file-browser-menu.js +265 -0
  101. package/dist/bot/{handlers → menus}/inline-menu.js +7 -34
  102. package/dist/bot/menus/mcp-catalog-menu.js +97 -0
  103. package/dist/bot/menus/message-history-menu.js +107 -0
  104. package/dist/bot/menus/model-selection-menu.js +66 -0
  105. package/dist/bot/menus/permission-menu.js +116 -0
  106. package/dist/bot/{commands/projects.js → menus/project-selection-menu.js} +5 -105
  107. package/dist/bot/{handlers/question.js → menus/question-menu.js} +10 -166
  108. package/dist/bot/menus/rename-menu.js +6 -0
  109. package/dist/bot/menus/scheduled-task-menu.js +42 -0
  110. package/dist/bot/menus/session-selection-menu.js +115 -0
  111. package/dist/bot/menus/settings-menu.js +52 -0
  112. package/dist/bot/menus/skills-catalog-menu.js +99 -0
  113. package/dist/bot/menus/variant-selection-menu.js +67 -0
  114. package/dist/bot/menus/worktree-selection-menu.js +76 -0
  115. package/dist/bot/{utils → messages}/assistant-rendering.js +2 -2
  116. package/dist/bot/messages/busy-blocked-renderer.js +11 -0
  117. package/dist/bot/messages/external-user-input-notification.js +22 -0
  118. package/dist/bot/messages/scheduled-task-delivery.js +63 -0
  119. package/dist/bot/{utils → messages}/send-downloaded-file.js +1 -1
  120. package/dist/bot/messages/summary-message-formatter.js +130 -0
  121. package/dist/bot/{utils → messages}/thinking-message.js +1 -4
  122. package/dist/bot/messages/thinking-rendering.js +66 -0
  123. package/dist/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
  124. package/dist/bot/middleware/interaction-guard.js +2 -2
  125. package/dist/bot/middleware/unknown-command.js +1 -1
  126. package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
  127. package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
  128. package/dist/bot/pinned/pinned-message-types.js +1 -0
  129. package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
  130. package/dist/bot/render/types.js +1 -0
  131. package/dist/bot/routers/command-router.js +73 -0
  132. package/dist/bot/routers/message-router.js +150 -0
  133. package/dist/bot/services/attach-presentation.js +40 -0
  134. package/dist/bot/services/event-subscription-service.js +1116 -0
  135. package/dist/bot/services/project-switch-presentation.js +26 -0
  136. package/dist/bot/streaming/compact-progress-streamer.js +162 -0
  137. package/dist/bot/{utils → streaming}/finalize-assistant-response.js +9 -3
  138. package/dist/bot/streaming/response-streamer.js +16 -1
  139. package/dist/cli.js +4 -4
  140. package/dist/config.js +9 -18
  141. package/dist/i18n/ar.js +31 -6
  142. package/dist/i18n/de.js +32 -7
  143. package/dist/i18n/en.js +32 -7
  144. package/dist/i18n/es.js +32 -7
  145. package/dist/i18n/fr.js +32 -7
  146. package/dist/i18n/ru.js +32 -7
  147. package/dist/i18n/zh.js +32 -7
  148. package/dist/index.js +1 -1
  149. package/dist/opencode/ready-refresh.js +2 -2
  150. package/dist/{service → runtime/service}/manager.js +2 -2
  151. package/dist/runtime/service/types.js +1 -0
  152. package/package.json +1 -1
  153. package/dist/bot/commands/ls.js +0 -445
  154. package/dist/bot/commands/open.js +0 -319
  155. package/dist/bot/commands/tts.js +0 -13
  156. package/dist/bot/commands/worktree.js +0 -201
  157. package/dist/bot/handlers/permission.js +0 -235
  158. package/dist/bot/utils/browser-roots.js +0 -140
  159. package/dist/bot/utils/file-tree.js +0 -92
  160. package/dist/bot/utils/send-tts-response.js +0 -37
  161. package/dist/bot/utils/switch-project.js +0 -45
  162. package/dist/bot/utils/telegram-file-url.js +0 -11
  163. package/dist/interaction/busy.js +0 -8
  164. package/dist/interaction/cleanup.js +0 -32
  165. /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
  166. /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
  167. /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
  168. /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
  169. /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
  170. /package/dist/{agent/types.js → app/types/agent.js} +0 -0
  171. /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
  172. /package/dist/{model/types.js → app/types/model.js} +0 -0
  173. /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
  174. /package/dist/{permission/types.js → app/types/project.js} +0 -0
  175. /package/dist/{pinned/types.js → app/types/question.js} +0 -0
  176. /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
  177. /package/dist/{question/types.js → app/types/session.js} +0 -0
  178. /package/dist/{service/types.js → app/types/settings.js} +0 -0
  179. /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
  180. /package/dist/{variant/types.js → app/types/variant.js} +0 -0
  181. /package/dist/bot/commands/{help.js → help-command.js} +0 -0
  182. /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
  183. /package/dist/bot/{utils → messages}/telegram-text.js +0 -0
  184. /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
  185. /package/dist/{telegram → bot}/render/block-parser.js +0 -0
  186. /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
  187. /package/dist/{telegram → bot}/render/chunker.js +0 -0
  188. /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
  189. /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
  190. /package/dist/{telegram → bot}/render/pipeline.js +0 -0
  191. /package/dist/{telegram → bot}/render/validator.js +0 -0
  192. /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
  193. /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
package/.env.example CHANGED
@@ -80,11 +80,8 @@ OPENCODE_MODEL_ID=big-pickle
80
80
  # If exceeded, the bot stops waiting for the result and marks the run as failed.
81
81
  # SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES=120
82
82
 
83
- # Response streaming mode: "edit" (default) or "draft" (default: edit)
84
- # edit = uses editMessageText to update messages incrementally (may flicker)
85
- # draft = uses sendMessageDraft (Bot API 9.5+) for smooth animated draft previews,
86
- # then finalizes with sendMessage. Only works in private chats.
87
- # RESPONSE_STREAMING_MODE=edit
83
+ # Send scheduled task result/error messages without Telegram push notifications (default: false)
84
+ # SCHEDULED_TASK_DISABLE_NOTIFICATION=false
88
85
 
89
86
  # Stream update throttle in milliseconds for assistant/tool message edits (default: 1000)
90
87
  # Higher value = fewer Telegram edit requests, lower value = more real-time updates
@@ -98,15 +95,6 @@ OPENCODE_MODEL_ID=big-pickle
98
95
  # Supported locales: en, ar, de, es, fr, ru, zh
99
96
  # BOT_LOCALE=en
100
97
 
101
- # Hide thinking indicator messages (default: false)
102
- # HIDE_THINKING_MESSAGES=false
103
-
104
- # Hide tool call service messages (default: false)
105
- # HIDE_TOOL_CALL_MESSAGES=false
106
-
107
- # Hide tool file edit documents sent as .txt attachments (default: false)
108
- # HIDE_TOOL_FILE_MESSAGES=false
109
-
110
98
  # Track background sessions in the currently selected project/worktree (default: true)
111
99
  # Sends short notifications when detached or non-current sessions in the current directory reply,
112
100
  # ask questions, or request permissions. Other projects/worktrees are not tracked.
@@ -141,7 +129,7 @@ OPENCODE_MODEL_ID=big-pickle
141
129
 
142
130
  # Text-to-Speech credentials (optional)
143
131
  # TTS reply behavior is controlled globally with /tts and persisted in settings.json.
144
- # Provider: "openai" (default) or "google".
132
+ # Provider: "openai" (default), "elevenlabs", or "google".
145
133
  #
146
134
  # --- OpenAI-compatible (default) ---
147
135
  # Set TTS_API_URL and TTS_API_KEY to any OpenAI-compatible TTS endpoint.
@@ -150,6 +138,13 @@ OPENCODE_MODEL_ID=big-pickle
150
138
  # TTS_MODEL=gpt-4o-mini-tts
151
139
  # TTS_VOICE=alloy
152
140
  #
141
+ # --- ElevenLabs ---
142
+ # TTS_PROVIDER=elevenlabs
143
+ # TTS_API_URL=https://api.elevenlabs.io/v1
144
+ # TTS_API_KEY=
145
+ # TTS_MODEL=eleven_flash_v2_5
146
+ # TTS_VOICE=21m00Tcm4TlvDq8ikWAM
147
+ #
153
148
  # --- Google Cloud TTS ---
154
149
  # 1. Create a project at https://console.cloud.google.com
155
150
  # 2. Enable "Cloud Text-to-Speech API"
package/README.md CHANGED
@@ -23,6 +23,8 @@ Languages: English (`en`), العربية (`ar`), Deutsch (`de`), Español (`es`
23
23
  <img src="assets/screencast.gif" width="45%" alt="OpenCode Telegram Bot screencast" />
24
24
  </p>
25
25
 
26
+ > I use [boardown](https://github.com/grinev/boardown), my open-source Markdown-based task board, to plan and track this project. It stores tasks in plain `.md` files and can be used as a VS Code extension or a desktop app.
27
+
26
28
  ## Features
27
29
 
28
30
  - **Remote coding** — send prompts to OpenCode from anywhere, receive complete results with code sent as files
@@ -36,7 +38,8 @@ Languages: English (`en`), العربية (`ar`), Deutsch (`de`), Español (`es`
36
38
  - **Custom Commands** — run OpenCode custom commands (and built-ins like `init`/`review`) from an inline menu with confirmation
37
39
  - **Skills Catalog** — browse OpenCode skills from an inline menu and run them immediately or with arguments in the next message
38
40
  - **Interactive Q&A** — answer agent questions and approve permissions via inline buttons
39
- - **Voice prompts** — send voice/audio messages, transcribe them via a Whisper-compatible API, and optionally enable spoken replies with `/tts`
41
+ - **Runtime settings** — use `/settings` to change runtime preferences; see [Runtime Settings](#runtime-settings)
42
+ - **Voice prompts** — send voice/audio messages, transcribe them via a Whisper-compatible API, and optionally enable spoken replies in `/settings`
40
43
  - **File attachments** — send images, PDF documents, and text-based files to OpenCode, including multiple files in one Telegram album
41
44
  - **Scheduled tasks** — schedule prompts to run later or on a recurring interval; see [Scheduled Tasks](#scheduled-tasks)
42
45
  - **Context control** — compact context when it gets too large, right from the chat
@@ -139,7 +142,7 @@ opencode-telegram config
139
142
  | `/worktree` | Switch between existing git worktrees |
140
143
  | `/open` | Add a project by browsing directories |
141
144
  | `/ls` | List directory contents, then tap to open or download |
142
- | `/tts` | Toggle audio replies |
145
+ | `/settings` | Change bot settings |
143
146
  | `/rename` | Rename the current session |
144
147
  | `/commands` | Browse and run custom commands |
145
148
  | `/skills` | Browse and run OpenCode skills |
@@ -225,13 +228,10 @@ When installed via npm, the configuration wizard handles the initial setup. The
225
228
  | `COMMANDS_LIST_LIMIT` | Items per page in `/commands` and `/skills` | No | `10` |
226
229
  | `TASK_LIMIT` | Maximum number of scheduled tasks that can exist at once | No | `10` |
227
230
  | `SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES` | Maximum time the bot waits for one scheduled task run before marking it failed | No | `120` |
231
+ | `SCHEDULED_TASK_DISABLE_NOTIFICATION` | Send scheduled task result/error messages without Telegram push notifications | No | `false` |
228
232
  | `BASH_TOOL_DISPLAY_MAX_LENGTH` | Maximum displayed length for `bash` tool commands in Telegram summaries; longer commands are truncated | No | `128` |
229
- | `SERVICE_MESSAGES_INTERVAL_SEC` | Service messages interval (thinking + tool calls); keep `>=2` to avoid Telegram rate limits, `0` = immediate | No | `5` |
230
- | `HIDE_THINKING_MESSAGES` | Hide `💭 Thinking...` service messages | No | `false` |
231
- | `HIDE_TOOL_CALL_MESSAGES` | Hide tool-call service messages (`💻 bash ...`, `📖 read ...`, etc.) | No | `false` |
232
- | `HIDE_TOOL_FILE_MESSAGES` | Hide file edit documents sent as `.txt` attachments (`edit_*.txt`, `write_*.txt`) | No | `false` |
233
233
  | `TRACK_BACKGROUND_SESSIONS` | Track detached/non-current sessions in the current selected project/worktree and send short notifications | No | `true` |
234
- | `RESPONSE_STREAMING` | Stream assistant replies while they are generated across one or more Telegram messages | No | `true` |
234
+ | `RESPONSE_STREAM_THROTTLE_MS` | Stream update throttle in milliseconds for assistant, thinking, and tool message edits | No | `1000` |
235
235
  | `MESSAGE_FORMAT_MODE` | Assistant reply formatting mode: `markdown` (Telegram MarkdownV2) or `raw` | No | `markdown` |
236
236
  | `CODE_FILE_MAX_SIZE_KB` | Max file size (KB) to send as document | No | `100` |
237
237
  | `STT_API_URL` | Whisper-compatible API base URL (enables voice/audio transcription) | No | — |
@@ -239,11 +239,11 @@ When installed via npm, the configuration wizard handles the initial setup. The
239
239
  | `STT_MODEL` | STT model name passed to `/audio/transcriptions` | No | `whisper-large-v3-turbo` |
240
240
  | `STT_LANGUAGE` | Optional language hint (empty = provider auto-detect) | No | — |
241
241
  | `STT_NOTE_PROMPT` | Optional note prepended to the LLM prompt as `[Note: ...]` for voice transcriptions; empty / `false` / `0` disable it | No | — |
242
- | `TTS_PROVIDER` | TTS provider: `openai` for OpenAI-compatible APIs or `google` for Google Cloud TTS | No | `openai` |
243
- | `TTS_API_URL` | OpenAI-compatible TTS API base URL | No | — |
244
- | `TTS_API_KEY` | OpenAI-compatible TTS API key | No | — |
245
- | `TTS_MODEL` | OpenAI-compatible TTS model name passed to `/audio/speech` | No | `gpt-4o-mini-tts` |
246
- | `TTS_VOICE` | TTS voice name. Defaults to `alloy` for OpenAI-compatible APIs and `en-US-Studio-O` for Google Cloud TTS | No | provider-specific |
242
+ | `TTS_PROVIDER` | TTS provider: `openai` for OpenAI-compatible APIs, `elevenlabs` for ElevenLabs, or `google` for Google Cloud TTS | No | `openai` |
243
+ | `TTS_API_URL` | TTS API base URL for OpenAI-compatible APIs or ElevenLabs | No | — |
244
+ | `TTS_API_KEY` | TTS API key for OpenAI-compatible APIs or ElevenLabs | No | — |
245
+ | `TTS_MODEL` | TTS model name. Passed as `model` for OpenAI-compatible APIs and `model_id` for ElevenLabs | No | `gpt-4o-mini-tts` |
246
+ | `TTS_VOICE` | TTS voice name or ElevenLabs voice ID. Defaults to `alloy`, `21m00Tcm4TlvDq8ikWAM`, or `en-US-Studio-O` by provider | No | provider-specific |
247
247
  | `GOOGLE_APPLICATION_CREDENTIALS` | Path to a Google Cloud service account JSON key file for `TTS_PROVIDER=google` | No | — |
248
248
  | `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | No | `info` |
249
249
  | `LOG_RETENTION` | Number of log files to keep: launch files in `sources`, daily files in `installed` | No | `10` |
@@ -252,6 +252,16 @@ When installed via npm, the configuration wizard handles the initial setup. The
252
252
 
253
253
  Logs are written to `./logs` when running from sources and to the runtime config directory `logs/` folder in `installed` mode. Log rotation depends on runtime mode: `sources` creates one file per bot launch, while `installed` appends to one file per day. Old log files are removed according to `LOG_RETENTION`.
254
254
 
255
+ ### Runtime Settings
256
+
257
+ Runtime preferences are changed from `/settings` and stored in `settings.json`:
258
+
259
+ - Compact output mode
260
+ - Thinking content display
261
+ - Diff file attachments
262
+ - Response streaming mode: `edit` or `draft (experimental)`; applies only to final assistant replies, not thinking messages
263
+ - Audio replies: `off`, `all`, or `auto` when TTS is configured
264
+
255
265
  ### Reverse Proxy (Optional)
256
266
 
257
267
  For environments that block `api.telegram.org` but allow your own HTTPS endpoint (corporate networks, restricted regions), you can route Bot API traffic through a reverse proxy you control. This is an alternative to the SOCKS/HTTP forward proxy configured with `TELEGRAM_PROXY_URL`.
@@ -311,7 +321,7 @@ If `STT_API_URL` and `STT_API_KEY` are set, the bot will:
311
321
 
312
322
  If `STT_NOTE_PROMPT` is set to a non-empty value other than `false` or `0`, the bot prepends `[Note: ...]` to the transcription before sending it to the LLM. The recognized text shown in Telegram stays unchanged.
313
323
 
314
- If TTS credentials are configured, you can toggle spoken replies globally with `/tts`. The preference is stored in `settings.json` and persists across restarts.
324
+ If TTS credentials are configured, you can choose spoken reply behavior in `/settings`: `off` disables audio replies, `all` sends audio for every assistant reply, and `auto` sends audio only after voice/audio prompts. The preference is stored in `settings.json` and persists across restarts.
315
325
 
316
326
  OpenAI-compatible TTS configuration example:
317
327
 
@@ -323,6 +333,16 @@ TTS_MODEL=gpt-4o-mini-tts
323
333
  TTS_VOICE=alloy
324
334
  ```
325
335
 
336
+ ElevenLabs TTS configuration example:
337
+
338
+ ```env
339
+ TTS_PROVIDER=elevenlabs
340
+ TTS_API_URL=https://api.elevenlabs.io/v1
341
+ TTS_API_KEY=your-elevenlabs-api-key
342
+ TTS_MODEL=eleven_flash_v2_5
343
+ TTS_VOICE=21m00Tcm4TlvDq8ikWAM
344
+ ```
345
+
326
346
  Google Cloud TTS configuration example:
327
347
 
328
348
  ```env
@@ -1,22 +1,23 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { readFile } from "node:fs/promises";
3
- import { cleanupBotRuntime, createBot } from "../bot/index.js";
4
- import { config } from "../config.js";
5
- import { opencodeAutoRestartService } from "../opencode/auto-restart.js";
6
- import { notifyOpencodeReadyIfHealthy, registerOpenCodeReadyRefreshHandler, } from "../opencode/ready-refresh.js";
7
- import { loadSettings } from "../settings/manager.js";
8
- import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
9
- import { reconcileStoredModelSelection } from "../model/manager.js";
10
- import { getRuntimeMode } from "../runtime/mode.js";
11
- import { getRuntimePaths } from "../runtime/paths.js";
12
- import { clearServiceStateFile } from "../service/manager.js";
13
- import { getServiceStateFilePathFromEnv, isServiceChildProcess } from "../service/runtime.js";
14
- import { getLogFilePath, initializeLogger, logger } from "../utils/logger.js";
15
- import { safeBackgroundTask } from "../utils/safe-background-task.js";
3
+ import { cleanupBotRuntime, createBot } from "../../bot/index.js";
4
+ import { createScheduledTaskDeliverySender } from "../../bot/messages/scheduled-task-delivery.js";
5
+ import { config } from "../../config.js";
6
+ import { opencodeAutoRestartService } from "../../opencode/auto-restart.js";
7
+ import { notifyOpencodeReadyIfHealthy, registerOpenCodeReadyRefreshHandler, } from "../../opencode/ready-refresh.js";
8
+ import { loadSettings } from "../stores/settings-store.js";
9
+ import { scheduledTaskRuntime } from "../services/scheduled-task-runtime-service.js";
10
+ import { reconcileStoredModelSelection } from "../services/model-selection-service.js";
11
+ import { getRuntimeMode } from "../../runtime/mode.js";
12
+ import { getRuntimePaths } from "../../runtime/paths.js";
13
+ import { clearServiceStateFile } from "../../runtime/service/manager.js";
14
+ import { getServiceStateFilePathFromEnv, isServiceChildProcess } from "../../runtime/service/env.js";
15
+ import { getLogFilePath, initializeLogger, logger } from "../../utils/logger.js";
16
+ import { safeBackgroundTask } from "../../utils/safe-background-task.js";
16
17
  const SHUTDOWN_TIMEOUT_MS = 5000;
17
18
  async function getBotVersion() {
18
19
  try {
19
- const packageJsonPath = new URL("../../package.json", import.meta.url);
20
+ const packageJsonPath = new URL("../../../package.json", import.meta.url);
20
21
  const packageJsonContent = await readFile(packageJsonPath, "utf-8");
21
22
  const packageJson = JSON.parse(packageJsonContent);
22
23
  return packageJson.version ?? "unknown";
@@ -43,7 +44,7 @@ export async function startBotApp() {
43
44
  await reconcileStoredModelSelection();
44
45
  registerOpenCodeReadyRefreshHandler();
45
46
  const bot = createBot();
46
- await scheduledTaskRuntime.initialize(bot);
47
+ await scheduledTaskRuntime.initialize(bot, createScheduledTaskDeliverySender(bot.api, config.telegram.allowedUserId));
47
48
  safeBackgroundTask({
48
49
  taskName: "app.opencodeStartup",
49
50
  task: async () => {
@@ -1,4 +1,4 @@
1
- import { getAgentDisplayName } from "../../agent/types.js";
1
+ import { getAgentDisplayName } from "../types/agent.js";
2
2
  function formatDuration(elapsedMs) {
3
3
  const safeElapsedMs = Math.max(0, Math.round(elapsedMs));
4
4
  const totalSeconds = Math.floor(safeElapsedMs / 1000);
@@ -1,6 +1,11 @@
1
- import { formatModelDisplayName } from "../pinned/format.js";
2
- import { t } from "../i18n/index.js";
3
- import { formatCompactToolInfo } from "./formatter.js";
1
+ import { t } from "../../i18n/index.js";
2
+ import { formatCompactToolInfo } from "./summary-formatter.js";
3
+ function formatModelDisplayName(providerID, modelID) {
4
+ if (providerID && modelID) {
5
+ return `${providerID}/${modelID}`;
6
+ }
7
+ return t("pinned.unknown");
8
+ }
4
9
  function shouldPreferInputDetails(tool, input) {
5
10
  if (!input) {
6
11
  return false;
@@ -1,12 +1,8 @@
1
1
  import * as path from "path";
2
- import { config } from "../config.js";
3
- import { logger } from "../utils/logger.js";
4
- import { t } from "../i18n/index.js";
5
- import { getCurrentProject } from "../settings/manager.js";
6
- import { convertToTelegramMarkdownV2 } from "./markdown-to-telegram-v2.js";
7
- import { normalizeMarkdownForTelegramRendering } from "../telegram/render/markdown-normalizer.js";
8
- const TELEGRAM_MESSAGE_LIMIT = 4096;
9
- const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
2
+ import { config } from "../../config.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ import { t } from "../../i18n/index.js";
5
+ import { getCurrentProject } from "../stores/settings-store.js";
10
6
  function truncateWithEllipsis(text, maxLength) {
11
7
  if (text.length <= maxLength) {
12
8
  return text;
@@ -16,50 +12,6 @@ function truncateWithEllipsis(text, maxLength) {
16
12
  }
17
13
  return `${text.slice(0, maxLength - 3).trimEnd()}...`;
18
14
  }
19
- function endsWithOddTrailingBackslashes(text, start, end) {
20
- let backslashCount = 0;
21
- for (let index = end - 1; index >= start; index--) {
22
- if (text[index] !== "\\") {
23
- break;
24
- }
25
- backslashCount += 1;
26
- }
27
- return backslashCount % 2 === 1;
28
- }
29
- function resolveSplitEndIndex(text, currentIndex, maxLength, options) {
30
- const hardLimit = Math.min(text.length, currentIndex + maxLength);
31
- if (hardLimit >= text.length) {
32
- return text.length;
33
- }
34
- let endIndex = hardLimit;
35
- const breakPoint = text.lastIndexOf("\n", endIndex);
36
- if (breakPoint > currentIndex) {
37
- endIndex = breakPoint + 1;
38
- }
39
- if (!options?.avoidTrailingMarkdownEscape) {
40
- return endIndex;
41
- }
42
- while (endIndex > currentIndex && endsWithOddTrailingBackslashes(text, currentIndex, endIndex)) {
43
- endIndex -= 1;
44
- }
45
- return endIndex > currentIndex ? endIndex : hardLimit;
46
- }
47
- function splitText(text, maxLength, options) {
48
- const parts = [];
49
- let currentIndex = 0;
50
- while (currentIndex < text.length) {
51
- const endIndex = resolveSplitEndIndex(text, currentIndex, maxLength, options);
52
- if (endIndex <= currentIndex) {
53
- const fallbackEnd = Math.min(text.length, currentIndex + 1);
54
- parts.push(text.slice(currentIndex, fallbackEnd));
55
- currentIndex = fallbackEnd;
56
- continue;
57
- }
58
- parts.push(text.slice(currentIndex, endIndex));
59
- currentIndex = endIndex;
60
- }
61
- return parts;
62
- }
63
15
  export function normalizePathForDisplay(filePath) {
64
16
  const normalizedPath = filePath.replace(/\\/g, "/");
65
17
  const project = getCurrentProject();
@@ -81,86 +33,6 @@ export function normalizePathForDisplay(filePath) {
81
33
  }
82
34
  return normalizedPath;
83
35
  }
84
- export function formatSummary(text) {
85
- return formatSummaryWithMode(text, config.bot.messageFormatMode);
86
- }
87
- export function escapePlainTextForTelegramMarkdownV2(text) {
88
- return text.replace(MARKDOWN_V2_RESERVED_CHARS, "\\$1");
89
- }
90
- function formatMarkdownForTelegram(text) {
91
- try {
92
- const preprocessed = normalizeMarkdownForTelegramRendering(text);
93
- return escapeMarkdownV2PipesOutsideCode(convertToTelegramMarkdownV2(preprocessed));
94
- }
95
- catch (error) {
96
- logger.warn("[Formatter] Failed to convert markdown summary, falling back to raw text", error);
97
- return text;
98
- }
99
- }
100
- function escapeMarkdownV2PipesOutsideCode(text) {
101
- let result = "";
102
- let index = 0;
103
- let inInlineCode = false;
104
- let inCodeFence = false;
105
- while (index < text.length) {
106
- if (text.startsWith("```", index)) {
107
- result += "```";
108
- index += 3;
109
- inCodeFence = !inCodeFence;
110
- continue;
111
- }
112
- const char = text[index];
113
- if (!inCodeFence && char === "`") {
114
- inInlineCode = !inInlineCode;
115
- result += char;
116
- index += 1;
117
- continue;
118
- }
119
- if (!inCodeFence && !inInlineCode && char === "|" && text[index - 1] !== "\\") {
120
- result += "\\|";
121
- index += 1;
122
- continue;
123
- }
124
- result += char;
125
- index += 1;
126
- }
127
- return result;
128
- }
129
- export function formatSummaryWithMode(text, mode, maxLength = TELEGRAM_MESSAGE_LIMIT) {
130
- if (!text || text.trim().length === 0) {
131
- return [];
132
- }
133
- const normalizedMaxLength = Math.max(1, Math.floor(maxLength));
134
- const rawTextLimit = mode === "raw" ? Math.max(1, normalizedMaxLength - "```\n\n```".length) : normalizedMaxLength;
135
- const parts = splitText(text, rawTextLimit);
136
- const formattedParts = [];
137
- for (const part of parts) {
138
- const trimmed = part.trim();
139
- if (!trimmed) {
140
- continue;
141
- }
142
- if (mode === "markdown") {
143
- const converted = formatMarkdownForTelegram(trimmed);
144
- const convertedParts = splitText(converted, normalizedMaxLength, {
145
- avoidTrailingMarkdownEscape: true,
146
- });
147
- for (const convertedPart of convertedParts) {
148
- const normalizedPart = convertedPart.trim();
149
- if (normalizedPart) {
150
- formattedParts.push(normalizedPart);
151
- }
152
- }
153
- continue;
154
- }
155
- if (parts.length > 1) {
156
- formattedParts.push(`\`\`\`\n${trimmed}\n\`\`\``);
157
- }
158
- else {
159
- formattedParts.push(trimmed);
160
- }
161
- }
162
- return formattedParts;
163
- }
164
36
  function getToolDetails(tool, input) {
165
37
  if (!input) {
166
38
  return "";
@@ -355,6 +227,22 @@ export function formatCompactToolInfo(toolInfo, maxLength = 64, fallback = "-")
355
227
  }
356
228
  return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
357
229
  }
230
+ export function formatCompactToolActivity(toolInfo, maxLength = 64) {
231
+ if (!hasCompactToolDetails(toolInfo)) {
232
+ return null;
233
+ }
234
+ const formatted = formatCompactToolInfo(toolInfo, maxLength, "").trim();
235
+ return formatted || null;
236
+ }
237
+ function hasCompactToolDetails(toolInfo) {
238
+ if (typeof toolInfo.title === "string" && toolInfo.title.trim().length > 0) {
239
+ return true;
240
+ }
241
+ if (toolInfo.tool === "todowrite" && Array.isArray(toolInfo.metadata?.todos)) {
242
+ return true;
243
+ }
244
+ return getToolDetails(toolInfo.tool, toolInfo.input).trim().length > 0;
245
+ }
358
246
  function countLines(text) {
359
247
  return text.split("\n").length;
360
248
  }
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  export class ToolMessageBatcher {
3
3
  sendText;
4
4
  sendFile;
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class AssistantRunState {
3
3
  runs = new Map();
4
4
  startRun(sessionId, info) {
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class AttachManager {
3
3
  state = null;
4
4
  attach(sessionId, directory) {
@@ -1,5 +1,5 @@
1
- import { isScheduledTaskSessionIgnored } from "../scheduled-task/session-ignore.js";
2
- import { logger } from "../utils/logger.js";
1
+ import { isScheduledTaskSessionIgnored } from "../services/scheduled-task-session-ignore-service.js";
2
+ import { logger } from "../../utils/logger.js";
3
3
  class BackgroundSessionTracker {
4
4
  directory = null;
5
5
  onNotification = null;
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class ForegroundSessionState {
3
3
  activeSessions = new Map();
4
4
  markBusy(sessionId, directory) {
@@ -1,4 +1,8 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { permissionManager } from "./permission-manager.js";
2
+ import { questionManager } from "./question-manager.js";
3
+ import { renameManager } from "./rename-manager.js";
4
+ import { taskCreationManager } from "./scheduled-task-creation-manager.js";
5
+ import { logger } from "../../utils/logger.js";
2
6
  export const DEFAULT_ALLOWED_INTERACTION_COMMANDS = ["/help", "/status", "/abort", "/detach"];
3
7
  function normalizeCommand(command) {
4
8
  const trimmed = command.trim().toLowerCase();
@@ -104,3 +108,29 @@ class InteractionManager {
104
108
  }
105
109
  }
106
110
  export const interactionManager = new InteractionManager();
111
+ export function clearAllInteractionState(reason) {
112
+ const questionActive = questionManager.isActive();
113
+ const permissionActive = permissionManager.isActive();
114
+ const renameActive = renameManager.isWaitingForName();
115
+ const taskCreationActive = taskCreationManager.isActive();
116
+ const interactionSnapshot = interactionManager.getSnapshot();
117
+ questionManager.clear();
118
+ permissionManager.clear();
119
+ renameManager.clear();
120
+ taskCreationManager.clear();
121
+ interactionManager.clear(reason);
122
+ const hasAnyActiveState = questionActive ||
123
+ permissionActive ||
124
+ renameActive ||
125
+ taskCreationActive ||
126
+ interactionSnapshot !== null;
127
+ const message = `[InteractionCleanup] Cleared state: reason=${reason}, ` +
128
+ `questionActive=${questionActive}, permissionActive=${permissionActive}, ` +
129
+ `renameActive=${renameActive}, taskCreationActive=${taskCreationActive}, ` +
130
+ `interactionKind=${interactionSnapshot?.kind || "none"}`;
131
+ if (hasAnyActiveState) {
132
+ logger.info(message);
133
+ return;
134
+ }
135
+ logger.debug(message);
136
+ }
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class PermissionManager {
3
3
  state = {
4
4
  requestsByMessageId: new Map(),
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class QuestionManager {
3
3
  state = {
4
4
  questions: [],
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class RenameManager {
3
3
  state = {
4
4
  isWaiting: false,
@@ -1,5 +1,5 @@
1
- import { cloneParsedTaskSchedule, cloneScheduledTaskModel } from "./types.js";
2
- import { logger } from "../utils/logger.js";
1
+ import { cloneParsedTaskSchedule, cloneScheduledTaskModel } from "../types/scheduled-task.js";
2
+ import { logger } from "../../utils/logger.js";
3
3
  function cloneState(state) {
4
4
  return {
5
5
  ...state,