ccgather 1.0.1 → 1.2.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 +1087 -267
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -70,22 +70,85 @@ var init_config = __esm({
70
70
  }
71
71
  });
72
72
 
73
+ // src/lib/api.ts
74
+ async function fetchApi(endpoint, options = {}) {
75
+ const config = getConfig();
76
+ const apiToken = config.get("apiToken");
77
+ const apiUrl = getApiUrl();
78
+ if (!apiToken) {
79
+ return { success: false, error: "Not authenticated. Run: npx ccgather auth" };
80
+ }
81
+ try {
82
+ const response = await fetch(`${apiUrl}${endpoint}`, {
83
+ ...options,
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ Authorization: `Bearer ${apiToken}`,
87
+ ...options.headers
88
+ }
89
+ });
90
+ const data = await response.json();
91
+ if (!response.ok) {
92
+ return { success: false, error: data.error || `HTTP ${response.status}` };
93
+ }
94
+ return { success: true, data };
95
+ } catch (error2) {
96
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
97
+ return { success: false, error: message };
98
+ }
99
+ }
100
+ async function syncUsage(payload) {
101
+ return fetchApi("/cli/sync", {
102
+ method: "POST",
103
+ body: JSON.stringify(payload)
104
+ });
105
+ }
106
+ async function getStatus() {
107
+ return fetchApi("/cli/status");
108
+ }
109
+ async function verifyToken(token) {
110
+ try {
111
+ const apiUrl = getApiUrl();
112
+ const response = await fetch(`${apiUrl}/cli/verify`, {
113
+ method: "POST",
114
+ headers: {
115
+ "Content-Type": "application/json",
116
+ Authorization: `Bearer ${token}`
117
+ }
118
+ });
119
+ const data = await response.json();
120
+ if (!response.ok) {
121
+ return { success: false, error: data.error || "Invalid token" };
122
+ }
123
+ return { success: true, data };
124
+ } catch (error2) {
125
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
126
+ return { success: false, error: message };
127
+ }
128
+ }
129
+ var init_api = __esm({
130
+ "src/lib/api.ts"() {
131
+ "use strict";
132
+ init_config();
133
+ }
134
+ });
135
+
73
136
  // src/commands/reset.ts
74
137
  var reset_exports = {};
75
138
  __export(reset_exports, {
76
139
  reset: () => reset
77
140
  });
78
141
  function getClaudeSettingsDir() {
79
- return path3.join(os3.homedir(), ".claude");
142
+ return path4.join(os4.homedir(), ".claude");
80
143
  }
81
144
  function removeStopHook() {
82
145
  const claudeDir = getClaudeSettingsDir();
83
- const settingsPath = path3.join(claudeDir, "settings.json");
84
- if (!fs3.existsSync(settingsPath)) {
146
+ const settingsPath = path4.join(claudeDir, "settings.json");
147
+ if (!fs4.existsSync(settingsPath)) {
85
148
  return { success: true, message: "No settings file found" };
86
149
  }
87
150
  try {
88
- const content = fs3.readFileSync(settingsPath, "utf-8");
151
+ const content = fs4.readFileSync(settingsPath, "utf-8");
89
152
  const settings = JSON.parse(content);
90
153
  if (settings.hooks && typeof settings.hooks === "object") {
91
154
  const hooks = settings.hooks;
@@ -102,7 +165,7 @@ function removeStopHook() {
102
165
  }
103
166
  }
104
167
  }
105
- fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
168
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
106
169
  return { success: true, message: "Hook removed" };
107
170
  } catch (err) {
108
171
  return {
@@ -113,16 +176,16 @@ function removeStopHook() {
113
176
  }
114
177
  function removeSyncScript() {
115
178
  const claudeDir = getClaudeSettingsDir();
116
- const scriptPath = path3.join(claudeDir, "ccgather-sync.js");
117
- if (fs3.existsSync(scriptPath)) {
118
- fs3.unlinkSync(scriptPath);
179
+ const scriptPath = path4.join(claudeDir, "ccgather-sync.js");
180
+ if (fs4.existsSync(scriptPath)) {
181
+ fs4.unlinkSync(scriptPath);
119
182
  }
120
183
  }
121
184
  async function reset() {
122
185
  const config = getConfig();
123
- console.log(import_chalk3.default.bold("\n\u{1F504} CCgather Reset\n"));
186
+ console.log(import_chalk2.default.bold("\n\u{1F504} CCgather Reset\n"));
124
187
  if (!config.get("apiToken")) {
125
- console.log(import_chalk3.default.yellow("CCgather is not configured."));
188
+ console.log(import_chalk2.default.yellow("CCgather is not configured."));
126
189
  return;
127
190
  }
128
191
  const { confirmReset } = await import_inquirer.default.prompt([
@@ -134,87 +197,404 @@ async function reset() {
134
197
  }
135
198
  ]);
136
199
  if (!confirmReset) {
137
- console.log(import_chalk3.default.gray("Reset cancelled."));
200
+ console.log(import_chalk2.default.gray("Reset cancelled."));
138
201
  return;
139
202
  }
140
203
  const hookSpinner = (0, import_ora3.default)("Removing Claude Code hook...").start();
141
204
  const hookResult = removeStopHook();
142
205
  if (hookResult.success) {
143
- hookSpinner.succeed(import_chalk3.default.green("Hook removed"));
206
+ hookSpinner.succeed(import_chalk2.default.green("Hook removed"));
144
207
  } else {
145
- hookSpinner.warn(import_chalk3.default.yellow(`Could not remove hook: ${hookResult.message}`));
208
+ hookSpinner.warn(import_chalk2.default.yellow(`Could not remove hook: ${hookResult.message}`));
146
209
  }
147
210
  const scriptSpinner = (0, import_ora3.default)("Removing sync script...").start();
148
211
  try {
149
212
  removeSyncScript();
150
- scriptSpinner.succeed(import_chalk3.default.green("Sync script removed"));
213
+ scriptSpinner.succeed(import_chalk2.default.green("Sync script removed"));
151
214
  } catch {
152
- scriptSpinner.warn(import_chalk3.default.yellow("Could not remove sync script"));
215
+ scriptSpinner.warn(import_chalk2.default.yellow("Could not remove sync script"));
153
216
  }
154
217
  const { deleteAccount } = await import_inquirer.default.prompt([
155
218
  {
156
219
  type: "confirm",
157
220
  name: "deleteAccount",
158
- message: import_chalk3.default.red("Do you also want to delete your account from the leaderboard? (This cannot be undone)"),
221
+ message: import_chalk2.default.red("Do you also want to delete your account from the leaderboard? (This cannot be undone)"),
159
222
  default: false
160
223
  }
161
224
  ]);
162
225
  if (deleteAccount) {
163
- console.log(import_chalk3.default.yellow("\nAccount deletion is not yet implemented."));
164
- console.log(import_chalk3.default.gray("Please contact support to delete your account."));
226
+ console.log(import_chalk2.default.yellow("\nAccount deletion is not yet implemented."));
227
+ console.log(import_chalk2.default.gray("Please contact support to delete your account."));
165
228
  }
166
229
  const configSpinner = (0, import_ora3.default)("Resetting local configuration...").start();
167
230
  resetConfig();
168
- configSpinner.succeed(import_chalk3.default.green("Local configuration reset"));
231
+ configSpinner.succeed(import_chalk2.default.green("Local configuration reset"));
169
232
  console.log();
170
- console.log(import_chalk3.default.green.bold("\u2705 Reset complete!"));
233
+ console.log(import_chalk2.default.green.bold("\u2705 Reset complete!"));
171
234
  console.log();
172
- console.log(import_chalk3.default.gray("Your usage will no longer be tracked."));
173
- console.log(import_chalk3.default.gray("Run `npx ccgather` to set up again."));
235
+ console.log(import_chalk2.default.gray("Your usage will no longer be tracked."));
236
+ console.log(import_chalk2.default.gray("Run `npx ccgather` to set up again."));
174
237
  console.log();
175
238
  }
176
- var import_chalk3, import_ora3, fs3, path3, os3, import_inquirer;
239
+ var import_chalk2, import_ora3, fs4, path4, os4, import_inquirer;
177
240
  var init_reset = __esm({
178
241
  "src/commands/reset.ts"() {
179
242
  "use strict";
180
- import_chalk3 = __toESM(require("chalk"));
243
+ import_chalk2 = __toESM(require("chalk"));
181
244
  import_ora3 = __toESM(require("ora"));
182
- fs3 = __toESM(require("fs"));
183
- path3 = __toESM(require("path"));
184
- os3 = __toESM(require("os"));
245
+ fs4 = __toESM(require("fs"));
246
+ path4 = __toESM(require("path"));
247
+ os4 = __toESM(require("os"));
185
248
  import_inquirer = __toESM(require("inquirer"));
186
249
  init_config();
187
250
  }
188
251
  });
189
252
 
253
+ // src/lib/claude.ts
254
+ function getClaudeConfigDir() {
255
+ const platform3 = os6.platform();
256
+ if (platform3 === "win32") {
257
+ return path6.join(os6.homedir(), "AppData", "Roaming", "claude-code");
258
+ } else if (platform3 === "darwin") {
259
+ return path6.join(os6.homedir(), "Library", "Application Support", "claude-code");
260
+ } else {
261
+ return path6.join(os6.homedir(), ".config", "claude-code");
262
+ }
263
+ }
264
+ function getUsageFilePaths() {
265
+ const configDir = getClaudeConfigDir();
266
+ return [
267
+ path6.join(configDir, "usage.json"),
268
+ path6.join(configDir, "stats.json"),
269
+ path6.join(configDir, "data", "usage.json"),
270
+ path6.join(os6.homedir(), ".claude", "usage.json"),
271
+ path6.join(os6.homedir(), ".claude-code", "usage.json")
272
+ ];
273
+ }
274
+ function readClaudeUsage() {
275
+ const possiblePaths = getUsageFilePaths();
276
+ for (const filePath of possiblePaths) {
277
+ try {
278
+ if (fs6.existsSync(filePath)) {
279
+ const content = fs6.readFileSync(filePath, "utf-8");
280
+ const data = JSON.parse(content);
281
+ if (data.usage) {
282
+ return {
283
+ totalTokens: data.usage.total_tokens || 0,
284
+ totalSpent: data.usage.total_cost || 0,
285
+ modelBreakdown: data.usage.by_model || {},
286
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
287
+ };
288
+ }
289
+ if (data.sessions && data.sessions.length > 0) {
290
+ const breakdown = {};
291
+ let totalTokens = 0;
292
+ let totalSpent = 0;
293
+ for (const session of data.sessions) {
294
+ totalTokens += session.tokens_used || 0;
295
+ totalSpent += session.cost || 0;
296
+ breakdown[session.model] = (breakdown[session.model] || 0) + session.tokens_used;
297
+ }
298
+ return {
299
+ totalTokens,
300
+ totalSpent,
301
+ modelBreakdown: breakdown,
302
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
303
+ };
304
+ }
305
+ }
306
+ } catch (error2) {
307
+ }
308
+ }
309
+ return null;
310
+ }
311
+ function isClaudeCodeInstalled() {
312
+ const configDir = getClaudeConfigDir();
313
+ return fs6.existsSync(configDir);
314
+ }
315
+ function getMockUsageData() {
316
+ return {
317
+ totalTokens: Math.floor(Math.random() * 1e6) + 1e4,
318
+ totalSpent: Math.random() * 50 + 5,
319
+ modelBreakdown: {
320
+ "claude-3-5-sonnet-20241022": Math.floor(Math.random() * 5e5),
321
+ "claude-3-opus-20240229": Math.floor(Math.random() * 1e5),
322
+ "claude-3-haiku-20240307": Math.floor(Math.random() * 2e5)
323
+ },
324
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
325
+ };
326
+ }
327
+ var fs6, path6, os6;
328
+ var init_claude = __esm({
329
+ "src/lib/claude.ts"() {
330
+ "use strict";
331
+ fs6 = __toESM(require("fs"));
332
+ path6 = __toESM(require("path"));
333
+ os6 = __toESM(require("os"));
334
+ }
335
+ });
336
+
337
+ // src/commands/sync.ts
338
+ var sync_exports = {};
339
+ __export(sync_exports, {
340
+ sync: () => sync
341
+ });
342
+ function formatNumber2(num) {
343
+ if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
344
+ if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
345
+ return num.toString();
346
+ }
347
+ async function sync(options) {
348
+ const config = getConfig();
349
+ console.log(import_chalk4.default.bold("\n\u{1F504} CCgather Sync\n"));
350
+ if (!isAuthenticated()) {
351
+ console.log(import_chalk4.default.red("Not authenticated."));
352
+ console.log(import_chalk4.default.gray("Run: npx ccgather auth\n"));
353
+ process.exit(1);
354
+ }
355
+ if (!isClaudeCodeInstalled()) {
356
+ console.log(import_chalk4.default.yellow("\u26A0\uFE0F Claude Code installation not detected."));
357
+ console.log(import_chalk4.default.gray("Make sure Claude Code is installed and has been used at least once.\n"));
358
+ if (process.env.CCGATHER_DEMO === "true") {
359
+ console.log(import_chalk4.default.gray("Demo mode: Using mock data..."));
360
+ } else {
361
+ process.exit(1);
362
+ }
363
+ }
364
+ const lastSync = config.get("lastSync");
365
+ if (lastSync && !options.force) {
366
+ const lastSyncDate = new Date(lastSync);
367
+ const minInterval = 5 * 60 * 1e3;
368
+ const timeSinceSync = Date.now() - lastSyncDate.getTime();
369
+ if (timeSinceSync < minInterval) {
370
+ const remaining = Math.ceil((minInterval - timeSinceSync) / 1e3 / 60);
371
+ console.log(import_chalk4.default.yellow(`\u23F3 Please wait ${remaining} minutes before syncing again.`));
372
+ console.log(import_chalk4.default.gray("Use --force to override.\n"));
373
+ process.exit(0);
374
+ }
375
+ }
376
+ const spinner = (0, import_ora6.default)("Reading Claude Code usage data...").start();
377
+ let usageData = readClaudeUsage();
378
+ if (!usageData && process.env.CCGATHER_DEMO === "true") {
379
+ usageData = getMockUsageData();
380
+ }
381
+ if (!usageData) {
382
+ spinner.fail(import_chalk4.default.red("Failed to read usage data"));
383
+ console.log(import_chalk4.default.gray("\nPossible reasons:"));
384
+ console.log(import_chalk4.default.gray(" - Claude Code has not been used yet"));
385
+ console.log(import_chalk4.default.gray(" - Usage data file is missing or corrupted"));
386
+ console.log(import_chalk4.default.gray(" - Insufficient permissions to read the file\n"));
387
+ process.exit(1);
388
+ }
389
+ spinner.text = "Syncing to CCgather...";
390
+ const result = await syncUsage({
391
+ totalTokens: usageData.totalTokens,
392
+ totalSpent: usageData.totalSpent,
393
+ modelBreakdown: usageData.modelBreakdown,
394
+ timestamp: usageData.lastUpdated
395
+ });
396
+ if (!result.success) {
397
+ spinner.fail(import_chalk4.default.red("Sync failed"));
398
+ console.log(import_chalk4.default.red(`Error: ${result.error}
399
+ `));
400
+ process.exit(1);
401
+ }
402
+ config.set("lastSync", (/* @__PURE__ */ new Date()).toISOString());
403
+ spinner.succeed(import_chalk4.default.green("Sync complete!"));
404
+ console.log("\n" + import_chalk4.default.gray("\u2500".repeat(40)));
405
+ console.log(import_chalk4.default.bold("\u{1F4CA} Your Stats"));
406
+ console.log(import_chalk4.default.gray("\u2500".repeat(40)));
407
+ console.log(` ${import_chalk4.default.gray("Tokens:")} ${import_chalk4.default.white(formatNumber2(usageData.totalTokens))}`);
408
+ console.log(` ${import_chalk4.default.gray("Spent:")} ${import_chalk4.default.green("$" + usageData.totalSpent.toFixed(2))}`);
409
+ console.log(` ${import_chalk4.default.gray("Rank:")} ${import_chalk4.default.yellow("#" + result.data?.rank)}`);
410
+ if (options.verbose) {
411
+ console.log("\n" + import_chalk4.default.gray("Model Breakdown:"));
412
+ for (const [model, tokens] of Object.entries(usageData.modelBreakdown)) {
413
+ const shortModel = model.replace("claude-", "").replace(/-\d+$/, "");
414
+ console.log(` ${import_chalk4.default.gray(shortModel + ":")} ${formatNumber2(tokens)}`);
415
+ }
416
+ }
417
+ console.log(import_chalk4.default.gray("\u2500".repeat(40)));
418
+ console.log(import_chalk4.default.gray("\nView full leaderboard: https://ccgather.dev/leaderboard\n"));
419
+ }
420
+ var import_chalk4, import_ora6;
421
+ var init_sync = __esm({
422
+ "src/commands/sync.ts"() {
423
+ "use strict";
424
+ import_chalk4 = __toESM(require("chalk"));
425
+ import_ora6 = __toESM(require("ora"));
426
+ init_config();
427
+ init_api();
428
+ init_claude();
429
+ }
430
+ });
431
+
432
+ // src/commands/auth.ts
433
+ var auth_exports = {};
434
+ __export(auth_exports, {
435
+ auth: () => auth
436
+ });
437
+ async function auth(options) {
438
+ const config = getConfig();
439
+ console.log(import_chalk5.default.bold("\n\u{1F310} CCgather Authentication\n"));
440
+ const existingToken = config.get("apiToken");
441
+ if (existingToken) {
442
+ const { overwrite } = await import_inquirer2.default.prompt([
443
+ {
444
+ type: "confirm",
445
+ name: "overwrite",
446
+ message: "You are already authenticated. Do you want to re-authenticate?",
447
+ default: false
448
+ }
449
+ ]);
450
+ if (!overwrite) {
451
+ console.log(import_chalk5.default.gray("Authentication cancelled."));
452
+ return;
453
+ }
454
+ }
455
+ let token = options.token;
456
+ if (!token) {
457
+ console.log(import_chalk5.default.gray("Get your API token from: https://ccgather.dev/settings/api\n"));
458
+ const answers = await import_inquirer2.default.prompt([
459
+ {
460
+ type: "password",
461
+ name: "token",
462
+ message: "Enter your API token:",
463
+ mask: "*",
464
+ validate: (input) => {
465
+ if (!input || input.length < 10) {
466
+ return "Please enter a valid API token";
467
+ }
468
+ return true;
469
+ }
470
+ }
471
+ ]);
472
+ token = answers.token;
473
+ }
474
+ const spinner = (0, import_ora7.default)("Verifying token...").start();
475
+ const result = await verifyToken(token);
476
+ if (!result.success) {
477
+ spinner.fail(import_chalk5.default.red("Authentication failed"));
478
+ console.log(import_chalk5.default.red(`Error: ${result.error}`));
479
+ console.log(import_chalk5.default.gray("\nMake sure your token is correct and try again."));
480
+ process.exit(1);
481
+ }
482
+ config.set("apiToken", token);
483
+ config.set("userId", result.data?.userId);
484
+ spinner.succeed(import_chalk5.default.green("Authentication successful!"));
485
+ console.log(import_chalk5.default.gray(`
486
+ Welcome, ${import_chalk5.default.white(result.data?.username)}!`));
487
+ console.log(import_chalk5.default.gray("\nNext step: Sync your usage data:"));
488
+ console.log(import_chalk5.default.cyan(" npx ccgather sync\n"));
489
+ }
490
+ var import_chalk5, import_ora7, import_inquirer2;
491
+ var init_auth = __esm({
492
+ "src/commands/auth.ts"() {
493
+ "use strict";
494
+ import_chalk5 = __toESM(require("chalk"));
495
+ import_ora7 = __toESM(require("ora"));
496
+ import_inquirer2 = __toESM(require("inquirer"));
497
+ init_config();
498
+ init_api();
499
+ }
500
+ });
501
+
190
502
  // src/index.ts
191
503
  var import_commander = require("commander");
504
+ var import_inquirer3 = __toESM(require("inquirer"));
505
+ var import_chalk6 = __toESM(require("chalk"));
192
506
 
193
507
  // src/commands/submit.ts
194
- var import_chalk = __toESM(require("chalk"));
195
508
  var import_ora = __toESM(require("ora"));
509
+ var fs3 = __toESM(require("fs"));
510
+ var path3 = __toESM(require("path"));
511
+ var os3 = __toESM(require("os"));
512
+ init_config();
513
+
514
+ // src/lib/ccgather-json.ts
196
515
  var fs2 = __toESM(require("fs"));
197
516
  var path2 = __toESM(require("path"));
198
517
  var os2 = __toESM(require("os"));
199
- init_config();
200
518
 
201
- // src/lib/ccgather-json.ts
519
+ // src/lib/credentials.ts
202
520
  var fs = __toESM(require("fs"));
203
521
  var path = __toESM(require("path"));
204
522
  var os = __toESM(require("os"));
205
- var CCGATHER_JSON_VERSION = "1.0.0";
523
+ function getCredentialsPath() {
524
+ return path.join(os.homedir(), ".claude", ".credentials.json");
525
+ }
526
+ function mapSubscriptionToCCPlan(subscriptionType) {
527
+ if (!subscriptionType) {
528
+ return "free";
529
+ }
530
+ const type = subscriptionType.toLowerCase();
531
+ if (type === "max" || type.includes("max")) {
532
+ return "max";
533
+ }
534
+ if (type === "team" || type === "enterprise") {
535
+ return "team";
536
+ }
537
+ if (type === "pro") {
538
+ return "pro";
539
+ }
540
+ return "free";
541
+ }
542
+ function readCredentials() {
543
+ const credentialsPath = getCredentialsPath();
544
+ const defaultData = {
545
+ ccplan: null,
546
+ rateLimitTier: null
547
+ };
548
+ if (!fs.existsSync(credentialsPath)) {
549
+ return defaultData;
550
+ }
551
+ try {
552
+ const content = fs.readFileSync(credentialsPath, "utf-8");
553
+ const credentials = JSON.parse(content);
554
+ const oauthData = credentials.claudeAiOauth;
555
+ if (!oauthData) {
556
+ return defaultData;
557
+ }
558
+ const ccplan = mapSubscriptionToCCPlan(oauthData.subscriptionType);
559
+ const rateLimitTier = oauthData.rateLimitTier || null;
560
+ return {
561
+ ccplan,
562
+ rateLimitTier
563
+ };
564
+ } catch (error2) {
565
+ return defaultData;
566
+ }
567
+ }
568
+
569
+ // src/lib/ccgather-json.ts
570
+ var CCGATHER_JSON_VERSION = "1.2.0";
571
+ function extractProjectName(filePath) {
572
+ const parts = filePath.split(/[/\\]/);
573
+ const projectsIndex = parts.findIndex((p) => p === "projects");
574
+ if (projectsIndex >= 0 && parts[projectsIndex + 1]) {
575
+ try {
576
+ const encoded = parts[projectsIndex + 1];
577
+ const decoded = decodeURIComponent(encoded);
578
+ const pathParts = decoded.split(/[/\\]/);
579
+ return pathParts[pathParts.length - 1] || decoded;
580
+ } catch {
581
+ return parts[projectsIndex + 1];
582
+ }
583
+ }
584
+ return "unknown";
585
+ }
206
586
  function getCCGatherJsonPath() {
207
- return path.join(os.homedir(), ".claude", "ccgather.json");
587
+ return path2.join(os2.homedir(), ".claude", "ccgather.json");
208
588
  }
209
589
  function getClaudeProjectsDir() {
210
- return path.join(os.homedir(), ".claude", "projects");
590
+ return path2.join(os2.homedir(), ".claude", "projects");
211
591
  }
212
592
  function findJsonlFiles(dir) {
213
593
  const files = [];
214
594
  try {
215
- const entries = fs.readdirSync(dir, { withFileTypes: true });
595
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
216
596
  for (const entry of entries) {
217
- const fullPath = path.join(dir, entry.name);
597
+ const fullPath = path2.join(dir, entry.name);
218
598
  if (entry.isDirectory()) {
219
599
  files.push(...findJsonlFiles(fullPath));
220
600
  } else if (entry.name.endsWith(".jsonl")) {
@@ -230,7 +610,7 @@ function estimateCost(model, inputTokens, outputTokens) {
230
610
  "claude-opus-4": { input: 15, output: 75 },
231
611
  "claude-sonnet-4": { input: 3, output: 15 },
232
612
  "claude-haiku": { input: 0.25, output: 1.25 },
233
- "default": { input: 3, output: 15 }
613
+ default: { input: 3, output: 15 }
234
614
  };
235
615
  let modelKey = "default";
236
616
  for (const key of Object.keys(pricing)) {
@@ -246,7 +626,7 @@ function estimateCost(model, inputTokens, outputTokens) {
246
626
  }
247
627
  function scanUsageData() {
248
628
  const projectsDir = getClaudeProjectsDir();
249
- if (!fs.existsSync(projectsDir)) {
629
+ if (!fs2.existsSync(projectsDir)) {
250
630
  return null;
251
631
  }
252
632
  let totalInputTokens = 0;
@@ -257,13 +637,25 @@ function scanUsageData() {
257
637
  let sessionsCount = 0;
258
638
  const dates = /* @__PURE__ */ new Set();
259
639
  const models = {};
640
+ const projects = {};
641
+ const dailyData = {};
260
642
  let firstTimestamp = null;
261
643
  let lastTimestamp = null;
262
644
  const jsonlFiles = findJsonlFiles(projectsDir);
263
645
  sessionsCount = jsonlFiles.length;
264
646
  for (const filePath of jsonlFiles) {
647
+ const projectName = extractProjectName(filePath);
648
+ if (!projects[projectName]) {
649
+ projects[projectName] = {
650
+ tokens: 0,
651
+ cost: 0,
652
+ sessions: 0,
653
+ models: {}
654
+ };
655
+ }
656
+ projects[projectName].sessions++;
265
657
  try {
266
- const content = fs.readFileSync(filePath, "utf-8");
658
+ const content = fs2.readFileSync(filePath, "utf-8");
267
659
  const lines = content.split("\n").filter((line) => line.trim());
268
660
  for (const line of lines) {
269
661
  try {
@@ -277,12 +669,32 @@ function scanUsageData() {
277
669
  totalOutputTokens += outputTokens;
278
670
  totalCacheRead += usage.cache_read_input_tokens || 0;
279
671
  totalCacheWrite += usage.cache_creation_input_tokens || 0;
280
- totalCost += estimateCost(model, inputTokens, outputTokens);
672
+ const messageCost = estimateCost(model, inputTokens, outputTokens);
673
+ totalCost += messageCost;
281
674
  const totalModelTokens = inputTokens + outputTokens;
282
675
  models[model] = (models[model] || 0) + totalModelTokens;
676
+ projects[projectName].tokens += totalModelTokens;
677
+ projects[projectName].cost += messageCost;
678
+ projects[projectName].models[model] = (projects[projectName].models[model] || 0) + totalModelTokens;
283
679
  if (event.timestamp) {
284
680
  const date = new Date(event.timestamp).toISOString().split("T")[0];
285
681
  dates.add(date);
682
+ if (!dailyData[date]) {
683
+ dailyData[date] = {
684
+ tokens: 0,
685
+ cost: 0,
686
+ inputTokens: 0,
687
+ outputTokens: 0,
688
+ sessions: /* @__PURE__ */ new Set(),
689
+ models: {}
690
+ };
691
+ }
692
+ dailyData[date].tokens += totalModelTokens;
693
+ dailyData[date].cost += messageCost;
694
+ dailyData[date].inputTokens += inputTokens;
695
+ dailyData[date].outputTokens += outputTokens;
696
+ dailyData[date].sessions.add(filePath);
697
+ dailyData[date].models[model] = (dailyData[date].models[model] || 0) + totalModelTokens;
286
698
  if (!firstTimestamp || event.timestamp < firstTimestamp) {
287
699
  firstTimestamp = event.timestamp;
288
700
  }
@@ -301,6 +713,19 @@ function scanUsageData() {
301
713
  if (totalTokens === 0) {
302
714
  return null;
303
715
  }
716
+ for (const projectName of Object.keys(projects)) {
717
+ projects[projectName].cost = Math.round(projects[projectName].cost * 100) / 100;
718
+ }
719
+ const dailyUsage = Object.entries(dailyData).map(([date, data]) => ({
720
+ date,
721
+ tokens: data.tokens,
722
+ cost: Math.round(data.cost * 100) / 100,
723
+ inputTokens: data.inputTokens,
724
+ outputTokens: data.outputTokens,
725
+ sessions: data.sessions.size,
726
+ models: data.models
727
+ })).sort((a, b) => a.date.localeCompare(b.date));
728
+ const credentials = readCredentials();
304
729
  return {
305
730
  version: CCGATHER_JSON_VERSION,
306
731
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -319,16 +744,22 @@ function scanUsageData() {
319
744
  firstUsed: firstTimestamp ? new Date(firstTimestamp).toISOString().split("T")[0] : null,
320
745
  lastUsed: lastTimestamp ? new Date(lastTimestamp).toISOString().split("T")[0] : null
321
746
  },
322
- models
747
+ models,
748
+ projects,
749
+ dailyUsage,
750
+ account: {
751
+ ccplan: credentials.ccplan,
752
+ rateLimitTier: credentials.rateLimitTier
753
+ }
323
754
  };
324
755
  }
325
756
  function readCCGatherJson() {
326
757
  const jsonPath = getCCGatherJsonPath();
327
- if (!fs.existsSync(jsonPath)) {
758
+ if (!fs2.existsSync(jsonPath)) {
328
759
  return null;
329
760
  }
330
761
  try {
331
- const content = fs.readFileSync(jsonPath, "utf-8");
762
+ const content = fs2.readFileSync(jsonPath, "utf-8");
332
763
  return JSON.parse(content);
333
764
  } catch {
334
765
  return null;
@@ -336,11 +767,11 @@ function readCCGatherJson() {
336
767
  }
337
768
  function writeCCGatherJson(data) {
338
769
  const jsonPath = getCCGatherJsonPath();
339
- const claudeDir = path.dirname(jsonPath);
340
- if (!fs.existsSync(claudeDir)) {
341
- fs.mkdirSync(claudeDir, { recursive: true });
770
+ const claudeDir = path2.dirname(jsonPath);
771
+ if (!fs2.existsSync(claudeDir)) {
772
+ fs2.mkdirSync(claudeDir, { recursive: true });
342
773
  }
343
- fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
774
+ fs2.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
344
775
  }
345
776
  function scanAndSave() {
346
777
  const data = scanUsageData();
@@ -350,6 +781,173 @@ function scanAndSave() {
350
781
  return data;
351
782
  }
352
783
 
784
+ // src/lib/ui.ts
785
+ var import_chalk = __toESM(require("chalk"));
786
+ var colors = {
787
+ primary: import_chalk.default.hex("#DA7756"),
788
+ // Claude coral
789
+ secondary: import_chalk.default.hex("#F7931E"),
790
+ // Orange accent
791
+ success: import_chalk.default.hex("#22C55E"),
792
+ // Green
793
+ warning: import_chalk.default.hex("#F59E0B"),
794
+ // Amber
795
+ error: import_chalk.default.hex("#EF4444"),
796
+ // Red
797
+ muted: import_chalk.default.hex("#71717A"),
798
+ // Gray
799
+ dim: import_chalk.default.hex("#52525B"),
800
+ // Dark gray
801
+ white: import_chalk.default.white,
802
+ cyan: import_chalk.default.cyan,
803
+ // CCplan colors
804
+ max: import_chalk.default.hex("#F59E0B"),
805
+ // Gold
806
+ pro: import_chalk.default.hex("#3B82F6"),
807
+ // Blue
808
+ team: import_chalk.default.hex("#8B5CF6"),
809
+ // Purple
810
+ free: import_chalk.default.hex("#6B7280")
811
+ // Gray
812
+ };
813
+ var LOGO = `
814
+ ${colors.primary("\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557")} ${colors.secondary("\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
815
+ ${colors.primary("\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D")}${colors.secondary("\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557")}
816
+ ${colors.primary("\u2588\u2588\u2551 \u2588\u2588\u2551 ")}${colors.secondary("\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D")}
817
+ ${colors.primary("\u2588\u2588\u2551 \u2588\u2588\u2551 ")}${colors.secondary("\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557")}
818
+ ${colors.primary("\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}${colors.secondary("\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551")}
819
+ ${colors.primary("\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D")} ${colors.secondary("\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D")}
820
+ `;
821
+ var LOGO_COMPACT = `
822
+ ${colors.primary("CC")}${colors.secondary("gather")} ${colors.muted("- Where Claude Code Developers Gather")}
823
+ `;
824
+ var TAGLINE = colors.muted(" Where Claude Code Developers Gather");
825
+ var SLOGAN = colors.dim(" Gather. Compete. Rise.");
826
+ function getVersionLine(version) {
827
+ return colors.dim(` v${version} \u2022 ccgather.dev`);
828
+ }
829
+ var box = {
830
+ topLeft: "\u250C",
831
+ topRight: "\u2510",
832
+ bottomLeft: "\u2514",
833
+ bottomRight: "\u2518",
834
+ horizontal: "\u2500",
835
+ vertical: "\u2502",
836
+ leftT: "\u251C",
837
+ rightT: "\u2524"
838
+ };
839
+ function createBox(lines, width = 47) {
840
+ const paddedLines = lines.map((line) => {
841
+ const visibleLength = stripAnsi(line).length;
842
+ const padding = width - 2 - visibleLength;
843
+ return `${box.vertical} ${line}${" ".repeat(Math.max(0, padding))} ${box.vertical}`;
844
+ });
845
+ const top = colors.dim(` ${box.topLeft}${box.horizontal.repeat(width)}${box.topRight}`);
846
+ const bottom = colors.dim(` ${box.bottomLeft}${box.horizontal.repeat(width)}${box.bottomRight}`);
847
+ return [top, ...paddedLines.map((l) => colors.dim(" ") + l), bottom].join("\n");
848
+ }
849
+ function stripAnsi(str) {
850
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
851
+ }
852
+ function header(title, icon = "") {
853
+ const iconPart = icon ? `${icon} ` : "";
854
+ return `
855
+ ${colors.primary("\u2501".repeat(50))}
856
+ ${iconPart}${colors.white.bold(title)}
857
+ ${colors.primary("\u2501".repeat(50))}`;
858
+ }
859
+ function formatNumber(num) {
860
+ if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
861
+ if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
862
+ if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
863
+ return num.toLocaleString();
864
+ }
865
+ function formatCost(cost) {
866
+ return `$${cost.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
867
+ }
868
+ function getRankMedal(rank) {
869
+ if (rank === 1) return "\u{1F947}";
870
+ if (rank === 2) return "\u{1F948}";
871
+ if (rank === 3) return "\u{1F949}";
872
+ if (rank <= 10) return "\u{1F3C5}";
873
+ if (rank <= 100) return "\u{1F396}\uFE0F";
874
+ return "\u{1F4CA}";
875
+ }
876
+ function getCCplanBadge(ccplan) {
877
+ if (!ccplan) return "";
878
+ const badges = {
879
+ max: `${colors.max("\u{1F947} MAX")}`,
880
+ pro: `${colors.pro("\u{1F948} PRO")}`,
881
+ team: `${colors.team("\u{1F3E2} TEAM")}`,
882
+ free: `${colors.free("\u2B50 FREE")}`
883
+ };
884
+ return badges[ccplan.toLowerCase()] || "";
885
+ }
886
+ function getLevelInfo(tokens) {
887
+ const levels = [
888
+ { min: 0, level: 1, name: "Novice", icon: "\u{1F331}", color: colors.dim },
889
+ { min: 1e5, level: 2, name: "Apprentice", icon: "\u{1F4DA}", color: colors.muted },
890
+ { min: 5e5, level: 3, name: "Journeyman", icon: "\u26A1", color: colors.cyan },
891
+ { min: 1e6, level: 4, name: "Expert", icon: "\u{1F48E}", color: colors.pro },
892
+ { min: 5e6, level: 5, name: "Master", icon: "\u{1F525}", color: colors.warning },
893
+ { min: 1e7, level: 6, name: "Grandmaster", icon: "\u{1F451}", color: colors.max },
894
+ { min: 5e7, level: 7, name: "Legend", icon: "\u{1F31F}", color: colors.primary },
895
+ { min: 1e8, level: 8, name: "Mythic", icon: "\u{1F3C6}", color: colors.secondary }
896
+ ];
897
+ for (let i = levels.length - 1; i >= 0; i--) {
898
+ if (tokens >= levels[i].min) {
899
+ return levels[i];
900
+ }
901
+ }
902
+ return levels[0];
903
+ }
904
+ function createWelcomeBox(user) {
905
+ const levelInfo = user.level && user.levelName && user.levelIcon ? `${user.levelIcon} Level ${user.level} \u2022 ${user.levelName}` : "";
906
+ const ccplanBadge = user.ccplan ? getCCplanBadge(user.ccplan) : "";
907
+ const lines = [
908
+ `\u{1F44B} ${colors.white.bold(`Welcome back, ${user.username}!`)}`
909
+ ];
910
+ if (levelInfo || ccplanBadge) {
911
+ lines.push(`${levelInfo}${ccplanBadge ? ` ${ccplanBadge}` : ""}`);
912
+ }
913
+ if (user.globalRank) {
914
+ lines.push(`\u{1F30D} Global Rank: ${colors.primary(`#${user.globalRank}`)}`);
915
+ }
916
+ if (user.countryRank && user.countryCode) {
917
+ const flag = countryCodeToFlag(user.countryCode);
918
+ lines.push(`${flag} Country Rank: ${colors.primary(`#${user.countryRank}`)}`);
919
+ }
920
+ return createBox(lines);
921
+ }
922
+ function countryCodeToFlag(countryCode) {
923
+ if (!countryCode || countryCode.length !== 2) return "\u{1F310}";
924
+ const codePoints = countryCode.toUpperCase().split("").map((char) => 127462 + char.charCodeAt(0) - 65);
925
+ return String.fromCodePoint(...codePoints);
926
+ }
927
+ function success(message) {
928
+ return `${colors.success("\u2713")} ${message}`;
929
+ }
930
+ function error(message) {
931
+ return `${colors.error("\u2717")} ${message}`;
932
+ }
933
+ function printHeader(version) {
934
+ console.log(LOGO);
935
+ console.log(TAGLINE);
936
+ console.log(SLOGAN);
937
+ console.log();
938
+ console.log(getVersionLine(version));
939
+ console.log();
940
+ }
941
+ function printCompactHeader(version) {
942
+ console.log();
943
+ console.log(LOGO_COMPACT);
944
+ console.log(colors.dim(` v${version}`));
945
+ console.log();
946
+ }
947
+ function link(url) {
948
+ return colors.cyan.underline(url);
949
+ }
950
+
353
951
  // src/commands/submit.ts
354
952
  function ccgatherToUsageData(data) {
355
953
  return {
@@ -359,17 +957,19 @@ function ccgatherToUsageData(data) {
359
957
  outputTokens: data.usage.outputTokens,
360
958
  cacheReadTokens: data.usage.cacheReadTokens,
361
959
  cacheWriteTokens: data.usage.cacheWriteTokens,
362
- daysTracked: data.stats.daysTracked
960
+ daysTracked: data.stats.daysTracked,
961
+ ccplan: data.account?.ccplan || null,
962
+ rateLimitTier: data.account?.rateLimitTier || null
363
963
  };
364
964
  }
365
965
  function findCcJson() {
366
966
  const possiblePaths = [
367
- path2.join(process.cwd(), "cc.json"),
368
- path2.join(os2.homedir(), "cc.json"),
369
- path2.join(os2.homedir(), ".claude", "cc.json")
967
+ path3.join(process.cwd(), "cc.json"),
968
+ path3.join(os3.homedir(), "cc.json"),
969
+ path3.join(os3.homedir(), ".claude", "cc.json")
370
970
  ];
371
971
  for (const p of possiblePaths) {
372
- if (fs2.existsSync(p)) {
972
+ if (fs3.existsSync(p)) {
373
973
  return p;
374
974
  }
375
975
  }
@@ -377,7 +977,7 @@ function findCcJson() {
377
977
  }
378
978
  function parseCcJson(filePath) {
379
979
  try {
380
- const content = fs2.readFileSync(filePath, "utf-8");
980
+ const content = fs3.readFileSync(filePath, "utf-8");
381
981
  const data = JSON.parse(content);
382
982
  return {
383
983
  totalTokens: data.totalTokens || data.total_tokens || 0,
@@ -401,9 +1001,6 @@ function calculateDaysTracked(data) {
401
1001
  }
402
1002
  return 1;
403
1003
  }
404
- function formatNumber(num) {
405
- return num.toLocaleString();
406
- }
407
1004
  async function detectGitHubUsername() {
408
1005
  try {
409
1006
  const { execSync } = await import("child_process");
@@ -439,6 +1036,8 @@ async function submitToServer(username, data) {
439
1036
  cacheReadTokens: data.cacheReadTokens,
440
1037
  cacheWriteTokens: data.cacheWriteTokens,
441
1038
  daysTracked: data.daysTracked,
1039
+ ccplan: data.ccplan,
1040
+ rateLimitTier: data.rateLimitTier,
442
1041
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
443
1042
  })
444
1043
  });
@@ -447,25 +1046,30 @@ async function submitToServer(username, data) {
447
1046
  return { success: false, error: errorData.error || `HTTP ${response.status}` };
448
1047
  }
449
1048
  const result = await response.json();
450
- return { success: true, profileUrl: result.profileUrl };
1049
+ return { success: true, profileUrl: result.profileUrl, rank: result.rank };
451
1050
  } catch (err) {
452
1051
  return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
453
1052
  }
454
1053
  }
455
1054
  async function submit(options) {
456
- console.log(import_chalk.default.bold("\n\u{1F680} CCgather Submission Tool v1.0.0\n"));
457
- const spinner = (0, import_ora.default)("Detecting GitHub username...").start();
1055
+ printCompactHeader("1.2.0");
1056
+ console.log(header("Submit Usage Data", "\u{1F4E4}"));
1057
+ const spinner = (0, import_ora.default)({
1058
+ text: "Detecting GitHub username...",
1059
+ color: "cyan"
1060
+ }).start();
458
1061
  let username = await detectGitHubUsername();
459
1062
  spinner.stop();
460
1063
  if (username) {
461
- console.log(import_chalk.default.gray(`Detected GitHub username from repository: ${import_chalk.default.white(username)}`));
1064
+ console.log(`
1065
+ ${colors.muted("Detected:")} ${colors.white(username)}`);
462
1066
  }
463
- const inquirer2 = await import("inquirer");
464
- const { confirmedUsername } = await inquirer2.default.prompt([
1067
+ const inquirer4 = await import("inquirer");
1068
+ const { confirmedUsername } = await inquirer4.default.prompt([
465
1069
  {
466
1070
  type: "input",
467
1071
  name: "confirmedUsername",
468
- message: "GitHub username:",
1072
+ message: colors.muted("GitHub username:"),
469
1073
  default: username || "",
470
1074
  validate: (input) => input.trim().length > 0 || "Username is required"
471
1075
  }
@@ -477,18 +1081,21 @@ async function submit(options) {
477
1081
  if (ccgatherData) {
478
1082
  usageData = ccgatherToUsageData(ccgatherData);
479
1083
  dataSource = "ccgather.json";
480
- console.log(import_chalk.default.green(`\u2713 Found ${dataSource}`));
481
- console.log(import_chalk.default.gray(` Last scanned: ${new Date(ccgatherData.lastScanned).toLocaleString()}
482
- `));
1084
+ console.log(`
1085
+ ${success(`Found ${dataSource}`)}`);
1086
+ console.log(` ${colors.dim(`Last scanned: ${new Date(ccgatherData.lastScanned).toLocaleString()}`)}`);
1087
+ if (usageData.ccplan) {
1088
+ console.log(` ${colors.dim("CCplan:")} ${colors.primary(usageData.ccplan.toUpperCase())}`);
1089
+ }
483
1090
  }
484
1091
  if (!usageData) {
485
1092
  const ccJsonPath = findCcJson();
486
1093
  if (ccJsonPath) {
487
- const { useCcJson } = await inquirer2.default.prompt([
1094
+ const { useCcJson } = await inquirer4.default.prompt([
488
1095
  {
489
1096
  type: "confirm",
490
1097
  name: "useCcJson",
491
- message: `Found existing cc.json. Use this file?`,
1098
+ message: "Found existing cc.json. Use this file?",
492
1099
  default: true
493
1100
  }
494
1101
  ]);
@@ -499,34 +1106,45 @@ async function submit(options) {
499
1106
  }
500
1107
  }
501
1108
  if (!usageData) {
502
- const parseSpinner = (0, import_ora.default)("Scanning Claude Code usage data...").start();
1109
+ const parseSpinner = (0, import_ora.default)({
1110
+ text: "Scanning Claude Code usage data...",
1111
+ color: "cyan"
1112
+ }).start();
503
1113
  const scannedData = scanAndSave();
504
1114
  parseSpinner.stop();
505
1115
  if (scannedData) {
506
1116
  usageData = ccgatherToUsageData(scannedData);
507
1117
  dataSource = "Claude Code logs";
508
- console.log(import_chalk.default.green(`\u2713 Scanned and saved to ccgather.json`));
509
- console.log(import_chalk.default.gray(` Path: ${getCCGatherJsonPath()}
510
- `));
1118
+ console.log(`
1119
+ ${success("Scanned and saved to ccgather.json")}`);
1120
+ console.log(` ${colors.dim(`Path: ${getCCGatherJsonPath()}`)}`);
511
1121
  }
512
1122
  }
513
1123
  if (!usageData) {
514
- console.log(import_chalk.default.red("\n\u274C No usage data found."));
515
- console.log(import_chalk.default.gray("Make sure you have used Claude Code."));
516
- console.log(import_chalk.default.gray("Run: npx ccgather scan\n"));
1124
+ console.log(`
1125
+ ${error("No usage data found.")}`);
1126
+ console.log(` ${colors.muted("Make sure you have used Claude Code.")}`);
1127
+ console.log(` ${colors.muted("Run:")} ${colors.white("npx ccgather scan")}
1128
+ `);
517
1129
  process.exit(1);
518
1130
  }
519
1131
  if (dataSource && dataSource !== "Claude Code logs") {
520
- console.log(import_chalk.default.green(`\u2713 Using ${dataSource}
521
- `));
1132
+ console.log(`
1133
+ ${success(`Using ${dataSource}`)}`);
522
1134
  }
523
- console.log(import_chalk.default.bold("Summary:"));
524
- console.log(import_chalk.default.gray(` Total Cost: ${import_chalk.default.green("$" + formatNumber(Math.round(usageData.totalCost)))}`));
525
- console.log(import_chalk.default.gray(` Total Tokens: ${import_chalk.default.cyan(formatNumber(usageData.totalTokens))}`));
526
- console.log(import_chalk.default.gray(` Days Tracked: ${import_chalk.default.yellow(usageData.daysTracked.toString())}`));
1135
+ console.log();
1136
+ const summaryLines = [
1137
+ `${colors.muted("Total Cost")} ${colors.success(formatCost(usageData.totalCost))}`,
1138
+ `${colors.muted("Total Tokens")} ${colors.primary(formatNumber(usageData.totalTokens))}`,
1139
+ `${colors.muted("Days Tracked")} ${colors.warning(usageData.daysTracked.toString())}`
1140
+ ];
1141
+ if (usageData.ccplan) {
1142
+ summaryLines.push(`${colors.muted("CCplan")} ${colors.cyan(usageData.ccplan.toUpperCase())}`);
1143
+ }
1144
+ console.log(createBox(summaryLines));
527
1145
  console.log();
528
1146
  if (!options.yes) {
529
- const { confirmSubmit } = await inquirer2.default.prompt([
1147
+ const { confirmSubmit } = await inquirer4.default.prompt([
530
1148
  {
531
1149
  type: "confirm",
532
1150
  name: "confirmSubmit",
@@ -535,106 +1153,70 @@ async function submit(options) {
535
1153
  }
536
1154
  ]);
537
1155
  if (!confirmSubmit) {
538
- console.log(import_chalk.default.gray("\nSubmission cancelled."));
1156
+ console.log(`
1157
+ ${colors.muted("Submission cancelled.")}
1158
+ `);
539
1159
  return;
540
1160
  }
541
1161
  }
542
- const submitSpinner = (0, import_ora.default)("Submitting to CCgather...").start();
1162
+ const submitSpinner = (0, import_ora.default)({
1163
+ text: "Submitting to CCgather...",
1164
+ color: "cyan"
1165
+ }).start();
543
1166
  const result = await submitToServer(username, usageData);
544
1167
  if (result.success) {
545
- submitSpinner.succeed(import_chalk.default.green("Successfully submitted to CCgather!"));
1168
+ submitSpinner.succeed(colors.success("Successfully submitted to CCgather!"));
546
1169
  console.log();
547
- console.log(import_chalk.default.gray(`View your profile at: ${import_chalk.default.cyan(result.profileUrl || `https://ccgather.dev/u/${username}`)}`));
1170
+ const successLines = [
1171
+ `${colors.success("\u2713")} ${colors.white.bold("Submission Complete!")}`,
1172
+ "",
1173
+ `${colors.muted("Profile:")} ${link(result.profileUrl || `https://ccgather.dev/u/${username}`)}`
1174
+ ];
1175
+ if (result.rank) {
1176
+ successLines.push(`${colors.muted("Rank:")} ${colors.warning(`#${result.rank}`)}`);
1177
+ }
1178
+ console.log(createBox(successLines));
1179
+ console.log();
1180
+ console.log(` ${colors.dim("View leaderboard:")} ${link("https://ccgather.dev/leaderboard")}`);
548
1181
  console.log();
549
- console.log(import_chalk.default.bold("Done! \u{1F389}"));
550
1182
  } else {
551
- submitSpinner.fail(import_chalk.default.red("Failed to submit"));
552
- console.log(import_chalk.default.red(result.error || "Unknown error"));
1183
+ submitSpinner.fail(colors.error("Failed to submit"));
1184
+ console.log(`
1185
+ ${error(result.error || "Unknown error")}
1186
+ `);
553
1187
  process.exit(1);
554
1188
  }
555
- console.log();
556
1189
  }
557
1190
 
558
1191
  // src/commands/status.ts
559
- var import_chalk2 = __toESM(require("chalk"));
560
1192
  var import_ora2 = __toESM(require("ora"));
561
1193
  init_config();
562
-
563
- // src/lib/api.ts
564
- init_config();
565
- async function fetchApi(endpoint, options = {}) {
566
- const config = getConfig();
567
- const apiToken = config.get("apiToken");
568
- const apiUrl = getApiUrl();
569
- if (!apiToken) {
570
- return { success: false, error: "Not authenticated. Run: npx ccgather auth" };
571
- }
572
- try {
573
- const response = await fetch(`${apiUrl}${endpoint}`, {
574
- ...options,
575
- headers: {
576
- "Content-Type": "application/json",
577
- Authorization: `Bearer ${apiToken}`,
578
- ...options.headers
579
- }
580
- });
581
- const data = await response.json();
582
- if (!response.ok) {
583
- return { success: false, error: data.error || `HTTP ${response.status}` };
584
- }
585
- return { success: true, data };
586
- } catch (error) {
587
- const message = error instanceof Error ? error.message : "Unknown error";
588
- return { success: false, error: message };
589
- }
590
- }
591
- async function getStatus() {
592
- return fetchApi("/cli/status");
593
- }
594
-
595
- // src/commands/status.ts
596
- function formatNumber2(num) {
597
- if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
598
- if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
599
- if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
600
- return num.toString();
601
- }
602
- function getTierEmoji(tier) {
603
- const emojis = {
604
- free: "\u{1F193}",
605
- pro: "\u2B50",
606
- team: "\u{1F465}",
607
- enterprise: "\u{1F3E2}"
608
- };
609
- return emojis[tier.toLowerCase()] || "\u{1F3AF}";
610
- }
611
- function getRankMedal(rank) {
612
- if (rank === 1) return "\u{1F947}";
613
- if (rank === 2) return "\u{1F948}";
614
- if (rank === 3) return "\u{1F949}";
615
- if (rank <= 10) return "\u{1F3C5}";
616
- if (rank <= 100) return "\u{1F396}\uFE0F";
617
- return "\u{1F4CA}";
618
- }
1194
+ init_api();
619
1195
  async function status(options) {
620
1196
  if (!isAuthenticated()) {
621
1197
  if (options.json) {
622
1198
  console.log(JSON.stringify({ error: "Not authenticated" }));
623
1199
  } else {
624
- console.log(import_chalk2.default.red("\nNot authenticated."));
625
- console.log(import_chalk2.default.gray("Run: npx ccgather auth\n"));
1200
+ console.log(`
1201
+ ${error("Not authenticated.")}`);
1202
+ console.log(` ${colors.muted("Run:")} ${colors.white("npx ccgather auth")}
1203
+ `);
626
1204
  }
627
1205
  process.exit(1);
628
1206
  }
629
- const spinner = options.json ? null : (0, import_ora2.default)("Fetching your stats...").start();
1207
+ const spinner = options.json ? null : (0, import_ora2.default)({
1208
+ text: "Fetching your stats...",
1209
+ color: "cyan"
1210
+ }).start();
630
1211
  const result = await getStatus();
631
1212
  if (!result.success) {
632
- if (spinner) spinner.fail(import_chalk2.default.red("Failed to fetch status"));
1213
+ if (spinner) spinner.fail(colors.error("Failed to fetch status"));
633
1214
  if (options.json) {
634
1215
  console.log(JSON.stringify({ error: result.error }));
635
1216
  } else {
636
- console.log(import_chalk2.default.red(`Error: ${result.error}
637
- `));
1217
+ console.log(`
1218
+ ${error(result.error || "Unknown error")}
1219
+ `);
638
1220
  }
639
1221
  process.exit(1);
640
1222
  }
@@ -643,45 +1225,52 @@ async function status(options) {
643
1225
  console.log(JSON.stringify(stats, null, 2));
644
1226
  return;
645
1227
  }
646
- spinner?.succeed(import_chalk2.default.green("Status retrieved"));
647
- console.log("\n" + import_chalk2.default.bold("\u2550".repeat(42)));
648
- console.log(import_chalk2.default.bold.white(" \u{1F310} CCgather Status"));
649
- console.log(import_chalk2.default.bold("\u2550".repeat(42)));
1228
+ spinner?.succeed(colors.success("Status retrieved"));
1229
+ printCompactHeader("1.2.0");
1230
+ console.log(header("Your CCgather Stats", "\u{1F4CA}"));
1231
+ const levelInfo = getLevelInfo(stats.totalTokens);
650
1232
  const medal = getRankMedal(stats.rank);
651
- console.log(`
652
- ${medal} ${import_chalk2.default.bold.yellow(`Rank #${stats.rank}`)}`);
653
- console.log(import_chalk2.default.gray(` Top ${stats.percentile.toFixed(1)}% of all users`));
654
- console.log("\n" + import_chalk2.default.gray("\u2500".repeat(42)));
655
- console.log(
656
- ` ${import_chalk2.default.gray("Tokens")} ${import_chalk2.default.white(formatNumber2(stats.totalTokens))}`
657
- );
658
- console.log(
659
- ` ${import_chalk2.default.gray("Spent")} ${import_chalk2.default.green("$" + stats.totalSpent.toFixed(2))}`
660
- );
661
- console.log(
662
- ` ${import_chalk2.default.gray("Tier")} ${getTierEmoji(stats.tier)} ${import_chalk2.default.white(stats.tier)}`
663
- );
1233
+ console.log();
1234
+ console.log(` ${medal} ${colors.white.bold(`Rank #${stats.rank}`)}`);
1235
+ console.log(` ${colors.dim(`Top ${stats.percentile.toFixed(1)}% of all users`)}`);
1236
+ console.log();
1237
+ console.log(` ${levelInfo.icon} ${levelInfo.color(`Level ${levelInfo.level} \u2022 ${levelInfo.name}`)}`);
1238
+ if (stats.tier) {
1239
+ const badge = getCCplanBadge(stats.tier);
1240
+ if (badge) {
1241
+ console.log(` ${badge}`);
1242
+ }
1243
+ }
1244
+ console.log();
1245
+ const statsLines = [
1246
+ `${colors.muted("Total Tokens")} ${colors.primary(formatNumber(stats.totalTokens))}`,
1247
+ `${colors.muted("Total Spent")} ${colors.success(formatCost(stats.totalSpent))}`,
1248
+ `${colors.muted("Tier")} ${colors.cyan(stats.tier || "Unknown")}`
1249
+ ];
1250
+ console.log(createBox(statsLines));
664
1251
  if (stats.badges && stats.badges.length > 0) {
665
- console.log("\n" + import_chalk2.default.gray("\u2500".repeat(42)));
666
- console.log(import_chalk2.default.gray(" Badges"));
1252
+ console.log();
1253
+ console.log(` ${colors.muted("Badges")}`);
667
1254
  console.log(` ${stats.badges.join(" ")}`);
668
1255
  }
669
- console.log("\n" + import_chalk2.default.bold("\u2550".repeat(42)));
670
- console.log(import_chalk2.default.gray("\n View leaderboard: https://ccgather.dev/leaderboard\n"));
1256
+ console.log();
1257
+ console.log(colors.dim(" \u2500".repeat(25)));
1258
+ console.log(` ${colors.muted("View leaderboard:")} ${link("https://ccgather.dev/leaderboard")}`);
1259
+ console.log();
671
1260
  }
672
1261
 
673
1262
  // src/commands/setup-auto.ts
674
- var import_chalk4 = __toESM(require("chalk"));
1263
+ var import_chalk3 = __toESM(require("chalk"));
675
1264
  var import_ora4 = __toESM(require("ora"));
676
1265
  var http = __toESM(require("http"));
677
- var fs4 = __toESM(require("fs"));
678
- var path4 = __toESM(require("path"));
679
- var os4 = __toESM(require("os"));
1266
+ var fs5 = __toESM(require("fs"));
1267
+ var path5 = __toESM(require("path"));
1268
+ var os5 = __toESM(require("os"));
680
1269
  init_config();
681
1270
  init_config();
682
1271
  var CALLBACK_PORT = 9876;
683
1272
  function getClaudeSettingsDir2() {
684
- return path4.join(os4.homedir(), ".claude");
1273
+ return path5.join(os5.homedir(), ".claude");
685
1274
  }
686
1275
  async function openBrowser(url) {
687
1276
  const { default: open } = await import("open");
@@ -695,8 +1284,8 @@ function createCallbackServer() {
695
1284
  const token = url.searchParams.get("token");
696
1285
  const userId = url.searchParams.get("userId");
697
1286
  const username = url.searchParams.get("username");
698
- const error = url.searchParams.get("error");
699
- if (error) {
1287
+ const error2 = url.searchParams.get("error");
1288
+ if (error2) {
700
1289
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
701
1290
  res.end(`
702
1291
  <html>
@@ -704,14 +1293,14 @@ function createCallbackServer() {
704
1293
  <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a1a; color: #fff;">
705
1294
  <div style="text-align: center;">
706
1295
  <h1 style="color: #ef4444;">\u274C Authentication Failed</h1>
707
- <p>${error}</p>
1296
+ <p>${error2}</p>
708
1297
  <p style="color: #888;">You can close this window.</p>
709
1298
  </div>
710
1299
  </body>
711
1300
  </html>
712
1301
  `);
713
1302
  server.close();
714
- reject(new Error(error));
1303
+ reject(new Error(error2));
715
1304
  return;
716
1305
  }
717
1306
  if (token && userId && username) {
@@ -911,14 +1500,14 @@ if (usageData) {
911
1500
  }
912
1501
  function installStopHook() {
913
1502
  const claudeDir = getClaudeSettingsDir2();
914
- const settingsPath = path4.join(claudeDir, "settings.json");
915
- if (!fs4.existsSync(claudeDir)) {
916
- fs4.mkdirSync(claudeDir, { recursive: true });
1503
+ const settingsPath = path5.join(claudeDir, "settings.json");
1504
+ if (!fs5.existsSync(claudeDir)) {
1505
+ fs5.mkdirSync(claudeDir, { recursive: true });
917
1506
  }
918
1507
  let settings = {};
919
1508
  try {
920
- if (fs4.existsSync(settingsPath)) {
921
- const content = fs4.readFileSync(settingsPath, "utf-8");
1509
+ if (fs5.existsSync(settingsPath)) {
1510
+ const content = fs5.readFileSync(settingsPath, "utf-8");
922
1511
  settings = JSON.parse(content);
923
1512
  }
924
1513
  } catch {
@@ -927,7 +1516,7 @@ function installStopHook() {
927
1516
  settings.hooks = {};
928
1517
  }
929
1518
  const hooks = settings.hooks;
930
- const syncScriptPath = path4.join(claudeDir, "ccgather-sync.js");
1519
+ const syncScriptPath = path5.join(claudeDir, "ccgather-sync.js");
931
1520
  const hookCommand = `node "${syncScriptPath}"`;
932
1521
  if (!hooks.Stop || !Array.isArray(hooks.Stop)) {
933
1522
  hooks.Stop = [];
@@ -941,34 +1530,34 @@ function installStopHook() {
941
1530
  background: true
942
1531
  });
943
1532
  }
944
- fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
1533
+ fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
945
1534
  return { success: true, message: "Stop hook installed" };
946
1535
  }
947
1536
  function saveSyncScript(apiUrl, apiToken) {
948
1537
  const claudeDir = getClaudeSettingsDir2();
949
- const scriptPath = path4.join(claudeDir, "ccgather-sync.js");
1538
+ const scriptPath = path5.join(claudeDir, "ccgather-sync.js");
950
1539
  const scriptContent = generateSyncScript(apiUrl, apiToken);
951
- fs4.writeFileSync(scriptPath, scriptContent);
952
- if (os4.platform() !== "win32") {
953
- fs4.chmodSync(scriptPath, "755");
1540
+ fs5.writeFileSync(scriptPath, scriptContent);
1541
+ if (os5.platform() !== "win32") {
1542
+ fs5.chmodSync(scriptPath, "755");
954
1543
  }
955
1544
  }
956
1545
  async function setupAuto(options = {}) {
957
1546
  if (options.manual) {
958
- console.log(import_chalk4.default.bold("\n\u{1F527} Disabling Auto-Sync\n"));
1547
+ console.log(import_chalk3.default.bold("\n\u{1F527} Disabling Auto-Sync\n"));
959
1548
  const { reset: reset2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
960
1549
  await reset2();
961
- console.log(import_chalk4.default.green("\u2713 Auto-sync disabled. Use `npx ccgather` to submit manually."));
1550
+ console.log(import_chalk3.default.green("\u2713 Auto-sync disabled. Use `npx ccgather` to submit manually."));
962
1551
  return;
963
1552
  }
964
- console.log(import_chalk4.default.bold("\n\u26A0\uFE0F Auto-Sync Mode (Optional)\n"));
965
- console.log(import_chalk4.default.gray("This will install a hook that automatically syncs"));
966
- console.log(import_chalk4.default.gray("your usage data when Claude Code sessions end."));
1553
+ console.log(import_chalk3.default.bold("\n\u26A0\uFE0F Auto-Sync Mode (Optional)\n"));
1554
+ console.log(import_chalk3.default.gray("This will install a hook that automatically syncs"));
1555
+ console.log(import_chalk3.default.gray("your usage data when Claude Code sessions end."));
967
1556
  console.log();
968
- console.log(import_chalk4.default.yellow("Note: Manual submission (`npx ccgather`) is recommended for most users."));
1557
+ console.log(import_chalk3.default.yellow("Note: Manual submission (`npx ccgather`) is recommended for most users."));
969
1558
  console.log();
970
- const inquirer2 = await import("inquirer");
971
- const { proceed } = await inquirer2.default.prompt([
1559
+ const inquirer4 = await import("inquirer");
1560
+ const { proceed } = await inquirer4.default.prompt([
972
1561
  {
973
1562
  type: "confirm",
974
1563
  name: "proceed",
@@ -977,16 +1566,16 @@ async function setupAuto(options = {}) {
977
1566
  }
978
1567
  ]);
979
1568
  if (!proceed) {
980
- console.log(import_chalk4.default.gray("\nSetup cancelled. Use `npx ccgather` to submit manually."));
1569
+ console.log(import_chalk3.default.gray("\nSetup cancelled. Use `npx ccgather` to submit manually."));
981
1570
  return;
982
1571
  }
983
1572
  const config = getConfig();
984
1573
  const apiUrl = getApiUrl();
985
- console.log(import_chalk4.default.bold("\n\u{1F310} CCgather Setup\n"));
1574
+ console.log(import_chalk3.default.bold("\n\u{1F310} CCgather Setup\n"));
986
1575
  const existingToken = config.get("apiToken");
987
1576
  if (existingToken) {
988
- const inquirer3 = await import("inquirer");
989
- const { reconfigure } = await inquirer3.default.prompt([
1577
+ const inquirer5 = await import("inquirer");
1578
+ const { reconfigure } = await inquirer5.default.prompt([
990
1579
  {
991
1580
  type: "confirm",
992
1581
  name: "reconfigure",
@@ -995,11 +1584,11 @@ async function setupAuto(options = {}) {
995
1584
  }
996
1585
  ]);
997
1586
  if (!reconfigure) {
998
- console.log(import_chalk4.default.gray("Setup cancelled."));
1587
+ console.log(import_chalk3.default.gray("Setup cancelled."));
999
1588
  return;
1000
1589
  }
1001
1590
  }
1002
- console.log(import_chalk4.default.gray("Opening browser for GitHub authentication...\n"));
1591
+ console.log(import_chalk3.default.gray("Opening browser for GitHub authentication...\n"));
1003
1592
  const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
1004
1593
  const baseUrl = apiUrl.replace("/api", "");
1005
1594
  const authUrl = `${baseUrl}/cli/auth?callback=${encodeURIComponent(callbackUrl)}`;
@@ -1007,15 +1596,15 @@ async function setupAuto(options = {}) {
1007
1596
  try {
1008
1597
  await openBrowser(authUrl);
1009
1598
  } catch {
1010
- console.log(import_chalk4.default.yellow("Could not open browser automatically."));
1011
- console.log(import_chalk4.default.gray("Please open this URL manually:"));
1012
- console.log(import_chalk4.default.cyan(authUrl));
1599
+ console.log(import_chalk3.default.yellow("Could not open browser automatically."));
1600
+ console.log(import_chalk3.default.gray("Please open this URL manually:"));
1601
+ console.log(import_chalk3.default.cyan(authUrl));
1013
1602
  console.log();
1014
1603
  }
1015
1604
  const spinner = (0, import_ora4.default)("Waiting for authentication...").start();
1016
1605
  try {
1017
1606
  const authData = await serverPromise;
1018
- spinner.succeed(import_chalk4.default.green("Authentication successful!"));
1607
+ spinner.succeed(import_chalk3.default.green("Authentication successful!"));
1019
1608
  config.set("apiToken", authData.token);
1020
1609
  config.set("userId", authData.userId);
1021
1610
  const hookSpinner = (0, import_ora4.default)("Installing Claude Code hook...").start();
@@ -1023,102 +1612,329 @@ async function setupAuto(options = {}) {
1023
1612
  saveSyncScript(apiUrl, authData.token);
1024
1613
  const hookResult = installStopHook();
1025
1614
  if (hookResult.success) {
1026
- hookSpinner.succeed(import_chalk4.default.green("Hook installed successfully!"));
1615
+ hookSpinner.succeed(import_chalk3.default.green("Hook installed successfully!"));
1027
1616
  } else {
1028
- hookSpinner.fail(import_chalk4.default.red("Failed to install hook"));
1029
- console.log(import_chalk4.default.red(hookResult.message));
1617
+ hookSpinner.fail(import_chalk3.default.red("Failed to install hook"));
1618
+ console.log(import_chalk3.default.red(hookResult.message));
1030
1619
  }
1031
1620
  } catch (err) {
1032
- hookSpinner.fail(import_chalk4.default.red("Failed to install hook"));
1033
- console.log(import_chalk4.default.red(err instanceof Error ? err.message : "Unknown error"));
1621
+ hookSpinner.fail(import_chalk3.default.red("Failed to install hook"));
1622
+ console.log(import_chalk3.default.red(err instanceof Error ? err.message : "Unknown error"));
1034
1623
  }
1035
1624
  console.log();
1036
- console.log(import_chalk4.default.green.bold("\u2705 Setup complete!"));
1625
+ console.log(import_chalk3.default.green.bold("\u2705 Setup complete!"));
1037
1626
  console.log();
1038
- console.log(import_chalk4.default.gray(`Welcome, ${import_chalk4.default.white(authData.username)}!`));
1627
+ console.log(import_chalk3.default.gray(`Welcome, ${import_chalk3.default.white(authData.username)}!`));
1039
1628
  console.log();
1040
- console.log(import_chalk4.default.gray("Your Claude Code usage will now be automatically synced"));
1041
- console.log(import_chalk4.default.gray("to the leaderboard when each session ends."));
1629
+ console.log(import_chalk3.default.gray("Your Claude Code usage will now be automatically synced"));
1630
+ console.log(import_chalk3.default.gray("to the leaderboard when each session ends."));
1042
1631
  console.log();
1043
- console.log(import_chalk4.default.gray("View your stats:"));
1044
- console.log(import_chalk4.default.cyan(" npx ccgather status"));
1632
+ console.log(import_chalk3.default.gray("View your stats:"));
1633
+ console.log(import_chalk3.default.cyan(" npx ccgather status"));
1045
1634
  console.log();
1046
- console.log(import_chalk4.default.gray("View the leaderboard:"));
1047
- console.log(import_chalk4.default.cyan(" https://ccgather.dev/leaderboard"));
1635
+ console.log(import_chalk3.default.gray("View the leaderboard:"));
1636
+ console.log(import_chalk3.default.cyan(" https://ccgather.dev/leaderboard"));
1048
1637
  console.log();
1049
1638
  } catch (err) {
1050
- spinner.fail(import_chalk4.default.red("Authentication failed"));
1051
- console.log(import_chalk4.default.red(err instanceof Error ? err.message : "Unknown error"));
1639
+ spinner.fail(import_chalk3.default.red("Authentication failed"));
1640
+ console.log(import_chalk3.default.red(err instanceof Error ? err.message : "Unknown error"));
1052
1641
  process.exit(1);
1053
1642
  }
1054
1643
  }
1055
1644
 
1056
1645
  // src/commands/scan.ts
1057
- var import_chalk5 = __toESM(require("chalk"));
1058
1646
  var import_ora5 = __toESM(require("ora"));
1059
- function formatNumber3(num) {
1060
- if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
1061
- if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
1062
- return num.toLocaleString();
1063
- }
1064
1647
  function displayResults(data) {
1065
- const orange = import_chalk5.default.hex("#FF6B35");
1066
- const green = import_chalk5.default.hex("#10B981");
1067
- const gray = import_chalk5.default.gray;
1068
- const white = import_chalk5.default.white;
1069
- const bold = import_chalk5.default.bold;
1070
1648
  console.log();
1071
- 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"));
1073
- 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"));
1078
- 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"));
1084
- 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"));
1649
+ const usageLines = [
1650
+ `${colors.white.bold("\u{1F4CA} Usage Summary")}`,
1651
+ "",
1652
+ `${colors.muted("Total Tokens")} ${colors.primary(formatNumber(data.usage.totalTokens))}`,
1653
+ `${colors.muted("Total Cost")} ${colors.success(formatCost(data.usage.totalCost))}`,
1654
+ `${colors.muted("Input Tokens")} ${colors.white(formatNumber(data.usage.inputTokens))}`,
1655
+ `${colors.muted("Output Tokens")} ${colors.white(formatNumber(data.usage.outputTokens))}`
1656
+ ];
1657
+ if (data.usage.cacheReadTokens > 0 || data.usage.cacheWriteTokens > 0) {
1658
+ usageLines.push(`${colors.muted("Cache Read")} ${colors.dim(formatNumber(data.usage.cacheReadTokens))}`);
1659
+ usageLines.push(`${colors.muted("Cache Write")} ${colors.dim(formatNumber(data.usage.cacheWriteTokens))}`);
1660
+ }
1661
+ console.log(createBox(usageLines));
1662
+ console.log();
1663
+ const statsLines = [
1664
+ `${colors.white.bold("\u{1F4C8} Statistics")}`,
1665
+ "",
1666
+ `${colors.muted("Days Tracked")} ${colors.warning(data.stats.daysTracked.toString())}`,
1667
+ `${colors.muted("Sessions")} ${colors.white(data.stats.sessionsCount.toString())}`,
1668
+ `${colors.muted("First Used")} ${colors.dim(data.stats.firstUsed || "N/A")}`,
1669
+ `${colors.muted("Last Used")} ${colors.dim(data.stats.lastUsed || "N/A")}`
1670
+ ];
1671
+ console.log(createBox(statsLines));
1672
+ if (data.account?.ccplan) {
1673
+ console.log();
1674
+ const badge = getCCplanBadge(data.account.ccplan);
1675
+ console.log(` ${colors.muted("Account Plan:")} ${badge}`);
1676
+ if (data.account.rateLimitTier) {
1677
+ console.log(` ${colors.muted("Rate Limit:")} ${colors.dim(data.account.rateLimitTier)}`);
1678
+ }
1679
+ }
1085
1680
  if (Object.keys(data.models).length > 0) {
1086
1681
  console.log();
1087
- console.log(gray(" ") + bold("Model Breakdown:"));
1088
- for (const [model, tokens] of Object.entries(data.models)) {
1682
+ console.log(` ${colors.white.bold("\u{1F916} Model Breakdown")}`);
1683
+ console.log(colors.dim(" \u2500".repeat(25)));
1684
+ const sortedModels = Object.entries(data.models).sort(([, a], [, b]) => b - a).slice(0, 5);
1685
+ for (const [model, tokens] of sortedModels) {
1089
1686
  const shortModel = model.replace("claude-", "").substring(0, 20);
1090
- console.log(gray(" \u2022 ") + white(shortModel.padEnd(22)) + orange(formatNumber3(tokens)));
1687
+ console.log(` ${colors.dim("\u2022")} ${colors.white(shortModel.padEnd(22))} ${colors.primary(formatNumber(tokens))}`);
1688
+ }
1689
+ if (Object.keys(data.models).length > 5) {
1690
+ console.log(` ${colors.dim(`... and ${Object.keys(data.models).length - 5} more models`)}`);
1691
+ }
1692
+ }
1693
+ if (data.projects && Object.keys(data.projects).length > 0) {
1694
+ console.log();
1695
+ console.log(` ${colors.white.bold("\u{1F4C1} Top Projects")}`);
1696
+ console.log(colors.dim(" \u2500".repeat(25)));
1697
+ const sortedProjects = Object.entries(data.projects).sort(([, a], [, b]) => b.tokens - a.tokens).slice(0, 5);
1698
+ for (const [name, stats] of sortedProjects) {
1699
+ const displayName = name.length > 20 ? name.substring(0, 17) + "..." : name;
1700
+ console.log(
1701
+ ` ${colors.cyan(displayName.padEnd(20))} ${colors.primary(formatNumber(stats.tokens).padStart(8))} ${colors.success(formatCost(stats.cost).padStart(10))} ${colors.dim(`(${stats.sessions} sess)`)}`
1702
+ );
1703
+ }
1704
+ if (Object.keys(data.projects).length > 5) {
1705
+ console.log(` ${colors.dim(`... and ${Object.keys(data.projects).length - 5} more projects`)}`);
1091
1706
  }
1092
1707
  }
1093
1708
  console.log();
1094
- console.log(gray(` \u{1F4C1} Saved to: ${getCCGatherJsonPath()}`));
1709
+ console.log(colors.dim(" \u2500".repeat(25)));
1710
+ console.log(` ${colors.muted("Saved to:")} ${colors.dim(getCCGatherJsonPath())}`);
1095
1711
  console.log();
1096
1712
  }
1097
1713
  async function scan() {
1098
- console.log(import_chalk5.default.bold("\n\u{1F50D} Scanning Claude Code Usage\n"));
1099
- const spinner = (0, import_ora5.default)("Scanning JSONL files...").start();
1714
+ printCompactHeader("1.2.0");
1715
+ console.log(header("Scan Claude Code Usage", "\u{1F50D}"));
1716
+ const spinner = (0, import_ora5.default)({
1717
+ text: "Scanning JSONL files...",
1718
+ color: "cyan"
1719
+ }).start();
1100
1720
  const data = scanAndSave();
1101
1721
  if (!data) {
1102
- spinner.fail(import_chalk5.default.red("No usage data found"));
1103
- console.log(import_chalk5.default.gray("\nPossible reasons:"));
1104
- console.log(import_chalk5.default.gray(" \u2022 Claude Code has not been used yet"));
1105
- console.log(import_chalk5.default.gray(" \u2022 ~/.claude/projects/ directory is empty"));
1106
- console.log(import_chalk5.default.gray(" \u2022 No permission to read files\n"));
1722
+ spinner.fail(colors.error("No usage data found"));
1723
+ console.log();
1724
+ console.log(` ${error("Possible reasons:")}`);
1725
+ console.log(` ${colors.dim("\u2022")} Claude Code has not been used yet`);
1726
+ console.log(` ${colors.dim("\u2022")} ~/.claude/projects/ directory is empty`);
1727
+ console.log(` ${colors.dim("\u2022")} No permission to read files`);
1728
+ console.log();
1107
1729
  process.exit(1);
1108
1730
  }
1109
- spinner.succeed(import_chalk5.default.green("Scan complete!"));
1731
+ spinner.succeed(colors.success("Scan complete!"));
1110
1732
  displayResults(data);
1733
+ console.log(` ${colors.muted("Next:")} Run ${colors.white("npx ccgather")} to submit your data`);
1734
+ console.log();
1111
1735
  }
1112
1736
 
1113
1737
  // src/index.ts
1738
+ init_config();
1739
+ init_api();
1740
+ var VERSION = "1.2.0";
1114
1741
  var program = new import_commander.Command();
1115
- 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");
1742
+ program.name("ccgather").description("Submit your Claude Code usage to the CCgather leaderboard").version(VERSION).option("-y, --yes", "Skip confirmation prompt").option("--auto", "Enable automatic sync on session end").option("--manual", "Disable automatic sync").option("--no-menu", "Skip interactive menu (direct submit)");
1116
1743
  program.command("scan").description("Scan Claude Code usage and create ccgather.json").action(scan);
1117
- program.command("rank").description("View your current rank and stats").action(status);
1744
+ program.command("rank").description("View your current rank and stats").action(() => status({ json: false }));
1745
+ program.command("status").description("View your current rank and stats").option("--json", "Output as JSON").action((opts) => status(opts));
1746
+ program.command("sync").description("Sync usage data to CCgather").action(async () => {
1747
+ const { sync: sync2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
1748
+ await sync2({});
1749
+ });
1750
+ program.command("auth").description("Authenticate with CCgather").action(async () => {
1751
+ const { auth: auth2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
1752
+ await auth2({});
1753
+ });
1118
1754
  program.command("reset").description("Remove auto-sync hook and clear config").action(async () => {
1119
1755
  const { reset: reset2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
1120
1756
  await reset2();
1121
1757
  });
1758
+ async function showInteractiveMenu() {
1759
+ printHeader(VERSION);
1760
+ if (isAuthenticated()) {
1761
+ try {
1762
+ const result = await getStatus();
1763
+ if (result.success && result.data) {
1764
+ const stats = result.data;
1765
+ const levelInfo = getLevelInfo(stats.totalTokens);
1766
+ const welcomeBox = createWelcomeBox({
1767
+ username: getConfig().get("username") || "User",
1768
+ level: levelInfo.level,
1769
+ levelName: levelInfo.name,
1770
+ levelIcon: levelInfo.icon,
1771
+ globalRank: stats.rank,
1772
+ ccplan: stats.tier
1773
+ });
1774
+ console.log(welcomeBox);
1775
+ console.log();
1776
+ }
1777
+ } catch {
1778
+ }
1779
+ } else {
1780
+ console.log(colors.dim(' Not logged in. Run "ccgather auth" to authenticate.\n'));
1781
+ }
1782
+ const { action } = await import_inquirer3.default.prompt([
1783
+ {
1784
+ type: "list",
1785
+ name: "action",
1786
+ message: "What would you like to do?",
1787
+ choices: [
1788
+ { name: `\u{1F4E4} ${import_chalk6.default.white("Submit usage data")}`, value: "submit" },
1789
+ { name: `\u{1F4CA} ${import_chalk6.default.white("View my stats")}`, value: "status" },
1790
+ { name: `\u{1F50D} ${import_chalk6.default.white("Scan local usage")}`, value: "scan" },
1791
+ { name: `\u2699\uFE0F ${import_chalk6.default.white("Setup auto-sync")}`, value: "setup" },
1792
+ { name: `\u{1F527} ${import_chalk6.default.white("Settings")}`, value: "settings" },
1793
+ new import_inquirer3.default.Separator(),
1794
+ { name: `\u2753 ${import_chalk6.default.gray("Help")}`, value: "help" },
1795
+ { name: `\u{1F6AA} ${import_chalk6.default.gray("Exit")}`, value: "exit" }
1796
+ ],
1797
+ loop: false
1798
+ }
1799
+ ]);
1800
+ console.log();
1801
+ switch (action) {
1802
+ case "submit":
1803
+ await submit({ yes: false });
1804
+ break;
1805
+ case "status":
1806
+ await status({ json: false });
1807
+ break;
1808
+ case "scan":
1809
+ await scan();
1810
+ break;
1811
+ case "setup":
1812
+ await showSetupMenu();
1813
+ break;
1814
+ case "settings":
1815
+ await showSettingsMenu();
1816
+ break;
1817
+ case "help":
1818
+ showHelp();
1819
+ break;
1820
+ case "exit":
1821
+ console.log(colors.muted(" Goodbye! Happy coding with Claude! \u{1F44B}\n"));
1822
+ process.exit(0);
1823
+ }
1824
+ }
1825
+ async function showSetupMenu() {
1826
+ const { setupAction } = await import_inquirer3.default.prompt([
1827
+ {
1828
+ type: "list",
1829
+ name: "setupAction",
1830
+ message: "Auto-sync settings:",
1831
+ choices: [
1832
+ { name: `\u2705 ${import_chalk6.default.white("Enable auto-sync")}`, value: "enable" },
1833
+ { name: `\u274C ${import_chalk6.default.white("Disable auto-sync")}`, value: "disable" },
1834
+ { name: `\u2B05\uFE0F ${import_chalk6.default.gray("Back")}`, value: "back" }
1835
+ ]
1836
+ }
1837
+ ]);
1838
+ switch (setupAction) {
1839
+ case "enable":
1840
+ await setupAuto({ auto: true });
1841
+ break;
1842
+ case "disable":
1843
+ await setupAuto({ manual: true });
1844
+ break;
1845
+ case "back":
1846
+ await showInteractiveMenu();
1847
+ break;
1848
+ }
1849
+ }
1850
+ async function showSettingsMenu() {
1851
+ const config = getConfig();
1852
+ const isAuth = isAuthenticated();
1853
+ const { settingsAction } = await import_inquirer3.default.prompt([
1854
+ {
1855
+ type: "list",
1856
+ name: "settingsAction",
1857
+ message: "Settings:",
1858
+ choices: [
1859
+ {
1860
+ name: isAuth ? `\u{1F510} ${import_chalk6.default.white("Re-authenticate")}` : `\u{1F510} ${import_chalk6.default.white("Login / Authenticate")}`,
1861
+ value: "auth"
1862
+ },
1863
+ { name: `\u{1F5D1}\uFE0F ${import_chalk6.default.white("Reset all settings")}`, value: "reset" },
1864
+ { name: `\u{1F4CB} ${import_chalk6.default.white("Show current config")}`, value: "show" },
1865
+ { name: `\u2B05\uFE0F ${import_chalk6.default.gray("Back")}`, value: "back" }
1866
+ ]
1867
+ }
1868
+ ]);
1869
+ switch (settingsAction) {
1870
+ case "auth": {
1871
+ const { auth: auth2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
1872
+ await auth2({});
1873
+ break;
1874
+ }
1875
+ case "reset": {
1876
+ const { confirmReset } = await import_inquirer3.default.prompt([
1877
+ {
1878
+ type: "confirm",
1879
+ name: "confirmReset",
1880
+ message: import_chalk6.default.yellow("Are you sure you want to reset all settings?"),
1881
+ default: false
1882
+ }
1883
+ ]);
1884
+ if (confirmReset) {
1885
+ const { reset: reset2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
1886
+ await reset2();
1887
+ }
1888
+ break;
1889
+ }
1890
+ case "show":
1891
+ console.log();
1892
+ console.log(colors.muted(" Current Configuration:"));
1893
+ console.log(colors.dim(" \u2500".repeat(25)));
1894
+ console.log(` ${colors.muted("Username:")} ${config.get("username") || colors.dim("(not set)")}`);
1895
+ console.log(` ${colors.muted("API Token:")} ${config.get("apiToken") ? colors.success("\u2713 Set") : colors.error("\u2717 Not set")}`);
1896
+ console.log(` ${colors.muted("Auto-sync:")} ${config.get("autoSync") ? colors.success("Enabled") : colors.dim("Disabled")}`);
1897
+ console.log(` ${colors.muted("Last Sync:")} ${config.get("lastSync") || colors.dim("Never")}`);
1898
+ console.log();
1899
+ await promptContinue();
1900
+ await showSettingsMenu();
1901
+ break;
1902
+ case "back":
1903
+ await showInteractiveMenu();
1904
+ break;
1905
+ }
1906
+ }
1907
+ function showHelp() {
1908
+ console.log(colors.primary.bold(" CCgather CLI Commands"));
1909
+ console.log(colors.dim(" \u2500".repeat(25)));
1910
+ console.log();
1911
+ console.log(` ${colors.white("npx ccgather")} Interactive menu (default)`);
1912
+ console.log(` ${colors.white("npx ccgather scan")} Scan local Claude Code usage`);
1913
+ console.log(` ${colors.white("npx ccgather rank")} View your current rank`);
1914
+ console.log(` ${colors.white("npx ccgather sync")} Sync usage data`);
1915
+ console.log(` ${colors.white("npx ccgather auth")} Authenticate with CCgather`);
1916
+ console.log(` ${colors.white("npx ccgather reset")} Reset all settings`);
1917
+ console.log();
1918
+ console.log(colors.dim(" \u2500".repeat(25)));
1919
+ console.log(` ${colors.muted("Options:")}`);
1920
+ console.log(` ${colors.white("-y, --yes")} Skip confirmation prompts`);
1921
+ console.log(` ${colors.white("--auto")} Enable auto-sync`);
1922
+ console.log(` ${colors.white("--manual")} Disable auto-sync`);
1923
+ console.log(` ${colors.white("--no-menu")} Skip interactive menu`);
1924
+ console.log();
1925
+ console.log(` ${colors.muted("Documentation:")} ${link("https://ccgather.dev/docs")}`);
1926
+ console.log(` ${colors.muted("Leaderboard:")} ${link("https://ccgather.dev/leaderboard")}`);
1927
+ console.log();
1928
+ }
1929
+ async function promptContinue() {
1930
+ await import_inquirer3.default.prompt([
1931
+ {
1932
+ type: "input",
1933
+ name: "continue",
1934
+ message: colors.dim("Press Enter to continue...")
1935
+ }
1936
+ ]);
1937
+ }
1122
1938
  program.action(async (options) => {
1123
1939
  if (options.auto) {
1124
1940
  await setupAuto({ auto: true });
@@ -1128,6 +1944,10 @@ program.action(async (options) => {
1128
1944
  await setupAuto({ manual: true });
1129
1945
  return;
1130
1946
  }
1131
- await submit({ yes: options.yes });
1947
+ if (options.menu === false) {
1948
+ await submit({ yes: options.yes });
1949
+ return;
1950
+ }
1951
+ await showInteractiveMenu();
1132
1952
  });
1133
1953
  program.parse();