@poolzin/pool-bot 2026.2.21 → 2026.2.22

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 (369) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/agents/api-key-rotation.js +47 -0
  3. package/dist/agents/apply-patch-update.js +19 -9
  4. package/dist/agents/apply-patch.js +72 -47
  5. package/dist/agents/bash-tools.exec.js +141 -559
  6. package/dist/agents/cli-backends.js +49 -6
  7. package/dist/agents/cli-runner/helpers.js +69 -152
  8. package/dist/agents/cli-runner.js +70 -19
  9. package/dist/agents/identity.js +20 -1
  10. package/dist/agents/image-sanitization.js +9 -0
  11. package/dist/agents/live-auth-keys.js +123 -26
  12. package/dist/agents/live-model-filter.js +13 -4
  13. package/dist/agents/model-catalog.js +40 -9
  14. package/dist/agents/model-forward-compat.js +60 -23
  15. package/dist/agents/model-selection.js +134 -41
  16. package/dist/agents/pi-auth-json.js +2 -2
  17. package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
  18. package/dist/agents/pi-embedded-helpers/errors.js +140 -15
  19. package/dist/agents/pi-embedded-helpers/images.js +22 -12
  20. package/dist/agents/pi-embedded-helpers.js +2 -2
  21. package/dist/agents/pi-embedded-runner/abort.js +10 -3
  22. package/dist/agents/pi-embedded-runner/compact.js +230 -32
  23. package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
  24. package/dist/agents/pi-embedded-runner/google.js +109 -19
  25. package/dist/agents/pi-embedded-runner/history.js +35 -17
  26. package/dist/agents/pi-embedded-runner/run/attempt.js +386 -95
  27. package/dist/agents/pi-embedded-runner/run/images.js +81 -55
  28. package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
  29. package/dist/agents/pi-embedded-runner/run.js +193 -25
  30. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
  31. package/dist/agents/pi-embedded-runner/runs.js +17 -8
  32. package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
  33. package/dist/agents/pi-embedded-runner.js +1 -1
  34. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
  35. package/dist/agents/pi-embedded-subscribe.js +37 -0
  36. package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
  37. package/dist/agents/pi-model-discovery.js +9 -2
  38. package/dist/agents/pi-tool-definition-adapter.js +60 -8
  39. package/dist/agents/pi-tools.before-tool-call.js +1 -1
  40. package/dist/agents/pi-tools.js +113 -94
  41. package/dist/agents/pi-tools.read.js +337 -38
  42. package/dist/agents/poolbot-tools.js +14 -5
  43. package/dist/agents/sandbox/docker.js +10 -5
  44. package/dist/agents/sandbox/registry.js +96 -46
  45. package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
  46. package/dist/agents/sandbox-paths.js +43 -10
  47. package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
  48. package/dist/agents/session-tool-result-guard.js +39 -39
  49. package/dist/agents/session-transcript-repair.js +36 -33
  50. package/dist/agents/session-write-lock.js +62 -44
  51. package/dist/agents/skills/frontmatter.js +49 -88
  52. package/dist/agents/skills/workspace.js +335 -28
  53. package/dist/agents/subagent-announce.js +508 -174
  54. package/dist/agents/subagent-registry.js +45 -4
  55. package/dist/agents/subagent-spawn.js +16 -33
  56. package/dist/agents/system-prompt-report.js +27 -10
  57. package/dist/agents/system-prompt.js +26 -32
  58. package/dist/agents/tool-call-id.js +69 -17
  59. package/dist/agents/tool-display-common.js +1 -1
  60. package/dist/agents/tool-images.js +64 -31
  61. package/dist/agents/tools/canvas-tool.js +17 -11
  62. package/dist/agents/tools/common.js +37 -19
  63. package/dist/agents/tools/cron-tool.js +40 -38
  64. package/dist/agents/tools/gateway.js +70 -2
  65. package/dist/agents/tools/message-tool.js +181 -40
  66. package/dist/agents/tools/nodes-tool.js +128 -36
  67. package/dist/agents/tools/nodes-utils.js +12 -38
  68. package/dist/agents/tools/session-status-tool.js +24 -71
  69. package/dist/agents/tools/sessions-helpers.js +38 -210
  70. package/dist/agents/tools/sessions-spawn-tool.js +28 -198
  71. package/dist/agents/tools/telegram-actions.js +58 -7
  72. package/dist/agents/tools/web-fetch-utils.js +112 -7
  73. package/dist/agents/tools/web-fetch.js +279 -175
  74. package/dist/agents/tools/web-shared.js +71 -8
  75. package/dist/agents/usage.js +25 -16
  76. package/dist/auto-reply/commands-registry.data.js +85 -11
  77. package/dist/auto-reply/dispatch.js +40 -21
  78. package/dist/auto-reply/reply/abort.js +102 -33
  79. package/dist/auto-reply/reply/commands-core.js +82 -33
  80. package/dist/auto-reply/reply/commands-export-session.js +1 -1
  81. package/dist/auto-reply/reply/commands-info.js +41 -12
  82. package/dist/auto-reply/reply/commands-subagents.js +352 -100
  83. package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
  84. package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
  85. package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
  86. package/dist/auto-reply/reply/inbound-meta.js +12 -1
  87. package/dist/auto-reply/reply/mentions.js +18 -11
  88. package/dist/auto-reply/reply/normalize-reply.js +17 -8
  89. package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
  90. package/dist/auto-reply/reply/session.js +102 -21
  91. package/dist/auto-reply/reply/streaming-directives.js +16 -5
  92. package/dist/auto-reply/status.js +73 -50
  93. package/dist/browser/extension-relay.js +3 -3
  94. package/dist/browser/http-auth.js +1 -1
  95. package/dist/browser/paths.js +2 -2
  96. package/dist/build-info.json +3 -3
  97. package/dist/channels/allowlist-match.js +20 -0
  98. package/dist/channels/allowlists/resolve-utils.js +65 -2
  99. package/dist/channels/chat-type.js +8 -4
  100. package/dist/channels/dock.js +127 -35
  101. package/dist/channels/draft-stream-loop.js +6 -2
  102. package/dist/channels/plugins/actions/telegram.js +42 -18
  103. package/dist/channels/plugins/allowlist-match.js +1 -1
  104. package/dist/channels/plugins/group-mentions.js +51 -41
  105. package/dist/channels/plugins/message-action-names.js +2 -0
  106. package/dist/channels/plugins/message-actions.js +24 -5
  107. package/dist/channels/plugins/normalize/discord.js +26 -4
  108. package/dist/channels/plugins/normalize/signal.js +35 -22
  109. package/dist/channels/plugins/onboarding/helpers.js +8 -26
  110. package/dist/channels/plugins/outbound/imessage.js +15 -14
  111. package/dist/channels/registry.js +20 -7
  112. package/dist/cli/acp-cli.js +7 -5
  113. package/dist/cli/browser-cli-extension.js +25 -12
  114. package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
  115. package/dist/cli/browser-cli-state.js +101 -145
  116. package/dist/cli/command-options.js +28 -0
  117. package/dist/cli/completion-cli.js +6 -6
  118. package/dist/cli/cron-cli/register.cron-add.js +25 -1
  119. package/dist/cli/cron-cli/register.cron-edit.js +44 -0
  120. package/dist/cli/cron-cli/shared.js +7 -1
  121. package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
  122. package/dist/cli/daemon-cli/lifecycle.js +23 -247
  123. package/dist/cli/daemon-cli/register-service-commands.js +25 -4
  124. package/dist/cli/daemon-cli.js +1 -0
  125. package/dist/cli/devices-cli.js +33 -20
  126. package/dist/cli/gateway-cli/register.js +37 -105
  127. package/dist/cli/gateway-cli/run.js +49 -11
  128. package/dist/cli/nodes-camera.js +59 -4
  129. package/dist/cli/nodes-cli/register.camera.js +27 -24
  130. package/dist/cli/nodes-cli/rpc.js +21 -38
  131. package/dist/cli/qr-cli.js +2 -2
  132. package/dist/cli/skills-cli.format.js +2 -2
  133. package/dist/cli/update-cli/progress.js +2 -2
  134. package/dist/cli/update-cli/restart-helper.js +28 -7
  135. package/dist/cli/update-cli/shared.js +7 -7
  136. package/dist/cli/update-cli/status.js +1 -1
  137. package/dist/cli/update-cli/update-command.js +14 -8
  138. package/dist/cli/update-cli/wizard.js +2 -2
  139. package/dist/cli/update-cli.js +21 -1027
  140. package/dist/commands/auth-choice.apply.anthropic.js +10 -2
  141. package/dist/commands/channels/add-mutators.js +3 -35
  142. package/dist/commands/channels/add.js +39 -51
  143. package/dist/commands/config-validation.js +1 -1
  144. package/dist/commands/configure.gateway-auth.js +52 -15
  145. package/dist/commands/configure.gateway.js +84 -40
  146. package/dist/commands/doctor-completion.js +3 -3
  147. package/dist/commands/doctor-config-flow.js +536 -16
  148. package/dist/commands/doctor-gateway-services.js +103 -79
  149. package/dist/commands/doctor-memory-search.js +9 -9
  150. package/dist/commands/doctor-platform-notes.js +57 -30
  151. package/dist/commands/doctor-prompter.js +26 -15
  152. package/dist/commands/doctor-session-locks.js +1 -1
  153. package/dist/commands/doctor.js +21 -9
  154. package/dist/commands/model-picker.js +120 -95
  155. package/dist/commands/models/set.js +2 -21
  156. package/dist/commands/models/shared.js +65 -37
  157. package/dist/commands/onboard-helpers.js +81 -39
  158. package/dist/commands/openai-codex-oauth.js +1 -1
  159. package/dist/commands/sessions.js +52 -53
  160. package/dist/commands/status.summary.js +52 -34
  161. package/dist/commands/test-wizard-helpers.js +2 -2
  162. package/dist/config/defaults.js +79 -42
  163. package/dist/config/group-policy.js +50 -18
  164. package/dist/config/includes.js +37 -10
  165. package/dist/config/schema.help.js +5 -4
  166. package/dist/config/schema.hints.js +2 -2
  167. package/dist/config/schema.labels.js +1 -0
  168. package/dist/config/sessions/group.js +12 -11
  169. package/dist/config/sessions/paths.js +137 -11
  170. package/dist/config/sessions/store.js +185 -65
  171. package/dist/config/sessions/types.js +15 -1
  172. package/dist/config/sessions.js +1 -0
  173. package/dist/config/telegram-custom-commands.js +3 -2
  174. package/dist/config/types.js +2 -0
  175. package/dist/config/zod-schema.agent-defaults.js +6 -27
  176. package/dist/config/zod-schema.agent-runtime.js +171 -79
  177. package/dist/config/zod-schema.providers-core.js +138 -65
  178. package/dist/config/zod-schema.session.js +49 -22
  179. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  180. package/dist/cron/isolated-agent/run.js +224 -57
  181. package/dist/cron/normalize.js +48 -45
  182. package/dist/cron/run-log.js +14 -0
  183. package/dist/cron/service/jobs.js +190 -28
  184. package/dist/cron/service/normalize.js +29 -11
  185. package/dist/cron/service/store.js +30 -44
  186. package/dist/cron/service/timer.js +182 -96
  187. package/dist/cron/service.js +3 -0
  188. package/dist/cron/stagger.js +37 -0
  189. package/dist/daemon/inspect.js +132 -92
  190. package/dist/daemon/runtime-paths.js +25 -4
  191. package/dist/daemon/service-audit.js +47 -16
  192. package/dist/discord/accounts.js +23 -20
  193. package/dist/discord/monitor/agent-components.js +1115 -219
  194. package/dist/discord/monitor/allow-list.js +114 -34
  195. package/dist/discord/monitor/listeners.js +204 -97
  196. package/dist/discord/monitor/message-handler.js +21 -10
  197. package/dist/discord/monitor/message-handler.preflight.js +195 -101
  198. package/dist/discord/monitor/message-handler.process.js +384 -123
  199. package/dist/discord/monitor/message-utils.js +86 -23
  200. package/dist/discord/monitor/native-command.js +77 -57
  201. package/dist/discord/monitor/provider.js +122 -117
  202. package/dist/discord/monitor/reply-context.js +20 -16
  203. package/dist/discord/monitor/reply-delivery.js +40 -8
  204. package/dist/discord/monitor/rest-fetch.js +22 -0
  205. package/dist/discord/monitor/threading.js +117 -24
  206. package/dist/discord/send.js +2 -1
  207. package/dist/discord/send.outbound.js +124 -11
  208. package/dist/discord/send.shared.js +112 -72
  209. package/dist/discord/voice-message.js +3 -3
  210. package/dist/gateway/auth.js +119 -44
  211. package/dist/gateway/call.js +76 -34
  212. package/dist/gateway/channel-health-monitor.js +57 -50
  213. package/dist/gateway/client.js +63 -29
  214. package/dist/gateway/control-ui-contract.js +1 -1
  215. package/dist/gateway/gateway-config-prompts.shared.js +2 -2
  216. package/dist/gateway/net.js +109 -1
  217. package/dist/gateway/protocol/index.js +5 -8
  218. package/dist/gateway/protocol/schema/agent.js +19 -1
  219. package/dist/gateway/protocol/schema/channels.js +21 -0
  220. package/dist/gateway/protocol/schema/cron.js +43 -30
  221. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
  222. package/dist/gateway/protocol/schema/sessions.js +5 -1
  223. package/dist/gateway/protocol/schema.js +0 -1
  224. package/dist/gateway/server/presence-events.js +12 -0
  225. package/dist/gateway/server/ws-connection/message-handler.js +203 -212
  226. package/dist/gateway/server/ws-connection.js +58 -21
  227. package/dist/gateway/server-broadcast.js +18 -13
  228. package/dist/gateway/server-cron.js +177 -10
  229. package/dist/gateway/server-methods/agent-job.js +131 -38
  230. package/dist/gateway/server-methods/send.js +60 -14
  231. package/dist/gateway/server-methods/sessions.js +160 -96
  232. package/dist/gateway/server-methods/system.js +5 -7
  233. package/dist/gateway/server-methods-list.js +8 -0
  234. package/dist/gateway/server-methods.js +24 -8
  235. package/dist/gateway/server-node-events.js +278 -68
  236. package/dist/gateway/session-utils.fs.js +316 -75
  237. package/dist/gateway/session-utils.js +224 -70
  238. package/dist/gateway/sessions-patch.js +63 -20
  239. package/dist/gateway/test-temp-config.js +1 -1
  240. package/dist/gateway/tools-invoke-http.js +118 -70
  241. package/dist/gateway/ws-log.js +135 -107
  242. package/dist/hooks/frontmatter.js +36 -82
  243. package/dist/hooks/install.js +149 -139
  244. package/dist/hooks/internal-hooks.js +29 -4
  245. package/dist/hooks/plugin-hooks.js +2 -1
  246. package/dist/imessage/monitor/deliver.js +10 -4
  247. package/dist/imessage/monitor/monitor-provider.js +138 -375
  248. package/dist/imessage/monitor/runtime.js +4 -8
  249. package/dist/imessage/send.js +65 -19
  250. package/dist/infra/exec-approvals-allowlist.js +7 -0
  251. package/dist/infra/exec-approvals.js +35 -920
  252. package/dist/infra/exec-safe-bin-trust.js +64 -0
  253. package/dist/infra/heartbeat-runner.js +207 -134
  254. package/dist/infra/heartbeat-wake.js +183 -22
  255. package/dist/infra/install-source-utils.js +47 -0
  256. package/dist/infra/net/ssrf.js +170 -36
  257. package/dist/infra/outbound/deliver.js +224 -58
  258. package/dist/infra/outbound/message-action-spec.js +12 -5
  259. package/dist/infra/outbound/outbound-session.js +27 -25
  260. package/dist/infra/poolbot-root.js +32 -22
  261. package/dist/infra/ports.js +14 -11
  262. package/dist/infra/skills-remote.js +48 -37
  263. package/dist/infra/system-events.js +25 -11
  264. package/dist/infra/system-presence.js +26 -33
  265. package/dist/infra/tmp-poolbot-dir.js +81 -2
  266. package/dist/infra/wsl.js +37 -1
  267. package/dist/line/bot-message-context.js +163 -191
  268. package/dist/logging/subsystem.js +59 -22
  269. package/dist/markdown/ir.js +124 -50
  270. package/dist/media/store.js +1 -1
  271. package/dist/media-understanding/runner.entries.js +42 -25
  272. package/dist/media-understanding/runner.js +53 -488
  273. package/dist/memory/embeddings-gemini.js +53 -38
  274. package/dist/memory/manager-embedding-ops.js +48 -69
  275. package/dist/pairing/pairing-store.js +178 -119
  276. package/dist/plugin-sdk/index.js +34 -6
  277. package/dist/plugins/hooks.js +135 -14
  278. package/dist/plugins/install.js +190 -152
  279. package/dist/polls.js +11 -0
  280. package/dist/routing/resolve-route.js +190 -56
  281. package/dist/routing/session-key.js +38 -22
  282. package/dist/runtime.js +35 -9
  283. package/dist/security/audit-channel.js +1 -1
  284. package/dist/sessions/session-key-utils.js +29 -11
  285. package/dist/shared/frontmatter.js +5 -5
  286. package/dist/shared/node-list-types.js +1 -0
  287. package/dist/shared/string-normalization.js +15 -0
  288. package/dist/signal/monitor/event-handler.js +68 -36
  289. package/dist/signal/send.js +29 -37
  290. package/dist/slack/monitor/allow-list.js +10 -11
  291. package/dist/slack/monitor/commands.js +14 -3
  292. package/dist/slack/monitor/events/interactions.js +4 -4
  293. package/dist/slack/monitor/media.js +224 -16
  294. package/dist/slack/monitor/message-handler/dispatch.js +247 -13
  295. package/dist/slack/monitor/message-handler/prepare.js +128 -45
  296. package/dist/slack/monitor/slash.js +357 -144
  297. package/dist/slack/streaming.js +77 -0
  298. package/dist/telegram/accounts.js +40 -13
  299. package/dist/telegram/allowed-updates.js +3 -0
  300. package/dist/telegram/bot/delivery.js +129 -66
  301. package/dist/telegram/bot/helpers.js +136 -122
  302. package/dist/telegram/bot-handlers.js +600 -339
  303. package/dist/telegram/bot-message-context.js +115 -73
  304. package/dist/telegram/bot-message-dispatch.js +235 -104
  305. package/dist/telegram/bot-native-command-menu.js +3 -1
  306. package/dist/telegram/bot-native-commands.js +213 -193
  307. package/dist/telegram/bot.js +24 -132
  308. package/dist/telegram/draft-stream.js +84 -75
  309. package/dist/telegram/format.js +150 -6
  310. package/dist/telegram/send.js +415 -255
  311. package/dist/telegram/targets.js +21 -2
  312. package/dist/telegram/update-offset-store.js +19 -3
  313. package/dist/terminal/restore.js +5 -2
  314. package/dist/test-utils/fetch-mock.js +5 -0
  315. package/dist/version.js +18 -5
  316. package/dist/web/auto-reply/monitor/broadcast.js +7 -3
  317. package/dist/web/auto-reply/monitor/on-message.js +6 -3
  318. package/dist/web/inbound/media.js +34 -8
  319. package/dist/web/inbound/monitor.js +34 -17
  320. package/dist/web/inbound/send-api.js +18 -17
  321. package/dist/web/outbound.js +12 -5
  322. package/dist/wizard/clack-prompter.js +40 -7
  323. package/extensions/bluebubbles/package.json +1 -1
  324. package/extensions/copilot-proxy/package.json +1 -1
  325. package/extensions/diagnostics-otel/package.json +1 -1
  326. package/extensions/discord/package.json +1 -1
  327. package/extensions/feishu/package.json +1 -1
  328. package/extensions/google-antigravity-auth/package.json +1 -1
  329. package/extensions/google-gemini-cli-auth/package.json +1 -1
  330. package/extensions/googlechat/package.json +1 -1
  331. package/extensions/imessage/package.json +1 -1
  332. package/extensions/irc/package.json +1 -1
  333. package/extensions/line/package.json +1 -1
  334. package/extensions/llm-task/package.json +1 -1
  335. package/extensions/lobster/package.json +1 -1
  336. package/extensions/matrix/CHANGELOG.md +5 -0
  337. package/extensions/matrix/package.json +1 -1
  338. package/extensions/mattermost/package.json +1 -1
  339. package/extensions/memory-core/package.json +1 -1
  340. package/extensions/memory-lancedb/package.json +1 -1
  341. package/extensions/minimax-portal-auth/package.json +1 -1
  342. package/extensions/msteams/CHANGELOG.md +5 -0
  343. package/extensions/msteams/package.json +1 -1
  344. package/extensions/nextcloud-talk/package.json +1 -1
  345. package/extensions/nostr/CHANGELOG.md +5 -0
  346. package/extensions/nostr/package.json +1 -1
  347. package/extensions/open-prose/package.json +1 -1
  348. package/extensions/openai-codex-auth/package.json +1 -1
  349. package/extensions/signal/package.json +1 -1
  350. package/extensions/slack/package.json +1 -1
  351. package/extensions/telegram/package.json +1 -1
  352. package/extensions/tlon/package.json +1 -1
  353. package/extensions/twitch/CHANGELOG.md +5 -0
  354. package/extensions/twitch/package.json +1 -1
  355. package/extensions/voice-call/CHANGELOG.md +5 -0
  356. package/extensions/voice-call/package.json +1 -1
  357. package/extensions/whatsapp/package.json +1 -1
  358. package/extensions/zalo/CHANGELOG.md +5 -0
  359. package/extensions/zalo/package.json +1 -1
  360. package/extensions/zalouser/CHANGELOG.md +5 -0
  361. package/extensions/zalouser/package.json +1 -1
  362. package/package.json +1 -1
  363. package/skills/apple-reminders/SKILL.md +100 -49
  364. package/skills/coding-agent/SKILL.md +34 -28
  365. package/skills/github/SKILL.md +131 -16
  366. package/skills/imsg/SKILL.md +112 -15
  367. package/skills/openhue/SKILL.md +101 -19
  368. package/skills/tmux/SKILL.md +111 -79
  369. package/skills/weather/SKILL.md +88 -25
@@ -1,30 +1,25 @@
1
- // @ts-nocheck
2
1
  import { sequentialize } from "@grammyjs/runner";
3
2
  import { apiThrottler } from "@grammyjs/transformer-throttler";
4
3
  import { Bot, webhookCallback } from "grammy";
5
4
  import { resolveDefaultAgentId } from "../agents/agent-scope.js";
6
- import { isControlCommandMessage } from "../auto-reply/command-detection.js";
7
5
  import { resolveTextChunkLimit } from "../auto-reply/chunk.js";
6
+ import { isAbortRequestText } from "../auto-reply/reply/abort.js";
8
7
  import { DEFAULT_GROUP_HISTORY_LIMIT } from "../auto-reply/reply/history.js";
9
8
  import { isNativeCommandsExplicitlyDisabled, resolveNativeCommandsEnabled, resolveNativeSkillsEnabled, } from "../config/commands.js";
10
9
  import { loadConfig } from "../config/config.js";
11
10
  import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention, } from "../config/group-policy.js";
12
11
  import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
13
12
  import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
14
- import { createSubsystemLogger } from "../logging/subsystem.js";
15
13
  import { formatUncaughtError } from "../infra/errors.js";
16
- import { enqueueSystemEvent } from "../infra/system-events.js";
17
14
  import { getChildLogger } from "../logging.js";
18
- import { withTelegramApiErrorLogging } from "./api-logging.js";
19
- import { resolveAgentRoute } from "../routing/resolve-route.js";
15
+ import { createSubsystemLogger } from "../logging/subsystem.js";
20
16
  import { resolveTelegramAccount } from "./accounts.js";
21
- import { buildTelegramGroupPeerId, buildTelegramParentPeer, resolveTelegramForumThreadId, resolveTelegramStreamMode, } from "./bot/helpers.js";
22
17
  import { registerTelegramHandlers } from "./bot-handlers.js";
23
18
  import { createTelegramMessageProcessor } from "./bot-message.js";
24
19
  import { registerTelegramNativeCommands } from "./bot-native-commands.js";
25
20
  import { buildTelegramUpdateKey, createTelegramUpdateDedupe, resolveTelegramUpdateId, } from "./bot-updates.js";
21
+ import { buildTelegramGroupPeerId, resolveTelegramForumThreadId, resolveTelegramStreamMode, } from "./bot/helpers.js";
26
22
  import { resolveTelegramFetch } from "./fetch.js";
27
- import { wasSentByBot } from "./sent-message-cache.js";
28
23
  export function getTelegramSequentialKey(ctx) {
29
24
  // Handle reaction updates
30
25
  const reaction = ctx.update?.message_reaction;
@@ -38,10 +33,10 @@ export function getTelegramSequentialKey(ctx) {
38
33
  const chatId = msg?.chat?.id ?? ctx.chat?.id;
39
34
  const rawText = msg?.text ?? msg?.caption;
40
35
  const botUsername = ctx.me?.username;
41
- if (rawText &&
42
- isControlCommandMessage(rawText, undefined, botUsername ? { botUsername } : undefined)) {
43
- if (typeof chatId === "number")
36
+ if (isAbortRequestText(rawText, botUsername ? { botUsername } : undefined)) {
37
+ if (typeof chatId === "number") {
44
38
  return `telegram:${chatId}:control`;
39
+ }
45
40
  return "telegram:control";
46
41
  }
47
42
  const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup";
@@ -88,30 +83,29 @@ export function createTelegramBot(opts) {
88
83
  const bot = new Bot(opts.token, client ? { client } : undefined);
89
84
  bot.api.config.use(apiThrottler());
90
85
  bot.use(sequentialize(getTelegramSequentialKey));
91
- bot.catch((err) => {
92
- runtime.error?.(danger(`telegram bot error: ${formatUncaughtError(err)}`));
93
- });
94
86
  // Catch all errors from bot middleware to prevent unhandled rejections
95
87
  bot.catch((err) => {
96
- const message = err instanceof Error ? err.message : String(err);
97
- runtime.error?.(danger(`telegram bot error: ${message}`));
88
+ runtime.error?.(danger(`telegram bot error: ${formatUncaughtError(err)}`));
98
89
  });
99
90
  const recentUpdates = createTelegramUpdateDedupe();
100
91
  let lastUpdateId = typeof opts.updateOffset?.lastUpdateId === "number" ? opts.updateOffset.lastUpdateId : null;
101
92
  const recordUpdateId = (ctx) => {
102
93
  const updateId = resolveTelegramUpdateId(ctx);
103
- if (typeof updateId !== "number")
94
+ if (typeof updateId !== "number") {
104
95
  return;
105
- if (lastUpdateId !== null && updateId <= lastUpdateId)
96
+ }
97
+ if (lastUpdateId !== null && updateId <= lastUpdateId) {
106
98
  return;
99
+ }
107
100
  lastUpdateId = updateId;
108
101
  void opts.updateOffset?.onUpdateId?.(updateId);
109
102
  };
110
103
  const shouldSkipUpdate = (ctx) => {
111
104
  const updateId = resolveTelegramUpdateId(ctx);
112
105
  if (typeof updateId === "number" && lastUpdateId !== null) {
113
- if (updateId <= lastUpdateId)
106
+ if (updateId <= lastUpdateId) {
114
107
  return true;
108
+ }
115
109
  }
116
110
  const key = buildTelegramUpdateKey(ctx);
117
111
  const skipped = recentUpdates.check(key);
@@ -137,10 +131,10 @@ export function createTelegramBot(opts) {
137
131
  ];
138
132
  }
139
133
  if (value && typeof value === "object") {
140
- const obj = value;
141
- if (seen.has(obj))
134
+ if (seen.has(value)) {
142
135
  return "[Circular]";
143
- seen.add(obj);
136
+ }
137
+ seen.add(value);
144
138
  }
145
139
  return value;
146
140
  });
@@ -172,8 +166,7 @@ export function createTelegramBot(opts) {
172
166
  ? telegramCfg.allowFrom
173
167
  : undefined) ??
174
168
  (opts.allowFrom && opts.allowFrom.length > 0 ? opts.allowFrom : undefined);
175
- const replyToMode = opts.replyToMode ?? telegramCfg.replyToMode ?? "first";
176
- const streamMode = resolveTelegramStreamMode(telegramCfg);
169
+ const replyToMode = opts.replyToMode ?? telegramCfg.replyToMode ?? "off";
177
170
  const nativeEnabled = resolveNativeCommandsEnabled({
178
171
  providerId: "telegram",
179
172
  providerSetting: telegramCfg.commands?.native,
@@ -192,32 +185,7 @@ export function createTelegramBot(opts) {
192
185
  const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
193
186
  const mediaMaxBytes = (opts.mediaMaxMb ?? telegramCfg.mediaMaxMb ?? 5) * 1024 * 1024;
194
187
  const logger = getChildLogger({ module: "telegram-auto-reply" });
195
- let botHasTopicsEnabled;
196
- const resolveBotTopicsEnabled = async (ctx) => {
197
- if (typeof ctx?.me?.has_topics_enabled === "boolean") {
198
- botHasTopicsEnabled = ctx.me.has_topics_enabled;
199
- return botHasTopicsEnabled;
200
- }
201
- if (typeof botHasTopicsEnabled === "boolean")
202
- return botHasTopicsEnabled;
203
- if (typeof bot.api.getMe !== "function") {
204
- botHasTopicsEnabled = false;
205
- return botHasTopicsEnabled;
206
- }
207
- try {
208
- const me = await withTelegramApiErrorLogging({
209
- operation: "getMe",
210
- runtime,
211
- fn: () => bot.api.getMe(),
212
- });
213
- botHasTopicsEnabled = Boolean(me?.has_topics_enabled);
214
- }
215
- catch (err) {
216
- logVerbose(`telegram getMe failed: ${String(err)}`);
217
- botHasTopicsEnabled = false;
218
- }
219
- return botHasTopicsEnabled;
220
- };
188
+ const streamMode = resolveTelegramStreamMode(telegramCfg);
221
189
  const resolveGroupPolicy = (chatId) => resolveChannelGroupPolicy({
222
190
  cfg,
223
191
  channel: "telegram",
@@ -232,10 +200,12 @@ export function createTelegramBot(opts) {
232
200
  try {
233
201
  const store = loadSessionStore(storePath);
234
202
  const entry = store[sessionKey];
235
- if (entry?.groupActivation === "always")
203
+ if (entry?.groupActivation === "always") {
236
204
  return false;
237
- if (entry?.groupActivation === "mention")
205
+ }
206
+ if (entry?.groupActivation === "mention") {
238
207
  return true;
208
+ }
239
209
  }
240
210
  catch (err) {
241
211
  logVerbose(`Failed to load session for activation check: ${String(err)}`);
@@ -252,8 +222,9 @@ export function createTelegramBot(opts) {
252
222
  });
253
223
  const resolveTelegramGroupConfig = (chatId, messageThreadId) => {
254
224
  const groups = telegramCfg.groups;
255
- if (!groups)
225
+ if (!groups) {
256
226
  return { groupConfig: undefined, topicConfig: undefined };
227
+ }
257
228
  const groupKey = String(chatId);
258
229
  const groupConfig = groups[groupKey] ?? groups["*"];
259
230
  const topicConfig = messageThreadId != null ? groupConfig?.topics?.[String(messageThreadId)] : undefined;
@@ -279,7 +250,6 @@ export function createTelegramBot(opts) {
279
250
  streamMode,
280
251
  textLimit,
281
252
  opts,
282
- resolveBotTopicsEnabled,
283
253
  });
284
254
  registerTelegramNativeCommands({
285
255
  bot,
@@ -300,84 +270,6 @@ export function createTelegramBot(opts) {
300
270
  shouldSkipUpdate,
301
271
  opts,
302
272
  });
303
- // Handle emoji reactions to messages
304
- bot.on("message_reaction", async (ctx) => {
305
- try {
306
- const reaction = ctx.messageReaction;
307
- if (!reaction)
308
- return;
309
- if (shouldSkipUpdate(ctx))
310
- return;
311
- const chatId = reaction.chat.id;
312
- const messageId = reaction.message_id;
313
- const user = reaction.user;
314
- // Resolve reaction notification mode (default: "own")
315
- const reactionMode = telegramCfg.reactionNotifications ?? "own";
316
- if (reactionMode === "off")
317
- return;
318
- if (user?.is_bot)
319
- return;
320
- if (reactionMode === "own" && !wasSentByBot(chatId, messageId))
321
- return;
322
- // Detect added reactions
323
- const oldEmojis = new Set(reaction.old_reaction
324
- .filter((r) => r.type === "emoji")
325
- .map((r) => r.emoji));
326
- const addedReactions = reaction.new_reaction
327
- .filter((r) => r.type === "emoji")
328
- .filter((r) => !oldEmojis.has(r.emoji));
329
- if (addedReactions.length === 0)
330
- return;
331
- // Build sender label
332
- const senderName = user
333
- ? [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || user.username
334
- : undefined;
335
- const senderUsername = user?.username ? `@${user.username}` : undefined;
336
- let senderLabel = senderName;
337
- if (senderName && senderUsername) {
338
- senderLabel = `${senderName} (${senderUsername})`;
339
- }
340
- else if (!senderName && senderUsername) {
341
- senderLabel = senderUsername;
342
- }
343
- if (!senderLabel && user?.id) {
344
- senderLabel = `id:${user.id}`;
345
- }
346
- senderLabel = senderLabel || "unknown";
347
- // Reactions target a specific message_id; the Telegram Bot API does not include
348
- // message_thread_id on MessageReactionUpdated, so we route to the chat-level
349
- // session (forum topic routing is not available for reactions).
350
- const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup";
351
- const isForum = reaction.chat.is_forum === true;
352
- const resolvedThreadId = isForum
353
- ? resolveTelegramForumThreadId({ isForum, messageThreadId: undefined })
354
- : undefined;
355
- const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
356
- const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
357
- // Fresh config for bindings lookup; other routing inputs are payload-derived.
358
- const route = resolveAgentRoute({
359
- cfg: loadConfig(),
360
- channel: "telegram",
361
- accountId: account.accountId,
362
- peer: { kind: isGroup ? "group" : "dm", id: peerId },
363
- parentPeer,
364
- });
365
- const sessionKey = route.sessionKey;
366
- // Enqueue system event for each added reaction
367
- for (const r of addedReactions) {
368
- const emoji = r.emoji;
369
- const text = `Telegram reaction added: ${emoji} by ${senderLabel} on msg ${messageId}`;
370
- enqueueSystemEvent(text, {
371
- sessionKey: sessionKey,
372
- contextKey: `telegram:reaction:add:${chatId}:${messageId}:${user?.id ?? "anon"}:${emoji}`,
373
- });
374
- logVerbose(`telegram: reaction event enqueued: ${text}`);
375
- }
376
- }
377
- catch (err) {
378
- runtime.error?.(danger(`telegram reaction handler failed: ${String(err)}`));
379
- }
380
- });
381
273
  registerTelegramHandlers({
382
274
  cfg,
383
275
  accountId: account.accountId,
@@ -1,101 +1,110 @@
1
- const TELEGRAM_DRAFT_MAX_CHARS = 4096;
2
- const DEFAULT_THROTTLE_MS = 300;
1
+ import { createDraftStreamLoop } from "../channels/draft-stream-loop.js";
2
+ import { buildTelegramThreadParams } from "./bot/helpers.js";
3
+ const TELEGRAM_STREAM_MAX_CHARS = 4096;
4
+ const DEFAULT_THROTTLE_MS = 1000;
3
5
  export function createTelegramDraftStream(params) {
4
- const maxChars = Math.min(params.maxChars ?? TELEGRAM_DRAFT_MAX_CHARS, TELEGRAM_DRAFT_MAX_CHARS);
5
- const throttleMs = Math.max(50, params.throttleMs ?? DEFAULT_THROTTLE_MS);
6
- const rawDraftId = Number.isFinite(params.draftId) ? Math.trunc(params.draftId) : 1;
7
- const draftId = rawDraftId === 0 ? 1 : Math.abs(rawDraftId);
6
+ const maxChars = Math.min(params.maxChars ?? TELEGRAM_STREAM_MAX_CHARS, TELEGRAM_STREAM_MAX_CHARS);
7
+ const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS);
8
+ const minInitialChars = params.minInitialChars;
8
9
  const chatId = params.chatId;
9
- const threadParams = typeof params.messageThreadId === "number"
10
- ? { message_thread_id: Math.trunc(params.messageThreadId) }
11
- : undefined;
10
+ const threadParams = buildTelegramThreadParams(params.thread);
11
+ const replyParams = params.replyToMessageId != null
12
+ ? { ...threadParams, reply_to_message_id: params.replyToMessageId }
13
+ : threadParams;
14
+ let streamMessageId;
12
15
  let lastSentText = "";
13
- let lastSentAt = 0;
14
- let pendingText = "";
15
- let inFlight = false;
16
- let timer;
17
16
  let stopped = false;
18
- const sendDraft = async (text) => {
19
- if (stopped)
20
- return;
17
+ let isFinal = false;
18
+ const sendOrEditStreamMessage = async (text) => {
19
+ // Allow final flush even if stopped (e.g., after clear()).
20
+ if (stopped && !isFinal) {
21
+ return false;
22
+ }
21
23
  const trimmed = text.trimEnd();
22
- if (!trimmed)
23
- return;
24
+ if (!trimmed) {
25
+ return false;
26
+ }
24
27
  if (trimmed.length > maxChars) {
25
- // Drafts are capped at 4096 chars. Stop streaming once we exceed the cap
26
- // so we don't keep sending failing updates or a truncated preview.
28
+ // Telegram text messages/edits cap at 4096 chars.
29
+ // Stop streaming once we exceed the cap to avoid repeated API failures.
27
30
  stopped = true;
28
- params.warn?.(`telegram draft stream stopped (draft length ${trimmed.length} > ${maxChars})`);
29
- return;
31
+ params.warn?.(`telegram stream preview stopped (text length ${trimmed.length} > ${maxChars})`);
32
+ return false;
33
+ }
34
+ if (trimmed === lastSentText) {
35
+ return true;
36
+ }
37
+ // Debounce first preview send for better push notification quality.
38
+ if (typeof streamMessageId !== "number" && minInitialChars != null && !isFinal) {
39
+ if (trimmed.length < minInitialChars) {
40
+ return false;
41
+ }
30
42
  }
31
- if (trimmed === lastSentText)
32
- return;
33
43
  lastSentText = trimmed;
34
- lastSentAt = Date.now();
35
44
  try {
36
- await params.api.sendMessageDraft(chatId, draftId, trimmed, threadParams);
45
+ if (typeof streamMessageId === "number") {
46
+ await params.api.editMessageText(chatId, streamMessageId, trimmed);
47
+ return true;
48
+ }
49
+ const sent = await params.api.sendMessage(chatId, trimmed, replyParams);
50
+ const sentMessageId = sent?.message_id;
51
+ if (typeof sentMessageId !== "number" || !Number.isFinite(sentMessageId)) {
52
+ stopped = true;
53
+ params.warn?.("telegram stream preview stopped (missing message id from sendMessage)");
54
+ return false;
55
+ }
56
+ streamMessageId = Math.trunc(sentMessageId);
57
+ return true;
37
58
  }
38
59
  catch (err) {
39
60
  stopped = true;
40
- params.warn?.(`telegram draft stream failed: ${err instanceof Error ? err.message : String(err)}`);
61
+ params.warn?.(`telegram stream preview failed: ${err instanceof Error ? err.message : String(err)}`);
62
+ return false;
41
63
  }
42
64
  };
43
- const flush = async () => {
44
- if (timer) {
45
- clearTimeout(timer);
46
- timer = undefined;
47
- }
48
- if (inFlight) {
49
- schedule();
65
+ const loop = createDraftStreamLoop({
66
+ throttleMs,
67
+ isStopped: () => stopped,
68
+ sendOrEditStreamMessage,
69
+ });
70
+ const update = (text) => {
71
+ if (stopped || isFinal) {
50
72
  return;
51
73
  }
52
- const text = pendingText;
53
- pendingText = "";
54
- if (!text.trim()) {
55
- if (pendingText)
56
- schedule();
74
+ loop.update(text);
75
+ };
76
+ const stop = async () => {
77
+ isFinal = true;
78
+ await loop.flush();
79
+ };
80
+ const clear = async () => {
81
+ stopped = true;
82
+ loop.stop();
83
+ await loop.waitForInFlight();
84
+ const messageId = streamMessageId;
85
+ streamMessageId = undefined;
86
+ if (typeof messageId !== "number") {
57
87
  return;
58
88
  }
59
- inFlight = true;
60
89
  try {
61
- await sendDraft(text);
90
+ await params.api.deleteMessage(chatId, messageId);
62
91
  }
63
- finally {
64
- inFlight = false;
92
+ catch (err) {
93
+ params.warn?.(`telegram stream preview cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
65
94
  }
66
- if (pendingText)
67
- schedule();
68
- };
69
- const schedule = () => {
70
- if (timer)
71
- return;
72
- const delay = Math.max(0, throttleMs - (Date.now() - lastSentAt));
73
- timer = setTimeout(() => {
74
- void flush();
75
- }, delay);
76
95
  };
77
- const update = (text) => {
78
- if (stopped)
79
- return;
80
- pendingText = text;
81
- if (inFlight) {
82
- schedule();
83
- return;
84
- }
85
- if (!timer && Date.now() - lastSentAt >= throttleMs) {
86
- void flush();
87
- return;
88
- }
89
- schedule();
96
+ const forceNewMessage = () => {
97
+ streamMessageId = undefined;
98
+ lastSentText = "";
99
+ loop.resetPending();
90
100
  };
91
- const stop = () => {
92
- stopped = true;
93
- pendingText = "";
94
- if (timer) {
95
- clearTimeout(timer);
96
- timer = undefined;
97
- }
101
+ params.log?.(`telegram stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
102
+ return {
103
+ update,
104
+ flush: loop.flush,
105
+ messageId: () => streamMessageId,
106
+ clear,
107
+ stop,
108
+ forceNewMessage,
98
109
  };
99
- params.log?.(`telegram draft stream ready (draftId=${draftId}, maxChars=${maxChars}, throttleMs=${throttleMs})`);
100
- return { update, flush, stop };
101
110
  }
@@ -6,12 +6,66 @@ function escapeHtml(text) {
6
6
  function escapeHtmlAttr(text) {
7
7
  return escapeHtml(text).replace(/"/g, "&quot;");
8
8
  }
9
- function buildTelegramLink(link, _text) {
9
+ /**
10
+ * File extensions that share TLDs and commonly appear in code/documentation.
11
+ * These are wrapped in <code> tags to prevent Telegram from generating
12
+ * spurious domain registrar previews.
13
+ *
14
+ * Only includes extensions that are:
15
+ * 1. Commonly used as file extensions in code/docs
16
+ * 2. Rarely used as intentional domain references
17
+ *
18
+ * Excluded: .ai, .io, .tv, .fm (popular domain TLDs like x.ai, vercel.io, github.io)
19
+ */
20
+ const FILE_EXTENSIONS_WITH_TLD = new Set([
21
+ "md", // Markdown (Moldova) - very common in repos
22
+ "go", // Go language - common in Go projects
23
+ "py", // Python (Paraguay) - common in Python projects
24
+ "pl", // Perl (Poland) - common in Perl projects
25
+ "sh", // Shell (Saint Helena) - common for scripts
26
+ "am", // Automake files (Armenia)
27
+ "at", // Assembly (Austria)
28
+ "be", // Backend files (Belgium)
29
+ "cc", // C++ source (Cocos Islands)
30
+ ]);
31
+ /** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */
32
+ function isAutoLinkedFileRef(href, label) {
33
+ const stripped = href.replace(/^https?:\/\//i, "");
34
+ if (stripped !== label) {
35
+ return false;
36
+ }
37
+ const dotIndex = label.lastIndexOf(".");
38
+ if (dotIndex < 1) {
39
+ return false;
40
+ }
41
+ const ext = label.slice(dotIndex + 1).toLowerCase();
42
+ if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) {
43
+ return false;
44
+ }
45
+ // Reject if any path segment before the filename contains a dot (looks like a domain)
46
+ const segments = label.split("/");
47
+ if (segments.length > 1) {
48
+ for (let i = 0; i < segments.length - 1; i++) {
49
+ if (segments[i].includes(".")) {
50
+ return false;
51
+ }
52
+ }
53
+ }
54
+ return true;
55
+ }
56
+ function buildTelegramLink(link, text) {
10
57
  const href = link.href.trim();
11
- if (!href)
58
+ if (!href) {
59
+ return null;
60
+ }
61
+ if (link.start === link.end) {
12
62
  return null;
13
- if (link.start === link.end)
63
+ }
64
+ // Suppress auto-linkified file references (e.g. README.md → http://README.md)
65
+ const label = text.slice(link.start, link.end);
66
+ if (isAutoLinkedFileRef(href, label)) {
14
67
  return null;
68
+ }
15
69
  const safeHref = escapeHtmlAttr(href);
16
70
  return {
17
71
  start: link.start,
@@ -28,6 +82,8 @@ function renderTelegramHtml(ir) {
28
82
  strikethrough: { open: "<s>", close: "</s>" },
29
83
  code: { open: "<code>", close: "</code>" },
30
84
  code_block: { open: "<pre><code>", close: "</code></pre>" },
85
+ spoiler: { open: "<tg-spoiler>", close: "</tg-spoiler>" },
86
+ blockquote: { open: "<blockquote>", close: "</blockquote>" },
31
87
  },
32
88
  escapeText: escapeHtml,
33
89
  buildLink: buildTelegramLink,
@@ -36,28 +92,116 @@ function renderTelegramHtml(ir) {
36
92
  export function markdownToTelegramHtml(markdown, options = {}) {
37
93
  const ir = markdownToIR(markdown ?? "", {
38
94
  linkify: true,
95
+ enableSpoilers: true,
39
96
  headingStyle: "none",
40
97
  blockquotePrefix: "",
41
98
  tableMode: options.tableMode,
42
99
  });
43
- return renderTelegramHtml(ir);
100
+ const html = renderTelegramHtml(ir);
101
+ // Apply file reference wrapping if requested (for chunked rendering)
102
+ if (options.wrapFileRefs !== false) {
103
+ return wrapFileReferencesInHtml(html);
104
+ }
105
+ return html;
106
+ }
107
+ /**
108
+ * Wraps standalone file references (with TLD extensions) in <code> tags.
109
+ * This prevents Telegram from treating them as URLs and generating
110
+ * irrelevant domain registrar previews.
111
+ *
112
+ * Runs AFTER markdown→HTML conversion to avoid modifying HTML attributes.
113
+ * Skips content inside <code>, <pre>, and <a> tags to avoid nesting issues.
114
+ */
115
+ /** Escape regex metacharacters in a string */
116
+ function escapeRegex(str) {
117
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
118
+ }
119
+ const FILE_EXTENSIONS_PATTERN = Array.from(FILE_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
120
+ const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
121
+ const FILE_REFERENCE_PATTERN = new RegExp(`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`, "gi");
122
+ const ORPHANED_TLD_PATTERN = new RegExp(`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=[^a-zA-Z0-9/]|$)`, "g");
123
+ const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
124
+ function wrapStandaloneFileRef(match, prefix, filename) {
125
+ if (filename.startsWith("//")) {
126
+ return match;
127
+ }
128
+ if (/https?:\/\/$/i.test(prefix)) {
129
+ return match;
130
+ }
131
+ return `${prefix}<code>${escapeHtml(filename)}</code>`;
132
+ }
133
+ function wrapSegmentFileRefs(text, codeDepth, preDepth, anchorDepth) {
134
+ if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) {
135
+ return text;
136
+ }
137
+ const wrappedStandalone = text.replace(FILE_REFERENCE_PATTERN, wrapStandaloneFileRef);
138
+ return wrappedStandalone.replace(ORPHANED_TLD_PATTERN, (match, prefix, tld) => prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`);
139
+ }
140
+ export function wrapFileReferencesInHtml(html) {
141
+ // Safety-net: de-linkify auto-generated anchors where href="http://<label>" (defense in depth for textMode: "html")
142
+ AUTO_LINKED_ANCHOR_PATTERN.lastIndex = 0;
143
+ const deLinkified = html.replace(AUTO_LINKED_ANCHOR_PATTERN, (_match, label) => {
144
+ if (!isAutoLinkedFileRef(`http://${label}`, label)) {
145
+ return _match;
146
+ }
147
+ return `<code>${escapeHtml(label)}</code>`;
148
+ });
149
+ // Track nesting depth for tags that should not be modified
150
+ let codeDepth = 0;
151
+ let preDepth = 0;
152
+ let anchorDepth = 0;
153
+ let result = "";
154
+ let lastIndex = 0;
155
+ // Process tags token-by-token so we can skip protected regions while wrapping plain text.
156
+ HTML_TAG_PATTERN.lastIndex = 0;
157
+ let match;
158
+ while ((match = HTML_TAG_PATTERN.exec(deLinkified)) !== null) {
159
+ const tagStart = match.index;
160
+ const tagEnd = HTML_TAG_PATTERN.lastIndex;
161
+ const isClosing = match[1] === "</";
162
+ const tagName = match[2].toLowerCase();
163
+ // Process text before this tag
164
+ const textBefore = deLinkified.slice(lastIndex, tagStart);
165
+ result += wrapSegmentFileRefs(textBefore, codeDepth, preDepth, anchorDepth);
166
+ // Update tag depth (clamp at 0 for malformed HTML with stray closing tags)
167
+ if (tagName === "code") {
168
+ codeDepth = isClosing ? Math.max(0, codeDepth - 1) : codeDepth + 1;
169
+ }
170
+ else if (tagName === "pre") {
171
+ preDepth = isClosing ? Math.max(0, preDepth - 1) : preDepth + 1;
172
+ }
173
+ else if (tagName === "a") {
174
+ anchorDepth = isClosing ? Math.max(0, anchorDepth - 1) : anchorDepth + 1;
175
+ }
176
+ // Add the tag itself
177
+ result += deLinkified.slice(tagStart, tagEnd);
178
+ lastIndex = tagEnd;
179
+ }
180
+ // Process remaining text
181
+ const remainingText = deLinkified.slice(lastIndex);
182
+ result += wrapSegmentFileRefs(remainingText, codeDepth, preDepth, anchorDepth);
183
+ return result;
44
184
  }
45
185
  export function renderTelegramHtmlText(text, options = {}) {
46
186
  const textMode = options.textMode ?? "markdown";
47
- if (textMode === "html")
187
+ if (textMode === "html") {
188
+ // For HTML mode, trust caller markup - don't modify
48
189
  return text;
190
+ }
191
+ // markdownToTelegramHtml already wraps file references by default
49
192
  return markdownToTelegramHtml(text, { tableMode: options.tableMode });
50
193
  }
51
194
  export function markdownToTelegramChunks(markdown, limit, options = {}) {
52
195
  const ir = markdownToIR(markdown ?? "", {
53
196
  linkify: true,
197
+ enableSpoilers: true,
54
198
  headingStyle: "none",
55
199
  blockquotePrefix: "",
56
200
  tableMode: options.tableMode,
57
201
  });
58
202
  const chunks = chunkMarkdownIR(ir, limit);
59
203
  return chunks.map((chunk) => ({
60
- html: renderTelegramHtml(chunk),
204
+ html: wrapFileReferencesInHtml(renderTelegramHtml(chunk)),
61
205
  text: chunk.text,
62
206
  }));
63
207
  }