claude-cup 0.4.1 → 0.7.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/web/index.html +127 -127
- package/mcp-server/src/index.js +25 -4
- package/package.json +1 -1
- package/src/aggregator.js +317 -297
- package/src/cli.js +55 -19
- package/src/leaderboard.js +370 -49
- package/src/statusline.js +71 -71
- package/src/tui.js +218 -20
- package/src/usage-api.js +250 -250
package/src/statusline.js
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
// `claude-cup statusline`: one-line meter for Claude Code's statusline.
|
|
2
|
-
// Claude Code (>= 2.1.80) pipes session JSON to the statusline command on
|
|
3
|
-
// stdin, including rate_limits for Pro/Max accounts. We parse it and print
|
|
4
|
-
// a single line. No network calls - this runs on every statusline refresh.
|
|
5
|
-
//
|
|
6
|
-
// Glyph language matches Claude Code itself: the ✻ spark, █░ meter,
|
|
7
|
-
// dim · separators. No emoji (renders everywhere, including conhost).
|
|
8
|
-
|
|
9
|
-
const CLAY = '\x1b[38;2;217;119;87m';
|
|
10
|
-
const EMBER = '\x1b[38;2;198;97;63m';
|
|
11
|
-
const BOLD = '\x1b[1m';
|
|
12
|
-
const DIM = '\x1b[2m';
|
|
13
|
-
const RESET = '\x1b[0m';
|
|
14
|
-
const SPARK = '\u273b'; // ✻
|
|
15
|
-
|
|
16
|
-
// All known fields are percentages (used_percentage 42 = 42%, utilization 1 = 1%).
|
|
17
|
-
function pctOf(bucket) {
|
|
18
|
-
if (!bucket || typeof bucket !== 'object') return null;
|
|
19
|
-
const v = bucket.used_percentage ?? bucket.utilization ?? bucket.percent;
|
|
20
|
-
if (typeof v !== 'number' || !Number.isFinite(v)) return null;
|
|
21
|
-
return Math.max(0, Math.min(100, v));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function countdown(resetsAt) {
|
|
25
|
-
if (!resetsAt) return null;
|
|
26
|
-
const ms = Date.parse(resetsAt) - Date.now();
|
|
27
|
-
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
28
|
-
const h = Math.floor(ms / 3600000);
|
|
29
|
-
const m = Math.floor((ms % 3600000) / 60000);
|
|
30
|
-
return h > 0 ? `${h}h${m}m` : `${m}m`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Pure formatter (exported for tests). Returns the statusline string. */
|
|
34
|
-
export function formatStatusline(input) {
|
|
35
|
-
const rl = input?.rate_limits || {};
|
|
36
|
-
const five = pctOf(rl.five_hour);
|
|
37
|
-
const seven = pctOf(rl.seven_day);
|
|
38
|
-
|
|
39
|
-
if (five === null && seven === null) {
|
|
40
|
-
return `${CLAY}${SPARK}${RESET} ${DIM}claude-cup${RESET}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const parts = [];
|
|
44
|
-
if (five !== null) {
|
|
45
|
-
const slots = 8;
|
|
46
|
-
const filled = Math.round((five / 100) * slots);
|
|
47
|
-
const color = five > 85 ? EMBER : CLAY;
|
|
48
|
-
const bar =
|
|
49
|
-
`${color}${'\u2588'.repeat(filled)}${RESET}` + `${DIM}${'\u2591'.repeat(slots - filled)}${RESET}`;
|
|
50
|
-
parts.push(`${color}${SPARK}${RESET} ${bar} ${BOLD}${Math.round(five)}%${RESET}`);
|
|
51
|
-
const reset = countdown(rl.five_hour?.resets_at);
|
|
52
|
-
if (reset) parts.push(`${DIM}\u21bb ${reset}${RESET}`);
|
|
53
|
-
}
|
|
54
|
-
if (seven !== null) {
|
|
55
|
-
const sevenStr = seven > 85 ? `${EMBER}${Math.round(seven)}%${RESET}` : `${Math.round(seven)}%`;
|
|
56
|
-
parts.push(`${DIM}7d${RESET} ${sevenStr}`);
|
|
57
|
-
}
|
|
58
|
-
return parts.join(` ${DIM}\u00b7${RESET} `);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function runStatusline() {
|
|
62
|
-
let raw = '';
|
|
63
|
-
for await (const chunk of process.stdin) raw += chunk;
|
|
64
|
-
let input = null;
|
|
65
|
-
try {
|
|
66
|
-
input = JSON.parse(raw);
|
|
67
|
-
} catch {
|
|
68
|
-
/* no/bad input: print the idle form */
|
|
69
|
-
}
|
|
70
|
-
process.stdout.write(formatStatusline(input) + '\n');
|
|
71
|
-
}
|
|
1
|
+
// `claude-cup statusline`: one-line meter for Claude Code's statusline.
|
|
2
|
+
// Claude Code (>= 2.1.80) pipes session JSON to the statusline command on
|
|
3
|
+
// stdin, including rate_limits for Pro/Max accounts. We parse it and print
|
|
4
|
+
// a single line. No network calls - this runs on every statusline refresh.
|
|
5
|
+
//
|
|
6
|
+
// Glyph language matches Claude Code itself: the ✻ spark, █░ meter,
|
|
7
|
+
// dim · separators. No emoji (renders everywhere, including conhost).
|
|
8
|
+
|
|
9
|
+
const CLAY = '\x1b[38;2;217;119;87m';
|
|
10
|
+
const EMBER = '\x1b[38;2;198;97;63m';
|
|
11
|
+
const BOLD = '\x1b[1m';
|
|
12
|
+
const DIM = '\x1b[2m';
|
|
13
|
+
const RESET = '\x1b[0m';
|
|
14
|
+
const SPARK = '\u273b'; // ✻
|
|
15
|
+
|
|
16
|
+
// All known fields are percentages (used_percentage 42 = 42%, utilization 1 = 1%).
|
|
17
|
+
function pctOf(bucket) {
|
|
18
|
+
if (!bucket || typeof bucket !== 'object') return null;
|
|
19
|
+
const v = bucket.used_percentage ?? bucket.utilization ?? bucket.percent;
|
|
20
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) return null;
|
|
21
|
+
return Math.max(0, Math.min(100, v));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function countdown(resetsAt) {
|
|
25
|
+
if (!resetsAt) return null;
|
|
26
|
+
const ms = Date.parse(resetsAt) - Date.now();
|
|
27
|
+
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
28
|
+
const h = Math.floor(ms / 3600000);
|
|
29
|
+
const m = Math.floor((ms % 3600000) / 60000);
|
|
30
|
+
return h > 0 ? `${h}h${m}m` : `${m}m`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Pure formatter (exported for tests). Returns the statusline string. */
|
|
34
|
+
export function formatStatusline(input) {
|
|
35
|
+
const rl = input?.rate_limits || {};
|
|
36
|
+
const five = pctOf(rl.five_hour);
|
|
37
|
+
const seven = pctOf(rl.seven_day);
|
|
38
|
+
|
|
39
|
+
if (five === null && seven === null) {
|
|
40
|
+
return `${CLAY}${SPARK}${RESET} ${DIM}claude-cup${RESET}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (five !== null) {
|
|
45
|
+
const slots = 8;
|
|
46
|
+
const filled = Math.round((five / 100) * slots);
|
|
47
|
+
const color = five > 85 ? EMBER : CLAY;
|
|
48
|
+
const bar =
|
|
49
|
+
`${color}${'\u2588'.repeat(filled)}${RESET}` + `${DIM}${'\u2591'.repeat(slots - filled)}${RESET}`;
|
|
50
|
+
parts.push(`${color}${SPARK}${RESET} ${bar} ${BOLD}${Math.round(five)}%${RESET}`);
|
|
51
|
+
const reset = countdown(rl.five_hour?.resets_at);
|
|
52
|
+
if (reset) parts.push(`${DIM}\u21bb ${reset}${RESET}`);
|
|
53
|
+
}
|
|
54
|
+
if (seven !== null) {
|
|
55
|
+
const sevenStr = seven > 85 ? `${EMBER}${Math.round(seven)}%${RESET}` : `${Math.round(seven)}%`;
|
|
56
|
+
parts.push(`${DIM}7d${RESET} ${sevenStr}`);
|
|
57
|
+
}
|
|
58
|
+
return parts.join(` ${DIM}\u00b7${RESET} `);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function runStatusline() {
|
|
62
|
+
let raw = '';
|
|
63
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
64
|
+
let input = null;
|
|
65
|
+
try {
|
|
66
|
+
input = JSON.parse(raw);
|
|
67
|
+
} catch {
|
|
68
|
+
/* no/bad input: print the idle form */
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write(formatStatusline(input) + '\n');
|
|
71
|
+
}
|
package/src/tui.js
CHANGED
|
@@ -56,6 +56,7 @@ export function leaderboardColorFor(pct) {
|
|
|
56
56
|
|
|
57
57
|
const mix = (a, b, t) => a.map((v, i) => Math.round(v + (b[i] - v) * t));
|
|
58
58
|
const fmt = (n) => (n >= 1e6 ? (n / 1e6).toFixed(2) + 'M' : n >= 1e3 ? (n / 1e3).toFixed(1) + 'k' : String(n ?? 0));
|
|
59
|
+
const fmtRank = (n) => Number(n || 0).toLocaleString('en-US');
|
|
59
60
|
|
|
60
61
|
function countdown(resetsAt) {
|
|
61
62
|
if (!resetsAt) return '';
|
|
@@ -235,8 +236,8 @@ function drawMascot(g, x, y, pose = {}) {
|
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
// ---- football animations for the mascot ----
|
|
238
|
-
export const MASCOT_ANIMS = ['kick', 'juggle', 'header', 'dribble', 'goal'];
|
|
239
|
-
export const ANIM_FRAMES = { kick: 16, juggle: 20, header: 16, dribble: 20, goal: 18 };
|
|
239
|
+
export const MASCOT_ANIMS = ['kick', 'juggle', 'header', 'dribble', 'goal', 'rainbow', 'bicycle', 'volley'];
|
|
240
|
+
export const ANIM_FRAMES = { kick: 16, juggle: 20, header: 16, dribble: 20, goal: 18, rainbow: 20, bicycle: 22, volley: 18 };
|
|
240
241
|
const CONFETTI = [
|
|
241
242
|
[106, 155, 204], // sky
|
|
242
243
|
[120, 140, 93], // olive
|
|
@@ -330,6 +331,139 @@ export function footballScene(anim, mx, my, cols) {
|
|
|
330
331
|
y: my + ((dy0 + Math.floor(f / 2)) % 5), // rows my..my+4
|
|
331
332
|
color: CONFETTI[(i + f) % CONFETTI.length],
|
|
332
333
|
}));
|
|
334
|
+
} else if (anim.name === 'rainbow') {
|
|
335
|
+
// Brazilian rainbow flick: ball goes from back heel, arcs OVER the head,
|
|
336
|
+
// lands in front. Path designed to avoid eye cells (mx+3,my+2) & (mx+8,my+2).
|
|
337
|
+
// Lookup table -- every frame has an explicit (dx, dy, pose) entry so the
|
|
338
|
+
// arc is glitch-free and reads cleanly even at 140ms ticks.
|
|
339
|
+
// NB: no shiftX during the arc. shiftX moves the eye columns
|
|
340
|
+
// (left eye = mx+3+shiftX, right eye = mx+8+shiftX), which would otherwise
|
|
341
|
+
// place an eye exactly where the ball or trail crosses my+2.
|
|
342
|
+
const path = [
|
|
343
|
+
// f 0-3: idle, ball at back heel
|
|
344
|
+
{ dx: 1, dy: 4, pose: { legPhase: 0 } },
|
|
345
|
+
{ dx: 1, dy: 4, pose: { legPhase: 1 } },
|
|
346
|
+
{ dx: 1, dy: 4, pose: { legPhase: 0 } },
|
|
347
|
+
{ dx: 1, dy: 4, pose: { legPhase: 1 } },
|
|
348
|
+
// f 4: heel flick
|
|
349
|
+
{ dx: 1, dy: 3, pose: {} },
|
|
350
|
+
// f 5-11: arc OVER the head, skipping eye cells (no shiftX)
|
|
351
|
+
{ dx: 2, dy: 2, pose: {} }, // 5 -- past hip (mx+2 != mx+3 eye)
|
|
352
|
+
{ dx: 4, dy: 1, pose: { armTwitch: 1 } }, // 6 -- rising, above eye row
|
|
353
|
+
{ dx: 5, dy: 0, pose: { armTwitch: 1 } }, // 7 -- peak left
|
|
354
|
+
{ dx: 7, dy: 0, pose: { armTwitch: 1 } }, // 8 -- peak right
|
|
355
|
+
{ dx: 9, dy: 1, pose: { armTwitch: 1 } }, // 9 -- descending, above eye row
|
|
356
|
+
{ dx: 10, dy: 2, pose: {} }, // 10 -- past right eye (mx+10 != mx+8)
|
|
357
|
+
{ dx: 12, dy: 3, pose: {} }, // 11 -- approaching ground
|
|
358
|
+
// f 12: landed in front
|
|
359
|
+
{ dx: 13, dy: 4, pose: { legPhase: 1 } },
|
|
360
|
+
// f 13-19: settles, mascot returns to neutral (safe to shift now -- ball far from eye row)
|
|
361
|
+
{ dx: 13, dy: 4, pose: { shiftX: 1, legPhase: 0 } },
|
|
362
|
+
{ dx: 13, dy: 4, pose: { shiftX: 1, legPhase: 1 } },
|
|
363
|
+
{ dx: 13, dy: 4, pose: { legPhase: 0 } },
|
|
364
|
+
{ dx: 13, dy: 4, pose: { legPhase: 1 } },
|
|
365
|
+
{ dx: 13, dy: 4, pose: { legPhase: 0 } },
|
|
366
|
+
{ dx: 13, dy: 4, pose: { legPhase: 1 } },
|
|
367
|
+
{ dx: 13, dy: 4, pose: { legPhase: 0 } },
|
|
368
|
+
];
|
|
369
|
+
const step = path[Math.min(f, path.length - 1)];
|
|
370
|
+
out.ball = { x: mx + step.dx, y: my + step.dy };
|
|
371
|
+
out.pose = step.pose;
|
|
372
|
+
// Trail: a single dim dot one cell behind the rising arc (frames 5-11 only),
|
|
373
|
+
// placed on the same y so it never crosses the eye row.
|
|
374
|
+
if (f >= 5 && f <= 11) {
|
|
375
|
+
const prev = path[f - 1];
|
|
376
|
+
out.trail = [{ x: mx + prev.dx, y: my + prev.dy }];
|
|
377
|
+
}
|
|
378
|
+
} else if (anim.name === 'bicycle') {
|
|
379
|
+
// Bicycle kick: ball drops from upper right, mascot leaps (armsUp signals
|
|
380
|
+
// the backwards leap), strikes mid-air, ball ROCKETS out left with trail.
|
|
381
|
+
// Path skips eye cells (mx+3,my+2) and (mx+8,my+2) by going through my+1
|
|
382
|
+
// when crossing eye columns.
|
|
383
|
+
const path = [
|
|
384
|
+
// f 0-2: ball drops in from upper right
|
|
385
|
+
{ ball: { dx: 15, dy: 0 }, pose: {} },
|
|
386
|
+
{ ball: { dx: 14, dy: 1 }, pose: {} },
|
|
387
|
+
{ ball: { dx: 13, dy: 2 }, pose: {} },
|
|
388
|
+
// f 3-4: mascot leaps (armsUp), ball at chest height
|
|
389
|
+
{ ball: { dx: 13, dy: 3 }, pose: { armsUp: true } },
|
|
390
|
+
{ ball: { dx: 13, dy: 3 }, pose: { armsUp: true } },
|
|
391
|
+
// f 5: STRIKE -- armsUp + kick
|
|
392
|
+
{ ball: { dx: 12, dy: 3 }, pose: { armsUp: true, kick: 1 } },
|
|
393
|
+
// f 6-9: ball rockets out LEFT, picking altitude as it goes
|
|
394
|
+
{ ball: { dx: 10, dy: 2 }, pose: { armsUp: true, kick: 1 } }, // 6
|
|
395
|
+
{ ball: { dx: 7, dy: 2 }, pose: { armsUp: true } }, // 7 -- skips mx+8 col
|
|
396
|
+
{ ball: { dx: 4, dy: 1 }, pose: { armsUp: true } }, // 8 -- above eye row
|
|
397
|
+
{ ball: { dx: 1, dy: 1 }, pose: {} }, // 9 -- almost out
|
|
398
|
+
// f 10: ball exits off-screen left (clipped by drawFootball bounds)
|
|
399
|
+
{ ball: { dx: -2, dy: 1 }, pose: {} },
|
|
400
|
+
// f 11-15: mascot lands and steadies
|
|
401
|
+
{ ball: null, pose: { legPhase: 1 } },
|
|
402
|
+
{ ball: null, pose: { legPhase: 0 } },
|
|
403
|
+
{ ball: null, pose: { legPhase: 1 } },
|
|
404
|
+
{ ball: null, pose: { legPhase: 0 } },
|
|
405
|
+
{ ball: null, pose: { legPhase: 1 } },
|
|
406
|
+
// f 16-21: hold pause (ta-da beat)
|
|
407
|
+
{ ball: null, pose: {} },
|
|
408
|
+
{ ball: null, pose: {} },
|
|
409
|
+
{ ball: null, pose: {} },
|
|
410
|
+
{ ball: null, pose: {} },
|
|
411
|
+
{ ball: null, pose: {} },
|
|
412
|
+
{ ball: null, pose: {} },
|
|
413
|
+
];
|
|
414
|
+
const step = path[Math.min(f, path.length - 1)];
|
|
415
|
+
out.pose = step.pose;
|
|
416
|
+
if (step.ball) out.ball = { x: mx + step.ball.dx, y: my + step.ball.dy };
|
|
417
|
+
// Trail: two dots EAST of the ball on the same y (frames 6-9 only).
|
|
418
|
+
// Same-y avoids ever landing on the eye row.
|
|
419
|
+
if (f >= 6 && f <= 9 && step.ball) {
|
|
420
|
+
const by = my + step.ball.dy;
|
|
421
|
+
out.trail = [
|
|
422
|
+
{ x: mx + step.ball.dx + 2, y: by },
|
|
423
|
+
{ x: mx + step.ball.dx + 4, y: by },
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
} else if (anim.name === 'volley') {
|
|
427
|
+
// Side-foot volley: ball arcs in from the left at chest height, mascot
|
|
428
|
+
// strikes a flat volley out to the right. Existing pose.kick (right foot)
|
|
429
|
+
// matches the right-going strike direction.
|
|
430
|
+
const path = [
|
|
431
|
+
// f 0-2: ball arcs in from off-screen left, above eye row
|
|
432
|
+
{ ball: { dx: -4, dy: 1 }, pose: {} },
|
|
433
|
+
{ ball: { dx: -1, dy: 1 }, pose: {} },
|
|
434
|
+
{ ball: { dx: 2, dy: 1 }, pose: {} },
|
|
435
|
+
// f 3-4: mascot leans into it, ball descends to strike zone
|
|
436
|
+
{ ball: { dx: 5, dy: 2 }, pose: { shiftX: 1 } }, // 3 -- between eyes safely
|
|
437
|
+
{ ball: { dx: 9, dy: 3 }, pose: { shiftX: 1 } }, // 4 -- below eye row
|
|
438
|
+
// f 5: STRIKE
|
|
439
|
+
{ ball: { dx: 12, dy: 3 }, pose: { shiftX: 1, kick: 1, armTwitch: 1 } },
|
|
440
|
+
// f 6-9: rocketing right, picking altitude
|
|
441
|
+
{ ball: { dx: 14, dy: 2 }, pose: { kick: 1 } },
|
|
442
|
+
{ ball: { dx: 17, dy: 1 }, pose: {} },
|
|
443
|
+
{ ball: { dx: 20, dy: 0 }, pose: {} },
|
|
444
|
+
{ ball: { dx: 23, dy: 0 }, pose: {} },
|
|
445
|
+
// f 10-12: ball exits off-screen right (clipped by drawFootball bounds)
|
|
446
|
+
{ ball: { dx: 27, dy: 0 }, pose: {} },
|
|
447
|
+
{ ball: { dx: 31, dy: 0 }, pose: {} },
|
|
448
|
+
{ ball: { dx: 36, dy: 0 }, pose: {} },
|
|
449
|
+
// f 13-17: mascot resumes tippy-tap
|
|
450
|
+
{ ball: null, pose: { legPhase: 1 } },
|
|
451
|
+
{ ball: null, pose: { legPhase: 0 } },
|
|
452
|
+
{ ball: null, pose: { legPhase: 1 } },
|
|
453
|
+
{ ball: null, pose: { legPhase: 0 } },
|
|
454
|
+
{ ball: null, pose: { legPhase: 1 } },
|
|
455
|
+
];
|
|
456
|
+
const step = path[Math.min(f, path.length - 1)];
|
|
457
|
+
out.pose = step.pose;
|
|
458
|
+
if (step.ball) out.ball = { x: mx + step.ball.dx, y: my + step.ball.dy };
|
|
459
|
+
// Trail: two dots WEST of the ball on the same y (frames 6-10 only).
|
|
460
|
+
if (f >= 6 && f <= 10 && step.ball) {
|
|
461
|
+
const by = my + step.ball.dy;
|
|
462
|
+
out.trail = [
|
|
463
|
+
{ x: mx + step.ball.dx - 2, y: by },
|
|
464
|
+
{ x: mx + step.ball.dx - 4, y: by },
|
|
465
|
+
];
|
|
466
|
+
}
|
|
333
467
|
}
|
|
334
468
|
return out;
|
|
335
469
|
}
|
|
@@ -405,8 +539,10 @@ export function composeFrame(state) {
|
|
|
405
539
|
|
|
406
540
|
g.text(2, 0, ` ${spark} claude cup `, { fg: CLAY, bold: true });
|
|
407
541
|
const now = new Date();
|
|
542
|
+
const cupDays = typeof state.cupDaysLeft === 'number' ? state.cupDaysLeft : null;
|
|
543
|
+
const cupSuffix = cupDays === null ? '' : (cupDays > 0 ? ` \u00b7 cup d-${cupDays}` : ' \u00b7 cup ended');
|
|
408
544
|
const dateLabel = ` ${now.toLocaleDateString('en-US', { weekday: 'long' })} \u00b7 ${now
|
|
409
|
-
.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} `.toLowerCase();
|
|
545
|
+
.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}${cupSuffix} `.toLowerCase();
|
|
410
546
|
g.text(cols - 3 - dateLabel.length, 0, dateLabel, eyebrow);
|
|
411
547
|
|
|
412
548
|
// ---- the golden trophy ----
|
|
@@ -580,6 +716,59 @@ export function composeFrame(state) {
|
|
|
580
716
|
g.text(sx + 15, y, String(stats.buildRate ?? 0), { fg: CLAY, bold: true });
|
|
581
717
|
y++;
|
|
582
718
|
}
|
|
719
|
+
// Leaderboard -- clean 3-row standings, name + rank only.
|
|
720
|
+
// ABOVE name #rank (gold, matches cup elite color)
|
|
721
|
+
// RANK you #rank (highlighted clay, the user's own row)
|
|
722
|
+
// BELOW name #rank
|
|
723
|
+
const lbState = state.leaderboard;
|
|
724
|
+
if (lbState && lbState.rank > 0) {
|
|
725
|
+
const tierColor = lbState.tier === 'Master' ? GOLD :
|
|
726
|
+
lbState.tier === 'Craftsman' ? CLAY :
|
|
727
|
+
lbState.tier === 'Builder' ? KRAFT : CLOUD;
|
|
728
|
+
const nameCol = 15;
|
|
729
|
+
const rankCol = 28; // wide enough to fit longest names neatly
|
|
730
|
+
// Fallback: derive neighbor ranks from user rank if cache predates nextRank/belowRank
|
|
731
|
+
const nextRank = lbState.nextRank || Math.max(1, lbState.rank - 1);
|
|
732
|
+
const belowRank = lbState.belowRank || (lbState.rank + 1);
|
|
733
|
+
|
|
734
|
+
// Row 1: ABOVE (or overtake banner if recent)
|
|
735
|
+
const justOvertook = lbState.passedTs && (Date.now() - lbState.passedTs) < 5000;
|
|
736
|
+
if (y < rows - 2) {
|
|
737
|
+
if (justOvertook && lbState.passedName && lbState.passedVerb) {
|
|
738
|
+
g.put(sx, y, '\u273b', { fg: CLAY });
|
|
739
|
+
const bannerTxt = `you just ${lbState.passedVerb} ${lbState.passedName}`.slice(0, colW - 2);
|
|
740
|
+
g.text(sx + 2, y, bannerTxt, { fg: CLAY, bold: true });
|
|
741
|
+
} else if (lbState.nextName) {
|
|
742
|
+
g.text(sx, y, 'ABOVE'.padEnd(nameCol), eyebrow);
|
|
743
|
+
g.text(sx + nameCol, y, String(lbState.nextName).slice(0, rankCol - nameCol - 1),
|
|
744
|
+
{ fg: GOLD, bold: true });
|
|
745
|
+
g.text(sx + rankCol, y, `#${fmtRank(nextRank)}`, { fg: GOLD, bold: true });
|
|
746
|
+
}
|
|
747
|
+
y++;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Row 2: RANK -- the user's own row, highlighted clay, rank + tier
|
|
751
|
+
if (y < rows - 2) {
|
|
752
|
+
g.text(sx, y, 'RANK'.padEnd(nameCol), { fg: CLAY, bold: true });
|
|
753
|
+
g.text(sx + nameCol, y, 'you', { fg: CLAY, bold: true });
|
|
754
|
+
const rankTxt = `#${fmtRank(lbState.rank)}`;
|
|
755
|
+
g.text(sx + rankCol, y, rankTxt, { fg: CLAY, bold: true });
|
|
756
|
+
const tierStart = sx + rankCol + rankTxt.length + 1;
|
|
757
|
+
const tierTxt = `\u00b7 ${lbState.tier}`;
|
|
758
|
+
if (tierStart + tierTxt.length <= sx + colW) {
|
|
759
|
+
g.text(tierStart, y, tierTxt, { fg: tierColor, bold: true });
|
|
760
|
+
}
|
|
761
|
+
y++;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Row 3: BELOW
|
|
765
|
+
if (y < rows - 2 && lbState.belowName) {
|
|
766
|
+
g.text(sx, y, 'BELOW'.padEnd(nameCol), eyebrow);
|
|
767
|
+
g.text(sx + nameCol, y, String(lbState.belowName).slice(0, rankCol - nameCol - 1), { fg: CLOUD });
|
|
768
|
+
g.text(sx + rankCol, y, `#${fmtRank(belowRank)}`, { fg: CLOUD });
|
|
769
|
+
y++;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
583
772
|
if (y < rows - 2) {
|
|
584
773
|
const GREEN = [76, 175, 80];
|
|
585
774
|
g.text(sx, y, 'ECO MODE'.padEnd(15), { fg: GREEN });
|
|
@@ -593,22 +782,8 @@ export function composeFrame(state) {
|
|
|
593
782
|
y++;
|
|
594
783
|
}
|
|
595
784
|
statRow('TOKENS TODAY', fmt(stats.totalTokens));
|
|
596
|
-
const eff = stats.assistantMessages > 0 ? `${fmt(Math.round(stats.totalTokens / stats.assistantMessages))} tok/reply` : '\u2014';
|
|
597
|
-
statRow('EFFICIENCY', eff);
|
|
598
|
-
statRow('REPLIES', fmt(stats.assistantMessages));
|
|
599
785
|
statRow('EST. COST', `$${(stats.cost ?? 0).toFixed(2)}`);
|
|
600
786
|
statRow('BURN RATE', `${fmt(stats.burnRate)} tok/min`);
|
|
601
|
-
if (usage?.fiveHour) {
|
|
602
|
-
const tl = usage.timeLeft;
|
|
603
|
-
if (!tl) statRow('TIME LEFT', 'measuring pace...', { fg: CLOUD, dim: true });
|
|
604
|
-
else if (tl.outlasts) statRow('TIME LEFT', 'lasts past reset', { bold: true });
|
|
605
|
-
else {
|
|
606
|
-
const h = Math.floor(tl.minutes / 60);
|
|
607
|
-
const m = Math.round(tl.minutes % 60);
|
|
608
|
-
const dur = h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
609
|
-
statRow('TIME LEFT', `~${dur} at this pace`, tl.minutes < 30 ? { fg: EMBER, bold: true } : { bold: true });
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
787
|
if (usage?.sevenDay) {
|
|
613
788
|
const sd = usage.sevenDay;
|
|
614
789
|
const mini = 8;
|
|
@@ -643,7 +818,7 @@ export function composeFrame(state) {
|
|
|
643
818
|
}
|
|
644
819
|
|
|
645
820
|
// startTui uses surfaceRelFor + bodyHeightFor for the droplet/bubble physics.
|
|
646
|
-
export function startTui({ aggregator, poller, watcher, eco, url, getPower, getLeaderboard, out = process.stdout }) {
|
|
821
|
+
export function startTui({ aggregator, poller, watcher, eco, url, getPower, getLeaderboard, getCupDaysLeft, out = process.stdout }) {
|
|
647
822
|
let colorMode = 24;
|
|
648
823
|
try {
|
|
649
824
|
if (out.hasColors && !out.hasColors(16777216)) colorMode = 256;
|
|
@@ -663,6 +838,7 @@ export function startTui({ aggregator, poller, watcher, eco, url, getPower, getL
|
|
|
663
838
|
let mascotAnim = null; // { name, frame, frames } | null
|
|
664
839
|
let lastAnim = null;
|
|
665
840
|
let nextAnimAt = Date.now() + nextAnimDelay();
|
|
841
|
+
let prevLb = null; // last leaderboard snapshot, for celebration triggers
|
|
666
842
|
|
|
667
843
|
const spawn = () => {
|
|
668
844
|
if (falling.length > 26) return;
|
|
@@ -686,7 +862,8 @@ export function startTui({ aggregator, poller, watcher, eco, url, getPower, getL
|
|
|
686
862
|
const seedCount = Math.min(24, snap.toolCalls + snap.assistantMessages);
|
|
687
863
|
for (let i = 0; i < seedCount; i++) setTimeout(() => spawn(), 300 + i * 170);
|
|
688
864
|
|
|
689
|
-
|
|
865
|
+
// alt screen, hide cursor, set tab title (OSC 0 + 1 + 2 for maximum compatibility)
|
|
866
|
+
out.write('\x1b[?1049h\x1b[?25l\x1b]0;claude-cup\x07\x1b]1;claude-cup\x07\x1b]2;claude-cup\x07');
|
|
690
867
|
|
|
691
868
|
const render = () => {
|
|
692
869
|
frame++;
|
|
@@ -695,7 +872,21 @@ export function startTui({ aggregator, poller, watcher, eco, url, getPower, getL
|
|
|
695
872
|
const hasLimit = usage && usage.fiveHour && (usage.status === 'ok' || usage.status === 'stale');
|
|
696
873
|
const lb = typeof getLeaderboard === 'function' ? getLeaderboard() : null;
|
|
697
874
|
const br = stats.buildRate ?? 0;
|
|
698
|
-
const fill =
|
|
875
|
+
const fill = (lb && lb.percentile > 0)
|
|
876
|
+
? lb.percentile
|
|
877
|
+
: Math.min(95, Math.round(Math.sqrt(br) * 42));
|
|
878
|
+
|
|
879
|
+
// Celebration: rank improved by 10+ OR tier upgraded -> fire goal animation
|
|
880
|
+
if (lb && lb.rank > 0) {
|
|
881
|
+
const tierOrder = { 'Builder': 0, 'Rising Builder': 1, 'Master Builder': 2, 'Elite Builder': 3 };
|
|
882
|
+
const tierUp = prevLb && tierOrder[lb.tier] > tierOrder[prevLb.tier];
|
|
883
|
+
const rankJump = prevLb && prevLb.rank > 0 && (prevLb.rank - lb.rank) >= 10;
|
|
884
|
+
if (!mascotAnim && (tierUp || rankJump)) {
|
|
885
|
+
mascotAnim = { name: 'goal', frame: 0, frames: ANIM_FRAMES.goal };
|
|
886
|
+
lastAnim = 'goal';
|
|
887
|
+
}
|
|
888
|
+
prevLb = lb;
|
|
889
|
+
}
|
|
699
890
|
|
|
700
891
|
const rows = Math.min(out.rows || 24, 32);
|
|
701
892
|
const bodyH = bodyHeightFor(rows);
|
|
@@ -767,10 +958,17 @@ export function startTui({ aggregator, poller, watcher, eco, url, getPower, getL
|
|
|
767
958
|
url,
|
|
768
959
|
colorMode,
|
|
769
960
|
mascot: mascotAnim,
|
|
961
|
+
leaderboard: lb,
|
|
962
|
+
cupDaysLeft: typeof getCupDaysLeft === 'function' ? getCupDaysLeft() : null,
|
|
770
963
|
powerLevel: power.powerLevel,
|
|
771
964
|
richness: power.richness,
|
|
772
965
|
});
|
|
773
966
|
out.write('\x1b[H' + frameStr + '\x1b[J');
|
|
967
|
+
// Re-assert the tab/window title once per second (every 7th render at 140ms).
|
|
968
|
+
// Defends against VS Code / Cursor / shell prompts overwriting our title.
|
|
969
|
+
if (frame % 7 === 0) {
|
|
970
|
+
out.write('\x1b]0;claude-cup\x07\x1b]2;claude-cup\x07');
|
|
971
|
+
}
|
|
774
972
|
|
|
775
973
|
// advance the animation for the next tick; reschedule when it finishes
|
|
776
974
|
if (mascotAnim) {
|