@poolzin/pool-bot 2026.2.11 → 2026.2.18

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 (535) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/agents/agent-scope.js +4 -0
  3. package/dist/agents/announce-idempotency.js +14 -0
  4. package/dist/agents/auth-profiles/usage.js +22 -0
  5. package/dist/agents/auth-profiles.js +1 -1
  6. package/dist/agents/auth-profiles.resolve-auth-profile-order.fixtures.js +23 -0
  7. package/dist/agents/bash-tools.exec-runtime.js +438 -0
  8. package/dist/agents/bash-tools.shared.js +6 -0
  9. package/dist/agents/cli-runner/reliability.js +61 -0
  10. package/dist/agents/cli-watchdog-defaults.js +11 -0
  11. package/dist/agents/command-poll-backoff.js +63 -0
  12. package/dist/agents/current-time.js +16 -0
  13. package/dist/agents/glob-pattern.js +42 -0
  14. package/dist/agents/memory-search.js +33 -0
  15. package/dist/agents/model-alias-lines.js +18 -0
  16. package/dist/agents/model-auth-label.js +61 -0
  17. package/dist/agents/model-fallback.js +59 -8
  18. package/dist/agents/models-config.e2e-harness.js +115 -0
  19. package/dist/agents/ollama-stream.js +11 -3
  20. package/dist/agents/openclaw-tools.js +135 -0
  21. package/dist/agents/pi-auth-json.js +118 -0
  22. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +147 -0
  23. package/dist/agents/pi-embedded-subscribe.e2e-harness.js +90 -0
  24. package/dist/agents/pi-embedded-subscribe.handlers.compaction.js +63 -0
  25. package/dist/agents/pi-embedded-subscribe.handlers.tools.media.test-helpers.js +30 -0
  26. package/dist/agents/pi-extensions/session-manager-runtime-registry.js +23 -0
  27. package/dist/agents/pi-tools.before-tool-call.js +145 -4
  28. package/dist/agents/pi-tools.js +29 -9
  29. package/dist/agents/pi-tools.policy.js +85 -92
  30. package/dist/agents/pi-tools.schema.js +54 -27
  31. package/dist/agents/queued-file-writer.js +22 -0
  32. package/dist/agents/sandbox/docker.js +133 -40
  33. package/dist/agents/sandbox/fs-bridge.js +146 -0
  34. package/dist/agents/sandbox/fs-paths.js +205 -0
  35. package/dist/agents/sandbox/hash.js +4 -0
  36. package/dist/agents/sandbox/validate-sandbox-security.js +157 -0
  37. package/dist/agents/sandbox-paths.js +3 -0
  38. package/dist/agents/sandbox-tool-policy.js +26 -0
  39. package/dist/agents/sanitize-for-prompt.js +18 -0
  40. package/dist/agents/session-dirs.js +20 -0
  41. package/dist/agents/session-write-lock.js +203 -39
  42. package/dist/agents/skills/filter.js +24 -0
  43. package/dist/agents/skills/tools-dir.js +9 -0
  44. package/dist/agents/skills-install-download.js +290 -0
  45. package/dist/agents/skills-install-output.js +30 -0
  46. package/dist/agents/skills-install.download-test-utils.js +36 -0
  47. package/dist/agents/skills.e2e-test-helpers.js +13 -0
  48. package/dist/agents/subagent-announce-queue.js +59 -15
  49. package/dist/agents/subagent-depth.js +137 -0
  50. package/dist/agents/subagent-registry.js +448 -96
  51. package/dist/agents/subagent-spawn.js +262 -0
  52. package/dist/agents/system-prompt.js +52 -10
  53. package/dist/agents/test-helpers/fast-tool-stubs.js +18 -0
  54. package/dist/agents/test-helpers/host-sandbox-fs-bridge.js +74 -0
  55. package/dist/agents/tool-display-common.js +782 -0
  56. package/dist/agents/tool-loop-detection.js +466 -0
  57. package/dist/agents/tool-policy.js +6 -0
  58. package/dist/agents/tools/image-tool.js +1 -1
  59. package/dist/agents/tools/sessions-access.js +178 -0
  60. package/dist/agents/tools/sessions-resolution.js +206 -0
  61. package/dist/agents/tools/subagents-tool.js +616 -0
  62. package/dist/agents/workspace-dir.js +18 -0
  63. package/dist/agents/workspace-dirs.js +14 -0
  64. package/dist/agents/workspace.js +70 -0
  65. package/dist/auto-reply/heartbeat-reply-payload.js +18 -0
  66. package/dist/auto-reply/reply/commands-export-session.js +163 -0
  67. package/dist/auto-reply/reply/commands-mesh.js +245 -0
  68. package/dist/auto-reply/reply/commands-setunset.js +28 -0
  69. package/dist/auto-reply/reply/commands-slash-parse.js +31 -0
  70. package/dist/auto-reply/reply/commands-system-prompt.js +117 -0
  71. package/dist/auto-reply/reply/directive-handling.levels.js +17 -0
  72. package/dist/auto-reply/reply/directive-handling.params.js +1 -0
  73. package/dist/auto-reply/reply/directive-parsing.js +36 -0
  74. package/dist/auto-reply/reply/dispatcher-registry.js +43 -0
  75. package/dist/auto-reply/reply/elevated-unavailable.js +20 -0
  76. package/dist/auto-reply/reply/post-compaction-audit.js +96 -0
  77. package/dist/auto-reply/reply/post-compaction-context.js +98 -0
  78. package/dist/auto-reply/reply/reply-delivery.js +92 -0
  79. package/dist/auto-reply/reply/session-reset-prompt.js +1 -0
  80. package/dist/auto-reply/reply/session-run-accounting.js +33 -0
  81. package/dist/auto-reply/reply.directive.directive-behavior.e2e-harness.js +115 -0
  82. package/dist/auto-reply/reply.directive.directive-behavior.e2e-mocks.js +12 -0
  83. package/dist/browser/bridge-auth-registry.js +26 -0
  84. package/dist/browser/client-actions-url.js +10 -0
  85. package/dist/browser/control-auth.js +73 -0
  86. package/dist/browser/csrf.js +64 -0
  87. package/dist/browser/http-auth.js +52 -0
  88. package/dist/browser/paths.js +37 -0
  89. package/dist/browser/proxy-files.js +32 -0
  90. package/dist/browser/pw-ai-state.js +7 -0
  91. package/dist/browser/resolved-config-refresh.js +42 -0
  92. package/dist/browser/routes/path-output.js +1 -0
  93. package/dist/browser/server-context.chrome-test-harness.js +20 -0
  94. package/dist/browser/server-middleware.js +31 -0
  95. package/dist/browser/test-port.js +16 -0
  96. package/dist/build-info.json +3 -3
  97. package/dist/canvas-host/file-resolver.js +43 -0
  98. package/dist/channels/account-summary.js +19 -0
  99. package/dist/channels/draft-stream-loop.js +77 -0
  100. package/dist/channels/plugins/account-helpers.js +26 -0
  101. package/dist/channels/telegram/allow-from.js +10 -0
  102. package/dist/cli/browser-cli-resize.js +22 -0
  103. package/dist/cli/browser-cli-shared.js +8 -0
  104. package/dist/cli/clawbot-cli.js +5 -0
  105. package/dist/cli/completion-cli.js +566 -0
  106. package/dist/cli/config-cli.js +63 -5
  107. package/dist/cli/daemon-cli/lifecycle-core.js +256 -0
  108. package/dist/cli/daemon-cli/register-service-commands.js +60 -0
  109. package/dist/cli/daemon-cli-compat.js +80 -0
  110. package/dist/cli/nodes-cli/pairing-render.js +26 -0
  111. package/dist/cli/program/action-reparse.js +17 -0
  112. package/dist/cli/program/command-registry.js +17 -0
  113. package/dist/cli/program/program-context.js +8 -0
  114. package/dist/cli/program/register.subclis.js +7 -0
  115. package/dist/cli/program/routes.js +233 -0
  116. package/dist/cli/qr-cli.js +132 -0
  117. package/dist/cli/requirements-test-fixtures.js +17 -0
  118. package/dist/cli/respawn-policy.js +4 -0
  119. package/dist/cli/shared/parse-port.js +18 -0
  120. package/dist/cli/skills-cli.format.js +241 -0
  121. package/dist/cli/update-cli/progress.js +121 -0
  122. package/dist/cli/update-cli/restart-helper.js +108 -0
  123. package/dist/cli/update-cli/shared.js +196 -0
  124. package/dist/cli/update-cli/status.js +97 -0
  125. package/dist/cli/update-cli/suppress-deprecations.js +17 -0
  126. package/dist/cli/update-cli/update-command.js +506 -0
  127. package/dist/cli/update-cli/wizard.js +130 -0
  128. package/dist/cli/update-cli.js +3 -9
  129. package/dist/cli/windows-argv.js +69 -0
  130. package/dist/commands/auth-choice-legacy.js +20 -0
  131. package/dist/commands/auth-choice.apply-helpers.js +8 -0
  132. package/dist/commands/channel-test-helpers.js +19 -0
  133. package/dist/commands/cleanup-plan.js +10 -0
  134. package/dist/commands/cleanup-utils.js +7 -0
  135. package/dist/commands/config-validation.js +15 -0
  136. package/dist/commands/doctor-completion.js +112 -0
  137. package/dist/commands/doctor-memory-search.js +119 -0
  138. package/dist/commands/doctor-session-locks.js +73 -0
  139. package/dist/commands/doctor.e2e-harness.js +364 -0
  140. package/dist/commands/gateway-presence.js +19 -0
  141. package/dist/commands/model-default.js +35 -0
  142. package/dist/commands/models/fallbacks-shared.js +102 -0
  143. package/dist/commands/models/shared.js +24 -0
  144. package/dist/commands/onboard-auth.config-gateways.js +64 -0
  145. package/dist/commands/onboard-auth.config-litellm.js +45 -0
  146. package/dist/commands/onboard-auth.config-shared.js +116 -0
  147. package/dist/commands/onboard-config.js +16 -0
  148. package/dist/commands/onboard-non-interactive.test-helpers.js +31 -0
  149. package/dist/commands/onboard-provider-auth-flags.js +136 -0
  150. package/dist/commands/openai-codex-oauth.js +40 -0
  151. package/dist/commands/test-runtime-config-helpers.js +21 -0
  152. package/dist/commands/test-wizard-helpers.js +68 -0
  153. package/dist/commands/vllm-setup.js +66 -0
  154. package/dist/compat/legacy-names.js +2 -0
  155. package/dist/config/backup-rotation.js +19 -0
  156. package/dist/config/env-preserve.js +122 -0
  157. package/dist/config/includes-scan.js +78 -0
  158. package/dist/config/plugins-allowlist.js +13 -0
  159. package/dist/config/schema.help.js +256 -0
  160. package/dist/config/schema.hints.js +189 -0
  161. package/dist/config/schema.irc.js +20 -0
  162. package/dist/config/schema.labels.js +317 -0
  163. package/dist/config/sessions/delivery-info.js +40 -0
  164. package/dist/config/types.irc.js +1 -0
  165. package/dist/config/zod-schema.agent-defaults.js +14 -0
  166. package/dist/config/zod-schema.agent-model.js +10 -0
  167. package/dist/config/zod-schema.agent-runtime.js +14 -0
  168. package/dist/config/zod-schema.allowdeny.js +35 -0
  169. package/dist/config/zod-schema.sensitive.js +4 -0
  170. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  171. package/dist/cron/isolated-agent/skills-snapshot.js +26 -0
  172. package/dist/cron/isolated-agent/subagent-followup.js +127 -0
  173. package/dist/cron/isolated-agent.mocks.js +12 -0
  174. package/dist/cron/isolated-agent.test-setup.js +22 -0
  175. package/dist/cron/legacy-delivery.js +43 -0
  176. package/dist/cron/webhook-url.js +22 -0
  177. package/dist/daemon/arg-split.js +40 -0
  178. package/dist/daemon/exec-file.js +23 -0
  179. package/dist/daemon/output.js +6 -0
  180. package/dist/daemon/runtime-format.js +31 -0
  181. package/dist/daemon/schtasks-exec.js +4 -0
  182. package/dist/daemon/service-audit.js +22 -0
  183. package/dist/discord/client.js +41 -0
  184. package/dist/discord/components-registry.js +57 -0
  185. package/dist/discord/components.js +816 -0
  186. package/dist/discord/guilds.js +12 -0
  187. package/dist/discord/monitor/gateway-plugin.js +48 -0
  188. package/dist/discord/monitor/presence.js +30 -0
  189. package/dist/discord/send.components.js +115 -0
  190. package/dist/discord/send.shared.js +4 -0
  191. package/dist/discord/ui.js +26 -0
  192. package/dist/discord/voice-message.js +254 -0
  193. package/dist/gateway/agent-event-assistant-text.js +5 -0
  194. package/dist/gateway/agent-prompt.js +33 -0
  195. package/dist/gateway/auth-rate-limit.js +136 -0
  196. package/dist/gateway/channel-health-monitor.js +114 -0
  197. package/dist/gateway/control-ui-contract.js +1 -0
  198. package/dist/gateway/control-ui-csp.js +15 -0
  199. package/dist/gateway/gateway-config-prompts.shared.js +25 -0
  200. package/dist/gateway/http-auth-helpers.js +18 -0
  201. package/dist/gateway/http-common.js +18 -0
  202. package/dist/gateway/http-endpoint-helpers.js +27 -0
  203. package/dist/gateway/node-invoke-sanitize.js +11 -0
  204. package/dist/gateway/node-invoke-system-run-approval.js +205 -0
  205. package/dist/gateway/probe-auth.js +21 -0
  206. package/dist/gateway/protocol/index.js +7 -2
  207. package/dist/gateway/protocol/schema/mesh.js +54 -0
  208. package/dist/gateway/protocol/schema/protocol-schemas.js +7 -0
  209. package/dist/gateway/protocol/schema.js +1 -0
  210. package/dist/gateway/server/ws-connection/auth-messages.js +54 -0
  211. package/dist/gateway/server-channels.js +11 -0
  212. package/dist/gateway/server-methods/attachment-normalize.js +16 -0
  213. package/dist/gateway/server-methods/base-hash.js +8 -0
  214. package/dist/gateway/server-methods/mesh.js +700 -0
  215. package/dist/gateway/server-methods/nodes.handlers.invoke-result.js +55 -0
  216. package/dist/gateway/server-methods/restart-request.js +13 -0
  217. package/dist/gateway/server-methods/validation.js +8 -0
  218. package/dist/gateway/server.agent.gateway-server-agent.mocks.js +35 -0
  219. package/dist/gateway/server.e2e-registry-helpers.js +1 -0
  220. package/dist/gateway/server.e2e-ws-harness.js +20 -0
  221. package/dist/gateway/test-helpers.js +2 -0
  222. package/dist/gateway/test-helpers.server.js +3 -1
  223. package/dist/gateway/test-http-response.js +12 -0
  224. package/dist/gateway/test-openai-responses-model.js +20 -0
  225. package/dist/gateway/test-temp-config.js +30 -0
  226. package/dist/gateway/test-with-server.js +32 -0
  227. package/dist/hooks/bundled/bootstrap-extra-files/handler.js +46 -0
  228. package/dist/imessage/monitor/abort-handler.js +23 -0
  229. package/dist/imessage/monitor/inbound-processing.js +346 -0
  230. package/dist/imessage/monitor/parse-notification.js +64 -0
  231. package/dist/imessage/target-parsing-helpers.js +92 -0
  232. package/dist/infra/archive.js +244 -20
  233. package/dist/infra/detect-package-manager.js +26 -0
  234. package/dist/infra/exec-approvals-allowlist.js +257 -0
  235. package/dist/infra/exec-approvals-analysis.js +770 -0
  236. package/dist/infra/exec-approvals.js +13 -0
  237. package/dist/infra/file-lock.js +1 -0
  238. package/dist/infra/gemini-auth.js +39 -0
  239. package/dist/infra/heartbeat-active-hours.js +85 -0
  240. package/dist/infra/heartbeat-events-filter.js +50 -0
  241. package/dist/infra/heartbeat-runner.test-utils.js +39 -0
  242. package/dist/infra/http-body.js +265 -0
  243. package/dist/infra/install-package-dir.js +50 -0
  244. package/dist/infra/install-safe-path.js +49 -0
  245. package/dist/infra/json-files.js +49 -0
  246. package/dist/infra/jsonl-socket.js +52 -0
  247. package/dist/infra/map-size.js +14 -0
  248. package/dist/infra/net/hostname.js +7 -0
  249. package/dist/infra/npm-registry-spec.js +39 -0
  250. package/dist/infra/openclaw-root.js +109 -0
  251. package/dist/infra/outbound/delivery-queue.js +214 -0
  252. package/dist/infra/outbound/identity.js +23 -0
  253. package/dist/infra/outbound/message-action-params.js +307 -0
  254. package/dist/infra/outbound/tool-payload.js +21 -0
  255. package/dist/infra/package-json.js +23 -0
  256. package/dist/infra/pairing-files.js +19 -0
  257. package/dist/infra/pairing-token.js +9 -0
  258. package/dist/infra/path-prepend.js +51 -0
  259. package/dist/infra/path-safety.js +16 -0
  260. package/dist/infra/process-respawn.js +49 -0
  261. package/dist/infra/runtime-status.js +16 -0
  262. package/dist/infra/session-cost-usage.types.js +1 -0
  263. package/dist/infra/session-maintenance-warning.js +89 -0
  264. package/dist/infra/system-run-command.js +78 -0
  265. package/dist/infra/tmp-openclaw-dir.js +81 -0
  266. package/dist/infra/tmp-poolbot-dir.js +2 -0
  267. package/dist/infra/update-channels.js +19 -0
  268. package/dist/line/actions.js +45 -0
  269. package/dist/line/channel-access-token.js +9 -0
  270. package/dist/line/flex-templates/basic-cards.js +332 -0
  271. package/dist/line/flex-templates/common.js +18 -0
  272. package/dist/line/flex-templates/media-control-cards.js +453 -0
  273. package/dist/line/flex-templates/message.js +10 -0
  274. package/dist/line/flex-templates/schedule-cards.js +399 -0
  275. package/dist/line/flex-templates/types.js +1 -0
  276. package/dist/line/webhook-node.js +100 -0
  277. package/dist/line/webhook-utils.js +11 -0
  278. package/dist/logging/diagnostic-session-state.js +73 -0
  279. package/dist/logging/diagnostic.js +22 -0
  280. package/dist/logging/timestamps.js +14 -0
  281. package/dist/markdown/whatsapp.js +62 -0
  282. package/dist/media/base64.js +34 -0
  283. package/dist/media/local-roots.js +32 -0
  284. package/dist/media/outbound-attachment.js +10 -0
  285. package/dist/media/read-response-with-limit.js +41 -0
  286. package/dist/media/sniff-mime-from-base64.js +19 -0
  287. package/dist/media-understanding/audio-preflight.js +67 -0
  288. package/dist/media-understanding/fs.js +13 -0
  289. package/dist/media-understanding/output-extract.js +26 -0
  290. package/dist/media-understanding/providers/audio.test-helpers.js +34 -0
  291. package/dist/media-understanding/providers/google/inline-data.js +64 -0
  292. package/dist/media-understanding/providers/shared.js +7 -0
  293. package/dist/media-understanding/runner.entries.js +459 -0
  294. package/dist/memory/batch-error-utils.js +11 -0
  295. package/dist/memory/batch-http.js +27 -0
  296. package/dist/memory/batch-output.js +29 -0
  297. package/dist/memory/batch-runner.js +22 -0
  298. package/dist/memory/batch-upload.js +23 -0
  299. package/dist/memory/batch-utils.js +26 -0
  300. package/dist/memory/embeddings-debug.js +11 -0
  301. package/dist/memory/embeddings-remote-client.js +22 -0
  302. package/dist/memory/embeddings-remote-fetch.js +14 -0
  303. package/dist/memory/embeddings.js +36 -9
  304. package/dist/memory/hybrid.js +24 -5
  305. package/dist/memory/manager-embedding-ops.js +616 -0
  306. package/dist/memory/manager-sync-ops.js +953 -0
  307. package/dist/memory/manager.js +76 -28
  308. package/dist/memory/mmr.js +164 -0
  309. package/dist/memory/qmd-manager.js +1061 -0
  310. package/dist/memory/qmd-query-parser.js +107 -0
  311. package/dist/memory/qmd-scope.js +93 -0
  312. package/dist/memory/query-expansion.js +331 -0
  313. package/dist/memory/search-manager.js +0 -1
  314. package/dist/memory/sync-index.js +21 -0
  315. package/dist/memory/sync-progress.js +22 -0
  316. package/dist/memory/sync-stale.js +30 -0
  317. package/dist/memory/temporal-decay.js +119 -0
  318. package/dist/memory/test-embeddings-mock.js +16 -0
  319. package/dist/memory/test-manager-helpers.js +14 -0
  320. package/dist/memory/test-runtime-mocks.js +11 -0
  321. package/dist/node-host/invoke-browser.js +177 -0
  322. package/dist/node-host/invoke.js +685 -0
  323. package/dist/pairing/setup-code.js +285 -0
  324. package/dist/plugin-sdk/account-id.js +1 -0
  325. package/dist/plugin-sdk/agent-media-payload.js +13 -0
  326. package/dist/plugin-sdk/allow-from.js +47 -0
  327. package/dist/plugin-sdk/command-auth.js +23 -0
  328. package/dist/plugin-sdk/config-paths.js +9 -0
  329. package/dist/plugin-sdk/file-lock.js +116 -0
  330. package/dist/plugin-sdk/json-store.js +31 -0
  331. package/dist/plugin-sdk/onboarding.js +28 -0
  332. package/dist/plugin-sdk/provider-auth-result.js +29 -0
  333. package/dist/plugin-sdk/slack-message-actions.js +133 -0
  334. package/dist/plugin-sdk/status-helpers.js +35 -0
  335. package/dist/plugin-sdk/text-chunking.js +31 -0
  336. package/dist/plugin-sdk/tool-send.js +12 -0
  337. package/dist/plugin-sdk/webhook-path.js +27 -0
  338. package/dist/plugin-sdk/webhook-targets.js +34 -0
  339. package/dist/plugins/hooks.test-helpers.js +21 -0
  340. package/dist/plugins/uninstall.js +171 -0
  341. package/dist/process/kill-tree.js +98 -0
  342. package/dist/process/supervisor/adapters/child.js +143 -0
  343. package/dist/process/supervisor/adapters/env.js +13 -0
  344. package/dist/process/supervisor/adapters/pty.js +148 -0
  345. package/dist/process/supervisor/index.js +10 -0
  346. package/dist/process/supervisor/registry.js +117 -0
  347. package/dist/process/supervisor/supervisor.js +244 -0
  348. package/dist/process/supervisor/types.js +1 -0
  349. package/dist/providers/google-shared.test-helpers.js +75 -0
  350. package/dist/security/audit-channel.js +419 -0
  351. package/dist/security/audit-tool-policy.js +1 -0
  352. package/dist/security/scan-paths.js +12 -0
  353. package/dist/sessions/input-provenance.js +55 -0
  354. package/dist/sessions/session-key-utils.js +7 -0
  355. package/dist/shared/chat-content.js +31 -0
  356. package/dist/shared/chat-envelope.js +45 -0
  357. package/dist/shared/config-eval.js +117 -0
  358. package/dist/shared/device-auth.js +16 -0
  359. package/dist/shared/entry-metadata.js +9 -0
  360. package/dist/shared/entry-status.js +25 -0
  361. package/dist/shared/frontmatter.js +98 -0
  362. package/dist/shared/model-param-b.js +19 -0
  363. package/dist/shared/net/ipv4.js +17 -0
  364. package/dist/shared/node-match.js +53 -0
  365. package/dist/shared/pid-alive.js +12 -0
  366. package/dist/shared/process-scoped-map.js +10 -0
  367. package/dist/shared/requirements.js +128 -0
  368. package/dist/shared/subagents-format.js +84 -0
  369. package/dist/shared/usage-aggregates.js +28 -0
  370. package/dist/signal/monitor/mentions.js +45 -0
  371. package/dist/signal/rpc-context.js +19 -0
  372. package/dist/slack/blocks-fallback.js +76 -0
  373. package/dist/slack/blocks-input.js +40 -0
  374. package/dist/slack/draft-stream.js +106 -0
  375. package/dist/slack/message-actions.js +51 -0
  376. package/dist/slack/modal-metadata.js +32 -0
  377. package/dist/slack/monitor/events/interactions.js +462 -0
  378. package/dist/slack/monitor/room-context.js +17 -0
  379. package/dist/slack/stream-mode.js +41 -0
  380. package/dist/telegram/bot-native-command-menu.js +64 -0
  381. package/dist/telegram/bot.media.e2e-harness.js +81 -0
  382. package/dist/telegram/button-types.js +1 -0
  383. package/dist/telegram/group-access.js +65 -0
  384. package/dist/telegram/outbound-params.js +21 -0
  385. package/dist/telegram/poll-vote-cache.js +21 -0
  386. package/dist/terminal/health-style.js +36 -0
  387. package/dist/test-utils/chunk-test-helpers.js +21 -0
  388. package/dist/test-utils/env.js +72 -0
  389. package/dist/test-utils/exec-assertions.js +12 -0
  390. package/dist/test-utils/imessage-test-plugin.js +54 -0
  391. package/dist/test-utils/mock-http-response.js +17 -0
  392. package/dist/test-utils/vitest-mock-fn.js +1 -0
  393. package/dist/tts/tts-core.js +550 -0
  394. package/dist/utils/chunk-items.js +10 -0
  395. package/dist/utils/reaction-level.js +52 -0
  396. package/dist/utils/safe-json.js +22 -0
  397. package/dist/utils/with-timeout.js +14 -0
  398. package/dist/web/media.js +17 -5
  399. package/dist/whatsapp/resolve-outbound-target.js +42 -0
  400. package/dist/wizard/onboarding.completion.js +74 -0
  401. package/extensions/bluebubbles/package.json +1 -1
  402. package/extensions/bluebubbles/src/account-resolve.ts +29 -0
  403. package/extensions/bluebubbles/src/monitor-normalize.ts +796 -0
  404. package/extensions/bluebubbles/src/monitor-processing.ts +1007 -0
  405. package/extensions/bluebubbles/src/monitor-reply-cache.ts +185 -0
  406. package/extensions/bluebubbles/src/monitor-shared.ts +51 -0
  407. package/extensions/bluebubbles/src/multipart.ts +32 -0
  408. package/extensions/bluebubbles/src/send-helpers.ts +53 -0
  409. package/extensions/bluebubbles/src/test-harness.ts +50 -0
  410. package/extensions/bluebubbles/src/test-mocks.ts +11 -0
  411. package/extensions/copilot-proxy/package.json +1 -1
  412. package/extensions/device-pair/index.ts +554 -0
  413. package/extensions/diagnostics-otel/package.json +1 -1
  414. package/extensions/discord/package.json +1 -1
  415. package/extensions/discord/src/channel.js +366 -0
  416. package/extensions/discord/src/runtime.js +10 -0
  417. package/extensions/feishu/index.ts +63 -0
  418. package/extensions/feishu/src/accounts.ts +114 -0
  419. package/extensions/feishu/src/bitable.ts +739 -0
  420. package/extensions/feishu/src/bot.ts +965 -0
  421. package/extensions/feishu/src/channel.ts +351 -0
  422. package/extensions/feishu/src/client.ts +118 -0
  423. package/extensions/feishu/src/config-schema.ts +206 -0
  424. package/extensions/feishu/src/dedup.ts +33 -0
  425. package/extensions/feishu/src/directory.ts +177 -0
  426. package/extensions/feishu/src/doc-schema.ts +47 -0
  427. package/extensions/feishu/src/docx.ts +536 -0
  428. package/extensions/feishu/src/drive-schema.ts +46 -0
  429. package/extensions/feishu/src/drive.ts +227 -0
  430. package/extensions/feishu/src/dynamic-agent.ts +131 -0
  431. package/extensions/feishu/src/media.ts +449 -0
  432. package/extensions/feishu/src/mention.ts +126 -0
  433. package/extensions/feishu/src/monitor.ts +330 -0
  434. package/extensions/feishu/src/onboarding.ts +359 -0
  435. package/extensions/feishu/src/outbound.ts +55 -0
  436. package/extensions/feishu/src/perm-schema.ts +52 -0
  437. package/extensions/feishu/src/perm.ts +173 -0
  438. package/extensions/feishu/src/policy.ts +84 -0
  439. package/extensions/feishu/src/probe.ts +44 -0
  440. package/extensions/feishu/src/reactions.ts +160 -0
  441. package/extensions/feishu/src/reply-dispatcher.ts +239 -0
  442. package/extensions/feishu/src/runtime.ts +14 -0
  443. package/extensions/feishu/src/send-result.ts +29 -0
  444. package/extensions/feishu/src/send.ts +335 -0
  445. package/extensions/feishu/src/streaming-card.ts +223 -0
  446. package/extensions/feishu/src/targets.ts +78 -0
  447. package/extensions/feishu/src/tools-config.ts +21 -0
  448. package/extensions/feishu/src/types.ts +81 -0
  449. package/extensions/feishu/src/typing.ts +80 -0
  450. package/extensions/feishu/src/wiki-schema.ts +55 -0
  451. package/extensions/feishu/src/wiki.ts +232 -0
  452. package/extensions/google-antigravity-auth/package.json +1 -1
  453. package/extensions/google-gemini-cli-auth/package.json +1 -1
  454. package/extensions/googlechat/package.json +1 -1
  455. package/extensions/imessage/package.json +1 -1
  456. package/extensions/imessage/src/channel.js +253 -0
  457. package/extensions/imessage/src/runtime.js +10 -0
  458. package/extensions/irc/index.ts +17 -0
  459. package/extensions/irc/src/accounts.ts +268 -0
  460. package/extensions/irc/src/channel.ts +367 -0
  461. package/extensions/irc/src/client.ts +439 -0
  462. package/extensions/irc/src/config-schema.ts +97 -0
  463. package/extensions/irc/src/connect-options.ts +30 -0
  464. package/extensions/irc/src/control-chars.ts +22 -0
  465. package/extensions/irc/src/inbound.ts +334 -0
  466. package/extensions/irc/src/monitor.ts +147 -0
  467. package/extensions/irc/src/normalize.ts +117 -0
  468. package/extensions/irc/src/onboarding.ts +479 -0
  469. package/extensions/irc/src/policy.ts +157 -0
  470. package/extensions/irc/src/probe.ts +53 -0
  471. package/extensions/irc/src/protocol.ts +169 -0
  472. package/extensions/irc/src/runtime.ts +14 -0
  473. package/extensions/irc/src/send.ts +88 -0
  474. package/extensions/irc/src/types.ts +93 -0
  475. package/extensions/line/package.json +1 -1
  476. package/extensions/llm-task/package.json +1 -1
  477. package/extensions/lobster/package.json +1 -1
  478. package/extensions/matrix/CHANGELOG.md +5 -0
  479. package/extensions/matrix/package.json +1 -1
  480. package/extensions/matrix/src/matrix/client-bootstrap.ts +39 -0
  481. package/extensions/mattermost/package.json +1 -1
  482. package/extensions/mattermost/src/mattermost/monitor-onchar.ts +25 -0
  483. package/extensions/mattermost/src/mattermost/monitor-websocket.ts +221 -0
  484. package/extensions/mattermost/src/mattermost/reactions.ts +130 -0
  485. package/extensions/mattermost/src/mattermost/reconnect.ts +103 -0
  486. package/extensions/memory-core/package.json +1 -1
  487. package/extensions/memory-lancedb/package.json +1 -1
  488. package/extensions/minimax-portal-auth/index.ts +161 -0
  489. package/extensions/minimax-portal-auth/oauth.ts +247 -0
  490. package/extensions/msteams/CHANGELOG.md +5 -0
  491. package/extensions/msteams/package.json +1 -1
  492. package/extensions/msteams/src/file-lock.ts +1 -0
  493. package/extensions/msteams/src/graph.ts +92 -0
  494. package/extensions/msteams/src/mentions.ts +114 -0
  495. package/extensions/msteams/src/test-runtime.ts +16 -0
  496. package/extensions/nextcloud-talk/package.json +1 -1
  497. package/extensions/nostr/CHANGELOG.md +5 -0
  498. package/extensions/nostr/package.json +1 -1
  499. package/extensions/open-prose/package.json +1 -1
  500. package/extensions/openai-codex-auth/index.ts +177 -0
  501. package/extensions/phone-control/index.ts +421 -0
  502. package/extensions/shared/resolve-target-test-helpers.ts +66 -0
  503. package/extensions/signal/package.json +1 -1
  504. package/extensions/signal/src/channel.js +273 -0
  505. package/extensions/signal/src/runtime.js +10 -0
  506. package/extensions/slack/package.json +1 -1
  507. package/extensions/slack/src/channel.js +489 -0
  508. package/extensions/slack/src/runtime.js +10 -0
  509. package/extensions/talk-voice/index.ts +150 -0
  510. package/extensions/telegram/package.json +1 -1
  511. package/extensions/telegram/src/channel.js +424 -0
  512. package/extensions/telegram/src/runtime.js +10 -0
  513. package/extensions/thread-ownership/index.ts +133 -0
  514. package/extensions/tlon/package.json +1 -1
  515. package/extensions/tlon/src/account-fields.ts +25 -0
  516. package/extensions/tlon/src/urbit/base-url.ts +57 -0
  517. package/extensions/tlon/src/urbit/channel-client.ts +157 -0
  518. package/extensions/tlon/src/urbit/channel-ops.ts +164 -0
  519. package/extensions/tlon/src/urbit/context.ts +47 -0
  520. package/extensions/tlon/src/urbit/errors.ts +51 -0
  521. package/extensions/tlon/src/urbit/fetch.ts +39 -0
  522. package/extensions/twitch/CHANGELOG.md +5 -0
  523. package/extensions/twitch/package.json +1 -1
  524. package/extensions/twitch/src/test-fixtures.ts +30 -0
  525. package/extensions/voice-call/CHANGELOG.md +5 -0
  526. package/extensions/voice-call/package.json +1 -1
  527. package/extensions/voice-call/src/allowlist.ts +19 -0
  528. package/extensions/whatsapp/package.json +1 -1
  529. package/extensions/whatsapp/src/channel.js +429 -0
  530. package/extensions/whatsapp/src/runtime.js +10 -0
  531. package/extensions/zalo/CHANGELOG.md +5 -0
  532. package/extensions/zalo/package.json +1 -1
  533. package/extensions/zalouser/CHANGELOG.md +5 -0
  534. package/extensions/zalouser/package.json +1 -1
  535. package/package.json +1 -1
@@ -0,0 +1,1007 @@
1
+ import type { PoolBotConfig } from "poolbot/plugin-sdk";
2
+ import {
3
+ createReplyPrefixOptions,
4
+ logAckFailure,
5
+ logInboundDrop,
6
+ logTypingFailure,
7
+ resolveAckReaction,
8
+ resolveControlCommandGate,
9
+ } from "poolbot/plugin-sdk";
10
+ import { downloadBlueBubblesAttachment } from "./attachments.js";
11
+ import { markBlueBubblesChatRead, sendBlueBubblesTyping } from "./chat.js";
12
+ import { sendBlueBubblesMedia } from "./media-send.js";
13
+ import {
14
+ buildMessagePlaceholder,
15
+ formatGroupAllowlistEntry,
16
+ formatGroupMembers,
17
+ formatReplyTag,
18
+ parseTapbackText,
19
+ resolveGroupFlagFromChatGuid,
20
+ resolveTapbackContext,
21
+ type NormalizedWebhookMessage,
22
+ type NormalizedWebhookReaction,
23
+ } from "./monitor-normalize.js";
24
+ import {
25
+ getShortIdForUuid,
26
+ rememberBlueBubblesReplyCache,
27
+ resolveBlueBubblesMessageId,
28
+ resolveReplyContextFromCache,
29
+ } from "./monitor-reply-cache.js";
30
+ import type {
31
+ BlueBubblesCoreRuntime,
32
+ BlueBubblesRuntimeEnv,
33
+ WebhookTarget,
34
+ } from "./monitor-shared.js";
35
+ import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
36
+ import { normalizeBlueBubblesReactionInput, sendBlueBubblesReaction } from "./reactions.js";
37
+ import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";
38
+ import { formatBlueBubblesChatTarget, isAllowedBlueBubblesSender } from "./targets.js";
39
+
40
+ const DEFAULT_TEXT_LIMIT = 4000;
41
+ const invalidAckReactions = new Set<string>();
42
+ const REPLY_DIRECTIVE_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:\s*[^\]\n]+)\s*\]\]/gi;
43
+
44
+ export function logVerbose(
45
+ core: BlueBubblesCoreRuntime,
46
+ runtime: BlueBubblesRuntimeEnv,
47
+ message: string,
48
+ ): void {
49
+ if (core.logging.shouldLogVerbose()) {
50
+ runtime.log?.(`[bluebubbles] ${message}`);
51
+ }
52
+ }
53
+
54
+ function logGroupAllowlistHint(params: {
55
+ runtime: BlueBubblesRuntimeEnv;
56
+ reason: string;
57
+ entry: string | null;
58
+ chatName?: string;
59
+ accountId?: string;
60
+ }): void {
61
+ const log = params.runtime.log ?? console.log;
62
+ const nameHint = params.chatName ? ` (group name: ${params.chatName})` : "";
63
+ const accountHint = params.accountId
64
+ ? ` (or channels.bluebubbles.accounts.${params.accountId}.groupAllowFrom)`
65
+ : "";
66
+ if (params.entry) {
67
+ log(
68
+ `[bluebubbles] group message blocked (${params.reason}). Allow this group by adding ` +
69
+ `"${params.entry}" to channels.bluebubbles.groupAllowFrom${nameHint}.`,
70
+ );
71
+ log(
72
+ `[bluebubbles] add to config: channels.bluebubbles.groupAllowFrom=["${params.entry}"]${accountHint}.`,
73
+ );
74
+ return;
75
+ }
76
+ log(
77
+ `[bluebubbles] group message blocked (${params.reason}). Allow groups by setting ` +
78
+ `channels.bluebubbles.groupPolicy="open" or adding a group id to ` +
79
+ `channels.bluebubbles.groupAllowFrom${accountHint}${nameHint}.`,
80
+ );
81
+ }
82
+
83
+ function resolveBlueBubblesAckReaction(params: {
84
+ cfg: PoolBotConfig;
85
+ agentId: string;
86
+ core: BlueBubblesCoreRuntime;
87
+ runtime: BlueBubblesRuntimeEnv;
88
+ }): string | null {
89
+ const raw = resolveAckReaction(params.cfg, params.agentId).trim();
90
+ if (!raw) {
91
+ return null;
92
+ }
93
+ try {
94
+ normalizeBlueBubblesReactionInput(raw);
95
+ return raw;
96
+ } catch {
97
+ const key = raw.toLowerCase();
98
+ if (!invalidAckReactions.has(key)) {
99
+ invalidAckReactions.add(key);
100
+ logVerbose(
101
+ params.core,
102
+ params.runtime,
103
+ `ack reaction skipped (unsupported for BlueBubbles): ${raw}`,
104
+ );
105
+ }
106
+ return null;
107
+ }
108
+ }
109
+
110
+ export async function processMessage(
111
+ message: NormalizedWebhookMessage,
112
+ target: WebhookTarget,
113
+ ): Promise<void> {
114
+ const { account, config, runtime, core, statusSink } = target;
115
+ const privateApiEnabled = getCachedBlueBubblesPrivateApiStatus(account.accountId) !== false;
116
+
117
+ const groupFlag = resolveGroupFlagFromChatGuid(message.chatGuid);
118
+ const isGroup = typeof groupFlag === "boolean" ? groupFlag : message.isGroup;
119
+
120
+ const text = message.text.trim();
121
+ const attachments = message.attachments ?? [];
122
+ const placeholder = buildMessagePlaceholder(message);
123
+ // Check if text is a tapback pattern (e.g., 'Loved "hello"') and transform to emoji format
124
+ // For tapbacks, we'll append [[reply_to:N]] at the end; for regular messages, prepend it
125
+ const tapbackContext = resolveTapbackContext(message);
126
+ const tapbackParsed = parseTapbackText({
127
+ text,
128
+ emojiHint: tapbackContext?.emojiHint,
129
+ actionHint: tapbackContext?.actionHint,
130
+ requireQuoted: !tapbackContext,
131
+ });
132
+ const isTapbackMessage = Boolean(tapbackParsed);
133
+ const rawBody = tapbackParsed
134
+ ? tapbackParsed.action === "removed"
135
+ ? `removed ${tapbackParsed.emoji} reaction`
136
+ : `reacted with ${tapbackParsed.emoji}`
137
+ : text || placeholder;
138
+
139
+ const cacheMessageId = message.messageId?.trim();
140
+ let messageShortId: string | undefined;
141
+ const cacheInboundMessage = () => {
142
+ if (!cacheMessageId) {
143
+ return;
144
+ }
145
+ const cacheEntry = rememberBlueBubblesReplyCache({
146
+ accountId: account.accountId,
147
+ messageId: cacheMessageId,
148
+ chatGuid: message.chatGuid,
149
+ chatIdentifier: message.chatIdentifier,
150
+ chatId: message.chatId,
151
+ senderLabel: message.fromMe ? "me" : message.senderId,
152
+ body: rawBody,
153
+ timestamp: message.timestamp ?? Date.now(),
154
+ });
155
+ messageShortId = cacheEntry.shortId;
156
+ };
157
+
158
+ if (message.fromMe) {
159
+ // Cache from-me messages so reply context can resolve sender/body.
160
+ cacheInboundMessage();
161
+ return;
162
+ }
163
+
164
+ if (!rawBody) {
165
+ logVerbose(core, runtime, `drop: empty text sender=${message.senderId}`);
166
+ return;
167
+ }
168
+ logVerbose(
169
+ core,
170
+ runtime,
171
+ `msg sender=${message.senderId} group=${isGroup} textLen=${text.length} attachments=${attachments.length} chatGuid=${message.chatGuid ?? ""} chatId=${message.chatId ?? ""}`,
172
+ );
173
+
174
+ const dmPolicy = account.config.dmPolicy ?? "pairing";
175
+ const groupPolicy = account.config.groupPolicy ?? "allowlist";
176
+ const configAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
177
+ const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((entry) => String(entry));
178
+ const storeAllowFrom = await core.channel.pairing
179
+ .readAllowFromStore("bluebubbles")
180
+ .catch(() => []);
181
+ const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]
182
+ .map((entry) => String(entry).trim())
183
+ .filter(Boolean);
184
+ const effectiveGroupAllowFrom = [
185
+ ...(configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom),
186
+ ...storeAllowFrom,
187
+ ]
188
+ .map((entry) => String(entry).trim())
189
+ .filter(Boolean);
190
+ const groupAllowEntry = formatGroupAllowlistEntry({
191
+ chatGuid: message.chatGuid,
192
+ chatId: message.chatId ?? undefined,
193
+ chatIdentifier: message.chatIdentifier ?? undefined,
194
+ });
195
+ const groupName = message.chatName?.trim() || undefined;
196
+
197
+ if (isGroup) {
198
+ if (groupPolicy === "disabled") {
199
+ logVerbose(core, runtime, "Blocked BlueBubbles group message (groupPolicy=disabled)");
200
+ logGroupAllowlistHint({
201
+ runtime,
202
+ reason: "groupPolicy=disabled",
203
+ entry: groupAllowEntry,
204
+ chatName: groupName,
205
+ accountId: account.accountId,
206
+ });
207
+ return;
208
+ }
209
+ if (groupPolicy === "allowlist") {
210
+ if (effectiveGroupAllowFrom.length === 0) {
211
+ logVerbose(core, runtime, "Blocked BlueBubbles group message (no allowlist)");
212
+ logGroupAllowlistHint({
213
+ runtime,
214
+ reason: "groupPolicy=allowlist (empty allowlist)",
215
+ entry: groupAllowEntry,
216
+ chatName: groupName,
217
+ accountId: account.accountId,
218
+ });
219
+ return;
220
+ }
221
+ const allowed = isAllowedBlueBubblesSender({
222
+ allowFrom: effectiveGroupAllowFrom,
223
+ sender: message.senderId,
224
+ chatId: message.chatId ?? undefined,
225
+ chatGuid: message.chatGuid ?? undefined,
226
+ chatIdentifier: message.chatIdentifier ?? undefined,
227
+ });
228
+ if (!allowed) {
229
+ logVerbose(
230
+ core,
231
+ runtime,
232
+ `Blocked BlueBubbles sender ${message.senderId} (not in groupAllowFrom)`,
233
+ );
234
+ logVerbose(
235
+ core,
236
+ runtime,
237
+ `drop: group sender not allowed sender=${message.senderId} allowFrom=${effectiveGroupAllowFrom.join(",")}`,
238
+ );
239
+ logGroupAllowlistHint({
240
+ runtime,
241
+ reason: "groupPolicy=allowlist (not allowlisted)",
242
+ entry: groupAllowEntry,
243
+ chatName: groupName,
244
+ accountId: account.accountId,
245
+ });
246
+ return;
247
+ }
248
+ }
249
+ } else {
250
+ if (dmPolicy === "disabled") {
251
+ logVerbose(core, runtime, `Blocked BlueBubbles DM from ${message.senderId}`);
252
+ logVerbose(core, runtime, `drop: dmPolicy disabled sender=${message.senderId}`);
253
+ return;
254
+ }
255
+ if (dmPolicy !== "open") {
256
+ const allowed = isAllowedBlueBubblesSender({
257
+ allowFrom: effectiveAllowFrom,
258
+ sender: message.senderId,
259
+ chatId: message.chatId ?? undefined,
260
+ chatGuid: message.chatGuid ?? undefined,
261
+ chatIdentifier: message.chatIdentifier ?? undefined,
262
+ });
263
+ if (!allowed) {
264
+ if (dmPolicy === "pairing") {
265
+ const { code, created } = await core.channel.pairing.upsertPairingRequest({
266
+ channel: "bluebubbles",
267
+ id: message.senderId,
268
+ meta: { name: message.senderName },
269
+ });
270
+ runtime.log?.(
271
+ `[bluebubbles] pairing request sender=${message.senderId} created=${created}`,
272
+ );
273
+ if (created) {
274
+ logVerbose(core, runtime, `bluebubbles pairing request sender=${message.senderId}`);
275
+ try {
276
+ await sendMessageBlueBubbles(
277
+ message.senderId,
278
+ core.channel.pairing.buildPairingReply({
279
+ channel: "bluebubbles",
280
+ idLine: `Your BlueBubbles sender id: ${message.senderId}`,
281
+ code,
282
+ }),
283
+ { cfg: config, accountId: account.accountId },
284
+ );
285
+ statusSink?.({ lastOutboundAt: Date.now() });
286
+ } catch (err) {
287
+ logVerbose(
288
+ core,
289
+ runtime,
290
+ `bluebubbles pairing reply failed for ${message.senderId}: ${String(err)}`,
291
+ );
292
+ runtime.error?.(
293
+ `[bluebubbles] pairing reply failed sender=${message.senderId}: ${String(err)}`,
294
+ );
295
+ }
296
+ }
297
+ } else {
298
+ logVerbose(
299
+ core,
300
+ runtime,
301
+ `Blocked unauthorized BlueBubbles sender ${message.senderId} (dmPolicy=${dmPolicy})`,
302
+ );
303
+ logVerbose(
304
+ core,
305
+ runtime,
306
+ `drop: dm sender not allowed sender=${message.senderId} allowFrom=${effectiveAllowFrom.join(",")}`,
307
+ );
308
+ }
309
+ return;
310
+ }
311
+ }
312
+ }
313
+
314
+ const chatId = message.chatId ?? undefined;
315
+ const chatGuid = message.chatGuid ?? undefined;
316
+ const chatIdentifier = message.chatIdentifier ?? undefined;
317
+ const peerId = isGroup
318
+ ? (chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group"))
319
+ : message.senderId;
320
+
321
+ const route = core.channel.routing.resolveAgentRoute({
322
+ cfg: config,
323
+ channel: "bluebubbles",
324
+ accountId: account.accountId,
325
+ peer: {
326
+ kind: isGroup ? "group" : "direct",
327
+ id: peerId,
328
+ },
329
+ });
330
+
331
+ // Mention gating for group chats (parity with iMessage/WhatsApp)
332
+ const messageText = text;
333
+ const mentionRegexes = core.channel.mentions.buildMentionRegexes(config, route.agentId);
334
+ const wasMentioned = isGroup
335
+ ? core.channel.mentions.matchesMentionPatterns(messageText, mentionRegexes)
336
+ : true;
337
+ const canDetectMention = mentionRegexes.length > 0;
338
+ const requireMention = core.channel.groups.resolveRequireMention({
339
+ cfg: config,
340
+ channel: "bluebubbles",
341
+ groupId: peerId,
342
+ accountId: account.accountId,
343
+ });
344
+
345
+ // Command gating (parity with iMessage/WhatsApp)
346
+ const useAccessGroups = config.commands?.useAccessGroups !== false;
347
+ const hasControlCmd = core.channel.text.hasControlCommand(messageText, config);
348
+ const ownerAllowedForCommands =
349
+ effectiveAllowFrom.length > 0
350
+ ? isAllowedBlueBubblesSender({
351
+ allowFrom: effectiveAllowFrom,
352
+ sender: message.senderId,
353
+ chatId: message.chatId ?? undefined,
354
+ chatGuid: message.chatGuid ?? undefined,
355
+ chatIdentifier: message.chatIdentifier ?? undefined,
356
+ })
357
+ : false;
358
+ const groupAllowedForCommands =
359
+ effectiveGroupAllowFrom.length > 0
360
+ ? isAllowedBlueBubblesSender({
361
+ allowFrom: effectiveGroupAllowFrom,
362
+ sender: message.senderId,
363
+ chatId: message.chatId ?? undefined,
364
+ chatGuid: message.chatGuid ?? undefined,
365
+ chatIdentifier: message.chatIdentifier ?? undefined,
366
+ })
367
+ : false;
368
+ const dmAuthorized = dmPolicy === "open" || ownerAllowedForCommands;
369
+ const commandGate = resolveControlCommandGate({
370
+ useAccessGroups,
371
+ authorizers: [
372
+ { configured: effectiveAllowFrom.length > 0, allowed: ownerAllowedForCommands },
373
+ { configured: effectiveGroupAllowFrom.length > 0, allowed: groupAllowedForCommands },
374
+ ],
375
+ allowTextCommands: true,
376
+ hasControlCommand: hasControlCmd,
377
+ });
378
+ const commandAuthorized = isGroup ? commandGate.commandAuthorized : dmAuthorized;
379
+
380
+ // Block control commands from unauthorized senders in groups
381
+ if (isGroup && commandGate.shouldBlock) {
382
+ logInboundDrop({
383
+ log: (msg) => logVerbose(core, runtime, msg),
384
+ channel: "bluebubbles",
385
+ reason: "control command (unauthorized)",
386
+ target: message.senderId,
387
+ });
388
+ return;
389
+ }
390
+
391
+ // Allow control commands to bypass mention gating when authorized (parity with iMessage)
392
+ const shouldBypassMention =
393
+ isGroup && requireMention && !wasMentioned && commandAuthorized && hasControlCmd;
394
+ const effectiveWasMentioned = wasMentioned || shouldBypassMention;
395
+
396
+ // Skip group messages that require mention but weren't mentioned
397
+ if (isGroup && requireMention && canDetectMention && !wasMentioned && !shouldBypassMention) {
398
+ logVerbose(core, runtime, `bluebubbles: skipping group message (no mention)`);
399
+ return;
400
+ }
401
+
402
+ // Cache allowed inbound messages so later replies can resolve sender/body without
403
+ // surfacing dropped content (allowlist/mention/command gating).
404
+ cacheInboundMessage();
405
+
406
+ const baseUrl = account.config.serverUrl?.trim();
407
+ const password = account.config.password?.trim();
408
+ const maxBytes =
409
+ account.config.mediaMaxMb && account.config.mediaMaxMb > 0
410
+ ? account.config.mediaMaxMb * 1024 * 1024
411
+ : 8 * 1024 * 1024;
412
+
413
+ let mediaUrls: string[] = [];
414
+ let mediaPaths: string[] = [];
415
+ let mediaTypes: string[] = [];
416
+ if (attachments.length > 0) {
417
+ if (!baseUrl || !password) {
418
+ logVerbose(core, runtime, "attachment download skipped (missing serverUrl/password)");
419
+ } else {
420
+ for (const attachment of attachments) {
421
+ if (!attachment.guid) {
422
+ continue;
423
+ }
424
+ if (attachment.totalBytes && attachment.totalBytes > maxBytes) {
425
+ logVerbose(
426
+ core,
427
+ runtime,
428
+ `attachment too large guid=${attachment.guid} bytes=${attachment.totalBytes}`,
429
+ );
430
+ continue;
431
+ }
432
+ try {
433
+ const downloaded = await downloadBlueBubblesAttachment(attachment, {
434
+ cfg: config,
435
+ accountId: account.accountId,
436
+ maxBytes,
437
+ });
438
+ const saved = await core.channel.media.saveMediaBuffer(
439
+ Buffer.from(downloaded.buffer),
440
+ downloaded.contentType,
441
+ "inbound",
442
+ maxBytes,
443
+ );
444
+ mediaPaths.push(saved.path);
445
+ mediaUrls.push(saved.path);
446
+ if (saved.contentType) {
447
+ mediaTypes.push(saved.contentType);
448
+ }
449
+ } catch (err) {
450
+ logVerbose(
451
+ core,
452
+ runtime,
453
+ `attachment download failed guid=${attachment.guid} err=${String(err)}`,
454
+ );
455
+ }
456
+ }
457
+ }
458
+ }
459
+ let replyToId = message.replyToId;
460
+ let replyToBody = message.replyToBody;
461
+ let replyToSender = message.replyToSender;
462
+ let replyToShortId: string | undefined;
463
+
464
+ if (isTapbackMessage && tapbackContext?.replyToId) {
465
+ replyToId = tapbackContext.replyToId;
466
+ }
467
+
468
+ if (replyToId) {
469
+ const cached = resolveReplyContextFromCache({
470
+ accountId: account.accountId,
471
+ replyToId,
472
+ chatGuid: message.chatGuid,
473
+ chatIdentifier: message.chatIdentifier,
474
+ chatId: message.chatId,
475
+ });
476
+ if (cached) {
477
+ if (!replyToBody && cached.body) {
478
+ replyToBody = cached.body;
479
+ }
480
+ if (!replyToSender && cached.senderLabel) {
481
+ replyToSender = cached.senderLabel;
482
+ }
483
+ replyToShortId = cached.shortId;
484
+ if (core.logging.shouldLogVerbose()) {
485
+ const preview = (cached.body ?? "").replace(/\s+/g, " ").slice(0, 120);
486
+ logVerbose(
487
+ core,
488
+ runtime,
489
+ `reply-context cache hit replyToId=${replyToId} sender=${replyToSender ?? ""} body="${preview}"`,
490
+ );
491
+ }
492
+ }
493
+ }
494
+
495
+ // If no cached short ID, try to get one from the UUID directly
496
+ if (replyToId && !replyToShortId) {
497
+ replyToShortId = getShortIdForUuid(replyToId);
498
+ }
499
+
500
+ // Use inline [[reply_to:N]] tag format
501
+ // For tapbacks/reactions: append at end (e.g., "reacted with ❤️ [[reply_to:4]]")
502
+ // For regular replies: prepend at start (e.g., "[[reply_to:4]] Awesome")
503
+ const replyTag = formatReplyTag({ replyToId, replyToShortId });
504
+ const baseBody = replyTag
505
+ ? isTapbackMessage
506
+ ? `${rawBody} ${replyTag}`
507
+ : `${replyTag} ${rawBody}`
508
+ : rawBody;
509
+ // Build fromLabel the same way as iMessage/Signal (formatInboundFromLabel):
510
+ // group label + id for groups, sender for DMs.
511
+ // The sender identity is included in the envelope body via formatInboundEnvelope.
512
+ const senderLabel = message.senderName || `user:${message.senderId}`;
513
+ const fromLabel = isGroup
514
+ ? `${message.chatName?.trim() || "Group"} id:${peerId}`
515
+ : senderLabel !== message.senderId
516
+ ? `${senderLabel} id:${message.senderId}`
517
+ : senderLabel;
518
+ const groupSubject = isGroup ? message.chatName?.trim() || undefined : undefined;
519
+ const groupMembers = isGroup
520
+ ? formatGroupMembers({
521
+ participants: message.participants,
522
+ fallback: message.senderId ? { id: message.senderId, name: message.senderName } : undefined,
523
+ })
524
+ : undefined;
525
+ const storePath = core.channel.session.resolveStorePath(config.session?.store, {
526
+ agentId: route.agentId,
527
+ });
528
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
529
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
530
+ storePath,
531
+ sessionKey: route.sessionKey,
532
+ });
533
+ const body = core.channel.reply.formatInboundEnvelope({
534
+ channel: "BlueBubbles",
535
+ from: fromLabel,
536
+ timestamp: message.timestamp,
537
+ previousTimestamp,
538
+ envelope: envelopeOptions,
539
+ body: baseBody,
540
+ chatType: isGroup ? "group" : "direct",
541
+ sender: { name: message.senderName || undefined, id: message.senderId },
542
+ });
543
+ let chatGuidForActions = chatGuid;
544
+ if (!chatGuidForActions && baseUrl && password) {
545
+ const resolveTarget =
546
+ isGroup && (chatId || chatIdentifier)
547
+ ? chatId
548
+ ? ({ kind: "chat_id", chatId } as const)
549
+ : ({ kind: "chat_identifier", chatIdentifier: chatIdentifier ?? "" } as const)
550
+ : ({ kind: "handle", address: message.senderId } as const);
551
+ if (resolveTarget.kind !== "chat_identifier" || resolveTarget.chatIdentifier) {
552
+ chatGuidForActions =
553
+ (await resolveChatGuidForTarget({
554
+ baseUrl,
555
+ password,
556
+ target: resolveTarget,
557
+ })) ?? undefined;
558
+ }
559
+ }
560
+
561
+ const ackReactionScope = config.messages?.ackReactionScope ?? "group-mentions";
562
+ const removeAckAfterReply = config.messages?.removeAckAfterReply ?? false;
563
+ const ackReactionValue = resolveBlueBubblesAckReaction({
564
+ cfg: config,
565
+ agentId: route.agentId,
566
+ core,
567
+ runtime,
568
+ });
569
+ const shouldAckReaction = () =>
570
+ Boolean(
571
+ ackReactionValue &&
572
+ core.channel.reactions.shouldAckReaction({
573
+ scope: ackReactionScope,
574
+ isDirect: !isGroup,
575
+ isGroup,
576
+ isMentionableGroup: isGroup,
577
+ requireMention: Boolean(requireMention),
578
+ canDetectMention,
579
+ effectiveWasMentioned,
580
+ shouldBypassMention,
581
+ }),
582
+ );
583
+ const ackMessageId = message.messageId?.trim() || "";
584
+ const ackReactionPromise =
585
+ shouldAckReaction() && ackMessageId && chatGuidForActions && ackReactionValue
586
+ ? sendBlueBubblesReaction({
587
+ chatGuid: chatGuidForActions,
588
+ messageGuid: ackMessageId,
589
+ emoji: ackReactionValue,
590
+ opts: { cfg: config, accountId: account.accountId },
591
+ }).then(
592
+ () => true,
593
+ (err) => {
594
+ logVerbose(
595
+ core,
596
+ runtime,
597
+ `ack reaction failed chatGuid=${chatGuidForActions} msg=${ackMessageId}: ${String(err)}`,
598
+ );
599
+ return false;
600
+ },
601
+ )
602
+ : null;
603
+
604
+ // Respect sendReadReceipts config (parity with WhatsApp)
605
+ const sendReadReceipts = account.config.sendReadReceipts !== false;
606
+ if (chatGuidForActions && baseUrl && password && sendReadReceipts) {
607
+ try {
608
+ await markBlueBubblesChatRead(chatGuidForActions, {
609
+ cfg: config,
610
+ accountId: account.accountId,
611
+ });
612
+ logVerbose(core, runtime, `marked read chatGuid=${chatGuidForActions}`);
613
+ } catch (err) {
614
+ runtime.error?.(`[bluebubbles] mark read failed: ${String(err)}`);
615
+ }
616
+ } else if (!sendReadReceipts) {
617
+ logVerbose(core, runtime, "mark read skipped (sendReadReceipts=false)");
618
+ } else {
619
+ logVerbose(core, runtime, "mark read skipped (missing chatGuid or credentials)");
620
+ }
621
+
622
+ const outboundTarget = isGroup
623
+ ? formatBlueBubblesChatTarget({
624
+ chatId,
625
+ chatGuid: chatGuidForActions ?? chatGuid,
626
+ chatIdentifier,
627
+ }) || peerId
628
+ : chatGuidForActions
629
+ ? formatBlueBubblesChatTarget({ chatGuid: chatGuidForActions })
630
+ : message.senderId;
631
+
632
+ const maybeEnqueueOutboundMessageId = (messageId?: string, snippet?: string) => {
633
+ const trimmed = messageId?.trim();
634
+ if (!trimmed || trimmed === "ok" || trimmed === "unknown") {
635
+ return;
636
+ }
637
+ // Cache outbound message to get short ID
638
+ const cacheEntry = rememberBlueBubblesReplyCache({
639
+ accountId: account.accountId,
640
+ messageId: trimmed,
641
+ chatGuid: chatGuidForActions ?? chatGuid,
642
+ chatIdentifier,
643
+ chatId,
644
+ senderLabel: "me",
645
+ body: snippet ?? "",
646
+ timestamp: Date.now(),
647
+ });
648
+ const displayId = cacheEntry.shortId || trimmed;
649
+ const preview = snippet ? ` "${snippet.slice(0, 12)}${snippet.length > 12 ? "…" : ""}"` : "";
650
+ core.system.enqueueSystemEvent(`Assistant sent${preview} [message_id:${displayId}]`, {
651
+ sessionKey: route.sessionKey,
652
+ contextKey: `bluebubbles:outbound:${outboundTarget}:${trimmed}`,
653
+ });
654
+ };
655
+ const sanitizeReplyDirectiveText = (value: string): string => {
656
+ if (privateApiEnabled) {
657
+ return value;
658
+ }
659
+ return value
660
+ .replace(REPLY_DIRECTIVE_TAG_RE, " ")
661
+ .replace(/[ \t]+/g, " ")
662
+ .trim();
663
+ };
664
+
665
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
666
+ Body: body,
667
+ BodyForAgent: rawBody,
668
+ RawBody: rawBody,
669
+ CommandBody: rawBody,
670
+ BodyForCommands: rawBody,
671
+ MediaUrl: mediaUrls[0],
672
+ MediaUrls: mediaUrls.length > 0 ? mediaUrls : undefined,
673
+ MediaPath: mediaPaths[0],
674
+ MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined,
675
+ MediaType: mediaTypes[0],
676
+ MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined,
677
+ From: isGroup ? `group:${peerId}` : `bluebubbles:${message.senderId}`,
678
+ To: `bluebubbles:${outboundTarget}`,
679
+ SessionKey: route.sessionKey,
680
+ AccountId: route.accountId,
681
+ ChatType: isGroup ? "group" : "direct",
682
+ ConversationLabel: fromLabel,
683
+ // Use short ID for token savings (agent can use this to reference the message)
684
+ ReplyToId: replyToShortId || replyToId,
685
+ ReplyToIdFull: replyToId,
686
+ ReplyToBody: replyToBody,
687
+ ReplyToSender: replyToSender,
688
+ GroupSubject: groupSubject,
689
+ GroupMembers: groupMembers,
690
+ SenderName: message.senderName || undefined,
691
+ SenderId: message.senderId,
692
+ Provider: "bluebubbles",
693
+ Surface: "bluebubbles",
694
+ // Use short ID for token savings (agent can use this to reference the message)
695
+ MessageSid: messageShortId || message.messageId,
696
+ MessageSidFull: message.messageId,
697
+ Timestamp: message.timestamp,
698
+ OriginatingChannel: "bluebubbles",
699
+ OriginatingTo: `bluebubbles:${outboundTarget}`,
700
+ WasMentioned: effectiveWasMentioned,
701
+ CommandAuthorized: commandAuthorized,
702
+ });
703
+
704
+ let sentMessage = false;
705
+ let streamingActive = false;
706
+ let typingRestartTimer: NodeJS.Timeout | undefined;
707
+ const typingRestartDelayMs = 150;
708
+ const clearTypingRestartTimer = () => {
709
+ if (typingRestartTimer) {
710
+ clearTimeout(typingRestartTimer);
711
+ typingRestartTimer = undefined;
712
+ }
713
+ };
714
+ const restartTypingSoon = () => {
715
+ if (!streamingActive || !chatGuidForActions || !baseUrl || !password) {
716
+ return;
717
+ }
718
+ clearTypingRestartTimer();
719
+ typingRestartTimer = setTimeout(() => {
720
+ typingRestartTimer = undefined;
721
+ if (!streamingActive) {
722
+ return;
723
+ }
724
+ sendBlueBubblesTyping(chatGuidForActions, true, {
725
+ cfg: config,
726
+ accountId: account.accountId,
727
+ }).catch((err) => {
728
+ runtime.error?.(`[bluebubbles] typing restart failed: ${String(err)}`);
729
+ });
730
+ }, typingRestartDelayMs);
731
+ };
732
+ try {
733
+ const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
734
+ cfg: config,
735
+ agentId: route.agentId,
736
+ channel: "bluebubbles",
737
+ accountId: account.accountId,
738
+ });
739
+ await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
740
+ ctx: ctxPayload,
741
+ cfg: config,
742
+ dispatcherOptions: {
743
+ ...prefixOptions,
744
+ deliver: async (payload, info) => {
745
+ const rawReplyToId =
746
+ privateApiEnabled && typeof payload.replyToId === "string"
747
+ ? payload.replyToId.trim()
748
+ : "";
749
+ // Resolve short ID (e.g., "5") to full UUID
750
+ const replyToMessageGuid = rawReplyToId
751
+ ? resolveBlueBubblesMessageId(rawReplyToId, { requireKnownShortId: true })
752
+ : "";
753
+ const mediaList = payload.mediaUrls?.length
754
+ ? payload.mediaUrls
755
+ : payload.mediaUrl
756
+ ? [payload.mediaUrl]
757
+ : [];
758
+ if (mediaList.length > 0) {
759
+ const tableMode = core.channel.text.resolveMarkdownTableMode({
760
+ cfg: config,
761
+ channel: "bluebubbles",
762
+ accountId: account.accountId,
763
+ });
764
+ const text = sanitizeReplyDirectiveText(
765
+ core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
766
+ );
767
+ let first = true;
768
+ for (const mediaUrl of mediaList) {
769
+ const caption = first ? text : undefined;
770
+ first = false;
771
+ const result = await sendBlueBubblesMedia({
772
+ cfg: config,
773
+ to: outboundTarget,
774
+ mediaUrl,
775
+ caption: caption ?? undefined,
776
+ replyToId: replyToMessageGuid || null,
777
+ accountId: account.accountId,
778
+ });
779
+ const cachedBody = (caption ?? "").trim() || "<media:attachment>";
780
+ maybeEnqueueOutboundMessageId(result.messageId, cachedBody);
781
+ sentMessage = true;
782
+ statusSink?.({ lastOutboundAt: Date.now() });
783
+ if (info.kind === "block") {
784
+ restartTypingSoon();
785
+ }
786
+ }
787
+ return;
788
+ }
789
+
790
+ const textLimit =
791
+ account.config.textChunkLimit && account.config.textChunkLimit > 0
792
+ ? account.config.textChunkLimit
793
+ : DEFAULT_TEXT_LIMIT;
794
+ const chunkMode = account.config.chunkMode ?? "length";
795
+ const tableMode = core.channel.text.resolveMarkdownTableMode({
796
+ cfg: config,
797
+ channel: "bluebubbles",
798
+ accountId: account.accountId,
799
+ });
800
+ const text = sanitizeReplyDirectiveText(
801
+ core.channel.text.convertMarkdownTables(payload.text ?? "", tableMode),
802
+ );
803
+ const chunks =
804
+ chunkMode === "newline"
805
+ ? core.channel.text.chunkTextWithMode(text, textLimit, chunkMode)
806
+ : core.channel.text.chunkMarkdownText(text, textLimit);
807
+ if (!chunks.length && text) {
808
+ chunks.push(text);
809
+ }
810
+ if (!chunks.length) {
811
+ return;
812
+ }
813
+ for (const chunk of chunks) {
814
+ const result = await sendMessageBlueBubbles(outboundTarget, chunk, {
815
+ cfg: config,
816
+ accountId: account.accountId,
817
+ replyToMessageGuid: replyToMessageGuid || undefined,
818
+ });
819
+ maybeEnqueueOutboundMessageId(result.messageId, chunk);
820
+ sentMessage = true;
821
+ statusSink?.({ lastOutboundAt: Date.now() });
822
+ if (info.kind === "block") {
823
+ restartTypingSoon();
824
+ }
825
+ }
826
+ },
827
+ onReplyStart: async () => {
828
+ if (!chatGuidForActions) {
829
+ return;
830
+ }
831
+ if (!baseUrl || !password) {
832
+ return;
833
+ }
834
+ streamingActive = true;
835
+ clearTypingRestartTimer();
836
+ try {
837
+ await sendBlueBubblesTyping(chatGuidForActions, true, {
838
+ cfg: config,
839
+ accountId: account.accountId,
840
+ });
841
+ } catch (err) {
842
+ runtime.error?.(`[bluebubbles] typing start failed: ${String(err)}`);
843
+ }
844
+ },
845
+ onIdle: async () => {
846
+ if (!chatGuidForActions) {
847
+ return;
848
+ }
849
+ if (!baseUrl || !password) {
850
+ return;
851
+ }
852
+ // Intentionally no-op for block streaming. We stop typing in finally
853
+ // after the run completes to avoid flicker between paragraph blocks.
854
+ },
855
+ onError: (err, info) => {
856
+ runtime.error?.(`BlueBubbles ${info.kind} reply failed: ${String(err)}`);
857
+ },
858
+ },
859
+ replyOptions: {
860
+ onModelSelected,
861
+ disableBlockStreaming:
862
+ typeof account.config.blockStreaming === "boolean"
863
+ ? !account.config.blockStreaming
864
+ : undefined,
865
+ },
866
+ });
867
+ } finally {
868
+ const shouldStopTyping =
869
+ Boolean(chatGuidForActions && baseUrl && password) && (streamingActive || !sentMessage);
870
+ streamingActive = false;
871
+ clearTypingRestartTimer();
872
+ if (sentMessage && chatGuidForActions && ackMessageId) {
873
+ core.channel.reactions.removeAckReactionAfterReply({
874
+ removeAfterReply: removeAckAfterReply,
875
+ ackReactionPromise,
876
+ ackReactionValue: ackReactionValue ?? null,
877
+ remove: () =>
878
+ sendBlueBubblesReaction({
879
+ chatGuid: chatGuidForActions,
880
+ messageGuid: ackMessageId,
881
+ emoji: ackReactionValue ?? "",
882
+ remove: true,
883
+ opts: { cfg: config, accountId: account.accountId },
884
+ }),
885
+ onError: (err) => {
886
+ logAckFailure({
887
+ log: (msg) => logVerbose(core, runtime, msg),
888
+ channel: "bluebubbles",
889
+ target: `${chatGuidForActions}/${ackMessageId}`,
890
+ error: err,
891
+ });
892
+ },
893
+ });
894
+ }
895
+ if (shouldStopTyping && chatGuidForActions) {
896
+ // Stop typing after streaming completes to avoid a stuck indicator.
897
+ sendBlueBubblesTyping(chatGuidForActions, false, {
898
+ cfg: config,
899
+ accountId: account.accountId,
900
+ }).catch((err) => {
901
+ logTypingFailure({
902
+ log: (msg) => logVerbose(core, runtime, msg),
903
+ channel: "bluebubbles",
904
+ action: "stop",
905
+ target: chatGuidForActions,
906
+ error: err,
907
+ });
908
+ });
909
+ }
910
+ }
911
+ }
912
+
913
+ export async function processReaction(
914
+ reaction: NormalizedWebhookReaction,
915
+ target: WebhookTarget,
916
+ ): Promise<void> {
917
+ const { account, config, runtime, core } = target;
918
+ if (reaction.fromMe) {
919
+ return;
920
+ }
921
+
922
+ const dmPolicy = account.config.dmPolicy ?? "pairing";
923
+ const groupPolicy = account.config.groupPolicy ?? "allowlist";
924
+ const configAllowFrom = (account.config.allowFrom ?? []).map((entry) => String(entry));
925
+ const configGroupAllowFrom = (account.config.groupAllowFrom ?? []).map((entry) => String(entry));
926
+ const storeAllowFrom = await core.channel.pairing
927
+ .readAllowFromStore("bluebubbles")
928
+ .catch(() => []);
929
+ const effectiveAllowFrom = [...configAllowFrom, ...storeAllowFrom]
930
+ .map((entry) => String(entry).trim())
931
+ .filter(Boolean);
932
+ const effectiveGroupAllowFrom = [
933
+ ...(configGroupAllowFrom.length > 0 ? configGroupAllowFrom : configAllowFrom),
934
+ ...storeAllowFrom,
935
+ ]
936
+ .map((entry) => String(entry).trim())
937
+ .filter(Boolean);
938
+
939
+ if (reaction.isGroup) {
940
+ if (groupPolicy === "disabled") {
941
+ return;
942
+ }
943
+ if (groupPolicy === "allowlist") {
944
+ if (effectiveGroupAllowFrom.length === 0) {
945
+ return;
946
+ }
947
+ const allowed = isAllowedBlueBubblesSender({
948
+ allowFrom: effectiveGroupAllowFrom,
949
+ sender: reaction.senderId,
950
+ chatId: reaction.chatId ?? undefined,
951
+ chatGuid: reaction.chatGuid ?? undefined,
952
+ chatIdentifier: reaction.chatIdentifier ?? undefined,
953
+ });
954
+ if (!allowed) {
955
+ return;
956
+ }
957
+ }
958
+ } else {
959
+ if (dmPolicy === "disabled") {
960
+ return;
961
+ }
962
+ if (dmPolicy !== "open") {
963
+ const allowed = isAllowedBlueBubblesSender({
964
+ allowFrom: effectiveAllowFrom,
965
+ sender: reaction.senderId,
966
+ chatId: reaction.chatId ?? undefined,
967
+ chatGuid: reaction.chatGuid ?? undefined,
968
+ chatIdentifier: reaction.chatIdentifier ?? undefined,
969
+ });
970
+ if (!allowed) {
971
+ return;
972
+ }
973
+ }
974
+ }
975
+
976
+ const chatId = reaction.chatId ?? undefined;
977
+ const chatGuid = reaction.chatGuid ?? undefined;
978
+ const chatIdentifier = reaction.chatIdentifier ?? undefined;
979
+ const peerId = reaction.isGroup
980
+ ? (chatGuid ?? chatIdentifier ?? (chatId ? String(chatId) : "group"))
981
+ : reaction.senderId;
982
+
983
+ const route = core.channel.routing.resolveAgentRoute({
984
+ cfg: config,
985
+ channel: "bluebubbles",
986
+ accountId: account.accountId,
987
+ peer: {
988
+ kind: reaction.isGroup ? "group" : "direct",
989
+ id: peerId,
990
+ },
991
+ });
992
+
993
+ const senderLabel = reaction.senderName || reaction.senderId;
994
+ const chatLabel = reaction.isGroup ? ` in group:${peerId}` : "";
995
+ // Use short ID for token savings
996
+ const messageDisplayId = getShortIdForUuid(reaction.messageId) || reaction.messageId;
997
+ // Format: "Tyler reacted with ❤️ [[reply_to:5]]" or "Tyler removed ❤️ reaction [[reply_to:5]]"
998
+ const text =
999
+ reaction.action === "removed"
1000
+ ? `${senderLabel} removed ${reaction.emoji} reaction [[reply_to:${messageDisplayId}]]${chatLabel}`
1001
+ : `${senderLabel} reacted with ${reaction.emoji} [[reply_to:${messageDisplayId}]]${chatLabel}`;
1002
+ core.system.enqueueSystemEvent(text, {
1003
+ sessionKey: route.sessionKey,
1004
+ contextKey: `bluebubbles:reaction:${reaction.action}:${peerId}:${reaction.messageId}:${reaction.senderId}:${reaction.emoji}`,
1005
+ });
1006
+ logVerbose(core, runtime, `reaction event enqueued: ${text}`);
1007
+ }