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,1835 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* crewswarm Vibe local server
|
|
4
|
+
*
|
|
5
|
+
* Standalone local mode owns:
|
|
6
|
+
* - project persistence
|
|
7
|
+
* - file listing / read / write
|
|
8
|
+
* - Codex CLI passthrough for local coding
|
|
9
|
+
* - lightweight local chat history
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import http from "node:http";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { WebSocketServer } from "ws";
|
|
19
|
+
import { shouldSkipGeminiPassthroughLine } from "../../lib/gemini-cli-passthrough-noise.mjs";
|
|
20
|
+
import { normalizeProjectDir } from "../../lib/runtime/project-dir.mjs";
|
|
21
|
+
import { resolveCursorLaunchSpec } from "../../lib/engines/cursor-launcher.mjs";
|
|
22
|
+
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const PORT = Number(process.env.STUDIO_PORT || 3333);
|
|
25
|
+
const DIST_DIR = path.join(__dirname, "dist");
|
|
26
|
+
const WORKSPACE_DIR = __dirname;
|
|
27
|
+
const STUDIO_DATA_DIR = process.env.STUDIO_DATA_DIR
|
|
28
|
+
? path.resolve(process.env.STUDIO_DATA_DIR)
|
|
29
|
+
: path.join(__dirname, ".studio-data");
|
|
30
|
+
const MESSAGE_DIR = path.join(STUDIO_DATA_DIR, "project-messages");
|
|
31
|
+
const terminalSessions = new Map();
|
|
32
|
+
const PTY_HOST = path.join(__dirname, "scripts", "studio-pty-host.py");
|
|
33
|
+
const DEFAULT_PROJECT_ID = "studio-local";
|
|
34
|
+
const CREWSWARM_CFG_DIR = path.join(os.homedir(), ".crewswarm");
|
|
35
|
+
const SHARED_PROJECTS_FILE = path.join(CREWSWARM_CFG_DIR, "projects.json");
|
|
36
|
+
const UI_STATE_FILE = path.join(CREWSWARM_CFG_DIR, "ui-state.json");
|
|
37
|
+
const DEFAULT_PROJECT = {
|
|
38
|
+
id: DEFAULT_PROJECT_ID,
|
|
39
|
+
name: "Studio Workspace",
|
|
40
|
+
outputDir: WORKSPACE_DIR,
|
|
41
|
+
description: "Local Studio workspace",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const MIME_TYPES = {
|
|
45
|
+
".html": "text/html",
|
|
46
|
+
".js": "application/javascript",
|
|
47
|
+
".css": "text/css",
|
|
48
|
+
".json": "application/json",
|
|
49
|
+
".png": "image/png",
|
|
50
|
+
".jpg": "image/jpeg",
|
|
51
|
+
".svg": "image/svg+xml",
|
|
52
|
+
".ico": "image/x-icon",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const COMPRESSIBLE_EXTENSIONS = new Set([".html", ".js", ".css", ".json", ".svg"]);
|
|
56
|
+
const WORKSPACE_SCAN_CACHE_TTL_MS = Number(process.env.STUDIO_SCAN_CACHE_TTL_MS || 1_500);
|
|
57
|
+
const workspaceScanCache = new Map();
|
|
58
|
+
const auditFileCache = new Map();
|
|
59
|
+
|
|
60
|
+
export function ensureDataDirs() {
|
|
61
|
+
fs.mkdirSync(STUDIO_DATA_DIR, { recursive: true });
|
|
62
|
+
fs.mkdirSync(MESSAGE_DIR, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function slugify(value = "") {
|
|
66
|
+
return value
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
69
|
+
.replace(/^-+|-+$/g, "")
|
|
70
|
+
.slice(0, 64);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureProjects() {
|
|
74
|
+
ensureDataDirs();
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(fs.readFileSync(SHARED_PROJECTS_FILE, "utf8"));
|
|
77
|
+
const sharedProjects = Array.isArray(parsed)
|
|
78
|
+
? parsed
|
|
79
|
+
: parsed && typeof parsed === "object"
|
|
80
|
+
? Object.values(parsed)
|
|
81
|
+
: [];
|
|
82
|
+
const deduped = sharedProjects.filter(
|
|
83
|
+
(project) => project && project.id && project.id !== DEFAULT_PROJECT_ID,
|
|
84
|
+
);
|
|
85
|
+
return [...deduped, DEFAULT_PROJECT];
|
|
86
|
+
} catch {
|
|
87
|
+
return [DEFAULT_PROJECT];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function readProjects() {
|
|
92
|
+
return ensureProjects();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function writeProjects(projects) {
|
|
96
|
+
ensureDataDirs();
|
|
97
|
+
fs.mkdirSync(CREWSWARM_CFG_DIR, { recursive: true });
|
|
98
|
+
let existing = {};
|
|
99
|
+
try {
|
|
100
|
+
existing = JSON.parse(fs.readFileSync(SHARED_PROJECTS_FILE, "utf8"));
|
|
101
|
+
} catch {
|
|
102
|
+
existing = {};
|
|
103
|
+
}
|
|
104
|
+
const next = { ...(existing && typeof existing === "object" ? existing : {}) };
|
|
105
|
+
for (const project of Array.isArray(projects) ? projects : []) {
|
|
106
|
+
if (!project?.id || project.id === DEFAULT_PROJECT_ID) continue;
|
|
107
|
+
next[project.id] = {
|
|
108
|
+
...(next[project.id] || {}),
|
|
109
|
+
...project,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(SHARED_PROJECTS_FILE, JSON.stringify(next, null, 2));
|
|
113
|
+
clearWorkspaceCaches();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readUiState() {
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(fs.readFileSync(UI_STATE_FILE, "utf8"));
|
|
119
|
+
} catch {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeUiState(nextState = {}) {
|
|
125
|
+
ensureDataDirs();
|
|
126
|
+
fs.mkdirSync(CREWSWARM_CFG_DIR, { recursive: true });
|
|
127
|
+
fs.writeFileSync(UI_STATE_FILE, JSON.stringify(nextState, null, 2));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function clearWorkspaceCaches() {
|
|
131
|
+
workspaceScanCache.clear();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveStudioProjectPath(rawPath, fallback = WORKSPACE_DIR) {
|
|
135
|
+
const normalized = normalizeProjectDir(rawPath);
|
|
136
|
+
if (normalized) return normalized;
|
|
137
|
+
const source =
|
|
138
|
+
rawPath == null || String(rawPath).trim() === "" ? fallback : String(rawPath);
|
|
139
|
+
return path.resolve(source);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getAllowedRoots() {
|
|
143
|
+
const roots = new Set([WORKSPACE_DIR]);
|
|
144
|
+
for (const project of readProjects()) {
|
|
145
|
+
if (project?.outputDir) {
|
|
146
|
+
roots.add(resolveStudioProjectPath(project.outputDir));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return [...roots];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isWithinAllowedRoots(targetPath) {
|
|
153
|
+
const resolved = path.resolve(targetPath);
|
|
154
|
+
return getAllowedRoots().some(
|
|
155
|
+
(root) => resolved === root || resolved.startsWith(`${root}${path.sep}`),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveRequestPath(url = "/") {
|
|
160
|
+
const parsedUrl = new URL(url, "http://127.0.0.1");
|
|
161
|
+
let pathname = decodeURIComponent(parsedUrl.pathname);
|
|
162
|
+
|
|
163
|
+
if (pathname === "/") {
|
|
164
|
+
pathname = "/index.html";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (pathname.startsWith("/dist/")) {
|
|
168
|
+
pathname = pathname.slice("/dist".length);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const relativePath = pathname.replace(/^\/+/, "");
|
|
172
|
+
const distPath = path.join(DIST_DIR, relativePath);
|
|
173
|
+
// Fall back to source directory if file doesn't exist in dist (e.g. unbundled dev mode)
|
|
174
|
+
if (!fs.existsSync(distPath)) {
|
|
175
|
+
const srcPath = path.join(__dirname, relativePath);
|
|
176
|
+
if (fs.existsSync(srcPath)) return srcPath;
|
|
177
|
+
}
|
|
178
|
+
return distPath;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getCacheControlHeader(filePath) {
|
|
182
|
+
const relativePath = path.relative(DIST_DIR, filePath).replace(/\\/g, "/");
|
|
183
|
+
if (relativePath.startsWith("assets/")) {
|
|
184
|
+
return "public, max-age=31536000, immutable";
|
|
185
|
+
}
|
|
186
|
+
return "no-cache";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getEncodedAsset(filePath, acceptEncoding = "") {
|
|
190
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
191
|
+
if (!COMPRESSIBLE_EXTENSIONS.has(ext)) {
|
|
192
|
+
return { filePath, encoding: null };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (acceptEncoding.includes("br")) {
|
|
196
|
+
const brotliPath = `${filePath}.br`;
|
|
197
|
+
if (fs.existsSync(brotliPath)) {
|
|
198
|
+
return { filePath: brotliPath, encoding: "br" };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (acceptEncoding.includes("gzip")) {
|
|
203
|
+
const gzipPath = `${filePath}.gz`;
|
|
204
|
+
if (fs.existsSync(gzipPath)) {
|
|
205
|
+
return { filePath: gzipPath, encoding: "gzip" };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { filePath, encoding: null };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readUtf8FileSyncWithRetry(filePath, attempts = 3) {
|
|
213
|
+
let lastError = null;
|
|
214
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
215
|
+
try {
|
|
216
|
+
return fs.readFileSync(filePath, "utf8");
|
|
217
|
+
} catch (error) {
|
|
218
|
+
lastError = error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
throw lastError;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readFileWithRetry(filePath, attempts = 3, delayMs = 25) {
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
const tryRead = (attempt) => {
|
|
227
|
+
fs.readFile(filePath, (err, content) => {
|
|
228
|
+
if (!err) {
|
|
229
|
+
resolve(content);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (attempt < attempts) {
|
|
233
|
+
setTimeout(() => tryRead(attempt + 1), delayMs);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
reject(err);
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
tryRead(1);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function sendJson(res, statusCode, payload) {
|
|
244
|
+
res.writeHead(statusCode, {
|
|
245
|
+
"Content-Type": "application/json",
|
|
246
|
+
"Access-Control-Allow-Origin": "*",
|
|
247
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
248
|
+
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
|
|
249
|
+
});
|
|
250
|
+
res.end(JSON.stringify(payload));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function readBody(req) {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const chunks = [];
|
|
256
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
257
|
+
req.on("end", () => {
|
|
258
|
+
if (!chunks.length) {
|
|
259
|
+
resolve({});
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
264
|
+
} catch (error) {
|
|
265
|
+
reject(error);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
req.on("error", reject);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function listWorkspaceFiles(scanDir) {
|
|
273
|
+
const resolvedScanDir = path.resolve(scanDir);
|
|
274
|
+
const cached = workspaceScanCache.get(resolvedScanDir);
|
|
275
|
+
if (cached && Date.now() - cached.createdAt < WORKSPACE_SCAN_CACHE_TTL_MS) {
|
|
276
|
+
return cached.files;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const ALLOWED_EXT = new Set([
|
|
280
|
+
".html",
|
|
281
|
+
".css",
|
|
282
|
+
".js",
|
|
283
|
+
".mjs",
|
|
284
|
+
".ts",
|
|
285
|
+
".json",
|
|
286
|
+
".md",
|
|
287
|
+
".sh",
|
|
288
|
+
".txt",
|
|
289
|
+
".yaml",
|
|
290
|
+
".yml",
|
|
291
|
+
]);
|
|
292
|
+
const MAX_FILES = 800;
|
|
293
|
+
const results = [];
|
|
294
|
+
|
|
295
|
+
function walk(dir, depth) {
|
|
296
|
+
if (depth > 6 || results.length >= MAX_FILES) return;
|
|
297
|
+
|
|
298
|
+
let entries;
|
|
299
|
+
try {
|
|
300
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
301
|
+
} catch {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
if (results.length >= MAX_FILES) return;
|
|
307
|
+
if (
|
|
308
|
+
entry.name.startsWith(".") ||
|
|
309
|
+
entry.name === "node_modules" ||
|
|
310
|
+
entry.name === "dist"
|
|
311
|
+
) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const full = path.join(dir, entry.name);
|
|
316
|
+
if (!isWithinAllowedRoots(full)) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (entry.isDirectory()) {
|
|
321
|
+
walk(full, depth + 1);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!entry.isFile()) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
330
|
+
if (!ALLOWED_EXT.has(ext)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
const stat = fs.statSync(full);
|
|
336
|
+
results.push({ path: full, size: stat.size, mtime: stat.mtimeMs });
|
|
337
|
+
} catch {
|
|
338
|
+
// Skip unreadable files.
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
walk(resolvedScanDir, 0);
|
|
344
|
+
results.sort((a, b) => b.mtime - a.mtime);
|
|
345
|
+
workspaceScanCache.set(resolvedScanDir, {
|
|
346
|
+
createdAt: Date.now(),
|
|
347
|
+
files: results,
|
|
348
|
+
});
|
|
349
|
+
return results;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function invalidateWorkspaceScanCache(targetPath) {
|
|
353
|
+
const resolved = path.resolve(targetPath);
|
|
354
|
+
|
|
355
|
+
for (const cache of [workspaceScanCache, auditFileCache]) {
|
|
356
|
+
for (const [root] of cache) {
|
|
357
|
+
if (
|
|
358
|
+
resolved === root ||
|
|
359
|
+
resolved.startsWith(`${root}${path.sep}`) ||
|
|
360
|
+
root.startsWith(`${resolved}${path.sep}`)
|
|
361
|
+
) {
|
|
362
|
+
cache.delete(root);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
function projectMessageFile(projectId) {
|
|
370
|
+
const safeId = slugify(projectId || "general") || "general";
|
|
371
|
+
return path.join(MESSAGE_DIR, `${safeId}.jsonl`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function loadCrewswarmRtToken() {
|
|
375
|
+
const envToken = (process.env.CREWSWARM_RT_AUTH_TOKEN || "").trim();
|
|
376
|
+
if (envToken) return envToken;
|
|
377
|
+
for (const file of [
|
|
378
|
+
path.join(process.env.HOME || "", ".crewswarm", "crewswarm.json"),
|
|
379
|
+
path.join(process.env.HOME || "", ".crewswarm", "config.json"),
|
|
380
|
+
]) {
|
|
381
|
+
try {
|
|
382
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
383
|
+
const token =
|
|
384
|
+
parsed?.rt?.authToken || parsed?.env?.CREWSWARM_RT_AUTH_TOKEN || "";
|
|
385
|
+
if (token) return token;
|
|
386
|
+
} catch {
|
|
387
|
+
// Ignore unreadable local config files.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return "";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function appendProjectMessage(projectId, message) {
|
|
394
|
+
ensureDataDirs();
|
|
395
|
+
fs.appendFileSync(projectMessageFile(projectId), `${JSON.stringify(message)}\n`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function readProjectMessages(projectId, limit = 50) {
|
|
399
|
+
const file = projectMessageFile(projectId);
|
|
400
|
+
if (!fs.existsSync(file)) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let lines;
|
|
405
|
+
try {
|
|
406
|
+
lines = readUtf8FileSyncWithRetry(file)
|
|
407
|
+
.split("\n")
|
|
408
|
+
.map((line) => line.trim())
|
|
409
|
+
.filter(Boolean);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.warn(
|
|
412
|
+
`[vibe] Failed to read project messages for ${projectId}: ${error.message}`,
|
|
413
|
+
);
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return lines
|
|
418
|
+
.slice(-Math.max(1, Math.min(Number(limit) || 50, 200)))
|
|
419
|
+
.map((line) => {
|
|
420
|
+
try {
|
|
421
|
+
return JSON.parse(line);
|
|
422
|
+
} catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
.filter(Boolean);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function sendSseHeaders(res) {
|
|
430
|
+
if (res.headersSent || res.writableEnded) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
res.writeHead(200, {
|
|
434
|
+
"Content-Type": "text/event-stream",
|
|
435
|
+
"Cache-Control": "no-cache",
|
|
436
|
+
Connection: "keep-alive",
|
|
437
|
+
"Access-Control-Allow-Origin": "*",
|
|
438
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
439
|
+
"Access-Control-Allow-Methods": "GET,POST,OPTIONS",
|
|
440
|
+
});
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function sendSseEvent(res, payload) {
|
|
445
|
+
if (res.writableEnded) return;
|
|
446
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function randomSessionId(prefix) {
|
|
450
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function getTerminalCommand() {
|
|
454
|
+
const binary =
|
|
455
|
+
process.env.STUDIO_SHELL_BIN ||
|
|
456
|
+
resolveExistingBinary([
|
|
457
|
+
process.env.SHELL,
|
|
458
|
+
"/bin/zsh",
|
|
459
|
+
"/bin/bash",
|
|
460
|
+
"/bin/sh",
|
|
461
|
+
]) ||
|
|
462
|
+
"/bin/sh";
|
|
463
|
+
const argsJson = process.env.STUDIO_SHELL_ARGS_JSON;
|
|
464
|
+
const argsRaw = process.env.STUDIO_SHELL_ARGS;
|
|
465
|
+
let parsedArgs = [];
|
|
466
|
+
if (argsJson) {
|
|
467
|
+
try {
|
|
468
|
+
const parsed = JSON.parse(argsJson);
|
|
469
|
+
if (Array.isArray(parsed)) {
|
|
470
|
+
parsedArgs = parsed.map((value) => String(value));
|
|
471
|
+
}
|
|
472
|
+
} catch {}
|
|
473
|
+
}
|
|
474
|
+
if (!parsedArgs.length) {
|
|
475
|
+
parsedArgs = (argsRaw || "")
|
|
476
|
+
.split(" ")
|
|
477
|
+
.map((value) => value.trim())
|
|
478
|
+
.filter(Boolean);
|
|
479
|
+
}
|
|
480
|
+
const args = parsedArgs.length ? parsedArgs : ["-i"];
|
|
481
|
+
const resolvedArgs = args.map((arg, index) => {
|
|
482
|
+
if (
|
|
483
|
+
process.env.STUDIO_SHELL_BIN &&
|
|
484
|
+
index === 0 &&
|
|
485
|
+
(arg.endsWith(".js") || arg.endsWith(".mjs")) &&
|
|
486
|
+
!path.isAbsolute(arg)
|
|
487
|
+
) {
|
|
488
|
+
return path.join(WORKSPACE_DIR, arg);
|
|
489
|
+
}
|
|
490
|
+
return arg;
|
|
491
|
+
});
|
|
492
|
+
return { command: binary, args: resolvedArgs };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function resolveExistingBinary(candidates = []) {
|
|
496
|
+
for (const candidate of candidates) {
|
|
497
|
+
if (candidate && candidate.includes("/") && fs.existsSync(candidate)) {
|
|
498
|
+
return candidate;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return "";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function resolvePythonBinary() {
|
|
505
|
+
return (
|
|
506
|
+
resolveExistingBinary([
|
|
507
|
+
process.env.STUDIO_PYTHON_BIN,
|
|
508
|
+
process.env.PYTHON,
|
|
509
|
+
"/usr/local/bin/python3",
|
|
510
|
+
"/opt/homebrew/bin/python3",
|
|
511
|
+
"/usr/bin/python3",
|
|
512
|
+
]) || "python3"
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function normalizeTerminalSize(value, fallback) {
|
|
517
|
+
const parsed = Number.parseInt(String(value || ""), 10);
|
|
518
|
+
if (!Number.isFinite(parsed)) {
|
|
519
|
+
return fallback;
|
|
520
|
+
}
|
|
521
|
+
return Math.max(1, parsed);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function getCursorCommand() {
|
|
525
|
+
const configuredBinary = process.env.STUDIO_CURSOR_BIN || process.env.CURSOR_CLI_BIN;
|
|
526
|
+
return resolveCursorLaunchSpec(configuredBinary);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Same defaults as crew-lead / gateway `runCursorCliTask` (not the IDE `cursor` opener). */
|
|
530
|
+
function resolveStudioCursorModel(bodyModel) {
|
|
531
|
+
const cursorDefault =
|
|
532
|
+
process.env.CREWSWARM_CURSOR_MODEL ||
|
|
533
|
+
process.env.CURSOR_DEFAULT_MODEL ||
|
|
534
|
+
"composer-2-fast";
|
|
535
|
+
let m =
|
|
536
|
+
(bodyModel && String(bodyModel).trim()) ||
|
|
537
|
+
process.env.STUDIO_CURSOR_MODEL ||
|
|
538
|
+
cursorDefault;
|
|
539
|
+
if (String(m).includes("/")) m = cursorDefault;
|
|
540
|
+
else if (String(m).includes("sonnet-4.6")) m = "sonnet-4.5";
|
|
541
|
+
return m;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function createCursorStreamRelay(onChunk, onTerminal) {
|
|
545
|
+
let transcript = "";
|
|
546
|
+
let lineBuffer = "";
|
|
547
|
+
let sawAssistantDelta = false;
|
|
548
|
+
let lastAssistantNorm = "";
|
|
549
|
+
|
|
550
|
+
const appendText = (text) => {
|
|
551
|
+
if (!text) return;
|
|
552
|
+
transcript += text;
|
|
553
|
+
onChunk?.(text);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const handleLine = (line) => {
|
|
557
|
+
const trimmed = line.trim();
|
|
558
|
+
if (!trimmed) return;
|
|
559
|
+
|
|
560
|
+
try {
|
|
561
|
+
const event = JSON.parse(trimmed);
|
|
562
|
+
if (event.type === "stream_event" && event.event?.type === "content_block_delta") {
|
|
563
|
+
const t = event.event.delta?.text || "";
|
|
564
|
+
if (t) {
|
|
565
|
+
sawAssistantDelta = true;
|
|
566
|
+
appendText(t);
|
|
567
|
+
}
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (event.type === "assistant") {
|
|
571
|
+
const content = event.message?.content;
|
|
572
|
+
let combined = "";
|
|
573
|
+
if (Array.isArray(content)) {
|
|
574
|
+
for (const chunk of content) {
|
|
575
|
+
if (chunk?.type === "text" && chunk.text) combined += chunk.text;
|
|
576
|
+
}
|
|
577
|
+
} else if (typeof content === "string") {
|
|
578
|
+
combined = content;
|
|
579
|
+
}
|
|
580
|
+
const norm = combined.replace(/\r/g, "").trim();
|
|
581
|
+
if (norm && norm === lastAssistantNorm) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (norm) lastAssistantNorm = norm;
|
|
585
|
+
if (!sawAssistantDelta && combined) {
|
|
586
|
+
if (Array.isArray(content)) {
|
|
587
|
+
for (const chunk of content) {
|
|
588
|
+
if (chunk?.type === "text" && chunk.text) appendText(chunk.text);
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
appendText(combined);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (event.type === "result") {
|
|
598
|
+
if (!transcript.trim() && (event.result || event.text)) {
|
|
599
|
+
appendText(String(event.result || event.text || ""));
|
|
600
|
+
}
|
|
601
|
+
onTerminal?.();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (event.type === "error") {
|
|
606
|
+
appendText(`${event.message || trimmed}\n`);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
appendText(`${line}\n`);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
push(chunk) {
|
|
616
|
+
lineBuffer += chunk.toString("utf8");
|
|
617
|
+
while (lineBuffer.includes("\n")) {
|
|
618
|
+
const newlineIndex = lineBuffer.indexOf("\n");
|
|
619
|
+
const line = lineBuffer.slice(0, newlineIndex);
|
|
620
|
+
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
621
|
+
handleLine(line);
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
finish() {
|
|
625
|
+
if (lineBuffer.trim()) {
|
|
626
|
+
handleLine(lineBuffer);
|
|
627
|
+
lineBuffer = "";
|
|
628
|
+
}
|
|
629
|
+
return transcript.trim();
|
|
630
|
+
},
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function stripAnsi(text) {
|
|
635
|
+
return String(text || "").replace(/\u001B\[[0-9;]*[A-Za-z]/g, "");
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function appendNormalizedChunk(current, next) {
|
|
639
|
+
const incoming = String(next || "").replace(/\r/g, "").trimEnd();
|
|
640
|
+
if (!incoming) return current;
|
|
641
|
+
return current ? `${current}\n${incoming}` : incoming;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function summarizeCliFailure(engine, rawTranscript) {
|
|
645
|
+
const lines = stripAnsi(rawTranscript)
|
|
646
|
+
.split(/\r?\n/)
|
|
647
|
+
.map((line) => line.trim())
|
|
648
|
+
.filter(Boolean);
|
|
649
|
+
|
|
650
|
+
const candidates = lines.filter((line) =>
|
|
651
|
+
/error|failed|fatal|transport channel closed|handshaking/i.test(line),
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
const selected = (candidates.length > 0 ? candidates : lines).slice(-3);
|
|
655
|
+
if (selected.length === 0) {
|
|
656
|
+
return `${engine} exited without producing a response.`;
|
|
657
|
+
}
|
|
658
|
+
return selected.join("\n");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function createCodexStreamRelay(onChunk) {
|
|
662
|
+
let transcript = "";
|
|
663
|
+
let rawTranscript = "";
|
|
664
|
+
let lineBuffer = "";
|
|
665
|
+
let collectingAssistant = false;
|
|
666
|
+
let stopCollection = false;
|
|
667
|
+
|
|
668
|
+
const appendAssistant = (text) => {
|
|
669
|
+
const normalized = String(text || "").replace(/\r/g, "").trimEnd();
|
|
670
|
+
if (!normalized) return;
|
|
671
|
+
transcript = appendNormalizedChunk(transcript, normalized);
|
|
672
|
+
onChunk?.(`${normalized}\n`);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const isNoiseLine = (line) => {
|
|
676
|
+
const trimmed = line.trim();
|
|
677
|
+
return (
|
|
678
|
+
!trimmed ||
|
|
679
|
+
trimmed === "--------" ||
|
|
680
|
+
trimmed === "user" ||
|
|
681
|
+
trimmed.startsWith("mcp:") ||
|
|
682
|
+
trimmed.startsWith("OpenAI Codex ") ||
|
|
683
|
+
trimmed.startsWith("workdir:") ||
|
|
684
|
+
trimmed.startsWith("model:") ||
|
|
685
|
+
trimmed.startsWith("provider:") ||
|
|
686
|
+
trimmed.startsWith("approval:") ||
|
|
687
|
+
trimmed.startsWith("sandbox:") ||
|
|
688
|
+
trimmed.startsWith("reasoning effort:") ||
|
|
689
|
+
trimmed.startsWith("reasoning summaries:") ||
|
|
690
|
+
trimmed.startsWith("session id:") ||
|
|
691
|
+
/^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*\b(ERROR|WARN|INFO)\b/.test(trimmed) ||
|
|
692
|
+
/rmcp::/i.test(trimmed) ||
|
|
693
|
+
/ERROR rmcp::transport::worker/.test(trimmed) ||
|
|
694
|
+
/mcp startup: failed/.test(trimmed) ||
|
|
695
|
+
(/\/mcp/i.test(trimmed) &&
|
|
696
|
+
/127\.0\.0\.1:\d+|localhost:\d+/i.test(trimmed) &&
|
|
697
|
+
/Connection refused|ConnectError|Transport channel closed|tcp connect error/i.test(
|
|
698
|
+
trimmed,
|
|
699
|
+
))
|
|
700
|
+
);
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const handleLine = (line) => {
|
|
704
|
+
const cleaned = stripAnsi(line).replace(/\r/g, "");
|
|
705
|
+
rawTranscript = appendNormalizedChunk(rawTranscript, cleaned);
|
|
706
|
+
const trimmed = cleaned.trim();
|
|
707
|
+
if (!trimmed) return;
|
|
708
|
+
|
|
709
|
+
if (/^tokens used$/i.test(trimmed)) {
|
|
710
|
+
stopCollection = true;
|
|
711
|
+
collectingAssistant = false;
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (trimmed === "codex") {
|
|
716
|
+
collectingAssistant = true;
|
|
717
|
+
stopCollection = false;
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (stopCollection || isNoiseLine(trimmed)) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (collectingAssistant) {
|
|
726
|
+
appendAssistant(trimmed);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
push(chunk) {
|
|
732
|
+
lineBuffer += chunk.toString("utf8");
|
|
733
|
+
while (lineBuffer.includes("\n")) {
|
|
734
|
+
const newlineIndex = lineBuffer.indexOf("\n");
|
|
735
|
+
const line = lineBuffer.slice(0, newlineIndex);
|
|
736
|
+
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
737
|
+
handleLine(line);
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
finish() {
|
|
741
|
+
if (lineBuffer.trim()) {
|
|
742
|
+
handleLine(lineBuffer);
|
|
743
|
+
lineBuffer = "";
|
|
744
|
+
}
|
|
745
|
+
return transcript.trim() || summarizeCliFailure("codex", rawTranscript);
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function createCrewCliStreamRelay(onChunk) {
|
|
751
|
+
let transcript = "";
|
|
752
|
+
let rawTranscript = "";
|
|
753
|
+
let lineBuffer = "";
|
|
754
|
+
let collectingAssistant = false;
|
|
755
|
+
|
|
756
|
+
const appendAssistant = (text) => {
|
|
757
|
+
const normalized = String(text || "").replace(/\r/g, "").trimEnd();
|
|
758
|
+
if (!normalized) return;
|
|
759
|
+
transcript = appendNormalizedChunk(transcript, normalized);
|
|
760
|
+
onChunk?.(`${normalized}\n`);
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const handleLine = (line) => {
|
|
764
|
+
const cleaned = stripAnsi(line).replace(/\r/g, "");
|
|
765
|
+
rawTranscript = appendNormalizedChunk(rawTranscript, cleaned);
|
|
766
|
+
const trimmed = cleaned.trim();
|
|
767
|
+
if (!trimmed) {
|
|
768
|
+
if (collectingAssistant && transcript) {
|
|
769
|
+
appendAssistant("");
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (trimmed === "--- Agent Response ---") {
|
|
775
|
+
collectingAssistant = true;
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (trimmed === "Pipeline timeline:") {
|
|
780
|
+
collectingAssistant = false;
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (collectingAssistant) {
|
|
785
|
+
appendAssistant(cleaned);
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
push(chunk) {
|
|
791
|
+
lineBuffer += chunk.toString("utf8");
|
|
792
|
+
while (lineBuffer.includes("\n")) {
|
|
793
|
+
const newlineIndex = lineBuffer.indexOf("\n");
|
|
794
|
+
const line = lineBuffer.slice(0, newlineIndex);
|
|
795
|
+
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
796
|
+
handleLine(line);
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
finish() {
|
|
800
|
+
if (lineBuffer.trim()) {
|
|
801
|
+
handleLine(lineBuffer);
|
|
802
|
+
lineBuffer = "";
|
|
803
|
+
}
|
|
804
|
+
return transcript.trim() || summarizeCliFailure("crew-cli", rawTranscript);
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function createGeminiStreamRelay(onChunk, onDone) {
|
|
810
|
+
let transcript = "";
|
|
811
|
+
let rawTranscript = "";
|
|
812
|
+
let lineBuffer = "";
|
|
813
|
+
|
|
814
|
+
const appendAssistant = (text) => {
|
|
815
|
+
const normalized = String(text || "").replace(/\r/g, "").trimEnd();
|
|
816
|
+
if (!normalized) return;
|
|
817
|
+
transcript = appendNormalizedChunk(transcript, normalized);
|
|
818
|
+
onChunk?.(`${normalized}\n`);
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const handleLine = (line) => {
|
|
822
|
+
const cleaned = stripAnsi(line).replace(/\r/g, "");
|
|
823
|
+
rawTranscript = appendNormalizedChunk(rawTranscript, cleaned);
|
|
824
|
+
const trimmed = cleaned.trim();
|
|
825
|
+
if (!trimmed) return;
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
const event = JSON.parse(trimmed);
|
|
829
|
+
if (event.type === "message" && event.role === "assistant" && event.content) {
|
|
830
|
+
appendAssistant(event.content);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (event.type === "result") {
|
|
834
|
+
onDone?.();
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (event.type === "error") {
|
|
838
|
+
appendAssistant(event.message || trimmed);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
} catch {
|
|
842
|
+
// Fall through so plain-text stderr still surfaces.
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (!trimmed.startsWith("{")) {
|
|
846
|
+
if (shouldSkipGeminiPassthroughLine(trimmed)) return;
|
|
847
|
+
appendAssistant(trimmed);
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
return {
|
|
852
|
+
push(chunk) {
|
|
853
|
+
lineBuffer += chunk.toString("utf8");
|
|
854
|
+
while (lineBuffer.includes("\n")) {
|
|
855
|
+
const newlineIndex = lineBuffer.indexOf("\n");
|
|
856
|
+
const line = lineBuffer.slice(0, newlineIndex);
|
|
857
|
+
lineBuffer = lineBuffer.slice(newlineIndex + 1);
|
|
858
|
+
handleLine(line);
|
|
859
|
+
}
|
|
860
|
+
},
|
|
861
|
+
finish() {
|
|
862
|
+
if (lineBuffer.trim()) {
|
|
863
|
+
handleLine(lineBuffer);
|
|
864
|
+
lineBuffer = "";
|
|
865
|
+
}
|
|
866
|
+
return transcript.trim() || summarizeCliFailure("gemini", rawTranscript);
|
|
867
|
+
},
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function createDefaultCliRelay(onChunk) {
|
|
872
|
+
let transcript = "";
|
|
873
|
+
|
|
874
|
+
return {
|
|
875
|
+
push(chunk) {
|
|
876
|
+
const text = chunk.toString("utf8");
|
|
877
|
+
transcript += text;
|
|
878
|
+
onChunk?.(text);
|
|
879
|
+
},
|
|
880
|
+
finish() {
|
|
881
|
+
return transcript.trim();
|
|
882
|
+
},
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function createCliRelay(engine, onChunk, onDone) {
|
|
887
|
+
if (engine === "cursor") {
|
|
888
|
+
return createCursorStreamRelay(onChunk, onDone);
|
|
889
|
+
}
|
|
890
|
+
if (engine === "codex") {
|
|
891
|
+
return createCodexStreamRelay(onChunk);
|
|
892
|
+
}
|
|
893
|
+
if (engine === "gemini") {
|
|
894
|
+
return createGeminiStreamRelay(onChunk, onDone);
|
|
895
|
+
}
|
|
896
|
+
if (engine === "crew-cli") {
|
|
897
|
+
return createCrewCliStreamRelay(onChunk);
|
|
898
|
+
}
|
|
899
|
+
return createDefaultCliRelay(onChunk);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function broadcastTerminalMessage(sessionId, payload) {
|
|
903
|
+
const session = terminalSessions.get(sessionId);
|
|
904
|
+
if (!session) return;
|
|
905
|
+
const message = JSON.stringify(payload);
|
|
906
|
+
for (const socket of session.sockets) {
|
|
907
|
+
if (socket.readyState === 1) {
|
|
908
|
+
socket.send(message);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export function createTerminalSession({ projectDir, onData, onExit, cols, rows }) {
|
|
914
|
+
const cwd = resolveStudioProjectPath(projectDir, WORKSPACE_DIR);
|
|
915
|
+
if (!isWithinAllowedRoots(cwd)) {
|
|
916
|
+
throw new Error("projectDir is outside configured project roots");
|
|
917
|
+
}
|
|
918
|
+
if (!fs.existsSync(PTY_HOST)) {
|
|
919
|
+
throw new Error(`terminal host missing at ${PTY_HOST}`);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const sessionId = randomSessionId("terminal");
|
|
923
|
+
const { command, args } = getTerminalCommand();
|
|
924
|
+
const pythonBin = resolvePythonBinary();
|
|
925
|
+
const initialCols = normalizeTerminalSize(cols, 120);
|
|
926
|
+
const initialRows = normalizeTerminalSize(rows, 32);
|
|
927
|
+
const child = spawn(pythonBin, [PTY_HOST, cwd, command, ...args], {
|
|
928
|
+
cwd,
|
|
929
|
+
env: {
|
|
930
|
+
...process.env,
|
|
931
|
+
TERM: process.env.TERM || "xterm-256color",
|
|
932
|
+
FORCE_COLOR: "0",
|
|
933
|
+
HISTFILE: process.env.HISTFILE || "/dev/null",
|
|
934
|
+
// Avoid macOS zsh session persistence writes in sandboxed terminals.
|
|
935
|
+
SHELL_SESSIONS_DISABLE: process.env.SHELL_SESSIONS_DISABLE || "1",
|
|
936
|
+
STUDIO_TERM_COLS: String(initialCols),
|
|
937
|
+
STUDIO_TERM_ROWS: String(initialRows),
|
|
938
|
+
},
|
|
939
|
+
stdio: ["pipe", "pipe", "pipe", "pipe"],
|
|
940
|
+
});
|
|
941
|
+
const session = {
|
|
942
|
+
id: sessionId,
|
|
943
|
+
child,
|
|
944
|
+
control: child.stdio[3],
|
|
945
|
+
cwd,
|
|
946
|
+
cols: initialCols,
|
|
947
|
+
rows: initialRows,
|
|
948
|
+
sockets: new Set(),
|
|
949
|
+
};
|
|
950
|
+
terminalSessions.set(sessionId, session);
|
|
951
|
+
|
|
952
|
+
const relay = (chunk) => {
|
|
953
|
+
onData?.(chunk.toString("utf8"));
|
|
954
|
+
broadcastTerminalMessage(sessionId, {
|
|
955
|
+
type: "output",
|
|
956
|
+
data: chunk.toString("utf8"),
|
|
957
|
+
});
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
child.stdout.on("data", relay);
|
|
961
|
+
child.stderr.on("data", relay);
|
|
962
|
+
child.on("error", (error) => {
|
|
963
|
+
broadcastTerminalMessage(sessionId, { type: "error", message: error.message });
|
|
964
|
+
});
|
|
965
|
+
child.on("close", (exitCode) => {
|
|
966
|
+
const normalizedExitCode = Number(exitCode ?? 0);
|
|
967
|
+
onExit?.(normalizedExitCode);
|
|
968
|
+
broadcastTerminalMessage(sessionId, { type: "exit", exitCode: normalizedExitCode });
|
|
969
|
+
for (const socket of session.sockets) {
|
|
970
|
+
socket.close();
|
|
971
|
+
}
|
|
972
|
+
terminalSessions.delete(sessionId);
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
return { sessionId, cwd };
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
export function writeTerminalSession(sessionId, data) {
|
|
979
|
+
const session = terminalSessions.get(sessionId);
|
|
980
|
+
if (!session) {
|
|
981
|
+
throw new Error("terminal session not found");
|
|
982
|
+
}
|
|
983
|
+
session.child.stdin.write(data);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
export function resizeTerminalSession(sessionId, cols, rows) {
|
|
987
|
+
const session = terminalSessions.get(sessionId);
|
|
988
|
+
if (!session) {
|
|
989
|
+
throw new Error("terminal session not found");
|
|
990
|
+
}
|
|
991
|
+
const nextCols = normalizeTerminalSize(cols, session.cols || 120);
|
|
992
|
+
const nextRows = normalizeTerminalSize(rows, session.rows || 32);
|
|
993
|
+
session.cols = nextCols;
|
|
994
|
+
session.rows = nextRows;
|
|
995
|
+
session.control?.write(`${JSON.stringify({ type: "resize", cols: nextCols, rows: nextRows })}\n`);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
export function closeTerminalSession(sessionId) {
|
|
999
|
+
const session = terminalSessions.get(sessionId);
|
|
1000
|
+
if (!session) return false;
|
|
1001
|
+
session.control?.write(`${JSON.stringify({ type: "close" })}\n`);
|
|
1002
|
+
session.child.kill("SIGTERM");
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const SUPPORTED_ENGINES = ["codex", "claude", "cursor", "gemini", "opencode", "crew-cli"];
|
|
1007
|
+
|
|
1008
|
+
export function getCliCommand(engine, projectDir, message, modelOverride) {
|
|
1009
|
+
switch (engine) {
|
|
1010
|
+
case "codex": {
|
|
1011
|
+
const binary = process.env.STUDIO_CODEX_BIN || "codex";
|
|
1012
|
+
const model = modelOverride || process.env.CREWSWARM_CODEX_MODEL || "";
|
|
1013
|
+
const prefixArgs = (process.env.STUDIO_CODEX_BIN_ARGS || "")
|
|
1014
|
+
.split(" ")
|
|
1015
|
+
.map((value) => value.trim())
|
|
1016
|
+
.filter(Boolean);
|
|
1017
|
+
const resolvedPrefixArgs = prefixArgs.map((arg, index) => {
|
|
1018
|
+
if (
|
|
1019
|
+
index === 0 &&
|
|
1020
|
+
(arg.endsWith(".js") || arg.endsWith(".mjs")) &&
|
|
1021
|
+
!path.isAbsolute(arg)
|
|
1022
|
+
) {
|
|
1023
|
+
return path.join(WORKSPACE_DIR, arg);
|
|
1024
|
+
}
|
|
1025
|
+
return arg;
|
|
1026
|
+
});
|
|
1027
|
+
if (process.env.STUDIO_CODEX_BIN) {
|
|
1028
|
+
return { command: binary, args: [...resolvedPrefixArgs, projectDir, message], stdin: null };
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
command: binary,
|
|
1032
|
+
args: ["-a", "never", "exec", "--sandbox", "danger-full-access", "--skip-git-repo-check", "--color", "never", ...(model ? ["--model", model] : []), "-C", projectDir, message],
|
|
1033
|
+
stdin: null,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
case "claude":
|
|
1037
|
+
// Claude Code uses OAuth — no API key needed
|
|
1038
|
+
{
|
|
1039
|
+
const args = ["-p", "--setting-sources", "user"];
|
|
1040
|
+
const model = modelOverride || process.env.CREWSWARM_CLAUDE_CODE_MODEL || "";
|
|
1041
|
+
// Add workspace directory context
|
|
1042
|
+
if (projectDir) args.push("--add-dir", projectDir);
|
|
1043
|
+
if (model) args.push("--model", model);
|
|
1044
|
+
return {
|
|
1045
|
+
command: "claude",
|
|
1046
|
+
args,
|
|
1047
|
+
stdin: message,
|
|
1048
|
+
stripEnv: ["CLAUDECODE", "CLAUDE_CODE"],
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
case "cursor":
|
|
1052
|
+
{
|
|
1053
|
+
const cursorSpec = getCursorCommand();
|
|
1054
|
+
const cwd = projectDir || WORKSPACE_DIR;
|
|
1055
|
+
const model = resolveStudioCursorModel(modelOverride);
|
|
1056
|
+
const args = [
|
|
1057
|
+
...cursorSpec.argsPrefix,
|
|
1058
|
+
"-p",
|
|
1059
|
+
"--force",
|
|
1060
|
+
"--trust",
|
|
1061
|
+
"--output-format",
|
|
1062
|
+
"stream-json",
|
|
1063
|
+
"--stream-partial-output",
|
|
1064
|
+
message,
|
|
1065
|
+
"--model",
|
|
1066
|
+
model,
|
|
1067
|
+
"--workspace",
|
|
1068
|
+
cwd,
|
|
1069
|
+
];
|
|
1070
|
+
return {
|
|
1071
|
+
command: cursorSpec.bin,
|
|
1072
|
+
args,
|
|
1073
|
+
stdin: null,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
case "gemini":
|
|
1077
|
+
{
|
|
1078
|
+
const args = ["-p", message, "--output-format", "stream-json", "--yolo"];
|
|
1079
|
+
const model = modelOverride || process.env.CREWSWARM_GEMINI_CLI_MODEL || "";
|
|
1080
|
+
if (model) args.push("-m", model);
|
|
1081
|
+
// Add workspace directory to allow file operations in projectDir (gemini uses --include-directories)
|
|
1082
|
+
if (projectDir) args.push("--include-directories", projectDir);
|
|
1083
|
+
return {
|
|
1084
|
+
command: "gemini",
|
|
1085
|
+
args,
|
|
1086
|
+
stdin: null,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
case "opencode":
|
|
1090
|
+
{
|
|
1091
|
+
let model = modelOverride || process.env.OPENCODE_MODEL || process.env.CREWSWARM_OPENCODE_MODEL || "";
|
|
1092
|
+
if (!model) {
|
|
1093
|
+
try {
|
|
1094
|
+
const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
|
|
1095
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
1096
|
+
model = cfg.opencodeModel || "";
|
|
1097
|
+
} catch {}
|
|
1098
|
+
}
|
|
1099
|
+
if (!model) model = "opencode/gpt-5.2";
|
|
1100
|
+
const args = ["run", "-m", model, message];
|
|
1101
|
+
// Add workspace directory context
|
|
1102
|
+
if (projectDir) args.push("--dir", projectDir);
|
|
1103
|
+
return {
|
|
1104
|
+
command: "opencode",
|
|
1105
|
+
args,
|
|
1106
|
+
stdin: null,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
case "crew-cli": {
|
|
1110
|
+
const crewBin = path.join(__dirname, "..", "..", "crew-cli", "bin", "crew.js");
|
|
1111
|
+
const model = modelOverride || process.env.CREWSWARM_CREW_CLI_MODEL || "";
|
|
1112
|
+
return {
|
|
1113
|
+
command: "node",
|
|
1114
|
+
args: [crewBin, "chat", message, ...(projectDir ? ["--project", projectDir] : []), ...(model ? ["--model", model] : [])],
|
|
1115
|
+
stdin: null,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
default:
|
|
1119
|
+
return null;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Backward compat alias
|
|
1124
|
+
export function getCodexCommand(projectDir, message) {
|
|
1125
|
+
return getCliCommand("codex", projectDir, message, undefined);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
export function runCli({
|
|
1129
|
+
projectDir,
|
|
1130
|
+
projectId,
|
|
1131
|
+
engine,
|
|
1132
|
+
message,
|
|
1133
|
+
onChunk,
|
|
1134
|
+
onTrace,
|
|
1135
|
+
model,
|
|
1136
|
+
}) {
|
|
1137
|
+
return new Promise((resolve, reject) => {
|
|
1138
|
+
const cmd = getCliCommand(engine, projectDir, message, model);
|
|
1139
|
+
if (!cmd) {
|
|
1140
|
+
reject(new Error(`Unknown engine "${engine}". Supported: ${SUPPORTED_ENGINES.join(", ")}`));
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const childEnv = { ...process.env, FORCE_COLOR: "0" };
|
|
1145
|
+
// Strip env vars that block nested CLI sessions (e.g. CLAUDECODE)
|
|
1146
|
+
if (cmd.stripEnv) {
|
|
1147
|
+
for (const key of cmd.stripEnv) {
|
|
1148
|
+
delete childEnv[key];
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const child = spawn(cmd.command, cmd.args, {
|
|
1153
|
+
cwd: projectDir,
|
|
1154
|
+
env: childEnv,
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
// Pipe stdin for CLIs that read prompt from stdin (claude, cursor)
|
|
1158
|
+
if (cmd.stdin) {
|
|
1159
|
+
child.stdin.write(cmd.stdin);
|
|
1160
|
+
child.stdin.end();
|
|
1161
|
+
} else {
|
|
1162
|
+
// Close stdin so CLIs like opencode don't hang waiting for input
|
|
1163
|
+
child.stdin.end();
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
let transcript = "";
|
|
1167
|
+
const relay = createCliRelay(engine, onChunk, () => {
|
|
1168
|
+
if (!child.killed) {
|
|
1169
|
+
child.kill("SIGTERM");
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
const handleOutput = (chunk) => {
|
|
1173
|
+
onTrace?.(chunk.toString("utf8"));
|
|
1174
|
+
relay.push(chunk);
|
|
1175
|
+
};
|
|
1176
|
+
child.stdout.on("data", handleOutput);
|
|
1177
|
+
child.stderr.on("data", handleOutput);
|
|
1178
|
+
child.on("close", (exitCode) => {
|
|
1179
|
+
const normalizedExitCode = Number(exitCode ?? 0);
|
|
1180
|
+
transcript = relay.finish();
|
|
1181
|
+
// Cursor agent often exits non-zero after we SIGTERM following a `result` event; treat as OK if we got text.
|
|
1182
|
+
const effectiveExit =
|
|
1183
|
+
engine === "cursor" && transcript.trim()
|
|
1184
|
+
? 0
|
|
1185
|
+
: normalizedExitCode;
|
|
1186
|
+
appendProjectMessage(projectId, {
|
|
1187
|
+
role: "assistant",
|
|
1188
|
+
content: transcript.trim(),
|
|
1189
|
+
ts: Date.now(),
|
|
1190
|
+
source: "studio-cli",
|
|
1191
|
+
metadata: { engine, exitCode: effectiveExit },
|
|
1192
|
+
});
|
|
1193
|
+
resolve({ exitCode: effectiveExit, transcript: transcript.trim() });
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
child.on("error", reject);
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Backward compat alias
|
|
1201
|
+
export function runCodexCli(opts) {
|
|
1202
|
+
return runCli(opts);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function handleCliChatLocally(req, res, body) {
|
|
1206
|
+
let message = String(body.message || "").trim();
|
|
1207
|
+
const projectDir = resolveStudioProjectPath(body.projectDir, WORKSPACE_DIR);
|
|
1208
|
+
const projectId = body.projectId || DEFAULT_PROJECT_ID;
|
|
1209
|
+
const engine = body.engine || "";
|
|
1210
|
+
|
|
1211
|
+
// Inject open file context from Vibe editor into the message
|
|
1212
|
+
if (body.activeFile) {
|
|
1213
|
+
const ctx = [`[Currently open file: ${body.activeFile}]`];
|
|
1214
|
+
if (body.selectedText) {
|
|
1215
|
+
ctx.push(`[Selected text (lines ${body.selectionStart || "?"}-${body.selectionEnd || "?"}):\n${body.selectedText}\n]`);
|
|
1216
|
+
}
|
|
1217
|
+
message = `${ctx.join("\n")}\n\n${message}`;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
if (!message) {
|
|
1221
|
+
sendJson(res, 400, { error: "message is required" });
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
if (!SUPPORTED_ENGINES.includes(engine)) {
|
|
1226
|
+
sendJson(res, 400, {
|
|
1227
|
+
error: `Unsupported engine "${engine || "unknown"}". Supported: ${SUPPORTED_ENGINES.join(", ")}`,
|
|
1228
|
+
});
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (!isWithinAllowedRoots(projectDir)) {
|
|
1233
|
+
sendJson(res, 403, { error: "projectDir is outside configured project roots" });
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
appendProjectMessage(projectId, {
|
|
1238
|
+
role: "user",
|
|
1239
|
+
content: message,
|
|
1240
|
+
ts: Date.now(),
|
|
1241
|
+
source: "cli",
|
|
1242
|
+
metadata: { engine, agentName: "You", agentEmoji: "👤" },
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
if (!sendSseHeaders(res)) {
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
let clientClosed = false;
|
|
1250
|
+
|
|
1251
|
+
runCli({
|
|
1252
|
+
projectDir,
|
|
1253
|
+
projectId,
|
|
1254
|
+
engine,
|
|
1255
|
+
message,
|
|
1256
|
+
model: body.model ? String(body.model) : undefined,
|
|
1257
|
+
onChunk(text) {
|
|
1258
|
+
if (!clientClosed) {
|
|
1259
|
+
sendSseEvent(res, { type: "chunk", text });
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
onTrace(text) {
|
|
1263
|
+
if (!clientClosed) {
|
|
1264
|
+
sendSseEvent(res, { type: "trace", text });
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
})
|
|
1268
|
+
.then(({ exitCode, transcript }) => {
|
|
1269
|
+
if (!clientClosed) {
|
|
1270
|
+
sendSseEvent(res, { type: "done", exitCode, transcript });
|
|
1271
|
+
res.end();
|
|
1272
|
+
}
|
|
1273
|
+
})
|
|
1274
|
+
.catch((error) => {
|
|
1275
|
+
if (!clientClosed) {
|
|
1276
|
+
sendSseEvent(res, { type: "chunk", text: `${error.message}\n` });
|
|
1277
|
+
sendSseEvent(res, { type: "done", exitCode: 1 });
|
|
1278
|
+
res.end();
|
|
1279
|
+
}
|
|
1280
|
+
appendProjectMessage(projectId, {
|
|
1281
|
+
role: "assistant",
|
|
1282
|
+
content: error.message,
|
|
1283
|
+
ts: Date.now(),
|
|
1284
|
+
source: "studio-cli",
|
|
1285
|
+
metadata: { engine, exitCode: 1 },
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
req.on("close", () => {
|
|
1290
|
+
clientClosed = true;
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async function handleCliChatViaCrewLead(req, res, body) {
|
|
1295
|
+
let message = String(body.message || "").trim();
|
|
1296
|
+
const projectDir = resolveStudioProjectPath(body.projectDir, WORKSPACE_DIR);
|
|
1297
|
+
|
|
1298
|
+
// Inject open file context from Vibe editor into the message
|
|
1299
|
+
if (body.activeFile) {
|
|
1300
|
+
const ctx = [`[Currently open file: ${body.activeFile}]`];
|
|
1301
|
+
if (body.selectedText) {
|
|
1302
|
+
ctx.push(`[Selected text (lines ${body.selectionStart || "?"}-${body.selectionEnd || "?"}):\n${body.selectedText}\n]`);
|
|
1303
|
+
}
|
|
1304
|
+
message = `${ctx.join("\n")}\n\n${message}`;
|
|
1305
|
+
}
|
|
1306
|
+
const projectId = body.projectId || DEFAULT_PROJECT_ID;
|
|
1307
|
+
const sessionId = String(body.sessionId || "studio-cli");
|
|
1308
|
+
const engine = body.engine || "";
|
|
1309
|
+
const model = body.model ? String(body.model) : "";
|
|
1310
|
+
const token = loadCrewswarmRtToken();
|
|
1311
|
+
|
|
1312
|
+
if (!token) {
|
|
1313
|
+
throw new Error("CrewSwarm RT auth token unavailable");
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const upstream = await fetch("http://127.0.0.1:5010/api/engine-passthrough", {
|
|
1317
|
+
method: "POST",
|
|
1318
|
+
headers: {
|
|
1319
|
+
"content-type": "application/json",
|
|
1320
|
+
authorization: `Bearer ${token}`,
|
|
1321
|
+
},
|
|
1322
|
+
body: JSON.stringify({
|
|
1323
|
+
engine,
|
|
1324
|
+
message,
|
|
1325
|
+
projectDir,
|
|
1326
|
+
projectId,
|
|
1327
|
+
sessionId,
|
|
1328
|
+
...(model ? { model } : {}),
|
|
1329
|
+
}),
|
|
1330
|
+
// Align with Vibe client CHAT_STREAM_TIMEOUT_MS (10m) — 240s was aborting long Codex/OpenCode runs
|
|
1331
|
+
signal: AbortSignal.timeout(
|
|
1332
|
+
Number(process.env.STUDIO_CREW_LEAD_FETCH_MS || "600000"),
|
|
1333
|
+
),
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
if (!upstream.ok || !upstream.body) {
|
|
1337
|
+
const text = await upstream.text().catch(() => "");
|
|
1338
|
+
throw new Error(text || `crew-lead passthrough failed (${upstream.status})`);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
appendProjectMessage(projectId, {
|
|
1342
|
+
role: "user",
|
|
1343
|
+
content: message,
|
|
1344
|
+
ts: Date.now(),
|
|
1345
|
+
source: "cli",
|
|
1346
|
+
metadata: { engine, agentName: "You", agentEmoji: "👤" },
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
if (!sendSseHeaders(res)) {
|
|
1350
|
+
throw new Error("SSE response already started");
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
let clientClosed = false;
|
|
1354
|
+
let sseBuffer = "";
|
|
1355
|
+
let transcript = "";
|
|
1356
|
+
let stderrText = "";
|
|
1357
|
+
let exitCode = 1;
|
|
1358
|
+
|
|
1359
|
+
const reader = upstream.body.getReader();
|
|
1360
|
+
req.on("close", () => {
|
|
1361
|
+
clientClosed = true;
|
|
1362
|
+
reader.cancel().catch(() => {});
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
const parseEventPayload = (rawEvent) => {
|
|
1366
|
+
const dataLines = rawEvent
|
|
1367
|
+
.split("\n")
|
|
1368
|
+
.filter((line) => line.startsWith("data:"))
|
|
1369
|
+
.map((line) => line.slice(5).trim())
|
|
1370
|
+
.join("\n");
|
|
1371
|
+
if (!dataLines) return null;
|
|
1372
|
+
try {
|
|
1373
|
+
return JSON.parse(dataLines);
|
|
1374
|
+
} catch {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
while (true) {
|
|
1380
|
+
const { done, value } = await reader.read();
|
|
1381
|
+
if (done) break;
|
|
1382
|
+
const text = Buffer.from(value).toString("utf8");
|
|
1383
|
+
if (!clientClosed) {
|
|
1384
|
+
res.write(text);
|
|
1385
|
+
}
|
|
1386
|
+
sseBuffer += text;
|
|
1387
|
+
|
|
1388
|
+
while (sseBuffer.includes("\n\n")) {
|
|
1389
|
+
const boundary = sseBuffer.indexOf("\n\n");
|
|
1390
|
+
const rawEvent = sseBuffer.slice(0, boundary);
|
|
1391
|
+
sseBuffer = sseBuffer.slice(boundary + 2);
|
|
1392
|
+
const payload = parseEventPayload(rawEvent);
|
|
1393
|
+
if (!payload) continue;
|
|
1394
|
+
if (payload.type === "chunk" && payload.text) {
|
|
1395
|
+
transcript += payload.text;
|
|
1396
|
+
} else if (payload.type === "stderr" && payload.text) {
|
|
1397
|
+
stderrText += payload.text;
|
|
1398
|
+
} else if (payload.type === "done") {
|
|
1399
|
+
exitCode = Number(payload.exitCode ?? 0);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
appendProjectMessage(projectId, {
|
|
1405
|
+
role: "assistant",
|
|
1406
|
+
content: (transcript || stderrText || "(no output)").trim(),
|
|
1407
|
+
ts: Date.now(),
|
|
1408
|
+
source: "cli",
|
|
1409
|
+
agent: engine,
|
|
1410
|
+
metadata: { engine, exitCode, agentName: engine, agentEmoji: "⚡" },
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
if (!clientClosed) {
|
|
1414
|
+
res.end();
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function handleCliChat(req, res, body) {
|
|
1419
|
+
handleCliChatViaCrewLead(req, res, body).catch((error) => {
|
|
1420
|
+
if (!res.headersSent && !res.writableEnded) {
|
|
1421
|
+
console.warn(`[studio] crew-lead passthrough unavailable, falling back local: ${error.message}`);
|
|
1422
|
+
handleCliChatLocally(req, res, body);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
console.warn(`[studio] crew-lead stream failed after response start: ${error.message}`);
|
|
1426
|
+
if (!res.writableEnded) {
|
|
1427
|
+
sendSseEvent(res, { type: "trace", text: `crew-lead stream interrupted: ${error.message}` });
|
|
1428
|
+
sendSseEvent(res, { type: "done", exitCode: 1 });
|
|
1429
|
+
res.end();
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/** Dashboard HTTP API (crew-lead proxy, agents list, token). Override if dashboard is not on :4319. */
|
|
1435
|
+
const DASHBOARD_PROXY_TARGET = String(
|
|
1436
|
+
process.env.CREWSWARM_DASHBOARD_URL || "http://127.0.0.1:4319",
|
|
1437
|
+
).replace(/\/$/, "");
|
|
1438
|
+
|
|
1439
|
+
async function readRequestBuffer(req) {
|
|
1440
|
+
const chunks = [];
|
|
1441
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
1442
|
+
return chunks.length ? Buffer.concat(chunks) : Buffer.alloc(0);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* Server-side forward to the dashboard so the browser stays same-origin (e.g. localhost:3333).
|
|
1447
|
+
* Avoids CORS when :4319 responds with Access-Control-Allow-Origin: http://localhost:4319 or when
|
|
1448
|
+
* the page is http://localhost:3333 but fetch targets http://127.0.0.1:4319.
|
|
1449
|
+
*/
|
|
1450
|
+
async function proxyRequestToDashboard(req, res, pathWithQuery) {
|
|
1451
|
+
const targetUrl = `${DASHBOARD_PROXY_TARGET}${pathWithQuery}`;
|
|
1452
|
+
const headers = {};
|
|
1453
|
+
for (const name of ["content-type", "authorization", "accept"]) {
|
|
1454
|
+
const v = req.headers[name];
|
|
1455
|
+
if (v) headers[name] = v;
|
|
1456
|
+
}
|
|
1457
|
+
const init = {
|
|
1458
|
+
method: req.method,
|
|
1459
|
+
headers,
|
|
1460
|
+
signal: AbortSignal.timeout(660000),
|
|
1461
|
+
};
|
|
1462
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1463
|
+
init.body = await readRequestBuffer(req);
|
|
1464
|
+
}
|
|
1465
|
+
let upstream;
|
|
1466
|
+
try {
|
|
1467
|
+
upstream = await fetch(targetUrl, init);
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
console.warn("[vibe] dashboard proxy fetch failed:", err?.message || err);
|
|
1470
|
+
sendJson(res, 502, {
|
|
1471
|
+
ok: false,
|
|
1472
|
+
error: `dashboard unreachable (${DASHBOARD_PROXY_TARGET}): ${err?.message || err}`,
|
|
1473
|
+
});
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (!upstream.ok) {
|
|
1477
|
+
const txt = await upstream.text().catch(() => "");
|
|
1478
|
+
res.writeHead(upstream.status, {
|
|
1479
|
+
"content-type": upstream.headers.get("content-type") || "application/json",
|
|
1480
|
+
});
|
|
1481
|
+
res.end(txt || JSON.stringify({ ok: false, error: String(upstream.status) }));
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const ct = upstream.headers.get("content-type") || "";
|
|
1485
|
+
const out = {
|
|
1486
|
+
"content-type": ct || "application/octet-stream",
|
|
1487
|
+
"cache-control": upstream.headers.get("cache-control") || "no-cache",
|
|
1488
|
+
};
|
|
1489
|
+
if (ct.includes("text/event-stream")) {
|
|
1490
|
+
out.connection = "keep-alive";
|
|
1491
|
+
}
|
|
1492
|
+
res.writeHead(upstream.status, out);
|
|
1493
|
+
if (!upstream.body) {
|
|
1494
|
+
res.end();
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const reader = upstream.body.getReader();
|
|
1498
|
+
req.on("close", () => reader.cancel().catch(() => {}));
|
|
1499
|
+
try {
|
|
1500
|
+
while (true) {
|
|
1501
|
+
const { done, value } = await reader.read();
|
|
1502
|
+
if (done) break;
|
|
1503
|
+
if (value?.byteLength) res.write(Buffer.from(value));
|
|
1504
|
+
}
|
|
1505
|
+
} catch (err) {
|
|
1506
|
+
console.warn("[vibe] dashboard proxy stream:", err?.message || err);
|
|
1507
|
+
} finally {
|
|
1508
|
+
try {
|
|
1509
|
+
res.end();
|
|
1510
|
+
} catch {
|
|
1511
|
+
/* ignore */
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
export function createOrUpdateProject(body) {
|
|
1517
|
+
const name = String(body.name || "").trim();
|
|
1518
|
+
const outputDirRaw = String(body.outputDir || "").trim();
|
|
1519
|
+
const description = String(body.description || "").trim();
|
|
1520
|
+
|
|
1521
|
+
if (!name) {
|
|
1522
|
+
return { status: 400, payload: { error: "name is required" } };
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (!outputDirRaw) {
|
|
1526
|
+
return { status: 400, payload: { error: "outputDir is required" } };
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const outputDir = resolveStudioProjectPath(outputDirRaw);
|
|
1530
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1531
|
+
const roadmapFile = path.join(outputDir, "ROADMAP.md");
|
|
1532
|
+
|
|
1533
|
+
const projects = readProjects();
|
|
1534
|
+
const existing = projects.find((project) => path.resolve(project.outputDir) === outputDir);
|
|
1535
|
+
const id = existing?.id || slugify(name) || `project-${Date.now()}`;
|
|
1536
|
+
const project = {
|
|
1537
|
+
...(existing || {}),
|
|
1538
|
+
id,
|
|
1539
|
+
name,
|
|
1540
|
+
outputDir,
|
|
1541
|
+
description,
|
|
1542
|
+
roadmapFile: existing?.roadmapFile || roadmapFile,
|
|
1543
|
+
featuresDoc: existing?.featuresDoc || "",
|
|
1544
|
+
tags: Array.isArray(existing?.tags) ? existing.tags : [],
|
|
1545
|
+
created: existing?.created || new Date().toISOString(),
|
|
1546
|
+
status: existing?.status || "active",
|
|
1547
|
+
};
|
|
1548
|
+
const nextProjects = existing
|
|
1549
|
+
? projects.map((entry) => (entry.id === existing.id ? project : entry))
|
|
1550
|
+
: [...projects, project];
|
|
1551
|
+
|
|
1552
|
+
writeProjects(nextProjects);
|
|
1553
|
+
invalidateWorkspaceScanCache(outputDir);
|
|
1554
|
+
return { status: 200, payload: { ok: true, project } };
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
export const server = http.createServer(async (req, res) => {
|
|
1558
|
+
const parsedUrl = new URL(req.url || "/", "http://127.0.0.1");
|
|
1559
|
+
|
|
1560
|
+
if (req.method === "OPTIONS") {
|
|
1561
|
+
sendJson(res, 204, {});
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const dashboardProxyPath = parsedUrl.pathname + (parsedUrl.search || "");
|
|
1566
|
+
if (parsedUrl.pathname === "/api/chat/unified" && req.method === "POST") {
|
|
1567
|
+
await proxyRequestToDashboard(req, res, dashboardProxyPath);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
if (parsedUrl.pathname === "/api/auth/token" && req.method === "GET") {
|
|
1571
|
+
await proxyRequestToDashboard(req, res, dashboardProxyPath);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (parsedUrl.pathname === "/api/agents" && req.method === "GET") {
|
|
1575
|
+
await proxyRequestToDashboard(req, res, dashboardProxyPath);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
if (parsedUrl.pathname === "/api/studio/projects" && req.method === "GET") {
|
|
1580
|
+
sendJson(res, 200, {
|
|
1581
|
+
ok: true,
|
|
1582
|
+
projects: readProjects(),
|
|
1583
|
+
});
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (parsedUrl.pathname === "/api/studio/projects" && req.method === "POST") {
|
|
1588
|
+
try {
|
|
1589
|
+
const body = await readBody(req);
|
|
1590
|
+
const { status, payload } = createOrUpdateProject(body);
|
|
1591
|
+
sendJson(res, status, payload);
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
sendJson(res, 400, { error: error.message });
|
|
1594
|
+
}
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (parsedUrl.pathname === "/api/studio/active-project") {
|
|
1599
|
+
if (req.method === "GET") {
|
|
1600
|
+
const uiState = readUiState();
|
|
1601
|
+
sendJson(res, 200, {
|
|
1602
|
+
ok: true,
|
|
1603
|
+
projectId: String(uiState.chatActiveProjectId || "general"),
|
|
1604
|
+
});
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (req.method === "POST") {
|
|
1608
|
+
try {
|
|
1609
|
+
const body = await readBody(req);
|
|
1610
|
+
const normalizedProjectId =
|
|
1611
|
+
body?.projectId && String(body.projectId).trim()
|
|
1612
|
+
? String(body.projectId).trim()
|
|
1613
|
+
: "general";
|
|
1614
|
+
const uiState = readUiState();
|
|
1615
|
+
uiState.chatActiveProjectId = normalizedProjectId;
|
|
1616
|
+
writeUiState(uiState);
|
|
1617
|
+
sendJson(res, 200, { ok: true, projectId: normalizedProjectId });
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
sendJson(res, 400, { error: error.message });
|
|
1620
|
+
}
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (parsedUrl.pathname === "/api/studio/files" && req.method === "GET") {
|
|
1626
|
+
const requestedDir = parsedUrl.searchParams.get("dir");
|
|
1627
|
+
const scanDir = requestedDir
|
|
1628
|
+
? resolveStudioProjectPath(requestedDir, WORKSPACE_DIR)
|
|
1629
|
+
: WORKSPACE_DIR;
|
|
1630
|
+
if (!isWithinAllowedRoots(scanDir)) {
|
|
1631
|
+
sendJson(res, 403, { error: "path outside configured project roots" });
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
sendJson(res, 200, { files: listWorkspaceFiles(scanDir) });
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
if (parsedUrl.pathname === "/api/studio/file-content" && req.method === "GET") {
|
|
1640
|
+
const filePath = parsedUrl.searchParams.get("path") || "";
|
|
1641
|
+
const resolvedPath = resolveStudioProjectPath(filePath, "");
|
|
1642
|
+
if (!filePath || !isWithinAllowedRoots(resolvedPath)) {
|
|
1643
|
+
sendJson(res, 400, { error: "invalid path" });
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
try {
|
|
1648
|
+
const raw = fs.readFileSync(resolvedPath, "utf8");
|
|
1649
|
+
sendJson(res, 200, { content: raw, lines: raw.split("\n").length });
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
sendJson(res, 404, { error: error.message });
|
|
1652
|
+
}
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (parsedUrl.pathname === "/api/studio/file-content" && req.method === "POST") {
|
|
1657
|
+
try {
|
|
1658
|
+
const body = await readBody(req);
|
|
1659
|
+
const resolvedPath = resolveStudioProjectPath(String(body.path || ""), "");
|
|
1660
|
+
const content = typeof body.content === "string" ? body.content : "";
|
|
1661
|
+
if (!resolvedPath || !isWithinAllowedRoots(resolvedPath)) {
|
|
1662
|
+
sendJson(res, 400, { error: "invalid path" });
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
|
|
1667
|
+
fs.writeFileSync(resolvedPath, content);
|
|
1668
|
+
invalidateWorkspaceScanCache(resolvedPath);
|
|
1669
|
+
const stat = fs.statSync(resolvedPath);
|
|
1670
|
+
sendJson(res, 200, { ok: true, path: resolvedPath, size: stat.size });
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
sendJson(res, 400, { error: error.message });
|
|
1673
|
+
}
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
if (parsedUrl.pathname === "/api/studio/project-messages" && req.method === "GET") {
|
|
1678
|
+
sendJson(res, 200, {
|
|
1679
|
+
messages: readProjectMessages(
|
|
1680
|
+
parsedUrl.searchParams.get("projectId") || DEFAULT_PROJECT_ID,
|
|
1681
|
+
parsedUrl.searchParams.get("limit") || "50",
|
|
1682
|
+
),
|
|
1683
|
+
});
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
|
|
1688
|
+
if (parsedUrl.pathname === "/api/studio/engines" && req.method === "GET") {
|
|
1689
|
+
sendJson(res, 200, { engines: SUPPORTED_ENGINES });
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
if (parsedUrl.pathname === "/api/studio/chat/unified" && req.method === "POST") {
|
|
1694
|
+
try {
|
|
1695
|
+
const body = await readBody(req);
|
|
1696
|
+
if (body.mode !== "cli") {
|
|
1697
|
+
sendJson(res, 400, {
|
|
1698
|
+
error: "Local Studio chat only supports CLI passthrough right now",
|
|
1699
|
+
});
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
handleCliChat(req, res, body);
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
sendJson(res, 400, { error: error.message });
|
|
1705
|
+
}
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (parsedUrl.pathname === "/api/studio/terminal/start" && req.method === "POST") {
|
|
1710
|
+
try {
|
|
1711
|
+
const body = await readBody(req);
|
|
1712
|
+
const { sessionId, cwd } = createTerminalSession({
|
|
1713
|
+
projectDir: String(body.projectDir || WORKSPACE_DIR),
|
|
1714
|
+
cols: body.cols,
|
|
1715
|
+
rows: body.rows,
|
|
1716
|
+
});
|
|
1717
|
+
sendJson(res, 200, { ok: true, sessionId, cwd });
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
sendJson(res, 400, { error: error.message });
|
|
1720
|
+
}
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
if (parsedUrl.pathname === "/api/studio/terminal" && req.method === "DELETE") {
|
|
1725
|
+
const sessionId = parsedUrl.searchParams.get("sessionId") || "";
|
|
1726
|
+
if (!sessionId) {
|
|
1727
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
sendJson(res, 200, { ok: closeTerminalSession(sessionId) });
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
const filePath = resolveRequestPath(req.url);
|
|
1735
|
+
const ext = path.extname(filePath);
|
|
1736
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
1737
|
+
const { filePath: servedPath, encoding } = getEncodedAsset(
|
|
1738
|
+
filePath,
|
|
1739
|
+
req.headers["accept-encoding"] || "",
|
|
1740
|
+
);
|
|
1741
|
+
|
|
1742
|
+
readFileWithRetry(servedPath)
|
|
1743
|
+
.then((content) => {
|
|
1744
|
+
const headers = {
|
|
1745
|
+
"Content-Type": contentType,
|
|
1746
|
+
"Cache-Control": getCacheControlHeader(filePath),
|
|
1747
|
+
Vary: "Accept-Encoding",
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
if (encoding) {
|
|
1751
|
+
headers["Content-Encoding"] = encoding;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
res.writeHead(200, headers);
|
|
1755
|
+
res.end(content);
|
|
1756
|
+
})
|
|
1757
|
+
.catch((err) => {
|
|
1758
|
+
if (err.code === "ENOENT") {
|
|
1759
|
+
readFileWithRetry(path.join(DIST_DIR, "index.html"))
|
|
1760
|
+
.then((html) => {
|
|
1761
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1762
|
+
res.end(html);
|
|
1763
|
+
})
|
|
1764
|
+
.catch(() => {
|
|
1765
|
+
res.writeHead(500);
|
|
1766
|
+
res.end("Server error");
|
|
1767
|
+
});
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
res.writeHead(500);
|
|
1771
|
+
res.end("Server error");
|
|
1772
|
+
});
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
const terminalWss = new WebSocketServer({ noServer: true });
|
|
1776
|
+
|
|
1777
|
+
terminalWss.on("connection", (socket, request) => {
|
|
1778
|
+
const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
|
|
1779
|
+
const sessionId = requestUrl.searchParams.get("sessionId") || "";
|
|
1780
|
+
const session = terminalSessions.get(sessionId);
|
|
1781
|
+
|
|
1782
|
+
if (!session) {
|
|
1783
|
+
socket.send(JSON.stringify({ type: "error", message: "terminal session not found" }));
|
|
1784
|
+
socket.close();
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
session.sockets.add(socket);
|
|
1789
|
+
socket.send(JSON.stringify({ type: "ready", sessionId, cwd: session.cwd }));
|
|
1790
|
+
|
|
1791
|
+
socket.on("message", (raw) => {
|
|
1792
|
+
try {
|
|
1793
|
+
const message = JSON.parse(raw.toString("utf8"));
|
|
1794
|
+
if (message.type === "input" && typeof message.data === "string") {
|
|
1795
|
+
writeTerminalSession(sessionId, message.data);
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (message.type === "resize") {
|
|
1799
|
+
resizeTerminalSession(sessionId, message.cols, message.rows);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if (message.type === "kill") {
|
|
1803
|
+
closeTerminalSession(sessionId);
|
|
1804
|
+
}
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
socket.send(JSON.stringify({ type: "error", message: error.message }));
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
socket.on("close", () => {
|
|
1811
|
+
session.sockets.delete(socket);
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
server.on("upgrade", (request, socket, head) => {
|
|
1816
|
+
const parsedUrl = new URL(request.url || "/", "http://127.0.0.1");
|
|
1817
|
+
if (parsedUrl.pathname !== "/ws/studio/terminal") {
|
|
1818
|
+
socket.destroy();
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
terminalWss.handleUpgrade(request, socket, head, (ws) => {
|
|
1823
|
+
terminalWss.emit("connection", ws, request);
|
|
1824
|
+
});
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
if (process.env.STUDIO_DISABLE_LISTEN !== "1") {
|
|
1828
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
1829
|
+
ensureProjects();
|
|
1830
|
+
const address = server.address();
|
|
1831
|
+
const boundPort =
|
|
1832
|
+
address && typeof address === "object" ? address.port : PORT;
|
|
1833
|
+
console.log(`🐝 crewswarm Vibe running at http://127.0.0.1:${boundPort}`);
|
|
1834
|
+
});
|
|
1835
|
+
}
|