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 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 { startRenderLoop, renderSummary } from "./ui.js";
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 stopThinkRender = startRenderLoop(thinkingSwarm, { remaining: 0, usageCap, dirty: false });
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
- stopThinkRender();
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
- process.stdout.write("\x1B[?25l");
1235
- const restore = () => process.stdout.write("\x1B[?25h\n");
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
- console.log(chalk.cyan(`\n ◆ Assessing...\n`));
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, makeProgressLog(), memory);
1339
- process.stdout.write(`\x1B[2K\r`);
1340
- process.stdout.write("\x1B[?25h");
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
- console.log(chalk.dim(` ${steer.reasoning}`));
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
- console.log(chalk.dim(` ${steer.reasoning}`));
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
- process.stdout.write("\x1B[?25h");
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
- if (flex) {
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
- const stopRender = startRenderLoop(swarm, liveConfig);
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
- stopRender();
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
- finally {
1410
- stopRender();
1411
- console.log(renderSummary(swarm));
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
- console.log(chalk.cyan(`\n ◆ Assessing...\n`));
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, makeProgressLog(), memory);
1465
- process.stdout.write(`\x1B[2K\r`);
1466
- process.stdout.write("\x1B[?25h");
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
- console.log(chalk.dim(` ${steer.reasoning}`));
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; // re-steer — steerer will see the hint
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
- console.log(chalk.dim(` ${steer.reasoning}`));
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; // re-steer
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
- process.stdout.write("\x1B[?25h");
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; // steering failed — stop, don't re-run old tasks
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
- export declare function renderFrame(swarm: Swarm, showHotkeys?: boolean): string;
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
- export declare function startRenderLoop(swarm: Swarm, liveConfig?: LiveConfig): () => void;
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
- export function renderFrame(swarm, showHotkeys = false) {
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 barW = Math.min(30, w - 50);
23
- const pct = swarm.total > 0 ? swarm.completed / swarm.total : 0;
24
- const filled = Math.round(pct * barW);
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(" MERGING")
32
- : "") + stoppingTag;
33
- const modelTag = swarm.model ? chalk.dim(` [${swarm.model}]`) : "";
34
- out.push("");
35
- out.push(` ${chalk.bold.white("CLAUDE OVERNIGHT")}${modelTag}${phaseLabel} ${bar} ` +
36
- `${swarm.completed}/${swarm.total} ` +
37
- chalk.cyan(`${swarm.active} active`) +
38
- " " +
39
- chalk.gray(`${swarm.pending} queued`) +
40
- " " +
41
- chalk.gray(`\u23F1 ${fmtDur(Date.now() - swarm.startedAt)}`));
42
- // Stats line — show wave cost + overall if there's a base
43
- const tokIn = fmtTokens(swarm.totalInputTokens);
44
- const tokOut = fmtTokens(swarm.totalOutputTokens);
45
- const waveCost = swarm.totalCostUsd;
46
- const totalCost = swarm.baseCostUsd + waveCost;
47
- let costStr = "";
48
- if (totalCost > 0) {
49
- costStr = swarm.baseCostUsd > 0
50
- ? chalk.yellow(`$${waveCost.toFixed(3)}`) + chalk.dim(` / $${totalCost.toFixed(2)} total`)
51
- : chalk.yellow(`$${waveCost.toFixed(3)}`);
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 stopping");
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}% finishing active`);
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
- // Show dots indicator for which window we're viewing
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} budget hit`)
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
- hour12: false,
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 fmtRow(a, w) {
176
- const id = String(a.id).padStart(3);
177
- const elapsed = a.status === "running" && a.startedAt
178
- ? " " + chalk.dim(fmtDur(Date.now() - a.startedAt))
179
- : "";
180
- const spin = SPINNER[Math.floor(Date.now() / 250) % SPINNER.length];
181
- const icon = a.status === "running"
182
- ? chalk.blue(`${spin} run`) + elapsed
183
- : a.status === "done"
184
- ? chalk.green("\u2713 done")
185
- : chalk.red("\u2717 err ");
186
- const taskW = Math.max(20, Math.min(36, w - 50));
187
- const task = truncate(a.task.prompt, taskW).padEnd(taskW);
188
- let action;
189
- if (a.currentTool) {
190
- action = chalk.yellow(a.currentTool);
191
- }
192
- else if (a.status === "running") {
193
- action = chalk.dim(truncate(a.lastText || "...", 24));
194
- }
195
- else if (a.status === "done") {
196
- const dur = fmtDur((a.finishedAt || Date.now()) - (a.startedAt || Date.now()));
197
- const cost = a.costUsd != null ? ` $${a.costUsd.toFixed(3)}` : "";
198
- const files = a.filesChanged != null && a.filesChanged > 0
199
- ? chalk.dim(` ${a.filesChanged}f`)
200
- : "";
201
- action = chalk.dim(`${dur}${cost}${files}`);
202
- }
203
- else {
204
- action = chalk.red(truncate(a.error || "error", 24));
205
- }
206
- return ` ${id} ${icon} ${task} ${action}`;
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
- else if (s === "q" || s === "Q" || s === "\x03") {
307
- if (swarm.aborted)
308
- process.exit(0); // second press = force quit
309
- swarm.abort();
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
- process.stdin.on("data", keyHandler);
313
- }
314
- return () => {
315
- clearInterval(interval);
316
- if (keyHandler) {
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
- try {
325
- process.stdout.write("\x1B[H\x1B[J");
326
- process.stdout.write(renderFrame(swarm));
327
- process.stdout.write("\x1B[?25h");
451
+ else {
452
+ renderBar(rl.utilization);
328
453
  }
329
- catch { }
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
- "#".padStart(3) +
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
- function startPlainLog(swarm) {
402
- let lastSeq = swarm.logSequence;
403
- let lastCompleted = -1;
404
- const write = (line) => {
405
- try {
406
- process.stdout.write(line + "\n");
407
- }
408
- catch {
409
- clearInterval(interval);
410
- }
411
- };
412
- const interval = setInterval(() => {
413
- const currentSeq = swarm.logSequence;
414
- if (currentSeq > lastSeq) {
415
- // Read the most recent (currentSeq - lastSeq) entries from the tail of the log
416
- const newCount = currentSeq - lastSeq;
417
- const available = swarm.logs.length;
418
- const toShow = Math.min(newCount, available);
419
- for (const entry of swarm.logs.slice(available - toShow)) {
420
- const t = new Date(entry.time).toLocaleTimeString("en", { hour12: false });
421
- const tag = entry.agentId < 0 ? "[sys]" : `[${entry.agentId}]`;
422
- write(`${t} ${tag} ${entry.text}`);
423
- }
424
- lastSeq = currentSeq;
425
- }
426
- if (swarm.completed !== lastCompleted) {
427
- lastCompleted = swarm.completed;
428
- write(`progress: ${swarm.completed}/${swarm.total} done, ${swarm.active} active, ${swarm.pending} queued`);
429
- }
430
- }, 500);
431
- return () => {
432
- clearInterval(interval);
433
- write(`done: ${swarm.completed}/${swarm.total} tasks, ${fmtDur(Date.now() - swarm.startedAt)}`);
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.6.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": {