ccclub 0.3.13 → 0.3.15

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 +85 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -56,20 +56,17 @@ var PLAN_LABELS = {
56
56
  api: "API"
57
57
  };
58
58
  var MODEL_PRICING = {
59
- // Claude Opus 4.5+
59
+ // Claude models. We track API-equivalent value, matching ccusage's calculated mode.
60
60
  "claude-opus-4-7": { input: 5, output: 25, cacheCreation: 6.25, cacheRead: 0.5 },
61
61
  "claude-opus-4-6": { input: 5, output: 25, cacheCreation: 6.25, cacheRead: 0.5 },
62
62
  "claude-opus-4-5": { input: 5, output: 25, cacheCreation: 6.25, cacheRead: 0.5 },
63
63
  "claude-opus-4-5-20251101": { input: 5, output: 25, cacheCreation: 6.25, cacheRead: 0.5 },
64
- // Opus 4.0–4.1
65
64
  "claude-opus-4-1-20250805": { input: 15, output: 75, cacheCreation: 18.75, cacheRead: 1.5 },
66
- // Sonnet
67
65
  "claude-sonnet-4-6": { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.3 },
68
66
  "claude-sonnet-4-5": { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.3 },
69
67
  "claude-sonnet-4-5-20250929": { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.3 },
70
68
  "claude-sonnet-4-20250514": { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.3 },
71
69
  "claude-3-5-sonnet-20241022": { input: 3, output: 15, cacheCreation: 3.75, cacheRead: 0.3 },
72
- // Haiku
73
70
  "claude-haiku-4-5-20251001": { input: 1, output: 5, cacheCreation: 1.25, cacheRead: 0.1 },
74
71
  "claude-3-5-haiku-20241022": { input: 0.8, output: 4, cacheCreation: 1, cacheRead: 0.08 },
75
72
  // OpenAI GPT/Codex family.
@@ -79,7 +76,12 @@ var MODEL_PRICING = {
79
76
  "gpt-5.4-nano": { input: 0.2, output: 1.25, cacheCreation: 0, cacheRead: 0.02 },
80
77
  "gpt-5.3-codex": { input: 1.75, output: 14, cacheCreation: 0, cacheRead: 0.175 },
81
78
  "gpt-5.2-codex": { input: 1.75, output: 14, cacheCreation: 0, cacheRead: 0.175 },
79
+ "gpt-5.1-codex": { input: 1.25, output: 10, cacheCreation: 0, cacheRead: 0.125 },
80
+ "gpt-5.1-codex-max": { input: 1.25, output: 10, cacheCreation: 0, cacheRead: 0.125 },
81
+ "gpt-5.1-codex-mini": { input: 0.25, output: 2, cacheCreation: 0, cacheRead: 0.025 },
82
82
  "gpt-5-codex": { input: 1.25, output: 10, cacheCreation: 0, cacheRead: 0.125 },
83
+ "codex-mini-latest": { input: 1.5, output: 6, cacheCreation: 0, cacheRead: 0.375 },
84
+ "codex-auto-review": { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
83
85
  "gpt-5": { input: 1.25, output: 10, cacheCreation: 0, cacheRead: 0.125 },
84
86
  "gpt-5-mini": { input: 0.25, output: 2, cacheCreation: 0, cacheRead: 0.025 },
85
87
  "gpt-5-nano": { input: 0.05, output: 0.4, cacheCreation: 0, cacheRead: 5e-3 }
@@ -94,7 +96,12 @@ var FAMILY_FALLBACK = {
94
96
  "gpt-5.4": MODEL_PRICING["gpt-5.4"],
95
97
  "gpt-5.3-codex": MODEL_PRICING["gpt-5.3-codex"],
96
98
  "gpt-5.2-codex": MODEL_PRICING["gpt-5.2-codex"],
99
+ "gpt-5.1-codex-mini": MODEL_PRICING["gpt-5.1-codex-mini"],
100
+ "gpt-5.1-codex-max": MODEL_PRICING["gpt-5.1-codex-max"],
101
+ "gpt-5.1-codex": MODEL_PRICING["gpt-5.1-codex"],
97
102
  "gpt-5-codex": MODEL_PRICING["gpt-5-codex"],
103
+ "codex-mini-latest": MODEL_PRICING["codex-mini-latest"],
104
+ "codex-auto-review": MODEL_PRICING["codex-auto-review"],
98
105
  "gpt-5-nano": MODEL_PRICING["gpt-5-nano"],
99
106
  "gpt-5-mini": MODEL_PRICING["gpt-5-mini"],
100
107
  "gpt-5": MODEL_PRICING["gpt-5"],
@@ -336,7 +343,7 @@ function isHeartbeatInstalled() {
336
343
  }
337
344
 
338
345
  // src/commands/sync.ts
339
- import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
346
+ import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
340
347
  import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
341
348
  import { join as join11 } from "path";
342
349
  import { homedir as homedir11 } from "os";
@@ -544,7 +551,7 @@ async function collectClaudeUsage() {
544
551
  const files = await globFiles(projectDirs, "**/*.jsonl");
545
552
  const entries = [];
546
553
  const turns = [];
547
- const seen = /* @__PURE__ */ new Set();
554
+ const entryIndexByDedupeKey = /* @__PURE__ */ new Map();
548
555
  const seenTurns = /* @__PURE__ */ new Set();
549
556
  for (const file of files) {
550
557
  await readJsonlFile(file, (value) => {
@@ -565,7 +572,8 @@ async function collectClaudeUsage() {
565
572
  const usage = value.message.usage;
566
573
  const sessionId = value.sessionId || "";
567
574
  const requestId = asString(value.requestId);
568
- const dedupeKey = requestId ? `${source}:${sessionId}:${requestId}` : [
575
+ const messageId = asString(value.message.id);
576
+ const dedupeKey = messageId ? requestId ? `${messageId}:${requestId}` : messageId : [
569
577
  source,
570
578
  sessionId,
571
579
  timestamp,
@@ -574,15 +582,13 @@ async function collectClaudeUsage() {
574
582
  usage.cache_creation_input_tokens ?? 0,
575
583
  usage.cache_read_input_tokens ?? 0
576
584
  ].join(":");
577
- if (seen.has(dedupeKey)) return;
578
- seen.add(dedupeKey);
579
585
  const inputTokens = usage.input_tokens || 0;
580
586
  const outputTokens = usage.output_tokens || 0;
581
587
  const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
582
588
  const cacheReadTokens = usage.cache_read_input_tokens || 0;
583
589
  const model = value.message.model || "unknown";
584
590
  const costUSD = value.costUSD && value.costUSD > 0 ? value.costUSD : calculateCost(model, inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens);
585
- entries.push({
591
+ const entry = {
586
592
  source,
587
593
  timestamp,
588
594
  sessionId,
@@ -594,7 +600,16 @@ async function collectClaudeUsage() {
594
600
  cacheReadTokens,
595
601
  totalTokens: inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens,
596
602
  costUSD
597
- });
603
+ };
604
+ const existingIndex = entryIndexByDedupeKey.get(dedupeKey);
605
+ if (existingIndex != null) {
606
+ if (entry.totalTokens > entries[existingIndex].totalTokens) {
607
+ entries[existingIndex] = entry;
608
+ }
609
+ return;
610
+ }
611
+ entryIndexByDedupeKey.set(dedupeKey, entries.length);
612
+ entries.push(entry);
598
613
  });
599
614
  }
600
615
  return { source, entries, turns, files: files.length, warnings: [] };
@@ -606,12 +621,28 @@ var claudeCollector = {
606
621
  };
607
622
 
608
623
  // src/sources/codex.ts
624
+ import { readFile as readFile4 } from "fs/promises";
609
625
  import { join as join7, relative, sep } from "path";
610
626
  import { homedir as homedir7 } from "os";
627
+ var CODEX_FAST_COST_MULTIPLIER = 2;
628
+ var codexFastServiceTierRegex = /(?:^|\n)\s*service_tier\s*=\s*["']?(?:fast|priority)["']?/iu;
629
+ function getCodexHomes() {
630
+ return parsePathList(process.env[CODEX_HOME_ENV], [join7(homedir7(), DEFAULT_CODEX_DIR)]);
631
+ }
611
632
  function getCodexSessionDirs() {
612
- const homes = parsePathList(process.env[CODEX_HOME_ENV], [join7(homedir7(), DEFAULT_CODEX_DIR)]);
633
+ const homes = getCodexHomes();
613
634
  return existingDirectories(homes.map((home) => join7(home, "sessions")));
614
635
  }
636
+ async function getCodexCostMultiplier() {
637
+ for (const home of getCodexHomes()) {
638
+ try {
639
+ const config = await readFile4(join7(home, "config.toml"), "utf8");
640
+ if (codexFastServiceTierRegex.test(config)) return CODEX_FAST_COST_MULTIPLIER;
641
+ } catch {
642
+ }
643
+ }
644
+ return 1;
645
+ }
615
646
  function normalizeRawUsage(value) {
616
647
  const record = asRecord(value);
617
648
  if (record == null) return null;
@@ -619,7 +650,7 @@ function normalizeRawUsage(value) {
619
650
  const cachedInputTokens = asNumber(record.cached_input_tokens ?? record.cache_read_input_tokens);
620
651
  const outputTokens = asNumber(record.output_tokens);
621
652
  const reasoningTokens = asNumber(record.reasoning_output_tokens);
622
- const fallbackTotal = inputTokens + outputTokens;
653
+ const fallbackTotal = inputTokens + outputTokens + reasoningTokens;
623
654
  const totalTokens = asNumber(record.total_tokens) || fallbackTotal;
624
655
  return { inputTokens, cachedInputTokens, outputTokens, reasoningTokens, totalTokens };
625
656
  }
@@ -643,7 +674,10 @@ function sessionIdForFile(sessionDir, file) {
643
674
  }
644
675
  async function collectCodexUsage() {
645
676
  const source = "codex";
646
- const sessionDirs = await getCodexSessionDirs();
677
+ const [sessionDirs, costMultiplier] = await Promise.all([
678
+ getCodexSessionDirs(),
679
+ getCodexCostMultiplier()
680
+ ]);
647
681
  const files = await globFiles(sessionDirs, "**/*.jsonl");
648
682
  const entries = [];
649
683
  const turns = [];
@@ -733,7 +767,7 @@ async function collectCodexUsage() {
733
767
  cacheReadTokens,
734
768
  reasoningTokens: 0,
735
769
  totalTokens,
736
- costUSD: calculateCost(model, inputTokens, rawUsage.outputTokens, 0, cacheReadTokens)
770
+ costUSD: calculateCost(model, inputTokens, rawUsage.outputTokens, 0, cacheReadTokens) * costMultiplier
737
771
  });
738
772
  });
739
773
  if (!sawTaskStarted) {
@@ -1336,12 +1370,12 @@ async function doSync(firstSync = false, silent = false) {
1336
1370
  const lastSyncPath = getLastSyncPath();
1337
1371
  let lastSync = null;
1338
1372
  if (existsSync4(lastSyncPath)) {
1339
- lastSync = (await readFile4(lastSyncPath, "utf-8")).trim() || null;
1373
+ lastSync = (await readFile5(lastSyncPath, "utf-8")).trim() || null;
1340
1374
  }
1341
1375
  let lastSyncBySource = {};
1342
1376
  if (existsSync4(getLastSyncBySourcePath())) {
1343
1377
  try {
1344
- lastSyncBySource = JSON.parse(await readFile4(getLastSyncBySourcePath(), "utf-8"));
1378
+ lastSyncBySource = JSON.parse(await readFile5(getLastSyncBySourcePath(), "utf-8"));
1345
1379
  } catch {
1346
1380
  lastSyncBySource = {};
1347
1381
  }
@@ -1675,6 +1709,7 @@ async function getUpdateResult() {
1675
1709
 
1676
1710
  // src/commands/rank.ts
1677
1711
  var ACTIVE_THRESHOLD_MS = 15 * 60 * 1e3;
1712
+ var AGENT_ORDER = ["claude", "codex", "opencode", "amp", "pi"];
1678
1713
  async function rankCommand(options) {
1679
1714
  const config = await requireConfig();
1680
1715
  if (!isHookInstalled()) await installHook();
@@ -1811,10 +1846,12 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
1811
1846
  ${data.group.name}`));
1812
1847
  const periodLabel = { daily: "TODAY", yesterday: "YESTERDAY", weekly: "7 DAYS", monthly: "30 DAYS", "all-time": "ALL TIME" };
1813
1848
  const now = Date.now();
1814
- const activeCount = data.rankings.filter((r) => isEntryActive(r, now)).length;
1849
+ const activeEntries = data.rankings.filter((r) => isEntryActive(r, now));
1850
+ const activeCount = activeEntries.length;
1815
1851
  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`));
1816
1852
  if (activeCount > 0) {
1817
- console.log(theme.success(` ${activeCount} active`));
1853
+ const activeSplit = formatActiveSourceSplit(activeEntries);
1854
+ console.log(theme.success(` ${activeCount} active`) + (activeSplit ? ` ${activeSplit}` : ""));
1818
1855
  }
1819
1856
  console.log("");
1820
1857
  const activeRankings = showAll || data.rankings.length <= 15 ? data.rankings : data.rankings.filter((r) => r.costUSD > 0 || r.userId === config.userId);
@@ -1911,6 +1948,34 @@ function isEntryActive(entry, now) {
1911
1948
  const activeAt = new Date(value).getTime();
1912
1949
  return Number.isFinite(activeAt) && now - activeAt < ACTIVE_THRESHOLD_MS;
1913
1950
  }
1951
+ function activeSourceForEntry(entry) {
1952
+ return entry.lastActiveSource ?? entry.agents?.[0];
1953
+ }
1954
+ function formatActiveSourceSplit(entries) {
1955
+ const counts = /* @__PURE__ */ new Map();
1956
+ for (const entry of entries) {
1957
+ const source = activeSourceForEntry(entry);
1958
+ if (!source) continue;
1959
+ counts.set(source, (counts.get(source) ?? 0) + 1);
1960
+ }
1961
+ const sources = AGENT_ORDER.filter((source) => (counts.get(source) ?? 0) > 0);
1962
+ if (sources.length === 0) return "";
1963
+ if (sources.length === 2 && sources.includes("claude") && sources.includes("codex")) {
1964
+ return [
1965
+ theme.muted("Claude"),
1966
+ theme.successBold(String(counts.get("claude") ?? 0)) + theme.faint(":"),
1967
+ theme.successBold(String(counts.get("codex") ?? 0)),
1968
+ theme.muted("Codex")
1969
+ ].join(" ");
1970
+ }
1971
+ return sources.map((source) => formatActiveSourceScore(source, counts.get(source) ?? 0, false)).join(theme.faint(" \xB7 "));
1972
+ }
1973
+ function formatActiveSourceScore(source, count, countFirst) {
1974
+ const icon = source === "claude" ? theme.gold("\u25CF") : source === "codex" ? theme.linkText("\u25CF") : theme.success("\u25CF");
1975
+ const label = source === "claude" ? "Claude" : formatAgentLabel(source);
1976
+ const coloredCount = theme.successBold(String(count));
1977
+ return countFirst ? `${coloredCount} ${icon} ${theme.muted(label)}` : `${icon} ${theme.muted(label)} ${coloredCount}`;
1978
+ }
1914
1979
  function podiumStyle(rank) {
1915
1980
  if (rank === 1) return theme.gold;
1916
1981
  if (rank === 2) return theme.silver;
@@ -2323,7 +2388,7 @@ async function hookCommand() {
2323
2388
  }
2324
2389
 
2325
2390
  // src/index.ts
2326
- var VERSION = "0.3.13";
2391
+ var VERSION = "0.3.15";
2327
2392
  startUpdateCheck(VERSION);
2328
2393
  var program = new Command();
2329
2394
  if (process.argv.slice(2).includes("-v")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccclub",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "type": "module",
5
5
  "description": "Claude Code and Codex leaderboard among friends for coding agent tokens and costs",
6
6
  "bin": {