@poolzin/pool-bot 2026.2.17 → 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 (469) hide show
  1. package/CHANGELOG.md +17 -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.resolve-auth-profile-order.fixtures.js +23 -0
  5. package/dist/agents/bash-tools.exec-runtime.js +438 -0
  6. package/dist/agents/bash-tools.shared.js +6 -0
  7. package/dist/agents/cli-runner/reliability.js +61 -0
  8. package/dist/agents/cli-watchdog-defaults.js +11 -0
  9. package/dist/agents/command-poll-backoff.js +63 -0
  10. package/dist/agents/current-time.js +16 -0
  11. package/dist/agents/model-alias-lines.js +18 -0
  12. package/dist/agents/model-auth-label.js +61 -0
  13. package/dist/agents/models-config.e2e-harness.js +115 -0
  14. package/dist/agents/ollama-stream.js +11 -3
  15. package/dist/agents/openclaw-tools.js +135 -0
  16. package/dist/agents/pi-auth-json.js +118 -0
  17. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +147 -0
  18. package/dist/agents/pi-embedded-subscribe.e2e-harness.js +90 -0
  19. package/dist/agents/pi-embedded-subscribe.handlers.compaction.js +63 -0
  20. package/dist/agents/pi-embedded-subscribe.handlers.tools.media.test-helpers.js +30 -0
  21. package/dist/agents/pi-extensions/session-manager-runtime-registry.js +23 -0
  22. package/dist/agents/pi-tools.js +2 -0
  23. package/dist/agents/queued-file-writer.js +22 -0
  24. package/dist/agents/sandbox/docker.js +133 -40
  25. package/dist/agents/sandbox/fs-bridge.js +146 -0
  26. package/dist/agents/sandbox/fs-paths.js +205 -0
  27. package/dist/agents/sandbox/hash.js +4 -0
  28. package/dist/agents/sandbox-paths.js +3 -0
  29. package/dist/agents/session-dirs.js +20 -0
  30. package/dist/agents/skills/filter.js +24 -0
  31. package/dist/agents/skills/tools-dir.js +9 -0
  32. package/dist/agents/skills-install-download.js +290 -0
  33. package/dist/agents/skills-install-output.js +30 -0
  34. package/dist/agents/skills-install.download-test-utils.js +36 -0
  35. package/dist/agents/skills.e2e-test-helpers.js +13 -0
  36. package/dist/agents/subagent-announce-queue.js +59 -15
  37. package/dist/agents/subagent-depth.js +137 -0
  38. package/dist/agents/subagent-registry.js +448 -96
  39. package/dist/agents/subagent-spawn.js +262 -0
  40. package/dist/agents/test-helpers/fast-tool-stubs.js +18 -0
  41. package/dist/agents/test-helpers/host-sandbox-fs-bridge.js +74 -0
  42. package/dist/agents/tool-display-common.js +782 -0
  43. package/dist/agents/tools/image-tool.js +1 -1
  44. package/dist/agents/tools/sessions-access.js +178 -0
  45. package/dist/agents/tools/sessions-resolution.js +206 -0
  46. package/dist/agents/tools/subagents-tool.js +616 -0
  47. package/dist/agents/workspace-dir.js +18 -0
  48. package/dist/agents/workspace-dirs.js +14 -0
  49. package/dist/agents/workspace.js +70 -0
  50. package/dist/auto-reply/heartbeat-reply-payload.js +18 -0
  51. package/dist/auto-reply/reply/commands-export-session.js +163 -0
  52. package/dist/auto-reply/reply/commands-mesh.js +245 -0
  53. package/dist/auto-reply/reply/commands-setunset.js +28 -0
  54. package/dist/auto-reply/reply/commands-slash-parse.js +31 -0
  55. package/dist/auto-reply/reply/commands-system-prompt.js +117 -0
  56. package/dist/auto-reply/reply/directive-handling.levels.js +17 -0
  57. package/dist/auto-reply/reply/directive-handling.params.js +1 -0
  58. package/dist/auto-reply/reply/directive-parsing.js +36 -0
  59. package/dist/auto-reply/reply/dispatcher-registry.js +43 -0
  60. package/dist/auto-reply/reply/elevated-unavailable.js +20 -0
  61. package/dist/auto-reply/reply/reply-delivery.js +92 -0
  62. package/dist/auto-reply/reply/session-reset-prompt.js +1 -0
  63. package/dist/auto-reply/reply/session-run-accounting.js +33 -0
  64. package/dist/auto-reply/reply.directive.directive-behavior.e2e-harness.js +115 -0
  65. package/dist/auto-reply/reply.directive.directive-behavior.e2e-mocks.js +12 -0
  66. package/dist/browser/bridge-auth-registry.js +26 -0
  67. package/dist/browser/client-actions-url.js +10 -0
  68. package/dist/browser/control-auth.js +73 -0
  69. package/dist/browser/csrf.js +64 -0
  70. package/dist/browser/http-auth.js +52 -0
  71. package/dist/browser/paths.js +37 -0
  72. package/dist/browser/proxy-files.js +32 -0
  73. package/dist/browser/pw-ai-state.js +7 -0
  74. package/dist/browser/resolved-config-refresh.js +42 -0
  75. package/dist/browser/routes/path-output.js +1 -0
  76. package/dist/browser/server-context.chrome-test-harness.js +20 -0
  77. package/dist/browser/server-middleware.js +31 -0
  78. package/dist/browser/test-port.js +16 -0
  79. package/dist/build-info.json +3 -3
  80. package/dist/canvas-host/file-resolver.js +43 -0
  81. package/dist/channels/account-summary.js +19 -0
  82. package/dist/channels/draft-stream-loop.js +77 -0
  83. package/dist/channels/plugins/account-helpers.js +26 -0
  84. package/dist/channels/telegram/allow-from.js +10 -0
  85. package/dist/cli/browser-cli-resize.js +22 -0
  86. package/dist/cli/browser-cli-shared.js +8 -0
  87. package/dist/cli/clawbot-cli.js +5 -0
  88. package/dist/cli/completion-cli.js +566 -0
  89. package/dist/cli/config-cli.js +63 -5
  90. package/dist/cli/daemon-cli/lifecycle-core.js +256 -0
  91. package/dist/cli/daemon-cli/register-service-commands.js +60 -0
  92. package/dist/cli/daemon-cli-compat.js +80 -0
  93. package/dist/cli/nodes-cli/pairing-render.js +26 -0
  94. package/dist/cli/program/action-reparse.js +17 -0
  95. package/dist/cli/program/command-registry.js +17 -0
  96. package/dist/cli/program/program-context.js +8 -0
  97. package/dist/cli/program/register.subclis.js +7 -0
  98. package/dist/cli/program/routes.js +233 -0
  99. package/dist/cli/qr-cli.js +132 -0
  100. package/dist/cli/requirements-test-fixtures.js +17 -0
  101. package/dist/cli/respawn-policy.js +4 -0
  102. package/dist/cli/shared/parse-port.js +18 -0
  103. package/dist/cli/skills-cli.format.js +241 -0
  104. package/dist/cli/update-cli/progress.js +121 -0
  105. package/dist/cli/update-cli/restart-helper.js +108 -0
  106. package/dist/cli/update-cli/shared.js +196 -0
  107. package/dist/cli/update-cli/status.js +97 -0
  108. package/dist/cli/update-cli/suppress-deprecations.js +17 -0
  109. package/dist/cli/update-cli/update-command.js +506 -0
  110. package/dist/cli/update-cli/wizard.js +130 -0
  111. package/dist/cli/update-cli.js +3 -9
  112. package/dist/cli/windows-argv.js +69 -0
  113. package/dist/commands/auth-choice-legacy.js +20 -0
  114. package/dist/commands/auth-choice.apply-helpers.js +8 -0
  115. package/dist/commands/channel-test-helpers.js +19 -0
  116. package/dist/commands/cleanup-plan.js +10 -0
  117. package/dist/commands/cleanup-utils.js +7 -0
  118. package/dist/commands/config-validation.js +15 -0
  119. package/dist/commands/doctor-completion.js +112 -0
  120. package/dist/commands/doctor-memory-search.js +119 -0
  121. package/dist/commands/doctor-session-locks.js +73 -0
  122. package/dist/commands/doctor.e2e-harness.js +364 -0
  123. package/dist/commands/gateway-presence.js +19 -0
  124. package/dist/commands/model-default.js +35 -0
  125. package/dist/commands/models/fallbacks-shared.js +102 -0
  126. package/dist/commands/models/shared.js +24 -0
  127. package/dist/commands/onboard-auth.config-gateways.js +64 -0
  128. package/dist/commands/onboard-auth.config-litellm.js +45 -0
  129. package/dist/commands/onboard-auth.config-shared.js +116 -0
  130. package/dist/commands/onboard-config.js +16 -0
  131. package/dist/commands/onboard-non-interactive.test-helpers.js +31 -0
  132. package/dist/commands/onboard-provider-auth-flags.js +136 -0
  133. package/dist/commands/openai-codex-oauth.js +40 -0
  134. package/dist/commands/test-runtime-config-helpers.js +21 -0
  135. package/dist/commands/test-wizard-helpers.js +68 -0
  136. package/dist/commands/vllm-setup.js +66 -0
  137. package/dist/compat/legacy-names.js +2 -0
  138. package/dist/config/backup-rotation.js +19 -0
  139. package/dist/config/env-preserve.js +122 -0
  140. package/dist/config/includes-scan.js +78 -0
  141. package/dist/config/plugins-allowlist.js +13 -0
  142. package/dist/config/schema.help.js +256 -0
  143. package/dist/config/schema.hints.js +189 -0
  144. package/dist/config/schema.irc.js +20 -0
  145. package/dist/config/schema.labels.js +317 -0
  146. package/dist/config/sessions/delivery-info.js +40 -0
  147. package/dist/config/types.irc.js +1 -0
  148. package/dist/config/zod-schema.agent-model.js +10 -0
  149. package/dist/config/zod-schema.allowdeny.js +35 -0
  150. package/dist/config/zod-schema.sensitive.js +4 -0
  151. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  152. package/dist/cron/isolated-agent/skills-snapshot.js +26 -0
  153. package/dist/cron/isolated-agent/subagent-followup.js +127 -0
  154. package/dist/cron/isolated-agent.mocks.js +12 -0
  155. package/dist/cron/isolated-agent.test-setup.js +22 -0
  156. package/dist/cron/legacy-delivery.js +43 -0
  157. package/dist/cron/webhook-url.js +22 -0
  158. package/dist/daemon/arg-split.js +40 -0
  159. package/dist/daemon/exec-file.js +23 -0
  160. package/dist/daemon/output.js +6 -0
  161. package/dist/daemon/runtime-format.js +31 -0
  162. package/dist/daemon/schtasks-exec.js +4 -0
  163. package/dist/daemon/service-audit.js +22 -0
  164. package/dist/discord/client.js +41 -0
  165. package/dist/discord/components-registry.js +57 -0
  166. package/dist/discord/components.js +816 -0
  167. package/dist/discord/guilds.js +12 -0
  168. package/dist/discord/monitor/gateway-plugin.js +48 -0
  169. package/dist/discord/monitor/presence.js +30 -0
  170. package/dist/discord/send.components.js +115 -0
  171. package/dist/discord/send.shared.js +4 -0
  172. package/dist/discord/ui.js +26 -0
  173. package/dist/discord/voice-message.js +254 -0
  174. package/dist/gateway/agent-event-assistant-text.js +5 -0
  175. package/dist/gateway/agent-prompt.js +33 -0
  176. package/dist/gateway/auth-rate-limit.js +136 -0
  177. package/dist/gateway/channel-health-monitor.js +114 -0
  178. package/dist/gateway/control-ui-contract.js +1 -0
  179. package/dist/gateway/control-ui-csp.js +15 -0
  180. package/dist/gateway/gateway-config-prompts.shared.js +25 -0
  181. package/dist/gateway/http-auth-helpers.js +18 -0
  182. package/dist/gateway/http-common.js +18 -0
  183. package/dist/gateway/http-endpoint-helpers.js +27 -0
  184. package/dist/gateway/node-invoke-sanitize.js +11 -0
  185. package/dist/gateway/node-invoke-system-run-approval.js +205 -0
  186. package/dist/gateway/probe-auth.js +21 -0
  187. package/dist/gateway/protocol/index.js +7 -2
  188. package/dist/gateway/protocol/schema/mesh.js +54 -0
  189. package/dist/gateway/protocol/schema/protocol-schemas.js +7 -0
  190. package/dist/gateway/protocol/schema.js +1 -0
  191. package/dist/gateway/server/ws-connection/auth-messages.js +54 -0
  192. package/dist/gateway/server-channels.js +11 -0
  193. package/dist/gateway/server-methods/attachment-normalize.js +16 -0
  194. package/dist/gateway/server-methods/base-hash.js +8 -0
  195. package/dist/gateway/server-methods/mesh.js +700 -0
  196. package/dist/gateway/server-methods/nodes.handlers.invoke-result.js +55 -0
  197. package/dist/gateway/server-methods/restart-request.js +13 -0
  198. package/dist/gateway/server-methods/validation.js +8 -0
  199. package/dist/gateway/server.agent.gateway-server-agent.mocks.js +35 -0
  200. package/dist/gateway/server.e2e-registry-helpers.js +1 -0
  201. package/dist/gateway/server.e2e-ws-harness.js +20 -0
  202. package/dist/gateway/test-helpers.js +2 -0
  203. package/dist/gateway/test-helpers.server.js +3 -1
  204. package/dist/gateway/test-http-response.js +12 -0
  205. package/dist/gateway/test-openai-responses-model.js +20 -0
  206. package/dist/gateway/test-temp-config.js +30 -0
  207. package/dist/gateway/test-with-server.js +32 -0
  208. package/dist/hooks/bundled/bootstrap-extra-files/handler.js +46 -0
  209. package/dist/imessage/monitor/abort-handler.js +23 -0
  210. package/dist/imessage/monitor/inbound-processing.js +346 -0
  211. package/dist/imessage/monitor/parse-notification.js +64 -0
  212. package/dist/imessage/target-parsing-helpers.js +92 -0
  213. package/dist/infra/archive.js +244 -20
  214. package/dist/infra/detect-package-manager.js +26 -0
  215. package/dist/infra/exec-approvals-allowlist.js +257 -0
  216. package/dist/infra/exec-approvals-analysis.js +770 -0
  217. package/dist/infra/exec-approvals.js +13 -0
  218. package/dist/infra/file-lock.js +1 -0
  219. package/dist/infra/gemini-auth.js +39 -0
  220. package/dist/infra/heartbeat-active-hours.js +85 -0
  221. package/dist/infra/heartbeat-events-filter.js +50 -0
  222. package/dist/infra/heartbeat-runner.test-utils.js +39 -0
  223. package/dist/infra/http-body.js +265 -0
  224. package/dist/infra/install-package-dir.js +50 -0
  225. package/dist/infra/install-safe-path.js +49 -0
  226. package/dist/infra/json-files.js +49 -0
  227. package/dist/infra/jsonl-socket.js +52 -0
  228. package/dist/infra/map-size.js +14 -0
  229. package/dist/infra/net/hostname.js +7 -0
  230. package/dist/infra/npm-registry-spec.js +39 -0
  231. package/dist/infra/openclaw-root.js +109 -0
  232. package/dist/infra/outbound/delivery-queue.js +214 -0
  233. package/dist/infra/outbound/identity.js +23 -0
  234. package/dist/infra/outbound/message-action-params.js +307 -0
  235. package/dist/infra/outbound/tool-payload.js +21 -0
  236. package/dist/infra/package-json.js +23 -0
  237. package/dist/infra/pairing-files.js +19 -0
  238. package/dist/infra/pairing-token.js +9 -0
  239. package/dist/infra/path-prepend.js +51 -0
  240. package/dist/infra/process-respawn.js +49 -0
  241. package/dist/infra/runtime-status.js +16 -0
  242. package/dist/infra/session-cost-usage.types.js +1 -0
  243. package/dist/infra/session-maintenance-warning.js +89 -0
  244. package/dist/infra/system-run-command.js +78 -0
  245. package/dist/infra/tmp-openclaw-dir.js +81 -0
  246. package/dist/infra/tmp-poolbot-dir.js +2 -0
  247. package/dist/infra/update-channels.js +19 -0
  248. package/dist/line/actions.js +45 -0
  249. package/dist/line/channel-access-token.js +9 -0
  250. package/dist/line/flex-templates/basic-cards.js +332 -0
  251. package/dist/line/flex-templates/common.js +18 -0
  252. package/dist/line/flex-templates/media-control-cards.js +453 -0
  253. package/dist/line/flex-templates/message.js +10 -0
  254. package/dist/line/flex-templates/schedule-cards.js +399 -0
  255. package/dist/line/flex-templates/types.js +1 -0
  256. package/dist/line/webhook-node.js +100 -0
  257. package/dist/line/webhook-utils.js +11 -0
  258. package/dist/logging/timestamps.js +14 -0
  259. package/dist/markdown/whatsapp.js +62 -0
  260. package/dist/media/base64.js +34 -0
  261. package/dist/media/local-roots.js +32 -0
  262. package/dist/media/outbound-attachment.js +10 -0
  263. package/dist/media/read-response-with-limit.js +41 -0
  264. package/dist/media/sniff-mime-from-base64.js +19 -0
  265. package/dist/media-understanding/audio-preflight.js +67 -0
  266. package/dist/media-understanding/fs.js +13 -0
  267. package/dist/media-understanding/output-extract.js +26 -0
  268. package/dist/media-understanding/providers/audio.test-helpers.js +34 -0
  269. package/dist/media-understanding/providers/google/inline-data.js +64 -0
  270. package/dist/media-understanding/providers/shared.js +7 -0
  271. package/dist/media-understanding/runner.entries.js +459 -0
  272. package/dist/memory/batch-error-utils.js +11 -0
  273. package/dist/memory/batch-http.js +27 -0
  274. package/dist/memory/batch-output.js +29 -0
  275. package/dist/memory/batch-runner.js +22 -0
  276. package/dist/memory/batch-upload.js +23 -0
  277. package/dist/memory/batch-utils.js +26 -0
  278. package/dist/memory/embeddings-debug.js +11 -0
  279. package/dist/memory/embeddings-remote-client.js +22 -0
  280. package/dist/memory/embeddings-remote-fetch.js +14 -0
  281. package/dist/memory/manager-embedding-ops.js +616 -0
  282. package/dist/memory/manager-sync-ops.js +953 -0
  283. package/dist/memory/qmd-manager.js +1061 -0
  284. package/dist/memory/qmd-query-parser.js +107 -0
  285. package/dist/memory/qmd-scope.js +93 -0
  286. package/dist/memory/search-manager.js +0 -1
  287. package/dist/memory/sync-index.js +21 -0
  288. package/dist/memory/sync-progress.js +22 -0
  289. package/dist/memory/sync-stale.js +30 -0
  290. package/dist/memory/test-embeddings-mock.js +16 -0
  291. package/dist/memory/test-manager-helpers.js +14 -0
  292. package/dist/memory/test-runtime-mocks.js +11 -0
  293. package/dist/node-host/invoke-browser.js +177 -0
  294. package/dist/node-host/invoke.js +685 -0
  295. package/dist/pairing/setup-code.js +285 -0
  296. package/dist/plugin-sdk/account-id.js +1 -0
  297. package/dist/plugin-sdk/agent-media-payload.js +13 -0
  298. package/dist/plugin-sdk/allow-from.js +47 -0
  299. package/dist/plugin-sdk/command-auth.js +23 -0
  300. package/dist/plugin-sdk/config-paths.js +9 -0
  301. package/dist/plugin-sdk/file-lock.js +116 -0
  302. package/dist/plugin-sdk/json-store.js +31 -0
  303. package/dist/plugin-sdk/onboarding.js +28 -0
  304. package/dist/plugin-sdk/provider-auth-result.js +29 -0
  305. package/dist/plugin-sdk/slack-message-actions.js +133 -0
  306. package/dist/plugin-sdk/status-helpers.js +35 -0
  307. package/dist/plugin-sdk/text-chunking.js +31 -0
  308. package/dist/plugin-sdk/tool-send.js +12 -0
  309. package/dist/plugin-sdk/webhook-path.js +27 -0
  310. package/dist/plugin-sdk/webhook-targets.js +34 -0
  311. package/dist/plugins/hooks.test-helpers.js +21 -0
  312. package/dist/plugins/uninstall.js +171 -0
  313. package/dist/process/supervisor/adapters/child.js +143 -0
  314. package/dist/process/supervisor/adapters/env.js +13 -0
  315. package/dist/process/supervisor/adapters/pty.js +148 -0
  316. package/dist/process/supervisor/index.js +10 -0
  317. package/dist/process/supervisor/registry.js +117 -0
  318. package/dist/process/supervisor/supervisor.js +244 -0
  319. package/dist/process/supervisor/types.js +1 -0
  320. package/dist/providers/google-shared.test-helpers.js +75 -0
  321. package/dist/security/audit-channel.js +419 -0
  322. package/dist/security/audit-tool-policy.js +1 -0
  323. package/dist/security/scan-paths.js +12 -0
  324. package/dist/sessions/input-provenance.js +55 -0
  325. package/dist/sessions/session-key-utils.js +7 -0
  326. package/dist/shared/chat-content.js +31 -0
  327. package/dist/shared/chat-envelope.js +45 -0
  328. package/dist/shared/config-eval.js +117 -0
  329. package/dist/shared/device-auth.js +16 -0
  330. package/dist/shared/entry-metadata.js +9 -0
  331. package/dist/shared/entry-status.js +25 -0
  332. package/dist/shared/frontmatter.js +98 -0
  333. package/dist/shared/model-param-b.js +19 -0
  334. package/dist/shared/net/ipv4.js +17 -0
  335. package/dist/shared/node-match.js +53 -0
  336. package/dist/shared/requirements.js +128 -0
  337. package/dist/shared/subagents-format.js +84 -0
  338. package/dist/shared/usage-aggregates.js +28 -0
  339. package/dist/signal/monitor/mentions.js +45 -0
  340. package/dist/signal/rpc-context.js +19 -0
  341. package/dist/slack/blocks-fallback.js +76 -0
  342. package/dist/slack/blocks-input.js +40 -0
  343. package/dist/slack/draft-stream.js +106 -0
  344. package/dist/slack/message-actions.js +51 -0
  345. package/dist/slack/modal-metadata.js +32 -0
  346. package/dist/slack/monitor/events/interactions.js +462 -0
  347. package/dist/slack/monitor/room-context.js +17 -0
  348. package/dist/slack/stream-mode.js +41 -0
  349. package/dist/telegram/bot-native-command-menu.js +64 -0
  350. package/dist/telegram/bot.media.e2e-harness.js +81 -0
  351. package/dist/telegram/button-types.js +1 -0
  352. package/dist/telegram/group-access.js +65 -0
  353. package/dist/telegram/outbound-params.js +21 -0
  354. package/dist/telegram/poll-vote-cache.js +21 -0
  355. package/dist/terminal/health-style.js +36 -0
  356. package/dist/test-utils/chunk-test-helpers.js +21 -0
  357. package/dist/test-utils/env.js +72 -0
  358. package/dist/test-utils/exec-assertions.js +12 -0
  359. package/dist/test-utils/imessage-test-plugin.js +54 -0
  360. package/dist/test-utils/mock-http-response.js +17 -0
  361. package/dist/test-utils/vitest-mock-fn.js +1 -0
  362. package/dist/tts/tts-core.js +550 -0
  363. package/dist/utils/chunk-items.js +10 -0
  364. package/dist/utils/reaction-level.js +52 -0
  365. package/dist/utils/safe-json.js +22 -0
  366. package/dist/utils/with-timeout.js +14 -0
  367. package/dist/web/media.js +17 -5
  368. package/dist/whatsapp/resolve-outbound-target.js +42 -0
  369. package/dist/wizard/onboarding.completion.js +74 -0
  370. package/extensions/bluebubbles/src/account-resolve.ts +29 -0
  371. package/extensions/bluebubbles/src/monitor-normalize.ts +796 -0
  372. package/extensions/bluebubbles/src/monitor-processing.ts +1007 -0
  373. package/extensions/bluebubbles/src/monitor-reply-cache.ts +185 -0
  374. package/extensions/bluebubbles/src/monitor-shared.ts +51 -0
  375. package/extensions/bluebubbles/src/multipart.ts +32 -0
  376. package/extensions/bluebubbles/src/send-helpers.ts +53 -0
  377. package/extensions/bluebubbles/src/test-harness.ts +50 -0
  378. package/extensions/bluebubbles/src/test-mocks.ts +11 -0
  379. package/extensions/device-pair/index.ts +554 -0
  380. package/extensions/discord/src/channel.js +366 -0
  381. package/extensions/discord/src/runtime.js +10 -0
  382. package/extensions/feishu/index.ts +63 -0
  383. package/extensions/feishu/src/accounts.ts +114 -0
  384. package/extensions/feishu/src/bitable.ts +739 -0
  385. package/extensions/feishu/src/bot.ts +965 -0
  386. package/extensions/feishu/src/channel.ts +351 -0
  387. package/extensions/feishu/src/client.ts +118 -0
  388. package/extensions/feishu/src/config-schema.ts +206 -0
  389. package/extensions/feishu/src/dedup.ts +33 -0
  390. package/extensions/feishu/src/directory.ts +177 -0
  391. package/extensions/feishu/src/doc-schema.ts +47 -0
  392. package/extensions/feishu/src/docx.ts +536 -0
  393. package/extensions/feishu/src/drive-schema.ts +46 -0
  394. package/extensions/feishu/src/drive.ts +227 -0
  395. package/extensions/feishu/src/dynamic-agent.ts +131 -0
  396. package/extensions/feishu/src/media.ts +449 -0
  397. package/extensions/feishu/src/mention.ts +126 -0
  398. package/extensions/feishu/src/monitor.ts +330 -0
  399. package/extensions/feishu/src/onboarding.ts +359 -0
  400. package/extensions/feishu/src/outbound.ts +55 -0
  401. package/extensions/feishu/src/perm-schema.ts +52 -0
  402. package/extensions/feishu/src/perm.ts +173 -0
  403. package/extensions/feishu/src/policy.ts +84 -0
  404. package/extensions/feishu/src/probe.ts +44 -0
  405. package/extensions/feishu/src/reactions.ts +160 -0
  406. package/extensions/feishu/src/reply-dispatcher.ts +239 -0
  407. package/extensions/feishu/src/runtime.ts +14 -0
  408. package/extensions/feishu/src/send-result.ts +29 -0
  409. package/extensions/feishu/src/send.ts +335 -0
  410. package/extensions/feishu/src/streaming-card.ts +223 -0
  411. package/extensions/feishu/src/targets.ts +78 -0
  412. package/extensions/feishu/src/tools-config.ts +21 -0
  413. package/extensions/feishu/src/types.ts +81 -0
  414. package/extensions/feishu/src/typing.ts +80 -0
  415. package/extensions/feishu/src/wiki-schema.ts +55 -0
  416. package/extensions/feishu/src/wiki.ts +232 -0
  417. package/extensions/imessage/src/channel.js +253 -0
  418. package/extensions/imessage/src/runtime.js +10 -0
  419. package/extensions/irc/index.ts +17 -0
  420. package/extensions/irc/src/accounts.ts +268 -0
  421. package/extensions/irc/src/channel.ts +367 -0
  422. package/extensions/irc/src/client.ts +439 -0
  423. package/extensions/irc/src/config-schema.ts +97 -0
  424. package/extensions/irc/src/connect-options.ts +30 -0
  425. package/extensions/irc/src/control-chars.ts +22 -0
  426. package/extensions/irc/src/inbound.ts +334 -0
  427. package/extensions/irc/src/monitor.ts +147 -0
  428. package/extensions/irc/src/normalize.ts +117 -0
  429. package/extensions/irc/src/onboarding.ts +479 -0
  430. package/extensions/irc/src/policy.ts +157 -0
  431. package/extensions/irc/src/probe.ts +53 -0
  432. package/extensions/irc/src/protocol.ts +169 -0
  433. package/extensions/irc/src/runtime.ts +14 -0
  434. package/extensions/irc/src/send.ts +88 -0
  435. package/extensions/irc/src/types.ts +93 -0
  436. package/extensions/matrix/src/matrix/client-bootstrap.ts +39 -0
  437. package/extensions/mattermost/src/mattermost/monitor-onchar.ts +25 -0
  438. package/extensions/mattermost/src/mattermost/monitor-websocket.ts +221 -0
  439. package/extensions/mattermost/src/mattermost/reactions.ts +130 -0
  440. package/extensions/mattermost/src/mattermost/reconnect.ts +103 -0
  441. package/extensions/minimax-portal-auth/index.ts +161 -0
  442. package/extensions/minimax-portal-auth/oauth.ts +247 -0
  443. package/extensions/msteams/src/file-lock.ts +1 -0
  444. package/extensions/msteams/src/graph.ts +92 -0
  445. package/extensions/msteams/src/mentions.ts +114 -0
  446. package/extensions/msteams/src/test-runtime.ts +16 -0
  447. package/extensions/openai-codex-auth/index.ts +177 -0
  448. package/extensions/phone-control/index.ts +421 -0
  449. package/extensions/shared/resolve-target-test-helpers.ts +66 -0
  450. package/extensions/signal/src/channel.js +273 -0
  451. package/extensions/signal/src/runtime.js +10 -0
  452. package/extensions/slack/src/channel.js +489 -0
  453. package/extensions/slack/src/runtime.js +10 -0
  454. package/extensions/talk-voice/index.ts +150 -0
  455. package/extensions/telegram/src/channel.js +424 -0
  456. package/extensions/telegram/src/runtime.js +10 -0
  457. package/extensions/thread-ownership/index.ts +133 -0
  458. package/extensions/tlon/src/account-fields.ts +25 -0
  459. package/extensions/tlon/src/urbit/base-url.ts +57 -0
  460. package/extensions/tlon/src/urbit/channel-client.ts +157 -0
  461. package/extensions/tlon/src/urbit/channel-ops.ts +164 -0
  462. package/extensions/tlon/src/urbit/context.ts +47 -0
  463. package/extensions/tlon/src/urbit/errors.ts +51 -0
  464. package/extensions/tlon/src/urbit/fetch.ts +39 -0
  465. package/extensions/twitch/src/test-fixtures.ts +30 -0
  466. package/extensions/voice-call/src/allowlist.ts +19 -0
  467. package/extensions/whatsapp/src/channel.js +429 -0
  468. package/extensions/whatsapp/src/runtime.js +10 -0
  469. 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
+ }