crewswarm 0.9.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -7
- package/apps/dashboard/dist/assets/{index-D-sRshvg.css → index-C5-vlIwl.css} +1 -1
- package/apps/dashboard/dist/assets/index-CSooN9fi.js +2 -0
- package/apps/dashboard/dist/assets/index-CSooN9fi.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-spending-tab-DcXD5TQY.js +1 -0
- package/apps/dashboard/dist/assets/tab-spending-tab-DcXD5TQY.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-testing-tab-Ea5K-rsb.js +1 -0
- package/apps/dashboard/dist/index.html +83 -7
- package/apps/dashboard/dist/index.html.br +0 -0
- package/contrib/openclaw-plugin/index.ts +20 -11
- package/lib/autoharness/index.mjs +151 -1
- package/lib/chat/history.mjs +1 -1
- package/lib/contacts/identity-linker.mjs +24 -3
- package/lib/contacts/index.mjs +2 -1
- package/lib/crew-lead/chat-handler.mjs +56 -33
- package/lib/crew-lead/llm-caller.mjs +71 -14
- package/lib/crew-lead/prompts.mjs +4 -2
- package/lib/engines/rt-envelope.mjs +4 -1
- package/package.json +5 -3
- package/scripts/dashboard.mjs +216 -25
- package/scripts/health-check.mjs +70 -28
- package/scripts/restart-all-from-repo.sh +25 -21
- package/scripts/start.mjs +35 -15
- package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -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 +0 -2
- package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
- 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.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.br +0 -0
- package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
- package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js +0 -1
- 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 +0 -1
- 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.gz +0 -0
|
@@ -385,21 +385,78 @@ function _recordCrewLeadTokens(modelId, providerKey, usage) {
|
|
|
385
385
|
fs.writeFileSync(TOKEN_USAGE_FILE, JSON.stringify(data, null, 2));
|
|
386
386
|
} catch {}
|
|
387
387
|
|
|
388
|
-
// Calculate cost with cache discount -
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
388
|
+
// Calculate cost with cache discount — per-model pricing (matches dashboard usage-tab)
|
|
389
|
+
// Keys matched via model.includes(key); more specific keys must come first
|
|
390
|
+
const MODEL_PRICING = {
|
|
391
|
+
// xAI Grok
|
|
392
|
+
'grok-4-1-fast': { input: 0.20, output: 0.50 },
|
|
393
|
+
'grok-4-fast': { input: 0.20, output: 0.50 },
|
|
394
|
+
'grok-4': { input: 3.00, output: 15.00 },
|
|
395
|
+
'grok-3-mini': { input: 0.30, output: 0.50 },
|
|
396
|
+
'grok-3': { input: 3.00, output: 15.00 },
|
|
397
|
+
'grok-code-fast': { input: 0.20, output: 1.50 },
|
|
398
|
+
'grok-beta': { input: 5.00, output: 15.00 },
|
|
399
|
+
// OpenAI
|
|
400
|
+
'gpt-5.3-codex': { input: 2.50, output: 20.00 },
|
|
401
|
+
'gpt-5.2-codex': { input: 1.75, output: 14.00 },
|
|
402
|
+
'gpt-5.2': { input: 1.75, output: 14.00 },
|
|
403
|
+
'gpt-5.1-codex-max':{ input: 2.50, output: 20.00 },
|
|
404
|
+
'gpt-5.1-codex-mini':{ input: 0.25, output: 2.00 },
|
|
405
|
+
'gpt-5.1-codex': { input: 1.25, output: 10.00 },
|
|
406
|
+
'gpt-5.1': { input: 1.25, output: 10.00 },
|
|
407
|
+
'gpt-5-codex': { input: 1.25, output: 10.00 },
|
|
408
|
+
'gpt-5-nano': { input: 0.15, output: 0.60 },
|
|
409
|
+
'gpt-5': { input: 1.25, output: 10.00 },
|
|
410
|
+
'codex-mini': { input: 0.25, output: 2.00 },
|
|
411
|
+
'gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
412
|
+
'gpt-4o': { input: 2.50, output: 10.00 },
|
|
413
|
+
'gpt-4': { input: 30.0, output: 60.00 },
|
|
414
|
+
// DeepSeek
|
|
415
|
+
'deepseek-reasoner':{ input: 0.70, output: 2.50 },
|
|
416
|
+
'deepseek-chat': { input: 0.27, output: 1.10 },
|
|
417
|
+
// Mistral
|
|
418
|
+
'mistral-large': { input: 0.50, output: 1.50 },
|
|
419
|
+
'mistral-small': { input: 0.10, output: 0.30 },
|
|
420
|
+
// Google Gemini
|
|
421
|
+
'gemini-3.1-pro': { input: 2.50, output: 15.00 },
|
|
422
|
+
'gemini-3.1-flash': { input: 0.075, output: 0.30 },
|
|
423
|
+
'gemini-3-pro': { input: 2.50, output: 15.00 },
|
|
424
|
+
'gemini-3-flash': { input: 0.075, output: 0.30 },
|
|
425
|
+
'gemini-2.5-pro': { input: 1.25, output: 10.00 },
|
|
426
|
+
'gemini-2.5-flash-lite': { input: 0.04, output: 0.15 },
|
|
427
|
+
'gemini-2.5-flash': { input: 0.075, output: 0.30 },
|
|
428
|
+
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
429
|
+
// Anthropic
|
|
430
|
+
'claude-opus-4': { input: 15.0, output: 75.00 },
|
|
431
|
+
'claude-sonnet-4': { input: 3.00, output: 15.00 },
|
|
432
|
+
'claude-haiku-4': { input: 0.80, output: 4.00 },
|
|
433
|
+
// Groq-hosted
|
|
434
|
+
'llama-3.3': { input: 0.05, output: 0.05 },
|
|
435
|
+
'llama-3.1': { input: 0.05, output: 0.05 },
|
|
436
|
+
'gemma': { input: 0.05, output: 0.05 },
|
|
437
|
+
// Cerebras
|
|
438
|
+
'cerebras': { input: 0.10, output: 0.10 },
|
|
439
|
+
// Perplexity
|
|
440
|
+
'perplexity': { input: 1.00, output: 1.00 },
|
|
400
441
|
};
|
|
401
|
-
|
|
402
|
-
const
|
|
442
|
+
// Provider-level fallback for models not matched above
|
|
443
|
+
const PROVIDER_FALLBACK = {
|
|
444
|
+
groq: { input: 0.05, output: 0.05 },
|
|
445
|
+
google: { input: 0.075, output: 0.30 },
|
|
446
|
+
xai: { input: 0.20, output: 0.50 },
|
|
447
|
+
deepseek: { input: 0.27, output: 1.10 },
|
|
448
|
+
anthropic: { input: 3.00, output: 15.00 },
|
|
449
|
+
openai: { input: 1.25, output: 10.00 },
|
|
450
|
+
mistral: { input: 0.50, output: 1.50 },
|
|
451
|
+
nvidia: { input: 1.00, output: 1.00 },
|
|
452
|
+
cerebras: { input: 0.10, output: 0.10 },
|
|
453
|
+
perplexity: { input: 1.00, output: 1.00 },
|
|
454
|
+
};
|
|
455
|
+
const modelLower = modelId.toLowerCase();
|
|
456
|
+
const matchedKey = Object.keys(MODEL_PRICING).find(k => modelLower.includes(k));
|
|
457
|
+
const pricing = matchedKey
|
|
458
|
+
? { ...MODEL_PRICING[matchedKey], cached: MODEL_PRICING[matchedKey].input * 0.5 }
|
|
459
|
+
: { ...(PROVIDER_FALLBACK[providerKey] || { input: 1.0, output: 1.0 }), cached: (PROVIDER_FALLBACK[providerKey]?.input || 1.0) * 0.5 };
|
|
403
460
|
const uncachedInput = Math.max(0, p - cached);
|
|
404
461
|
const inputCost = (uncachedInput / 1_000_000) * pricing.input;
|
|
405
462
|
const cachedCost = (cached / 1_000_000) * pricing.cached;
|
|
@@ -244,6 +244,7 @@ export function buildSystemPrompt(cfg) {
|
|
|
244
244
|
"",
|
|
245
245
|
"ALL MARKERS: @@READ_FILE, @@WRITE_FILE...@@END_FILE, @@MKDIR, @@RUN_CMD, @@WEB_SEARCH, @@WEB_FETCH, @@SEARCH_HISTORY, @@TELEGRAM, @@WHATSAPP, @@DISPATCH, @@PIPELINE, @@PROJECT, @@PROMPT, @@TOOLS, @@GLOBALRULE, @@SERVICE, @@BRAIN, @@MEMORY, @@SKILL, @@CREATE_AGENT, @@REMOVE_AGENT, @@DEFINE_SKILL, @@DEFINE_WORKFLOW, @@STOP, @@KILL.",
|
|
246
246
|
'Self-teaching: if you make a tool mistake, emit @@PROMPT {"agent":"crew-lead","append":"learned: ..."} to remember it.',
|
|
247
|
+
'CRITICAL: You CANNOT use "set" on your own prompt (crew-lead). Only "append" is allowed for yourself. "set" will be blocked to prevent accidental self-wipe.',
|
|
247
248
|
"",
|
|
248
249
|
|
|
249
250
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -281,7 +282,7 @@ export function buildSystemPrompt(cfg) {
|
|
|
281
282
|
"",
|
|
282
283
|
"TEAM STATUS: You are the secretary. When asked about team status, answer immediately from health snapshot. Never say 'check the dashboard'.",
|
|
283
284
|
"Only state status/model/runtime facts verified in this turn from snapshot or tool output.",
|
|
284
|
-
"FULL ROSTER REQUESTS: If user asks for 'all agents', 'full roster', 'whole crew' — list EVERY agent from the health snapshot.
|
|
285
|
+
"FULL ROSTER REQUESTS: If user asks for 'all agents', 'full roster', 'whole crew' — list EVERY agent from the health snapshot.",
|
|
285
286
|
"",
|
|
286
287
|
|
|
287
288
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -364,6 +365,7 @@ export function buildSystemPrompt(cfg) {
|
|
|
364
365
|
"",
|
|
365
366
|
"- Never fabricate file contents, tool results, or system health output. Emit the tag; report ACTUAL results.",
|
|
366
367
|
"- Never describe what a command 'would' show. Run it.",
|
|
368
|
+
"- If the user asks you to run a command, you MUST emit @@RUN_CMD on its own line. Do NOT skip the tool and write fake output. If you think you already know the answer, run the command ANYWAY — your job is to verify, not guess.",
|
|
367
369
|
"- Never fabricate dispatch history. Only quote exact @@DISPATCH lines visible in conversation. If you don't see it, say so.",
|
|
368
370
|
"- Never invent URLs, gists, or 'prior search results'. Only cite what's in conversation history.",
|
|
369
371
|
"- If the user says you lied or made something up, accept it. Don't double down.",
|
|
@@ -376,7 +378,7 @@ export function buildSystemPrompt(cfg) {
|
|
|
376
378
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
377
379
|
"## § 9 — STYLE",
|
|
378
380
|
"",
|
|
379
|
-
"-
|
|
381
|
+
"- Be concise. No filler. Never cut yourself off mid-sentence — finish your thought.",
|
|
380
382
|
"- When user throws shade, roast back. Match their energy. Sharp, sarcastic, no cap.",
|
|
381
383
|
"- Every @@command you reference MUST appear as the actual @@ line in your reply. Prose descriptions execute nothing.",
|
|
382
384
|
].join("\n");
|
|
@@ -1068,11 +1068,14 @@ export async function handleRealtimeEnvelope(envelope, client, bridge) {
|
|
|
1068
1068
|
/and nothing else\b/i.test(prompt || "");
|
|
1069
1069
|
|
|
1070
1070
|
// Append original task spec for self-verification (LangChain pattern)
|
|
1071
|
-
// Skip strict-output prompts
|
|
1071
|
+
// Skip strict-output prompts and trivial/empty replies where the reminder adds noise.
|
|
1072
|
+
const replyStripped = (reply || "").replace(/\s+/g, " ").trim();
|
|
1073
|
+
const isTrivialReply = replyStripped.length < 50 || /^\(completed\)$/i.test(replyStripped);
|
|
1072
1074
|
if (
|
|
1073
1075
|
reply &&
|
|
1074
1076
|
prompt &&
|
|
1075
1077
|
!requestsExactReply &&
|
|
1078
|
+
!isTrivialReply &&
|
|
1076
1079
|
!reply.includes("[ORIGINAL TASK]")
|
|
1077
1080
|
) {
|
|
1078
1081
|
const taskSpecReminder = `\n\n---\n**[ORIGINAL TASK]:**\n${prompt.slice(0, 500)}${prompt.length > 500 ? "..." : ""}\n\nDoes your implementation address ALL requirements above?`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crewswarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Local-first multi-agent orchestration platform — coordinate AI coding agents, LLMs, and tools from a single dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -133,10 +133,12 @@
|
|
|
133
133
|
"release:check": "bash scripts/release-check.sh",
|
|
134
134
|
"test:report": "node scripts/test-report-summary.mjs",
|
|
135
135
|
"test:rerun": "node scripts/test-rerun.mjs",
|
|
136
|
-
"test:stale": "node scripts/test-rerun.mjs --stale"
|
|
136
|
+
"test:stale": "node scripts/test-rerun.mjs --stale",
|
|
137
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
137
138
|
},
|
|
138
139
|
"devDependencies": {
|
|
139
140
|
"@playwright/test": "^1.58.2",
|
|
140
|
-
"puppeteer-core": "^24.40.0"
|
|
141
|
+
"puppeteer-core": "^24.40.0",
|
|
142
|
+
"typescript": "^5.9.3"
|
|
141
143
|
}
|
|
142
144
|
}
|
package/scripts/dashboard.mjs
CHANGED
|
@@ -132,9 +132,21 @@ const OAUTH_TOKEN_TTL_MS = 25 * 60 * 1000; // 25 minutes (tokens typically expir
|
|
|
132
132
|
// Capture execFileSync at module load for sync token refresh
|
|
133
133
|
const { execFileSync: _oauthExecFileSync } = await import("node:child_process");
|
|
134
134
|
|
|
135
|
+
function getKeychainAccountCandidates() {
|
|
136
|
+
const candidates = [
|
|
137
|
+
process.env.CLAUDE_CODE_ACCOUNT,
|
|
138
|
+
process.env.USER,
|
|
139
|
+
process.env.LOGNAME,
|
|
140
|
+
];
|
|
141
|
+
try {
|
|
142
|
+
candidates.unshift(os.userInfo().username);
|
|
143
|
+
} catch { /* os user lookup can fail in restricted environments */ }
|
|
144
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
function refreshAnthropicOAuthToken() {
|
|
136
148
|
try {
|
|
137
|
-
for (const acct of
|
|
149
|
+
for (const acct of getKeychainAccountCandidates()) {
|
|
138
150
|
try {
|
|
139
151
|
const raw = _oauthExecFileSync("security", [
|
|
140
152
|
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
@@ -152,7 +164,7 @@ function refreshAnthropicOAuthToken() {
|
|
|
152
164
|
|
|
153
165
|
// Initial load at startup
|
|
154
166
|
try {
|
|
155
|
-
for (const acct of
|
|
167
|
+
for (const acct of getKeychainAccountCandidates()) {
|
|
156
168
|
try {
|
|
157
169
|
const raw = _oauthExecFileSync("security", [
|
|
158
170
|
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
@@ -1581,16 +1593,34 @@ const server = http.createServer(async (req, res) => {
|
|
|
1581
1593
|
// by inspecting run.json test_command
|
|
1582
1594
|
async function detectSuite(runDir) {
|
|
1583
1595
|
try {
|
|
1584
|
-
|
|
1585
|
-
|
|
1596
|
+
// Try run.json first
|
|
1597
|
+
let cmd = "";
|
|
1598
|
+
try {
|
|
1599
|
+
const r = JSON.parse(await fs.promises.readFile(path.join(runDir, "run.json"), "utf8"));
|
|
1600
|
+
cmd = r.test_command || "";
|
|
1601
|
+
} catch {
|
|
1602
|
+
// No run.json — try to infer from directory contents
|
|
1603
|
+
try {
|
|
1604
|
+
const ents = await fs.promises.readdir(runDir);
|
|
1605
|
+
const testDirs = ents.filter(e => !e.endsWith(".json") && !e.startsWith("."));
|
|
1606
|
+
// Check first test dir name for suite hint
|
|
1607
|
+
const first = testDirs[0] || "";
|
|
1608
|
+
if (first.includes("test-e2e-")) return "e2e";
|
|
1609
|
+
if (first.includes("test-integration-")) return "integration";
|
|
1610
|
+
if (first.includes("test-unit-")) return "unit";
|
|
1611
|
+
if (first.includes("spec-")) return "playwright";
|
|
1612
|
+
} catch {}
|
|
1613
|
+
return "unknown";
|
|
1614
|
+
}
|
|
1615
|
+
if (cmd.includes("test:playwright") || cmd.includes("playwright test")) return "playwright";
|
|
1586
1616
|
if (cmd.includes("test/e2e/") || cmd.includes("test:e2e")) return "e2e";
|
|
1587
1617
|
if (cmd.includes("test/integration/")) return "integration";
|
|
1588
1618
|
if (cmd.includes("test/unit/")) {
|
|
1589
|
-
// If it has ONLY unit tests, it's a unit run; if mixed, it's "all"
|
|
1590
1619
|
if (cmd.includes("test/integration/") || cmd.includes("test/e2e/")) return "all";
|
|
1591
1620
|
return "unit";
|
|
1592
1621
|
}
|
|
1593
|
-
|
|
1622
|
+
if (cmd.includes("test:all")) return "all";
|
|
1623
|
+
if (cmd.includes("crew-cli") || cmd.includes("--prefix crew-cli")) return "crew-cli";
|
|
1594
1624
|
const fileCount = (cmd.match(/\.test\.mjs/g) || []).length;
|
|
1595
1625
|
if (fileCount > 100) return "all";
|
|
1596
1626
|
if (fileCount > 15) return "unit";
|
|
@@ -1699,7 +1729,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1699
1729
|
let count = 0;
|
|
1700
1730
|
try {
|
|
1701
1731
|
for (const ent of await fs.promises.readdir(dir, { withFileTypes: true })) {
|
|
1702
|
-
if (ent.isDirectory()) { count += countTestCallsRecursive(path.join(dir, ent.name)); continue; }
|
|
1732
|
+
if (ent.isDirectory()) { count += await countTestCallsRecursive(path.join(dir, ent.name)); continue; }
|
|
1703
1733
|
if (!ent.name.match(/\.test\./)) continue;
|
|
1704
1734
|
const src = await fs.promises.readFile(path.join(dir, ent.name), "utf8");
|
|
1705
1735
|
const matches = src.match(/^\s*(test|it)\s*\(/gm);
|
|
@@ -1708,13 +1738,22 @@ const server = http.createServer(async (req, res) => {
|
|
|
1708
1738
|
} catch {}
|
|
1709
1739
|
return count;
|
|
1710
1740
|
}
|
|
1741
|
+
const [unitTests, intTests, e2eTests, pwTests, cliTests1, cliTests2, rootTests] = await Promise.all([
|
|
1742
|
+
countTestCalls(path.join(testFileDir, "unit"), /\.test\.mjs$/),
|
|
1743
|
+
countTestCalls(path.join(testFileDir, "integration"), /\.test\.mjs$/),
|
|
1744
|
+
countTestCalls(path.join(testFileDir, "e2e"), /\.test\.mjs$/),
|
|
1745
|
+
countTestCalls(testsE2eDir, /\.spec\.js$/),
|
|
1746
|
+
countTestCallsRecursive(crewCliTestDir),
|
|
1747
|
+
countTestCallsRecursive(crewCliTestDir2),
|
|
1748
|
+
countTestCalls(testFileDir, /\.test\./),
|
|
1749
|
+
]);
|
|
1711
1750
|
const testCounts = {
|
|
1712
|
-
unit:
|
|
1713
|
-
integration:
|
|
1714
|
-
e2e:
|
|
1715
|
-
playwright:
|
|
1716
|
-
"crew-cli":
|
|
1717
|
-
root:
|
|
1751
|
+
unit: unitTests,
|
|
1752
|
+
integration: intTests,
|
|
1753
|
+
e2e: e2eTests,
|
|
1754
|
+
playwright: pwTests,
|
|
1755
|
+
"crew-cli": cliTests1 + cliTests2,
|
|
1756
|
+
root: rootTests,
|
|
1718
1757
|
};
|
|
1719
1758
|
res.writeHead(200, { "content-type": "application/json" });
|
|
1720
1759
|
res.end(JSON.stringify({
|
|
@@ -1745,22 +1784,38 @@ const server = http.createServer(async (req, res) => {
|
|
|
1745
1784
|
const s = JSON.parse(await fs.promises.readFile(summaryFile, "utf8"));
|
|
1746
1785
|
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
1786
|
} else {
|
|
1748
|
-
|
|
1787
|
+
let timestamp = null;
|
|
1788
|
+
try { const runMeta = JSON.parse(await fs.promises.readFile(path.join(runDir, "run.json"), "utf8")); timestamp = runMeta.timestamp; } catch {}
|
|
1789
|
+
if (!timestamp) { try { const stat = await fs.promises.stat(runDir); timestamp = stat.mtime.toISOString(); } catch {} }
|
|
1749
1790
|
const ents = await fs.promises.readdir(runDir);
|
|
1750
1791
|
const testDirs = ents.filter(e => !e.endsWith(".json") && !e.startsWith("."));
|
|
1751
1792
|
let failed = 0;
|
|
1752
1793
|
for (const td of testDirs) { if (await exists(path.join(runDir, td, "failure.json"))) failed++; }
|
|
1753
|
-
entry = { runId: d, timestamp
|
|
1794
|
+
entry = { runId: d, timestamp, status: failed > 0 ? "failed" : "passed", passed: testDirs.length - failed, failed, skipped: 0, total: testDirs.length, duration_ms: 0 };
|
|
1754
1795
|
}
|
|
1755
1796
|
// Detect suite from test_command
|
|
1756
1797
|
try {
|
|
1757
1798
|
const r = JSON.parse(await fs.promises.readFile(path.join(runDir, "run.json"), "utf8"));
|
|
1758
1799
|
const cmd = r.test_command || "";
|
|
1759
|
-
if (cmd.includes("test
|
|
1800
|
+
if (cmd.includes("test:playwright") || cmd.includes("playwright test")) entry.suite = "playwright";
|
|
1801
|
+
else if (cmd.includes("test/e2e/") || cmd.includes("test:e2e")) entry.suite = "e2e";
|
|
1760
1802
|
else if (cmd.includes("test/integration/")) entry.suite = "integration";
|
|
1803
|
+
else if (cmd.includes("test:all")) entry.suite = "all";
|
|
1804
|
+
else if (cmd.includes("crew-cli") || cmd.includes("--prefix crew-cli")) entry.suite = "crew-cli";
|
|
1761
1805
|
else if (cmd.includes("test/unit/") && !cmd.includes("test/integration/")) entry.suite = "unit";
|
|
1762
1806
|
else { const fc = (cmd.match(/\.test\.mjs/g) || []).length; entry.suite = fc > 100 ? "all" : fc > 15 ? "unit" : "unknown"; }
|
|
1763
|
-
} catch {
|
|
1807
|
+
} catch {
|
|
1808
|
+
// No run.json — try to infer from directory contents
|
|
1809
|
+
try {
|
|
1810
|
+
const ents = await fs.promises.readdir(runDir);
|
|
1811
|
+
const first = ents.find(e => !e.endsWith(".json") && !e.startsWith(".")) || "";
|
|
1812
|
+
if (first.includes("test-e2e-")) entry.suite = "e2e";
|
|
1813
|
+
else if (first.includes("test-integration-")) entry.suite = "integration";
|
|
1814
|
+
else if (first.includes("test-unit-")) entry.suite = "unit";
|
|
1815
|
+
else if (first.includes("spec-")) entry.suite = "playwright";
|
|
1816
|
+
else entry.suite = "unknown";
|
|
1817
|
+
} catch { entry.suite = "unknown"; }
|
|
1818
|
+
}
|
|
1764
1819
|
history.push(entry);
|
|
1765
1820
|
} catch { /* skip */ }
|
|
1766
1821
|
}
|
|
@@ -1846,8 +1901,42 @@ const server = http.createServer(async (req, res) => {
|
|
|
1846
1901
|
const { spawn } = await import("node:child_process");
|
|
1847
1902
|
const progressFile = path.join(CREWSWARM_DIR, "test-results", ".test-progress.json");
|
|
1848
1903
|
const outputFile = path.join(CREWSWARM_DIR, "test-results", ".test-output.log");
|
|
1904
|
+
// Count total files for this suite so progress can show X/Y
|
|
1905
|
+
let files_total = 0;
|
|
1906
|
+
try {
|
|
1907
|
+
const testFileDir = path.join(CREWSWARM_DIR, "test");
|
|
1908
|
+
const testsE2eDir = path.join(CREWSWARM_DIR, "tests", "e2e");
|
|
1909
|
+
const crewCliTestDir = path.join(CREWSWARM_DIR, "crew-cli", "tests");
|
|
1910
|
+
const crewCliTestDir2 = path.join(CREWSWARM_DIR, "crew-cli", "test");
|
|
1911
|
+
const suiteKey = suite.replace("test:", "");
|
|
1912
|
+
if (suiteKey === "unit") files_total = (await fs.promises.readdir(path.join(testFileDir, "unit"))).filter(f => f.endsWith(".test.mjs")).length;
|
|
1913
|
+
else if (suiteKey === "integration") files_total = (await fs.promises.readdir(path.join(testFileDir, "integration"))).filter(f => f.endsWith(".test.mjs")).length;
|
|
1914
|
+
else if (suiteKey === "e2e") files_total = (await fs.promises.readdir(path.join(testFileDir, "e2e"))).filter(f => f.endsWith(".test.mjs")).length;
|
|
1915
|
+
else if (suiteKey === "playwright") files_total = (await fs.promises.readdir(testsE2eDir)).filter(f => f.endsWith(".spec.js")).length;
|
|
1916
|
+
else if (suite === "test") { // crew-cli
|
|
1917
|
+
const count = async (d) => { try { return (await fs.promises.readdir(d)).filter(f => f.match(/\.test\./)).length; } catch { return 0; } };
|
|
1918
|
+
files_total = await count(path.join(crewCliTestDir, "unit")) + await count(crewCliTestDir) + await count(crewCliTestDir2);
|
|
1919
|
+
} else if (suiteKey === "all") {
|
|
1920
|
+
// Sum all suites
|
|
1921
|
+
const count = async (d, p) => { try { return (await fs.promises.readdir(d)).filter(f => f.match(p)).length; } catch { return 0; } };
|
|
1922
|
+
const countR = async (d) => { try { return (await fs.promises.readdir(d)).filter(f => f.match(/\.test\./)).length; } catch { return 0; } };
|
|
1923
|
+
files_total = await count(path.join(testFileDir, "unit"), /\.test\.mjs$/)
|
|
1924
|
+
+ await count(path.join(testFileDir, "integration"), /\.test\.mjs$/)
|
|
1925
|
+
+ await count(path.join(testFileDir, "e2e"), /\.test\.mjs$/)
|
|
1926
|
+
+ await count(testsE2eDir, /\.spec\.js$/)
|
|
1927
|
+
+ await countR(path.join(crewCliTestDir, "unit")) + await countR(crewCliTestDir) + await countR(crewCliTestDir2);
|
|
1928
|
+
}
|
|
1929
|
+
} catch {}
|
|
1849
1930
|
// Write initial progress
|
|
1850
|
-
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: singleFile || "" }));
|
|
1931
|
+
await fs.promises.writeFile(progressFile, JSON.stringify({ suite, running: true, pid: 0, started: Date.now(), passed: 0, failed: 0, skipped: 0, files_done: 0, files_total, current_file: singleFile || "" }));
|
|
1932
|
+
// Clean up any stale progress from a previous interrupted run
|
|
1933
|
+
const staleProgress = path.join(CREWSWARM_DIR, "test-results", ".test-progress.json");
|
|
1934
|
+
try {
|
|
1935
|
+
const prev = JSON.parse(fs.readFileSync(staleProgress, "utf8"));
|
|
1936
|
+
if (prev.running && prev.pid) {
|
|
1937
|
+
try { process.kill(prev.pid, 0); process.kill(prev.pid, "SIGTERM"); } catch { /* already dead */ }
|
|
1938
|
+
}
|
|
1939
|
+
} catch { /* no stale progress */ }
|
|
1851
1940
|
let child;
|
|
1852
1941
|
const outFd = fs.openSync(outputFile, "w");
|
|
1853
1942
|
if (singleFile) {
|
|
@@ -1874,7 +1963,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
1874
1963
|
current_file = line.trim();
|
|
1875
1964
|
}
|
|
1876
1965
|
}
|
|
1877
|
-
|
|
1966
|
+
const prev = JSON.parse(fs.readFileSync(progressFile, "utf8"));
|
|
1967
|
+
fs.writeFileSync(progressFile, JSON.stringify({ suite, running: true, pid: child.pid, started: prev.started, passed, failed, skipped, files_done, files_total: prev.files_total || 0, current_file }));
|
|
1878
1968
|
} catch { /* file may not exist yet */ }
|
|
1879
1969
|
}, 2000);
|
|
1880
1970
|
child.on("exit", (code) => {
|
|
@@ -1897,6 +1987,30 @@ const server = http.createServer(async (req, res) => {
|
|
|
1897
1987
|
});
|
|
1898
1988
|
return;
|
|
1899
1989
|
}
|
|
1990
|
+
if (url.pathname === "/api/tests/stop" && req.method === "POST") {
|
|
1991
|
+
const progressFile = path.join(CREWSWARM_DIR, "test-results", ".test-progress.json");
|
|
1992
|
+
try {
|
|
1993
|
+
const data = JSON.parse(await fs.promises.readFile(progressFile, "utf8"));
|
|
1994
|
+
if (data.running && data.pid) {
|
|
1995
|
+
try { process.kill(data.pid, "SIGTERM"); } catch { /* already dead */ }
|
|
1996
|
+
// Also kill child processes (node --test spawns sub-processes)
|
|
1997
|
+
try { process.kill(-data.pid, "SIGTERM"); } catch { /* no process group */ }
|
|
1998
|
+
data.running = false;
|
|
1999
|
+
data.stopped = true;
|
|
2000
|
+
data.finished = Date.now();
|
|
2001
|
+
await fs.promises.writeFile(progressFile, JSON.stringify(data));
|
|
2002
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2003
|
+
res.end(JSON.stringify({ stopped: true, pid: data.pid }));
|
|
2004
|
+
} else {
|
|
2005
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2006
|
+
res.end(JSON.stringify({ stopped: false, reason: "no running test" }));
|
|
2007
|
+
}
|
|
2008
|
+
} catch {
|
|
2009
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2010
|
+
res.end(JSON.stringify({ stopped: false, reason: "no progress file" }));
|
|
2011
|
+
}
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
1900
2014
|
if (url.pathname === "/api/tests/progress" && req.method === "GET") {
|
|
1901
2015
|
const progressFile = path.join(CREWSWARM_DIR, "test-results", ".test-progress.json");
|
|
1902
2016
|
try {
|
|
@@ -4696,8 +4810,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4696
4810
|
if (!providers["anthropic-oauth"]) {
|
|
4697
4811
|
try {
|
|
4698
4812
|
const { execFileSync } = await import("node:child_process");
|
|
4699
|
-
const
|
|
4700
|
-
for (const acct of [userInfo().username, "jeffhobbs", "unknown"]) {
|
|
4813
|
+
for (const acct of getKeychainAccountCandidates()) {
|
|
4701
4814
|
try {
|
|
4702
4815
|
const raw = execFileSync("security", [
|
|
4703
4816
|
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
@@ -4732,8 +4845,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4732
4845
|
if (!token) {
|
|
4733
4846
|
if (providerId === "anthropic-oauth") {
|
|
4734
4847
|
const { execFileSync } = await import("node:child_process");
|
|
4735
|
-
const
|
|
4736
|
-
for (const acct of [userInfo().username, "jeffhobbs", "unknown"]) {
|
|
4848
|
+
for (const acct of getKeychainAccountCandidates()) {
|
|
4737
4849
|
try {
|
|
4738
4850
|
const raw = execFileSync("security", [
|
|
4739
4851
|
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
@@ -4862,8 +4974,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4862
4974
|
let token = getOAuthTokenCached(providerId);
|
|
4863
4975
|
if (!token && providerId === "anthropic-oauth") {
|
|
4864
4976
|
const { execFileSync } = await import("node:child_process");
|
|
4865
|
-
const
|
|
4866
|
-
for (const acct of [userInfo().username, "jeffhobbs", "unknown"]) {
|
|
4977
|
+
for (const acct of getKeychainAccountCandidates()) {
|
|
4867
4978
|
try {
|
|
4868
4979
|
const raw = execFileSync("security", [
|
|
4869
4980
|
"find-generic-password", "-s", "Claude Code-credentials", "-a", acct, "-w"
|
|
@@ -7822,6 +7933,86 @@ ORDER BY day DESC, cost DESC;`;
|
|
|
7822
7933
|
}
|
|
7823
7934
|
return;
|
|
7824
7935
|
}
|
|
7936
|
+
// ── crew-cli cost stats ──────────────────────────────────────────────────
|
|
7937
|
+
if (url.pathname === "/api/crew-cli-stats" && req.method === "GET") {
|
|
7938
|
+
const days = Number(url.searchParams.get("days") || "14");
|
|
7939
|
+
const cutoff = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
|
|
7940
|
+
try {
|
|
7941
|
+
// Scan known project directories for .crew/cost.json files
|
|
7942
|
+
const searchDirs = new Set();
|
|
7943
|
+
// 1. opencodeProject from config
|
|
7944
|
+
try {
|
|
7945
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), ".crewswarm", "crewswarm.json"), "utf8"));
|
|
7946
|
+
if (cfg.opencodeProject) searchDirs.add(cfg.opencodeProject.replace(/\/+$/, ""));
|
|
7947
|
+
} catch {}
|
|
7948
|
+
// 2. Registered projects
|
|
7949
|
+
try {
|
|
7950
|
+
const projFile = path.join(os.homedir(), ".crewswarm", "projects.json");
|
|
7951
|
+
const projs = JSON.parse(fs.readFileSync(projFile, "utf8"));
|
|
7952
|
+
for (const p of (projs.projects || projs || [])) {
|
|
7953
|
+
if (p.outputDir) searchDirs.add(p.outputDir.replace(/\/+$/, ""));
|
|
7954
|
+
}
|
|
7955
|
+
} catch {}
|
|
7956
|
+
// 3. Home .crew dir
|
|
7957
|
+
searchDirs.add(os.homedir());
|
|
7958
|
+
|
|
7959
|
+
const allEntries = [];
|
|
7960
|
+
for (const dir of searchDirs) {
|
|
7961
|
+
const costFile = path.join(dir, ".crew", "cost.json");
|
|
7962
|
+
try {
|
|
7963
|
+
const raw = JSON.parse(fs.readFileSync(costFile, "utf8"));
|
|
7964
|
+
for (const entry of (raw.entries || [])) {
|
|
7965
|
+
if (!entry.timestamp) continue;
|
|
7966
|
+
const day = entry.timestamp.slice(0, 10);
|
|
7967
|
+
if (day >= cutoff) {
|
|
7968
|
+
allEntries.push({ ...entry, day, project: dir });
|
|
7969
|
+
}
|
|
7970
|
+
}
|
|
7971
|
+
} catch {}
|
|
7972
|
+
}
|
|
7973
|
+
|
|
7974
|
+
// Roll up by day
|
|
7975
|
+
const byDay = {};
|
|
7976
|
+
let totalCost = 0;
|
|
7977
|
+
let totalCalls = 0;
|
|
7978
|
+
let totalPromptTokens = 0;
|
|
7979
|
+
let totalCompletionTokens = 0;
|
|
7980
|
+
for (const e of allEntries) {
|
|
7981
|
+
if (!byDay[e.day]) byDay[e.day] = { cost: 0, calls: 0, prompt_tokens: 0, completion_tokens: 0, byModel: {} };
|
|
7982
|
+
const d = byDay[e.day];
|
|
7983
|
+
const usd = Number(e.usd || 0);
|
|
7984
|
+
d.cost += usd;
|
|
7985
|
+
d.calls += 1;
|
|
7986
|
+
d.prompt_tokens += Number(e.promptTokens || 0);
|
|
7987
|
+
d.completion_tokens += Number(e.completionTokens || 0);
|
|
7988
|
+
const model = e.model || "unknown";
|
|
7989
|
+
if (!d.byModel[model]) d.byModel[model] = { cost: 0, calls: 0, prompt_tokens: 0, completion_tokens: 0 };
|
|
7990
|
+
d.byModel[model].cost += usd;
|
|
7991
|
+
d.byModel[model].calls += 1;
|
|
7992
|
+
d.byModel[model].prompt_tokens += Number(e.promptTokens || 0);
|
|
7993
|
+
d.byModel[model].completion_tokens += Number(e.completionTokens || 0);
|
|
7994
|
+
totalCost += usd;
|
|
7995
|
+
totalCalls += 1;
|
|
7996
|
+
totalPromptTokens += Number(e.promptTokens || 0);
|
|
7997
|
+
totalCompletionTokens += Number(e.completionTokens || 0);
|
|
7998
|
+
}
|
|
7999
|
+
|
|
8000
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
8001
|
+
res.end(JSON.stringify({
|
|
8002
|
+
ok: true,
|
|
8003
|
+
totalCost,
|
|
8004
|
+
totalCalls,
|
|
8005
|
+
totalPromptTokens,
|
|
8006
|
+
totalCompletionTokens,
|
|
8007
|
+
projects: [...searchDirs],
|
|
8008
|
+
byDay,
|
|
8009
|
+
}));
|
|
8010
|
+
} catch (e) {
|
|
8011
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
8012
|
+
res.end(JSON.stringify({ ok: false, error: e.message, byDay: {} }));
|
|
8013
|
+
}
|
|
8014
|
+
return;
|
|
8015
|
+
}
|
|
7825
8016
|
// ── OpenCode models API ──────────────────────────────────────────────────
|
|
7826
8017
|
if (url.pathname === "/api/opencode-models" && req.method === "GET") {
|
|
7827
8018
|
let models = [];
|