ccgather 1.0.0 → 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 +438 -110
  2. package/package.json +51 -51
package/dist/index.js CHANGED
@@ -76,16 +76,16 @@ __export(reset_exports, {
76
76
  reset: () => reset
77
77
  });
78
78
  function getClaudeSettingsDir() {
79
- return path2.join(os2.homedir(), ".claude");
79
+ return path4.join(os4.homedir(), ".claude");
80
80
  }
81
81
  function removeStopHook() {
82
82
  const claudeDir = getClaudeSettingsDir();
83
- const settingsPath = path2.join(claudeDir, "settings.json");
84
- if (!fs2.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 = fs2.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
- fs2.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 = path2.join(claudeDir, "ccgather-sync.js");
117
- if (fs2.existsSync(scriptPath)) {
118
- fs2.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() {
@@ -170,18 +170,18 @@ async function reset() {
170
170
  console.log(import_chalk3.default.green.bold("\u2705 Reset complete!"));
171
171
  console.log();
172
172
  console.log(import_chalk3.default.gray("Your usage will no longer be tracked."));
173
- console.log(import_chalk3.default.gray("Run `npx ccg` to set up again."));
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, fs2, path2, os2, 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
- fs2 = __toESM(require("fs"));
183
- path2 = __toESM(require("path"));
184
- os2 = __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,92 +193,201 @@ 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
202
+ var fs2 = __toESM(require("fs"));
203
+ var path2 = __toESM(require("path"));
204
+ var os2 = __toESM(require("os"));
205
+
206
+ // src/lib/credentials.ts
196
207
  var fs = __toESM(require("fs"));
197
208
  var path = __toESM(require("path"));
198
209
  var os = __toESM(require("os"));
199
- init_config();
200
- function findCcJson() {
201
- const possiblePaths = [
202
- path.join(process.cwd(), "cc.json"),
203
- path.join(os.homedir(), "cc.json"),
204
- path.join(os.homedir(), ".claude", "cc.json")
205
- ];
206
- for (const p of possiblePaths) {
207
- if (fs.existsSync(p)) {
208
- return p;
209
- }
210
+ function getCredentialsPath() {
211
+ return path.join(os.homedir(), ".claude", ".credentials.json");
212
+ }
213
+ function mapSubscriptionToCCPlan(subscriptionType) {
214
+ if (!subscriptionType) {
215
+ return "free";
210
216
  }
211
- return null;
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";
212
228
  }
213
- function parseCcJson(filePath) {
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
+ }
214
238
  try {
215
- const content = fs.readFileSync(filePath, "utf-8");
216
- const data = JSON.parse(content);
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;
217
247
  return {
218
- totalTokens: data.totalTokens || data.total_tokens || 0,
219
- totalCost: data.totalCost || data.total_cost || data.costUSD || 0,
220
- inputTokens: data.inputTokens || data.input_tokens || 0,
221
- outputTokens: data.outputTokens || data.output_tokens || 0,
222
- cacheReadTokens: data.cacheReadTokens || data.cache_read_tokens || 0,
223
- cacheWriteTokens: data.cacheWriteTokens || data.cache_write_tokens || 0,
224
- daysTracked: data.daysTracked || data.days_tracked || calculateDaysTracked(data)
248
+ ccplan,
249
+ rateLimitTier
225
250
  };
226
- } catch {
227
- return null;
251
+ } catch (error) {
252
+ return defaultData;
228
253
  }
229
254
  }
230
- function calculateDaysTracked(data) {
231
- if (data.dailyStats && Array.isArray(data.dailyStats)) {
232
- return data.dailyStats.length;
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
+ }
233
270
  }
234
- if (data.daily && typeof data.daily === "object") {
235
- return Object.keys(data.daily).length;
271
+ return "unknown";
272
+ }
273
+ function getCCGatherJsonPath() {
274
+ return path2.join(os2.homedir(), ".claude", "ccgather.json");
275
+ }
276
+ function getClaudeProjectsDir() {
277
+ return path2.join(os2.homedir(), ".claude", "projects");
278
+ }
279
+ function findJsonlFiles(dir) {
280
+ const files = [];
281
+ try {
282
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
283
+ for (const entry of entries) {
284
+ const fullPath = path2.join(dir, entry.name);
285
+ if (entry.isDirectory()) {
286
+ files.push(...findJsonlFiles(fullPath));
287
+ } else if (entry.name.endsWith(".jsonl")) {
288
+ files.push(fullPath);
289
+ }
290
+ }
291
+ } catch {
236
292
  }
237
- return 1;
293
+ return files;
238
294
  }
239
- function parseUsageFromJsonl() {
240
- const projectsDir = path.join(os.homedir(), ".claude", "projects");
241
- if (!fs.existsSync(projectsDir)) {
295
+ function estimateCost(model, inputTokens, outputTokens) {
296
+ const pricing = {
297
+ "claude-opus-4": { input: 15, output: 75 },
298
+ "claude-sonnet-4": { input: 3, output: 15 },
299
+ "claude-haiku": { input: 0.25, output: 1.25 },
300
+ default: { input: 3, output: 15 }
301
+ };
302
+ let modelKey = "default";
303
+ for (const key of Object.keys(pricing)) {
304
+ if (model.includes(key.replace("claude-", ""))) {
305
+ modelKey = key;
306
+ break;
307
+ }
308
+ }
309
+ const price = pricing[modelKey];
310
+ const inputCost = inputTokens / 1e6 * price.input;
311
+ const outputCost = outputTokens / 1e6 * price.output;
312
+ return Math.round((inputCost + outputCost) * 100) / 100;
313
+ }
314
+ function scanUsageData() {
315
+ const projectsDir = getClaudeProjectsDir();
316
+ if (!fs2.existsSync(projectsDir)) {
242
317
  return null;
243
318
  }
244
319
  let totalInputTokens = 0;
245
320
  let totalOutputTokens = 0;
246
321
  let totalCacheRead = 0;
247
322
  let totalCacheWrite = 0;
323
+ let totalCost = 0;
324
+ let sessionsCount = 0;
248
325
  const dates = /* @__PURE__ */ new Set();
249
- function findJsonlFiles(dir) {
250
- const files = [];
251
- try {
252
- const entries = fs.readdirSync(dir, { withFileTypes: true });
253
- for (const entry of entries) {
254
- const fullPath = path.join(dir, entry.name);
255
- if (entry.isDirectory()) {
256
- files.push(...findJsonlFiles(fullPath));
257
- } else if (entry.name.endsWith(".jsonl")) {
258
- files.push(fullPath);
259
- }
260
- }
261
- } catch {
262
- }
263
- return files;
264
- }
326
+ const models = {};
327
+ const projects = {};
328
+ const dailyData = {};
329
+ let firstTimestamp = null;
330
+ let lastTimestamp = null;
265
331
  const jsonlFiles = findJsonlFiles(projectsDir);
332
+ sessionsCount = jsonlFiles.length;
266
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++;
267
344
  try {
268
- const content = fs.readFileSync(filePath, "utf-8");
345
+ const content = fs2.readFileSync(filePath, "utf-8");
269
346
  const lines = content.split("\n").filter((line) => line.trim());
270
347
  for (const line of lines) {
271
348
  try {
272
349
  const event = JSON.parse(line);
273
350
  if (event.type === "assistant" && event.message?.usage) {
274
351
  const usage = event.message.usage;
275
- totalInputTokens += usage.input_tokens || 0;
276
- totalOutputTokens += usage.output_tokens || 0;
352
+ const model = event.message.model || "unknown";
353
+ const inputTokens = usage.input_tokens || 0;
354
+ const outputTokens = usage.output_tokens || 0;
355
+ totalInputTokens += inputTokens;
356
+ totalOutputTokens += outputTokens;
277
357
  totalCacheRead += usage.cache_read_input_tokens || 0;
278
358
  totalCacheWrite += usage.cache_creation_input_tokens || 0;
359
+ const messageCost = estimateCost(model, inputTokens, outputTokens);
360
+ totalCost += messageCost;
361
+ const totalModelTokens = inputTokens + outputTokens;
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;
279
366
  if (event.timestamp) {
280
367
  const date = new Date(event.timestamp).toISOString().split("T")[0];
281
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;
385
+ if (!firstTimestamp || event.timestamp < firstTimestamp) {
386
+ firstTimestamp = event.timestamp;
387
+ }
388
+ if (!lastTimestamp || event.timestamp > lastTimestamp) {
389
+ lastTimestamp = event.timestamp;
390
+ }
282
391
  }
283
392
  }
284
393
  } catch {
@@ -291,18 +400,125 @@ function parseUsageFromJsonl() {
291
400
  if (totalTokens === 0) {
292
401
  return null;
293
402
  }
294
- const costPerMillion = 3;
295
- const totalCost = totalTokens / 1e6 * costPerMillion;
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();
296
416
  return {
297
- totalTokens,
298
- totalCost: Math.round(totalCost * 100) / 100,
299
- inputTokens: totalInputTokens,
300
- outputTokens: totalOutputTokens,
301
- cacheReadTokens: totalCacheRead,
302
- cacheWriteTokens: totalCacheWrite,
303
- daysTracked: dates.size || 1
417
+ version: CCGATHER_JSON_VERSION,
418
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
419
+ lastScanned: (/* @__PURE__ */ new Date()).toISOString(),
420
+ usage: {
421
+ totalTokens,
422
+ totalCost: Math.round(totalCost * 100) / 100,
423
+ inputTokens: totalInputTokens,
424
+ outputTokens: totalOutputTokens,
425
+ cacheReadTokens: totalCacheRead,
426
+ cacheWriteTokens: totalCacheWrite
427
+ },
428
+ stats: {
429
+ daysTracked: dates.size,
430
+ sessionsCount,
431
+ firstUsed: firstTimestamp ? new Date(firstTimestamp).toISOString().split("T")[0] : null,
432
+ lastUsed: lastTimestamp ? new Date(lastTimestamp).toISOString().split("T")[0] : null
433
+ },
434
+ models,
435
+ projects,
436
+ dailyUsage,
437
+ account: {
438
+ ccplan: credentials.ccplan,
439
+ rateLimitTier: credentials.rateLimitTier
440
+ }
304
441
  };
305
442
  }
443
+ function readCCGatherJson() {
444
+ const jsonPath = getCCGatherJsonPath();
445
+ if (!fs2.existsSync(jsonPath)) {
446
+ return null;
447
+ }
448
+ try {
449
+ const content = fs2.readFileSync(jsonPath, "utf-8");
450
+ return JSON.parse(content);
451
+ } catch {
452
+ return null;
453
+ }
454
+ }
455
+ function writeCCGatherJson(data) {
456
+ const jsonPath = getCCGatherJsonPath();
457
+ const claudeDir = path2.dirname(jsonPath);
458
+ if (!fs2.existsSync(claudeDir)) {
459
+ fs2.mkdirSync(claudeDir, { recursive: true });
460
+ }
461
+ fs2.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
462
+ }
463
+ function scanAndSave() {
464
+ const data = scanUsageData();
465
+ if (data) {
466
+ writeCCGatherJson(data);
467
+ }
468
+ return data;
469
+ }
470
+
471
+ // src/commands/submit.ts
472
+ function ccgatherToUsageData(data) {
473
+ return {
474
+ totalTokens: data.usage.totalTokens,
475
+ totalCost: data.usage.totalCost,
476
+ inputTokens: data.usage.inputTokens,
477
+ outputTokens: data.usage.outputTokens,
478
+ cacheReadTokens: data.usage.cacheReadTokens,
479
+ cacheWriteTokens: data.usage.cacheWriteTokens,
480
+ daysTracked: data.stats.daysTracked
481
+ };
482
+ }
483
+ function findCcJson() {
484
+ const possiblePaths = [
485
+ path3.join(process.cwd(), "cc.json"),
486
+ path3.join(os3.homedir(), "cc.json"),
487
+ path3.join(os3.homedir(), ".claude", "cc.json")
488
+ ];
489
+ for (const p of possiblePaths) {
490
+ if (fs3.existsSync(p)) {
491
+ return p;
492
+ }
493
+ }
494
+ return null;
495
+ }
496
+ function parseCcJson(filePath) {
497
+ try {
498
+ const content = fs3.readFileSync(filePath, "utf-8");
499
+ const data = JSON.parse(content);
500
+ return {
501
+ totalTokens: data.totalTokens || data.total_tokens || 0,
502
+ totalCost: data.totalCost || data.total_cost || data.costUSD || 0,
503
+ inputTokens: data.inputTokens || data.input_tokens || 0,
504
+ outputTokens: data.outputTokens || data.output_tokens || 0,
505
+ cacheReadTokens: data.cacheReadTokens || data.cache_read_tokens || 0,
506
+ cacheWriteTokens: data.cacheWriteTokens || data.cache_write_tokens || 0,
507
+ daysTracked: data.daysTracked || data.days_tracked || calculateDaysTracked(data)
508
+ };
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
513
+ function calculateDaysTracked(data) {
514
+ if (data.dailyStats && Array.isArray(data.dailyStats)) {
515
+ return data.dailyStats.length;
516
+ }
517
+ if (data.daily && typeof data.daily === "object") {
518
+ return Object.keys(data.daily).length;
519
+ }
520
+ return 1;
521
+ }
306
522
  function formatNumber(num) {
307
523
  return num.toLocaleString();
308
524
  }
@@ -373,36 +589,55 @@ async function submit(options) {
373
589
  }
374
590
  ]);
375
591
  username = confirmedUsername.trim();
376
- const ccJsonPath = findCcJson();
377
592
  let usageData = null;
378
593
  let dataSource = "";
379
- if (ccJsonPath) {
380
- const { useCcJson } = await inquirer2.default.prompt([
381
- {
382
- type: "confirm",
383
- name: "useCcJson",
384
- message: `Found existing cc.json. Use this file?`,
385
- default: true
594
+ const ccgatherData = readCCGatherJson();
595
+ if (ccgatherData) {
596
+ usageData = ccgatherToUsageData(ccgatherData);
597
+ dataSource = "ccgather.json";
598
+ console.log(import_chalk.default.green(`\u2713 Found ${dataSource}`));
599
+ console.log(import_chalk.default.gray(` Last scanned: ${new Date(ccgatherData.lastScanned).toLocaleString()}
600
+ `));
601
+ }
602
+ if (!usageData) {
603
+ const ccJsonPath = findCcJson();
604
+ if (ccJsonPath) {
605
+ const { useCcJson } = await inquirer2.default.prompt([
606
+ {
607
+ type: "confirm",
608
+ name: "useCcJson",
609
+ message: `Found existing cc.json. Use this file?`,
610
+ default: true
611
+ }
612
+ ]);
613
+ if (useCcJson) {
614
+ usageData = parseCcJson(ccJsonPath);
615
+ dataSource = "cc.json";
386
616
  }
387
- ]);
388
- if (useCcJson) {
389
- usageData = parseCcJson(ccJsonPath);
390
- dataSource = "cc.json";
391
617
  }
392
618
  }
393
619
  if (!usageData) {
394
620
  const parseSpinner = (0, import_ora.default)("Scanning Claude Code usage data...").start();
395
- usageData = parseUsageFromJsonl();
621
+ const scannedData = scanAndSave();
396
622
  parseSpinner.stop();
397
- dataSource = "Claude Code logs";
623
+ if (scannedData) {
624
+ usageData = ccgatherToUsageData(scannedData);
625
+ dataSource = "Claude Code logs";
626
+ console.log(import_chalk.default.green(`\u2713 Scanned and saved to ccgather.json`));
627
+ console.log(import_chalk.default.gray(` Path: ${getCCGatherJsonPath()}
628
+ `));
629
+ }
398
630
  }
399
631
  if (!usageData) {
400
632
  console.log(import_chalk.default.red("\n\u274C No usage data found."));
401
- console.log(import_chalk.default.gray("Make sure you have used Claude Code or have a cc.json file."));
633
+ console.log(import_chalk.default.gray("Make sure you have used Claude Code."));
634
+ console.log(import_chalk.default.gray("Run: npx ccgather scan\n"));
402
635
  process.exit(1);
403
636
  }
404
- console.log(import_chalk.default.green(`\u2713 Using ${dataSource}
637
+ if (dataSource && dataSource !== "Claude Code logs") {
638
+ console.log(import_chalk.default.green(`\u2713 Using ${dataSource}
405
639
  `));
640
+ }
406
641
  console.log(import_chalk.default.bold("Summary:"));
407
642
  console.log(import_chalk.default.gray(` Total Cost: ${import_chalk.default.green("$" + formatNumber(Math.round(usageData.totalCost)))}`));
408
643
  console.log(import_chalk.default.gray(` Total Tokens: ${import_chalk.default.cyan(formatNumber(usageData.totalTokens))}`));
@@ -557,14 +792,14 @@ async function status(options) {
557
792
  var import_chalk4 = __toESM(require("chalk"));
558
793
  var import_ora4 = __toESM(require("ora"));
559
794
  var http = __toESM(require("http"));
560
- var fs3 = __toESM(require("fs"));
561
- var path3 = __toESM(require("path"));
562
- var os3 = __toESM(require("os"));
795
+ var fs5 = __toESM(require("fs"));
796
+ var path5 = __toESM(require("path"));
797
+ var os5 = __toESM(require("os"));
563
798
  init_config();
564
799
  init_config();
565
800
  var CALLBACK_PORT = 9876;
566
801
  function getClaudeSettingsDir2() {
567
- return path3.join(os3.homedir(), ".claude");
802
+ return path5.join(os5.homedir(), ".claude");
568
803
  }
569
804
  async function openBrowser(url) {
570
805
  const { default: open } = await import("open");
@@ -794,14 +1029,14 @@ if (usageData) {
794
1029
  }
795
1030
  function installStopHook() {
796
1031
  const claudeDir = getClaudeSettingsDir2();
797
- const settingsPath = path3.join(claudeDir, "settings.json");
798
- if (!fs3.existsSync(claudeDir)) {
799
- fs3.mkdirSync(claudeDir, { recursive: true });
1032
+ const settingsPath = path5.join(claudeDir, "settings.json");
1033
+ if (!fs5.existsSync(claudeDir)) {
1034
+ fs5.mkdirSync(claudeDir, { recursive: true });
800
1035
  }
801
1036
  let settings = {};
802
1037
  try {
803
- if (fs3.existsSync(settingsPath)) {
804
- const content = fs3.readFileSync(settingsPath, "utf-8");
1038
+ if (fs5.existsSync(settingsPath)) {
1039
+ const content = fs5.readFileSync(settingsPath, "utf-8");
805
1040
  settings = JSON.parse(content);
806
1041
  }
807
1042
  } catch {
@@ -810,7 +1045,7 @@ function installStopHook() {
810
1045
  settings.hooks = {};
811
1046
  }
812
1047
  const hooks = settings.hooks;
813
- const syncScriptPath = path3.join(claudeDir, "ccgather-sync.js");
1048
+ const syncScriptPath = path5.join(claudeDir, "ccgather-sync.js");
814
1049
  const hookCommand = `node "${syncScriptPath}"`;
815
1050
  if (!hooks.Stop || !Array.isArray(hooks.Stop)) {
816
1051
  hooks.Stop = [];
@@ -824,16 +1059,16 @@ function installStopHook() {
824
1059
  background: true
825
1060
  });
826
1061
  }
827
- fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1062
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
828
1063
  return { success: true, message: "Stop hook installed" };
829
1064
  }
830
1065
  function saveSyncScript(apiUrl, apiToken) {
831
1066
  const claudeDir = getClaudeSettingsDir2();
832
- const scriptPath = path3.join(claudeDir, "ccgather-sync.js");
1067
+ const scriptPath = path5.join(claudeDir, "ccgather-sync.js");
833
1068
  const scriptContent = generateSyncScript(apiUrl, apiToken);
834
- fs3.writeFileSync(scriptPath, scriptContent);
835
- if (os3.platform() !== "win32") {
836
- fs3.chmodSync(scriptPath, "755");
1069
+ fs5.writeFileSync(scriptPath, scriptContent);
1070
+ if (os5.platform() !== "win32") {
1071
+ fs5.chmodSync(scriptPath, "755");
837
1072
  }
838
1073
  }
839
1074
  async function setupAuto(options = {}) {
@@ -841,14 +1076,14 @@ async function setupAuto(options = {}) {
841
1076
  console.log(import_chalk4.default.bold("\n\u{1F527} Disabling Auto-Sync\n"));
842
1077
  const { reset: reset2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
843
1078
  await reset2();
844
- console.log(import_chalk4.default.green("\u2713 Auto-sync disabled. Use `npx ccg` to submit manually."));
1079
+ console.log(import_chalk4.default.green("\u2713 Auto-sync disabled. Use `npx ccgather` to submit manually."));
845
1080
  return;
846
1081
  }
847
1082
  console.log(import_chalk4.default.bold("\n\u26A0\uFE0F Auto-Sync Mode (Optional)\n"));
848
1083
  console.log(import_chalk4.default.gray("This will install a hook that automatically syncs"));
849
1084
  console.log(import_chalk4.default.gray("your usage data when Claude Code sessions end."));
850
1085
  console.log();
851
- console.log(import_chalk4.default.yellow("Note: Manual submission (`npx ccg`) is recommended for most users."));
1086
+ console.log(import_chalk4.default.yellow("Note: Manual submission (`npx ccgather`) is recommended for most users."));
852
1087
  console.log();
853
1088
  const inquirer2 = await import("inquirer");
854
1089
  const { proceed } = await inquirer2.default.prompt([
@@ -860,7 +1095,7 @@ async function setupAuto(options = {}) {
860
1095
  }
861
1096
  ]);
862
1097
  if (!proceed) {
863
- console.log(import_chalk4.default.gray("\nSetup cancelled. Use `npx ccg` to submit manually."));
1098
+ console.log(import_chalk4.default.gray("\nSetup cancelled. Use `npx ccgather` to submit manually."));
864
1099
  return;
865
1100
  }
866
1101
  const config = getConfig();
@@ -924,7 +1159,7 @@ async function setupAuto(options = {}) {
924
1159
  console.log(import_chalk4.default.gray("to the leaderboard when each session ends."));
925
1160
  console.log();
926
1161
  console.log(import_chalk4.default.gray("View your stats:"));
927
- console.log(import_chalk4.default.cyan(" npx ccg status"));
1162
+ console.log(import_chalk4.default.cyan(" npx ccgather status"));
928
1163
  console.log();
929
1164
  console.log(import_chalk4.default.gray("View the leaderboard:"));
930
1165
  console.log(import_chalk4.default.cyan(" https://ccgather.dev/leaderboard"));
@@ -936,9 +1171,102 @@ async function setupAuto(options = {}) {
936
1171
  }
937
1172
  }
938
1173
 
1174
+ // src/commands/scan.ts
1175
+ var import_chalk5 = __toESM(require("chalk"));
1176
+ var import_ora5 = __toESM(require("ora"));
1177
+ function formatNumber3(num) {
1178
+ if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
1179
+ if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
1180
+ return num.toLocaleString();
1181
+ }
1182
+ function displayResults(data) {
1183
+ const orange = import_chalk5.default.hex("#FF6B35");
1184
+ const green = import_chalk5.default.hex("#10B981");
1185
+ const gray = import_chalk5.default.gray;
1186
+ const white = import_chalk5.default.white;
1187
+ const bold = import_chalk5.default.bold;
1188
+ const cyan = import_chalk5.default.cyan;
1189
+ console.log();
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"));
1191
+ console.log(
1192
+ gray(" \u2502") + white(" \u{1F4CA} Usage Summary") + gray(" \u2502")
1193
+ );
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"));
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
+ );
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"));
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
+ );
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"));
1224
+ if (Object.keys(data.models).length > 0) {
1225
+ console.log();
1226
+ console.log(gray(" ") + bold("Model Breakdown:"));
1227
+ for (const [model, tokens] of Object.entries(data.models)) {
1228
+ const shortModel = model.replace("claude-", "").substring(0, 20);
1229
+ console.log(gray(" \u2022 ") + white(shortModel.padEnd(22)) + orange(formatNumber3(tokens)));
1230
+ }
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
+ }
1246
+ console.log();
1247
+ console.log(gray(` \u{1F4C1} Saved to: ${getCCGatherJsonPath()}`));
1248
+ console.log();
1249
+ }
1250
+ async function scan() {
1251
+ console.log(import_chalk5.default.bold("\n\u{1F50D} Scanning Claude Code Usage\n"));
1252
+ const spinner = (0, import_ora5.default)("Scanning JSONL files...").start();
1253
+ const data = scanAndSave();
1254
+ if (!data) {
1255
+ spinner.fail(import_chalk5.default.red("No usage data found"));
1256
+ console.log(import_chalk5.default.gray("\nPossible reasons:"));
1257
+ console.log(import_chalk5.default.gray(" \u2022 Claude Code has not been used yet"));
1258
+ console.log(import_chalk5.default.gray(" \u2022 ~/.claude/projects/ directory is empty"));
1259
+ console.log(import_chalk5.default.gray(" \u2022 No permission to read files\n"));
1260
+ process.exit(1);
1261
+ }
1262
+ spinner.succeed(import_chalk5.default.green("Scan complete!"));
1263
+ displayResults(data);
1264
+ }
1265
+
939
1266
  // src/index.ts
940
1267
  var program = new import_commander.Command();
941
- program.name("ccg").description("Submit your Claude Code usage to the CCgather leaderboard").version("1.0.0").option("-y, --yes", "Skip confirmation prompt").option("--auto", "Enable automatic sync on session end").option("--manual", "Disable automatic sync");
1268
+ program.name("ccgather").description("Submit your Claude Code usage to the CCgather leaderboard").version("1.0.0").option("-y, --yes", "Skip confirmation prompt").option("--auto", "Enable automatic sync on session end").option("--manual", "Disable automatic sync");
1269
+ program.command("scan").description("Scan Claude Code usage and create ccgather.json").action(scan);
942
1270
  program.command("rank").description("View your current rank and stats").action(status);
943
1271
  program.command("reset").description("Remove auto-sync hook and clear config").action(async () => {
944
1272
  const { reset: reset2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
package/package.json CHANGED
@@ -1,51 +1,51 @@
1
- {
2
- "name": "ccgather",
3
- "version": "1.0.0",
4
- "description": "CLI tool for syncing Claude Code usage data to CCgather leaderboard",
5
- "bin": {
6
- "ccgather": "./dist/index.js",
7
- "ccg": "./dist/index.js"
8
- },
9
- "main": "./dist/index.js",
10
- "types": "./dist/index.d.ts",
11
- "scripts": {
12
- "build": "tsup src/index.ts --format cjs --dts",
13
- "dev": "tsup src/index.ts --format cjs --dts --watch",
14
- "start": "node dist/index.js",
15
- "typecheck": "tsc --noEmit"
16
- },
17
- "keywords": [
18
- "claude",
19
- "anthropic",
20
- "claude-code",
21
- "ccgather",
22
- "leaderboard",
23
- "cli"
24
- ],
25
- "author": "",
26
- "license": "MIT",
27
- "dependencies": {
28
- "chalk": "^5.3.0",
29
- "commander": "^12.1.0",
30
- "conf": "^13.0.1",
31
- "inquirer": "^10.2.2",
32
- "open": "^10.1.0",
33
- "ora": "^8.1.0"
34
- },
35
- "devDependencies": {
36
- "@types/inquirer": "^9.0.7",
37
- "@types/node": "^22.10.2",
38
- "tsup": "^8.3.5",
39
- "typescript": "^5.7.2"
40
- },
41
- "engines": {
42
- "node": ">=18"
43
- },
44
- "files": [
45
- "dist"
46
- ],
47
- "repository": {
48
- "type": "git",
49
- "url": "https://github.com/your-username/ccgather"
50
- }
51
- }
1
+ {
2
+ "name": "ccgather",
3
+ "version": "1.1.0",
4
+ "description": "CLI tool for syncing Claude Code usage data to CCgather leaderboard",
5
+ "bin": {
6
+ "ccgather": "./dist/index.js",
7
+ "ccg": "./dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "scripts": {
12
+ "build": "tsup src/index.ts --format cjs --dts",
13
+ "dev": "tsup src/index.ts --format cjs --dts --watch",
14
+ "start": "node dist/index.js",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "keywords": [
18
+ "claude",
19
+ "anthropic",
20
+ "claude-code",
21
+ "ccgather",
22
+ "leaderboard",
23
+ "cli"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "chalk": "^5.3.0",
29
+ "commander": "^12.1.0",
30
+ "conf": "^13.0.1",
31
+ "inquirer": "^10.2.2",
32
+ "open": "^10.1.0",
33
+ "ora": "^8.1.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/inquirer": "^9.0.7",
37
+ "@types/node": "^22.10.2",
38
+ "tsup": "^8.3.5",
39
+ "typescript": "^5.7.2"
40
+ },
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "files": [
45
+ "dist"
46
+ ],
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/DHxYoon/CCgather"
50
+ }
51
+ }