squidclaw 3.0.13 → 3.0.14

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 (307) hide show
  1. package/dist/{accounts-PhtBJ8mA.js → accounts-BjEXPlGc.js} +1 -1
  2. package/dist/{accounts-Xp5W2xrR.js → accounts-CyBVeR-N.js} +1 -1
  3. package/dist/{accounts-BFC1okn9.js → accounts-D095MOmG.js} +7 -7
  4. package/dist/{acp-cli-Dt95OPXW.js → acp-cli-Dd2joFFS.js} +8 -8
  5. package/dist/{agent-scope-tUxfsPYq.js → agent-scope-C1XMay0T.js} +17 -17
  6. package/dist/{agents.config-DNPisWCH.js → agents.config-CRKsD30n.js} +2 -2
  7. package/dist/{api-key-rotation-Y59kKrr0.js → api-key-rotation-BcKKu9kK.js} +1 -1
  8. package/dist/{audio-preflight-DeBM0nTy.js → audio-preflight-g9rsstMv.js} +34 -34
  9. package/dist/{audio-transcription-runner-B7oPsr3U.js → audio-transcription-runner-Bnl3Ubjo.js} +23 -23
  10. package/dist/{audit-Bq3iosCf.js → audit-B4s_5Gn1.js} +29 -29
  11. package/dist/{auth-HYiP0mxx.js → auth-DHSeaNcB.js} +1 -1
  12. package/dist/{auth-choice-Du_pIfBh.js → auth-choice-C-7c5Td_.js} +13 -13
  13. package/dist/{auth-choice-CX8TDXmp.js → auth-choice-D7LiN5Ju.js} +11 -11
  14. package/dist/{auth-choice.apply-helpers-DRfHu1d8.js → auth-choice.apply-helpers-CxO2Wbq-.js} +1 -1
  15. package/dist/{auth-token-BQRI3c6e.js → auth-token-BxYYHBlc.js} +1 -1
  16. package/dist/{bonjour-discovery-BenDpM0y.js → bonjour-discovery-mtNewKSx.js} +1 -1
  17. package/dist/{browser-cli-Et8PyJjA.js → browser-cli-I1fzIVmS.js} +12 -12
  18. package/dist/build-info.json +2 -2
  19. package/dist/{call-DbkLm3eP.js → call-DkR5OGhh.js} +10 -10
  20. package/dist/canvas-host/a2ui/.bundle.hash +1 -1
  21. package/dist/{channel-account-context-C_WQRa6U.js → channel-account-context-e4ysObLI.js} +5 -5
  22. package/dist/{channel-activity-7kixHIgY.js → channel-activity-CCjlTlcN.js} +1 -1
  23. package/dist/{channel-options-D0TjqxeO.js → channel-options-BL5mHe-R.js} +3 -3
  24. package/dist/{channel-selection-BwbXcgj2.js → channel-selection-D_20zq3H.js} +1 -1
  25. package/dist/{channel-web-lfZ3VOOl.js → channel-web-C7Iij0H0.js} +16 -16
  26. package/dist/{channels-cli-Hr4vekE6.js → channels-cli-BMiEn81Y.js} +92 -92
  27. package/dist/{channels-status-issues-DeoqSoo-.js → channels-status-issues-N9kzQrD8.js} +1 -1
  28. package/dist/{chrome-gIqO4t7T.js → chrome-BNfSmFAM.js} +4 -4
  29. package/dist/{clawbot-cli-C-jGxnTq.js → clawbot-cli-B6-68gsT.js} +11 -11
  30. package/dist/cli/daemon-cli.js +1 -1
  31. package/dist/{cli-BH62lCfL.js → cli-ByaubO_B.js} +72 -72
  32. package/dist/{client-CeaUCJtL.js → client-BURktNyH.js} +2 -2
  33. package/dist/{command-secret-targets-BF4WzaME.js → command-secret-targets-WwcPUFAf.js} +4 -4
  34. package/dist/{commands-CD6EMOiw.js → commands-CiY9HjRt.js} +1 -1
  35. package/dist/{commands-registry-DCUj1QOi.js → commands-registry-0bpX7A_Q.js} +3 -3
  36. package/dist/{completion-cli-BIY_jjYY.js → completion-cli-8PdK97J-.js} +12 -12
  37. package/dist/{config-cli-DvNViMXW.js → config-cli-ClomIMw7.js} +7 -7
  38. package/dist/{config-guard-DwgnWjmj.js → config-guard-rLrk7eSS.js} +16 -16
  39. package/dist/{config-validation-CcqZ--gE.js → config-validation-BQEaXMk7.js} +3 -3
  40. package/dist/{configure-B5Zuhebc.js → configure-CoXGul3l.js} +17 -17
  41. package/dist/{control-ui-assets-x69xyZH-.js → control-ui-assets-Bz7TCLpb.js} +1 -1
  42. package/dist/{cron-cli-Bv-X4msQ.js → cron-cli-C3iKBccc.js} +11 -11
  43. package/dist/{daemon-cli-B-8-rGND.js → daemon-cli-BVcrlw5o.js} +15 -15
  44. package/dist/{daemon-install-2y4HHhYl.js → daemon-install-CXDWHEEO.js} +4 -4
  45. package/dist/{daemon-install-helpers-Cdo6Pryw.js → daemon-install-helpers-a4pGEsbA.js} +11 -11
  46. package/dist/{deliver-CVl43oM1.js → deliver-B1sllFkh.js} +7 -7
  47. package/dist/deliver-runtime-CaV7seKv.js +61 -0
  48. package/dist/deps-send-discord.runtime-DbS5pxta.js +36 -0
  49. package/dist/deps-send-imessage.runtime-Ji753_F7.js +35 -0
  50. package/dist/deps-send-signal.runtime-DLJqvPLf.js +34 -0
  51. package/dist/deps-send-slack.runtime-DrFxeZ1I.js +32 -0
  52. package/dist/{deps-send-telegram.runtime-C9lz8bf0.js → deps-send-telegram.runtime-CF-Ylrvr.js} +16 -16
  53. package/dist/deps-send-whatsapp.runtime-BOE0Ke6v.js +118 -0
  54. package/dist/{devices-cli-BT8K0hdG.js → devices-cli-Dn_norz3.js} +8 -8
  55. package/dist/{diagnostic-5bqrhGEp.js → diagnostic-CNl71AzW.js} +1 -1
  56. package/dist/{diagnostics-C0CazfQM.js → diagnostics-DZ99A0EW.js} +5 -5
  57. package/dist/{directory-cli-BZcT9ie1.js → directory-cli-ZzwXkW89.js} +7 -7
  58. package/dist/{dns-cli-CPYaeASu.js → dns-cli-DipYQjtY.js} +5 -5
  59. package/dist/{dock-CVTqzMqE.js → dock-C7N1CmcP.js} +4 -4
  60. package/dist/{docs-cli-C50vGyDh.js → docs-cli-C-Izm9Tb.js} +4 -4
  61. package/dist/{doctor-completion-Emprau8v.js → doctor-completion-CGmYWy0n.js} +2 -2
  62. package/dist/{doctor-config-flow-DSA-JE3n.js → doctor-config-flow-DDWA3Z2D.js} +15 -15
  63. package/dist/{enable-CAYj5OhY.js → enable-RJyJVP_B.js} +1 -1
  64. package/dist/{exec-approvals-allowlist-BO4EEQUI.js → exec-approvals-allowlist-Bu_oaDJC.js} +1 -1
  65. package/dist/{exec-approvals-cli-DSOHdWJR.js → exec-approvals-cli-Csducy9L.js} +16 -16
  66. package/dist/{exec-safe-bin-runtime-policy-CSnpe1AT.js → exec-safe-bin-runtime-policy-DRQwt8kj.js} +2 -2
  67. package/dist/{fetch-guard-VcWeni3c.js → fetch-guard-DNykKlIv.js} +1 -1
  68. package/dist/{fs-safe-CjHYKGbt.js → fs-safe-CKHYu4Vz.js} +24 -24
  69. package/dist/{gateway-cli-B2fScwuY.js → gateway-cli-CUph5Eqo.js} +152 -152
  70. package/dist/{gateway-rpc-DpqZnhyz.js → gateway-rpc-DoKw40aG.js} +1 -1
  71. package/dist/{health-BZcB9SMe.js → health-Cia3g9r1.js} +11 -11
  72. package/dist/{hooks-cli-BylHgZ52.js → hooks-cli-LDbU3XMU.js} +80 -80
  73. package/dist/{hooks-status-DqGd74DG.js → hooks-status-Dd_iWcBZ.js} +1 -1
  74. package/dist/{image-CulZw1up.js → image-D2a7rcK1.js} +5 -5
  75. package/dist/{image-ops-BpsIdA2j.js → image-ops-BIWika4g.js} +10 -10
  76. package/dist/image-runtime-DTxVPe7Z.js +55 -0
  77. package/dist/index.js +82 -82
  78. package/dist/{inspect-D5T8Bbzl.js → inspect-C3PHuODr.js} +4 -4
  79. package/dist/{install-safe-path-BoN-MLvX.js → install-safe-path-iCmVuopp.js} +25 -25
  80. package/dist/{installs-CPHMcmNj.js → installs-CyjlehlR.js} +9 -9
  81. package/dist/{ipv4-Bwl9ruCP.js → ipv4-CWVOQw5T.js} +1 -1
  82. package/dist/{ir-xnftdkOo.js → ir-BPM7rQYq.js} +8 -8
  83. package/dist/{issue-format-8t_ncgFx.js → issue-format-sA05c-6t.js} +1 -1
  84. package/dist/{json-files-DIEpaxUj.js → json-files-D6M304Qd.js} +8 -8
  85. package/dist/{lifecycle-core-DKiBjZdv.js → lifecycle-core-DsfZ6965.js} +5 -5
  86. package/dist/{local-roots-BrxGyJnb.js → local-roots-CMxJ8L4w.js} +3 -3
  87. package/dist/{login-CxDYXbgx.js → login-BjtBaVVG.js} +3 -3
  88. package/dist/{login-qr-zf4B1lpU.js → login-qr-JfRlpd7I.js} +6 -6
  89. package/dist/{logs-cli-CJAe1_dv.js → logs-cli-gv2Ql7GE.js} +9 -9
  90. package/dist/{manager-DBgRFvYb.js → manager-XeZQ6ws7.js} +14 -14
  91. package/dist/{manager-runtime-Dl31cGh-.js → manager-runtime-BvGqzQIY.js} +9 -9
  92. package/dist/{manifest-registry-Cx3mB9zS.js → manifest-registry-BaUPjNKb.js} +1 -1
  93. package/dist/{memory-cli-Bhoc-n_1.js → memory-cli-BUJHSsW6.js} +12 -12
  94. package/dist/{model-DYzjrxpA.js → model-Jy6fO59G.js} +2 -2
  95. package/dist/{model-catalog-kpDOW8eY.js → model-catalog-BfoWgIDd.js} +3 -3
  96. package/dist/{model-picker-BZwK54QT.js → model-picker-CxmHVCE1.js} +4 -4
  97. package/dist/{model-selection-ZV3PuYVR.js → model-selection-QgM_TOjC.js} +16 -16
  98. package/dist/{models-cli-BIxlIOFr.js → models-cli-DlbNL6it.js} +81 -81
  99. package/dist/{models-config-BLdDYoxi.js → models-config-DOD5jluc.js} +6 -6
  100. package/dist/{node-cli-6uYJccbd.js → node-cli-uJ5lFzxj.js} +33 -33
  101. package/dist/{node-command-policy-CKTXVa3D.js → node-command-policy-CwVo8Z1X.js} +1 -1
  102. package/dist/{node-service-DGPVb5ri.js → node-service-xGYkt3vb.js} +1 -1
  103. package/dist/{nodes-cli-CXLRfAYx.js → nodes-cli-eauCMbSr.js} +16 -16
  104. package/dist/{nodes-screen-C-tuRhA1.js → nodes-screen-iR_FzGNs.js} +7 -7
  105. package/dist/{npm-pack-install-DgSn7djE.js → npm-pack-install-s-mP9K0z.js} +18 -18
  106. package/dist/{npm-resolution-gw3vFNTB.js → npm-resolution-Ck7yFIM2.js} +4 -4
  107. package/dist/{onboard-CCaYDSO2.js → onboard-aISMp4Kt.js} +6 -6
  108. package/dist/{onboard-channels-DQnP5d3Y.js → onboard-channels-CELkEQUS.js} +21 -21
  109. package/dist/{onboard-custom-CJohrPzT.js → onboard-custom-BmAzWYbD.js} +4 -4
  110. package/dist/{onboard-helpers-Bcs_1b81.js → onboard-helpers-DRk4Y5uA.js} +10 -10
  111. package/dist/{onboard-hooks-A23nqe_3.js → onboard-hooks-lk1sMnDf.js} +4 -4
  112. package/dist/{onboard-remote-Hf9sTDHl.js → onboard-remote-Bj9cR8kF.js} +4 -4
  113. package/dist/{onboard-skills-Cpc8o6sG.js → onboard-skills-eRpyQ1la.js} +4 -4
  114. package/dist/{onboarding-wx00blu5.js → onboarding-Bl4RDn6W.js} +14 -14
  115. package/dist/{onboarding.finalize-B8wqvggG.js → onboarding.finalize-Bm7_INHb.js} +85 -85
  116. package/dist/{onboarding.gateway-config-CvWq5i_X.js → onboarding.gateway-config-BPyk6gwr.js} +18 -18
  117. package/dist/{onboarding.secret-input-CIN4p8mg.js → onboarding.secret-input-DlDt-I2O.js} +1 -1
  118. package/dist/{openai-model-default-BYfJa19m.js → openai-model-default-DnYP1Em1.js} +2 -2
  119. package/dist/{outbound-B3RZI-ai.js → outbound-BQtnr_z2.js} +3 -3
  120. package/dist/{outbound-attachment-BATDqOuj.js → outbound-attachment-WnXMTJC4.js} +2 -2
  121. package/dist/{pairing-cli-FMIw0yL6.js → pairing-cli-BsU-YnVT.js} +8 -8
  122. package/dist/{pairing-labels-MH31IXn_.js → pairing-labels-BXfoinTP.js} +1 -1
  123. package/dist/{pairing-store-KII9MIZX.js → pairing-store-C1FvWpDK.js} +3 -3
  124. package/dist/{path-alias-guards-B7H6jIIw.js → path-alias-guards-CCRXJArp.js} +3 -3
  125. package/dist/{path-safety-3wUzDqI9.js → path-safety-BWiXz6D4.js} +1 -1
  126. package/dist/{paths-BguvT84s.js → paths-Bg6h1q3v.js} +9 -9
  127. package/dist/{pi-embedded-helpers-D8WkqLZt.js → pi-embedded-helpers-DctimJJI.js} +6 -6
  128. package/dist/{pi-model-discovery-Bfyzj3Lq.js → pi-model-discovery-D63dINOn.js} +1 -1
  129. package/dist/{pi-model-discovery-runtime-nh-zh_Bp.js → pi-model-discovery-runtime-CcXGQcru.js} +5 -5
  130. package/dist/{pi-tools.before-tool-call.runtime-D4V6gUzq.js → pi-tools.before-tool-call.runtime-BMb_b90y.js} +5 -5
  131. package/dist/{pi-tools.policy-DCE9mhtw.js → pi-tools.policy-CXvZtJB7.js} +5 -5
  132. package/dist/{plugin-auto-enable-BJw5Rcbx.js → plugin-auto-enable-DMABTEO-.js} +3 -3
  133. package/dist/{plugin-registry-V0H8DaZf.js → plugin-registry-BSlUIHKX.js} +3 -3
  134. package/dist/plugin-sdk/accounts-B6gye1Jd.js +46 -0
  135. package/dist/plugin-sdk/accounts-C-BT6Po7.js +288 -0
  136. package/dist/plugin-sdk/accounts-DQDXV8eB.js +35 -0
  137. package/dist/plugin-sdk/active-listener-DZCohPuZ.js +50 -0
  138. package/dist/plugin-sdk/api-key-rotation-CrX0fvDj.js +181 -0
  139. package/dist/plugin-sdk/audio-preflight-xnWAFqH-.js +69 -0
  140. package/dist/plugin-sdk/audio-transcription-runner-BDmtq7-q.js +2176 -0
  141. package/dist/plugin-sdk/audit-membership-runtime-B9b-zRwg.js +58 -0
  142. package/dist/plugin-sdk/channel-activity-JjLRpUa_.js +94 -0
  143. package/dist/plugin-sdk/channel-web-DNWsxhYh.js +2256 -0
  144. package/dist/plugin-sdk/chrome-B5PWOUbr.js +2415 -0
  145. package/dist/plugin-sdk/commands-registry-BKeyJUxK.js +1125 -0
  146. package/dist/plugin-sdk/config-FhBFLsNm.js +17911 -0
  147. package/dist/plugin-sdk/deliver-DEbTlzFy.js +1694 -0
  148. package/dist/plugin-sdk/deliver-runtime-CO2uP-r9.js +32 -0
  149. package/dist/plugin-sdk/deps-send-discord.runtime-DIPW0tR4.js +23 -0
  150. package/dist/plugin-sdk/deps-send-imessage.runtime-ByGjRa1H.js +22 -0
  151. package/dist/plugin-sdk/deps-send-signal.runtime-Ca1awu4L.js +21 -0
  152. package/dist/plugin-sdk/deps-send-slack.runtime-CRzWCVkC.js +19 -0
  153. package/dist/plugin-sdk/deps-send-telegram.runtime-BWyavKp9.js +24 -0
  154. package/dist/plugin-sdk/deps-send-whatsapp.runtime-cC_XvHV8.js +57 -0
  155. package/dist/plugin-sdk/diagnostic-Dv9S12vm.js +319 -0
  156. package/dist/plugin-sdk/errors-B8oJXuCF.js +54 -0
  157. package/dist/plugin-sdk/fetch-guard-W_A4uSz2.js +156 -0
  158. package/dist/plugin-sdk/fs-safe-Dqmpk-Fr.js +352 -0
  159. package/dist/plugin-sdk/image-BSFy8d1M.js +2310 -0
  160. package/dist/plugin-sdk/image-ops-DN17S88I.js +584 -0
  161. package/dist/plugin-sdk/image-runtime-5YO31sjU.js +25 -0
  162. package/dist/plugin-sdk/imessage.js +2 -2
  163. package/dist/plugin-sdk/index.js +50 -50
  164. package/dist/plugin-sdk/ir-JaPZ0yKH.js +1296 -0
  165. package/dist/plugin-sdk/local-roots-BTW3ifco.js +186 -0
  166. package/dist/plugin-sdk/logger-DDdrdbDu.js +1163 -0
  167. package/dist/plugin-sdk/login-BXGRny-S.js +57 -0
  168. package/dist/plugin-sdk/login-qr-DTs92ap8.js +320 -0
  169. package/dist/plugin-sdk/manager-DzFj9oHX.js +3917 -0
  170. package/dist/plugin-sdk/manager-runtime-DrpyZvO0.js +15 -0
  171. package/dist/plugin-sdk/mattermost.js +3 -3
  172. package/dist/plugin-sdk/outbound-CQ7uBBML.js +212 -0
  173. package/dist/plugin-sdk/outbound-attachment-dTE6EVdX.js +19 -0
  174. package/dist/plugin-sdk/path-alias-guards-gBhrAn14.js +43 -0
  175. package/dist/plugin-sdk/paths-C6W4VHoa.js +166 -0
  176. package/dist/plugin-sdk/pi-embedded-helpers-CfzQPXDC.js +9627 -0
  177. package/dist/plugin-sdk/pi-model-discovery-Bt6B0MAj.js +134 -0
  178. package/dist/plugin-sdk/pi-model-discovery-runtime-BcgXpTmL.js +8 -0
  179. package/dist/plugin-sdk/pi-tools.before-tool-call.runtime-DYJQxhuo.js +354 -0
  180. package/dist/plugin-sdk/plugins-6NxPd6TS.js +864 -0
  181. package/dist/plugin-sdk/proxy-fetch-ZPEvp58f.js +38 -0
  182. package/dist/plugin-sdk/pw-ai-BFK39pwE.js +1938 -0
  183. package/dist/plugin-sdk/qmd-manager-6bozlfFg.js +1448 -0
  184. package/dist/plugin-sdk/query-expansion-eeVz_aEm.js +1011 -0
  185. package/dist/plugin-sdk/redact-BoNEjbpF.js +319 -0
  186. package/dist/plugin-sdk/reply-DgcrQBKL.js +98828 -0
  187. package/dist/plugin-sdk/resolve-outbound-target-CbaJ-kc2.js +40 -0
  188. package/dist/plugin-sdk/run-with-concurrency-5DMu9szx.js +1994 -0
  189. package/dist/plugin-sdk/runtime-whatsapp-login.runtime-jkgyeVsN.js +10 -0
  190. package/dist/plugin-sdk/runtime-whatsapp-outbound.runtime-DdLJJ1YC.js +19 -0
  191. package/dist/plugin-sdk/send-BSoMbeqA.js +3135 -0
  192. package/dist/plugin-sdk/send-Byyfc20_.js +503 -0
  193. package/dist/plugin-sdk/send-CI-xWEs7.js +2587 -0
  194. package/dist/plugin-sdk/send-DzP9EJqK.js +540 -0
  195. package/dist/plugin-sdk/send-MlSZ82sA.js +414 -0
  196. package/dist/plugin-sdk/session-DFy97tfW.js +169 -0
  197. package/dist/plugin-sdk/signal.js +2 -2
  198. package/dist/plugin-sdk/skill-commands-yum46YuA.js +342 -0
  199. package/dist/plugin-sdk/skills-DUphJGKn.js +1428 -0
  200. package/dist/plugin-sdk/slash-commands.runtime-5UW5KLyR.js +13 -0
  201. package/dist/plugin-sdk/slash-dispatch.runtime-4oQ2P_qo.js +52 -0
  202. package/dist/plugin-sdk/slash-skill-commands.runtime-y_mOLyS7.js +16 -0
  203. package/dist/plugin-sdk/ssrf-B3XRWBsP.js +202 -0
  204. package/dist/plugin-sdk/store-DFvIhzWZ.js +81 -0
  205. package/dist/plugin-sdk/subagent-registry-runtime-DtKXhKtl.js +52 -0
  206. package/dist/plugin-sdk/tables-bDM_jlLP.js +55 -0
  207. package/dist/{target-errors-DlzVutaL.js → plugin-sdk/target-errors-BVBW25Y3.js} +4 -4
  208. package/dist/plugin-sdk/thinking-Bu-w5mW5.js +1206 -0
  209. package/dist/plugin-sdk/tokens-CTIYTLWu.js +52 -0
  210. package/dist/plugin-sdk/tool-images-D0G_giwP.js +274 -0
  211. package/dist/plugin-sdk/web-DSXk7XCb.js +56 -0
  212. package/dist/plugin-sdk/whatsapp-actions-BOyA0Uuj.js +80 -0
  213. package/dist/{plugins-Cl_3OCyK.js → plugins-H53_4Gxb.js} +2 -2
  214. package/dist/{plugins-cli-CN2fty5U.js → plugins-cli-CQkxWdnt.js} +82 -82
  215. package/dist/{ports-Bop51hz6.js → ports-CKXuQJST.js} +2 -2
  216. package/dist/{ports-6i8smH3e.js → ports-CXjhFS7T.js} +1 -1
  217. package/dist/{program-context-ehHvCw9L.js → program-context-tphS7xu7.js} +41 -41
  218. package/dist/{prompt-select-styled-CSMviLJY.js → prompt-select-styled-DL2p1pfi.js} +40 -40
  219. package/dist/{provider-auth-helpers-CgNWlsqs.js → provider-auth-helpers-BqWiy-r-.js} +5 -5
  220. package/dist/{proxy-env-CRD7fbqp.js → proxy-env-D1tz4Z6a.js} +1 -1
  221. package/dist/{push-apns-DoYzx3tH.js → push-apns-C-YdARdy.js} +5 -5
  222. package/dist/{pw-ai-yKJj32B4.js → pw-ai-SVeR5d2o.js} +18 -18
  223. package/dist/{qmd-manager-AYDUTXmc.js → qmd-manager-B-XXhWVw.js} +20 -20
  224. package/dist/{qr-cli-cikAHfYn.js → qr-cli-B4fjHvn1.js} +2 -2
  225. package/dist/{query-expansion-CV2Z4_mS.js → query-expansion-Bf60ekMj.js} +12 -12
  226. package/dist/{redact-snapshot-C9T1079O.js → redact-snapshot-DRqM8Vla.js} +1 -1
  227. package/dist/{register.agent-BlxoyQt0.js → register.agent-BdiM0qkl.js} +97 -97
  228. package/dist/register.configure-wANXDbzQ.js +164 -0
  229. package/dist/{register.maintenance-C-Yv2mHK.js → register.maintenance-CMAt3Nl8.js} +93 -93
  230. package/dist/{register.message-CUNXtFOj.js → register.message-CbxQ5lgi.js} +73 -73
  231. package/dist/{register.onboard-BKXm1mL7.js → register.onboard-CbiTdKQX.js} +18 -18
  232. package/dist/{register.setup-DaT9AIAz.js → register.setup-KhsHMx2v.js} +21 -21
  233. package/dist/{register.status-health-sessions-7yZmSvUL.js → register.status-health-sessions-Bw5YDa_s.js} +86 -86
  234. package/dist/{reply-CeUYZhWu.js → reply-BWXzPVSJ.js} +149 -149
  235. package/dist/{rpc-BDpuIniF.js → rpc-DlN_W_L1.js} +1 -1
  236. package/dist/{runtime-Dw7Yw4OJ.js → runtime-Ci7mtLvH.js} +3 -3
  237. package/dist/{runtime-config-collectors-jhc1wDBg.js → runtime-config-collectors-CKmGmTQ5.js} +1 -1
  238. package/dist/{runtime-whatsapp-login.runtime-f293Z_er.js → runtime-whatsapp-login.runtime-Bxqt5jiO.js} +7 -7
  239. package/dist/runtime-whatsapp-outbound.runtime-B-UsXwvb.js +32 -0
  240. package/dist/{sandbox-BgsD1lf7.js → sandbox-Cuk1IWYT.js} +18 -18
  241. package/dist/{sandbox-cli-BnvmhiN9.js → sandbox-cli-BxgFeoAD.js} +25 -25
  242. package/dist/{secrets-cli-D7-K7P82.js → secrets-cli-DODLX29x.js} +11 -11
  243. package/dist/{security-cli-BFJAgNYH.js → security-cli-Ch83VrLP.js} +42 -42
  244. package/dist/{send-BvlkshC1.js → send-BfVgGHK6.js} +6 -6
  245. package/dist/{send-tK0H9nwq.js → send-Bw8LNCit.js} +5 -5
  246. package/dist/{send-CDms2FQA.js → send-C9UJKBSM.js} +4 -4
  247. package/dist/{send-CPFNtAP8.js → send-DX_fR45p.js} +11 -11
  248. package/dist/{send-C3Aeswif.js → send-FTQaNJPj.js} +8 -8
  249. package/dist/{server-OfKJG6Bo.js → server-Q9nnn04D.js} +20 -20
  250. package/dist/{server-context-B-0KzcZE.js → server-context-CX28l04l.js} +12 -12
  251. package/dist/{server-lifecycle-D6VNKVvQ.js → server-lifecycle-yAUMTJhv.js} +2 -2
  252. package/dist/{server-middleware-7cXowO4W.js → server-middleware-DIc4WJOS.js} +1 -1
  253. package/dist/{server-node-events-DWQhNK-0.js → server-node-events-qU3NTncQ.js} +73 -73
  254. package/dist/{service-Dk-UMipf.js → service-D4y0_Q5Z.js} +15 -15
  255. package/dist/{session-Da18ilJ0.js → session-CZJ5Ux6-.js} +1 -1
  256. package/dist/{sessions-CmjcNTJ3.js → sessions-C5H_BZSr.js} +15 -15
  257. package/dist/{shared-BzY0v0tS.js → shared-CxdBWgym.js} +3 -3
  258. package/dist/{shared-BcB-feC8.js → shared-DwNtcoQg.js} +1 -1
  259. package/dist/{skill-commands-BEWkEml_.js → skill-commands-BSRPnzXp.js} +5 -5
  260. package/dist/{skill-scanner-Bb5SMbCz.js → skill-scanner-rdr9cQew.js} +6 -6
  261. package/dist/{skills-Rnr7zPen.js → skills-BnBOxX1c.js} +3 -3
  262. package/dist/{skills-cli-BowIIIzH.js → skills-cli-CUgeYV1y.js} +5 -5
  263. package/dist/{skills-install-BLNCKuex.js → skills-install-BE04CZ6e.js} +6 -6
  264. package/dist/{skills-status-BCU-5otB.js → skills-status-DQw98BkG.js} +1 -1
  265. package/dist/{slash-commands.runtime-BgVgQ-Eh.js → slash-commands.runtime-BPtL2Qev.js} +11 -11
  266. package/dist/slash-dispatch.runtime-Cf9dq1k6.js +113 -0
  267. package/dist/{slash-skill-commands.runtime-4dOiU6U0.js → slash-skill-commands.runtime-9BYoANpd.js} +15 -15
  268. package/dist/{squidclaw-root-BcB7vo9M.js → squidclaw-root-CnE19yKj.js} +8 -8
  269. package/dist/{status-COTRBaam.js → status-rb5Jz-VU.js} +26 -26
  270. package/dist/{status.update-BUql4yz-.js → status.update-BNODJGA9.js} +2 -2
  271. package/dist/{store-aa15VM42.js → store-PDMRmC5Z.js} +5 -5
  272. package/dist/subagent-registry-runtime-CTiA365B.js +113 -0
  273. package/dist/{system-cli-CgCUbH_M.js → system-cli-DJYyTO07.js} +9 -9
  274. package/dist/{system-run-command-Ny1SbbOD.js → system-run-command-BgnCyvrj.js} +1 -1
  275. package/dist/{systemd-zbKl2Q3E.js → systemd-fP8tz4aL.js} +9 -9
  276. package/dist/{systemd-hints-C9_7ouv7.js → systemd-hints-BG_t__ZD.js} +6 -6
  277. package/dist/{systemd-linger-BM6JyzAr.js → systemd-linger-DgK8uuKY.js} +1 -1
  278. package/dist/{tables-jZMI8hLl.js → tables-DUSFF9-W.js} +1 -1
  279. package/dist/{tailnet-gTCqUBfJ.js → tailnet-CLkKVwDq.js} +1 -1
  280. package/dist/target-errors-BxwxgIDk.js +195 -0
  281. package/dist/{tool-images-DTl_LHMa.js → tool-images-FhSCiY-o.js} +1 -1
  282. package/dist/{tui-kDK-MAOc.js → tui-BMOMT-ma.js} +6 -6
  283. package/dist/{tui-cli-Dxy6fKkU.js → tui-cli-kqZ_-2Mz.js} +32 -32
  284. package/dist/{update-ZUTn6Jsu.js → update-blK9j2ag.js} +3 -3
  285. package/dist/{update-cli-D_QEiKyz.js → update-cli-gLKrP8UQ.js} +102 -102
  286. package/dist/{update-runner-DOXPSA_-.js → update-runner-C2UrF4oZ.js} +16 -16
  287. package/dist/web-VmjeceHS.js +117 -0
  288. package/dist/{webhooks-cli-D1iehjkP.js → webhooks-cli-BEXPBILa.js} +6 -6
  289. package/dist/{whatsapp-actions-hN5bwjWU.js → whatsapp-actions-DhiV181U.js} +17 -17
  290. package/dist/{with-timeout-BjaANd4G.js → with-timeout-BCAfkt03.js} +3 -3
  291. package/dist/{workspace-BITWyKG4.js → workspace-B-k5DNiQ.js} +1 -1
  292. package/dist/{workspace-dirs-B7O9BAHp.js → workspace-dirs-B2dNahfe.js} +1 -1
  293. package/dist/{ws-Bx8lpC1N.js → ws-DKt5HoA5.js} +2 -2
  294. package/dist/{wsl-wYxPJ8EO.js → wsl-CgxzAzRe.js} +2 -2
  295. package/package.json +1 -1
  296. package/dist/deliver-runtime-uwleBPlq.js +0 -61
  297. package/dist/deps-send-discord.runtime-CuZGpA7H.js +0 -36
  298. package/dist/deps-send-imessage.runtime-ByVW2alP.js +0 -35
  299. package/dist/deps-send-signal.runtime-Dl4GaCbQ.js +0 -34
  300. package/dist/deps-send-slack.runtime-BEV10FHj.js +0 -32
  301. package/dist/deps-send-whatsapp.runtime-Di0SEPNK.js +0 -118
  302. package/dist/image-runtime-B5Q4J9w2.js +0 -55
  303. package/dist/register.configure-CGptmTVZ.js +0 -164
  304. package/dist/runtime-whatsapp-outbound.runtime-uDhEmWe1.js +0 -32
  305. package/dist/slash-dispatch.runtime-D83FVeU7.js +0 -113
  306. package/dist/subagent-registry-runtime-CrT5RSO9.js +0 -113
  307. package/dist/web-DtSq_aUB.js +0 -117
@@ -0,0 +1,2587 @@
1
+ import { at as DEFAULT_ACCOUNT_ID, ot as normalizeAccountId } from "./run-with-concurrency-5DMu9szx.js";
2
+ import { c as resolveStateDir, d as resolveRequiredHomeDir, l as expandHomePrefix, o as resolveOAuthDir } from "./paths-8xF5kDne.js";
3
+ import { Ar as withFileLock$1, bn as resolveTelegramPreviewStreamMode, ei as isTruthyEnvValue, i as readConfigFileSnapshotForWrite, n as loadConfig, o as writeConfigFile } from "./config-FhBFLsNm.js";
4
+ import { D as safeParseJson, F as danger, L as logVerbose, a as createSubsystemLogger, f as CONFIG_DIR } from "./logger-DDdrdbDu.js";
5
+ import { h as normalizeMimeType, m as kindFromMime, p as isGifMedia, u as getFileExtension } from "./image-ops-DN17S88I.js";
6
+ import { C as resolveTelegramAccount, n as listChannelPlugins, t as getChannelPlugin } from "./plugins-6NxPd6TS.js";
7
+ import { r as writeJsonAtomic } from "./json-files-DJe5SBCd.js";
8
+ import { t as resolveFetch } from "./fetch-B_RcOnt9.js";
9
+ import { t as redactSensitiveText } from "./redact-BoNEjbpF.js";
10
+ import { i as formatUncaughtError, n as extractErrorCode, o as readErrorName, r as formatErrorMessage, t as collectErrorGraphCandidates } from "./errors-B8oJXuCF.js";
11
+ import { i as createTelegramRetryRunner, n as recordChannelActivity } from "./channel-activity-JjLRpUa_.js";
12
+ import { t as buildOutboundMediaLoadOptions } from "./load-options-DNSaiajj.js";
13
+ import { n as normalizePollInput } from "./polls-3WJMd-G-.js";
14
+ import { u as hasProxyEnvConfigured } from "./ssrf-B3XRWBsP.js";
15
+ import { i as resolveMarkdownTableMode, n as markdownToIR, t as chunkMarkdownIR, v as loadWebMedia } from "./ir-JaPZ0yKH.js";
16
+ import { t as renderMarkdownWithMarkers } from "./render-HmipMDlP.js";
17
+ import { n as normalizeTelegramLookupTarget, r as parseTelegramTarget, t as normalizeTelegramChatId } from "./targets-D46Aqz9j.js";
18
+ import { t as makeProxyFetch } from "./proxy-fetch-ZPEvp58f.js";
19
+ import fs, { readFileSync } from "node:fs";
20
+ import path from "node:path";
21
+ import os from "node:os";
22
+ import fs$1 from "node:fs/promises";
23
+ import crypto, { randomBytes } from "node:crypto";
24
+ import JSON5 from "json5";
25
+ import process$1 from "node:process";
26
+ import { EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
27
+ import * as dns from "node:dns";
28
+ import * as net$1 from "node:net";
29
+ import { Bot, HttpError, InputFile } from "grammy";
30
+
31
+ //#region src/channels/allow-from.ts
32
+ function mergeDmAllowFromSources(params) {
33
+ const storeEntries = params.dmPolicy === "allowlist" ? [] : params.storeAllowFrom ?? [];
34
+ return [...params.allowFrom ?? [], ...storeEntries].map((value) => String(value).trim()).filter(Boolean);
35
+ }
36
+ function resolveGroupAllowFromSources(params) {
37
+ const explicitGroupAllowFrom = Array.isArray(params.groupAllowFrom) && params.groupAllowFrom.length > 0 ? params.groupAllowFrom : void 0;
38
+ return (explicitGroupAllowFrom ? explicitGroupAllowFrom : params.fallbackToAllowFrom === false ? [] : params.allowFrom ?? []).map((value) => String(value).trim()).filter(Boolean);
39
+ }
40
+ function firstDefined(...values) {
41
+ for (const value of values) if (typeof value !== "undefined") return value;
42
+ }
43
+ function isSenderIdAllowed(allow, senderId, allowWhenEmpty) {
44
+ if (!allow.hasEntries) return allowWhenEmpty;
45
+ if (allow.hasWildcard) return true;
46
+ if (!senderId) return false;
47
+ return allow.entries.includes(senderId);
48
+ }
49
+
50
+ //#endregion
51
+ //#region src/channels/plugins/pairing.ts
52
+ function listPairingChannels() {
53
+ return listChannelPlugins().filter((plugin) => plugin.pairing).map((plugin) => plugin.id);
54
+ }
55
+ function getPairingAdapter(channelId) {
56
+ return getChannelPlugin(channelId)?.pairing ?? null;
57
+ }
58
+
59
+ //#endregion
60
+ //#region src/plugin-sdk/json-store.ts
61
+ async function readJsonFileWithFallback(filePath, fallback) {
62
+ try {
63
+ const parsed = safeParseJson(await fs.promises.readFile(filePath, "utf-8"));
64
+ if (parsed == null) return {
65
+ value: fallback,
66
+ exists: true
67
+ };
68
+ return {
69
+ value: parsed,
70
+ exists: true
71
+ };
72
+ } catch (err) {
73
+ if (err.code === "ENOENT") return {
74
+ value: fallback,
75
+ exists: false
76
+ };
77
+ return {
78
+ value: fallback,
79
+ exists: false
80
+ };
81
+ }
82
+ }
83
+ async function writeJsonFileAtomically(filePath, value) {
84
+ await writeJsonAtomic(filePath, value, {
85
+ mode: 384,
86
+ trailingNewline: true,
87
+ ensureDirMode: 448
88
+ });
89
+ }
90
+
91
+ //#endregion
92
+ //#region src/pairing/pairing-store.ts
93
+ const PAIRING_CODE_LENGTH = 8;
94
+ const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
95
+ const PAIRING_PENDING_TTL_MS = 3600 * 1e3;
96
+ const PAIRING_PENDING_MAX = 3;
97
+ const PAIRING_STORE_LOCK_OPTIONS = {
98
+ retries: {
99
+ retries: 10,
100
+ factor: 2,
101
+ minTimeout: 100,
102
+ maxTimeout: 1e4,
103
+ randomize: true
104
+ },
105
+ stale: 3e4
106
+ };
107
+ const allowFromReadCache = /* @__PURE__ */ new Map();
108
+ function resolveCredentialsDir(env = process.env) {
109
+ return resolveOAuthDir(env, resolveStateDir(env, () => resolveRequiredHomeDir(env, os.homedir)));
110
+ }
111
+ /** Sanitize channel ID for use in filenames (prevent path traversal). */
112
+ function safeChannelKey(channel) {
113
+ const raw = String(channel).trim().toLowerCase();
114
+ if (!raw) throw new Error("invalid pairing channel");
115
+ const safe = raw.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_");
116
+ if (!safe || safe === "_") throw new Error("invalid pairing channel");
117
+ return safe;
118
+ }
119
+ function resolvePairingPath(channel, env = process.env) {
120
+ return path.join(resolveCredentialsDir(env), `${safeChannelKey(channel)}-pairing.json`);
121
+ }
122
+ function safeAccountKey(accountId) {
123
+ const raw = String(accountId).trim().toLowerCase();
124
+ if (!raw) throw new Error("invalid pairing account id");
125
+ const safe = raw.replace(/[\\/:*?"<>|]/g, "_").replace(/\.\./g, "_");
126
+ if (!safe || safe === "_") throw new Error("invalid pairing account id");
127
+ return safe;
128
+ }
129
+ function resolveAllowFromPath(channel, env = process.env, accountId) {
130
+ const base = safeChannelKey(channel);
131
+ const normalizedAccountId = typeof accountId === "string" ? accountId.trim() : "";
132
+ if (!normalizedAccountId) return path.join(resolveCredentialsDir(env), `${base}-allowFrom.json`);
133
+ return path.join(resolveCredentialsDir(env), `${base}-${safeAccountKey(normalizedAccountId)}-allowFrom.json`);
134
+ }
135
+ async function readJsonFile(filePath, fallback) {
136
+ return await readJsonFileWithFallback(filePath, fallback);
137
+ }
138
+ async function writeJsonFile(filePath, value) {
139
+ await writeJsonFileAtomically(filePath, value);
140
+ }
141
+ async function readPairingRequests(filePath) {
142
+ const { value } = await readJsonFile(filePath, {
143
+ version: 1,
144
+ requests: []
145
+ });
146
+ return Array.isArray(value.requests) ? value.requests : [];
147
+ }
148
+ async function ensureJsonFile(filePath, fallback) {
149
+ try {
150
+ await fs.promises.access(filePath);
151
+ } catch {
152
+ await writeJsonFile(filePath, fallback);
153
+ }
154
+ }
155
+ async function withFileLock(filePath, fallback, fn) {
156
+ await ensureJsonFile(filePath, fallback);
157
+ return await withFileLock$1(filePath, PAIRING_STORE_LOCK_OPTIONS, async () => {
158
+ return await fn();
159
+ });
160
+ }
161
+ function parseTimestamp(value) {
162
+ if (!value) return null;
163
+ const parsed = Date.parse(value);
164
+ if (!Number.isFinite(parsed)) return null;
165
+ return parsed;
166
+ }
167
+ function isExpired(entry, nowMs) {
168
+ const createdAt = parseTimestamp(entry.createdAt);
169
+ if (!createdAt) return true;
170
+ return nowMs - createdAt > PAIRING_PENDING_TTL_MS;
171
+ }
172
+ function pruneExpiredRequests(reqs, nowMs) {
173
+ const kept = [];
174
+ let removed = false;
175
+ for (const req of reqs) {
176
+ if (isExpired(req, nowMs)) {
177
+ removed = true;
178
+ continue;
179
+ }
180
+ kept.push(req);
181
+ }
182
+ return {
183
+ requests: kept,
184
+ removed
185
+ };
186
+ }
187
+ function resolveLastSeenAt(entry) {
188
+ return parseTimestamp(entry.lastSeenAt) ?? parseTimestamp(entry.createdAt) ?? 0;
189
+ }
190
+ function pruneExcessRequests(reqs, maxPending) {
191
+ if (maxPending <= 0 || reqs.length <= maxPending) return {
192
+ requests: reqs,
193
+ removed: false
194
+ };
195
+ return {
196
+ requests: reqs.slice().toSorted((a, b) => resolveLastSeenAt(a) - resolveLastSeenAt(b)).slice(-maxPending),
197
+ removed: true
198
+ };
199
+ }
200
+ function randomCode() {
201
+ let out = "";
202
+ for (let i = 0; i < PAIRING_CODE_LENGTH; i++) {
203
+ const idx = crypto.randomInt(0, 32);
204
+ out += PAIRING_CODE_ALPHABET[idx];
205
+ }
206
+ return out;
207
+ }
208
+ function generateUniqueCode(existing) {
209
+ for (let attempt = 0; attempt < 500; attempt += 1) {
210
+ const code = randomCode();
211
+ if (!existing.has(code)) return code;
212
+ }
213
+ throw new Error("failed to generate unique pairing code");
214
+ }
215
+ function normalizePairingAccountId(accountId) {
216
+ return accountId?.trim().toLowerCase() || "";
217
+ }
218
+ function requestMatchesAccountId(entry, normalizedAccountId) {
219
+ if (!normalizedAccountId) return true;
220
+ return String(entry.meta?.accountId ?? "").trim().toLowerCase() === normalizedAccountId;
221
+ }
222
+ function shouldIncludeLegacyAllowFromEntries(normalizedAccountId) {
223
+ return !normalizedAccountId || normalizedAccountId === DEFAULT_ACCOUNT_ID;
224
+ }
225
+ function resolveAllowFromAccountId(accountId) {
226
+ return normalizePairingAccountId(accountId) || DEFAULT_ACCOUNT_ID;
227
+ }
228
+ function normalizeId(value) {
229
+ return String(value).trim();
230
+ }
231
+ function normalizeAllowEntry(channel, entry) {
232
+ const trimmed = entry.trim();
233
+ if (!trimmed) return "";
234
+ if (trimmed === "*") return "";
235
+ const adapter = getPairingAdapter(channel);
236
+ const normalized = adapter?.normalizeAllowEntry ? adapter.normalizeAllowEntry(trimmed) : trimmed;
237
+ return String(normalized).trim();
238
+ }
239
+ function normalizeAllowFromList(channel, store) {
240
+ return dedupePreserveOrder((Array.isArray(store.allowFrom) ? store.allowFrom : []).map((v) => normalizeAllowEntry(channel, String(v))).filter(Boolean));
241
+ }
242
+ function normalizeAllowFromInput(channel, entry) {
243
+ return normalizeAllowEntry(channel, normalizeId(entry));
244
+ }
245
+ function dedupePreserveOrder(entries) {
246
+ const seen = /* @__PURE__ */ new Set();
247
+ const out = [];
248
+ for (const entry of entries) {
249
+ const normalized = String(entry).trim();
250
+ if (!normalized || seen.has(normalized)) continue;
251
+ seen.add(normalized);
252
+ out.push(normalized);
253
+ }
254
+ return out;
255
+ }
256
+ async function readAllowFromStateForPath(channel, filePath) {
257
+ return (await readAllowFromStateForPathWithExists(channel, filePath)).entries;
258
+ }
259
+ function cloneAllowFromCacheEntry(entry) {
260
+ return {
261
+ exists: entry.exists,
262
+ mtimeMs: entry.mtimeMs,
263
+ size: entry.size,
264
+ entries: entry.entries.slice()
265
+ };
266
+ }
267
+ function setAllowFromReadCache(filePath, entry) {
268
+ allowFromReadCache.set(filePath, cloneAllowFromCacheEntry(entry));
269
+ }
270
+ function resolveAllowFromReadCacheHit(params) {
271
+ const cached = allowFromReadCache.get(params.filePath);
272
+ if (!cached) return null;
273
+ if (cached.exists !== params.exists) return null;
274
+ if (!params.exists) return cloneAllowFromCacheEntry(cached);
275
+ if (cached.mtimeMs !== params.mtimeMs || cached.size !== params.size) return null;
276
+ return cloneAllowFromCacheEntry(cached);
277
+ }
278
+ function resolveAllowFromReadCacheOrMissing(filePath, stat) {
279
+ const cached = resolveAllowFromReadCacheHit({
280
+ filePath,
281
+ exists: Boolean(stat),
282
+ mtimeMs: stat?.mtimeMs ?? null,
283
+ size: stat?.size ?? null
284
+ });
285
+ if (cached) return {
286
+ entries: cached.entries,
287
+ exists: cached.exists
288
+ };
289
+ if (!stat) {
290
+ setAllowFromReadCache(filePath, {
291
+ exists: false,
292
+ mtimeMs: null,
293
+ size: null,
294
+ entries: []
295
+ });
296
+ return {
297
+ entries: [],
298
+ exists: false
299
+ };
300
+ }
301
+ return null;
302
+ }
303
+ async function readAllowFromStateForPathWithExists(channel, filePath) {
304
+ let stat = null;
305
+ try {
306
+ stat = await fs.promises.stat(filePath);
307
+ } catch (err) {
308
+ if (err.code !== "ENOENT") throw err;
309
+ }
310
+ const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
311
+ if (cachedOrMissing) return cachedOrMissing;
312
+ if (!stat) return {
313
+ entries: [],
314
+ exists: false
315
+ };
316
+ const { value, exists } = await readJsonFile(filePath, {
317
+ version: 1,
318
+ allowFrom: []
319
+ });
320
+ const entries = normalizeAllowFromList(channel, value);
321
+ setAllowFromReadCache(filePath, {
322
+ exists,
323
+ mtimeMs: stat.mtimeMs,
324
+ size: stat.size,
325
+ entries
326
+ });
327
+ return {
328
+ entries,
329
+ exists
330
+ };
331
+ }
332
+ function readAllowFromStateForPathSync(channel, filePath) {
333
+ return readAllowFromStateForPathSyncWithExists(channel, filePath).entries;
334
+ }
335
+ function readAllowFromStateForPathSyncWithExists(channel, filePath) {
336
+ let stat = null;
337
+ try {
338
+ stat = fs.statSync(filePath);
339
+ } catch (err) {
340
+ if (err.code !== "ENOENT") return {
341
+ entries: [],
342
+ exists: false
343
+ };
344
+ }
345
+ const cachedOrMissing = resolveAllowFromReadCacheOrMissing(filePath, stat);
346
+ if (cachedOrMissing) return cachedOrMissing;
347
+ if (!stat) return {
348
+ entries: [],
349
+ exists: false
350
+ };
351
+ let raw = "";
352
+ try {
353
+ raw = fs.readFileSync(filePath, "utf8");
354
+ } catch (err) {
355
+ if (err.code === "ENOENT") return {
356
+ entries: [],
357
+ exists: false
358
+ };
359
+ return {
360
+ entries: [],
361
+ exists: false
362
+ };
363
+ }
364
+ try {
365
+ const entries = normalizeAllowFromList(channel, JSON.parse(raw));
366
+ setAllowFromReadCache(filePath, {
367
+ exists: true,
368
+ mtimeMs: stat.mtimeMs,
369
+ size: stat.size,
370
+ entries
371
+ });
372
+ return {
373
+ entries,
374
+ exists: true
375
+ };
376
+ } catch {
377
+ setAllowFromReadCache(filePath, {
378
+ exists: true,
379
+ mtimeMs: stat.mtimeMs,
380
+ size: stat.size,
381
+ entries: []
382
+ });
383
+ return {
384
+ entries: [],
385
+ exists: true
386
+ };
387
+ }
388
+ }
389
+ async function readAllowFromState(params) {
390
+ const { value } = await readJsonFile(params.filePath, {
391
+ version: 1,
392
+ allowFrom: []
393
+ });
394
+ return {
395
+ current: normalizeAllowFromList(params.channel, value),
396
+ normalized: normalizeAllowFromInput(params.channel, params.entry) || null
397
+ };
398
+ }
399
+ async function writeAllowFromState(filePath, allowFrom) {
400
+ await writeJsonFile(filePath, {
401
+ version: 1,
402
+ allowFrom
403
+ });
404
+ let stat = null;
405
+ try {
406
+ stat = await fs.promises.stat(filePath);
407
+ } catch {}
408
+ setAllowFromReadCache(filePath, {
409
+ exists: true,
410
+ mtimeMs: stat?.mtimeMs ?? null,
411
+ size: stat?.size ?? null,
412
+ entries: allowFrom.slice()
413
+ });
414
+ }
415
+ async function readNonDefaultAccountAllowFrom(params) {
416
+ const scopedPath = resolveAllowFromPath(params.channel, params.env, params.accountId);
417
+ return await readAllowFromStateForPath(params.channel, scopedPath);
418
+ }
419
+ function readNonDefaultAccountAllowFromSync(params) {
420
+ const scopedPath = resolveAllowFromPath(params.channel, params.env, params.accountId);
421
+ return readAllowFromStateForPathSync(params.channel, scopedPath);
422
+ }
423
+ async function updateAllowFromStoreEntry(params) {
424
+ const env = params.env ?? process.env;
425
+ const filePath = resolveAllowFromPath(params.channel, env, params.accountId);
426
+ return await withFileLock(filePath, {
427
+ version: 1,
428
+ allowFrom: []
429
+ }, async () => {
430
+ const { current, normalized } = await readAllowFromState({
431
+ channel: params.channel,
432
+ entry: params.entry,
433
+ filePath
434
+ });
435
+ if (!normalized) return {
436
+ changed: false,
437
+ allowFrom: current
438
+ };
439
+ const next = params.apply(current, normalized);
440
+ if (!next) return {
441
+ changed: false,
442
+ allowFrom: current
443
+ };
444
+ await writeAllowFromState(filePath, next);
445
+ return {
446
+ changed: true,
447
+ allowFrom: next
448
+ };
449
+ });
450
+ }
451
+ async function readChannelAllowFromStore(channel, env = process.env, accountId) {
452
+ const resolvedAccountId = resolveAllowFromAccountId(accountId);
453
+ if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) return await readNonDefaultAccountAllowFrom({
454
+ channel,
455
+ env,
456
+ accountId: resolvedAccountId
457
+ });
458
+ const scopedEntries = await readAllowFromStateForPath(channel, resolveAllowFromPath(channel, env, resolvedAccountId));
459
+ const legacyEntries = await readAllowFromStateForPath(channel, resolveAllowFromPath(channel, env));
460
+ return dedupePreserveOrder([...scopedEntries, ...legacyEntries]);
461
+ }
462
+ function readChannelAllowFromStoreSync(channel, env = process.env, accountId) {
463
+ const resolvedAccountId = resolveAllowFromAccountId(accountId);
464
+ if (!shouldIncludeLegacyAllowFromEntries(resolvedAccountId)) return readNonDefaultAccountAllowFromSync({
465
+ channel,
466
+ env,
467
+ accountId: resolvedAccountId
468
+ });
469
+ const scopedEntries = readAllowFromStateForPathSync(channel, resolveAllowFromPath(channel, env, resolvedAccountId));
470
+ const legacyEntries = readAllowFromStateForPathSync(channel, resolveAllowFromPath(channel, env));
471
+ return dedupePreserveOrder([...scopedEntries, ...legacyEntries]);
472
+ }
473
+ async function updateChannelAllowFromStore(params) {
474
+ return await updateAllowFromStoreEntry({
475
+ channel: params.channel,
476
+ entry: params.entry,
477
+ accountId: params.accountId,
478
+ env: params.env,
479
+ apply: params.apply
480
+ });
481
+ }
482
+ async function mutateChannelAllowFromStoreEntry(params, apply) {
483
+ return await updateChannelAllowFromStore({
484
+ ...params,
485
+ apply
486
+ });
487
+ }
488
+ async function addChannelAllowFromStoreEntry(params) {
489
+ return await mutateChannelAllowFromStoreEntry(params, (current, normalized) => {
490
+ if (current.includes(normalized)) return null;
491
+ return [...current, normalized];
492
+ });
493
+ }
494
+ async function removeChannelAllowFromStoreEntry(params) {
495
+ return await mutateChannelAllowFromStoreEntry(params, (current, normalized) => {
496
+ const next = current.filter((entry) => entry !== normalized);
497
+ if (next.length === current.length) return null;
498
+ return next;
499
+ });
500
+ }
501
+ async function upsertChannelPairingRequest(params) {
502
+ const env = params.env ?? process.env;
503
+ const filePath = resolvePairingPath(params.channel, env);
504
+ return await withFileLock(filePath, {
505
+ version: 1,
506
+ requests: []
507
+ }, async () => {
508
+ const now = (/* @__PURE__ */ new Date()).toISOString();
509
+ const nowMs = Date.now();
510
+ const id = normalizeId(params.id);
511
+ const normalizedAccountId = normalizePairingAccountId(params.accountId) || DEFAULT_ACCOUNT_ID;
512
+ const meta = {
513
+ ...params.meta && typeof params.meta === "object" ? Object.fromEntries(Object.entries(params.meta).map(([k, v]) => [k, String(v ?? "").trim()]).filter(([_, v]) => Boolean(v))) : void 0,
514
+ accountId: normalizedAccountId
515
+ };
516
+ let reqs = await readPairingRequests(filePath);
517
+ const { requests: prunedExpired, removed: expiredRemoved } = pruneExpiredRequests(reqs, nowMs);
518
+ reqs = prunedExpired;
519
+ const normalizedMatchingAccountId = normalizedAccountId;
520
+ const existingIdx = reqs.findIndex((r) => {
521
+ if (r.id !== id) return false;
522
+ return requestMatchesAccountId(r, normalizedMatchingAccountId);
523
+ });
524
+ const existingCodes = new Set(reqs.map((req) => String(req.code ?? "").trim().toUpperCase()));
525
+ if (existingIdx >= 0) {
526
+ const existing = reqs[existingIdx];
527
+ const code = (existing && typeof existing.code === "string" ? existing.code.trim() : "") || generateUniqueCode(existingCodes);
528
+ const next = {
529
+ id,
530
+ code,
531
+ createdAt: existing?.createdAt ?? now,
532
+ lastSeenAt: now,
533
+ meta: meta ?? existing?.meta
534
+ };
535
+ reqs[existingIdx] = next;
536
+ const { requests: capped } = pruneExcessRequests(reqs, PAIRING_PENDING_MAX);
537
+ await writeJsonFile(filePath, {
538
+ version: 1,
539
+ requests: capped
540
+ });
541
+ return {
542
+ code,
543
+ created: false
544
+ };
545
+ }
546
+ const { requests: capped, removed: cappedRemoved } = pruneExcessRequests(reqs, PAIRING_PENDING_MAX);
547
+ reqs = capped;
548
+ if (PAIRING_PENDING_MAX > 0 && reqs.length >= PAIRING_PENDING_MAX) {
549
+ if (expiredRemoved || cappedRemoved) await writeJsonFile(filePath, {
550
+ version: 1,
551
+ requests: reqs
552
+ });
553
+ return {
554
+ code: "",
555
+ created: false
556
+ };
557
+ }
558
+ const code = generateUniqueCode(existingCodes);
559
+ const next = {
560
+ id,
561
+ code,
562
+ createdAt: now,
563
+ lastSeenAt: now,
564
+ ...meta ? { meta } : {}
565
+ };
566
+ await writeJsonFile(filePath, {
567
+ version: 1,
568
+ requests: [...reqs, next]
569
+ });
570
+ return {
571
+ code,
572
+ created: true
573
+ };
574
+ });
575
+ }
576
+
577
+ //#endregion
578
+ //#region src/infra/wsl.ts
579
+ function isWSLEnv() {
580
+ if (process.env.WSL_INTEROP || process.env.WSL_DISTRO_NAME || process.env.WSLENV) return true;
581
+ return false;
582
+ }
583
+ /**
584
+ * Synchronously check if running in WSL.
585
+ * Checks env vars first, then /proc/version.
586
+ */
587
+ function isWSLSync() {
588
+ if (process.platform !== "linux") return false;
589
+ if (isWSLEnv()) return true;
590
+ try {
591
+ const release = readFileSync("/proc/version", "utf8").toLowerCase();
592
+ return release.includes("microsoft") || release.includes("wsl");
593
+ } catch {
594
+ return false;
595
+ }
596
+ }
597
+ /**
598
+ * Synchronously check if running in WSL2.
599
+ */
600
+ function isWSL2Sync() {
601
+ if (!isWSLSync()) return false;
602
+ try {
603
+ const version = readFileSync("/proc/version", "utf8").toLowerCase();
604
+ return version.includes("wsl2") || version.includes("microsoft-standard");
605
+ } catch {
606
+ return false;
607
+ }
608
+ }
609
+
610
+ //#endregion
611
+ //#region src/channels/location.ts
612
+ function resolveLocation(location) {
613
+ const source = location.source ?? (location.isLive ? "live" : location.name || location.address ? "place" : "pin");
614
+ const isLive = Boolean(location.isLive ?? source === "live");
615
+ return {
616
+ ...location,
617
+ source,
618
+ isLive
619
+ };
620
+ }
621
+ function formatAccuracy(accuracy) {
622
+ if (!Number.isFinite(accuracy)) return "";
623
+ return ` ±${Math.round(accuracy ?? 0)}m`;
624
+ }
625
+ function formatCoords(latitude, longitude) {
626
+ return `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`;
627
+ }
628
+ function formatLocationText(location) {
629
+ const resolved = resolveLocation(location);
630
+ const coords = formatCoords(resolved.latitude, resolved.longitude);
631
+ const accuracy = formatAccuracy(resolved.accuracy);
632
+ const caption = resolved.caption?.trim();
633
+ let header = "";
634
+ if (resolved.source === "live" || resolved.isLive) header = `🛰 Live location: ${coords}${accuracy}`;
635
+ else if (resolved.name || resolved.address) header = `📍 ${[resolved.name, resolved.address].filter(Boolean).join(" — ")} (${coords}${accuracy})`;
636
+ else header = `📍 ${coords}${accuracy}`;
637
+ return caption ? `${header}\n${caption}` : header;
638
+ }
639
+ function toLocationContext(location) {
640
+ const resolved = resolveLocation(location);
641
+ return {
642
+ LocationLat: resolved.latitude,
643
+ LocationLon: resolved.longitude,
644
+ LocationAccuracy: resolved.accuracy,
645
+ LocationName: resolved.name,
646
+ LocationAddress: resolved.address,
647
+ LocationSource: resolved.source,
648
+ LocationIsLive: resolved.isLive
649
+ };
650
+ }
651
+
652
+ //#endregion
653
+ //#region src/media/audio.ts
654
+ const TELEGRAM_VOICE_AUDIO_EXTENSIONS = new Set([
655
+ ".oga",
656
+ ".ogg",
657
+ ".opus",
658
+ ".mp3",
659
+ ".m4a"
660
+ ]);
661
+ /**
662
+ * MIME types compatible with voice messages.
663
+ * Telegram sendVoice supports OGG/Opus, MP3, and M4A.
664
+ * https://core.telegram.org/bots/api#sendvoice
665
+ */
666
+ const TELEGRAM_VOICE_MIME_TYPES = new Set([
667
+ "audio/ogg",
668
+ "audio/opus",
669
+ "audio/mpeg",
670
+ "audio/mp3",
671
+ "audio/mp4",
672
+ "audio/x-m4a",
673
+ "audio/m4a"
674
+ ]);
675
+ function isTelegramVoiceCompatibleAudio(opts) {
676
+ const mime = normalizeMimeType(opts.contentType);
677
+ if (mime && TELEGRAM_VOICE_MIME_TYPES.has(mime)) return true;
678
+ const fileName = opts.fileName?.trim();
679
+ if (!fileName) return false;
680
+ const ext = getFileExtension(fileName);
681
+ if (!ext) return false;
682
+ return TELEGRAM_VOICE_AUDIO_EXTENSIONS.has(ext);
683
+ }
684
+ /**
685
+ * Backward-compatible alias used across plugin/runtime call sites.
686
+ * Keeps existing behavior while making Telegram-specific policy explicit.
687
+ */
688
+ function isVoiceCompatibleAudio(opts) {
689
+ return isTelegramVoiceCompatibleAudio(opts);
690
+ }
691
+
692
+ //#endregion
693
+ //#region src/telegram/bot-access.ts
694
+ const warnedInvalidEntries = /* @__PURE__ */ new Set();
695
+ const log$1 = createSubsystemLogger("telegram/bot-access");
696
+ function warnInvalidAllowFromEntries(entries) {
697
+ if (process.env.VITEST || false) return;
698
+ for (const entry of entries) {
699
+ if (warnedInvalidEntries.has(entry)) continue;
700
+ warnedInvalidEntries.add(entry);
701
+ log$1.warn([
702
+ "Invalid allowFrom entry:",
703
+ JSON.stringify(entry),
704
+ "- allowFrom/groupAllowFrom authorization requires numeric Telegram sender IDs only.",
705
+ "If you had \"@username\" entries, re-run onboarding (it resolves @username to IDs) or replace them manually."
706
+ ].join(" "));
707
+ }
708
+ }
709
+ const normalizeAllowFrom = (list) => {
710
+ const entries = (list ?? []).map((value) => String(value).trim()).filter(Boolean);
711
+ const hasWildcard = entries.includes("*");
712
+ const normalized = entries.filter((value) => value !== "*").map((value) => value.replace(/^(telegram|tg):/i, ""));
713
+ const invalidEntries = normalized.filter((value) => !/^\d+$/.test(value));
714
+ if (invalidEntries.length > 0) warnInvalidAllowFromEntries([...new Set(invalidEntries)]);
715
+ return {
716
+ entries: normalized.filter((value) => /^\d+$/.test(value)),
717
+ hasWildcard,
718
+ hasEntries: entries.length > 0,
719
+ invalidEntries
720
+ };
721
+ };
722
+ const normalizeDmAllowFromWithStore = (params) => normalizeAllowFrom(mergeDmAllowFromSources(params));
723
+ const isSenderAllowed = (params) => {
724
+ const { allow, senderId } = params;
725
+ return isSenderIdAllowed(allow, senderId, true);
726
+ };
727
+ const resolveSenderAllowMatch = (params) => {
728
+ const { allow, senderId } = params;
729
+ if (allow.hasWildcard) return {
730
+ allowed: true,
731
+ matchKey: "*",
732
+ matchSource: "wildcard"
733
+ };
734
+ if (!allow.hasEntries) return { allowed: false };
735
+ if (senderId && allow.entries.includes(senderId)) return {
736
+ allowed: true,
737
+ matchKey: senderId,
738
+ matchSource: "id"
739
+ };
740
+ return { allowed: false };
741
+ };
742
+
743
+ //#endregion
744
+ //#region src/telegram/bot/helpers.ts
745
+ const TELEGRAM_GENERAL_TOPIC_ID = 1;
746
+ async function resolveTelegramGroupAllowFromContext(params) {
747
+ const accountId = normalizeAccountId(params.accountId);
748
+ const threadSpec = resolveTelegramThreadSpec({
749
+ isGroup: params.isGroup ?? false,
750
+ isForum: params.isForum,
751
+ messageThreadId: params.messageThreadId
752
+ });
753
+ const resolvedThreadId = threadSpec.scope === "forum" ? threadSpec.id : void 0;
754
+ const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : void 0;
755
+ const threadIdForConfig = resolvedThreadId ?? dmThreadId;
756
+ const storeAllowFrom = await readChannelAllowFromStore("telegram", process.env, accountId).catch(() => []);
757
+ const { groupConfig, topicConfig } = params.resolveTelegramGroupConfig(params.chatId, threadIdForConfig);
758
+ const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
759
+ return {
760
+ resolvedThreadId,
761
+ dmThreadId,
762
+ storeAllowFrom,
763
+ groupConfig,
764
+ topicConfig,
765
+ groupAllowOverride,
766
+ effectiveGroupAllow: normalizeAllowFrom(groupAllowOverride ?? params.groupAllowFrom),
767
+ hasGroupAllowOverride: typeof groupAllowOverride !== "undefined"
768
+ };
769
+ }
770
+ /**
771
+ * Resolve the thread ID for Telegram forum topics.
772
+ * For non-forum groups, returns undefined even if messageThreadId is present
773
+ * (reply threads in regular groups should not create separate sessions).
774
+ * For forum groups, returns the topic ID (or General topic ID=1 if unspecified).
775
+ */
776
+ function resolveTelegramForumThreadId(params) {
777
+ if (!params.isForum) return;
778
+ if (params.messageThreadId == null) return TELEGRAM_GENERAL_TOPIC_ID;
779
+ return params.messageThreadId;
780
+ }
781
+ function resolveTelegramThreadSpec(params) {
782
+ if (params.isGroup) return {
783
+ id: resolveTelegramForumThreadId({
784
+ isForum: params.isForum,
785
+ messageThreadId: params.messageThreadId
786
+ }),
787
+ scope: params.isForum ? "forum" : "none"
788
+ };
789
+ if (params.messageThreadId == null) return { scope: "dm" };
790
+ return {
791
+ id: params.messageThreadId,
792
+ scope: "dm"
793
+ };
794
+ }
795
+ /**
796
+ * Build thread params for Telegram API calls (messages, media).
797
+ *
798
+ * IMPORTANT: Thread IDs behave differently based on chat type:
799
+ * - DMs (private chats): Include message_thread_id when present (DM topics)
800
+ * - Forum topics: Skip thread_id=1 (General topic), include others
801
+ * - Regular groups: Thread IDs are ignored by Telegram
802
+ *
803
+ * General forum topic (id=1) must be treated like a regular supergroup send:
804
+ * Telegram rejects sendMessage/sendMedia with message_thread_id=1 ("thread not found").
805
+ *
806
+ * @param thread - Thread specification with ID and scope
807
+ * @returns API params object or undefined if thread_id should be omitted
808
+ */
809
+ function buildTelegramThreadParams(thread) {
810
+ if (thread?.id == null) return;
811
+ const normalized = Math.trunc(thread.id);
812
+ if (thread.scope === "dm") return normalized > 0 ? { message_thread_id: normalized } : void 0;
813
+ if (normalized === TELEGRAM_GENERAL_TOPIC_ID) return;
814
+ return { message_thread_id: normalized };
815
+ }
816
+ /**
817
+ * Build thread params for typing indicators (sendChatAction).
818
+ * Empirically, General topic (id=1) needs message_thread_id for typing to appear.
819
+ */
820
+ function buildTypingThreadParams(messageThreadId) {
821
+ if (messageThreadId == null) return;
822
+ return { message_thread_id: Math.trunc(messageThreadId) };
823
+ }
824
+ function resolveTelegramStreamMode(telegramCfg) {
825
+ return resolveTelegramPreviewStreamMode(telegramCfg);
826
+ }
827
+ function buildTelegramGroupPeerId(chatId, messageThreadId) {
828
+ return messageThreadId != null ? `${chatId}:topic:${messageThreadId}` : String(chatId);
829
+ }
830
+ /**
831
+ * Resolve the direct-message peer identifier for Telegram routing/session keys.
832
+ *
833
+ * In some Telegram DM deliveries (for example certain business/chat bridge flows),
834
+ * `chat.id` can differ from the actual sender user id. Prefer sender id when present
835
+ * so per-peer DM scopes isolate users correctly.
836
+ */
837
+ function resolveTelegramDirectPeerId(params) {
838
+ const senderId = params.senderId != null ? String(params.senderId).trim() : "";
839
+ if (senderId) return senderId;
840
+ return String(params.chatId);
841
+ }
842
+ function buildTelegramGroupFrom(chatId, messageThreadId) {
843
+ return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
844
+ }
845
+ /**
846
+ * Build parentPeer for forum topic binding inheritance.
847
+ * When a message comes from a forum topic, the peer ID includes the topic suffix
848
+ * (e.g., `-1001234567890:topic:99`). To allow bindings configured for the base
849
+ * group ID to match, we provide the parent group as `parentPeer` so the routing
850
+ * layer can fall back to it when the exact peer doesn't match.
851
+ */
852
+ function buildTelegramParentPeer(params) {
853
+ if (!params.isGroup || params.resolvedThreadId == null) return;
854
+ return {
855
+ kind: "group",
856
+ id: String(params.chatId)
857
+ };
858
+ }
859
+ function buildSenderName(msg) {
860
+ return [msg.from?.first_name, msg.from?.last_name].filter(Boolean).join(" ").trim() || msg.from?.username || void 0;
861
+ }
862
+ function resolveTelegramMediaPlaceholder(msg) {
863
+ if (!msg) return;
864
+ if (msg.photo) return "<media:image>";
865
+ if (msg.video || msg.video_note) return "<media:video>";
866
+ if (msg.audio || msg.voice) return "<media:audio>";
867
+ if (msg.document) return "<media:document>";
868
+ if (msg.sticker) return "<media:sticker>";
869
+ }
870
+ function buildSenderLabel(msg, senderId) {
871
+ const name = buildSenderName(msg);
872
+ const username = msg.from?.username ? `@${msg.from.username}` : void 0;
873
+ let label = name;
874
+ if (name && username) label = `${name} (${username})`;
875
+ else if (!name && username) label = username;
876
+ const fallbackId = (senderId != null && `${senderId}`.trim() ? `${senderId}`.trim() : void 0) ?? (msg.from?.id != null ? String(msg.from.id) : void 0);
877
+ const idPart = fallbackId ? `id:${fallbackId}` : void 0;
878
+ if (label && idPart) return `${label} ${idPart}`;
879
+ if (label) return label;
880
+ return idPart ?? "id:unknown";
881
+ }
882
+ function buildGroupLabel(msg, chatId, messageThreadId) {
883
+ const title = msg.chat?.title;
884
+ const topicSuffix = messageThreadId != null ? ` topic:${messageThreadId}` : "";
885
+ if (title) return `${title} id:${chatId}${topicSuffix}`;
886
+ return `group:${chatId}${topicSuffix}`;
887
+ }
888
+ function hasBotMention(msg, botUsername) {
889
+ if ((msg.text ?? msg.caption ?? "").toLowerCase().includes(`@${botUsername}`)) return true;
890
+ const entities = msg.entities ?? msg.caption_entities ?? [];
891
+ for (const ent of entities) {
892
+ if (ent.type !== "mention") continue;
893
+ if ((msg.text ?? msg.caption ?? "").slice(ent.offset, ent.offset + ent.length).toLowerCase() === `@${botUsername}`) return true;
894
+ }
895
+ return false;
896
+ }
897
+ function expandTextLinks(text, entities) {
898
+ if (!text || !entities?.length) return text;
899
+ const textLinks = entities.filter((entity) => entity.type === "text_link" && Boolean(entity.url)).toSorted((a, b) => b.offset - a.offset);
900
+ if (textLinks.length === 0) return text;
901
+ let result = text;
902
+ for (const entity of textLinks) {
903
+ const markdown = `[${text.slice(entity.offset, entity.offset + entity.length)}](${entity.url})`;
904
+ result = result.slice(0, entity.offset) + markdown + result.slice(entity.offset + entity.length);
905
+ }
906
+ return result;
907
+ }
908
+ function resolveTelegramReplyId(raw) {
909
+ if (!raw) return;
910
+ const parsed = Number(raw);
911
+ if (!Number.isFinite(parsed)) return;
912
+ return parsed;
913
+ }
914
+ function describeReplyTarget(msg) {
915
+ const reply = msg.reply_to_message;
916
+ const externalReply = msg.external_reply;
917
+ const quoteText = msg.quote?.text ?? externalReply?.quote?.text;
918
+ let body = "";
919
+ let kind = "reply";
920
+ if (typeof quoteText === "string") {
921
+ body = quoteText.trim();
922
+ if (body) kind = "quote";
923
+ }
924
+ const replyLike = reply ?? externalReply;
925
+ if (!body && replyLike) {
926
+ body = (replyLike.text ?? replyLike.caption ?? "").trim();
927
+ if (!body) {
928
+ body = resolveTelegramMediaPlaceholder(replyLike) ?? "";
929
+ if (!body) {
930
+ const locationData = extractTelegramLocation(replyLike);
931
+ if (locationData) body = formatLocationText(locationData);
932
+ }
933
+ }
934
+ }
935
+ if (!body) return null;
936
+ const senderLabel = (replyLike ? buildSenderName(replyLike) : void 0) ?? "unknown sender";
937
+ const forwardedFrom = replyLike?.forward_origin ? resolveForwardOrigin(replyLike.forward_origin) ?? void 0 : void 0;
938
+ return {
939
+ id: replyLike?.message_id ? String(replyLike.message_id) : void 0,
940
+ sender: senderLabel,
941
+ body,
942
+ kind,
943
+ forwardedFrom
944
+ };
945
+ }
946
+ function normalizeForwardedUserLabel(user) {
947
+ const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
948
+ const username = user.username?.trim() || void 0;
949
+ const id = String(user.id);
950
+ return {
951
+ display: (name && username ? `${name} (@${username})` : name || (username ? `@${username}` : void 0)) || `user:${id}`,
952
+ name: name || void 0,
953
+ username,
954
+ id
955
+ };
956
+ }
957
+ function normalizeForwardedChatLabel(chat, fallbackKind) {
958
+ const title = chat.title?.trim() || void 0;
959
+ const username = chat.username?.trim() || void 0;
960
+ const id = String(chat.id);
961
+ return {
962
+ display: title || (username ? `@${username}` : void 0) || `${fallbackKind}:${id}`,
963
+ title,
964
+ username,
965
+ id
966
+ };
967
+ }
968
+ function buildForwardedContextFromUser(params) {
969
+ const { display, name, username, id } = normalizeForwardedUserLabel(params.user);
970
+ if (!display) return null;
971
+ return {
972
+ from: display,
973
+ date: params.date,
974
+ fromType: params.type,
975
+ fromId: id,
976
+ fromUsername: username,
977
+ fromTitle: name
978
+ };
979
+ }
980
+ function buildForwardedContextFromHiddenName(params) {
981
+ const trimmed = params.name?.trim();
982
+ if (!trimmed) return null;
983
+ return {
984
+ from: trimmed,
985
+ date: params.date,
986
+ fromType: params.type,
987
+ fromTitle: trimmed
988
+ };
989
+ }
990
+ function buildForwardedContextFromChat(params) {
991
+ const fallbackKind = params.type === "channel" ? "channel" : "chat";
992
+ const { display, title, username, id } = normalizeForwardedChatLabel(params.chat, fallbackKind);
993
+ if (!display) return null;
994
+ const signature = params.signature?.trim() || void 0;
995
+ const from = signature ? `${display} (${signature})` : display;
996
+ const chatType = params.chat.type?.trim() || void 0;
997
+ return {
998
+ from,
999
+ date: params.date,
1000
+ fromType: params.type,
1001
+ fromId: id,
1002
+ fromUsername: username,
1003
+ fromTitle: title,
1004
+ fromSignature: signature,
1005
+ fromChatType: chatType,
1006
+ fromMessageId: params.messageId
1007
+ };
1008
+ }
1009
+ function resolveForwardOrigin(origin) {
1010
+ switch (origin.type) {
1011
+ case "user": return buildForwardedContextFromUser({
1012
+ user: origin.sender_user,
1013
+ date: origin.date,
1014
+ type: "user"
1015
+ });
1016
+ case "hidden_user": return buildForwardedContextFromHiddenName({
1017
+ name: origin.sender_user_name,
1018
+ date: origin.date,
1019
+ type: "hidden_user"
1020
+ });
1021
+ case "chat": return buildForwardedContextFromChat({
1022
+ chat: origin.sender_chat,
1023
+ date: origin.date,
1024
+ type: "chat",
1025
+ signature: origin.author_signature
1026
+ });
1027
+ case "channel": return buildForwardedContextFromChat({
1028
+ chat: origin.chat,
1029
+ date: origin.date,
1030
+ type: "channel",
1031
+ signature: origin.author_signature,
1032
+ messageId: origin.message_id
1033
+ });
1034
+ default: return null;
1035
+ }
1036
+ }
1037
+ /** Extract forwarded message origin info from Telegram message. */
1038
+ function normalizeForwardedContext(msg) {
1039
+ if (!msg.forward_origin) return null;
1040
+ return resolveForwardOrigin(msg.forward_origin);
1041
+ }
1042
+ function extractTelegramLocation(msg) {
1043
+ const { venue, location } = msg;
1044
+ if (venue) return {
1045
+ latitude: venue.location.latitude,
1046
+ longitude: venue.location.longitude,
1047
+ accuracy: venue.location.horizontal_accuracy,
1048
+ name: venue.title,
1049
+ address: venue.address,
1050
+ source: "place",
1051
+ isLive: false
1052
+ };
1053
+ if (location) {
1054
+ const isLive = typeof location.live_period === "number" && location.live_period > 0;
1055
+ return {
1056
+ latitude: location.latitude,
1057
+ longitude: location.longitude,
1058
+ accuracy: location.horizontal_accuracy,
1059
+ source: isLive ? "live" : "pin",
1060
+ isLive
1061
+ };
1062
+ }
1063
+ return null;
1064
+ }
1065
+
1066
+ //#endregion
1067
+ //#region src/infra/diagnostic-flags.ts
1068
+ const DIAGNOSTICS_ENV = "SQUIDCLAW_DIAGNOSTICS";
1069
+ function normalizeFlag(value) {
1070
+ return value.trim().toLowerCase();
1071
+ }
1072
+ function parseEnvFlags(raw) {
1073
+ if (!raw) return [];
1074
+ const trimmed = raw.trim();
1075
+ if (!trimmed) return [];
1076
+ const lowered = trimmed.toLowerCase();
1077
+ if ([
1078
+ "0",
1079
+ "false",
1080
+ "off",
1081
+ "none"
1082
+ ].includes(lowered)) return [];
1083
+ if ([
1084
+ "1",
1085
+ "true",
1086
+ "all",
1087
+ "*"
1088
+ ].includes(lowered)) return ["*"];
1089
+ return trimmed.split(/[,\s]+/).map(normalizeFlag).filter(Boolean);
1090
+ }
1091
+ function uniqueFlags(flags) {
1092
+ const seen = /* @__PURE__ */ new Set();
1093
+ const out = [];
1094
+ for (const flag of flags) {
1095
+ const normalized = normalizeFlag(flag);
1096
+ if (!normalized || seen.has(normalized)) continue;
1097
+ seen.add(normalized);
1098
+ out.push(normalized);
1099
+ }
1100
+ return out;
1101
+ }
1102
+ function resolveDiagnosticFlags(cfg, env = process.env) {
1103
+ const configFlags = Array.isArray(cfg?.diagnostics?.flags) ? cfg?.diagnostics?.flags : [];
1104
+ const envFlags = parseEnvFlags(env[DIAGNOSTICS_ENV]);
1105
+ return uniqueFlags([...configFlags, ...envFlags]);
1106
+ }
1107
+ function matchesDiagnosticFlag(flag, enabledFlags) {
1108
+ const target = normalizeFlag(flag);
1109
+ if (!target) return false;
1110
+ for (const raw of enabledFlags) {
1111
+ const enabled = normalizeFlag(raw);
1112
+ if (!enabled) continue;
1113
+ if (enabled === "*" || enabled === "all") return true;
1114
+ if (enabled.endsWith(".*")) {
1115
+ const prefix = enabled.slice(0, -2);
1116
+ if (target === prefix || target.startsWith(`${prefix}.`)) return true;
1117
+ }
1118
+ if (enabled.endsWith("*")) {
1119
+ const prefix = enabled.slice(0, -1);
1120
+ if (target.startsWith(prefix)) return true;
1121
+ }
1122
+ if (enabled === target) return true;
1123
+ }
1124
+ return false;
1125
+ }
1126
+ function isDiagnosticFlagEnabled(flag, cfg, env = process.env) {
1127
+ return matchesDiagnosticFlag(flag, resolveDiagnosticFlags(cfg, env));
1128
+ }
1129
+
1130
+ //#endregion
1131
+ //#region src/telegram/api-logging.ts
1132
+ const fallbackLogger = createSubsystemLogger("telegram/api");
1133
+ function resolveTelegramApiLogger(runtime, logger) {
1134
+ if (logger) return logger;
1135
+ if (runtime?.error) return runtime.error;
1136
+ return (message) => fallbackLogger.error(message);
1137
+ }
1138
+ async function withTelegramApiErrorLogging({ operation, fn, runtime, logger, shouldLog }) {
1139
+ try {
1140
+ return await fn();
1141
+ } catch (err) {
1142
+ if (!shouldLog || shouldLog(err)) {
1143
+ const errText = formatErrorMessage(err);
1144
+ resolveTelegramApiLogger(runtime, logger)(danger(`telegram ${operation} failed: ${errText}`));
1145
+ }
1146
+ throw err;
1147
+ }
1148
+ }
1149
+
1150
+ //#endregion
1151
+ //#region src/telegram/caption.ts
1152
+ const TELEGRAM_MAX_CAPTION_LENGTH = 1024;
1153
+ function splitTelegramCaption(text) {
1154
+ const trimmed = text?.trim() ?? "";
1155
+ if (!trimmed) return {
1156
+ caption: void 0,
1157
+ followUpText: void 0
1158
+ };
1159
+ if (trimmed.length > TELEGRAM_MAX_CAPTION_LENGTH) return {
1160
+ caption: void 0,
1161
+ followUpText: trimmed
1162
+ };
1163
+ return {
1164
+ caption: trimmed,
1165
+ followUpText: void 0
1166
+ };
1167
+ }
1168
+
1169
+ //#endregion
1170
+ //#region src/telegram/network-config.ts
1171
+ const TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV = "SQUIDCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY";
1172
+ const TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV = "SQUIDCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY";
1173
+ const TELEGRAM_DNS_RESULT_ORDER_ENV = "SQUIDCLAW_TELEGRAM_DNS_RESULT_ORDER";
1174
+ let wsl2SyncCache;
1175
+ function isWSL2SyncCached() {
1176
+ if (typeof wsl2SyncCache === "boolean") return wsl2SyncCache;
1177
+ wsl2SyncCache = isWSL2Sync();
1178
+ return wsl2SyncCache;
1179
+ }
1180
+ function resolveTelegramAutoSelectFamilyDecision(params) {
1181
+ const env = params?.env ?? process$1.env;
1182
+ const nodeMajor = typeof params?.nodeMajor === "number" ? params.nodeMajor : Number(process$1.versions.node.split(".")[0]);
1183
+ if (isTruthyEnvValue(env[TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV])) return {
1184
+ value: true,
1185
+ source: `env:${TELEGRAM_ENABLE_AUTO_SELECT_FAMILY_ENV}`
1186
+ };
1187
+ if (isTruthyEnvValue(env[TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV])) return {
1188
+ value: false,
1189
+ source: `env:${TELEGRAM_DISABLE_AUTO_SELECT_FAMILY_ENV}`
1190
+ };
1191
+ if (typeof params?.network?.autoSelectFamily === "boolean") return {
1192
+ value: params.network.autoSelectFamily,
1193
+ source: "config"
1194
+ };
1195
+ if (isWSL2SyncCached()) return {
1196
+ value: false,
1197
+ source: "default-wsl2"
1198
+ };
1199
+ if (Number.isFinite(nodeMajor) && nodeMajor >= 22) return {
1200
+ value: true,
1201
+ source: "default-node22"
1202
+ };
1203
+ return { value: null };
1204
+ }
1205
+ /**
1206
+ * Resolve DNS result order setting for Telegram network requests.
1207
+ * Some networks/ISPs have issues with IPv6 causing fetch failures.
1208
+ * Setting "ipv4first" prioritizes IPv4 addresses in DNS resolution.
1209
+ *
1210
+ * Priority:
1211
+ * 1. Environment variable SQUIDCLAW_TELEGRAM_DNS_RESULT_ORDER
1212
+ * 2. Config: channels.telegram.network.dnsResultOrder
1213
+ * 3. Default: "ipv4first" on Node 22+ (to work around common IPv6 issues)
1214
+ */
1215
+ function resolveTelegramDnsResultOrderDecision(params) {
1216
+ const env = params?.env ?? process$1.env;
1217
+ const nodeMajor = typeof params?.nodeMajor === "number" ? params.nodeMajor : Number(process$1.versions.node.split(".")[0]);
1218
+ const envValue = env[TELEGRAM_DNS_RESULT_ORDER_ENV]?.trim().toLowerCase();
1219
+ if (envValue === "ipv4first" || envValue === "verbatim") return {
1220
+ value: envValue,
1221
+ source: `env:${TELEGRAM_DNS_RESULT_ORDER_ENV}`
1222
+ };
1223
+ const configValue = (params?.network)?.dnsResultOrder?.trim().toLowerCase();
1224
+ if (configValue === "ipv4first" || configValue === "verbatim") return {
1225
+ value: configValue,
1226
+ source: "config"
1227
+ };
1228
+ if (Number.isFinite(nodeMajor) && nodeMajor >= 22) return {
1229
+ value: "ipv4first",
1230
+ source: "default-node22"
1231
+ };
1232
+ return { value: null };
1233
+ }
1234
+
1235
+ //#endregion
1236
+ //#region src/telegram/fetch.ts
1237
+ let appliedAutoSelectFamily = null;
1238
+ let appliedDnsResultOrder = null;
1239
+ let appliedGlobalDispatcherAutoSelectFamily = null;
1240
+ const log = createSubsystemLogger("telegram/network");
1241
+ function isProxyLikeDispatcher(dispatcher) {
1242
+ const ctorName = dispatcher?.constructor?.name;
1243
+ return typeof ctorName === "string" && ctorName.includes("ProxyAgent");
1244
+ }
1245
+ const FALLBACK_RETRY_ERROR_CODES = [
1246
+ "ETIMEDOUT",
1247
+ "ENETUNREACH",
1248
+ "EHOSTUNREACH",
1249
+ "UND_ERR_CONNECT_TIMEOUT",
1250
+ "UND_ERR_SOCKET"
1251
+ ];
1252
+ const IPV4_FALLBACK_RULES = [{
1253
+ name: "fetch-failed-envelope",
1254
+ matches: ({ message }) => message.includes("fetch failed")
1255
+ }, {
1256
+ name: "known-network-code",
1257
+ matches: ({ codes }) => FALLBACK_RETRY_ERROR_CODES.some((code) => codes.has(code))
1258
+ }];
1259
+ function applyTelegramNetworkWorkarounds(network) {
1260
+ const autoSelectDecision = resolveTelegramAutoSelectFamilyDecision({ network });
1261
+ if (autoSelectDecision.value !== null && autoSelectDecision.value !== appliedAutoSelectFamily) {
1262
+ if (typeof net$1.setDefaultAutoSelectFamily === "function") try {
1263
+ net$1.setDefaultAutoSelectFamily(autoSelectDecision.value);
1264
+ appliedAutoSelectFamily = autoSelectDecision.value;
1265
+ const label = autoSelectDecision.source ? ` (${autoSelectDecision.source})` : "";
1266
+ log.info(`autoSelectFamily=${autoSelectDecision.value}${label}`);
1267
+ } catch {}
1268
+ }
1269
+ if (autoSelectDecision.value !== null && autoSelectDecision.value !== appliedGlobalDispatcherAutoSelectFamily) {
1270
+ if (!(isProxyLikeDispatcher(getGlobalDispatcher()) && !hasProxyEnvConfigured())) try {
1271
+ setGlobalDispatcher(new EnvHttpProxyAgent({ connect: {
1272
+ autoSelectFamily: autoSelectDecision.value,
1273
+ autoSelectFamilyAttemptTimeout: 300
1274
+ } }));
1275
+ appliedGlobalDispatcherAutoSelectFamily = autoSelectDecision.value;
1276
+ log.info(`global undici dispatcher autoSelectFamily=${autoSelectDecision.value}`);
1277
+ } catch {}
1278
+ }
1279
+ const dnsDecision = resolveTelegramDnsResultOrderDecision({ network });
1280
+ if (dnsDecision.value !== null && dnsDecision.value !== appliedDnsResultOrder) {
1281
+ if (typeof dns.setDefaultResultOrder === "function") try {
1282
+ dns.setDefaultResultOrder(dnsDecision.value);
1283
+ appliedDnsResultOrder = dnsDecision.value;
1284
+ const label = dnsDecision.source ? ` (${dnsDecision.source})` : "";
1285
+ log.info(`dnsResultOrder=${dnsDecision.value}${label}`);
1286
+ } catch {}
1287
+ }
1288
+ }
1289
+ function collectErrorCodes(err) {
1290
+ const codes = /* @__PURE__ */ new Set();
1291
+ const queue = [err];
1292
+ const seen = /* @__PURE__ */ new Set();
1293
+ while (queue.length > 0) {
1294
+ const current = queue.shift();
1295
+ if (!current || seen.has(current)) continue;
1296
+ seen.add(current);
1297
+ if (typeof current === "object") {
1298
+ const code = current.code;
1299
+ if (typeof code === "string" && code.trim()) codes.add(code.trim().toUpperCase());
1300
+ const cause = current.cause;
1301
+ if (cause && !seen.has(cause)) queue.push(cause);
1302
+ const errors = current.errors;
1303
+ if (Array.isArray(errors)) {
1304
+ for (const nested of errors) if (nested && !seen.has(nested)) queue.push(nested);
1305
+ }
1306
+ }
1307
+ }
1308
+ return codes;
1309
+ }
1310
+ function shouldRetryWithIpv4Fallback(err) {
1311
+ const ctx = {
1312
+ message: err && typeof err === "object" && "message" in err ? String(err.message).toLowerCase() : "",
1313
+ codes: collectErrorCodes(err)
1314
+ };
1315
+ for (const rule of IPV4_FALLBACK_RULES) if (!rule.matches(ctx)) return false;
1316
+ return true;
1317
+ }
1318
+ function applyTelegramIpv4Fallback() {
1319
+ applyTelegramNetworkWorkarounds({
1320
+ autoSelectFamily: false,
1321
+ dnsResultOrder: "ipv4first"
1322
+ });
1323
+ log.warn("fetch fallback: forcing autoSelectFamily=false + dnsResultOrder=ipv4first");
1324
+ }
1325
+ function resolveTelegramFetch(proxyFetch, options) {
1326
+ applyTelegramNetworkWorkarounds(options?.network);
1327
+ const sourceFetch = proxyFetch ? resolveFetch(proxyFetch) : resolveFetch();
1328
+ if (!sourceFetch) throw new Error("fetch is not available; set channels.telegram.proxy in config");
1329
+ if (proxyFetch) return sourceFetch;
1330
+ return (async (input, init) => {
1331
+ try {
1332
+ return await sourceFetch(input, init);
1333
+ } catch (err) {
1334
+ if (shouldRetryWithIpv4Fallback(err)) {
1335
+ applyTelegramIpv4Fallback();
1336
+ return sourceFetch(input, init);
1337
+ }
1338
+ throw err;
1339
+ }
1340
+ });
1341
+ }
1342
+
1343
+ //#endregion
1344
+ //#region src/telegram/format.ts
1345
+ function escapeHtml(text) {
1346
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1347
+ }
1348
+ function escapeHtmlAttr(text) {
1349
+ return escapeHtml(text).replace(/"/g, "&quot;");
1350
+ }
1351
+ /**
1352
+ * File extensions that share TLDs and commonly appear in code/documentation.
1353
+ * These are wrapped in <code> tags to prevent Telegram from generating
1354
+ * spurious domain registrar previews.
1355
+ *
1356
+ * Only includes extensions that are:
1357
+ * 1. Commonly used as file extensions in code/docs
1358
+ * 2. Rarely used as intentional domain references
1359
+ *
1360
+ * Excluded: .ai, .io, .tv, .fm (popular domain TLDs like x.ai, vercel.io, github.io)
1361
+ */
1362
+ const FILE_EXTENSIONS_WITH_TLD = new Set([
1363
+ "md",
1364
+ "go",
1365
+ "py",
1366
+ "pl",
1367
+ "sh",
1368
+ "am",
1369
+ "at",
1370
+ "be",
1371
+ "cc"
1372
+ ]);
1373
+ /** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */
1374
+ function isAutoLinkedFileRef(href, label) {
1375
+ if (href.replace(/^https?:\/\//i, "") !== label) return false;
1376
+ const dotIndex = label.lastIndexOf(".");
1377
+ if (dotIndex < 1) return false;
1378
+ const ext = label.slice(dotIndex + 1).toLowerCase();
1379
+ if (!FILE_EXTENSIONS_WITH_TLD.has(ext)) return false;
1380
+ const segments = label.split("/");
1381
+ if (segments.length > 1) {
1382
+ for (let i = 0; i < segments.length - 1; i++) if (segments[i].includes(".")) return false;
1383
+ }
1384
+ return true;
1385
+ }
1386
+ function buildTelegramLink(link, text) {
1387
+ const href = link.href.trim();
1388
+ if (!href) return null;
1389
+ if (link.start === link.end) return null;
1390
+ if (isAutoLinkedFileRef(href, text.slice(link.start, link.end))) return null;
1391
+ const safeHref = escapeHtmlAttr(href);
1392
+ return {
1393
+ start: link.start,
1394
+ end: link.end,
1395
+ open: `<a href="${safeHref}">`,
1396
+ close: "</a>"
1397
+ };
1398
+ }
1399
+ function renderTelegramHtml(ir) {
1400
+ return renderMarkdownWithMarkers(ir, {
1401
+ styleMarkers: {
1402
+ bold: {
1403
+ open: "<b>",
1404
+ close: "</b>"
1405
+ },
1406
+ italic: {
1407
+ open: "<i>",
1408
+ close: "</i>"
1409
+ },
1410
+ strikethrough: {
1411
+ open: "<s>",
1412
+ close: "</s>"
1413
+ },
1414
+ code: {
1415
+ open: "<code>",
1416
+ close: "</code>"
1417
+ },
1418
+ code_block: {
1419
+ open: "<pre><code>",
1420
+ close: "</code></pre>"
1421
+ },
1422
+ spoiler: {
1423
+ open: "<tg-spoiler>",
1424
+ close: "</tg-spoiler>"
1425
+ },
1426
+ blockquote: {
1427
+ open: "<blockquote>",
1428
+ close: "</blockquote>"
1429
+ }
1430
+ },
1431
+ escapeText: escapeHtml,
1432
+ buildLink: buildTelegramLink
1433
+ });
1434
+ }
1435
+ function markdownToTelegramHtml(markdown, options = {}) {
1436
+ const html = renderTelegramHtml(markdownToIR(markdown ?? "", {
1437
+ linkify: true,
1438
+ enableSpoilers: true,
1439
+ headingStyle: "none",
1440
+ blockquotePrefix: "",
1441
+ tableMode: options.tableMode
1442
+ }));
1443
+ if (options.wrapFileRefs !== false) return wrapFileReferencesInHtml(html);
1444
+ return html;
1445
+ }
1446
+ /**
1447
+ * Wraps standalone file references (with TLD extensions) in <code> tags.
1448
+ * This prevents Telegram from treating them as URLs and generating
1449
+ * irrelevant domain registrar previews.
1450
+ *
1451
+ * Runs AFTER markdown→HTML conversion to avoid modifying HTML attributes.
1452
+ * Skips content inside <code>, <pre>, and <a> tags to avoid nesting issues.
1453
+ */
1454
+ /** Escape regex metacharacters in a string */
1455
+ function escapeRegex(str) {
1456
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1457
+ }
1458
+ const FILE_EXTENSIONS_PATTERN = Array.from(FILE_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
1459
+ const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
1460
+ const FILE_REFERENCE_PATTERN = new RegExp(`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=$|[^a-zA-Z0-9_\\-/])`, "gi");
1461
+ const ORPHANED_TLD_PATTERN = new RegExp(`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${FILE_EXTENSIONS_PATTERN}))(?=[^a-zA-Z0-9/]|$)`, "g");
1462
+ const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
1463
+ function wrapStandaloneFileRef(match, prefix, filename) {
1464
+ if (filename.startsWith("//")) return match;
1465
+ if (/https?:\/\/$/i.test(prefix)) return match;
1466
+ return `${prefix}<code>${escapeHtml(filename)}</code>`;
1467
+ }
1468
+ function wrapSegmentFileRefs(text, codeDepth, preDepth, anchorDepth) {
1469
+ if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) return text;
1470
+ return text.replace(FILE_REFERENCE_PATTERN, wrapStandaloneFileRef).replace(ORPHANED_TLD_PATTERN, (match, prefix, tld) => prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`);
1471
+ }
1472
+ function wrapFileReferencesInHtml(html) {
1473
+ AUTO_LINKED_ANCHOR_PATTERN.lastIndex = 0;
1474
+ const deLinkified = html.replace(AUTO_LINKED_ANCHOR_PATTERN, (_match, label) => {
1475
+ if (!isAutoLinkedFileRef(`http://${label}`, label)) return _match;
1476
+ return `<code>${escapeHtml(label)}</code>`;
1477
+ });
1478
+ let codeDepth = 0;
1479
+ let preDepth = 0;
1480
+ let anchorDepth = 0;
1481
+ let result = "";
1482
+ let lastIndex = 0;
1483
+ HTML_TAG_PATTERN.lastIndex = 0;
1484
+ let match;
1485
+ while ((match = HTML_TAG_PATTERN.exec(deLinkified)) !== null) {
1486
+ const tagStart = match.index;
1487
+ const tagEnd = HTML_TAG_PATTERN.lastIndex;
1488
+ const isClosing = match[1] === "</";
1489
+ const tagName = match[2].toLowerCase();
1490
+ const textBefore = deLinkified.slice(lastIndex, tagStart);
1491
+ result += wrapSegmentFileRefs(textBefore, codeDepth, preDepth, anchorDepth);
1492
+ if (tagName === "code") codeDepth = isClosing ? Math.max(0, codeDepth - 1) : codeDepth + 1;
1493
+ else if (tagName === "pre") preDepth = isClosing ? Math.max(0, preDepth - 1) : preDepth + 1;
1494
+ else if (tagName === "a") anchorDepth = isClosing ? Math.max(0, anchorDepth - 1) : anchorDepth + 1;
1495
+ result += deLinkified.slice(tagStart, tagEnd);
1496
+ lastIndex = tagEnd;
1497
+ }
1498
+ const remainingText = deLinkified.slice(lastIndex);
1499
+ result += wrapSegmentFileRefs(remainingText, codeDepth, preDepth, anchorDepth);
1500
+ return result;
1501
+ }
1502
+ function renderTelegramHtmlText(text, options = {}) {
1503
+ if ((options.textMode ?? "markdown") === "html") return text;
1504
+ return markdownToTelegramHtml(text, { tableMode: options.tableMode });
1505
+ }
1506
+ function splitTelegramChunkByHtmlLimit(chunk, htmlLimit, renderedHtmlLength) {
1507
+ const currentTextLength = chunk.text.length;
1508
+ if (currentTextLength <= 1) return [chunk];
1509
+ const proportionalLimit = Math.floor(currentTextLength * htmlLimit / Math.max(renderedHtmlLength, 1));
1510
+ const candidateLimit = Math.min(currentTextLength - 1, proportionalLimit);
1511
+ const split = splitMarkdownIRPreserveWhitespace(chunk, Number.isFinite(candidateLimit) && candidateLimit > 0 ? candidateLimit : Math.max(1, Math.floor(currentTextLength / 2)));
1512
+ if (split.length > 1) return split;
1513
+ return splitMarkdownIRPreserveWhitespace(chunk, Math.max(1, Math.floor(currentTextLength / 2)));
1514
+ }
1515
+ function sliceStyleSpans(styles, start, end) {
1516
+ return styles.flatMap((span) => {
1517
+ if (span.end <= start || span.start >= end) return [];
1518
+ const nextStart = Math.max(span.start, start) - start;
1519
+ const nextEnd = Math.min(span.end, end) - start;
1520
+ if (nextEnd <= nextStart) return [];
1521
+ return [{
1522
+ ...span,
1523
+ start: nextStart,
1524
+ end: nextEnd
1525
+ }];
1526
+ });
1527
+ }
1528
+ function sliceLinkSpans(links, start, end) {
1529
+ return links.flatMap((link) => {
1530
+ if (link.end <= start || link.start >= end) return [];
1531
+ const nextStart = Math.max(link.start, start) - start;
1532
+ const nextEnd = Math.min(link.end, end) - start;
1533
+ if (nextEnd <= nextStart) return [];
1534
+ return [{
1535
+ ...link,
1536
+ start: nextStart,
1537
+ end: nextEnd
1538
+ }];
1539
+ });
1540
+ }
1541
+ function splitMarkdownIRPreserveWhitespace(ir, limit) {
1542
+ if (!ir.text) return [];
1543
+ const normalizedLimit = Math.max(1, Math.floor(limit));
1544
+ if (normalizedLimit <= 0 || ir.text.length <= normalizedLimit) return [ir];
1545
+ const chunks = [];
1546
+ let cursor = 0;
1547
+ while (cursor < ir.text.length) {
1548
+ const end = Math.min(ir.text.length, cursor + normalizedLimit);
1549
+ chunks.push({
1550
+ text: ir.text.slice(cursor, end),
1551
+ styles: sliceStyleSpans(ir.styles, cursor, end),
1552
+ links: sliceLinkSpans(ir.links, cursor, end)
1553
+ });
1554
+ cursor = end;
1555
+ }
1556
+ return chunks;
1557
+ }
1558
+ function renderTelegramChunksWithinHtmlLimit(ir, limit) {
1559
+ const normalizedLimit = Math.max(1, Math.floor(limit));
1560
+ const pending = chunkMarkdownIR(ir, normalizedLimit);
1561
+ const rendered = [];
1562
+ while (pending.length > 0) {
1563
+ const chunk = pending.shift();
1564
+ if (!chunk) continue;
1565
+ const html = wrapFileReferencesInHtml(renderTelegramHtml(chunk));
1566
+ if (html.length <= normalizedLimit || chunk.text.length <= 1) {
1567
+ rendered.push({
1568
+ html,
1569
+ text: chunk.text
1570
+ });
1571
+ continue;
1572
+ }
1573
+ const split = splitTelegramChunkByHtmlLimit(chunk, normalizedLimit, html.length);
1574
+ if (split.length <= 1) {
1575
+ rendered.push({
1576
+ html,
1577
+ text: chunk.text
1578
+ });
1579
+ continue;
1580
+ }
1581
+ pending.unshift(...split);
1582
+ }
1583
+ return rendered;
1584
+ }
1585
+ function markdownToTelegramChunks(markdown, limit, options = {}) {
1586
+ return renderTelegramChunksWithinHtmlLimit(markdownToIR(markdown ?? "", {
1587
+ linkify: true,
1588
+ enableSpoilers: true,
1589
+ headingStyle: "none",
1590
+ blockquotePrefix: "",
1591
+ tableMode: options.tableMode
1592
+ }), limit);
1593
+ }
1594
+
1595
+ //#endregion
1596
+ //#region src/telegram/network-errors.ts
1597
+ const RECOVERABLE_ERROR_CODES = new Set([
1598
+ "ECONNRESET",
1599
+ "ECONNREFUSED",
1600
+ "EPIPE",
1601
+ "ETIMEDOUT",
1602
+ "ESOCKETTIMEDOUT",
1603
+ "ENETUNREACH",
1604
+ "EHOSTUNREACH",
1605
+ "ENOTFOUND",
1606
+ "EAI_AGAIN",
1607
+ "UND_ERR_CONNECT_TIMEOUT",
1608
+ "UND_ERR_HEADERS_TIMEOUT",
1609
+ "UND_ERR_BODY_TIMEOUT",
1610
+ "UND_ERR_SOCKET",
1611
+ "UND_ERR_ABORTED",
1612
+ "ECONNABORTED",
1613
+ "ERR_NETWORK"
1614
+ ]);
1615
+ const RECOVERABLE_ERROR_NAMES = new Set([
1616
+ "AbortError",
1617
+ "TimeoutError",
1618
+ "ConnectTimeoutError",
1619
+ "HeadersTimeoutError",
1620
+ "BodyTimeoutError"
1621
+ ]);
1622
+ const ALWAYS_RECOVERABLE_MESSAGES = new Set(["fetch failed", "typeerror: fetch failed"]);
1623
+ const RECOVERABLE_MESSAGE_SNIPPETS = [
1624
+ "undici",
1625
+ "network error",
1626
+ "network request",
1627
+ "client network socket disconnected",
1628
+ "socket hang up",
1629
+ "getaddrinfo",
1630
+ "timeout",
1631
+ "timed out"
1632
+ ];
1633
+ function normalizeCode(code) {
1634
+ return code?.trim().toUpperCase() ?? "";
1635
+ }
1636
+ function getErrorCode(err) {
1637
+ const direct = extractErrorCode(err);
1638
+ if (direct) return direct;
1639
+ if (!err || typeof err !== "object") return;
1640
+ const errno = err.errno;
1641
+ if (typeof errno === "string") return errno;
1642
+ if (typeof errno === "number") return String(errno);
1643
+ }
1644
+ function isRecoverableTelegramNetworkError(err, options = {}) {
1645
+ if (!err) return false;
1646
+ const allowMessageMatch = typeof options.allowMessageMatch === "boolean" ? options.allowMessageMatch : options.context !== "send";
1647
+ for (const candidate of collectErrorGraphCandidates(err, (current) => {
1648
+ const nested = [current.cause, current.reason];
1649
+ if (Array.isArray(current.errors)) nested.push(...current.errors);
1650
+ if (readErrorName(current) === "HttpError") nested.push(current.error);
1651
+ return nested;
1652
+ })) {
1653
+ const code = normalizeCode(getErrorCode(candidate));
1654
+ if (code && RECOVERABLE_ERROR_CODES.has(code)) return true;
1655
+ const name = readErrorName(candidate);
1656
+ if (name && RECOVERABLE_ERROR_NAMES.has(name)) return true;
1657
+ const message = formatErrorMessage(candidate).trim().toLowerCase();
1658
+ if (message && ALWAYS_RECOVERABLE_MESSAGES.has(message)) return true;
1659
+ if (allowMessageMatch && message) {
1660
+ if (RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) return true;
1661
+ }
1662
+ }
1663
+ return false;
1664
+ }
1665
+
1666
+ //#endregion
1667
+ //#region src/telegram/sent-message-cache.ts
1668
+ /**
1669
+ * In-memory cache of sent message IDs per chat.
1670
+ * Used to identify bot's own messages for reaction filtering ("own" mode).
1671
+ */
1672
+ const TTL_MS = 1440 * 60 * 1e3;
1673
+ const sentMessages = /* @__PURE__ */ new Map();
1674
+ function getChatKey(chatId) {
1675
+ return String(chatId);
1676
+ }
1677
+ function cleanupExpired(entry) {
1678
+ const now = Date.now();
1679
+ for (const [msgId, timestamp] of entry.timestamps) if (now - timestamp > TTL_MS) entry.timestamps.delete(msgId);
1680
+ }
1681
+ /**
1682
+ * Record a message ID as sent by the bot.
1683
+ */
1684
+ function recordSentMessage(chatId, messageId) {
1685
+ const key = getChatKey(chatId);
1686
+ let entry = sentMessages.get(key);
1687
+ if (!entry) {
1688
+ entry = { timestamps: /* @__PURE__ */ new Map() };
1689
+ sentMessages.set(key, entry);
1690
+ }
1691
+ entry.timestamps.set(messageId, Date.now());
1692
+ if (entry.timestamps.size > 100) cleanupExpired(entry);
1693
+ }
1694
+ /**
1695
+ * Check if a message was sent by the bot.
1696
+ */
1697
+ function wasSentByBot(chatId, messageId) {
1698
+ const key = getChatKey(chatId);
1699
+ const entry = sentMessages.get(key);
1700
+ if (!entry) return false;
1701
+ cleanupExpired(entry);
1702
+ return entry.timestamps.has(messageId);
1703
+ }
1704
+
1705
+ //#endregion
1706
+ //#region src/cron/store.ts
1707
+ const DEFAULT_CRON_DIR = path.join(CONFIG_DIR, "cron");
1708
+ const DEFAULT_CRON_STORE_PATH = path.join(DEFAULT_CRON_DIR, "jobs.json");
1709
+ const serializedStoreCache = /* @__PURE__ */ new Map();
1710
+ function resolveCronStorePath(storePath) {
1711
+ if (storePath?.trim()) {
1712
+ const raw = storePath.trim();
1713
+ if (raw.startsWith("~")) return path.resolve(expandHomePrefix(raw));
1714
+ return path.resolve(raw);
1715
+ }
1716
+ return DEFAULT_CRON_STORE_PATH;
1717
+ }
1718
+ async function loadCronStore(storePath) {
1719
+ try {
1720
+ const raw = await fs.promises.readFile(storePath, "utf-8");
1721
+ let parsed;
1722
+ try {
1723
+ parsed = JSON5.parse(raw);
1724
+ } catch (err) {
1725
+ throw new Error(`Failed to parse cron store at ${storePath}: ${String(err)}`, { cause: err });
1726
+ }
1727
+ const parsedRecord = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1728
+ const store = {
1729
+ version: 1,
1730
+ jobs: (Array.isArray(parsedRecord.jobs) ? parsedRecord.jobs : []).filter(Boolean)
1731
+ };
1732
+ serializedStoreCache.set(storePath, JSON.stringify(store, null, 2));
1733
+ return store;
1734
+ } catch (err) {
1735
+ if (err?.code === "ENOENT") {
1736
+ serializedStoreCache.delete(storePath);
1737
+ return {
1738
+ version: 1,
1739
+ jobs: []
1740
+ };
1741
+ }
1742
+ throw err;
1743
+ }
1744
+ }
1745
+ async function saveCronStore(storePath, store) {
1746
+ await fs.promises.mkdir(path.dirname(storePath), { recursive: true });
1747
+ const json = JSON.stringify(store, null, 2);
1748
+ const cached = serializedStoreCache.get(storePath);
1749
+ if (cached === json) return;
1750
+ let previous = cached ?? null;
1751
+ if (previous === null) try {
1752
+ previous = await fs.promises.readFile(storePath, "utf-8");
1753
+ } catch (err) {
1754
+ if (err.code !== "ENOENT") throw err;
1755
+ }
1756
+ if (previous === json) {
1757
+ serializedStoreCache.set(storePath, json);
1758
+ return;
1759
+ }
1760
+ const tmp = `${storePath}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
1761
+ await fs.promises.writeFile(tmp, json, "utf-8");
1762
+ if (previous !== null) try {
1763
+ await fs.promises.copyFile(storePath, `${storePath}.bak`);
1764
+ } catch {}
1765
+ await renameWithRetry(tmp, storePath);
1766
+ serializedStoreCache.set(storePath, json);
1767
+ }
1768
+ const RENAME_MAX_RETRIES = 3;
1769
+ const RENAME_BASE_DELAY_MS = 50;
1770
+ async function renameWithRetry(src, dest) {
1771
+ for (let attempt = 0; attempt <= RENAME_MAX_RETRIES; attempt++) try {
1772
+ await fs.promises.rename(src, dest);
1773
+ return;
1774
+ } catch (err) {
1775
+ const code = err.code;
1776
+ if (code === "EBUSY" && attempt < RENAME_MAX_RETRIES) {
1777
+ await new Promise((resolve) => setTimeout(resolve, RENAME_BASE_DELAY_MS * 2 ** attempt));
1778
+ continue;
1779
+ }
1780
+ if (code === "EPERM" || code === "EEXIST") {
1781
+ await fs.promises.copyFile(src, dest);
1782
+ await fs.promises.unlink(src).catch(() => {});
1783
+ return;
1784
+ }
1785
+ throw err;
1786
+ }
1787
+ }
1788
+
1789
+ //#endregion
1790
+ //#region src/telegram/target-writeback.ts
1791
+ const writebackLogger = createSubsystemLogger("telegram/target-writeback");
1792
+ function asObjectRecord(value) {
1793
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1794
+ return value;
1795
+ }
1796
+ function normalizeTelegramLookupTargetForMatch(raw) {
1797
+ const normalized = normalizeTelegramLookupTarget(raw);
1798
+ if (!normalized) return;
1799
+ return normalized.startsWith("@") ? normalized.toLowerCase() : normalized;
1800
+ }
1801
+ function normalizeTelegramTargetForMatch(raw) {
1802
+ const parsed = parseTelegramTarget(raw);
1803
+ const normalized = normalizeTelegramLookupTargetForMatch(parsed.chatId);
1804
+ if (!normalized) return;
1805
+ return `${normalized}|${parsed.messageThreadId == null ? "" : String(parsed.messageThreadId)}`;
1806
+ }
1807
+ function buildResolvedTelegramTarget(params) {
1808
+ const { raw, parsed, resolvedChatId } = params;
1809
+ if (parsed.messageThreadId == null) return resolvedChatId;
1810
+ return raw.includes(":topic:") ? `${resolvedChatId}:topic:${parsed.messageThreadId}` : `${resolvedChatId}:${parsed.messageThreadId}`;
1811
+ }
1812
+ function resolveLegacyRewrite(params) {
1813
+ const parsed = parseTelegramTarget(params.raw);
1814
+ if (normalizeTelegramChatId(parsed.chatId)) return null;
1815
+ const normalized = normalizeTelegramLookupTargetForMatch(parsed.chatId);
1816
+ if (!normalized) return null;
1817
+ return {
1818
+ matchKey: `${normalized}|${parsed.messageThreadId == null ? "" : String(parsed.messageThreadId)}`,
1819
+ resolvedTarget: buildResolvedTelegramTarget({
1820
+ raw: params.raw,
1821
+ parsed,
1822
+ resolvedChatId: params.resolvedChatId
1823
+ })
1824
+ };
1825
+ }
1826
+ function rewriteTargetIfMatch(params) {
1827
+ if (typeof params.rawValue !== "string" && typeof params.rawValue !== "number") return null;
1828
+ const value = String(params.rawValue).trim();
1829
+ if (!value) return null;
1830
+ if (normalizeTelegramTargetForMatch(value) !== params.matchKey) return null;
1831
+ return params.resolvedTarget;
1832
+ }
1833
+ function replaceTelegramDefaultToTargets(params) {
1834
+ let changed = false;
1835
+ const telegram = asObjectRecord(params.cfg.channels?.telegram);
1836
+ if (!telegram) return changed;
1837
+ const maybeReplace = (holder, key) => {
1838
+ const nextTarget = rewriteTargetIfMatch({
1839
+ rawValue: holder[key],
1840
+ matchKey: params.matchKey,
1841
+ resolvedTarget: params.resolvedTarget
1842
+ });
1843
+ if (!nextTarget) return;
1844
+ holder[key] = nextTarget;
1845
+ changed = true;
1846
+ };
1847
+ maybeReplace(telegram, "defaultTo");
1848
+ const accounts = asObjectRecord(telegram.accounts);
1849
+ if (!accounts) return changed;
1850
+ for (const accountId of Object.keys(accounts)) {
1851
+ const account = asObjectRecord(accounts[accountId]);
1852
+ if (!account) continue;
1853
+ maybeReplace(account, "defaultTo");
1854
+ }
1855
+ return changed;
1856
+ }
1857
+ async function maybePersistResolvedTelegramTarget(params) {
1858
+ const raw = params.rawTarget.trim();
1859
+ if (!raw) return;
1860
+ const rewrite = resolveLegacyRewrite({
1861
+ raw,
1862
+ resolvedChatId: params.resolvedChatId
1863
+ });
1864
+ if (!rewrite) return;
1865
+ const { matchKey, resolvedTarget } = rewrite;
1866
+ try {
1867
+ const { snapshot, writeOptions } = await readConfigFileSnapshotForWrite();
1868
+ const nextConfig = structuredClone(snapshot.config ?? {});
1869
+ if (replaceTelegramDefaultToTargets({
1870
+ cfg: nextConfig,
1871
+ matchKey,
1872
+ resolvedTarget
1873
+ })) {
1874
+ await writeConfigFile(nextConfig, writeOptions);
1875
+ if (params.verbose) writebackLogger.warn(`resolved Telegram defaultTo target ${raw} -> ${resolvedTarget}`);
1876
+ }
1877
+ } catch (err) {
1878
+ if (params.verbose) writebackLogger.warn(`failed to persist Telegram defaultTo target ${raw}: ${String(err)}`);
1879
+ }
1880
+ try {
1881
+ const storePath = resolveCronStorePath(params.cfg.cron?.store);
1882
+ const store = await loadCronStore(storePath);
1883
+ let cronChanged = false;
1884
+ for (const job of store.jobs) {
1885
+ if (job.delivery?.channel !== "telegram") continue;
1886
+ const nextTarget = rewriteTargetIfMatch({
1887
+ rawValue: job.delivery.to,
1888
+ matchKey,
1889
+ resolvedTarget
1890
+ });
1891
+ if (!nextTarget) continue;
1892
+ job.delivery.to = nextTarget;
1893
+ cronChanged = true;
1894
+ }
1895
+ if (cronChanged) {
1896
+ await saveCronStore(storePath, store);
1897
+ if (params.verbose) writebackLogger.warn(`resolved Telegram cron delivery target ${raw} -> ${resolvedTarget}`);
1898
+ }
1899
+ } catch (err) {
1900
+ if (params.verbose) writebackLogger.warn(`failed to persist Telegram cron target ${raw}: ${String(err)}`);
1901
+ }
1902
+ }
1903
+
1904
+ //#endregion
1905
+ //#region src/telegram/voice.ts
1906
+ function resolveTelegramVoiceDecision(opts) {
1907
+ if (!opts.wantsVoice) return { useVoice: false };
1908
+ if (isTelegramVoiceCompatibleAudio(opts)) return { useVoice: true };
1909
+ return {
1910
+ useVoice: false,
1911
+ reason: `media is ${opts.contentType ?? "unknown"} (${opts.fileName ?? "unknown"})`
1912
+ };
1913
+ }
1914
+ function resolveTelegramVoiceSend(opts) {
1915
+ const decision = resolveTelegramVoiceDecision(opts);
1916
+ if (decision.reason && opts.logFallback) opts.logFallback(`Telegram voice requested but ${decision.reason}; sending as audio file instead.`);
1917
+ return { useVoice: decision.useVoice };
1918
+ }
1919
+
1920
+ //#endregion
1921
+ //#region src/telegram/send.ts
1922
+ function resolveTelegramMessageIdOrThrow(result, context) {
1923
+ if (typeof result?.message_id === "number" && Number.isFinite(result.message_id)) return Math.trunc(result.message_id);
1924
+ throw new Error(`Telegram ${context} returned no message_id`);
1925
+ }
1926
+ const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
1927
+ const THREAD_NOT_FOUND_RE = /400:\s*Bad Request:\s*message thread not found/i;
1928
+ const MESSAGE_NOT_MODIFIED_RE = /400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i;
1929
+ const CHAT_NOT_FOUND_RE = /400: Bad Request: chat not found/i;
1930
+ const sendLogger = createSubsystemLogger("telegram/send");
1931
+ const diagLogger = createSubsystemLogger("telegram/diagnostic");
1932
+ function createTelegramHttpLogger(cfg) {
1933
+ if (!isDiagnosticFlagEnabled("telegram.http", cfg)) return () => {};
1934
+ return (label, err) => {
1935
+ if (!(err instanceof HttpError)) return;
1936
+ const detail = redactSensitiveText(formatUncaughtError(err.error ?? err));
1937
+ diagLogger.warn(`telegram http error (${label}): ${detail}`);
1938
+ };
1939
+ }
1940
+ function resolveTelegramClientOptions(account) {
1941
+ const proxyUrl = account.config.proxy?.trim();
1942
+ const fetchImpl = resolveTelegramFetch(proxyUrl ? makeProxyFetch(proxyUrl) : void 0, { network: account.config.network });
1943
+ const timeoutSeconds = typeof account.config.timeoutSeconds === "number" && Number.isFinite(account.config.timeoutSeconds) ? Math.max(1, Math.floor(account.config.timeoutSeconds)) : void 0;
1944
+ return fetchImpl || timeoutSeconds ? {
1945
+ ...fetchImpl ? { fetch: fetchImpl } : {},
1946
+ ...timeoutSeconds ? { timeoutSeconds } : {}
1947
+ } : void 0;
1948
+ }
1949
+ function resolveToken(explicit, params) {
1950
+ if (explicit?.trim()) return explicit.trim();
1951
+ if (!params.token) throw new Error(`Telegram bot token missing for account "${params.accountId}" (set channels.telegram.accounts.${params.accountId}.botToken/tokenFile or TELEGRAM_BOT_TOKEN for default).`);
1952
+ return params.token.trim();
1953
+ }
1954
+ async function resolveChatId(to, params) {
1955
+ const numericChatId = normalizeTelegramChatId(to);
1956
+ if (numericChatId) return numericChatId;
1957
+ const lookupTarget = normalizeTelegramLookupTarget(to);
1958
+ const getChat = params.api.getChat;
1959
+ if (!lookupTarget || typeof getChat !== "function") throw new Error("Telegram recipient must be a numeric chat ID");
1960
+ try {
1961
+ const chat = await getChat.call(params.api, lookupTarget);
1962
+ const resolved = normalizeTelegramChatId(String(chat?.id ?? ""));
1963
+ if (!resolved) throw new Error(`resolved chat id is not numeric (${String(chat?.id ?? "")})`);
1964
+ if (params.verbose) sendLogger.warn(`telegram recipient ${lookupTarget} resolved to numeric chat id ${resolved}`);
1965
+ return resolved;
1966
+ } catch (err) {
1967
+ const detail = formatErrorMessage(err);
1968
+ throw new Error(`Telegram recipient ${lookupTarget} could not be resolved to a numeric chat ID (${detail})`, { cause: err });
1969
+ }
1970
+ }
1971
+ async function resolveAndPersistChatId(params) {
1972
+ const chatId = await resolveChatId(params.lookupTarget, {
1973
+ api: params.api,
1974
+ verbose: params.verbose
1975
+ });
1976
+ await maybePersistResolvedTelegramTarget({
1977
+ cfg: params.cfg,
1978
+ rawTarget: params.persistTarget,
1979
+ resolvedChatId: chatId,
1980
+ verbose: params.verbose
1981
+ });
1982
+ return chatId;
1983
+ }
1984
+ function normalizeMessageId(raw) {
1985
+ if (typeof raw === "number" && Number.isFinite(raw)) return Math.trunc(raw);
1986
+ if (typeof raw === "string") {
1987
+ const value = raw.trim();
1988
+ if (!value) throw new Error("Message id is required for Telegram actions");
1989
+ const parsed = Number.parseInt(value, 10);
1990
+ if (Number.isFinite(parsed)) return parsed;
1991
+ }
1992
+ throw new Error("Message id is required for Telegram actions");
1993
+ }
1994
+ function isTelegramThreadNotFoundError(err) {
1995
+ return THREAD_NOT_FOUND_RE.test(formatErrorMessage(err));
1996
+ }
1997
+ function isTelegramMessageNotModifiedError(err) {
1998
+ return MESSAGE_NOT_MODIFIED_RE.test(formatErrorMessage(err));
1999
+ }
2000
+ function hasMessageThreadIdParam(params) {
2001
+ if (!params) return false;
2002
+ const value = params.message_thread_id;
2003
+ if (typeof value === "number") return Number.isFinite(value);
2004
+ if (typeof value === "string") return value.trim().length > 0;
2005
+ return false;
2006
+ }
2007
+ function removeMessageThreadIdParam(params) {
2008
+ if (!params || !hasMessageThreadIdParam(params)) return params;
2009
+ const next = { ...params };
2010
+ delete next.message_thread_id;
2011
+ return Object.keys(next).length > 0 ? next : void 0;
2012
+ }
2013
+ function isTelegramHtmlParseError(err) {
2014
+ return PARSE_ERR_RE.test(formatErrorMessage(err));
2015
+ }
2016
+ function buildTelegramThreadReplyParams(params) {
2017
+ const messageThreadId = params.messageThreadId != null ? params.messageThreadId : params.targetMessageThreadId;
2018
+ const threadScope = params.chatType === "direct" ? "dm" : "forum";
2019
+ const threadIdParams = buildTelegramThreadParams(messageThreadId != null ? {
2020
+ id: messageThreadId,
2021
+ scope: threadScope
2022
+ } : void 0);
2023
+ const threadParams = threadIdParams ? { ...threadIdParams } : {};
2024
+ if (params.replyToMessageId != null) {
2025
+ const replyToMessageId = Math.trunc(params.replyToMessageId);
2026
+ if (params.quoteText?.trim()) threadParams.reply_parameters = {
2027
+ message_id: replyToMessageId,
2028
+ quote: params.quoteText.trim()
2029
+ };
2030
+ else threadParams.reply_to_message_id = replyToMessageId;
2031
+ }
2032
+ return threadParams;
2033
+ }
2034
+ async function withTelegramHtmlParseFallback(params) {
2035
+ try {
2036
+ return await params.requestHtml(params.label);
2037
+ } catch (err) {
2038
+ if (!isTelegramHtmlParseError(err)) throw err;
2039
+ if (params.verbose) sendLogger.warn(`telegram ${params.label} failed with HTML parse error, retrying as plain text: ${formatErrorMessage(err)}`);
2040
+ return await params.requestPlain(`${params.label}-plain`);
2041
+ }
2042
+ }
2043
+ function resolveTelegramApiContext(opts) {
2044
+ const cfg = opts.cfg ?? loadConfig();
2045
+ const account = resolveTelegramAccount({
2046
+ cfg,
2047
+ accountId: opts.accountId
2048
+ });
2049
+ const token = resolveToken(opts.token, account);
2050
+ const client = resolveTelegramClientOptions(account);
2051
+ return {
2052
+ cfg,
2053
+ account,
2054
+ api: opts.api ?? new Bot(token, client ? { client } : void 0).api
2055
+ };
2056
+ }
2057
+ function createTelegramRequestWithDiag(params) {
2058
+ const request = createTelegramRetryRunner({
2059
+ retry: params.retry,
2060
+ configRetry: params.account.config.retry,
2061
+ verbose: params.verbose,
2062
+ ...params.shouldRetry ? { shouldRetry: params.shouldRetry } : {}
2063
+ });
2064
+ const logHttpError = createTelegramHttpLogger(params.cfg);
2065
+ return (fn, label, options) => {
2066
+ const runRequest = () => request(fn, label);
2067
+ return (params.useApiErrorLogging === false ? runRequest() : withTelegramApiErrorLogging({
2068
+ operation: label ?? "request",
2069
+ fn: runRequest,
2070
+ ...options?.shouldLog ? { shouldLog: options.shouldLog } : {}
2071
+ })).catch((err) => {
2072
+ logHttpError(label ?? "request", err);
2073
+ throw err;
2074
+ });
2075
+ };
2076
+ }
2077
+ function wrapTelegramChatNotFoundError(err, params) {
2078
+ if (!CHAT_NOT_FOUND_RE.test(formatErrorMessage(err))) return err;
2079
+ return new Error([
2080
+ `Telegram send failed: chat not found (chat_id=${params.chatId}).`,
2081
+ "Likely: bot not started in DM, bot removed from group/channel, group migrated (new -100… id), or wrong bot token.",
2082
+ `Input was: ${JSON.stringify(params.input)}.`
2083
+ ].join(" "));
2084
+ }
2085
+ async function withTelegramThreadFallback(params, label, verbose, attempt) {
2086
+ try {
2087
+ return await attempt(params, label);
2088
+ } catch (err) {
2089
+ if (!hasMessageThreadIdParam(params) || !isTelegramThreadNotFoundError(err)) throw err;
2090
+ if (verbose) sendLogger.warn(`telegram ${label} failed with message_thread_id, retrying without thread: ${formatErrorMessage(err)}`);
2091
+ return await attempt(removeMessageThreadIdParam(params), `${label}-threadless`);
2092
+ }
2093
+ }
2094
+ function createRequestWithChatNotFound(params) {
2095
+ return async (fn, label) => params.requestWithDiag(fn, label).catch((err) => {
2096
+ throw wrapTelegramChatNotFoundError(err, {
2097
+ chatId: params.chatId,
2098
+ input: params.input
2099
+ });
2100
+ });
2101
+ }
2102
+ function buildInlineKeyboard(buttons) {
2103
+ if (!buttons?.length) return;
2104
+ const rows = buttons.map((row) => row.filter((button) => button?.text && button?.callback_data).map((button) => ({
2105
+ text: button.text,
2106
+ callback_data: button.callback_data,
2107
+ ...button.style ? { style: button.style } : {}
2108
+ }))).filter((row) => row.length > 0);
2109
+ if (rows.length === 0) return;
2110
+ return { inline_keyboard: rows };
2111
+ }
2112
+ async function sendMessageTelegram(to, text, opts = {}) {
2113
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2114
+ const target = parseTelegramTarget(to);
2115
+ const chatId = await resolveAndPersistChatId({
2116
+ cfg,
2117
+ api,
2118
+ lookupTarget: target.chatId,
2119
+ persistTarget: to,
2120
+ verbose: opts.verbose
2121
+ });
2122
+ const mediaUrl = opts.mediaUrl?.trim();
2123
+ const replyMarkup = buildInlineKeyboard(opts.buttons);
2124
+ const threadParams = buildTelegramThreadReplyParams({
2125
+ targetMessageThreadId: target.messageThreadId,
2126
+ messageThreadId: opts.messageThreadId,
2127
+ chatType: target.chatType,
2128
+ replyToMessageId: opts.replyToMessageId,
2129
+ quoteText: opts.quoteText
2130
+ });
2131
+ const hasThreadParams = Object.keys(threadParams).length > 0;
2132
+ const requestWithChatNotFound = createRequestWithChatNotFound({
2133
+ requestWithDiag: createTelegramRequestWithDiag({
2134
+ cfg,
2135
+ account,
2136
+ retry: opts.retry,
2137
+ verbose: opts.verbose,
2138
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
2139
+ }),
2140
+ chatId,
2141
+ input: to
2142
+ });
2143
+ const textMode = opts.textMode ?? "markdown";
2144
+ const tableMode = resolveMarkdownTableMode({
2145
+ cfg,
2146
+ channel: "telegram",
2147
+ accountId: account.accountId
2148
+ });
2149
+ const renderHtmlText = (value) => renderTelegramHtmlText(value, {
2150
+ textMode,
2151
+ tableMode
2152
+ });
2153
+ const linkPreviewOptions = account.config.linkPreview ?? true ? void 0 : { is_disabled: true };
2154
+ const sendTelegramText = async (rawText, params, fallbackText) => {
2155
+ return await withTelegramThreadFallback(params, "message", opts.verbose, async (effectiveParams, label) => {
2156
+ const htmlText = renderHtmlText(rawText);
2157
+ const baseParams = effectiveParams ? { ...effectiveParams } : {};
2158
+ if (linkPreviewOptions) baseParams.link_preview_options = linkPreviewOptions;
2159
+ const hasBaseParams = Object.keys(baseParams).length > 0;
2160
+ const sendParams = {
2161
+ parse_mode: "HTML",
2162
+ ...baseParams,
2163
+ ...opts.silent === true ? { disable_notification: true } : {}
2164
+ };
2165
+ return await withTelegramHtmlParseFallback({
2166
+ label,
2167
+ verbose: opts.verbose,
2168
+ requestHtml: (retryLabel) => requestWithChatNotFound(() => api.sendMessage(chatId, htmlText, sendParams), retryLabel),
2169
+ requestPlain: (retryLabel) => {
2170
+ const plainParams = hasBaseParams ? baseParams : void 0;
2171
+ return requestWithChatNotFound(() => plainParams ? api.sendMessage(chatId, fallbackText ?? rawText, plainParams) : api.sendMessage(chatId, fallbackText ?? rawText), retryLabel);
2172
+ }
2173
+ });
2174
+ });
2175
+ };
2176
+ if (mediaUrl) {
2177
+ const media = await loadWebMedia(mediaUrl, buildOutboundMediaLoadOptions({
2178
+ maxBytes: opts.maxBytes,
2179
+ mediaLocalRoots: opts.mediaLocalRoots
2180
+ }));
2181
+ const kind = kindFromMime(media.contentType ?? void 0);
2182
+ const isGif = isGifMedia({
2183
+ contentType: media.contentType,
2184
+ fileName: media.fileName
2185
+ });
2186
+ const isVideoNote = kind === "video" && opts.asVideoNote === true;
2187
+ const fileName = media.fileName ?? (isGif ? "animation.gif" : inferFilename(kind)) ?? "file";
2188
+ const file = new InputFile(media.buffer, fileName);
2189
+ let caption;
2190
+ let followUpText;
2191
+ if (isVideoNote) {
2192
+ caption = void 0;
2193
+ followUpText = text.trim() ? text : void 0;
2194
+ } else {
2195
+ const split = splitTelegramCaption(text);
2196
+ caption = split.caption;
2197
+ followUpText = split.followUpText;
2198
+ }
2199
+ const htmlCaption = caption ? renderHtmlText(caption) : void 0;
2200
+ const needsSeparateText = Boolean(followUpText);
2201
+ const baseMediaParams = {
2202
+ ...hasThreadParams ? threadParams : {},
2203
+ ...!needsSeparateText && replyMarkup ? { reply_markup: replyMarkup } : {}
2204
+ };
2205
+ const mediaParams = {
2206
+ ...htmlCaption ? {
2207
+ caption: htmlCaption,
2208
+ parse_mode: "HTML"
2209
+ } : {},
2210
+ ...baseMediaParams,
2211
+ ...opts.silent === true ? { disable_notification: true } : {}
2212
+ };
2213
+ const sendMedia = async (label, sender) => await withTelegramThreadFallback(mediaParams, label, opts.verbose, async (effectiveParams, retryLabel) => requestWithChatNotFound(() => sender(effectiveParams), retryLabel));
2214
+ const mediaSender = (() => {
2215
+ if (isGif) return {
2216
+ label: "animation",
2217
+ sender: (effectiveParams) => api.sendAnimation(chatId, file, effectiveParams)
2218
+ };
2219
+ if (kind === "image") return {
2220
+ label: "photo",
2221
+ sender: (effectiveParams) => api.sendPhoto(chatId, file, effectiveParams)
2222
+ };
2223
+ if (kind === "video") {
2224
+ if (isVideoNote) return {
2225
+ label: "video_note",
2226
+ sender: (effectiveParams) => api.sendVideoNote(chatId, file, effectiveParams)
2227
+ };
2228
+ return {
2229
+ label: "video",
2230
+ sender: (effectiveParams) => api.sendVideo(chatId, file, effectiveParams)
2231
+ };
2232
+ }
2233
+ if (kind === "audio") {
2234
+ const { useVoice } = resolveTelegramVoiceSend({
2235
+ wantsVoice: opts.asVoice === true,
2236
+ contentType: media.contentType,
2237
+ fileName,
2238
+ logFallback: logVerbose
2239
+ });
2240
+ if (useVoice) return {
2241
+ label: "voice",
2242
+ sender: (effectiveParams) => api.sendVoice(chatId, file, effectiveParams)
2243
+ };
2244
+ return {
2245
+ label: "audio",
2246
+ sender: (effectiveParams) => api.sendAudio(chatId, file, effectiveParams)
2247
+ };
2248
+ }
2249
+ return {
2250
+ label: "document",
2251
+ sender: (effectiveParams) => api.sendDocument(chatId, file, effectiveParams)
2252
+ };
2253
+ })();
2254
+ const result = await sendMedia(mediaSender.label, mediaSender.sender);
2255
+ const mediaMessageId = resolveTelegramMessageIdOrThrow(result, "media send");
2256
+ const resolvedChatId = String(result?.chat?.id ?? chatId);
2257
+ recordSentMessage(chatId, mediaMessageId);
2258
+ recordChannelActivity({
2259
+ channel: "telegram",
2260
+ accountId: account.accountId,
2261
+ direction: "outbound"
2262
+ });
2263
+ if (needsSeparateText && followUpText) {
2264
+ const textParams = hasThreadParams || replyMarkup ? {
2265
+ ...threadParams,
2266
+ ...replyMarkup ? { reply_markup: replyMarkup } : {}
2267
+ } : void 0;
2268
+ const textMessageId = resolveTelegramMessageIdOrThrow(await sendTelegramText(followUpText, textParams), "text follow-up send");
2269
+ recordSentMessage(chatId, textMessageId);
2270
+ return {
2271
+ messageId: String(textMessageId),
2272
+ chatId: resolvedChatId
2273
+ };
2274
+ }
2275
+ return {
2276
+ messageId: String(mediaMessageId),
2277
+ chatId: resolvedChatId
2278
+ };
2279
+ }
2280
+ if (!text || !text.trim()) throw new Error("Message must be non-empty for Telegram sends");
2281
+ const res = await sendTelegramText(text, hasThreadParams || replyMarkup ? {
2282
+ ...threadParams,
2283
+ ...replyMarkup ? { reply_markup: replyMarkup } : {}
2284
+ } : void 0, opts.plainText);
2285
+ const messageId = resolveTelegramMessageIdOrThrow(res, "text send");
2286
+ recordSentMessage(chatId, messageId);
2287
+ recordChannelActivity({
2288
+ channel: "telegram",
2289
+ accountId: account.accountId,
2290
+ direction: "outbound"
2291
+ });
2292
+ return {
2293
+ messageId: String(messageId),
2294
+ chatId: String(res?.chat?.id ?? chatId)
2295
+ };
2296
+ }
2297
+ async function reactMessageTelegram(chatIdInput, messageIdInput, emoji, opts = {}) {
2298
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2299
+ const rawTarget = String(chatIdInput);
2300
+ const chatId = await resolveAndPersistChatId({
2301
+ cfg,
2302
+ api,
2303
+ lookupTarget: rawTarget,
2304
+ persistTarget: rawTarget,
2305
+ verbose: opts.verbose
2306
+ });
2307
+ const messageId = normalizeMessageId(messageIdInput);
2308
+ const requestWithDiag = createTelegramRequestWithDiag({
2309
+ cfg,
2310
+ account,
2311
+ retry: opts.retry,
2312
+ verbose: opts.verbose,
2313
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
2314
+ });
2315
+ const remove = opts.remove === true;
2316
+ const trimmedEmoji = emoji.trim();
2317
+ const reactions = remove || !trimmedEmoji ? [] : [{
2318
+ type: "emoji",
2319
+ emoji: trimmedEmoji
2320
+ }];
2321
+ if (typeof api.setMessageReaction !== "function") throw new Error("Telegram reactions are unavailable in this bot API.");
2322
+ try {
2323
+ await requestWithDiag(() => api.setMessageReaction(chatId, messageId, reactions), "reaction");
2324
+ } catch (err) {
2325
+ const msg = err instanceof Error ? err.message : String(err);
2326
+ if (/REACTION_INVALID/i.test(msg)) return {
2327
+ ok: false,
2328
+ warning: `Reaction unavailable: ${trimmedEmoji}`
2329
+ };
2330
+ throw err;
2331
+ }
2332
+ return { ok: true };
2333
+ }
2334
+ async function deleteMessageTelegram(chatIdInput, messageIdInput, opts = {}) {
2335
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2336
+ const rawTarget = String(chatIdInput);
2337
+ const chatId = await resolveAndPersistChatId({
2338
+ cfg,
2339
+ api,
2340
+ lookupTarget: rawTarget,
2341
+ persistTarget: rawTarget,
2342
+ verbose: opts.verbose
2343
+ });
2344
+ const messageId = normalizeMessageId(messageIdInput);
2345
+ await createTelegramRequestWithDiag({
2346
+ cfg,
2347
+ account,
2348
+ retry: opts.retry,
2349
+ verbose: opts.verbose,
2350
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
2351
+ })(() => api.deleteMessage(chatId, messageId), "deleteMessage");
2352
+ logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`);
2353
+ return { ok: true };
2354
+ }
2355
+ async function editMessageTelegram(chatIdInput, messageIdInput, text, opts = {}) {
2356
+ const { cfg, account, api } = resolveTelegramApiContext({
2357
+ ...opts,
2358
+ cfg: opts.cfg
2359
+ });
2360
+ const rawTarget = String(chatIdInput);
2361
+ const chatId = await resolveAndPersistChatId({
2362
+ cfg,
2363
+ api,
2364
+ lookupTarget: rawTarget,
2365
+ persistTarget: rawTarget,
2366
+ verbose: opts.verbose
2367
+ });
2368
+ const messageId = normalizeMessageId(messageIdInput);
2369
+ const requestWithDiag = createTelegramRequestWithDiag({
2370
+ cfg,
2371
+ account,
2372
+ retry: opts.retry,
2373
+ verbose: opts.verbose
2374
+ });
2375
+ const requestWithEditShouldLog = (fn, label, shouldLog) => requestWithDiag(fn, label, shouldLog ? { shouldLog } : void 0);
2376
+ const htmlText = renderTelegramHtmlText(text, {
2377
+ textMode: opts.textMode ?? "markdown",
2378
+ tableMode: resolveMarkdownTableMode({
2379
+ cfg,
2380
+ channel: "telegram",
2381
+ accountId: account.accountId
2382
+ })
2383
+ });
2384
+ const shouldTouchButtons = opts.buttons !== void 0;
2385
+ const builtKeyboard = shouldTouchButtons ? buildInlineKeyboard(opts.buttons) : void 0;
2386
+ const replyMarkup = shouldTouchButtons ? builtKeyboard ?? { inline_keyboard: [] } : void 0;
2387
+ const editParams = { parse_mode: "HTML" };
2388
+ if (opts.linkPreview === false) editParams.link_preview_options = { is_disabled: true };
2389
+ if (replyMarkup !== void 0) editParams.reply_markup = replyMarkup;
2390
+ const plainParams = {};
2391
+ if (opts.linkPreview === false) plainParams.link_preview_options = { is_disabled: true };
2392
+ if (replyMarkup !== void 0) plainParams.reply_markup = replyMarkup;
2393
+ try {
2394
+ await withTelegramHtmlParseFallback({
2395
+ label: "editMessage",
2396
+ verbose: opts.verbose,
2397
+ requestHtml: (retryLabel) => requestWithEditShouldLog(() => api.editMessageText(chatId, messageId, htmlText, editParams), retryLabel, (err) => !isTelegramMessageNotModifiedError(err)),
2398
+ requestPlain: (retryLabel) => requestWithEditShouldLog(() => Object.keys(plainParams).length > 0 ? api.editMessageText(chatId, messageId, text, plainParams) : api.editMessageText(chatId, messageId, text), retryLabel, (plainErr) => !isTelegramMessageNotModifiedError(plainErr))
2399
+ });
2400
+ } catch (err) {
2401
+ if (isTelegramMessageNotModifiedError(err)) {} else throw err;
2402
+ }
2403
+ logVerbose(`[telegram] Edited message ${messageId} in chat ${chatId}`);
2404
+ return {
2405
+ ok: true,
2406
+ messageId: String(messageId),
2407
+ chatId
2408
+ };
2409
+ }
2410
+ function inferFilename(kind) {
2411
+ switch (kind) {
2412
+ case "image": return "image.jpg";
2413
+ case "video": return "video.mp4";
2414
+ case "audio": return "audio.ogg";
2415
+ default: return "file.bin";
2416
+ }
2417
+ }
2418
+ /**
2419
+ * Send a sticker to a Telegram chat by file_id.
2420
+ * @param to - Chat ID or username (e.g., "123456789" or "@username")
2421
+ * @param fileId - Telegram file_id of the sticker to send
2422
+ * @param opts - Optional configuration
2423
+ */
2424
+ async function sendStickerTelegram(to, fileId, opts = {}) {
2425
+ if (!fileId?.trim()) throw new Error("Telegram sticker file_id is required");
2426
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2427
+ const target = parseTelegramTarget(to);
2428
+ const chatId = await resolveAndPersistChatId({
2429
+ cfg,
2430
+ api,
2431
+ lookupTarget: target.chatId,
2432
+ persistTarget: to,
2433
+ verbose: opts.verbose
2434
+ });
2435
+ const threadParams = buildTelegramThreadReplyParams({
2436
+ targetMessageThreadId: target.messageThreadId,
2437
+ messageThreadId: opts.messageThreadId,
2438
+ chatType: target.chatType,
2439
+ replyToMessageId: opts.replyToMessageId
2440
+ });
2441
+ const hasThreadParams = Object.keys(threadParams).length > 0;
2442
+ const requestWithChatNotFound = createRequestWithChatNotFound({
2443
+ requestWithDiag: createTelegramRequestWithDiag({
2444
+ cfg,
2445
+ account,
2446
+ retry: opts.retry,
2447
+ verbose: opts.verbose,
2448
+ useApiErrorLogging: false
2449
+ }),
2450
+ chatId,
2451
+ input: to
2452
+ });
2453
+ const result = await withTelegramThreadFallback(hasThreadParams ? threadParams : void 0, "sticker", opts.verbose, async (effectiveParams, label) => requestWithChatNotFound(() => api.sendSticker(chatId, fileId.trim(), effectiveParams), label));
2454
+ const messageId = resolveTelegramMessageIdOrThrow(result, "sticker send");
2455
+ const resolvedChatId = String(result?.chat?.id ?? chatId);
2456
+ recordSentMessage(chatId, messageId);
2457
+ recordChannelActivity({
2458
+ channel: "telegram",
2459
+ accountId: account.accountId,
2460
+ direction: "outbound"
2461
+ });
2462
+ return {
2463
+ messageId: String(messageId),
2464
+ chatId: resolvedChatId
2465
+ };
2466
+ }
2467
+ /**
2468
+ * Send a poll to a Telegram chat.
2469
+ * @param to - Chat ID or username (e.g., "123456789" or "@username")
2470
+ * @param poll - Poll input with question, options, maxSelections, and optional durationHours
2471
+ * @param opts - Optional configuration
2472
+ */
2473
+ async function sendPollTelegram(to, poll, opts = {}) {
2474
+ const { cfg, account, api } = resolveTelegramApiContext(opts);
2475
+ const target = parseTelegramTarget(to);
2476
+ const chatId = await resolveAndPersistChatId({
2477
+ cfg,
2478
+ api,
2479
+ lookupTarget: target.chatId,
2480
+ persistTarget: to,
2481
+ verbose: opts.verbose
2482
+ });
2483
+ const normalizedPoll = normalizePollInput(poll, { maxOptions: 10 });
2484
+ const threadParams = buildTelegramThreadReplyParams({
2485
+ targetMessageThreadId: target.messageThreadId,
2486
+ messageThreadId: opts.messageThreadId,
2487
+ chatType: target.chatType,
2488
+ replyToMessageId: opts.replyToMessageId
2489
+ });
2490
+ const pollOptions = normalizedPoll.options;
2491
+ const requestWithChatNotFound = createRequestWithChatNotFound({
2492
+ requestWithDiag: createTelegramRequestWithDiag({
2493
+ cfg,
2494
+ account,
2495
+ retry: opts.retry,
2496
+ verbose: opts.verbose,
2497
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
2498
+ }),
2499
+ chatId,
2500
+ input: to
2501
+ });
2502
+ const durationSeconds = normalizedPoll.durationSeconds;
2503
+ if (durationSeconds === void 0 && normalizedPoll.durationHours !== void 0) throw new Error("Telegram poll durationHours is not supported. Use durationSeconds (5-600) instead.");
2504
+ if (durationSeconds !== void 0 && (durationSeconds < 5 || durationSeconds > 600)) throw new Error("Telegram poll durationSeconds must be between 5 and 600");
2505
+ const result = await withTelegramThreadFallback({
2506
+ allows_multiple_answers: normalizedPoll.maxSelections > 1,
2507
+ is_anonymous: opts.isAnonymous ?? true,
2508
+ ...durationSeconds !== void 0 ? { open_period: durationSeconds } : {},
2509
+ ...Object.keys(threadParams).length > 0 ? threadParams : {},
2510
+ ...opts.silent === true ? { disable_notification: true } : {}
2511
+ }, "poll", opts.verbose, async (effectiveParams, label) => requestWithChatNotFound(() => api.sendPoll(chatId, normalizedPoll.question, pollOptions, effectiveParams), label));
2512
+ const messageId = resolveTelegramMessageIdOrThrow(result, "poll send");
2513
+ const resolvedChatId = String(result?.chat?.id ?? chatId);
2514
+ const pollId = result?.poll?.id;
2515
+ recordSentMessage(chatId, messageId);
2516
+ recordChannelActivity({
2517
+ channel: "telegram",
2518
+ accountId: account.accountId,
2519
+ direction: "outbound"
2520
+ });
2521
+ return {
2522
+ messageId: String(messageId),
2523
+ chatId: resolvedChatId,
2524
+ pollId
2525
+ };
2526
+ }
2527
+ /**
2528
+ * Create a forum topic in a Telegram supergroup.
2529
+ * Requires the bot to have `can_manage_topics` permission.
2530
+ *
2531
+ * @param chatId - Supergroup chat ID
2532
+ * @param name - Topic name (1-128 characters)
2533
+ * @param opts - Optional configuration
2534
+ */
2535
+ async function createForumTopicTelegram(chatId, name, opts = {}) {
2536
+ if (!name?.trim()) throw new Error("Forum topic name is required");
2537
+ const trimmedName = name.trim();
2538
+ if (trimmedName.length > 128) throw new Error("Forum topic name must be 128 characters or fewer");
2539
+ const cfg = loadConfig();
2540
+ const account = resolveTelegramAccount({
2541
+ cfg,
2542
+ accountId: opts.accountId
2543
+ });
2544
+ const token = resolveToken(opts.token, account);
2545
+ const client = resolveTelegramClientOptions(account);
2546
+ const api = opts.api ?? new Bot(token, client ? { client } : void 0).api;
2547
+ const normalizedChatId = await resolveAndPersistChatId({
2548
+ cfg,
2549
+ api,
2550
+ lookupTarget: parseTelegramTarget(chatId).chatId,
2551
+ persistTarget: chatId,
2552
+ verbose: opts.verbose
2553
+ });
2554
+ const request = createTelegramRetryRunner({
2555
+ retry: opts.retry,
2556
+ configRetry: account.config.retry,
2557
+ verbose: opts.verbose,
2558
+ shouldRetry: (err) => isRecoverableTelegramNetworkError(err, { context: "send" })
2559
+ });
2560
+ const logHttpError = createTelegramHttpLogger(cfg);
2561
+ const requestWithDiag = (fn, label) => withTelegramApiErrorLogging({
2562
+ operation: label ?? "request",
2563
+ fn: () => request(fn, label)
2564
+ }).catch((err) => {
2565
+ logHttpError(label ?? "request", err);
2566
+ throw err;
2567
+ });
2568
+ const extra = {};
2569
+ if (opts.iconColor != null) extra.icon_color = opts.iconColor;
2570
+ if (opts.iconCustomEmojiId?.trim()) extra.icon_custom_emoji_id = opts.iconCustomEmojiId.trim();
2571
+ const hasExtra = Object.keys(extra).length > 0;
2572
+ const result = await requestWithDiag(() => api.createForumTopic(normalizedChatId, trimmedName, hasExtra ? extra : void 0), "createForumTopic");
2573
+ const topicId = result.message_thread_id;
2574
+ recordChannelActivity({
2575
+ channel: "telegram",
2576
+ accountId: account.accountId,
2577
+ direction: "outbound"
2578
+ });
2579
+ return {
2580
+ topicId,
2581
+ name: result.name ?? trimmedName,
2582
+ chatId: normalizedChatId
2583
+ };
2584
+ }
2585
+
2586
+ //#endregion
2587
+ export { readChannelAllowFromStoreSync as $, expandTextLinks as A, resolveTelegramThreadSpec as B, buildSenderName as C, buildTelegramThreadParams as D, buildTelegramParentPeer as E, resolveTelegramForumThreadId as F, isVoiceCompatibleAudio as G, normalizeAllowFrom as H, resolveTelegramGroupAllowFromContext as I, isWSL2Sync as J, formatLocationText as K, resolveTelegramMediaPlaceholder as L, hasBotMention as M, normalizeForwardedContext as N, buildTypingThreadParams as O, resolveTelegramDirectPeerId as P, readChannelAllowFromStore as Q, resolveTelegramReplyId as R, buildSenderLabel as S, buildTelegramGroupPeerId as T, normalizeDmAllowFromWithStore as U, isSenderAllowed as V, resolveSenderAllowMatch as W, isWSLSync as X, isWSLEnv as Y, addChannelAllowFromStoreEntry as Z, wrapFileReferencesInHtml as _, reactMessageTelegram as a, listPairingChannels as at, withTelegramApiErrorLogging as b, sendStickerTelegram as c, mergeDmAllowFromSources as ct, resolveCronStorePath as d, removeChannelAllowFromStoreEntry as et, wasSentByBot as f, renderTelegramHtmlText as g, markdownToTelegramHtml as h, editMessageTelegram as i, getPairingAdapter as it, extractTelegramLocation as j, describeReplyTarget as k, resolveTelegramVoiceSend as l, resolveGroupAllowFromSources as lt, markdownToTelegramChunks as m, createForumTopicTelegram as n, readJsonFileWithFallback as nt, sendMessageTelegram as o, firstDefined as ot, isRecoverableTelegramNetworkError as p, toLocationContext as q, deleteMessageTelegram as r, writeJsonFileAtomically as rt, sendPollTelegram as s, isSenderIdAllowed as st, buildInlineKeyboard as t, upsertChannelPairingRequest as tt, loadCronStore as u, resolveTelegramFetch as v, buildTelegramGroupFrom as w, buildGroupLabel as x, splitTelegramCaption as y, resolveTelegramStreamMode as z };