ccclub 0.3.5 → 0.3.7

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 +131 -51
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -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
  }
@@ -1728,73 +1728,74 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
1728
1728
  const activeRankings = showAll || data.rankings.length <= 15 ? data.rankings : data.rankings.filter((r) => r.costUSD > 0 || r.userId === config.userId);
1729
1729
  const hiddenCount = data.rankings.length - activeRankings.length;
1730
1730
  const hasPlan = activeRankings.some((r) => r.plan);
1731
- const hasUsage = activeRankings.some((r) => r.usageSnapshot);
1732
1731
  const hasAgents = activeRankings.some(
1733
1732
  (r) => r.agents && r.agents.length > 0 && !(r.agents.length === 1 && r.agents[0] === "claude")
1734
1733
  );
1734
+ const plainRows = activeRankings.map((entry) => {
1735
+ const isActive = isEntryActive(entry, now);
1736
+ const tokens = showCache ? entry.totalTokens : entry.inputTokens + entry.outputTokens + (entry.reasoningTokens || 0);
1737
+ const roi = formatRoi(entry, hasPlan);
1738
+ return {
1739
+ entry,
1740
+ isActive,
1741
+ rank: `${entry.userId === config.userId ? "\u2192" : " "}${entry.rank}`,
1742
+ name: `${isActive ? "\u25CF " : ""}${entry.displayName}`,
1743
+ agents: formatAgents(entry),
1744
+ cost: `$${entry.costUSD.toFixed(2)}`,
1745
+ tokens: formatTokens(tokens),
1746
+ roi,
1747
+ turns: String(entry.chatCount),
1748
+ perTurn: entry.chatCount > 0 ? `$${(entry.costUSD / entry.chatCount).toFixed(2)}` : "\u2014"
1749
+ };
1750
+ });
1735
1751
  const head = ["#", "Name", "Cost", "Tokens"];
1736
- const hasActive = activeRankings.some((r) => r.lastSync && now - new Date(r.lastSync).getTime() < ACTIVE_THRESHOLD_MS);
1737
- const widths = [5, hasActive ? 30 : 20, 12, 10];
1752
+ const widths = [
1753
+ columnWidth("#", plainRows.map((r) => r.rank), 2, 3),
1754
+ columnWidth("Name", plainRows.map((r) => r.name), 10, 18),
1755
+ columnWidth("Cost", plainRows.map((r) => r.cost), 5, 9),
1756
+ columnWidth("Tokens", plainRows.map((r) => r.tokens), 6, 8)
1757
+ ];
1738
1758
  if (hasAgents) {
1739
1759
  head.splice(2, 0, "Agents");
1740
- widths.splice(2, 0, 16);
1760
+ widths.splice(2, 0, columnWidth("Agents", plainRows.map((r) => r.agents), 6, 28));
1741
1761
  }
1742
1762
  if (hasPlan) {
1743
- head.push("Monthly ROI");
1744
- widths.push(15);
1763
+ head.push("ROI");
1764
+ widths.push(columnWidth("ROI", plainRows.map((r) => r.roi), 3, 11));
1745
1765
  }
1746
1766
  head.push("Turns", "$/Turn");
1747
- widths.push(8, 9);
1748
- if (hasUsage) {
1749
- head.push("Usage 7d");
1750
- widths.push(10);
1751
- }
1767
+ widths.push(
1768
+ columnWidth("Turns", plainRows.map((r) => r.turns), 3, 6),
1769
+ columnWidth("$/Turn", plainRows.map((r) => r.perTurn), 6, 7)
1770
+ );
1752
1771
  const table = new Table({
1753
1772
  head: head.map((h) => chalk6.cyan(h)),
1754
1773
  style: { head: [], border: [] },
1755
1774
  colWidths: widths
1756
1775
  });
1757
- for (const entry of activeRankings) {
1776
+ for (const plain of plainRows) {
1777
+ const { entry } = plain;
1758
1778
  const isMe = entry.userId === config.userId;
1759
- const tokens = showCache ? entry.totalTokens : entry.inputTokens + entry.outputTokens + (entry.reasoningTokens || 0);
1760
1779
  const marker = isMe ? chalk6.green("\u2192") : " ";
1761
- const isActive = entry.lastSync && now - new Date(entry.lastSync).getTime() < ACTIVE_THRESHOLD_MS;
1762
1780
  const id = (s) => s;
1763
1781
  const c = isMe ? chalk6.green : entry.rank === 1 ? chalk6.yellow : id;
1764
1782
  const nameC = isMe ? chalk6.green.bold : entry.rank === 1 ? chalk6.yellow.bold : id;
1765
- const displayName = isActive ? `${entry.displayName} ${chalk6.green("(active)")}` : entry.displayName;
1783
+ const nameWidth = Math.max(widths[1] - 2, 4);
1784
+ const displayName = plain.isActive ? `${chalk6.green("\u25CF")} ${nameC(truncateDisplay(entry.displayName, Math.max(nameWidth - 2, 1)))}` : nameC(truncateDisplay(entry.displayName, nameWidth));
1766
1785
  const row = [
1767
1786
  `${marker}${c(String(entry.rank))}`,
1768
- nameC(displayName)
1787
+ displayName
1769
1788
  ];
1770
1789
  if (hasAgents) {
1771
- row.push(c(formatAgents(entry.agents)));
1790
+ const agentWidth = Math.max(widths[2] - 2, 4);
1791
+ row.push(c(truncateDisplay(plain.agents, agentWidth)));
1772
1792
  }
1773
- row.push(c(`$${entry.costUSD.toFixed(2)}`), c(formatTokens(tokens)));
1793
+ row.push(c(plain.cost), c(plain.tokens));
1774
1794
  if (hasPlan) {
1775
- if (entry.plan && entry.plan !== "api") {
1776
- const price = PLAN_PRICES[entry.plan];
1777
- const monthly = entry.monthlyCostUSD || 0;
1778
- const roi = price > 0 ? Math.round(monthly / price * 100) : 0;
1779
- const roiStr = `$${price}/${roi}%`;
1780
- const roiC = roi >= 100 ? chalk6.green.bold(roiStr) : roi >= 50 ? chalk6.yellow(roiStr) : chalk6.dim(roiStr);
1781
- row.push(roiC);
1782
- } else if (entry.plan === "api") {
1783
- row.push(chalk6.dim("API"));
1784
- } else {
1785
- row.push(chalk6.dim("\u2014"));
1786
- }
1787
- }
1788
- row.push(c(String(entry.chatCount)));
1789
- row.push(entry.chatCount > 0 ? c(`$${(entry.costUSD / entry.chatCount).toFixed(2)}`) : chalk6.dim("\u2014"));
1790
- if (hasUsage) {
1791
- if (entry.usageSnapshot) {
1792
- const { sevenDay } = entry.usageSnapshot;
1793
- row.push(c(`${Math.round(sevenDay)}%`));
1794
- } else {
1795
- row.push(chalk6.dim("\u2014"));
1796
- }
1795
+ row.push(colorRoi(plain.roi, entry));
1797
1796
  }
1797
+ row.push(c(plain.turns));
1798
+ row.push(entry.chatCount > 0 ? c(plain.perTurn) : chalk6.dim("\u2014"));
1798
1799
  table.push(row);
1799
1800
  }
1800
1801
  console.log(table.toString());
@@ -1812,9 +1813,76 @@ function printGroup(data, code, period, config, showCache = false, showAll = fal
1812
1813
  }
1813
1814
  }
1814
1815
  }
1815
- function formatAgents(agents) {
1816
- if (!agents || agents.length === 0) return "\u2014";
1817
- return agents.map((agent) => AGENT_LABELS[agent] ?? agent).join(", ");
1816
+ function isEntryActive(entry, now) {
1817
+ return Boolean(entry.lastSync && now - new Date(entry.lastSync).getTime() < ACTIVE_THRESHOLD_MS);
1818
+ }
1819
+ function formatRoi(entry, hasPlan) {
1820
+ if (!hasPlan) return "";
1821
+ if (entry.plan && entry.plan !== "api") {
1822
+ const price = PLAN_PRICES[entry.plan];
1823
+ const monthly = entry.monthlyCostUSD || 0;
1824
+ const roi = price > 0 ? Math.round(monthly / price * 100) : 0;
1825
+ return `$${price}/${roi}%`;
1826
+ }
1827
+ if (entry.plan === "api") return "API";
1828
+ return "\u2014";
1829
+ }
1830
+ function colorRoi(roiStr, entry) {
1831
+ if (entry.plan && entry.plan !== "api") {
1832
+ const price = PLAN_PRICES[entry.plan];
1833
+ const monthly = entry.monthlyCostUSD || 0;
1834
+ const roi = price > 0 ? Math.round(monthly / price * 100) : 0;
1835
+ return roi >= 100 ? chalk6.green.bold(roiStr) : roi >= 50 ? chalk6.yellow(roiStr) : chalk6.dim(roiStr);
1836
+ }
1837
+ return chalk6.dim(roiStr);
1838
+ }
1839
+ function formatAgents(entry) {
1840
+ if (entry.agentBreakdown && entry.agentBreakdown.length > 0) {
1841
+ if (entry.agentBreakdown.length === 1) {
1842
+ return formatAgentLabel(entry.agentBreakdown[0].source);
1843
+ }
1844
+ const visible = entry.agentBreakdown.slice(0, 2).map((agent) => `${formatAgentLabel(agent.source)} (${agent.percent}%)`);
1845
+ if (entry.agentBreakdown.length > visible.length) {
1846
+ visible.push(`+${entry.agentBreakdown.length - visible.length}`);
1847
+ }
1848
+ return visible.join(", ");
1849
+ }
1850
+ if (!entry.agents || entry.agents.length === 0) return "\u2014";
1851
+ return entry.agents.map(formatAgentLabel).join(", ");
1852
+ }
1853
+ function formatAgentLabel(agent) {
1854
+ return AGENT_LABELS[agent] ?? agent;
1855
+ }
1856
+ function columnWidth(header, values, minContent, maxContent) {
1857
+ const contentWidth = Math.max(visualWidth(header), ...values.map(visualWidth));
1858
+ return Math.min(Math.max(contentWidth, minContent), maxContent) + 2;
1859
+ }
1860
+ function visualWidth(value) {
1861
+ let width = 0;
1862
+ for (const char of value) width += charWidth(char);
1863
+ return width;
1864
+ }
1865
+ function charWidth(char) {
1866
+ const code = char.codePointAt(0) ?? 0;
1867
+ if (code === 0 || code < 32 || code >= 127 && code < 160) return 0;
1868
+ if (code >= 4352 && (code <= 4447 || code === 9001 || code === 9002 || code >= 11904 && code <= 42191 && code !== 12351 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65049 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510)) {
1869
+ return 2;
1870
+ }
1871
+ return 1;
1872
+ }
1873
+ function truncateDisplay(value, maxWidth) {
1874
+ if (visualWidth(value) <= maxWidth) return value;
1875
+ if (maxWidth <= 1) return "\u2026";
1876
+ let result = "";
1877
+ let width = 0;
1878
+ const limit = maxWidth - 1;
1879
+ for (const char of value) {
1880
+ const next = charWidth(char);
1881
+ if (width + next > limit) break;
1882
+ result += char;
1883
+ width += next;
1884
+ }
1885
+ return `${result}\u2026`;
1818
1886
  }
1819
1887
  var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587";
1820
1888
  function renderActivity(data, range) {
@@ -2160,10 +2228,14 @@ async function hookCommand() {
2160
2228
  }
2161
2229
 
2162
2230
  // src/index.ts
2163
- var VERSION = "0.3.5";
2231
+ var VERSION = "0.3.7";
2164
2232
  startUpdateCheck(VERSION);
2165
2233
  var program = new Command();
2166
- program.name("ccclub").description("Coding agent usage leaderboard among friends").version(VERSION, "-v, -V, --version");
2234
+ if (process.argv.slice(2).includes("-v")) {
2235
+ console.log(VERSION);
2236
+ process.exit(0);
2237
+ }
2238
+ program.name("ccclub").description("Claude Code, Codex, OpenCode, Amp, and pi-agent usage leaderboard among friends").version(VERSION);
2167
2239
  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);
2168
2240
  program.command("init").description("Create a group and get started (first-time setup)").action(initCommand);
2169
2241
  program.command("join").description("Join a group with a 6-letter invite code").argument("[invite-code]", "6-character invite code").action((code) => {
@@ -2181,7 +2253,7 @@ program.command("join").description("Join a group with a 6-letter invite code").
2181
2253
  }
2182
2254
  return joinCommand(code);
2183
2255
  });
2184
- 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(
2256
+ 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(
2185
2257
  (options) => syncCommand({ ...options, full: options.full || options.force })
2186
2258
  );
2187
2259
  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);
@@ -2190,6 +2262,13 @@ program.command("leave").description("Leave a group").argument("[code]", "Group
2190
2262
  program.command("show-data").description("Preview exactly what gets uploaded (privacy check)").action(showDataCommand);
2191
2263
  program.command("hook", { hidden: true }).description("Set up auto-sync hook").action(hookCommand);
2192
2264
  program.addHelpText("after", `
2265
+ Setup:
2266
+ ccclub init Create your group and enable auto-sync
2267
+ ccclub join <code> Join a friend's group
2268
+
2269
+ Supported agents:
2270
+ Claude Code, Codex, OpenCode, Amp, pi-agent
2271
+
2193
2272
  Leaderboard options:
2194
2273
  -d <period> Time window: 1 | 7 | 30 | all (default: today)
2195
2274
  -g <code> Show a specific group
@@ -2198,9 +2277,10 @@ Leaderboard options:
2198
2277
  --all Show all members including inactive ones
2199
2278
 
2200
2279
  Examples:
2280
+ $ npx ccclub init First-time setup
2201
2281
  $ ccclub Show today's leaderboard (default)
2202
2282
  $ ccclub -d 1|7|30|all Time window (default: today)
2203
2283
  $ ccclub --global Global public leaderboard
2204
- $ ccclub sync --force Force full re-sync of all data
2284
+ $ ccclub show-data Preview exactly what gets uploaded
2205
2285
  `);
2206
2286
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccclub",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "type": "module",
5
5
  "description": "Claude Code and Codex leaderboard among friends for coding agent tokens and costs",
6
6
  "bin": {