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.
- package/.env.example +155 -0
- package/LICENSE +21 -0
- package/README.md +316 -0
- package/apps/dashboard/dist/assets/chat-core-BwSoInmZ.js +1 -0
- package/apps/dashboard/dist/assets/chat-core-BwSoInmZ.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +1 -0
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
- package/apps/dashboard/dist/assets/components-CSUb80ze.js +1 -0
- package/apps/dashboard/dist/assets/components-CSUb80ze.js.br +0 -0
- package/apps/dashboard/dist/assets/core-utils-CAVnDoe1.js +1 -0
- package/apps/dashboard/dist/assets/core-utils-CAVnDoe1.js.br +0 -0
- package/apps/dashboard/dist/assets/index-CF0aJRtC.css +1 -0
- package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
- package/apps/dashboard/dist/assets/index-Px49zu76.js +2 -0
- package/apps/dashboard/dist/assets/index-Px49zu76.js.br +0 -0
- package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js +1 -0
- package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
- package/apps/dashboard/dist/assets/setup-wizard-i3eEixlo.js +1 -0
- package/apps/dashboard/dist/assets/setup-wizard-i3eEixlo.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-agents-tab-BThdsdJY.js +1 -0
- package/apps/dashboard/dist/assets/tab-agents-tab-BThdsdJY.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-benchmarks-tab-DfCuAClu.js +1 -0
- package/apps/dashboard/dist/assets/tab-comms-tab-eHpOSBhG.js +1 -0
- package/apps/dashboard/dist/assets/tab-comms-tab-eHpOSBhG.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-contacts-tab-yEegNyO4.js +1 -0
- package/apps/dashboard/dist/assets/tab-contacts-tab-yEegNyO4.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-engines-tab-C3DYxTwy.js +1 -0
- package/apps/dashboard/dist/assets/tab-engines-tab-C3DYxTwy.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-memory-tab-C59BYFQD.js +1 -0
- package/apps/dashboard/dist/assets/tab-memory-tab-C59BYFQD.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-9Ur7pXWA.js +1 -0
- package/apps/dashboard/dist/assets/tab-models-tab-9Ur7pXWA.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-D7mnDelU.js +1 -0
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-D7mnDelU.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-projects-tab-C6h2Mv1K.js +1 -0
- package/apps/dashboard/dist/assets/tab-projects-tab-C6h2Mv1K.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-prompts-tab-C0wZvWK3.js +1 -0
- package/apps/dashboard/dist/assets/tab-prompts-tab-C0wZvWK3.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-services-tab-DBj_w3bc.js +1 -0
- package/apps/dashboard/dist/assets/tab-services-tab-DBj_w3bc.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-ezeqAjZk.js +1 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-ezeqAjZk.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-BYdU2whk.js +1 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-BYdU2whk.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-spending-tab-Bg6w9t_p.js +1 -0
- package/apps/dashboard/dist/assets/tab-spending-tab-Bg6w9t_p.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BBV9HB2X.js +1 -0
- package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BBV9HB2X.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-swarm-tab-ChqLlEVs.js +1 -0
- package/apps/dashboard/dist/assets/tab-swarm-tab-ChqLlEVs.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-usage-tab-B2UWXenJ.js +1 -0
- package/apps/dashboard/dist/assets/tab-usage-tab-B2UWXenJ.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js +1 -0
- package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-workflows-tab-6QSXLJ0i.js +1 -0
- package/apps/dashboard/dist/assets/tab-workflows-tab-6QSXLJ0i.js.br +0 -0
- package/apps/dashboard/dist/favicon.png +0 -0
- package/apps/dashboard/dist/index.html +6466 -0
- package/apps/dashboard/dist/index.html.br +0 -0
- package/apps/dashboard/dist/index.html.gz +0 -0
- package/apps/dashboard/dist/signup.html +446 -0
- package/apps/dashboard/index.html +6442 -0
- package/apps/dashboard/package.json +15 -0
- package/apps/dashboard/src/app.js +2823 -0
- package/apps/dashboard/src/app.js.br +0 -0
- package/apps/dashboard/src/app.js.gz +0 -0
- package/apps/dashboard/src/chat/chat-actions.js +1847 -0
- package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
- package/apps/dashboard/src/chat/unified-messages.js +327 -0
- package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
- package/apps/dashboard/src/cli-process.js +208 -0
- package/apps/dashboard/src/cli-process.js.br +0 -0
- package/apps/dashboard/src/cli-process.js.gz +0 -0
- package/apps/dashboard/src/components/active-tasks-panel.js +175 -0
- package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
- package/apps/dashboard/src/core/api.js +18 -0
- package/apps/dashboard/src/core/api.js.br +0 -0
- package/apps/dashboard/src/core/dom.js +220 -0
- package/apps/dashboard/src/core/dom.js.br +0 -0
- package/apps/dashboard/src/core/state.js +91 -0
- package/apps/dashboard/src/core/state.js.br +0 -0
- package/apps/dashboard/src/core/task-manager.js +134 -0
- package/apps/dashboard/src/core/task-manager.js.br +0 -0
- package/apps/dashboard/src/orchestration-status.js +127 -0
- package/apps/dashboard/src/orchestration-status.js.br +0 -0
- package/apps/dashboard/src/setup-wizard.js +555 -0
- package/apps/dashboard/src/setup-wizard.js.br +0 -0
- package/apps/dashboard/src/styles.css +2085 -0
- package/apps/dashboard/src/styles.css.br +0 -0
- package/apps/dashboard/src/styles.css.gz +0 -0
- package/apps/dashboard/src/tabs/agents-tab.js +2237 -0
- package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/benchmarks-tab.js +229 -0
- package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/comms-tab.js +955 -0
- package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/contacts-tab.js +654 -0
- package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/engines-tab.js +175 -0
- package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/memory-tab.js +182 -0
- package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/models-tab.js +441 -0
- package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/pm-loop-tab.js +185 -0
- package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js +663 -0
- package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
- package/apps/dashboard/src/tabs/prompts-tab.js +160 -0
- package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/services-tab.js +202 -0
- package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/settings-tab.js +803 -0
- package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/skills-tab.js +284 -0
- package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/spending-tab.js +173 -0
- package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-chat-tab.js +660 -0
- package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-tab.js +538 -0
- package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/usage-tab.js +390 -0
- package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/waves-tab.js +238 -0
- package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/workflows-tab.js +747 -0
- package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
- package/apps/vibe/.crew/agent-memory/pipeline.json +249 -0
- package/apps/vibe/.crew/cost.json +17 -0
- package/apps/vibe/.crew/json-parse-metrics.jsonl +22 -0
- package/apps/vibe/.crew/pipeline-metrics.jsonl +22 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +1 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +2 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +5 -0
- package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +5 -0
- package/apps/vibe/.crew/sandbox.json +7 -0
- package/apps/vibe/.crew/session.json +285 -0
- package/apps/vibe/.crew/training-data.jsonl +0 -0
- package/apps/vibe/.github/workflows/studio-quality.yml +37 -0
- package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +12 -0
- package/apps/vibe/.studio-data/project-messages/general.jsonl +54 -0
- package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +10 -0
- package/apps/vibe/ARCHITECTURE.md +3393 -0
- package/apps/vibe/QUICK-REFERENCE.md +211 -0
- package/apps/vibe/README.md +76 -0
- package/apps/vibe/ROADMAP.md +41 -0
- package/apps/vibe/STUDIO-SETUP-COMPLETE.md +35 -0
- package/apps/vibe/VISUAL-GUIDE.md +378 -0
- package/apps/vibe/capture-demo.mjs +160 -0
- package/apps/vibe/capture-vibe-assets.mjs +71 -0
- package/apps/vibe/capture-vibe-video.mjs +260 -0
- package/apps/vibe/check-buttons.js +41 -0
- package/apps/vibe/diagnose.html +106 -0
- package/apps/vibe/fix-buttons.js +103 -0
- package/apps/vibe/index.html +3401 -0
- package/apps/vibe/package-lock.json +920 -0
- package/apps/vibe/package.json +31 -0
- package/apps/vibe/public/favicon.png +0 -0
- package/apps/vibe/scripts/studio-pty-host.py +117 -0
- package/apps/vibe/server.mjs +1835 -0
- package/apps/vibe/src/main.js +2846 -0
- package/apps/vibe/src/register-all-languages.js +98 -0
- package/apps/vibe/start-studio.sh +11 -0
- package/apps/vibe/test/accessibility-tests.js +77 -0
- package/apps/vibe/test/browser-performance-audit.mjs +205 -0
- package/apps/vibe/test/performance-tests.js +120 -0
- package/apps/vibe/test/security-tests.js +213 -0
- package/apps/vibe/tests/e2e.local.mjs +54 -0
- package/apps/vibe/tests/server.smoke.mjs +106 -0
- package/apps/vibe/update_website.mjs +74 -0
- package/apps/vibe/vite.config.js +19 -0
- package/apps/vibe/watch-server.mjs +108 -0
- package/contrib/openclaw-plugin/README.md +199 -0
- package/contrib/openclaw-plugin/index.ts +306 -0
- package/contrib/openclaw-plugin/openclaw.plugin.json +41 -0
- package/contrib/openclaw-plugin/package.json +27 -0
- package/contrib/openclaw-plugin/skills/crewswarm/SKILL.md +88 -0
- package/crew-lead.mjs +649 -0
- package/engines/claude-code.json +36 -0
- package/engines/codex.json +37 -0
- package/engines/crew-cli.json +42 -0
- package/engines/cursor.json +40 -0
- package/engines/docker-sandbox.json +38 -0
- package/engines/gemini-cli.json +75 -0
- package/engines/opencode.json +31 -0
- package/gateway-bridge.mjs +1575 -0
- package/install.sh +738 -0
- package/lib/agent-registry.mjs +232 -0
- package/lib/agents/daemon.mjs +121 -0
- package/lib/agents/dispatch.mjs +225 -0
- package/lib/agents/permissions.mjs +90 -0
- package/lib/agents/platform-formatting.mjs +102 -0
- package/lib/agents/registry.mjs +81 -0
- package/lib/agents/tool-instructions.mjs +257 -0
- package/lib/agents/validation.mjs +75 -0
- package/lib/approval/policy-manager.mjs +221 -0
- package/lib/autoharness/index.mjs +391 -0
- package/lib/bridges/cli-executor.mjs +332 -0
- package/lib/bridges/gateway-ws.mjs +345 -0
- package/lib/bridges/integration.mjs +229 -0
- package/lib/bridges/rag-helper.mjs +90 -0
- package/lib/browser/opencode-passthrough-filter.js +44 -0
- package/lib/browser/passthrough-stderr.js +109 -0
- package/lib/chat/autonomous-mentions.mjs +373 -0
- package/lib/chat/history.mjs +82 -0
- package/lib/chat/mention-routing-intent.mjs +136 -0
- package/lib/chat/participants.mjs +95 -0
- package/lib/chat/project-messages-rag.mjs +265 -0
- package/lib/chat/project-messages.mjs +479 -0
- package/lib/chat/shared-chat-prompt-overlay.mjs +52 -0
- package/lib/chat/thread-binding.mjs +34 -0
- package/lib/chat/unified-history.mjs +223 -0
- package/lib/chat/unified-wrapper.mjs +41 -0
- package/lib/cli-process-tracker.mjs +228 -0
- package/lib/collections/index.mjs +433 -0
- package/lib/contacts/identity-linker.mjs +248 -0
- package/lib/contacts/index.mjs +341 -0
- package/lib/crew-judge/PROMPT.md +93 -0
- package/lib/crew-judge/judge.mjs +260 -0
- package/lib/crew-lead/agent-manager.mjs +125 -0
- package/lib/crew-lead/background.mjs +270 -0
- package/lib/crew-lead/brain.mjs +110 -0
- package/lib/crew-lead/chat-handler.mjs +2603 -0
- package/lib/crew-lead/chat-handler.mjs.bak +1274 -0
- package/lib/crew-lead/classifier.mjs +83 -0
- package/lib/crew-lead/http-server.mjs +4824 -0
- package/lib/crew-lead/intent.mjs +102 -0
- package/lib/crew-lead/interval-manager.mjs +41 -0
- package/lib/crew-lead/llm-caller.mjs +544 -0
- package/lib/crew-lead/prompts.mjs +392 -0
- package/lib/crew-lead/retry-manager.mjs +118 -0
- package/lib/crew-lead/tools.mjs +318 -0
- package/lib/crew-lead/wave-dispatcher.mjs +798 -0
- package/lib/crew-lead/waves-config.json +73 -0
- package/lib/crew-lead/waves-loader.mjs +110 -0
- package/lib/crew-lead/ws-router.mjs +428 -0
- package/lib/dispatch/parsers.mjs +299 -0
- package/lib/domain-planning/detector.mjs +196 -0
- package/lib/domain-planning/prompts/crew-pm-cli.md +96 -0
- package/lib/domain-planning/prompts/crew-pm-core.md +122 -0
- package/lib/domain-planning/prompts/crew-pm-frontend.md +111 -0
- package/lib/engines/crew-cli-sandbox.mjs +422 -0
- package/lib/engines/crew-cli.mjs +155 -0
- package/lib/engines/cursor-launcher.mjs +110 -0
- package/lib/engines/engine-registry.mjs +253 -0
- package/lib/engines/llm-direct.mjs +184 -0
- package/lib/engines/opencode.mjs +256 -0
- package/lib/engines/ouroboros.mjs +114 -0
- package/lib/engines/rt-envelope.mjs +1643 -0
- package/lib/engines/rt-envelope.mjs.backup-current +870 -0
- package/lib/engines/runners.mjs +1367 -0
- package/lib/gemini-cli-passthrough-noise.mjs +37 -0
- package/lib/integrations/code-search.mjs +259 -0
- package/lib/integrations/greptile.mjs +148 -0
- package/lib/integrations/multimodal.mjs +313 -0
- package/lib/integrations/telegram-streaming.mjs +153 -0
- package/lib/integrations/tts.mjs +312 -0
- package/lib/integrations/twitter-links.mjs +294 -0
- package/lib/memory/shared-adapter.mjs +296 -0
- package/lib/pipeline/manager.mjs +539 -0
- package/lib/preferences/extractor.mjs +347 -0
- package/lib/project-dir.mjs +20 -0
- package/lib/runtime/config.mjs +388 -0
- package/lib/runtime/dlq.mjs +170 -0
- package/lib/runtime/log-rotation.mjs +82 -0
- package/lib/runtime/logger.mjs +58 -0
- package/lib/runtime/memory.mjs +421 -0
- package/lib/runtime/paths.mjs +76 -0
- package/lib/runtime/project-dir.mjs +127 -0
- package/lib/runtime/spending.mjs +204 -0
- package/lib/runtime/startup-guard.mjs +291 -0
- package/lib/runtime/task-lease.mjs +234 -0
- package/lib/runtime/telemetry-schema.mjs +208 -0
- package/lib/runtime/telemetry.mjs +101 -0
- package/lib/runtime/utils.mjs +64 -0
- package/lib/skills/index.mjs +265 -0
- package/lib/tools/browser.mjs +135 -0
- package/lib/tools/executor.mjs +913 -0
- package/lib/types.d.ts +57 -0
- package/package.json +106 -0
- package/pm-loop.mjs +1626 -0
- package/prompts/coder-back.md +27 -0
- package/prompts/coder-front.md +27 -0
- package/prompts/coder.md +28 -0
- package/prompts/copywriter.md +17 -0
- package/prompts/fixer.md +39 -0
- package/prompts/frontend.md +23 -0
- package/prompts/github.md +24 -0
- package/prompts/main.md +39 -0
- package/prompts/pm-cli.md +95 -0
- package/prompts/pm-core.md +121 -0
- package/prompts/pm-frontend.md +110 -0
- package/prompts/pm.md +234 -0
- package/prompts/qa.md +44 -0
- package/prompts/security.md +19 -0
- package/scripts/build-crew-chat.sh +28 -0
- package/scripts/build-llms-full.mjs +52 -0
- package/scripts/chatmock-login.sh +16 -0
- package/scripts/chatmock-serve.sh +16 -0
- package/scripts/check-dashboard.mjs +88 -0
- package/scripts/crew-scribe.mjs +326 -0
- package/scripts/dashboard-helpers.mjs +391 -0
- package/scripts/dashboard-validation.mjs +198 -0
- package/scripts/dashboard.mjs +9717 -0
- package/scripts/dlq-replay.mjs +61 -0
- package/scripts/doctor.mjs +196 -0
- package/scripts/file-lock.mjs +186 -0
- package/scripts/fresh-machine-smoke.sh +323 -0
- package/scripts/generate-changelog.mjs +227 -0
- package/scripts/generate-openapi.mjs +334 -0
- package/scripts/health-check.mjs +229 -0
- package/scripts/install-docker.sh +213 -0
- package/scripts/mcp-server.mjs +1625 -0
- package/scripts/opencrew-rt-daemon.mjs +568 -0
- package/scripts/openswitchctl +646 -0
- package/scripts/refactor-configs.mjs +39 -0
- package/scripts/release-check.sh +46 -0
- package/scripts/resolve-node-bin.sh +25 -0
- package/scripts/restart-all-from-repo.sh +329 -0
- package/scripts/restart-crew-lead.sh +98 -0
- package/scripts/restart-dashboard.sh +104 -0
- package/scripts/restart-service.sh +274 -0
- package/scripts/run-accessibility-audit.mjs +356 -0
- package/scripts/run-integration-bounded.mjs +188 -0
- package/scripts/run-scheduled-pipeline.mjs +230 -0
- package/scripts/run.mjs +41 -0
- package/scripts/scan-skills.mjs +79 -0
- package/scripts/setup-firewall.sh +128 -0
- package/scripts/smoke-dispatch.mjs +149 -0
- package/scripts/smoke.sh +163 -0
- package/scripts/start-crew.mjs +328 -0
- package/scripts/start.mjs +146 -0
- package/scripts/swiftbar-restart-service.sh +19 -0
- package/scripts/sync-agents.mjs +152 -0
- package/scripts/sync-prompts.mjs +79 -0
- package/scripts/validate-config.mjs +337 -0
- package/scripts/wow.mjs +89 -0
- package/telegram-bridge.mjs +2421 -0
- package/unified-orchestrator.mjs +519 -0
- 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); });
|