crewswarm 0.8.1-beta

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 (362) hide show
  1. package/.env.example +155 -0
  2. package/LICENSE +21 -0
  3. package/README.md +316 -0
  4. package/apps/dashboard/dist/assets/chat-core-BwSoInmZ.js +1 -0
  5. package/apps/dashboard/dist/assets/chat-core-BwSoInmZ.js.br +0 -0
  6. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +1 -0
  7. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  8. package/apps/dashboard/dist/assets/components-CSUb80ze.js +1 -0
  9. package/apps/dashboard/dist/assets/components-CSUb80ze.js.br +0 -0
  10. package/apps/dashboard/dist/assets/core-utils-CAVnDoe1.js +1 -0
  11. package/apps/dashboard/dist/assets/core-utils-CAVnDoe1.js.br +0 -0
  12. package/apps/dashboard/dist/assets/index-CF0aJRtC.css +1 -0
  13. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  14. package/apps/dashboard/dist/assets/index-Px49zu76.js +2 -0
  15. package/apps/dashboard/dist/assets/index-Px49zu76.js.br +0 -0
  16. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js +1 -0
  17. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  18. package/apps/dashboard/dist/assets/setup-wizard-i3eEixlo.js +1 -0
  19. package/apps/dashboard/dist/assets/setup-wizard-i3eEixlo.js.br +0 -0
  20. package/apps/dashboard/dist/assets/tab-agents-tab-BThdsdJY.js +1 -0
  21. package/apps/dashboard/dist/assets/tab-agents-tab-BThdsdJY.js.br +0 -0
  22. package/apps/dashboard/dist/assets/tab-benchmarks-tab-DfCuAClu.js +1 -0
  23. package/apps/dashboard/dist/assets/tab-comms-tab-eHpOSBhG.js +1 -0
  24. package/apps/dashboard/dist/assets/tab-comms-tab-eHpOSBhG.js.br +0 -0
  25. package/apps/dashboard/dist/assets/tab-contacts-tab-yEegNyO4.js +1 -0
  26. package/apps/dashboard/dist/assets/tab-contacts-tab-yEegNyO4.js.br +0 -0
  27. package/apps/dashboard/dist/assets/tab-engines-tab-C3DYxTwy.js +1 -0
  28. package/apps/dashboard/dist/assets/tab-engines-tab-C3DYxTwy.js.br +0 -0
  29. package/apps/dashboard/dist/assets/tab-memory-tab-C59BYFQD.js +1 -0
  30. package/apps/dashboard/dist/assets/tab-memory-tab-C59BYFQD.js.br +0 -0
  31. package/apps/dashboard/dist/assets/tab-models-tab-9Ur7pXWA.js +1 -0
  32. package/apps/dashboard/dist/assets/tab-models-tab-9Ur7pXWA.js.br +0 -0
  33. package/apps/dashboard/dist/assets/tab-pm-loop-tab-D7mnDelU.js +1 -0
  34. package/apps/dashboard/dist/assets/tab-pm-loop-tab-D7mnDelU.js.br +0 -0
  35. package/apps/dashboard/dist/assets/tab-projects-tab-C6h2Mv1K.js +1 -0
  36. package/apps/dashboard/dist/assets/tab-projects-tab-C6h2Mv1K.js.br +0 -0
  37. package/apps/dashboard/dist/assets/tab-prompts-tab-C0wZvWK3.js +1 -0
  38. package/apps/dashboard/dist/assets/tab-prompts-tab-C0wZvWK3.js.br +0 -0
  39. package/apps/dashboard/dist/assets/tab-services-tab-DBj_w3bc.js +1 -0
  40. package/apps/dashboard/dist/assets/tab-services-tab-DBj_w3bc.js.br +0 -0
  41. package/apps/dashboard/dist/assets/tab-settings-tab-ezeqAjZk.js +1 -0
  42. package/apps/dashboard/dist/assets/tab-settings-tab-ezeqAjZk.js.br +0 -0
  43. package/apps/dashboard/dist/assets/tab-skills-tab-BYdU2whk.js +1 -0
  44. package/apps/dashboard/dist/assets/tab-skills-tab-BYdU2whk.js.br +0 -0
  45. package/apps/dashboard/dist/assets/tab-spending-tab-Bg6w9t_p.js +1 -0
  46. package/apps/dashboard/dist/assets/tab-spending-tab-Bg6w9t_p.js.br +0 -0
  47. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BBV9HB2X.js +1 -0
  48. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BBV9HB2X.js.br +0 -0
  49. package/apps/dashboard/dist/assets/tab-swarm-tab-ChqLlEVs.js +1 -0
  50. package/apps/dashboard/dist/assets/tab-swarm-tab-ChqLlEVs.js.br +0 -0
  51. package/apps/dashboard/dist/assets/tab-usage-tab-B2UWXenJ.js +1 -0
  52. package/apps/dashboard/dist/assets/tab-usage-tab-B2UWXenJ.js.br +0 -0
  53. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js +1 -0
  54. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-workflows-tab-6QSXLJ0i.js +1 -0
  56. package/apps/dashboard/dist/assets/tab-workflows-tab-6QSXLJ0i.js.br +0 -0
  57. package/apps/dashboard/dist/favicon.png +0 -0
  58. package/apps/dashboard/dist/index.html +6466 -0
  59. package/apps/dashboard/dist/index.html.br +0 -0
  60. package/apps/dashboard/dist/index.html.gz +0 -0
  61. package/apps/dashboard/dist/signup.html +446 -0
  62. package/apps/dashboard/index.html +6442 -0
  63. package/apps/dashboard/package.json +15 -0
  64. package/apps/dashboard/src/app.js +2823 -0
  65. package/apps/dashboard/src/app.js.br +0 -0
  66. package/apps/dashboard/src/app.js.gz +0 -0
  67. package/apps/dashboard/src/chat/chat-actions.js +1847 -0
  68. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  69. package/apps/dashboard/src/chat/unified-messages.js +327 -0
  70. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  71. package/apps/dashboard/src/cli-process.js +208 -0
  72. package/apps/dashboard/src/cli-process.js.br +0 -0
  73. package/apps/dashboard/src/cli-process.js.gz +0 -0
  74. package/apps/dashboard/src/components/active-tasks-panel.js +175 -0
  75. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  76. package/apps/dashboard/src/core/api.js +18 -0
  77. package/apps/dashboard/src/core/api.js.br +0 -0
  78. package/apps/dashboard/src/core/dom.js +220 -0
  79. package/apps/dashboard/src/core/dom.js.br +0 -0
  80. package/apps/dashboard/src/core/state.js +91 -0
  81. package/apps/dashboard/src/core/state.js.br +0 -0
  82. package/apps/dashboard/src/core/task-manager.js +134 -0
  83. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  84. package/apps/dashboard/src/orchestration-status.js +127 -0
  85. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  86. package/apps/dashboard/src/setup-wizard.js +555 -0
  87. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  88. package/apps/dashboard/src/styles.css +2085 -0
  89. package/apps/dashboard/src/styles.css.br +0 -0
  90. package/apps/dashboard/src/styles.css.gz +0 -0
  91. package/apps/dashboard/src/tabs/agents-tab.js +2237 -0
  92. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  93. package/apps/dashboard/src/tabs/benchmarks-tab.js +229 -0
  94. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  95. package/apps/dashboard/src/tabs/comms-tab.js +955 -0
  96. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  97. package/apps/dashboard/src/tabs/contacts-tab.js +654 -0
  98. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  99. package/apps/dashboard/src/tabs/engines-tab.js +175 -0
  100. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  101. package/apps/dashboard/src/tabs/memory-tab.js +182 -0
  102. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  103. package/apps/dashboard/src/tabs/models-tab.js +441 -0
  104. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  105. package/apps/dashboard/src/tabs/pm-loop-tab.js +185 -0
  106. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  107. package/apps/dashboard/src/tabs/projects-tab.js +663 -0
  108. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  109. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  110. package/apps/dashboard/src/tabs/prompts-tab.js +160 -0
  111. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  112. package/apps/dashboard/src/tabs/services-tab.js +202 -0
  113. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  114. package/apps/dashboard/src/tabs/settings-tab.js +803 -0
  115. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  116. package/apps/dashboard/src/tabs/skills-tab.js +284 -0
  117. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  118. package/apps/dashboard/src/tabs/spending-tab.js +173 -0
  119. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  120. package/apps/dashboard/src/tabs/swarm-chat-tab.js +660 -0
  121. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  122. package/apps/dashboard/src/tabs/swarm-tab.js +538 -0
  123. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  124. package/apps/dashboard/src/tabs/usage-tab.js +390 -0
  125. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  126. package/apps/dashboard/src/tabs/waves-tab.js +238 -0
  127. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  128. package/apps/dashboard/src/tabs/workflows-tab.js +747 -0
  129. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  130. package/apps/vibe/.crew/agent-memory/pipeline.json +249 -0
  131. package/apps/vibe/.crew/cost.json +17 -0
  132. package/apps/vibe/.crew/json-parse-metrics.jsonl +22 -0
  133. package/apps/vibe/.crew/pipeline-metrics.jsonl +22 -0
  134. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +5 -0
  135. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +5 -0
  136. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +1 -0
  137. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +5 -0
  138. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +5 -0
  139. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +5 -0
  140. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +5 -0
  141. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +5 -0
  142. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +5 -0
  143. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +5 -0
  144. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +5 -0
  145. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +5 -0
  146. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +5 -0
  147. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +5 -0
  148. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +5 -0
  149. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +5 -0
  150. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +5 -0
  151. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +5 -0
  152. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +2 -0
  153. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +5 -0
  154. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +5 -0
  155. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +5 -0
  156. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +5 -0
  157. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +5 -0
  158. package/apps/vibe/.crew/sandbox.json +7 -0
  159. package/apps/vibe/.crew/session.json +285 -0
  160. package/apps/vibe/.crew/training-data.jsonl +0 -0
  161. package/apps/vibe/.github/workflows/studio-quality.yml +37 -0
  162. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +12 -0
  163. package/apps/vibe/.studio-data/project-messages/general.jsonl +54 -0
  164. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +10 -0
  165. package/apps/vibe/ARCHITECTURE.md +3393 -0
  166. package/apps/vibe/QUICK-REFERENCE.md +211 -0
  167. package/apps/vibe/README.md +76 -0
  168. package/apps/vibe/ROADMAP.md +41 -0
  169. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +35 -0
  170. package/apps/vibe/VISUAL-GUIDE.md +378 -0
  171. package/apps/vibe/capture-demo.mjs +160 -0
  172. package/apps/vibe/capture-vibe-assets.mjs +71 -0
  173. package/apps/vibe/capture-vibe-video.mjs +260 -0
  174. package/apps/vibe/check-buttons.js +41 -0
  175. package/apps/vibe/diagnose.html +106 -0
  176. package/apps/vibe/fix-buttons.js +103 -0
  177. package/apps/vibe/index.html +3401 -0
  178. package/apps/vibe/package-lock.json +920 -0
  179. package/apps/vibe/package.json +31 -0
  180. package/apps/vibe/public/favicon.png +0 -0
  181. package/apps/vibe/scripts/studio-pty-host.py +117 -0
  182. package/apps/vibe/server.mjs +1835 -0
  183. package/apps/vibe/src/main.js +2846 -0
  184. package/apps/vibe/src/register-all-languages.js +98 -0
  185. package/apps/vibe/start-studio.sh +11 -0
  186. package/apps/vibe/test/accessibility-tests.js +77 -0
  187. package/apps/vibe/test/browser-performance-audit.mjs +205 -0
  188. package/apps/vibe/test/performance-tests.js +120 -0
  189. package/apps/vibe/test/security-tests.js +213 -0
  190. package/apps/vibe/tests/e2e.local.mjs +54 -0
  191. package/apps/vibe/tests/server.smoke.mjs +106 -0
  192. package/apps/vibe/update_website.mjs +74 -0
  193. package/apps/vibe/vite.config.js +19 -0
  194. package/apps/vibe/watch-server.mjs +108 -0
  195. package/contrib/openclaw-plugin/README.md +199 -0
  196. package/contrib/openclaw-plugin/index.ts +306 -0
  197. package/contrib/openclaw-plugin/openclaw.plugin.json +41 -0
  198. package/contrib/openclaw-plugin/package.json +27 -0
  199. package/contrib/openclaw-plugin/skills/crewswarm/SKILL.md +88 -0
  200. package/crew-lead.mjs +649 -0
  201. package/engines/claude-code.json +36 -0
  202. package/engines/codex.json +37 -0
  203. package/engines/crew-cli.json +42 -0
  204. package/engines/cursor.json +40 -0
  205. package/engines/docker-sandbox.json +38 -0
  206. package/engines/gemini-cli.json +75 -0
  207. package/engines/opencode.json +31 -0
  208. package/gateway-bridge.mjs +1575 -0
  209. package/install.sh +738 -0
  210. package/lib/agent-registry.mjs +232 -0
  211. package/lib/agents/daemon.mjs +121 -0
  212. package/lib/agents/dispatch.mjs +225 -0
  213. package/lib/agents/permissions.mjs +90 -0
  214. package/lib/agents/platform-formatting.mjs +102 -0
  215. package/lib/agents/registry.mjs +81 -0
  216. package/lib/agents/tool-instructions.mjs +257 -0
  217. package/lib/agents/validation.mjs +75 -0
  218. package/lib/approval/policy-manager.mjs +221 -0
  219. package/lib/autoharness/index.mjs +391 -0
  220. package/lib/bridges/cli-executor.mjs +332 -0
  221. package/lib/bridges/gateway-ws.mjs +345 -0
  222. package/lib/bridges/integration.mjs +229 -0
  223. package/lib/bridges/rag-helper.mjs +90 -0
  224. package/lib/browser/opencode-passthrough-filter.js +44 -0
  225. package/lib/browser/passthrough-stderr.js +109 -0
  226. package/lib/chat/autonomous-mentions.mjs +373 -0
  227. package/lib/chat/history.mjs +82 -0
  228. package/lib/chat/mention-routing-intent.mjs +136 -0
  229. package/lib/chat/participants.mjs +95 -0
  230. package/lib/chat/project-messages-rag.mjs +265 -0
  231. package/lib/chat/project-messages.mjs +479 -0
  232. package/lib/chat/shared-chat-prompt-overlay.mjs +52 -0
  233. package/lib/chat/thread-binding.mjs +34 -0
  234. package/lib/chat/unified-history.mjs +223 -0
  235. package/lib/chat/unified-wrapper.mjs +41 -0
  236. package/lib/cli-process-tracker.mjs +228 -0
  237. package/lib/collections/index.mjs +433 -0
  238. package/lib/contacts/identity-linker.mjs +248 -0
  239. package/lib/contacts/index.mjs +341 -0
  240. package/lib/crew-judge/PROMPT.md +93 -0
  241. package/lib/crew-judge/judge.mjs +260 -0
  242. package/lib/crew-lead/agent-manager.mjs +125 -0
  243. package/lib/crew-lead/background.mjs +270 -0
  244. package/lib/crew-lead/brain.mjs +110 -0
  245. package/lib/crew-lead/chat-handler.mjs +2603 -0
  246. package/lib/crew-lead/chat-handler.mjs.bak +1274 -0
  247. package/lib/crew-lead/classifier.mjs +83 -0
  248. package/lib/crew-lead/http-server.mjs +4824 -0
  249. package/lib/crew-lead/intent.mjs +102 -0
  250. package/lib/crew-lead/interval-manager.mjs +41 -0
  251. package/lib/crew-lead/llm-caller.mjs +544 -0
  252. package/lib/crew-lead/prompts.mjs +392 -0
  253. package/lib/crew-lead/retry-manager.mjs +118 -0
  254. package/lib/crew-lead/tools.mjs +318 -0
  255. package/lib/crew-lead/wave-dispatcher.mjs +798 -0
  256. package/lib/crew-lead/waves-config.json +73 -0
  257. package/lib/crew-lead/waves-loader.mjs +110 -0
  258. package/lib/crew-lead/ws-router.mjs +428 -0
  259. package/lib/dispatch/parsers.mjs +299 -0
  260. package/lib/domain-planning/detector.mjs +196 -0
  261. package/lib/domain-planning/prompts/crew-pm-cli.md +96 -0
  262. package/lib/domain-planning/prompts/crew-pm-core.md +122 -0
  263. package/lib/domain-planning/prompts/crew-pm-frontend.md +111 -0
  264. package/lib/engines/crew-cli-sandbox.mjs +422 -0
  265. package/lib/engines/crew-cli.mjs +155 -0
  266. package/lib/engines/cursor-launcher.mjs +110 -0
  267. package/lib/engines/engine-registry.mjs +253 -0
  268. package/lib/engines/llm-direct.mjs +184 -0
  269. package/lib/engines/opencode.mjs +256 -0
  270. package/lib/engines/ouroboros.mjs +114 -0
  271. package/lib/engines/rt-envelope.mjs +1643 -0
  272. package/lib/engines/rt-envelope.mjs.backup-current +870 -0
  273. package/lib/engines/runners.mjs +1367 -0
  274. package/lib/gemini-cli-passthrough-noise.mjs +37 -0
  275. package/lib/integrations/code-search.mjs +259 -0
  276. package/lib/integrations/greptile.mjs +148 -0
  277. package/lib/integrations/multimodal.mjs +313 -0
  278. package/lib/integrations/telegram-streaming.mjs +153 -0
  279. package/lib/integrations/tts.mjs +312 -0
  280. package/lib/integrations/twitter-links.mjs +294 -0
  281. package/lib/memory/shared-adapter.mjs +296 -0
  282. package/lib/pipeline/manager.mjs +539 -0
  283. package/lib/preferences/extractor.mjs +347 -0
  284. package/lib/project-dir.mjs +20 -0
  285. package/lib/runtime/config.mjs +388 -0
  286. package/lib/runtime/dlq.mjs +170 -0
  287. package/lib/runtime/log-rotation.mjs +82 -0
  288. package/lib/runtime/logger.mjs +58 -0
  289. package/lib/runtime/memory.mjs +421 -0
  290. package/lib/runtime/paths.mjs +76 -0
  291. package/lib/runtime/project-dir.mjs +127 -0
  292. package/lib/runtime/spending.mjs +204 -0
  293. package/lib/runtime/startup-guard.mjs +291 -0
  294. package/lib/runtime/task-lease.mjs +234 -0
  295. package/lib/runtime/telemetry-schema.mjs +208 -0
  296. package/lib/runtime/telemetry.mjs +101 -0
  297. package/lib/runtime/utils.mjs +64 -0
  298. package/lib/skills/index.mjs +265 -0
  299. package/lib/tools/browser.mjs +135 -0
  300. package/lib/tools/executor.mjs +913 -0
  301. package/lib/types.d.ts +57 -0
  302. package/package.json +106 -0
  303. package/pm-loop.mjs +1626 -0
  304. package/prompts/coder-back.md +27 -0
  305. package/prompts/coder-front.md +27 -0
  306. package/prompts/coder.md +28 -0
  307. package/prompts/copywriter.md +17 -0
  308. package/prompts/fixer.md +39 -0
  309. package/prompts/frontend.md +23 -0
  310. package/prompts/github.md +24 -0
  311. package/prompts/main.md +39 -0
  312. package/prompts/pm-cli.md +95 -0
  313. package/prompts/pm-core.md +121 -0
  314. package/prompts/pm-frontend.md +110 -0
  315. package/prompts/pm.md +234 -0
  316. package/prompts/qa.md +44 -0
  317. package/prompts/security.md +19 -0
  318. package/scripts/build-crew-chat.sh +28 -0
  319. package/scripts/build-llms-full.mjs +52 -0
  320. package/scripts/chatmock-login.sh +16 -0
  321. package/scripts/chatmock-serve.sh +16 -0
  322. package/scripts/check-dashboard.mjs +88 -0
  323. package/scripts/crew-scribe.mjs +326 -0
  324. package/scripts/dashboard-helpers.mjs +391 -0
  325. package/scripts/dashboard-validation.mjs +198 -0
  326. package/scripts/dashboard.mjs +9717 -0
  327. package/scripts/dlq-replay.mjs +61 -0
  328. package/scripts/doctor.mjs +196 -0
  329. package/scripts/file-lock.mjs +186 -0
  330. package/scripts/fresh-machine-smoke.sh +323 -0
  331. package/scripts/generate-changelog.mjs +227 -0
  332. package/scripts/generate-openapi.mjs +334 -0
  333. package/scripts/health-check.mjs +229 -0
  334. package/scripts/install-docker.sh +213 -0
  335. package/scripts/mcp-server.mjs +1625 -0
  336. package/scripts/opencrew-rt-daemon.mjs +568 -0
  337. package/scripts/openswitchctl +646 -0
  338. package/scripts/refactor-configs.mjs +39 -0
  339. package/scripts/release-check.sh +46 -0
  340. package/scripts/resolve-node-bin.sh +25 -0
  341. package/scripts/restart-all-from-repo.sh +329 -0
  342. package/scripts/restart-crew-lead.sh +98 -0
  343. package/scripts/restart-dashboard.sh +104 -0
  344. package/scripts/restart-service.sh +274 -0
  345. package/scripts/run-accessibility-audit.mjs +356 -0
  346. package/scripts/run-integration-bounded.mjs +188 -0
  347. package/scripts/run-scheduled-pipeline.mjs +230 -0
  348. package/scripts/run.mjs +41 -0
  349. package/scripts/scan-skills.mjs +79 -0
  350. package/scripts/setup-firewall.sh +128 -0
  351. package/scripts/smoke-dispatch.mjs +149 -0
  352. package/scripts/smoke.sh +163 -0
  353. package/scripts/start-crew.mjs +328 -0
  354. package/scripts/start.mjs +146 -0
  355. package/scripts/swiftbar-restart-service.sh +19 -0
  356. package/scripts/sync-agents.mjs +152 -0
  357. package/scripts/sync-prompts.mjs +79 -0
  358. package/scripts/validate-config.mjs +337 -0
  359. package/scripts/wow.mjs +89 -0
  360. package/telegram-bridge.mjs +2421 -0
  361. package/unified-orchestrator.mjs +519 -0
  362. package/whatsapp-bridge.mjs +1481 -0
@@ -0,0 +1,1481 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * whatsapp-bridge.mjs — Connects WhatsApp to the crewswarm RT bus.
4
+ *
5
+ * Personal bot approach (WhatsApp Web automation via Baileys).
6
+ * Your phone number becomes a "linked device" — same as the WhatsApp
7
+ * Web / Desktop experience. No Business API or Meta approval needed.
8
+ *
9
+ * What it does:
10
+ * 1. On first run: prints a QR code → scan with WhatsApp on your phone
11
+ * 2. Connects to the RT bus (18889) as "crew-whatsapp"
12
+ * 3. Forwards incoming messages → crew-lead
13
+ * 4. Listens for crew-lead responses → sends them back to WhatsApp
14
+ *
15
+ * Auth persists in ~/.crewswarm/whatsapp-auth/ — no re-scan after restart.
16
+ *
17
+ * Usage:
18
+ * node whatsapp-bridge.mjs
19
+ *
20
+ * Allowed senders (allowlist):
21
+ * Set WA_ALLOWED_NUMBERS=+15551234567,+15559876543 in env or crewswarm.json
22
+ * Leave empty to allow any sender (open bot — not recommended).
23
+ *
24
+ * Commands (same as Telegram bridge):
25
+ * /projects — list registered projects
26
+ * /project <name> — set active project context
27
+ * /home — clear active project
28
+ * /status — show bridge status
29
+ */
30
+
31
+ import { createRequire } from "node:module";
32
+ import {
33
+ readFileSync, writeFileSync, existsSync,
34
+ appendFileSync, mkdirSync,
35
+ } from "node:fs";
36
+ import { join } from "node:path";
37
+ import { homedir } from "node:os";
38
+ import { randomUUID } from "node:crypto";
39
+ import http from "node:http";
40
+ import WebSocket from "ws";
41
+ import { loadHistory, appendHistory } from "./lib/chat/history.mjs";
42
+ import { shouldUseUnifiedHistory, formatUnifiedHistory } from "./lib/chat/unified-history.mjs";
43
+ import { trackContact, getContact, updatePreferences, saveMessage as saveContactMessage } from "./lib/contacts/index.mjs";
44
+ import { extractPreferences, shouldExtract, buildPreferencePrompt } from "./lib/preferences/extractor.mjs";
45
+ import { analyzeImage, transcribeAudio, hasVisionProvider, hasAudioProvider } from "./lib/integrations/multimodal.mjs";
46
+ import { textToSpeech, hasTTSProvider, chunkTextForTTS, getVoiceForAgent } from "./lib/integrations/tts.mjs";
47
+ import { execCrewLeadTools } from "./lib/crew-lead/tools.mjs";
48
+ import { buildToolInstructions, hasEngineConfigured, getToolPermissions } from "./lib/agents/tool-instructions.mjs";
49
+ import { getPlatformFormatting } from "./lib/agents/platform-formatting.mjs";
50
+ import { saveBridgeMessage } from "./lib/bridges/integration.mjs";
51
+ import { enrichTwitterLinks } from "./lib/integrations/twitter-links.mjs";
52
+ import { applySharedChatPromptOverlay } from "./lib/chat/shared-chat-prompt-overlay.mjs";
53
+
54
+ const require = createRequire(import.meta.url);
55
+
56
+ // ── Config ─────────────────────────────────────────────────────────────────────
57
+
58
+ const CREW_CFG_PATH = join(homedir(), ".crewswarm", "crewswarm.json");
59
+ const WA_AUTH_DIR = join(homedir(), ".crewswarm", "whatsapp-auth");
60
+ const LOG_PATH = join(homedir(), ".crewswarm", "logs", "whatsapp-bridge.jsonl");
61
+ const PID_PATH = join(homedir(), ".crewswarm", "logs", "whatsapp-bridge.pid");
62
+ const MSG_LOG = join(homedir(), ".crewswarm", "logs", "whatsapp-messages.jsonl");
63
+ const CONTEXT_FILE = join(process.cwd(), "memory", "whatsapp-context.md");
64
+
65
+ mkdirSync(join(homedir(), ".crewswarm", "logs"), { recursive: true });
66
+ mkdirSync(WA_AUTH_DIR, { recursive: true });
67
+
68
+ function loadCfg() {
69
+ try { return JSON.parse(readFileSync(CREW_CFG_PATH, "utf8")); } catch {}
70
+ return {};
71
+ }
72
+ const cfg = loadCfg();
73
+ const env = cfg.env || {};
74
+
75
+ const RT_URL = process.env.CREWSWARM_RT_URL || env.CREWSWARM_RT_URL || "ws://127.0.0.1:18889";
76
+ const RT_TOKEN = process.env.CREWSWARM_RT_AUTH_TOKEN || env.CREWSWARM_RT_AUTH_TOKEN || (() => {
77
+ // Fall back to ~/.crewswarm/crewswarm.json → rt.authToken (canonical location)
78
+ try {
79
+ const c = JSON.parse(readFileSync(join(homedir(), ".crewswarm", "crewswarm.json"), "utf8"));
80
+ return c?.rt?.authToken || "";
81
+ } catch { return ""; }
82
+ })();
83
+ const CREW_LEAD_URL = process.env.CREW_LEAD_URL || "http://127.0.0.1:5010";
84
+ const DASHBOARD_URL = process.env.DASHBOARD_URL || "http://127.0.0.1:4319";
85
+ const AGENT_NAME = "crew-whatsapp";
86
+ const TARGET = process.env.WA_TARGET_AGENT || env.WA_TARGET_AGENT || "crew-lead";
87
+ const HTTP_PORT = parseInt(process.env.WA_HTTP_PORT || env.WA_HTTP_PORT || "5015", 10);
88
+
89
+ // Allowlist — phone numbers in international format, e.g. "+15551234567"
90
+ // Numbers are normalised to JID format: "15551234567@s.whatsapp.net"
91
+
92
+ // Contact names — loaded from whatsapp-bridge.json (saved by dashboard)
93
+ const WA_BRIDGE_CFG_PATH = join(homedir(), ".crewswarm", "whatsapp-bridge.json");
94
+
95
+ function loadAllowedNumbers() {
96
+ // 1. Check env var first (backward compatibility)
97
+ const fromEnv = process.env.WA_ALLOWED_NUMBERS || env.WA_ALLOWED_NUMBERS || "";
98
+ if (fromEnv) {
99
+ return fromEnv.split(",").map(s => s.trim()).filter(Boolean);
100
+ }
101
+
102
+ // 2. Fall back to whatsapp-bridge.json (dashboard-set)
103
+ try {
104
+ const c = JSON.parse(readFileSync(WA_BRIDGE_CFG_PATH, "utf8"));
105
+ return c.allowedNumbers || [];
106
+ } catch { return []; }
107
+ }
108
+
109
+ function loadUserRouting() {
110
+ try {
111
+ const c = JSON.parse(readFileSync(WA_BRIDGE_CFG_PATH, "utf8"));
112
+ return c.userRouting || {};
113
+ } catch { return {}; }
114
+ }
115
+
116
+ // TTS configuration
117
+ function getTTSConfig() {
118
+ try {
119
+ const c = JSON.parse(readFileSync(WA_BRIDGE_CFG_PATH, "utf8"));
120
+ return c.tts || { enabled: false, provider: "auto", perUserOverrides: {} };
121
+ } catch {
122
+ return { enabled: false, provider: "auto", perUserOverrides: {} };
123
+ }
124
+ }
125
+
126
+ // Check if TTS is enabled for a specific user
127
+ function isTTSEnabled(jid) {
128
+ const config = getTTSConfig();
129
+
130
+ // Check per-user override first
131
+ if (config.perUserOverrides && config.perUserOverrides[jid] !== undefined) {
132
+ return config.perUserOverrides[jid];
133
+ }
134
+
135
+ // Fall back to global setting
136
+ return config.enabled === true;
137
+ }
138
+
139
+ const ALLOWED_RAW = loadAllowedNumbers();
140
+ const ALLOWED_JIDS = new Set(
141
+ ALLOWED_RAW.map(s => s.replace(/^\+/, ""))
142
+ .filter(Boolean)
143
+ .map(n => `${n}@s.whatsapp.net`)
144
+ );
145
+ const ALLOWLIST_ENABLED = ALLOWED_JIDS.size > 0;
146
+
147
+ // Per-user routing: maps "+1234..." or "1234...@s.whatsapp.net" → agent name
148
+ const USER_ROUTING = loadUserRouting();
149
+
150
+ // Resolve which agent a specific JID should talk to
151
+ function getTargetAgent(jid, sock) {
152
+ // Check JID format first: "15551234567@s.whatsapp.net" → agent
153
+ if (USER_ROUTING[jid]) return USER_ROUTING[jid];
154
+
155
+ // Extract digits from JID
156
+ let digits = jid.split("@")[0];
157
+
158
+ // Handle @lid (self-chat) — map to the owner's real number
159
+ if (jid.endsWith("@lid") && sock?.user?.id) {
160
+ digits = sock.user.id.split(":")[0];
161
+ }
162
+
163
+ // Check phone format: "+15551234567" → agent
164
+ if (USER_ROUTING[`+${digits}`]) return USER_ROUTING[`+${digits}`];
165
+ if (USER_ROUTING[digits]) return USER_ROUTING[digits];
166
+
167
+ // Fall back to default TARGET
168
+ return TARGET;
169
+ }
170
+
171
+ function loadContactNames() {
172
+ try {
173
+ const c = JSON.parse(readFileSync(WA_BRIDGE_CFG_PATH, "utf8"));
174
+ return c.contactNames || {};
175
+ } catch { return {}; }
176
+ }
177
+ // Resolve a JID like "15551234567@s.whatsapp.net" → "Jeff" or "+15551234567"
178
+ // Also handles @lid (self-chat) by looking up the bot's actual number
179
+ function resolveDisplayName(jid, sock) {
180
+ let digits = jid.split("@")[0];
181
+
182
+ // Handle @lid (self-chat) — map to the owner's real number
183
+ if (jid.endsWith("@lid") && sock?.user?.id) {
184
+ digits = sock.user.id.split(":")[0];
185
+ }
186
+
187
+ const names = loadContactNames();
188
+ return names[digits] || names[`+${digits}`] || `+${digits}`;
189
+ }
190
+
191
+ // ── Logging ────────────────────────────────────────────────────────────────────
192
+
193
+ function log(level, msg, data = {}) {
194
+ const entry = { ts: new Date().toISOString(), level, msg, ...data };
195
+ console.log(`[whatsapp-bridge] [${level}] ${msg}`, Object.keys(data).length ? data : "");
196
+ try { appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n"); } catch {}
197
+ }
198
+
199
+ // ── Singleton guard — kill stale duplicate before writing our PID ──────────
200
+ try {
201
+ if (existsSync(PID_PATH)) {
202
+ const existingPid = parseInt(readFileSync(PID_PATH, "utf8").trim(), 10);
203
+ if (existingPid && existingPid !== process.pid) {
204
+ try {
205
+ process.kill(existingPid, 0); // throws ESRCH if not running
206
+ process.kill(existingPid, "SIGTERM");
207
+ log("warn", `Killed stale whatsapp-bridge (pid ${existingPid}) — only one instance allowed`);
208
+ await new Promise(r => setTimeout(r, 500));
209
+ } catch (e) {
210
+ if (e.code !== "ESRCH") log("warn", `Could not kill old bridge pid ${existingPid}: ${e.message}`);
211
+ }
212
+ }
213
+ }
214
+ } catch {}
215
+ writeFileSync(PID_PATH, String(process.pid));
216
+ process.on("exit", () => { try { writeFileSync(PID_PATH, ""); } catch {} });
217
+
218
+ // ── Message helpers ────────────────────────────────────────────────────────────
219
+
220
+ function logMessage({ direction, jid, text }) {
221
+ const entry = { ts: new Date().toISOString(), direction, jid, text };
222
+ try { appendFileSync(MSG_LOG, JSON.stringify(entry) + "\n"); } catch {}
223
+ }
224
+
225
+ function splitMessage(text, maxLen = 4000) {
226
+ if (text.length <= maxLen) return [text];
227
+ const chunks = [];
228
+ let i = 0;
229
+ while (i < text.length) { chunks.push(text.slice(i, i + maxLen)); i += maxLen; }
230
+ return chunks;
231
+ }
232
+
233
+ // ── Deduplication (prevent RT + SSE double-send) ──────────────────────────────
234
+
235
+ const lastSentByJid = new Map();
236
+ const DEDUPE_WINDOW_MS = 30000;
237
+ const DEDUPE_MIN_LEN = 200;
238
+
239
+ function dedupeKey(text) {
240
+ return text.replace(/^✅ \*.+?\* finished:\n/, "").trim();
241
+ }
242
+
243
+ function shouldSkipDuplicate(jid, text) {
244
+ if (!text) return false;
245
+ const key = dedupeKey(text);
246
+ if (key.length < DEDUPE_MIN_LEN) return false;
247
+ const last = lastSentByJid.get(jid);
248
+ if (!last || Date.now() - last.ts > DEDUPE_WINDOW_MS) return false;
249
+ const lk = dedupeKey(last.content);
250
+ return lk === key || (lk.length > 200 && key.length > 200 && lk.slice(0, 200) === key.slice(0, 200));
251
+ }
252
+
253
+ // ── Conversation history — NOW PERSISTENT (uses lib/chat/history.mjs) ─────────
254
+ // History survives restarts and is shared with crew-lead's session system
255
+ // Format: ~/.crewswarm/chat-history/whatsapp/{jid}.jsonl
256
+ // Each WhatsApp user gets isolated, persistent history (last 2000 messages)
257
+
258
+ function getHistory(jid) {
259
+ const contactId = `whatsapp:${jid}`;
260
+
261
+ // Check if this user has unified identity enabled
262
+ if (shouldUseUnifiedHistory(contactId)) {
263
+ // Load unified history from all linked platforms
264
+ return formatUnifiedHistory(contactId);
265
+ }
266
+
267
+ // Otherwise, use platform-specific history (existing behavior)
268
+ return loadHistory("whatsapp", jid);
269
+ }
270
+
271
+ function addToHistory(jid, role, content, agent = null) {
272
+ appendHistory("whatsapp", jid, role, content, agent);
273
+ }
274
+
275
+ function formatHistory(jid) {
276
+ const hist = getHistory(jid);
277
+ if (!hist.length) return "";
278
+ return "\n\n--- Conversation history ---\n" +
279
+ hist.map(h => `${h.role === "user" ? "User" : "You"}: ${h.content}`).join("\n") +
280
+ "\n--- End history ---";
281
+ }
282
+
283
+ // ── Persistent context file (for agent memory injection) ──────────────────────
284
+
285
+ const MAX_CONTEXT_TURNS = 30;
286
+ let persistedTurns = [];
287
+
288
+ function loadPersistedTurns() {
289
+ try {
290
+ const raw = readFileSync(CONTEXT_FILE, "utf8");
291
+ const match = raw.match(/<!-- turns:(.*?) -->/s);
292
+ if (match) persistedTurns = JSON.parse(match[1]);
293
+ } catch {}
294
+ }
295
+
296
+ function writeContextFile() {
297
+ try {
298
+ const lines = persistedTurns.slice(-MAX_CONTEXT_TURNS).map(t =>
299
+ `**${t.role === "user" ? (t.name || "User") : "crewswarm"}** (${t.ts.slice(0,16)}): ${t.text}`
300
+ ).join("\n\n");
301
+ const content = [
302
+ "# WhatsApp Conversation Context",
303
+ "",
304
+ `Last updated: ${new Date().toISOString()}`,
305
+ "",
306
+ "Recent WhatsApp chat history for agent memory continuity.",
307
+ "",
308
+ "---",
309
+ "",
310
+ lines,
311
+ "",
312
+ `<!-- turns:${JSON.stringify(persistedTurns.slice(-MAX_CONTEXT_TURNS))} -->`,
313
+ ].join("\n");
314
+ writeFileSync(CONTEXT_FILE, content, "utf8");
315
+ } catch {}
316
+ }
317
+
318
+ function persistTurn(role, text, name) {
319
+ persistedTurns.push({ role, text: text.slice(0, 500), name, ts: new Date().toISOString() });
320
+ if (persistedTurns.length > MAX_CONTEXT_TURNS * 2) {
321
+ persistedTurns = persistedTurns.slice(-MAX_CONTEXT_TURNS);
322
+ }
323
+ writeContextFile();
324
+ }
325
+
326
+ // ── Active sessions (JID → metadata) ─────────────────────────────────────────
327
+
328
+ const activeSessions = new Map();
329
+ const lastReplyTime = new Map();
330
+ const activeProjectByJid = new Map();
331
+
332
+ // ── Project helpers ───────────────────────────────────────────────────────────
333
+
334
+ async function fetchProjects() {
335
+ const r = await fetch(`${DASHBOARD_URL}/api/projects`, { signal: AbortSignal.timeout(5000) });
336
+ const d = await r.json();
337
+ return d.projects || [];
338
+ }
339
+
340
+ // ── RT Bus ────────────────────────────────────────────────────────────────────
341
+
342
+ let rtClient = null;
343
+ let reconnectTimer = null; // Track reconnect timer to prevent duplicates
344
+ let connecting = false; // Prevent concurrent connection attempts
345
+ let reconnectAttempts = 0; // For exponential backoff
346
+
347
+ function connectRT(sendToJid) {
348
+ // Prevent duplicate connections
349
+ if (connecting) {
350
+ log("warn", "RT connection already in progress, skipping", { stack: new Error().stack.split('\n').slice(1,4).join('\n') });
351
+ return Promise.reject(new Error("Connection in progress"));
352
+ }
353
+
354
+ log("info", "connectRT called", { connecting, hasClient: !!rtClient, attempt: reconnectAttempts + 1 });
355
+
356
+ // Clear any pending reconnect
357
+ if (reconnectTimer) {
358
+ clearTimeout(reconnectTimer);
359
+ reconnectTimer = null;
360
+ log("info", "Cleared pending reconnect timer");
361
+ }
362
+
363
+ connecting = true;
364
+
365
+ return new Promise((resolve, reject) => {
366
+ const ws = new WebSocket(RT_URL);
367
+ let ready = false;
368
+
369
+ const client = {
370
+ publish({ channel, type, to, taskId, correlationId, payload }) {
371
+ if (ws.readyState !== WebSocket.OPEN) return;
372
+ ws.send(JSON.stringify({ type: "publish", channel, messageType: type, to, taskId, correlationId, priority: "high", payload }));
373
+ },
374
+ ack({ messageId }) {
375
+ if (ws.readyState !== WebSocket.OPEN) return;
376
+ ws.send(JSON.stringify({ type: "ack", messageId, status: "received" }));
377
+ },
378
+ isReady: () => ready,
379
+ ws,
380
+ };
381
+
382
+ ws.on("open", () => {
383
+ log("info", "RT socket open", { timestamp: new Date().toISOString() });
384
+ });
385
+
386
+ ws.on("message", async (raw) => {
387
+ let p;
388
+ try { p = JSON.parse(raw.toString()); } catch { return; }
389
+
390
+ if (p.type === "server.hello") {
391
+ ws.send(JSON.stringify({ type: "hello", agent: AGENT_NAME, token: RT_TOKEN }));
392
+ return;
393
+ }
394
+
395
+ if (p.type === "hello.ack") {
396
+ ws.send(JSON.stringify({ type: "subscribe", channels: ["command", "assign", "done", "status", "events"] }));
397
+ ready = true;
398
+ connecting = false; // Connection successful
399
+ resolve(client);
400
+ log("info", `RT connected as ${AGENT_NAME}`, { timestamp: new Date().toISOString() });
401
+ return;
402
+ }
403
+
404
+ if (p.type === "message" && p.envelope) {
405
+ const env = p.envelope;
406
+ if (env.messageId) client.ack({ messageId: env.messageId });
407
+
408
+ const from = env.from || env.sender_agent_id || "";
409
+ const content = env.payload?.content ? String(env.payload.content).trim() : "";
410
+ const isChatReply = env.messageType === "chat.reply" || env.type === "chat.reply";
411
+ const rawSessionId = env.payload?.sessionId;
412
+
413
+ // Extract JID from sessionId format "whatsapp-<jid>" — STRICT: reject non-whatsapp sessions
414
+ const sessionId = rawSessionId?.startsWith("whatsapp-")
415
+ ? rawSessionId.slice(9) // Extract JID: "whatsapp-13109...@s.whatsapp.net" → "13109...@s.whatsapp.net"
416
+ : null; // Not a WhatsApp session — do NOT fall through
417
+
418
+ if ((from === TARGET || isChatReply) && content && content.length > 2) {
419
+ const isHeartbeat = env.type === "agent.heartbeat" || env.channel === "status";
420
+ const isTaskNoise = content.startsWith("@@DISPATCH") || content.startsWith("[bridge]");
421
+ if (!isHeartbeat && !isTaskNoise) {
422
+ // CRITICAL: Only send to the specific session, NEVER broadcast to all
423
+ // The sessionId must match exactly to prevent sending to wrong contacts
424
+ if (sessionId && activeSessions.has(sessionId)) {
425
+ const jid = sessionId;
426
+ // Allowlist check on outbound — never send to unauthorized JIDs
427
+ if (ALLOWLIST_ENABLED && !ALLOWED_JIDS.has(jid)) {
428
+ log("warn", "RT reply blocked by allowlist — not sending to unauthorized JID", { jid, from });
429
+ return;
430
+ }
431
+ const lastReply = lastReplyTime.get(jid) || 0;
432
+ if (Date.now() - lastReply < 2000) {
433
+ // Skip - too soon after last reply (debounce)
434
+ } else {
435
+ lastReplyTime.set(jid, Date.now());
436
+ log("info", "Forwarding crew-lead reply to WhatsApp", { jid, preview: content.slice(0, 80) });
437
+ addToHistory(jid, "assistant", content);
438
+ persistTurn("assistant", content, "crewswarm");
439
+ logMessage({ direction: "outbound", jid, text: content });
440
+ await sendToJid(jid, content);
441
+ }
442
+ } else {
443
+ log("warn", "Reply without valid sessionId - NOT sending to prevent wrong recipient", { rawSessionId, sessionId, from, hasSession: !!activeSessions.has(sessionId) });
444
+ }
445
+ }
446
+ }
447
+ }
448
+ });
449
+
450
+ ws.on("error", (e) => {
451
+ log("error", "RT error", { error: e.message });
452
+ connecting = false;
453
+ if (!ready) reject(e);
454
+ });
455
+
456
+ ws.on("close", (code, reason) => {
457
+ const wasReady = ready;
458
+ ready = false;
459
+ rtClient = null;
460
+ connecting = false;
461
+
462
+ // If code 1000 and reason is "replaced", don't reconnect - we got evicted by our own new connection
463
+ if (code === 1000 && reason && reason.toString().includes("replaced")) {
464
+ log("info", "RT socket evicted by newer connection, not reconnecting");
465
+ reconnectAttempts = 0; // Reset counter on clean replacement
466
+ return;
467
+ }
468
+
469
+ // Exponential backoff: 3s, 6s, 12s, 24s, max 30s
470
+ reconnectAttempts++;
471
+ const backoffMs = Math.min(3000 * Math.pow(2, Math.min(reconnectAttempts - 1, 3)), 30000);
472
+
473
+ log("warn", `RT socket closed (code ${code}), reconnecting in ${backoffMs/1000}s (attempt ${reconnectAttempts})`, { wasReady, reason: reason?.toString() });
474
+
475
+ if (!wasReady) reject(new Error("RT closed before ready"));
476
+
477
+ // Reconnect after exponential backoff delay
478
+ reconnectTimer = setTimeout(() => {
479
+ reconnectTimer = null;
480
+ connectRT(sendToJid).then(c => {
481
+ rtClient = c;
482
+ reconnectAttempts = 0; // Reset on successful connection
483
+ }).catch(() => {});
484
+ }, backoffMs);
485
+ });
486
+ });
487
+ }
488
+
489
+ // ── SSE listener (crew-lead /events) ─────────────────────────────────────────
490
+
491
+ async function listenForAgentReplies(sendToJid) {
492
+ const EVENTS_URL = `${CREW_LEAD_URL}/events`;
493
+ while (true) {
494
+ try {
495
+ const res = await fetch(EVENTS_URL, { signal: AbortSignal.timeout(120000) });
496
+ if (!res.body) { await new Promise(r => setTimeout(r, 5000)); continue; }
497
+ const reader = res.body.getReader();
498
+ const decoder = new TextDecoder();
499
+ let buf = "";
500
+ while (true) {
501
+ const { done, value } = await reader.read();
502
+ if (done) break;
503
+ buf += decoder.decode(value, { stream: true });
504
+ const lines = buf.split("\n");
505
+ buf = lines.pop();
506
+ for (const line of lines) {
507
+ if (!line.startsWith("data: ")) continue;
508
+ try {
509
+ const d = JSON.parse(line.slice(6));
510
+ if (!d.from || !d.content) continue;
511
+ // Route to specific contact if sessionId is "whatsapp-<jid>"
512
+ const whatsappJid = d.sessionId?.startsWith("whatsapp-")
513
+ ? d.sessionId.slice(9) : null;
514
+
515
+ // CRITICAL: Only send to the matched JID, NEVER broadcast to all sessions
516
+ if (!whatsappJid || !activeSessions.has(whatsappJid)) {
517
+ log("warn", "SSE reply without valid sessionId - NOT sending to prevent wrong recipient", {
518
+ sessionId: d.sessionId,
519
+ from: d.from,
520
+ hasSession: whatsappJid ? activeSessions.has(whatsappJid) : false
521
+ });
522
+ continue;
523
+ }
524
+
525
+ const jid = whatsappJid;
526
+ // Allowlist check on outbound — never send to unauthorized JIDs
527
+ if (ALLOWLIST_ENABLED && !ALLOWED_JIDS.has(jid)) {
528
+ log("warn", "SSE reply blocked by allowlist — not sending to unauthorized JID", { jid, from: d.from });
529
+ continue;
530
+ }
531
+ if (shouldSkipDuplicate(jid, d.content)) {
532
+ log("info", "SSE reply already sent via RT path — skipping", { jid, from: d.from });
533
+ continue;
534
+ }
535
+ const preview = d.content.length > 300 ? d.content.slice(0, 300) + "…" : d.content;
536
+ const msg = `✅ *${d.from}* finished:\n${preview}\n\nReply to follow up.`;
537
+ log("info", "Agent reply forwarded to WhatsApp (SSE)", { jid, from: d.from });
538
+ await sendToJid(jid, msg);
539
+ } catch {}
540
+ }
541
+ }
542
+ } catch (e) {
543
+ log("warn", "SSE disconnected, retrying in 5s", { error: e.message });
544
+ await new Promise(r => setTimeout(r, 5000));
545
+ }
546
+ }
547
+ }
548
+
549
+ // ── Slash command handler ─────────────────────────────────────────────────────
550
+
551
+ async function handleCommand(jid, text, sendToJid) {
552
+ const lower = text.toLowerCase().trim();
553
+
554
+ if (lower === "/status") {
555
+ const rtOk = rtClient?.isReady() ? "✅ connected" : "⚠️ disconnected";
556
+ const sessions = [...activeSessions.keys()].length;
557
+ await sendToJid(jid, `*crewswarm WhatsApp Bridge*\n\nRT bus: ${rtOk}\nActive sessions: ${sessions}\nTarget: ${TARGET}\nAllowlist: ${ALLOWLIST_ENABLED ? `${ALLOWED_JIDS.size} numbers` : "open"}`);
558
+ return true;
559
+ }
560
+
561
+ if (lower === "/projects" || lower === "/project") {
562
+ try {
563
+ const projects = await fetchProjects();
564
+ if (!projects.length) {
565
+ await sendToJid(jid, "No projects registered yet. Create one via the dashboard.");
566
+ return true;
567
+ }
568
+ const current = activeProjectByJid.get(jid);
569
+ const lines = projects.map(p => {
570
+ const active = current && current.id === p.id ? " ✅" : "";
571
+ const pct = p.roadmap?.total ? Math.round((p.roadmap.done / p.roadmap.total) * 100) : 0;
572
+ return `• *${p.name}*${active} — ${pct}% done\n /project ${p.name}\n 📁 ${p.outputDir || "?"}`;
573
+ });
574
+ await sendToJid(jid, `*Projects (${projects.length}):*\n\n${lines.join("\n\n")}\n\n_Use /project <name> to set context. /home to clear._`);
575
+ } catch (e) {
576
+ await sendToJid(jid, `⚠️ Could not fetch projects: ${e.message}`);
577
+ }
578
+ return true;
579
+ }
580
+
581
+ if (lower === "/home" || lower === "/project off" || lower === "/project clear") {
582
+ activeProjectByJid.delete(jid);
583
+ await sendToJid(jid, "✅ Back to general mode — no active project.");
584
+ return true;
585
+ }
586
+
587
+ if (lower.startsWith("/project ")) {
588
+ const query = text.slice(9).trim().toLowerCase();
589
+ try {
590
+ const projects = await fetchProjects();
591
+ const match = projects.find(p =>
592
+ p.name.toLowerCase() === query ||
593
+ p.name.toLowerCase().includes(query) ||
594
+ (p.outputDir && p.outputDir.toLowerCase().includes(query))
595
+ );
596
+ if (!match) {
597
+ const names = projects.map(p => ` • ${p.name}`).join("\n");
598
+ await sendToJid(jid, `❌ No project matching "${query}".\n\nAvailable:\n${names || "(none)"}`);
599
+ return true;
600
+ }
601
+ activeProjectByJid.set(jid, { id: match.id, name: match.name, outputDir: match.outputDir });
602
+ await sendToJid(jid, `✅ *${match.name}* is now the active project.\n📁 ${match.outputDir || "?"}\n\nEvery message includes this project's context. Use /home to clear.`);
603
+ } catch (e) {
604
+ await sendToJid(jid, `⚠️ Could not look up projects: ${e.message}`);
605
+ }
606
+ return true;
607
+ }
608
+
609
+ return false;
610
+ }
611
+
612
+ // ── Main — boot Baileys ───────────────────────────────────────────────────────
613
+
614
+ async function main() {
615
+ loadPersistedTurns();
616
+ log("info", `Loaded ${persistedTurns.length} persisted conversation turns`);
617
+
618
+ // Dynamic import of Baileys (ESM-only package)
619
+ const qrTerminal = require("qrcode-terminal");
620
+ const {
621
+ default: makeWASocket,
622
+ useMultiFileAuthState,
623
+ DisconnectReason,
624
+ fetchLatestBaileysVersion,
625
+ Browsers,
626
+ } = await import("@whiskeysockets/baileys");
627
+
628
+ const { state, saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR);
629
+ const { version } = await fetchLatestBaileysVersion();
630
+ log("info", `Baileys version: ${version.join(".")}`);
631
+
632
+ const sock = makeWASocket({
633
+ version,
634
+ auth: state,
635
+ // Show as "Chrome (Mac)" — least suspicious to WA servers
636
+ browser: Browsers.macOS("Chrome"),
637
+ printQRInTerminal: false,
638
+ // Reduce unnecessary reconnects and noise
639
+ syncFullHistory: false,
640
+ markOnlineOnConnect: false,
641
+ generateHighQualityLinkPreview: false,
642
+ logger: {
643
+ level: "silent",
644
+ trace: () => {}, debug: () => {}, info: () => {},
645
+ warn: (o, m) => log("warn", m || String(o)),
646
+ error: (o, m) => log("error", m || String(o)),
647
+ fatal: (o, m) => log("error", `FATAL: ${m || String(o)}`),
648
+ child: () => ({ trace: () => {}, debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, fatal: () => {} }),
649
+ },
650
+ });
651
+
652
+ // ── Send helper ─────────────────────────────────────────────────────────────
653
+
654
+ async function sendToJid(jid, text, agentId = null) {
655
+ if (shouldSkipDuplicate(jid, text)) {
656
+ log("info", "Skipping duplicate", { jid, len: text.length });
657
+ return;
658
+ }
659
+ lastSentByJid.set(jid, { content: text, ts: Date.now() });
660
+
661
+ // Check if TTS is enabled for this user
662
+ const useTTS = isTTSEnabled(jid) && hasTTSProvider();
663
+
664
+ if (useTTS) {
665
+ try {
666
+ const ttsConfig = getTTSConfig();
667
+
668
+ // Get voice for this specific agent (NEW)
669
+ const voiceConfig = getVoiceForAgent(agentId, ttsConfig.voiceMap || {});
670
+
671
+ // Chunk text if too long for TTS (max 5000 chars)
672
+ const chunks = chunkTextForTTS(text, 4500);
673
+
674
+ for (const chunk of chunks) {
675
+ // Convert text to speech with agent-specific voice
676
+ const audioBuffer = await textToSpeech(chunk, {
677
+ provider: voiceConfig.provider,
678
+ voiceId: voiceConfig.voiceId,
679
+ voice: voiceConfig.voice,
680
+ modelId: voiceConfig.modelId
681
+ });
682
+
683
+ // Send as voice message (WhatsApp PTT - Push To Talk)
684
+ await Promise.race([
685
+ sock.sendMessage(jid, {
686
+ audio: audioBuffer,
687
+ mimetype: "audio/mpeg",
688
+ ptt: true // Push-to-talk (voice message)
689
+ }),
690
+ new Promise((_, reject) => setTimeout(() => reject(new Error("sendVoice timeout (20s)")), 20000))
691
+ ]);
692
+
693
+ log("info", "TTS voice sent", { jid, textLength: chunk.length, agentId, voice: voiceConfig.voiceId || voiceConfig.voice });
694
+ }
695
+ return;
696
+ } catch (ttsErr) {
697
+ log("warn", "TTS failed, falling back to text", { error: ttsErr.message, jid });
698
+ // Fall through to text mode
699
+ }
700
+ }
701
+
702
+ // Standard text mode (or TTS fallback)
703
+ const chunks = splitMessage(text);
704
+ for (const chunk of chunks) {
705
+ try {
706
+ // Baileys sock.sendMessage has no built-in timeout — race against a timer
707
+ // so a stale-but-connected socket never freezes the reply path.
708
+ await Promise.race([
709
+ sock.sendMessage(jid, { text: chunk }),
710
+ new Promise((_, reject) => setTimeout(() => reject(new Error("sendMessage timeout (15s)")), 15000)),
711
+ ]);
712
+ } catch (e) {
713
+ log("error", "sendMessage failed", { jid, error: e.message });
714
+ // If socket appears stale, trigger reconnect so next message works
715
+ if (e.message.includes("timeout") || e.message.includes("Connection Closed")) {
716
+ log("warn", "Socket stale — reconnecting", { jid });
717
+ try { await sock.end(new Error("stale socket")); } catch {}
718
+ }
719
+ }
720
+ }
721
+ }
722
+
723
+ // Send location pin (lat/long coordinates)
724
+ async function sendLocation(jid, lat, long, name, address) {
725
+ try {
726
+ await sock.sendMessage(jid, {
727
+ location: {
728
+ degreesLatitude: lat,
729
+ degreesLongitude: long,
730
+ name: name || "",
731
+ address: address || ""
732
+ }
733
+ });
734
+ log("info", "Sent location", { jid, name, lat, long });
735
+ } catch (e) {
736
+ log("error", "sendLocation failed", { jid, error: e.message });
737
+ }
738
+ }
739
+
740
+ // ── Connection updates (QR / connected / disconnected) ─────────────────────
741
+
742
+ sock.ev.on("connection.update", async (update) => {
743
+ const { connection, lastDisconnect, qr } = update;
744
+
745
+ if (qr) {
746
+ console.log("\n🟢 Scan this QR code with WhatsApp on your phone:");
747
+ console.log(" WhatsApp → Linked Devices → Link a Device\n");
748
+ qrTerminal.generate(qr, { small: true });
749
+ console.log("\n (QR code expires in ~60s — restart if it times out)\n");
750
+ }
751
+
752
+ if (connection === "close") {
753
+ const code = lastDisconnect?.error?.output?.statusCode;
754
+ const reason = DisconnectReason[code] || code;
755
+ const shouldReconnect = code !== DisconnectReason.loggedOut;
756
+ log("warn", `Connection closed: ${reason}`, { code, shouldReconnect });
757
+
758
+ if (shouldReconnect) {
759
+ log("info", "Reconnecting in 5s...");
760
+ setTimeout(main, 5000);
761
+ } else {
762
+ log("error", "Logged out — delete ~/.crewswarm/whatsapp-auth/ and re-run to re-authenticate.");
763
+ process.exit(1);
764
+ }
765
+ }
766
+
767
+ if (connection === "open") {
768
+ const jid = sock.user?.id || "unknown";
769
+ console.log(`\n✅ WhatsApp bridge connected`);
770
+ console.log(` Number: ${jid.split(":")[0]}`);
771
+ console.log(` Auth: ${WA_AUTH_DIR}`);
772
+ console.log(` RT: ${RT_URL} (as ${AGENT_NAME})`);
773
+ console.log(` Default route: → ${TARGET}`);
774
+ if (Object.keys(USER_ROUTING).length > 0) {
775
+ console.log(` Per-user routing:`);
776
+ Object.entries(USER_ROUTING).forEach(([num, agent]) => {
777
+ console.log(` ${num} → ${agent}`);
778
+ });
779
+ }
780
+ console.log(` Allowlist: ${ALLOWLIST_ENABLED ? [...ALLOWED_JIDS].join(", ") : "open (any sender)"}\n`);
781
+
782
+ // Connect RT bus and SSE now that WhatsApp is up
783
+ connectRT(sendToJid).then(c => { rtClient = c; }).catch(e => {
784
+ log("warn", "RT unavailable at startup", { error: e.message });
785
+ });
786
+ listenForAgentReplies(sendToJid);
787
+ }
788
+ });
789
+
790
+ // ── Credential save ─────────────────────────────────────────────────────────
791
+
792
+ sock.ev.on("creds.update", saveCreds);
793
+
794
+ // ── Incoming messages ───────────────────────────────────────────────────────
795
+
796
+ sock.ev.on("messages.upsert", async ({ messages, type }) => {
797
+ if (type !== "notify") return;
798
+
799
+ for (const msg of messages) {
800
+ const jid = msg.key.remoteJid;
801
+ if (!jid) continue;
802
+
803
+ // Only handle 1:1 chats (not groups)
804
+ const isGroup = jid.endsWith("@g.us");
805
+ if (isGroup) continue;
806
+
807
+ // WhatsApp multi-device uses @lid (Linked Identity) JIDs for self-chat messages.
808
+ // These arrive as fromMe:true with a @lid suffix — this is the personal bot pattern.
809
+ const isSelfChatLid = msg.key.fromMe && jid.endsWith("@lid");
810
+ const ownJid = sock.user?.id?.split(":")[0] + "@s.whatsapp.net";
811
+ const isSelfChatOwn = msg.key.fromMe && jid === ownJid;
812
+
813
+ // Block outgoing messages that aren't self-chat (i.e. bot's own replies going out)
814
+ if (msg.key.fromMe && !isSelfChatLid && !isSelfChatOwn) continue;
815
+
816
+ // ── Allowlist check (before any media processing) ─────────────────
817
+ if (!isSelfChatLid && !isSelfChatOwn) {
818
+ if (ALLOWLIST_ENABLED && !ALLOWED_JIDS.has(jid)) {
819
+ log("warn", "Silently ignored unauthorized sender", { jid });
820
+ continue;
821
+ }
822
+ }
823
+
824
+ // ── Handle Image Messages ───────────────────────────────────────────
825
+ if (msg.message?.imageMessage && hasVisionProvider()) {
826
+ try {
827
+ const imgMsg = msg.message.imageMessage;
828
+ const caption = imgMsg.caption || "What's in this image? Describe it in detail.";
829
+
830
+ log("info", "Processing image from WhatsApp", { jid, caption });
831
+
832
+ // Download image using Baileys helper
833
+ const { default: makeWASocket, downloadMediaMessage } = require("@whiskeysockets/baileys");
834
+ const buffer = await downloadMediaMessage(msg, "buffer", {});
835
+ const base64 = buffer.toString("base64");
836
+ const dataUri = `data:image/jpeg;base64,${base64}`;
837
+
838
+ await sock.sendMessage(jid, { text: "🖼️ Analyzing image..." });
839
+ const analysis = await analyzeImage(dataUri, caption);
840
+
841
+ // Forward to crew-lead with context
842
+ const displayName = resolveDisplayName(jid, sock);
843
+ const targetAgent = getTargetAgent(jid, sock);
844
+ const fullMessage = `[Image from ${displayName}]\nUser's question: ${caption}\n\nImage analysis:\n${analysis}`;
845
+
846
+ // Track contact
847
+ const phoneNumber = jid.split("@")[0].replace(/\D/g, '').replace(/^1/, '');
848
+ const contactId = `whatsapp:${jid}`;
849
+ trackContact(contactId, 'whatsapp', displayName, { phone: `+${phoneNumber}` });
850
+ saveContactMessage(contactId, 'user', fullMessage);
851
+
852
+ // Add to history
853
+ addToHistory(jid, "user", fullMessage);
854
+ logMessage({ direction: "inbound", jid, text: caption });
855
+
856
+ // Get active project for this JID
857
+ const activeProj = activeProjectByJid.get(jid);
858
+
859
+ fetch(`${CREW_LEAD_URL}/chat`, {
860
+ method: "POST",
861
+ headers: { "content-type": "application/json", ...(RT_TOKEN ? { authorization: `Bearer ${RT_TOKEN}` } : {}) },
862
+ body: JSON.stringify({
863
+ message: fullMessage,
864
+ sessionId: `whatsapp-${jid}`,
865
+ firstName: displayName,
866
+ projectId: activeProj?.id || undefined,
867
+ ...(targetAgent !== "crew-lead" ? { targetAgent } : {}),
868
+ }),
869
+ signal: AbortSignal.timeout(65000),
870
+ }).then(async r => {
871
+ const d = await r.json();
872
+ if (d.reply) {
873
+ addToHistory(jid, "assistant", d.reply);
874
+ await sock.sendMessage(jid, { text: d.reply });
875
+ }
876
+ }).catch(async e => {
877
+ log("error", "crew-lead HTTP error (image)", { error: e.message, targetAgent });
878
+ await sock.sendMessage(jid, { text: `⚠️ Error: ${e.message.slice(0, 100)}` });
879
+ });
880
+ continue;
881
+ } catch (err) {
882
+ log("error", "Image analysis failed", { jid, error: err.message });
883
+ await sock.sendMessage(jid, { text: `⚠️ Image analysis failed: ${err.message}` });
884
+ continue;
885
+ }
886
+ }
887
+
888
+ // ── Handle Voice/Audio Messages ─────────────────────────────────────
889
+ if ((msg.message?.audioMessage) && hasAudioProvider()) {
890
+ try {
891
+ log("info", "Processing voice message from WhatsApp", { jid });
892
+
893
+ // Download audio using Baileys helper
894
+ const { default: makeWASocket, downloadMediaMessage } = require("@whiskeysockets/baileys");
895
+ const buffer = await downloadMediaMessage(msg, "buffer", {});
896
+
897
+ await sock.sendMessage(jid, { text: "🎤 Transcribing voice..." });
898
+ const transcription = await transcribeAudio(buffer);
899
+
900
+ if (!transcription || transcription.trim().length === 0) {
901
+ await sock.sendMessage(jid, { text: "⚠️ Could not transcribe audio (empty result)" });
902
+ continue;
903
+ }
904
+
905
+ log("info", "Voice transcribed", { jid, length: transcription.length });
906
+
907
+ // Forward transcription to crew-lead
908
+ const displayName = resolveDisplayName(jid, sock);
909
+ const targetAgent = getTargetAgent(jid, sock);
910
+ const fullMessage = `[Voice message from ${displayName}]\nTranscription: ${transcription}`;
911
+
912
+ // Track contact
913
+ const phoneNumber = jid.split("@")[0].replace(/\D/g, '').replace(/^1/, '');
914
+ const contactId = `whatsapp:${jid}`;
915
+ trackContact(contactId, 'whatsapp', displayName, { phone: `+${phoneNumber}` });
916
+ saveContactMessage(contactId, 'user', fullMessage);
917
+
918
+ // Add to history
919
+ addToHistory(jid, "user", fullMessage);
920
+ logMessage({ direction: "inbound", jid, text: transcription });
921
+
922
+ // Get active project for this JID
923
+ const activeProj = activeProjectByJid.get(jid);
924
+
925
+ fetch(`${CREW_LEAD_URL}/chat`, {
926
+ method: "POST",
927
+ headers: { "content-type": "application/json", ...(RT_TOKEN ? { authorization: `Bearer ${RT_TOKEN}` } : {}) },
928
+ body: JSON.stringify({
929
+ message: fullMessage,
930
+ sessionId: `whatsapp-${jid}`,
931
+ firstName: displayName,
932
+ projectId: activeProj?.id || undefined,
933
+ ...(targetAgent !== "crew-lead" ? { targetAgent } : {}),
934
+ }),
935
+ signal: AbortSignal.timeout(65000),
936
+ }).then(async r => {
937
+ const d = await r.json();
938
+ if (d.reply) {
939
+ addToHistory(jid, "assistant", d.reply);
940
+ await sock.sendMessage(jid, { text: d.reply });
941
+ }
942
+ }).catch(async e => {
943
+ log("error", "crew-lead HTTP error (voice)", { error: e.message, targetAgent });
944
+ await sock.sendMessage(jid, { text: `⚠️ Error: ${e.message.slice(0, 100)}` });
945
+ });
946
+ continue;
947
+ } catch (err) {
948
+ log("error", "Voice transcription failed", { jid, error: err.message });
949
+ await sock.sendMessage(jid, { text: `⚠️ Voice transcription failed: ${err.message}` });
950
+ continue;
951
+ }
952
+ }
953
+
954
+ // Extract text content
955
+ const text = (
956
+ msg.message?.conversation ||
957
+ msg.message?.extendedTextMessage?.text ||
958
+ msg.message?.buttonsResponseMessage?.selectedDisplayText ||
959
+ ""
960
+ ).trim();
961
+
962
+ if (!text) continue;
963
+
964
+ log("info", "Incoming WhatsApp message", { jid, fromMe: msg.key.fromMe, preview: text.slice(0, 80) });
965
+ logMessage({ direction: "inbound", jid, text });
966
+
967
+ // Track session for reply routing (always reply to the chat JID)
968
+ activeSessions.set(jid, { jid, lastSeen: Date.now() });
969
+
970
+ // Handle slash commands
971
+ if (text.startsWith("/")) {
972
+ const handled = await handleCommand(jid, text, sendToJid);
973
+ if (handled) continue;
974
+ }
975
+
976
+ // History + persistence
977
+ addToHistory(jid, "user", text);
978
+ persistTurn("user", text, resolveDisplayName(jid));
979
+
980
+ const activeProj = activeProjectByJid.get(jid);
981
+
982
+ // Save user message to project RAG
983
+ if (activeProj?.id && activeProj.id !== 'general') {
984
+ saveBridgeMessage(
985
+ 'whatsapp',
986
+ activeProj.id,
987
+ jid,
988
+ 'user',
989
+ text,
990
+ null, // user message
991
+ { phoneNumber: jid.split("@")[0], displayName: resolveDisplayName(jid) }
992
+ );
993
+ }
994
+
995
+ // Resolve display name from address book (falls back to +number)
996
+ // Pass sock so @lid can be resolved to actual phone number
997
+ const displayName = resolveDisplayName(jid, sock);
998
+
999
+ // Extract actual phone number (handle @lid)
1000
+ let phoneNumber = jid.split("@")[0];
1001
+ if (jid.endsWith("@lid") && sock?.user?.id) {
1002
+ phoneNumber = sock.user.id.split(":")[0];
1003
+ }
1004
+
1005
+ // Get the target agent for this specific user (supports per-user routing)
1006
+ const targetAgent = getTargetAgent(jid, sock);
1007
+
1008
+ log("info", "Routing WhatsApp message", {
1009
+ jid,
1010
+ displayName,
1011
+ phoneNumber: `+${phoneNumber}`,
1012
+ targetAgent,
1013
+ hasProject: !!activeProj
1014
+ });
1015
+
1016
+ // FAST PATH: Direct LLM call for non-crew-lead agents (bypasses gateway routing)
1017
+ // crew-lead uses its own chat handler, but other agents (crew-loco, etc.) should
1018
+ // call their LLM directly for instant responses.
1019
+ if (targetAgent !== "crew-lead") {
1020
+ try {
1021
+ const enrichedInput = await enrichTwitterLinks(text, {
1022
+ source: "whatsapp:direct-agent",
1023
+ });
1024
+ // Track contact in universal contacts DB
1025
+ const contactId = `whatsapp:${jid}`;
1026
+ trackContact(contactId, 'whatsapp', displayName, { phone: `+${phoneNumber}` });
1027
+
1028
+ // Save user message to contact history
1029
+ saveContactMessage(contactId, 'user', text);
1030
+
1031
+ // Load contact profile
1032
+ const contact = getContact(contactId);
1033
+
1034
+ // Load agent config
1035
+ const csSwarm = JSON.parse(readFileSync(join(homedir(), ".crewswarm", "crewswarm.json"), "utf8"));
1036
+ const agentCfg = csSwarm.agents.find(a => a.id === targetAgent);
1037
+ if (!agentCfg?.model) {
1038
+ throw new Error(`Agent ${targetAgent} not found or no model configured`);
1039
+ }
1040
+
1041
+ // Parse model string
1042
+ const [providerKey, ...modelParts] = agentCfg.model.split("/");
1043
+ let modelId = modelParts.join("/");
1044
+ const provider = csSwarm.providers?.[providerKey];
1045
+ if (!provider?.apiKey) {
1046
+ throw new Error(`No API key for provider ${providerKey}`);
1047
+ }
1048
+ // OpenRouter requires full ID (e.g. openrouter/hunter-alpha), not bare "hunter-alpha"
1049
+ if ((providerKey === "openrouter" || (provider.baseUrl || "").includes("openrouter.ai")) && modelId && !modelId.startsWith("openrouter/")) {
1050
+ modelId = "openrouter/" + modelId;
1051
+ }
1052
+
1053
+ // Load system prompt
1054
+ const agentPrompts = JSON.parse(readFileSync(join(homedir(), ".crewswarm", "agent-prompts.json"), "utf8"));
1055
+ const bareId = targetAgent.replace(/^crew-/, "");
1056
+ let sysPrompt = agentPrompts[bareId] || `You are ${targetAgent}.`;
1057
+ sysPrompt = applySharedChatPromptOverlay(sysPrompt, targetAgent);
1058
+
1059
+ // CRITICAL: Prepend agent identity so agents know who they are
1060
+ const agentDisplayName = agentCfg.identity?.name || targetAgent;
1061
+ const emoji = agentCfg.identity?.emoji || '🤖';
1062
+ sysPrompt = `# Your Identity\n\nYou are **${agentDisplayName}** (${emoji} ${targetAgent}) — responding in WhatsApp direct chat.\n\n` + sysPrompt;
1063
+
1064
+ // Build intelligent tool instructions
1065
+ const hasEngine = hasEngineConfigured(agentCfg);
1066
+ const permissions = getToolPermissions(targetAgent, agentCfg);
1067
+ const toolInstructions = buildToolInstructions({
1068
+ agentId: targetAgent,
1069
+ permissions,
1070
+ hasEngine,
1071
+ agentConfig: agentCfg // Pass full config to enforce global engine settings
1072
+ });
1073
+
1074
+ sysPrompt += toolInstructions;
1075
+
1076
+ // Inject user preferences into system prompt
1077
+ if (contact?.preferences && Object.keys(contact.preferences).length > 0) {
1078
+ sysPrompt = buildPreferencePrompt(sysPrompt, contact.preferences, displayName);
1079
+ }
1080
+
1081
+ // Inject platform-specific formatting instructions
1082
+ const platformFormatting = getPlatformFormatting('whatsapp');
1083
+ sysPrompt += platformFormatting;
1084
+
1085
+ // Build API request
1086
+ const messages = [
1087
+ { role: "system", content: sysPrompt },
1088
+ ...getHistory(jid).map(h => ({
1089
+ role: h.role,
1090
+ content: h.content,
1091
+ // Inject agent identity for assistant messages
1092
+ ...(h.role === 'assistant' && { name: targetAgent })
1093
+ })),
1094
+ {
1095
+ role: "user",
1096
+ content: enrichedInput.text,
1097
+ ...(displayName && displayName !== "User" && {
1098
+ name: displayName,
1099
+ })
1100
+ }
1101
+ ];
1102
+
1103
+ // Prepend phone number context (phoneNumber already extracted above with @lid handling)
1104
+ messages[messages.length - 1].content = `[From: ${displayName} / +${phoneNumber}]\n${enrichedInput.text}`;
1105
+
1106
+ log("info", "Built message for LLM", {
1107
+ displayName,
1108
+ phoneNumber: `+${phoneNumber}`,
1109
+ hasPreferences: !!(contact?.preferences && Object.keys(contact.preferences).length > 0),
1110
+ nameField: messages[messages.length - 1].name,
1111
+ contentPrefix: messages[messages.length - 1].content.split('\n')[0]
1112
+ });
1113
+
1114
+ // Call LLM directly
1115
+ const response = await fetch(provider.baseUrl + "/chat/completions", {
1116
+ method: "POST",
1117
+ headers: {
1118
+ "content-type": "application/json",
1119
+ "authorization": `Bearer ${provider.apiKey}`,
1120
+ },
1121
+ body: JSON.stringify({
1122
+ model: modelId,
1123
+ messages,
1124
+ temperature: 0.7,
1125
+ }),
1126
+ signal: AbortSignal.timeout(30000),
1127
+ });
1128
+
1129
+ if (!response.ok) {
1130
+ throw new Error(`LLM API returned ${response.status}`);
1131
+ }
1132
+
1133
+ const data = await response.json();
1134
+ let reply = data.choices?.[0]?.message?.content;
1135
+
1136
+ // Execute any @@READ_FILE, @@WRITE_FILE, @@MKDIR tools in the reply
1137
+ const toolResults = await execCrewLeadTools(reply);
1138
+ if (toolResults.length > 0) {
1139
+ // Call LLM again with tool results
1140
+ const toolResultText = toolResults.join("\n\n");
1141
+
1142
+ const followUpMessages = [
1143
+ ...messages,
1144
+ { role: "assistant", content: reply },
1145
+ { role: "user", content: `[Tool execution results]\n\n${toolResultText}\n\nContinue your response based on these results.` }
1146
+ ];
1147
+
1148
+ const followUpRes = await fetch(baseUrl, {
1149
+ method: "POST",
1150
+ headers: {
1151
+ "Content-Type": "application/json",
1152
+ "Authorization": `Bearer ${apiKey}`
1153
+ },
1154
+ body: JSON.stringify({
1155
+ model: modelId,
1156
+ messages: followUpMessages,
1157
+ temperature: 0.7,
1158
+ // Reasoning models (o1/o3/gpt-5 series) don't support max_tokens parameter
1159
+ ...(/^(o1|o3|gpt-5)/i.test(modelId) ? {} : { max_tokens: 2000 })
1160
+ })
1161
+ });
1162
+
1163
+ if (followUpRes.ok) {
1164
+ const followUpData = await followUpRes.json();
1165
+ reply = followUpData.choices?.[0]?.message?.content || reply;
1166
+ }
1167
+ }
1168
+
1169
+ // Check for @@WEB_SEARCH tags and execute them
1170
+ if (reply && reply.includes("@@WEB_SEARCH")) {
1171
+ const searchMatch = reply.match(/@@WEB_SEARCH\s+(.+?)(?=\n|$)/);
1172
+ if (searchMatch) {
1173
+ const query = searchMatch[1].trim();
1174
+ log("info", "Executing @@WEB_SEARCH", { query });
1175
+
1176
+ try {
1177
+ // Call Brave search API (same as crew-lead uses)
1178
+ const braveKey = csSwarm.providers?.brave?.apiKey || process.env.BRAVE_API_KEY;
1179
+ if (braveKey) {
1180
+ const searchRes = await fetch(`https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}`, {
1181
+ headers: { "X-Subscription-Token": braveKey, "Accept": "application/json" },
1182
+ signal: AbortSignal.timeout(10000),
1183
+ });
1184
+ if (searchRes.ok) {
1185
+ const searchData = await searchRes.json();
1186
+ const results = searchData.web?.results?.slice(0, 5) || [];
1187
+ const searchResults = results.map(r => `${r.title}\n${r.description}\n${r.url}`).join("\n\n");
1188
+
1189
+ // Call LLM again with search results
1190
+ const followUpMessages = [
1191
+ ...messages,
1192
+ { role: "assistant", content: reply },
1193
+ { role: "user", content: `[Search results for: ${query}]\n\n${searchResults}\n\nUsing these results, give your final answer. No @@WEB_SEARCH tags.` }
1194
+ ];
1195
+
1196
+ const followUpRes = await fetch(provider.baseUrl + "/chat/completions", {
1197
+ method: "POST",
1198
+ headers: {
1199
+ "content-type": "application/json",
1200
+ "authorization": `Bearer ${provider.apiKey}`,
1201
+ },
1202
+ body: JSON.stringify({
1203
+ model: modelId,
1204
+ messages: followUpMessages,
1205
+ temperature: 0.7,
1206
+ }),
1207
+ signal: AbortSignal.timeout(30000),
1208
+ });
1209
+
1210
+ if (followUpRes.ok) {
1211
+ const followUpData = await followUpRes.json();
1212
+ reply = followUpData.choices?.[0]?.message?.content || reply;
1213
+ }
1214
+ }
1215
+ }
1216
+ } catch (searchErr) {
1217
+ log("warn", "Web search failed", { error: searchErr.message });
1218
+ // Continue with original reply
1219
+ }
1220
+ }
1221
+ }
1222
+
1223
+ // Check for @@WEB_FETCH tags and execute them
1224
+ if (reply && reply.includes("@@WEB_FETCH")) {
1225
+ const fetchMatch = reply.match(/@@WEB_FETCH\s+(https?:\/\/[^\s\n]+)/);
1226
+ if (fetchMatch) {
1227
+ const url = fetchMatch[1].trim();
1228
+ log("info", "Executing @@WEB_FETCH", { url });
1229
+
1230
+ try {
1231
+ const fetchRes = await fetch(url, {
1232
+ headers: { "User-Agent": "crewswarm/1.0" },
1233
+ signal: AbortSignal.timeout(15000),
1234
+ });
1235
+ if (fetchRes.ok) {
1236
+ const html = await fetchRes.text();
1237
+ // Simple text extraction (just strip HTML tags for basic content)
1238
+ const textContent = html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').slice(0, 5000);
1239
+
1240
+ // Call LLM again with fetched content
1241
+ const followUpMessages = [
1242
+ ...messages,
1243
+ { role: "assistant", content: reply },
1244
+ { role: "user", content: `[Content from ${url}]\n\n${textContent}\n\nUsing this content, give your final answer. No @@WEB_FETCH tags.` }
1245
+ ];
1246
+
1247
+ const followUpRes = await fetch(provider.baseUrl + "/chat/completions", {
1248
+ method: "POST",
1249
+ headers: {
1250
+ "content-type": "application/json",
1251
+ "authorization": `Bearer ${provider.apiKey}`,
1252
+ },
1253
+ body: JSON.stringify({
1254
+ model: modelId,
1255
+ messages: followUpMessages,
1256
+ temperature: 0.7,
1257
+ }),
1258
+ signal: AbortSignal.timeout(30000),
1259
+ });
1260
+
1261
+ if (followUpRes.ok) {
1262
+ const followUpData = await followUpRes.json();
1263
+ reply = followUpData.choices?.[0]?.message?.content || reply;
1264
+ }
1265
+ }
1266
+ } catch (fetchErr) {
1267
+ log("warn", "Web fetch failed", { error: fetchErr.message });
1268
+ // Continue with original reply
1269
+ }
1270
+ }
1271
+ }
1272
+
1273
+ // Check for @@CLI execution (new feature - direct CLI calls from agents)
1274
+ const cliMatch = reply.match(/@@CLI\s+(\w+)\s+(.+)/s);
1275
+ if (cliMatch) {
1276
+ const cliName = cliMatch[1].toLowerCase();
1277
+ const task = cliMatch[2].trim();
1278
+ const preText = reply.slice(0, cliMatch.index).trim();
1279
+
1280
+ // Send any text before the CLI call
1281
+ if (preText) {
1282
+ addToHistory(jid, "assistant", preText);
1283
+ persistTurn("assistant", preText, targetAgent);
1284
+ logMessage({ direction: "outbound", jid, text: preText });
1285
+ lastReplyTime.set(jid, Date.now());
1286
+ await sendToJid(jid, preText);
1287
+ }
1288
+
1289
+ // Show "⚡ Working..." message
1290
+ await sendToJid(jid, `⚡ Running ${cliName}...`);
1291
+ log("info", "Agent CLI invocation", { targetAgent, cli: cliName, task: task.slice(0, 100) });
1292
+
1293
+ try {
1294
+ // Dynamic import of CLI executor
1295
+ const { executeCLI } = await import("./lib/bridges/cli-executor.mjs");
1296
+
1297
+ const result = await executeCLI(
1298
+ cliName,
1299
+ task,
1300
+ targetAgent,
1301
+ {
1302
+ jid,
1303
+ sessionId: `whatsapp-${jid}`,
1304
+ projectDir: null
1305
+ },
1306
+ null // No streaming output for now
1307
+ );
1308
+
1309
+ // Send result
1310
+ const output = result.stdout || result.stderr || "(no output)";
1311
+ const resultText = `✅ ${cliName} completed\n\n${output.slice(0, 3000)}${output.length > 3000 ? '\n\n...(truncated)' : ''}`;
1312
+ addToHistory(jid, "assistant", resultText);
1313
+ persistTurn("assistant", resultText, targetAgent);
1314
+ logMessage({ direction: "outbound", jid, text: resultText });
1315
+ lastReplyTime.set(jid, Date.now());
1316
+ await sendToJid(jid, resultText);
1317
+
1318
+ // Save to contacts DB
1319
+ saveContactMessage(contactId, 'assistant', resultText);
1320
+
1321
+ log("info", "CLI execution completed", { targetAgent, cli: cliName, exitCode: result.exitCode });
1322
+ } catch (cliErr) {
1323
+ const errText = `❌ ${cliName} failed: ${cliErr.message}`;
1324
+ addToHistory(jid, "assistant", errText);
1325
+ logMessage({ direction: "outbound", jid, text: errText });
1326
+ await sendToJid(jid, errText);
1327
+ log("error", "CLI execution failed", { targetAgent, cli: cliName, error: cliErr.message });
1328
+ }
1329
+ return;
1330
+ }
1331
+
1332
+ if (reply) {
1333
+ addToHistory(jid, "assistant", reply, targetAgent);
1334
+ persistTurn("assistant", reply, "crewswarm");
1335
+
1336
+ // Save agent reply to project RAG
1337
+ const activeProj = activeProjectByJid.get(jid);
1338
+ if (activeProj?.id && activeProj.id !== 'general') {
1339
+ saveBridgeMessage(
1340
+ 'whatsapp',
1341
+ activeProj.id,
1342
+ jid,
1343
+ 'assistant',
1344
+ reply,
1345
+ targetAgent,
1346
+ { phoneNumber: `+${phoneNumber}`, displayName }
1347
+ );
1348
+ }
1349
+
1350
+ logMessage({ direction: "outbound", jid, text: reply });
1351
+ lastReplyTime.set(jid, Date.now());
1352
+ await sendToJid(jid, reply, targetAgent);
1353
+
1354
+ // Save assistant message to contact history
1355
+ saveContactMessage(contactId, 'assistant', reply);
1356
+
1357
+ // Auto-extract preferences if conditions are met
1358
+ if (shouldExtract(contact.message_count + 1, text)) {
1359
+ log("info", "Extracting preferences", { contactId, messageCount: contact.message_count + 1 });
1360
+
1361
+ // Extract preferences (async, don't block reply)
1362
+ extractPreferences(
1363
+ getHistory(jid),
1364
+ async (msgs) => {
1365
+ // LLM caller wrapper
1366
+ const res = await fetch(provider.baseUrl + "/chat/completions", {
1367
+ method: "POST",
1368
+ headers: {
1369
+ "content-type": "application/json",
1370
+ "authorization": `Bearer ${provider.apiKey}`,
1371
+ },
1372
+ body: JSON.stringify({
1373
+ model: modelId,
1374
+ messages: msgs,
1375
+ temperature: 0.3,
1376
+ }),
1377
+ signal: AbortSignal.timeout(15000),
1378
+ });
1379
+ const data = await res.json();
1380
+ return data.choices?.[0]?.message?.content || '{}';
1381
+ },
1382
+ 'food' // Domain: food preferences (for crew-loco)
1383
+ ).then(prefs => {
1384
+ if (Object.keys(prefs).length > 0) {
1385
+ updatePreferences(contactId, prefs);
1386
+ log("info", "Preferences updated", { contactId, prefs });
1387
+ }
1388
+ }).catch(err => {
1389
+ log("warn", "Preference extraction failed", { error: err.message });
1390
+ });
1391
+ }
1392
+
1393
+ log("info", "Fast LLM reply sent", { agent: targetAgent, model: modelId, replyLength: reply.length });
1394
+ }
1395
+ } catch (e) {
1396
+ log("error", `Direct LLM call to ${targetAgent} failed: ${e.message}`);
1397
+ await sendToJid(jid, `⚠️ ${targetAgent} error: ${e.message.slice(0, 80)}`);
1398
+ }
1399
+ } else {
1400
+ // crew-lead path: use the chat handler
1401
+ fetch(`${CREW_LEAD_URL}/chat`, {
1402
+ method: "POST",
1403
+ headers: { "content-type": "application/json", ...(RT_TOKEN ? { authorization: `Bearer ${RT_TOKEN}` } : {}) },
1404
+ body: JSON.stringify({
1405
+ message: text,
1406
+ sessionId: `whatsapp-${jid}`,
1407
+ firstName: displayName,
1408
+ projectId: activeProj?.id || undefined,
1409
+ }),
1410
+ signal: AbortSignal.timeout(65000),
1411
+ }).then(async r => {
1412
+ const d = await r.json();
1413
+ if (d.reply) {
1414
+ addToHistory(jid, "assistant", d.reply);
1415
+ persistTurn("assistant", d.reply, "crewswarm");
1416
+ logMessage({ direction: "outbound", jid, text: d.reply });
1417
+ lastReplyTime.set(jid, Date.now());
1418
+ await sendToJid(jid, d.reply, "crew-lead");
1419
+ }
1420
+ }).catch(async e => {
1421
+ log("error", "crew-lead HTTP error", { error: e.message });
1422
+ await sendToJid(jid, `⚠️ Error: ${e.message.slice(0, 100)}`);
1423
+ });
1424
+ }
1425
+ }
1426
+ });
1427
+
1428
+ // ── Outbound HTTP API ────────────────────────────────────────────────────────
1429
+ // POST /send { "jid": "15551234567@s.whatsapp.net", "text": "hello" }
1430
+ // POST /send { "phone": "+15551234567", "text": "hello" }
1431
+ // Used by crew-lead @@WHATSAPP tool.
1432
+
1433
+ const httpServer = http.createServer(async (req, res) => {
1434
+ if (req.method === "POST" && req.url === "/send") {
1435
+ let body = "";
1436
+ req.on("data", d => { body += d; });
1437
+ req.on("end", async () => {
1438
+ try {
1439
+ const { jid, phone, text } = JSON.parse(body);
1440
+ if (!text) { res.writeHead(400); res.end(JSON.stringify({ error: "text required" })); return; }
1441
+ let targetJid = jid;
1442
+ if (!targetJid && phone) {
1443
+ targetJid = phone.replace(/^\+/, "").replace(/\D/g, "") + "@s.whatsapp.net";
1444
+ }
1445
+ if (!targetJid) { res.writeHead(400); res.end(JSON.stringify({ error: "jid or phone required" })); return; }
1446
+ // Allowlist check on outbound — never send to unauthorized JIDs
1447
+ if (ALLOWLIST_ENABLED && !ALLOWED_JIDS.has(targetJid)) {
1448
+ log("warn", "HTTP /send blocked by allowlist", { targetJid });
1449
+ res.writeHead(403); res.end(JSON.stringify({ error: "JID not in allowlist" })); return;
1450
+ }
1451
+ await sendToJid(targetJid, text);
1452
+ logMessage({ direction: "outbound", jid: targetJid, text });
1453
+ res.writeHead(200, { "Content-Type": "application/json" });
1454
+ res.end(JSON.stringify({ ok: true, jid: targetJid }));
1455
+ } catch (e) {
1456
+ res.writeHead(500); res.end(JSON.stringify({ error: e.message }));
1457
+ }
1458
+ });
1459
+ return;
1460
+ }
1461
+ if (req.method === "GET" && req.url === "/health") {
1462
+ res.writeHead(200, { "Content-Type": "application/json" });
1463
+ res.end(JSON.stringify({ ok: true, number: sock.user?.id?.split(":")[0] || null }));
1464
+ return;
1465
+ }
1466
+ res.writeHead(404); res.end("Not found");
1467
+ });
1468
+
1469
+ httpServer.on("error", (err) => {
1470
+ if (err.code === "EADDRINUSE") {
1471
+ log("warn", `Port ${HTTP_PORT} already in use — HTTP API disabled. Is another WhatsApp bridge running? Set WA_HTTP_PORT to use a different port.`);
1472
+ } else {
1473
+ log("error", `HTTP server error: ${err.message}`);
1474
+ }
1475
+ });
1476
+ httpServer.listen(HTTP_PORT, "127.0.0.1", () => {
1477
+ log("info", `WhatsApp HTTP API listening on :${HTTP_PORT}`);
1478
+ });
1479
+ }
1480
+
1481
+ main().catch(e => { console.error(e); process.exit(1); });