crewswarm 0.9.2 → 0.9.4
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/README.md +22 -9
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js +1 -0
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
- package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
- package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
- package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
- package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
- package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
- package/apps/dashboard/dist/index.html +135 -15
- package/apps/dashboard/dist/index.html.br +0 -0
- package/apps/dashboard/dist/index.html.gz +0 -0
- package/apps/vibe/README.md +2 -2
- package/apps/vibe/package.json +1 -1
- package/apps/vibe/server.mjs +101 -56
- package/crew-lead.mjs +34 -4
- package/lib/bridges/cli-executor.mjs +1 -1
- package/lib/bridges/gateway-ws.mjs +4 -0
- package/lib/browser/passthrough-stderr.js +1 -0
- package/lib/chat/project-messages.mjs +3 -5
- package/lib/cli-process-tracker.mjs +3 -2
- package/lib/contacts/identity-linker.mjs +1 -0
- package/lib/crew-judge/judge.mjs +19 -18
- package/lib/crew-lead/agent-manager.mjs +1 -1
- package/lib/crew-lead/background.mjs +14 -1
- package/lib/crew-lead/chat-handler.mjs +38 -1
- package/lib/crew-lead/http-server.mjs +106 -57
- package/lib/crew-lead/llm-caller.mjs +24 -8
- package/lib/crew-lead/prompts.mjs +14 -1
- package/lib/crew-lead/tools.mjs +3 -2
- package/lib/crew-lead/wave-dispatcher.mjs +19 -5
- package/lib/crew-lead/ws-router.mjs +219 -27
- package/lib/engines/crew-cli.mjs +1 -1
- package/lib/engines/engine-registry.mjs +14 -3
- package/lib/engines/rt-envelope.mjs +1 -0
- package/lib/engines/runners.mjs +28 -4
- package/lib/gemini-cli-passthrough-noise.mjs +1 -1
- package/lib/integrations/code-search.mjs +4 -3
- package/lib/memory/shared-adapter.mjs +23 -10
- package/lib/pipeline/manager.mjs +2 -1
- package/lib/runtime/config.mjs +1 -1
- package/lib/runtime/paths.mjs +12 -8
- package/lib/runtime/spending.mjs +2 -1
- package/package.json +42 -14
- package/scripts/capture-build-flow.mjs +118 -0
- package/scripts/coverage-report.mjs +209 -0
- package/scripts/coverage-summary.mjs +47 -0
- package/scripts/dashboard-validation.mjs +76 -0
- package/scripts/dashboard.mjs +1667 -551
- package/scripts/generate-openapi.mjs +683 -277
- package/scripts/live-bridge-matrix.mjs +79 -0
- package/scripts/live-cli-matrix.mjs +166 -0
- package/scripts/live-crewchat-check.mjs +42 -0
- package/scripts/live-engine-matrix.mjs +50 -0
- package/scripts/live-provider-failover-matrix.mjs +107 -0
- package/scripts/live-provider-matrix.mjs +228 -0
- package/scripts/restart-all-from-repo.sh +4 -4
- package/scripts/restart-service.sh +12 -9
- package/scripts/smoke-dispatch.mjs +4 -1
- package/scripts/test-blast-radius.mjs +204 -0
- package/scripts/test-report-summary.mjs +88 -0
- package/scripts/test-reporter.mjs +651 -0
- package/scripts/test-rerun.mjs +136 -0
- package/scripts/tmux-bridge +130 -0
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js +0 -1
- package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
- package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
- package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js +0 -1
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
- package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js +0 -1
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
- package/apps/dashboard/index.html +0 -6529
- package/apps/dashboard/package.json +0 -15
- package/apps/dashboard/src/app.js +0 -2828
- 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 +0 -1847
- package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
- package/apps/dashboard/src/chat/unified-messages.js +0 -327
- package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
- package/apps/dashboard/src/cli-process.js +0 -208
- 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 +0 -175
- package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
- package/apps/dashboard/src/core/api.js +0 -18
- package/apps/dashboard/src/core/api.js.br +0 -0
- package/apps/dashboard/src/core/dom.js +0 -228
- package/apps/dashboard/src/core/dom.js.br +0 -0
- package/apps/dashboard/src/core/state.js +0 -91
- package/apps/dashboard/src/core/state.js.br +0 -0
- package/apps/dashboard/src/core/task-manager.js +0 -134
- package/apps/dashboard/src/core/task-manager.js.br +0 -0
- package/apps/dashboard/src/orchestration-status.js +0 -127
- package/apps/dashboard/src/orchestration-status.js.br +0 -0
- package/apps/dashboard/src/setup-wizard.js +0 -562
- package/apps/dashboard/src/setup-wizard.js.br +0 -0
- package/apps/dashboard/src/styles.css +0 -2085
- 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 +0 -2237
- package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
- package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/comms-tab.js +0 -955
- package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
- package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/engines-tab.js +0 -175
- package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/memory-tab.js +0 -182
- package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/models-tab.js +0 -450
- package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
- package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/projects-tab.js +0 -663
- 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 +0 -160
- package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/services-tab.js +0 -202
- package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/settings-tab.js +0 -861
- package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/skills-tab.js +0 -284
- package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/spending-tab.js +0 -173
- package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
- package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
- package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/usage-tab.js +0 -390
- package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/waves-tab.js +0 -238
- package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
- package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
- package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
- package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
- package/apps/vibe/.crew/cost.json +0 -17
- package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
- package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
- package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
- package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
- package/apps/vibe/.crew/sandbox.json +0 -7
- package/apps/vibe/.crew/session.json +0 -330
- package/apps/vibe/.crew/training-data.jsonl +0 -0
- package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
- package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
- package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
- package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
- package/apps/vibe/ARCHITECTURE.md +0 -3393
- package/apps/vibe/QUICK-REFERENCE.md +0 -211
- package/apps/vibe/ROADMAP.md +0 -41
- package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
- package/apps/vibe/VISUAL-GUIDE.md +0 -378
- package/apps/vibe/capture-demo.mjs +0 -160
- package/apps/vibe/capture-full-demo.mjs +0 -255
- package/apps/vibe/capture-quickstart.mjs +0 -256
- package/apps/vibe/capture-vibe-assets.mjs +0 -71
- package/apps/vibe/capture-vibe-video.mjs +0 -260
- package/apps/vibe/check-buttons.js +0 -41
- package/apps/vibe/diagnose.html +0 -106
- package/apps/vibe/fix-buttons.js +0 -103
- package/apps/vibe/index.html +0 -3404
- package/apps/vibe/package-lock.json +0 -920
- package/apps/vibe/scripts/studio-pty-host.py +0 -117
- package/apps/vibe/src/main.js +0 -2940
- package/apps/vibe/src/register-all-languages.js +0 -98
- package/apps/vibe/start-studio.sh +0 -11
- package/apps/vibe/test/accessibility-tests.js +0 -77
- package/apps/vibe/test/browser-performance-audit.mjs +0 -205
- package/apps/vibe/test/performance-tests.js +0 -120
- package/apps/vibe/test/security-tests.js +0 -213
- package/apps/vibe/tests/e2e.local.mjs +0 -54
- package/apps/vibe/tests/server.smoke.mjs +0 -106
- package/apps/vibe/update_website.mjs +0 -74
- package/apps/vibe/vite.config.js +0 -19
- package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
- package/lib/engines/rt-envelope.mjs.backup-current +0 -870
package/scripts/dashboard.mjs
CHANGED
|
@@ -30,9 +30,25 @@ import {
|
|
|
30
30
|
} from "../lib/agents/tool-instructions.mjs";
|
|
31
31
|
import {
|
|
32
32
|
StartBuildSchema,
|
|
33
|
+
EnhancePromptSchema,
|
|
33
34
|
StartPMLoopSchema,
|
|
34
35
|
ServiceActionSchema,
|
|
35
36
|
ImportSkillSchema,
|
|
37
|
+
AgentConfigCreateSchema,
|
|
38
|
+
AgentConfigDeleteSchema,
|
|
39
|
+
AgentResetSessionSchema,
|
|
40
|
+
ProviderAddSchema,
|
|
41
|
+
ProviderSaveSchema,
|
|
42
|
+
ProviderTestSchema,
|
|
43
|
+
ProviderBuiltinTestSchema,
|
|
44
|
+
ContinuousBuildSchema,
|
|
45
|
+
ReplayDLQSchema,
|
|
46
|
+
DeleteProjectSchema,
|
|
47
|
+
UpdateProjectSchema,
|
|
48
|
+
RoadmapWriteSchema,
|
|
49
|
+
RoadmapRetryFailedSchema,
|
|
50
|
+
ContactDeleteSchema,
|
|
51
|
+
ContactSendSchema,
|
|
36
52
|
validate,
|
|
37
53
|
} from "./dashboard-validation.mjs";
|
|
38
54
|
import { execCrewLeadTools } from "../lib/crew-lead/tools.mjs";
|
|
@@ -108,6 +124,103 @@ if (!lockResult.ok) {
|
|
|
108
124
|
process.exit(1);
|
|
109
125
|
}
|
|
110
126
|
|
|
127
|
+
// ── OAuth token cache with TTL refresh ──────────────────────────────────────
|
|
128
|
+
const _oauthTokenCache = {};
|
|
129
|
+
const _oauthTokenTimestamp = {}; // providerId → Date.now() when cached
|
|
130
|
+
const OAUTH_TOKEN_TTL_MS = 25 * 60 * 1000; // 25 minutes (tokens typically expire in 30-60 min)
|
|
131
|
+
|
|
132
|
+
// Capture execFileSync at module load for sync token refresh
|
|
133
|
+
const { execFileSync: _oauthExecFileSync } = await import("node:child_process");
|
|
134
|
+
|
|
135
|
+
function refreshAnthropicOAuthToken() {
|
|
136
|
+
try {
|
|
137
|
+
for (const acct of [os.userInfo().username, "jeffhobbs", "unknown"]) {
|
|
138
|
+
try {
|
|
139
|
+
const raw = _oauthExecFileSync("security", [
|
|
140
|
+
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
141
|
+
], { encoding: "utf8", timeout: 5000 }).trim();
|
|
142
|
+
const parsed = JSON.parse(raw);
|
|
143
|
+
if (parsed?.claudeAiOauth?.accessToken) {
|
|
144
|
+
_oauthTokenCache["anthropic-oauth"] = parsed.claudeAiOauth.accessToken;
|
|
145
|
+
_oauthTokenTimestamp["anthropic-oauth"] = Date.now();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
} catch { /* try next account */ }
|
|
149
|
+
}
|
|
150
|
+
} catch { /* keychain not accessible */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Initial load at startup
|
|
154
|
+
try {
|
|
155
|
+
for (const acct of [os.userInfo().username, "jeffhobbs", "unknown"]) {
|
|
156
|
+
try {
|
|
157
|
+
const raw = _oauthExecFileSync("security", [
|
|
158
|
+
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
159
|
+
], { encoding: "utf8", timeout: 5000 }).trim();
|
|
160
|
+
const parsed = JSON.parse(raw);
|
|
161
|
+
if (parsed?.claudeAiOauth?.accessToken) {
|
|
162
|
+
_oauthTokenCache["anthropic-oauth"] = parsed.claudeAiOauth.accessToken;
|
|
163
|
+
_oauthTokenTimestamp["anthropic-oauth"] = Date.now();
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
} catch { /* try next account */ }
|
|
167
|
+
}
|
|
168
|
+
} catch { /* keychain not accessible at startup (launchd) */ }
|
|
169
|
+
|
|
170
|
+
// OpenAI/Codex OAuth — read from disk (no keychain needed)
|
|
171
|
+
async function readOpenAIOAuthToken() {
|
|
172
|
+
const tokenPaths = [
|
|
173
|
+
path.join(os.homedir(), ".codex", "auth.json"),
|
|
174
|
+
path.join(os.homedir(), ".local", "share", "opencode", "auth.json"),
|
|
175
|
+
path.join(os.homedir(), ".codex-auth", "auth.json"),
|
|
176
|
+
];
|
|
177
|
+
for (const p of tokenPaths) {
|
|
178
|
+
try {
|
|
179
|
+
const raw = await fs.promises.readFile(p, "utf8");
|
|
180
|
+
const data = JSON.parse(raw);
|
|
181
|
+
// Codex CLI chatgpt mode: { tokens: { access_token, refresh_token } }
|
|
182
|
+
if (data.tokens?.access_token) return data.tokens.access_token;
|
|
183
|
+
// openai-codex nested entry
|
|
184
|
+
const entry = data["openai-codex"] || data["codex"] || data["openai"];
|
|
185
|
+
if (entry?.access) return entry.access;
|
|
186
|
+
// flat
|
|
187
|
+
if (data.access_token) return data.access_token;
|
|
188
|
+
if (data.token) return data.token;
|
|
189
|
+
} catch { /* try next */ }
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function refreshOpenAIOAuthToken() {
|
|
195
|
+
try {
|
|
196
|
+
const t = await readOpenAIOAuthToken();
|
|
197
|
+
if (t) {
|
|
198
|
+
_oauthTokenCache["openai-oauth"] = t;
|
|
199
|
+
_oauthTokenTimestamp["openai-oauth"] = Date.now();
|
|
200
|
+
}
|
|
201
|
+
} catch { /* ignore */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Pre-load OpenAI token at startup
|
|
205
|
+
try {
|
|
206
|
+
const t = await readOpenAIOAuthToken();
|
|
207
|
+
if (t) {
|
|
208
|
+
_oauthTokenCache["openai-oauth"] = t;
|
|
209
|
+
_oauthTokenTimestamp["openai-oauth"] = Date.now();
|
|
210
|
+
}
|
|
211
|
+
} catch { /* ignore */ }
|
|
212
|
+
|
|
213
|
+
function getOAuthTokenCached(providerId) {
|
|
214
|
+
const cachedAt = _oauthTokenTimestamp[providerId] || 0;
|
|
215
|
+
const age = Date.now() - cachedAt;
|
|
216
|
+
// Refresh if token is stale (older than TTL)
|
|
217
|
+
if (age > OAUTH_TOKEN_TTL_MS) {
|
|
218
|
+
if (providerId === "anthropic-oauth") refreshAnthropicOAuthToken();
|
|
219
|
+
else if (providerId === "openai-oauth") refreshOpenAIOAuthToken(); // async, best-effort
|
|
220
|
+
}
|
|
221
|
+
return _oauthTokenCache[providerId] || null;
|
|
222
|
+
}
|
|
223
|
+
|
|
111
224
|
const opencodeBase = process.env.OPENCODE_URL || "http://127.0.0.1:4096";
|
|
112
225
|
|
|
113
226
|
function resolveCommandPath(bin, extraPaths = []) {
|
|
@@ -134,6 +247,7 @@ function resolveCommandPath(bin, extraPaths = []) {
|
|
|
134
247
|
const out = execSync(`command -v "${candidate.replace(/"/g, '\\"')}"`, {
|
|
135
248
|
stdio: ["ignore", "pipe", "ignore"],
|
|
136
249
|
shell: "/bin/zsh",
|
|
250
|
+
timeout: 1500,
|
|
137
251
|
env: {
|
|
138
252
|
...process.env,
|
|
139
253
|
PATH: [
|
|
@@ -960,10 +1074,236 @@ Keep the same intent; make it specific enough for a PM to break into small tasks
|
|
|
960
1074
|
return content;
|
|
961
1075
|
}
|
|
962
1076
|
|
|
1077
|
+
function getRtAuthToken() {
|
|
1078
|
+
try {
|
|
1079
|
+
return readSwarmConfigSafe()?.rt?.authToken || "";
|
|
1080
|
+
} catch {
|
|
1081
|
+
return "";
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function resolvePlannerEngine(preferredEngine = null, preferredModel = null) {
|
|
1086
|
+
if (preferredEngine) {
|
|
1087
|
+
return {
|
|
1088
|
+
engine: preferredEngine,
|
|
1089
|
+
model: preferredModel || null,
|
|
1090
|
+
permissionMode: null,
|
|
1091
|
+
sandbox: preferredEngine === "codex" ? "read-only" : null,
|
|
1092
|
+
source: "request",
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const cfg = readSwarmConfigSafe();
|
|
1097
|
+
const agents = Array.isArray(cfg?.agents) ? cfg.agents : [];
|
|
1098
|
+
const pm = agents.find((agent) => agent?.id === "crew-pm") || {};
|
|
1099
|
+
|
|
1100
|
+
if (pm.useClaudeCode) {
|
|
1101
|
+
return {
|
|
1102
|
+
engine: "claude",
|
|
1103
|
+
model: pm.claudeCodeModel || null,
|
|
1104
|
+
// Claude's plan mode can exit 0 with no streamed text for this planner path.
|
|
1105
|
+
// Use the normal direct lane here; the build-planner prompt already forbids edits.
|
|
1106
|
+
permissionMode: null,
|
|
1107
|
+
sandbox: null,
|
|
1108
|
+
source: "crew-pm",
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
if (pm.useCodex) {
|
|
1112
|
+
return {
|
|
1113
|
+
engine: "codex",
|
|
1114
|
+
model: pm.codexModel || null,
|
|
1115
|
+
permissionMode: null,
|
|
1116
|
+
sandbox: "read-only",
|
|
1117
|
+
source: "crew-pm",
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
if (pm.useCursorCli) {
|
|
1121
|
+
return {
|
|
1122
|
+
engine: "cursor",
|
|
1123
|
+
model: pm.cursorCliModel || null,
|
|
1124
|
+
permissionMode: null,
|
|
1125
|
+
sandbox: null,
|
|
1126
|
+
source: "crew-pm",
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
if (pm.useGeminiCli) {
|
|
1130
|
+
return {
|
|
1131
|
+
engine: "gemini",
|
|
1132
|
+
model: pm.geminiCliModel || null,
|
|
1133
|
+
permissionMode: null,
|
|
1134
|
+
sandbox: null,
|
|
1135
|
+
source: "crew-pm",
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
if (pm.useCrewCLI) {
|
|
1139
|
+
return {
|
|
1140
|
+
engine: "crew-cli",
|
|
1141
|
+
model: pm.crewCliModel || null,
|
|
1142
|
+
permissionMode: null,
|
|
1143
|
+
sandbox: null,
|
|
1144
|
+
source: "crew-pm",
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
if (pm.useOpenCode) {
|
|
1148
|
+
return {
|
|
1149
|
+
engine: "opencode",
|
|
1150
|
+
model: pm.opencodeModel || null,
|
|
1151
|
+
permissionMode: null,
|
|
1152
|
+
sandbox: null,
|
|
1153
|
+
source: "crew-pm",
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const fallbacks = [
|
|
1158
|
+
commandExists(process.env.CLAUDE_CODE_BIN || "claude") && { engine: "claude", model: process.env.CREWSWARM_CLAUDE_CODE_MODEL || null, permissionMode: null, sandbox: null, source: "fallback" },
|
|
1159
|
+
commandExists(process.env.CODEX_CLI_BIN || "codex") && { engine: "codex", model: process.env.CREWSWARM_CODEX_MODEL || null, permissionMode: null, sandbox: "read-only", source: "fallback" },
|
|
1160
|
+
commandExists(process.env.CURSOR_CLI_BIN || path.join(os.homedir(), ".local", "bin", "agent"), [path.join(os.homedir(), ".local", "bin", "agent")]) && { engine: "cursor", model: process.env.CREWSWARM_CURSOR_MODEL || process.env.CURSOR_DEFAULT_MODEL || null, permissionMode: null, sandbox: null, source: "fallback" },
|
|
1161
|
+
commandExists(process.env.GEMINI_CLI_BIN || "gemini") && { engine: "gemini", model: process.env.CREWSWARM_GEMINI_CLI_MODEL || null, permissionMode: null, sandbox: null, source: "fallback" },
|
|
1162
|
+
commandExists(process.env.CREWSWARM_OPENCODE_BIN || "opencode") && { engine: "opencode", model: process.env.CREWSWARM_OPENCODE_MODEL || null, permissionMode: null, sandbox: null, source: "fallback" },
|
|
1163
|
+
].filter(Boolean);
|
|
1164
|
+
|
|
1165
|
+
return fallbacks[0] || null;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function buildRequirementPlanningPrompt(userText, projectDir = null) {
|
|
1169
|
+
return [
|
|
1170
|
+
"You are the planning stage for CrewSwarm's Build tab.",
|
|
1171
|
+
"Transform the user's rough build idea into a concrete build brief that crew-pm can execute.",
|
|
1172
|
+
"If repository context is relevant, inspect the workspace before answering. Do not edit files.",
|
|
1173
|
+
"",
|
|
1174
|
+
"Output format:",
|
|
1175
|
+
"## Build Brief",
|
|
1176
|
+
"A 1-2 paragraph concrete requirement with explicit scope and deliverables.",
|
|
1177
|
+
"",
|
|
1178
|
+
"## Acceptance Criteria",
|
|
1179
|
+
"- 3 to 7 flat bullets",
|
|
1180
|
+
"",
|
|
1181
|
+
"## Constraints / Assumptions",
|
|
1182
|
+
"- Flat bullets only when needed",
|
|
1183
|
+
"",
|
|
1184
|
+
"Rules:",
|
|
1185
|
+
"- Preserve the user's intent; do not invent a different product.",
|
|
1186
|
+
"- Make it specific enough for PM decomposition and agent dispatch.",
|
|
1187
|
+
"- Mention likely subsystems or files only if you have evidence from the repo.",
|
|
1188
|
+
"- Keep it concise and actionable.",
|
|
1189
|
+
"- Do not include implementation code, shell commands, or extra commentary.",
|
|
1190
|
+
projectDir ? `- Current project directory: ${projectDir}` : "",
|
|
1191
|
+
"",
|
|
1192
|
+
"User idea:",
|
|
1193
|
+
userText.trim(),
|
|
1194
|
+
].filter(Boolean).join("\n");
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async function collectClaudePlannerTextDirect({ message, projectDir, model = null }) {
|
|
1198
|
+
const claudeBin = resolveCommandPath(process.env.CLAUDE_CODE_BIN || "claude", [
|
|
1199
|
+
path.join(os.homedir(), ".local", "bin", "claude"),
|
|
1200
|
+
"/usr/local/bin/claude",
|
|
1201
|
+
"/opt/homebrew/bin/claude",
|
|
1202
|
+
]) || (process.env.CLAUDE_CODE_BIN || "claude");
|
|
1203
|
+
const { execFile } = await import("node:child_process");
|
|
1204
|
+
const { promisify } = await import("node:util");
|
|
1205
|
+
const execFileAsync = promisify(execFile);
|
|
1206
|
+
const args = ["-p"];
|
|
1207
|
+
if (projectDir) args.push("--add-dir", projectDir);
|
|
1208
|
+
if (model) args.push("--model", model);
|
|
1209
|
+
// Match the fixed engine-passthrough Claude invocation:
|
|
1210
|
+
// skip user MCP startup and terminate option parsing before the prompt.
|
|
1211
|
+
args.push(
|
|
1212
|
+
"--strict-mcp-config",
|
|
1213
|
+
"--mcp-config",
|
|
1214
|
+
path.join(os.homedir(), ".crewswarm", "config", "empty-mcp.json"),
|
|
1215
|
+
"--",
|
|
1216
|
+
message,
|
|
1217
|
+
);
|
|
1218
|
+
const { stdout, stderr } = await execFileAsync(claudeBin, args, {
|
|
1219
|
+
cwd: projectDir || process.cwd(),
|
|
1220
|
+
env: process.env,
|
|
1221
|
+
timeout: Number(process.env.CREWSWARM_PLANNER_TIMEOUT_MS || 300000),
|
|
1222
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
1223
|
+
});
|
|
1224
|
+
const trimmed = String(stdout || "").trim();
|
|
1225
|
+
if (trimmed) return trimmed;
|
|
1226
|
+
const stderrText = String(stderr || "").trim();
|
|
1227
|
+
if (stderrText) throw new Error(stderrText);
|
|
1228
|
+
throw new Error("planner produced no output");
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
async function collectPassthroughText({
|
|
1232
|
+
engine,
|
|
1233
|
+
message,
|
|
1234
|
+
projectDir,
|
|
1235
|
+
model = null,
|
|
1236
|
+
permissionMode = null,
|
|
1237
|
+
sandbox = null,
|
|
1238
|
+
forceL2 = false,
|
|
1239
|
+
}) {
|
|
1240
|
+
if (engine === "claude") {
|
|
1241
|
+
return collectClaudePlannerTextDirect({ message, projectDir, model });
|
|
1242
|
+
}
|
|
1243
|
+
const token = getRtAuthToken();
|
|
1244
|
+
const crewLeadPort = process.env.CREW_LEAD_PORT || "5010";
|
|
1245
|
+
let upstream;
|
|
1246
|
+
try {
|
|
1247
|
+
upstream = await fetch(`http://127.0.0.1:${crewLeadPort}/api/engine-passthrough`, {
|
|
1248
|
+
method: "POST",
|
|
1249
|
+
headers: {
|
|
1250
|
+
"content-type": "application/json",
|
|
1251
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
1252
|
+
"x-passthrough-continue": "false",
|
|
1253
|
+
},
|
|
1254
|
+
body: JSON.stringify({
|
|
1255
|
+
engine,
|
|
1256
|
+
message,
|
|
1257
|
+
projectDir: projectDir || process.cwd(),
|
|
1258
|
+
sessionId: "build-planner",
|
|
1259
|
+
...(model ? { model } : {}),
|
|
1260
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
1261
|
+
...(sandbox ? { sandbox } : {}),
|
|
1262
|
+
...(forceL2 ? { forceL2: true } : {}),
|
|
1263
|
+
}),
|
|
1264
|
+
signal: AbortSignal.timeout(300_000),
|
|
1265
|
+
});
|
|
1266
|
+
} catch (fetchErr) {
|
|
1267
|
+
throw new Error(`planner fetch failed for ${engine}: ${fetchErr.message}`);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
if (!upstream.ok) {
|
|
1271
|
+
throw new Error(`planner upstream ${upstream.status}`);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
let rawSSE;
|
|
1275
|
+
try {
|
|
1276
|
+
rawSSE = await upstream.text();
|
|
1277
|
+
} catch (readErr) {
|
|
1278
|
+
throw new Error(`planner SSE read failed for ${engine}: ${readErr.message}`);
|
|
1279
|
+
}
|
|
1280
|
+
let text = "";
|
|
1281
|
+
let stderr = "";
|
|
1282
|
+
let exitCode = 0;
|
|
1283
|
+
|
|
1284
|
+
for (const line of rawSSE.split("\n")) {
|
|
1285
|
+
if (!line.startsWith("data: ")) continue;
|
|
1286
|
+
try {
|
|
1287
|
+
const ev = JSON.parse(line.slice(6));
|
|
1288
|
+
if (ev.type === "chunk" && ev.text) text += ev.text;
|
|
1289
|
+
else if (ev.type === "stderr" && ev.text) stderr += ev.text;
|
|
1290
|
+
else if (ev.type === "done") exitCode = ev.exitCode ?? 0;
|
|
1291
|
+
} catch {}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const trimmed = text.trim();
|
|
1295
|
+
if (!trimmed && stderr.trim()) {
|
|
1296
|
+
throw new Error(stderr.trim());
|
|
1297
|
+
}
|
|
1298
|
+
if (!trimmed) {
|
|
1299
|
+
throw new Error(`planner produced no output${exitCode ? ` (exit ${exitCode})` : ""}`);
|
|
1300
|
+
}
|
|
1301
|
+
return trimmed;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
963
1304
|
async function getPhasedProgress(limit = 80) {
|
|
964
1305
|
const { readFile } = await import("node:fs/promises");
|
|
965
|
-
|
|
966
|
-
if (!existsSync(phasedDispatchLog)) return [];
|
|
1306
|
+
if (!(await fs.promises.access(phasedDispatchLog).then(() => true, () => false))) return [];
|
|
967
1307
|
try {
|
|
968
1308
|
const content = await readFile(phasedDispatchLog, "utf8");
|
|
969
1309
|
const lines = content.trim().split("\n").filter(Boolean).slice(-limit);
|
|
@@ -1116,8 +1456,41 @@ function serveStatic(req, res, filePath) {
|
|
|
1116
1456
|
}
|
|
1117
1457
|
}
|
|
1118
1458
|
|
|
1459
|
+
/**
|
|
1460
|
+
* Make an HTTP request to crew-lead using http.request with agent:false.
|
|
1461
|
+
* This bypasses Node 25's undici connection pool used by fetch(), which gets
|
|
1462
|
+
* blocked when Chrome SSE EventSource connections saturate the pool.
|
|
1463
|
+
*/
|
|
1464
|
+
function crewLeadRequest(path, { method = "GET", body = null, timeout = 10000, port } = {}) {
|
|
1465
|
+
const crewPort = port || process.env.CREW_LEAD_PORT || "5010";
|
|
1466
|
+
return new Promise((resolve, reject) => {
|
|
1467
|
+
const options = {
|
|
1468
|
+
hostname: "127.0.0.1",
|
|
1469
|
+
port: parseInt(crewPort, 10),
|
|
1470
|
+
path,
|
|
1471
|
+
method,
|
|
1472
|
+
timeout,
|
|
1473
|
+
agent: false,
|
|
1474
|
+
headers: { "content-type": "application/json", connection: "close" },
|
|
1475
|
+
};
|
|
1476
|
+
const req = http.request(options, (res) => {
|
|
1477
|
+
let d = "";
|
|
1478
|
+
res.on("data", (chunk) => (d += chunk));
|
|
1479
|
+
res.on("end", () => {
|
|
1480
|
+
try { resolve({ ok: res.statusCode < 400, status: res.statusCode, data: JSON.parse(d) }); }
|
|
1481
|
+
catch { resolve({ ok: res.statusCode < 400, status: res.statusCode, data: d }); }
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
req.on("error", (err) => reject(err));
|
|
1485
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("crew-lead request timeout")); });
|
|
1486
|
+
if (body) req.write(typeof body === "string" ? body : JSON.stringify(body));
|
|
1487
|
+
req.end();
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1119
1491
|
const server = http.createServer(async (req, res) => {
|
|
1120
1492
|
const url = new URL(req.url || "/", `http://localhost:${listenPort}`);
|
|
1493
|
+
const exists = (p) => fs.promises.access(p).then(() => true, () => false);
|
|
1121
1494
|
|
|
1122
1495
|
// CORS: echo local dev Origins so http://localhost:3333 (Vibe) works with http://127.0.0.1:4319
|
|
1123
1496
|
// (browsers treat hostnames as distinct origins). Non-local callers still get *.
|
|
@@ -1173,7 +1546,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1173
1546
|
if (url.pathname === "/crew-chat.html") {
|
|
1174
1547
|
const chatFile = path.join(CREWSWARM_DIR, "crew-chat.html");
|
|
1175
1548
|
try {
|
|
1176
|
-
const chatHtml = fs.
|
|
1549
|
+
const chatHtml = await fs.promises.readFile(chatFile, "utf8");
|
|
1177
1550
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1178
1551
|
res.end(chatHtml);
|
|
1179
1552
|
} catch {
|
|
@@ -1185,7 +1558,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1185
1558
|
if (url.pathname === "/signup" || url.pathname === "/signup.html") {
|
|
1186
1559
|
const signupFile = path.join(FRONTEND_SRC, "public", "signup.html");
|
|
1187
1560
|
try {
|
|
1188
|
-
const signupHtml = fs.
|
|
1561
|
+
const signupHtml = await fs.promises.readFile(signupFile, "utf8");
|
|
1189
1562
|
res.writeHead(200, {
|
|
1190
1563
|
"content-type": "text/html; charset=utf-8",
|
|
1191
1564
|
"cache-control": "no-store, no-cache, must-revalidate",
|
|
@@ -1198,6 +1571,333 @@ const server = http.createServer(async (req, res) => {
|
|
|
1198
1571
|
}
|
|
1199
1572
|
return;
|
|
1200
1573
|
}
|
|
1574
|
+
// ── Test results API ─────────────────────────────────────────────────────
|
|
1575
|
+
if (url.pathname === "/api/tests/summary" && req.method === "GET") {
|
|
1576
|
+
const resultsDir = path.join(CREWSWARM_DIR, "test-results");
|
|
1577
|
+
const runsDir = path.join(resultsDir, "runs");
|
|
1578
|
+
try {
|
|
1579
|
+
const allRuns = (await fs.promises.readdir(runsDir)).filter(d => /^\d{4}-/.test(d)).sort().reverse();
|
|
1580
|
+
// Collect the last run per suite type (unit, integration, e2e, all)
|
|
1581
|
+
// by inspecting run.json test_command
|
|
1582
|
+
async function detectSuite(runDir) {
|
|
1583
|
+
try {
|
|
1584
|
+
const r = JSON.parse(await fs.promises.readFile(path.join(runDir, "run.json"), "utf8"));
|
|
1585
|
+
const cmd = r.test_command || "";
|
|
1586
|
+
if (cmd.includes("test/e2e/") || cmd.includes("test:e2e")) return "e2e";
|
|
1587
|
+
if (cmd.includes("test/integration/")) return "integration";
|
|
1588
|
+
if (cmd.includes("test/unit/")) {
|
|
1589
|
+
// If it has ONLY unit tests, it's a unit run; if mixed, it's "all"
|
|
1590
|
+
if (cmd.includes("test/integration/") || cmd.includes("test/e2e/")) return "all";
|
|
1591
|
+
return "unit";
|
|
1592
|
+
}
|
|
1593
|
+
// Check file count heuristic: >100 files = probably "all"
|
|
1594
|
+
const fileCount = (cmd.match(/\.test\.mjs/g) || []).length;
|
|
1595
|
+
if (fileCount > 100) return "all";
|
|
1596
|
+
if (fileCount > 15) return "unit";
|
|
1597
|
+
return "unknown";
|
|
1598
|
+
} catch { return "unknown"; }
|
|
1599
|
+
}
|
|
1600
|
+
async function readRunData(runId) {
|
|
1601
|
+
const runDir = path.join(runsDir, runId);
|
|
1602
|
+
const summaryFile = path.join(runDir, "summary.json");
|
|
1603
|
+
let data;
|
|
1604
|
+
if (await exists(summaryFile)) {
|
|
1605
|
+
data = JSON.parse(await fs.promises.readFile(summaryFile, "utf8"));
|
|
1606
|
+
} else {
|
|
1607
|
+
// Fall back to .last-run.json if this is the matching run
|
|
1608
|
+
const lastRunFile = path.join(resultsDir, ".last-run.json");
|
|
1609
|
+
try {
|
|
1610
|
+
const lr = JSON.parse(await fs.promises.readFile(lastRunFile, "utf8"));
|
|
1611
|
+
if (lr.runId === runId) { data = lr; }
|
|
1612
|
+
} catch {}
|
|
1613
|
+
}
|
|
1614
|
+
if (!data) {
|
|
1615
|
+
// Fall back to counting test artifact dirs + failure.json presence
|
|
1616
|
+
let timestamp = null;
|
|
1617
|
+
try { const r = JSON.parse(await fs.promises.readFile(path.join(runDir, "run.json"), "utf8")); timestamp = r.timestamp; } catch {}
|
|
1618
|
+
const ents = await fs.promises.readdir(runDir);
|
|
1619
|
+
const testDirs = ents.filter(e => !e.endsWith(".json") && !e.startsWith("."));
|
|
1620
|
+
let failed = 0;
|
|
1621
|
+
for (const td of testDirs) { if (await exists(path.join(runDir, td, "failure.json"))) failed++; }
|
|
1622
|
+
data = { runId, timestamp, passed: testDirs.length - failed, failed, skipped: 0, total: testDirs.length, duration_ms: 0 };
|
|
1623
|
+
}
|
|
1624
|
+
data.runId = runId;
|
|
1625
|
+
data.suite = await detectSuite(runDir);
|
|
1626
|
+
// Read failures and skips if available
|
|
1627
|
+
const failFile = path.join(runDir, "failures.json");
|
|
1628
|
+
const skipFile = path.join(runDir, "skips.json");
|
|
1629
|
+
let failures = [];
|
|
1630
|
+
let skips = [];
|
|
1631
|
+
try { failures = JSON.parse(await fs.promises.readFile(failFile, "utf8")); } catch {}
|
|
1632
|
+
try { skips = JSON.parse(await fs.promises.readFile(skipFile, "utf8")); } catch {}
|
|
1633
|
+
data.failures = failures.map(f => ({
|
|
1634
|
+
name: f.name || f.testId || "",
|
|
1635
|
+
file: (f.file_fingerprint?.relative_file || f.file || "").replace(CREWSWARM_DIR + "/", ""),
|
|
1636
|
+
error: typeof f.error === "string" ? f.error : (f.error?.message || f.error?.failureType || JSON.stringify(f.error || "").slice(0, 300)),
|
|
1637
|
+
classification: f.classification || "unknown",
|
|
1638
|
+
rerun_command: f.rerun_command || f.selector?.command || "",
|
|
1639
|
+
}));
|
|
1640
|
+
data.skips = skips.map(s => ({
|
|
1641
|
+
name: s.name || s.testId || "",
|
|
1642
|
+
file: (s.file_fingerprint?.relative_file || s.file || "").replace(CREWSWARM_DIR + "/", ""),
|
|
1643
|
+
}));
|
|
1644
|
+
return data;
|
|
1645
|
+
}
|
|
1646
|
+
// Find latest run for each suite type
|
|
1647
|
+
const suiteRuns = {};
|
|
1648
|
+
let latestOverall = null;
|
|
1649
|
+
for (const runId of allRuns) {
|
|
1650
|
+
if (Object.keys(suiteRuns).length >= 4 && latestOverall) break;
|
|
1651
|
+
try {
|
|
1652
|
+
const runDir = path.join(runsDir, runId);
|
|
1653
|
+
const suite = await detectSuite(runDir);
|
|
1654
|
+
if (!latestOverall) latestOverall = runId;
|
|
1655
|
+
if (!suiteRuns[suite]) suiteRuns[suite] = runId;
|
|
1656
|
+
} catch {}
|
|
1657
|
+
}
|
|
1658
|
+
// Build per-suite summaries
|
|
1659
|
+
const suites = {};
|
|
1660
|
+
for (const [suite, runId] of Object.entries(suiteRuns)) {
|
|
1661
|
+
try { suites[suite] = await readRunData(runId); } catch { /* skip broken runs */ }
|
|
1662
|
+
}
|
|
1663
|
+
// Count test files on disk for reference
|
|
1664
|
+
const testFileDir = path.join(CREWSWARM_DIR, "test");
|
|
1665
|
+
const testsE2eDir = path.join(CREWSWARM_DIR, "tests", "e2e");
|
|
1666
|
+
const crewCliTestDir = path.join(CREWSWARM_DIR, "crew-cli", "tests");
|
|
1667
|
+
const crewCliTestDir2 = path.join(CREWSWARM_DIR, "crew-cli", "test");
|
|
1668
|
+
let unitFiles = 0, intFiles = 0, e2eFiles = 0, playwrightFiles = 0, crewCliFiles = 0, rootFiles = 0;
|
|
1669
|
+
try { unitFiles = (await fs.promises.readdir(path.join(testFileDir, "unit"))).filter(f => f.endsWith(".test.mjs")).length; } catch {}
|
|
1670
|
+
try { intFiles = (await fs.promises.readdir(path.join(testFileDir, "integration"))).filter(f => f.endsWith(".test.mjs")).length; } catch {}
|
|
1671
|
+
try { e2eFiles = (await fs.promises.readdir(path.join(testFileDir, "e2e"))).filter(f => f.endsWith(".test.mjs")).length; } catch {}
|
|
1672
|
+
try { playwrightFiles = (await fs.promises.readdir(testsE2eDir)).filter(f => f.endsWith(".spec.js")).length; } catch {}
|
|
1673
|
+
try { rootFiles = (await fs.promises.readdir(testFileDir)).filter(f => f.match(/\.test\./)).length; } catch {}
|
|
1674
|
+
// crew-cli: tests/unit/*.test.js + tests/*.test.js + test/*.test.ts
|
|
1675
|
+
try {
|
|
1676
|
+
const unitDir = path.join(crewCliTestDir, "unit");
|
|
1677
|
+
crewCliFiles += (await fs.promises.readdir(unitDir)).filter(f => f.match(/\.test\./)).length;
|
|
1678
|
+
} catch {}
|
|
1679
|
+
try { crewCliFiles += (await fs.promises.readdir(crewCliTestDir)).filter(f => f.match(/\.test\./)).length; } catch {}
|
|
1680
|
+
try { crewCliFiles += (await fs.promises.readdir(crewCliTestDir2)).filter(f => f.match(/\.test\./)).length; } catch {}
|
|
1681
|
+
// Remove "unknown" suite if it has no data
|
|
1682
|
+
if (suites.unknown && (!suites.unknown.total || suites.unknown.total === 0)) delete suites.unknown;
|
|
1683
|
+
let latest = null;
|
|
1684
|
+
try { if (latestOverall) latest = await readRunData(latestOverall); } catch {}
|
|
1685
|
+
// Count actual test()/it() calls in source files (ground truth)
|
|
1686
|
+
async function countTestCalls(dir, pattern) {
|
|
1687
|
+
let count = 0;
|
|
1688
|
+
try {
|
|
1689
|
+
const files = (await fs.promises.readdir(dir)).filter(f => f.match(pattern));
|
|
1690
|
+
for (const f of files) {
|
|
1691
|
+
const src = await fs.promises.readFile(path.join(dir, f), "utf8");
|
|
1692
|
+
const matches = src.match(/^\s*(test|it)\s*\(/gm);
|
|
1693
|
+
if (matches) count += matches.length;
|
|
1694
|
+
}
|
|
1695
|
+
} catch {}
|
|
1696
|
+
return count;
|
|
1697
|
+
}
|
|
1698
|
+
async function countTestCallsRecursive(dir) {
|
|
1699
|
+
let count = 0;
|
|
1700
|
+
try {
|
|
1701
|
+
for (const ent of await fs.promises.readdir(dir, { withFileTypes: true })) {
|
|
1702
|
+
if (ent.isDirectory()) { count += countTestCallsRecursive(path.join(dir, ent.name)); continue; }
|
|
1703
|
+
if (!ent.name.match(/\.test\./)) continue;
|
|
1704
|
+
const src = await fs.promises.readFile(path.join(dir, ent.name), "utf8");
|
|
1705
|
+
const matches = src.match(/^\s*(test|it)\s*\(/gm);
|
|
1706
|
+
if (matches) count += matches.length;
|
|
1707
|
+
}
|
|
1708
|
+
} catch {}
|
|
1709
|
+
return count;
|
|
1710
|
+
}
|
|
1711
|
+
const testCounts = {
|
|
1712
|
+
unit: countTestCalls(path.join(testFileDir, "unit"), /\.test\.mjs$/),
|
|
1713
|
+
integration: countTestCalls(path.join(testFileDir, "integration"), /\.test\.mjs$/),
|
|
1714
|
+
e2e: countTestCalls(path.join(testFileDir, "e2e"), /\.test\.mjs$/),
|
|
1715
|
+
playwright: countTestCalls(testsE2eDir, /\.spec\.js$/),
|
|
1716
|
+
"crew-cli": countTestCallsRecursive(crewCliTestDir) + countTestCallsRecursive(crewCliTestDir2),
|
|
1717
|
+
root: countTestCalls(testFileDir, /\.test\./),
|
|
1718
|
+
};
|
|
1719
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1720
|
+
res.end(JSON.stringify({
|
|
1721
|
+
latest,
|
|
1722
|
+
suites,
|
|
1723
|
+
fileCounts: { unit: unitFiles, integration: intFiles, e2e: e2eFiles, playwright: playwrightFiles, "crew-cli": crewCliFiles, root: rootFiles, total: unitFiles + intFiles + e2eFiles + playwrightFiles + crewCliFiles + rootFiles },
|
|
1724
|
+
testCounts,
|
|
1725
|
+
}));
|
|
1726
|
+
} catch (e) {
|
|
1727
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1728
|
+
res.end(JSON.stringify({ latest: null, suites: {}, fileCounts: {}, error: e.message }));
|
|
1729
|
+
}
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
if (url.pathname === "/api/tests/history" && req.method === "GET") {
|
|
1733
|
+
const resultsDir = path.join(CREWSWARM_DIR, "test-results");
|
|
1734
|
+
const runsDir = path.join(resultsDir, "runs");
|
|
1735
|
+
try {
|
|
1736
|
+
const dirs = (await fs.promises.readdir(runsDir)).filter(d => /^\d{4}-/.test(d)).sort().reverse().slice(0, 40);
|
|
1737
|
+
const history = [];
|
|
1738
|
+
for (const d of dirs) {
|
|
1739
|
+
const runDir = path.join(runsDir, d);
|
|
1740
|
+
try {
|
|
1741
|
+
let entry = null;
|
|
1742
|
+
// Prefer summary.json (has all data)
|
|
1743
|
+
const summaryFile = path.join(runDir, "summary.json");
|
|
1744
|
+
if (await exists(summaryFile)) {
|
|
1745
|
+
const s = JSON.parse(await fs.promises.readFile(summaryFile, "utf8"));
|
|
1746
|
+
entry = { runId: d, timestamp: s.timestamp, status: s.status || (s.failed > 0 ? "failed" : "passed"), passed: s.passed || 0, failed: s.failed || 0, skipped: s.skipped || 0, total: s.total || 0, duration_ms: s.duration_ms || 0 };
|
|
1747
|
+
} else {
|
|
1748
|
+
const runMeta = JSON.parse(await fs.promises.readFile(path.join(runDir, "run.json"), "utf8"));
|
|
1749
|
+
const ents = await fs.promises.readdir(runDir);
|
|
1750
|
+
const testDirs = ents.filter(e => !e.endsWith(".json") && !e.startsWith("."));
|
|
1751
|
+
let failed = 0;
|
|
1752
|
+
for (const td of testDirs) { if (await exists(path.join(runDir, td, "failure.json"))) failed++; }
|
|
1753
|
+
entry = { runId: d, timestamp: runMeta.timestamp, status: failed > 0 ? "failed" : "passed", passed: testDirs.length - failed, failed, skipped: 0, total: testDirs.length, duration_ms: 0 };
|
|
1754
|
+
}
|
|
1755
|
+
// Detect suite from test_command
|
|
1756
|
+
try {
|
|
1757
|
+
const r = JSON.parse(await fs.promises.readFile(path.join(runDir, "run.json"), "utf8"));
|
|
1758
|
+
const cmd = r.test_command || "";
|
|
1759
|
+
if (cmd.includes("test/e2e/")) entry.suite = "e2e";
|
|
1760
|
+
else if (cmd.includes("test/integration/")) entry.suite = "integration";
|
|
1761
|
+
else if (cmd.includes("test/unit/") && !cmd.includes("test/integration/")) entry.suite = "unit";
|
|
1762
|
+
else { const fc = (cmd.match(/\.test\.mjs/g) || []).length; entry.suite = fc > 100 ? "all" : fc > 15 ? "unit" : "unknown"; }
|
|
1763
|
+
} catch { entry.suite = "unknown"; }
|
|
1764
|
+
history.push(entry);
|
|
1765
|
+
} catch { /* skip */ }
|
|
1766
|
+
}
|
|
1767
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1768
|
+
res.end(JSON.stringify({ history }));
|
|
1769
|
+
} catch (e) {
|
|
1770
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1771
|
+
res.end(JSON.stringify({ history: [], error: e.message }));
|
|
1772
|
+
}
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (url.pathname === "/api/tests/run-detail" && req.method === "GET") {
|
|
1776
|
+
const resultsDir = path.join(CREWSWARM_DIR, "test-results");
|
|
1777
|
+
const runsDir = path.join(resultsDir, "runs");
|
|
1778
|
+
const runId = url.searchParams.get("runId");
|
|
1779
|
+
if (!runId) { res.writeHead(400, { "content-type": "application/json" }); res.end(JSON.stringify({ error: "Missing runId" })); return; }
|
|
1780
|
+
try {
|
|
1781
|
+
const runDir = path.join(runsDir, runId);
|
|
1782
|
+
// Get timestamp from run.json
|
|
1783
|
+
let timestamp = null;
|
|
1784
|
+
try { const r = JSON.parse(await fs.promises.readFile(path.join(runDir, "run.json"), "utf8")); timestamp = r.timestamp; } catch {}
|
|
1785
|
+
// Try summary.json for totals
|
|
1786
|
+
let passed = 0, failed = 0, skipped = 0, total = 0, duration_ms = 0;
|
|
1787
|
+
const summaryFile = path.join(runDir, "summary.json");
|
|
1788
|
+
if (await exists(summaryFile)) {
|
|
1789
|
+
const s = JSON.parse(await fs.promises.readFile(summaryFile, "utf8"));
|
|
1790
|
+
passed = s.passed || 0; failed = s.failed || 0; skipped = s.skipped || 0; total = s.total || 0; duration_ms = s.duration_ms || 0;
|
|
1791
|
+
if (!timestamp) timestamp = s.timestamp;
|
|
1792
|
+
}
|
|
1793
|
+
// If no summary, count from test artifact dirs
|
|
1794
|
+
if (total === 0) {
|
|
1795
|
+
const ents = await fs.promises.readdir(runDir);
|
|
1796
|
+
const testDirs = ents.filter(e => !e.endsWith(".json") && !e.startsWith("."));
|
|
1797
|
+
for (const td of testDirs) {
|
|
1798
|
+
total++;
|
|
1799
|
+
if (await exists(path.join(runDir, td, "failure.json"))) failed++;
|
|
1800
|
+
else passed++;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
// Read failures.json and skips.json
|
|
1804
|
+
let failures = [], skips = [];
|
|
1805
|
+
try { failures = JSON.parse(await fs.promises.readFile(path.join(runDir, "failures.json"), "utf8")); } catch {}
|
|
1806
|
+
try { skips = JSON.parse(await fs.promises.readFile(path.join(runDir, "skips.json"), "utf8")); } catch {}
|
|
1807
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1808
|
+
res.end(JSON.stringify({
|
|
1809
|
+
runId, timestamp, passed, failed, skipped, total, duration_ms,
|
|
1810
|
+
failures: failures.map(f => {
|
|
1811
|
+
let err = f.error;
|
|
1812
|
+
if (typeof err === "object" && err !== null) err = err.message || err.failureType || JSON.stringify(err).slice(0, 300);
|
|
1813
|
+
return {
|
|
1814
|
+
name: f.name || f.testId || "", file: (f.file_fingerprint?.relative_file || f.file || "").replace(CREWSWARM_DIR + "/", ""),
|
|
1815
|
+
error: String(err || ""),
|
|
1816
|
+
rerun_command: f.rerun_command || f.selector?.command || "",
|
|
1817
|
+
};
|
|
1818
|
+
}),
|
|
1819
|
+
skips: skips.map(s => ({
|
|
1820
|
+
name: s.name || s.testId || "", file: (s.file_fingerprint?.relative_file || s.file || "").replace(CREWSWARM_DIR + "/", ""),
|
|
1821
|
+
})),
|
|
1822
|
+
}));
|
|
1823
|
+
} catch (e) {
|
|
1824
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1825
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
1826
|
+
}
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (url.pathname === "/api/tests/run" && req.method === "POST") {
|
|
1830
|
+
let body = "";
|
|
1831
|
+
req.on("data", (c) => (body += c));
|
|
1832
|
+
req.on("end", async () => {
|
|
1833
|
+
let suite = "test:unit";
|
|
1834
|
+
try { const parsed = JSON.parse(body); suite = parsed.suite || suite; } catch { /* default */ }
|
|
1835
|
+
const allowed = ["test:unit", "test:integration", "test:e2e", "test:all", "test", "test:e2e:vibe"];
|
|
1836
|
+
if (!allowed.includes(suite)) {
|
|
1837
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
1838
|
+
res.end(JSON.stringify({ error: "Invalid suite: " + suite }));
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const { spawn } = await import("node:child_process");
|
|
1842
|
+
const progressFile = path.join(CREWSWARM_DIR, "test-results", ".test-progress.json");
|
|
1843
|
+
const outputFile = path.join(CREWSWARM_DIR, "test-results", ".test-output.log");
|
|
1844
|
+
// Write initial progress
|
|
1845
|
+
await fs.promises.writeFile(progressFile, JSON.stringify({ suite, running: true, pid: 0, started: Date.now(), passed: 0, failed: 0, skipped: 0, files_done: 0, current_file: "" }));
|
|
1846
|
+
const outFd = fs.openSync(outputFile, "w");
|
|
1847
|
+
const child = spawn("npm", ["run", suite], { cwd: CREWSWARM_DIR, stdio: ["ignore", outFd, outFd], detached: true });
|
|
1848
|
+
child.unref();
|
|
1849
|
+
fs.closeSync(outFd);
|
|
1850
|
+
// Update progress by tailing the output file
|
|
1851
|
+
const progressInterval = setInterval(() => {
|
|
1852
|
+
try {
|
|
1853
|
+
const out = fs.readFileSync(outputFile, "utf8");
|
|
1854
|
+
const lines = out.split("\n");
|
|
1855
|
+
let passed = 0, failed = 0, skipped = 0, files_done = 0, current_file = "";
|
|
1856
|
+
for (const line of lines) {
|
|
1857
|
+
if (line.startsWith(" ✓ ")) passed++;
|
|
1858
|
+
else if (line.startsWith(" ✗ ")) failed++;
|
|
1859
|
+
else if (line.includes("(skipped)")) skipped++;
|
|
1860
|
+
else if (line.endsWith(".test.mjs") || line.endsWith(".test.ts") || line.endsWith(".test.js")) {
|
|
1861
|
+
files_done++;
|
|
1862
|
+
current_file = line.trim();
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
fs.writeFileSync(progressFile, JSON.stringify({ suite, running: true, pid: child.pid, started: JSON.parse(fs.readFileSync(progressFile, "utf8")).started, passed, failed, skipped, files_done, current_file }));
|
|
1866
|
+
} catch { /* file may not exist yet */ }
|
|
1867
|
+
}, 2000);
|
|
1868
|
+
child.on("exit", (code) => {
|
|
1869
|
+
clearInterval(progressInterval);
|
|
1870
|
+
try {
|
|
1871
|
+
const out = fs.readFileSync(outputFile, "utf8");
|
|
1872
|
+
const lines = out.split("\n");
|
|
1873
|
+
let passed = 0, failed = 0, skipped = 0, files_done = 0;
|
|
1874
|
+
for (const line of lines) {
|
|
1875
|
+
if (line.startsWith(" ✓ ")) passed++;
|
|
1876
|
+
else if (line.startsWith(" ✗ ")) failed++;
|
|
1877
|
+
else if (line.includes("(skipped)")) skipped++;
|
|
1878
|
+
else if (line.endsWith(".test.mjs") || line.endsWith(".test.ts") || line.endsWith(".test.js")) files_done++;
|
|
1879
|
+
}
|
|
1880
|
+
fs.writeFileSync(progressFile, JSON.stringify({ suite, running: false, pid: child.pid, started: JSON.parse(fs.readFileSync(progressFile, "utf8")).started, finished: Date.now(), exitCode: code, passed, failed, skipped, files_done, current_file: "" }));
|
|
1881
|
+
} catch {}
|
|
1882
|
+
});
|
|
1883
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1884
|
+
res.end(JSON.stringify({ started: true, suite, pid: child.pid }));
|
|
1885
|
+
});
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
if (url.pathname === "/api/tests/progress" && req.method === "GET") {
|
|
1889
|
+
const progressFile = path.join(CREWSWARM_DIR, "test-results", ".test-progress.json");
|
|
1890
|
+
try {
|
|
1891
|
+
const data = JSON.parse(await fs.promises.readFile(progressFile, "utf8"));
|
|
1892
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1893
|
+
res.end(JSON.stringify(data));
|
|
1894
|
+
} catch {
|
|
1895
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1896
|
+
res.end(JSON.stringify({ running: false }));
|
|
1897
|
+
}
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1201
1901
|
// ── First-run detection ──────────────────────────────────────────────────
|
|
1202
1902
|
if (url.pathname === "/api/first-run-status" && req.method === "GET") {
|
|
1203
1903
|
const cfg = readSwarmConfigSafe();
|
|
@@ -1221,16 +1921,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
1221
1921
|
|
|
1222
1922
|
const hasApiKeys = configuredProviders.length > 0;
|
|
1223
1923
|
|
|
1224
|
-
// Check crew-lead health
|
|
1924
|
+
// Check crew-lead health (uses http.request, not fetch — avoids undici pool saturation from SSE)
|
|
1225
1925
|
let crewLeadUp = false;
|
|
1226
1926
|
try {
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
const resp = await fetch("http://127.0.0.1:5010/health", {
|
|
1230
|
-
signal: controller.signal,
|
|
1231
|
-
});
|
|
1232
|
-
clearTimeout(timer);
|
|
1233
|
-
crewLeadUp = resp.ok;
|
|
1927
|
+
const { ok } = await crewLeadRequest("/health", { timeout: 2000 });
|
|
1928
|
+
crewLeadUp = ok;
|
|
1234
1929
|
} catch {
|
|
1235
1930
|
crewLeadUp = false;
|
|
1236
1931
|
}
|
|
@@ -1280,11 +1975,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
1280
1975
|
const engine = String(url.searchParams.get("engine") || "opencode").trim();
|
|
1281
1976
|
const limit = Math.min(Number(url.searchParams.get("limit") || "20"), 50);
|
|
1282
1977
|
const projectId = String(url.searchParams.get("projectId") || "").trim();
|
|
1283
|
-
const projectDir = (() => {
|
|
1978
|
+
const projectDir = await (async () => {
|
|
1284
1979
|
if (!projectId || projectId === "general") return "";
|
|
1285
1980
|
try {
|
|
1286
1981
|
const registryFile = path.join(CFG_DIR, "projects.json");
|
|
1287
|
-
const projects = JSON.parse(fs.
|
|
1982
|
+
const projects = JSON.parse(await fs.promises.readFile(registryFile, "utf8"));
|
|
1288
1983
|
return String(projects?.[projectId]?.outputDir || "").trim();
|
|
1289
1984
|
} catch {
|
|
1290
1985
|
return "";
|
|
@@ -1333,19 +2028,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
1333
2028
|
const limit = Math.min(Number(url.searchParams.get("limit") || "20"), 50);
|
|
1334
2029
|
const sessionsBase = path.join(os.homedir(), ".codex", "sessions");
|
|
1335
2030
|
const sessions = [];
|
|
1336
|
-
if (
|
|
1337
|
-
const years = fs.
|
|
2031
|
+
if (await exists(sessionsBase)) {
|
|
2032
|
+
const years = (await fs.promises.readdir(sessionsBase)).filter(d => /^\d{4}$/.test(d));
|
|
1338
2033
|
for (const year of years.sort().reverse()) {
|
|
1339
2034
|
const yearDir = path.join(sessionsBase, year);
|
|
1340
|
-
const months = fs.
|
|
2035
|
+
const months = (await fs.promises.readdir(yearDir)).filter(d => /^\d{2}$/.test(d));
|
|
1341
2036
|
for (const month of months.sort().reverse()) {
|
|
1342
2037
|
const monthDir = path.join(yearDir, month);
|
|
1343
|
-
const days = fs.
|
|
2038
|
+
const days = (await fs.promises.readdir(monthDir)).filter(d => /^\d{2}$/.test(d));
|
|
1344
2039
|
for (const day of days.sort().reverse()) {
|
|
1345
2040
|
const dayDir = path.join(yearDir, month, day);
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
.map(f => ({ f, mt: fs.statSync(path.join(dayDir, f)).mtimeMs }))
|
|
2041
|
+
const rawFiles = (await fs.promises.readdir(dayDir)).filter(f => f.endsWith(".jsonl"));
|
|
2042
|
+
const files = (await Promise.all(rawFiles.map(async f => ({ f, mt: (await fs.promises.stat(path.join(dayDir, f))).mtimeMs }))))
|
|
1349
2043
|
.sort((a, b) => b.mt - a.mt);
|
|
1350
2044
|
for (const { f } of files) {
|
|
1351
2045
|
if (sessions.length >= limit) break;
|
|
@@ -1353,7 +2047,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1353
2047
|
const filePath = path.join(dayDir, f);
|
|
1354
2048
|
|
|
1355
2049
|
// Skip files larger than 10MB to avoid memory issues
|
|
1356
|
-
const stats = fs.
|
|
2050
|
+
const stats = await fs.promises.stat(filePath);
|
|
1357
2051
|
if (stats.size > 10 * 1024 * 1024) {
|
|
1358
2052
|
sessions.push({
|
|
1359
2053
|
id: sessionId,
|
|
@@ -1365,7 +2059,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1365
2059
|
}
|
|
1366
2060
|
|
|
1367
2061
|
const messages = [];
|
|
1368
|
-
const allLines = fs.
|
|
2062
|
+
const allLines = (await fs.promises.readFile(filePath, "utf8")).trim().split("\n");
|
|
1369
2063
|
const lines = allLines.slice(-100); // Last 100 lines only
|
|
1370
2064
|
let firstUserMsg = "";
|
|
1371
2065
|
for (const line of lines) {
|
|
@@ -1405,22 +2099,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
1405
2099
|
const qDir = url.searchParams.get("dir") || process.cwd();
|
|
1406
2100
|
const dirKey = qDir.replace(/\//g, "-");
|
|
1407
2101
|
const projectsBase = path.join(os.homedir(), ".claude", "projects");
|
|
1408
|
-
const candidates =
|
|
1409
|
-
? fs.
|
|
2102
|
+
const candidates = await exists(projectsBase)
|
|
2103
|
+
? (await fs.promises.readdir(projectsBase)).filter(d => d === dirKey || d.endsWith(dirKey.split("-").slice(-2).join("-")))
|
|
1410
2104
|
: [];
|
|
1411
2105
|
const sessions = [];
|
|
1412
2106
|
for (const cand of candidates) {
|
|
1413
2107
|
const sessDir = path.join(projectsBase, cand);
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
.map(f => ({ f, mt: fs.statSync(path.join(sessDir, f)).mtimeMs }))
|
|
2108
|
+
const rawSessFiles = (await fs.promises.readdir(sessDir)).filter(f => f.endsWith(".jsonl"));
|
|
2109
|
+
const files = (await Promise.all(rawSessFiles.map(async f => ({ f, mt: (await fs.promises.stat(path.join(sessDir, f))).mtimeMs }))))
|
|
1417
2110
|
.sort((a, b) => b.mt - a.mt)
|
|
1418
2111
|
.slice(0, limit)
|
|
1419
2112
|
.map(x => x.f);
|
|
1420
2113
|
for (const file of files) {
|
|
1421
2114
|
const sessionId = file.replace(".jsonl", "");
|
|
1422
2115
|
const messages = [];
|
|
1423
|
-
const lines = fs.
|
|
2116
|
+
const lines = (await fs.promises.readFile(path.join(sessDir, file), "utf8")).trim().split("\n");
|
|
1424
2117
|
for (const line of lines) {
|
|
1425
2118
|
try {
|
|
1426
2119
|
const d = JSON.parse(line);
|
|
@@ -1444,13 +2137,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
1444
2137
|
const limit = Math.min(Number(url.searchParams.get("limit") || "20"), 50);
|
|
1445
2138
|
const historyBase = path.join(os.homedir(), ".gemini", "history");
|
|
1446
2139
|
const sessions = [];
|
|
1447
|
-
if (
|
|
1448
|
-
const projects = fs.
|
|
2140
|
+
if (await exists(historyBase)) {
|
|
2141
|
+
const projects = await fs.promises.readdir(historyBase);
|
|
1449
2142
|
for (const proj of projects) {
|
|
1450
2143
|
const sessionFile = path.join(historyBase, proj, "session.jsonl");
|
|
1451
|
-
if (
|
|
2144
|
+
if (await exists(sessionFile)) {
|
|
1452
2145
|
const messages = [];
|
|
1453
|
-
const lines = fs.
|
|
2146
|
+
const lines = (await fs.promises.readFile(sessionFile, "utf8")).trim().split("\n");
|
|
1454
2147
|
let firstUserMsg = "";
|
|
1455
2148
|
for (const line of lines) {
|
|
1456
2149
|
try {
|
|
@@ -1464,7 +2157,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1464
2157
|
} catch { }
|
|
1465
2158
|
}
|
|
1466
2159
|
if (messages.length) {
|
|
1467
|
-
const stat = fs.
|
|
2160
|
+
const stat = await fs.promises.stat(sessionFile);
|
|
1468
2161
|
sessions.push({
|
|
1469
2162
|
id: proj,
|
|
1470
2163
|
title: firstUserMsg || proj,
|
|
@@ -1486,33 +2179,32 @@ const server = http.createServer(async (req, res) => {
|
|
|
1486
2179
|
const limit = Math.min(Number(url.searchParams.get("limit") || "20"), 50);
|
|
1487
2180
|
const sessionsBase = path.join(process.cwd(), ".crew", "sessions");
|
|
1488
2181
|
const sessions = [];
|
|
1489
|
-
if (
|
|
1490
|
-
const engines = fs.
|
|
2182
|
+
if (await exists(sessionsBase)) {
|
|
2183
|
+
const engines = await fs.promises.readdir(sessionsBase);
|
|
1491
2184
|
for (const engine of engines) {
|
|
1492
2185
|
const engineDir = path.join(sessionsBase, engine);
|
|
1493
|
-
const projects = fs.
|
|
2186
|
+
const projects = await fs.promises.readdir(engineDir);
|
|
1494
2187
|
for (const project of projects) {
|
|
1495
2188
|
const projectSessionsDir = path.join(engineDir, project, "sessions");
|
|
1496
|
-
if (!
|
|
1497
|
-
const years = fs.
|
|
2189
|
+
if (!await exists(projectSessionsDir)) continue;
|
|
2190
|
+
const years = (await fs.promises.readdir(projectSessionsDir)).filter(d => /^\d{4}$/.test(d));
|
|
1498
2191
|
for (const year of years.sort().reverse()) {
|
|
1499
2192
|
const yearDir = path.join(projectSessionsDir, year);
|
|
1500
|
-
const months = fs.
|
|
2193
|
+
const months = (await fs.promises.readdir(yearDir)).filter(d => /^\d{2}$/.test(d));
|
|
1501
2194
|
for (const month of months.sort().reverse()) {
|
|
1502
2195
|
const monthDir = path.join(yearDir, month);
|
|
1503
|
-
const days = fs.
|
|
2196
|
+
const days = (await fs.promises.readdir(monthDir)).filter(d => /^\d{2}$/.test(d));
|
|
1504
2197
|
for (const day of days.sort().reverse()) {
|
|
1505
2198
|
const dayDir = path.join(yearDir, month, day);
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
.map(f => ({ f, mt: fs.statSync(path.join(dayDir, f)).mtimeMs }))
|
|
2199
|
+
const rawDayFiles = (await fs.promises.readdir(dayDir)).filter(f => f.endsWith(".jsonl"));
|
|
2200
|
+
const files = (await Promise.all(rawDayFiles.map(async f => ({ f, mt: (await fs.promises.stat(path.join(dayDir, f))).mtimeMs }))))
|
|
1509
2201
|
.sort((a, b) => b.mt - a.mt);
|
|
1510
2202
|
for (const { f } of files) {
|
|
1511
2203
|
if (sessions.length >= limit) break;
|
|
1512
2204
|
const sessionId = f.replace(".jsonl", "");
|
|
1513
2205
|
const messages = [];
|
|
1514
2206
|
// Limit to last 100 lines to avoid huge session files
|
|
1515
|
-
const allLines = fs.
|
|
2207
|
+
const allLines = (await fs.promises.readFile(path.join(dayDir, f), "utf8")).trim().split("\n");
|
|
1516
2208
|
const lines = allLines.slice(-100);
|
|
1517
2209
|
let firstUserMsg = "";
|
|
1518
2210
|
for (const line of lines) {
|
|
@@ -1588,8 +2280,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1588
2280
|
"passthrough-sessions.json",
|
|
1589
2281
|
);
|
|
1590
2282
|
let sessions = {};
|
|
1591
|
-
if (
|
|
1592
|
-
sessions = JSON.parse(fs.
|
|
2283
|
+
if (await exists(sessionFile)) {
|
|
2284
|
+
sessions = JSON.parse(await fs.promises.readFile(sessionFile, "utf8"));
|
|
1593
2285
|
}
|
|
1594
2286
|
res.writeHead(200, { "content-type": "application/json" });
|
|
1595
2287
|
res.end(JSON.stringify({ sessions }));
|
|
@@ -1613,11 +2305,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
1613
2305
|
"passthrough-sessions.json",
|
|
1614
2306
|
);
|
|
1615
2307
|
let sessions = {};
|
|
1616
|
-
if (
|
|
1617
|
-
sessions = JSON.parse(fs.
|
|
2308
|
+
if (await exists(sessionFile)) {
|
|
2309
|
+
sessions = JSON.parse(await fs.promises.readFile(sessionFile, "utf8"));
|
|
1618
2310
|
}
|
|
1619
2311
|
delete sessions[key];
|
|
1620
|
-
fs.
|
|
2312
|
+
await fs.promises.writeFile(
|
|
1621
2313
|
sessionFile,
|
|
1622
2314
|
JSON.stringify(sessions, null, 2),
|
|
1623
2315
|
"utf8",
|
|
@@ -1641,7 +2333,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1641
2333
|
);
|
|
1642
2334
|
let usage = { calls: 0, prompt: 0, completion: 0, byModel: {} };
|
|
1643
2335
|
try {
|
|
1644
|
-
usage = JSON.parse(fs.
|
|
2336
|
+
usage = JSON.parse(await fs.promises.readFile(usageFile, "utf8"));
|
|
1645
2337
|
} catch { }
|
|
1646
2338
|
res.writeHead(200, { "content-type": "application/json" });
|
|
1647
2339
|
res.end(JSON.stringify(usage));
|
|
@@ -1738,13 +2430,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
1738
2430
|
const histDir = path.join(os.homedir(), ".crewswarm", "chat-history");
|
|
1739
2431
|
const sessions = [];
|
|
1740
2432
|
try {
|
|
1741
|
-
const files = fs
|
|
1742
|
-
.readdirSync(histDir)
|
|
2433
|
+
const files = (await fs.promises.readdir(histDir))
|
|
1743
2434
|
.filter((f) => f.startsWith("telegram-") && f.endsWith(".jsonl"));
|
|
1744
2435
|
for (const file of files) {
|
|
1745
2436
|
const chatId = file.replace(/^telegram-/, "").replace(/\.jsonl$/, "");
|
|
1746
|
-
const lines = fs
|
|
1747
|
-
.
|
|
2437
|
+
const lines = (await fs.promises
|
|
2438
|
+
.readFile(path.join(histDir, file), "utf8"))
|
|
1748
2439
|
.split("\n")
|
|
1749
2440
|
.filter(Boolean);
|
|
1750
2441
|
const msgs = lines
|
|
@@ -1770,7 +2461,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1770
2461
|
return;
|
|
1771
2462
|
}
|
|
1772
2463
|
|
|
1773
|
-
if (
|
|
2464
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
1774
2465
|
res.writeHead(200, { "content-type": "application/json" });
|
|
1775
2466
|
res.end(JSON.stringify({ ok: true, uptime: Math.round(process.uptime()) }));
|
|
1776
2467
|
return;
|
|
@@ -1849,7 +2540,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1849
2540
|
// Credential keys are never exposed here
|
|
1850
2541
|
let cfgEnv = {};
|
|
1851
2542
|
try {
|
|
1852
|
-
cfgEnv = JSON.parse(fs.
|
|
2543
|
+
cfgEnv = JSON.parse(await fs.promises.readFile(CFG_FILE, "utf8")).env || {};
|
|
1853
2544
|
} catch { }
|
|
1854
2545
|
const result = {};
|
|
1855
2546
|
for (const v of vars) {
|
|
@@ -1875,9 +2566,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
1875
2566
|
}
|
|
1876
2567
|
const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
|
|
1877
2568
|
try {
|
|
1878
|
-
const raw = (() => {
|
|
2569
|
+
const raw = await (async () => {
|
|
1879
2570
|
try {
|
|
1880
|
-
return fs.
|
|
2571
|
+
return await fs.promises.readFile(cfgPath, "utf8");
|
|
1881
2572
|
} catch {
|
|
1882
2573
|
return "{}";
|
|
1883
2574
|
}
|
|
@@ -1908,7 +2599,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1908
2599
|
return;
|
|
1909
2600
|
}
|
|
1910
2601
|
|
|
1911
|
-
// ── Auth Token (for
|
|
2602
|
+
// ── Auth Token (for Vibe) ─────────────────────────────────────────────────────
|
|
1912
2603
|
if (url.pathname === "/api/auth/token") {
|
|
1913
2604
|
res.writeHead(200, {
|
|
1914
2605
|
"content-type": "application/json",
|
|
@@ -2070,7 +2761,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2070
2761
|
|
|
2071
2762
|
try {
|
|
2072
2763
|
// Load agent config
|
|
2073
|
-
const csSwarm = JSON.parse(fs.
|
|
2764
|
+
const csSwarm = JSON.parse(await fs.promises.readFile(CFG_FILE, "utf8"));
|
|
2074
2765
|
const agentCfg = csSwarm.agents?.find((a) => a.id === agentId);
|
|
2075
2766
|
if (!agentCfg?.model) {
|
|
2076
2767
|
res.writeHead(404, { "content-type": "application/json" });
|
|
@@ -2103,7 +2794,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2103
2794
|
let systemPrompt = `You are ${agentId}.`;
|
|
2104
2795
|
try {
|
|
2105
2796
|
const agentPrompts = JSON.parse(
|
|
2106
|
-
fs.
|
|
2797
|
+
await fs.promises.readFile(agentPromptsPath, "utf8"),
|
|
2107
2798
|
);
|
|
2108
2799
|
const bareId = agentId.replace(/^crew-/, "");
|
|
2109
2800
|
systemPrompt =
|
|
@@ -2295,14 +2986,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
2295
2986
|
);
|
|
2296
2987
|
|
|
2297
2988
|
try {
|
|
2298
|
-
if (!
|
|
2989
|
+
if (!await exists(historyPath)) {
|
|
2299
2990
|
res.writeHead(200, { "content-type": "application/json" });
|
|
2300
2991
|
res.end(JSON.stringify({ history: [] }));
|
|
2301
2992
|
return;
|
|
2302
2993
|
}
|
|
2303
2994
|
|
|
2304
|
-
const lines = fs
|
|
2305
|
-
.
|
|
2995
|
+
const lines = (await fs.promises
|
|
2996
|
+
.readFile(historyPath, "utf8"))
|
|
2306
2997
|
.split("\n")
|
|
2307
2998
|
.filter(Boolean);
|
|
2308
2999
|
const history = lines.map((line) => JSON.parse(line));
|
|
@@ -2335,24 +3026,76 @@ const server = http.createServer(async (req, res) => {
|
|
|
2335
3026
|
if (url.pathname === "/api/enhance-prompt" && req.method === "POST") {
|
|
2336
3027
|
let body = "";
|
|
2337
3028
|
for await (const chunk of req) body += chunk;
|
|
2338
|
-
|
|
2339
|
-
|
|
3029
|
+
let parsed;
|
|
3030
|
+
try { parsed = JSON.parse(body || "{}"); } catch {
|
|
2340
3031
|
res.writeHead(400, { "content-type": "application/json" });
|
|
2341
|
-
res.end(JSON.stringify({ error: "
|
|
3032
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
2342
3033
|
return;
|
|
2343
3034
|
}
|
|
3035
|
+
const vr = validate(EnhancePromptSchema, parsed);
|
|
3036
|
+
if (!vr.ok) {
|
|
3037
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3038
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
const { text, projectId, engine: requestedEngine, model: requestedModel } = vr.data;
|
|
2344
3042
|
try {
|
|
2345
|
-
|
|
3043
|
+
// Default to cwd for planner context — engines need repo access to produce aware briefs.
|
|
3044
|
+
// Claude/Cursor/Codex use --add-dir for safe read access; crew-cli uses --project.
|
|
3045
|
+
// Fall back to temp dir only if no project context is available.
|
|
3046
|
+
let projectDir = process.cwd();
|
|
3047
|
+
if (projectId) {
|
|
3048
|
+
const regPath = path.join(CFG_DIR, "projects.json");
|
|
3049
|
+
if (await exists(regPath)) {
|
|
3050
|
+
const reg = JSON.parse(await fs.promises.readFile(regPath, "utf8") || "{}");
|
|
3051
|
+
const proj = reg[projectId];
|
|
3052
|
+
if (proj?.outputDir) projectDir = proj.outputDir;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
await fs.promises.mkdir(projectDir, { recursive: true });
|
|
3056
|
+
|
|
3057
|
+
const planner = resolvePlannerEngine(requestedEngine, requestedModel);
|
|
3058
|
+
if (!planner) throw new Error("No planning engine is configured or installed");
|
|
3059
|
+
|
|
3060
|
+
const planned = await collectPassthroughText({
|
|
3061
|
+
engine: planner.engine,
|
|
3062
|
+
message: buildRequirementPlanningPrompt(text, projectDir),
|
|
3063
|
+
projectDir,
|
|
3064
|
+
model: planner.model,
|
|
3065
|
+
permissionMode: planner.permissionMode,
|
|
3066
|
+
sandbox: planner.sandbox,
|
|
3067
|
+
forceL2: planner.engine === "crew-cli",
|
|
3068
|
+
});
|
|
3069
|
+
|
|
2346
3070
|
res.writeHead(200, { "content-type": "application/json" });
|
|
2347
|
-
res.end(JSON.stringify({
|
|
3071
|
+
res.end(JSON.stringify({
|
|
3072
|
+
enhanced: planned,
|
|
3073
|
+
engine: planner.engine,
|
|
3074
|
+
model: planner.model,
|
|
3075
|
+
mode: planner.permissionMode || planner.sandbox || "prompt",
|
|
3076
|
+
source: planner.source,
|
|
3077
|
+
}));
|
|
2348
3078
|
} catch (err) {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
enhanced
|
|
2354
|
-
|
|
2355
|
-
|
|
3079
|
+
try {
|
|
3080
|
+
const enhanced = await enhancePromptWithGroq(text);
|
|
3081
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
3082
|
+
res.end(JSON.stringify({
|
|
3083
|
+
enhanced,
|
|
3084
|
+
engine: "groq",
|
|
3085
|
+
model: "llama-3.3-70b-versatile",
|
|
3086
|
+
mode: "fallback-rewrite",
|
|
3087
|
+
source: "fallback",
|
|
3088
|
+
warning: err?.message || String(err),
|
|
3089
|
+
}));
|
|
3090
|
+
} catch (fallbackErr) {
|
|
3091
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
3092
|
+
res.end(
|
|
3093
|
+
JSON.stringify({
|
|
3094
|
+
error: fallbackErr?.message || err?.message || String(fallbackErr || err),
|
|
3095
|
+
enhanced: null,
|
|
3096
|
+
}),
|
|
3097
|
+
);
|
|
3098
|
+
}
|
|
2356
3099
|
}
|
|
2357
3100
|
return;
|
|
2358
3101
|
}
|
|
@@ -2375,13 +3118,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
2375
3118
|
// Resolve project output dir if projectId provided
|
|
2376
3119
|
let projectEnv = {};
|
|
2377
3120
|
if (projectId) {
|
|
2378
|
-
const { existsSync: ex } = await import("node:fs");
|
|
2379
3121
|
const { readFile: rf } = await import("node:fs/promises");
|
|
2380
3122
|
const regPath = path.join(
|
|
2381
3123
|
CFG_DIR,
|
|
2382
3124
|
"projects.json",
|
|
2383
3125
|
);
|
|
2384
|
-
if (
|
|
3126
|
+
if (await exists(regPath)) {
|
|
2385
3127
|
const reg = JSON.parse(await rf(regPath, "utf8").catch(() => "{}"));
|
|
2386
3128
|
const proj = reg[projectId];
|
|
2387
3129
|
if (proj) {
|
|
@@ -2397,8 +3139,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2397
3139
|
}
|
|
2398
3140
|
}
|
|
2399
3141
|
const { spawn } = await import("node:child_process");
|
|
2400
|
-
|
|
2401
|
-
if (!existsSync(phasedOrchestrator))
|
|
3142
|
+
if (!(await exists(phasedOrchestrator)))
|
|
2402
3143
|
throw new Error(
|
|
2403
3144
|
"phased-orchestrator.mjs not found at " + phasedOrchestrator,
|
|
2404
3145
|
);
|
|
@@ -2442,10 +3183,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
2442
3183
|
projectId ? "phased-" + projectId + ".pid" : "phased-orchestrator.pid",
|
|
2443
3184
|
);
|
|
2444
3185
|
try {
|
|
2445
|
-
const pidStr = fs.
|
|
3186
|
+
const pidStr = (await fs.promises.readFile(pidFile, "utf8")).trim();
|
|
2446
3187
|
const pid = parseInt(pidStr, 10);
|
|
2447
3188
|
if (pid) process.kill(pid, "SIGTERM");
|
|
2448
|
-
fs.
|
|
3189
|
+
await fs.promises.unlink(pidFile);
|
|
2449
3190
|
res.writeHead(200, { "content-type": "application/json" });
|
|
2450
3191
|
res.end(JSON.stringify({ ok: true, pid }));
|
|
2451
3192
|
} catch (e) {
|
|
@@ -2457,18 +3198,27 @@ const server = http.createServer(async (req, res) => {
|
|
|
2457
3198
|
if (url.pathname === "/api/continuous-build" && req.method === "POST") {
|
|
2458
3199
|
let body = "";
|
|
2459
3200
|
for await (const chunk of req) body += chunk;
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
3201
|
+
let parsed;
|
|
3202
|
+
try { parsed = JSON.parse(body || "{}"); } catch {
|
|
3203
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3204
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
3205
|
+
return;
|
|
3206
|
+
}
|
|
3207
|
+
const vr = validate(ContinuousBuildSchema, parsed);
|
|
3208
|
+
if (!vr.ok) {
|
|
3209
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3210
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
const { requirement, projectId } = vr.data;
|
|
2463
3214
|
let projectEnv = {};
|
|
2464
3215
|
if (projectId) {
|
|
2465
|
-
const { existsSync: ex } = await import("node:fs");
|
|
2466
3216
|
const { readFile: rf } = await import("node:fs/promises");
|
|
2467
3217
|
const regPath = path.join(
|
|
2468
3218
|
CFG_DIR,
|
|
2469
3219
|
"projects.json",
|
|
2470
3220
|
);
|
|
2471
|
-
if (
|
|
3221
|
+
if (await exists(regPath)) {
|
|
2472
3222
|
const reg = JSON.parse(await rf(regPath, "utf8").catch(() => "{}"));
|
|
2473
3223
|
const proj = reg[projectId];
|
|
2474
3224
|
if (proj) {
|
|
@@ -2484,8 +3234,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2484
3234
|
}
|
|
2485
3235
|
}
|
|
2486
3236
|
const { spawn } = await import("node:child_process");
|
|
2487
|
-
|
|
2488
|
-
if (!existsSync(continuousBuild))
|
|
3237
|
+
if (!(await exists(continuousBuild)))
|
|
2489
3238
|
throw new Error("continuous-build.mjs not found at " + continuousBuild);
|
|
2490
3239
|
const proc = spawn(PREFERRED_NODE_BIN, [continuousBuild, requirement], {
|
|
2491
3240
|
cwd: CREWSWARM_DIR,
|
|
@@ -2533,10 +3282,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
2533
3282
|
projectId ? "continuous-" + projectId + ".pid" : "continuous-build.pid",
|
|
2534
3283
|
);
|
|
2535
3284
|
try {
|
|
2536
|
-
const pidStr = fs.
|
|
3285
|
+
const pidStr = (await fs.promises.readFile(pidFile, "utf8")).trim();
|
|
2537
3286
|
const pid = parseInt(pidStr, 10);
|
|
2538
3287
|
if (pid) process.kill(pid, "SIGTERM");
|
|
2539
|
-
fs.
|
|
3288
|
+
await fs.promises.unlink(pidFile);
|
|
2540
3289
|
res.writeHead(200, { "content-type": "application/json" });
|
|
2541
3290
|
res.end(JSON.stringify({ ok: true, pid }));
|
|
2542
3291
|
} catch (e) {
|
|
@@ -2546,7 +3295,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2546
3295
|
return;
|
|
2547
3296
|
}
|
|
2548
3297
|
if (url.pathname === "/api/continuous-build/log" && req.method === "GET") {
|
|
2549
|
-
const { existsSync } = await import("node:fs");
|
|
2550
3298
|
const { readFile } = await import("node:fs/promises");
|
|
2551
3299
|
const logPath = path.join(
|
|
2552
3300
|
CREWSWARM_DIR,
|
|
@@ -2554,7 +3302,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2554
3302
|
"continuous-build.jsonl",
|
|
2555
3303
|
);
|
|
2556
3304
|
let lines = [];
|
|
2557
|
-
if (
|
|
3305
|
+
if (await exists(logPath)) {
|
|
2558
3306
|
const raw = await readFile(logPath, "utf8").catch(() => "");
|
|
2559
3307
|
lines = raw
|
|
2560
3308
|
.trim()
|
|
@@ -2651,7 +3399,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
2651
3399
|
|
|
2652
3400
|
// ── Waves Configuration APIs ──────────────────────────────────────────
|
|
2653
3401
|
if (url.pathname === "/api/waves/config" && req.method === "GET") {
|
|
2654
|
-
const { existsSync, readFileSync } = await import("node:fs");
|
|
2655
3402
|
const { readFile: rf } = await import("node:fs/promises");
|
|
2656
3403
|
const wavesConfigPath = path.join(
|
|
2657
3404
|
CREWSWARM_DIR,
|
|
@@ -2660,7 +3407,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2660
3407
|
"waves-config.json",
|
|
2661
3408
|
);
|
|
2662
3409
|
|
|
2663
|
-
if (!
|
|
3410
|
+
if (!(await exists(wavesConfigPath))) {
|
|
2664
3411
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2665
3412
|
res.end(JSON.stringify({ ok: false, error: "Waves config not found" }));
|
|
2666
3413
|
return;
|
|
@@ -2675,7 +3422,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2675
3422
|
raw = await rf(wavesConfigPath, "utf8");
|
|
2676
3423
|
} catch {
|
|
2677
3424
|
try {
|
|
2678
|
-
raw =
|
|
3425
|
+
raw = await fs.promises.readFile(wavesConfigPath, "utf8");
|
|
2679
3426
|
} catch {
|
|
2680
3427
|
raw = JSON.stringify(getEmbeddedWavesConfig());
|
|
2681
3428
|
}
|
|
@@ -2729,8 +3476,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2729
3476
|
|
|
2730
3477
|
try {
|
|
2731
3478
|
// Check if we have a backup
|
|
2732
|
-
|
|
2733
|
-
if (existsSync(wavesConfigBackup)) {
|
|
3479
|
+
if (await exists(wavesConfigBackup)) {
|
|
2734
3480
|
const defaultConfig = await rf(wavesConfigBackup, "utf8");
|
|
2735
3481
|
await wf(wavesConfigPath, defaultConfig, "utf8");
|
|
2736
3482
|
} else {
|
|
@@ -2881,7 +3627,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2881
3627
|
if (!isValidWorkflowName(name))
|
|
2882
3628
|
throw new Error("Invalid workflow name");
|
|
2883
3629
|
const fp = getWorkflowFile(name);
|
|
2884
|
-
if (
|
|
3630
|
+
if (await exists(fp)) await fs.promises.unlink(fp);
|
|
2885
3631
|
workflowRuntime.runs.delete(name);
|
|
2886
3632
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2887
3633
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -2961,11 +3707,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
2961
3707
|
|
|
2962
3708
|
// ── Project management APIs ───────────────────────────────────────────
|
|
2963
3709
|
if (url.pathname === "/api/projects" && req.method === "GET") {
|
|
2964
|
-
const { existsSync } = await import("node:fs");
|
|
2965
3710
|
const { readFile: rf } = await import("node:fs/promises");
|
|
2966
3711
|
const registryFile = path.join(CFG_DIR, "projects.json");
|
|
2967
3712
|
let projects = {};
|
|
2968
|
-
if (
|
|
3713
|
+
if (await exists(registryFile)) {
|
|
2969
3714
|
projects = JSON.parse(await rf(registryFile, "utf8").catch(() => "{}"));
|
|
2970
3715
|
}
|
|
2971
3716
|
// Enrich each project with live roadmap stats and running status.
|
|
@@ -2979,7 +3724,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2979
3724
|
failed = 0,
|
|
2980
3725
|
pending = 0,
|
|
2981
3726
|
total = 0;
|
|
2982
|
-
if (
|
|
3727
|
+
if (await exists(project.roadmapFile)) {
|
|
2983
3728
|
const rm = await rf(project.roadmapFile, "utf8").catch(() => "");
|
|
2984
3729
|
const lines = rm.split("\n").filter((l) => /^- \[/.test(l));
|
|
2985
3730
|
total = lines.length;
|
|
@@ -2989,7 +3734,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
2989
3734
|
}
|
|
2990
3735
|
let running = false;
|
|
2991
3736
|
const pidPath = path.join(logsDir2, `pm-loop-${id}.pid`);
|
|
2992
|
-
if (
|
|
3737
|
+
if (await exists(pidPath)) {
|
|
2993
3738
|
try {
|
|
2994
3739
|
const pidStr = await rf(pidPath, "utf8").catch(() => "");
|
|
2995
3740
|
const pid = parseInt(pidStr.trim(), 10);
|
|
@@ -3056,12 +3801,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
3056
3801
|
body || "{}",
|
|
3057
3802
|
);
|
|
3058
3803
|
if (!name || !outputDir) throw new Error("name and outputDir required");
|
|
3059
|
-
const { existsSync, mkdirSync } = await import("node:fs");
|
|
3060
3804
|
const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
|
|
3061
3805
|
// Create output dir and ROADMAP.md if they don't exist
|
|
3062
|
-
if (!
|
|
3806
|
+
if (!(await exists(outputDir))) await fs.promises.mkdir(outputDir, { recursive: true });
|
|
3063
3807
|
const roadmapFile = path.join(outputDir, "ROADMAP.md");
|
|
3064
|
-
if (!
|
|
3808
|
+
if (!(await exists(roadmapFile))) {
|
|
3065
3809
|
await wf(
|
|
3066
3810
|
roadmapFile,
|
|
3067
3811
|
`# ${name} — Living Roadmap\n\n> Managed by pm-loop.mjs. Add \`- [ ] items\` here at any time.\n\n---\n\n## Phase 0 — Getting Started\n\n- [ ] Create the initial project structure and entry point\n`,
|
|
@@ -3073,7 +3817,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3073
3817
|
.replace(/^-|-$/g, "");
|
|
3074
3818
|
const registryFile = path.join(CFG_DIR, "projects.json");
|
|
3075
3819
|
let projects = {};
|
|
3076
|
-
if (
|
|
3820
|
+
if (await exists(registryFile))
|
|
3077
3821
|
projects = JSON.parse(await rf(registryFile, "utf8").catch(() => "{}"));
|
|
3078
3822
|
projects[id] = {
|
|
3079
3823
|
id,
|
|
@@ -3097,13 +3841,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
3097
3841
|
if (url.pathname === "/api/projects/delete" && req.method === "POST") {
|
|
3098
3842
|
let body = "";
|
|
3099
3843
|
for await (const chunk of req) body += chunk;
|
|
3100
|
-
|
|
3101
|
-
|
|
3844
|
+
let parsed;
|
|
3845
|
+
try { parsed = JSON.parse(body || "{}"); } catch {
|
|
3846
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3847
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
const vr = validate(DeleteProjectSchema, parsed);
|
|
3851
|
+
if (!vr.ok) {
|
|
3852
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3853
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
3854
|
+
return;
|
|
3855
|
+
}
|
|
3856
|
+
const { projectId } = vr.data;
|
|
3102
3857
|
const registryFile = path.join(CFG_DIR, "projects.json");
|
|
3103
|
-
const { existsSync, rmSync, writeFileSync, unlinkSync } = await import("node:fs");
|
|
3104
3858
|
const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
|
|
3105
3859
|
let projects = {};
|
|
3106
|
-
if (
|
|
3860
|
+
if (await exists(registryFile))
|
|
3107
3861
|
projects = JSON.parse(await rf(registryFile, "utf8").catch(() => "{}"));
|
|
3108
3862
|
if (!projects[projectId])
|
|
3109
3863
|
throw new Error("Project not found: " + projectId);
|
|
@@ -3112,7 +3866,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3112
3866
|
const stopPath = path.join(logsDir, `pm-loop-${projectId}.stop`);
|
|
3113
3867
|
const logPath = path.join(logsDir, `pm-loop-${projectId}.jsonl`);
|
|
3114
3868
|
let stoppedPmLoop = false;
|
|
3115
|
-
if (
|
|
3869
|
+
if (await exists(pidPath)) {
|
|
3116
3870
|
const pidStr = await rf(pidPath, "utf8").catch(() => "");
|
|
3117
3871
|
const pid = parseInt(pidStr.trim(), 10);
|
|
3118
3872
|
if (pid) {
|
|
@@ -3123,18 +3877,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
3123
3877
|
}
|
|
3124
3878
|
}
|
|
3125
3879
|
try {
|
|
3126
|
-
|
|
3880
|
+
await fs.promises.writeFile(stopPath, new Date().toISOString());
|
|
3127
3881
|
} catch { }
|
|
3128
3882
|
for (const cleanupPath of [pidPath, stopPath, logPath]) {
|
|
3129
|
-
if (!
|
|
3883
|
+
if (!(await exists(cleanupPath))) continue;
|
|
3130
3884
|
try {
|
|
3131
|
-
|
|
3885
|
+
await fs.promises.unlink(cleanupPath);
|
|
3132
3886
|
} catch { }
|
|
3133
3887
|
}
|
|
3134
3888
|
const projectMessageDir = path.join(CFG_DIR, "project-messages", projectId);
|
|
3135
|
-
if (
|
|
3889
|
+
if (await exists(projectMessageDir)) {
|
|
3136
3890
|
try {
|
|
3137
|
-
|
|
3891
|
+
await fs.promises.rm(projectMessageDir, { recursive: true, force: true });
|
|
3138
3892
|
} catch { }
|
|
3139
3893
|
}
|
|
3140
3894
|
delete projects[projectId];
|
|
@@ -3146,14 +3900,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
3146
3900
|
if (url.pathname === "/api/projects/update" && req.method === "POST") {
|
|
3147
3901
|
let body = "";
|
|
3148
3902
|
for await (const chunk of req) body += chunk;
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3903
|
+
let parsed;
|
|
3904
|
+
try { parsed = JSON.parse(body || "{}"); } catch {
|
|
3905
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3906
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
3907
|
+
return;
|
|
3908
|
+
}
|
|
3909
|
+
const vr = validate(UpdateProjectSchema, parsed);
|
|
3910
|
+
if (!vr.ok) {
|
|
3911
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
3912
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
3913
|
+
return;
|
|
3914
|
+
}
|
|
3915
|
+
const { projectId, autoAdvance, name, description, outputDir } = vr.data;
|
|
3152
3916
|
const registryFile = path.join(CFG_DIR, "projects.json");
|
|
3153
|
-
const { existsSync } = await import("node:fs");
|
|
3154
3917
|
const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
|
|
3155
3918
|
let projects = {};
|
|
3156
|
-
if (
|
|
3919
|
+
if (await exists(registryFile))
|
|
3157
3920
|
projects = JSON.parse(await rf(registryFile, "utf8").catch(() => "{}"));
|
|
3158
3921
|
if (!projects[projectId])
|
|
3159
3922
|
throw new Error("Project not found: " + projectId);
|
|
@@ -3168,7 +3931,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
3168
3931
|
return;
|
|
3169
3932
|
}
|
|
3170
3933
|
if (url.pathname === "/api/pm-loop/status" && req.method === "GET") {
|
|
3171
|
-
const { existsSync } = await import("node:fs");
|
|
3172
3934
|
const { readFile: rf } = await import("node:fs/promises");
|
|
3173
3935
|
const statusProjectId = url.searchParams.get("projectId") || "";
|
|
3174
3936
|
const suffix = statusProjectId ? `-${statusProjectId}` : "";
|
|
@@ -3179,7 +3941,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3179
3941
|
);
|
|
3180
3942
|
let running = false,
|
|
3181
3943
|
pid = null;
|
|
3182
|
-
if (
|
|
3944
|
+
if (await exists(pidPath)) {
|
|
3183
3945
|
const pidStr = await rf(pidPath, "utf8").catch(() => "");
|
|
3184
3946
|
pid = parseInt(pidStr.trim(), 10);
|
|
3185
3947
|
if (pid) {
|
|
@@ -3213,9 +3975,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
3213
3975
|
}
|
|
3214
3976
|
const { dryRun, projectId, pmOptions = {} } = vr.data;
|
|
3215
3977
|
const { spawn } = await import("node:child_process");
|
|
3216
|
-
const { existsSync, mkdirSync, unlinkSync } = await import("node:fs");
|
|
3217
3978
|
const { readFile: rf } = await import("node:fs/promises");
|
|
3218
|
-
if (!
|
|
3979
|
+
if (!(await exists(pmLoop)))
|
|
3219
3980
|
throw new Error("pm-loop.mjs not found at " + pmLoop);
|
|
3220
3981
|
// Resolve project config if projectId provided
|
|
3221
3982
|
let projectDir = null,
|
|
@@ -3223,7 +3984,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3223
3984
|
projectFeaturesDoc = null;
|
|
3224
3985
|
if (projectId) {
|
|
3225
3986
|
const registryFile = path.join(CFG_DIR, "projects.json");
|
|
3226
|
-
if (
|
|
3987
|
+
if (await exists(registryFile)) {
|
|
3227
3988
|
const reg = JSON.parse(
|
|
3228
3989
|
await rf(registryFile, "utf8").catch(() => "{}"),
|
|
3229
3990
|
);
|
|
@@ -3247,7 +4008,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3247
4008
|
"orchestrator-logs",
|
|
3248
4009
|
`pm-loop${pidSuffix}.stop`,
|
|
3249
4010
|
);
|
|
3250
|
-
if (
|
|
4011
|
+
if (await exists(pidFile)) {
|
|
3251
4012
|
const pidStr = await rf(pidFile, "utf8").catch(() => "");
|
|
3252
4013
|
const existingPid = parseInt(pidStr.trim(), 10);
|
|
3253
4014
|
if (existingPid) {
|
|
@@ -3269,13 +4030,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
3269
4030
|
}
|
|
3270
4031
|
}
|
|
3271
4032
|
// Clear any stale stop file
|
|
3272
|
-
if (
|
|
4033
|
+
if (await exists(stopFilePath)) {
|
|
3273
4034
|
try {
|
|
3274
|
-
|
|
4035
|
+
await fs.promises.unlink(stopFilePath);
|
|
3275
4036
|
} catch { }
|
|
3276
4037
|
}
|
|
3277
4038
|
const logsDir = path.join(CREWSWARM_DIR, "orchestrator-logs");
|
|
3278
|
-
if (!
|
|
4039
|
+
if (!(await exists(logsDir))) await fs.promises.mkdir(logsDir, { recursive: true });
|
|
3279
4040
|
// Load RT token so pm-loop and its child gateway-bridge --send can authenticate with the RT daemon
|
|
3280
4041
|
let rtToken = process.env.CREWSWARM_RT_AUTH_TOKEN || "";
|
|
3281
4042
|
if (!rtToken) {
|
|
@@ -3367,13 +4128,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
3367
4128
|
let body = "";
|
|
3368
4129
|
for await (const chunk of req) body += chunk;
|
|
3369
4130
|
const { projectId } = JSON.parse(body || "{}");
|
|
3370
|
-
const { writeFileSync, mkdirSync, existsSync } = await import("node:fs");
|
|
3371
4131
|
const logsDir = path.join(CREWSWARM_DIR, "orchestrator-logs");
|
|
3372
|
-
if (!
|
|
4132
|
+
if (!(await exists(logsDir))) await fs.promises.mkdir(logsDir, { recursive: true });
|
|
3373
4133
|
// Write project-specific stop file if projectId provided
|
|
3374
4134
|
const suffix = projectId ? `-${projectId}` : "";
|
|
3375
4135
|
const stopFilePath = path.join(logsDir, `pm-loop${suffix}.stop`);
|
|
3376
|
-
|
|
4136
|
+
await fs.promises.writeFile(stopFilePath, new Date().toISOString());
|
|
3377
4137
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3378
4138
|
res.end(
|
|
3379
4139
|
JSON.stringify({
|
|
@@ -3384,10 +4144,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
3384
4144
|
return;
|
|
3385
4145
|
}
|
|
3386
4146
|
if (url.pathname === "/api/pm-loop/log" && req.method === "GET") {
|
|
3387
|
-
const { existsSync } = await import("node:fs");
|
|
3388
4147
|
const { readFile } = await import("node:fs/promises");
|
|
3389
4148
|
let lines = [];
|
|
3390
|
-
if (
|
|
4149
|
+
if (await exists(pmLogFile)) {
|
|
3391
4150
|
const raw = await readFile(pmLogFile, "utf8").catch(() => "");
|
|
3392
4151
|
lines = raw
|
|
3393
4152
|
.trim()
|
|
@@ -3408,10 +4167,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
3408
4167
|
return;
|
|
3409
4168
|
}
|
|
3410
4169
|
if (url.pathname === "/api/pm-loop/roadmap" && req.method === "GET") {
|
|
3411
|
-
const { existsSync } = await import("node:fs");
|
|
3412
4170
|
const { readFile } = await import("node:fs/promises");
|
|
3413
4171
|
let content = "(ROADMAP.md not found — create website/ROADMAP.md)";
|
|
3414
|
-
if (
|
|
4172
|
+
if (await exists(roadmapFile)) {
|
|
3415
4173
|
content = await readFile(roadmapFile, "utf8").catch(
|
|
3416
4174
|
() => "(unreadable)",
|
|
3417
4175
|
);
|
|
@@ -3423,10 +4181,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
3423
4181
|
if (url.pathname === "/api/dlq/replay" && req.method === "POST") {
|
|
3424
4182
|
let body = "";
|
|
3425
4183
|
for await (const chunk of req) body += chunk;
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
4184
|
+
let parsed;
|
|
4185
|
+
try { parsed = JSON.parse(body); } catch {
|
|
4186
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
4187
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
const vr = validate(ReplayDLQSchema, parsed);
|
|
4191
|
+
if (!vr.ok) {
|
|
4192
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
4193
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
4194
|
+
return;
|
|
4195
|
+
}
|
|
4196
|
+
const { key } = vr.data;
|
|
4197
|
+
const { execSync: _execSync } = await import("node:child_process");
|
|
4198
|
+
const { promisify: _promisify } = await import("node:util");
|
|
4199
|
+
const execAsync = _promisify(_execSync);
|
|
4200
|
+
await execAsync(`"${ctlPath}" dlq-replay "${key}"`, {
|
|
3430
4201
|
encoding: "utf8",
|
|
3431
4202
|
timeout: 10000,
|
|
3432
4203
|
});
|
|
@@ -3451,7 +4222,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
3451
4222
|
// ── Settings: Preset (from setup wizard) ────────────────────────────────
|
|
3452
4223
|
// ── Engine detection for first-run wizard ─────────────────────────────
|
|
3453
4224
|
if (url.pathname === "/api/first-run-engines" && req.method === "GET") {
|
|
3454
|
-
const {
|
|
4225
|
+
const { exec: _execFRE } = await import("node:child_process");
|
|
4226
|
+
const { promisify: _promisifyFRE } = await import("node:util");
|
|
4227
|
+
const _execAsyncFRE = _promisifyFRE(_execFRE);
|
|
3455
4228
|
// Expand PATH to include common install locations (launchd has restricted PATH)
|
|
3456
4229
|
const extraPaths = [
|
|
3457
4230
|
`${os.homedir()}/.local/bin`,
|
|
@@ -3477,13 +4250,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
3477
4250
|
];
|
|
3478
4251
|
for (const { id, bin } of checks) {
|
|
3479
4252
|
// Check common paths directly (launchd PATH is restricted)
|
|
3480
|
-
|
|
4253
|
+
const foundInSearchDirs = await Promise.any(searchDirs.map(d => fs.promises.access(path.join(d, bin)).then(() => true))).catch(() => false);
|
|
4254
|
+
if (foundInSearchDirs) {
|
|
3481
4255
|
engines[id] = true;
|
|
3482
4256
|
continue;
|
|
3483
4257
|
}
|
|
3484
4258
|
// Fallback: try login shell
|
|
3485
4259
|
try {
|
|
3486
|
-
|
|
4260
|
+
await _execAsyncFRE(`/bin/zsh -lc 'command -v ${bin}'`, { timeout: 5000 });
|
|
3487
4261
|
engines[id] = true;
|
|
3488
4262
|
} catch {
|
|
3489
4263
|
engines[id] = false;
|
|
@@ -3499,7 +4273,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3499
4273
|
let token = "";
|
|
3500
4274
|
try {
|
|
3501
4275
|
token =
|
|
3502
|
-
JSON.parse(fs.
|
|
4276
|
+
JSON.parse(await fs.promises.readFile(csConfigPath, "utf8"))?.rt?.authToken ||
|
|
3503
4277
|
"";
|
|
3504
4278
|
} catch { }
|
|
3505
4279
|
if (!token) token = process.env.CREWSWARM_RT_AUTH_TOKEN || "";
|
|
@@ -3513,13 +4287,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
3513
4287
|
const { token } = JSON.parse(body);
|
|
3514
4288
|
const csDir = path.join(os.homedir(), ".crewswarm");
|
|
3515
4289
|
const csConfigPath = path.join(csDir, "crewswarm.json");
|
|
3516
|
-
fs.
|
|
4290
|
+
await fs.promises.mkdir(csDir, { recursive: true });
|
|
3517
4291
|
let cfg = {};
|
|
3518
4292
|
try {
|
|
3519
|
-
cfg = JSON.parse(fs.
|
|
4293
|
+
cfg = JSON.parse(await fs.promises.readFile(csConfigPath, "utf8"));
|
|
3520
4294
|
} catch { }
|
|
3521
4295
|
cfg.rt = { ...(cfg.rt || {}), authToken: token };
|
|
3522
|
-
fs.
|
|
4296
|
+
await fs.promises.writeFile(csConfigPath, JSON.stringify(cfg, null, 2));
|
|
3523
4297
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3524
4298
|
res.end(JSON.stringify({ ok: true }));
|
|
3525
4299
|
return;
|
|
@@ -3529,15 +4303,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
3529
4303
|
const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
|
|
3530
4304
|
let locked = false;
|
|
3531
4305
|
try {
|
|
3532
|
-
const {
|
|
4306
|
+
const { exec: _lockExec } = await import("node:child_process");
|
|
4307
|
+
const { promisify: _lockPromisify } = await import("node:util");
|
|
4308
|
+
const _lockExecAsync = _lockPromisify(_lockExec);
|
|
3533
4309
|
// Use stat command which is more reliable than ls
|
|
3534
|
-
const
|
|
3535
|
-
locked =
|
|
4310
|
+
const { stdout: lockOut } = await _lockExecAsync(`stat -f "%Sf" "${cfgPath}"`, { encoding: 'utf8' });
|
|
4311
|
+
locked = lockOut.trim().includes('uchg');
|
|
3536
4312
|
} catch (e) {
|
|
3537
4313
|
// If stat fails, try ls as fallback
|
|
3538
4314
|
try {
|
|
3539
|
-
const
|
|
3540
|
-
|
|
4315
|
+
const { exec: _lockExec2 } = await import("node:child_process");
|
|
4316
|
+
const { promisify: _lockPromisify2 } = await import("node:util");
|
|
4317
|
+
const _lockExecAsync2 = _lockPromisify2(_lockExec2);
|
|
4318
|
+
const { stdout: lockOut2 } = await _lockExecAsync2(`ls -lO "${cfgPath}"`, { encoding: 'utf8' });
|
|
4319
|
+
locked = lockOut2.includes('uchg');
|
|
3541
4320
|
} catch { }
|
|
3542
4321
|
}
|
|
3543
4322
|
res.writeHead(200, { "content-type": "application/json" });
|
|
@@ -3547,7 +4326,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
3547
4326
|
if (url.pathname === "/api/config/lock" && req.method === "POST") {
|
|
3548
4327
|
const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
|
|
3549
4328
|
try {
|
|
3550
|
-
|
|
4329
|
+
const { exec: _chflagsExec } = await import("node:child_process");
|
|
4330
|
+
const { promisify: _chflagsPromisify } = await import("node:util");
|
|
4331
|
+
await _chflagsPromisify(_chflagsExec)(`chflags uchg "${cfgPath}"`);
|
|
3551
4332
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3552
4333
|
res.end(JSON.stringify({ ok: true, locked: true }));
|
|
3553
4334
|
} catch (e) {
|
|
@@ -3559,7 +4340,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
3559
4340
|
if (url.pathname === "/api/config/unlock" && req.method === "POST") {
|
|
3560
4341
|
const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
|
|
3561
4342
|
try {
|
|
3562
|
-
|
|
4343
|
+
const { exec: _chflagsExec2 } = await import("node:child_process");
|
|
4344
|
+
const { promisify: _chflagsPromisify2 } = await import("node:util");
|
|
4345
|
+
await _chflagsPromisify2(_chflagsExec2)(`chflags nouchg "${cfgPath}"`);
|
|
3563
4346
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3564
4347
|
res.end(JSON.stringify({ ok: true, locked: false }));
|
|
3565
4348
|
} catch (e) {
|
|
@@ -3583,7 +4366,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3583
4366
|
"groq/moonshotai/kimi-k2-instruct-0905";
|
|
3584
4367
|
let crewLeadModel = process.env.CREWSWARM_CREW_LEAD_MODEL || "";
|
|
3585
4368
|
try {
|
|
3586
|
-
const c = JSON.parse(fs.
|
|
4369
|
+
const c = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
|
|
3587
4370
|
if (c.opencodeProject) dir = c.opencodeProject;
|
|
3588
4371
|
if (c.opencodeFallbackModel) fallbackModel = c.opencodeFallbackModel;
|
|
3589
4372
|
if (c.opencodeModel) opencodeModel = c.opencodeModel;
|
|
@@ -3614,10 +4397,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
3614
4397
|
}
|
|
3615
4398
|
const cfgDir = path.join(os.homedir(), ".crewswarm");
|
|
3616
4399
|
const cfgPath = path.join(cfgDir, "crewswarm.json");
|
|
3617
|
-
fs.
|
|
4400
|
+
await fs.promises.mkdir(cfgDir, { recursive: true });
|
|
3618
4401
|
let cfg = {};
|
|
3619
4402
|
try {
|
|
3620
|
-
cfg = JSON.parse(fs.
|
|
4403
|
+
cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
|
|
3621
4404
|
} catch { }
|
|
3622
4405
|
if (dir !== undefined) {
|
|
3623
4406
|
if (dir) cfg.opencodeProject = dir;
|
|
@@ -3684,38 +4467,38 @@ const server = http.createServer(async (req, res) => {
|
|
|
3684
4467
|
const csConfig = path.join(csDir, "crewswarm.json");
|
|
3685
4468
|
const csSwarmConfig = path.join(csDir, "crewswarm.json");
|
|
3686
4469
|
const ocConfig = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
3687
|
-
function readCSConfig() {
|
|
4470
|
+
async function readCSConfig() {
|
|
3688
4471
|
try {
|
|
3689
|
-
return JSON.parse(fs.
|
|
4472
|
+
return JSON.parse(await fs.promises.readFile(csConfig, "utf8"));
|
|
3690
4473
|
} catch {
|
|
3691
4474
|
return {};
|
|
3692
4475
|
}
|
|
3693
4476
|
}
|
|
3694
|
-
function readCSSwarmConfig() {
|
|
4477
|
+
async function readCSSwarmConfig() {
|
|
3695
4478
|
try {
|
|
3696
|
-
return JSON.parse(fs.
|
|
4479
|
+
return JSON.parse(await fs.promises.readFile(csSwarmConfig, "utf8"));
|
|
3697
4480
|
} catch {
|
|
3698
4481
|
return {};
|
|
3699
4482
|
}
|
|
3700
4483
|
}
|
|
3701
|
-
function writeCSSwarmConfig(c) {
|
|
3702
|
-
fs.
|
|
3703
|
-
fs.
|
|
4484
|
+
async function writeCSSwarmConfig(c) {
|
|
4485
|
+
await fs.promises.mkdir(csDir, { recursive: true });
|
|
4486
|
+
await fs.promises.writeFile(csSwarmConfig, JSON.stringify(c, null, 2));
|
|
3704
4487
|
}
|
|
3705
|
-
function readOCConfig() {
|
|
4488
|
+
async function readOCConfig() {
|
|
3706
4489
|
try {
|
|
3707
|
-
return JSON.parse(fs.
|
|
4490
|
+
return JSON.parse(await fs.promises.readFile(ocConfig, "utf8"));
|
|
3708
4491
|
} catch {
|
|
3709
4492
|
return null;
|
|
3710
4493
|
}
|
|
3711
4494
|
}
|
|
3712
|
-
function writeOCConfig(c) {
|
|
3713
|
-
fs.
|
|
4495
|
+
async function writeOCConfig(c) {
|
|
4496
|
+
await fs.promises.writeFile(ocConfig, JSON.stringify(c, null, 4));
|
|
3714
4497
|
}
|
|
3715
|
-
function getBuiltinKey(id) {
|
|
3716
|
-
const sw = readCSSwarmConfig();
|
|
3717
|
-
const cs = readCSConfig();
|
|
3718
|
-
const oc = readOCConfig();
|
|
4498
|
+
async function getBuiltinKey(id) {
|
|
4499
|
+
const sw = await readCSSwarmConfig();
|
|
4500
|
+
const cs = await readCSConfig();
|
|
4501
|
+
const oc = await readOCConfig();
|
|
3719
4502
|
return (
|
|
3720
4503
|
sw?.providers?.[id]?.apiKey ||
|
|
3721
4504
|
sw?.env?.[id.toUpperCase() + "_API_KEY"] ||
|
|
@@ -3726,10 +4509,257 @@ const server = http.createServer(async (req, res) => {
|
|
|
3726
4509
|
);
|
|
3727
4510
|
}
|
|
3728
4511
|
|
|
4512
|
+
// ── OAuth / subscription provider status + model config ──────────────────
|
|
4513
|
+
if (url.pathname === "/api/oauth/status" && req.method === "GET") {
|
|
4514
|
+
const providers = {};
|
|
4515
|
+
// Use startup cache first; fall back to live keychain only if cache missed
|
|
4516
|
+
providers["anthropic-oauth"] = !!getOAuthTokenCached("anthropic-oauth");
|
|
4517
|
+
if (!providers["anthropic-oauth"]) {
|
|
4518
|
+
try {
|
|
4519
|
+
const { execFileSync } = await import("node:child_process");
|
|
4520
|
+
const { userInfo } = await import("node:os");
|
|
4521
|
+
for (const acct of [userInfo().username, "jeffhobbs", "unknown"]) {
|
|
4522
|
+
try {
|
|
4523
|
+
const raw = execFileSync("security", [
|
|
4524
|
+
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
4525
|
+
], { encoding: "utf8", timeout: 5000 }).trim();
|
|
4526
|
+
const parsed = JSON.parse(raw);
|
|
4527
|
+
if (parsed?.claudeAiOauth?.accessToken) {
|
|
4528
|
+
_oauthTokenCache["anthropic-oauth"] = parsed.claudeAiOauth.accessToken;
|
|
4529
|
+
providers["anthropic-oauth"] = true;
|
|
4530
|
+
break;
|
|
4531
|
+
}
|
|
4532
|
+
} catch { /* try next */ }
|
|
4533
|
+
}
|
|
4534
|
+
} catch { providers["anthropic-oauth"] = false; }
|
|
4535
|
+
}
|
|
4536
|
+
providers["openai-oauth"] = !!getOAuthTokenCached("openai-oauth");
|
|
4537
|
+
if (!providers["openai-oauth"]) {
|
|
4538
|
+
try {
|
|
4539
|
+
const t = await readOpenAIOAuthToken();
|
|
4540
|
+
if (t) { _oauthTokenCache["openai-oauth"] = t; providers["openai-oauth"] = true; }
|
|
4541
|
+
} catch { providers["openai-oauth"] = false; }
|
|
4542
|
+
}
|
|
4543
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4544
|
+
res.end(JSON.stringify({ ok: true, providers }));
|
|
4545
|
+
return;
|
|
4546
|
+
}
|
|
4547
|
+
if (url.pathname === "/api/oauth/test" && req.method === "POST") {
|
|
4548
|
+
let body = "";
|
|
4549
|
+
for await (const chunk of req) body += chunk;
|
|
4550
|
+
const { providerId, model } = JSON.parse(body);
|
|
4551
|
+
try {
|
|
4552
|
+
let token = getOAuthTokenCached(providerId);
|
|
4553
|
+
if (!token) {
|
|
4554
|
+
if (providerId === "anthropic-oauth") {
|
|
4555
|
+
const { execFileSync } = await import("node:child_process");
|
|
4556
|
+
const { userInfo } = await import("node:os");
|
|
4557
|
+
for (const acct of [userInfo().username, "jeffhobbs", "unknown"]) {
|
|
4558
|
+
try {
|
|
4559
|
+
const raw = execFileSync("security", [
|
|
4560
|
+
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
4561
|
+
], { encoding: "utf8", timeout: 5000 }).trim();
|
|
4562
|
+
const t = JSON.parse(raw)?.claudeAiOauth?.accessToken;
|
|
4563
|
+
if (t) { token = t; _oauthTokenCache[providerId] = t; break; }
|
|
4564
|
+
} catch { /* try next */ }
|
|
4565
|
+
}
|
|
4566
|
+
} else if (providerId === "openai-oauth") {
|
|
4567
|
+
token = await readOpenAIOAuthToken();
|
|
4568
|
+
if (token) _oauthTokenCache[providerId] = token;
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
if (!token) {
|
|
4572
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4573
|
+
res.end(JSON.stringify({ ok: false, error: "No OAuth token found — run login command first" }));
|
|
4574
|
+
return;
|
|
4575
|
+
}
|
|
4576
|
+
// Quick test call
|
|
4577
|
+
if (providerId === "anthropic-oauth") {
|
|
4578
|
+
const { computeVersionSuffix, buildBillingBlock, signBody } = await import("../crew-cli/dist/engine.mjs").catch(() => ({}));
|
|
4579
|
+
let responseText, testModel = model || "claude-sonnet-4-6";
|
|
4580
|
+
{
|
|
4581
|
+
const xxhash = (await import("xxhash-wasm")).default;
|
|
4582
|
+
const wasm = await xxhash();
|
|
4583
|
+
const CCH_SEED = BigInt("0x6E52736AC806831E");
|
|
4584
|
+
const VERSION = "2.1.87", SALT = "59cf53e54c78";
|
|
4585
|
+
const msg = "reply with exactly: OK";
|
|
4586
|
+
const chars = [4,7,20].map(i => i < msg.length ? msg[i] : "0").join("");
|
|
4587
|
+
const suffix = crypto.subtle
|
|
4588
|
+
? await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${SALT}${chars}${VERSION}`))
|
|
4589
|
+
.then(b => Array.from(new Uint8Array(b)).map(x=>x.toString(16).padStart(2,"0")).join("").slice(0,3))
|
|
4590
|
+
: createHash("sha256").update(`${SALT}${chars}${VERSION}`).digest("hex").slice(0,3);
|
|
4591
|
+
const billingText = `x-anthropic-billing-header: cc_version=${VERSION}.${suffix}; cc_entrypoint=cli; cch=00000;`;
|
|
4592
|
+
const supportsThinking = !testModel.includes("haiku");
|
|
4593
|
+
const bodyObj = {
|
|
4594
|
+
model: testModel, max_tokens: 50,
|
|
4595
|
+
...(supportsThinking ? { thinking: { type: "adaptive" } } : {}),
|
|
4596
|
+
metadata: { user_id: "user_crewswarm_test" },
|
|
4597
|
+
system: [{ type: "text", text: billingText }],
|
|
4598
|
+
messages: [{ role: "user", content: msg }],
|
|
4599
|
+
};
|
|
4600
|
+
const { system, messages, ...rest } = bodyObj;
|
|
4601
|
+
const bodyStr = JSON.stringify({ ...rest, system, messages });
|
|
4602
|
+
const cch = Number(wasm.h64(bodyStr, CCH_SEED) & BigInt(0xfffff)).toString(16).padStart(5,"0");
|
|
4603
|
+
const signed = bodyStr.replace("cch=00000", `cch=${cch}`);
|
|
4604
|
+
const BETAS = "claude-code-20250219,oauth-2025-04-20,adaptive-thinking-2026-01-28,research-preview-2026-02-01,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27";
|
|
4605
|
+
const r = await fetch("https://api.anthropic.com/v1/messages?beta=true", {
|
|
4606
|
+
method: "POST",
|
|
4607
|
+
headers: {
|
|
4608
|
+
Authorization: `Bearer ${token}`, "content-type": "application/json",
|
|
4609
|
+
"anthropic-version": "2023-06-01", "anthropic-beta": BETAS,
|
|
4610
|
+
"anthropic-dangerous-direct-browser-access": "true",
|
|
4611
|
+
"x-app": "cli", "user-agent": "claude-cli/2.1.87 (external, cli)",
|
|
4612
|
+
"x-claude-code-session-id": crypto.randomUUID(),
|
|
4613
|
+
},
|
|
4614
|
+
body: signed,
|
|
4615
|
+
signal: AbortSignal.timeout(15000),
|
|
4616
|
+
});
|
|
4617
|
+
const data = await r.json();
|
|
4618
|
+
if (!r.ok) throw new Error(data?.error?.message || r.statusText);
|
|
4619
|
+
responseText = data?.content?.find(b => b.type === "text")?.text?.trim() || "OK";
|
|
4620
|
+
}
|
|
4621
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4622
|
+
res.end(JSON.stringify({ ok: true, response: responseText, model: testModel }));
|
|
4623
|
+
} else if (providerId === "openai-oauth") {
|
|
4624
|
+
const testModel = model || "gpt-5.4";
|
|
4625
|
+
// Codex CLI backend requires streaming (SSE)
|
|
4626
|
+
const r = await fetch("https://chatgpt.com/backend-api/codex/responses", {
|
|
4627
|
+
method: "POST",
|
|
4628
|
+
headers: {
|
|
4629
|
+
Authorization: `Bearer ${token}`,
|
|
4630
|
+
"Content-Type": "application/json",
|
|
4631
|
+
"User-Agent": "openai-codex-cli/0.1.0",
|
|
4632
|
+
},
|
|
4633
|
+
body: JSON.stringify({
|
|
4634
|
+
model: testModel,
|
|
4635
|
+
instructions: "Reply with exactly: OK",
|
|
4636
|
+
input: [{ role: "user", content: "ping" }],
|
|
4637
|
+
stream: true,
|
|
4638
|
+
store: false,
|
|
4639
|
+
}),
|
|
4640
|
+
signal: AbortSignal.timeout(20000),
|
|
4641
|
+
});
|
|
4642
|
+
if (!r.ok) {
|
|
4643
|
+
const errData = await r.json().catch(() => ({}));
|
|
4644
|
+
throw new Error(errData?.detail || errData?.error?.message || r.statusText);
|
|
4645
|
+
}
|
|
4646
|
+
// Consume SSE stream and collect output_text delta chunks
|
|
4647
|
+
let responseText = "";
|
|
4648
|
+
const decoder = new TextDecoder();
|
|
4649
|
+
for await (const chunk of r.body) {
|
|
4650
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
4651
|
+
for (const line of text.split("\n")) {
|
|
4652
|
+
if (!line.startsWith("data: ")) continue;
|
|
4653
|
+
const raw = line.slice(6).trim();
|
|
4654
|
+
if (raw === "[DONE]") break;
|
|
4655
|
+
try {
|
|
4656
|
+
const evt = JSON.parse(raw);
|
|
4657
|
+
// response.output_text.delta event
|
|
4658
|
+
if (evt.type === "response.output_text.delta") responseText += evt.delta || "";
|
|
4659
|
+
// completed event with full output
|
|
4660
|
+
if (evt.type === "response.completed") {
|
|
4661
|
+
const items = evt.response?.output?.flatMap(o => o.content || []) || [];
|
|
4662
|
+
const full = items.find(c => c.type === "output_text")?.text;
|
|
4663
|
+
if (full) responseText = full;
|
|
4664
|
+
}
|
|
4665
|
+
} catch { /* skip malformed */ }
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4669
|
+
res.end(JSON.stringify({ ok: true, response: responseText.trim() || "OK", model: testModel }));
|
|
4670
|
+
} else {
|
|
4671
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4672
|
+
res.end(JSON.stringify({ ok: false, error: "Unknown provider" }));
|
|
4673
|
+
}
|
|
4674
|
+
} catch(e) {
|
|
4675
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4676
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
4677
|
+
}
|
|
4678
|
+
return;
|
|
4679
|
+
}
|
|
4680
|
+
if (url.pathname === "/api/oauth/models" && req.method === "GET") {
|
|
4681
|
+
const { providerId } = Object.fromEntries(url.searchParams);
|
|
4682
|
+
try {
|
|
4683
|
+
let token = getOAuthTokenCached(providerId);
|
|
4684
|
+
if (!token && providerId === "anthropic-oauth") {
|
|
4685
|
+
const { execFileSync } = await import("node:child_process");
|
|
4686
|
+
const { userInfo } = await import("node:os");
|
|
4687
|
+
for (const acct of [userInfo().username, "jeffhobbs", "unknown"]) {
|
|
4688
|
+
try {
|
|
4689
|
+
const raw = execFileSync("security", [
|
|
4690
|
+
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
4691
|
+
], { encoding: "utf8", timeout: 5000 }).trim();
|
|
4692
|
+
const t = JSON.parse(raw)?.claudeAiOauth?.accessToken;
|
|
4693
|
+
if (t) { token = t; _oauthTokenCache[providerId] = t; break; }
|
|
4694
|
+
} catch { /* try next */ }
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
if (providerId === "anthropic-oauth") {
|
|
4698
|
+
if (!token) throw new Error("No Anthropic OAuth token");
|
|
4699
|
+
const r = await fetch("https://api.anthropic.com/v1/models", {
|
|
4700
|
+
headers: {
|
|
4701
|
+
Authorization: `Bearer ${token}`,
|
|
4702
|
+
"anthropic-version": "2023-06-01",
|
|
4703
|
+
"anthropic-beta": "claude-code-20250219,oauth-2025-04-20",
|
|
4704
|
+
"x-app": "cli",
|
|
4705
|
+
},
|
|
4706
|
+
signal: AbortSignal.timeout(10000),
|
|
4707
|
+
});
|
|
4708
|
+
const data = await r.json();
|
|
4709
|
+
const models = (data?.data || []).map(m => ({ id: m.id, name: m.display_name || m.id }));
|
|
4710
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4711
|
+
res.end(JSON.stringify({ ok: true, models }));
|
|
4712
|
+
} else if (providerId === "openai-oauth") {
|
|
4713
|
+
// Codex subscription doesn't expose a /models list endpoint —
|
|
4714
|
+
// return the known codex-accessible models directly
|
|
4715
|
+
const models = [
|
|
4716
|
+
{ id: "gpt-5.4", name: "GPT-5.4 · Latest · Recommended" },
|
|
4717
|
+
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini · Smaller frontier" },
|
|
4718
|
+
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex · Codex-optimized" },
|
|
4719
|
+
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex · Frontier agentic" },
|
|
4720
|
+
{ id: "gpt-5.2", name: "GPT-5.2 · Professional / long-running agents" },
|
|
4721
|
+
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max · Deep & fast reasoning" },
|
|
4722
|
+
{ id: "gpt-5.1-codex-mini", name: "GPT-5.1 Codex Mini · Fast & cheap" },
|
|
4723
|
+
{ id: "gpt-4o", name: "GPT-4o · Fast & capable" },
|
|
4724
|
+
];
|
|
4725
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4726
|
+
res.end(JSON.stringify({ ok: true, models }));
|
|
4727
|
+
} else {
|
|
4728
|
+
throw new Error("Unknown providerId");
|
|
4729
|
+
}
|
|
4730
|
+
} catch(e) {
|
|
4731
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4732
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
4733
|
+
}
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
if (url.pathname === "/api/oauth/model" && req.method === "GET") {
|
|
4737
|
+
const cfg = readSwarmConfigSafe() || {};
|
|
4738
|
+
const models = {
|
|
4739
|
+
claudeOauthModel: cfg.claudeOauthModel || "claude-sonnet-4-6",
|
|
4740
|
+
openaiOauthModel: cfg.openaiOauthModel || "gpt-5.4",
|
|
4741
|
+
};
|
|
4742
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4743
|
+
res.end(JSON.stringify({ ok: true, models }));
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4746
|
+
if (url.pathname === "/api/oauth/model" && req.method === "POST") {
|
|
4747
|
+
let body = "";
|
|
4748
|
+
for await (const chunk of req) body += chunk;
|
|
4749
|
+
const data = JSON.parse(body);
|
|
4750
|
+
const cfg = readSwarmConfigSafe() || {};
|
|
4751
|
+
if (data.claudeOauthModel) cfg.claudeOauthModel = data.claudeOauthModel;
|
|
4752
|
+
if (data.openaiOauthModel) cfg.openaiOauthModel = data.openaiOauthModel;
|
|
4753
|
+
await safeWriteConfig(cfg);
|
|
4754
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
4755
|
+
res.end(JSON.stringify({ ok: true }));
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
|
|
3729
4759
|
if (url.pathname === "/api/providers/builtin" && req.method === "GET") {
|
|
3730
4760
|
const keys = {};
|
|
3731
4761
|
for (const id of Object.keys(BUILTIN_URLS)) {
|
|
3732
|
-
keys[id] = getBuiltinKey(id) ? "SET" : "";
|
|
4762
|
+
keys[id] = await getBuiltinKey(id) ? "SET" : "";
|
|
3733
4763
|
}
|
|
3734
4764
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3735
4765
|
res.end(JSON.stringify({ ok: true, keys }));
|
|
@@ -3746,16 +4776,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
3746
4776
|
if (providerId === "openai-local" && !(apiKey && apiKey.trim()))
|
|
3747
4777
|
apiKey = "key";
|
|
3748
4778
|
// Write to ~/.crewswarm/crewswarm.json
|
|
3749
|
-
const cfg = readCSSwarmConfig();
|
|
4779
|
+
const cfg = await readCSSwarmConfig();
|
|
3750
4780
|
if (!cfg.providers) cfg.providers = {};
|
|
3751
4781
|
cfg.providers[providerId] = {
|
|
3752
4782
|
...(cfg.providers[providerId] || {}),
|
|
3753
4783
|
apiKey,
|
|
3754
4784
|
baseUrl: BUILTIN_URLS[providerId],
|
|
3755
4785
|
};
|
|
3756
|
-
writeCSSwarmConfig(cfg);
|
|
4786
|
+
await writeCSSwarmConfig(cfg);
|
|
3757
4787
|
// Sync to ~/.openclaw/openclaw.json if it exists (legacy compat)
|
|
3758
|
-
const oc = readOCConfig();
|
|
4788
|
+
const oc = await readOCConfig();
|
|
3759
4789
|
if (oc) {
|
|
3760
4790
|
if (!oc.models) oc.models = {};
|
|
3761
4791
|
if (!oc.models.providers) oc.models.providers = {};
|
|
@@ -3767,7 +4797,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3767
4797
|
};
|
|
3768
4798
|
}
|
|
3769
4799
|
oc.models.providers[providerId].apiKey = apiKey;
|
|
3770
|
-
writeOCConfig(oc);
|
|
4800
|
+
await writeOCConfig(oc);
|
|
3771
4801
|
}
|
|
3772
4802
|
res.writeHead(200, { "content-type": "application/json" });
|
|
3773
4803
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -3779,8 +4809,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
3779
4809
|
) {
|
|
3780
4810
|
let body = "";
|
|
3781
4811
|
for await (const chunk of req) body += chunk;
|
|
3782
|
-
|
|
3783
|
-
|
|
4812
|
+
let parsed;
|
|
4813
|
+
try { parsed = JSON.parse(body); } catch {
|
|
4814
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
4815
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
4816
|
+
return;
|
|
4817
|
+
}
|
|
4818
|
+
const vr = validate(ProviderBuiltinTestSchema, parsed);
|
|
4819
|
+
if (!vr.ok) {
|
|
4820
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
4821
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
4822
|
+
return;
|
|
4823
|
+
}
|
|
4824
|
+
const { providerId } = vr.data;
|
|
4825
|
+
const apiKey = await getBuiltinKey(providerId);
|
|
3784
4826
|
const baseUrl = BUILTIN_URLS[providerId] || "";
|
|
3785
4827
|
if (providerId === "ollama") {
|
|
3786
4828
|
try {
|
|
@@ -3875,20 +4917,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3875
4917
|
return b;
|
|
3876
4918
|
})()
|
|
3877
4919
|
: null;
|
|
3878
|
-
const token = (
|
|
3879
|
-
try {
|
|
3880
|
-
return (
|
|
3881
|
-
JSON.parse(
|
|
3882
|
-
fs.readFileSync(
|
|
3883
|
-
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
3884
|
-
"utf8",
|
|
3885
|
-
),
|
|
3886
|
-
)?.rt?.authToken || ""
|
|
3887
|
-
);
|
|
3888
|
-
} catch {
|
|
3889
|
-
return "";
|
|
3890
|
-
}
|
|
3891
|
-
})();
|
|
4920
|
+
const token = resolveCrewLeadAuthToken();
|
|
3892
4921
|
const r = await fetch(
|
|
3893
4922
|
"http://127.0.0.1:5010/api/settings/bg-consciousness",
|
|
3894
4923
|
{
|
|
@@ -3925,20 +4954,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3925
4954
|
return b;
|
|
3926
4955
|
})()
|
|
3927
4956
|
: null;
|
|
3928
|
-
const token = (
|
|
3929
|
-
try {
|
|
3930
|
-
return (
|
|
3931
|
-
JSON.parse(
|
|
3932
|
-
fs.readFileSync(
|
|
3933
|
-
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
3934
|
-
"utf8",
|
|
3935
|
-
),
|
|
3936
|
-
)?.rt?.authToken || ""
|
|
3937
|
-
);
|
|
3938
|
-
} catch {
|
|
3939
|
-
return "";
|
|
3940
|
-
}
|
|
3941
|
-
})();
|
|
4957
|
+
const token = resolveCrewLeadAuthToken();
|
|
3942
4958
|
const r = await fetch(
|
|
3943
4959
|
"http://127.0.0.1:5010/api/settings/cursor-waves",
|
|
3944
4960
|
{
|
|
@@ -3975,20 +4991,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
3975
4991
|
return b;
|
|
3976
4992
|
})()
|
|
3977
4993
|
: null;
|
|
3978
|
-
const token = (
|
|
3979
|
-
try {
|
|
3980
|
-
return (
|
|
3981
|
-
JSON.parse(
|
|
3982
|
-
fs.readFileSync(
|
|
3983
|
-
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
3984
|
-
"utf8",
|
|
3985
|
-
),
|
|
3986
|
-
)?.rt?.authToken || ""
|
|
3987
|
-
);
|
|
3988
|
-
} catch {
|
|
3989
|
-
return "";
|
|
3990
|
-
}
|
|
3991
|
-
})();
|
|
4994
|
+
const token = resolveCrewLeadAuthToken();
|
|
3992
4995
|
const r = await fetch(
|
|
3993
4996
|
"http://127.0.0.1:5010/api/settings/claude-code",
|
|
3994
4997
|
{
|
|
@@ -4014,6 +5017,27 @@ const server = http.createServer(async (req, res) => {
|
|
|
4014
5017
|
}
|
|
4015
5018
|
return;
|
|
4016
5019
|
}
|
|
5020
|
+
// ── Proxy /api/settings/tmux-bridge → crew-lead:5010 ─────────────────────
|
|
5021
|
+
if (url.pathname === "/api/settings/tmux-bridge") {
|
|
5022
|
+
try {
|
|
5023
|
+
const rawBody =
|
|
5024
|
+
req.method === "POST"
|
|
5025
|
+
? await (async () => { let b = ""; for await (const c of req) b += c; return b; })()
|
|
5026
|
+
: null;
|
|
5027
|
+
const token = resolveCrewLeadAuthToken();
|
|
5028
|
+
const { data } = await crewLeadRequest("/api/settings/tmux-bridge", {
|
|
5029
|
+
method: req.method,
|
|
5030
|
+
body: rawBody || null,
|
|
5031
|
+
timeout: 8000,
|
|
5032
|
+
});
|
|
5033
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
5034
|
+
res.end(typeof data === "string" ? data : JSON.stringify(data));
|
|
5035
|
+
} catch (e) {
|
|
5036
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
5037
|
+
res.end(JSON.stringify({ ok: false, error: "crew-lead unreachable: " + e.message }));
|
|
5038
|
+
}
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
4017
5041
|
// ── Codex CLI executor toggle ──────────────────────────────────────────────
|
|
4018
5042
|
if (url.pathname === "/api/settings/codex") {
|
|
4019
5043
|
const { readFile, writeFile } = await import("node:fs/promises");
|
|
@@ -4099,7 +5123,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4099
5123
|
process.env.CREWSWARM_CREW_CLI_ENABLED === "1";
|
|
4100
5124
|
const installed =
|
|
4101
5125
|
commandExists("crew") ||
|
|
4102
|
-
|
|
5126
|
+
await exists(path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js"));
|
|
4103
5127
|
res.writeHead(200, { "content-type": "application/json" });
|
|
4104
5128
|
res.end(JSON.stringify({ enabled, installed }));
|
|
4105
5129
|
} catch {
|
|
@@ -4169,9 +5193,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4169
5193
|
return;
|
|
4170
5194
|
}
|
|
4171
5195
|
try {
|
|
4172
|
-
const raw = (() => {
|
|
5196
|
+
const raw = await (async () => {
|
|
4173
5197
|
try {
|
|
4174
|
-
return fs.
|
|
5198
|
+
return await fs.promises.readFile(CFG_FILE, "utf8");
|
|
4175
5199
|
} catch {
|
|
4176
5200
|
return "{}";
|
|
4177
5201
|
}
|
|
@@ -4223,7 +5247,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4223
5247
|
process.env.CREWSWARM_OPENCODE_ENABLED === "1";
|
|
4224
5248
|
const installed =
|
|
4225
5249
|
commandExists(process.env.CREWSWARM_OPENCODE_BIN || "opencode") ||
|
|
4226
|
-
|
|
5250
|
+
await exists(path.join(os.homedir(), ".opencode", "bin", "opencode"));
|
|
4227
5251
|
res.writeHead(200, { "content-type": "application/json" });
|
|
4228
5252
|
res.end(JSON.stringify({ enabled, installed }));
|
|
4229
5253
|
} catch {
|
|
@@ -4373,7 +5397,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4373
5397
|
const cfgPath = CFG_FILE;
|
|
4374
5398
|
if (req.method === "GET") {
|
|
4375
5399
|
try {
|
|
4376
|
-
const cfg = JSON.parse(fs.
|
|
5400
|
+
const cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
|
|
4377
5401
|
const roles = cfg.roleToolDefaults || {};
|
|
4378
5402
|
res.writeHead(200, { "content-type": "application/json" });
|
|
4379
5403
|
res.end(JSON.stringify({ roles }));
|
|
@@ -4388,7 +5412,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4388
5412
|
let body = "";
|
|
4389
5413
|
for await (const chunk of req) body += chunk;
|
|
4390
5414
|
const { roles } = JSON.parse(body || "{}");
|
|
4391
|
-
const cfg = JSON.parse(fs.
|
|
5415
|
+
const cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
|
|
4392
5416
|
cfg.roleToolDefaults = roles || {};
|
|
4393
5417
|
const writeErr = await safeWriteConfig(cfg);
|
|
4394
5418
|
if (writeErr) {
|
|
@@ -4410,7 +5434,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4410
5434
|
const cfgPath = CFG_FILE;
|
|
4411
5435
|
if (req.method === "GET") {
|
|
4412
5436
|
try {
|
|
4413
|
-
const cfg = JSON.parse(fs.
|
|
5437
|
+
const cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
|
|
4414
5438
|
const caps = cfg.globalSpendingCaps || {};
|
|
4415
5439
|
res.writeHead(200, { "content-type": "application/json" });
|
|
4416
5440
|
res.end(
|
|
@@ -4432,7 +5456,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4432
5456
|
const { dailyTokenLimit, dailyCostLimitUSD } = JSON.parse(
|
|
4433
5457
|
body || "{}",
|
|
4434
5458
|
);
|
|
4435
|
-
const cfg = JSON.parse(fs.
|
|
5459
|
+
const cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
|
|
4436
5460
|
cfg.globalSpendingCaps = {
|
|
4437
5461
|
dailyTokenLimit: dailyTokenLimit ?? undefined,
|
|
4438
5462
|
dailyCostLimitUSD: dailyCostLimitUSD ?? undefined,
|
|
@@ -4462,8 +5486,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
4462
5486
|
const rulesPath = path.join(CFG_DIR, "global-rules.md");
|
|
4463
5487
|
if (req.method === "GET") {
|
|
4464
5488
|
try {
|
|
4465
|
-
const content =
|
|
4466
|
-
? fs.
|
|
5489
|
+
const content = await exists(rulesPath)
|
|
5490
|
+
? await fs.promises.readFile(rulesPath, "utf8")
|
|
4467
5491
|
: "";
|
|
4468
5492
|
res.writeHead(200, { "content-type": "application/json" });
|
|
4469
5493
|
res.end(JSON.stringify({ content }));
|
|
@@ -4478,7 +5502,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4478
5502
|
let body = "";
|
|
4479
5503
|
for await (const chunk of req) body += chunk;
|
|
4480
5504
|
const { content } = JSON.parse(body || "{}");
|
|
4481
|
-
fs.
|
|
5505
|
+
await fs.promises.writeFile(rulesPath, content || "", "utf8");
|
|
4482
5506
|
res.writeHead(200, { "content-type": "application/json" });
|
|
4483
5507
|
res.end(JSON.stringify({ ok: true }));
|
|
4484
5508
|
} catch (e) {
|
|
@@ -4528,20 +5552,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4528
5552
|
for await (const c of req) b += c;
|
|
4529
5553
|
return b;
|
|
4530
5554
|
})();
|
|
4531
|
-
const token = (
|
|
4532
|
-
try {
|
|
4533
|
-
return (
|
|
4534
|
-
JSON.parse(
|
|
4535
|
-
fs.readFileSync(
|
|
4536
|
-
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
4537
|
-
"utf8",
|
|
4538
|
-
),
|
|
4539
|
-
)?.rt?.authToken || ""
|
|
4540
|
-
);
|
|
4541
|
-
} catch {
|
|
4542
|
-
return "";
|
|
4543
|
-
}
|
|
4544
|
-
})();
|
|
5555
|
+
const token = resolveCrewLeadAuthToken();
|
|
4545
5556
|
const upstream = await fetch(
|
|
4546
5557
|
"http://127.0.0.1:5010/api/engine-passthrough",
|
|
4547
5558
|
{
|
|
@@ -4606,20 +5617,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4606
5617
|
return b;
|
|
4607
5618
|
})()
|
|
4608
5619
|
: null;
|
|
4609
|
-
const token = (
|
|
4610
|
-
try {
|
|
4611
|
-
return (
|
|
4612
|
-
JSON.parse(
|
|
4613
|
-
fs.readFileSync(
|
|
4614
|
-
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
4615
|
-
"utf8",
|
|
4616
|
-
),
|
|
4617
|
-
)?.rt?.authToken || ""
|
|
4618
|
-
);
|
|
4619
|
-
} catch {
|
|
4620
|
-
return "";
|
|
4621
|
-
}
|
|
4622
|
-
})();
|
|
5620
|
+
const token = resolveCrewLeadAuthToken();
|
|
4623
5621
|
const r = await fetch(
|
|
4624
5622
|
"http://127.0.0.1:5010/api/settings/global-fallback",
|
|
4625
5623
|
{
|
|
@@ -4657,7 +5655,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4657
5655
|
);
|
|
4658
5656
|
const deviceJsonAlt = path.join(os.homedir(), ".openclaw", "device.json");
|
|
4659
5657
|
const installed =
|
|
4660
|
-
|
|
5658
|
+
await exists(deviceJson) || await exists(deviceJsonAlt);
|
|
4661
5659
|
res.writeHead(200, { "content-type": "application/json" });
|
|
4662
5660
|
res.end(JSON.stringify({ ok: true, installed }));
|
|
4663
5661
|
return;
|
|
@@ -4667,10 +5665,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
4667
5665
|
try {
|
|
4668
5666
|
let online = false;
|
|
4669
5667
|
try {
|
|
4670
|
-
const
|
|
4671
|
-
|
|
4672
|
-
});
|
|
4673
|
-
online = health.ok;
|
|
5668
|
+
const { ok } = await crewLeadRequest("/health", { timeout: 1500 });
|
|
5669
|
+
online = ok;
|
|
4674
5670
|
} catch { }
|
|
4675
5671
|
if (!online) {
|
|
4676
5672
|
const { execSync: es } = await import("node:child_process");
|
|
@@ -4690,11 +5686,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
4690
5686
|
return;
|
|
4691
5687
|
}
|
|
4692
5688
|
|
|
4693
|
-
// ── Models API (list available models for
|
|
5689
|
+
// ── Models API (list available models for crewchat dropdown) ─────────────
|
|
4694
5690
|
if (url.pathname === "/api/models" && req.method === "GET") {
|
|
4695
5691
|
try {
|
|
4696
5692
|
const csSwarm = JSON.parse(
|
|
4697
|
-
fs.
|
|
5693
|
+
await fs.promises.readFile(
|
|
4698
5694
|
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
4699
5695
|
"utf8",
|
|
4700
5696
|
),
|
|
@@ -4780,7 +5776,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4780
5776
|
return;
|
|
4781
5777
|
}
|
|
4782
5778
|
|
|
4783
|
-
// ── CLI Chat API (
|
|
5779
|
+
// ── CLI Chat API (crewchat CLI mode passthrough) ──────────────────────
|
|
4784
5780
|
if (url.pathname === "/api/cli/chat" && req.method === "POST") {
|
|
4785
5781
|
let body = "";
|
|
4786
5782
|
for await (const chunk of req) body += chunk;
|
|
@@ -4802,8 +5798,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
4802
5798
|
if (projectId) {
|
|
4803
5799
|
try {
|
|
4804
5800
|
const registryFile = path.join(CFG_DIR, "projects.json");
|
|
4805
|
-
const projects =
|
|
4806
|
-
? JSON.parse(fs.
|
|
5801
|
+
const projects = await exists(registryFile)
|
|
5802
|
+
? JSON.parse(await fs.promises.readFile(registryFile, "utf8"))
|
|
4807
5803
|
: {};
|
|
4808
5804
|
projectDir = projects?.[projectId]?.outputDir || null;
|
|
4809
5805
|
} catch { }
|
|
@@ -4904,7 +5900,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4904
5900
|
|
|
4905
5901
|
// Load agent config
|
|
4906
5902
|
const csSwarm = JSON.parse(
|
|
4907
|
-
fs.
|
|
5903
|
+
await fs.promises.readFile(
|
|
4908
5904
|
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
4909
5905
|
"utf8",
|
|
4910
5906
|
),
|
|
@@ -4944,7 +5940,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4944
5940
|
);
|
|
4945
5941
|
let systemPrompt = `You are ${agentId}.`;
|
|
4946
5942
|
try {
|
|
4947
|
-
const prompts = JSON.parse(fs.
|
|
5943
|
+
const prompts = JSON.parse(await fs.promises.readFile(promptPath, "utf8"));
|
|
4948
5944
|
const bareId = agentId.replace(/^crew-/, "");
|
|
4949
5945
|
systemPrompt = prompts[agentId] || prompts[bareId] || systemPrompt;
|
|
4950
5946
|
} catch { }
|
|
@@ -5020,7 +6016,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5020
6016
|
return;
|
|
5021
6017
|
}
|
|
5022
6018
|
|
|
5023
|
-
// ── Chat Agent API (
|
|
6019
|
+
// ── Chat Agent API (crewchat direct agent chat) ──────────────────────────
|
|
5024
6020
|
if (url.pathname === "/api/chat-agent" && req.method === "POST") {
|
|
5025
6021
|
let body = "";
|
|
5026
6022
|
for await (const chunk of req) body += chunk;
|
|
@@ -5068,7 +6064,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5068
6064
|
return;
|
|
5069
6065
|
}
|
|
5070
6066
|
|
|
5071
|
-
// ── Dispatch API (
|
|
6067
|
+
// ── Dispatch API (crewchat agent direct mode) ─────────────────────────────
|
|
5072
6068
|
if (url.pathname === "/api/dispatch" && req.method === "POST") {
|
|
5073
6069
|
let body = "";
|
|
5074
6070
|
for await (const chunk of req) body += chunk;
|
|
@@ -5139,7 +6135,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5139
6135
|
}
|
|
5140
6136
|
|
|
5141
6137
|
if (url.pathname === "/api/transcribe-audio" && req.method === "POST") {
|
|
5142
|
-
// Expects multipart/form-data with audio file (
|
|
6138
|
+
// Expects multipart/form-data with audio file (crewchat: m4a, Dashboard: webm)
|
|
5143
6139
|
// Per Groq docs: https://console.groq.com/docs/speech-to-text — file, model required
|
|
5144
6140
|
const sendJson = (status, body) => {
|
|
5145
6141
|
if (res.headersSent) return;
|
|
@@ -5149,7 +6145,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5149
6145
|
try {
|
|
5150
6146
|
const busboy = await import("busboy");
|
|
5151
6147
|
const chunks = [];
|
|
5152
|
-
let mimeType = "audio/m4a"; //
|
|
6148
|
+
let mimeType = "audio/m4a"; // crewchat default
|
|
5153
6149
|
let resolved = false;
|
|
5154
6150
|
const resolveOnce = () => {
|
|
5155
6151
|
if (resolved) return;
|
|
@@ -5235,8 +6231,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
5235
6231
|
if (!resolvedProjectDir && projectId) {
|
|
5236
6232
|
try {
|
|
5237
6233
|
const registryFile = path.join(CFG_DIR, "projects.json");
|
|
5238
|
-
const projects =
|
|
5239
|
-
? JSON.parse(fs.
|
|
6234
|
+
const projects = await exists(registryFile)
|
|
6235
|
+
? JSON.parse(await fs.promises.readFile(registryFile, "utf8"))
|
|
5240
6236
|
: {};
|
|
5241
6237
|
resolvedProjectDir = projects?.[projectId]?.outputDir || null;
|
|
5242
6238
|
} catch { }
|
|
@@ -5269,7 +6265,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
5269
6265
|
`http://127.0.0.1:${listenPort}/api/engine-passthrough`,
|
|
5270
6266
|
{
|
|
5271
6267
|
method: "POST",
|
|
5272
|
-
headers: {
|
|
6268
|
+
headers: {
|
|
6269
|
+
"content-type": "application/json",
|
|
6270
|
+
...(clAuthToken ? { authorization: `Bearer ${clAuthToken}` } : {}),
|
|
6271
|
+
},
|
|
5273
6272
|
body: JSON.stringify(payload),
|
|
5274
6273
|
signal: AbortSignal.timeout(240000),
|
|
5275
6274
|
},
|
|
@@ -5371,7 +6370,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5371
6370
|
}
|
|
5372
6371
|
|
|
5373
6372
|
if (wantAgentSSE && upstream.body) {
|
|
5374
|
-
// SSE streaming path (Vibe
|
|
6373
|
+
// SSE streaming path (Dashboard or Vibe)
|
|
5375
6374
|
res.writeHead(200, {
|
|
5376
6375
|
"content-type": "text/event-stream",
|
|
5377
6376
|
"cache-control": "no-cache",
|
|
@@ -5471,7 +6470,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5471
6470
|
}
|
|
5472
6471
|
|
|
5473
6472
|
if (wantSSE && upstream.body) {
|
|
5474
|
-
// SSE streaming path (Vibe
|
|
6473
|
+
// SSE streaming path (Dashboard or Vibe)
|
|
5475
6474
|
res.writeHead(200, {
|
|
5476
6475
|
"content-type": "text/event-stream",
|
|
5477
6476
|
"cache-control": "no-cache",
|
|
@@ -5820,9 +6819,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
5820
6819
|
const { readFile, writeFile } = await import("node:fs/promises");
|
|
5821
6820
|
let body = "";
|
|
5822
6821
|
for await (const chunk of req) body += chunk;
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
6822
|
+
let parsed;
|
|
6823
|
+
try { parsed = JSON.parse(body); } catch {
|
|
6824
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
6825
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
6826
|
+
return;
|
|
6827
|
+
}
|
|
6828
|
+
const vr = validate(ProviderSaveSchema, parsed);
|
|
6829
|
+
if (!vr.ok) {
|
|
6830
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
6831
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
6834
|
+
const { providerId, apiKey } = vr.data;
|
|
5826
6835
|
const cfgPath = CFG_FILE;
|
|
5827
6836
|
const cfg = JSON.parse(await readFile(cfgPath, "utf8"));
|
|
5828
6837
|
const fromModels = cfg?.models?.providers?.[providerId];
|
|
@@ -5870,8 +6879,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
5870
6879
|
const { readFile, writeFile } = await import("node:fs/promises");
|
|
5871
6880
|
let body = "";
|
|
5872
6881
|
for await (const chunk of req) body += chunk;
|
|
5873
|
-
|
|
5874
|
-
|
|
6882
|
+
let parsed;
|
|
6883
|
+
try { parsed = JSON.parse(body); } catch {
|
|
6884
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
6885
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
6886
|
+
return;
|
|
6887
|
+
}
|
|
6888
|
+
const vr = validate(ProviderAddSchema, parsed);
|
|
6889
|
+
if (!vr.ok) {
|
|
6890
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
6891
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
6892
|
+
return;
|
|
6893
|
+
}
|
|
6894
|
+
const { id, baseUrl, apiKey, api } = vr.data;
|
|
5875
6895
|
const cfgPath = CFG_FILE;
|
|
5876
6896
|
const cfg = JSON.parse(await readFile(cfgPath, "utf8"));
|
|
5877
6897
|
if (!cfg.models) cfg.models = {};
|
|
@@ -6191,7 +7211,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
6191
7211
|
if (url.pathname === "/api/providers/test" && req.method === "POST") {
|
|
6192
7212
|
let body = "";
|
|
6193
7213
|
for await (const chunk of req) body += chunk;
|
|
6194
|
-
|
|
7214
|
+
let parsed;
|
|
7215
|
+
try { parsed = JSON.parse(body); } catch {
|
|
7216
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
7217
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
7218
|
+
return;
|
|
7219
|
+
}
|
|
7220
|
+
const vr = validate(ProviderTestSchema, parsed);
|
|
7221
|
+
if (!vr.ok) {
|
|
7222
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
7223
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
7224
|
+
return;
|
|
7225
|
+
}
|
|
7226
|
+
const { providerId } = vr.data;
|
|
6195
7227
|
const { readFile } = await import("node:fs/promises");
|
|
6196
7228
|
const cfgPath = CFG_FILE;
|
|
6197
7229
|
const cfg = JSON.parse(await readFile(cfgPath, "utf8").catch(() => "{}"));
|
|
@@ -6618,7 +7650,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
6618
7650
|
try {
|
|
6619
7651
|
const { execFile } = await import("node:child_process");
|
|
6620
7652
|
const ocBin = path.join(os.homedir(), ".opencode", "bin", "opencode");
|
|
6621
|
-
const bin =
|
|
7653
|
+
const bin = await exists(ocBin) ? ocBin : "opencode";
|
|
6622
7654
|
models = await new Promise((resolve, reject) => {
|
|
6623
7655
|
const child = execFile(
|
|
6624
7656
|
bin,
|
|
@@ -6644,7 +7676,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
6644
7676
|
"opencode",
|
|
6645
7677
|
"auth.json",
|
|
6646
7678
|
);
|
|
6647
|
-
const auth = JSON.parse(fs.
|
|
7679
|
+
const auth = JSON.parse(await fs.promises.readFile(authPath, "utf8"));
|
|
6648
7680
|
const providers = Object.keys(auth || {}).map((k) => k.toLowerCase());
|
|
6649
7681
|
const knownModels = {
|
|
6650
7682
|
openai: [
|
|
@@ -6908,6 +7940,16 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
6908
7940
|
for (const m of defaultModels) {
|
|
6909
7941
|
if (!allModels.includes(m)) allModels.push(m);
|
|
6910
7942
|
}
|
|
7943
|
+
// Inject OAuth subscription models so they appear in agent primary/fallback dropdowns
|
|
7944
|
+
const CLAUDE_OAUTH_MODELS = [
|
|
7945
|
+
'claude-haiku-4-5-20251001',
|
|
7946
|
+
'claude-sonnet-4-6',
|
|
7947
|
+
'claude-opus-4-6',
|
|
7948
|
+
];
|
|
7949
|
+
const OPENAI_OAUTH_MODELS = ['gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5.2', 'gpt-5.1-codex-max', 'gpt-5.1-codex-mini', 'gpt-4o'];
|
|
7950
|
+
for (const m of [...CLAUDE_OAUTH_MODELS, ...OPENAI_OAUTH_MODELS]) {
|
|
7951
|
+
if (!allModels.includes(m)) allModels.push(m);
|
|
7952
|
+
}
|
|
6911
7953
|
const roleToolDefaults = cfg.roleToolDefaults || {};
|
|
6912
7954
|
res.writeHead(200, { "content-type": "application/json" });
|
|
6913
7955
|
res.end(
|
|
@@ -6985,7 +8027,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
6985
8027
|
await import("../lib/memory/shared-adapter.mjs");
|
|
6986
8028
|
const brainPath = path.join(CREWSWARM_DIR, "memory", "brain.md");
|
|
6987
8029
|
|
|
6988
|
-
if (!
|
|
8030
|
+
if (!await exists(brainPath)) {
|
|
6989
8031
|
res.statusCode = 404;
|
|
6990
8032
|
res.end(JSON.stringify({ ok: false, error: "brain.md not found" }));
|
|
6991
8033
|
return;
|
|
@@ -7204,6 +8246,18 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7204
8246
|
const { readFile, writeFile } = await import("node:fs/promises");
|
|
7205
8247
|
let body = "";
|
|
7206
8248
|
for await (const chunk of req) body += chunk;
|
|
8249
|
+
let parsed;
|
|
8250
|
+
try { parsed = JSON.parse(body); } catch {
|
|
8251
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8252
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
8253
|
+
return;
|
|
8254
|
+
}
|
|
8255
|
+
const vr = validate(AgentConfigCreateSchema, parsed);
|
|
8256
|
+
if (!vr.ok) {
|
|
8257
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8258
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
8259
|
+
return;
|
|
8260
|
+
}
|
|
7207
8261
|
const {
|
|
7208
8262
|
id,
|
|
7209
8263
|
model,
|
|
@@ -7212,7 +8266,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7212
8266
|
theme,
|
|
7213
8267
|
systemPrompt,
|
|
7214
8268
|
alsoAllow: reqAlsoAllow,
|
|
7215
|
-
} =
|
|
8269
|
+
} = vr.data;
|
|
7216
8270
|
const rawId = String(id || "");
|
|
7217
8271
|
const normalizedId =
|
|
7218
8272
|
rawId && !rawId.startsWith("crew-")
|
|
@@ -7302,8 +8356,19 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7302
8356
|
const { readFile, writeFile } = await import("node:fs/promises");
|
|
7303
8357
|
let body = "";
|
|
7304
8358
|
for await (const chunk of req) body += chunk;
|
|
7305
|
-
|
|
7306
|
-
|
|
8359
|
+
let parsed;
|
|
8360
|
+
try { parsed = JSON.parse(body); } catch {
|
|
8361
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8362
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
8363
|
+
return;
|
|
8364
|
+
}
|
|
8365
|
+
const vr = validate(AgentConfigDeleteSchema, parsed);
|
|
8366
|
+
if (!vr.ok) {
|
|
8367
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8368
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
8369
|
+
return;
|
|
8370
|
+
}
|
|
8371
|
+
const { agentId } = vr.data;
|
|
7307
8372
|
const cfgPath = CFG_FILE;
|
|
7308
8373
|
const promptsPath = path.join(CFG_DIR, "agent-prompts.json");
|
|
7309
8374
|
const cfg = JSON.parse(await readFile(cfgPath, "utf8"));
|
|
@@ -7351,12 +8416,19 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7351
8416
|
) {
|
|
7352
8417
|
let body = "";
|
|
7353
8418
|
for await (const chunk of req) body += chunk;
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
res.writeHead(400);
|
|
7357
|
-
res.end(JSON.stringify({ error: "
|
|
8419
|
+
let parsed;
|
|
8420
|
+
try { parsed = JSON.parse(body || "{}"); } catch {
|
|
8421
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8422
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
8423
|
+
return;
|
|
8424
|
+
}
|
|
8425
|
+
const vr = validate(AgentResetSessionSchema, parsed);
|
|
8426
|
+
if (!vr.ok) {
|
|
8427
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8428
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
7358
8429
|
return;
|
|
7359
8430
|
}
|
|
8431
|
+
const { agentId } = vr.data;
|
|
7360
8432
|
const { execFile } = await import("node:child_process");
|
|
7361
8433
|
const bridgePath = path.join(CREWSWARM_DIR, "gateway-bridge.mjs");
|
|
7362
8434
|
// 1. Reset the agent session via gateway-bridge --reset-session
|
|
@@ -7389,9 +8461,8 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7389
8461
|
|
|
7390
8462
|
if (url.pathname === "/api/crew/start" && req.method === "POST") {
|
|
7391
8463
|
const { spawn: spawnProc } = await import("node:child_process");
|
|
7392
|
-
const { existsSync: eS } = await import("node:fs");
|
|
7393
8464
|
const crewScript = path.join(CREWSWARM_DIR, "scripts", "start-crew.mjs");
|
|
7394
|
-
if (!
|
|
8465
|
+
if (!(await exists(crewScript)))
|
|
7395
8466
|
throw new Error(
|
|
7396
8467
|
"start-crew.mjs not found — is the dashboard running from the crewswarm repo?",
|
|
7397
8468
|
);
|
|
@@ -7434,9 +8505,19 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7434
8505
|
const { writeFile } = await import("node:fs/promises");
|
|
7435
8506
|
let body = "";
|
|
7436
8507
|
for await (const chunk of req) body += chunk;
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
8508
|
+
let parsed;
|
|
8509
|
+
try { parsed = JSON.parse(body); } catch {
|
|
8510
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8511
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
8512
|
+
return;
|
|
8513
|
+
}
|
|
8514
|
+
const vr = validate(RoadmapWriteSchema, parsed);
|
|
8515
|
+
if (!vr.ok) {
|
|
8516
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8517
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
8518
|
+
return;
|
|
8519
|
+
}
|
|
8520
|
+
const { roadmapFile, content } = vr.data;
|
|
7440
8521
|
await writeFile(roadmapFile, content, "utf8");
|
|
7441
8522
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7442
8523
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -7448,8 +8529,19 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7448
8529
|
const { readFile, writeFile } = await import("node:fs/promises");
|
|
7449
8530
|
let body = "";
|
|
7450
8531
|
for await (const chunk of req) body += chunk;
|
|
7451
|
-
|
|
7452
|
-
|
|
8532
|
+
let parsed;
|
|
8533
|
+
try { parsed = JSON.parse(body); } catch {
|
|
8534
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8535
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
8536
|
+
return;
|
|
8537
|
+
}
|
|
8538
|
+
const vr = validate(RoadmapRetryFailedSchema, parsed);
|
|
8539
|
+
if (!vr.ok) {
|
|
8540
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
8541
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
8542
|
+
return;
|
|
8543
|
+
}
|
|
8544
|
+
const { roadmapFile } = vr.data;
|
|
7453
8545
|
const content = await readFile(roadmapFile, "utf8");
|
|
7454
8546
|
// Strip [!] markers back to [ ] and remove failure timestamps
|
|
7455
8547
|
const reset = content
|
|
@@ -7492,13 +8584,13 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7492
8584
|
"agent-prompts.json",
|
|
7493
8585
|
);
|
|
7494
8586
|
try {
|
|
7495
|
-
const promptsRaw = JSON.parse(fs.
|
|
8587
|
+
const promptsRaw = JSON.parse(await fs.promises.readFile(promptsFile, "utf8"));
|
|
7496
8588
|
const { applySharedChatPromptOverlay } = await import(
|
|
7497
8589
|
"../lib/chat/shared-chat-prompt-overlay.mjs"
|
|
7498
8590
|
);
|
|
7499
8591
|
let agents = [];
|
|
7500
8592
|
try {
|
|
7501
|
-
const cfg = JSON.parse(fs.
|
|
8593
|
+
const cfg = JSON.parse(await fs.promises.readFile(CFG_FILE, "utf8"));
|
|
7502
8594
|
const list = Array.isArray(cfg.agents)
|
|
7503
8595
|
? cfg.agents
|
|
7504
8596
|
: Array.isArray(cfg.agents?.list)
|
|
@@ -7561,13 +8653,13 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7561
8653
|
try {
|
|
7562
8654
|
let prompts = {};
|
|
7563
8655
|
try {
|
|
7564
|
-
prompts = JSON.parse(fs.
|
|
8656
|
+
prompts = JSON.parse(await fs.promises.readFile(promptsFile, "utf8"));
|
|
7565
8657
|
} catch { }
|
|
7566
8658
|
|
|
7567
8659
|
const keySet = new Set(Object.keys(prompts));
|
|
7568
8660
|
let agentIds = [];
|
|
7569
8661
|
try {
|
|
7570
|
-
const cfg = JSON.parse(fs.
|
|
8662
|
+
const cfg = JSON.parse(await fs.promises.readFile(CFG_FILE, "utf8"));
|
|
7571
8663
|
const list = Array.isArray(cfg.agents)
|
|
7572
8664
|
? cfg.agents
|
|
7573
8665
|
: Array.isArray(cfg.agents?.list)
|
|
@@ -7586,7 +8678,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7586
8678
|
delete prompts[canonicalAgent.slice(5)];
|
|
7587
8679
|
}
|
|
7588
8680
|
|
|
7589
|
-
fs.
|
|
8681
|
+
await fs.promises.writeFile(promptsFile, JSON.stringify(prompts, null, 2), "utf8");
|
|
7590
8682
|
console.log(
|
|
7591
8683
|
`[dashboard] Prompt updated for ${canonicalAgent} (${prompt.length} chars)`,
|
|
7592
8684
|
);
|
|
@@ -7624,11 +8716,11 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7624
8716
|
]);
|
|
7625
8717
|
const MAX_FILES = 500;
|
|
7626
8718
|
const results = [];
|
|
7627
|
-
function walk(dir, depth) {
|
|
8719
|
+
async function walk(dir, depth) {
|
|
7628
8720
|
if (depth > 5) return;
|
|
7629
8721
|
let entries;
|
|
7630
8722
|
try {
|
|
7631
|
-
entries = fs.
|
|
8723
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
7632
8724
|
} catch {
|
|
7633
8725
|
return;
|
|
7634
8726
|
}
|
|
@@ -7636,12 +8728,12 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7636
8728
|
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
7637
8729
|
const full = path.join(dir, e.name);
|
|
7638
8730
|
if (e.isDirectory()) {
|
|
7639
|
-
walk(full, depth + 1);
|
|
8731
|
+
await walk(full, depth + 1);
|
|
7640
8732
|
} else if (e.isFile()) {
|
|
7641
8733
|
const ext = path.extname(e.name).toLowerCase();
|
|
7642
8734
|
if (!ALLOWED_EXT.has(ext)) continue;
|
|
7643
8735
|
try {
|
|
7644
|
-
const st = fs.
|
|
8736
|
+
const st = await fs.promises.stat(full);
|
|
7645
8737
|
results.push({ path: full, size: st.size, mtime: st.mtimeMs });
|
|
7646
8738
|
} catch {
|
|
7647
8739
|
/* skip */
|
|
@@ -7650,7 +8742,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7650
8742
|
}
|
|
7651
8743
|
}
|
|
7652
8744
|
}
|
|
7653
|
-
walk(scanDir, 0);
|
|
8745
|
+
await walk(scanDir, 0);
|
|
7654
8746
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
7655
8747
|
res.writeHead(200, {
|
|
7656
8748
|
"content-type": "application/json",
|
|
@@ -7669,7 +8761,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7669
8761
|
return;
|
|
7670
8762
|
}
|
|
7671
8763
|
try {
|
|
7672
|
-
const raw = fs.
|
|
8764
|
+
const raw = await fs.promises.readFile(filePath, "utf8");
|
|
7673
8765
|
const lines = raw.split("\n");
|
|
7674
8766
|
const content =
|
|
7675
8767
|
lines.length > 300
|
|
@@ -7710,17 +8802,17 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7710
8802
|
"telegram-messages.jsonl",
|
|
7711
8803
|
);
|
|
7712
8804
|
|
|
7713
|
-
function loadTgConfig() {
|
|
8805
|
+
async function loadTgConfig() {
|
|
7714
8806
|
try {
|
|
7715
|
-
return JSON.parse(fs.
|
|
8807
|
+
return JSON.parse(await fs.promises.readFile(TG_CONFIG_PATH, "utf8"));
|
|
7716
8808
|
} catch {
|
|
7717
8809
|
return {};
|
|
7718
8810
|
}
|
|
7719
8811
|
}
|
|
7720
8812
|
|
|
7721
|
-
function isTgRunning() {
|
|
8813
|
+
async function isTgRunning() {
|
|
7722
8814
|
try {
|
|
7723
|
-
const pid = parseInt(fs.
|
|
8815
|
+
const pid = parseInt((await fs.promises.readFile(TG_PID_PATH, "utf8")).trim(), 10);
|
|
7724
8816
|
if (!pid) return false;
|
|
7725
8817
|
process.kill(pid, 0);
|
|
7726
8818
|
return true;
|
|
@@ -7730,15 +8822,15 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7730
8822
|
}
|
|
7731
8823
|
|
|
7732
8824
|
if (url.pathname === "/api/telegram/status") {
|
|
7733
|
-
const running = isTgRunning();
|
|
7734
|
-
const cfg = loadTgConfig();
|
|
8825
|
+
const running = await isTgRunning();
|
|
8826
|
+
const cfg = await loadTgConfig();
|
|
7735
8827
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7736
8828
|
res.end(JSON.stringify({ running, botName: cfg.botName || "" }));
|
|
7737
8829
|
return;
|
|
7738
8830
|
}
|
|
7739
8831
|
|
|
7740
8832
|
if (url.pathname === "/api/telegram/config" && req.method === "GET") {
|
|
7741
|
-
const cfg = loadTgConfig();
|
|
8833
|
+
const cfg = await loadTgConfig();
|
|
7742
8834
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7743
8835
|
res.end(
|
|
7744
8836
|
JSON.stringify({
|
|
@@ -7757,9 +8849,9 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7757
8849
|
let raw = "";
|
|
7758
8850
|
for await (const chunk of req) raw += chunk;
|
|
7759
8851
|
const body = JSON.parse(raw || "{}");
|
|
7760
|
-
const existing = loadTgConfig();
|
|
8852
|
+
const existing = await loadTgConfig();
|
|
7761
8853
|
const updated = { ...existing, ...body };
|
|
7762
|
-
fs.
|
|
8854
|
+
await fs.promises.writeFile(TG_CONFIG_PATH, JSON.stringify(updated, null, 2));
|
|
7763
8855
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7764
8856
|
res.end(JSON.stringify({ ok: true }));
|
|
7765
8857
|
return;
|
|
@@ -7770,19 +8862,19 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7770
8862
|
for await (const chunk of req) raw += chunk;
|
|
7771
8863
|
const body = JSON.parse(raw || "{}");
|
|
7772
8864
|
if (body.token) {
|
|
7773
|
-
const existing = loadTgConfig();
|
|
7774
|
-
fs.
|
|
8865
|
+
const existing = await loadTgConfig();
|
|
8866
|
+
await fs.promises.writeFile(
|
|
7775
8867
|
TG_CONFIG_PATH,
|
|
7776
8868
|
JSON.stringify({ ...existing, ...body }, null, 2),
|
|
7777
8869
|
);
|
|
7778
8870
|
}
|
|
7779
|
-
const cfg = loadTgConfig();
|
|
8871
|
+
const cfg = await loadTgConfig();
|
|
7780
8872
|
if (!cfg.token) {
|
|
7781
8873
|
res.writeHead(400, { "content-type": "application/json" });
|
|
7782
8874
|
res.end(JSON.stringify({ error: "No token configured" }));
|
|
7783
8875
|
return;
|
|
7784
8876
|
}
|
|
7785
|
-
if (isTgRunning()) {
|
|
8877
|
+
if (await isTgRunning()) {
|
|
7786
8878
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7787
8879
|
res.end(JSON.stringify({ ok: true, message: "Already running" }));
|
|
7788
8880
|
return;
|
|
@@ -7807,7 +8899,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7807
8899
|
|
|
7808
8900
|
if (url.pathname === "/api/telegram/stop" && req.method === "POST") {
|
|
7809
8901
|
try {
|
|
7810
|
-
const pid = parseInt(fs.
|
|
8902
|
+
const pid = parseInt((await fs.promises.readFile(TG_PID_PATH, "utf8")).trim(), 10);
|
|
7811
8903
|
if (pid) process.kill(pid, "SIGTERM");
|
|
7812
8904
|
} catch { }
|
|
7813
8905
|
res.writeHead(200, { "content-type": "application/json" });
|
|
@@ -7817,7 +8909,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7817
8909
|
|
|
7818
8910
|
if (url.pathname === "/api/telegram/messages") {
|
|
7819
8911
|
try {
|
|
7820
|
-
const raw = fs.
|
|
8912
|
+
const raw = await fs.promises.readFile(TG_MSG_PATH, "utf8");
|
|
7821
8913
|
const msgs = raw
|
|
7822
8914
|
.trim()
|
|
7823
8915
|
.split("\n")
|
|
@@ -7847,7 +8939,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7847
8939
|
"logs",
|
|
7848
8940
|
"telegram-bridge.jsonl",
|
|
7849
8941
|
);
|
|
7850
|
-
const raw = fs.
|
|
8942
|
+
const raw = await fs.promises.readFile(TG_LOG_PATH, "utf8");
|
|
7851
8943
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
7852
8944
|
const topics = new Map(); // chatId:threadId -> {chatId, threadId, lastSeen, text}
|
|
7853
8945
|
|
|
@@ -7899,16 +8991,16 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7899
8991
|
);
|
|
7900
8992
|
const WA_AUTH_DIR = path.join(os.homedir(), ".crewswarm", "whatsapp-auth");
|
|
7901
8993
|
|
|
7902
|
-
function loadWaCfg() {
|
|
8994
|
+
async function loadWaCfg() {
|
|
7903
8995
|
try {
|
|
7904
|
-
return JSON.parse(fs.
|
|
8996
|
+
return JSON.parse(await fs.promises.readFile(WA_CONFIG_PATH, "utf8"));
|
|
7905
8997
|
} catch {
|
|
7906
8998
|
return {};
|
|
7907
8999
|
}
|
|
7908
9000
|
}
|
|
7909
|
-
function isWaRunning() {
|
|
9001
|
+
async function isWaRunning() {
|
|
7910
9002
|
try {
|
|
7911
|
-
const pid = parseInt(fs.
|
|
9003
|
+
const pid = parseInt((await fs.promises.readFile(WA_PID_PATH, "utf8")).trim(), 10);
|
|
7912
9004
|
if (!pid) return false;
|
|
7913
9005
|
process.kill(pid, 0);
|
|
7914
9006
|
return true;
|
|
@@ -7927,16 +9019,16 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7927
9019
|
}
|
|
7928
9020
|
|
|
7929
9021
|
if (url.pathname === "/api/whatsapp/status") {
|
|
7930
|
-
const running = isWaRunning();
|
|
7931
|
-
const authSaved =
|
|
7932
|
-
const cfg = loadWaCfg();
|
|
9022
|
+
const running = await isWaRunning();
|
|
9023
|
+
const authSaved = await exists(path.join(WA_AUTH_DIR, "creds.json"));
|
|
9024
|
+
const cfg = await loadWaCfg();
|
|
7933
9025
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7934
9026
|
res.end(JSON.stringify({ running, authSaved, number: cfg.number || "" }));
|
|
7935
9027
|
return;
|
|
7936
9028
|
}
|
|
7937
9029
|
|
|
7938
9030
|
if (url.pathname === "/api/whatsapp/config" && req.method === "GET") {
|
|
7939
|
-
const cfg = loadWaCfg();
|
|
9031
|
+
const cfg = await loadWaCfg();
|
|
7940
9032
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7941
9033
|
res.end(
|
|
7942
9034
|
JSON.stringify({
|
|
@@ -7953,8 +9045,8 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7953
9045
|
let raw = "";
|
|
7954
9046
|
for await (const chunk of req) raw += chunk;
|
|
7955
9047
|
const body = JSON.parse(raw || "{}");
|
|
7956
|
-
const existing = loadWaCfg();
|
|
7957
|
-
fs.
|
|
9048
|
+
const existing = await loadWaCfg();
|
|
9049
|
+
await fs.promises.writeFile(
|
|
7958
9050
|
WA_CONFIG_PATH,
|
|
7959
9051
|
JSON.stringify({ ...existing, ...body }, null, 2),
|
|
7960
9052
|
);
|
|
@@ -7965,13 +9057,13 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7965
9057
|
".crewswarm",
|
|
7966
9058
|
"crewswarm.json",
|
|
7967
9059
|
);
|
|
7968
|
-
const swarm = JSON.parse(fs.
|
|
9060
|
+
const swarm = JSON.parse(await fs.promises.readFile(swarmPath, "utf8"));
|
|
7969
9061
|
swarm.env = swarm.env || {};
|
|
7970
9062
|
if (body.allowedNumbers !== undefined) {
|
|
7971
9063
|
swarm.env.WA_ALLOWED_NUMBERS = (body.allowedNumbers || []).join(",");
|
|
7972
9064
|
}
|
|
7973
9065
|
if (body.targetAgent) swarm.env.WA_TARGET_AGENT = body.targetAgent;
|
|
7974
|
-
fs.
|
|
9066
|
+
await fs.promises.writeFile(swarmPath, JSON.stringify(swarm, null, 2));
|
|
7975
9067
|
} catch { }
|
|
7976
9068
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7977
9069
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -7979,24 +9071,21 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7979
9071
|
}
|
|
7980
9072
|
|
|
7981
9073
|
if (url.pathname === "/api/whatsapp/start" && req.method === "POST") {
|
|
7982
|
-
if (isWaRunning()) {
|
|
9074
|
+
if (await isWaRunning()) {
|
|
7983
9075
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7984
9076
|
res.end(JSON.stringify({ ok: true, message: "Already running" }));
|
|
7985
9077
|
return;
|
|
7986
9078
|
}
|
|
7987
|
-
const cfg = loadWaCfg();
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
return {};
|
|
7998
|
-
}
|
|
7999
|
-
})();
|
|
9079
|
+
const cfg = await loadWaCfg();
|
|
9080
|
+
let swarm = {};
|
|
9081
|
+
try {
|
|
9082
|
+
swarm = JSON.parse(
|
|
9083
|
+
await fs.promises.readFile(
|
|
9084
|
+
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
9085
|
+
"utf8",
|
|
9086
|
+
),
|
|
9087
|
+
);
|
|
9088
|
+
} catch { }
|
|
8000
9089
|
const waEnv = swarm.env || {};
|
|
8001
9090
|
const { spawn: spawnBridge } = await import("node:child_process");
|
|
8002
9091
|
const bridgePath = path.join(CREWSWARM_DIR, "whatsapp-bridge.mjs");
|
|
@@ -8018,7 +9107,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8018
9107
|
});
|
|
8019
9108
|
proc.unref();
|
|
8020
9109
|
await new Promise((r) => setTimeout(r, 1500));
|
|
8021
|
-
const running = isWaRunning();
|
|
9110
|
+
const running = await isWaRunning();
|
|
8022
9111
|
res.writeHead(running ? 200 : 500, { "content-type": "application/json" });
|
|
8023
9112
|
res.end(
|
|
8024
9113
|
JSON.stringify({
|
|
@@ -8034,7 +9123,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8034
9123
|
|
|
8035
9124
|
if (url.pathname === "/api/whatsapp/stop" && req.method === "POST") {
|
|
8036
9125
|
try {
|
|
8037
|
-
const pid = parseInt(fs.
|
|
9126
|
+
const pid = parseInt((await fs.promises.readFile(WA_PID_PATH, "utf8")).trim(), 10);
|
|
8038
9127
|
if (pid) process.kill(pid, "SIGTERM");
|
|
8039
9128
|
} catch { }
|
|
8040
9129
|
res.writeHead(200, { "content-type": "application/json" });
|
|
@@ -8044,7 +9133,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8044
9133
|
|
|
8045
9134
|
if (url.pathname === "/api/whatsapp/messages") {
|
|
8046
9135
|
try {
|
|
8047
|
-
const raw = fs.
|
|
9136
|
+
const raw = await fs.promises.readFile(WA_MSG_PATH, "utf8");
|
|
8048
9137
|
const msgs = raw
|
|
8049
9138
|
.trim()
|
|
8050
9139
|
.split("\n")
|
|
@@ -8117,11 +9206,22 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8117
9206
|
}
|
|
8118
9207
|
|
|
8119
9208
|
if (url.pathname === "/api/contacts/delete" && req.method === "POST") {
|
|
9209
|
+
let raw = "";
|
|
9210
|
+
for await (const chunk of req) raw += chunk;
|
|
9211
|
+
let parsed;
|
|
9212
|
+
try { parsed = JSON.parse(raw || "{}"); } catch {
|
|
9213
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
9214
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
9215
|
+
return;
|
|
9216
|
+
}
|
|
9217
|
+
const vr = validate(ContactDeleteSchema, parsed);
|
|
9218
|
+
if (!vr.ok) {
|
|
9219
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
9220
|
+
res.end(JSON.stringify({ ok: false, error: vr.error }));
|
|
9221
|
+
return;
|
|
9222
|
+
}
|
|
9223
|
+
const { contactId } = vr.data;
|
|
8120
9224
|
try {
|
|
8121
|
-
let raw = "";
|
|
8122
|
-
for await (const chunk of req) raw += chunk;
|
|
8123
|
-
const { contactId } = JSON.parse(raw || "{}");
|
|
8124
|
-
if (!contactId) throw new Error("contactId required");
|
|
8125
9225
|
const { deleteContact } = await import("../lib/contacts/index.mjs");
|
|
8126
9226
|
deleteContact(contactId);
|
|
8127
9227
|
res.writeHead(200, { "content-type": "application/json" });
|
|
@@ -8134,12 +9234,22 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8134
9234
|
}
|
|
8135
9235
|
|
|
8136
9236
|
if (url.pathname === "/api/contacts/send" && req.method === "POST") {
|
|
9237
|
+
let raw = "";
|
|
9238
|
+
for await (const chunk of req) raw += chunk;
|
|
9239
|
+
let parsedSend;
|
|
9240
|
+
try { parsedSend = JSON.parse(raw || "{}"); } catch {
|
|
9241
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
9242
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
9243
|
+
return;
|
|
9244
|
+
}
|
|
9245
|
+
const vrSend = validate(ContactSendSchema, parsedSend);
|
|
9246
|
+
if (!vrSend.ok) {
|
|
9247
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
9248
|
+
res.end(JSON.stringify({ ok: false, error: vrSend.error }));
|
|
9249
|
+
return;
|
|
9250
|
+
}
|
|
9251
|
+
const { contactId, platform, message } = vrSend.data;
|
|
8137
9252
|
try {
|
|
8138
|
-
let raw = "";
|
|
8139
|
-
for await (const chunk of req) raw += chunk;
|
|
8140
|
-
const { contactId, platform, message } = JSON.parse(raw || "{}");
|
|
8141
|
-
if (!contactId || !message)
|
|
8142
|
-
throw new Error("contactId and message required");
|
|
8143
9253
|
|
|
8144
9254
|
const { getContact } = await import("../lib/contacts/index.mjs");
|
|
8145
9255
|
const contact = getContact(contactId);
|
|
@@ -8155,12 +9265,19 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8155
9265
|
: platformLinks.whatsapp;
|
|
8156
9266
|
if (waJid) {
|
|
8157
9267
|
// Call WhatsApp bridge's sendMessage function
|
|
8158
|
-
const waUrl = `http://127.0.0.1:${process.env.WA_HTTP_PORT ||
|
|
8159
|
-
await fetch(waUrl, {
|
|
9268
|
+
const waUrl = `http://127.0.0.1:${process.env.WA_HTTP_PORT || 5015}/send`;
|
|
9269
|
+
const waRes = await fetch(waUrl, {
|
|
8160
9270
|
method: "POST",
|
|
8161
9271
|
headers: { "content-type": "application/json" },
|
|
8162
|
-
body: JSON.stringify({ jid: waJid, message }),
|
|
9272
|
+
body: JSON.stringify({ jid: waJid, text: message }),
|
|
8163
9273
|
});
|
|
9274
|
+
const waData = await waRes.json().catch(() => ({}));
|
|
9275
|
+
if (!waRes.ok || !waData.ok) {
|
|
9276
|
+
throw new Error(
|
|
9277
|
+
waData.error ||
|
|
9278
|
+
`WhatsApp send failed (${waRes.status})`,
|
|
9279
|
+
);
|
|
9280
|
+
}
|
|
8164
9281
|
}
|
|
8165
9282
|
}
|
|
8166
9283
|
|
|
@@ -8176,10 +9293,10 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8176
9293
|
".crewswarm",
|
|
8177
9294
|
"telegram-bridge.json",
|
|
8178
9295
|
);
|
|
8179
|
-
const tgCfg = JSON.parse(fs.
|
|
9296
|
+
const tgCfg = JSON.parse(await fs.promises.readFile(TG_CONFIG_PATH, "utf8"));
|
|
8180
9297
|
const botToken = tgCfg.token;
|
|
8181
9298
|
if (botToken) {
|
|
8182
|
-
await fetch(
|
|
9299
|
+
const tgRes = await fetch(
|
|
8183
9300
|
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
8184
9301
|
{
|
|
8185
9302
|
method: "POST",
|
|
@@ -8187,6 +9304,14 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8187
9304
|
body: JSON.stringify({ chat_id: tgChatId, text: message }),
|
|
8188
9305
|
},
|
|
8189
9306
|
);
|
|
9307
|
+
const tgData = await tgRes.json().catch(() => ({}));
|
|
9308
|
+
if (!tgRes.ok || !tgData.ok) {
|
|
9309
|
+
throw new Error(
|
|
9310
|
+
tgData.description ||
|
|
9311
|
+
tgData.error ||
|
|
9312
|
+
`Telegram send failed (${tgRes.status})`,
|
|
9313
|
+
);
|
|
9314
|
+
}
|
|
8190
9315
|
}
|
|
8191
9316
|
}
|
|
8192
9317
|
}
|
|
@@ -8204,9 +9329,49 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8204
9329
|
if (url.pathname === "/api/services/status") {
|
|
8205
9330
|
let services;
|
|
8206
9331
|
try {
|
|
8207
|
-
const {
|
|
9332
|
+
const { exec: execCb } = await import("node:child_process");
|
|
8208
9333
|
const net = await import("node:net");
|
|
8209
9334
|
|
|
9335
|
+
// Single ps snapshot — avoids spawning dozens of pgrep child processes
|
|
9336
|
+
const psSnapshot = await new Promise((resolve) => {
|
|
9337
|
+
execCb("ps ax -o pid=,command=", { encoding: "utf8", timeout: 2000 }, (err, stdout) => {
|
|
9338
|
+
resolve(err ? "" : stdout || "");
|
|
9339
|
+
});
|
|
9340
|
+
});
|
|
9341
|
+
|
|
9342
|
+
function findPid(pattern) {
|
|
9343
|
+
const re = new RegExp(pattern);
|
|
9344
|
+
for (const line of psSnapshot.split("\n")) {
|
|
9345
|
+
const trimmed = line.trim();
|
|
9346
|
+
if (!trimmed) continue;
|
|
9347
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
9348
|
+
if (spaceIdx < 0) continue;
|
|
9349
|
+
const pid = parseInt(trimmed.slice(0, spaceIdx), 10);
|
|
9350
|
+
const cmd = trimmed.slice(spaceIdx + 1);
|
|
9351
|
+
if (re.test(cmd)) return pid;
|
|
9352
|
+
}
|
|
9353
|
+
return null;
|
|
9354
|
+
}
|
|
9355
|
+
|
|
9356
|
+
function findAllPids(pattern) {
|
|
9357
|
+
const re = new RegExp(pattern);
|
|
9358
|
+
const pids = [];
|
|
9359
|
+
for (const line of psSnapshot.split("\n")) {
|
|
9360
|
+
const trimmed = line.trim();
|
|
9361
|
+
if (!trimmed) continue;
|
|
9362
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
9363
|
+
if (spaceIdx < 0) continue;
|
|
9364
|
+
const pid = parseInt(trimmed.slice(0, spaceIdx), 10);
|
|
9365
|
+
const cmd = trimmed.slice(spaceIdx + 1);
|
|
9366
|
+
if (re.test(cmd)) pids.push(pid);
|
|
9367
|
+
}
|
|
9368
|
+
return pids;
|
|
9369
|
+
}
|
|
9370
|
+
|
|
9371
|
+
function countProcs(pattern) {
|
|
9372
|
+
return findAllPids(pattern).length;
|
|
9373
|
+
}
|
|
9374
|
+
|
|
8210
9375
|
function portListening(port, timeoutMs = 2000) {
|
|
8211
9376
|
return new Promise((resolve) => {
|
|
8212
9377
|
const sock = new net.default.Socket();
|
|
@@ -8240,9 +9405,9 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8240
9405
|
}
|
|
8241
9406
|
}
|
|
8242
9407
|
|
|
8243
|
-
function pidRunning(pidFile) {
|
|
9408
|
+
async function pidRunning(pidFile) {
|
|
8244
9409
|
try {
|
|
8245
|
-
const pid = parseInt(fs.
|
|
9410
|
+
const pid = parseInt((await fs.promises.readFile(pidFile, "utf8")).trim(), 10);
|
|
8246
9411
|
if (!pid) return null;
|
|
8247
9412
|
process.kill(pid, 0);
|
|
8248
9413
|
return pid;
|
|
@@ -8251,85 +9416,41 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8251
9416
|
}
|
|
8252
9417
|
}
|
|
8253
9418
|
|
|
8254
|
-
function
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
8260
|
-
}).trim();
|
|
8261
|
-
return parseInt(out, 10) || 0;
|
|
8262
|
-
} catch {
|
|
8263
|
-
return 0;
|
|
8264
|
-
}
|
|
8265
|
-
}
|
|
8266
|
-
|
|
8267
|
-
function getPid(pattern) {
|
|
8268
|
-
try {
|
|
8269
|
-
const out = execSync(`pgrep -f "${pattern}"`, {
|
|
8270
|
-
encoding: "utf8",
|
|
8271
|
-
timeout: 300,
|
|
8272
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
8273
|
-
}).trim();
|
|
8274
|
-
const pids = out
|
|
8275
|
-
.split("\n")
|
|
8276
|
-
.filter(Boolean)
|
|
8277
|
-
.map((p) => parseInt(p, 10));
|
|
8278
|
-
return pids.length > 0 ? pids[0] : null;
|
|
8279
|
-
} catch {
|
|
8280
|
-
return null;
|
|
8281
|
-
}
|
|
8282
|
-
}
|
|
8283
|
-
|
|
8284
|
-
function getAllPids(pattern) {
|
|
8285
|
-
try {
|
|
8286
|
-
const out = execSync(`pgrep -f "${pattern}"`, {
|
|
8287
|
-
encoding: "utf8",
|
|
8288
|
-
timeout: 300,
|
|
8289
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
8290
|
-
}).trim();
|
|
8291
|
-
return out
|
|
8292
|
-
.split("\n")
|
|
8293
|
-
.filter(Boolean)
|
|
8294
|
-
.map((p) => parseInt(p, 10));
|
|
8295
|
-
} catch {
|
|
8296
|
-
return [];
|
|
9419
|
+
async function commandExistsFast(bin, extraPaths = []) {
|
|
9420
|
+
const candidate = String(bin || "").trim();
|
|
9421
|
+
if (!candidate) return false;
|
|
9422
|
+
if (candidate.includes("/")) {
|
|
9423
|
+
return await exists(candidate);
|
|
8297
9424
|
}
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
return out ? new Date(out).getTime() : null;
|
|
8308
|
-
} catch {
|
|
8309
|
-
return null;
|
|
9425
|
+
const checks = [
|
|
9426
|
+
...extraPaths,
|
|
9427
|
+
path.join("/usr/local/bin", candidate),
|
|
9428
|
+
path.join("/opt/homebrew/bin", candidate),
|
|
9429
|
+
path.join(os.homedir(), ".local", "bin", candidate),
|
|
9430
|
+
path.join(os.homedir(), "bin", candidate),
|
|
9431
|
+
];
|
|
9432
|
+
for (const item of checks) {
|
|
9433
|
+
if (item && await exists(item)) return true;
|
|
8310
9434
|
}
|
|
9435
|
+
return false;
|
|
8311
9436
|
}
|
|
8312
9437
|
|
|
8313
9438
|
const crewLeadPort = Number(process.env.CREW_LEAD_PORT || 5010);
|
|
8314
|
-
const tgPid = pidRunning(
|
|
9439
|
+
const tgPid = await pidRunning(
|
|
8315
9440
|
path.join(os.homedir(), ".crewswarm", "logs", "telegram-bridge.pid"),
|
|
8316
9441
|
);
|
|
8317
9442
|
const waPid =
|
|
8318
|
-
pidRunning(
|
|
9443
|
+
(await pidRunning(
|
|
8319
9444
|
path.join(os.homedir(), ".crewswarm", "logs", "whatsapp-bridge.pid"),
|
|
8320
|
-
) ||
|
|
9445
|
+
)) || findPid("whatsapp-bridge\\.mjs");
|
|
9446
|
+
|
|
9447
|
+
// Fire network checks in parallel — all process lookups are instant (ps snapshot)
|
|
8321
9448
|
const rtStatusPromise = fetch("http://127.0.0.1:18889/status", {
|
|
8322
|
-
signal: AbortSignal.timeout(
|
|
9449
|
+
signal: AbortSignal.timeout(5000),
|
|
8323
9450
|
});
|
|
8324
9451
|
const mcpHealthPromise = httpOk("http://127.0.0.1:5020/health", 3000);
|
|
8325
9452
|
const [
|
|
8326
|
-
rtUp,
|
|
8327
|
-
crewLeadUp,
|
|
8328
|
-
gwUp,
|
|
8329
|
-
ocPortUp,
|
|
8330
|
-
dashUp,
|
|
8331
|
-
studioUp,
|
|
8332
|
-
watchUp,
|
|
9453
|
+
rtUp, crewLeadUp, gwUp, ocPortUp, dashUp, studioUp, watchUp,
|
|
8333
9454
|
] = await Promise.all([
|
|
8334
9455
|
portListening(18889),
|
|
8335
9456
|
portListening(crewLeadPort),
|
|
@@ -8339,37 +9460,38 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8339
9460
|
portListening(3333),
|
|
8340
9461
|
portListening(3334),
|
|
8341
9462
|
]);
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
const
|
|
9463
|
+
|
|
9464
|
+
// All PID lookups are instant — pure JS regex against ps snapshot
|
|
9465
|
+
const rtPid = findPid("opencrew-rt-daemon");
|
|
9466
|
+
const crewLeadPid = findPid("crew-lead\\.mjs");
|
|
9467
|
+
const gwPid = findPid("openclaw-gateway");
|
|
9468
|
+
const ocPid = findPid("\\.opencode serve") || findPid("opencode serve") || findPid("bin/\\.opencode");
|
|
9469
|
+
const mcpPid = findPid("mcp-server\\.mjs");
|
|
9470
|
+
const studioPid = findPid("apps/vibe/server\\.mjs") || findPid("npm.*studio:start");
|
|
9471
|
+
const watchPid = findPid("watch-server\\.mjs");
|
|
9472
|
+
|
|
9473
|
+
// commandExists — fs.existsSync only, no shell spawn
|
|
9474
|
+
const codexInstalled = await commandExistsFast(process.env.CODEX_CLI_BIN || "codex");
|
|
9475
|
+
const claudeInstalled = await commandExistsFast(process.env.CLAUDE_CODE_BIN || "claude");
|
|
9476
|
+
const cursorInstalled = (await commandExistsFast(
|
|
9477
|
+
process.env.CURSOR_CLI_BIN || path.join(os.homedir(), ".local", "bin", "agent"),
|
|
9478
|
+
[path.join(os.homedir(), ".local", "bin", "agent")],
|
|
9479
|
+
)) || (await commandExistsFast("agent", [path.join(os.homedir(), ".local", "bin", "agent")]));
|
|
9480
|
+
const geminiInstalled = await commandExistsFast(process.env.GEMINI_CLI_BIN || "gemini");
|
|
9481
|
+
const crewCliInstalled = (await commandExistsFast("crew", [
|
|
9482
|
+
path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js"),
|
|
9483
|
+
])) || (await exists(path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js")));
|
|
8345
9484
|
const oclawPaired =
|
|
8346
|
-
|
|
9485
|
+
(await exists(
|
|
8347
9486
|
path.join(os.homedir(), ".openclaw", "devices", "paired.json"),
|
|
8348
|
-
) ||
|
|
8349
|
-
|
|
8350
|
-
const ocPid =
|
|
8351
|
-
getPid("\\.opencode serve") ||
|
|
8352
|
-
getPid("opencode serve") ||
|
|
8353
|
-
getPid("bin/.opencode") ||
|
|
8354
|
-
getPid("/.opencode");
|
|
9487
|
+
)) ||
|
|
9488
|
+
(await exists(path.join(os.homedir(), ".openclaw", "device.json")));
|
|
8355
9489
|
const ocUp = ocPortUp || ocPid !== null;
|
|
8356
|
-
|
|
8357
|
-
const claudeInstalled = commandExists(process.env.CLAUDE_CODE_BIN || "claude");
|
|
8358
|
-
const cursorInstalled = commandExists(
|
|
8359
|
-
process.env.CURSOR_CLI_BIN ||
|
|
8360
|
-
path.join(os.homedir(), ".local", "bin", "agent"),
|
|
8361
|
-
[path.join(os.homedir(), ".local", "bin", "agent")],
|
|
8362
|
-
) || commandExists("agent", [path.join(os.homedir(), ".local", "bin", "agent")]);
|
|
8363
|
-
const geminiInstalled = commandExists(process.env.GEMINI_CLI_BIN || "gemini");
|
|
8364
|
-
const crewCliInstalled =
|
|
8365
|
-
commandExists("crew", [
|
|
8366
|
-
path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js"),
|
|
8367
|
-
]) ||
|
|
8368
|
-
fs.existsSync(path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js"));
|
|
9490
|
+
|
|
8369
9491
|
let swarmCfg = {};
|
|
8370
9492
|
try {
|
|
8371
9493
|
swarmCfg = JSON.parse(
|
|
8372
|
-
fs.
|
|
9494
|
+
await fs.promises.readFile(
|
|
8373
9495
|
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
8374
9496
|
"utf8",
|
|
8375
9497
|
),
|
|
@@ -8391,10 +9513,6 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8391
9513
|
cfgEnv.CREWSWARM_OPENCODE_ENABLED === "1" ||
|
|
8392
9514
|
process.env.CREWSWARM_OPENCODE_ENABLED === "on" ||
|
|
8393
9515
|
process.env.CREWSWARM_OPENCODE_ENABLED === "1";
|
|
8394
|
-
const mcpPid = getPid("mcp-server.mjs");
|
|
8395
|
-
const studioPid =
|
|
8396
|
-
getPid("apps/vibe/server.mjs") || getPid("npm.*studio:start");
|
|
8397
|
-
const watchPid = getPid("watch-server.mjs");
|
|
8398
9516
|
|
|
8399
9517
|
// Agent count: ask RT bus which agents are actually connected (most reliable source)
|
|
8400
9518
|
let agentsOnline = 0;
|
|
@@ -8408,11 +9526,11 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8408
9526
|
(a) => String(a).toLowerCase() !== "crew-lead",
|
|
8409
9527
|
);
|
|
8410
9528
|
agentsOnline = rtAgentList.length;
|
|
8411
|
-
agentPids =
|
|
9529
|
+
agentPids = findAllPids("gateway-bridge\\.mjs --rt-daemon");
|
|
8412
9530
|
} catch {
|
|
8413
|
-
// RT not reachable — fall back to
|
|
8414
|
-
agentsOnline = countProcs("gateway-bridge
|
|
8415
|
-
agentPids =
|
|
9531
|
+
// RT not reachable — fall back to ps snapshot for count, config for names
|
|
9532
|
+
agentsOnline = countProcs("gateway-bridge\\.mjs --rt-daemon");
|
|
9533
|
+
agentPids = findAllPids("gateway-bridge\\.mjs --rt-daemon");
|
|
8416
9534
|
try {
|
|
8417
9535
|
rtAgentList = (swarmCfg.agents || [])
|
|
8418
9536
|
.map((a) => a.id)
|
|
@@ -8428,7 +9546,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8428
9546
|
} catch { }
|
|
8429
9547
|
if (agentsTotal === 0) agentsTotal = 14;
|
|
8430
9548
|
agentsTotal = Math.max(agentsTotal, agentsOnline);
|
|
8431
|
-
const pmCount = countProcs("pm-loop
|
|
9549
|
+
const pmCount = countProcs("pm-loop\\.mjs");
|
|
8432
9550
|
|
|
8433
9551
|
services = [
|
|
8434
9552
|
{
|
|
@@ -8458,7 +9576,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8458
9576
|
{
|
|
8459
9577
|
id: "crew-lead",
|
|
8460
9578
|
label: "crew-lead",
|
|
8461
|
-
description: "Chat commander — dashboard chat,
|
|
9579
|
+
description: "Chat commander — dashboard chat, crewchat, Telegram",
|
|
8462
9580
|
port: crewLeadPort,
|
|
8463
9581
|
running: crewLeadUp,
|
|
8464
9582
|
canRestart: true,
|
|
@@ -8590,7 +9708,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8590
9708
|
label: "Vibe UI",
|
|
8591
9709
|
description: studioUp
|
|
8592
9710
|
? "Monaco editor + agent chat — Cursor-like IDE for crewswarm"
|
|
8593
|
-
: "Run: npm run
|
|
9711
|
+
: "Run: npm run vibe:start (port 3333)",
|
|
8594
9712
|
port: 3333,
|
|
8595
9713
|
running: studioUp,
|
|
8596
9714
|
canRestart: true,
|
|
@@ -8602,7 +9720,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8602
9720
|
label: "Vibe Watch Server",
|
|
8603
9721
|
description: watchUp
|
|
8604
9722
|
? "CLI → Vibe live reload WebSocket relay (port 3334)"
|
|
8605
|
-
: "Run: npm run
|
|
9723
|
+
: "Run: npm run vibe:watch — enables live file reload in Vibe",
|
|
8606
9724
|
port: 3334,
|
|
8607
9725
|
running: watchUp,
|
|
8608
9726
|
canRestart: true,
|
|
@@ -8637,12 +9755,8 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8637
9755
|
canRestart: false,
|
|
8638
9756
|
pid: (() => {
|
|
8639
9757
|
try {
|
|
8640
|
-
const pids =
|
|
8641
|
-
|
|
8642
|
-
timeout: 300,
|
|
8643
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
8644
|
-
}).trim().split("\n").filter(Boolean).map(p => parseInt(p, 10));
|
|
8645
|
-
return pids.length === 1 ? pids[0] : pids;
|
|
9758
|
+
const pids = findAllPids("pm-loop\\.mjs");
|
|
9759
|
+
return pids.length === 1 ? pids[0] : (pids.length > 0 ? pids : null);
|
|
8646
9760
|
} catch {
|
|
8647
9761
|
return null;
|
|
8648
9762
|
}
|
|
@@ -8854,17 +9968,15 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8854
9968
|
} else if (id === "telegram") {
|
|
8855
9969
|
try {
|
|
8856
9970
|
const pid = parseInt(
|
|
8857
|
-
fs
|
|
8858
|
-
.
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
)
|
|
8867
|
-
.trim(),
|
|
9971
|
+
(await fs.promises.readFile(
|
|
9972
|
+
path.join(
|
|
9973
|
+
os.homedir(),
|
|
9974
|
+
".crewswarm",
|
|
9975
|
+
"logs",
|
|
9976
|
+
"telegram-bridge.pid",
|
|
9977
|
+
),
|
|
9978
|
+
"utf8",
|
|
9979
|
+
)).trim(),
|
|
8868
9980
|
10,
|
|
8869
9981
|
);
|
|
8870
9982
|
if (pid) process.kill(pid, "SIGTERM");
|
|
@@ -8872,17 +9984,15 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8872
9984
|
} else if (id === "whatsapp") {
|
|
8873
9985
|
try {
|
|
8874
9986
|
const pid = parseInt(
|
|
8875
|
-
fs
|
|
8876
|
-
.
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
)
|
|
8885
|
-
.trim(),
|
|
9987
|
+
(await fs.promises.readFile(
|
|
9988
|
+
path.join(
|
|
9989
|
+
os.homedir(),
|
|
9990
|
+
".crewswarm",
|
|
9991
|
+
"logs",
|
|
9992
|
+
"whatsapp-bridge.pid",
|
|
9993
|
+
),
|
|
9994
|
+
"utf8",
|
|
9995
|
+
)).trim(),
|
|
8886
9996
|
10,
|
|
8887
9997
|
);
|
|
8888
9998
|
if (pid) process.kill(pid, "SIGTERM");
|
|
@@ -8898,8 +10008,8 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8898
10008
|
let killed = false;
|
|
8899
10009
|
|
|
8900
10010
|
try {
|
|
8901
|
-
if (
|
|
8902
|
-
const pid = parseInt(fs.
|
|
10011
|
+
if (await exists(pidFile)) {
|
|
10012
|
+
const pid = parseInt((await fs.promises.readFile(pidFile, "utf8")).trim(), 10);
|
|
8903
10013
|
if (pid && !isNaN(pid)) {
|
|
8904
10014
|
try {
|
|
8905
10015
|
process.kill(pid, 0); // Check if process exists
|
|
@@ -8911,7 +10021,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
8911
10021
|
);
|
|
8912
10022
|
} catch (e) {
|
|
8913
10023
|
// Process doesn't exist, clean up stale PID file
|
|
8914
|
-
fs.
|
|
10024
|
+
await fs.promises.writeFile(pidFile, "");
|
|
8915
10025
|
}
|
|
8916
10026
|
}
|
|
8917
10027
|
}
|
|
@@ -9153,20 +10263,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9153
10263
|
.filter(Boolean)
|
|
9154
10264
|
.join("\n");
|
|
9155
10265
|
|
|
9156
|
-
const token = (
|
|
9157
|
-
try {
|
|
9158
|
-
return (
|
|
9159
|
-
JSON.parse(
|
|
9160
|
-
fs.readFileSync(
|
|
9161
|
-
path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
|
|
9162
|
-
"utf8",
|
|
9163
|
-
),
|
|
9164
|
-
)?.rt?.authToken || ""
|
|
9165
|
-
);
|
|
9166
|
-
} catch {
|
|
9167
|
-
return "";
|
|
9168
|
-
}
|
|
9169
|
-
})();
|
|
10266
|
+
const token = resolveCrewLeadAuthToken();
|
|
9170
10267
|
try {
|
|
9171
10268
|
const upstream = await fetch(
|
|
9172
10269
|
"http://127.0.0.1:5010/api/engine-passthrough",
|
|
@@ -9282,13 +10379,11 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9282
10379
|
const userDir = path.join(os.homedir(), ".crewswarm", "engines");
|
|
9283
10380
|
const enginesMap = {};
|
|
9284
10381
|
for (const dir of [bundledDir, userDir]) {
|
|
9285
|
-
if (!
|
|
9286
|
-
for (const f of fs
|
|
9287
|
-
.readdirSync(dir)
|
|
9288
|
-
.filter((f) => f.endsWith(".json"))) {
|
|
10382
|
+
if (!await exists(dir)) continue;
|
|
10383
|
+
for (const f of (await fs.promises.readdir(dir)).filter((f) => f.endsWith(".json"))) {
|
|
9289
10384
|
try {
|
|
9290
10385
|
const eng = JSON.parse(
|
|
9291
|
-
fs.
|
|
10386
|
+
await fs.promises.readFile(path.join(dir, f), "utf8"),
|
|
9292
10387
|
);
|
|
9293
10388
|
if (eng.id)
|
|
9294
10389
|
enginesMap[eng.id] = {
|
|
@@ -9302,11 +10397,11 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9302
10397
|
const configPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
|
|
9303
10398
|
let envVars = {};
|
|
9304
10399
|
try {
|
|
9305
|
-
const cfg = JSON.parse(fs.
|
|
10400
|
+
const cfg = JSON.parse(await fs.promises.readFile(configPath, "utf8"));
|
|
9306
10401
|
envVars = cfg.env || {};
|
|
9307
10402
|
} catch { }
|
|
9308
10403
|
|
|
9309
|
-
const engines = Object.values(enginesMap).map((eng) => {
|
|
10404
|
+
const engines = await Promise.all(Object.values(enginesMap).map(async (eng) => {
|
|
9310
10405
|
let installed = false;
|
|
9311
10406
|
try {
|
|
9312
10407
|
const bin = eng.bin || eng.id;
|
|
@@ -9315,7 +10410,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9315
10410
|
} catch {
|
|
9316
10411
|
if (eng.binAlternate) {
|
|
9317
10412
|
const alt = eng.binAlternate.replace(/^~/, os.homedir());
|
|
9318
|
-
installed =
|
|
10413
|
+
installed = await exists(alt);
|
|
9319
10414
|
}
|
|
9320
10415
|
}
|
|
9321
10416
|
const missingEnv = eng.requiresAuth
|
|
@@ -9336,7 +10431,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9336
10431
|
ready: installed && missingEnv.length === 0,
|
|
9337
10432
|
enabled,
|
|
9338
10433
|
};
|
|
9339
|
-
});
|
|
10434
|
+
}));
|
|
9340
10435
|
res.writeHead(200, { "content-type": "application/json" });
|
|
9341
10436
|
res.end(JSON.stringify({ engines }));
|
|
9342
10437
|
} catch (err) {
|
|
@@ -9363,10 +10458,10 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9363
10458
|
if (!eng.id || !eng.label)
|
|
9364
10459
|
throw new Error("Engine descriptor must have id and label");
|
|
9365
10460
|
const engDir = path.join(os.homedir(), ".crewswarm", "engines");
|
|
9366
|
-
if (!
|
|
10461
|
+
if (!await exists(engDir)) await fs.promises.mkdir(engDir, { recursive: true });
|
|
9367
10462
|
const outPath = path.join(engDir, `${eng.id}.json`);
|
|
9368
10463
|
if (!outPath.startsWith(engDir)) throw new Error("Invalid engine id");
|
|
9369
|
-
fs.
|
|
10464
|
+
await fs.promises.writeFile(outPath, JSON.stringify(eng, null, 2), "utf8");
|
|
9370
10465
|
res.writeHead(200, { "content-type": "application/json" });
|
|
9371
10466
|
res.end(JSON.stringify({ ok: true, id: eng.id, label: eng.label }));
|
|
9372
10467
|
} catch (err) {
|
|
@@ -9389,8 +10484,8 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9389
10484
|
let engineDef = null;
|
|
9390
10485
|
for (const dir of [bundledDir, userDir]) {
|
|
9391
10486
|
const p = path.join(dir, `${engineId}.json`);
|
|
9392
|
-
if (
|
|
9393
|
-
engineDef = JSON.parse(fs.
|
|
10487
|
+
if (await exists(p)) {
|
|
10488
|
+
engineDef = JSON.parse(await fs.promises.readFile(p, "utf8"));
|
|
9394
10489
|
break;
|
|
9395
10490
|
}
|
|
9396
10491
|
}
|
|
@@ -9401,12 +10496,12 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9401
10496
|
|
|
9402
10497
|
const envVarName = engineDef.envToggle;
|
|
9403
10498
|
const configPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
|
|
9404
|
-
const cfg = JSON.parse(fs.
|
|
10499
|
+
const cfg = JSON.parse(await fs.promises.readFile(configPath, "utf8"));
|
|
9405
10500
|
|
|
9406
10501
|
if (!cfg.env) cfg.env = {};
|
|
9407
10502
|
cfg.env[envVarName] = enabled ? "1" : "off";
|
|
9408
10503
|
|
|
9409
|
-
fs.
|
|
10504
|
+
await fs.promises.writeFile(configPath, JSON.stringify(cfg, null, 2), "utf8");
|
|
9410
10505
|
|
|
9411
10506
|
res.writeHead(200, { "content-type": "application/json" });
|
|
9412
10507
|
res.end(JSON.stringify({ ok: true, engineId, enabled, envVar: envVarName }));
|
|
@@ -9427,7 +10522,7 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9427
10522
|
return;
|
|
9428
10523
|
}
|
|
9429
10524
|
try {
|
|
9430
|
-
fs.
|
|
10525
|
+
await fs.promises.unlink(target);
|
|
9431
10526
|
} catch { }
|
|
9432
10527
|
res.writeHead(200, { "content-type": "application/json" });
|
|
9433
10528
|
res.end(JSON.stringify({ ok: true }));
|
|
@@ -9583,13 +10678,13 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
9583
10678
|
".crewswarm",
|
|
9584
10679
|
"skills",
|
|
9585
10680
|
);
|
|
9586
|
-
if (!
|
|
9587
|
-
fs.
|
|
10681
|
+
if (!await exists(skillsDir))
|
|
10682
|
+
await fs.promises.mkdir(skillsDir, { recursive: true });
|
|
9588
10683
|
const outPath = path.join(skillsDir, `${skillName}.json`);
|
|
9589
10684
|
// Final path traversal guard
|
|
9590
10685
|
if (!outPath.startsWith(skillsDir))
|
|
9591
10686
|
throw new Error("Invalid skill name");
|
|
9592
|
-
fs.
|
|
10687
|
+
await fs.promises.writeFile(outPath, JSON.stringify(skill, null, 2), "utf8");
|
|
9593
10688
|
|
|
9594
10689
|
res.writeHead(200, { "content-type": "application/json" });
|
|
9595
10690
|
res.end(
|
|
@@ -9683,14 +10778,35 @@ if (process.argv.includes("--print-html")) {
|
|
|
9683
10778
|
}
|
|
9684
10779
|
|
|
9685
10780
|
process.on("uncaughtException", (err) => {
|
|
10781
|
+
const msg = String(err?.message || err || "");
|
|
9686
10782
|
console.error(
|
|
9687
10783
|
"[dashboard] uncaughtException:",
|
|
9688
|
-
err?.stack ||
|
|
10784
|
+
err?.stack || msg,
|
|
9689
10785
|
);
|
|
9690
10786
|
|
|
9691
|
-
//
|
|
9692
|
-
|
|
9693
|
-
|
|
10787
|
+
// Benign errors from engine passthrough / SSE streams — keep alive
|
|
10788
|
+
if (
|
|
10789
|
+
msg === "terminated" ||
|
|
10790
|
+
msg === "aborted" ||
|
|
10791
|
+
/client.*disconnect/i.test(msg) ||
|
|
10792
|
+
/socket hang up/i.test(msg) ||
|
|
10793
|
+
/ECONNRESET/i.test(msg) ||
|
|
10794
|
+
/EPIPE/i.test(msg) ||
|
|
10795
|
+
/fetch failed/i.test(msg) ||
|
|
10796
|
+
/UND_ERR/i.test(msg)
|
|
10797
|
+
) {
|
|
10798
|
+
console.error("[dashboard] Non-fatal uncaughtException — keeping alive");
|
|
10799
|
+
return;
|
|
10800
|
+
}
|
|
10801
|
+
|
|
10802
|
+
// Fatal errors: port conflicts, permissions, OOM — must exit
|
|
10803
|
+
if (/EADDRINUSE|EACCES|out of memory|cannot allocate/i.test(msg)) {
|
|
10804
|
+
console.error("[dashboard] FATAL — exiting due to uncaught exception");
|
|
10805
|
+
process.exit(1);
|
|
10806
|
+
}
|
|
10807
|
+
|
|
10808
|
+
// Default: log but keep alive — engine passthrough errors shouldn't kill the dashboard
|
|
10809
|
+
console.error("[dashboard] Unexpected uncaughtException — keeping alive (not fatal)");
|
|
9694
10810
|
});
|
|
9695
10811
|
|
|
9696
10812
|
process.on("unhandledRejection", (reason) => {
|