claude-overnight 1.6.1 → 1.8.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/dist/index.js +74 -54
- package/dist/planner.d.ts +1 -0
- package/dist/planner.js +4 -0
- package/dist/ui.d.ts +56 -3
- package/dist/ui.js +389 -267
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import { createInterface } from "readline";
|
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
9
9
|
import { Swarm } from "./swarm.js";
|
|
10
|
-
import { planTasks, refinePlan, detectModelTier, steerWave, identifyThemes, buildThinkingTasks, orchestrate } from "./planner.js";
|
|
11
|
-
import {
|
|
10
|
+
import { planTasks, refinePlan, detectModelTier, steerWave, identifyThemes, buildThinkingTasks, orchestrate, getTotalPlannerCost, getPlannerRateLimitInfo } from "./planner.js";
|
|
11
|
+
import { RunDisplay, renderSummary } from "./ui.js";
|
|
12
12
|
// ── CLI flag parsing ──
|
|
13
13
|
function parseCliFlags(argv) {
|
|
14
14
|
const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget"]);
|
|
@@ -1091,14 +1091,22 @@ async function main() {
|
|
|
1091
1091
|
agentTimeoutMs,
|
|
1092
1092
|
usageCap, allowExtraUsage, extraUsageBudget,
|
|
1093
1093
|
});
|
|
1094
|
-
const
|
|
1094
|
+
const thinkRunInfo = {
|
|
1095
|
+
accIn: 0, accOut: 0, accCost: 0, accCompleted: 0, accFailed: 0,
|
|
1096
|
+
sessionsBudget: budget ?? 10, waveNum: -1, remaining: (budget ?? 10),
|
|
1097
|
+
model: plannerModel, startedAt: Date.now(),
|
|
1098
|
+
};
|
|
1099
|
+
const thinkDisplay = new RunDisplay(thinkRunInfo, { remaining: 0, usageCap, dirty: false });
|
|
1100
|
+
thinkDisplay.setWave(thinkingSwarm);
|
|
1101
|
+
thinkDisplay.start();
|
|
1095
1102
|
try {
|
|
1096
1103
|
await thinkingSwarm.run();
|
|
1097
1104
|
}
|
|
1098
1105
|
finally {
|
|
1099
|
-
|
|
1106
|
+
thinkDisplay.pause();
|
|
1107
|
+
console.log(renderSummary(thinkingSwarm));
|
|
1108
|
+
thinkDisplay.stop();
|
|
1100
1109
|
}
|
|
1101
|
-
console.log(renderSummary(thinkingSwarm));
|
|
1102
1110
|
thinkingUsed = thinkingSwarm.completed + thinkingSwarm.failed;
|
|
1103
1111
|
thinkingCost = thinkingSwarm.totalCostUsd;
|
|
1104
1112
|
thinkingIn = thinkingSwarm.totalInputTokens;
|
|
@@ -1231,8 +1239,10 @@ async function main() {
|
|
|
1231
1239
|
process.exit(0);
|
|
1232
1240
|
}
|
|
1233
1241
|
// ── Run (wave loop) ──
|
|
1234
|
-
|
|
1235
|
-
|
|
1242
|
+
const restore = () => { try {
|
|
1243
|
+
process.stdout.write("\x1B[?25h\n");
|
|
1244
|
+
}
|
|
1245
|
+
catch { } };
|
|
1236
1246
|
const runStartedAt = resuming && resumeState?.startedAt ? new Date(resumeState.startedAt).getTime() : Date.now();
|
|
1237
1247
|
// Wave-loop state — either fresh or resumed
|
|
1238
1248
|
mkdirSync(join(runDir, "reflections"), { recursive: true });
|
|
@@ -1294,6 +1304,17 @@ async function main() {
|
|
|
1294
1304
|
liveConfig.remaining = remaining;
|
|
1295
1305
|
liveConfig.usageCap = usageCap;
|
|
1296
1306
|
const maxOverheadBudget = Math.max(4, Math.ceil((budget ?? 10) * 0.15));
|
|
1307
|
+
// Unified display — one instance for the entire run
|
|
1308
|
+
const runInfoRef = {
|
|
1309
|
+
accIn, accOut, accCost, accCompleted, accFailed,
|
|
1310
|
+
sessionsBudget: budget ?? tasks.length, waveNum, remaining,
|
|
1311
|
+
model: workerModel, startedAt: runStartedAt,
|
|
1312
|
+
};
|
|
1313
|
+
const display = new RunDisplay(runInfoRef, liveConfig);
|
|
1314
|
+
const rlGetter = () => {
|
|
1315
|
+
const rl = getPlannerRateLimitInfo();
|
|
1316
|
+
return { utilization: rl.utilization, isUsingOverage: rl.isUsingOverage, windows: rl.windows, resetsAt: rl.resetsAt };
|
|
1317
|
+
};
|
|
1297
1318
|
// For flex + branch strategy: create one target branch, waves merge via yolo into it
|
|
1298
1319
|
let runBranch;
|
|
1299
1320
|
let originalRef;
|
|
@@ -1315,29 +1336,33 @@ async function main() {
|
|
|
1315
1336
|
const gracefulStop = (signal) => {
|
|
1316
1337
|
if (stopping) {
|
|
1317
1338
|
currentSwarm?.cleanup();
|
|
1339
|
+
display.stop();
|
|
1318
1340
|
restore();
|
|
1319
1341
|
process.exit(0);
|
|
1320
1342
|
}
|
|
1321
1343
|
stopping = true;
|
|
1322
|
-
process.stdout.write(`\n ${chalk.yellow(`${signal}: stopping... (send again to force)`)}\n`);
|
|
1323
1344
|
currentSwarm?.abort();
|
|
1324
1345
|
};
|
|
1325
1346
|
process.on("SIGINT", () => gracefulStop("SIGINT"));
|
|
1326
1347
|
process.on("SIGTERM", () => gracefulStop("SIGTERM"));
|
|
1327
|
-
process.on("uncaughtException", (err) => { currentSwarm?.abort(); currentSwarm?.cleanup(); restore(); console.error(chalk.red(`\n Uncaught: ${err.message}`)); process.exit(1); });
|
|
1328
|
-
process.on("unhandledRejection", (reason) => { currentSwarm?.abort(); currentSwarm?.cleanup(); restore(); console.error(chalk.red(`\n Unhandled: ${reason instanceof Error ? reason.message : reason}`)); process.exit(1); });
|
|
1348
|
+
process.on("uncaughtException", (err) => { currentSwarm?.abort(); currentSwarm?.cleanup(); display.stop(); restore(); console.error(chalk.red(`\n Uncaught: ${err.message}`)); process.exit(1); });
|
|
1349
|
+
process.on("unhandledRejection", (reason) => { currentSwarm?.abort(); currentSwarm?.cleanup(); display.stop(); restore(); console.error(chalk.red(`\n Unhandled: ${reason instanceof Error ? reason.message : reason}`)); process.exit(1); });
|
|
1350
|
+
// Helper: sync mutable runInfo with accumulated state
|
|
1351
|
+
const syncRunInfo = () => Object.assign(runInfoRef, { accIn, accOut, accCost, accCompleted, accFailed, waveNum, remaining });
|
|
1329
1352
|
// When resuming a flex run with no queued tasks, steer immediately to get the next wave
|
|
1330
1353
|
if (resuming && flex && currentTasks.length === 0 && remaining > 0) {
|
|
1331
1354
|
let steerAttempts = 0;
|
|
1355
|
+
display.setSteering(rlGetter);
|
|
1356
|
+
display.start();
|
|
1332
1357
|
while (currentTasks.length === 0 && remaining > 0 && !objectiveComplete && steerAttempts < 3) {
|
|
1333
1358
|
steerAttempts++;
|
|
1334
|
-
|
|
1335
|
-
process.stdout.write("\x1B[?25l");
|
|
1359
|
+
const plannerCostBefore = getTotalPlannerCost();
|
|
1336
1360
|
try {
|
|
1337
1361
|
const memory = readRunMemory(runDir, previousKnowledge || undefined);
|
|
1338
|
-
const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency,
|
|
1339
|
-
|
|
1340
|
-
|
|
1362
|
+
const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, (text) => display.updateText(text), memory);
|
|
1363
|
+
const steerCost = getTotalPlannerCost() - plannerCostBefore;
|
|
1364
|
+
accCost += steerCost;
|
|
1365
|
+
syncRunInfo();
|
|
1341
1366
|
if (steer.statusUpdate)
|
|
1342
1367
|
writeStatus(runDir, steer.statusUpdate);
|
|
1343
1368
|
if (steer.goalUpdate)
|
|
@@ -1345,24 +1370,20 @@ async function main() {
|
|
|
1345
1370
|
if (steer.done || steer.tasks.length === 0) {
|
|
1346
1371
|
const hasVerification = waveHistory.some(w => w.kind.includes("verif"));
|
|
1347
1372
|
if (!hasVerification && remaining >= 1) {
|
|
1348
|
-
|
|
1349
|
-
console.log(chalk.yellow(` Done blocked — verification required before completion\n`));
|
|
1373
|
+
display.updateText(`Done blocked \u2014 verification required`);
|
|
1350
1374
|
lastWaveKind = "done-blocked";
|
|
1351
1375
|
continue;
|
|
1352
1376
|
}
|
|
1353
|
-
console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
|
|
1354
1377
|
objectiveComplete = true;
|
|
1355
1378
|
remaining = 0;
|
|
1356
1379
|
}
|
|
1357
1380
|
else {
|
|
1358
1381
|
const isOverhead = steer.waveKind !== "execute";
|
|
1359
1382
|
if (isOverhead && overheadBudgetUsed + steer.tasks.length > maxOverheadBudget) {
|
|
1360
|
-
|
|
1361
|
-
console.log(chalk.yellow(` Overhead budget exhausted (${overheadBudgetUsed}/${maxOverheadBudget}) — re-assessing\n`));
|
|
1383
|
+
display.updateText(`Overhead budget exhausted (${overheadBudgetUsed}/${maxOverheadBudget}) \u2014 re-assessing`);
|
|
1362
1384
|
lastWaveKind = "overhead-capped";
|
|
1363
1385
|
continue;
|
|
1364
1386
|
}
|
|
1365
|
-
console.log(chalk.dim(` ${steer.reasoning}\n`));
|
|
1366
1387
|
currentTasks = steer.tasks.map(t => ({
|
|
1367
1388
|
...t,
|
|
1368
1389
|
model: t.model === "planner" ? plannerModel : t.model === "worker" ? workerModel
|
|
@@ -1374,42 +1395,45 @@ async function main() {
|
|
|
1374
1395
|
}
|
|
1375
1396
|
}
|
|
1376
1397
|
catch (err) {
|
|
1377
|
-
|
|
1398
|
+
const steerCost = getTotalPlannerCost() - plannerCostBefore;
|
|
1399
|
+
accCost += steerCost;
|
|
1400
|
+
display.stop();
|
|
1378
1401
|
console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
|
|
1379
1402
|
break;
|
|
1380
1403
|
}
|
|
1381
1404
|
}
|
|
1382
1405
|
}
|
|
1406
|
+
// Start unified display (first call — idempotent)
|
|
1407
|
+
if (!display.runInfo.startedAt)
|
|
1408
|
+
display.runInfo.startedAt = runStartedAt;
|
|
1409
|
+
display.start();
|
|
1383
1410
|
while (remaining > 0 && currentTasks.length > 0 && !stopping) {
|
|
1384
1411
|
if (currentTasks.length > remaining)
|
|
1385
1412
|
currentTasks = currentTasks.slice(0, remaining);
|
|
1386
|
-
|
|
1387
|
-
const costSoFar = accCost > 0 ? ` · $${accCost.toFixed(2)} spent` : "";
|
|
1388
|
-
console.log(chalk.cyan(`\n ◆ Wave ${waveNum + 1}`) + chalk.dim(` · ${currentTasks.length} tasks · ${remaining} remaining${costSoFar}\n`));
|
|
1389
|
-
}
|
|
1413
|
+
syncRunInfo();
|
|
1390
1414
|
const swarm = new Swarm({
|
|
1391
1415
|
tasks: currentTasks, concurrency, cwd, model: workerModel, permissionMode, allowedTools,
|
|
1392
1416
|
useWorktrees, mergeStrategy: waveMerge, agentTimeoutMs, usageCap, allowExtraUsage, extraUsageBudget,
|
|
1393
1417
|
baseCostUsd: accCost,
|
|
1394
1418
|
});
|
|
1395
1419
|
currentSwarm = swarm;
|
|
1396
|
-
|
|
1420
|
+
display.setWave(swarm);
|
|
1421
|
+
display.resume();
|
|
1397
1422
|
try {
|
|
1398
1423
|
await swarm.run();
|
|
1399
1424
|
}
|
|
1400
1425
|
catch (err) {
|
|
1401
1426
|
if (isAuthError(err)) {
|
|
1402
|
-
|
|
1427
|
+
display.stop();
|
|
1403
1428
|
restore();
|
|
1404
1429
|
console.error(chalk.red(`\n Authentication failed — check your API key or run: claude auth\n`));
|
|
1405
1430
|
process.exit(1);
|
|
1406
1431
|
}
|
|
1407
1432
|
throw err;
|
|
1408
1433
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1434
|
+
// Show wave summary (pauses render, prints, then we switch to steering)
|
|
1435
|
+
display.pause();
|
|
1436
|
+
console.log(renderSummary(swarm));
|
|
1413
1437
|
// Accumulate
|
|
1414
1438
|
accCost += swarm.totalCostUsd;
|
|
1415
1439
|
accIn += swarm.totalInputTokens;
|
|
@@ -1418,11 +1442,9 @@ async function main() {
|
|
|
1418
1442
|
accFailed += swarm.failed;
|
|
1419
1443
|
accTools += swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
1420
1444
|
remaining = Math.max(0, remaining - swarm.completed - swarm.failed);
|
|
1421
|
-
// Sanity check: remaining should never drop below budget - total consumed
|
|
1422
1445
|
const expectedFloor = Math.max(0, (budget ?? 0) - accCompleted - accFailed);
|
|
1423
1446
|
if (remaining < expectedFloor)
|
|
1424
1447
|
remaining = expectedFloor;
|
|
1425
|
-
// Apply live config changes if user adjusted budget/threshold mid-wave
|
|
1426
1448
|
if (liveConfig.dirty) {
|
|
1427
1449
|
remaining = liveConfig.remaining;
|
|
1428
1450
|
usageCap = liveConfig.usageCap;
|
|
@@ -1453,48 +1475,44 @@ async function main() {
|
|
|
1453
1475
|
if (!flex || remaining <= 0 || swarm.aborted || swarm.cappedOut)
|
|
1454
1476
|
break;
|
|
1455
1477
|
// ── Steer: assess and compose the next wave ──
|
|
1478
|
+
syncRunInfo();
|
|
1479
|
+
display.setSteering(rlGetter);
|
|
1480
|
+
display.resume();
|
|
1456
1481
|
let steered = false;
|
|
1457
1482
|
let steerAttempts = 0;
|
|
1458
1483
|
while (!steered && remaining > 0 && !stopping && steerAttempts < 3) {
|
|
1459
1484
|
steerAttempts++;
|
|
1460
|
-
|
|
1461
|
-
process.stdout.write("\x1B[?25l");
|
|
1485
|
+
const plannerCostBefore = getTotalPlannerCost();
|
|
1462
1486
|
try {
|
|
1463
1487
|
const memory = readRunMemory(runDir, previousKnowledge || undefined);
|
|
1464
|
-
const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency,
|
|
1465
|
-
|
|
1466
|
-
|
|
1488
|
+
const steer = await steerWave(objective, waveHistory, remaining, cwd, plannerModel, workerModel, permissionMode, concurrency, (text) => display.updateText(text), memory);
|
|
1489
|
+
const steerCost = getTotalPlannerCost() - plannerCostBefore;
|
|
1490
|
+
accCost += steerCost;
|
|
1491
|
+
syncRunInfo();
|
|
1467
1492
|
if (steer.statusUpdate)
|
|
1468
1493
|
writeStatus(runDir, steer.statusUpdate);
|
|
1469
|
-
if (steer.goalUpdate)
|
|
1494
|
+
if (steer.goalUpdate)
|
|
1470
1495
|
writeGoalUpdate(runDir, steer.goalUpdate);
|
|
1471
|
-
console.log(chalk.dim(` Goal refined: ${steer.goalUpdate.slice(0, 100)}\n`));
|
|
1472
|
-
}
|
|
1473
1496
|
const execWaves = waveHistory.filter(w => w.kind === "execute").length;
|
|
1474
1497
|
if (execWaves > 0 && execWaves % 5 === 0)
|
|
1475
1498
|
archiveMilestone(runDir, waveNum);
|
|
1476
1499
|
if (steer.done || steer.tasks.length === 0) {
|
|
1477
1500
|
const hasVerification = waveHistory.some(w => w.kind.includes("verif"));
|
|
1478
1501
|
if (!hasVerification && remaining >= 1) {
|
|
1479
|
-
|
|
1480
|
-
console.log(chalk.yellow(` Done blocked — verification required before completion\n`));
|
|
1502
|
+
display.updateText(`Done blocked \u2014 verification required`);
|
|
1481
1503
|
lastWaveKind = "done-blocked";
|
|
1482
|
-
continue;
|
|
1504
|
+
continue;
|
|
1483
1505
|
}
|
|
1484
|
-
console.log(chalk.green(` \u2713 ${steer.reasoning}\n`));
|
|
1485
1506
|
objectiveComplete = true;
|
|
1486
1507
|
remaining = 0;
|
|
1487
1508
|
break;
|
|
1488
1509
|
}
|
|
1489
1510
|
const isOverhead = steer.waveKind !== "execute";
|
|
1490
1511
|
if (isOverhead && overheadBudgetUsed + steer.tasks.length > maxOverheadBudget) {
|
|
1491
|
-
|
|
1492
|
-
console.log(chalk.yellow(` Overhead budget exhausted (${overheadBudgetUsed}/${maxOverheadBudget}) — re-assessing\n`));
|
|
1512
|
+
display.updateText(`Overhead budget exhausted (${overheadBudgetUsed}/${maxOverheadBudget}) \u2014 re-assessing`);
|
|
1493
1513
|
lastWaveKind = "overhead-capped";
|
|
1494
|
-
continue;
|
|
1514
|
+
continue;
|
|
1495
1515
|
}
|
|
1496
|
-
console.log(chalk.dim(` ${steer.reasoning}\n`));
|
|
1497
|
-
// Resolve model aliases: "planner" → plannerModel, "worker" → workerModel
|
|
1498
1516
|
currentTasks = steer.tasks.map(t => ({
|
|
1499
1517
|
...t,
|
|
1500
1518
|
model: t.model === "planner" ? plannerModel : t.model === "worker" ? workerModel
|
|
@@ -1506,16 +1524,18 @@ async function main() {
|
|
|
1506
1524
|
steered = true;
|
|
1507
1525
|
}
|
|
1508
1526
|
catch (err) {
|
|
1509
|
-
|
|
1527
|
+
const steerCost = getTotalPlannerCost() - plannerCostBefore;
|
|
1528
|
+
accCost += steerCost;
|
|
1529
|
+
display.stop();
|
|
1510
1530
|
console.log(chalk.yellow(` Steering failed: ${err.message?.slice(0, 80)} \u2014 stopping\n`));
|
|
1511
|
-
// Don't zero out remaining — preserve unspent budget so resume works
|
|
1512
1531
|
break;
|
|
1513
1532
|
}
|
|
1514
1533
|
}
|
|
1515
1534
|
if (!steered)
|
|
1516
|
-
break;
|
|
1535
|
+
break;
|
|
1517
1536
|
waveNum++;
|
|
1518
1537
|
}
|
|
1538
|
+
display.stop();
|
|
1519
1539
|
// Only truly "done" if steering explicitly completed the objective (or non-flex single wave with budget exhausted)
|
|
1520
1540
|
const trulyDone = objectiveComplete || (!flex && remaining <= 0);
|
|
1521
1541
|
const finalPhase = trulyDone ? "done" : "capped";
|
package/dist/planner.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface RunMemory {
|
|
|
37
37
|
}
|
|
38
38
|
export type ModelTier = "opus" | "sonnet" | "haiku" | "unknown";
|
|
39
39
|
export declare function detectModelTier(model: string): ModelTier;
|
|
40
|
+
export declare function getTotalPlannerCost(): number;
|
|
40
41
|
export declare function getPlannerRateLimitInfo(): PlannerRateLimitInfo;
|
|
41
42
|
export declare function planTasks(objective: string, cwd: string, plannerModel: string, workerModel: string, permissionMode: PermMode, budget: number | undefined, concurrency: number, onLog: (text: string) => void, flexNote?: string, outFile?: string): Promise<Task[]>;
|
|
42
43
|
export declare function identifyThemes(objective: string, count: number, model: string, permissionMode: PermMode, onLog?: (text: string) => void): Promise<string[]>;
|
package/dist/planner.js
CHANGED
|
@@ -200,6 +200,9 @@ async function runPlannerQuery(prompt, opts, onLog) {
|
|
|
200
200
|
}
|
|
201
201
|
throw new Error("Planner query failed after retries");
|
|
202
202
|
}
|
|
203
|
+
/** Cumulative cost of all planner queries (steering, orchestration, etc.) across the session. */
|
|
204
|
+
let _totalPlannerCostUsd = 0;
|
|
205
|
+
export function getTotalPlannerCost() { return _totalPlannerCostUsd; }
|
|
203
206
|
/** Shared mutable rate limit state that planner queries write to for UI display. Reset per query. */
|
|
204
207
|
let _plannerRateLimitInfo = {
|
|
205
208
|
utilization: 0, status: "", isUsingOverage: false, windows: new Map(), costUsd: 0,
|
|
@@ -310,6 +313,7 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
310
313
|
if (typeof r.total_cost_usd === "number") {
|
|
311
314
|
costUsd = r.total_cost_usd;
|
|
312
315
|
_plannerRateLimitInfo.costUsd += costUsd;
|
|
316
|
+
_totalPlannerCostUsd += costUsd;
|
|
313
317
|
}
|
|
314
318
|
if (msg.subtype === "success")
|
|
315
319
|
resultText = r.result || "";
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,11 +1,64 @@
|
|
|
1
1
|
import type { Swarm } from "./swarm.js";
|
|
2
|
-
|
|
2
|
+
import type { RateLimitWindow } from "./types.js";
|
|
3
|
+
/** Cumulative run-level stats — mutable, updated between phases. */
|
|
4
|
+
export interface RunInfo {
|
|
5
|
+
accIn: number;
|
|
6
|
+
accOut: number;
|
|
7
|
+
accCost: number;
|
|
8
|
+
accCompleted: number;
|
|
9
|
+
accFailed: number;
|
|
10
|
+
sessionsBudget: number;
|
|
11
|
+
waveNum: number;
|
|
12
|
+
remaining: number;
|
|
13
|
+
model?: string;
|
|
14
|
+
startedAt: number;
|
|
15
|
+
}
|
|
3
16
|
/** Mutable config that can be changed live during execution. */
|
|
4
17
|
export interface LiveConfig {
|
|
5
18
|
remaining: number;
|
|
6
19
|
usageCap: number | undefined;
|
|
7
|
-
/** Set by hotkey handler when user changes a value. Cleared after main loop reads it. */
|
|
8
20
|
dirty: boolean;
|
|
9
21
|
}
|
|
10
|
-
|
|
22
|
+
type RLGetter = () => {
|
|
23
|
+
utilization: number;
|
|
24
|
+
isUsingOverage: boolean;
|
|
25
|
+
windows: Map<string, RateLimitWindow>;
|
|
26
|
+
resetsAt?: number;
|
|
27
|
+
};
|
|
28
|
+
export declare class RunDisplay {
|
|
29
|
+
readonly runInfo: RunInfo;
|
|
30
|
+
private liveConfig?;
|
|
31
|
+
private swarm?;
|
|
32
|
+
private steeringText?;
|
|
33
|
+
private rlGetter?;
|
|
34
|
+
private interval?;
|
|
35
|
+
private keyHandler?;
|
|
36
|
+
private inputMode;
|
|
37
|
+
private inputBuf;
|
|
38
|
+
private started;
|
|
39
|
+
private readonly isTTY;
|
|
40
|
+
private lastSeq;
|
|
41
|
+
private lastCompleted;
|
|
42
|
+
constructor(runInfo: RunInfo, liveConfig?: LiveConfig);
|
|
43
|
+
/** Start the persistent render loop. Call once at the beginning of the run. */
|
|
44
|
+
start(): void;
|
|
45
|
+
/** Switch to wave mode — show agent table + events. */
|
|
46
|
+
setWave(swarm: Swarm): void;
|
|
47
|
+
/** Switch to steering mode — show assessment text. */
|
|
48
|
+
setSteering(rlGetter?: RLGetter): void;
|
|
49
|
+
/** Update the steering text. */
|
|
50
|
+
updateText(text: string): void;
|
|
51
|
+
/** Pause rendering (e.g. to print a summary). */
|
|
52
|
+
pause(): void;
|
|
53
|
+
/** Resume rendering after a pause. */
|
|
54
|
+
resume(): void;
|
|
55
|
+
/** Stop and clean up. */
|
|
56
|
+
stop(): void;
|
|
57
|
+
private resumeInterval;
|
|
58
|
+
private render;
|
|
59
|
+
private hasHotkeys;
|
|
60
|
+
private setupHotkeys;
|
|
61
|
+
private plainTick;
|
|
62
|
+
}
|
|
11
63
|
export declare function renderSummary(swarm: Swarm): string;
|
|
64
|
+
export {};
|
package/dist/ui.js
CHANGED
|
@@ -4,6 +4,226 @@ const WINDOW_SHORT_NAMES = {
|
|
|
4
4
|
five_hour: "5h", seven_day: "7d", seven_day_opus: "7d op",
|
|
5
5
|
seven_day_sonnet: "7d sn", overage: "extra",
|
|
6
6
|
};
|
|
7
|
+
// ── Unified display ──
|
|
8
|
+
export class RunDisplay {
|
|
9
|
+
runInfo;
|
|
10
|
+
liveConfig;
|
|
11
|
+
swarm;
|
|
12
|
+
steeringText;
|
|
13
|
+
rlGetter;
|
|
14
|
+
interval;
|
|
15
|
+
keyHandler;
|
|
16
|
+
inputMode = "none";
|
|
17
|
+
inputBuf = "";
|
|
18
|
+
started = false;
|
|
19
|
+
isTTY;
|
|
20
|
+
// Plain-log state
|
|
21
|
+
lastSeq = 0;
|
|
22
|
+
lastCompleted = -1;
|
|
23
|
+
constructor(runInfo, liveConfig) {
|
|
24
|
+
this.runInfo = runInfo;
|
|
25
|
+
this.liveConfig = liveConfig;
|
|
26
|
+
this.isTTY = !!process.stdout.isTTY;
|
|
27
|
+
}
|
|
28
|
+
/** Start the persistent render loop. Call once at the beginning of the run. */
|
|
29
|
+
start() {
|
|
30
|
+
if (this.started)
|
|
31
|
+
return;
|
|
32
|
+
this.started = true;
|
|
33
|
+
this.setupHotkeys();
|
|
34
|
+
this.resumeInterval();
|
|
35
|
+
}
|
|
36
|
+
/** Switch to wave mode — show agent table + events. */
|
|
37
|
+
setWave(swarm) {
|
|
38
|
+
this.swarm = swarm;
|
|
39
|
+
this.steeringText = undefined;
|
|
40
|
+
this.rlGetter = undefined;
|
|
41
|
+
this.lastSeq = 0;
|
|
42
|
+
this.lastCompleted = -1;
|
|
43
|
+
}
|
|
44
|
+
/** Switch to steering mode — show assessment text. */
|
|
45
|
+
setSteering(rlGetter) {
|
|
46
|
+
this.swarm = undefined;
|
|
47
|
+
this.steeringText = "Assessing...";
|
|
48
|
+
this.rlGetter = rlGetter;
|
|
49
|
+
}
|
|
50
|
+
/** Update the steering text. */
|
|
51
|
+
updateText(text) { this.steeringText = text; }
|
|
52
|
+
/** Pause rendering (e.g. to print a summary). */
|
|
53
|
+
pause() {
|
|
54
|
+
if (this.interval) {
|
|
55
|
+
clearInterval(this.interval);
|
|
56
|
+
this.interval = undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Resume rendering after a pause. */
|
|
60
|
+
resume() {
|
|
61
|
+
if (!this.started || this.interval)
|
|
62
|
+
return;
|
|
63
|
+
if (this.isTTY)
|
|
64
|
+
try {
|
|
65
|
+
process.stdout.write("\x1B[?25l");
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
this.resumeInterval();
|
|
69
|
+
}
|
|
70
|
+
/** Stop and clean up. */
|
|
71
|
+
stop() {
|
|
72
|
+
this.pause();
|
|
73
|
+
if (this.keyHandler) {
|
|
74
|
+
process.stdin.removeListener("data", this.keyHandler);
|
|
75
|
+
this.keyHandler = undefined;
|
|
76
|
+
try {
|
|
77
|
+
process.stdin.setRawMode(false);
|
|
78
|
+
process.stdin.pause();
|
|
79
|
+
}
|
|
80
|
+
catch { }
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
process.stdout.write("\x1B[?25h");
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
this.started = false;
|
|
87
|
+
}
|
|
88
|
+
// ── Internals ──
|
|
89
|
+
resumeInterval() {
|
|
90
|
+
if (this.interval)
|
|
91
|
+
return;
|
|
92
|
+
if (!this.isTTY) {
|
|
93
|
+
this.interval = setInterval(() => this.plainTick(), 500);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
process.stdout.write("\x1B[?25l\x1B[H\x1B[J");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.interval = setInterval(() => {
|
|
103
|
+
try {
|
|
104
|
+
process.stdout.write("\x1B[H\x1B[J");
|
|
105
|
+
process.stdout.write(this.render());
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
this.pause();
|
|
109
|
+
}
|
|
110
|
+
}, 250);
|
|
111
|
+
}
|
|
112
|
+
render() {
|
|
113
|
+
if (this.swarm) {
|
|
114
|
+
let frame = renderFrame(this.swarm, this.hasHotkeys(), this.runInfo);
|
|
115
|
+
if (this.inputMode !== "none") {
|
|
116
|
+
const label = this.inputMode === "budget" ? "New budget (remaining sessions)" : "New usage cap (0-100%)";
|
|
117
|
+
frame += `\n ${chalk.cyan(">")} ${label}: ${this.inputBuf}\u2588`;
|
|
118
|
+
}
|
|
119
|
+
return frame;
|
|
120
|
+
}
|
|
121
|
+
if (this.steeringText != null) {
|
|
122
|
+
let frame = renderSteeringFrame(this.runInfo, this.steeringText, this.hasHotkeys(), this.rlGetter);
|
|
123
|
+
if (this.inputMode === "budget") {
|
|
124
|
+
frame += `\n ${chalk.cyan(">")} New budget (remaining sessions): ${this.inputBuf}\u2588`;
|
|
125
|
+
}
|
|
126
|
+
return frame;
|
|
127
|
+
}
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
hasHotkeys() {
|
|
131
|
+
return !!this.liveConfig && !!process.stdin.isTTY;
|
|
132
|
+
}
|
|
133
|
+
setupHotkeys() {
|
|
134
|
+
if (!this.liveConfig || !process.stdin.isTTY)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
process.stdin.setRawMode(true);
|
|
138
|
+
process.stdin.resume();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const lc = this.liveConfig;
|
|
144
|
+
this.keyHandler = (buf) => {
|
|
145
|
+
const s = buf.toString();
|
|
146
|
+
if (this.inputMode !== "none") {
|
|
147
|
+
if (s === "\r" || s === "\n") {
|
|
148
|
+
const val = parseFloat(this.inputBuf);
|
|
149
|
+
if (this.inputMode === "budget" && !isNaN(val) && val > 0) {
|
|
150
|
+
lc.remaining = Math.round(val);
|
|
151
|
+
lc.dirty = true;
|
|
152
|
+
this.swarm?.log(-1, `Budget changed to ${lc.remaining} remaining`);
|
|
153
|
+
}
|
|
154
|
+
else if (this.inputMode === "threshold" && !isNaN(val) && val >= 0 && val <= 100) {
|
|
155
|
+
const frac = val / 100;
|
|
156
|
+
lc.usageCap = frac > 0 ? frac : undefined;
|
|
157
|
+
lc.dirty = true;
|
|
158
|
+
if (this.swarm)
|
|
159
|
+
this.swarm.usageCap = lc.usageCap;
|
|
160
|
+
this.swarm?.log(-1, `Usage cap changed to ${val > 0 ? val + "%" : "unlimited"}`);
|
|
161
|
+
}
|
|
162
|
+
this.inputMode = "none";
|
|
163
|
+
this.inputBuf = "";
|
|
164
|
+
}
|
|
165
|
+
else if (s === "\x1B" || s === "\x03") {
|
|
166
|
+
this.inputMode = "none";
|
|
167
|
+
this.inputBuf = "";
|
|
168
|
+
}
|
|
169
|
+
else if (s === "\x7F") {
|
|
170
|
+
this.inputBuf = this.inputBuf.slice(0, -1);
|
|
171
|
+
}
|
|
172
|
+
else if (/^[0-9.]$/.test(s)) {
|
|
173
|
+
this.inputBuf += s;
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (s === "b" || s === "B") {
|
|
178
|
+
this.inputMode = "budget";
|
|
179
|
+
this.inputBuf = "";
|
|
180
|
+
}
|
|
181
|
+
else if (s === "t" || s === "T") {
|
|
182
|
+
if (this.swarm) {
|
|
183
|
+
this.inputMode = "threshold";
|
|
184
|
+
this.inputBuf = "";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (s === "q" || s === "Q" || s === "\x03") {
|
|
188
|
+
if (this.swarm) {
|
|
189
|
+
if (this.swarm.aborted)
|
|
190
|
+
process.exit(0);
|
|
191
|
+
this.swarm.abort();
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
process.stdin.on("data", this.keyHandler);
|
|
199
|
+
}
|
|
200
|
+
plainTick() {
|
|
201
|
+
if (!this.swarm)
|
|
202
|
+
return;
|
|
203
|
+
const swarm = this.swarm;
|
|
204
|
+
const write = (line) => { try {
|
|
205
|
+
process.stdout.write(line + "\n");
|
|
206
|
+
}
|
|
207
|
+
catch { } };
|
|
208
|
+
const currentSeq = swarm.logSequence;
|
|
209
|
+
if (currentSeq > this.lastSeq) {
|
|
210
|
+
const newCount = currentSeq - this.lastSeq;
|
|
211
|
+
const available = swarm.logs.length;
|
|
212
|
+
const toShow = Math.min(newCount, available);
|
|
213
|
+
for (const entry of swarm.logs.slice(available - toShow)) {
|
|
214
|
+
const t = new Date(entry.time).toLocaleTimeString("en", { hour12: false });
|
|
215
|
+
const tag = entry.agentId < 0 ? "[sys]" : `[${entry.agentId}]`;
|
|
216
|
+
write(`${t} ${tag} ${entry.text}`);
|
|
217
|
+
}
|
|
218
|
+
this.lastSeq = currentSeq;
|
|
219
|
+
}
|
|
220
|
+
if (swarm.completed !== this.lastCompleted) {
|
|
221
|
+
this.lastCompleted = swarm.completed;
|
|
222
|
+
write(`progress: ${swarm.completed}/${swarm.total} done, ${swarm.active} active, ${swarm.pending} queued`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ── Pure render functions ──
|
|
7
227
|
function colorEvent(text) {
|
|
8
228
|
if (text === "Done" || text.startsWith("Merged ") || text.startsWith("Committed "))
|
|
9
229
|
return chalk.green(text);
|
|
@@ -15,51 +235,66 @@ function colorEvent(text) {
|
|
|
15
235
|
return chalk.yellow(text);
|
|
16
236
|
return text;
|
|
17
237
|
}
|
|
18
|
-
|
|
238
|
+
/** Render the shared header block (title + stats + usage bars). */
|
|
239
|
+
function renderHeader(out, w, p) {
|
|
240
|
+
const barW = Math.min(30, w - 50);
|
|
241
|
+
const filled = Math.round(p.barPct * barW);
|
|
242
|
+
const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(barW - filled));
|
|
243
|
+
const modelTag = p.model ? chalk.dim(` [${p.model}]`) : "";
|
|
244
|
+
const phaseTag = p.phase ? " " + p.phase : "";
|
|
245
|
+
out.push("");
|
|
246
|
+
out.push(` ${chalk.bold.white("CLAUDE OVERNIGHT")}${modelTag}${phaseTag} ${bar} ` +
|
|
247
|
+
`${p.barLabel} ` +
|
|
248
|
+
(p.active > 0 ? chalk.cyan(`${p.active} active`) + " " : "") +
|
|
249
|
+
(p.queued > 0 ? chalk.gray(`${p.queued} queued`) + " " : "") +
|
|
250
|
+
chalk.gray(`\u23F1 ${fmtDur(Date.now() - p.startedAt)}`));
|
|
251
|
+
// Stats line
|
|
252
|
+
const tokIn = fmtTokens(p.totalIn);
|
|
253
|
+
const tokOut = fmtTokens(p.totalOut);
|
|
254
|
+
const costStr = p.totalCost > 0 ? chalk.yellow(`$${p.totalCost.toFixed(2)}`) : "";
|
|
255
|
+
const waveLabel = p.waveNum >= 0 ? `wave ${p.waveNum + 1} \u00b7 ` : "";
|
|
256
|
+
const sessionStr = chalk.dim(` ${waveLabel}`) +
|
|
257
|
+
chalk.white(`${p.sessionsUsed}/${p.sessionsBudget}`) +
|
|
258
|
+
chalk.dim(` sessions \u00b7 ${p.remaining} left`);
|
|
259
|
+
out.push(chalk.gray(` \u2191 ${tokIn} in \u2193 ${tokOut} out`) +
|
|
260
|
+
(costStr ? ` ${costStr}` : "") +
|
|
261
|
+
sessionStr);
|
|
262
|
+
}
|
|
263
|
+
function renderFrame(swarm, showHotkeys, runInfo) {
|
|
19
264
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
20
265
|
const out = [];
|
|
21
266
|
// ── Header ──
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const bar = chalk.green("\u2588".repeat(filled)) +
|
|
26
|
-
chalk.gray("\u2591".repeat(barW - filled));
|
|
27
|
-
const stoppingTag = swarm.aborted ? chalk.yellow(" STOPPING") : "";
|
|
28
|
-
const phaseLabel = (swarm.phase === "planning"
|
|
29
|
-
? chalk.magenta(" PLANNING")
|
|
267
|
+
const stoppingTag = swarm.aborted ? chalk.yellow("STOPPING") : "";
|
|
268
|
+
const phaseLabel = swarm.phase === "planning"
|
|
269
|
+
? chalk.magenta("PLANNING")
|
|
30
270
|
: swarm.phase === "merging"
|
|
31
|
-
? chalk.yellow("
|
|
32
|
-
: ""
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
out
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
out.push(chalk.gray(` \u2191 ${tokIn} in \u2193 ${tokOut} out`) +
|
|
54
|
-
(costStr ? ` ${costStr}` : ""));
|
|
55
|
-
// ── Usage bar(s) — cycle through windows every 3s ──
|
|
271
|
+
? chalk.yellow("MERGING")
|
|
272
|
+
: "";
|
|
273
|
+
const phase = [phaseLabel, stoppingTag].filter(Boolean).join(" ");
|
|
274
|
+
const waveUsed = swarm.completed + swarm.failed;
|
|
275
|
+
renderHeader(out, w, {
|
|
276
|
+
model: runInfo?.model ?? swarm.model,
|
|
277
|
+
phase,
|
|
278
|
+
barPct: swarm.total > 0 ? swarm.completed / swarm.total : 0,
|
|
279
|
+
barLabel: `${swarm.completed}/${swarm.total}`,
|
|
280
|
+
active: swarm.active,
|
|
281
|
+
queued: swarm.pending,
|
|
282
|
+
startedAt: runInfo?.startedAt ?? swarm.startedAt,
|
|
283
|
+
totalIn: (runInfo?.accIn ?? 0) + swarm.totalInputTokens,
|
|
284
|
+
totalOut: (runInfo?.accOut ?? 0) + swarm.totalOutputTokens,
|
|
285
|
+
totalCost: (runInfo?.accCost ?? swarm.baseCostUsd) + swarm.totalCostUsd,
|
|
286
|
+
waveNum: runInfo?.waveNum ?? -1,
|
|
287
|
+
sessionsUsed: (runInfo ? runInfo.accCompleted + runInfo.accFailed : 0) + waveUsed,
|
|
288
|
+
sessionsBudget: runInfo?.sessionsBudget ?? swarm.total,
|
|
289
|
+
remaining: Math.max(0, (runInfo?.remaining ?? swarm.total) - waveUsed),
|
|
290
|
+
});
|
|
291
|
+
// ── Usage bar(s) ──
|
|
56
292
|
const windows = Array.from(swarm.rateLimitWindows.values());
|
|
57
293
|
const rlPct = swarm.rateLimitUtilization;
|
|
58
294
|
if (rlPct > 0 || swarm.rateLimitResetsAt || swarm.cappedOut || windows.length > 0) {
|
|
59
295
|
const barW = Math.min(30, w - 40);
|
|
60
296
|
const capFrac = swarm.usageCap;
|
|
61
297
|
const capMark = capFrac != null && capFrac < 1 ? Math.round(capFrac * barW) : -1;
|
|
62
|
-
// Show primary usage bar
|
|
63
298
|
const renderBar = (pct, windowLabel) => {
|
|
64
299
|
const filled = Math.round(pct * barW);
|
|
65
300
|
let barStr = "";
|
|
@@ -74,10 +309,10 @@ export function renderFrame(swarm, showHotkeys = false) {
|
|
|
74
309
|
let label = `${Math.round(pct * 100)}% used`;
|
|
75
310
|
if (swarm.cappedOut) {
|
|
76
311
|
if (swarm.isUsingOverage && !swarm.allowExtraUsage) {
|
|
77
|
-
label = chalk.red("Extra usage blocked
|
|
312
|
+
label = chalk.red("Extra usage blocked \u2014 stopping");
|
|
78
313
|
}
|
|
79
314
|
else {
|
|
80
|
-
label = chalk.yellow(`Capped at ${capFrac != null ? Math.round(capFrac * 100) : 100}%
|
|
315
|
+
label = chalk.yellow(`Capped at ${capFrac != null ? Math.round(capFrac * 100) : 100}% \u2014 finishing active`);
|
|
81
316
|
}
|
|
82
317
|
}
|
|
83
318
|
else if (swarm.rateLimitResetsAt && swarm.rateLimitResetsAt > Date.now()) {
|
|
@@ -93,13 +328,11 @@ export function renderFrame(swarm, showHotkeys = false) {
|
|
|
93
328
|
out.push(` ${prefix}${barStr} ${label}`);
|
|
94
329
|
};
|
|
95
330
|
if (windows.length > 1) {
|
|
96
|
-
// Cycle through windows every 3 seconds
|
|
97
331
|
const cycleIdx = Math.floor(Date.now() / 3000) % windows.length;
|
|
98
332
|
const win = windows[cycleIdx];
|
|
99
333
|
const shortName = WINDOW_SHORT_NAMES[win.type] ?? win.type.replace(/_/g, " ");
|
|
100
334
|
renderBar(win.utilization, shortName);
|
|
101
|
-
|
|
102
|
-
const dots = windows.map((_, i) => i === cycleIdx ? "●" : "○").join("");
|
|
335
|
+
const dots = windows.map((_, i) => i === cycleIdx ? "\u25CF" : "\u25CB").join("");
|
|
103
336
|
out[out.length - 1] += chalk.dim(` ${dots}`);
|
|
104
337
|
}
|
|
105
338
|
else {
|
|
@@ -119,7 +352,7 @@ export function renderFrame(swarm, showHotkeys = false) {
|
|
|
119
352
|
barStr += chalk.gray("\u2591");
|
|
120
353
|
}
|
|
121
354
|
const label = swarm.cappedOut
|
|
122
|
-
? chalk.red(`$${swarm.overageCostUsd.toFixed(2)}/$${swarm.extraUsageBudget}
|
|
355
|
+
? chalk.red(`$${swarm.overageCostUsd.toFixed(2)}/$${swarm.extraUsageBudget} \u2014 budget hit`)
|
|
123
356
|
: `$${swarm.overageCostUsd.toFixed(2)}/$${swarm.extraUsageBudget}`;
|
|
124
357
|
out.push(` ${chalk.dim("Extra ")}${barStr} ${label}`);
|
|
125
358
|
}
|
|
@@ -134,12 +367,10 @@ export function renderFrame(swarm, showHotkeys = false) {
|
|
|
134
367
|
" ".repeat(Math.max(1, w - 56)) +
|
|
135
368
|
"Action"));
|
|
136
369
|
out.push(chalk.gray(" " + "\u2500".repeat(Math.min(w - 4, 100))));
|
|
137
|
-
for (const a of show)
|
|
370
|
+
for (const a of show)
|
|
138
371
|
out.push(fmtRow(a, w));
|
|
139
|
-
|
|
140
|
-
if (swarm.pending > 0) {
|
|
372
|
+
if (swarm.pending > 0)
|
|
141
373
|
out.push(chalk.gray(` ... + ${swarm.pending} queued`));
|
|
142
|
-
}
|
|
143
374
|
}
|
|
144
375
|
// ── Merge results ──
|
|
145
376
|
if (swarm.mergeResults.length > 0) {
|
|
@@ -155,16 +386,11 @@ export function renderFrame(swarm, showHotkeys = false) {
|
|
|
155
386
|
}
|
|
156
387
|
// ── Event log ──
|
|
157
388
|
out.push("");
|
|
158
|
-
out.push(chalk.gray(" \u2500\u2500\u2500 Events " +
|
|
159
|
-
"\u2500".repeat(Math.min(w - 16, 90))));
|
|
389
|
+
out.push(chalk.gray(" \u2500\u2500\u2500 Events " + "\u2500".repeat(Math.min(w - 16, 90))));
|
|
160
390
|
const logN = Math.min(10, swarm.logs.length);
|
|
161
391
|
for (const entry of swarm.logs.slice(-logN)) {
|
|
162
|
-
const t = new Date(entry.time).toLocaleTimeString("en", {
|
|
163
|
-
|
|
164
|
-
});
|
|
165
|
-
const tag = entry.agentId < 0
|
|
166
|
-
? chalk.magenta("[sys]")
|
|
167
|
-
: chalk.cyan(`[${entry.agentId}]`);
|
|
392
|
+
const t = new Date(entry.time).toLocaleTimeString("en", { hour12: false });
|
|
393
|
+
const tag = entry.agentId < 0 ? chalk.magenta("[sys]") : chalk.cyan(`[${entry.agentId}]`);
|
|
168
394
|
out.push(chalk.gray(` ${t} `) + tag + ` ${colorEvent(truncate(entry.text, w - 22))}`);
|
|
169
395
|
}
|
|
170
396
|
if (showHotkeys)
|
|
@@ -172,184 +398,78 @@ export function renderFrame(swarm, showHotkeys = false) {
|
|
|
172
398
|
out.push("");
|
|
173
399
|
return out.join("\n");
|
|
174
400
|
}
|
|
175
|
-
function
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
?
|
|
183
|
-
:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
function truncate(s, max) {
|
|
209
|
-
return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
|
|
210
|
-
}
|
|
211
|
-
function fmtTokens(n) {
|
|
212
|
-
if (n >= 1_000_000)
|
|
213
|
-
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
214
|
-
if (n >= 1_000)
|
|
215
|
-
return `${(n / 1_000).toFixed(1)}K`;
|
|
216
|
-
return String(n);
|
|
217
|
-
}
|
|
218
|
-
function fmtDur(ms) {
|
|
219
|
-
const s = Math.floor(ms / 1000);
|
|
220
|
-
if (s < 60)
|
|
221
|
-
return `${s}s`;
|
|
222
|
-
const m = Math.floor(s / 60);
|
|
223
|
-
if (m < 60)
|
|
224
|
-
return `${m}m ${s % 60}s`;
|
|
225
|
-
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
226
|
-
}
|
|
227
|
-
export function startRenderLoop(swarm, liveConfig) {
|
|
228
|
-
if (!process.stdout.isTTY) {
|
|
229
|
-
return startPlainLog(swarm);
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
process.stdout.write("\x1B[?25l\x1B[2J\x1B[H");
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
return () => { };
|
|
236
|
-
}
|
|
237
|
-
// Live hotkey input state
|
|
238
|
-
let inputMode = "none";
|
|
239
|
-
let inputBuf = "";
|
|
240
|
-
const hasHotkeys = !!liveConfig && !!process.stdin.isTTY;
|
|
241
|
-
const render = () => {
|
|
242
|
-
let frame = renderFrame(swarm, hasHotkeys);
|
|
243
|
-
if (inputMode !== "none") {
|
|
244
|
-
const label = inputMode === "budget" ? "New budget (remaining sessions)" : "New usage cap (0-100%)";
|
|
245
|
-
frame += `\n ${chalk.cyan(">")} ${label}: ${inputBuf}█`;
|
|
246
|
-
}
|
|
247
|
-
return frame;
|
|
248
|
-
};
|
|
249
|
-
const interval = setInterval(() => {
|
|
250
|
-
try {
|
|
251
|
-
process.stdout.write("\x1B[H\x1B[J");
|
|
252
|
-
process.stdout.write(render());
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
clearInterval(interval);
|
|
256
|
-
}
|
|
257
|
-
}, 250);
|
|
258
|
-
// Keyboard listener for live controls
|
|
259
|
-
let keyHandler;
|
|
260
|
-
if (liveConfig && process.stdin.isTTY) {
|
|
261
|
-
try {
|
|
262
|
-
process.stdin.setRawMode(true);
|
|
263
|
-
process.stdin.resume();
|
|
264
|
-
}
|
|
265
|
-
catch { }
|
|
266
|
-
keyHandler = (buf) => {
|
|
267
|
-
const s = buf.toString();
|
|
268
|
-
if (inputMode !== "none") {
|
|
269
|
-
if (s === "\r" || s === "\n") {
|
|
270
|
-
const val = parseFloat(inputBuf);
|
|
271
|
-
if (inputMode === "budget" && !isNaN(val) && val > 0) {
|
|
272
|
-
liveConfig.remaining = Math.round(val);
|
|
273
|
-
liveConfig.dirty = true;
|
|
274
|
-
swarm.log(-1, `Budget changed to ${liveConfig.remaining} remaining`);
|
|
275
|
-
}
|
|
276
|
-
else if (inputMode === "threshold" && !isNaN(val) && val >= 0 && val <= 100) {
|
|
277
|
-
const frac = val / 100;
|
|
278
|
-
liveConfig.usageCap = frac > 0 ? frac : undefined;
|
|
279
|
-
liveConfig.dirty = true;
|
|
280
|
-
swarm.usageCap = liveConfig.usageCap;
|
|
281
|
-
swarm.log(-1, `Usage cap changed to ${val > 0 ? val + "%" : "unlimited"}`);
|
|
282
|
-
}
|
|
283
|
-
inputMode = "none";
|
|
284
|
-
inputBuf = "";
|
|
285
|
-
}
|
|
286
|
-
else if (s === "\x1B" || s === "\x03") {
|
|
287
|
-
inputMode = "none";
|
|
288
|
-
inputBuf = "";
|
|
289
|
-
}
|
|
290
|
-
else if (s === "\x7F") {
|
|
291
|
-
inputBuf = inputBuf.slice(0, -1);
|
|
292
|
-
}
|
|
293
|
-
else if (/^[0-9.]$/.test(s)) {
|
|
294
|
-
inputBuf += s;
|
|
295
|
-
}
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
if (s === "b" || s === "B") {
|
|
299
|
-
inputMode = "budget";
|
|
300
|
-
inputBuf = "";
|
|
301
|
-
}
|
|
302
|
-
else if (s === "t" || s === "T") {
|
|
303
|
-
inputMode = "threshold";
|
|
304
|
-
inputBuf = "";
|
|
401
|
+
function renderSteeringFrame(runInfo, steeringText, showHotkeys, rlGetter) {
|
|
402
|
+
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
403
|
+
const out = [];
|
|
404
|
+
const totalUsed = runInfo.accCompleted + runInfo.accFailed;
|
|
405
|
+
renderHeader(out, w, {
|
|
406
|
+
model: runInfo.model,
|
|
407
|
+
phase: chalk.magenta("STEERING"),
|
|
408
|
+
barPct: runInfo.sessionsBudget > 0 ? totalUsed / runInfo.sessionsBudget : 0,
|
|
409
|
+
barLabel: `${totalUsed}/${runInfo.sessionsBudget}`,
|
|
410
|
+
active: 0,
|
|
411
|
+
queued: 0,
|
|
412
|
+
startedAt: runInfo.startedAt,
|
|
413
|
+
totalIn: runInfo.accIn,
|
|
414
|
+
totalOut: runInfo.accOut,
|
|
415
|
+
totalCost: runInfo.accCost,
|
|
416
|
+
waveNum: runInfo.waveNum,
|
|
417
|
+
sessionsUsed: totalUsed,
|
|
418
|
+
sessionsBudget: runInfo.sessionsBudget,
|
|
419
|
+
remaining: runInfo.remaining,
|
|
420
|
+
});
|
|
421
|
+
// Usage bar from planner rate limit
|
|
422
|
+
const rl = rlGetter?.();
|
|
423
|
+
if (rl && (rl.utilization > 0 || rl.windows.size > 0)) {
|
|
424
|
+
const rlBarW = Math.min(30, w - 40);
|
|
425
|
+
const renderBar = (pct, label) => {
|
|
426
|
+
const f = Math.round(pct * rlBarW);
|
|
427
|
+
let barStr = "";
|
|
428
|
+
for (let i = 0; i < rlBarW; i++) {
|
|
429
|
+
if (i < f)
|
|
430
|
+
barStr += pct > 0.9 ? chalk.red("\u2588") : pct > 0.75 ? chalk.yellow("\u2588") : chalk.blue("\u2588");
|
|
431
|
+
else
|
|
432
|
+
barStr += chalk.gray("\u2591");
|
|
305
433
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
434
|
+
let lbl = `${Math.round(pct * 100)}% used`;
|
|
435
|
+
if (rl.isUsingOverage)
|
|
436
|
+
lbl += chalk.red(" [EXTRA USAGE]");
|
|
437
|
+
if (rl.resetsAt && rl.resetsAt > Date.now()) {
|
|
438
|
+
const waitSec = Math.ceil((rl.resetsAt - Date.now()) / 1000);
|
|
439
|
+
const mm = Math.floor(waitSec / 60), ss = waitSec % 60;
|
|
440
|
+
lbl = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
|
|
310
441
|
}
|
|
442
|
+
const prefix = label ? chalk.dim(label.padEnd(6)) : chalk.dim("Usage ");
|
|
443
|
+
out.push(` ${prefix}${barStr} ${lbl}`);
|
|
311
444
|
};
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
process.stdin.removeListener("data", keyHandler);
|
|
318
|
-
try {
|
|
319
|
-
process.stdin.setRawMode(false);
|
|
320
|
-
process.stdin.pause();
|
|
321
|
-
}
|
|
322
|
-
catch { }
|
|
445
|
+
if (rl.windows.size > 1) {
|
|
446
|
+
const wins = Array.from(rl.windows.values());
|
|
447
|
+
const idx = Math.floor(Date.now() / 3000) % wins.length;
|
|
448
|
+
const shortName = wins[idx].type.replace(/_/g, " ").slice(0, 5);
|
|
449
|
+
renderBar(wins[idx].utilization, shortName);
|
|
323
450
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
process.stdout.write(renderFrame(swarm));
|
|
327
|
-
process.stdout.write("\x1B[?25h");
|
|
451
|
+
else {
|
|
452
|
+
renderBar(rl.utilization);
|
|
328
453
|
}
|
|
329
|
-
|
|
330
|
-
|
|
454
|
+
}
|
|
455
|
+
out.push("");
|
|
456
|
+
out.push(chalk.gray(" " + "\u2500".repeat(Math.min(w - 4, 60))));
|
|
457
|
+
const clean = steeringText.replace(/\n/g, " ");
|
|
458
|
+
const maxTextW = w - 8;
|
|
459
|
+
out.push(` ${chalk.cyan("\u25C6")} ${clean.length > maxTextW ? clean.slice(0, maxTextW - 1) + "\u2026" : clean}`);
|
|
460
|
+
out.push("");
|
|
461
|
+
if (showHotkeys)
|
|
462
|
+
out.push(chalk.dim(" [b] budget [q] stop"));
|
|
463
|
+
out.push("");
|
|
464
|
+
return out.join("\n");
|
|
331
465
|
}
|
|
332
466
|
export function renderSummary(swarm) {
|
|
333
467
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
334
468
|
const out = [];
|
|
335
|
-
// Fixed column widths: #(3) + Status(6) + Duration(8) + Files(5) + Tools(5) + Cost(8)
|
|
336
|
-
// gaps: 6×2 = 12, indent = 2 → task gets the rest
|
|
337
469
|
const fixedW = 3 + 6 + 8 + 5 + 5 + 8 + 12 + 2;
|
|
338
470
|
const taskW = Math.max(10, w - fixedW);
|
|
339
|
-
const hdr = chalk.gray(" " +
|
|
340
|
-
"
|
|
341
|
-
" " +
|
|
342
|
-
"Status".padEnd(6) +
|
|
343
|
-
" " +
|
|
344
|
-
"Task".padEnd(taskW) +
|
|
345
|
-
" " +
|
|
346
|
-
"Duration".padStart(8) +
|
|
347
|
-
" " +
|
|
348
|
-
"Files".padStart(5) +
|
|
349
|
-
" " +
|
|
350
|
-
"Tools".padStart(5) +
|
|
351
|
-
" " +
|
|
352
|
-
"Cost".padStart(8));
|
|
471
|
+
const hdr = chalk.gray(" " + "#".padStart(3) + " " + "Status".padEnd(6) + " " + "Task".padEnd(taskW) +
|
|
472
|
+
" " + "Duration".padStart(8) + " " + "Files".padStart(5) + " " + "Tools".padStart(5) + " " + "Cost".padStart(8));
|
|
353
473
|
const sep = chalk.gray(" " + "\u2500".repeat(Math.min(w - 4, fixedW + taskW)));
|
|
354
474
|
out.push("");
|
|
355
475
|
out.push(hdr);
|
|
@@ -360,25 +480,16 @@ export function renderSummary(swarm) {
|
|
|
360
480
|
swarm.agents.filter(a => a.status === "error"),
|
|
361
481
|
].filter(g => g.length > 0);
|
|
362
482
|
const thinSep = chalk.gray(" " + "\u254C".repeat(Math.min(w - 4, fixedW + taskW)));
|
|
363
|
-
let totalDurMs = 0;
|
|
364
|
-
let totalFiles = 0;
|
|
365
|
-
let totalTools = 0;
|
|
366
|
-
let totalCost = 0;
|
|
483
|
+
let totalDurMs = 0, totalFiles = 0, totalTools = 0, totalCost = 0;
|
|
367
484
|
for (let gi = 0; gi < groups.length; gi++) {
|
|
368
485
|
if (gi > 0)
|
|
369
486
|
out.push(thinSep);
|
|
370
487
|
for (const a of groups[gi]) {
|
|
371
488
|
const id = String(a.id).padStart(3);
|
|
372
489
|
const ok = a.status === "done";
|
|
373
|
-
const status = ok
|
|
374
|
-
? chalk.green("\u2713 done")
|
|
375
|
-
: a.status === "running"
|
|
376
|
-
? chalk.blue("~ run ")
|
|
377
|
-
: chalk.red("\u2717 err ");
|
|
490
|
+
const status = ok ? chalk.green("\u2713 done") : a.status === "running" ? chalk.blue("~ run ") : chalk.red("\u2717 err ");
|
|
378
491
|
const task = truncate(a.task.prompt, taskW).padEnd(taskW);
|
|
379
|
-
const durMs = a.startedAt != null
|
|
380
|
-
? (a.finishedAt ?? Date.now()) - a.startedAt
|
|
381
|
-
: 0;
|
|
492
|
+
const durMs = a.startedAt != null ? (a.finishedAt ?? Date.now()) - a.startedAt : 0;
|
|
382
493
|
const dur = fmtDur(durMs).padStart(8);
|
|
383
494
|
const files = String(a.filesChanged ?? 0).padStart(5);
|
|
384
495
|
const tools = String(a.toolCalls).padStart(5);
|
|
@@ -392,44 +503,55 @@ export function renderSummary(swarm) {
|
|
|
392
503
|
}
|
|
393
504
|
}
|
|
394
505
|
out.push(sep);
|
|
395
|
-
// Totals row
|
|
396
506
|
const label = `${swarm.agents.length} tasks`.padEnd(taskW);
|
|
397
507
|
out.push(chalk.bold(` ${"".padStart(3)} ${"Total ".padEnd(6)} ${label} ${fmtDur(totalDurMs).padStart(8)} ${String(totalFiles).padStart(5)} ${String(totalTools).padStart(5)} ${`$${totalCost.toFixed(3)}`.padStart(8)}`));
|
|
398
508
|
out.push("");
|
|
399
509
|
return out.join("\n");
|
|
400
510
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
511
|
+
// ── Formatting helpers ──
|
|
512
|
+
function fmtRow(a, w) {
|
|
513
|
+
const id = String(a.id).padStart(3);
|
|
514
|
+
const elapsed = a.status === "running" && a.startedAt ? " " + chalk.dim(fmtDur(Date.now() - a.startedAt)) : "";
|
|
515
|
+
const spin = SPINNER[Math.floor(Date.now() / 250) % SPINNER.length];
|
|
516
|
+
const icon = a.status === "running"
|
|
517
|
+
? chalk.blue(`${spin} run`) + elapsed
|
|
518
|
+
: a.status === "done" ? chalk.green("\u2713 done") : chalk.red("\u2717 err ");
|
|
519
|
+
const taskW = Math.max(20, Math.min(36, w - 50));
|
|
520
|
+
const task = truncate(a.task.prompt, taskW).padEnd(taskW);
|
|
521
|
+
let action;
|
|
522
|
+
if (a.currentTool) {
|
|
523
|
+
action = chalk.yellow(a.currentTool);
|
|
524
|
+
}
|
|
525
|
+
else if (a.status === "running") {
|
|
526
|
+
action = chalk.dim(truncate(a.lastText || "...", 24));
|
|
527
|
+
}
|
|
528
|
+
else if (a.status === "done") {
|
|
529
|
+
const dur = fmtDur((a.finishedAt || Date.now()) - (a.startedAt || Date.now()));
|
|
530
|
+
const cost = a.costUsd != null ? ` $${a.costUsd.toFixed(3)}` : "";
|
|
531
|
+
const files = a.filesChanged != null && a.filesChanged > 0 ? chalk.dim(` ${a.filesChanged}f`) : "";
|
|
532
|
+
action = chalk.dim(`${dur}${cost}${files}`);
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
action = chalk.red(truncate(a.error || "error", 24));
|
|
536
|
+
}
|
|
537
|
+
return ` ${id} ${icon} ${task} ${action}`;
|
|
538
|
+
}
|
|
539
|
+
function truncate(s, max) {
|
|
540
|
+
return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
|
|
541
|
+
}
|
|
542
|
+
function fmtTokens(n) {
|
|
543
|
+
if (n >= 1_000_000)
|
|
544
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
545
|
+
if (n >= 1_000)
|
|
546
|
+
return `${(n / 1_000).toFixed(1)}K`;
|
|
547
|
+
return String(n);
|
|
548
|
+
}
|
|
549
|
+
function fmtDur(ms) {
|
|
550
|
+
const s = Math.floor(ms / 1000);
|
|
551
|
+
if (s < 60)
|
|
552
|
+
return `${s}s`;
|
|
553
|
+
const m = Math.floor(s / 60);
|
|
554
|
+
if (m < 60)
|
|
555
|
+
return `${m}m ${s % 60}s`;
|
|
556
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
435
557
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Run 10, 100, or 1000 Claude agents overnight. Parallel autonomous AI coding with thinking waves, iterative quality steering, crash recovery, and rate limit handling. Built on the Claude Agent SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|