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.
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js +1 -0
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js +1 -0
- package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
- package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
- package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js +2 -0
- package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
- package/apps/dashboard/dist/assets/{index-CF0aJRtC.css → index-D-sRshvg.css} +1 -1
- package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
- package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
- package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js +1 -0
- package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js +1 -0
- package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js +1 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +1 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
- package/apps/dashboard/dist/index.html +56 -7
- package/apps/dashboard/dist/index.html.br +0 -0
- package/apps/dashboard/dist/index.html.gz +0 -0
- package/apps/vibe/server.mjs +98 -53
- package/lib/bridges/cli-executor.mjs +1 -1
- package/lib/browser/passthrough-stderr.js +1 -0
- package/lib/chat/project-messages.mjs +3 -5
- package/lib/cli-process-tracker.mjs +3 -2
- package/lib/contacts/identity-linker.mjs +1 -0
- package/lib/crew-judge/judge.mjs +19 -18
- package/lib/crew-lead/agent-manager.mjs +1 -1
- package/lib/crew-lead/background.mjs +14 -1
- package/lib/crew-lead/chat-handler.mjs +4 -1
- package/lib/crew-lead/http-server.mjs +73 -65
- package/lib/crew-lead/prompts.mjs +7 -1
- package/lib/crew-lead/tools.mjs +3 -2
- package/lib/crew-lead/wave-dispatcher.mjs +4 -2
- package/lib/engines/crew-cli.mjs +1 -1
- package/lib/engines/engine-registry.mjs +11 -9
- package/lib/engines/runners.mjs +23 -2
- package/lib/gemini-cli-passthrough-noise.mjs +1 -1
- package/lib/integrations/code-search.mjs +4 -3
- package/lib/memory/shared-adapter.mjs +23 -10
- package/lib/pipeline/manager.mjs +2 -1
- package/lib/runtime/config.mjs +1 -1
- package/lib/runtime/spending.mjs +2 -1
- package/package.json +17 -9
- package/scripts/dashboard-validation.mjs +2 -0
- package/scripts/dashboard.mjs +1110 -484
- package/scripts/generate-openapi.mjs +683 -277
- package/scripts/restart-service.sh +12 -9
- package/apps/dashboard/dist/assets/chat-core-3KirthZA.js +0 -1
- package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js +0 -1
- package/apps/dashboard/dist/assets/index-GSWxxEPO.js +0 -2
- package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js +0 -1
- package/apps/dashboard/dist/assets/tab-settings-tab-BselH1c0.js +0 -1
- package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js +0 -1
package/scripts/dashboard.mjs
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1471
|
-
|
|
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.
|
|
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 (
|
|
1580
|
-
const years = fs.
|
|
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.
|
|
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.
|
|
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
|
|
1590
|
-
|
|
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.
|
|
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.
|
|
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 =
|
|
1652
|
-
? fs.
|
|
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
|
|
1658
|
-
|
|
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.
|
|
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 (
|
|
1691
|
-
const projects = fs.
|
|
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 (
|
|
2144
|
+
if (await exists(sessionFile)) {
|
|
1695
2145
|
const messages = [];
|
|
1696
|
-
const lines = fs.
|
|
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.
|
|
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 (
|
|
1733
|
-
const engines = fs.
|
|
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.
|
|
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 (!
|
|
1740
|
-
const years = fs.
|
|
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.
|
|
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.
|
|
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
|
|
1750
|
-
|
|
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.
|
|
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 (
|
|
1835
|
-
sessions = JSON.parse(fs.
|
|
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 (
|
|
1860
|
-
sessions = JSON.parse(fs.
|
|
2308
|
+
if (await exists(sessionFile)) {
|
|
2309
|
+
sessions = JSON.parse(await fs.promises.readFile(sessionFile, "utf8"));
|
|
1861
2310
|
}
|
|
1862
2311
|
delete sessions[key];
|
|
1863
|
-
fs.
|
|
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.
|
|
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
|
-
.
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (!
|
|
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
|
-
.
|
|
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 (
|
|
2602
|
-
const reg = JSON.parse(fs.
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 (
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
3806
|
+
if (!(await exists(outputDir))) await fs.promises.mkdir(outputDir, { recursive: true });
|
|
3368
3807
|
const roadmapFile = path.join(outputDir, "ROADMAP.md");
|
|
3369
|
-
if (!
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
3880
|
+
await fs.promises.writeFile(stopPath, new Date().toISOString());
|
|
3443
3881
|
} catch { }
|
|
3444
3882
|
for (const cleanupPath of [pidPath, stopPath, logPath]) {
|
|
3445
|
-
if (!
|
|
3883
|
+
if (!(await exists(cleanupPath))) continue;
|
|
3446
3884
|
try {
|
|
3447
|
-
|
|
3885
|
+
await fs.promises.unlink(cleanupPath);
|
|
3448
3886
|
} catch { }
|
|
3449
3887
|
}
|
|
3450
3888
|
const projectMessageDir = path.join(CFG_DIR, "project-messages", projectId);
|
|
3451
|
-
if (
|
|
3889
|
+
if (await exists(projectMessageDir)) {
|
|
3452
3890
|
try {
|
|
3453
|
-
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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 (
|
|
4033
|
+
if (await exists(stopFilePath)) {
|
|
3599
4034
|
try {
|
|
3600
|
-
|
|
4035
|
+
await fs.promises.unlink(stopFilePath);
|
|
3601
4036
|
} catch { }
|
|
3602
4037
|
}
|
|
3603
4038
|
const logsDir = path.join(CREWSWARM_DIR, "orchestrator-logs");
|
|
3604
|
-
if (!
|
|
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 (!
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
4290
|
+
await fs.promises.mkdir(csDir, { recursive: true });
|
|
3854
4291
|
let cfg = {};
|
|
3855
4292
|
try {
|
|
3856
|
-
cfg = JSON.parse(fs.
|
|
4293
|
+
cfg = JSON.parse(await fs.promises.readFile(csConfigPath, "utf8"));
|
|
3857
4294
|
} catch { }
|
|
3858
4295
|
cfg.rt = { ...(cfg.rt || {}), authToken: token };
|
|
3859
|
-
fs.
|
|
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 {
|
|
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
|
|
3872
|
-
locked =
|
|
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
|
|
3877
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
4400
|
+
await fs.promises.mkdir(cfgDir, { recursive: true });
|
|
3955
4401
|
let cfg = {};
|
|
3956
4402
|
try {
|
|
3957
|
-
cfg = JSON.parse(fs.
|
|
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.
|
|
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.
|
|
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.
|
|
4040
|
-
fs.
|
|
4484
|
+
async function writeCSSwarmConfig(c) {
|
|
4485
|
+
await fs.promises.mkdir(csDir, { recursive: true });
|
|
4486
|
+
await fs.promises.writeFile(csSwarmConfig, JSON.stringify(c, null, 2));
|
|
4041
4487
|
}
|
|
4042
|
-
function readOCConfig() {
|
|
4488
|
+
async function readOCConfig() {
|
|
4043
4489
|
try {
|
|
4044
|
-
return JSON.parse(fs.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
4815
|
-
? fs.
|
|
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.
|
|
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
|
-
|
|
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
|
|
5020
|
-
|
|
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.
|
|
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 =
|
|
5155
|
-
? JSON.parse(fs.
|
|
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.
|
|
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.
|
|
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 =
|
|
5588
|
-
? JSON.parse(fs.
|
|
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: {
|
|
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 =
|
|
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.
|
|
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 (!
|
|
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 (!
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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.
|
|
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
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
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
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
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
|
-
) ||
|
|
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(
|
|
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
|
-
|
|
8812
|
-
|
|
8813
|
-
const
|
|
9463
|
+
|
|
9464
|
+
// All PID lookups are instant — pure JS regex against ps snapshot
|
|
9465
|
+
const rtPid = findPid("opencrew-rt-daemon");
|
|
9466
|
+
const crewLeadPid = findPid("crew-lead\\.mjs");
|
|
9467
|
+
const gwPid = findPid("openclaw-gateway");
|
|
9468
|
+
const ocPid = findPid("\\.opencode serve") || findPid("opencode serve") || findPid("bin/\\.opencode");
|
|
9469
|
+
const mcpPid = findPid("mcp-server\\.mjs");
|
|
9470
|
+
const studioPid = findPid("apps/vibe/server\\.mjs") || findPid("npm.*studio:start");
|
|
9471
|
+
const watchPid = findPid("watch-server\\.mjs");
|
|
9472
|
+
|
|
9473
|
+
// commandExists — fs.existsSync only, no shell spawn
|
|
9474
|
+
const codexInstalled = await commandExistsFast(process.env.CODEX_CLI_BIN || "codex");
|
|
9475
|
+
const claudeInstalled = await commandExistsFast(process.env.CLAUDE_CODE_BIN || "claude");
|
|
9476
|
+
const cursorInstalled = (await commandExistsFast(
|
|
9477
|
+
process.env.CURSOR_CLI_BIN || path.join(os.homedir(), ".local", "bin", "agent"),
|
|
9478
|
+
[path.join(os.homedir(), ".local", "bin", "agent")],
|
|
9479
|
+
)) || (await commandExistsFast("agent", [path.join(os.homedir(), ".local", "bin", "agent")]));
|
|
9480
|
+
const geminiInstalled = await commandExistsFast(process.env.GEMINI_CLI_BIN || "gemini");
|
|
9481
|
+
const crewCliInstalled = (await commandExistsFast("crew", [
|
|
9482
|
+
path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js"),
|
|
9483
|
+
])) || (await exists(path.join(CREWSWARM_DIR, "crew-cli", "dist", "index.js")));
|
|
8814
9484
|
const oclawPaired =
|
|
8815
|
-
|
|
9485
|
+
(await exists(
|
|
8816
9486
|
path.join(os.homedir(), ".openclaw", "devices", "paired.json"),
|
|
8817
|
-
) ||
|
|
8818
|
-
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
9529
|
+
agentPids = findAllPids("gateway-bridge\\.mjs --rt-daemon");
|
|
8881
9530
|
} catch {
|
|
8882
|
-
// RT not reachable — fall back to
|
|
8883
|
-
agentsOnline = countProcs("gateway-bridge
|
|
8884
|
-
agentPids =
|
|
9531
|
+
// RT not reachable — fall back to ps snapshot for count, config for names
|
|
9532
|
+
agentsOnline = countProcs("gateway-bridge\\.mjs --rt-daemon");
|
|
9533
|
+
agentPids = findAllPids("gateway-bridge\\.mjs --rt-daemon");
|
|
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
|
|
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 =
|
|
9110
|
-
|
|
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
|
-
.
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
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
|
-
.
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
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 (
|
|
9371
|
-
const pid = parseInt(fs.
|
|
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.
|
|
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 (!
|
|
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.
|
|
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.
|
|
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 =
|
|
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 (!
|
|
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.
|
|
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 (
|
|
9862
|
-
engineDef = JSON.parse(fs.
|
|
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.
|
|
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.
|
|
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.
|
|
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 (!
|
|
10056
|
-
fs.
|
|
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.
|
|
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(
|