ccclub 0.3.6 → 0.3.8

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.
Files changed (2) hide show
  1. package/dist/index.js +121 -94
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command, Option } from "commander";
6
6
  // src/commands/init.ts
7
7
  import { createInterface as createInterface2 } from "readline/promises";
8
8
  import { stdin, stdout } from "process";
9
- import chalk4 from "chalk";
9
+ import chalk5 from "chalk";
10
10
  import ora2 from "ora";
11
11
 
12
12
  // src/config.ts
@@ -585,7 +585,7 @@ function normalizeRawUsage(value) {
585
585
  const cachedInputTokens = asNumber(record.cached_input_tokens ?? record.cache_read_input_tokens);
586
586
  const outputTokens = asNumber(record.output_tokens);
587
587
  const reasoningTokens = asNumber(record.reasoning_output_tokens);
588
- const fallbackTotal = inputTokens + outputTokens + reasoningTokens;
588
+ const fallbackTotal = inputTokens + outputTokens;
589
589
  const totalTokens = asNumber(record.total_tokens) || fallbackTotal;
590
590
  return { inputTokens, cachedInputTokens, outputTokens, reasoningTokens, totalTokens };
591
591
  }
@@ -669,9 +669,9 @@ async function collectCodexUsage() {
669
669
  outputTokens: rawUsage.outputTokens,
670
670
  cacheCreationTokens: 0,
671
671
  cacheReadTokens,
672
- reasoningTokens: rawUsage.reasoningTokens,
672
+ reasoningTokens: 0,
673
673
  totalTokens,
674
- costUSD: calculateCost(model, inputTokens, rawUsage.outputTokens, 0, cacheReadTokens, rawUsage.reasoningTokens)
674
+ costUSD: calculateCost(model, inputTokens, rawUsage.outputTokens, 0, cacheReadTokens)
675
675
  });
676
676
  turns.push({ source, timestamp, key: dedupeKey });
677
677
  });
@@ -1197,7 +1197,7 @@ async function fetchUsageLimits() {
1197
1197
  }
1198
1198
 
1199
1199
  // src/commands/sync.ts
1200
- var SYNC_FORMAT_VERSION = "7";
1200
+ var SYNC_FORMAT_VERSION = "8";
1201
1201
  function getSyncVersionPath() {
1202
1202
  return join11(homedir11(), CCCLUB_CONFIG_DIR, "sync-version");
1203
1203
  }
@@ -1355,7 +1355,32 @@ function getLatestBlockStartBySource(blocks) {
1355
1355
 
1356
1356
  // src/global-install.ts
1357
1357
  import { exec as exec2 } from "child_process";
1358
+ import chalk4 from "chalk";
1359
+
1360
+ // src/theme.ts
1358
1361
  import chalk3 from "chalk";
1362
+ var theme = {
1363
+ title: chalk3.hex("#F1EDE7").bold,
1364
+ text: chalk3.hex("#E8E4DE"),
1365
+ muted: chalk3.hex("#8A8480"),
1366
+ faint: chalk3.hex("#5A5550"),
1367
+ brand: chalk3.hex("#D4935E"),
1368
+ link: chalk3.hex("#7AB7C6").underline,
1369
+ linkText: chalk3.hex("#7AB7C6"),
1370
+ success: chalk3.hex("#63B486"),
1371
+ successBold: chalk3.hex("#63B486").bold,
1372
+ gold: chalk3.hex("#D6B56D"),
1373
+ goldBold: chalk3.hex("#D6B56D").bold,
1374
+ silver: chalk3.hex("#AEB7BF"),
1375
+ silverBold: chalk3.hex("#AEB7BF").bold,
1376
+ bronze: chalk3.hex("#C58A61"),
1377
+ bronzeBold: chalk3.hex("#C58A61"),
1378
+ warning: chalk3.hex("#D4A85C"),
1379
+ warningBold: chalk3.hex("#D4A85C").bold,
1380
+ danger: chalk3.hex("#D26A6A")
1381
+ };
1382
+
1383
+ // src/global-install.ts
1359
1384
  function run(cmd) {
1360
1385
  return new Promise((resolve2) => {
1361
1386
  exec2(cmd, (err, stdout5) => resolve2(err ? "" : stdout5.trim()));
@@ -1364,13 +1389,13 @@ function run(cmd) {
1364
1389
  async function ensureGlobalInstall() {
1365
1390
  const globalList = await run("npm list -g ccclub --depth=0");
1366
1391
  if (globalList.includes("ccclub@")) return;
1367
- console.log(chalk3.dim("\n Installing ccclub globally so you can run it directly..."));
1392
+ console.log(chalk4.dim("\n Installing ccclub globally so you can run it directly..."));
1368
1393
  const result = await run("npm install -g ccclub");
1369
1394
  if (result) {
1370
- console.log(chalk3.green(" Done!") + chalk3.dim(" You can now use ") + chalk3.white("ccclub") + chalk3.dim(" directly."));
1395
+ console.log(theme.success(" Done!") + chalk4.dim(" You can now use ") + theme.text("ccclub") + chalk4.dim(" directly."));
1371
1396
  } else {
1372
- console.log(chalk3.dim(" Could not auto-install. Run manually:"));
1373
- console.log(chalk3.white(" npm install -g ccclub"));
1397
+ console.log(chalk4.dim(" Could not auto-install. Run manually:"));
1398
+ console.log(theme.text(" npm install -g ccclub"));
1374
1399
  }
1375
1400
  }
1376
1401
 
@@ -1378,28 +1403,28 @@ async function ensureGlobalInstall() {
1378
1403
  async function initCommand() {
1379
1404
  const existing = await loadConfig();
1380
1405
  if (existing) {
1381
- console.log(chalk4.yellow("Already initialized!"));
1406
+ console.log(chalk5.yellow("Already initialized!"));
1382
1407
  console.log(` User: ${existing.displayName}`);
1383
1408
  console.log(` Groups: ${existing.groups.join(", ") || "(none)"}`);
1384
1409
  if (!isHookInstalled()) {
1385
1410
  const hookOk = await installHook();
1386
- if (hookOk) console.log(chalk4.green(" Auto-sync hook installed!"));
1411
+ if (hookOk) console.log(chalk5.green(" Auto-sync hook installed!"));
1387
1412
  }
1388
1413
  if (!isHeartbeatInstalled()) {
1389
1414
  const heartbeatOk = await installHeartbeat();
1390
- if (heartbeatOk) console.log(chalk4.green(" Background sync installed!"));
1415
+ if (heartbeatOk) console.log(chalk5.green(" Background sync installed!"));
1391
1416
  }
1392
- console.log(chalk4.dim('\n Run "ccclub" to see the leaderboard'));
1417
+ console.log(chalk5.dim('\n Run "ccclub" to see the leaderboard'));
1393
1418
  return;
1394
1419
  }
1395
1420
  const rl = createInterface2({ input: stdin, output: stdout });
1396
1421
  try {
1397
1422
  const defaultName = getDefaultDisplayName();
1398
- const prompt = defaultName ? chalk4.bold(`Your display name (${defaultName}): `) : chalk4.bold("Your display name: ");
1423
+ const prompt = defaultName ? chalk5.bold(`Your display name (${defaultName}): `) : chalk5.bold("Your display name: ");
1399
1424
  const input = await rl.question(prompt);
1400
1425
  const displayName = input.trim() || defaultName || "";
1401
1426
  if (!displayName) {
1402
- console.error(chalk4.red("Name cannot be empty"));
1427
+ console.error(chalk5.red("Name cannot be empty"));
1403
1428
  return;
1404
1429
  }
1405
1430
  const spinner = ora2("Setting up...").start();
@@ -1436,17 +1461,17 @@ async function initCommand() {
1436
1461
  ]);
1437
1462
  spinner.succeed("ccclub initialized!");
1438
1463
  if (!hookOk) {
1439
- console.log(chalk4.dim(' Tip: run "ccclub hook" to set up auto-sync'));
1464
+ console.log(chalk5.dim(' Tip: run "ccclub hook" to set up auto-sync'));
1440
1465
  }
1441
1466
  if (!heartbeatOk) {
1442
- console.log(chalk4.dim(' Tip: run "ccclub sync" manually to refresh non-Claude agent usage'));
1467
+ console.log(chalk5.dim(' Tip: run "ccclub sync" manually to refresh non-Claude agent usage'));
1443
1468
  }
1444
1469
  console.log("");
1445
- console.log(chalk4.bold(" Invite friends to compete:"));
1470
+ console.log(chalk5.bold(" Invite friends to compete:"));
1446
1471
  console.log("");
1447
- console.log(` ${chalk4.cyan.underline(`${apiUrl}/invite/${data.groupCode}`)}`);
1472
+ console.log(` ${theme.link(`${apiUrl}/invite/${data.groupCode}`)}`);
1448
1473
  console.log("");
1449
- console.log(chalk4.dim(` or: npx ccclub join ${data.groupCode}`));
1474
+ console.log(chalk5.dim(` or: npx ccclub join ${data.groupCode}`));
1450
1475
  console.log("");
1451
1476
  await doSync(true);
1452
1477
  await ensureGlobalInstall();
@@ -1457,14 +1482,14 @@ async function initCommand() {
1457
1482
  }
1458
1483
  function printQuickStart() {
1459
1484
  console.log("");
1460
- console.log(chalk4.dim(" Run ") + chalk4.white("ccclub") + chalk4.dim(" to see the leaderboard. ") + chalk4.white("ccclub -h") + chalk4.dim(" for all commands."));
1485
+ console.log(chalk5.dim(" Run ") + theme.text("ccclub") + chalk5.dim(" to see the leaderboard. ") + theme.text("ccclub -h") + chalk5.dim(" for all commands."));
1461
1486
  console.log("");
1462
1487
  }
1463
1488
 
1464
1489
  // src/commands/join.ts
1465
1490
  import { createInterface as createInterface3 } from "readline/promises";
1466
1491
  import { stdin as stdin2, stdout as stdout2 } from "process";
1467
- import chalk5 from "chalk";
1492
+ import chalk6 from "chalk";
1468
1493
  import ora3 from "ora";
1469
1494
  async function joinCommand(inviteCode) {
1470
1495
  let config = await loadConfig();
@@ -1478,11 +1503,11 @@ async function joinCommand(inviteCode) {
1478
1503
  const rl = createInterface3({ input: stdin2, output: stdout2 });
1479
1504
  try {
1480
1505
  const defaultName = getDefaultDisplayName();
1481
- const prompt = defaultName ? chalk5.bold(`Your display name (${defaultName}): `) : chalk5.bold("Your display name: ");
1506
+ const prompt = defaultName ? chalk6.bold(`Your display name (${defaultName}): `) : chalk6.bold("Your display name: ");
1482
1507
  const input = await rl.question(prompt);
1483
1508
  displayName = input.trim() || defaultName || "";
1484
1509
  if (!displayName) {
1485
- console.error(chalk5.red("Name cannot be empty"));
1510
+ console.error(chalk6.red("Name cannot be empty"));
1486
1511
  return;
1487
1512
  }
1488
1513
  } finally {
@@ -1531,12 +1556,11 @@ async function joinCommand(inviteCode) {
1531
1556
  await ensureGlobalInstall();
1532
1557
  }
1533
1558
  console.log("");
1534
- console.log(chalk5.dim(" Run ") + chalk5.white("ccclub") + chalk5.dim(" to see the leaderboard. ") + chalk5.white("ccclub -h") + chalk5.dim(" for all commands."));
1559
+ console.log(chalk6.dim(" Run ") + chalk6.white("ccclub") + chalk6.dim(" to see the leaderboard. ") + chalk6.white("ccclub -h") + chalk6.dim(" for all commands."));
1535
1560
  console.log("");
1536
1561
  }
1537
1562
 
1538
1563
  // src/commands/rank.ts
1539
- import chalk6 from "chalk";
1540
1564
  import Table from "cli-table3";
1541
1565
  import ora4 from "ora";
1542
1566
 
@@ -1595,11 +1619,11 @@ async function rankCommand(options) {
1595
1619
  Usage: ccclub -d <period>
1596
1620
 
1597
1621
  Options:
1598
- ${chalk6.white("ccclub -d 1")} Yesterday
1599
- ${chalk6.white("ccclub -d 7")} Last 7 days
1600
- ${chalk6.white("ccclub -d 30")} Last 30 days
1601
- ${chalk6.white("ccclub -d all")} All time
1602
- ${chalk6.white("ccclub")} Today (default)
1622
+ ${theme.text("ccclub -d 1")} Yesterday
1623
+ ${theme.text("ccclub -d 7")} Last 7 days
1624
+ ${theme.text("ccclub -d 30")} Last 30 days
1625
+ ${theme.text("ccclub -d all")} All time
1626
+ ${theme.text("ccclub")} Today (default)
1603
1627
  `;
1604
1628
  if (options.days) {
1605
1629
  if (options.days === true) {
@@ -1609,7 +1633,7 @@ async function rankCommand(options) {
1609
1633
  const DAYS_MAP = { "1": "yesterday", "7": "weekly", "30": "monthly", "all": "all-time" };
1610
1634
  const mapped = DAYS_MAP[options.days];
1611
1635
  if (!mapped) {
1612
- console.log(chalk6.red(`
1636
+ console.log(theme.danger(`
1613
1637
  Unknown value: -d ${options.days}`));
1614
1638
  console.log(DAYS_HINT);
1615
1639
  return;
@@ -1633,7 +1657,7 @@ async function rankCommand(options) {
1633
1657
  codes = config.groups.length > 0 ? config.groups : [];
1634
1658
  }
1635
1659
  if (codes.length === 0) {
1636
- console.log(chalk6.red("No group found. Run 'ccclub init' or 'ccclub join <code>' first."));
1660
+ console.log(theme.danger("No group found. Run 'ccclub init' or 'ccclub join <code>' first."));
1637
1661
  return;
1638
1662
  }
1639
1663
  const localUsagePromise = fetchUsageLimits().catch(() => null);
@@ -1671,7 +1695,7 @@ async function rankCommand(options) {
1671
1695
  for (let i = 0; i < groupResults.length; i++) {
1672
1696
  const { code, rankData, activityData, range } = groupResults[i];
1673
1697
  if (!rankData) {
1674
- console.log(chalk6.red(`
1698
+ console.log(theme.danger(`
1675
1699
  Couldn't load leaderboard for ${code}`));
1676
1700
  continue;
1677
1701
  }
@@ -1683,10 +1707,10 @@ async function rankCommand(options) {
1683
1707
  if (activityData) renderActivity(activityData, range);
1684
1708
  if (i < groupResults.length - 1) console.log("");
1685
1709
  }
1686
- console.log(chalk6.dim("\n Tokens = input + output + reasoning ") + chalk6.yellow("(cache excluded)") + chalk6.dim(". Use ") + chalk6.white("--cache") + chalk6.dim(" to include cache tokens."));
1710
+ console.log(theme.muted("\n Tokens = input + output + reasoning ") + theme.warning("(cache excluded)") + theme.muted(". Use ") + theme.text("--cache") + theme.muted(" to include cache tokens."));
1687
1711
  const update = await getUpdateResult();
1688
1712
  if (update) {
1689
- console.log(chalk6.yellow("\n Update available") + chalk6.dim(`: ${update.current} \u2192 ${update.latest} Run `) + chalk6.cyan("npm i -g ccclub@latest"));
1713
+ console.log(theme.warningBold("\n Update available") + theme.muted(`: ${update.current} \u2192 ${update.latest} Run `) + theme.linkText("npm i -g ccclub@latest"));
1690
1714
  }
1691
1715
  } catch (err) {
1692
1716
  spinner.fail(`Error: ${formatFetchError(err)}`);
@@ -1709,26 +1733,25 @@ function formatTokens(n) {
1709
1733
  }
1710
1734
  function printGroup(data, code, period, config, showCache = false, showAll = false) {
1711
1735
  if (data.rankings.length === 0) {
1712
- console.log(chalk6.bold(`
1736
+ console.log(theme.title(`
1713
1737
  ${data.group.name}`));
1714
- console.log(chalk6.yellow(" No data for this period yet"));
1715
- console.log(chalk6.dim(" Sync your data first: ccclub sync"));
1738
+ console.log(theme.warning(" No data for this period yet"));
1739
+ console.log(theme.muted(" Sync your data first: ccclub sync"));
1716
1740
  return;
1717
1741
  }
1718
- console.log(chalk6.bold(`
1742
+ console.log(theme.title(`
1719
1743
  ${data.group.name}`));
1720
1744
  const periodLabel = { daily: "TODAY", yesterday: "YESTERDAY", weekly: "7 DAYS", monthly: "30 DAYS", "all-time": "ALL TIME" };
1721
1745
  const now = Date.now();
1722
1746
  const activeCount = data.rankings.filter((r) => r.lastSync && now - new Date(r.lastSync).getTime() < ACTIVE_THRESHOLD_MS).length;
1723
- console.log(chalk6.dim(` ${periodLabel[period] || period.toUpperCase()} \xB7 ${data.start.slice(0, 10)} \u2192 ${data.end.slice(0, 10)} \xB7 ${data.group.memberCount} members`));
1747
+ console.log(theme.muted(` ${periodLabel[period] || period.toUpperCase()} \xB7 ${data.start.slice(0, 10)} \u2192 ${data.end.slice(0, 10)} \xB7 ${data.group.memberCount} members`));
1724
1748
  if (activeCount > 0) {
1725
- console.log(chalk6.green(` ${activeCount} active`));
1749
+ console.log(theme.success(` ${activeCount} active`));
1726
1750
  }
1727
1751
  console.log("");
1728
1752
  const activeRankings = showAll || data.rankings.length <= 15 ? data.rankings : data.rankings.filter((r) => r.costUSD > 0 || r.userId === config.userId);
1729
1753
  const hiddenCount = data.rankings.length - activeRankings.length;
1730
1754
  const hasPlan = activeRankings.some((r) => r.plan);
1731
- const hasUsage = activeRankings.some((r) => r.usageSnapshot);
1732
1755
  const hasAgents = activeRankings.some(
1733
1756
  (r) => r.agents && r.agents.length > 0 && !(r.agents.length === 1 && r.agents[0] === "claude")
1734
1757
  );
@@ -1746,8 +1769,7 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
1746
1769
  tokens: formatTokens(tokens),
1747
1770
  roi,
1748
1771
  turns: String(entry.chatCount),
1749
- perTurn: entry.chatCount > 0 ? `$${(entry.costUSD / entry.chatCount).toFixed(2)}` : "\u2014",
1750
- usage: entry.usageSnapshot ? `${Math.round(entry.usageSnapshot.sevenDay)}%` : "\u2014"
1772
+ perTurn: entry.chatCount > 0 ? `$${(entry.costUSD / entry.chatCount).toFixed(2)}` : "\u2014"
1751
1773
  };
1752
1774
  });
1753
1775
  const head = ["#", "Name", "Cost", "Tokens"];
@@ -1759,7 +1781,7 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
1759
1781
  ];
1760
1782
  if (hasAgents) {
1761
1783
  head.splice(2, 0, "Agents");
1762
- widths.splice(2, 0, columnWidth("Agents", plainRows.map((r) => r.agents), 6, 24));
1784
+ widths.splice(2, 0, columnWidth("Agents", plainRows.map((r) => r.agents), 6, 28));
1763
1785
  }
1764
1786
  if (hasPlan) {
1765
1787
  head.push("ROI");
@@ -1770,61 +1792,66 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
1770
1792
  columnWidth("Turns", plainRows.map((r) => r.turns), 3, 6),
1771
1793
  columnWidth("$/Turn", plainRows.map((r) => r.perTurn), 6, 7)
1772
1794
  );
1773
- if (hasUsage) {
1774
- head.push("Usage");
1775
- widths.push(columnWidth("Usage", plainRows.map((r) => r.usage), 5, 6));
1776
- }
1777
1795
  const table = new Table({
1778
- head: head.map((h) => chalk6.cyan(h)),
1779
- style: { head: [], border: [] },
1796
+ head: head.map((h) => theme.linkText(h)),
1797
+ style: { head: [], border: ["gray"] },
1780
1798
  colWidths: widths
1781
1799
  });
1782
1800
  for (const plain of plainRows) {
1783
1801
  const { entry } = plain;
1784
1802
  const isMe = entry.userId === config.userId;
1785
- const marker = isMe ? chalk6.green("\u2192") : " ";
1786
- const id = (s) => s;
1787
- const c = isMe ? chalk6.green : entry.rank === 1 ? chalk6.yellow : id;
1788
- const nameC = isMe ? chalk6.green.bold : entry.rank === 1 ? chalk6.yellow.bold : id;
1803
+ const rowStyle = isMe ? theme.success : podiumStyle(entry.rank);
1804
+ const nameStyle = isMe ? theme.successBold : podiumNameStyle(entry.rank);
1805
+ const rankStyle = podiumNameStyle(entry.rank);
1806
+ const marker = isMe ? theme.success("\u2192") : " ";
1789
1807
  const nameWidth = Math.max(widths[1] - 2, 4);
1790
- const displayName = plain.isActive ? `${chalk6.green("\u25CF")} ${nameC(truncateDisplay(entry.displayName, Math.max(nameWidth - 2, 1)))}` : nameC(truncateDisplay(entry.displayName, nameWidth));
1808
+ const displayName = plain.isActive ? `${theme.success("\u25CF")} ${nameStyle(truncateDisplay(entry.displayName, Math.max(nameWidth - 2, 1)))}` : nameStyle(truncateDisplay(entry.displayName, nameWidth));
1791
1809
  const row = [
1792
- `${marker}${c(String(entry.rank))}`,
1810
+ `${marker}${rankStyle(String(entry.rank))}`,
1793
1811
  displayName
1794
1812
  ];
1795
1813
  if (hasAgents) {
1796
1814
  const agentWidth = Math.max(widths[2] - 2, 4);
1797
- row.push(c(truncateDisplay(plain.agents, agentWidth)));
1815
+ row.push(rowStyle(truncateDisplay(plain.agents, agentWidth)));
1798
1816
  }
1799
- row.push(c(plain.cost), c(plain.tokens));
1817
+ row.push(rowStyle(plain.cost), rowStyle(plain.tokens));
1800
1818
  if (hasPlan) {
1801
1819
  row.push(colorRoi(plain.roi, entry));
1802
1820
  }
1803
- row.push(c(plain.turns));
1804
- row.push(entry.chatCount > 0 ? c(plain.perTurn) : chalk6.dim("\u2014"));
1805
- if (hasUsage) {
1806
- row.push(entry.usageSnapshot ? c(plain.usage) : chalk6.dim("\u2014"));
1807
- }
1821
+ row.push(rowStyle(plain.turns));
1822
+ row.push(entry.chatCount > 0 ? rowStyle(plain.perTurn) : theme.faint("\u2014"));
1808
1823
  table.push(row);
1809
1824
  }
1810
1825
  console.log(table.toString());
1811
1826
  if (hiddenCount > 0) {
1812
- console.log(chalk6.dim(` ${hiddenCount} inactive member${hiddenCount > 1 ? "s" : ""} hidden \xB7 ccclub --all to show`));
1827
+ console.log(theme.muted(` ${hiddenCount} inactive member${hiddenCount > 1 ? "s" : ""} hidden \xB7 ccclub --all to show`));
1813
1828
  }
1814
- console.log(chalk6.dim(" Dashboard: ") + chalk6.green(`${config.apiUrl}/g/${code}`));
1829
+ console.log(theme.muted(" Dashboard: ") + theme.link(`${config.apiUrl}/g/${code}`));
1815
1830
  if (code !== "global") {
1816
- console.log(chalk6.dim(" Invite: ") + chalk6.hex("#d4935e").underline(`${config.apiUrl}/invite/${code}`));
1831
+ console.log(theme.muted(" Invite: ") + theme.link(`${config.apiUrl}/invite/${code}`));
1817
1832
  }
1818
1833
  if (hasPlan) {
1819
1834
  const me = data.rankings.find((r) => r.userId === config.userId);
1820
1835
  if (me && !me.plan) {
1821
- console.log(chalk6.dim(" Set your plan: ") + chalk6.white("ccclub profile --plan pro|max100|max200|api"));
1836
+ console.log(theme.muted(" Set your plan: ") + theme.text("ccclub profile --plan pro|max100|max200|api"));
1822
1837
  }
1823
1838
  }
1824
1839
  }
1825
1840
  function isEntryActive(entry, now) {
1826
1841
  return Boolean(entry.lastSync && now - new Date(entry.lastSync).getTime() < ACTIVE_THRESHOLD_MS);
1827
1842
  }
1843
+ function podiumStyle(rank) {
1844
+ if (rank === 1) return theme.gold;
1845
+ if (rank === 2) return theme.silver;
1846
+ if (rank === 3) return theme.bronze;
1847
+ return theme.text;
1848
+ }
1849
+ function podiumNameStyle(rank) {
1850
+ if (rank === 1) return theme.goldBold;
1851
+ if (rank === 2) return theme.silverBold;
1852
+ if (rank === 3) return theme.bronzeBold;
1853
+ return theme.text;
1854
+ }
1828
1855
  function formatRoi(entry, hasPlan) {
1829
1856
  if (!hasPlan) return "";
1830
1857
  if (entry.plan && entry.plan !== "api") {
@@ -1841,16 +1868,16 @@ function colorRoi(roiStr, entry) {
1841
1868
  const price = PLAN_PRICES[entry.plan];
1842
1869
  const monthly = entry.monthlyCostUSD || 0;
1843
1870
  const roi = price > 0 ? Math.round(monthly / price * 100) : 0;
1844
- return roi >= 100 ? chalk6.green.bold(roiStr) : roi >= 50 ? chalk6.yellow(roiStr) : chalk6.dim(roiStr);
1871
+ return roi >= 100 ? theme.successBold(roiStr) : roi >= 50 ? theme.warning(roiStr) : theme.faint(roiStr);
1845
1872
  }
1846
- return chalk6.dim(roiStr);
1873
+ return theme.faint(roiStr);
1847
1874
  }
1848
1875
  function formatAgents(entry) {
1849
1876
  if (entry.agentBreakdown && entry.agentBreakdown.length > 0) {
1850
1877
  if (entry.agentBreakdown.length === 1) {
1851
1878
  return formatAgentLabel(entry.agentBreakdown[0].source);
1852
1879
  }
1853
- const visible = entry.agentBreakdown.slice(0, 2).map((agent) => `${formatAgentLabel(agent.source)} ${agent.percent}%`);
1880
+ const visible = entry.agentBreakdown.slice(0, 2).map((agent) => `${formatAgentLabel(agent.source)} (${agent.percent}%)`);
1854
1881
  if (entry.agentBreakdown.length > visible.length) {
1855
1882
  visible.push(`+${entry.agentBreakdown.length - visible.length}`);
1856
1883
  }
@@ -1917,7 +1944,7 @@ function renderActivity(data, range) {
1917
1944
  }
1918
1945
  }
1919
1946
  if (globalMax === 0) globalMax = 1;
1920
- console.log(chalk6.dim(`
1947
+ console.log(theme.muted(`
1921
1948
  Activity (${range})`));
1922
1949
  for (let i = 0; i < active.length; i++) {
1923
1950
  const user = active[i];
@@ -1929,22 +1956,10 @@ function renderActivity(data, range) {
1929
1956
  return SPARK_CHARS[idx];
1930
1957
  }).join("");
1931
1958
  const total = user.blocks.reduce((s, b) => s + b.cost, 0);
1932
- const displayWidth = [...user.displayName].reduce((w, ch) => w + (ch.charCodeAt(0) > 127 ? 2 : 1), 0);
1933
1959
  const maxWidth = 12;
1934
- let name = user.displayName;
1935
- if (displayWidth > maxWidth) {
1936
- let w = 0;
1937
- let cut = 0;
1938
- for (const ch of name) {
1939
- const cw = ch.charCodeAt(0) > 127 ? 2 : 1;
1940
- if (w + cw > maxWidth) break;
1941
- w += cw;
1942
- cut++;
1943
- }
1944
- name = [...name].slice(0, cut).join("");
1945
- }
1946
- const pad = " ".repeat(Math.max(0, maxWidth - displayWidth));
1947
- console.log(` ${chalk6.dim(name + pad)} ${spark} ${chalk6.dim("$" + total.toFixed(2))}`);
1960
+ const name = truncateDisplay(user.displayName, maxWidth);
1961
+ const pad = " ".repeat(Math.max(0, maxWidth - visualWidth(name)));
1962
+ console.log(` ${theme.muted(name + pad)} ${theme.brand(spark)} ${theme.muted("$" + total.toFixed(2))}`);
1948
1963
  }
1949
1964
  const axisArr = new Array(bucketCount).fill(" ");
1950
1965
  if (range === "24h" || range === "yesterday") {
@@ -1967,7 +1982,7 @@ function renderActivity(data, range) {
1967
1982
  for (let c = 0; c < label.length && b + c < bucketCount; c++) axisArr[b + c] = label[c];
1968
1983
  }
1969
1984
  }
1970
- console.log(chalk6.dim(" " + " ".repeat(12) + " " + axisArr.join("")));
1985
+ console.log(theme.faint(" " + " ".repeat(12) + " " + axisArr.join("")));
1971
1986
  }
1972
1987
 
1973
1988
  // src/commands/profile.ts
@@ -2137,7 +2152,7 @@ async function createGroupCommand() {
2137
2152
  }
2138
2153
  spinner.succeed(`Created "${data.groupName}"`);
2139
2154
  console.log("");
2140
- console.log(` ${chalk9.cyan.underline(`${config.apiUrl}/invite/${data.groupCode}`)}`);
2155
+ console.log(` ${theme.link(`${config.apiUrl}/invite/${data.groupCode}`)}`);
2141
2156
  console.log("");
2142
2157
  console.log(chalk9.dim(` or: npx ccclub join ${data.groupCode}`));
2143
2158
  } finally {
@@ -2237,10 +2252,14 @@ async function hookCommand() {
2237
2252
  }
2238
2253
 
2239
2254
  // src/index.ts
2240
- var VERSION = "0.3.6";
2255
+ var VERSION = "0.3.8";
2241
2256
  startUpdateCheck(VERSION);
2242
2257
  var program = new Command();
2243
- program.name("ccclub").description("Coding agent usage leaderboard among friends").version(VERSION, "-v, -V, --version");
2258
+ if (process.argv.slice(2).includes("-v")) {
2259
+ console.log(VERSION);
2260
+ process.exit(0);
2261
+ }
2262
+ program.name("ccclub").description("Claude Code, Codex, OpenCode, Amp, and pi-agent usage leaderboard among friends").version(VERSION);
2244
2263
  program.command("rank", { isDefault: true, hidden: true }).description("Show leaderboard").option("-d, --days [days]", "Time window: 1 | 7 | 30 | all (default: today)").addOption(new Option("-p, --period [period]").hideHelp()).option("-g, --group <code>", "Group invite code").option("--global", "Show global public ranking").option("--cache", "Include cache tokens in count").option("--all", "Show all members including those with no activity").action(rankCommand);
2245
2264
  program.command("init").description("Create a group and get started (first-time setup)").action(initCommand);
2246
2265
  program.command("join").description("Join a group with a 6-letter invite code").argument("[invite-code]", "6-character invite code").action((code) => {
@@ -2258,7 +2277,7 @@ program.command("join").description("Join a group with a 6-letter invite code").
2258
2277
  }
2259
2278
  return joinCommand(code);
2260
2279
  });
2261
- program.command("sync").description("Upload usage data (runs automatically on session end)").addOption(new Option("-s, --silent").hideHelp()).option("-f, --force", "Force full re-sync of all data").addOption(new Option("--full", "Same as --force").hideHelp()).action(
2280
+ program.command("sync").description("Upload local coding agent usage now (auto-sync also runs after setup)").addOption(new Option("-s, --silent").hideHelp()).option("-f, --force", "Re-scan and upload all local usage logs").addOption(new Option("--full", "Same as --force").hideHelp()).action(
2262
2281
  (options) => syncCommand({ ...options, full: options.full || options.force })
2263
2282
  );
2264
2283
  program.command("profile").description("View or update your profile").option("-n, --name <name>", "Set display name").option("--avatar <url>", "Set avatar URL (empty to reset)").option("--public", "Make profile visible in global ranking").option("--private", "Hide from global ranking").option("--plan <plan>", "pro ($20) | max100 ($100) | max200 ($200) | api | none").option("--url <url>", "Link your name to a URL (GitHub, website, etc.)").action(profileCommand);
@@ -2267,6 +2286,13 @@ program.command("leave").description("Leave a group").argument("[code]", "Group
2267
2286
  program.command("show-data").description("Preview exactly what gets uploaded (privacy check)").action(showDataCommand);
2268
2287
  program.command("hook", { hidden: true }).description("Set up auto-sync hook").action(hookCommand);
2269
2288
  program.addHelpText("after", `
2289
+ Setup:
2290
+ ccclub init Create your group and enable auto-sync
2291
+ ccclub join <code> Join a friend's group
2292
+
2293
+ Supported agents:
2294
+ Claude Code, Codex, OpenCode, Amp, pi-agent
2295
+
2270
2296
  Leaderboard options:
2271
2297
  -d <period> Time window: 1 | 7 | 30 | all (default: today)
2272
2298
  -g <code> Show a specific group
@@ -2275,9 +2301,10 @@ Leaderboard options:
2275
2301
  --all Show all members including inactive ones
2276
2302
 
2277
2303
  Examples:
2304
+ $ npx ccclub init First-time setup
2278
2305
  $ ccclub Show today's leaderboard (default)
2279
2306
  $ ccclub -d 1|7|30|all Time window (default: today)
2280
2307
  $ ccclub --global Global public leaderboard
2281
- $ ccclub sync --force Force full re-sync of all data
2308
+ $ ccclub show-data Preview exactly what gets uploaded
2282
2309
  `);
2283
2310
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccclub",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "type": "module",
5
5
  "description": "Claude Code and Codex leaderboard among friends for coding agent tokens and costs",
6
6
  "bin": {