crewswarm 0.9.3 → 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.
Files changed (73) hide show
  1. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js +1 -0
  2. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
  3. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
  4. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
  5. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  6. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  7. package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
  8. package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
  9. package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
  10. package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
  11. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  12. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  13. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  14. package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
  15. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  16. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  17. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  18. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  19. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
  20. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
  21. package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
  22. package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
  23. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  24. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  25. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
  26. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
  27. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
  28. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
  29. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  30. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  31. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  32. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
  33. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
  34. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  35. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  36. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  37. package/apps/dashboard/dist/index.html +56 -7
  38. package/apps/dashboard/dist/index.html.br +0 -0
  39. package/apps/dashboard/dist/index.html.gz +0 -0
  40. package/apps/vibe/server.mjs +98 -53
  41. package/lib/bridges/cli-executor.mjs +1 -1
  42. package/lib/browser/passthrough-stderr.js +1 -0
  43. package/lib/chat/project-messages.mjs +3 -5
  44. package/lib/cli-process-tracker.mjs +3 -2
  45. package/lib/contacts/identity-linker.mjs +1 -0
  46. package/lib/crew-judge/judge.mjs +19 -18
  47. package/lib/crew-lead/agent-manager.mjs +1 -1
  48. package/lib/crew-lead/background.mjs +14 -1
  49. package/lib/crew-lead/chat-handler.mjs +4 -1
  50. package/lib/crew-lead/http-server.mjs +73 -65
  51. package/lib/crew-lead/prompts.mjs +7 -1
  52. package/lib/crew-lead/tools.mjs +3 -2
  53. package/lib/crew-lead/wave-dispatcher.mjs +4 -2
  54. package/lib/engines/crew-cli.mjs +1 -1
  55. package/lib/engines/engine-registry.mjs +11 -9
  56. package/lib/engines/runners.mjs +23 -2
  57. package/lib/gemini-cli-passthrough-noise.mjs +1 -1
  58. package/lib/integrations/code-search.mjs +4 -3
  59. package/lib/memory/shared-adapter.mjs +23 -10
  60. package/lib/pipeline/manager.mjs +2 -1
  61. package/lib/runtime/config.mjs +1 -1
  62. package/lib/runtime/spending.mjs +2 -1
  63. package/package.json +17 -9
  64. package/scripts/dashboard-validation.mjs +2 -0
  65. package/scripts/dashboard.mjs +1110 -484
  66. package/scripts/generate-openapi.mjs +683 -277
  67. package/scripts/restart-service.sh +12 -9
  68. package/apps/dashboard/dist/assets/chat-core-3KirthZA.js +0 -1
  69. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
  70. package/apps/dashboard/dist/assets/index-GSWxxEPO.js +0 -2
  71. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js +0 -1
  72. package/apps/dashboard/dist/assets/tab-settings-tab-BselH1c0.js +0 -1
  73. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js +0 -1
@@ -124,6 +124,103 @@ if (!lockResult.ok) {
124
124
  process.exit(1);
125
125
  }
126
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
+
127
224
  const opencodeBase = process.env.OPENCODE_URL || "http://127.0.0.1:4096";
128
225
 
129
226
  function resolveCommandPath(bin, extraPaths = []) {
@@ -150,6 +247,7 @@ function resolveCommandPath(bin, extraPaths = []) {
150
247
  const out = execSync(`command -v "${candidate.replace(/"/g, '\\"')}"`, {
151
248
  stdio: ["ignore", "pipe", "ignore"],
152
249
  shell: "/bin/zsh",
250
+ timeout: 1500,
153
251
  env: {
154
252
  ...process.env,
155
253
  PATH: [
@@ -1205,8 +1303,7 @@ async function collectPassthroughText({
1205
1303
 
1206
1304
  async function getPhasedProgress(limit = 80) {
1207
1305
  const { readFile } = await import("node:fs/promises");
1208
- const { existsSync } = await import("node:fs");
1209
- if (!existsSync(phasedDispatchLog)) return [];
1306
+ if (!(await fs.promises.access(phasedDispatchLog).then(() => true, () => false))) return [];
1210
1307
  try {
1211
1308
  const content = await readFile(phasedDispatchLog, "utf8");
1212
1309
  const lines = content.trim().split("\n").filter(Boolean).slice(-limit);
@@ -1359,8 +1456,41 @@ function serveStatic(req, res, filePath) {
1359
1456
  }
1360
1457
  }
1361
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
+
1362
1491
  const server = http.createServer(async (req, res) => {
1363
1492
  const url = new URL(req.url || "/", `http://localhost:${listenPort}`);
1493
+ const exists = (p) => fs.promises.access(p).then(() => true, () => false);
1364
1494
 
1365
1495
  // CORS: echo local dev Origins so http://localhost:3333 (Vibe) works with http://127.0.0.1:4319
1366
1496
  // (browsers treat hostnames as distinct origins). Non-local callers still get *.
@@ -1416,7 +1546,7 @@ const server = http.createServer(async (req, res) => {
1416
1546
  if (url.pathname === "/crew-chat.html") {
1417
1547
  const chatFile = path.join(CREWSWARM_DIR, "crew-chat.html");
1418
1548
  try {
1419
- const chatHtml = fs.readFileSync(chatFile, "utf8");
1549
+ const chatHtml = await fs.promises.readFile(chatFile, "utf8");
1420
1550
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1421
1551
  res.end(chatHtml);
1422
1552
  } catch {
@@ -1428,7 +1558,7 @@ const server = http.createServer(async (req, res) => {
1428
1558
  if (url.pathname === "/signup" || url.pathname === "/signup.html") {
1429
1559
  const signupFile = path.join(FRONTEND_SRC, "public", "signup.html");
1430
1560
  try {
1431
- const signupHtml = fs.readFileSync(signupFile, "utf8");
1561
+ const signupHtml = await fs.promises.readFile(signupFile, "utf8");
1432
1562
  res.writeHead(200, {
1433
1563
  "content-type": "text/html; charset=utf-8",
1434
1564
  "cache-control": "no-store, no-cache, must-revalidate",
@@ -1441,6 +1571,333 @@ const server = http.createServer(async (req, res) => {
1441
1571
  }
1442
1572
  return;
1443
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
+
1444
1901
  // ── First-run detection ──────────────────────────────────────────────────
1445
1902
  if (url.pathname === "/api/first-run-status" && req.method === "GET") {
1446
1903
  const cfg = readSwarmConfigSafe();
@@ -1464,16 +1921,11 @@ const server = http.createServer(async (req, res) => {
1464
1921
 
1465
1922
  const hasApiKeys = configuredProviders.length > 0;
1466
1923
 
1467
- // Check crew-lead health
1924
+ // Check crew-lead health (uses http.request, not fetch — avoids undici pool saturation from SSE)
1468
1925
  let crewLeadUp = false;
1469
1926
  try {
1470
- const controller = new AbortController();
1471
- const timer = setTimeout(() => controller.abort(), 2000);
1472
- const resp = await fetch("http://127.0.0.1:5010/health", {
1473
- signal: controller.signal,
1474
- });
1475
- clearTimeout(timer);
1476
- crewLeadUp = resp.ok;
1927
+ const { ok } = await crewLeadRequest("/health", { timeout: 2000 });
1928
+ crewLeadUp = ok;
1477
1929
  } catch {
1478
1930
  crewLeadUp = false;
1479
1931
  }
@@ -1523,11 +1975,11 @@ const server = http.createServer(async (req, res) => {
1523
1975
  const engine = String(url.searchParams.get("engine") || "opencode").trim();
1524
1976
  const limit = Math.min(Number(url.searchParams.get("limit") || "20"), 50);
1525
1977
  const projectId = String(url.searchParams.get("projectId") || "").trim();
1526
- const projectDir = (() => {
1978
+ const projectDir = await (async () => {
1527
1979
  if (!projectId || projectId === "general") return "";
1528
1980
  try {
1529
1981
  const registryFile = path.join(CFG_DIR, "projects.json");
1530
- const projects = JSON.parse(fs.readFileSync(registryFile, "utf8"));
1982
+ const projects = JSON.parse(await fs.promises.readFile(registryFile, "utf8"));
1531
1983
  return String(projects?.[projectId]?.outputDir || "").trim();
1532
1984
  } catch {
1533
1985
  return "";
@@ -1576,19 +2028,18 @@ const server = http.createServer(async (req, res) => {
1576
2028
  const limit = Math.min(Number(url.searchParams.get("limit") || "20"), 50);
1577
2029
  const sessionsBase = path.join(os.homedir(), ".codex", "sessions");
1578
2030
  const sessions = [];
1579
- if (fs.existsSync(sessionsBase)) {
1580
- const years = fs.readdirSync(sessionsBase).filter(d => /^\d{4}$/.test(d));
2031
+ if (await exists(sessionsBase)) {
2032
+ const years = (await fs.promises.readdir(sessionsBase)).filter(d => /^\d{4}$/.test(d));
1581
2033
  for (const year of years.sort().reverse()) {
1582
2034
  const yearDir = path.join(sessionsBase, year);
1583
- const months = fs.readdirSync(yearDir).filter(d => /^\d{2}$/.test(d));
2035
+ const months = (await fs.promises.readdir(yearDir)).filter(d => /^\d{2}$/.test(d));
1584
2036
  for (const month of months.sort().reverse()) {
1585
2037
  const monthDir = path.join(yearDir, month);
1586
- const days = fs.readdirSync(monthDir).filter(d => /^\d{2}$/.test(d));
2038
+ const days = (await fs.promises.readdir(monthDir)).filter(d => /^\d{2}$/.test(d));
1587
2039
  for (const day of days.sort().reverse()) {
1588
2040
  const dayDir = path.join(yearDir, month, day);
1589
- const files = fs.readdirSync(dayDir)
1590
- .filter(f => f.endsWith(".jsonl"))
1591
- .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 }))))
1592
2043
  .sort((a, b) => b.mt - a.mt);
1593
2044
  for (const { f } of files) {
1594
2045
  if (sessions.length >= limit) break;
@@ -1596,7 +2047,7 @@ const server = http.createServer(async (req, res) => {
1596
2047
  const filePath = path.join(dayDir, f);
1597
2048
 
1598
2049
  // Skip files larger than 10MB to avoid memory issues
1599
- const stats = fs.statSync(filePath);
2050
+ const stats = await fs.promises.stat(filePath);
1600
2051
  if (stats.size > 10 * 1024 * 1024) {
1601
2052
  sessions.push({
1602
2053
  id: sessionId,
@@ -1608,7 +2059,7 @@ const server = http.createServer(async (req, res) => {
1608
2059
  }
1609
2060
 
1610
2061
  const messages = [];
1611
- const allLines = fs.readFileSync(filePath, "utf8").trim().split("\n");
2062
+ const allLines = (await fs.promises.readFile(filePath, "utf8")).trim().split("\n");
1612
2063
  const lines = allLines.slice(-100); // Last 100 lines only
1613
2064
  let firstUserMsg = "";
1614
2065
  for (const line of lines) {
@@ -1648,22 +2099,21 @@ const server = http.createServer(async (req, res) => {
1648
2099
  const qDir = url.searchParams.get("dir") || process.cwd();
1649
2100
  const dirKey = qDir.replace(/\//g, "-");
1650
2101
  const projectsBase = path.join(os.homedir(), ".claude", "projects");
1651
- const candidates = fs.existsSync(projectsBase)
1652
- ? fs.readdirSync(projectsBase).filter(d => d === dirKey || d.endsWith(dirKey.split("-").slice(-2).join("-")))
2102
+ const candidates = await exists(projectsBase)
2103
+ ? (await fs.promises.readdir(projectsBase)).filter(d => d === dirKey || d.endsWith(dirKey.split("-").slice(-2).join("-")))
1653
2104
  : [];
1654
2105
  const sessions = [];
1655
2106
  for (const cand of candidates) {
1656
2107
  const sessDir = path.join(projectsBase, cand);
1657
- const files = fs.readdirSync(sessDir)
1658
- .filter(f => f.endsWith(".jsonl"))
1659
- .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 }))))
1660
2110
  .sort((a, b) => b.mt - a.mt)
1661
2111
  .slice(0, limit)
1662
2112
  .map(x => x.f);
1663
2113
  for (const file of files) {
1664
2114
  const sessionId = file.replace(".jsonl", "");
1665
2115
  const messages = [];
1666
- const lines = fs.readFileSync(path.join(sessDir, file), "utf8").trim().split("\n");
2116
+ const lines = (await fs.promises.readFile(path.join(sessDir, file), "utf8")).trim().split("\n");
1667
2117
  for (const line of lines) {
1668
2118
  try {
1669
2119
  const d = JSON.parse(line);
@@ -1687,13 +2137,13 @@ const server = http.createServer(async (req, res) => {
1687
2137
  const limit = Math.min(Number(url.searchParams.get("limit") || "20"), 50);
1688
2138
  const historyBase = path.join(os.homedir(), ".gemini", "history");
1689
2139
  const sessions = [];
1690
- if (fs.existsSync(historyBase)) {
1691
- const projects = fs.readdirSync(historyBase);
2140
+ if (await exists(historyBase)) {
2141
+ const projects = await fs.promises.readdir(historyBase);
1692
2142
  for (const proj of projects) {
1693
2143
  const sessionFile = path.join(historyBase, proj, "session.jsonl");
1694
- if (fs.existsSync(sessionFile)) {
2144
+ if (await exists(sessionFile)) {
1695
2145
  const messages = [];
1696
- const lines = fs.readFileSync(sessionFile, "utf8").trim().split("\n");
2146
+ const lines = (await fs.promises.readFile(sessionFile, "utf8")).trim().split("\n");
1697
2147
  let firstUserMsg = "";
1698
2148
  for (const line of lines) {
1699
2149
  try {
@@ -1707,7 +2157,7 @@ const server = http.createServer(async (req, res) => {
1707
2157
  } catch { }
1708
2158
  }
1709
2159
  if (messages.length) {
1710
- const stat = fs.statSync(sessionFile);
2160
+ const stat = await fs.promises.stat(sessionFile);
1711
2161
  sessions.push({
1712
2162
  id: proj,
1713
2163
  title: firstUserMsg || proj,
@@ -1729,33 +2179,32 @@ const server = http.createServer(async (req, res) => {
1729
2179
  const limit = Math.min(Number(url.searchParams.get("limit") || "20"), 50);
1730
2180
  const sessionsBase = path.join(process.cwd(), ".crew", "sessions");
1731
2181
  const sessions = [];
1732
- if (fs.existsSync(sessionsBase)) {
1733
- const engines = fs.readdirSync(sessionsBase);
2182
+ if (await exists(sessionsBase)) {
2183
+ const engines = await fs.promises.readdir(sessionsBase);
1734
2184
  for (const engine of engines) {
1735
2185
  const engineDir = path.join(sessionsBase, engine);
1736
- const projects = fs.readdirSync(engineDir);
2186
+ const projects = await fs.promises.readdir(engineDir);
1737
2187
  for (const project of projects) {
1738
2188
  const projectSessionsDir = path.join(engineDir, project, "sessions");
1739
- if (!fs.existsSync(projectSessionsDir)) continue;
1740
- const years = fs.readdirSync(projectSessionsDir).filter(d => /^\d{4}$/.test(d));
2189
+ if (!await exists(projectSessionsDir)) continue;
2190
+ const years = (await fs.promises.readdir(projectSessionsDir)).filter(d => /^\d{4}$/.test(d));
1741
2191
  for (const year of years.sort().reverse()) {
1742
2192
  const yearDir = path.join(projectSessionsDir, year);
1743
- const months = fs.readdirSync(yearDir).filter(d => /^\d{2}$/.test(d));
2193
+ const months = (await fs.promises.readdir(yearDir)).filter(d => /^\d{2}$/.test(d));
1744
2194
  for (const month of months.sort().reverse()) {
1745
2195
  const monthDir = path.join(yearDir, month);
1746
- const days = fs.readdirSync(monthDir).filter(d => /^\d{2}$/.test(d));
2196
+ const days = (await fs.promises.readdir(monthDir)).filter(d => /^\d{2}$/.test(d));
1747
2197
  for (const day of days.sort().reverse()) {
1748
2198
  const dayDir = path.join(yearDir, month, day);
1749
- const files = fs.readdirSync(dayDir)
1750
- .filter(f => f.endsWith(".jsonl"))
1751
- .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 }))))
1752
2201
  .sort((a, b) => b.mt - a.mt);
1753
2202
  for (const { f } of files) {
1754
2203
  if (sessions.length >= limit) break;
1755
2204
  const sessionId = f.replace(".jsonl", "");
1756
2205
  const messages = [];
1757
2206
  // Limit to last 100 lines to avoid huge session files
1758
- const allLines = fs.readFileSync(path.join(dayDir, f), "utf8").trim().split("\n");
2207
+ const allLines = (await fs.promises.readFile(path.join(dayDir, f), "utf8")).trim().split("\n");
1759
2208
  const lines = allLines.slice(-100);
1760
2209
  let firstUserMsg = "";
1761
2210
  for (const line of lines) {
@@ -1831,8 +2280,8 @@ const server = http.createServer(async (req, res) => {
1831
2280
  "passthrough-sessions.json",
1832
2281
  );
1833
2282
  let sessions = {};
1834
- if (fs.existsSync(sessionFile)) {
1835
- sessions = JSON.parse(fs.readFileSync(sessionFile, "utf8"));
2283
+ if (await exists(sessionFile)) {
2284
+ sessions = JSON.parse(await fs.promises.readFile(sessionFile, "utf8"));
1836
2285
  }
1837
2286
  res.writeHead(200, { "content-type": "application/json" });
1838
2287
  res.end(JSON.stringify({ sessions }));
@@ -1856,11 +2305,11 @@ const server = http.createServer(async (req, res) => {
1856
2305
  "passthrough-sessions.json",
1857
2306
  );
1858
2307
  let sessions = {};
1859
- if (fs.existsSync(sessionFile)) {
1860
- sessions = JSON.parse(fs.readFileSync(sessionFile, "utf8"));
2308
+ if (await exists(sessionFile)) {
2309
+ sessions = JSON.parse(await fs.promises.readFile(sessionFile, "utf8"));
1861
2310
  }
1862
2311
  delete sessions[key];
1863
- fs.writeFileSync(
2312
+ await fs.promises.writeFile(
1864
2313
  sessionFile,
1865
2314
  JSON.stringify(sessions, null, 2),
1866
2315
  "utf8",
@@ -1884,7 +2333,7 @@ const server = http.createServer(async (req, res) => {
1884
2333
  );
1885
2334
  let usage = { calls: 0, prompt: 0, completion: 0, byModel: {} };
1886
2335
  try {
1887
- usage = JSON.parse(fs.readFileSync(usageFile, "utf8"));
2336
+ usage = JSON.parse(await fs.promises.readFile(usageFile, "utf8"));
1888
2337
  } catch { }
1889
2338
  res.writeHead(200, { "content-type": "application/json" });
1890
2339
  res.end(JSON.stringify(usage));
@@ -1981,13 +2430,12 @@ const server = http.createServer(async (req, res) => {
1981
2430
  const histDir = path.join(os.homedir(), ".crewswarm", "chat-history");
1982
2431
  const sessions = [];
1983
2432
  try {
1984
- const files = fs
1985
- .readdirSync(histDir)
2433
+ const files = (await fs.promises.readdir(histDir))
1986
2434
  .filter((f) => f.startsWith("telegram-") && f.endsWith(".jsonl"));
1987
2435
  for (const file of files) {
1988
2436
  const chatId = file.replace(/^telegram-/, "").replace(/\.jsonl$/, "");
1989
- const lines = fs
1990
- .readFileSync(path.join(histDir, file), "utf8")
2437
+ const lines = (await fs.promises
2438
+ .readFile(path.join(histDir, file), "utf8"))
1991
2439
  .split("\n")
1992
2440
  .filter(Boolean);
1993
2441
  const msgs = lines
@@ -2013,7 +2461,7 @@ const server = http.createServer(async (req, res) => {
2013
2461
  return;
2014
2462
  }
2015
2463
 
2016
- if ((url.pathname === "/health" || url.pathname === "/api/health") && req.method === "GET") {
2464
+ if (url.pathname === "/health" && req.method === "GET") {
2017
2465
  res.writeHead(200, { "content-type": "application/json" });
2018
2466
  res.end(JSON.stringify({ ok: true, uptime: Math.round(process.uptime()) }));
2019
2467
  return;
@@ -2092,7 +2540,7 @@ const server = http.createServer(async (req, res) => {
2092
2540
  // Credential keys are never exposed here
2093
2541
  let cfgEnv = {};
2094
2542
  try {
2095
- cfgEnv = JSON.parse(fs.readFileSync(CFG_FILE, "utf8")).env || {};
2543
+ cfgEnv = JSON.parse(await fs.promises.readFile(CFG_FILE, "utf8")).env || {};
2096
2544
  } catch { }
2097
2545
  const result = {};
2098
2546
  for (const v of vars) {
@@ -2118,9 +2566,9 @@ const server = http.createServer(async (req, res) => {
2118
2566
  }
2119
2567
  const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
2120
2568
  try {
2121
- const raw = (() => {
2569
+ const raw = await (async () => {
2122
2570
  try {
2123
- return fs.readFileSync(cfgPath, "utf8");
2571
+ return await fs.promises.readFile(cfgPath, "utf8");
2124
2572
  } catch {
2125
2573
  return "{}";
2126
2574
  }
@@ -2313,7 +2761,7 @@ const server = http.createServer(async (req, res) => {
2313
2761
 
2314
2762
  try {
2315
2763
  // Load agent config
2316
- const csSwarm = JSON.parse(fs.readFileSync(CFG_FILE, "utf8"));
2764
+ const csSwarm = JSON.parse(await fs.promises.readFile(CFG_FILE, "utf8"));
2317
2765
  const agentCfg = csSwarm.agents?.find((a) => a.id === agentId);
2318
2766
  if (!agentCfg?.model) {
2319
2767
  res.writeHead(404, { "content-type": "application/json" });
@@ -2346,7 +2794,7 @@ const server = http.createServer(async (req, res) => {
2346
2794
  let systemPrompt = `You are ${agentId}.`;
2347
2795
  try {
2348
2796
  const agentPrompts = JSON.parse(
2349
- fs.readFileSync(agentPromptsPath, "utf8"),
2797
+ await fs.promises.readFile(agentPromptsPath, "utf8"),
2350
2798
  );
2351
2799
  const bareId = agentId.replace(/^crew-/, "");
2352
2800
  systemPrompt =
@@ -2538,14 +2986,14 @@ const server = http.createServer(async (req, res) => {
2538
2986
  );
2539
2987
 
2540
2988
  try {
2541
- if (!fs.existsSync(historyPath)) {
2989
+ if (!await exists(historyPath)) {
2542
2990
  res.writeHead(200, { "content-type": "application/json" });
2543
2991
  res.end(JSON.stringify({ history: [] }));
2544
2992
  return;
2545
2993
  }
2546
2994
 
2547
- const lines = fs
2548
- .readFileSync(historyPath, "utf8")
2995
+ const lines = (await fs.promises
2996
+ .readFile(historyPath, "utf8"))
2549
2997
  .split("\n")
2550
2998
  .filter(Boolean);
2551
2999
  const history = lines.map((line) => JSON.parse(line));
@@ -2598,13 +3046,13 @@ const server = http.createServer(async (req, res) => {
2598
3046
  let projectDir = process.cwd();
2599
3047
  if (projectId) {
2600
3048
  const regPath = path.join(CFG_DIR, "projects.json");
2601
- if (fs.existsSync(regPath)) {
2602
- const reg = JSON.parse(fs.readFileSync(regPath, "utf8") || "{}");
3049
+ if (await exists(regPath)) {
3050
+ const reg = JSON.parse(await fs.promises.readFile(regPath, "utf8") || "{}");
2603
3051
  const proj = reg[projectId];
2604
3052
  if (proj?.outputDir) projectDir = proj.outputDir;
2605
3053
  }
2606
3054
  }
2607
- fs.mkdirSync(projectDir, { recursive: true });
3055
+ await fs.promises.mkdir(projectDir, { recursive: true });
2608
3056
 
2609
3057
  const planner = resolvePlannerEngine(requestedEngine, requestedModel);
2610
3058
  if (!planner) throw new Error("No planning engine is configured or installed");
@@ -2670,13 +3118,12 @@ const server = http.createServer(async (req, res) => {
2670
3118
  // Resolve project output dir if projectId provided
2671
3119
  let projectEnv = {};
2672
3120
  if (projectId) {
2673
- const { existsSync: ex } = await import("node:fs");
2674
3121
  const { readFile: rf } = await import("node:fs/promises");
2675
3122
  const regPath = path.join(
2676
3123
  CFG_DIR,
2677
3124
  "projects.json",
2678
3125
  );
2679
- if (ex(regPath)) {
3126
+ if (await exists(regPath)) {
2680
3127
  const reg = JSON.parse(await rf(regPath, "utf8").catch(() => "{}"));
2681
3128
  const proj = reg[projectId];
2682
3129
  if (proj) {
@@ -2692,8 +3139,7 @@ const server = http.createServer(async (req, res) => {
2692
3139
  }
2693
3140
  }
2694
3141
  const { spawn } = await import("node:child_process");
2695
- const { existsSync } = await import("node:fs");
2696
- if (!existsSync(phasedOrchestrator))
3142
+ if (!(await exists(phasedOrchestrator)))
2697
3143
  throw new Error(
2698
3144
  "phased-orchestrator.mjs not found at " + phasedOrchestrator,
2699
3145
  );
@@ -2737,10 +3183,10 @@ const server = http.createServer(async (req, res) => {
2737
3183
  projectId ? "phased-" + projectId + ".pid" : "phased-orchestrator.pid",
2738
3184
  );
2739
3185
  try {
2740
- const pidStr = fs.readFileSync(pidFile, "utf8").trim();
3186
+ const pidStr = (await fs.promises.readFile(pidFile, "utf8")).trim();
2741
3187
  const pid = parseInt(pidStr, 10);
2742
3188
  if (pid) process.kill(pid, "SIGTERM");
2743
- fs.unlinkSync(pidFile);
3189
+ await fs.promises.unlink(pidFile);
2744
3190
  res.writeHead(200, { "content-type": "application/json" });
2745
3191
  res.end(JSON.stringify({ ok: true, pid }));
2746
3192
  } catch (e) {
@@ -2767,13 +3213,12 @@ const server = http.createServer(async (req, res) => {
2767
3213
  const { requirement, projectId } = vr.data;
2768
3214
  let projectEnv = {};
2769
3215
  if (projectId) {
2770
- const { existsSync: ex } = await import("node:fs");
2771
3216
  const { readFile: rf } = await import("node:fs/promises");
2772
3217
  const regPath = path.join(
2773
3218
  CFG_DIR,
2774
3219
  "projects.json",
2775
3220
  );
2776
- if (ex(regPath)) {
3221
+ if (await exists(regPath)) {
2777
3222
  const reg = JSON.parse(await rf(regPath, "utf8").catch(() => "{}"));
2778
3223
  const proj = reg[projectId];
2779
3224
  if (proj) {
@@ -2789,8 +3234,7 @@ const server = http.createServer(async (req, res) => {
2789
3234
  }
2790
3235
  }
2791
3236
  const { spawn } = await import("node:child_process");
2792
- const { existsSync } = await import("node:fs");
2793
- if (!existsSync(continuousBuild))
3237
+ if (!(await exists(continuousBuild)))
2794
3238
  throw new Error("continuous-build.mjs not found at " + continuousBuild);
2795
3239
  const proc = spawn(PREFERRED_NODE_BIN, [continuousBuild, requirement], {
2796
3240
  cwd: CREWSWARM_DIR,
@@ -2838,10 +3282,10 @@ const server = http.createServer(async (req, res) => {
2838
3282
  projectId ? "continuous-" + projectId + ".pid" : "continuous-build.pid",
2839
3283
  );
2840
3284
  try {
2841
- const pidStr = fs.readFileSync(pidFile, "utf8").trim();
3285
+ const pidStr = (await fs.promises.readFile(pidFile, "utf8")).trim();
2842
3286
  const pid = parseInt(pidStr, 10);
2843
3287
  if (pid) process.kill(pid, "SIGTERM");
2844
- fs.unlinkSync(pidFile);
3288
+ await fs.promises.unlink(pidFile);
2845
3289
  res.writeHead(200, { "content-type": "application/json" });
2846
3290
  res.end(JSON.stringify({ ok: true, pid }));
2847
3291
  } catch (e) {
@@ -2851,7 +3295,6 @@ const server = http.createServer(async (req, res) => {
2851
3295
  return;
2852
3296
  }
2853
3297
  if (url.pathname === "/api/continuous-build/log" && req.method === "GET") {
2854
- const { existsSync } = await import("node:fs");
2855
3298
  const { readFile } = await import("node:fs/promises");
2856
3299
  const logPath = path.join(
2857
3300
  CREWSWARM_DIR,
@@ -2859,7 +3302,7 @@ const server = http.createServer(async (req, res) => {
2859
3302
  "continuous-build.jsonl",
2860
3303
  );
2861
3304
  let lines = [];
2862
- if (existsSync(logPath)) {
3305
+ if (await exists(logPath)) {
2863
3306
  const raw = await readFile(logPath, "utf8").catch(() => "");
2864
3307
  lines = raw
2865
3308
  .trim()
@@ -2956,7 +3399,6 @@ const server = http.createServer(async (req, res) => {
2956
3399
 
2957
3400
  // ── Waves Configuration APIs ──────────────────────────────────────────
2958
3401
  if (url.pathname === "/api/waves/config" && req.method === "GET") {
2959
- const { existsSync, readFileSync } = await import("node:fs");
2960
3402
  const { readFile: rf } = await import("node:fs/promises");
2961
3403
  const wavesConfigPath = path.join(
2962
3404
  CREWSWARM_DIR,
@@ -2965,7 +3407,7 @@ const server = http.createServer(async (req, res) => {
2965
3407
  "waves-config.json",
2966
3408
  );
2967
3409
 
2968
- if (!existsSync(wavesConfigPath)) {
3410
+ if (!(await exists(wavesConfigPath))) {
2969
3411
  res.writeHead(404, { "Content-Type": "application/json" });
2970
3412
  res.end(JSON.stringify({ ok: false, error: "Waves config not found" }));
2971
3413
  return;
@@ -2980,7 +3422,7 @@ const server = http.createServer(async (req, res) => {
2980
3422
  raw = await rf(wavesConfigPath, "utf8");
2981
3423
  } catch {
2982
3424
  try {
2983
- raw = readFileSync(wavesConfigPath, "utf8");
3425
+ raw = await fs.promises.readFile(wavesConfigPath, "utf8");
2984
3426
  } catch {
2985
3427
  raw = JSON.stringify(getEmbeddedWavesConfig());
2986
3428
  }
@@ -3034,8 +3476,7 @@ const server = http.createServer(async (req, res) => {
3034
3476
 
3035
3477
  try {
3036
3478
  // Check if we have a backup
3037
- const { existsSync } = await import("node:fs");
3038
- if (existsSync(wavesConfigBackup)) {
3479
+ if (await exists(wavesConfigBackup)) {
3039
3480
  const defaultConfig = await rf(wavesConfigBackup, "utf8");
3040
3481
  await wf(wavesConfigPath, defaultConfig, "utf8");
3041
3482
  } else {
@@ -3186,7 +3627,7 @@ const server = http.createServer(async (req, res) => {
3186
3627
  if (!isValidWorkflowName(name))
3187
3628
  throw new Error("Invalid workflow name");
3188
3629
  const fp = getWorkflowFile(name);
3189
- if (fs.existsSync(fp)) fs.unlinkSync(fp);
3630
+ if (await exists(fp)) await fs.promises.unlink(fp);
3190
3631
  workflowRuntime.runs.delete(name);
3191
3632
  res.writeHead(200, { "Content-Type": "application/json" });
3192
3633
  res.end(JSON.stringify({ ok: true }));
@@ -3266,11 +3707,10 @@ const server = http.createServer(async (req, res) => {
3266
3707
 
3267
3708
  // ── Project management APIs ───────────────────────────────────────────
3268
3709
  if (url.pathname === "/api/projects" && req.method === "GET") {
3269
- const { existsSync } = await import("node:fs");
3270
3710
  const { readFile: rf } = await import("node:fs/promises");
3271
3711
  const registryFile = path.join(CFG_DIR, "projects.json");
3272
3712
  let projects = {};
3273
- if (existsSync(registryFile)) {
3713
+ if (await exists(registryFile)) {
3274
3714
  projects = JSON.parse(await rf(registryFile, "utf8").catch(() => "{}"));
3275
3715
  }
3276
3716
  // Enrich each project with live roadmap stats and running status.
@@ -3284,7 +3724,7 @@ const server = http.createServer(async (req, res) => {
3284
3724
  failed = 0,
3285
3725
  pending = 0,
3286
3726
  total = 0;
3287
- if (existsSync(project.roadmapFile)) {
3727
+ if (await exists(project.roadmapFile)) {
3288
3728
  const rm = await rf(project.roadmapFile, "utf8").catch(() => "");
3289
3729
  const lines = rm.split("\n").filter((l) => /^- \[/.test(l));
3290
3730
  total = lines.length;
@@ -3294,7 +3734,7 @@ const server = http.createServer(async (req, res) => {
3294
3734
  }
3295
3735
  let running = false;
3296
3736
  const pidPath = path.join(logsDir2, `pm-loop-${id}.pid`);
3297
- if (existsSync(pidPath)) {
3737
+ if (await exists(pidPath)) {
3298
3738
  try {
3299
3739
  const pidStr = await rf(pidPath, "utf8").catch(() => "");
3300
3740
  const pid = parseInt(pidStr.trim(), 10);
@@ -3361,12 +3801,11 @@ const server = http.createServer(async (req, res) => {
3361
3801
  body || "{}",
3362
3802
  );
3363
3803
  if (!name || !outputDir) throw new Error("name and outputDir required");
3364
- const { existsSync, mkdirSync } = await import("node:fs");
3365
3804
  const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
3366
3805
  // Create output dir and ROADMAP.md if they don't exist
3367
- if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
3806
+ if (!(await exists(outputDir))) await fs.promises.mkdir(outputDir, { recursive: true });
3368
3807
  const roadmapFile = path.join(outputDir, "ROADMAP.md");
3369
- if (!existsSync(roadmapFile)) {
3808
+ if (!(await exists(roadmapFile))) {
3370
3809
  await wf(
3371
3810
  roadmapFile,
3372
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`,
@@ -3378,7 +3817,7 @@ const server = http.createServer(async (req, res) => {
3378
3817
  .replace(/^-|-$/g, "");
3379
3818
  const registryFile = path.join(CFG_DIR, "projects.json");
3380
3819
  let projects = {};
3381
- if (existsSync(registryFile))
3820
+ if (await exists(registryFile))
3382
3821
  projects = JSON.parse(await rf(registryFile, "utf8").catch(() => "{}"));
3383
3822
  projects[id] = {
3384
3823
  id,
@@ -3416,10 +3855,9 @@ const server = http.createServer(async (req, res) => {
3416
3855
  }
3417
3856
  const { projectId } = vr.data;
3418
3857
  const registryFile = path.join(CFG_DIR, "projects.json");
3419
- const { existsSync, rmSync, writeFileSync, unlinkSync } = await import("node:fs");
3420
3858
  const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
3421
3859
  let projects = {};
3422
- if (existsSync(registryFile))
3860
+ if (await exists(registryFile))
3423
3861
  projects = JSON.parse(await rf(registryFile, "utf8").catch(() => "{}"));
3424
3862
  if (!projects[projectId])
3425
3863
  throw new Error("Project not found: " + projectId);
@@ -3428,7 +3866,7 @@ const server = http.createServer(async (req, res) => {
3428
3866
  const stopPath = path.join(logsDir, `pm-loop-${projectId}.stop`);
3429
3867
  const logPath = path.join(logsDir, `pm-loop-${projectId}.jsonl`);
3430
3868
  let stoppedPmLoop = false;
3431
- if (existsSync(pidPath)) {
3869
+ if (await exists(pidPath)) {
3432
3870
  const pidStr = await rf(pidPath, "utf8").catch(() => "");
3433
3871
  const pid = parseInt(pidStr.trim(), 10);
3434
3872
  if (pid) {
@@ -3439,18 +3877,18 @@ const server = http.createServer(async (req, res) => {
3439
3877
  }
3440
3878
  }
3441
3879
  try {
3442
- writeFileSync(stopPath, new Date().toISOString());
3880
+ await fs.promises.writeFile(stopPath, new Date().toISOString());
3443
3881
  } catch { }
3444
3882
  for (const cleanupPath of [pidPath, stopPath, logPath]) {
3445
- if (!existsSync(cleanupPath)) continue;
3883
+ if (!(await exists(cleanupPath))) continue;
3446
3884
  try {
3447
- unlinkSync(cleanupPath);
3885
+ await fs.promises.unlink(cleanupPath);
3448
3886
  } catch { }
3449
3887
  }
3450
3888
  const projectMessageDir = path.join(CFG_DIR, "project-messages", projectId);
3451
- if (existsSync(projectMessageDir)) {
3889
+ if (await exists(projectMessageDir)) {
3452
3890
  try {
3453
- rmSync(projectMessageDir, { recursive: true, force: true });
3891
+ await fs.promises.rm(projectMessageDir, { recursive: true, force: true });
3454
3892
  } catch { }
3455
3893
  }
3456
3894
  delete projects[projectId];
@@ -3476,10 +3914,9 @@ const server = http.createServer(async (req, res) => {
3476
3914
  }
3477
3915
  const { projectId, autoAdvance, name, description, outputDir } = vr.data;
3478
3916
  const registryFile = path.join(CFG_DIR, "projects.json");
3479
- const { existsSync } = await import("node:fs");
3480
3917
  const { readFile: rf, writeFile: wf } = await import("node:fs/promises");
3481
3918
  let projects = {};
3482
- if (existsSync(registryFile))
3919
+ if (await exists(registryFile))
3483
3920
  projects = JSON.parse(await rf(registryFile, "utf8").catch(() => "{}"));
3484
3921
  if (!projects[projectId])
3485
3922
  throw new Error("Project not found: " + projectId);
@@ -3494,7 +3931,6 @@ const server = http.createServer(async (req, res) => {
3494
3931
  return;
3495
3932
  }
3496
3933
  if (url.pathname === "/api/pm-loop/status" && req.method === "GET") {
3497
- const { existsSync } = await import("node:fs");
3498
3934
  const { readFile: rf } = await import("node:fs/promises");
3499
3935
  const statusProjectId = url.searchParams.get("projectId") || "";
3500
3936
  const suffix = statusProjectId ? `-${statusProjectId}` : "";
@@ -3505,7 +3941,7 @@ const server = http.createServer(async (req, res) => {
3505
3941
  );
3506
3942
  let running = false,
3507
3943
  pid = null;
3508
- if (existsSync(pidPath)) {
3944
+ if (await exists(pidPath)) {
3509
3945
  const pidStr = await rf(pidPath, "utf8").catch(() => "");
3510
3946
  pid = parseInt(pidStr.trim(), 10);
3511
3947
  if (pid) {
@@ -3539,9 +3975,8 @@ const server = http.createServer(async (req, res) => {
3539
3975
  }
3540
3976
  const { dryRun, projectId, pmOptions = {} } = vr.data;
3541
3977
  const { spawn } = await import("node:child_process");
3542
- const { existsSync, mkdirSync, unlinkSync } = await import("node:fs");
3543
3978
  const { readFile: rf } = await import("node:fs/promises");
3544
- if (!existsSync(pmLoop))
3979
+ if (!(await exists(pmLoop)))
3545
3980
  throw new Error("pm-loop.mjs not found at " + pmLoop);
3546
3981
  // Resolve project config if projectId provided
3547
3982
  let projectDir = null,
@@ -3549,7 +3984,7 @@ const server = http.createServer(async (req, res) => {
3549
3984
  projectFeaturesDoc = null;
3550
3985
  if (projectId) {
3551
3986
  const registryFile = path.join(CFG_DIR, "projects.json");
3552
- if (existsSync(registryFile)) {
3987
+ if (await exists(registryFile)) {
3553
3988
  const reg = JSON.parse(
3554
3989
  await rf(registryFile, "utf8").catch(() => "{}"),
3555
3990
  );
@@ -3573,7 +4008,7 @@ const server = http.createServer(async (req, res) => {
3573
4008
  "orchestrator-logs",
3574
4009
  `pm-loop${pidSuffix}.stop`,
3575
4010
  );
3576
- if (existsSync(pidFile)) {
4011
+ if (await exists(pidFile)) {
3577
4012
  const pidStr = await rf(pidFile, "utf8").catch(() => "");
3578
4013
  const existingPid = parseInt(pidStr.trim(), 10);
3579
4014
  if (existingPid) {
@@ -3595,13 +4030,13 @@ const server = http.createServer(async (req, res) => {
3595
4030
  }
3596
4031
  }
3597
4032
  // Clear any stale stop file
3598
- if (existsSync(stopFilePath)) {
4033
+ if (await exists(stopFilePath)) {
3599
4034
  try {
3600
- unlinkSync(stopFilePath);
4035
+ await fs.promises.unlink(stopFilePath);
3601
4036
  } catch { }
3602
4037
  }
3603
4038
  const logsDir = path.join(CREWSWARM_DIR, "orchestrator-logs");
3604
- if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
4039
+ if (!(await exists(logsDir))) await fs.promises.mkdir(logsDir, { recursive: true });
3605
4040
  // Load RT token so pm-loop and its child gateway-bridge --send can authenticate with the RT daemon
3606
4041
  let rtToken = process.env.CREWSWARM_RT_AUTH_TOKEN || "";
3607
4042
  if (!rtToken) {
@@ -3693,13 +4128,12 @@ const server = http.createServer(async (req, res) => {
3693
4128
  let body = "";
3694
4129
  for await (const chunk of req) body += chunk;
3695
4130
  const { projectId } = JSON.parse(body || "{}");
3696
- const { writeFileSync, mkdirSync, existsSync } = await import("node:fs");
3697
4131
  const logsDir = path.join(CREWSWARM_DIR, "orchestrator-logs");
3698
- if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
4132
+ if (!(await exists(logsDir))) await fs.promises.mkdir(logsDir, { recursive: true });
3699
4133
  // Write project-specific stop file if projectId provided
3700
4134
  const suffix = projectId ? `-${projectId}` : "";
3701
4135
  const stopFilePath = path.join(logsDir, `pm-loop${suffix}.stop`);
3702
- writeFileSync(stopFilePath, new Date().toISOString());
4136
+ await fs.promises.writeFile(stopFilePath, new Date().toISOString());
3703
4137
  res.writeHead(200, { "content-type": "application/json" });
3704
4138
  res.end(
3705
4139
  JSON.stringify({
@@ -3710,10 +4144,9 @@ const server = http.createServer(async (req, res) => {
3710
4144
  return;
3711
4145
  }
3712
4146
  if (url.pathname === "/api/pm-loop/log" && req.method === "GET") {
3713
- const { existsSync } = await import("node:fs");
3714
4147
  const { readFile } = await import("node:fs/promises");
3715
4148
  let lines = [];
3716
- if (existsSync(pmLogFile)) {
4149
+ if (await exists(pmLogFile)) {
3717
4150
  const raw = await readFile(pmLogFile, "utf8").catch(() => "");
3718
4151
  lines = raw
3719
4152
  .trim()
@@ -3734,10 +4167,9 @@ const server = http.createServer(async (req, res) => {
3734
4167
  return;
3735
4168
  }
3736
4169
  if (url.pathname === "/api/pm-loop/roadmap" && req.method === "GET") {
3737
- const { existsSync } = await import("node:fs");
3738
4170
  const { readFile } = await import("node:fs/promises");
3739
4171
  let content = "(ROADMAP.md not found — create website/ROADMAP.md)";
3740
- if (existsSync(roadmapFile)) {
4172
+ if (await exists(roadmapFile)) {
3741
4173
  content = await readFile(roadmapFile, "utf8").catch(
3742
4174
  () => "(unreadable)",
3743
4175
  );
@@ -3762,8 +4194,10 @@ const server = http.createServer(async (req, res) => {
3762
4194
  return;
3763
4195
  }
3764
4196
  const { key } = vr.data;
3765
- const { execSync } = await import("node:child_process");
3766
- execSync(`"${ctlPath}" dlq-replay "${key}"`, {
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}"`, {
3767
4201
  encoding: "utf8",
3768
4202
  timeout: 10000,
3769
4203
  });
@@ -3788,7 +4222,9 @@ const server = http.createServer(async (req, res) => {
3788
4222
  // ── Settings: Preset (from setup wizard) ────────────────────────────────
3789
4223
  // ── Engine detection for first-run wizard ─────────────────────────────
3790
4224
  if (url.pathname === "/api/first-run-engines" && req.method === "GET") {
3791
- const { execSync } = await import("node:child_process");
4225
+ const { exec: _execFRE } = await import("node:child_process");
4226
+ const { promisify: _promisifyFRE } = await import("node:util");
4227
+ const _execAsyncFRE = _promisifyFRE(_execFRE);
3792
4228
  // Expand PATH to include common install locations (launchd has restricted PATH)
3793
4229
  const extraPaths = [
3794
4230
  `${os.homedir()}/.local/bin`,
@@ -3814,13 +4250,14 @@ const server = http.createServer(async (req, res) => {
3814
4250
  ];
3815
4251
  for (const { id, bin } of checks) {
3816
4252
  // Check common paths directly (launchd PATH is restricted)
3817
- if (searchDirs.some(d => fs.existsSync(path.join(d, bin)))) {
4253
+ const foundInSearchDirs = await Promise.any(searchDirs.map(d => fs.promises.access(path.join(d, bin)).then(() => true))).catch(() => false);
4254
+ if (foundInSearchDirs) {
3818
4255
  engines[id] = true;
3819
4256
  continue;
3820
4257
  }
3821
4258
  // Fallback: try login shell
3822
4259
  try {
3823
- execSync(`/bin/zsh -lc 'command -v ${bin}'`, { stdio: "pipe", timeout: 5000 });
4260
+ await _execAsyncFRE(`/bin/zsh -lc 'command -v ${bin}'`, { timeout: 5000 });
3824
4261
  engines[id] = true;
3825
4262
  } catch {
3826
4263
  engines[id] = false;
@@ -3836,7 +4273,7 @@ const server = http.createServer(async (req, res) => {
3836
4273
  let token = "";
3837
4274
  try {
3838
4275
  token =
3839
- JSON.parse(fs.readFileSync(csConfigPath, "utf8"))?.rt?.authToken ||
4276
+ JSON.parse(await fs.promises.readFile(csConfigPath, "utf8"))?.rt?.authToken ||
3840
4277
  "";
3841
4278
  } catch { }
3842
4279
  if (!token) token = process.env.CREWSWARM_RT_AUTH_TOKEN || "";
@@ -3850,13 +4287,13 @@ const server = http.createServer(async (req, res) => {
3850
4287
  const { token } = JSON.parse(body);
3851
4288
  const csDir = path.join(os.homedir(), ".crewswarm");
3852
4289
  const csConfigPath = path.join(csDir, "crewswarm.json");
3853
- fs.mkdirSync(csDir, { recursive: true });
4290
+ await fs.promises.mkdir(csDir, { recursive: true });
3854
4291
  let cfg = {};
3855
4292
  try {
3856
- cfg = JSON.parse(fs.readFileSync(csConfigPath, "utf8"));
4293
+ cfg = JSON.parse(await fs.promises.readFile(csConfigPath, "utf8"));
3857
4294
  } catch { }
3858
4295
  cfg.rt = { ...(cfg.rt || {}), authToken: token };
3859
- fs.writeFileSync(csConfigPath, JSON.stringify(cfg, null, 2));
4296
+ await fs.promises.writeFile(csConfigPath, JSON.stringify(cfg, null, 2));
3860
4297
  res.writeHead(200, { "content-type": "application/json" });
3861
4298
  res.end(JSON.stringify({ ok: true }));
3862
4299
  return;
@@ -3866,15 +4303,20 @@ const server = http.createServer(async (req, res) => {
3866
4303
  const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
3867
4304
  let locked = false;
3868
4305
  try {
3869
- const { execSync } = require("child_process");
4306
+ const { exec: _lockExec } = await import("node:child_process");
4307
+ const { promisify: _lockPromisify } = await import("node:util");
4308
+ const _lockExecAsync = _lockPromisify(_lockExec);
3870
4309
  // Use stat command which is more reliable than ls
3871
- const output = execSync(`stat -f "%Sf" "${cfgPath}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
3872
- locked = output.trim().includes('uchg');
4310
+ const { stdout: lockOut } = await _lockExecAsync(`stat -f "%Sf" "${cfgPath}"`, { encoding: 'utf8' });
4311
+ locked = lockOut.trim().includes('uchg');
3873
4312
  } catch (e) {
3874
4313
  // If stat fails, try ls as fallback
3875
4314
  try {
3876
- const output2 = execSync(`ls -lO "${cfgPath}"`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
3877
- locked = output2.includes('uchg');
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');
3878
4320
  } catch { }
3879
4321
  }
3880
4322
  res.writeHead(200, { "content-type": "application/json" });
@@ -3884,7 +4326,9 @@ const server = http.createServer(async (req, res) => {
3884
4326
  if (url.pathname === "/api/config/lock" && req.method === "POST") {
3885
4327
  const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
3886
4328
  try {
3887
- execSync(`chflags uchg "${cfgPath}"`);
4329
+ const { exec: _chflagsExec } = await import("node:child_process");
4330
+ const { promisify: _chflagsPromisify } = await import("node:util");
4331
+ await _chflagsPromisify(_chflagsExec)(`chflags uchg "${cfgPath}"`);
3888
4332
  res.writeHead(200, { "content-type": "application/json" });
3889
4333
  res.end(JSON.stringify({ ok: true, locked: true }));
3890
4334
  } catch (e) {
@@ -3896,7 +4340,9 @@ const server = http.createServer(async (req, res) => {
3896
4340
  if (url.pathname === "/api/config/unlock" && req.method === "POST") {
3897
4341
  const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
3898
4342
  try {
3899
- execSync(`chflags nouchg "${cfgPath}"`);
4343
+ const { exec: _chflagsExec2 } = await import("node:child_process");
4344
+ const { promisify: _chflagsPromisify2 } = await import("node:util");
4345
+ await _chflagsPromisify2(_chflagsExec2)(`chflags nouchg "${cfgPath}"`);
3900
4346
  res.writeHead(200, { "content-type": "application/json" });
3901
4347
  res.end(JSON.stringify({ ok: true, locked: false }));
3902
4348
  } catch (e) {
@@ -3920,7 +4366,7 @@ const server = http.createServer(async (req, res) => {
3920
4366
  "groq/moonshotai/kimi-k2-instruct-0905";
3921
4367
  let crewLeadModel = process.env.CREWSWARM_CREW_LEAD_MODEL || "";
3922
4368
  try {
3923
- const c = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
4369
+ const c = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
3924
4370
  if (c.opencodeProject) dir = c.opencodeProject;
3925
4371
  if (c.opencodeFallbackModel) fallbackModel = c.opencodeFallbackModel;
3926
4372
  if (c.opencodeModel) opencodeModel = c.opencodeModel;
@@ -3951,10 +4397,10 @@ const server = http.createServer(async (req, res) => {
3951
4397
  }
3952
4398
  const cfgDir = path.join(os.homedir(), ".crewswarm");
3953
4399
  const cfgPath = path.join(cfgDir, "crewswarm.json");
3954
- fs.mkdirSync(cfgDir, { recursive: true });
4400
+ await fs.promises.mkdir(cfgDir, { recursive: true });
3955
4401
  let cfg = {};
3956
4402
  try {
3957
- cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
4403
+ cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
3958
4404
  } catch { }
3959
4405
  if (dir !== undefined) {
3960
4406
  if (dir) cfg.opencodeProject = dir;
@@ -4021,38 +4467,38 @@ const server = http.createServer(async (req, res) => {
4021
4467
  const csConfig = path.join(csDir, "crewswarm.json");
4022
4468
  const csSwarmConfig = path.join(csDir, "crewswarm.json");
4023
4469
  const ocConfig = path.join(os.homedir(), ".openclaw", "openclaw.json");
4024
- function readCSConfig() {
4470
+ async function readCSConfig() {
4025
4471
  try {
4026
- return JSON.parse(fs.readFileSync(csConfig, "utf8"));
4472
+ return JSON.parse(await fs.promises.readFile(csConfig, "utf8"));
4027
4473
  } catch {
4028
4474
  return {};
4029
4475
  }
4030
4476
  }
4031
- function readCSSwarmConfig() {
4477
+ async function readCSSwarmConfig() {
4032
4478
  try {
4033
- return JSON.parse(fs.readFileSync(csSwarmConfig, "utf8"));
4479
+ return JSON.parse(await fs.promises.readFile(csSwarmConfig, "utf8"));
4034
4480
  } catch {
4035
4481
  return {};
4036
4482
  }
4037
4483
  }
4038
- function writeCSSwarmConfig(c) {
4039
- fs.mkdirSync(csDir, { recursive: true });
4040
- fs.writeFileSync(csSwarmConfig, JSON.stringify(c, null, 2));
4484
+ async function writeCSSwarmConfig(c) {
4485
+ await fs.promises.mkdir(csDir, { recursive: true });
4486
+ await fs.promises.writeFile(csSwarmConfig, JSON.stringify(c, null, 2));
4041
4487
  }
4042
- function readOCConfig() {
4488
+ async function readOCConfig() {
4043
4489
  try {
4044
- return JSON.parse(fs.readFileSync(ocConfig, "utf8"));
4490
+ return JSON.parse(await fs.promises.readFile(ocConfig, "utf8"));
4045
4491
  } catch {
4046
4492
  return null;
4047
4493
  }
4048
4494
  }
4049
- function writeOCConfig(c) {
4050
- fs.writeFileSync(ocConfig, JSON.stringify(c, null, 4));
4495
+ async function writeOCConfig(c) {
4496
+ await fs.promises.writeFile(ocConfig, JSON.stringify(c, null, 4));
4051
4497
  }
4052
- function getBuiltinKey(id) {
4053
- const sw = readCSSwarmConfig();
4054
- const cs = readCSConfig();
4055
- const oc = readOCConfig();
4498
+ async function getBuiltinKey(id) {
4499
+ const sw = await readCSSwarmConfig();
4500
+ const cs = await readCSConfig();
4501
+ const oc = await readOCConfig();
4056
4502
  return (
4057
4503
  sw?.providers?.[id]?.apiKey ||
4058
4504
  sw?.env?.[id.toUpperCase() + "_API_KEY"] ||
@@ -4063,10 +4509,257 @@ const server = http.createServer(async (req, res) => {
4063
4509
  );
4064
4510
  }
4065
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
+
4066
4759
  if (url.pathname === "/api/providers/builtin" && req.method === "GET") {
4067
4760
  const keys = {};
4068
4761
  for (const id of Object.keys(BUILTIN_URLS)) {
4069
- keys[id] = getBuiltinKey(id) ? "SET" : "";
4762
+ keys[id] = await getBuiltinKey(id) ? "SET" : "";
4070
4763
  }
4071
4764
  res.writeHead(200, { "content-type": "application/json" });
4072
4765
  res.end(JSON.stringify({ ok: true, keys }));
@@ -4083,16 +4776,16 @@ const server = http.createServer(async (req, res) => {
4083
4776
  if (providerId === "openai-local" && !(apiKey && apiKey.trim()))
4084
4777
  apiKey = "key";
4085
4778
  // Write to ~/.crewswarm/crewswarm.json
4086
- const cfg = readCSSwarmConfig();
4779
+ const cfg = await readCSSwarmConfig();
4087
4780
  if (!cfg.providers) cfg.providers = {};
4088
4781
  cfg.providers[providerId] = {
4089
4782
  ...(cfg.providers[providerId] || {}),
4090
4783
  apiKey,
4091
4784
  baseUrl: BUILTIN_URLS[providerId],
4092
4785
  };
4093
- writeCSSwarmConfig(cfg);
4786
+ await writeCSSwarmConfig(cfg);
4094
4787
  // Sync to ~/.openclaw/openclaw.json if it exists (legacy compat)
4095
- const oc = readOCConfig();
4788
+ const oc = await readOCConfig();
4096
4789
  if (oc) {
4097
4790
  if (!oc.models) oc.models = {};
4098
4791
  if (!oc.models.providers) oc.models.providers = {};
@@ -4104,7 +4797,7 @@ const server = http.createServer(async (req, res) => {
4104
4797
  };
4105
4798
  }
4106
4799
  oc.models.providers[providerId].apiKey = apiKey;
4107
- writeOCConfig(oc);
4800
+ await writeOCConfig(oc);
4108
4801
  }
4109
4802
  res.writeHead(200, { "content-type": "application/json" });
4110
4803
  res.end(JSON.stringify({ ok: true }));
@@ -4129,7 +4822,7 @@ const server = http.createServer(async (req, res) => {
4129
4822
  return;
4130
4823
  }
4131
4824
  const { providerId } = vr.data;
4132
- const apiKey = getBuiltinKey(providerId);
4825
+ const apiKey = await getBuiltinKey(providerId);
4133
4826
  const baseUrl = BUILTIN_URLS[providerId] || "";
4134
4827
  if (providerId === "ollama") {
4135
4828
  try {
@@ -4224,20 +4917,7 @@ const server = http.createServer(async (req, res) => {
4224
4917
  return b;
4225
4918
  })()
4226
4919
  : null;
4227
- const token = (() => {
4228
- try {
4229
- return (
4230
- JSON.parse(
4231
- fs.readFileSync(
4232
- path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
4233
- "utf8",
4234
- ),
4235
- )?.rt?.authToken || ""
4236
- );
4237
- } catch {
4238
- return "";
4239
- }
4240
- })();
4920
+ const token = resolveCrewLeadAuthToken();
4241
4921
  const r = await fetch(
4242
4922
  "http://127.0.0.1:5010/api/settings/bg-consciousness",
4243
4923
  {
@@ -4274,20 +4954,7 @@ const server = http.createServer(async (req, res) => {
4274
4954
  return b;
4275
4955
  })()
4276
4956
  : null;
4277
- const token = (() => {
4278
- try {
4279
- return (
4280
- JSON.parse(
4281
- fs.readFileSync(
4282
- path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
4283
- "utf8",
4284
- ),
4285
- )?.rt?.authToken || ""
4286
- );
4287
- } catch {
4288
- return "";
4289
- }
4290
- })();
4957
+ const token = resolveCrewLeadAuthToken();
4291
4958
  const r = await fetch(
4292
4959
  "http://127.0.0.1:5010/api/settings/cursor-waves",
4293
4960
  {
@@ -4324,20 +4991,7 @@ const server = http.createServer(async (req, res) => {
4324
4991
  return b;
4325
4992
  })()
4326
4993
  : null;
4327
- const token = (() => {
4328
- try {
4329
- return (
4330
- JSON.parse(
4331
- fs.readFileSync(
4332
- path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
4333
- "utf8",
4334
- ),
4335
- )?.rt?.authToken || ""
4336
- );
4337
- } catch {
4338
- return "";
4339
- }
4340
- })();
4994
+ const token = resolveCrewLeadAuthToken();
4341
4995
  const r = await fetch(
4342
4996
  "http://127.0.0.1:5010/api/settings/claude-code",
4343
4997
  {
@@ -4363,6 +5017,27 @@ const server = http.createServer(async (req, res) => {
4363
5017
  }
4364
5018
  return;
4365
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
+ }
4366
5041
  // ── Codex CLI executor toggle ──────────────────────────────────────────────
4367
5042
  if (url.pathname === "/api/settings/codex") {
4368
5043
  const { readFile, writeFile } = await import("node:fs/promises");
@@ -4448,7 +5123,7 @@ const server = http.createServer(async (req, res) => {
4448
5123
  process.env.CREWSWARM_CREW_CLI_ENABLED === "1";
4449
5124
  const installed =
4450
5125
  commandExists("crew") ||
4451
- fs.existsSync(path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js"));
5126
+ await exists(path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js"));
4452
5127
  res.writeHead(200, { "content-type": "application/json" });
4453
5128
  res.end(JSON.stringify({ enabled, installed }));
4454
5129
  } catch {
@@ -4518,9 +5193,9 @@ const server = http.createServer(async (req, res) => {
4518
5193
  return;
4519
5194
  }
4520
5195
  try {
4521
- const raw = (() => {
5196
+ const raw = await (async () => {
4522
5197
  try {
4523
- return fs.readFileSync(CFG_FILE, "utf8");
5198
+ return await fs.promises.readFile(CFG_FILE, "utf8");
4524
5199
  } catch {
4525
5200
  return "{}";
4526
5201
  }
@@ -4572,7 +5247,7 @@ const server = http.createServer(async (req, res) => {
4572
5247
  process.env.CREWSWARM_OPENCODE_ENABLED === "1";
4573
5248
  const installed =
4574
5249
  commandExists(process.env.CREWSWARM_OPENCODE_BIN || "opencode") ||
4575
- fs.existsSync(path.join(os.homedir(), ".opencode", "bin", "opencode"));
5250
+ await exists(path.join(os.homedir(), ".opencode", "bin", "opencode"));
4576
5251
  res.writeHead(200, { "content-type": "application/json" });
4577
5252
  res.end(JSON.stringify({ enabled, installed }));
4578
5253
  } catch {
@@ -4722,7 +5397,7 @@ const server = http.createServer(async (req, res) => {
4722
5397
  const cfgPath = CFG_FILE;
4723
5398
  if (req.method === "GET") {
4724
5399
  try {
4725
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
5400
+ const cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
4726
5401
  const roles = cfg.roleToolDefaults || {};
4727
5402
  res.writeHead(200, { "content-type": "application/json" });
4728
5403
  res.end(JSON.stringify({ roles }));
@@ -4737,7 +5412,7 @@ const server = http.createServer(async (req, res) => {
4737
5412
  let body = "";
4738
5413
  for await (const chunk of req) body += chunk;
4739
5414
  const { roles } = JSON.parse(body || "{}");
4740
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
5415
+ const cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
4741
5416
  cfg.roleToolDefaults = roles || {};
4742
5417
  const writeErr = await safeWriteConfig(cfg);
4743
5418
  if (writeErr) {
@@ -4759,7 +5434,7 @@ const server = http.createServer(async (req, res) => {
4759
5434
  const cfgPath = CFG_FILE;
4760
5435
  if (req.method === "GET") {
4761
5436
  try {
4762
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
5437
+ const cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
4763
5438
  const caps = cfg.globalSpendingCaps || {};
4764
5439
  res.writeHead(200, { "content-type": "application/json" });
4765
5440
  res.end(
@@ -4781,7 +5456,7 @@ const server = http.createServer(async (req, res) => {
4781
5456
  const { dailyTokenLimit, dailyCostLimitUSD } = JSON.parse(
4782
5457
  body || "{}",
4783
5458
  );
4784
- const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
5459
+ const cfg = JSON.parse(await fs.promises.readFile(cfgPath, "utf8"));
4785
5460
  cfg.globalSpendingCaps = {
4786
5461
  dailyTokenLimit: dailyTokenLimit ?? undefined,
4787
5462
  dailyCostLimitUSD: dailyCostLimitUSD ?? undefined,
@@ -4811,8 +5486,8 @@ const server = http.createServer(async (req, res) => {
4811
5486
  const rulesPath = path.join(CFG_DIR, "global-rules.md");
4812
5487
  if (req.method === "GET") {
4813
5488
  try {
4814
- const content = fs.existsSync(rulesPath)
4815
- ? fs.readFileSync(rulesPath, "utf8")
5489
+ const content = await exists(rulesPath)
5490
+ ? await fs.promises.readFile(rulesPath, "utf8")
4816
5491
  : "";
4817
5492
  res.writeHead(200, { "content-type": "application/json" });
4818
5493
  res.end(JSON.stringify({ content }));
@@ -4827,7 +5502,7 @@ const server = http.createServer(async (req, res) => {
4827
5502
  let body = "";
4828
5503
  for await (const chunk of req) body += chunk;
4829
5504
  const { content } = JSON.parse(body || "{}");
4830
- fs.writeFileSync(rulesPath, content || "", "utf8");
5505
+ await fs.promises.writeFile(rulesPath, content || "", "utf8");
4831
5506
  res.writeHead(200, { "content-type": "application/json" });
4832
5507
  res.end(JSON.stringify({ ok: true }));
4833
5508
  } catch (e) {
@@ -4877,20 +5552,7 @@ const server = http.createServer(async (req, res) => {
4877
5552
  for await (const c of req) b += c;
4878
5553
  return b;
4879
5554
  })();
4880
- const token = (() => {
4881
- try {
4882
- return (
4883
- JSON.parse(
4884
- fs.readFileSync(
4885
- path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
4886
- "utf8",
4887
- ),
4888
- )?.rt?.authToken || ""
4889
- );
4890
- } catch {
4891
- return "";
4892
- }
4893
- })();
5555
+ const token = resolveCrewLeadAuthToken();
4894
5556
  const upstream = await fetch(
4895
5557
  "http://127.0.0.1:5010/api/engine-passthrough",
4896
5558
  {
@@ -4955,20 +5617,7 @@ const server = http.createServer(async (req, res) => {
4955
5617
  return b;
4956
5618
  })()
4957
5619
  : null;
4958
- const token = (() => {
4959
- try {
4960
- return (
4961
- JSON.parse(
4962
- fs.readFileSync(
4963
- path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
4964
- "utf8",
4965
- ),
4966
- )?.rt?.authToken || ""
4967
- );
4968
- } catch {
4969
- return "";
4970
- }
4971
- })();
5620
+ const token = resolveCrewLeadAuthToken();
4972
5621
  const r = await fetch(
4973
5622
  "http://127.0.0.1:5010/api/settings/global-fallback",
4974
5623
  {
@@ -5006,7 +5655,7 @@ const server = http.createServer(async (req, res) => {
5006
5655
  );
5007
5656
  const deviceJsonAlt = path.join(os.homedir(), ".openclaw", "device.json");
5008
5657
  const installed =
5009
- fs.existsSync(deviceJson) || fs.existsSync(deviceJsonAlt);
5658
+ await exists(deviceJson) || await exists(deviceJsonAlt);
5010
5659
  res.writeHead(200, { "content-type": "application/json" });
5011
5660
  res.end(JSON.stringify({ ok: true, installed }));
5012
5661
  return;
@@ -5016,10 +5665,8 @@ const server = http.createServer(async (req, res) => {
5016
5665
  try {
5017
5666
  let online = false;
5018
5667
  try {
5019
- const health = await fetch("http://127.0.0.1:5010/health", {
5020
- signal: AbortSignal.timeout(1500),
5021
- });
5022
- online = health.ok;
5668
+ const { ok } = await crewLeadRequest("/health", { timeout: 1500 });
5669
+ online = ok;
5023
5670
  } catch { }
5024
5671
  if (!online) {
5025
5672
  const { execSync: es } = await import("node:child_process");
@@ -5043,7 +5690,7 @@ const server = http.createServer(async (req, res) => {
5043
5690
  if (url.pathname === "/api/models" && req.method === "GET") {
5044
5691
  try {
5045
5692
  const csSwarm = JSON.parse(
5046
- fs.readFileSync(
5693
+ await fs.promises.readFile(
5047
5694
  path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
5048
5695
  "utf8",
5049
5696
  ),
@@ -5151,8 +5798,8 @@ const server = http.createServer(async (req, res) => {
5151
5798
  if (projectId) {
5152
5799
  try {
5153
5800
  const registryFile = path.join(CFG_DIR, "projects.json");
5154
- const projects = fs.existsSync(registryFile)
5155
- ? JSON.parse(fs.readFileSync(registryFile, "utf8"))
5801
+ const projects = await exists(registryFile)
5802
+ ? JSON.parse(await fs.promises.readFile(registryFile, "utf8"))
5156
5803
  : {};
5157
5804
  projectDir = projects?.[projectId]?.outputDir || null;
5158
5805
  } catch { }
@@ -5253,7 +5900,7 @@ const server = http.createServer(async (req, res) => {
5253
5900
 
5254
5901
  // Load agent config
5255
5902
  const csSwarm = JSON.parse(
5256
- fs.readFileSync(
5903
+ await fs.promises.readFile(
5257
5904
  path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
5258
5905
  "utf8",
5259
5906
  ),
@@ -5293,7 +5940,7 @@ const server = http.createServer(async (req, res) => {
5293
5940
  );
5294
5941
  let systemPrompt = `You are ${agentId}.`;
5295
5942
  try {
5296
- const prompts = JSON.parse(fs.readFileSync(promptPath, "utf8"));
5943
+ const prompts = JSON.parse(await fs.promises.readFile(promptPath, "utf8"));
5297
5944
  const bareId = agentId.replace(/^crew-/, "");
5298
5945
  systemPrompt = prompts[agentId] || prompts[bareId] || systemPrompt;
5299
5946
  } catch { }
@@ -5584,8 +6231,8 @@ const server = http.createServer(async (req, res) => {
5584
6231
  if (!resolvedProjectDir && projectId) {
5585
6232
  try {
5586
6233
  const registryFile = path.join(CFG_DIR, "projects.json");
5587
- const projects = fs.existsSync(registryFile)
5588
- ? JSON.parse(fs.readFileSync(registryFile, "utf8"))
6234
+ const projects = await exists(registryFile)
6235
+ ? JSON.parse(await fs.promises.readFile(registryFile, "utf8"))
5589
6236
  : {};
5590
6237
  resolvedProjectDir = projects?.[projectId]?.outputDir || null;
5591
6238
  } catch { }
@@ -5618,7 +6265,10 @@ const server = http.createServer(async (req, res) => {
5618
6265
  `http://127.0.0.1:${listenPort}/api/engine-passthrough`,
5619
6266
  {
5620
6267
  method: "POST",
5621
- headers: { "content-type": "application/json" },
6268
+ headers: {
6269
+ "content-type": "application/json",
6270
+ ...(clAuthToken ? { authorization: `Bearer ${clAuthToken}` } : {}),
6271
+ },
5622
6272
  body: JSON.stringify(payload),
5623
6273
  signal: AbortSignal.timeout(240000),
5624
6274
  },
@@ -7000,7 +7650,7 @@ ORDER BY day DESC, cost DESC;`;
7000
7650
  try {
7001
7651
  const { execFile } = await import("node:child_process");
7002
7652
  const ocBin = path.join(os.homedir(), ".opencode", "bin", "opencode");
7003
- const bin = fs.existsSync(ocBin) ? ocBin : "opencode";
7653
+ const bin = await exists(ocBin) ? ocBin : "opencode";
7004
7654
  models = await new Promise((resolve, reject) => {
7005
7655
  const child = execFile(
7006
7656
  bin,
@@ -7026,7 +7676,7 @@ ORDER BY day DESC, cost DESC;`;
7026
7676
  "opencode",
7027
7677
  "auth.json",
7028
7678
  );
7029
- const auth = JSON.parse(fs.readFileSync(authPath, "utf8"));
7679
+ const auth = JSON.parse(await fs.promises.readFile(authPath, "utf8"));
7030
7680
  const providers = Object.keys(auth || {}).map((k) => k.toLowerCase());
7031
7681
  const knownModels = {
7032
7682
  openai: [
@@ -7290,6 +7940,16 @@ ORDER BY day DESC, cost DESC;`;
7290
7940
  for (const m of defaultModels) {
7291
7941
  if (!allModels.includes(m)) allModels.push(m);
7292
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
+ }
7293
7953
  const roleToolDefaults = cfg.roleToolDefaults || {};
7294
7954
  res.writeHead(200, { "content-type": "application/json" });
7295
7955
  res.end(
@@ -7367,7 +8027,7 @@ ORDER BY day DESC, cost DESC;`;
7367
8027
  await import("../lib/memory/shared-adapter.mjs");
7368
8028
  const brainPath = path.join(CREWSWARM_DIR, "memory", "brain.md");
7369
8029
 
7370
- if (!fs.existsSync(brainPath)) {
8030
+ if (!await exists(brainPath)) {
7371
8031
  res.statusCode = 404;
7372
8032
  res.end(JSON.stringify({ ok: false, error: "brain.md not found" }));
7373
8033
  return;
@@ -7801,9 +8461,8 @@ ORDER BY day DESC, cost DESC;`;
7801
8461
 
7802
8462
  if (url.pathname === "/api/crew/start" && req.method === "POST") {
7803
8463
  const { spawn: spawnProc } = await import("node:child_process");
7804
- const { existsSync: eS } = await import("node:fs");
7805
8464
  const crewScript = path.join(CREWSWARM_DIR, "scripts", "start-crew.mjs");
7806
- if (!eS(crewScript))
8465
+ if (!(await exists(crewScript)))
7807
8466
  throw new Error(
7808
8467
  "start-crew.mjs not found — is the dashboard running from the crewswarm repo?",
7809
8468
  );
@@ -7925,13 +8584,13 @@ ORDER BY day DESC, cost DESC;`;
7925
8584
  "agent-prompts.json",
7926
8585
  );
7927
8586
  try {
7928
- const promptsRaw = JSON.parse(fs.readFileSync(promptsFile, "utf8"));
8587
+ const promptsRaw = JSON.parse(await fs.promises.readFile(promptsFile, "utf8"));
7929
8588
  const { applySharedChatPromptOverlay } = await import(
7930
8589
  "../lib/chat/shared-chat-prompt-overlay.mjs"
7931
8590
  );
7932
8591
  let agents = [];
7933
8592
  try {
7934
- const cfg = JSON.parse(fs.readFileSync(CFG_FILE, "utf8"));
8593
+ const cfg = JSON.parse(await fs.promises.readFile(CFG_FILE, "utf8"));
7935
8594
  const list = Array.isArray(cfg.agents)
7936
8595
  ? cfg.agents
7937
8596
  : Array.isArray(cfg.agents?.list)
@@ -7994,13 +8653,13 @@ ORDER BY day DESC, cost DESC;`;
7994
8653
  try {
7995
8654
  let prompts = {};
7996
8655
  try {
7997
- prompts = JSON.parse(fs.readFileSync(promptsFile, "utf8"));
8656
+ prompts = JSON.parse(await fs.promises.readFile(promptsFile, "utf8"));
7998
8657
  } catch { }
7999
8658
 
8000
8659
  const keySet = new Set(Object.keys(prompts));
8001
8660
  let agentIds = [];
8002
8661
  try {
8003
- const cfg = JSON.parse(fs.readFileSync(CFG_FILE, "utf8"));
8662
+ const cfg = JSON.parse(await fs.promises.readFile(CFG_FILE, "utf8"));
8004
8663
  const list = Array.isArray(cfg.agents)
8005
8664
  ? cfg.agents
8006
8665
  : Array.isArray(cfg.agents?.list)
@@ -8019,7 +8678,7 @@ ORDER BY day DESC, cost DESC;`;
8019
8678
  delete prompts[canonicalAgent.slice(5)];
8020
8679
  }
8021
8680
 
8022
- fs.writeFileSync(promptsFile, JSON.stringify(prompts, null, 2), "utf8");
8681
+ await fs.promises.writeFile(promptsFile, JSON.stringify(prompts, null, 2), "utf8");
8023
8682
  console.log(
8024
8683
  `[dashboard] Prompt updated for ${canonicalAgent} (${prompt.length} chars)`,
8025
8684
  );
@@ -8057,11 +8716,11 @@ ORDER BY day DESC, cost DESC;`;
8057
8716
  ]);
8058
8717
  const MAX_FILES = 500;
8059
8718
  const results = [];
8060
- function walk(dir, depth) {
8719
+ async function walk(dir, depth) {
8061
8720
  if (depth > 5) return;
8062
8721
  let entries;
8063
8722
  try {
8064
- entries = fs.readdirSync(dir, { withFileTypes: true });
8723
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
8065
8724
  } catch {
8066
8725
  return;
8067
8726
  }
@@ -8069,12 +8728,12 @@ ORDER BY day DESC, cost DESC;`;
8069
8728
  if (e.name.startsWith(".") || e.name === "node_modules") continue;
8070
8729
  const full = path.join(dir, e.name);
8071
8730
  if (e.isDirectory()) {
8072
- walk(full, depth + 1);
8731
+ await walk(full, depth + 1);
8073
8732
  } else if (e.isFile()) {
8074
8733
  const ext = path.extname(e.name).toLowerCase();
8075
8734
  if (!ALLOWED_EXT.has(ext)) continue;
8076
8735
  try {
8077
- const st = fs.statSync(full);
8736
+ const st = await fs.promises.stat(full);
8078
8737
  results.push({ path: full, size: st.size, mtime: st.mtimeMs });
8079
8738
  } catch {
8080
8739
  /* skip */
@@ -8083,7 +8742,7 @@ ORDER BY day DESC, cost DESC;`;
8083
8742
  }
8084
8743
  }
8085
8744
  }
8086
- walk(scanDir, 0);
8745
+ await walk(scanDir, 0);
8087
8746
  results.sort((a, b) => b.mtime - a.mtime);
8088
8747
  res.writeHead(200, {
8089
8748
  "content-type": "application/json",
@@ -8102,7 +8761,7 @@ ORDER BY day DESC, cost DESC;`;
8102
8761
  return;
8103
8762
  }
8104
8763
  try {
8105
- const raw = fs.readFileSync(filePath, "utf8");
8764
+ const raw = await fs.promises.readFile(filePath, "utf8");
8106
8765
  const lines = raw.split("\n");
8107
8766
  const content =
8108
8767
  lines.length > 300
@@ -8143,17 +8802,17 @@ ORDER BY day DESC, cost DESC;`;
8143
8802
  "telegram-messages.jsonl",
8144
8803
  );
8145
8804
 
8146
- function loadTgConfig() {
8805
+ async function loadTgConfig() {
8147
8806
  try {
8148
- return JSON.parse(fs.readFileSync(TG_CONFIG_PATH, "utf8"));
8807
+ return JSON.parse(await fs.promises.readFile(TG_CONFIG_PATH, "utf8"));
8149
8808
  } catch {
8150
8809
  return {};
8151
8810
  }
8152
8811
  }
8153
8812
 
8154
- function isTgRunning() {
8813
+ async function isTgRunning() {
8155
8814
  try {
8156
- const pid = parseInt(fs.readFileSync(TG_PID_PATH, "utf8").trim(), 10);
8815
+ const pid = parseInt((await fs.promises.readFile(TG_PID_PATH, "utf8")).trim(), 10);
8157
8816
  if (!pid) return false;
8158
8817
  process.kill(pid, 0);
8159
8818
  return true;
@@ -8163,15 +8822,15 @@ ORDER BY day DESC, cost DESC;`;
8163
8822
  }
8164
8823
 
8165
8824
  if (url.pathname === "/api/telegram/status") {
8166
- const running = isTgRunning();
8167
- const cfg = loadTgConfig();
8825
+ const running = await isTgRunning();
8826
+ const cfg = await loadTgConfig();
8168
8827
  res.writeHead(200, { "content-type": "application/json" });
8169
8828
  res.end(JSON.stringify({ running, botName: cfg.botName || "" }));
8170
8829
  return;
8171
8830
  }
8172
8831
 
8173
8832
  if (url.pathname === "/api/telegram/config" && req.method === "GET") {
8174
- const cfg = loadTgConfig();
8833
+ const cfg = await loadTgConfig();
8175
8834
  res.writeHead(200, { "content-type": "application/json" });
8176
8835
  res.end(
8177
8836
  JSON.stringify({
@@ -8190,9 +8849,9 @@ ORDER BY day DESC, cost DESC;`;
8190
8849
  let raw = "";
8191
8850
  for await (const chunk of req) raw += chunk;
8192
8851
  const body = JSON.parse(raw || "{}");
8193
- const existing = loadTgConfig();
8852
+ const existing = await loadTgConfig();
8194
8853
  const updated = { ...existing, ...body };
8195
- fs.writeFileSync(TG_CONFIG_PATH, JSON.stringify(updated, null, 2));
8854
+ await fs.promises.writeFile(TG_CONFIG_PATH, JSON.stringify(updated, null, 2));
8196
8855
  res.writeHead(200, { "content-type": "application/json" });
8197
8856
  res.end(JSON.stringify({ ok: true }));
8198
8857
  return;
@@ -8203,19 +8862,19 @@ ORDER BY day DESC, cost DESC;`;
8203
8862
  for await (const chunk of req) raw += chunk;
8204
8863
  const body = JSON.parse(raw || "{}");
8205
8864
  if (body.token) {
8206
- const existing = loadTgConfig();
8207
- fs.writeFileSync(
8865
+ const existing = await loadTgConfig();
8866
+ await fs.promises.writeFile(
8208
8867
  TG_CONFIG_PATH,
8209
8868
  JSON.stringify({ ...existing, ...body }, null, 2),
8210
8869
  );
8211
8870
  }
8212
- const cfg = loadTgConfig();
8871
+ const cfg = await loadTgConfig();
8213
8872
  if (!cfg.token) {
8214
8873
  res.writeHead(400, { "content-type": "application/json" });
8215
8874
  res.end(JSON.stringify({ error: "No token configured" }));
8216
8875
  return;
8217
8876
  }
8218
- if (isTgRunning()) {
8877
+ if (await isTgRunning()) {
8219
8878
  res.writeHead(200, { "content-type": "application/json" });
8220
8879
  res.end(JSON.stringify({ ok: true, message: "Already running" }));
8221
8880
  return;
@@ -8240,7 +8899,7 @@ ORDER BY day DESC, cost DESC;`;
8240
8899
 
8241
8900
  if (url.pathname === "/api/telegram/stop" && req.method === "POST") {
8242
8901
  try {
8243
- const pid = parseInt(fs.readFileSync(TG_PID_PATH, "utf8").trim(), 10);
8902
+ const pid = parseInt((await fs.promises.readFile(TG_PID_PATH, "utf8")).trim(), 10);
8244
8903
  if (pid) process.kill(pid, "SIGTERM");
8245
8904
  } catch { }
8246
8905
  res.writeHead(200, { "content-type": "application/json" });
@@ -8250,7 +8909,7 @@ ORDER BY day DESC, cost DESC;`;
8250
8909
 
8251
8910
  if (url.pathname === "/api/telegram/messages") {
8252
8911
  try {
8253
- const raw = fs.readFileSync(TG_MSG_PATH, "utf8");
8912
+ const raw = await fs.promises.readFile(TG_MSG_PATH, "utf8");
8254
8913
  const msgs = raw
8255
8914
  .trim()
8256
8915
  .split("\n")
@@ -8280,7 +8939,7 @@ ORDER BY day DESC, cost DESC;`;
8280
8939
  "logs",
8281
8940
  "telegram-bridge.jsonl",
8282
8941
  );
8283
- const raw = fs.readFileSync(TG_LOG_PATH, "utf8");
8942
+ const raw = await fs.promises.readFile(TG_LOG_PATH, "utf8");
8284
8943
  const lines = raw.trim().split("\n").filter(Boolean);
8285
8944
  const topics = new Map(); // chatId:threadId -> {chatId, threadId, lastSeen, text}
8286
8945
 
@@ -8332,16 +8991,16 @@ ORDER BY day DESC, cost DESC;`;
8332
8991
  );
8333
8992
  const WA_AUTH_DIR = path.join(os.homedir(), ".crewswarm", "whatsapp-auth");
8334
8993
 
8335
- function loadWaCfg() {
8994
+ async function loadWaCfg() {
8336
8995
  try {
8337
- return JSON.parse(fs.readFileSync(WA_CONFIG_PATH, "utf8"));
8996
+ return JSON.parse(await fs.promises.readFile(WA_CONFIG_PATH, "utf8"));
8338
8997
  } catch {
8339
8998
  return {};
8340
8999
  }
8341
9000
  }
8342
- function isWaRunning() {
9001
+ async function isWaRunning() {
8343
9002
  try {
8344
- const pid = parseInt(fs.readFileSync(WA_PID_PATH, "utf8").trim(), 10);
9003
+ const pid = parseInt((await fs.promises.readFile(WA_PID_PATH, "utf8")).trim(), 10);
8345
9004
  if (!pid) return false;
8346
9005
  process.kill(pid, 0);
8347
9006
  return true;
@@ -8360,16 +9019,16 @@ ORDER BY day DESC, cost DESC;`;
8360
9019
  }
8361
9020
 
8362
9021
  if (url.pathname === "/api/whatsapp/status") {
8363
- const running = isWaRunning();
8364
- const authSaved = fs.existsSync(path.join(WA_AUTH_DIR, "creds.json"));
8365
- 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();
8366
9025
  res.writeHead(200, { "content-type": "application/json" });
8367
9026
  res.end(JSON.stringify({ running, authSaved, number: cfg.number || "" }));
8368
9027
  return;
8369
9028
  }
8370
9029
 
8371
9030
  if (url.pathname === "/api/whatsapp/config" && req.method === "GET") {
8372
- const cfg = loadWaCfg();
9031
+ const cfg = await loadWaCfg();
8373
9032
  res.writeHead(200, { "content-type": "application/json" });
8374
9033
  res.end(
8375
9034
  JSON.stringify({
@@ -8386,8 +9045,8 @@ ORDER BY day DESC, cost DESC;`;
8386
9045
  let raw = "";
8387
9046
  for await (const chunk of req) raw += chunk;
8388
9047
  const body = JSON.parse(raw || "{}");
8389
- const existing = loadWaCfg();
8390
- fs.writeFileSync(
9048
+ const existing = await loadWaCfg();
9049
+ await fs.promises.writeFile(
8391
9050
  WA_CONFIG_PATH,
8392
9051
  JSON.stringify({ ...existing, ...body }, null, 2),
8393
9052
  );
@@ -8398,13 +9057,13 @@ ORDER BY day DESC, cost DESC;`;
8398
9057
  ".crewswarm",
8399
9058
  "crewswarm.json",
8400
9059
  );
8401
- const swarm = JSON.parse(fs.readFileSync(swarmPath, "utf8"));
9060
+ const swarm = JSON.parse(await fs.promises.readFile(swarmPath, "utf8"));
8402
9061
  swarm.env = swarm.env || {};
8403
9062
  if (body.allowedNumbers !== undefined) {
8404
9063
  swarm.env.WA_ALLOWED_NUMBERS = (body.allowedNumbers || []).join(",");
8405
9064
  }
8406
9065
  if (body.targetAgent) swarm.env.WA_TARGET_AGENT = body.targetAgent;
8407
- fs.writeFileSync(swarmPath, JSON.stringify(swarm, null, 2));
9066
+ await fs.promises.writeFile(swarmPath, JSON.stringify(swarm, null, 2));
8408
9067
  } catch { }
8409
9068
  res.writeHead(200, { "content-type": "application/json" });
8410
9069
  res.end(JSON.stringify({ ok: true }));
@@ -8412,24 +9071,21 @@ ORDER BY day DESC, cost DESC;`;
8412
9071
  }
8413
9072
 
8414
9073
  if (url.pathname === "/api/whatsapp/start" && req.method === "POST") {
8415
- if (isWaRunning()) {
9074
+ if (await isWaRunning()) {
8416
9075
  res.writeHead(200, { "content-type": "application/json" });
8417
9076
  res.end(JSON.stringify({ ok: true, message: "Already running" }));
8418
9077
  return;
8419
9078
  }
8420
- const cfg = loadWaCfg();
8421
- const swarm = (() => {
8422
- try {
8423
- return JSON.parse(
8424
- fs.readFileSync(
8425
- path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
8426
- "utf8",
8427
- ),
8428
- );
8429
- } catch {
8430
- return {};
8431
- }
8432
- })();
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 { }
8433
9089
  const waEnv = swarm.env || {};
8434
9090
  const { spawn: spawnBridge } = await import("node:child_process");
8435
9091
  const bridgePath = path.join(CREWSWARM_DIR, "whatsapp-bridge.mjs");
@@ -8451,7 +9107,7 @@ ORDER BY day DESC, cost DESC;`;
8451
9107
  });
8452
9108
  proc.unref();
8453
9109
  await new Promise((r) => setTimeout(r, 1500));
8454
- const running = isWaRunning();
9110
+ const running = await isWaRunning();
8455
9111
  res.writeHead(running ? 200 : 500, { "content-type": "application/json" });
8456
9112
  res.end(
8457
9113
  JSON.stringify({
@@ -8467,7 +9123,7 @@ ORDER BY day DESC, cost DESC;`;
8467
9123
 
8468
9124
  if (url.pathname === "/api/whatsapp/stop" && req.method === "POST") {
8469
9125
  try {
8470
- const pid = parseInt(fs.readFileSync(WA_PID_PATH, "utf8").trim(), 10);
9126
+ const pid = parseInt((await fs.promises.readFile(WA_PID_PATH, "utf8")).trim(), 10);
8471
9127
  if (pid) process.kill(pid, "SIGTERM");
8472
9128
  } catch { }
8473
9129
  res.writeHead(200, { "content-type": "application/json" });
@@ -8477,7 +9133,7 @@ ORDER BY day DESC, cost DESC;`;
8477
9133
 
8478
9134
  if (url.pathname === "/api/whatsapp/messages") {
8479
9135
  try {
8480
- const raw = fs.readFileSync(WA_MSG_PATH, "utf8");
9136
+ const raw = await fs.promises.readFile(WA_MSG_PATH, "utf8");
8481
9137
  const msgs = raw
8482
9138
  .trim()
8483
9139
  .split("\n")
@@ -8637,7 +9293,7 @@ ORDER BY day DESC, cost DESC;`;
8637
9293
  ".crewswarm",
8638
9294
  "telegram-bridge.json",
8639
9295
  );
8640
- const tgCfg = JSON.parse(fs.readFileSync(TG_CONFIG_PATH, "utf8"));
9296
+ const tgCfg = JSON.parse(await fs.promises.readFile(TG_CONFIG_PATH, "utf8"));
8641
9297
  const botToken = tgCfg.token;
8642
9298
  if (botToken) {
8643
9299
  const tgRes = await fetch(
@@ -8673,9 +9329,49 @@ ORDER BY day DESC, cost DESC;`;
8673
9329
  if (url.pathname === "/api/services/status") {
8674
9330
  let services;
8675
9331
  try {
8676
- const { execSync } = await import("node:child_process");
9332
+ const { exec: execCb } = await import("node:child_process");
8677
9333
  const net = await import("node:net");
8678
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
+
8679
9375
  function portListening(port, timeoutMs = 2000) {
8680
9376
  return new Promise((resolve) => {
8681
9377
  const sock = new net.default.Socket();
@@ -8709,9 +9405,9 @@ ORDER BY day DESC, cost DESC;`;
8709
9405
  }
8710
9406
  }
8711
9407
 
8712
- function pidRunning(pidFile) {
9408
+ async function pidRunning(pidFile) {
8713
9409
  try {
8714
- const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
9410
+ const pid = parseInt((await fs.promises.readFile(pidFile, "utf8")).trim(), 10);
8715
9411
  if (!pid) return null;
8716
9412
  process.kill(pid, 0);
8717
9413
  return pid;
@@ -8720,85 +9416,41 @@ ORDER BY day DESC, cost DESC;`;
8720
9416
  }
8721
9417
  }
8722
9418
 
8723
- function countProcs(pattern) {
8724
- try {
8725
- const out = execSync(`pgrep -f "${pattern}" | wc -l`, {
8726
- encoding: "utf8",
8727
- timeout: 300,
8728
- stdio: ["pipe", "pipe", "pipe"],
8729
- }).trim();
8730
- return parseInt(out, 10) || 0;
8731
- } catch {
8732
- return 0;
8733
- }
8734
- }
8735
-
8736
- function getPid(pattern) {
8737
- try {
8738
- const out = execSync(`pgrep -f "${pattern}"`, {
8739
- encoding: "utf8",
8740
- timeout: 300,
8741
- stdio: ["pipe", "pipe", "pipe"],
8742
- }).trim();
8743
- const pids = out
8744
- .split("\n")
8745
- .filter(Boolean)
8746
- .map((p) => parseInt(p, 10));
8747
- return pids.length > 0 ? pids[0] : null;
8748
- } catch {
8749
- return null;
8750
- }
8751
- }
8752
-
8753
- function getAllPids(pattern) {
8754
- try {
8755
- const out = execSync(`pgrep -f "${pattern}"`, {
8756
- encoding: "utf8",
8757
- timeout: 300,
8758
- stdio: ["pipe", "pipe", "pipe"],
8759
- }).trim();
8760
- return out
8761
- .split("\n")
8762
- .filter(Boolean)
8763
- .map((p) => parseInt(p, 10));
8764
- } catch {
8765
- 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);
8766
9424
  }
8767
- }
8768
-
8769
- function procStartTime(pid) {
8770
- try {
8771
- const out = execSync(`ps -p ${pid} -o lstart=`, {
8772
- encoding: "utf8",
8773
- timeout: 300,
8774
- stdio: ["pipe", "pipe", "pipe"],
8775
- }).trim();
8776
- return out ? new Date(out).getTime() : null;
8777
- } catch {
8778
- 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;
8779
9434
  }
9435
+ return false;
8780
9436
  }
8781
9437
 
8782
9438
  const crewLeadPort = Number(process.env.CREW_LEAD_PORT || 5010);
8783
- const tgPid = pidRunning(
9439
+ const tgPid = await pidRunning(
8784
9440
  path.join(os.homedir(), ".crewswarm", "logs", "telegram-bridge.pid"),
8785
9441
  );
8786
9442
  const waPid =
8787
- pidRunning(
9443
+ (await pidRunning(
8788
9444
  path.join(os.homedir(), ".crewswarm", "logs", "whatsapp-bridge.pid"),
8789
- ) || getPid("whatsapp-bridge.mjs");
9445
+ )) || findPid("whatsapp-bridge\\.mjs");
9446
+
9447
+ // Fire network checks in parallel — all process lookups are instant (ps snapshot)
8790
9448
  const rtStatusPromise = fetch("http://127.0.0.1:18889/status", {
8791
- signal: AbortSignal.timeout(2000),
9449
+ signal: AbortSignal.timeout(5000),
8792
9450
  });
8793
9451
  const mcpHealthPromise = httpOk("http://127.0.0.1:5020/health", 3000);
8794
9452
  const [
8795
- rtUp,
8796
- crewLeadUp,
8797
- gwUp,
8798
- ocPortUp,
8799
- dashUp,
8800
- studioUp,
8801
- watchUp,
9453
+ rtUp, crewLeadUp, gwUp, ocPortUp, dashUp, studioUp, watchUp,
8802
9454
  ] = await Promise.all([
8803
9455
  portListening(18889),
8804
9456
  portListening(crewLeadPort),
@@ -8808,37 +9460,38 @@ ORDER BY day DESC, cost DESC;`;
8808
9460
  portListening(3333),
8809
9461
  portListening(3334),
8810
9462
  ]);
8811
- const rtPid = getPid("opencrew-rt-daemon");
8812
- const crewLeadPid = getPid("crew-lead.mjs");
8813
- const gwPid = getPid("openclaw-gateway");
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")));
8814
9484
  const oclawPaired =
8815
- fs.existsSync(
9485
+ (await exists(
8816
9486
  path.join(os.homedir(), ".openclaw", "devices", "paired.json"),
8817
- ) ||
8818
- fs.existsSync(path.join(os.homedir(), ".openclaw", "device.json"));
8819
- const ocPid =
8820
- getPid("\\.opencode serve") ||
8821
- getPid("opencode serve") ||
8822
- getPid("bin/.opencode") ||
8823
- getPid("/.opencode");
9487
+ )) ||
9488
+ (await exists(path.join(os.homedir(), ".openclaw", "device.json")));
8824
9489
  const ocUp = ocPortUp || ocPid !== null;
8825
- const codexInstalled = commandExists(process.env.CODEX_CLI_BIN || "codex");
8826
- const claudeInstalled = commandExists(process.env.CLAUDE_CODE_BIN || "claude");
8827
- const cursorInstalled = commandExists(
8828
- process.env.CURSOR_CLI_BIN ||
8829
- path.join(os.homedir(), ".local", "bin", "agent"),
8830
- [path.join(os.homedir(), ".local", "bin", "agent")],
8831
- ) || commandExists("agent", [path.join(os.homedir(), ".local", "bin", "agent")]);
8832
- const geminiInstalled = commandExists(process.env.GEMINI_CLI_BIN || "gemini");
8833
- const crewCliInstalled =
8834
- commandExists("crew", [
8835
- path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js"),
8836
- ]) ||
8837
- fs.existsSync(path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js"));
9490
+
8838
9491
  let swarmCfg = {};
8839
9492
  try {
8840
9493
  swarmCfg = JSON.parse(
8841
- fs.readFileSync(
9494
+ await fs.promises.readFile(
8842
9495
  path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
8843
9496
  "utf8",
8844
9497
  ),
@@ -8860,10 +9513,6 @@ ORDER BY day DESC, cost DESC;`;
8860
9513
  cfgEnv.CREWSWARM_OPENCODE_ENABLED === "1" ||
8861
9514
  process.env.CREWSWARM_OPENCODE_ENABLED === "on" ||
8862
9515
  process.env.CREWSWARM_OPENCODE_ENABLED === "1";
8863
- const mcpPid = getPid("mcp-server.mjs");
8864
- const studioPid =
8865
- getPid("apps/vibe/server.mjs") || getPid("npm.*studio:start");
8866
- const watchPid = getPid("watch-server.mjs");
8867
9516
 
8868
9517
  // Agent count: ask RT bus which agents are actually connected (most reliable source)
8869
9518
  let agentsOnline = 0;
@@ -8877,11 +9526,11 @@ ORDER BY day DESC, cost DESC;`;
8877
9526
  (a) => String(a).toLowerCase() !== "crew-lead",
8878
9527
  );
8879
9528
  agentsOnline = rtAgentList.length;
8880
- agentPids = getAllPids("gateway-bridge.mjs --rt-daemon");
9529
+ agentPids = findAllPids("gateway-bridge\\.mjs --rt-daemon");
8881
9530
  } catch {
8882
- // RT not reachable — fall back to pgrep for count, config for names
8883
- agentsOnline = countProcs("gateway-bridge.mjs --rt-daemon");
8884
- agentPids = getAllPids("gateway-bridge.mjs --rt-daemon");
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");
8885
9534
  try {
8886
9535
  rtAgentList = (swarmCfg.agents || [])
8887
9536
  .map((a) => a.id)
@@ -8897,7 +9546,7 @@ ORDER BY day DESC, cost DESC;`;
8897
9546
  } catch { }
8898
9547
  if (agentsTotal === 0) agentsTotal = 14;
8899
9548
  agentsTotal = Math.max(agentsTotal, agentsOnline);
8900
- const pmCount = countProcs("pm-loop.mjs");
9549
+ const pmCount = countProcs("pm-loop\\.mjs");
8901
9550
 
8902
9551
  services = [
8903
9552
  {
@@ -9106,12 +9755,8 @@ ORDER BY day DESC, cost DESC;`;
9106
9755
  canRestart: false,
9107
9756
  pid: (() => {
9108
9757
  try {
9109
- const pids = execSync("pgrep -f 'pm-loop.mjs'", {
9110
- encoding: "utf8",
9111
- timeout: 300,
9112
- stdio: ["pipe", "pipe", "pipe"],
9113
- }).trim().split("\n").filter(Boolean).map(p => parseInt(p, 10));
9114
- 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);
9115
9760
  } catch {
9116
9761
  return null;
9117
9762
  }
@@ -9323,17 +9968,15 @@ ORDER BY day DESC, cost DESC;`;
9323
9968
  } else if (id === "telegram") {
9324
9969
  try {
9325
9970
  const pid = parseInt(
9326
- fs
9327
- .readFileSync(
9328
- path.join(
9329
- os.homedir(),
9330
- ".crewswarm",
9331
- "logs",
9332
- "telegram-bridge.pid",
9333
- ),
9334
- "utf8",
9335
- )
9336
- .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(),
9337
9980
  10,
9338
9981
  );
9339
9982
  if (pid) process.kill(pid, "SIGTERM");
@@ -9341,17 +9984,15 @@ ORDER BY day DESC, cost DESC;`;
9341
9984
  } else if (id === "whatsapp") {
9342
9985
  try {
9343
9986
  const pid = parseInt(
9344
- fs
9345
- .readFileSync(
9346
- path.join(
9347
- os.homedir(),
9348
- ".crewswarm",
9349
- "logs",
9350
- "whatsapp-bridge.pid",
9351
- ),
9352
- "utf8",
9353
- )
9354
- .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(),
9355
9996
  10,
9356
9997
  );
9357
9998
  if (pid) process.kill(pid, "SIGTERM");
@@ -9367,8 +10008,8 @@ ORDER BY day DESC, cost DESC;`;
9367
10008
  let killed = false;
9368
10009
 
9369
10010
  try {
9370
- if (fs.existsSync(pidFile)) {
9371
- const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
10011
+ if (await exists(pidFile)) {
10012
+ const pid = parseInt((await fs.promises.readFile(pidFile, "utf8")).trim(), 10);
9372
10013
  if (pid && !isNaN(pid)) {
9373
10014
  try {
9374
10015
  process.kill(pid, 0); // Check if process exists
@@ -9380,7 +10021,7 @@ ORDER BY day DESC, cost DESC;`;
9380
10021
  );
9381
10022
  } catch (e) {
9382
10023
  // Process doesn't exist, clean up stale PID file
9383
- fs.writeFileSync(pidFile, "");
10024
+ await fs.promises.writeFile(pidFile, "");
9384
10025
  }
9385
10026
  }
9386
10027
  }
@@ -9622,20 +10263,7 @@ ORDER BY day DESC, cost DESC;`;
9622
10263
  .filter(Boolean)
9623
10264
  .join("\n");
9624
10265
 
9625
- const token = (() => {
9626
- try {
9627
- return (
9628
- JSON.parse(
9629
- fs.readFileSync(
9630
- path.join(os.homedir(), ".crewswarm", "crewswarm.json"),
9631
- "utf8",
9632
- ),
9633
- )?.rt?.authToken || ""
9634
- );
9635
- } catch {
9636
- return "";
9637
- }
9638
- })();
10266
+ const token = resolveCrewLeadAuthToken();
9639
10267
  try {
9640
10268
  const upstream = await fetch(
9641
10269
  "http://127.0.0.1:5010/api/engine-passthrough",
@@ -9751,13 +10379,11 @@ ORDER BY day DESC, cost DESC;`;
9751
10379
  const userDir = path.join(os.homedir(), ".crewswarm", "engines");
9752
10380
  const enginesMap = {};
9753
10381
  for (const dir of [bundledDir, userDir]) {
9754
- if (!fs.existsSync(dir)) continue;
9755
- for (const f of fs
9756
- .readdirSync(dir)
9757
- .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"))) {
9758
10384
  try {
9759
10385
  const eng = JSON.parse(
9760
- fs.readFileSync(path.join(dir, f), "utf8"),
10386
+ await fs.promises.readFile(path.join(dir, f), "utf8"),
9761
10387
  );
9762
10388
  if (eng.id)
9763
10389
  enginesMap[eng.id] = {
@@ -9771,11 +10397,11 @@ ORDER BY day DESC, cost DESC;`;
9771
10397
  const configPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
9772
10398
  let envVars = {};
9773
10399
  try {
9774
- const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
10400
+ const cfg = JSON.parse(await fs.promises.readFile(configPath, "utf8"));
9775
10401
  envVars = cfg.env || {};
9776
10402
  } catch { }
9777
10403
 
9778
- const engines = Object.values(enginesMap).map((eng) => {
10404
+ const engines = await Promise.all(Object.values(enginesMap).map(async (eng) => {
9779
10405
  let installed = false;
9780
10406
  try {
9781
10407
  const bin = eng.bin || eng.id;
@@ -9784,7 +10410,7 @@ ORDER BY day DESC, cost DESC;`;
9784
10410
  } catch {
9785
10411
  if (eng.binAlternate) {
9786
10412
  const alt = eng.binAlternate.replace(/^~/, os.homedir());
9787
- installed = fs.existsSync(alt);
10413
+ installed = await exists(alt);
9788
10414
  }
9789
10415
  }
9790
10416
  const missingEnv = eng.requiresAuth
@@ -9805,7 +10431,7 @@ ORDER BY day DESC, cost DESC;`;
9805
10431
  ready: installed && missingEnv.length === 0,
9806
10432
  enabled,
9807
10433
  };
9808
- });
10434
+ }));
9809
10435
  res.writeHead(200, { "content-type": "application/json" });
9810
10436
  res.end(JSON.stringify({ engines }));
9811
10437
  } catch (err) {
@@ -9832,10 +10458,10 @@ ORDER BY day DESC, cost DESC;`;
9832
10458
  if (!eng.id || !eng.label)
9833
10459
  throw new Error("Engine descriptor must have id and label");
9834
10460
  const engDir = path.join(os.homedir(), ".crewswarm", "engines");
9835
- if (!fs.existsSync(engDir)) fs.mkdirSync(engDir, { recursive: true });
10461
+ if (!await exists(engDir)) await fs.promises.mkdir(engDir, { recursive: true });
9836
10462
  const outPath = path.join(engDir, `${eng.id}.json`);
9837
10463
  if (!outPath.startsWith(engDir)) throw new Error("Invalid engine id");
9838
- fs.writeFileSync(outPath, JSON.stringify(eng, null, 2), "utf8");
10464
+ await fs.promises.writeFile(outPath, JSON.stringify(eng, null, 2), "utf8");
9839
10465
  res.writeHead(200, { "content-type": "application/json" });
9840
10466
  res.end(JSON.stringify({ ok: true, id: eng.id, label: eng.label }));
9841
10467
  } catch (err) {
@@ -9858,8 +10484,8 @@ ORDER BY day DESC, cost DESC;`;
9858
10484
  let engineDef = null;
9859
10485
  for (const dir of [bundledDir, userDir]) {
9860
10486
  const p = path.join(dir, `${engineId}.json`);
9861
- if (fs.existsSync(p)) {
9862
- engineDef = JSON.parse(fs.readFileSync(p, "utf8"));
10487
+ if (await exists(p)) {
10488
+ engineDef = JSON.parse(await fs.promises.readFile(p, "utf8"));
9863
10489
  break;
9864
10490
  }
9865
10491
  }
@@ -9870,12 +10496,12 @@ ORDER BY day DESC, cost DESC;`;
9870
10496
 
9871
10497
  const envVarName = engineDef.envToggle;
9872
10498
  const configPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
9873
- const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
10499
+ const cfg = JSON.parse(await fs.promises.readFile(configPath, "utf8"));
9874
10500
 
9875
10501
  if (!cfg.env) cfg.env = {};
9876
10502
  cfg.env[envVarName] = enabled ? "1" : "off";
9877
10503
 
9878
- fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf8");
10504
+ await fs.promises.writeFile(configPath, JSON.stringify(cfg, null, 2), "utf8");
9879
10505
 
9880
10506
  res.writeHead(200, { "content-type": "application/json" });
9881
10507
  res.end(JSON.stringify({ ok: true, engineId, enabled, envVar: envVarName }));
@@ -9896,7 +10522,7 @@ ORDER BY day DESC, cost DESC;`;
9896
10522
  return;
9897
10523
  }
9898
10524
  try {
9899
- fs.unlinkSync(target);
10525
+ await fs.promises.unlink(target);
9900
10526
  } catch { }
9901
10527
  res.writeHead(200, { "content-type": "application/json" });
9902
10528
  res.end(JSON.stringify({ ok: true }));
@@ -10052,13 +10678,13 @@ ORDER BY day DESC, cost DESC;`;
10052
10678
  ".crewswarm",
10053
10679
  "skills",
10054
10680
  );
10055
- if (!fs.existsSync(skillsDir))
10056
- fs.mkdirSync(skillsDir, { recursive: true });
10681
+ if (!await exists(skillsDir))
10682
+ await fs.promises.mkdir(skillsDir, { recursive: true });
10057
10683
  const outPath = path.join(skillsDir, `${skillName}.json`);
10058
10684
  // Final path traversal guard
10059
10685
  if (!outPath.startsWith(skillsDir))
10060
10686
  throw new Error("Invalid skill name");
10061
- fs.writeFileSync(outPath, JSON.stringify(skill, null, 2), "utf8");
10687
+ await fs.promises.writeFile(outPath, JSON.stringify(skill, null, 2), "utf8");
10062
10688
 
10063
10689
  res.writeHead(200, { "content-type": "application/json" });
10064
10690
  res.end(