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,1437 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import os from "node:os";
3
+ import chalk from "chalk";
4
+ import { WebSocketServer } from "ws";
5
+ import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
6
+ import { loadModelCatalog, resetModelCatalogCacheForTest, } from "../agents/model-catalog.js";
7
+ import { resolveConfiguredModelRef } from "../agents/model-selection.js";
8
+ import { CANVAS_HOST_PATH } from "../canvas-host/a2ui.js";
9
+ import { createCanvasHostHandler, startCanvasHost, } from "../canvas-host/server.js";
10
+ import { createDefaultDeps } from "../cli/deps.js";
11
+ import { getHealthSnapshot } from "../commands/health.js";
12
+ import { CONFIG_PATH_CLAWDBOT, isNixMode, loadConfig, migrateLegacyConfig, readConfigFileSnapshot, STATE_DIR_CLAWDBOT, writeConfigFile, } from "../config/config.js";
13
+ import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
14
+ import { runCronIsolatedAgentTurn } from "../cron/isolated-agent.js";
15
+ import { appendCronRunLog, resolveCronRunLogPath } from "../cron/run-log.js";
16
+ import { CronService } from "../cron/service.js";
17
+ import { resolveCronStorePath } from "../cron/store.js";
18
+ import { startGmailWatcher, stopGmailWatcher } from "../hooks/gmail-watcher.js";
19
+ import { clearAgentRunContext, getAgentRunContext, onAgentEvent, registerAgentRunContext, } from "../infra/agent-events.js";
20
+ import { startGatewayBonjourAdvertiser } from "../infra/bonjour.js";
21
+ import { startNodeBridgeServer } from "../infra/bridge/server.js";
22
+ import { resolveCanvasHostUrl } from "../infra/canvas-host-url.js";
23
+ import { GatewayLockError } from "../infra/gateway-lock.js";
24
+ import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
25
+ import { startHeartbeatRunner } from "../infra/heartbeat-runner.js";
26
+ import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
27
+ import { getMachineDisplayName } from "../infra/machine-name.js";
28
+ import { ensureClawdbotCliOnPath } from "../infra/path-env.js";
29
+ import { enqueueSystemEvent } from "../infra/system-events.js";
30
+ import { listSystemPresence, upsertPresence, } from "../infra/system-presence.js";
31
+ import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6, } from "../infra/tailnet.js";
32
+ import { disableTailscaleFunnel, disableTailscaleServe, enableTailscaleFunnel, enableTailscaleServe, getTailnetHostname, } from "../infra/tailscale.js";
33
+ import { loadVoiceWakeConfig } from "../infra/voicewake.js";
34
+ import { WIDE_AREA_DISCOVERY_DOMAIN, writeWideAreaBridgeZone, } from "../infra/widearea-dns.js";
35
+ import { rawDataToString } from "../infra/ws.js";
36
+ import { createSubsystemLogger, getChildLogger, getResolvedLoggerSettings, runtimeForLogger, } from "../logging.js";
37
+ import { setCommandLaneConcurrency } from "../process/command-queue.js";
38
+ import { runOnboardingWizard } from "../wizard/onboarding.js";
39
+ import { assertGatewayAuthConfigured, authorizeGatewayConnect, } from "./auth.js";
40
+ import { startGatewayConfigReloader, } from "./config-reload.js";
41
+ import { normalizeControlUiBasePath } from "./control-ui.js";
42
+ import { resolveHooksConfig } from "./hooks.js";
43
+ import { isLoopbackAddress, isLoopbackHost, resolveGatewayBindHost, } from "./net.js";
44
+ import { createBridgeHandlers } from "./server-bridge.js";
45
+ import { createBridgeSubscriptionManager, } from "./server-bridge-subscriptions.js";
46
+ import { startBrowserControlServerIfEnabled } from "./server-browser.js";
47
+ import { createAgentEventHandler, createChatRunState } from "./server-chat.js";
48
+ import { DEDUPE_MAX, DEDUPE_TTL_MS, HANDSHAKE_TIMEOUT_MS, HEALTH_REFRESH_INTERVAL_MS, MAX_BUFFERED_BYTES, MAX_PAYLOAD_BYTES, TICK_INTERVAL_MS, } from "./server-constants.js";
49
+ import { formatBonjourInstanceName, resolveBonjourCliPath, resolveTailnetDnsHint, } from "./server-discovery.js";
50
+ import { attachGatewayUpgradeHandler, createGatewayHttpServer, createHooksRequestHandler, } from "./server-http.js";
51
+ import { handleGatewayRequest } from "./server-methods.js";
52
+ import { createProviderManager } from "./server-providers.js";
53
+ import { formatError } from "./server-utils.js";
54
+ import { formatForLog, logWs, summarizeAgentEventForWsLog } from "./ws-log.js";
55
+ ensureClawdbotCliOnPath();
56
+ const log = createSubsystemLogger("gateway");
57
+ const logCanvas = log.child("canvas");
58
+ const logBridge = log.child("bridge");
59
+ const logDiscovery = log.child("discovery");
60
+ const logTailscale = log.child("tailscale");
61
+ const logProviders = log.child("providers");
62
+ const logBrowser = log.child("browser");
63
+ const logHealth = log.child("health");
64
+ const logCron = log.child("cron");
65
+ const logReload = log.child("reload");
66
+ const logHooks = log.child("hooks");
67
+ const logWsControl = log.child("ws");
68
+ const logWhatsApp = logProviders.child("whatsapp");
69
+ const logTelegram = logProviders.child("telegram");
70
+ const logDiscord = logProviders.child("discord");
71
+ const logSlack = logProviders.child("slack");
72
+ const logSignal = logProviders.child("signal");
73
+ const logIMessage = logProviders.child("imessage");
74
+ const canvasRuntime = runtimeForLogger(logCanvas);
75
+ const whatsappRuntimeEnv = runtimeForLogger(logWhatsApp);
76
+ const telegramRuntimeEnv = runtimeForLogger(logTelegram);
77
+ const discordRuntimeEnv = runtimeForLogger(logDiscord);
78
+ const slackRuntimeEnv = runtimeForLogger(logSlack);
79
+ const signalRuntimeEnv = runtimeForLogger(logSignal);
80
+ const imessageRuntimeEnv = runtimeForLogger(logIMessage);
81
+ // Test-only escape hatch: model catalog is cached at module scope for the
82
+ // process lifetime, which is fine for the real gateway daemon, but makes
83
+ // isolated unit tests harder. Keep this intentionally obscure.
84
+ export function __resetModelCatalogCacheForTest() {
85
+ resetModelCatalogCacheForTest();
86
+ }
87
+ async function loadGatewayModelCatalog() {
88
+ return await loadModelCatalog({ config: loadConfig() });
89
+ }
90
+ import { ErrorCodes, errorShape, formatValidationErrors, PROTOCOL_VERSION, validateConnectParams, validateRequestFrame, } from "./protocol/index.js";
91
+ const METHODS = [
92
+ "health",
93
+ "providers.status",
94
+ "status",
95
+ "config.get",
96
+ "config.set",
97
+ "config.schema",
98
+ "wizard.start",
99
+ "wizard.next",
100
+ "wizard.cancel",
101
+ "wizard.status",
102
+ "talk.mode",
103
+ "models.list",
104
+ "skills.status",
105
+ "skills.install",
106
+ "skills.update",
107
+ "voicewake.get",
108
+ "voicewake.set",
109
+ "sessions.list",
110
+ "sessions.patch",
111
+ "sessions.reset",
112
+ "sessions.delete",
113
+ "sessions.compact",
114
+ "last-heartbeat",
115
+ "set-heartbeats",
116
+ "wake",
117
+ "node.pair.request",
118
+ "node.pair.list",
119
+ "node.pair.approve",
120
+ "node.pair.reject",
121
+ "node.pair.verify",
122
+ "node.rename",
123
+ "node.list",
124
+ "node.describe",
125
+ "node.invoke",
126
+ "cron.list",
127
+ "cron.status",
128
+ "cron.add",
129
+ "cron.update",
130
+ "cron.remove",
131
+ "cron.run",
132
+ "cron.runs",
133
+ "system-presence",
134
+ "system-event",
135
+ "send",
136
+ "agent",
137
+ "agent.wait",
138
+ "web.login.start",
139
+ "web.login.wait",
140
+ "web.logout",
141
+ "telegram.logout",
142
+ // WebChat WebSocket-native chat methods
143
+ "chat.history",
144
+ "chat.abort",
145
+ "chat.send",
146
+ ];
147
+ const EVENTS = [
148
+ "agent",
149
+ "chat",
150
+ "presence",
151
+ "tick",
152
+ "talk.mode",
153
+ "shutdown",
154
+ "health",
155
+ "heartbeat",
156
+ "cron",
157
+ "node.pair.requested",
158
+ "node.pair.resolved",
159
+ "voicewake.changed",
160
+ ];
161
+ let presenceVersion = 1;
162
+ let healthVersion = 1;
163
+ let healthCache = null;
164
+ let healthRefresh = null;
165
+ let broadcastHealthUpdate = null;
166
+ function buildSnapshot() {
167
+ const presence = listSystemPresence();
168
+ const uptimeMs = Math.round(process.uptime() * 1000);
169
+ // Health is async; caller should await getHealthSnapshot and replace later if needed.
170
+ const emptyHealth = {};
171
+ return {
172
+ presence,
173
+ health: emptyHealth,
174
+ stateVersion: { presence: presenceVersion, health: healthVersion },
175
+ uptimeMs,
176
+ // Surface resolved paths so UIs can display the true config location.
177
+ configPath: CONFIG_PATH_CLAWDBOT,
178
+ stateDir: STATE_DIR_CLAWDBOT,
179
+ };
180
+ }
181
+ async function refreshHealthSnapshot(_opts) {
182
+ if (!healthRefresh) {
183
+ healthRefresh = (async () => {
184
+ const snap = await getHealthSnapshot(undefined);
185
+ healthCache = snap;
186
+ healthVersion += 1;
187
+ if (broadcastHealthUpdate) {
188
+ broadcastHealthUpdate(snap);
189
+ }
190
+ return snap;
191
+ })().finally(() => {
192
+ healthRefresh = null;
193
+ });
194
+ }
195
+ return healthRefresh;
196
+ }
197
+ export async function startGatewayServer(port = 18789, opts = {}) {
198
+ const configSnapshot = await readConfigFileSnapshot();
199
+ if (configSnapshot.legacyIssues.length > 0) {
200
+ if (isNixMode) {
201
+ throw new Error("Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and restart.");
202
+ }
203
+ const { config: migrated, changes } = migrateLegacyConfig(configSnapshot.parsed);
204
+ if (!migrated) {
205
+ throw new Error('Legacy config entries detected but auto-migration failed. Run "clawdbot doctor" to migrate.');
206
+ }
207
+ await writeConfigFile(migrated);
208
+ if (changes.length > 0) {
209
+ log.info(`gateway: migrated legacy config entries:\n${changes
210
+ .map((entry) => `- ${entry}`)
211
+ .join("\n")}`);
212
+ }
213
+ }
214
+ const cfgAtStart = loadConfig();
215
+ const bindMode = opts.bind ?? cfgAtStart.gateway?.bind ?? "loopback";
216
+ const bindHost = opts.host ?? resolveGatewayBindHost(bindMode);
217
+ if (!bindHost) {
218
+ throw new Error("gateway bind is tailnet, but no tailnet interface was found; refusing to start gateway");
219
+ }
220
+ const controlUiEnabled = opts.controlUiEnabled ?? cfgAtStart.gateway?.controlUi?.enabled ?? true;
221
+ const controlUiBasePath = normalizeControlUiBasePath(cfgAtStart.gateway?.controlUi?.basePath);
222
+ const authBase = cfgAtStart.gateway?.auth ?? {};
223
+ const authOverrides = opts.auth ?? {};
224
+ const authConfig = {
225
+ ...authBase,
226
+ ...authOverrides,
227
+ };
228
+ const tailscaleBase = cfgAtStart.gateway?.tailscale ?? {};
229
+ const tailscaleOverrides = opts.tailscale ?? {};
230
+ const tailscaleConfig = {
231
+ ...tailscaleBase,
232
+ ...tailscaleOverrides,
233
+ };
234
+ const tailscaleMode = tailscaleConfig.mode ?? "off";
235
+ const token = authConfig.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
236
+ const password = authConfig.password ?? process.env.CLAWDBOT_GATEWAY_PASSWORD ?? undefined;
237
+ const authMode = authConfig.mode ?? (password ? "password" : token ? "token" : "none");
238
+ const allowTailscale = authConfig.allowTailscale ??
239
+ (tailscaleMode === "serve" && authMode !== "password");
240
+ const resolvedAuth = {
241
+ mode: authMode,
242
+ token,
243
+ password,
244
+ allowTailscale,
245
+ };
246
+ let hooksConfig = resolveHooksConfig(cfgAtStart);
247
+ const canvasHostEnabled = process.env.CLAWDBOT_SKIP_CANVAS_HOST !== "1" &&
248
+ cfgAtStart.canvasHost?.enabled !== false;
249
+ assertGatewayAuthConfigured(resolvedAuth);
250
+ if (tailscaleMode === "funnel" && authMode !== "password") {
251
+ throw new Error("tailscale funnel requires gateway auth mode=password (set gateway.auth.password or CLAWDBOT_GATEWAY_PASSWORD)");
252
+ }
253
+ if (tailscaleMode !== "off" && !isLoopbackHost(bindHost)) {
254
+ throw new Error("tailscale serve/funnel requires gateway bind=loopback (127.0.0.1)");
255
+ }
256
+ if (!isLoopbackHost(bindHost) && authMode === "none") {
257
+ throw new Error(`refusing to bind gateway to ${bindHost}:${port} without auth (set gateway.auth or CLAWDBOT_GATEWAY_TOKEN)`);
258
+ }
259
+ const wizardRunner = opts.wizardRunner ?? runOnboardingWizard;
260
+ const wizardSessions = new Map();
261
+ const findRunningWizard = () => {
262
+ for (const [id, session] of wizardSessions) {
263
+ if (session.getStatus() === "running")
264
+ return id;
265
+ }
266
+ return null;
267
+ };
268
+ const purgeWizardSession = (id) => {
269
+ const session = wizardSessions.get(id);
270
+ if (!session)
271
+ return;
272
+ if (session.getStatus() === "running")
273
+ return;
274
+ wizardSessions.delete(id);
275
+ };
276
+ const dispatchWakeHook = (value) => {
277
+ enqueueSystemEvent(value.text);
278
+ if (value.mode === "now") {
279
+ requestHeartbeatNow({ reason: "hook:wake" });
280
+ }
281
+ };
282
+ const dispatchAgentHook = (value) => {
283
+ const sessionKey = value.sessionKey.trim()
284
+ ? value.sessionKey.trim()
285
+ : `hook:${randomUUID()}`;
286
+ const jobId = randomUUID();
287
+ const now = Date.now();
288
+ const job = {
289
+ id: jobId,
290
+ name: value.name,
291
+ enabled: true,
292
+ createdAtMs: now,
293
+ updatedAtMs: now,
294
+ schedule: { kind: "at", atMs: now },
295
+ sessionTarget: "isolated",
296
+ wakeMode: value.wakeMode,
297
+ payload: {
298
+ kind: "agentTurn",
299
+ message: value.message,
300
+ thinking: value.thinking,
301
+ timeoutSeconds: value.timeoutSeconds,
302
+ deliver: value.deliver,
303
+ channel: value.channel,
304
+ to: value.to,
305
+ },
306
+ state: { nextRunAtMs: now },
307
+ };
308
+ const runId = randomUUID();
309
+ void (async () => {
310
+ try {
311
+ const cfg = loadConfig();
312
+ const result = await runCronIsolatedAgentTurn({
313
+ cfg,
314
+ deps,
315
+ job,
316
+ message: value.message,
317
+ sessionKey,
318
+ lane: "cron",
319
+ });
320
+ const summary = result.summary?.trim() || result.error?.trim() || result.status;
321
+ const prefix = result.status === "ok"
322
+ ? `Hook ${value.name}`
323
+ : `Hook ${value.name} (${result.status})`;
324
+ enqueueSystemEvent(`${prefix}: ${summary}`.trim());
325
+ if (value.wakeMode === "now") {
326
+ requestHeartbeatNow({ reason: `hook:${jobId}` });
327
+ }
328
+ }
329
+ catch (err) {
330
+ logHooks.warn(`hook agent failed: ${String(err)}`);
331
+ enqueueSystemEvent(`Hook ${value.name} (error): ${String(err)}`);
332
+ if (value.wakeMode === "now") {
333
+ requestHeartbeatNow({ reason: `hook:${jobId}:error` });
334
+ }
335
+ }
336
+ })();
337
+ return runId;
338
+ };
339
+ let canvasHost = null;
340
+ let canvasHostServer = null;
341
+ if (canvasHostEnabled) {
342
+ try {
343
+ const handler = await createCanvasHostHandler({
344
+ runtime: canvasRuntime,
345
+ rootDir: cfgAtStart.canvasHost?.root,
346
+ basePath: CANVAS_HOST_PATH,
347
+ allowInTests: opts.allowCanvasHostInTests,
348
+ liveReload: cfgAtStart.canvasHost?.liveReload,
349
+ });
350
+ if (handler.rootDir) {
351
+ canvasHost = handler;
352
+ logCanvas.info(`canvas host mounted at http://${bindHost}:${port}${CANVAS_HOST_PATH}/ (root ${handler.rootDir})`);
353
+ }
354
+ }
355
+ catch (err) {
356
+ logCanvas.warn(`canvas host failed to start: ${String(err)}`);
357
+ }
358
+ }
359
+ const handleHooksRequest = createHooksRequestHandler({
360
+ getHooksConfig: () => hooksConfig,
361
+ bindHost,
362
+ port,
363
+ logHooks,
364
+ dispatchAgentHook,
365
+ dispatchWakeHook,
366
+ });
367
+ const httpServer = createGatewayHttpServer({
368
+ canvasHost,
369
+ controlUiEnabled,
370
+ controlUiBasePath,
371
+ handleHooksRequest,
372
+ });
373
+ let bonjourStop = null;
374
+ let bridge = null;
375
+ const bridgeSubscriptions = createBridgeSubscriptionManager();
376
+ const isMobilePlatform = (platform) => {
377
+ const p = typeof platform === "string" ? platform.trim().toLowerCase() : "";
378
+ if (!p)
379
+ return false;
380
+ return (p.startsWith("ios") || p.startsWith("ipados") || p.startsWith("android"));
381
+ };
382
+ const hasConnectedMobileNode = () => {
383
+ const connected = bridge?.listConnected?.() ?? [];
384
+ return connected.some((n) => isMobilePlatform(n.platform));
385
+ };
386
+ try {
387
+ await new Promise((resolve, reject) => {
388
+ const onError = (err) => {
389
+ httpServer.off("listening", onListening);
390
+ reject(err);
391
+ };
392
+ const onListening = () => {
393
+ httpServer.off("error", onError);
394
+ resolve();
395
+ };
396
+ httpServer.once("error", onError);
397
+ httpServer.once("listening", onListening);
398
+ httpServer.listen(port, bindHost);
399
+ });
400
+ }
401
+ catch (err) {
402
+ const code = err.code;
403
+ if (code === "EADDRINUSE") {
404
+ throw new GatewayLockError(`another gateway instance is already listening on ws://${bindHost}:${port}`, err);
405
+ }
406
+ throw new GatewayLockError(`failed to bind gateway socket on ws://${bindHost}:${port}: ${String(err)}`, err);
407
+ }
408
+ const wss = new WebSocketServer({
409
+ noServer: true,
410
+ maxPayload: MAX_PAYLOAD_BYTES,
411
+ });
412
+ attachGatewayUpgradeHandler({ httpServer, wss, canvasHost });
413
+ const clients = new Set();
414
+ let seq = 0;
415
+ // Track per-run sequence to detect out-of-order/lost agent events.
416
+ const agentRunSeq = new Map();
417
+ const dedupe = new Map();
418
+ const chatRunState = createChatRunState();
419
+ const chatRunRegistry = chatRunState.registry;
420
+ const chatRunBuffers = chatRunState.buffers;
421
+ const chatDeltaSentAt = chatRunState.deltaSentAt;
422
+ const addChatRun = chatRunRegistry.add;
423
+ const removeChatRun = chatRunRegistry.remove;
424
+ const resolveSessionKeyForRun = (runId) => {
425
+ const cached = getAgentRunContext(runId)?.sessionKey;
426
+ if (cached)
427
+ return cached;
428
+ const cfg = loadConfig();
429
+ const storePath = resolveStorePath(cfg.session?.store);
430
+ const store = loadSessionStore(storePath);
431
+ const found = Object.entries(store).find(([, entry]) => entry?.sessionId === runId);
432
+ const sessionKey = found?.[0];
433
+ if (sessionKey) {
434
+ registerAgentRunContext(runId, { sessionKey });
435
+ }
436
+ return sessionKey;
437
+ };
438
+ const chatAbortControllers = new Map();
439
+ setCommandLaneConcurrency("cron", cfgAtStart.cron?.maxConcurrentRuns ?? 1);
440
+ setCommandLaneConcurrency("main", cfgAtStart.agent?.maxConcurrent ?? 1);
441
+ const cronLogger = getChildLogger({
442
+ module: "cron",
443
+ });
444
+ const deps = createDefaultDeps();
445
+ const buildCronService = (cfg) => {
446
+ const storePath = resolveCronStorePath(cfg.cron?.store);
447
+ const cronEnabled = process.env.CLAWDBOT_SKIP_CRON !== "1" && cfg.cron?.enabled !== false;
448
+ const cron = new CronService({
449
+ storePath,
450
+ cronEnabled,
451
+ enqueueSystemEvent,
452
+ requestHeartbeatNow,
453
+ runIsolatedAgentJob: async ({ job, message }) => {
454
+ const runtimeConfig = loadConfig();
455
+ return await runCronIsolatedAgentTurn({
456
+ cfg: runtimeConfig,
457
+ deps,
458
+ job,
459
+ message,
460
+ sessionKey: `cron:${job.id}`,
461
+ lane: "cron",
462
+ });
463
+ },
464
+ log: getChildLogger({ module: "cron", storePath }),
465
+ onEvent: (evt) => {
466
+ broadcast("cron", evt, { dropIfSlow: true });
467
+ if (evt.action === "finished") {
468
+ const logPath = resolveCronRunLogPath({
469
+ storePath,
470
+ jobId: evt.jobId,
471
+ });
472
+ void appendCronRunLog(logPath, {
473
+ ts: Date.now(),
474
+ jobId: evt.jobId,
475
+ action: "finished",
476
+ status: evt.status,
477
+ error: evt.error,
478
+ summary: evt.summary,
479
+ runAtMs: evt.runAtMs,
480
+ durationMs: evt.durationMs,
481
+ nextRunAtMs: evt.nextRunAtMs,
482
+ }).catch((err) => {
483
+ cronLogger.warn({ err: String(err), logPath }, "cron: run log append failed");
484
+ });
485
+ }
486
+ },
487
+ });
488
+ return { cron, storePath, cronEnabled };
489
+ };
490
+ let { cron, storePath: cronStorePath } = buildCronService(cfgAtStart);
491
+ const providerManager = createProviderManager({
492
+ loadConfig,
493
+ logWhatsApp,
494
+ logTelegram,
495
+ logDiscord,
496
+ logSlack,
497
+ logSignal,
498
+ logIMessage,
499
+ whatsappRuntimeEnv,
500
+ telegramRuntimeEnv,
501
+ discordRuntimeEnv,
502
+ slackRuntimeEnv,
503
+ signalRuntimeEnv,
504
+ imessageRuntimeEnv,
505
+ });
506
+ const { getRuntimeSnapshot, startProviders, startWhatsAppProvider, startTelegramProvider, startDiscordProvider, startSlackProvider, startSignalProvider, startIMessageProvider, stopWhatsAppProvider, stopTelegramProvider, stopDiscordProvider, stopSlackProvider, stopSignalProvider, stopIMessageProvider, markWhatsAppLoggedOut, } = providerManager;
507
+ const broadcast = (event, payload, opts) => {
508
+ const eventSeq = ++seq;
509
+ const frame = JSON.stringify({
510
+ type: "event",
511
+ event,
512
+ payload,
513
+ seq: eventSeq,
514
+ stateVersion: opts?.stateVersion,
515
+ });
516
+ const logMeta = {
517
+ event,
518
+ seq: eventSeq,
519
+ clients: clients.size,
520
+ dropIfSlow: opts?.dropIfSlow,
521
+ presenceVersion: opts?.stateVersion?.presence,
522
+ healthVersion: opts?.stateVersion?.health,
523
+ };
524
+ if (event === "agent") {
525
+ Object.assign(logMeta, summarizeAgentEventForWsLog(payload));
526
+ }
527
+ logWs("out", "event", logMeta);
528
+ for (const c of clients) {
529
+ const slow = c.socket.bufferedAmount > MAX_BUFFERED_BYTES;
530
+ if (slow && opts?.dropIfSlow)
531
+ continue;
532
+ if (slow) {
533
+ try {
534
+ c.socket.close(1008, "slow consumer");
535
+ }
536
+ catch {
537
+ /* ignore */
538
+ }
539
+ continue;
540
+ }
541
+ try {
542
+ c.socket.send(frame);
543
+ }
544
+ catch {
545
+ /* ignore */
546
+ }
547
+ }
548
+ };
549
+ const wideAreaDiscoveryEnabled = cfgAtStart.discovery?.wideArea?.enabled === true;
550
+ const bridgeEnabled = (() => {
551
+ if (cfgAtStart.bridge?.enabled !== undefined)
552
+ return cfgAtStart.bridge.enabled === true;
553
+ return process.env.CLAWDBOT_BRIDGE_ENABLED !== "0";
554
+ })();
555
+ const bridgePort = (() => {
556
+ if (typeof cfgAtStart.bridge?.port === "number" &&
557
+ cfgAtStart.bridge.port > 0) {
558
+ return cfgAtStart.bridge.port;
559
+ }
560
+ if (process.env.CLAWDBOT_BRIDGE_PORT !== undefined) {
561
+ const parsed = Number.parseInt(process.env.CLAWDBOT_BRIDGE_PORT, 10);
562
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 18790;
563
+ }
564
+ return 18790;
565
+ })();
566
+ const bridgeHost = (() => {
567
+ // Back-compat: allow an env var override when no bind policy is configured.
568
+ if (cfgAtStart.bridge?.bind === undefined) {
569
+ const env = process.env.CLAWDBOT_BRIDGE_HOST?.trim();
570
+ if (env)
571
+ return env;
572
+ }
573
+ const bind = cfgAtStart.bridge?.bind ?? (wideAreaDiscoveryEnabled ? "tailnet" : "lan");
574
+ if (bind === "loopback")
575
+ return "127.0.0.1";
576
+ if (bind === "lan")
577
+ return "0.0.0.0";
578
+ const tailnetIPv4 = pickPrimaryTailnetIPv4();
579
+ const tailnetIPv6 = pickPrimaryTailnetIPv6();
580
+ if (bind === "tailnet") {
581
+ return tailnetIPv4 ?? tailnetIPv6 ?? null;
582
+ }
583
+ if (bind === "auto") {
584
+ return tailnetIPv4 ?? tailnetIPv6 ?? "0.0.0.0";
585
+ }
586
+ return "0.0.0.0";
587
+ })();
588
+ const canvasHostPort = (() => {
589
+ const configured = cfgAtStart.canvasHost?.port;
590
+ if (typeof configured === "number" && configured > 0)
591
+ return configured;
592
+ return 18793;
593
+ })();
594
+ if (canvasHostEnabled && bridgeEnabled && bridgeHost) {
595
+ try {
596
+ const started = await startCanvasHost({
597
+ runtime: canvasRuntime,
598
+ rootDir: cfgAtStart.canvasHost?.root,
599
+ port: canvasHostPort,
600
+ listenHost: bridgeHost,
601
+ allowInTests: opts.allowCanvasHostInTests,
602
+ liveReload: cfgAtStart.canvasHost?.liveReload,
603
+ handler: canvasHost ?? undefined,
604
+ ownsHandler: canvasHost ? false : undefined,
605
+ });
606
+ if (started.port > 0) {
607
+ canvasHostServer = started;
608
+ }
609
+ }
610
+ catch (err) {
611
+ logCanvas.warn(`failed to start on ${bridgeHost}:${canvasHostPort}: ${String(err)}`);
612
+ }
613
+ }
614
+ const bridgeSubscribe = bridgeSubscriptions.subscribe;
615
+ const bridgeUnsubscribe = bridgeSubscriptions.unsubscribe;
616
+ const bridgeUnsubscribeAll = bridgeSubscriptions.unsubscribeAll;
617
+ const bridgeSendEvent = (opts) => {
618
+ bridge?.sendEvent(opts);
619
+ };
620
+ const bridgeListConnected = () => bridge?.listConnected() ?? [];
621
+ const bridgeSendToSession = (sessionKey, event, payload) => bridgeSubscriptions.sendToSession(sessionKey, event, payload, bridgeSendEvent);
622
+ const bridgeSendToAllSubscribed = (event, payload) => bridgeSubscriptions.sendToAllSubscribed(event, payload, bridgeSendEvent);
623
+ const bridgeSendToAllConnected = (event, payload) => bridgeSubscriptions.sendToAllConnected(event, payload, bridgeListConnected, bridgeSendEvent);
624
+ const broadcastVoiceWakeChanged = (triggers) => {
625
+ const payload = { triggers };
626
+ broadcast("voicewake.changed", payload, { dropIfSlow: true });
627
+ bridgeSendToAllConnected("voicewake.changed", payload);
628
+ };
629
+ const { handleBridgeRequest, handleBridgeEvent } = createBridgeHandlers({
630
+ deps,
631
+ broadcast,
632
+ bridgeSendToSession,
633
+ bridgeSubscribe,
634
+ bridgeUnsubscribe,
635
+ broadcastVoiceWakeChanged,
636
+ addChatRun,
637
+ removeChatRun,
638
+ chatAbortControllers,
639
+ chatRunBuffers,
640
+ chatDeltaSentAt,
641
+ dedupe,
642
+ agentRunSeq,
643
+ getHealthCache: () => healthCache,
644
+ refreshHealthSnapshot,
645
+ loadGatewayModelCatalog,
646
+ logBridge,
647
+ });
648
+ const machineDisplayName = await getMachineDisplayName();
649
+ const canvasHostPortForBridge = canvasHostServer?.port;
650
+ const canvasHostHostForBridge = canvasHostServer &&
651
+ bridgeHost &&
652
+ bridgeHost !== "0.0.0.0" &&
653
+ bridgeHost !== "::"
654
+ ? bridgeHost
655
+ : undefined;
656
+ const nodePresenceTimers = new Map();
657
+ const stopNodePresenceTimer = (nodeId) => {
658
+ const timer = nodePresenceTimers.get(nodeId);
659
+ if (timer) {
660
+ clearInterval(timer);
661
+ }
662
+ nodePresenceTimers.delete(nodeId);
663
+ };
664
+ const beaconNodePresence = (node, reason) => {
665
+ const host = node.displayName?.trim() || node.nodeId;
666
+ const rawIp = node.remoteIp?.trim();
667
+ const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined;
668
+ const version = node.version?.trim() || "unknown";
669
+ const platform = node.platform?.trim() || undefined;
670
+ const deviceFamily = node.deviceFamily?.trim() || undefined;
671
+ const modelIdentifier = node.modelIdentifier?.trim() || undefined;
672
+ const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason ${reason}`;
673
+ upsertPresence(node.nodeId, {
674
+ host,
675
+ ip,
676
+ version,
677
+ platform,
678
+ deviceFamily,
679
+ modelIdentifier,
680
+ mode: "remote",
681
+ reason,
682
+ lastInputSeconds: 0,
683
+ instanceId: node.nodeId,
684
+ text,
685
+ });
686
+ presenceVersion += 1;
687
+ broadcast("presence", { presence: listSystemPresence() }, {
688
+ dropIfSlow: true,
689
+ stateVersion: {
690
+ presence: presenceVersion,
691
+ health: healthVersion,
692
+ },
693
+ });
694
+ };
695
+ const startNodePresenceTimer = (node) => {
696
+ stopNodePresenceTimer(node.nodeId);
697
+ nodePresenceTimers.set(node.nodeId, setInterval(() => {
698
+ beaconNodePresence(node, "periodic");
699
+ }, 180_000));
700
+ };
701
+ if (bridgeEnabled && bridgePort > 0 && bridgeHost) {
702
+ try {
703
+ const started = await startNodeBridgeServer({
704
+ host: bridgeHost,
705
+ port: bridgePort,
706
+ serverName: machineDisplayName,
707
+ canvasHostPort: canvasHostPortForBridge,
708
+ canvasHostHost: canvasHostHostForBridge,
709
+ onRequest: (nodeId, req) => handleBridgeRequest(nodeId, req),
710
+ onAuthenticated: async (node) => {
711
+ beaconNodePresence(node, "node-connected");
712
+ startNodePresenceTimer(node);
713
+ try {
714
+ const cfg = await loadVoiceWakeConfig();
715
+ started.sendEvent({
716
+ nodeId: node.nodeId,
717
+ event: "voicewake.changed",
718
+ payloadJSON: JSON.stringify({ triggers: cfg.triggers }),
719
+ });
720
+ }
721
+ catch {
722
+ // Best-effort only.
723
+ }
724
+ },
725
+ onDisconnected: (node) => {
726
+ bridgeUnsubscribeAll(node.nodeId);
727
+ stopNodePresenceTimer(node.nodeId);
728
+ beaconNodePresence(node, "node-disconnected");
729
+ },
730
+ onEvent: handleBridgeEvent,
731
+ onPairRequested: (request) => {
732
+ broadcast("node.pair.requested", request, { dropIfSlow: true });
733
+ },
734
+ });
735
+ if (started.port > 0) {
736
+ bridge = started;
737
+ logBridge.info(`listening on tcp://${bridgeHost}:${bridge.port} (node)`);
738
+ }
739
+ }
740
+ catch (err) {
741
+ logBridge.warn(`failed to start: ${String(err)}`);
742
+ }
743
+ }
744
+ else if (bridgeEnabled && bridgePort > 0 && !bridgeHost) {
745
+ logBridge.warn("bind policy requested tailnet IP, but no tailnet interface was found; refusing to start bridge");
746
+ }
747
+ const tailnetDns = await resolveTailnetDnsHint();
748
+ try {
749
+ const sshPortEnv = process.env.CLAWDBOT_SSH_PORT?.trim();
750
+ const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN;
751
+ const sshPort = Number.isFinite(sshPortParsed) && sshPortParsed > 0
752
+ ? sshPortParsed
753
+ : undefined;
754
+ const bonjour = await startGatewayBonjourAdvertiser({
755
+ instanceName: formatBonjourInstanceName(machineDisplayName),
756
+ gatewayPort: port,
757
+ bridgePort: bridge?.port,
758
+ canvasPort: canvasHostPortForBridge,
759
+ sshPort,
760
+ tailnetDns,
761
+ cliPath: resolveBonjourCliPath(),
762
+ });
763
+ bonjourStop = bonjour.stop;
764
+ }
765
+ catch (err) {
766
+ logDiscovery.warn(`bonjour advertising failed: ${String(err)}`);
767
+ }
768
+ if (wideAreaDiscoveryEnabled && bridge?.port) {
769
+ const tailnetIPv4 = pickPrimaryTailnetIPv4();
770
+ if (!tailnetIPv4) {
771
+ logDiscovery.warn("discovery.wideArea.enabled is true, but no Tailscale IPv4 address was found; skipping unicast DNS-SD zone update");
772
+ }
773
+ else {
774
+ try {
775
+ const tailnetIPv6 = pickPrimaryTailnetIPv6();
776
+ const result = await writeWideAreaBridgeZone({
777
+ bridgePort: bridge.port,
778
+ displayName: formatBonjourInstanceName(machineDisplayName),
779
+ tailnetIPv4,
780
+ tailnetIPv6: tailnetIPv6 ?? undefined,
781
+ tailnetDns,
782
+ });
783
+ logDiscovery.info(`wide-area DNS-SD ${result.changed ? "updated" : "unchanged"} (${WIDE_AREA_DISCOVERY_DOMAIN} → ${result.zonePath})`);
784
+ }
785
+ catch (err) {
786
+ logDiscovery.warn(`wide-area discovery update failed: ${String(err)}`);
787
+ }
788
+ }
789
+ }
790
+ broadcastHealthUpdate = (snap) => {
791
+ broadcast("health", snap, {
792
+ stateVersion: { presence: presenceVersion, health: healthVersion },
793
+ });
794
+ bridgeSendToAllSubscribed("health", snap);
795
+ };
796
+ // periodic keepalive
797
+ const tickInterval = setInterval(() => {
798
+ const payload = { ts: Date.now() };
799
+ broadcast("tick", payload, { dropIfSlow: true });
800
+ bridgeSendToAllSubscribed("tick", payload);
801
+ }, TICK_INTERVAL_MS);
802
+ // periodic health refresh to keep cached snapshot warm
803
+ const healthInterval = setInterval(() => {
804
+ void refreshHealthSnapshot({ probe: true }).catch((err) => logHealth.error(`refresh failed: ${formatError(err)}`));
805
+ }, HEALTH_REFRESH_INTERVAL_MS);
806
+ // Prime cache so first client gets a snapshot without waiting.
807
+ void refreshHealthSnapshot({ probe: true }).catch((err) => logHealth.error(`initial refresh failed: ${formatError(err)}`));
808
+ // dedupe cache cleanup
809
+ const dedupeCleanup = setInterval(() => {
810
+ const now = Date.now();
811
+ for (const [k, v] of dedupe) {
812
+ if (now - v.ts > DEDUPE_TTL_MS)
813
+ dedupe.delete(k);
814
+ }
815
+ if (dedupe.size > DEDUPE_MAX) {
816
+ const entries = [...dedupe.entries()].sort((a, b) => a[1].ts - b[1].ts);
817
+ for (let i = 0; i < dedupe.size - DEDUPE_MAX; i++) {
818
+ dedupe.delete(entries[i][0]);
819
+ }
820
+ }
821
+ }, 60_000);
822
+ const agentUnsub = onAgentEvent(createAgentEventHandler({
823
+ broadcast,
824
+ bridgeSendToSession,
825
+ agentRunSeq,
826
+ chatRunState,
827
+ resolveSessionKeyForRun,
828
+ clearAgentRunContext,
829
+ }));
830
+ const heartbeatUnsub = onHeartbeatEvent((evt) => {
831
+ broadcast("heartbeat", evt, { dropIfSlow: true });
832
+ });
833
+ let heartbeatRunner = startHeartbeatRunner({ cfg: cfgAtStart });
834
+ void cron
835
+ .start()
836
+ .catch((err) => logCron.error(`failed to start: ${String(err)}`));
837
+ wss.on("connection", (socket, upgradeReq) => {
838
+ let client = null;
839
+ let closed = false;
840
+ const connId = randomUUID();
841
+ const remoteAddr = socket._socket?.remoteAddress;
842
+ const canvasHostPortForWs = canvasHostServer?.port ?? (canvasHost ? port : undefined);
843
+ const canvasHostOverride = bridgeHost && bridgeHost !== "0.0.0.0" && bridgeHost !== "::"
844
+ ? bridgeHost
845
+ : undefined;
846
+ const canvasHostUrl = resolveCanvasHostUrl({
847
+ canvasPort: canvasHostPortForWs,
848
+ hostOverride: canvasHostServer ? canvasHostOverride : undefined,
849
+ requestHost: upgradeReq.headers.host,
850
+ forwardedProto: upgradeReq.headers["x-forwarded-proto"],
851
+ localAddress: upgradeReq.socket?.localAddress,
852
+ });
853
+ logWs("in", "open", { connId, remoteAddr });
854
+ const isWebchatConnect = (params) => params?.client?.mode === "webchat" ||
855
+ params?.client?.name === "webchat-ui";
856
+ const send = (obj) => {
857
+ try {
858
+ socket.send(JSON.stringify(obj));
859
+ }
860
+ catch {
861
+ /* ignore */
862
+ }
863
+ };
864
+ const close = () => {
865
+ if (closed)
866
+ return;
867
+ closed = true;
868
+ clearTimeout(handshakeTimer);
869
+ if (client)
870
+ clients.delete(client);
871
+ try {
872
+ socket.close(1000);
873
+ }
874
+ catch {
875
+ /* ignore */
876
+ }
877
+ };
878
+ socket.once("error", (err) => {
879
+ logWsControl.warn(`error conn=${connId} remote=${remoteAddr ?? "?"}: ${formatError(err)}`);
880
+ close();
881
+ });
882
+ socket.once("close", (code, reason) => {
883
+ if (!client) {
884
+ logWsControl.warn(`closed before connect conn=${connId} remote=${remoteAddr ?? "?"} code=${code ?? "n/a"} reason=${reason?.toString() || "n/a"}`);
885
+ }
886
+ if (client && isWebchatConnect(client.connect)) {
887
+ logWsControl.info(`webchat disconnected code=${code} reason=${reason?.toString() || "n/a"} conn=${connId}`);
888
+ }
889
+ if (client?.presenceKey) {
890
+ // mark presence as disconnected
891
+ upsertPresence(client.presenceKey, {
892
+ reason: "disconnect",
893
+ });
894
+ presenceVersion += 1;
895
+ broadcast("presence", { presence: listSystemPresence() }, {
896
+ dropIfSlow: true,
897
+ stateVersion: { presence: presenceVersion, health: healthVersion },
898
+ });
899
+ }
900
+ logWs("out", "close", {
901
+ connId,
902
+ code,
903
+ reason: reason?.toString(),
904
+ });
905
+ close();
906
+ });
907
+ const handshakeTimer = setTimeout(() => {
908
+ if (!client) {
909
+ logWsControl.warn(`handshake timeout conn=${connId} remote=${remoteAddr ?? "?"}`);
910
+ close();
911
+ }
912
+ }, HANDSHAKE_TIMEOUT_MS);
913
+ socket.on("message", async (data) => {
914
+ if (closed)
915
+ return;
916
+ const text = rawDataToString(data);
917
+ try {
918
+ const parsed = JSON.parse(text);
919
+ if (!client) {
920
+ // Handshake must be a normal request:
921
+ // { type:"req", method:"connect", params: ConnectParams }.
922
+ if (!validateRequestFrame(parsed) ||
923
+ parsed.method !== "connect" ||
924
+ !validateConnectParams(parsed.params)) {
925
+ if (validateRequestFrame(parsed)) {
926
+ const req = parsed;
927
+ send({
928
+ type: "res",
929
+ id: req.id,
930
+ ok: false,
931
+ error: errorShape(ErrorCodes.INVALID_REQUEST, req.method === "connect"
932
+ ? `invalid connect params: ${formatValidationErrors(validateConnectParams.errors)}`
933
+ : "invalid handshake: first request must be connect"),
934
+ });
935
+ }
936
+ else {
937
+ logWsControl.warn(`invalid handshake conn=${connId} remote=${remoteAddr ?? "?"}`);
938
+ }
939
+ socket.close(1008, "invalid handshake");
940
+ close();
941
+ return;
942
+ }
943
+ const frame = parsed;
944
+ const connectParams = frame.params;
945
+ // protocol negotiation
946
+ const { minProtocol, maxProtocol } = connectParams;
947
+ if (maxProtocol < PROTOCOL_VERSION ||
948
+ minProtocol > PROTOCOL_VERSION) {
949
+ logWsControl.warn(`protocol mismatch conn=${connId} remote=${remoteAddr ?? "?"} client=${connectParams.client.name} ${connectParams.client.mode} v${connectParams.client.version}`);
950
+ send({
951
+ type: "res",
952
+ id: frame.id,
953
+ ok: false,
954
+ error: errorShape(ErrorCodes.INVALID_REQUEST, "protocol mismatch", {
955
+ details: { expectedProtocol: PROTOCOL_VERSION },
956
+ }),
957
+ });
958
+ socket.close(1002, "protocol mismatch");
959
+ close();
960
+ return;
961
+ }
962
+ const authResult = await authorizeGatewayConnect({
963
+ auth: resolvedAuth,
964
+ connectAuth: connectParams.auth,
965
+ req: upgradeReq,
966
+ });
967
+ if (!authResult.ok) {
968
+ logWsControl.warn(`unauthorized conn=${connId} remote=${remoteAddr ?? "?"} client=${connectParams.client.name} ${connectParams.client.mode} v${connectParams.client.version}`);
969
+ send({
970
+ type: "res",
971
+ id: frame.id,
972
+ ok: false,
973
+ error: errorShape(ErrorCodes.INVALID_REQUEST, "unauthorized"),
974
+ });
975
+ socket.close(1008, "unauthorized");
976
+ close();
977
+ return;
978
+ }
979
+ const authMethod = authResult.method ?? "none";
980
+ const shouldTrackPresence = connectParams.client.mode !== "cli";
981
+ const presenceKey = shouldTrackPresence
982
+ ? connectParams.client.instanceId || connId
983
+ : undefined;
984
+ logWs("in", "connect", {
985
+ connId,
986
+ client: connectParams.client.name,
987
+ version: connectParams.client.version,
988
+ mode: connectParams.client.mode,
989
+ instanceId: connectParams.client.instanceId,
990
+ platform: connectParams.client.platform,
991
+ auth: authMethod,
992
+ });
993
+ if (isWebchatConnect(connectParams)) {
994
+ logWsControl.info(`webchat connected conn=${connId} remote=${remoteAddr ?? "?"} client=${connectParams.client.name} ${connectParams.client.mode} v${connectParams.client.version}`);
995
+ }
996
+ if (presenceKey) {
997
+ upsertPresence(presenceKey, {
998
+ host: connectParams.client.name || os.hostname(),
999
+ ip: isLoopbackAddress(remoteAddr) ? undefined : remoteAddr,
1000
+ version: connectParams.client.version,
1001
+ platform: connectParams.client.platform,
1002
+ deviceFamily: connectParams.client.deviceFamily,
1003
+ modelIdentifier: connectParams.client.modelIdentifier,
1004
+ mode: connectParams.client.mode,
1005
+ instanceId: connectParams.client.instanceId,
1006
+ reason: "connect",
1007
+ });
1008
+ presenceVersion += 1;
1009
+ }
1010
+ const snapshot = buildSnapshot();
1011
+ if (healthCache) {
1012
+ snapshot.health = healthCache;
1013
+ snapshot.stateVersion.health = healthVersion;
1014
+ }
1015
+ const helloOk = {
1016
+ type: "hello-ok",
1017
+ protocol: PROTOCOL_VERSION,
1018
+ server: {
1019
+ version: process.env.CLAWDBOT_VERSION ??
1020
+ process.env.npm_package_version ??
1021
+ "dev",
1022
+ commit: process.env.GIT_COMMIT,
1023
+ host: os.hostname(),
1024
+ connId,
1025
+ },
1026
+ features: { methods: METHODS, events: EVENTS },
1027
+ snapshot,
1028
+ canvasHostUrl,
1029
+ policy: {
1030
+ maxPayload: MAX_PAYLOAD_BYTES,
1031
+ maxBufferedBytes: MAX_BUFFERED_BYTES,
1032
+ tickIntervalMs: TICK_INTERVAL_MS,
1033
+ },
1034
+ };
1035
+ clearTimeout(handshakeTimer);
1036
+ client = { socket, connect: connectParams, connId, presenceKey };
1037
+ logWs("out", "hello-ok", {
1038
+ connId,
1039
+ methods: METHODS.length,
1040
+ events: EVENTS.length,
1041
+ presence: snapshot.presence.length,
1042
+ stateVersion: snapshot.stateVersion.presence,
1043
+ });
1044
+ send({ type: "res", id: frame.id, ok: true, payload: helloOk });
1045
+ clients.add(client);
1046
+ void refreshHealthSnapshot({ probe: true }).catch((err) => logHealth.error(`post-connect health refresh failed: ${formatError(err)}`));
1047
+ return;
1048
+ }
1049
+ // After handshake, accept only req frames
1050
+ if (!validateRequestFrame(parsed)) {
1051
+ send({
1052
+ type: "res",
1053
+ id: parsed?.id ?? "invalid",
1054
+ ok: false,
1055
+ error: errorShape(ErrorCodes.INVALID_REQUEST, `invalid request frame: ${formatValidationErrors(validateRequestFrame.errors)}`),
1056
+ });
1057
+ return;
1058
+ }
1059
+ const req = parsed;
1060
+ logWs("in", "req", {
1061
+ connId,
1062
+ id: req.id,
1063
+ method: req.method,
1064
+ });
1065
+ const respond = (ok, payload, error, meta) => {
1066
+ send({ type: "res", id: req.id, ok, payload, error });
1067
+ logWs("out", "res", {
1068
+ connId,
1069
+ id: req.id,
1070
+ ok,
1071
+ method: req.method,
1072
+ errorCode: error?.code,
1073
+ errorMessage: error?.message,
1074
+ ...meta,
1075
+ });
1076
+ };
1077
+ void (async () => {
1078
+ await handleGatewayRequest({
1079
+ req,
1080
+ respond,
1081
+ client,
1082
+ isWebchatConnect,
1083
+ context: {
1084
+ deps,
1085
+ cron,
1086
+ cronStorePath,
1087
+ loadGatewayModelCatalog,
1088
+ getHealthCache: () => healthCache,
1089
+ refreshHealthSnapshot,
1090
+ logHealth,
1091
+ incrementPresenceVersion: () => {
1092
+ presenceVersion += 1;
1093
+ return presenceVersion;
1094
+ },
1095
+ getHealthVersion: () => healthVersion,
1096
+ broadcast,
1097
+ bridge,
1098
+ bridgeSendToSession,
1099
+ hasConnectedMobileNode,
1100
+ agentRunSeq,
1101
+ chatAbortControllers,
1102
+ chatRunBuffers,
1103
+ chatDeltaSentAt,
1104
+ addChatRun,
1105
+ removeChatRun,
1106
+ dedupe,
1107
+ wizardSessions,
1108
+ findRunningWizard,
1109
+ purgeWizardSession,
1110
+ getRuntimeSnapshot,
1111
+ startWhatsAppProvider,
1112
+ stopWhatsAppProvider,
1113
+ stopTelegramProvider,
1114
+ markWhatsAppLoggedOut,
1115
+ wizardRunner,
1116
+ broadcastVoiceWakeChanged,
1117
+ },
1118
+ });
1119
+ })().catch((err) => {
1120
+ log.error(`request handler failed: ${formatForLog(err)}`);
1121
+ respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, formatForLog(err)));
1122
+ });
1123
+ }
1124
+ catch (err) {
1125
+ log.error(`parse/handle error: ${String(err)}`);
1126
+ logWs("out", "parse-error", { connId, error: formatForLog(err) });
1127
+ // If still in handshake, close; otherwise respond error
1128
+ if (!client) {
1129
+ close();
1130
+ }
1131
+ }
1132
+ });
1133
+ });
1134
+ const { provider: agentProvider, model: agentModel } = resolveConfiguredModelRef({
1135
+ cfg: cfgAtStart,
1136
+ defaultProvider: DEFAULT_PROVIDER,
1137
+ defaultModel: DEFAULT_MODEL,
1138
+ });
1139
+ const modelRef = `${agentProvider}/${agentModel}`;
1140
+ log.info(`agent model: ${modelRef}`, {
1141
+ consoleMessage: `agent model: ${chalk.whiteBright(modelRef)}`,
1142
+ });
1143
+ log.info(`listening on ws://${bindHost}:${port} (PID ${process.pid})`);
1144
+ log.info(`log file: ${getResolvedLoggerSettings().file}`);
1145
+ if (isNixMode) {
1146
+ log.info("gateway: running in Nix mode (config managed externally)");
1147
+ }
1148
+ let tailscaleCleanup = null;
1149
+ if (tailscaleMode !== "off") {
1150
+ try {
1151
+ if (tailscaleMode === "serve") {
1152
+ await enableTailscaleServe(port);
1153
+ }
1154
+ else {
1155
+ await enableTailscaleFunnel(port);
1156
+ }
1157
+ const host = await getTailnetHostname().catch(() => null);
1158
+ if (host) {
1159
+ const uiPath = controlUiBasePath ? `${controlUiBasePath}/` : "/";
1160
+ logTailscale.info(`${tailscaleMode} enabled: https://${host}${uiPath} (WS via wss://${host})`);
1161
+ }
1162
+ else {
1163
+ logTailscale.info(`${tailscaleMode} enabled`);
1164
+ }
1165
+ }
1166
+ catch (err) {
1167
+ logTailscale.warn(`${tailscaleMode} failed: ${err instanceof Error ? err.message : String(err)}`);
1168
+ }
1169
+ if (tailscaleConfig.resetOnExit) {
1170
+ tailscaleCleanup = async () => {
1171
+ try {
1172
+ if (tailscaleMode === "serve") {
1173
+ await disableTailscaleServe();
1174
+ }
1175
+ else {
1176
+ await disableTailscaleFunnel();
1177
+ }
1178
+ }
1179
+ catch (err) {
1180
+ logTailscale.warn(`${tailscaleMode} cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1181
+ }
1182
+ };
1183
+ }
1184
+ }
1185
+ // Start clawd browser control server (unless disabled via config).
1186
+ let browserControl = null;
1187
+ try {
1188
+ browserControl = await startBrowserControlServerIfEnabled();
1189
+ }
1190
+ catch (err) {
1191
+ logBrowser.error(`server failed to start: ${String(err)}`);
1192
+ }
1193
+ // Start Gmail watcher if configured (hooks.gmail.account).
1194
+ if (process.env.CLAWDBOT_SKIP_GMAIL_WATCHER !== "1") {
1195
+ try {
1196
+ const gmailResult = await startGmailWatcher(cfgAtStart);
1197
+ if (gmailResult.started) {
1198
+ logHooks.info("gmail watcher started");
1199
+ }
1200
+ else if (gmailResult.reason &&
1201
+ gmailResult.reason !== "hooks not enabled" &&
1202
+ gmailResult.reason !== "no gmail account configured") {
1203
+ logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`);
1204
+ }
1205
+ }
1206
+ catch (err) {
1207
+ logHooks.error(`gmail watcher failed to start: ${String(err)}`);
1208
+ }
1209
+ }
1210
+ // Launch configured providers (WhatsApp Web, Discord, Slack, Telegram) so gateway replies via the
1211
+ // surface the message came from. Tests can opt out via CLAWDBOT_SKIP_PROVIDERS.
1212
+ if (process.env.CLAWDBOT_SKIP_PROVIDERS !== "1") {
1213
+ try {
1214
+ await startProviders();
1215
+ }
1216
+ catch (err) {
1217
+ logProviders.error(`provider startup failed: ${String(err)}`);
1218
+ }
1219
+ }
1220
+ else {
1221
+ logProviders.info("skipping provider start (CLAWDBOT_SKIP_PROVIDERS=1)");
1222
+ }
1223
+ const applyHotReload = async (plan, nextConfig) => {
1224
+ if (plan.reloadHooks) {
1225
+ try {
1226
+ hooksConfig = resolveHooksConfig(nextConfig);
1227
+ }
1228
+ catch (err) {
1229
+ logHooks.warn(`hooks config reload failed: ${String(err)}`);
1230
+ }
1231
+ }
1232
+ if (plan.restartHeartbeat) {
1233
+ heartbeatRunner.stop();
1234
+ heartbeatRunner = startHeartbeatRunner({ cfg: nextConfig });
1235
+ }
1236
+ if (plan.restartCron) {
1237
+ cron.stop();
1238
+ const next = buildCronService(nextConfig);
1239
+ cron = next.cron;
1240
+ cronStorePath = next.storePath;
1241
+ void cron
1242
+ .start()
1243
+ .catch((err) => logCron.error(`failed to start: ${String(err)}`));
1244
+ }
1245
+ if (plan.restartBrowserControl) {
1246
+ if (browserControl) {
1247
+ await browserControl.stop().catch(() => { });
1248
+ }
1249
+ try {
1250
+ browserControl = await startBrowserControlServerIfEnabled();
1251
+ }
1252
+ catch (err) {
1253
+ logBrowser.error(`server failed to start: ${String(err)}`);
1254
+ }
1255
+ }
1256
+ if (plan.restartGmailWatcher) {
1257
+ await stopGmailWatcher().catch(() => { });
1258
+ if (process.env.CLAWDBOT_SKIP_GMAIL_WATCHER !== "1") {
1259
+ try {
1260
+ const gmailResult = await startGmailWatcher(nextConfig);
1261
+ if (gmailResult.started) {
1262
+ logHooks.info("gmail watcher started");
1263
+ }
1264
+ else if (gmailResult.reason &&
1265
+ gmailResult.reason !== "hooks not enabled" &&
1266
+ gmailResult.reason !== "no gmail account configured") {
1267
+ logHooks.warn(`gmail watcher not started: ${gmailResult.reason}`);
1268
+ }
1269
+ }
1270
+ catch (err) {
1271
+ logHooks.error(`gmail watcher failed to start: ${String(err)}`);
1272
+ }
1273
+ }
1274
+ else {
1275
+ logHooks.info("skipping gmail watcher restart (CLAWDBOT_SKIP_GMAIL_WATCHER=1)");
1276
+ }
1277
+ }
1278
+ if (plan.restartProviders.size > 0) {
1279
+ if (process.env.CLAWDBOT_SKIP_PROVIDERS === "1") {
1280
+ logProviders.info("skipping provider reload (CLAWDBOT_SKIP_PROVIDERS=1)");
1281
+ }
1282
+ else {
1283
+ const restartProvider = async (name, stop, start) => {
1284
+ logProviders.info(`restarting ${name} provider`);
1285
+ await stop();
1286
+ await start();
1287
+ };
1288
+ if (plan.restartProviders.has("whatsapp")) {
1289
+ await restartProvider("whatsapp", stopWhatsAppProvider, startWhatsAppProvider);
1290
+ }
1291
+ if (plan.restartProviders.has("telegram")) {
1292
+ await restartProvider("telegram", stopTelegramProvider, startTelegramProvider);
1293
+ }
1294
+ if (plan.restartProviders.has("discord")) {
1295
+ await restartProvider("discord", stopDiscordProvider, startDiscordProvider);
1296
+ }
1297
+ if (plan.restartProviders.has("slack")) {
1298
+ await restartProvider("slack", stopSlackProvider, startSlackProvider);
1299
+ }
1300
+ if (plan.restartProviders.has("signal")) {
1301
+ await restartProvider("signal", stopSignalProvider, startSignalProvider);
1302
+ }
1303
+ if (plan.restartProviders.has("imessage")) {
1304
+ await restartProvider("imessage", stopIMessageProvider, startIMessageProvider);
1305
+ }
1306
+ }
1307
+ }
1308
+ setCommandLaneConcurrency("cron", nextConfig.cron?.maxConcurrentRuns ?? 1);
1309
+ setCommandLaneConcurrency("main", nextConfig.agent?.maxConcurrent ?? 1);
1310
+ if (plan.hotReasons.length > 0) {
1311
+ logReload.info(`config hot reload applied (${plan.hotReasons.join(", ")})`);
1312
+ }
1313
+ else if (plan.noopPaths.length > 0) {
1314
+ logReload.info(`config change applied (dynamic reads: ${plan.noopPaths.join(", ")})`);
1315
+ }
1316
+ };
1317
+ const requestGatewayRestart = (plan, _nextConfig) => {
1318
+ const reasons = plan.restartReasons.length
1319
+ ? plan.restartReasons.join(", ")
1320
+ : plan.changedPaths.join(", ");
1321
+ logReload.warn(`config change requires gateway restart (${reasons})`);
1322
+ if (process.listenerCount("SIGUSR1") === 0) {
1323
+ logReload.warn("no SIGUSR1 listener found; restart skipped");
1324
+ return;
1325
+ }
1326
+ process.emit("SIGUSR1");
1327
+ };
1328
+ const configReloader = startGatewayConfigReloader({
1329
+ initialConfig: cfgAtStart,
1330
+ readSnapshot: readConfigFileSnapshot,
1331
+ onHotReload: applyHotReload,
1332
+ onRestart: requestGatewayRestart,
1333
+ log: {
1334
+ info: (msg) => logReload.info(msg),
1335
+ warn: (msg) => logReload.warn(msg),
1336
+ error: (msg) => logReload.error(msg),
1337
+ },
1338
+ watchPath: CONFIG_PATH_CLAWDBOT,
1339
+ });
1340
+ return {
1341
+ close: async (opts) => {
1342
+ const reasonRaw = typeof opts?.reason === "string" ? opts.reason.trim() : "";
1343
+ const reason = reasonRaw || "gateway stopping";
1344
+ const restartExpectedMs = typeof opts?.restartExpectedMs === "number" &&
1345
+ Number.isFinite(opts.restartExpectedMs)
1346
+ ? Math.max(0, Math.floor(opts.restartExpectedMs))
1347
+ : null;
1348
+ if (bonjourStop) {
1349
+ try {
1350
+ await bonjourStop();
1351
+ }
1352
+ catch {
1353
+ /* ignore */
1354
+ }
1355
+ }
1356
+ if (tailscaleCleanup) {
1357
+ await tailscaleCleanup();
1358
+ }
1359
+ if (canvasHost) {
1360
+ try {
1361
+ await canvasHost.close();
1362
+ }
1363
+ catch {
1364
+ /* ignore */
1365
+ }
1366
+ }
1367
+ if (canvasHostServer) {
1368
+ try {
1369
+ await canvasHostServer.close();
1370
+ }
1371
+ catch {
1372
+ /* ignore */
1373
+ }
1374
+ }
1375
+ if (bridge) {
1376
+ try {
1377
+ await bridge.close();
1378
+ }
1379
+ catch {
1380
+ /* ignore */
1381
+ }
1382
+ }
1383
+ await stopWhatsAppProvider();
1384
+ await stopTelegramProvider();
1385
+ await stopDiscordProvider();
1386
+ await stopSlackProvider();
1387
+ await stopSignalProvider();
1388
+ await stopIMessageProvider();
1389
+ await stopGmailWatcher();
1390
+ cron.stop();
1391
+ heartbeatRunner.stop();
1392
+ for (const timer of nodePresenceTimers.values()) {
1393
+ clearInterval(timer);
1394
+ }
1395
+ nodePresenceTimers.clear();
1396
+ broadcast("shutdown", {
1397
+ reason,
1398
+ restartExpectedMs,
1399
+ });
1400
+ clearInterval(tickInterval);
1401
+ clearInterval(healthInterval);
1402
+ clearInterval(dedupeCleanup);
1403
+ if (agentUnsub) {
1404
+ try {
1405
+ agentUnsub();
1406
+ }
1407
+ catch {
1408
+ /* ignore */
1409
+ }
1410
+ }
1411
+ if (heartbeatUnsub) {
1412
+ try {
1413
+ heartbeatUnsub();
1414
+ }
1415
+ catch {
1416
+ /* ignore */
1417
+ }
1418
+ }
1419
+ chatRunState.clear();
1420
+ for (const c of clients) {
1421
+ try {
1422
+ c.socket.close(1012, "service restart");
1423
+ }
1424
+ catch {
1425
+ /* ignore */
1426
+ }
1427
+ }
1428
+ clients.clear();
1429
+ await configReloader.stop().catch(() => { });
1430
+ if (browserControl) {
1431
+ await browserControl.stop().catch(() => { });
1432
+ }
1433
+ await new Promise((resolve) => wss.close(() => resolve()));
1434
+ await new Promise((resolve, reject) => httpServer.close((err) => (err ? reject(err) : resolve())));
1435
+ },
1436
+ };
1437
+ }