ccgather 1.3.2 → 1.3.3

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 +1089 -1058
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -70,1202 +70,1230 @@ 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" };
73
+ // src/lib/credentials.ts
74
+ function getCredentialsPath() {
75
+ return path.join(os.homedir(), ".claude", ".credentials.json");
76
+ }
77
+ function mapSubscriptionToCCPlan(subscriptionType) {
78
+ if (!subscriptionType) {
79
+ return "free";
80
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 };
81
+ const type = subscriptionType.toLowerCase();
82
+ if (type === "max" || type.includes("max")) {
83
+ return "max";
98
84
  }
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
- var init_api = __esm({
110
- "src/lib/api.ts"() {
111
- "use strict";
112
- init_config();
85
+ if (type === "pro") {
86
+ return "pro";
113
87
  }
114
- });
115
-
116
- // src/commands/reset.ts
117
- var reset_exports = {};
118
- __export(reset_exports, {
119
- reset: () => reset
120
- });
121
- function getClaudeSettingsDir() {
122
- return path4.join(os4.homedir(), ".claude");
88
+ if (type === "free") {
89
+ return "free";
90
+ }
91
+ return type;
123
92
  }
124
- function removeStopHook() {
125
- const claudeDir = getClaudeSettingsDir();
126
- const settingsPath = path4.join(claudeDir, "settings.json");
127
- if (!fs4.existsSync(settingsPath)) {
128
- return { success: true, message: "No settings file found" };
93
+ function readCredentials() {
94
+ const credentialsPath = getCredentialsPath();
95
+ const defaultData = {
96
+ ccplan: null,
97
+ rateLimitTier: null
98
+ };
99
+ if (!fs.existsSync(credentialsPath)) {
100
+ return defaultData;
129
101
  }
130
102
  try {
131
- const content = fs4.readFileSync(settingsPath, "utf-8");
132
- const settings = JSON.parse(content);
133
- if (settings.hooks && typeof settings.hooks === "object") {
134
- const hooks = settings.hooks;
135
- if (hooks.Stop && Array.isArray(hooks.Stop)) {
136
- hooks.Stop = hooks.Stop.filter((hook) => {
137
- if (typeof hook === "object" && hook !== null) {
138
- const h = hook;
139
- return typeof h.command !== "string" || !h.command.includes("ccgather");
140
- }
141
- return true;
142
- });
143
- if (hooks.Stop.length === 0) {
144
- delete hooks.Stop;
145
- }
146
- }
103
+ const content = fs.readFileSync(credentialsPath, "utf-8");
104
+ const credentials = JSON.parse(content);
105
+ const oauthData = credentials.claudeAiOauth;
106
+ if (!oauthData) {
107
+ return defaultData;
147
108
  }
148
- fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
149
- return { success: true, message: "Hook removed" };
150
- } catch (err) {
109
+ const ccplan = mapSubscriptionToCCPlan(oauthData.subscriptionType);
110
+ const rateLimitTier = oauthData.rateLimitTier || null;
151
111
  return {
152
- success: false,
153
- message: err instanceof Error ? err.message : "Unknown error"
112
+ ccplan,
113
+ rateLimitTier
154
114
  };
115
+ } catch (error2) {
116
+ return defaultData;
155
117
  }
156
118
  }
157
- function removeSyncScript() {
158
- const claudeDir = getClaudeSettingsDir();
159
- const scriptPath = path4.join(claudeDir, "ccgather-sync.js");
160
- if (fs4.existsSync(scriptPath)) {
161
- fs4.unlinkSync(scriptPath);
162
- }
163
- }
164
- async function reset() {
165
- const config = getConfig();
166
- console.log(import_chalk2.default.bold("\n\u{1F504} CCgather Reset\n"));
167
- if (!config.get("apiToken")) {
168
- console.log(import_chalk2.default.yellow("CCgather is not configured."));
169
- return;
119
+ var fs, path, os;
120
+ var init_credentials = __esm({
121
+ "src/lib/credentials.ts"() {
122
+ "use strict";
123
+ fs = __toESM(require("fs"));
124
+ path = __toESM(require("path"));
125
+ os = __toESM(require("os"));
170
126
  }
171
- const { confirmReset } = await import_inquirer.default.prompt([
172
- {
173
- type: "confirm",
174
- name: "confirmReset",
175
- message: "This will remove the CCgather hook and local configuration. Continue?",
176
- default: false
127
+ });
128
+
129
+ // src/lib/ccgather-json.ts
130
+ function extractProjectName(filePath) {
131
+ const parts = filePath.split(/[/\\]/);
132
+ const projectsIndex = parts.findIndex((p) => p === "projects");
133
+ if (projectsIndex >= 0 && parts[projectsIndex + 1]) {
134
+ try {
135
+ const encoded = parts[projectsIndex + 1];
136
+ const decoded = decodeURIComponent(encoded);
137
+ const pathParts = decoded.split(/[/\\]/);
138
+ return pathParts[pathParts.length - 1] || decoded;
139
+ } catch {
140
+ return parts[projectsIndex + 1];
177
141
  }
178
- ]);
179
- if (!confirmReset) {
180
- console.log(import_chalk2.default.gray("Reset cancelled."));
181
- return;
182
- }
183
- const hookSpinner = (0, import_ora3.default)("Removing Claude Code hook...").start();
184
- const hookResult = removeStopHook();
185
- if (hookResult.success) {
186
- hookSpinner.succeed(import_chalk2.default.green("Hook removed"));
187
- } else {
188
- hookSpinner.warn(import_chalk2.default.yellow(`Could not remove hook: ${hookResult.message}`));
189
142
  }
190
- const scriptSpinner = (0, import_ora3.default)("Removing sync script...").start();
143
+ return "unknown";
144
+ }
145
+ function getCCGatherJsonPath() {
146
+ return path2.join(os2.homedir(), ".claude", "ccgather.json");
147
+ }
148
+ function getClaudeProjectsDir() {
149
+ return path2.join(os2.homedir(), ".claude", "projects");
150
+ }
151
+ function findJsonlFiles(dir) {
152
+ const files = [];
191
153
  try {
192
- removeSyncScript();
193
- scriptSpinner.succeed(import_chalk2.default.green("Sync script removed"));
154
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
155
+ for (const entry of entries) {
156
+ const fullPath = path2.join(dir, entry.name);
157
+ if (entry.isDirectory()) {
158
+ files.push(...findJsonlFiles(fullPath));
159
+ } else if (entry.name.endsWith(".jsonl")) {
160
+ files.push(fullPath);
161
+ }
162
+ }
194
163
  } catch {
195
- scriptSpinner.warn(import_chalk2.default.yellow("Could not remove sync script"));
196
164
  }
197
- const { deleteAccount } = await import_inquirer.default.prompt([
198
- {
199
- type: "confirm",
200
- name: "deleteAccount",
201
- message: import_chalk2.default.red("Do you also want to delete your account from the leaderboard? (This cannot be undone)"),
202
- default: false
165
+ return files;
166
+ }
167
+ function estimateCost(model, inputTokens, outputTokens) {
168
+ const pricing = {
169
+ "claude-opus-4": { input: 15, output: 75 },
170
+ "claude-sonnet-4": { input: 3, output: 15 },
171
+ "claude-haiku": { input: 0.25, output: 1.25 },
172
+ default: { input: 3, output: 15 }
173
+ };
174
+ let modelKey = "default";
175
+ for (const key of Object.keys(pricing)) {
176
+ if (model.includes(key.replace("claude-", ""))) {
177
+ modelKey = key;
178
+ break;
203
179
  }
204
- ]);
205
- if (deleteAccount) {
206
- console.log(import_chalk2.default.yellow("\nAccount deletion is not yet implemented."));
207
- console.log(import_chalk2.default.gray("Please contact support to delete your account."));
208
180
  }
209
- const configSpinner = (0, import_ora3.default)("Resetting local configuration...").start();
210
- resetConfig();
211
- configSpinner.succeed(import_chalk2.default.green("Local configuration reset"));
212
- console.log();
213
- console.log(import_chalk2.default.green.bold("\u2705 Reset complete!"));
214
- console.log();
215
- console.log(import_chalk2.default.gray("Your usage will no longer be tracked."));
216
- console.log(import_chalk2.default.gray("Run `npx ccgather` to set up again."));
217
- console.log();
181
+ const price = pricing[modelKey];
182
+ const inputCost = inputTokens / 1e6 * price.input;
183
+ const outputCost = outputTokens / 1e6 * price.output;
184
+ return Math.round((inputCost + outputCost) * 100) / 100;
218
185
  }
219
- var import_chalk2, import_ora3, fs4, path4, os4, import_inquirer;
220
- var init_reset = __esm({
221
- "src/commands/reset.ts"() {
222
- "use strict";
223
- import_chalk2 = __toESM(require("chalk"));
224
- import_ora3 = __toESM(require("ora"));
225
- fs4 = __toESM(require("fs"));
226
- path4 = __toESM(require("path"));
227
- os4 = __toESM(require("os"));
228
- import_inquirer = __toESM(require("inquirer"));
229
- init_config();
186
+ function scanUsageData(options = {}) {
187
+ const projectsDir = getClaudeProjectsDir();
188
+ if (!fs2.existsSync(projectsDir)) {
189
+ return null;
230
190
  }
231
- });
232
-
233
- // src/lib/claude.ts
234
- function getClaudeConfigDir() {
235
- const platform3 = os6.platform();
236
- if (platform3 === "win32") {
237
- return path6.join(os6.homedir(), "AppData", "Roaming", "claude-code");
238
- } else if (platform3 === "darwin") {
239
- return path6.join(os6.homedir(), "Library", "Application Support", "claude-code");
240
- } else {
241
- return path6.join(os6.homedir(), ".config", "claude-code");
191
+ const days = options.days ?? 30;
192
+ let cutoffDate = null;
193
+ if (days > 0) {
194
+ const cutoff = /* @__PURE__ */ new Date();
195
+ cutoff.setDate(cutoff.getDate() - days);
196
+ cutoff.setHours(0, 0, 0, 0);
197
+ cutoffDate = cutoff.toISOString();
242
198
  }
243
- }
244
- function getUsageFilePaths() {
245
- const configDir = getClaudeConfigDir();
246
- return [
247
- path6.join(configDir, "usage.json"),
248
- path6.join(configDir, "stats.json"),
249
- path6.join(configDir, "data", "usage.json"),
250
- path6.join(os6.homedir(), ".claude", "usage.json"),
251
- path6.join(os6.homedir(), ".claude-code", "usage.json")
252
- ];
253
- }
254
- function readClaudeUsage() {
255
- const possiblePaths = getUsageFilePaths();
256
- for (const filePath of possiblePaths) {
199
+ let totalInputTokens = 0;
200
+ let totalOutputTokens = 0;
201
+ let totalCacheRead = 0;
202
+ let totalCacheWrite = 0;
203
+ let totalCost = 0;
204
+ let sessionsCount = 0;
205
+ const dates = /* @__PURE__ */ new Set();
206
+ const models = {};
207
+ const projects = {};
208
+ const dailyData = {};
209
+ let firstTimestamp = null;
210
+ let lastTimestamp = null;
211
+ const jsonlFiles = findJsonlFiles(projectsDir);
212
+ sessionsCount = jsonlFiles.length;
213
+ for (const filePath of jsonlFiles) {
214
+ const projectName = extractProjectName(filePath);
215
+ if (!projects[projectName]) {
216
+ projects[projectName] = {
217
+ tokens: 0,
218
+ cost: 0,
219
+ sessions: 0,
220
+ models: {}
221
+ };
222
+ }
223
+ projects[projectName].sessions++;
257
224
  try {
258
- if (fs6.existsSync(filePath)) {
259
- const content = fs6.readFileSync(filePath, "utf-8");
260
- const data = JSON.parse(content);
261
- if (data.usage) {
262
- return {
263
- totalTokens: data.usage.total_tokens || 0,
264
- totalSpent: data.usage.total_cost || 0,
265
- modelBreakdown: data.usage.by_model || {},
266
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
267
- };
268
- }
269
- if (data.sessions && data.sessions.length > 0) {
270
- const breakdown = {};
271
- let totalTokens = 0;
272
- let totalSpent = 0;
273
- for (const session of data.sessions) {
274
- totalTokens += session.tokens_used || 0;
275
- totalSpent += session.cost || 0;
276
- breakdown[session.model] = (breakdown[session.model] || 0) + session.tokens_used;
225
+ const content = fs2.readFileSync(filePath, "utf-8");
226
+ const lines = content.split("\n").filter((line) => line.trim());
227
+ for (const line of lines) {
228
+ try {
229
+ const event = JSON.parse(line);
230
+ if (event.type === "assistant" && event.message?.usage) {
231
+ if (cutoffDate && event.timestamp && event.timestamp < cutoffDate) {
232
+ continue;
233
+ }
234
+ const usage = event.message.usage;
235
+ const model = event.message.model || "unknown";
236
+ const inputTokens = usage.input_tokens || 0;
237
+ const outputTokens = usage.output_tokens || 0;
238
+ totalInputTokens += inputTokens;
239
+ totalOutputTokens += outputTokens;
240
+ totalCacheRead += usage.cache_read_input_tokens || 0;
241
+ totalCacheWrite += usage.cache_creation_input_tokens || 0;
242
+ const messageCost = estimateCost(model, inputTokens, outputTokens);
243
+ totalCost += messageCost;
244
+ const totalModelTokens = inputTokens + outputTokens;
245
+ models[model] = (models[model] || 0) + totalModelTokens;
246
+ projects[projectName].tokens += totalModelTokens;
247
+ projects[projectName].cost += messageCost;
248
+ projects[projectName].models[model] = (projects[projectName].models[model] || 0) + totalModelTokens;
249
+ if (event.timestamp) {
250
+ const date = new Date(event.timestamp).toISOString().split("T")[0];
251
+ dates.add(date);
252
+ if (!dailyData[date]) {
253
+ dailyData[date] = {
254
+ tokens: 0,
255
+ cost: 0,
256
+ inputTokens: 0,
257
+ outputTokens: 0,
258
+ sessions: /* @__PURE__ */ new Set(),
259
+ models: {}
260
+ };
261
+ }
262
+ dailyData[date].tokens += totalModelTokens;
263
+ dailyData[date].cost += messageCost;
264
+ dailyData[date].inputTokens += inputTokens;
265
+ dailyData[date].outputTokens += outputTokens;
266
+ dailyData[date].sessions.add(filePath);
267
+ dailyData[date].models[model] = (dailyData[date].models[model] || 0) + totalModelTokens;
268
+ if (!firstTimestamp || event.timestamp < firstTimestamp) {
269
+ firstTimestamp = event.timestamp;
270
+ }
271
+ if (!lastTimestamp || event.timestamp > lastTimestamp) {
272
+ lastTimestamp = event.timestamp;
273
+ }
274
+ }
277
275
  }
278
- return {
279
- totalTokens,
280
- totalSpent,
281
- modelBreakdown: breakdown,
282
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
283
- };
276
+ } catch {
284
277
  }
285
278
  }
286
- } catch (error2) {
279
+ } catch {
287
280
  }
288
281
  }
289
- return null;
290
- }
291
- function isClaudeCodeInstalled() {
292
- const configDir = getClaudeConfigDir();
293
- return fs6.existsSync(configDir);
294
- }
295
- function getMockUsageData() {
282
+ const totalTokens = totalInputTokens + totalOutputTokens;
283
+ if (totalTokens === 0) {
284
+ return null;
285
+ }
286
+ for (const projectName of Object.keys(projects)) {
287
+ projects[projectName].cost = Math.round(projects[projectName].cost * 100) / 100;
288
+ }
289
+ const dailyUsage = Object.entries(dailyData).map(([date, data]) => ({
290
+ date,
291
+ tokens: data.tokens,
292
+ cost: Math.round(data.cost * 100) / 100,
293
+ inputTokens: data.inputTokens,
294
+ outputTokens: data.outputTokens,
295
+ sessions: data.sessions.size,
296
+ models: data.models
297
+ })).sort((a, b) => a.date.localeCompare(b.date));
298
+ const credentials = readCredentials();
296
299
  return {
297
- totalTokens: Math.floor(Math.random() * 1e6) + 1e4,
298
- totalSpent: Math.random() * 50 + 5,
299
- modelBreakdown: {
300
- "claude-3-5-sonnet-20241022": Math.floor(Math.random() * 5e5),
301
- "claude-3-opus-20240229": Math.floor(Math.random() * 1e5),
302
- "claude-3-haiku-20240307": Math.floor(Math.random() * 2e5)
300
+ version: CCGATHER_JSON_VERSION,
301
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
302
+ lastScanned: (/* @__PURE__ */ new Date()).toISOString(),
303
+ usage: {
304
+ totalTokens,
305
+ totalCost: Math.round(totalCost * 100) / 100,
306
+ inputTokens: totalInputTokens,
307
+ outputTokens: totalOutputTokens,
308
+ cacheReadTokens: totalCacheRead,
309
+ cacheWriteTokens: totalCacheWrite
303
310
  },
304
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
311
+ stats: {
312
+ daysTracked: dates.size,
313
+ sessionsCount,
314
+ firstUsed: firstTimestamp ? new Date(firstTimestamp).toISOString().split("T")[0] : null,
315
+ lastUsed: lastTimestamp ? new Date(lastTimestamp).toISOString().split("T")[0] : null
316
+ },
317
+ models,
318
+ projects,
319
+ dailyUsage,
320
+ account: {
321
+ ccplan: credentials.ccplan,
322
+ rateLimitTier: credentials.rateLimitTier
323
+ }
305
324
  };
306
325
  }
307
- var fs6, path6, os6;
308
- var init_claude = __esm({
309
- "src/lib/claude.ts"() {
326
+ function readCCGatherJson() {
327
+ const jsonPath = getCCGatherJsonPath();
328
+ if (!fs2.existsSync(jsonPath)) {
329
+ return null;
330
+ }
331
+ try {
332
+ const content = fs2.readFileSync(jsonPath, "utf-8");
333
+ return JSON.parse(content);
334
+ } catch {
335
+ return null;
336
+ }
337
+ }
338
+ function writeCCGatherJson(data) {
339
+ const jsonPath = getCCGatherJsonPath();
340
+ const claudeDir = path2.dirname(jsonPath);
341
+ if (!fs2.existsSync(claudeDir)) {
342
+ fs2.mkdirSync(claudeDir, { recursive: true });
343
+ }
344
+ fs2.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
345
+ }
346
+ function scanAndSave(options = {}) {
347
+ const data = scanUsageData(options);
348
+ if (data) {
349
+ writeCCGatherJson(data);
350
+ }
351
+ return data;
352
+ }
353
+ var fs2, path2, os2, CCGATHER_JSON_VERSION;
354
+ var init_ccgather_json = __esm({
355
+ "src/lib/ccgather-json.ts"() {
310
356
  "use strict";
311
- fs6 = __toESM(require("fs"));
312
- path6 = __toESM(require("path"));
313
- os6 = __toESM(require("os"));
357
+ fs2 = __toESM(require("fs"));
358
+ path2 = __toESM(require("path"));
359
+ os2 = __toESM(require("os"));
360
+ init_credentials();
361
+ CCGATHER_JSON_VERSION = "1.2.0";
314
362
  }
315
363
  });
316
364
 
317
- // src/commands/sync.ts
318
- var sync_exports = {};
319
- __export(sync_exports, {
320
- sync: () => sync
321
- });
322
- function formatNumber2(num) {
365
+ // src/lib/ui.ts
366
+ function getVersionLine(version) {
367
+ return colors.dim(` v${version} \u2022 ccgather.com`);
368
+ }
369
+ function createBox(lines, width = 47) {
370
+ const paddedLines = lines.map((line) => {
371
+ const visibleLength = stripAnsi(line).length;
372
+ const padding = width - 2 - visibleLength;
373
+ return `${box.vertical} ${line}${" ".repeat(Math.max(0, padding))} ${box.vertical}`;
374
+ });
375
+ const top = colors.dim(` ${box.topLeft}${box.horizontal.repeat(width)}${box.topRight}`);
376
+ const bottom = colors.dim(` ${box.bottomLeft}${box.horizontal.repeat(width)}${box.bottomRight}`);
377
+ return [top, ...paddedLines.map((l) => colors.dim(" ") + l), bottom].join("\n");
378
+ }
379
+ function stripAnsi(str) {
380
+ return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
381
+ }
382
+ function header(title, icon = "") {
383
+ const iconPart = icon ? `${icon} ` : "";
384
+ return `
385
+ ${colors.primary("\u2501".repeat(50))}
386
+ ${iconPart}${colors.white.bold(title)}
387
+ ${colors.primary("\u2501".repeat(50))}`;
388
+ }
389
+ function formatNumber(num) {
390
+ if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
323
391
  if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
324
392
  if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
325
- return num.toString();
393
+ return num.toLocaleString();
326
394
  }
327
- async function sync(options) {
328
- const config = getConfig();
329
- console.log(import_chalk4.default.bold("\n\u{1F504} CCgather Sync\n"));
330
- if (!isAuthenticated()) {
331
- console.log(import_chalk4.default.red("Not authenticated."));
332
- console.log(import_chalk4.default.gray("Run: npx ccgather auth\n"));
333
- process.exit(1);
334
- }
335
- if (!isClaudeCodeInstalled()) {
336
- console.log(import_chalk4.default.yellow("\u26A0\uFE0F Claude Code installation not detected."));
337
- console.log(
338
- import_chalk4.default.gray("Make sure Claude Code is installed and has been used at least once.\n")
339
- );
340
- if (process.env.CCGATHER_DEMO === "true") {
341
- console.log(import_chalk4.default.gray("Demo mode: Using mock data..."));
342
- } else {
343
- process.exit(1);
344
- }
345
- }
346
- const lastSync = config.get("lastSync");
347
- if (lastSync && !options.force) {
348
- const lastSyncDate = new Date(lastSync);
349
- const minInterval = 5 * 60 * 1e3;
350
- const timeSinceSync = Date.now() - lastSyncDate.getTime();
351
- if (timeSinceSync < minInterval) {
352
- const remaining = Math.ceil((minInterval - timeSinceSync) / 1e3 / 60);
353
- console.log(import_chalk4.default.yellow(`\u23F3 Please wait ${remaining} minutes before syncing again.`));
354
- console.log(import_chalk4.default.gray("Use --force to override.\n"));
355
- process.exit(0);
395
+ function formatCost(cost) {
396
+ return `$${cost.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
397
+ }
398
+ function getRankMedal(rank) {
399
+ if (rank === 1) return "\u{1F947}";
400
+ if (rank === 2) return "\u{1F948}";
401
+ if (rank === 3) return "\u{1F949}";
402
+ if (rank <= 10) return "\u{1F3C5}";
403
+ if (rank <= 100) return "\u{1F396}\uFE0F";
404
+ return "\u{1F4CA}";
405
+ }
406
+ function getCCplanBadge(ccplan) {
407
+ if (!ccplan) return "";
408
+ const badges = {
409
+ max: `${colors.max("\u{1F680} MAX")}`,
410
+ pro: `${colors.pro("\u26A1 PRO")}`,
411
+ team: `${colors.team("\u{1F465} TEAM")}`,
412
+ free: `${colors.free("\u26AA FREE")}`
413
+ };
414
+ return badges[ccplan.toLowerCase()] || "";
415
+ }
416
+ function getLevelInfo(tokens) {
417
+ const levels = [
418
+ { min: 0, level: 1, name: "Novice", icon: "\u{1F331}", color: colors.dim },
419
+ { min: 1e5, level: 2, name: "Apprentice", icon: "\u{1F4DA}", color: colors.muted },
420
+ { min: 5e5, level: 3, name: "Journeyman", icon: "\u26A1", color: colors.cyan },
421
+ { min: 1e6, level: 4, name: "Expert", icon: "\u{1F48E}", color: colors.pro },
422
+ { min: 5e6, level: 5, name: "Master", icon: "\u{1F525}", color: colors.warning },
423
+ { min: 1e7, level: 6, name: "Grandmaster", icon: "\u{1F451}", color: colors.max },
424
+ { min: 5e7, level: 7, name: "Legend", icon: "\u{1F31F}", color: colors.primary },
425
+ { min: 1e8, level: 8, name: "Mythic", icon: "\u{1F3C6}", color: colors.secondary }
426
+ ];
427
+ for (let i = levels.length - 1; i >= 0; i--) {
428
+ if (tokens >= levels[i].min) {
429
+ return levels[i];
356
430
  }
357
431
  }
358
- const spinner = (0, import_ora6.default)("Reading Claude Code usage data...").start();
359
- let usageData = readClaudeUsage();
360
- if (!usageData && process.env.CCGATHER_DEMO === "true") {
361
- usageData = getMockUsageData();
362
- }
363
- if (!usageData) {
364
- spinner.fail(import_chalk4.default.red("Failed to read usage data"));
365
- console.log(import_chalk4.default.gray("\nPossible reasons:"));
366
- console.log(import_chalk4.default.gray(" - Claude Code has not been used yet"));
367
- console.log(import_chalk4.default.gray(" - Usage data file is missing or corrupted"));
368
- console.log(import_chalk4.default.gray(" - Insufficient permissions to read the file\n"));
369
- process.exit(1);
432
+ return levels[0];
433
+ }
434
+ function createWelcomeBox(user) {
435
+ const levelInfo = user.level && user.levelName && user.levelIcon ? `${user.levelIcon} Level ${user.level} \u2022 ${user.levelName}` : "";
436
+ const ccplanBadge = user.ccplan ? getCCplanBadge(user.ccplan) : "";
437
+ const lines = [`\u{1F44B} ${colors.white.bold(`Welcome back, ${user.username}!`)}`];
438
+ if (levelInfo || ccplanBadge) {
439
+ lines.push(`${levelInfo}${ccplanBadge ? ` ${ccplanBadge}` : ""}`);
370
440
  }
371
- spinner.text = "Syncing to CCgather...";
372
- const result = await syncUsage({
373
- totalTokens: usageData.totalTokens,
374
- totalSpent: usageData.totalSpent,
375
- modelBreakdown: usageData.modelBreakdown,
376
- timestamp: usageData.lastUpdated
377
- });
378
- if (!result.success) {
379
- spinner.fail(import_chalk4.default.red("Sync failed"));
380
- console.log(import_chalk4.default.red(`Error: ${result.error}
381
- `));
382
- process.exit(1);
441
+ if (user.globalRank) {
442
+ lines.push(`\u{1F30D} Global Rank: ${colors.primary(`#${user.globalRank}`)}`);
383
443
  }
384
- config.set("lastSync", (/* @__PURE__ */ new Date()).toISOString());
385
- spinner.succeed(import_chalk4.default.green("Sync complete!"));
386
- console.log("\n" + import_chalk4.default.gray("\u2500".repeat(40)));
387
- console.log(import_chalk4.default.bold("\u{1F4CA} Your Stats"));
388
- console.log(import_chalk4.default.gray("\u2500".repeat(40)));
389
- console.log(` ${import_chalk4.default.gray("Tokens:")} ${import_chalk4.default.white(formatNumber2(usageData.totalTokens))}`);
390
- console.log(
391
- ` ${import_chalk4.default.gray("Spent:")} ${import_chalk4.default.green("$" + usageData.totalSpent.toFixed(2))}`
392
- );
393
- console.log(` ${import_chalk4.default.gray("Rank:")} ${import_chalk4.default.yellow("#" + result.data?.rank)}`);
394
- if (options.verbose) {
395
- console.log("\n" + import_chalk4.default.gray("Model Breakdown:"));
396
- for (const [model, tokens] of Object.entries(usageData.modelBreakdown)) {
397
- const shortModel = model.replace("claude-", "").replace(/-\d+$/, "");
398
- console.log(` ${import_chalk4.default.gray(shortModel + ":")} ${formatNumber2(tokens)}`);
399
- }
444
+ if (user.countryRank && user.countryCode) {
445
+ const flag = countryCodeToFlag(user.countryCode);
446
+ lines.push(`${flag} Country Rank: ${colors.primary(`#${user.countryRank}`)}`);
400
447
  }
401
- console.log(import_chalk4.default.gray("\u2500".repeat(40)));
402
- console.log(import_chalk4.default.gray("\nView full leaderboard: https://ccgather.com/leaderboard\n"));
448
+ return createBox(lines);
403
449
  }
404
- var import_chalk4, import_ora6;
405
- var init_sync = __esm({
406
- "src/commands/sync.ts"() {
450
+ function countryCodeToFlag(countryCode) {
451
+ if (!countryCode || countryCode.length !== 2) return "\u{1F310}";
452
+ const codePoints = countryCode.toUpperCase().split("").map((char) => 127462 + char.charCodeAt(0) - 65);
453
+ return String.fromCodePoint(...codePoints);
454
+ }
455
+ function success(message) {
456
+ return `${colors.success("\u2713")} ${message}`;
457
+ }
458
+ function error(message) {
459
+ return `${colors.error("\u2717")} ${message}`;
460
+ }
461
+ function printHeader(version) {
462
+ console.log(LOGO);
463
+ console.log(TAGLINE);
464
+ console.log(SLOGAN);
465
+ console.log();
466
+ console.log(getVersionLine(version));
467
+ console.log();
468
+ }
469
+ function printCompactHeader(version) {
470
+ console.log();
471
+ console.log(LOGO_COMPACT);
472
+ console.log(colors.dim(` v${version}`));
473
+ console.log();
474
+ }
475
+ function link(url) {
476
+ return colors.cyan.underline(url);
477
+ }
478
+ var import_chalk, colors, LOGO, LOGO_COMPACT, TAGLINE, SLOGAN, box;
479
+ var init_ui = __esm({
480
+ "src/lib/ui.ts"() {
407
481
  "use strict";
408
- import_chalk4 = __toESM(require("chalk"));
409
- import_ora6 = __toESM(require("ora"));
410
- init_config();
411
- init_api();
412
- init_claude();
482
+ import_chalk = __toESM(require("chalk"));
483
+ colors = {
484
+ primary: import_chalk.default.hex("#DA7756"),
485
+ // Claude coral
486
+ secondary: import_chalk.default.hex("#F7931E"),
487
+ // Orange accent
488
+ success: import_chalk.default.hex("#22C55E"),
489
+ // Green
490
+ warning: import_chalk.default.hex("#F59E0B"),
491
+ // Amber
492
+ error: import_chalk.default.hex("#EF4444"),
493
+ // Red
494
+ muted: import_chalk.default.hex("#71717A"),
495
+ // Gray
496
+ dim: import_chalk.default.hex("#52525B"),
497
+ // Dark gray
498
+ white: import_chalk.default.white,
499
+ cyan: import_chalk.default.cyan,
500
+ // CCplan colors
501
+ max: import_chalk.default.hex("#F59E0B"),
502
+ // Gold
503
+ pro: import_chalk.default.hex("#3B82F6"),
504
+ // Blue
505
+ team: import_chalk.default.hex("#8B5CF6"),
506
+ // Purple
507
+ free: import_chalk.default.hex("#6B7280")
508
+ // Gray
509
+ };
510
+ LOGO = `
511
+ ${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")}
512
+ ${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")}
513
+ ${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")}
514
+ ${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")}
515
+ ${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")}
516
+ ${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")}
517
+ `;
518
+ LOGO_COMPACT = `
519
+ ${colors.primary("CC")}${colors.secondary("gather")} ${colors.muted("- Where Claude Code Developers Gather")}
520
+ `;
521
+ TAGLINE = colors.muted(" Where Claude Code Developers Gather");
522
+ SLOGAN = colors.dim(" Gather. Compete. Rise.");
523
+ box = {
524
+ topLeft: "\u250C",
525
+ topRight: "\u2510",
526
+ bottomLeft: "\u2514",
527
+ bottomRight: "\u2518",
528
+ horizontal: "\u2500",
529
+ vertical: "\u2502",
530
+ leftT: "\u251C",
531
+ rightT: "\u2524"
532
+ };
413
533
  }
414
534
  });
415
535
 
416
- // src/commands/auth.ts
417
- var auth_exports = {};
418
- __export(auth_exports, {
419
- auth: () => auth
420
- });
421
- async function auth(options) {
422
- const config = getConfig();
423
- console.log(import_chalk5.default.bold("\n\u{1F510} CCgather Authentication\n"));
424
- const existingToken = config.get("apiToken");
425
- if (existingToken) {
426
- const { overwrite } = await import_inquirer2.default.prompt([
427
- {
428
- type: "confirm",
429
- name: "overwrite",
430
- message: "You are already authenticated. Do you want to re-authenticate?",
431
- default: false
432
- }
433
- ]);
434
- if (!overwrite) {
435
- console.log(import_chalk5.default.gray("Authentication cancelled."));
436
- return;
536
+ // src/commands/submit.ts
537
+ function ccgatherToUsageData(data) {
538
+ return {
539
+ totalTokens: data.usage.totalTokens,
540
+ totalCost: data.usage.totalCost,
541
+ inputTokens: data.usage.inputTokens,
542
+ outputTokens: data.usage.outputTokens,
543
+ cacheReadTokens: data.usage.cacheReadTokens,
544
+ cacheWriteTokens: data.usage.cacheWriteTokens,
545
+ daysTracked: data.stats.daysTracked,
546
+ ccplan: data.account?.ccplan || null,
547
+ rateLimitTier: data.account?.rateLimitTier || null
548
+ };
549
+ }
550
+ function findCcJson() {
551
+ const possiblePaths = [
552
+ path3.join(process.cwd(), "cc.json"),
553
+ path3.join(os3.homedir(), "cc.json"),
554
+ path3.join(os3.homedir(), ".claude", "cc.json")
555
+ ];
556
+ for (const p of possiblePaths) {
557
+ if (fs3.existsSync(p)) {
558
+ return p;
437
559
  }
438
560
  }
439
- if (options.token) {
440
- await authenticateWithToken(options.token);
441
- return;
561
+ return null;
562
+ }
563
+ function parseCcJson(filePath) {
564
+ try {
565
+ const content = fs3.readFileSync(filePath, "utf-8");
566
+ const data = JSON.parse(content);
567
+ return {
568
+ totalTokens: data.totalTokens || data.total_tokens || 0,
569
+ totalCost: data.totalCost || data.total_cost || data.costUSD || 0,
570
+ inputTokens: data.inputTokens || data.input_tokens || 0,
571
+ outputTokens: data.outputTokens || data.output_tokens || 0,
572
+ cacheReadTokens: data.cacheReadTokens || data.cache_read_tokens || 0,
573
+ cacheWriteTokens: data.cacheWriteTokens || data.cache_write_tokens || 0,
574
+ daysTracked: data.daysTracked || data.days_tracked || calculateDaysTracked(data)
575
+ };
576
+ } catch {
577
+ return null;
578
+ }
579
+ }
580
+ function calculateDaysTracked(data) {
581
+ if (data.dailyStats && Array.isArray(data.dailyStats)) {
582
+ return data.dailyStats.length;
442
583
  }
584
+ if (data.daily && typeof data.daily === "object") {
585
+ return Object.keys(data.daily).length;
586
+ }
587
+ return 1;
588
+ }
589
+ async function submitToServer(data) {
443
590
  const apiUrl = getApiUrl();
444
- const spinner = (0, import_ora7.default)("Initializing authentication...").start();
591
+ const config = getConfig();
592
+ const apiToken = config.get("apiToken");
593
+ if (!apiToken) {
594
+ return { success: false, error: "Not authenticated. Please run 'ccgather auth' first." };
595
+ }
445
596
  try {
446
- const response = await fetch(`${apiUrl}/cli/auth/device`, {
447
- method: "POST"
597
+ const response = await fetch(`${apiUrl}/cli/submit`, {
598
+ method: "POST",
599
+ headers: {
600
+ "Content-Type": "application/json",
601
+ Authorization: `Bearer ${apiToken}`
602
+ },
603
+ body: JSON.stringify({
604
+ totalTokens: data.totalTokens,
605
+ totalSpent: data.totalCost,
606
+ inputTokens: data.inputTokens,
607
+ outputTokens: data.outputTokens,
608
+ cacheReadTokens: data.cacheReadTokens,
609
+ cacheWriteTokens: data.cacheWriteTokens,
610
+ daysTracked: data.daysTracked,
611
+ ccplan: data.ccplan,
612
+ rateLimitTier: data.rateLimitTier,
613
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
614
+ })
448
615
  });
449
616
  if (!response.ok) {
450
- spinner.fail(import_chalk5.default.red("Failed to initialize authentication"));
451
- console.log(import_chalk5.default.red("\nPlease check your internet connection and try again."));
452
- process.exit(1);
617
+ const errorData = await response.json().catch(() => ({}));
618
+ return { success: false, error: errorData.error || `HTTP ${response.status}` };
453
619
  }
454
- const deviceData = await response.json();
455
- spinner.stop();
456
- console.log(import_chalk5.default.gray(" Opening browser for authentication...\n"));
457
- console.log(import_chalk5.default.gray(" If browser doesn't open, visit:"));
458
- console.log(` \u{1F517} ${import_chalk5.default.cyan.underline(deviceData.verification_uri_complete)}`);
459
- console.log();
460
- try {
461
- await (0, import_open.default)(deviceData.verification_uri_complete);
462
- } catch {
463
- console.log(import_chalk5.default.yellow(" Could not open browser automatically."));
464
- console.log(import_chalk5.default.yellow(" Please open the URL above manually."));
465
- }
466
- const pollSpinner = (0, import_ora7.default)("Waiting for authorization...").start();
467
- const startTime = Date.now();
468
- const expiresAt = startTime + deviceData.expires_in * 1e3;
469
- const pollInterval = Math.max(deviceData.interval * 1e3, 5e3);
470
- while (Date.now() < expiresAt) {
471
- await sleep(pollInterval);
472
- try {
473
- const pollResponse = await fetch(
474
- `${apiUrl}/cli/auth/device/poll?device_code=${deviceData.device_code}`
475
- );
476
- const pollData = await pollResponse.json();
477
- if (pollData.status === "authorized" && pollData.token) {
478
- pollSpinner.succeed(import_chalk5.default.green("Authentication successful!"));
479
- config.set("apiToken", pollData.token);
480
- config.set("userId", pollData.userId);
481
- config.set("username", pollData.username);
482
- console.log(import_chalk5.default.gray(`
483
- Welcome, ${import_chalk5.default.white(pollData.username)}!`));
484
- console.log(import_chalk5.default.gray("\nYou can now submit your usage data:"));
485
- console.log(import_chalk5.default.cyan(" npx ccgather submit\n"));
486
- return;
487
- }
488
- if (pollData.status === "expired" || pollData.status === "used") {
489
- pollSpinner.fail(import_chalk5.default.red("Authentication expired or already used"));
490
- console.log(import_chalk5.default.gray('\nPlease run "ccgather auth" to try again.\n'));
491
- process.exit(1);
492
- }
493
- const remaining = Math.ceil((expiresAt - Date.now()) / 1e3);
494
- pollSpinner.text = `Waiting for authorization... (${remaining}s remaining)`;
495
- } catch {
496
- }
497
- }
498
- pollSpinner.fail(import_chalk5.default.red("Authentication timed out"));
499
- console.log(import_chalk5.default.gray('\nPlease run "ccgather auth" to try again.\n'));
500
- process.exit(1);
501
- } catch (error2) {
502
- spinner.fail(import_chalk5.default.red("Authentication failed"));
503
- console.log(import_chalk5.default.red(`
504
- Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`));
505
- process.exit(1);
620
+ const result = await response.json();
621
+ return { success: true, profileUrl: result.profileUrl, rank: result.rank };
622
+ } catch (err) {
623
+ return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
506
624
  }
507
625
  }
508
- async function authenticateWithToken(token) {
509
- const config = getConfig();
510
- const apiUrl = getApiUrl();
511
- const spinner = (0, import_ora7.default)("Verifying token...").start();
512
- try {
513
- const response = await fetch(`${apiUrl}/cli/verify`, {
514
- method: "POST",
515
- headers: {
516
- "Content-Type": "application/json",
517
- Authorization: `Bearer ${token}`
518
- }
519
- });
520
- if (!response.ok) {
521
- spinner.fail(import_chalk5.default.red("Authentication failed"));
522
- const errorData = await response.json().catch(() => ({}));
523
- console.log(import_chalk5.default.red(`Error: ${errorData.error || "Invalid token"}`));
524
- console.log(import_chalk5.default.gray("\nMake sure your token is correct and try again."));
525
- process.exit(1);
526
- }
527
- const data = await response.json();
528
- config.set("apiToken", token);
529
- config.set("userId", data.userId);
530
- config.set("username", data.username);
531
- spinner.succeed(import_chalk5.default.green("Authentication successful!"));
532
- console.log(import_chalk5.default.gray(`
533
- Welcome, ${import_chalk5.default.white(data.username)}!`));
534
- console.log(import_chalk5.default.gray("\nNext step: Submit your usage data:"));
535
- console.log(import_chalk5.default.cyan(" npx ccgather submit\n"));
536
- } catch (error2) {
537
- spinner.fail(import_chalk5.default.red("Authentication failed"));
538
- console.log(import_chalk5.default.red(`
539
- Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`));
626
+ async function submit(options) {
627
+ printCompactHeader("1.2.1");
628
+ console.log(header("Submit Usage Data", "\u{1F4E4}"));
629
+ if (!isAuthenticated()) {
630
+ console.log(`
631
+ ${error("Not authenticated.")}`);
632
+ console.log(` ${colors.muted("Please run:")} ${colors.white("npx ccgather auth")}
633
+ `);
540
634
  process.exit(1);
541
635
  }
542
- }
543
- function sleep(ms) {
544
- return new Promise((resolve) => setTimeout(resolve, ms));
545
- }
546
- var import_chalk5, import_ora7, import_inquirer2, import_open;
547
- var init_auth = __esm({
548
- "src/commands/auth.ts"() {
549
- "use strict";
550
- import_chalk5 = __toESM(require("chalk"));
551
- import_ora7 = __toESM(require("ora"));
552
- import_inquirer2 = __toESM(require("inquirer"));
553
- import_open = __toESM(require("open"));
554
- init_config();
555
- }
556
- });
557
-
558
- // src/index.ts
559
- var import_commander = require("commander");
560
- var import_inquirer3 = __toESM(require("inquirer"));
561
- var import_chalk6 = __toESM(require("chalk"));
562
- var import_update_notifier = __toESM(require("update-notifier"));
563
-
564
- // src/commands/submit.ts
565
- var import_ora = __toESM(require("ora"));
566
- var fs3 = __toESM(require("fs"));
567
- var path3 = __toESM(require("path"));
568
- var os3 = __toESM(require("os"));
569
- init_config();
570
-
571
- // src/lib/ccgather-json.ts
572
- var fs2 = __toESM(require("fs"));
573
- var path2 = __toESM(require("path"));
574
- var os2 = __toESM(require("os"));
575
-
576
- // src/lib/credentials.ts
577
- var fs = __toESM(require("fs"));
578
- var path = __toESM(require("path"));
579
- var os = __toESM(require("os"));
580
- function getCredentialsPath() {
581
- return path.join(os.homedir(), ".claude", ".credentials.json");
582
- }
583
- function mapSubscriptionToCCPlan(subscriptionType) {
584
- if (!subscriptionType) {
585
- return "free";
586
- }
587
- const type = subscriptionType.toLowerCase();
588
- if (type === "max" || type.includes("max")) {
589
- return "max";
590
- }
591
- if (type === "pro") {
592
- return "pro";
593
- }
594
- if (type === "free") {
595
- return "free";
596
- }
597
- return type;
598
- }
599
- function readCredentials() {
600
- const credentialsPath = getCredentialsPath();
601
- const defaultData = {
602
- ccplan: null,
603
- rateLimitTier: null
604
- };
605
- if (!fs.existsSync(credentialsPath)) {
606
- return defaultData;
607
- }
608
- try {
609
- const content = fs.readFileSync(credentialsPath, "utf-8");
610
- const credentials = JSON.parse(content);
611
- const oauthData = credentials.claudeAiOauth;
612
- if (!oauthData) {
613
- return defaultData;
614
- }
615
- const ccplan = mapSubscriptionToCCPlan(oauthData.subscriptionType);
616
- const rateLimitTier = oauthData.rateLimitTier || null;
617
- return {
618
- ccplan,
619
- rateLimitTier
620
- };
621
- } catch (error2) {
622
- return defaultData;
636
+ const config = getConfig();
637
+ const username = config.get("username");
638
+ if (username) {
639
+ console.log(`
640
+ ${colors.muted("Logged in as:")} ${colors.white(username)}`);
623
641
  }
624
- }
625
-
626
- // src/lib/ccgather-json.ts
627
- var CCGATHER_JSON_VERSION = "1.2.0";
628
- function extractProjectName(filePath) {
629
- const parts = filePath.split(/[/\\]/);
630
- const projectsIndex = parts.findIndex((p) => p === "projects");
631
- if (projectsIndex >= 0 && parts[projectsIndex + 1]) {
632
- try {
633
- const encoded = parts[projectsIndex + 1];
634
- const decoded = decodeURIComponent(encoded);
635
- const pathParts = decoded.split(/[/\\]/);
636
- return pathParts[pathParts.length - 1] || decoded;
637
- } catch {
638
- return parts[projectsIndex + 1];
642
+ let usageData = null;
643
+ let dataSource = "";
644
+ const ccgatherData = readCCGatherJson();
645
+ if (ccgatherData) {
646
+ usageData = ccgatherToUsageData(ccgatherData);
647
+ dataSource = "ccgather.json";
648
+ console.log(`
649
+ ${success(`Found ${dataSource}`)}`);
650
+ console.log(
651
+ ` ${colors.dim(`Last scanned: ${new Date(ccgatherData.lastScanned).toLocaleString()}`)}`
652
+ );
653
+ if (usageData.ccplan) {
654
+ console.log(` ${colors.dim("CCplan:")} ${colors.primary(usageData.ccplan.toUpperCase())}`);
639
655
  }
640
656
  }
641
- return "unknown";
642
- }
643
- function getCCGatherJsonPath() {
644
- return path2.join(os2.homedir(), ".claude", "ccgather.json");
645
- }
646
- function getClaudeProjectsDir() {
647
- return path2.join(os2.homedir(), ".claude", "projects");
648
- }
649
- function findJsonlFiles(dir) {
650
- const files = [];
651
- try {
652
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
653
- for (const entry of entries) {
654
- const fullPath = path2.join(dir, entry.name);
655
- if (entry.isDirectory()) {
656
- files.push(...findJsonlFiles(fullPath));
657
- } else if (entry.name.endsWith(".jsonl")) {
658
- files.push(fullPath);
657
+ if (!usageData) {
658
+ const ccJsonPath = findCcJson();
659
+ if (ccJsonPath) {
660
+ const inquirer4 = await import("inquirer");
661
+ const { useCcJson } = await inquirer4.default.prompt([
662
+ {
663
+ type: "confirm",
664
+ name: "useCcJson",
665
+ message: "Found existing cc.json. Use this file?",
666
+ default: true
667
+ }
668
+ ]);
669
+ if (useCcJson) {
670
+ usageData = parseCcJson(ccJsonPath);
671
+ dataSource = "cc.json";
659
672
  }
660
673
  }
661
- } catch {
662
674
  }
663
- return files;
664
- }
665
- function estimateCost(model, inputTokens, outputTokens) {
666
- const pricing = {
667
- "claude-opus-4": { input: 15, output: 75 },
668
- "claude-sonnet-4": { input: 3, output: 15 },
669
- "claude-haiku": { input: 0.25, output: 1.25 },
670
- default: { input: 3, output: 15 }
671
- };
672
- let modelKey = "default";
673
- for (const key of Object.keys(pricing)) {
674
- if (model.includes(key.replace("claude-", ""))) {
675
- modelKey = key;
676
- break;
675
+ if (!usageData) {
676
+ const parseSpinner = (0, import_ora.default)({
677
+ text: "Scanning Claude Code usage data...",
678
+ color: "cyan"
679
+ }).start();
680
+ const scannedData = scanAndSave();
681
+ parseSpinner.stop();
682
+ if (scannedData) {
683
+ usageData = ccgatherToUsageData(scannedData);
684
+ dataSource = "Claude Code logs";
685
+ console.log(`
686
+ ${success("Scanned and saved to ccgather.json")}`);
687
+ console.log(` ${colors.dim(`Path: ${getCCGatherJsonPath()}`)}`);
677
688
  }
678
689
  }
679
- const price = pricing[modelKey];
680
- const inputCost = inputTokens / 1e6 * price.input;
681
- const outputCost = outputTokens / 1e6 * price.output;
682
- return Math.round((inputCost + outputCost) * 100) / 100;
683
- }
684
- function scanUsageData(options = {}) {
685
- const projectsDir = getClaudeProjectsDir();
686
- if (!fs2.existsSync(projectsDir)) {
687
- return null;
690
+ if (!usageData) {
691
+ console.log(`
692
+ ${error("No usage data found.")}`);
693
+ console.log(` ${colors.muted("Make sure you have used Claude Code.")}`);
694
+ console.log(` ${colors.muted("Run:")} ${colors.white("npx ccgather scan")}
695
+ `);
696
+ process.exit(1);
688
697
  }
689
- const days = options.days ?? 30;
690
- let cutoffDate = null;
691
- if (days > 0) {
692
- const cutoff = /* @__PURE__ */ new Date();
693
- cutoff.setDate(cutoff.getDate() - days);
694
- cutoff.setHours(0, 0, 0, 0);
695
- cutoffDate = cutoff.toISOString();
698
+ if (dataSource && dataSource !== "Claude Code logs") {
699
+ console.log(`
700
+ ${success(`Using ${dataSource}`)}`);
696
701
  }
697
- let totalInputTokens = 0;
698
- let totalOutputTokens = 0;
699
- let totalCacheRead = 0;
700
- let totalCacheWrite = 0;
701
- let totalCost = 0;
702
- let sessionsCount = 0;
703
- const dates = /* @__PURE__ */ new Set();
704
- const models = {};
705
- const projects = {};
706
- const dailyData = {};
707
- let firstTimestamp = null;
708
- let lastTimestamp = null;
709
- const jsonlFiles = findJsonlFiles(projectsDir);
710
- sessionsCount = jsonlFiles.length;
711
- for (const filePath of jsonlFiles) {
712
- const projectName = extractProjectName(filePath);
713
- if (!projects[projectName]) {
714
- projects[projectName] = {
715
- tokens: 0,
716
- cost: 0,
717
- sessions: 0,
718
- models: {}
719
- };
720
- }
721
- projects[projectName].sessions++;
722
- try {
723
- const content = fs2.readFileSync(filePath, "utf-8");
724
- const lines = content.split("\n").filter((line) => line.trim());
725
- for (const line of lines) {
726
- try {
727
- const event = JSON.parse(line);
728
- if (event.type === "assistant" && event.message?.usage) {
729
- if (cutoffDate && event.timestamp && event.timestamp < cutoffDate) {
730
- continue;
731
- }
732
- const usage = event.message.usage;
733
- const model = event.message.model || "unknown";
734
- const inputTokens = usage.input_tokens || 0;
735
- const outputTokens = usage.output_tokens || 0;
736
- totalInputTokens += inputTokens;
737
- totalOutputTokens += outputTokens;
738
- totalCacheRead += usage.cache_read_input_tokens || 0;
739
- totalCacheWrite += usage.cache_creation_input_tokens || 0;
740
- const messageCost = estimateCost(model, inputTokens, outputTokens);
741
- totalCost += messageCost;
742
- const totalModelTokens = inputTokens + outputTokens;
743
- models[model] = (models[model] || 0) + totalModelTokens;
744
- projects[projectName].tokens += totalModelTokens;
745
- projects[projectName].cost += messageCost;
746
- projects[projectName].models[model] = (projects[projectName].models[model] || 0) + totalModelTokens;
747
- if (event.timestamp) {
748
- const date = new Date(event.timestamp).toISOString().split("T")[0];
749
- dates.add(date);
750
- if (!dailyData[date]) {
751
- dailyData[date] = {
752
- tokens: 0,
753
- cost: 0,
754
- inputTokens: 0,
755
- outputTokens: 0,
756
- sessions: /* @__PURE__ */ new Set(),
757
- models: {}
758
- };
759
- }
760
- dailyData[date].tokens += totalModelTokens;
761
- dailyData[date].cost += messageCost;
762
- dailyData[date].inputTokens += inputTokens;
763
- dailyData[date].outputTokens += outputTokens;
764
- dailyData[date].sessions.add(filePath);
765
- dailyData[date].models[model] = (dailyData[date].models[model] || 0) + totalModelTokens;
766
- if (!firstTimestamp || event.timestamp < firstTimestamp) {
767
- firstTimestamp = event.timestamp;
768
- }
769
- if (!lastTimestamp || event.timestamp > lastTimestamp) {
770
- lastTimestamp = event.timestamp;
771
- }
772
- }
773
- }
774
- } catch {
775
- }
702
+ if (!usageData.ccplan) {
703
+ const inquirer4 = await import("inquirer");
704
+ const { selectedCCplan } = await inquirer4.default.prompt([
705
+ {
706
+ type: "list",
707
+ name: "selectedCCplan",
708
+ message: colors.muted("Select your Claude plan:"),
709
+ choices: [
710
+ { name: "\u{1F680} Max", value: "max" },
711
+ { name: "\u26A1 Pro", value: "pro" },
712
+ { name: "\u26AA Free", value: "free" },
713
+ { name: "\u{1F465} Team / Enterprise", value: "team" },
714
+ { name: "\u23ED\uFE0F Skip", value: null }
715
+ ],
716
+ default: "free"
776
717
  }
777
- } catch {
778
- }
779
- }
780
- const totalTokens = totalInputTokens + totalOutputTokens;
781
- if (totalTokens === 0) {
782
- return null;
718
+ ]);
719
+ usageData.ccplan = selectedCCplan;
783
720
  }
784
- for (const projectName of Object.keys(projects)) {
785
- projects[projectName].cost = Math.round(projects[projectName].cost * 100) / 100;
721
+ console.log();
722
+ const summaryLines = [
723
+ `${colors.muted("Total Cost")} ${colors.success(formatCost(usageData.totalCost))}`,
724
+ `${colors.muted("Total Tokens")} ${colors.primary(formatNumber(usageData.totalTokens))}`,
725
+ `${colors.muted("Days Tracked")} ${colors.warning(usageData.daysTracked.toString())}`
726
+ ];
727
+ if (usageData.ccplan) {
728
+ summaryLines.push(
729
+ `${colors.muted("CCplan")} ${colors.cyan(usageData.ccplan.toUpperCase())}`
730
+ );
786
731
  }
787
- const dailyUsage = Object.entries(dailyData).map(([date, data]) => ({
788
- date,
789
- tokens: data.tokens,
790
- cost: Math.round(data.cost * 100) / 100,
791
- inputTokens: data.inputTokens,
792
- outputTokens: data.outputTokens,
793
- sessions: data.sessions.size,
794
- models: data.models
795
- })).sort((a, b) => a.date.localeCompare(b.date));
796
- const credentials = readCredentials();
797
- return {
798
- version: CCGATHER_JSON_VERSION,
799
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
800
- lastScanned: (/* @__PURE__ */ new Date()).toISOString(),
801
- usage: {
802
- totalTokens,
803
- totalCost: Math.round(totalCost * 100) / 100,
804
- inputTokens: totalInputTokens,
805
- outputTokens: totalOutputTokens,
806
- cacheReadTokens: totalCacheRead,
807
- cacheWriteTokens: totalCacheWrite
808
- },
809
- stats: {
810
- daysTracked: dates.size,
811
- sessionsCount,
812
- firstUsed: firstTimestamp ? new Date(firstTimestamp).toISOString().split("T")[0] : null,
813
- lastUsed: lastTimestamp ? new Date(lastTimestamp).toISOString().split("T")[0] : null
814
- },
815
- models,
816
- projects,
817
- dailyUsage,
818
- account: {
819
- ccplan: credentials.ccplan,
820
- rateLimitTier: credentials.rateLimitTier
732
+ console.log(createBox(summaryLines));
733
+ console.log();
734
+ if (!options.yes) {
735
+ const inquirer4 = await import("inquirer");
736
+ const { confirmSubmit } = await inquirer4.default.prompt([
737
+ {
738
+ type: "confirm",
739
+ name: "confirmSubmit",
740
+ message: "Submit to CCgather leaderboard?",
741
+ default: true
742
+ }
743
+ ]);
744
+ if (!confirmSubmit) {
745
+ console.log(`
746
+ ${colors.muted("Submission cancelled.")}
747
+ `);
748
+ return;
821
749
  }
822
- };
823
- }
824
- function readCCGatherJson() {
825
- const jsonPath = getCCGatherJsonPath();
826
- if (!fs2.existsSync(jsonPath)) {
827
- return null;
828
750
  }
829
- try {
830
- const content = fs2.readFileSync(jsonPath, "utf-8");
831
- return JSON.parse(content);
832
- } catch {
833
- return null;
834
- }
835
- }
836
- function writeCCGatherJson(data) {
837
- const jsonPath = getCCGatherJsonPath();
838
- const claudeDir = path2.dirname(jsonPath);
839
- if (!fs2.existsSync(claudeDir)) {
840
- fs2.mkdirSync(claudeDir, { recursive: true });
751
+ const submitSpinner = (0, import_ora.default)({
752
+ text: "Submitting to CCgather...",
753
+ color: "cyan"
754
+ }).start();
755
+ const result = await submitToServer(usageData);
756
+ if (result.success) {
757
+ submitSpinner.succeed(colors.success("Successfully submitted to CCgather!"));
758
+ console.log();
759
+ const successLines = [
760
+ `${colors.success("\u2713")} ${colors.white.bold("Submission Complete!")}`,
761
+ "",
762
+ `${colors.muted("Profile:")} ${link(result.profileUrl || `https://ccgather.com/u/${username}`)}`
763
+ ];
764
+ if (result.rank) {
765
+ successLines.push(`${colors.muted("Rank:")} ${colors.warning(`#${result.rank}`)}`);
766
+ }
767
+ console.log(createBox(successLines));
768
+ console.log();
769
+ console.log(` ${colors.dim("View leaderboard:")} ${link("https://ccgather.com/leaderboard")}`);
770
+ console.log();
771
+ } else {
772
+ submitSpinner.fail(colors.error("Failed to submit"));
773
+ console.log(`
774
+ ${error(result.error || "Unknown error")}`);
775
+ if (result.error?.includes("auth") || result.error?.includes("token")) {
776
+ console.log(`
777
+ ${colors.muted("Try running:")} ${colors.white("npx ccgather auth")}`);
778
+ }
779
+ console.log();
780
+ process.exit(1);
841
781
  }
842
- fs2.writeFileSync(jsonPath, JSON.stringify(data, null, 2));
843
782
  }
844
- function scanAndSave(options = {}) {
845
- const data = scanUsageData(options);
846
- if (data) {
847
- writeCCGatherJson(data);
783
+ var import_ora, fs3, path3, os3;
784
+ var init_submit = __esm({
785
+ "src/commands/submit.ts"() {
786
+ "use strict";
787
+ import_ora = __toESM(require("ora"));
788
+ fs3 = __toESM(require("fs"));
789
+ path3 = __toESM(require("path"));
790
+ os3 = __toESM(require("os"));
791
+ init_config();
792
+ init_ccgather_json();
793
+ init_ui();
848
794
  }
849
- return data;
850
- }
795
+ });
851
796
 
852
- // src/lib/ui.ts
853
- var import_chalk = __toESM(require("chalk"));
854
- var colors = {
855
- primary: import_chalk.default.hex("#DA7756"),
856
- // Claude coral
857
- secondary: import_chalk.default.hex("#F7931E"),
858
- // Orange accent
859
- success: import_chalk.default.hex("#22C55E"),
860
- // Green
861
- warning: import_chalk.default.hex("#F59E0B"),
862
- // Amber
863
- error: import_chalk.default.hex("#EF4444"),
864
- // Red
865
- muted: import_chalk.default.hex("#71717A"),
866
- // Gray
867
- dim: import_chalk.default.hex("#52525B"),
868
- // Dark gray
869
- white: import_chalk.default.white,
870
- cyan: import_chalk.default.cyan,
871
- // CCplan colors
872
- max: import_chalk.default.hex("#F59E0B"),
873
- // Gold
874
- pro: import_chalk.default.hex("#3B82F6"),
875
- // Blue
876
- team: import_chalk.default.hex("#8B5CF6"),
877
- // Purple
878
- free: import_chalk.default.hex("#6B7280")
879
- // Gray
880
- };
881
- var LOGO = `
882
- ${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")}
883
- ${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")}
884
- ${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")}
885
- ${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")}
886
- ${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")}
887
- ${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")}
888
- `;
889
- var LOGO_COMPACT = `
890
- ${colors.primary("CC")}${colors.secondary("gather")} ${colors.muted("- Where Claude Code Developers Gather")}
891
- `;
892
- var TAGLINE = colors.muted(" Where Claude Code Developers Gather");
893
- var SLOGAN = colors.dim(" Gather. Compete. Rise.");
894
- function getVersionLine(version) {
895
- return colors.dim(` v${version} \u2022 ccgather.com`);
797
+ // src/lib/api.ts
798
+ async function fetchApi(endpoint, options = {}) {
799
+ const config = getConfig();
800
+ const apiToken = config.get("apiToken");
801
+ const apiUrl = getApiUrl();
802
+ if (!apiToken) {
803
+ return { success: false, error: "Not authenticated. Run: npx ccgather auth" };
804
+ }
805
+ try {
806
+ const response = await fetch(`${apiUrl}${endpoint}`, {
807
+ ...options,
808
+ headers: {
809
+ "Content-Type": "application/json",
810
+ Authorization: `Bearer ${apiToken}`,
811
+ ...options.headers
812
+ }
813
+ });
814
+ const data = await response.json();
815
+ if (!response.ok) {
816
+ return { success: false, error: data.error || `HTTP ${response.status}` };
817
+ }
818
+ return { success: true, data };
819
+ } catch (error2) {
820
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
821
+ return { success: false, error: message };
822
+ }
896
823
  }
897
- var box = {
898
- topLeft: "\u250C",
899
- topRight: "\u2510",
900
- bottomLeft: "\u2514",
901
- bottomRight: "\u2518",
902
- horizontal: "\u2500",
903
- vertical: "\u2502",
904
- leftT: "\u251C",
905
- rightT: "\u2524"
906
- };
907
- function createBox(lines, width = 47) {
908
- const paddedLines = lines.map((line) => {
909
- const visibleLength = stripAnsi(line).length;
910
- const padding = width - 2 - visibleLength;
911
- return `${box.vertical} ${line}${" ".repeat(Math.max(0, padding))} ${box.vertical}`;
824
+ async function syncUsage(payload) {
825
+ return fetchApi("/cli/sync", {
826
+ method: "POST",
827
+ body: JSON.stringify(payload)
912
828
  });
913
- const top = colors.dim(` ${box.topLeft}${box.horizontal.repeat(width)}${box.topRight}`);
914
- const bottom = colors.dim(` ${box.bottomLeft}${box.horizontal.repeat(width)}${box.bottomRight}`);
915
- return [top, ...paddedLines.map((l) => colors.dim(" ") + l), bottom].join("\n");
916
- }
917
- function stripAnsi(str) {
918
- return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
919
- }
920
- function header(title, icon = "") {
921
- const iconPart = icon ? `${icon} ` : "";
922
- return `
923
- ${colors.primary("\u2501".repeat(50))}
924
- ${iconPart}${colors.white.bold(title)}
925
- ${colors.primary("\u2501".repeat(50))}`;
926
829
  }
927
- function formatNumber(num) {
928
- if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
929
- if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
930
- if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
931
- return num.toLocaleString();
830
+ async function getStatus() {
831
+ return fetchApi("/cli/status");
932
832
  }
933
- function formatCost(cost) {
934
- return `$${cost.toLocaleString(void 0, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
833
+ var init_api = __esm({
834
+ "src/lib/api.ts"() {
835
+ "use strict";
836
+ init_config();
837
+ }
838
+ });
839
+
840
+ // src/commands/reset.ts
841
+ var reset_exports = {};
842
+ __export(reset_exports, {
843
+ reset: () => reset
844
+ });
845
+ function getClaudeSettingsDir() {
846
+ return path4.join(os4.homedir(), ".claude");
935
847
  }
936
- function getRankMedal(rank) {
937
- if (rank === 1) return "\u{1F947}";
938
- if (rank === 2) return "\u{1F948}";
939
- if (rank === 3) return "\u{1F949}";
940
- if (rank <= 10) return "\u{1F3C5}";
941
- if (rank <= 100) return "\u{1F396}\uFE0F";
942
- return "\u{1F4CA}";
848
+ function removeStopHook() {
849
+ const claudeDir = getClaudeSettingsDir();
850
+ const settingsPath = path4.join(claudeDir, "settings.json");
851
+ if (!fs4.existsSync(settingsPath)) {
852
+ return { success: true, message: "No settings file found" };
853
+ }
854
+ try {
855
+ const content = fs4.readFileSync(settingsPath, "utf-8");
856
+ const settings = JSON.parse(content);
857
+ if (settings.hooks && typeof settings.hooks === "object") {
858
+ const hooks = settings.hooks;
859
+ if (hooks.Stop && Array.isArray(hooks.Stop)) {
860
+ hooks.Stop = hooks.Stop.filter((hook) => {
861
+ if (typeof hook === "object" && hook !== null) {
862
+ const h = hook;
863
+ return typeof h.command !== "string" || !h.command.includes("ccgather");
864
+ }
865
+ return true;
866
+ });
867
+ if (hooks.Stop.length === 0) {
868
+ delete hooks.Stop;
869
+ }
870
+ }
871
+ }
872
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
873
+ return { success: true, message: "Hook removed" };
874
+ } catch (err) {
875
+ return {
876
+ success: false,
877
+ message: err instanceof Error ? err.message : "Unknown error"
878
+ };
879
+ }
943
880
  }
944
- function getCCplanBadge(ccplan) {
945
- if (!ccplan) return "";
946
- const badges = {
947
- max: `${colors.max("\u{1F680} MAX")}`,
948
- pro: `${colors.pro("\u26A1 PRO")}`,
949
- team: `${colors.team("\u{1F465} TEAM")}`,
950
- free: `${colors.free("\u26AA FREE")}`
951
- };
952
- return badges[ccplan.toLowerCase()] || "";
881
+ function removeSyncScript() {
882
+ const claudeDir = getClaudeSettingsDir();
883
+ const scriptPath = path4.join(claudeDir, "ccgather-sync.js");
884
+ if (fs4.existsSync(scriptPath)) {
885
+ fs4.unlinkSync(scriptPath);
886
+ }
953
887
  }
954
- function getLevelInfo(tokens) {
955
- const levels = [
956
- { min: 0, level: 1, name: "Novice", icon: "\u{1F331}", color: colors.dim },
957
- { min: 1e5, level: 2, name: "Apprentice", icon: "\u{1F4DA}", color: colors.muted },
958
- { min: 5e5, level: 3, name: "Journeyman", icon: "\u26A1", color: colors.cyan },
959
- { min: 1e6, level: 4, name: "Expert", icon: "\u{1F48E}", color: colors.pro },
960
- { min: 5e6, level: 5, name: "Master", icon: "\u{1F525}", color: colors.warning },
961
- { min: 1e7, level: 6, name: "Grandmaster", icon: "\u{1F451}", color: colors.max },
962
- { min: 5e7, level: 7, name: "Legend", icon: "\u{1F31F}", color: colors.primary },
963
- { min: 1e8, level: 8, name: "Mythic", icon: "\u{1F3C6}", color: colors.secondary }
964
- ];
965
- for (let i = levels.length - 1; i >= 0; i--) {
966
- if (tokens >= levels[i].min) {
967
- return levels[i];
888
+ async function reset() {
889
+ const config = getConfig();
890
+ console.log(import_chalk2.default.bold("\n\u{1F504} CCgather Reset\n"));
891
+ if (!config.get("apiToken")) {
892
+ console.log(import_chalk2.default.yellow("CCgather is not configured."));
893
+ return;
894
+ }
895
+ const { confirmReset } = await import_inquirer.default.prompt([
896
+ {
897
+ type: "confirm",
898
+ name: "confirmReset",
899
+ message: "This will remove the CCgather hook and local configuration. Continue?",
900
+ default: false
968
901
  }
902
+ ]);
903
+ if (!confirmReset) {
904
+ console.log(import_chalk2.default.gray("Reset cancelled."));
905
+ return;
969
906
  }
970
- return levels[0];
971
- }
972
- function createWelcomeBox(user) {
973
- const levelInfo = user.level && user.levelName && user.levelIcon ? `${user.levelIcon} Level ${user.level} \u2022 ${user.levelName}` : "";
974
- const ccplanBadge = user.ccplan ? getCCplanBadge(user.ccplan) : "";
975
- const lines = [`\u{1F44B} ${colors.white.bold(`Welcome back, ${user.username}!`)}`];
976
- if (levelInfo || ccplanBadge) {
977
- lines.push(`${levelInfo}${ccplanBadge ? ` ${ccplanBadge}` : ""}`);
907
+ const hookSpinner = (0, import_ora3.default)("Removing Claude Code hook...").start();
908
+ const hookResult = removeStopHook();
909
+ if (hookResult.success) {
910
+ hookSpinner.succeed(import_chalk2.default.green("Hook removed"));
911
+ } else {
912
+ hookSpinner.warn(import_chalk2.default.yellow(`Could not remove hook: ${hookResult.message}`));
978
913
  }
979
- if (user.globalRank) {
980
- lines.push(`\u{1F30D} Global Rank: ${colors.primary(`#${user.globalRank}`)}`);
914
+ const scriptSpinner = (0, import_ora3.default)("Removing sync script...").start();
915
+ try {
916
+ removeSyncScript();
917
+ scriptSpinner.succeed(import_chalk2.default.green("Sync script removed"));
918
+ } catch {
919
+ scriptSpinner.warn(import_chalk2.default.yellow("Could not remove sync script"));
981
920
  }
982
- if (user.countryRank && user.countryCode) {
983
- const flag = countryCodeToFlag(user.countryCode);
984
- lines.push(`${flag} Country Rank: ${colors.primary(`#${user.countryRank}`)}`);
921
+ const { deleteAccount } = await import_inquirer.default.prompt([
922
+ {
923
+ type: "confirm",
924
+ name: "deleteAccount",
925
+ message: import_chalk2.default.red("Do you also want to delete your account from the leaderboard? (This cannot be undone)"),
926
+ default: false
927
+ }
928
+ ]);
929
+ if (deleteAccount) {
930
+ console.log(import_chalk2.default.yellow("\nAccount deletion is not yet implemented."));
931
+ console.log(import_chalk2.default.gray("Please contact support to delete your account."));
985
932
  }
986
- return createBox(lines);
987
- }
988
- function countryCodeToFlag(countryCode) {
989
- if (!countryCode || countryCode.length !== 2) return "\u{1F310}";
990
- const codePoints = countryCode.toUpperCase().split("").map((char) => 127462 + char.charCodeAt(0) - 65);
991
- return String.fromCodePoint(...codePoints);
992
- }
993
- function success(message) {
994
- return `${colors.success("\u2713")} ${message}`;
995
- }
996
- function error(message) {
997
- return `${colors.error("\u2717")} ${message}`;
998
- }
999
- function printHeader(version) {
1000
- console.log(LOGO);
1001
- console.log(TAGLINE);
1002
- console.log(SLOGAN);
1003
- console.log();
1004
- console.log(getVersionLine(version));
1005
- console.log();
1006
- }
1007
- function printCompactHeader(version) {
933
+ const configSpinner = (0, import_ora3.default)("Resetting local configuration...").start();
934
+ resetConfig();
935
+ configSpinner.succeed(import_chalk2.default.green("Local configuration reset"));
1008
936
  console.log();
1009
- console.log(LOGO_COMPACT);
1010
- console.log(colors.dim(` v${version}`));
937
+ console.log(import_chalk2.default.green.bold("\u2705 Reset complete!"));
938
+ console.log();
939
+ console.log(import_chalk2.default.gray("Your usage will no longer be tracked."));
940
+ console.log(import_chalk2.default.gray("Run `npx ccgather` to set up again."));
1011
941
  console.log();
1012
942
  }
1013
- function link(url) {
1014
- return colors.cyan.underline(url);
1015
- }
943
+ var import_chalk2, import_ora3, fs4, path4, os4, import_inquirer;
944
+ var init_reset = __esm({
945
+ "src/commands/reset.ts"() {
946
+ "use strict";
947
+ import_chalk2 = __toESM(require("chalk"));
948
+ import_ora3 = __toESM(require("ora"));
949
+ fs4 = __toESM(require("fs"));
950
+ path4 = __toESM(require("path"));
951
+ os4 = __toESM(require("os"));
952
+ import_inquirer = __toESM(require("inquirer"));
953
+ init_config();
954
+ }
955
+ });
1016
956
 
1017
- // src/commands/submit.ts
1018
- function ccgatherToUsageData(data) {
1019
- return {
1020
- totalTokens: data.usage.totalTokens,
1021
- totalCost: data.usage.totalCost,
1022
- inputTokens: data.usage.inputTokens,
1023
- outputTokens: data.usage.outputTokens,
1024
- cacheReadTokens: data.usage.cacheReadTokens,
1025
- cacheWriteTokens: data.usage.cacheWriteTokens,
1026
- daysTracked: data.stats.daysTracked,
1027
- ccplan: data.account?.ccplan || null,
1028
- rateLimitTier: data.account?.rateLimitTier || null
1029
- };
957
+ // src/lib/claude.ts
958
+ function getClaudeConfigDir() {
959
+ const platform3 = os6.platform();
960
+ if (platform3 === "win32") {
961
+ return path6.join(os6.homedir(), "AppData", "Roaming", "claude-code");
962
+ } else if (platform3 === "darwin") {
963
+ return path6.join(os6.homedir(), "Library", "Application Support", "claude-code");
964
+ } else {
965
+ return path6.join(os6.homedir(), ".config", "claude-code");
966
+ }
1030
967
  }
1031
- function findCcJson() {
1032
- const possiblePaths = [
1033
- path3.join(process.cwd(), "cc.json"),
1034
- path3.join(os3.homedir(), "cc.json"),
1035
- path3.join(os3.homedir(), ".claude", "cc.json")
968
+ function getUsageFilePaths() {
969
+ const configDir = getClaudeConfigDir();
970
+ return [
971
+ path6.join(configDir, "usage.json"),
972
+ path6.join(configDir, "stats.json"),
973
+ path6.join(configDir, "data", "usage.json"),
974
+ path6.join(os6.homedir(), ".claude", "usage.json"),
975
+ path6.join(os6.homedir(), ".claude-code", "usage.json")
1036
976
  ];
1037
- for (const p of possiblePaths) {
1038
- if (fs3.existsSync(p)) {
1039
- return p;
977
+ }
978
+ function readClaudeUsage() {
979
+ const possiblePaths = getUsageFilePaths();
980
+ for (const filePath of possiblePaths) {
981
+ try {
982
+ if (fs6.existsSync(filePath)) {
983
+ const content = fs6.readFileSync(filePath, "utf-8");
984
+ const data = JSON.parse(content);
985
+ if (data.usage) {
986
+ return {
987
+ totalTokens: data.usage.total_tokens || 0,
988
+ totalSpent: data.usage.total_cost || 0,
989
+ modelBreakdown: data.usage.by_model || {},
990
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
991
+ };
992
+ }
993
+ if (data.sessions && data.sessions.length > 0) {
994
+ const breakdown = {};
995
+ let totalTokens = 0;
996
+ let totalSpent = 0;
997
+ for (const session of data.sessions) {
998
+ totalTokens += session.tokens_used || 0;
999
+ totalSpent += session.cost || 0;
1000
+ breakdown[session.model] = (breakdown[session.model] || 0) + session.tokens_used;
1001
+ }
1002
+ return {
1003
+ totalTokens,
1004
+ totalSpent,
1005
+ modelBreakdown: breakdown,
1006
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1007
+ };
1008
+ }
1009
+ }
1010
+ } catch (error2) {
1040
1011
  }
1041
1012
  }
1042
1013
  return null;
1043
1014
  }
1044
- function parseCcJson(filePath) {
1045
- try {
1046
- const content = fs3.readFileSync(filePath, "utf-8");
1047
- const data = JSON.parse(content);
1048
- return {
1049
- totalTokens: data.totalTokens || data.total_tokens || 0,
1050
- totalCost: data.totalCost || data.total_cost || data.costUSD || 0,
1051
- inputTokens: data.inputTokens || data.input_tokens || 0,
1052
- outputTokens: data.outputTokens || data.output_tokens || 0,
1053
- cacheReadTokens: data.cacheReadTokens || data.cache_read_tokens || 0,
1054
- cacheWriteTokens: data.cacheWriteTokens || data.cache_write_tokens || 0,
1055
- daysTracked: data.daysTracked || data.days_tracked || calculateDaysTracked(data)
1056
- };
1057
- } catch {
1058
- return null;
1059
- }
1015
+ function isClaudeCodeInstalled() {
1016
+ const configDir = getClaudeConfigDir();
1017
+ return fs6.existsSync(configDir);
1060
1018
  }
1061
- function calculateDaysTracked(data) {
1062
- if (data.dailyStats && Array.isArray(data.dailyStats)) {
1063
- return data.dailyStats.length;
1064
- }
1065
- if (data.daily && typeof data.daily === "object") {
1066
- return Object.keys(data.daily).length;
1067
- }
1068
- return 1;
1019
+ function getMockUsageData() {
1020
+ return {
1021
+ totalTokens: Math.floor(Math.random() * 1e6) + 1e4,
1022
+ totalSpent: Math.random() * 50 + 5,
1023
+ modelBreakdown: {
1024
+ "claude-3-5-sonnet-20241022": Math.floor(Math.random() * 5e5),
1025
+ "claude-3-opus-20240229": Math.floor(Math.random() * 1e5),
1026
+ "claude-3-haiku-20240307": Math.floor(Math.random() * 2e5)
1027
+ },
1028
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
1029
+ };
1069
1030
  }
1070
- async function submitToServer(data) {
1071
- const apiUrl = getApiUrl();
1072
- const config = getConfig();
1073
- const apiToken = config.get("apiToken");
1074
- if (!apiToken) {
1075
- return { success: false, error: "Not authenticated. Please run 'ccgather auth' first." };
1076
- }
1077
- try {
1078
- const response = await fetch(`${apiUrl}/cli/submit`, {
1079
- method: "POST",
1080
- headers: {
1081
- "Content-Type": "application/json",
1082
- Authorization: `Bearer ${apiToken}`
1083
- },
1084
- body: JSON.stringify({
1085
- totalTokens: data.totalTokens,
1086
- totalSpent: data.totalCost,
1087
- inputTokens: data.inputTokens,
1088
- outputTokens: data.outputTokens,
1089
- cacheReadTokens: data.cacheReadTokens,
1090
- cacheWriteTokens: data.cacheWriteTokens,
1091
- daysTracked: data.daysTracked,
1092
- ccplan: data.ccplan,
1093
- rateLimitTier: data.rateLimitTier,
1094
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1095
- })
1096
- });
1097
- if (!response.ok) {
1098
- const errorData = await response.json().catch(() => ({}));
1099
- return { success: false, error: errorData.error || `HTTP ${response.status}` };
1100
- }
1101
- const result = await response.json();
1102
- return { success: true, profileUrl: result.profileUrl, rank: result.rank };
1103
- } catch (err) {
1104
- return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
1031
+ var fs6, path6, os6;
1032
+ var init_claude = __esm({
1033
+ "src/lib/claude.ts"() {
1034
+ "use strict";
1035
+ fs6 = __toESM(require("fs"));
1036
+ path6 = __toESM(require("path"));
1037
+ os6 = __toESM(require("os"));
1105
1038
  }
1039
+ });
1040
+
1041
+ // src/commands/sync.ts
1042
+ var sync_exports = {};
1043
+ __export(sync_exports, {
1044
+ sync: () => sync
1045
+ });
1046
+ function formatNumber2(num) {
1047
+ if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
1048
+ if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
1049
+ return num.toString();
1106
1050
  }
1107
- async function submit(options) {
1108
- printCompactHeader("1.2.1");
1109
- console.log(header("Submit Usage Data", "\u{1F4E4}"));
1051
+ async function sync(options) {
1052
+ const config = getConfig();
1053
+ console.log(import_chalk4.default.bold("\n\u{1F504} CCgather Sync\n"));
1110
1054
  if (!isAuthenticated()) {
1111
- console.log(`
1112
- ${error("Not authenticated.")}`);
1113
- console.log(` ${colors.muted("Please run:")} ${colors.white("npx ccgather auth")}
1114
- `);
1055
+ console.log(import_chalk4.default.red("Not authenticated."));
1056
+ console.log(import_chalk4.default.gray("Run: npx ccgather auth\n"));
1115
1057
  process.exit(1);
1116
1058
  }
1117
- const config = getConfig();
1118
- const username = config.get("username");
1119
- if (username) {
1120
- console.log(`
1121
- ${colors.muted("Logged in as:")} ${colors.white(username)}`);
1122
- }
1123
- let usageData = null;
1124
- let dataSource = "";
1125
- const ccgatherData = readCCGatherJson();
1126
- if (ccgatherData) {
1127
- usageData = ccgatherToUsageData(ccgatherData);
1128
- dataSource = "ccgather.json";
1129
- console.log(`
1130
- ${success(`Found ${dataSource}`)}`);
1059
+ if (!isClaudeCodeInstalled()) {
1060
+ console.log(import_chalk4.default.yellow("\u26A0\uFE0F Claude Code installation not detected."));
1131
1061
  console.log(
1132
- ` ${colors.dim(`Last scanned: ${new Date(ccgatherData.lastScanned).toLocaleString()}`)}`
1062
+ import_chalk4.default.gray("Make sure Claude Code is installed and has been used at least once.\n")
1133
1063
  );
1134
- if (usageData.ccplan) {
1135
- console.log(` ${colors.dim("CCplan:")} ${colors.primary(usageData.ccplan.toUpperCase())}`);
1136
- }
1137
- }
1138
- if (!usageData) {
1139
- const ccJsonPath = findCcJson();
1140
- if (ccJsonPath) {
1141
- const inquirer4 = await import("inquirer");
1142
- const { useCcJson } = await inquirer4.default.prompt([
1143
- {
1144
- type: "confirm",
1145
- name: "useCcJson",
1146
- message: "Found existing cc.json. Use this file?",
1147
- default: true
1148
- }
1149
- ]);
1150
- if (useCcJson) {
1151
- usageData = parseCcJson(ccJsonPath);
1152
- dataSource = "cc.json";
1153
- }
1064
+ if (process.env.CCGATHER_DEMO === "true") {
1065
+ console.log(import_chalk4.default.gray("Demo mode: Using mock data..."));
1066
+ } else {
1067
+ process.exit(1);
1154
1068
  }
1155
1069
  }
1156
- if (!usageData) {
1157
- const parseSpinner = (0, import_ora.default)({
1158
- text: "Scanning Claude Code usage data...",
1159
- color: "cyan"
1160
- }).start();
1161
- const scannedData = scanAndSave();
1162
- parseSpinner.stop();
1163
- if (scannedData) {
1164
- usageData = ccgatherToUsageData(scannedData);
1165
- dataSource = "Claude Code logs";
1166
- console.log(`
1167
- ${success("Scanned and saved to ccgather.json")}`);
1168
- console.log(` ${colors.dim(`Path: ${getCCGatherJsonPath()}`)}`);
1070
+ const lastSync = config.get("lastSync");
1071
+ if (lastSync && !options.force) {
1072
+ const lastSyncDate = new Date(lastSync);
1073
+ const minInterval = 5 * 60 * 1e3;
1074
+ const timeSinceSync = Date.now() - lastSyncDate.getTime();
1075
+ if (timeSinceSync < minInterval) {
1076
+ const remaining = Math.ceil((minInterval - timeSinceSync) / 1e3 / 60);
1077
+ console.log(import_chalk4.default.yellow(`\u23F3 Please wait ${remaining} minutes before syncing again.`));
1078
+ console.log(import_chalk4.default.gray("Use --force to override.\n"));
1079
+ process.exit(0);
1169
1080
  }
1170
1081
  }
1082
+ const spinner = (0, import_ora6.default)("Reading Claude Code usage data...").start();
1083
+ let usageData = readClaudeUsage();
1084
+ if (!usageData && process.env.CCGATHER_DEMO === "true") {
1085
+ usageData = getMockUsageData();
1086
+ }
1171
1087
  if (!usageData) {
1172
- console.log(`
1173
- ${error("No usage data found.")}`);
1174
- console.log(` ${colors.muted("Make sure you have used Claude Code.")}`);
1175
- console.log(` ${colors.muted("Run:")} ${colors.white("npx ccgather scan")}
1176
- `);
1088
+ spinner.fail(import_chalk4.default.red("Failed to read usage data"));
1089
+ console.log(import_chalk4.default.gray("\nPossible reasons:"));
1090
+ console.log(import_chalk4.default.gray(" - Claude Code has not been used yet"));
1091
+ console.log(import_chalk4.default.gray(" - Usage data file is missing or corrupted"));
1092
+ console.log(import_chalk4.default.gray(" - Insufficient permissions to read the file\n"));
1177
1093
  process.exit(1);
1178
1094
  }
1179
- if (dataSource && dataSource !== "Claude Code logs") {
1180
- console.log(`
1181
- ${success(`Using ${dataSource}`)}`);
1095
+ spinner.text = "Syncing to CCgather...";
1096
+ const result = await syncUsage({
1097
+ totalTokens: usageData.totalTokens,
1098
+ totalSpent: usageData.totalSpent,
1099
+ modelBreakdown: usageData.modelBreakdown,
1100
+ timestamp: usageData.lastUpdated
1101
+ });
1102
+ if (!result.success) {
1103
+ spinner.fail(import_chalk4.default.red("Sync failed"));
1104
+ console.log(import_chalk4.default.red(`Error: ${result.error}
1105
+ `));
1106
+ process.exit(1);
1182
1107
  }
1183
- if (!usageData.ccplan) {
1184
- const inquirer4 = await import("inquirer");
1185
- const { selectedCCplan } = await inquirer4.default.prompt([
1186
- {
1187
- type: "list",
1188
- name: "selectedCCplan",
1189
- message: colors.muted("Select your Claude plan:"),
1190
- choices: [
1191
- { name: "\u{1F680} Max", value: "max" },
1192
- { name: "\u26A1 Pro", value: "pro" },
1193
- { name: "\u26AA Free", value: "free" },
1194
- { name: "\u{1F465} Team / Enterprise", value: "team" },
1195
- { name: "\u23ED\uFE0F Skip", value: null }
1196
- ],
1197
- default: "free"
1198
- }
1199
- ]);
1200
- usageData.ccplan = selectedCCplan;
1108
+ config.set("lastSync", (/* @__PURE__ */ new Date()).toISOString());
1109
+ spinner.succeed(import_chalk4.default.green("Sync complete!"));
1110
+ console.log("\n" + import_chalk4.default.gray("\u2500".repeat(40)));
1111
+ console.log(import_chalk4.default.bold("\u{1F4CA} Your Stats"));
1112
+ console.log(import_chalk4.default.gray("\u2500".repeat(40)));
1113
+ console.log(` ${import_chalk4.default.gray("Tokens:")} ${import_chalk4.default.white(formatNumber2(usageData.totalTokens))}`);
1114
+ console.log(
1115
+ ` ${import_chalk4.default.gray("Spent:")} ${import_chalk4.default.green("$" + usageData.totalSpent.toFixed(2))}`
1116
+ );
1117
+ console.log(` ${import_chalk4.default.gray("Rank:")} ${import_chalk4.default.yellow("#" + result.data?.rank)}`);
1118
+ if (options.verbose) {
1119
+ console.log("\n" + import_chalk4.default.gray("Model Breakdown:"));
1120
+ for (const [model, tokens] of Object.entries(usageData.modelBreakdown)) {
1121
+ const shortModel = model.replace("claude-", "").replace(/-\d+$/, "");
1122
+ console.log(` ${import_chalk4.default.gray(shortModel + ":")} ${formatNumber2(tokens)}`);
1123
+ }
1201
1124
  }
1202
- console.log();
1203
- const summaryLines = [
1204
- `${colors.muted("Total Cost")} ${colors.success(formatCost(usageData.totalCost))}`,
1205
- `${colors.muted("Total Tokens")} ${colors.primary(formatNumber(usageData.totalTokens))}`,
1206
- `${colors.muted("Days Tracked")} ${colors.warning(usageData.daysTracked.toString())}`
1207
- ];
1208
- if (usageData.ccplan) {
1209
- summaryLines.push(
1210
- `${colors.muted("CCplan")} ${colors.cyan(usageData.ccplan.toUpperCase())}`
1211
- );
1125
+ console.log(import_chalk4.default.gray("\u2500".repeat(40)));
1126
+ console.log(import_chalk4.default.gray("\nView full leaderboard: https://ccgather.com/leaderboard\n"));
1127
+ }
1128
+ var import_chalk4, import_ora6;
1129
+ var init_sync = __esm({
1130
+ "src/commands/sync.ts"() {
1131
+ "use strict";
1132
+ import_chalk4 = __toESM(require("chalk"));
1133
+ import_ora6 = __toESM(require("ora"));
1134
+ init_config();
1135
+ init_api();
1136
+ init_claude();
1212
1137
  }
1213
- console.log(createBox(summaryLines));
1214
- console.log();
1215
- if (!options.yes) {
1216
- const inquirer4 = await import("inquirer");
1217
- const { confirmSubmit } = await inquirer4.default.prompt([
1138
+ });
1139
+
1140
+ // src/commands/auth.ts
1141
+ var auth_exports = {};
1142
+ __export(auth_exports, {
1143
+ auth: () => auth
1144
+ });
1145
+ async function auth(options) {
1146
+ const config = getConfig();
1147
+ console.log(import_chalk5.default.bold("\n\u{1F510} CCgather Authentication\n"));
1148
+ const existingToken = config.get("apiToken");
1149
+ if (existingToken) {
1150
+ const { overwrite } = await import_inquirer2.default.prompt([
1218
1151
  {
1219
1152
  type: "confirm",
1220
- name: "confirmSubmit",
1221
- message: "Submit to CCgather leaderboard?",
1222
- default: true
1153
+ name: "overwrite",
1154
+ message: "You are already authenticated. Do you want to re-authenticate?",
1155
+ default: false
1223
1156
  }
1224
1157
  ]);
1225
- if (!confirmSubmit) {
1226
- console.log(`
1227
- ${colors.muted("Submission cancelled.")}
1228
- `);
1158
+ if (!overwrite) {
1159
+ console.log(import_chalk5.default.gray("Authentication cancelled."));
1229
1160
  return;
1230
1161
  }
1231
1162
  }
1232
- const submitSpinner = (0, import_ora.default)({
1233
- text: "Submitting to CCgather...",
1234
- color: "cyan"
1235
- }).start();
1236
- const result = await submitToServer(usageData);
1237
- if (result.success) {
1238
- submitSpinner.succeed(colors.success("Successfully submitted to CCgather!"));
1239
- console.log();
1240
- const successLines = [
1241
- `${colors.success("\u2713")} ${colors.white.bold("Submission Complete!")}`,
1242
- "",
1243
- `${colors.muted("Profile:")} ${link(result.profileUrl || `https://ccgather.com/u/${username}`)}`
1244
- ];
1245
- if (result.rank) {
1246
- successLines.push(`${colors.muted("Rank:")} ${colors.warning(`#${result.rank}`)}`);
1163
+ if (options.token) {
1164
+ await authenticateWithToken(options.token);
1165
+ return;
1166
+ }
1167
+ const apiUrl = getApiUrl();
1168
+ const spinner = (0, import_ora7.default)("Initializing authentication...").start();
1169
+ try {
1170
+ const response = await fetch(`${apiUrl}/cli/auth/device`, {
1171
+ method: "POST"
1172
+ });
1173
+ if (!response.ok) {
1174
+ spinner.fail(import_chalk5.default.red("Failed to initialize authentication"));
1175
+ console.log(import_chalk5.default.red("\nPlease check your internet connection and try again."));
1176
+ process.exit(1);
1247
1177
  }
1248
- console.log(createBox(successLines));
1249
- console.log();
1250
- console.log(` ${colors.dim("View leaderboard:")} ${link("https://ccgather.com/leaderboard")}`);
1178
+ const deviceData = await response.json();
1179
+ spinner.stop();
1180
+ console.log(import_chalk5.default.gray(" Opening browser for authentication...\n"));
1181
+ console.log(import_chalk5.default.gray(" If browser doesn't open, visit:"));
1182
+ console.log(` \u{1F517} ${import_chalk5.default.cyan.underline(deviceData.verification_uri_complete)}`);
1251
1183
  console.log();
1252
- } else {
1253
- submitSpinner.fail(colors.error("Failed to submit"));
1254
- console.log(`
1255
- ${error(result.error || "Unknown error")}`);
1256
- if (result.error?.includes("auth") || result.error?.includes("token")) {
1257
- console.log(`
1258
- ${colors.muted("Try running:")} ${colors.white("npx ccgather auth")}`);
1184
+ try {
1185
+ await (0, import_open.default)(deviceData.verification_uri_complete);
1186
+ } catch {
1187
+ console.log(import_chalk5.default.yellow(" Could not open browser automatically."));
1188
+ console.log(import_chalk5.default.yellow(" Please open the URL above manually."));
1259
1189
  }
1260
- console.log();
1190
+ const pollSpinner = (0, import_ora7.default)("Waiting for authorization...").start();
1191
+ const startTime = Date.now();
1192
+ const expiresAt = startTime + deviceData.expires_in * 1e3;
1193
+ const pollInterval = Math.max(deviceData.interval * 1e3, 5e3);
1194
+ while (Date.now() < expiresAt) {
1195
+ await sleep(pollInterval);
1196
+ try {
1197
+ const pollResponse = await fetch(
1198
+ `${apiUrl}/cli/auth/device/poll?device_code=${deviceData.device_code}`
1199
+ );
1200
+ const pollData = await pollResponse.json();
1201
+ if (pollData.status === "authorized" && pollData.token) {
1202
+ pollSpinner.succeed(import_chalk5.default.green("Authentication successful!"));
1203
+ config.set("apiToken", pollData.token);
1204
+ config.set("userId", pollData.userId);
1205
+ config.set("username", pollData.username);
1206
+ console.log(import_chalk5.default.gray(`
1207
+ Welcome, ${import_chalk5.default.white(pollData.username)}!
1208
+ `));
1209
+ console.log(import_chalk5.default.bold("\u{1F4CA} Submitting your usage data...\n"));
1210
+ await submit({ yes: true });
1211
+ return;
1212
+ }
1213
+ if (pollData.status === "expired" || pollData.status === "used") {
1214
+ pollSpinner.fail(import_chalk5.default.red("Authentication expired or already used"));
1215
+ console.log(import_chalk5.default.gray('\nPlease run "ccgather auth" to try again.\n'));
1216
+ process.exit(1);
1217
+ }
1218
+ const remaining = Math.ceil((expiresAt - Date.now()) / 1e3);
1219
+ pollSpinner.text = `Waiting for authorization... (${remaining}s remaining)`;
1220
+ } catch {
1221
+ }
1222
+ }
1223
+ pollSpinner.fail(import_chalk5.default.red("Authentication timed out"));
1224
+ console.log(import_chalk5.default.gray('\nPlease run "ccgather auth" to try again.\n'));
1225
+ process.exit(1);
1226
+ } catch (error2) {
1227
+ spinner.fail(import_chalk5.default.red("Authentication failed"));
1228
+ console.log(import_chalk5.default.red(`
1229
+ Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`));
1230
+ process.exit(1);
1231
+ }
1232
+ }
1233
+ async function authenticateWithToken(token) {
1234
+ const config = getConfig();
1235
+ const apiUrl = getApiUrl();
1236
+ const spinner = (0, import_ora7.default)("Verifying token...").start();
1237
+ try {
1238
+ const response = await fetch(`${apiUrl}/cli/verify`, {
1239
+ method: "POST",
1240
+ headers: {
1241
+ "Content-Type": "application/json",
1242
+ Authorization: `Bearer ${token}`
1243
+ }
1244
+ });
1245
+ if (!response.ok) {
1246
+ spinner.fail(import_chalk5.default.red("Authentication failed"));
1247
+ const errorData = await response.json().catch(() => ({}));
1248
+ console.log(import_chalk5.default.red(`Error: ${errorData.error || "Invalid token"}`));
1249
+ console.log(import_chalk5.default.gray("\nMake sure your token is correct and try again."));
1250
+ process.exit(1);
1251
+ }
1252
+ const data = await response.json();
1253
+ config.set("apiToken", token);
1254
+ config.set("userId", data.userId);
1255
+ config.set("username", data.username);
1256
+ spinner.succeed(import_chalk5.default.green("Authentication successful!"));
1257
+ console.log(import_chalk5.default.gray(`
1258
+ Welcome, ${import_chalk5.default.white(data.username)}!
1259
+ `));
1260
+ console.log(import_chalk5.default.bold("\u{1F4CA} Submitting your usage data...\n"));
1261
+ await submit({ yes: true });
1262
+ } catch (error2) {
1263
+ spinner.fail(import_chalk5.default.red("Authentication failed"));
1264
+ console.log(import_chalk5.default.red(`
1265
+ Error: ${error2 instanceof Error ? error2.message : "Unknown error"}`));
1261
1266
  process.exit(1);
1262
1267
  }
1263
1268
  }
1269
+ function sleep(ms) {
1270
+ return new Promise((resolve) => setTimeout(resolve, ms));
1271
+ }
1272
+ var import_chalk5, import_ora7, import_inquirer2, import_open;
1273
+ var init_auth = __esm({
1274
+ "src/commands/auth.ts"() {
1275
+ "use strict";
1276
+ import_chalk5 = __toESM(require("chalk"));
1277
+ import_ora7 = __toESM(require("ora"));
1278
+ import_inquirer2 = __toESM(require("inquirer"));
1279
+ import_open = __toESM(require("open"));
1280
+ init_config();
1281
+ init_submit();
1282
+ }
1283
+ });
1284
+
1285
+ // src/index.ts
1286
+ var import_commander = require("commander");
1287
+ var import_inquirer3 = __toESM(require("inquirer"));
1288
+ var import_chalk6 = __toESM(require("chalk"));
1289
+ var import_update_notifier = __toESM(require("update-notifier"));
1290
+ init_submit();
1264
1291
 
1265
1292
  // src/commands/status.ts
1266
1293
  var import_ora2 = __toESM(require("ora"));
1267
1294
  init_config();
1268
1295
  init_api();
1296
+ init_ui();
1269
1297
  async function status(options) {
1270
1298
  if (!isAuthenticated()) {
1271
1299
  if (options.json) {
@@ -1725,6 +1753,8 @@ async function setupAuto(options = {}) {
1725
1753
 
1726
1754
  // src/commands/scan.ts
1727
1755
  var import_ora5 = __toESM(require("ora"));
1756
+ init_ccgather_json();
1757
+ init_ui();
1728
1758
  function displayResults(data) {
1729
1759
  console.log();
1730
1760
  const usageLines = [
@@ -1843,9 +1873,10 @@ async function scan(options = {}) {
1843
1873
  }
1844
1874
 
1845
1875
  // src/index.ts
1876
+ init_ui();
1846
1877
  init_config();
1847
1878
  init_api();
1848
- var VERSION = "1.3.2";
1879
+ var VERSION = "1.3.3";
1849
1880
  var pkg = { name: "ccgather", version: VERSION };
1850
1881
  var notifier = (0, import_update_notifier.default)({ pkg, updateCheckInterval: 1e3 * 60 * 60 });
1851
1882
  notifier.notify({