clawdbot 2026.1.4-1

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 (550) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/LICENSE +21 -0
  3. package/README-header.png +0 -0
  4. package/README.md +297 -0
  5. package/dist/agents/agent-paths.js +17 -0
  6. package/dist/agents/bash-process-registry.js +126 -0
  7. package/dist/agents/bash-tools.js +837 -0
  8. package/dist/agents/clawdbot-tools.js +30 -0
  9. package/dist/agents/clawdis-tools.js +27 -0
  10. package/dist/agents/context.js +34 -0
  11. package/dist/agents/defaults.js +6 -0
  12. package/dist/agents/model-auth.js +112 -0
  13. package/dist/agents/model-catalog.js +55 -0
  14. package/dist/agents/model-fallback.js +191 -0
  15. package/dist/agents/model-scan.js +263 -0
  16. package/dist/agents/model-selection.js +116 -0
  17. package/dist/agents/models-config.js +49 -0
  18. package/dist/agents/pi-embedded-helpers.js +74 -0
  19. package/dist/agents/pi-embedded-runner.js +407 -0
  20. package/dist/agents/pi-embedded-subscribe.js +568 -0
  21. package/dist/agents/pi-embedded-utils.js +20 -0
  22. package/dist/agents/pi-embedded.js +1 -0
  23. package/dist/agents/pi-oauth.js +88 -0
  24. package/dist/agents/pi-tools.js +433 -0
  25. package/dist/agents/sandbox-paths.js +68 -0
  26. package/dist/agents/sandbox.js +644 -0
  27. package/dist/agents/shell-utils.js +53 -0
  28. package/dist/agents/skills-install.js +244 -0
  29. package/dist/agents/skills-status.js +157 -0
  30. package/dist/agents/skills.js +470 -0
  31. package/dist/agents/steerable-agent-loop.js +338 -0
  32. package/dist/agents/steerable-provider-transport.js +48 -0
  33. package/dist/agents/system-prompt.js +104 -0
  34. package/dist/agents/tool-display.js +162 -0
  35. package/dist/agents/tool-images.js +138 -0
  36. package/dist/agents/tools/browser-tool.js +339 -0
  37. package/dist/agents/tools/canvas-tool.js +193 -0
  38. package/dist/agents/tools/common.js +88 -0
  39. package/dist/agents/tools/cron-tool.js +124 -0
  40. package/dist/agents/tools/discord-actions-guild.js +186 -0
  41. package/dist/agents/tools/discord-actions-messaging.js +285 -0
  42. package/dist/agents/tools/discord-actions-moderation.js +70 -0
  43. package/dist/agents/tools/discord-actions.js +56 -0
  44. package/dist/agents/tools/discord-schema.js +199 -0
  45. package/dist/agents/tools/discord-tool.js +16 -0
  46. package/dist/agents/tools/gateway-tool.js +46 -0
  47. package/dist/agents/tools/gateway.js +27 -0
  48. package/dist/agents/tools/image-tool.js +132 -0
  49. package/dist/agents/tools/nodes-tool.js +413 -0
  50. package/dist/agents/tools/nodes-utils.js +92 -0
  51. package/dist/agents/tools/sessions-helpers.js +88 -0
  52. package/dist/agents/tools/sessions-history-tool.js +53 -0
  53. package/dist/agents/tools/sessions-list-tool.js +143 -0
  54. package/dist/agents/tools/sessions-send-helpers.js +100 -0
  55. package/dist/agents/tools/sessions-send-tool.js +347 -0
  56. package/dist/agents/tools/slack-actions.js +129 -0
  57. package/dist/agents/tools/slack-schema.js +59 -0
  58. package/dist/agents/tools/slack-tool.js +16 -0
  59. package/dist/agents/usage.js +39 -0
  60. package/dist/agents/workspace.js +241 -0
  61. package/dist/auto-reply/chunk.js +76 -0
  62. package/dist/auto-reply/envelope.js +38 -0
  63. package/dist/auto-reply/group-activation.js +20 -0
  64. package/dist/auto-reply/heartbeat.js +57 -0
  65. package/dist/auto-reply/model.js +14 -0
  66. package/dist/auto-reply/reply/abort.js +14 -0
  67. package/dist/auto-reply/reply/agent-runner.js +371 -0
  68. package/dist/auto-reply/reply/block-streaming.js +34 -0
  69. package/dist/auto-reply/reply/body.js +29 -0
  70. package/dist/auto-reply/reply/commands.js +207 -0
  71. package/dist/auto-reply/reply/directive-handling.js +361 -0
  72. package/dist/auto-reply/reply/directives.js +47 -0
  73. package/dist/auto-reply/reply/followup-runner.js +149 -0
  74. package/dist/auto-reply/reply/groups.js +91 -0
  75. package/dist/auto-reply/reply/mentions.js +38 -0
  76. package/dist/auto-reply/reply/model-selection.js +114 -0
  77. package/dist/auto-reply/reply/queue.js +399 -0
  78. package/dist/auto-reply/reply/reply-tags.js +26 -0
  79. package/dist/auto-reply/reply/session-updates.js +87 -0
  80. package/dist/auto-reply/reply/session.js +160 -0
  81. package/dist/auto-reply/reply/typing.js +75 -0
  82. package/dist/auto-reply/reply.js +535 -0
  83. package/dist/auto-reply/send-policy.js +28 -0
  84. package/dist/auto-reply/status.js +158 -0
  85. package/dist/auto-reply/templating.js +9 -0
  86. package/dist/auto-reply/thinking.js +49 -0
  87. package/dist/auto-reply/tokens.js +2 -0
  88. package/dist/auto-reply/tool-meta.js +74 -0
  89. package/dist/auto-reply/transcription.js +57 -0
  90. package/dist/auto-reply/types.js +1 -0
  91. package/dist/browser/bridge-server.js +37 -0
  92. package/dist/browser/cdp.js +382 -0
  93. package/dist/browser/chrome.js +432 -0
  94. package/dist/browser/client-actions-core.js +67 -0
  95. package/dist/browser/client-actions-observe.js +24 -0
  96. package/dist/browser/client-actions-types.js +1 -0
  97. package/dist/browser/client-actions.js +3 -0
  98. package/dist/browser/client-fetch.js +43 -0
  99. package/dist/browser/client.js +105 -0
  100. package/dist/browser/config.js +140 -0
  101. package/dist/browser/constants.js +4 -0
  102. package/dist/browser/profiles-service.js +122 -0
  103. package/dist/browser/profiles.js +85 -0
  104. package/dist/browser/pw-ai.js +2 -0
  105. package/dist/browser/pw-session.js +144 -0
  106. package/dist/browser/pw-tools-core.js +363 -0
  107. package/dist/browser/routes/agent.js +535 -0
  108. package/dist/browser/routes/basic.js +155 -0
  109. package/dist/browser/routes/index.js +8 -0
  110. package/dist/browser/routes/tabs.js +105 -0
  111. package/dist/browser/routes/utils.js +62 -0
  112. package/dist/browser/screenshot.js +40 -0
  113. package/dist/browser/server-context.js +377 -0
  114. package/dist/browser/server.js +81 -0
  115. package/dist/browser/target-id.js +18 -0
  116. package/dist/browser/trash.js +21 -0
  117. package/dist/canvas-host/a2ui/.bundle.hash +1 -0
  118. package/dist/canvas-host/a2ui/a2ui.bundle.js +17768 -0
  119. package/dist/canvas-host/a2ui/index.html +246 -0
  120. package/dist/canvas-host/a2ui.js +187 -0
  121. package/dist/canvas-host/server.js +382 -0
  122. package/dist/cli/browser-cli-actions-input.js +459 -0
  123. package/dist/cli/browser-cli-actions-observe.js +56 -0
  124. package/dist/cli/browser-cli-examples.js +31 -0
  125. package/dist/cli/browser-cli-inspect.js +97 -0
  126. package/dist/cli/browser-cli-manage.js +286 -0
  127. package/dist/cli/browser-cli-shared.js +1 -0
  128. package/dist/cli/browser-cli.js +26 -0
  129. package/dist/cli/canvas-cli.js +416 -0
  130. package/dist/cli/cron-cli.js +454 -0
  131. package/dist/cli/deps.js +17 -0
  132. package/dist/cli/dns-cli.js +180 -0
  133. package/dist/cli/gateway-cli.js +489 -0
  134. package/dist/cli/gateway-rpc.js +20 -0
  135. package/dist/cli/hooks-cli.js +135 -0
  136. package/dist/cli/models-cli.js +248 -0
  137. package/dist/cli/nodes-camera.js +57 -0
  138. package/dist/cli/nodes-canvas.js +26 -0
  139. package/dist/cli/nodes-cli.js +946 -0
  140. package/dist/cli/nodes-screen.js +37 -0
  141. package/dist/cli/parse-duration.js +20 -0
  142. package/dist/cli/ports.js +97 -0
  143. package/dist/cli/program.js +406 -0
  144. package/dist/cli/prompt.js +19 -0
  145. package/dist/cli/tui-cli.js +35 -0
  146. package/dist/cli/wait.js +8 -0
  147. package/dist/commands/agent.js +645 -0
  148. package/dist/commands/antigravity-oauth.js +327 -0
  149. package/dist/commands/configure.js +480 -0
  150. package/dist/commands/doctor.js +484 -0
  151. package/dist/commands/health.js +108 -0
  152. package/dist/commands/models/aliases.js +64 -0
  153. package/dist/commands/models/fallbacks.js +99 -0
  154. package/dist/commands/models/image-fallbacks.js +99 -0
  155. package/dist/commands/models/list.js +323 -0
  156. package/dist/commands/models/scan.js +266 -0
  157. package/dist/commands/models/set-image.js +23 -0
  158. package/dist/commands/models/set.js +23 -0
  159. package/dist/commands/models/shared.js +72 -0
  160. package/dist/commands/models.js +7 -0
  161. package/dist/commands/onboard-auth.js +70 -0
  162. package/dist/commands/onboard-helpers.js +295 -0
  163. package/dist/commands/onboard-interactive.js +17 -0
  164. package/dist/commands/onboard-non-interactive.js +202 -0
  165. package/dist/commands/onboard-providers.js +634 -0
  166. package/dist/commands/onboard-remote.js +120 -0
  167. package/dist/commands/onboard-skills.js +148 -0
  168. package/dist/commands/onboard-types.js +1 -0
  169. package/dist/commands/onboard.js +12 -0
  170. package/dist/commands/send.js +124 -0
  171. package/dist/commands/sessions.js +212 -0
  172. package/dist/commands/setup.js +58 -0
  173. package/dist/commands/signal-install.js +135 -0
  174. package/dist/commands/status.js +207 -0
  175. package/dist/commands/update.js +16 -0
  176. package/dist/config/config.js +6 -0
  177. package/dist/config/defaults.js +61 -0
  178. package/dist/config/io.js +147 -0
  179. package/dist/config/legacy-migrate.js +13 -0
  180. package/dist/config/legacy.js +159 -0
  181. package/dist/config/paths.js +71 -0
  182. package/dist/config/schema.js +150 -0
  183. package/dist/config/sessions.js +282 -0
  184. package/dist/config/talk.js +31 -0
  185. package/dist/config/types.js +1 -0
  186. package/dist/config/validation.js +29 -0
  187. package/dist/config/zod-schema.js +831 -0
  188. package/dist/control-ui/assets/index-BFID3yAA.css +1 -0
  189. package/dist/control-ui/assets/index-CE_axlTS.js +2235 -0
  190. package/dist/control-ui/assets/index-CE_axlTS.js.map +1 -0
  191. package/dist/control-ui/index.html +15 -0
  192. package/dist/cron/isolated-agent.js +499 -0
  193. package/dist/cron/run-log.js +72 -0
  194. package/dist/cron/schedule.js +24 -0
  195. package/dist/cron/service.js +471 -0
  196. package/dist/cron/store.js +43 -0
  197. package/dist/cron/types.js +1 -0
  198. package/dist/daemon/constants.js +10 -0
  199. package/dist/daemon/launchd.js +276 -0
  200. package/dist/daemon/legacy.js +63 -0
  201. package/dist/daemon/program-args.js +76 -0
  202. package/dist/daemon/schtasks.js +257 -0
  203. package/dist/daemon/service.js +60 -0
  204. package/dist/daemon/systemd.js +266 -0
  205. package/dist/discord/index.js +2 -0
  206. package/dist/discord/monitor.js +1188 -0
  207. package/dist/discord/probe.js +54 -0
  208. package/dist/discord/send.js +577 -0
  209. package/dist/discord/token.js +8 -0
  210. package/dist/gateway/auth.js +121 -0
  211. package/dist/gateway/call.js +94 -0
  212. package/dist/gateway/chat-attachments.js +41 -0
  213. package/dist/gateway/client.js +180 -0
  214. package/dist/gateway/config-reload.js +274 -0
  215. package/dist/gateway/control-ui.js +184 -0
  216. package/dist/gateway/hooks-mapping.js +282 -0
  217. package/dist/gateway/hooks.js +168 -0
  218. package/dist/gateway/net.js +29 -0
  219. package/dist/gateway/protocol/index.js +61 -0
  220. package/dist/gateway/protocol/schema.js +560 -0
  221. package/dist/gateway/server-bridge-subscriptions.js +93 -0
  222. package/dist/gateway/server-bridge.js +1013 -0
  223. package/dist/gateway/server-browser.js +12 -0
  224. package/dist/gateway/server-chat.js +159 -0
  225. package/dist/gateway/server-constants.js +8 -0
  226. package/dist/gateway/server-discovery.js +62 -0
  227. package/dist/gateway/server-http.js +165 -0
  228. package/dist/gateway/server-methods/agent-job.js +125 -0
  229. package/dist/gateway/server-methods/agent.js +250 -0
  230. package/dist/gateway/server-methods/chat.js +200 -0
  231. package/dist/gateway/server-methods/config.js +50 -0
  232. package/dist/gateway/server-methods/connect.js +6 -0
  233. package/dist/gateway/server-methods/cron.js +83 -0
  234. package/dist/gateway/server-methods/health.js +28 -0
  235. package/dist/gateway/server-methods/models.js +16 -0
  236. package/dist/gateway/server-methods/nodes.js +294 -0
  237. package/dist/gateway/server-methods/providers.js +217 -0
  238. package/dist/gateway/server-methods/send.js +166 -0
  239. package/dist/gateway/server-methods/sessions.js +305 -0
  240. package/dist/gateway/server-methods/skills.js +83 -0
  241. package/dist/gateway/server-methods/system.js +118 -0
  242. package/dist/gateway/server-methods/talk.js +22 -0
  243. package/dist/gateway/server-methods/types.js +1 -0
  244. package/dist/gateway/server-methods/voicewake.js +30 -0
  245. package/dist/gateway/server-methods/web.js +58 -0
  246. package/dist/gateway/server-methods/wizard.js +100 -0
  247. package/dist/gateway/server-methods.js +53 -0
  248. package/dist/gateway/server-providers.js +644 -0
  249. package/dist/gateway/server-shared.js +1 -0
  250. package/dist/gateway/server-utils.js +35 -0
  251. package/dist/gateway/server.js +1437 -0
  252. package/dist/gateway/session-utils.js +216 -0
  253. package/dist/gateway/ws-log.js +349 -0
  254. package/dist/gateway/ws-logging.js +8 -0
  255. package/dist/globals.js +41 -0
  256. package/dist/hooks/gmail-ops.js +236 -0
  257. package/dist/hooks/gmail-setup-utils.js +278 -0
  258. package/dist/hooks/gmail-watcher.js +175 -0
  259. package/dist/hooks/gmail.js +177 -0
  260. package/dist/imessage/client.js +165 -0
  261. package/dist/imessage/index.js +3 -0
  262. package/dist/imessage/monitor.js +272 -0
  263. package/dist/imessage/probe.js +26 -0
  264. package/dist/imessage/send.js +83 -0
  265. package/dist/imessage/targets.js +176 -0
  266. package/dist/index.js +50 -0
  267. package/dist/infra/agent-events.js +46 -0
  268. package/dist/infra/binaries.js +9 -0
  269. package/dist/infra/bonjour-discovery.js +163 -0
  270. package/dist/infra/bonjour.js +200 -0
  271. package/dist/infra/bridge/server.js +562 -0
  272. package/dist/infra/canvas-host-url.js +54 -0
  273. package/dist/infra/env.js +8 -0
  274. package/dist/infra/errors.js +28 -0
  275. package/dist/infra/gateway-lock.js +8 -0
  276. package/dist/infra/heartbeat-events.js +21 -0
  277. package/dist/infra/heartbeat-runner.js +453 -0
  278. package/dist/infra/heartbeat-wake.js +61 -0
  279. package/dist/infra/is-main.js +37 -0
  280. package/dist/infra/machine-name.js +40 -0
  281. package/dist/infra/node-pairing.js +211 -0
  282. package/dist/infra/pam.js +42 -0
  283. package/dist/infra/path-env.js +92 -0
  284. package/dist/infra/ports.js +87 -0
  285. package/dist/infra/provider-summary.js +80 -0
  286. package/dist/infra/restart.js +29 -0
  287. package/dist/infra/retry.js +16 -0
  288. package/dist/infra/runtime-guard.js +59 -0
  289. package/dist/infra/system-events.js +44 -0
  290. package/dist/infra/system-presence.js +216 -0
  291. package/dist/infra/tailnet.js +46 -0
  292. package/dist/infra/tailscale.js +149 -0
  293. package/dist/infra/voicewake.js +77 -0
  294. package/dist/infra/widearea-dns.js +123 -0
  295. package/dist/infra/ws.js +13 -0
  296. package/dist/logger.js +52 -0
  297. package/dist/logging.js +490 -0
  298. package/dist/macos/gateway-daemon.js +141 -0
  299. package/dist/macos/relay.js +46 -0
  300. package/dist/media/constants.js +33 -0
  301. package/dist/media/host.js +42 -0
  302. package/dist/media/image-ops.js +121 -0
  303. package/dist/media/mime.js +115 -0
  304. package/dist/media/parse.js +81 -0
  305. package/dist/media/server.js +64 -0
  306. package/dist/media/store.js +139 -0
  307. package/dist/process/command-queue.js +97 -0
  308. package/dist/process/exec.js +75 -0
  309. package/dist/protocol.schema.json +2918 -0
  310. package/dist/provider-web.js +8 -0
  311. package/dist/providers/web/index.js +2 -0
  312. package/dist/runtime.js +8 -0
  313. package/dist/sessions/send-policy.js +68 -0
  314. package/dist/signal/client.js +134 -0
  315. package/dist/signal/daemon.js +69 -0
  316. package/dist/signal/index.js +3 -0
  317. package/dist/signal/monitor.js +336 -0
  318. package/dist/signal/probe.js +46 -0
  319. package/dist/signal/send.js +91 -0
  320. package/dist/slack/actions.js +97 -0
  321. package/dist/slack/index.js +5 -0
  322. package/dist/slack/monitor.js +1029 -0
  323. package/dist/slack/probe.js +47 -0
  324. package/dist/slack/send.js +131 -0
  325. package/dist/slack/token.js +10 -0
  326. package/dist/telegram/bot.js +394 -0
  327. package/dist/telegram/download.js +34 -0
  328. package/dist/telegram/index.js +4 -0
  329. package/dist/telegram/monitor.js +47 -0
  330. package/dist/telegram/probe.js +63 -0
  331. package/dist/telegram/proxy.js +9 -0
  332. package/dist/telegram/send.js +138 -0
  333. package/dist/telegram/token.js +30 -0
  334. package/dist/telegram/webhook-set.js +12 -0
  335. package/dist/telegram/webhook.js +56 -0
  336. package/dist/tui/commands.js +74 -0
  337. package/dist/tui/components/assistant-message.js +16 -0
  338. package/dist/tui/components/chat-log.js +92 -0
  339. package/dist/tui/components/custom-editor.js +53 -0
  340. package/dist/tui/components/selectors.js +8 -0
  341. package/dist/tui/components/tool-execution.js +111 -0
  342. package/dist/tui/components/user-message.js +17 -0
  343. package/dist/tui/gateway-chat.js +140 -0
  344. package/dist/tui/layout.js +41 -0
  345. package/dist/tui/message-list.js +57 -0
  346. package/dist/tui/theme/theme.js +80 -0
  347. package/dist/tui/theme.js +25 -0
  348. package/dist/tui/tui.js +708 -0
  349. package/dist/utils.js +133 -0
  350. package/dist/version.js +18 -0
  351. package/dist/web/active-listener.js +7 -0
  352. package/dist/web/auto-reply.js +1203 -0
  353. package/dist/web/inbound.js +481 -0
  354. package/dist/web/login-qr.js +204 -0
  355. package/dist/web/login.js +59 -0
  356. package/dist/web/media.js +148 -0
  357. package/dist/web/outbound.js +67 -0
  358. package/dist/web/qr-image.js +97 -0
  359. package/dist/web/reconnect.js +60 -0
  360. package/dist/web/reply-heartbeat-wake.js +61 -0
  361. package/dist/web/session.js +346 -0
  362. package/dist/wizard/clack-prompter.js +56 -0
  363. package/dist/wizard/onboarding.js +452 -0
  364. package/dist/wizard/prompts.js +6 -0
  365. package/dist/wizard/session.js +203 -0
  366. package/docs/AGENTS.default.md +116 -0
  367. package/docs/CNAME +1 -0
  368. package/docs/RELEASING.md +64 -0
  369. package/docs/_config.yml +51 -0
  370. package/docs/_layouts/default.html +145 -0
  371. package/docs/agent-send.md +21 -0
  372. package/docs/agent.md +104 -0
  373. package/docs/android/connect.md +131 -0
  374. package/docs/architecture.md +89 -0
  375. package/docs/assets/markdown.css +130 -0
  376. package/docs/assets/pixel-lobster.svg +60 -0
  377. package/docs/assets/terminal.css +497 -0
  378. package/docs/assets/theme.js +55 -0
  379. package/docs/audio.md +50 -0
  380. package/docs/background-process.md +74 -0
  381. package/docs/bash.md +32 -0
  382. package/docs/bonjour.md +159 -0
  383. package/docs/browser.md +289 -0
  384. package/docs/camera.md +152 -0
  385. package/docs/clawd.md +199 -0
  386. package/docs/clawdbot-mac.md +104 -0
  387. package/docs/configuration.md +1177 -0
  388. package/docs/control-api.md +49 -0
  389. package/docs/control-ui.md +83 -0
  390. package/docs/cron.md +374 -0
  391. package/docs/dashboard.md +17 -0
  392. package/docs/device-models.md +46 -0
  393. package/docs/discord.md +293 -0
  394. package/docs/discovery.md +112 -0
  395. package/docs/docker.md +251 -0
  396. package/docs/docs.json +86 -0
  397. package/docs/doctor.md +47 -0
  398. package/docs/elevated.md +31 -0
  399. package/docs/faq.md +640 -0
  400. package/docs/gateway/pairing.md +109 -0
  401. package/docs/gateway-lock.md +28 -0
  402. package/docs/gateway.md +174 -0
  403. package/docs/gmail-pubsub.md +191 -0
  404. package/docs/grammy.md +27 -0
  405. package/docs/group-messages.md +71 -0
  406. package/docs/groups.md +78 -0
  407. package/docs/health.md +28 -0
  408. package/docs/heartbeat.md +64 -0
  409. package/docs/images.md +52 -0
  410. package/docs/imessage.md +63 -0
  411. package/docs/index.md +182 -0
  412. package/docs/ios/connect.md +177 -0
  413. package/docs/ios/spec.md +236 -0
  414. package/docs/location-command.md +95 -0
  415. package/docs/logging.md +99 -0
  416. package/docs/lore.md +131 -0
  417. package/docs/mac/bun.md +133 -0
  418. package/docs/mac/canvas.md +161 -0
  419. package/docs/mac/child-process.md +72 -0
  420. package/docs/mac/dev-setup.md +81 -0
  421. package/docs/mac/health.md +28 -0
  422. package/docs/mac/icon.md +26 -0
  423. package/docs/mac/logging.md +51 -0
  424. package/docs/mac/menu-bar.md +69 -0
  425. package/docs/mac/peekaboo.md +170 -0
  426. package/docs/mac/permissions.md +40 -0
  427. package/docs/mac/release.md +76 -0
  428. package/docs/mac/remote.md +57 -0
  429. package/docs/mac/signing.md +41 -0
  430. package/docs/mac/skills.md +27 -0
  431. package/docs/mac/voice-overlay.md +52 -0
  432. package/docs/mac/voicewake.md +56 -0
  433. package/docs/mac/webchat.md +27 -0
  434. package/docs/mac/xpc.md +40 -0
  435. package/docs/models.md +90 -0
  436. package/docs/nix.md +49 -0
  437. package/docs/nodes.md +157 -0
  438. package/docs/onboarding-config-protocol.md +29 -0
  439. package/docs/onboarding.md +185 -0
  440. package/docs/presence.md +133 -0
  441. package/docs/queue.md +78 -0
  442. package/docs/refactor/browser-control-simplification.md +58 -0
  443. package/docs/refactor/canvas-a2ui.md +93 -0
  444. package/docs/refactor/cli-unification.md +64 -0
  445. package/docs/refactor/gateway-client.md +31 -0
  446. package/docs/refactor/gateway.md +99 -0
  447. package/docs/refactor/new-arch.md +171 -0
  448. package/docs/refactor/tui.md +26 -0
  449. package/docs/refactor/web-gateway-troubleshooting.md +37 -0
  450. package/docs/refactor/webagent-session.md +46 -0
  451. package/docs/remote-gateway-readme.md +148 -0
  452. package/docs/remote.md +66 -0
  453. package/docs/research/memory.md +227 -0
  454. package/docs/rpc.md +35 -0
  455. package/docs/security.md +168 -0
  456. package/docs/session-tool.md +119 -0
  457. package/docs/session.md +84 -0
  458. package/docs/sessions.md +8 -0
  459. package/docs/setup.md +118 -0
  460. package/docs/signal.md +113 -0
  461. package/docs/skills-config.md +58 -0
  462. package/docs/skills.md +149 -0
  463. package/docs/slack.md +158 -0
  464. package/docs/surface.md +20 -0
  465. package/docs/tailscale.md +71 -0
  466. package/docs/talk.md +79 -0
  467. package/docs/telegram.md +90 -0
  468. package/docs/templates/AGENTS.md +126 -0
  469. package/docs/templates/BOOTSTRAP.md +53 -0
  470. package/docs/templates/IDENTITY.md +17 -0
  471. package/docs/templates/SOUL.md +41 -0
  472. package/docs/templates/TOOLS.md +41 -0
  473. package/docs/templates/USER.md +22 -0
  474. package/docs/test.md +35 -0
  475. package/docs/thinking.md +46 -0
  476. package/docs/tools.md +248 -0
  477. package/docs/troubleshooting.md +227 -0
  478. package/docs/tui.md +69 -0
  479. package/docs/typebox.md +42 -0
  480. package/docs/voicewake.md +61 -0
  481. package/docs/web.md +115 -0
  482. package/docs/webchat.md +34 -0
  483. package/docs/webhook.md +132 -0
  484. package/docs/whatsapp-clawd.jpg +0 -0
  485. package/docs/whatsapp.md +142 -0
  486. package/docs/wizard.md +158 -0
  487. package/package.json +186 -0
  488. package/skills/apple-notes/SKILL.md +50 -0
  489. package/skills/apple-reminders/SKILL.md +67 -0
  490. package/skills/bear-notes/SKILL.md +79 -0
  491. package/skills/bird/SKILL.md +25 -0
  492. package/skills/blogwatcher/SKILL.md +46 -0
  493. package/skills/blucli/SKILL.md +27 -0
  494. package/skills/brave-search/SKILL.md +30 -0
  495. package/skills/brave-search/scripts/content.mjs +53 -0
  496. package/skills/brave-search/scripts/search.mjs +79 -0
  497. package/skills/camsnap/SKILL.md +25 -0
  498. package/skills/clawdhub/SKILL.md +53 -0
  499. package/skills/coding-agent/SKILL.md +275 -0
  500. package/skills/discord/SKILL.md +369 -0
  501. package/skills/eightctl/SKILL.md +29 -0
  502. package/skills/food-order/SKILL.md +41 -0
  503. package/skills/gemini/SKILL.md +23 -0
  504. package/skills/gifgrep/SKILL.md +47 -0
  505. package/skills/github/SKILL.md +47 -0
  506. package/skills/gog/SKILL.md +36 -0
  507. package/skills/goplaces/SKILL.md +30 -0
  508. package/skills/imsg/SKILL.md +25 -0
  509. package/skills/local-places/SERVER_README.md +101 -0
  510. package/skills/local-places/SKILL.md +91 -0
  511. package/skills/local-places/pyproject.toml +27 -0
  512. package/skills/local-places/src/local_places/__init__.py +2 -0
  513. package/skills/local-places/src/local_places/__pycache__/__init__.cpython-314.pyc +0 -0
  514. package/skills/local-places/src/local_places/__pycache__/google_places.cpython-314.pyc +0 -0
  515. package/skills/local-places/src/local_places/__pycache__/main.cpython-314.pyc +0 -0
  516. package/skills/local-places/src/local_places/__pycache__/schemas.cpython-314.pyc +0 -0
  517. package/skills/local-places/src/local_places/google_places.py +314 -0
  518. package/skills/local-places/src/local_places/main.py +65 -0
  519. package/skills/local-places/src/local_places/schemas.py +107 -0
  520. package/skills/mcporter/SKILL.md +38 -0
  521. package/skills/nano-banana-pro/SKILL.md +29 -0
  522. package/skills/nano-banana-pro/scripts/generate_image.py +167 -0
  523. package/skills/nano-pdf/SKILL.md +20 -0
  524. package/skills/notion/SKILL.md +156 -0
  525. package/skills/obsidian/SKILL.md +55 -0
  526. package/skills/openai-image-gen/SKILL.md +31 -0
  527. package/skills/openai-image-gen/scripts/gen.py +173 -0
  528. package/skills/openai-whisper/SKILL.md +19 -0
  529. package/skills/openai-whisper-api/SKILL.md +43 -0
  530. package/skills/openai-whisper-api/scripts/transcribe.sh +85 -0
  531. package/skills/openhue/SKILL.md +30 -0
  532. package/skills/oracle/SKILL.md +105 -0
  533. package/skills/ordercli/SKILL.md +47 -0
  534. package/skills/peekaboo/SKILL.md +153 -0
  535. package/skills/qmd/SKILL.md +26 -0
  536. package/skills/sag/SKILL.md +62 -0
  537. package/skills/slack/SKILL.md +143 -0
  538. package/skills/songsee/SKILL.md +29 -0
  539. package/skills/sonoscli/SKILL.md +26 -0
  540. package/skills/spotify-player/SKILL.md +34 -0
  541. package/skills/summarize/SKILL.md +49 -0
  542. package/skills/things-mac/SKILL.md +61 -0
  543. package/skills/tmux/SKILL.md +121 -0
  544. package/skills/tmux/scripts/find-sessions.sh +112 -0
  545. package/skills/tmux/scripts/wait-for-text.sh +83 -0
  546. package/skills/trello/SKILL.md +84 -0
  547. package/skills/video-frames/SKILL.md +29 -0
  548. package/skills/video-frames/scripts/frame.sh +81 -0
  549. package/skills/wacli/SKILL.md +42 -0
  550. package/skills/weather/SKILL.md +49 -0
@@ -0,0 +1,1203 @@
1
+ import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
2
+ import { formatAgentEnvelope } from "../auto-reply/envelope.js";
3
+ import { normalizeGroupActivation, parseActivationCommand, } from "../auto-reply/group-activation.js";
4
+ import { HEARTBEAT_PROMPT, stripHeartbeatToken, } from "../auto-reply/heartbeat.js";
5
+ import { getReplyFromConfig } from "../auto-reply/reply.js";
6
+ import { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
7
+ import { waitForever } from "../cli/wait.js";
8
+ import { loadConfig } from "../config/config.js";
9
+ import { DEFAULT_IDLE_MINUTES, loadSessionStore, resolveGroupSessionKey, resolveSessionKey, resolveStorePath, saveSessionStore, updateLastRoute, } from "../config/sessions.js";
10
+ import { logVerbose, shouldLogVerbose } from "../globals.js";
11
+ import { emitHeartbeatEvent } from "../infra/heartbeat-events.js";
12
+ import { enqueueSystemEvent } from "../infra/system-events.js";
13
+ import { createSubsystemLogger, getChildLogger } from "../logging.js";
14
+ import { defaultRuntime } from "../runtime.js";
15
+ import { isSelfChatMode, jidToE164, normalizeE164 } from "../utils.js";
16
+ import { setActiveWebListener } from "./active-listener.js";
17
+ import { monitorWebInbox } from "./inbound.js";
18
+ import { loadWebMedia } from "./media.js";
19
+ import { sendMessageWhatsApp } from "./outbound.js";
20
+ import { computeBackoff, newConnectionId, resolveHeartbeatSeconds, resolveReconnectPolicy, sleepWithAbort, } from "./reconnect.js";
21
+ import { formatError, getWebAuthAgeMs, readWebSelfId } from "./session.js";
22
+ const DEFAULT_GROUP_HISTORY_LIMIT = 50;
23
+ const whatsappLog = createSubsystemLogger("gateway/providers/whatsapp");
24
+ const whatsappInboundLog = whatsappLog.child("inbound");
25
+ const whatsappOutboundLog = whatsappLog.child("outbound");
26
+ const whatsappHeartbeatLog = whatsappLog.child("heartbeat");
27
+ // Send via the active gateway-backed listener. The monitor already owns the single
28
+ // Baileys session, so use its send API directly.
29
+ async function sendWithIpcFallback(to, message, opts) {
30
+ return sendMessageWhatsApp(to, message, opts);
31
+ }
32
+ const DEFAULT_WEB_MEDIA_BYTES = 5 * 1024 * 1024;
33
+ const formatDuration = (ms) => ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${ms}ms`;
34
+ export { HEARTBEAT_PROMPT, HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN };
35
+ function elide(text, limit = 400) {
36
+ if (!text)
37
+ return text;
38
+ if (text.length <= limit)
39
+ return text;
40
+ return `${text.slice(0, limit)}… (truncated ${text.length - limit} chars)`;
41
+ }
42
+ function buildMentionConfig(cfg) {
43
+ const gc = cfg.routing?.groupChat;
44
+ const mentionRegexes = gc?.mentionPatterns
45
+ ?.map((p) => {
46
+ try {
47
+ return new RegExp(p, "i");
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ })
53
+ .filter((r) => Boolean(r)) ?? [];
54
+ return { mentionRegexes, allowFrom: cfg.whatsapp?.allowFrom };
55
+ }
56
+ function isBotMentioned(msg, mentionCfg) {
57
+ const clean = (text) => text
58
+ // Remove zero-width and directionality markers WhatsApp injects around display names
59
+ .replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")
60
+ .toLowerCase();
61
+ const isSelfChat = isSelfChatMode(msg.selfE164, mentionCfg.allowFrom);
62
+ if (msg.mentionedJids?.length && !isSelfChat) {
63
+ const normalizedMentions = msg.mentionedJids
64
+ .map((jid) => jidToE164(jid) ?? jid)
65
+ .filter(Boolean);
66
+ if (msg.selfE164 && normalizedMentions.includes(msg.selfE164))
67
+ return true;
68
+ if (msg.selfJid && msg.selfE164) {
69
+ // Some mentions use the bare JID; match on E.164 to be safe.
70
+ const bareSelf = msg.selfJid.replace(/:\\d+/, "");
71
+ if (normalizedMentions.includes(bareSelf))
72
+ return true;
73
+ }
74
+ }
75
+ else if (msg.mentionedJids?.length && isSelfChat) {
76
+ // Self-chat mode: ignore WhatsApp @mention JIDs, otherwise @mentioning the owner in group chats triggers the bot.
77
+ }
78
+ const bodyClean = clean(msg.body);
79
+ if (mentionCfg.mentionRegexes.some((re) => re.test(bodyClean)))
80
+ return true;
81
+ // Fallback: detect body containing our own number (with or without +, spacing)
82
+ if (msg.selfE164) {
83
+ const selfDigits = msg.selfE164.replace(/\D/g, "");
84
+ if (selfDigits) {
85
+ const bodyDigits = bodyClean.replace(/[^\d]/g, "");
86
+ if (bodyDigits.includes(selfDigits))
87
+ return true;
88
+ const bodyNoSpace = msg.body.replace(/[\s-]/g, "");
89
+ const pattern = new RegExp(`\\+?${selfDigits}`, "i");
90
+ if (pattern.test(bodyNoSpace))
91
+ return true;
92
+ }
93
+ }
94
+ return false;
95
+ }
96
+ function debugMention(msg, mentionCfg) {
97
+ const result = isBotMentioned(msg, mentionCfg);
98
+ const details = {
99
+ from: msg.from,
100
+ body: msg.body,
101
+ bodyClean: msg.body
102
+ .replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")
103
+ .toLowerCase(),
104
+ mentionedJids: msg.mentionedJids ?? null,
105
+ selfJid: msg.selfJid ?? null,
106
+ selfE164: msg.selfE164 ?? null,
107
+ };
108
+ return { wasMentioned: result, details };
109
+ }
110
+ export { stripHeartbeatToken };
111
+ function isSilentReply(payload) {
112
+ if (!payload)
113
+ return false;
114
+ const text = payload.text?.trim();
115
+ if (!text || text !== SILENT_REPLY_TOKEN)
116
+ return false;
117
+ if (payload.mediaUrl || payload.mediaUrls?.length)
118
+ return false;
119
+ return true;
120
+ }
121
+ function resolveHeartbeatReplyPayload(replyResult) {
122
+ if (!replyResult)
123
+ return undefined;
124
+ if (!Array.isArray(replyResult))
125
+ return replyResult;
126
+ for (let idx = replyResult.length - 1; idx >= 0; idx -= 1) {
127
+ const payload = replyResult[idx];
128
+ if (!payload)
129
+ continue;
130
+ if (payload.text ||
131
+ payload.mediaUrl ||
132
+ (payload.mediaUrls && payload.mediaUrls.length > 0)) {
133
+ return payload;
134
+ }
135
+ }
136
+ return undefined;
137
+ }
138
+ export async function runWebHeartbeatOnce(opts) {
139
+ const { cfg: cfgOverride, to, verbose = false, sessionId, overrideBody, dryRun = false, } = opts;
140
+ const replyResolver = opts.replyResolver ?? getReplyFromConfig;
141
+ const sender = opts.sender ?? sendWithIpcFallback;
142
+ const runId = newConnectionId();
143
+ const heartbeatLogger = getChildLogger({
144
+ module: "web-heartbeat",
145
+ runId,
146
+ to,
147
+ });
148
+ const cfg = cfgOverride ?? loadConfig();
149
+ const sessionCfg = cfg.session;
150
+ const sessionScope = sessionCfg?.scope ?? "per-sender";
151
+ const mainKey = sessionCfg?.mainKey;
152
+ const sessionKey = resolveSessionKey(sessionScope, { From: to }, mainKey);
153
+ if (sessionId) {
154
+ const storePath = resolveStorePath(cfg.session?.store);
155
+ const store = loadSessionStore(storePath);
156
+ const current = store[sessionKey] ?? {};
157
+ store[sessionKey] = {
158
+ ...current,
159
+ sessionId,
160
+ updatedAt: Date.now(),
161
+ };
162
+ await saveSessionStore(storePath, store);
163
+ }
164
+ const sessionSnapshot = getSessionSnapshot(cfg, to, true);
165
+ if (verbose) {
166
+ heartbeatLogger.info({
167
+ to,
168
+ sessionKey: sessionSnapshot.key,
169
+ sessionId: sessionId ?? sessionSnapshot.entry?.sessionId ?? null,
170
+ sessionFresh: sessionSnapshot.fresh,
171
+ idleMinutes: sessionSnapshot.idleMinutes,
172
+ }, "heartbeat session snapshot");
173
+ }
174
+ if (overrideBody && overrideBody.trim().length === 0) {
175
+ throw new Error("Override body must be non-empty when provided.");
176
+ }
177
+ try {
178
+ if (overrideBody) {
179
+ if (dryRun) {
180
+ whatsappHeartbeatLog.info(`[dry-run] web send -> ${to}: ${elide(overrideBody.trim(), 200)} (manual message)`);
181
+ return;
182
+ }
183
+ const sendResult = await sender(to, overrideBody, { verbose });
184
+ emitHeartbeatEvent({
185
+ status: "sent",
186
+ to,
187
+ preview: overrideBody.slice(0, 160),
188
+ hasMedia: false,
189
+ });
190
+ heartbeatLogger.info({
191
+ to,
192
+ messageId: sendResult.messageId,
193
+ chars: overrideBody.length,
194
+ reason: "manual-message",
195
+ }, "manual heartbeat message sent");
196
+ whatsappHeartbeatLog.info(`manual heartbeat sent to ${to} (id ${sendResult.messageId})`);
197
+ return;
198
+ }
199
+ const replyResult = await replyResolver({
200
+ Body: HEARTBEAT_PROMPT,
201
+ From: to,
202
+ To: to,
203
+ MessageSid: sessionId ?? sessionSnapshot.entry?.sessionId,
204
+ }, { isHeartbeat: true }, cfg);
205
+ const replyPayload = resolveHeartbeatReplyPayload(replyResult);
206
+ if (!replyPayload ||
207
+ (!replyPayload.text &&
208
+ !replyPayload.mediaUrl &&
209
+ !replyPayload.mediaUrls?.length)) {
210
+ heartbeatLogger.info({
211
+ to,
212
+ reason: "empty-reply",
213
+ sessionId: sessionSnapshot.entry?.sessionId ?? null,
214
+ }, "heartbeat skipped");
215
+ if (shouldLogVerbose()) {
216
+ whatsappHeartbeatLog.debug("heartbeat ok (empty reply)");
217
+ }
218
+ emitHeartbeatEvent({ status: "ok-empty", to });
219
+ return;
220
+ }
221
+ const hasMedia = Boolean(replyPayload.mediaUrl || (replyPayload.mediaUrls?.length ?? 0) > 0);
222
+ const stripped = stripHeartbeatToken(replyPayload.text, {
223
+ mode: "heartbeat",
224
+ maxAckChars: 30,
225
+ });
226
+ if (stripped.shouldSkip && !hasMedia) {
227
+ // Don't let heartbeats keep sessions alive: restore previous updatedAt so idle expiry still works.
228
+ const storePath = resolveStorePath(cfg.session?.store);
229
+ const store = loadSessionStore(storePath);
230
+ if (sessionSnapshot.entry && store[sessionSnapshot.key]) {
231
+ store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt;
232
+ await saveSessionStore(storePath, store);
233
+ }
234
+ heartbeatLogger.info({ to, reason: "heartbeat-token", rawLength: replyPayload.text?.length }, "heartbeat skipped");
235
+ if (shouldLogVerbose()) {
236
+ whatsappHeartbeatLog.debug("heartbeat ok (HEARTBEAT_OK)");
237
+ }
238
+ emitHeartbeatEvent({ status: "ok-token", to });
239
+ return;
240
+ }
241
+ if (hasMedia) {
242
+ heartbeatLogger.warn({ to }, "heartbeat reply contained media; sending text only");
243
+ }
244
+ const finalText = stripped.text || replyPayload.text || "";
245
+ if (dryRun) {
246
+ heartbeatLogger.info({ to, reason: "dry-run", chars: finalText.length }, "heartbeat dry-run");
247
+ whatsappHeartbeatLog.info(`[dry-run] heartbeat -> ${to}: ${elide(finalText, 200)}`);
248
+ return;
249
+ }
250
+ const sendResult = await sender(to, finalText, { verbose });
251
+ emitHeartbeatEvent({
252
+ status: "sent",
253
+ to,
254
+ preview: finalText.slice(0, 160),
255
+ hasMedia,
256
+ });
257
+ heartbeatLogger.info({
258
+ to,
259
+ messageId: sendResult.messageId,
260
+ chars: finalText.length,
261
+ preview: elide(finalText, 140),
262
+ }, "heartbeat sent");
263
+ whatsappHeartbeatLog.info(`heartbeat alert sent to ${to}`);
264
+ }
265
+ catch (err) {
266
+ const reason = formatError(err);
267
+ heartbeatLogger.warn({ to, error: reason }, "heartbeat failed");
268
+ whatsappHeartbeatLog.warn(`heartbeat failed (${reason})`);
269
+ emitHeartbeatEvent({ status: "failed", to, reason });
270
+ throw err;
271
+ }
272
+ }
273
+ function getSessionRecipients(cfg) {
274
+ const sessionCfg = cfg.session;
275
+ const scope = sessionCfg?.scope ?? "per-sender";
276
+ if (scope === "global")
277
+ return [];
278
+ const storePath = resolveStorePath(cfg.session?.store);
279
+ const store = loadSessionStore(storePath);
280
+ const isGroupKey = (key) => key.startsWith("group:") ||
281
+ key.includes(":group:") ||
282
+ key.includes(":channel:") ||
283
+ key.includes("@g.us");
284
+ const isCronKey = (key) => key.startsWith("cron:");
285
+ const recipients = Object.entries(store)
286
+ .filter(([key]) => key !== "global" && key !== "unknown")
287
+ .filter(([key]) => !isGroupKey(key) && !isCronKey(key))
288
+ .map(([_, entry]) => ({
289
+ to: entry?.lastChannel === "whatsapp" && entry?.lastTo
290
+ ? normalizeE164(entry.lastTo)
291
+ : "",
292
+ updatedAt: entry?.updatedAt ?? 0,
293
+ }))
294
+ .filter(({ to }) => to.length > 1)
295
+ .sort((a, b) => b.updatedAt - a.updatedAt);
296
+ // Dedupe while preserving recency ordering.
297
+ const seen = new Set();
298
+ return recipients.filter((r) => {
299
+ if (seen.has(r.to))
300
+ return false;
301
+ seen.add(r.to);
302
+ return true;
303
+ });
304
+ }
305
+ export function resolveHeartbeatRecipients(cfg, opts = {}) {
306
+ if (opts.to)
307
+ return { recipients: [normalizeE164(opts.to)], source: "flag" };
308
+ const sessionRecipients = getSessionRecipients(cfg);
309
+ const allowFrom = Array.isArray(cfg.whatsapp?.allowFrom) && cfg.whatsapp.allowFrom.length > 0
310
+ ? cfg.whatsapp.allowFrom.filter((v) => v !== "*").map(normalizeE164)
311
+ : [];
312
+ const unique = (list) => [...new Set(list.filter(Boolean))];
313
+ if (opts.all) {
314
+ const all = unique([...sessionRecipients.map((s) => s.to), ...allowFrom]);
315
+ return { recipients: all, source: "all" };
316
+ }
317
+ if (sessionRecipients.length === 1) {
318
+ return { recipients: [sessionRecipients[0].to], source: "session-single" };
319
+ }
320
+ if (sessionRecipients.length > 1) {
321
+ return {
322
+ recipients: sessionRecipients.map((s) => s.to),
323
+ source: "session-ambiguous",
324
+ };
325
+ }
326
+ return { recipients: allowFrom, source: "allowFrom" };
327
+ }
328
+ function getSessionSnapshot(cfg, from, isHeartbeat = false) {
329
+ const sessionCfg = cfg.session;
330
+ const scope = sessionCfg?.scope ?? "per-sender";
331
+ const key = resolveSessionKey(scope, { From: from, To: "", Body: "" }, sessionCfg?.mainKey);
332
+ const store = loadSessionStore(resolveStorePath(sessionCfg?.store));
333
+ const entry = store[key];
334
+ const idleMinutes = Math.max((isHeartbeat
335
+ ? (sessionCfg?.heartbeatIdleMinutes ?? sessionCfg?.idleMinutes)
336
+ : sessionCfg?.idleMinutes) ?? DEFAULT_IDLE_MINUTES, 1);
337
+ const fresh = !!(entry && Date.now() - entry.updatedAt <= idleMinutes * 60_000);
338
+ return { key, entry, fresh, idleMinutes };
339
+ }
340
+ async function deliverWebReply(params) {
341
+ const { replyResult, msg, maxMediaBytes, textLimit, replyLogger, connectionId, skipLog, } = params;
342
+ const replyStarted = Date.now();
343
+ const textChunks = chunkText(replyResult.text || "", textLimit);
344
+ const mediaList = replyResult.mediaUrls?.length
345
+ ? replyResult.mediaUrls
346
+ : replyResult.mediaUrl
347
+ ? [replyResult.mediaUrl]
348
+ : [];
349
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
350
+ const sendWithRetry = async (fn, label, maxAttempts = 3) => {
351
+ let lastErr;
352
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
353
+ try {
354
+ return await fn();
355
+ }
356
+ catch (err) {
357
+ lastErr = err;
358
+ const errText = formatError(err);
359
+ const isLast = attempt === maxAttempts;
360
+ const shouldRetry = /closed|reset|timed\s*out|disconnect/i.test(errText);
361
+ if (!shouldRetry || isLast) {
362
+ throw err;
363
+ }
364
+ const backoffMs = 500 * attempt;
365
+ logVerbose(`Retrying ${label} to ${msg.from} after failure (${attempt}/${maxAttempts - 1}) in ${backoffMs}ms: ${errText}`);
366
+ await sleep(backoffMs);
367
+ }
368
+ }
369
+ throw lastErr;
370
+ };
371
+ // Text-only replies
372
+ if (mediaList.length === 0 && textChunks.length) {
373
+ const totalChunks = textChunks.length;
374
+ for (const [index, chunk] of textChunks.entries()) {
375
+ const chunkStarted = Date.now();
376
+ await sendWithRetry(() => msg.reply(chunk), "text");
377
+ if (!skipLog) {
378
+ const durationMs = Date.now() - chunkStarted;
379
+ whatsappOutboundLog.debug(`Sent chunk ${index + 1}/${totalChunks} to ${msg.from} (${durationMs.toFixed(0)}ms)`);
380
+ }
381
+ }
382
+ replyLogger.info({
383
+ correlationId: msg.id ?? newConnectionId(),
384
+ connectionId: connectionId ?? null,
385
+ to: msg.from,
386
+ from: msg.to,
387
+ text: elide(replyResult.text, 240),
388
+ mediaUrl: null,
389
+ mediaSizeBytes: null,
390
+ mediaKind: null,
391
+ durationMs: Date.now() - replyStarted,
392
+ }, "auto-reply sent (text)");
393
+ return;
394
+ }
395
+ const remainingText = [...textChunks];
396
+ // Media (with optional caption on first item)
397
+ for (const [index, mediaUrl] of mediaList.entries()) {
398
+ const caption = index === 0 ? remainingText.shift() || undefined : undefined;
399
+ try {
400
+ const media = await loadWebMedia(mediaUrl, maxMediaBytes);
401
+ if (shouldLogVerbose()) {
402
+ logVerbose(`Web auto-reply media size: ${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB`);
403
+ logVerbose(`Web auto-reply media source: ${mediaUrl} (kind ${media.kind})`);
404
+ }
405
+ if (media.kind === "image") {
406
+ await sendWithRetry(() => msg.sendMedia({
407
+ image: media.buffer,
408
+ caption,
409
+ mimetype: media.contentType,
410
+ }), "media:image");
411
+ }
412
+ else if (media.kind === "audio") {
413
+ await sendWithRetry(() => msg.sendMedia({
414
+ audio: media.buffer,
415
+ ptt: true,
416
+ mimetype: media.contentType,
417
+ caption,
418
+ }), "media:audio");
419
+ }
420
+ else if (media.kind === "video") {
421
+ await sendWithRetry(() => msg.sendMedia({
422
+ video: media.buffer,
423
+ caption,
424
+ mimetype: media.contentType,
425
+ }), "media:video");
426
+ }
427
+ else {
428
+ const fileName = media.fileName ?? mediaUrl.split("/").pop() ?? "file";
429
+ const mimetype = media.contentType ?? "application/octet-stream";
430
+ await sendWithRetry(() => msg.sendMedia({
431
+ document: media.buffer,
432
+ fileName,
433
+ caption,
434
+ mimetype,
435
+ }), "media:document");
436
+ }
437
+ whatsappOutboundLog.info(`Sent media reply to ${msg.from} (${(media.buffer.length / (1024 * 1024)).toFixed(2)}MB)`);
438
+ replyLogger.info({
439
+ correlationId: msg.id ?? newConnectionId(),
440
+ connectionId: connectionId ?? null,
441
+ to: msg.from,
442
+ from: msg.to,
443
+ text: caption ?? null,
444
+ mediaUrl,
445
+ mediaSizeBytes: media.buffer.length,
446
+ mediaKind: media.kind,
447
+ durationMs: Date.now() - replyStarted,
448
+ }, "auto-reply sent (media)");
449
+ }
450
+ catch (err) {
451
+ whatsappOutboundLog.error(`Failed sending web media to ${msg.from}: ${formatError(err)}`);
452
+ replyLogger.warn({ err, mediaUrl }, "failed to send web media reply");
453
+ if (index === 0) {
454
+ const warning = err instanceof Error
455
+ ? `⚠️ Media failed: ${err.message}`
456
+ : "⚠️ Media failed.";
457
+ const fallbackTextParts = [
458
+ remainingText.shift() ?? caption ?? "",
459
+ warning,
460
+ ].filter(Boolean);
461
+ const fallbackText = fallbackTextParts.join("\n");
462
+ if (fallbackText) {
463
+ whatsappOutboundLog.warn(`Media skipped; sent text-only to ${msg.from}`);
464
+ await msg.reply(fallbackText);
465
+ }
466
+ }
467
+ }
468
+ }
469
+ // Remaining text chunks after media
470
+ for (const chunk of remainingText) {
471
+ await msg.reply(chunk);
472
+ }
473
+ }
474
+ export async function monitorWebProvider(verbose, listenerFactory = monitorWebInbox, keepAlive = true, replyResolver = getReplyFromConfig, runtime = defaultRuntime, abortSignal, tuning = {}) {
475
+ const runId = newConnectionId();
476
+ const replyLogger = getChildLogger({ module: "web-auto-reply", runId });
477
+ const heartbeatLogger = getChildLogger({ module: "web-heartbeat", runId });
478
+ const reconnectLogger = getChildLogger({ module: "web-reconnect", runId });
479
+ const status = {
480
+ running: true,
481
+ connected: false,
482
+ reconnectAttempts: 0,
483
+ lastConnectedAt: null,
484
+ lastDisconnect: null,
485
+ lastMessageAt: null,
486
+ lastEventAt: null,
487
+ lastError: null,
488
+ };
489
+ const emitStatus = () => {
490
+ tuning.statusSink?.({
491
+ ...status,
492
+ lastDisconnect: status.lastDisconnect
493
+ ? { ...status.lastDisconnect }
494
+ : null,
495
+ });
496
+ };
497
+ emitStatus();
498
+ const cfg = loadConfig();
499
+ const configuredMaxMb = cfg.agent?.mediaMaxMb;
500
+ const maxMediaBytes = typeof configuredMaxMb === "number" && configuredMaxMb > 0
501
+ ? configuredMaxMb * 1024 * 1024
502
+ : DEFAULT_WEB_MEDIA_BYTES;
503
+ const heartbeatSeconds = resolveHeartbeatSeconds(cfg, tuning.heartbeatSeconds);
504
+ const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
505
+ const mentionConfig = buildMentionConfig(cfg);
506
+ const sessionStorePath = resolveStorePath(cfg.session?.store);
507
+ const groupHistoryLimit = cfg.routing?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
508
+ const groupHistories = new Map();
509
+ const groupMemberNames = new Map();
510
+ const sleep = tuning.sleep ??
511
+ ((ms, signal) => sleepWithAbort(ms, signal ?? abortSignal));
512
+ const stopRequested = () => abortSignal?.aborted === true;
513
+ const abortPromise = abortSignal &&
514
+ new Promise((resolve) => abortSignal.addEventListener("abort", () => resolve("aborted"), {
515
+ once: true,
516
+ }));
517
+ const noteGroupMember = (conversationId, e164, name) => {
518
+ if (!e164 || !name)
519
+ return;
520
+ const normalized = normalizeE164(e164);
521
+ const key = normalized ?? e164;
522
+ if (!key)
523
+ return;
524
+ let roster = groupMemberNames.get(conversationId);
525
+ if (!roster) {
526
+ roster = new Map();
527
+ groupMemberNames.set(conversationId, roster);
528
+ }
529
+ roster.set(key, name);
530
+ };
531
+ const formatGroupMembers = (participants, roster, fallbackE164) => {
532
+ const seen = new Set();
533
+ const ordered = [];
534
+ if (participants?.length) {
535
+ for (const entry of participants) {
536
+ if (!entry)
537
+ continue;
538
+ const normalized = normalizeE164(entry) ?? entry;
539
+ if (!normalized || seen.has(normalized))
540
+ continue;
541
+ seen.add(normalized);
542
+ ordered.push(normalized);
543
+ }
544
+ }
545
+ if (roster) {
546
+ for (const entry of roster.keys()) {
547
+ const normalized = normalizeE164(entry) ?? entry;
548
+ if (!normalized || seen.has(normalized))
549
+ continue;
550
+ seen.add(normalized);
551
+ ordered.push(normalized);
552
+ }
553
+ }
554
+ if (ordered.length === 0 && fallbackE164) {
555
+ const normalized = normalizeE164(fallbackE164) ?? fallbackE164;
556
+ if (normalized)
557
+ ordered.push(normalized);
558
+ }
559
+ if (ordered.length === 0)
560
+ return undefined;
561
+ return ordered
562
+ .map((entry) => {
563
+ const name = roster?.get(entry);
564
+ return name ? `${name} (${entry})` : entry;
565
+ })
566
+ .join(", ");
567
+ };
568
+ const resolveGroupResolution = (conversationId) => resolveGroupSessionKey({
569
+ From: conversationId,
570
+ ChatType: "group",
571
+ Surface: "whatsapp",
572
+ });
573
+ const resolveGroupRequireMentionFor = (conversationId) => {
574
+ const groupId = resolveGroupResolution(conversationId)?.id ?? conversationId;
575
+ const groupConfig = cfg.whatsapp?.groups?.[groupId];
576
+ if (typeof groupConfig?.requireMention === "boolean") {
577
+ return groupConfig.requireMention;
578
+ }
579
+ const groupDefault = cfg.whatsapp?.groups?.["*"]?.requireMention;
580
+ if (typeof groupDefault === "boolean")
581
+ return groupDefault;
582
+ return true;
583
+ };
584
+ const resolveGroupActivationFor = (conversationId) => {
585
+ const key = resolveGroupResolution(conversationId)?.key ??
586
+ (conversationId.startsWith("group:")
587
+ ? conversationId
588
+ : `whatsapp:group:${conversationId}`);
589
+ const store = loadSessionStore(sessionStorePath);
590
+ const entry = store[key];
591
+ const requireMention = resolveGroupRequireMentionFor(conversationId);
592
+ const defaultActivation = requireMention === false ? "always" : "mention";
593
+ return (normalizeGroupActivation(entry?.groupActivation) ?? defaultActivation);
594
+ };
595
+ const resolveOwnerList = (selfE164) => {
596
+ const allowFrom = mentionConfig.allowFrom;
597
+ const raw = Array.isArray(allowFrom) && allowFrom.length > 0
598
+ ? allowFrom
599
+ : selfE164
600
+ ? [selfE164]
601
+ : [];
602
+ return raw
603
+ .filter((entry) => Boolean(entry && entry !== "*"))
604
+ .map((entry) => normalizeE164(entry))
605
+ .filter((entry) => Boolean(entry));
606
+ };
607
+ const isOwnerSender = (msg) => {
608
+ const sender = normalizeE164(msg.senderE164 ?? "");
609
+ if (!sender)
610
+ return false;
611
+ const owners = resolveOwnerList(msg.selfE164 ?? undefined);
612
+ return owners.includes(sender);
613
+ };
614
+ const isStatusCommand = (body) => {
615
+ const trimmed = body.trim().toLowerCase();
616
+ if (!trimmed)
617
+ return false;
618
+ return (trimmed === "/status" ||
619
+ trimmed === "status" ||
620
+ trimmed.startsWith("/status "));
621
+ };
622
+ const stripMentionsForCommand = (text, selfE164) => {
623
+ let result = text;
624
+ for (const re of mentionConfig.mentionRegexes) {
625
+ result = result.replace(re, " ");
626
+ }
627
+ if (selfE164) {
628
+ const digits = selfE164.replace(/\D/g, "");
629
+ if (digits) {
630
+ const pattern = new RegExp(`\\+?${digits}`, "g");
631
+ result = result.replace(pattern, " ");
632
+ }
633
+ }
634
+ return result.replace(/\s+/g, " ").trim();
635
+ };
636
+ // Avoid noisy MaxListenersExceeded warnings in test environments where
637
+ // multiple gateway instances may be constructed.
638
+ const currentMaxListeners = process.getMaxListeners?.() ?? 10;
639
+ if (process.setMaxListeners && currentMaxListeners < 50) {
640
+ process.setMaxListeners(50);
641
+ }
642
+ let sigintStop = false;
643
+ const handleSigint = () => {
644
+ sigintStop = true;
645
+ };
646
+ process.once("SIGINT", handleSigint);
647
+ let reconnectAttempts = 0;
648
+ // Track recently sent messages to prevent echo loops
649
+ const recentlySent = new Set();
650
+ const MAX_RECENT_MESSAGES = 100;
651
+ while (true) {
652
+ if (stopRequested())
653
+ break;
654
+ const connectionId = newConnectionId();
655
+ const startedAt = Date.now();
656
+ let heartbeat = null;
657
+ let watchdogTimer = null;
658
+ let lastMessageAt = null;
659
+ let handledMessages = 0;
660
+ let _lastInboundMsg = null;
661
+ // Watchdog to detect stuck message processing (e.g., event emitter died)
662
+ // Should be significantly longer than the reply heartbeat interval to avoid false positives
663
+ const MESSAGE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes without any messages
664
+ const WATCHDOG_CHECK_MS = 60 * 1000; // Check every minute
665
+ const backgroundTasks = new Set();
666
+ const formatReplyContext = (msg) => {
667
+ if (!msg.replyToBody)
668
+ return null;
669
+ const sender = msg.replyToSender ?? "unknown sender";
670
+ const idPart = msg.replyToId ? ` id:${msg.replyToId}` : "";
671
+ return `[Replying to ${sender}${idPart}]\n${msg.replyToBody}\n[/Replying]`;
672
+ };
673
+ const buildLine = (msg) => {
674
+ // Build message prefix: explicit config > default based on allowFrom
675
+ let messagePrefix = cfg.messages?.messagePrefix;
676
+ if (messagePrefix === undefined) {
677
+ const hasAllowFrom = (cfg.whatsapp?.allowFrom?.length ?? 0) > 0;
678
+ messagePrefix = hasAllowFrom ? "" : "[clawdbot]";
679
+ }
680
+ const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
681
+ const senderLabel = msg.chatType === "group"
682
+ ? `${msg.senderName ?? msg.senderE164 ?? "Someone"}: `
683
+ : "";
684
+ const replyContext = formatReplyContext(msg);
685
+ const baseLine = `${prefixStr}${senderLabel}${msg.body}${replyContext ? `\n\n${replyContext}` : ""}`;
686
+ // Wrap with standardized envelope for the agent.
687
+ return formatAgentEnvelope({
688
+ surface: "WhatsApp",
689
+ from: msg.chatType === "group"
690
+ ? msg.from
691
+ : msg.from?.replace(/^whatsapp:/, ""),
692
+ timestamp: msg.timestamp,
693
+ body: baseLine,
694
+ });
695
+ };
696
+ const processMessage = async (msg) => {
697
+ status.lastMessageAt = Date.now();
698
+ status.lastEventAt = status.lastMessageAt;
699
+ emitStatus();
700
+ const conversationId = msg.conversationId ?? msg.from;
701
+ let combinedBody = buildLine(msg);
702
+ let shouldClearGroupHistory = false;
703
+ if (msg.chatType === "group") {
704
+ const history = groupHistories.get(conversationId) ?? [];
705
+ const historyWithoutCurrent = history.length > 0 ? history.slice(0, -1) : [];
706
+ if (historyWithoutCurrent.length > 0) {
707
+ const historyText = historyWithoutCurrent
708
+ .map((m) => formatAgentEnvelope({
709
+ surface: "WhatsApp",
710
+ from: conversationId,
711
+ timestamp: m.timestamp,
712
+ body: `${m.sender}: ${m.body}`,
713
+ }))
714
+ .join("\\n");
715
+ combinedBody = `[Chat messages since your last reply - for context]\\n${historyText}\\n\\n[Current message - respond to this]\\n${buildLine(msg)}`;
716
+ }
717
+ // Always surface who sent the triggering message so the agent can address them.
718
+ const senderLabel = msg.senderName && msg.senderE164
719
+ ? `${msg.senderName} (${msg.senderE164})`
720
+ : (msg.senderName ?? msg.senderE164 ?? "Unknown");
721
+ combinedBody = `${combinedBody}\\n[from: ${senderLabel}]`;
722
+ shouldClearGroupHistory = true;
723
+ }
724
+ // Echo detection uses combined body so we don't respond twice.
725
+ if (recentlySent.has(combinedBody)) {
726
+ logVerbose(`Skipping auto-reply: detected echo for combined message`);
727
+ recentlySent.delete(combinedBody);
728
+ return;
729
+ }
730
+ const correlationId = msg.id ?? newConnectionId();
731
+ replyLogger.info({
732
+ connectionId,
733
+ correlationId,
734
+ from: msg.chatType === "group" ? conversationId : msg.from,
735
+ to: msg.to,
736
+ body: elide(combinedBody, 240),
737
+ mediaType: msg.mediaType ?? null,
738
+ mediaPath: msg.mediaPath ?? null,
739
+ }, "inbound web message");
740
+ const fromDisplay = msg.chatType === "group" ? conversationId : msg.from;
741
+ const kindLabel = msg.mediaType ? `, ${msg.mediaType}` : "";
742
+ whatsappInboundLog.info(`Inbound message ${fromDisplay} -> ${msg.to} (${msg.chatType}${kindLabel}, ${combinedBody.length} chars)`);
743
+ if (shouldLogVerbose()) {
744
+ whatsappInboundLog.debug(`Inbound body: ${elide(combinedBody, 400)}`);
745
+ }
746
+ if (msg.chatType !== "group") {
747
+ const sessionCfg = cfg.session;
748
+ const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
749
+ const storePath = resolveStorePath(sessionCfg?.store);
750
+ const to = (() => {
751
+ if (msg.senderE164)
752
+ return normalizeE164(msg.senderE164);
753
+ // In direct chats, `msg.from` is already the canonical conversation id,
754
+ // which is an E.164 string (e.g. "+1555"). Only fall back to JID parsing
755
+ // when we were handed a JID-like string.
756
+ if (msg.from.includes("@"))
757
+ return jidToE164(msg.from);
758
+ return normalizeE164(msg.from);
759
+ })();
760
+ if (to) {
761
+ const task = updateLastRoute({
762
+ storePath,
763
+ sessionKey: mainKey,
764
+ channel: "whatsapp",
765
+ to,
766
+ }).catch((err) => {
767
+ replyLogger.warn({ error: formatError(err), storePath, sessionKey: mainKey, to }, "failed updating last route");
768
+ });
769
+ backgroundTasks.add(task);
770
+ void task.finally(() => {
771
+ backgroundTasks.delete(task);
772
+ });
773
+ }
774
+ }
775
+ const responsePrefix = cfg.messages?.responsePrefix;
776
+ const textLimit = resolveTextChunkLimit(cfg, "whatsapp");
777
+ let didLogHeartbeatStrip = false;
778
+ let didSendReply = false;
779
+ let toolSendChain = Promise.resolve();
780
+ const sendToolResult = (payload) => {
781
+ if (!payload?.text &&
782
+ !payload?.mediaUrl &&
783
+ !(payload?.mediaUrls?.length ?? 0)) {
784
+ return;
785
+ }
786
+ if (isSilentReply(payload))
787
+ return;
788
+ const toolPayload = { ...payload };
789
+ if (toolPayload.text?.includes(HEARTBEAT_TOKEN)) {
790
+ const stripped = stripHeartbeatToken(toolPayload.text, {
791
+ mode: "message",
792
+ });
793
+ if (stripped.didStrip && !didLogHeartbeatStrip) {
794
+ didLogHeartbeatStrip = true;
795
+ logVerbose("Stripped stray HEARTBEAT_OK token from web reply");
796
+ }
797
+ const hasMedia = Boolean(toolPayload.mediaUrl || (toolPayload.mediaUrls?.length ?? 0) > 0);
798
+ if (stripped.shouldSkip && !hasMedia)
799
+ return;
800
+ toolPayload.text = stripped.text;
801
+ }
802
+ if (responsePrefix &&
803
+ toolPayload.text &&
804
+ toolPayload.text.trim() !== HEARTBEAT_TOKEN &&
805
+ !toolPayload.text.startsWith(responsePrefix)) {
806
+ toolPayload.text = `${responsePrefix} ${toolPayload.text}`;
807
+ }
808
+ toolSendChain = toolSendChain
809
+ .then(async () => {
810
+ await deliverWebReply({
811
+ replyResult: toolPayload,
812
+ msg,
813
+ maxMediaBytes,
814
+ textLimit,
815
+ replyLogger,
816
+ connectionId,
817
+ skipLog: true,
818
+ });
819
+ didSendReply = true;
820
+ if (toolPayload.text) {
821
+ recentlySent.add(toolPayload.text);
822
+ if (recentlySent.size > MAX_RECENT_MESSAGES) {
823
+ const firstKey = recentlySent.values().next().value;
824
+ if (firstKey)
825
+ recentlySent.delete(firstKey);
826
+ }
827
+ }
828
+ })
829
+ .catch((err) => {
830
+ whatsappOutboundLog.error(`Failed sending web tool update to ${msg.from ?? conversationId}: ${formatError(err)}`);
831
+ });
832
+ };
833
+ const sendBlockReply = (payload) => {
834
+ if (!payload?.text &&
835
+ !payload?.mediaUrl &&
836
+ !(payload?.mediaUrls?.length ?? 0)) {
837
+ return;
838
+ }
839
+ if (isSilentReply(payload))
840
+ return;
841
+ const blockPayload = { ...payload };
842
+ if (responsePrefix &&
843
+ blockPayload.text &&
844
+ blockPayload.text.trim() !== HEARTBEAT_TOKEN &&
845
+ !blockPayload.text.startsWith(responsePrefix)) {
846
+ blockPayload.text = `${responsePrefix} ${blockPayload.text}`;
847
+ }
848
+ toolSendChain = toolSendChain
849
+ .then(async () => {
850
+ await deliverWebReply({
851
+ replyResult: blockPayload,
852
+ msg,
853
+ maxMediaBytes,
854
+ textLimit,
855
+ replyLogger,
856
+ connectionId,
857
+ skipLog: true,
858
+ });
859
+ didSendReply = true;
860
+ if (blockPayload.text) {
861
+ recentlySent.add(blockPayload.text);
862
+ recentlySent.add(combinedBody);
863
+ if (recentlySent.size > MAX_RECENT_MESSAGES) {
864
+ const firstKey = recentlySent.values().next().value;
865
+ if (firstKey)
866
+ recentlySent.delete(firstKey);
867
+ }
868
+ }
869
+ })
870
+ .catch((err) => {
871
+ whatsappOutboundLog.error(`Failed sending web block update to ${msg.from ?? conversationId}: ${formatError(err)}`);
872
+ });
873
+ };
874
+ const replyResult = await (replyResolver ?? getReplyFromConfig)({
875
+ Body: combinedBody,
876
+ From: msg.from,
877
+ To: msg.to,
878
+ MessageSid: msg.id,
879
+ ReplyToId: msg.replyToId,
880
+ ReplyToBody: msg.replyToBody,
881
+ ReplyToSender: msg.replyToSender,
882
+ MediaPath: msg.mediaPath,
883
+ MediaUrl: msg.mediaUrl,
884
+ MediaType: msg.mediaType,
885
+ ChatType: msg.chatType,
886
+ GroupSubject: msg.groupSubject,
887
+ GroupMembers: formatGroupMembers(msg.groupParticipants, groupMemberNames.get(conversationId), msg.senderE164),
888
+ SenderName: msg.senderName,
889
+ SenderE164: msg.senderE164,
890
+ WasMentioned: msg.wasMentioned,
891
+ Surface: "whatsapp",
892
+ }, {
893
+ onReplyStart: msg.sendComposing,
894
+ onToolResult: sendToolResult,
895
+ onBlockReply: sendBlockReply,
896
+ });
897
+ const replyList = replyResult
898
+ ? Array.isArray(replyResult)
899
+ ? replyResult
900
+ : [replyResult]
901
+ : [];
902
+ const sendableReplies = replyList.filter((payload) => !isSilentReply(payload));
903
+ if (sendableReplies.length === 0) {
904
+ await toolSendChain;
905
+ if (shouldClearGroupHistory && didSendReply) {
906
+ groupHistories.set(conversationId, []);
907
+ }
908
+ logVerbose("Skipping auto-reply: silent token or no text/media returned from resolver");
909
+ return;
910
+ }
911
+ await toolSendChain;
912
+ for (const replyPayload of sendableReplies) {
913
+ if (replyPayload.text?.includes(HEARTBEAT_TOKEN)) {
914
+ const stripped = stripHeartbeatToken(replyPayload.text, {
915
+ mode: "message",
916
+ });
917
+ if (stripped.didStrip && !didLogHeartbeatStrip) {
918
+ didLogHeartbeatStrip = true;
919
+ logVerbose("Stripped stray HEARTBEAT_OK token from web reply");
920
+ }
921
+ const hasMedia = Boolean(replyPayload.mediaUrl || (replyPayload.mediaUrls?.length ?? 0) > 0);
922
+ if (stripped.shouldSkip && !hasMedia)
923
+ continue;
924
+ replyPayload.text = stripped.text;
925
+ }
926
+ if (responsePrefix &&
927
+ replyPayload.text &&
928
+ replyPayload.text.trim() !== HEARTBEAT_TOKEN &&
929
+ !replyPayload.text.startsWith(responsePrefix)) {
930
+ replyPayload.text = `${responsePrefix} ${replyPayload.text}`;
931
+ }
932
+ try {
933
+ await deliverWebReply({
934
+ replyResult: replyPayload,
935
+ msg,
936
+ maxMediaBytes,
937
+ textLimit,
938
+ replyLogger,
939
+ connectionId,
940
+ });
941
+ didSendReply = true;
942
+ if (replyPayload.text) {
943
+ recentlySent.add(replyPayload.text);
944
+ recentlySent.add(combinedBody); // Prevent echo on the combined text itself
945
+ logVerbose(`Added to echo detection set (size now: ${recentlySent.size}): ${replyPayload.text.substring(0, 50)}...`);
946
+ if (recentlySent.size > MAX_RECENT_MESSAGES) {
947
+ const firstKey = recentlySent.values().next().value;
948
+ if (firstKey)
949
+ recentlySent.delete(firstKey);
950
+ }
951
+ }
952
+ const fromDisplay = msg.chatType === "group" ? conversationId : (msg.from ?? "unknown");
953
+ const hasMedia = Boolean(replyPayload.mediaUrl || replyPayload.mediaUrls?.length);
954
+ whatsappOutboundLog.info(`Auto-replied to ${fromDisplay}${hasMedia ? " (media)" : ""}`);
955
+ if (shouldLogVerbose()) {
956
+ const preview = replyPayload.text != null
957
+ ? elide(replyPayload.text, 400)
958
+ : "<media>";
959
+ whatsappOutboundLog.debug(`Reply body: ${preview}${hasMedia ? " (media)" : ""}`);
960
+ }
961
+ }
962
+ catch (err) {
963
+ whatsappOutboundLog.error(`Failed sending web auto-reply to ${msg.from ?? conversationId}: ${formatError(err)}`);
964
+ }
965
+ }
966
+ if (shouldClearGroupHistory && didSendReply) {
967
+ groupHistories.set(conversationId, []);
968
+ }
969
+ };
970
+ const listener = await (listenerFactory ?? monitorWebInbox)({
971
+ verbose,
972
+ onMessage: async (msg) => {
973
+ handledMessages += 1;
974
+ lastMessageAt = Date.now();
975
+ status.lastMessageAt = lastMessageAt;
976
+ status.lastEventAt = lastMessageAt;
977
+ emitStatus();
978
+ _lastInboundMsg = msg;
979
+ const conversationId = msg.conversationId ?? msg.from;
980
+ // Same-phone mode logging retained
981
+ if (msg.from === msg.to) {
982
+ logVerbose(`📱 Same-phone mode detected (from === to: ${msg.from})`);
983
+ }
984
+ // Skip if this is a message we just sent (echo detection)
985
+ if (recentlySent.has(msg.body)) {
986
+ whatsappInboundLog.debug("Skipping echo: detected recently sent message");
987
+ logVerbose(`Skipping auto-reply: detected echo (message matches recently sent text)`);
988
+ recentlySent.delete(msg.body);
989
+ return;
990
+ }
991
+ if (msg.chatType === "group") {
992
+ noteGroupMember(conversationId, msg.senderE164, msg.senderName);
993
+ const commandBody = stripMentionsForCommand(msg.body, msg.selfE164);
994
+ const activationCommand = parseActivationCommand(commandBody);
995
+ const isOwner = isOwnerSender(msg);
996
+ const statusCommand = isStatusCommand(commandBody);
997
+ const shouldBypassMention = isOwner && (activationCommand.hasCommand || statusCommand);
998
+ if (activationCommand.hasCommand && !isOwner) {
999
+ logVerbose(`Ignoring /activation from non-owner in group ${conversationId}`);
1000
+ return;
1001
+ }
1002
+ if (!shouldBypassMention) {
1003
+ const history = groupHistories.get(conversationId) ??
1004
+ [];
1005
+ history.push({
1006
+ sender: msg.senderName ?? msg.senderE164 ?? "Unknown",
1007
+ body: msg.body,
1008
+ timestamp: msg.timestamp,
1009
+ });
1010
+ while (history.length > groupHistoryLimit)
1011
+ history.shift();
1012
+ groupHistories.set(conversationId, history);
1013
+ }
1014
+ const mentionDebug = debugMention(msg, mentionConfig);
1015
+ replyLogger.debug({
1016
+ conversationId,
1017
+ wasMentioned: mentionDebug.wasMentioned,
1018
+ ...mentionDebug.details,
1019
+ }, "group mention debug");
1020
+ const wasMentioned = mentionDebug.wasMentioned;
1021
+ msg.wasMentioned = wasMentioned;
1022
+ const activation = resolveGroupActivationFor(conversationId);
1023
+ const requireMention = activation !== "always";
1024
+ if (!shouldBypassMention && requireMention && !wasMentioned) {
1025
+ logVerbose(`Group message stored for context (no mention detected) in ${conversationId}: ${msg.body}`);
1026
+ return;
1027
+ }
1028
+ }
1029
+ return processMessage(msg);
1030
+ },
1031
+ });
1032
+ status.connected = true;
1033
+ status.lastConnectedAt = Date.now();
1034
+ status.lastEventAt = status.lastConnectedAt;
1035
+ status.lastError = null;
1036
+ emitStatus();
1037
+ // Surface a concise connection event for the next main-session turn/heartbeat.
1038
+ const { e164: selfE164 } = readWebSelfId();
1039
+ enqueueSystemEvent(`WhatsApp gateway connected${selfE164 ? ` as ${selfE164}` : ""}.`);
1040
+ setActiveWebListener(listener);
1041
+ const closeListener = async () => {
1042
+ setActiveWebListener(null);
1043
+ if (heartbeat)
1044
+ clearInterval(heartbeat);
1045
+ if (watchdogTimer)
1046
+ clearInterval(watchdogTimer);
1047
+ if (backgroundTasks.size > 0) {
1048
+ await Promise.allSettled(backgroundTasks);
1049
+ backgroundTasks.clear();
1050
+ }
1051
+ try {
1052
+ await listener.close();
1053
+ }
1054
+ catch (err) {
1055
+ logVerbose(`Socket close failed: ${formatError(err)}`);
1056
+ }
1057
+ };
1058
+ if (keepAlive) {
1059
+ heartbeat = setInterval(() => {
1060
+ const authAgeMs = getWebAuthAgeMs();
1061
+ const minutesSinceLastMessage = lastMessageAt
1062
+ ? Math.floor((Date.now() - lastMessageAt) / 60000)
1063
+ : null;
1064
+ const logData = {
1065
+ connectionId,
1066
+ reconnectAttempts,
1067
+ messagesHandled: handledMessages,
1068
+ lastMessageAt,
1069
+ authAgeMs,
1070
+ uptimeMs: Date.now() - startedAt,
1071
+ ...(minutesSinceLastMessage !== null && minutesSinceLastMessage > 30
1072
+ ? { minutesSinceLastMessage }
1073
+ : {}),
1074
+ };
1075
+ // Warn if no messages in 30+ minutes
1076
+ if (minutesSinceLastMessage && minutesSinceLastMessage > 30) {
1077
+ heartbeatLogger.warn(logData, "⚠️ web gateway heartbeat - no messages in 30+ minutes");
1078
+ }
1079
+ else {
1080
+ heartbeatLogger.info(logData, "web gateway heartbeat");
1081
+ }
1082
+ }, heartbeatSeconds * 1000);
1083
+ // Watchdog: Auto-restart if no messages received for MESSAGE_TIMEOUT_MS
1084
+ watchdogTimer = setInterval(() => {
1085
+ if (lastMessageAt) {
1086
+ const timeSinceLastMessage = Date.now() - lastMessageAt;
1087
+ if (timeSinceLastMessage > MESSAGE_TIMEOUT_MS) {
1088
+ const minutesSinceLastMessage = Math.floor(timeSinceLastMessage / 60000);
1089
+ heartbeatLogger.warn({
1090
+ connectionId,
1091
+ minutesSinceLastMessage,
1092
+ lastMessageAt: new Date(lastMessageAt),
1093
+ messagesHandled: handledMessages,
1094
+ }, "Message timeout detected - forcing reconnect");
1095
+ whatsappHeartbeatLog.warn(`No messages received in ${minutesSinceLastMessage}m - restarting connection`);
1096
+ void closeListener().catch((err) => {
1097
+ logVerbose(`Close listener failed: ${formatError(err)}`);
1098
+ }); // Trigger reconnect
1099
+ listener.signalClose?.({
1100
+ status: 499,
1101
+ isLoggedOut: false,
1102
+ error: "watchdog-timeout",
1103
+ });
1104
+ }
1105
+ }
1106
+ }, WATCHDOG_CHECK_MS);
1107
+ }
1108
+ whatsappLog.info("Listening for personal WhatsApp inbound messages.");
1109
+ if (process.stdout.isTTY || process.stderr.isTTY) {
1110
+ whatsappLog.raw("Ctrl+C to stop.");
1111
+ }
1112
+ if (!keepAlive) {
1113
+ await closeListener();
1114
+ return;
1115
+ }
1116
+ const reason = await Promise.race([
1117
+ listener.onClose?.catch((err) => {
1118
+ reconnectLogger.error({ error: formatError(err) }, "listener.onClose rejected");
1119
+ return { status: 500, isLoggedOut: false, error: err };
1120
+ }) ?? waitForever(),
1121
+ abortPromise ?? waitForever(),
1122
+ ]);
1123
+ const uptimeMs = Date.now() - startedAt;
1124
+ if (uptimeMs > heartbeatSeconds * 1000) {
1125
+ reconnectAttempts = 0; // Healthy stretch; reset the backoff.
1126
+ }
1127
+ status.reconnectAttempts = reconnectAttempts;
1128
+ emitStatus();
1129
+ if (stopRequested() || sigintStop || reason === "aborted") {
1130
+ await closeListener();
1131
+ break;
1132
+ }
1133
+ const statusCode = (typeof reason === "object" && reason && "status" in reason
1134
+ ? reason.status
1135
+ : undefined) ?? "unknown";
1136
+ const loggedOut = typeof reason === "object" &&
1137
+ reason &&
1138
+ "isLoggedOut" in reason &&
1139
+ reason.isLoggedOut;
1140
+ const errorStr = formatError(reason);
1141
+ status.connected = false;
1142
+ status.lastEventAt = Date.now();
1143
+ status.lastDisconnect = {
1144
+ at: status.lastEventAt,
1145
+ status: typeof statusCode === "number" ? statusCode : undefined,
1146
+ error: errorStr,
1147
+ loggedOut: Boolean(loggedOut),
1148
+ };
1149
+ status.lastError = errorStr;
1150
+ status.reconnectAttempts = reconnectAttempts;
1151
+ emitStatus();
1152
+ reconnectLogger.info({
1153
+ connectionId,
1154
+ status: statusCode,
1155
+ loggedOut,
1156
+ reconnectAttempts,
1157
+ error: errorStr,
1158
+ }, "web reconnect: connection closed");
1159
+ enqueueSystemEvent(`WhatsApp gateway disconnected (status ${statusCode ?? "unknown"})`);
1160
+ if (loggedOut) {
1161
+ runtime.error("WhatsApp session logged out. Run `clawdbot login --provider web` to relink.");
1162
+ await closeListener();
1163
+ break;
1164
+ }
1165
+ reconnectAttempts += 1;
1166
+ status.reconnectAttempts = reconnectAttempts;
1167
+ emitStatus();
1168
+ if (reconnectPolicy.maxAttempts > 0 &&
1169
+ reconnectAttempts >= reconnectPolicy.maxAttempts) {
1170
+ reconnectLogger.warn({
1171
+ connectionId,
1172
+ status: statusCode,
1173
+ reconnectAttempts,
1174
+ maxAttempts: reconnectPolicy.maxAttempts,
1175
+ }, "web reconnect: max attempts reached; continuing in degraded mode");
1176
+ runtime.error(`WhatsApp Web reconnect: max attempts reached (${reconnectAttempts}/${reconnectPolicy.maxAttempts}). Stopping web monitoring.`);
1177
+ await closeListener();
1178
+ break;
1179
+ }
1180
+ const delay = computeBackoff(reconnectPolicy, reconnectAttempts);
1181
+ reconnectLogger.info({
1182
+ connectionId,
1183
+ status: statusCode,
1184
+ reconnectAttempts,
1185
+ maxAttempts: reconnectPolicy.maxAttempts || "unlimited",
1186
+ delayMs: delay,
1187
+ }, "web reconnect: scheduling retry");
1188
+ runtime.error(`WhatsApp Web connection closed (status ${statusCode}). Retry ${reconnectAttempts}/${reconnectPolicy.maxAttempts || "∞"} in ${formatDuration(delay)}… (${errorStr})`);
1189
+ await closeListener();
1190
+ try {
1191
+ await sleep(delay, abortSignal);
1192
+ }
1193
+ catch {
1194
+ break;
1195
+ }
1196
+ }
1197
+ status.running = false;
1198
+ status.connected = false;
1199
+ status.lastEventAt = Date.now();
1200
+ emitStatus();
1201
+ process.removeListener("SIGINT", handleSigint);
1202
+ }
1203
+ export { DEFAULT_WEB_MEDIA_BYTES };