ccgather 1.0.1 → 1.1.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.
Files changed (2) hide show
  1. package/dist/index.js +213 -60
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -76,16 +76,16 @@ __export(reset_exports, {
76
76
  reset: () => reset
77
77
  });
78
78
  function getClaudeSettingsDir() {
79
- return path3.join(os3.homedir(), ".claude");
79
+ return path4.join(os4.homedir(), ".claude");
80
80
  }
81
81
  function removeStopHook() {
82
82
  const claudeDir = getClaudeSettingsDir();
83
- const settingsPath = path3.join(claudeDir, "settings.json");
84
- if (!fs3.existsSync(settingsPath)) {
83
+ const settingsPath = path4.join(claudeDir, "settings.json");
84
+ if (!fs4.existsSync(settingsPath)) {
85
85
  return { success: true, message: "No settings file found" };
86
86
  }
87
87
  try {
88
- const content = fs3.readFileSync(settingsPath, "utf-8");
88
+ const content = fs4.readFileSync(settingsPath, "utf-8");
89
89
  const settings = JSON.parse(content);
90
90
  if (settings.hooks && typeof settings.hooks === "object") {
91
91
  const hooks = settings.hooks;
@@ -102,7 +102,7 @@ function removeStopHook() {
102
102
  }
103
103
  }
104
104
  }
105
- fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
105
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
106
106
  return { success: true, message: "Hook removed" };
107
107
  } catch (err) {
108
108
  return {
@@ -113,9 +113,9 @@ function removeStopHook() {
113
113
  }
114
114
  function removeSyncScript() {
115
115
  const claudeDir = getClaudeSettingsDir();
116
- const scriptPath = path3.join(claudeDir, "ccgather-sync.js");
117
- if (fs3.existsSync(scriptPath)) {
118
- fs3.unlinkSync(scriptPath);
116
+ const scriptPath = path4.join(claudeDir, "ccgather-sync.js");
117
+ if (fs4.existsSync(scriptPath)) {
118
+ fs4.unlinkSync(scriptPath);
119
119
  }
120
120
  }
121
121
  async function reset() {
@@ -173,15 +173,15 @@ async function reset() {
173
173
  console.log(import_chalk3.default.gray("Run `npx ccgather` to set up again."));
174
174
  console.log();
175
175
  }
176
- var import_chalk3, import_ora3, fs3, path3, os3, import_inquirer;
176
+ var import_chalk3, import_ora3, fs4, path4, os4, import_inquirer;
177
177
  var init_reset = __esm({
178
178
  "src/commands/reset.ts"() {
179
179
  "use strict";
180
180
  import_chalk3 = __toESM(require("chalk"));
181
181
  import_ora3 = __toESM(require("ora"));
182
- fs3 = __toESM(require("fs"));
183
- path3 = __toESM(require("path"));
184
- os3 = __toESM(require("os"));
182
+ fs4 = __toESM(require("fs"));
183
+ path4 = __toESM(require("path"));
184
+ os4 = __toESM(require("os"));
185
185
  import_inquirer = __toESM(require("inquirer"));
186
186
  init_config();
187
187
  }
@@ -193,28 +193,95 @@ var import_commander = require("commander");
193
193
  // src/commands/submit.ts
194
194
  var import_chalk = __toESM(require("chalk"));
195
195
  var import_ora = __toESM(require("ora"));
196
+ var fs3 = __toESM(require("fs"));
197
+ var path3 = __toESM(require("path"));
198
+ var os3 = __toESM(require("os"));
199
+ init_config();
200
+
201
+ // src/lib/ccgather-json.ts
196
202
  var fs2 = __toESM(require("fs"));
197
203
  var path2 = __toESM(require("path"));
198
204
  var os2 = __toESM(require("os"));
199
- init_config();
200
205
 
201
- // src/lib/ccgather-json.ts
206
+ // src/lib/credentials.ts
202
207
  var fs = __toESM(require("fs"));
203
208
  var path = __toESM(require("path"));
204
209
  var os = __toESM(require("os"));
205
- var CCGATHER_JSON_VERSION = "1.0.0";
210
+ function getCredentialsPath() {
211
+ return path.join(os.homedir(), ".claude", ".credentials.json");
212
+ }
213
+ function mapSubscriptionToCCPlan(subscriptionType) {
214
+ if (!subscriptionType) {
215
+ return "free";
216
+ }
217
+ const type = subscriptionType.toLowerCase();
218
+ if (type === "max" || type.includes("max")) {
219
+ return "max";
220
+ }
221
+ if (type === "team" || type === "enterprise") {
222
+ return "team";
223
+ }
224
+ if (type === "pro") {
225
+ return "pro";
226
+ }
227
+ return "free";
228
+ }
229
+ function readCredentials() {
230
+ const credentialsPath = getCredentialsPath();
231
+ const defaultData = {
232
+ ccplan: null,
233
+ rateLimitTier: null
234
+ };
235
+ if (!fs.existsSync(credentialsPath)) {
236
+ return defaultData;
237
+ }
238
+ try {
239
+ const content = fs.readFileSync(credentialsPath, "utf-8");
240
+ const credentials = JSON.parse(content);
241
+ const oauthData = credentials.claudeAiOauth;
242
+ if (!oauthData) {
243
+ return defaultData;
244
+ }
245
+ const ccplan = mapSubscriptionToCCPlan(oauthData.subscriptionType);
246
+ const rateLimitTier = oauthData.rateLimitTier || null;
247
+ return {
248
+ ccplan,
249
+ rateLimitTier
250
+ };
251
+ } catch (error) {
252
+ return defaultData;
253
+ }
254
+ }
255
+
256
+ // src/lib/ccgather-json.ts
257
+ var CCGATHER_JSON_VERSION = "1.2.0";
258
+ function extractProjectName(filePath) {
259
+ const parts = filePath.split(/[/\\]/);
260
+ const projectsIndex = parts.findIndex((p) => p === "projects");
261
+ if (projectsIndex >= 0 && parts[projectsIndex + 1]) {
262
+ try {
263
+ const encoded = parts[projectsIndex + 1];
264
+ const decoded = decodeURIComponent(encoded);
265
+ const pathParts = decoded.split(/[/\\]/);
266
+ return pathParts[pathParts.length - 1] || decoded;
267
+ } catch {
268
+ return parts[projectsIndex + 1];
269
+ }
270
+ }
271
+ return "unknown";
272
+ }
206
273
  function getCCGatherJsonPath() {
207
- return path.join(os.homedir(), ".claude", "ccgather.json");
274
+ return path2.join(os2.homedir(), ".claude", "ccgather.json");
208
275
  }
209
276
  function getClaudeProjectsDir() {
210
- return path.join(os.homedir(), ".claude", "projects");
277
+ return path2.join(os2.homedir(), ".claude", "projects");
211
278
  }
212
279
  function findJsonlFiles(dir) {
213
280
  const files = [];
214
281
  try {
215
- const entries = fs.readdirSync(dir, { withFileTypes: true });
282
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
216
283
  for (const entry of entries) {
217
- const fullPath = path.join(dir, entry.name);
284
+ const fullPath = path2.join(dir, entry.name);
218
285
  if (entry.isDirectory()) {
219
286
  files.push(...findJsonlFiles(fullPath));
220
287
  } else if (entry.name.endsWith(".jsonl")) {
@@ -230,7 +297,7 @@ function estimateCost(model, inputTokens, outputTokens) {
230
297
  "claude-opus-4": { input: 15, output: 75 },
231
298
  "claude-sonnet-4": { input: 3, output: 15 },
232
299
  "claude-haiku": { input: 0.25, output: 1.25 },
233
- "default": { input: 3, output: 15 }
300
+ default: { input: 3, output: 15 }
234
301
  };
235
302
  let modelKey = "default";
236
303
  for (const key of Object.keys(pricing)) {
@@ -246,7 +313,7 @@ function estimateCost(model, inputTokens, outputTokens) {
246
313
  }
247
314
  function scanUsageData() {
248
315
  const projectsDir = getClaudeProjectsDir();
249
- if (!fs.existsSync(projectsDir)) {
316
+ if (!fs2.existsSync(projectsDir)) {
250
317
  return null;
251
318
  }
252
319
  let totalInputTokens = 0;
@@ -257,13 +324,25 @@ function scanUsageData() {
257
324
  let sessionsCount = 0;
258
325
  const dates = /* @__PURE__ */ new Set();
259
326
  const models = {};
327
+ const projects = {};
328
+ const dailyData = {};
260
329
  let firstTimestamp = null;
261
330
  let lastTimestamp = null;
262
331
  const jsonlFiles = findJsonlFiles(projectsDir);
263
332
  sessionsCount = jsonlFiles.length;
264
333
  for (const filePath of jsonlFiles) {
334
+ const projectName = extractProjectName(filePath);
335
+ if (!projects[projectName]) {
336
+ projects[projectName] = {
337
+ tokens: 0,
338
+ cost: 0,
339
+ sessions: 0,
340
+ models: {}
341
+ };
342
+ }
343
+ projects[projectName].sessions++;
265
344
  try {
266
- const content = fs.readFileSync(filePath, "utf-8");
345
+ const content = fs2.readFileSync(filePath, "utf-8");
267
346
  const lines = content.split("\n").filter((line) => line.trim());
268
347
  for (const line of lines) {
269
348
  try {
@@ -277,12 +356,32 @@ function scanUsageData() {
277
356
  totalOutputTokens += outputTokens;
278
357
  totalCacheRead += usage.cache_read_input_tokens || 0;
279
358
  totalCacheWrite += usage.cache_creation_input_tokens || 0;
280
- totalCost += estimateCost(model, inputTokens, outputTokens);
359
+ const messageCost = estimateCost(model, inputTokens, outputTokens);
360
+ totalCost += messageCost;
281
361
  const totalModelTokens = inputTokens + outputTokens;
282
362
  models[model] = (models[model] || 0) + totalModelTokens;
363
+ projects[projectName].tokens += totalModelTokens;
364
+ projects[projectName].cost += messageCost;
365
+ projects[projectName].models[model] = (projects[projectName].models[model] || 0) + totalModelTokens;
283
366
  if (event.timestamp) {
284
367
  const date = new Date(event.timestamp).toISOString().split("T")[0];
285
368
  dates.add(date);
369
+ if (!dailyData[date]) {
370
+ dailyData[date] = {
371
+ tokens: 0,
372
+ cost: 0,
373
+ inputTokens: 0,
374
+ outputTokens: 0,
375
+ sessions: /* @__PURE__ */ new Set(),
376
+ models: {}
377
+ };
378
+ }
379
+ dailyData[date].tokens += totalModelTokens;
380
+ dailyData[date].cost += messageCost;
381
+ dailyData[date].inputTokens += inputTokens;
382
+ dailyData[date].outputTokens += outputTokens;
383
+ dailyData[date].sessions.add(filePath);
384
+ dailyData[date].models[model] = (dailyData[date].models[model] || 0) + totalModelTokens;
286
385
  if (!firstTimestamp || event.timestamp < firstTimestamp) {
287
386
  firstTimestamp = event.timestamp;
288
387
  }
@@ -301,6 +400,19 @@ function scanUsageData() {
301
400
  if (totalTokens === 0) {
302
401
  return null;
303
402
  }
403
+ for (const projectName of Object.keys(projects)) {
404
+ projects[projectName].cost = Math.round(projects[projectName].cost * 100) / 100;
405
+ }
406
+ const dailyUsage = Object.entries(dailyData).map(([date, data]) => ({
407
+ date,
408
+ tokens: data.tokens,
409
+ cost: Math.round(data.cost * 100) / 100,
410
+ inputTokens: data.inputTokens,
411
+ outputTokens: data.outputTokens,
412
+ sessions: data.sessions.size,
413
+ models: data.models
414
+ })).sort((a, b) => a.date.localeCompare(b.date));
415
+ const credentials = readCredentials();
304
416
  return {
305
417
  version: CCGATHER_JSON_VERSION,
306
418
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -319,16 +431,22 @@ function scanUsageData() {
319
431
  firstUsed: firstTimestamp ? new Date(firstTimestamp).toISOString().split("T")[0] : null,
320
432
  lastUsed: lastTimestamp ? new Date(lastTimestamp).toISOString().split("T")[0] : null
321
433
  },
322
- models
434
+ models,
435
+ projects,
436
+ dailyUsage,
437
+ account: {
438
+ ccplan: credentials.ccplan,
439
+ rateLimitTier: credentials.rateLimitTier
440
+ }
323
441
  };
324
442
  }
325
443
  function readCCGatherJson() {
326
444
  const jsonPath = getCCGatherJsonPath();
327
- if (!fs.existsSync(jsonPath)) {
445
+ if (!fs2.existsSync(jsonPath)) {
328
446
  return null;
329
447
  }
330
448
  try {
331
- const content = fs.readFileSync(jsonPath, "utf-8");
449
+ const content = fs2.readFileSync(jsonPath, "utf-8");
332
450
  return JSON.parse(content);
333
451
  } catch {
334
452
  return null;
@@ -336,11 +454,11 @@ function readCCGatherJson() {
336
454
  }
337
455
  function writeCCGatherJson(data) {
338
456
  const jsonPath = getCCGatherJsonPath();
339
- const claudeDir = path.dirname(jsonPath);
340
- if (!fs.existsSync(claudeDir)) {
341
- fs.mkdirSync(claudeDir, { recursive: true });
457
+ const claudeDir = path2.dirname(jsonPath);
458
+ if (!fs2.existsSync(claudeDir)) {
459
+ fs2.mkdirSync(claudeDir, { recursive: true });
342
460
  }
343
- fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
461
+ fs2.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
344
462
  }
345
463
  function scanAndSave() {
346
464
  const data = scanUsageData();
@@ -364,12 +482,12 @@ function ccgatherToUsageData(data) {
364
482
  }
365
483
  function findCcJson() {
366
484
  const possiblePaths = [
367
- path2.join(process.cwd(), "cc.json"),
368
- path2.join(os2.homedir(), "cc.json"),
369
- path2.join(os2.homedir(), ".claude", "cc.json")
485
+ path3.join(process.cwd(), "cc.json"),
486
+ path3.join(os3.homedir(), "cc.json"),
487
+ path3.join(os3.homedir(), ".claude", "cc.json")
370
488
  ];
371
489
  for (const p of possiblePaths) {
372
- if (fs2.existsSync(p)) {
490
+ if (fs3.existsSync(p)) {
373
491
  return p;
374
492
  }
375
493
  }
@@ -377,7 +495,7 @@ function findCcJson() {
377
495
  }
378
496
  function parseCcJson(filePath) {
379
497
  try {
380
- const content = fs2.readFileSync(filePath, "utf-8");
498
+ const content = fs3.readFileSync(filePath, "utf-8");
381
499
  const data = JSON.parse(content);
382
500
  return {
383
501
  totalTokens: data.totalTokens || data.total_tokens || 0,
@@ -674,14 +792,14 @@ async function status(options) {
674
792
  var import_chalk4 = __toESM(require("chalk"));
675
793
  var import_ora4 = __toESM(require("ora"));
676
794
  var http = __toESM(require("http"));
677
- var fs4 = __toESM(require("fs"));
678
- var path4 = __toESM(require("path"));
679
- var os4 = __toESM(require("os"));
795
+ var fs5 = __toESM(require("fs"));
796
+ var path5 = __toESM(require("path"));
797
+ var os5 = __toESM(require("os"));
680
798
  init_config();
681
799
  init_config();
682
800
  var CALLBACK_PORT = 9876;
683
801
  function getClaudeSettingsDir2() {
684
- return path4.join(os4.homedir(), ".claude");
802
+ return path5.join(os5.homedir(), ".claude");
685
803
  }
686
804
  async function openBrowser(url) {
687
805
  const { default: open } = await import("open");
@@ -911,14 +1029,14 @@ if (usageData) {
911
1029
  }
912
1030
  function installStopHook() {
913
1031
  const claudeDir = getClaudeSettingsDir2();
914
- const settingsPath = path4.join(claudeDir, "settings.json");
915
- if (!fs4.existsSync(claudeDir)) {
916
- fs4.mkdirSync(claudeDir, { recursive: true });
1032
+ const settingsPath = path5.join(claudeDir, "settings.json");
1033
+ if (!fs5.existsSync(claudeDir)) {
1034
+ fs5.mkdirSync(claudeDir, { recursive: true });
917
1035
  }
918
1036
  let settings = {};
919
1037
  try {
920
- if (fs4.existsSync(settingsPath)) {
921
- const content = fs4.readFileSync(settingsPath, "utf-8");
1038
+ if (fs5.existsSync(settingsPath)) {
1039
+ const content = fs5.readFileSync(settingsPath, "utf-8");
922
1040
  settings = JSON.parse(content);
923
1041
  }
924
1042
  } catch {
@@ -927,7 +1045,7 @@ function installStopHook() {
927
1045
  settings.hooks = {};
928
1046
  }
929
1047
  const hooks = settings.hooks;
930
- const syncScriptPath = path4.join(claudeDir, "ccgather-sync.js");
1048
+ const syncScriptPath = path5.join(claudeDir, "ccgather-sync.js");
931
1049
  const hookCommand = `node "${syncScriptPath}"`;
932
1050
  if (!hooks.Stop || !Array.isArray(hooks.Stop)) {
933
1051
  hooks.Stop = [];
@@ -941,16 +1059,16 @@ function installStopHook() {
941
1059
  background: true
942
1060
  });
943
1061
  }
944
- fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1062
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
945
1063
  return { success: true, message: "Stop hook installed" };
946
1064
  }
947
1065
  function saveSyncScript(apiUrl, apiToken) {
948
1066
  const claudeDir = getClaudeSettingsDir2();
949
- const scriptPath = path4.join(claudeDir, "ccgather-sync.js");
1067
+ const scriptPath = path5.join(claudeDir, "ccgather-sync.js");
950
1068
  const scriptContent = generateSyncScript(apiUrl, apiToken);
951
- fs4.writeFileSync(scriptPath, scriptContent);
952
- if (os4.platform() !== "win32") {
953
- fs4.chmodSync(scriptPath, "755");
1069
+ fs5.writeFileSync(scriptPath, scriptContent);
1070
+ if (os5.platform() !== "win32") {
1071
+ fs5.chmodSync(scriptPath, "755");
954
1072
  }
955
1073
  }
956
1074
  async function setupAuto(options = {}) {
@@ -1067,20 +1185,41 @@ function displayResults(data) {
1067
1185
  const gray = import_chalk5.default.gray;
1068
1186
  const white = import_chalk5.default.white;
1069
1187
  const bold = import_chalk5.default.bold;
1188
+ const cyan = import_chalk5.default.cyan;
1070
1189
  console.log();
1071
1190
  console.log(gray(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
1072
- console.log(gray(" \u2502") + white(" \u{1F4CA} Usage Summary") + gray(" \u2502"));
1191
+ console.log(
1192
+ gray(" \u2502") + white(" \u{1F4CA} Usage Summary") + gray(" \u2502")
1193
+ );
1073
1194
  console.log(gray(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
1074
- console.log(gray(" \u2502") + ` Total Tokens: ${orange(formatNumber3(data.usage.totalTokens).padEnd(15))}` + gray(" \u2502"));
1075
- console.log(gray(" \u2502") + ` Total Cost: ${green("$" + data.usage.totalCost.toFixed(2).padEnd(14))}` + gray(" \u2502"));
1076
- console.log(gray(" \u2502") + ` Input Tokens: ${white(formatNumber3(data.usage.inputTokens).padEnd(15))}` + gray(" \u2502"));
1077
- console.log(gray(" \u2502") + ` Output Tokens: ${white(formatNumber3(data.usage.outputTokens).padEnd(15))}` + gray(" \u2502"));
1195
+ console.log(
1196
+ gray(" \u2502") + ` Total Tokens: ${orange(formatNumber3(data.usage.totalTokens).padEnd(15))}` + gray(" \u2502")
1197
+ );
1198
+ console.log(
1199
+ gray(" \u2502") + ` Total Cost: ${green("$" + data.usage.totalCost.toFixed(2).padEnd(14))}` + gray(" \u2502")
1200
+ );
1201
+ console.log(
1202
+ gray(" \u2502") + ` Input Tokens: ${white(formatNumber3(data.usage.inputTokens).padEnd(15))}` + gray(" \u2502")
1203
+ );
1204
+ console.log(
1205
+ gray(" \u2502") + ` Output Tokens: ${white(formatNumber3(data.usage.outputTokens).padEnd(15))}` + gray(" \u2502")
1206
+ );
1078
1207
  console.log(gray(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
1079
- console.log(gray(" \u2502") + white(" \u{1F4C8} Stats") + gray(" \u2502"));
1080
- console.log(gray(" \u2502") + ` Days Tracked: ${white(data.stats.daysTracked.toString().padEnd(15))}` + gray(" \u2502"));
1081
- console.log(gray(" \u2502") + ` Sessions: ${white(data.stats.sessionsCount.toString().padEnd(15))}` + gray(" \u2502"));
1082
- console.log(gray(" \u2502") + ` First Used: ${gray((data.stats.firstUsed || "N/A").padEnd(15))}` + gray(" \u2502"));
1083
- console.log(gray(" \u2502") + ` Last Used: ${gray((data.stats.lastUsed || "N/A").padEnd(15))}` + gray(" \u2502"));
1208
+ console.log(
1209
+ gray(" \u2502") + white(" \u{1F4C8} Stats") + gray(" \u2502")
1210
+ );
1211
+ console.log(
1212
+ gray(" \u2502") + ` Days Tracked: ${white(data.stats.daysTracked.toString().padEnd(15))}` + gray(" \u2502")
1213
+ );
1214
+ console.log(
1215
+ gray(" \u2502") + ` Sessions: ${white(data.stats.sessionsCount.toString().padEnd(15))}` + gray(" \u2502")
1216
+ );
1217
+ console.log(
1218
+ gray(" \u2502") + ` First Used: ${gray((data.stats.firstUsed || "N/A").padEnd(15))}` + gray(" \u2502")
1219
+ );
1220
+ console.log(
1221
+ gray(" \u2502") + ` Last Used: ${gray((data.stats.lastUsed || "N/A").padEnd(15))}` + gray(" \u2502")
1222
+ );
1084
1223
  console.log(gray(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
1085
1224
  if (Object.keys(data.models).length > 0) {
1086
1225
  console.log();
@@ -1090,6 +1229,20 @@ function displayResults(data) {
1090
1229
  console.log(gray(" \u2022 ") + white(shortModel.padEnd(22)) + orange(formatNumber3(tokens)));
1091
1230
  }
1092
1231
  }
1232
+ if (data.projects && Object.keys(data.projects).length > 0) {
1233
+ console.log();
1234
+ console.log(gray(" ") + bold("Top Projects:"));
1235
+ const sortedProjects = Object.entries(data.projects).sort(([, a], [, b]) => b.tokens - a.tokens).slice(0, 5);
1236
+ for (const [name, stats] of sortedProjects) {
1237
+ const displayName = name.length > 25 ? name.substring(0, 22) + "..." : name;
1238
+ console.log(
1239
+ gray(" \u{1F4C1} ") + cyan(displayName.padEnd(25)) + orange(formatNumber3(stats.tokens).padStart(8)) + green(` $${stats.cost.toFixed(2).padStart(8)}`) + gray(` (${stats.sessions} sessions)`)
1240
+ );
1241
+ }
1242
+ if (Object.keys(data.projects).length > 5) {
1243
+ console.log(gray(` ... and ${Object.keys(data.projects).length - 5} more projects`));
1244
+ }
1245
+ }
1093
1246
  console.log();
1094
1247
  console.log(gray(` \u{1F4C1} Saved to: ${getCCGatherJsonPath()}`));
1095
1248
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccgather",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "CLI tool for syncing Claude Code usage data to CCgather leaderboard",
5
5
  "bin": {
6
6
  "ccgather": "./dist/index.js",