ccgather 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +958 -0
- package/package.json +51 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
// src/lib/config.ts
|
|
34
|
+
function getConfig() {
|
|
35
|
+
if (!configInstance) {
|
|
36
|
+
configInstance = new import_conf.default({
|
|
37
|
+
projectName: "ccgather",
|
|
38
|
+
defaults
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return configInstance;
|
|
42
|
+
}
|
|
43
|
+
function resetConfig() {
|
|
44
|
+
const config = getConfig();
|
|
45
|
+
config.clear();
|
|
46
|
+
Object.entries(defaults).forEach(([key, value]) => {
|
|
47
|
+
config.set(key, value);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function isAuthenticated() {
|
|
51
|
+
const config = getConfig();
|
|
52
|
+
return !!config.get("apiToken");
|
|
53
|
+
}
|
|
54
|
+
function getApiUrl() {
|
|
55
|
+
const config = getConfig();
|
|
56
|
+
return config.get("apiUrl") || defaults.apiUrl;
|
|
57
|
+
}
|
|
58
|
+
var import_conf, defaults, configInstance;
|
|
59
|
+
var init_config = __esm({
|
|
60
|
+
"src/lib/config.ts"() {
|
|
61
|
+
"use strict";
|
|
62
|
+
import_conf = __toESM(require("conf"));
|
|
63
|
+
defaults = {
|
|
64
|
+
apiUrl: "https://ccgather.dev/api",
|
|
65
|
+
autoSync: false,
|
|
66
|
+
syncInterval: 60,
|
|
67
|
+
verbose: false
|
|
68
|
+
};
|
|
69
|
+
configInstance = null;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// src/commands/reset.ts
|
|
74
|
+
var reset_exports = {};
|
|
75
|
+
__export(reset_exports, {
|
|
76
|
+
reset: () => reset
|
|
77
|
+
});
|
|
78
|
+
function getClaudeSettingsDir() {
|
|
79
|
+
return path2.join(os2.homedir(), ".claude");
|
|
80
|
+
}
|
|
81
|
+
function removeStopHook() {
|
|
82
|
+
const claudeDir = getClaudeSettingsDir();
|
|
83
|
+
const settingsPath = path2.join(claudeDir, "settings.json");
|
|
84
|
+
if (!fs2.existsSync(settingsPath)) {
|
|
85
|
+
return { success: true, message: "No settings file found" };
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const content = fs2.readFileSync(settingsPath, "utf-8");
|
|
89
|
+
const settings = JSON.parse(content);
|
|
90
|
+
if (settings.hooks && typeof settings.hooks === "object") {
|
|
91
|
+
const hooks = settings.hooks;
|
|
92
|
+
if (hooks.Stop && Array.isArray(hooks.Stop)) {
|
|
93
|
+
hooks.Stop = hooks.Stop.filter((hook) => {
|
|
94
|
+
if (typeof hook === "object" && hook !== null) {
|
|
95
|
+
const h = hook;
|
|
96
|
+
return typeof h.command !== "string" || !h.command.includes("ccgather");
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
if (hooks.Stop.length === 0) {
|
|
101
|
+
delete hooks.Stop;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
fs2.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
106
|
+
return { success: true, message: "Hook removed" };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
message: err instanceof Error ? err.message : "Unknown error"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function removeSyncScript() {
|
|
115
|
+
const claudeDir = getClaudeSettingsDir();
|
|
116
|
+
const scriptPath = path2.join(claudeDir, "ccgather-sync.js");
|
|
117
|
+
if (fs2.existsSync(scriptPath)) {
|
|
118
|
+
fs2.unlinkSync(scriptPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function reset() {
|
|
122
|
+
const config = getConfig();
|
|
123
|
+
console.log(import_chalk3.default.bold("\n\u{1F504} CCgather Reset\n"));
|
|
124
|
+
if (!config.get("apiToken")) {
|
|
125
|
+
console.log(import_chalk3.default.yellow("CCgather is not configured."));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const { confirmReset } = await import_inquirer.default.prompt([
|
|
129
|
+
{
|
|
130
|
+
type: "confirm",
|
|
131
|
+
name: "confirmReset",
|
|
132
|
+
message: "This will remove the CCgather hook and local configuration. Continue?",
|
|
133
|
+
default: false
|
|
134
|
+
}
|
|
135
|
+
]);
|
|
136
|
+
if (!confirmReset) {
|
|
137
|
+
console.log(import_chalk3.default.gray("Reset cancelled."));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const hookSpinner = (0, import_ora3.default)("Removing Claude Code hook...").start();
|
|
141
|
+
const hookResult = removeStopHook();
|
|
142
|
+
if (hookResult.success) {
|
|
143
|
+
hookSpinner.succeed(import_chalk3.default.green("Hook removed"));
|
|
144
|
+
} else {
|
|
145
|
+
hookSpinner.warn(import_chalk3.default.yellow(`Could not remove hook: ${hookResult.message}`));
|
|
146
|
+
}
|
|
147
|
+
const scriptSpinner = (0, import_ora3.default)("Removing sync script...").start();
|
|
148
|
+
try {
|
|
149
|
+
removeSyncScript();
|
|
150
|
+
scriptSpinner.succeed(import_chalk3.default.green("Sync script removed"));
|
|
151
|
+
} catch {
|
|
152
|
+
scriptSpinner.warn(import_chalk3.default.yellow("Could not remove sync script"));
|
|
153
|
+
}
|
|
154
|
+
const { deleteAccount } = await import_inquirer.default.prompt([
|
|
155
|
+
{
|
|
156
|
+
type: "confirm",
|
|
157
|
+
name: "deleteAccount",
|
|
158
|
+
message: import_chalk3.default.red("Do you also want to delete your account from the leaderboard? (This cannot be undone)"),
|
|
159
|
+
default: false
|
|
160
|
+
}
|
|
161
|
+
]);
|
|
162
|
+
if (deleteAccount) {
|
|
163
|
+
console.log(import_chalk3.default.yellow("\nAccount deletion is not yet implemented."));
|
|
164
|
+
console.log(import_chalk3.default.gray("Please contact support to delete your account."));
|
|
165
|
+
}
|
|
166
|
+
const configSpinner = (0, import_ora3.default)("Resetting local configuration...").start();
|
|
167
|
+
resetConfig();
|
|
168
|
+
configSpinner.succeed(import_chalk3.default.green("Local configuration reset"));
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(import_chalk3.default.green.bold("\u2705 Reset complete!"));
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(import_chalk3.default.gray("Your usage will no longer be tracked."));
|
|
173
|
+
console.log(import_chalk3.default.gray("Run `npx ccg` to set up again."));
|
|
174
|
+
console.log();
|
|
175
|
+
}
|
|
176
|
+
var import_chalk3, import_ora3, fs2, path2, os2, import_inquirer;
|
|
177
|
+
var init_reset = __esm({
|
|
178
|
+
"src/commands/reset.ts"() {
|
|
179
|
+
"use strict";
|
|
180
|
+
import_chalk3 = __toESM(require("chalk"));
|
|
181
|
+
import_ora3 = __toESM(require("ora"));
|
|
182
|
+
fs2 = __toESM(require("fs"));
|
|
183
|
+
path2 = __toESM(require("path"));
|
|
184
|
+
os2 = __toESM(require("os"));
|
|
185
|
+
import_inquirer = __toESM(require("inquirer"));
|
|
186
|
+
init_config();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// src/index.ts
|
|
191
|
+
var import_commander = require("commander");
|
|
192
|
+
|
|
193
|
+
// src/commands/submit.ts
|
|
194
|
+
var import_chalk = __toESM(require("chalk"));
|
|
195
|
+
var import_ora = __toESM(require("ora"));
|
|
196
|
+
var fs = __toESM(require("fs"));
|
|
197
|
+
var path = __toESM(require("path"));
|
|
198
|
+
var os = __toESM(require("os"));
|
|
199
|
+
init_config();
|
|
200
|
+
function findCcJson() {
|
|
201
|
+
const possiblePaths = [
|
|
202
|
+
path.join(process.cwd(), "cc.json"),
|
|
203
|
+
path.join(os.homedir(), "cc.json"),
|
|
204
|
+
path.join(os.homedir(), ".claude", "cc.json")
|
|
205
|
+
];
|
|
206
|
+
for (const p of possiblePaths) {
|
|
207
|
+
if (fs.existsSync(p)) {
|
|
208
|
+
return p;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function parseCcJson(filePath) {
|
|
214
|
+
try {
|
|
215
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
216
|
+
const data = JSON.parse(content);
|
|
217
|
+
return {
|
|
218
|
+
totalTokens: data.totalTokens || data.total_tokens || 0,
|
|
219
|
+
totalCost: data.totalCost || data.total_cost || data.costUSD || 0,
|
|
220
|
+
inputTokens: data.inputTokens || data.input_tokens || 0,
|
|
221
|
+
outputTokens: data.outputTokens || data.output_tokens || 0,
|
|
222
|
+
cacheReadTokens: data.cacheReadTokens || data.cache_read_tokens || 0,
|
|
223
|
+
cacheWriteTokens: data.cacheWriteTokens || data.cache_write_tokens || 0,
|
|
224
|
+
daysTracked: data.daysTracked || data.days_tracked || calculateDaysTracked(data)
|
|
225
|
+
};
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function calculateDaysTracked(data) {
|
|
231
|
+
if (data.dailyStats && Array.isArray(data.dailyStats)) {
|
|
232
|
+
return data.dailyStats.length;
|
|
233
|
+
}
|
|
234
|
+
if (data.daily && typeof data.daily === "object") {
|
|
235
|
+
return Object.keys(data.daily).length;
|
|
236
|
+
}
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
function parseUsageFromJsonl() {
|
|
240
|
+
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
241
|
+
if (!fs.existsSync(projectsDir)) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
let totalInputTokens = 0;
|
|
245
|
+
let totalOutputTokens = 0;
|
|
246
|
+
let totalCacheRead = 0;
|
|
247
|
+
let totalCacheWrite = 0;
|
|
248
|
+
const dates = /* @__PURE__ */ new Set();
|
|
249
|
+
function findJsonlFiles(dir) {
|
|
250
|
+
const files = [];
|
|
251
|
+
try {
|
|
252
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const fullPath = path.join(dir, entry.name);
|
|
255
|
+
if (entry.isDirectory()) {
|
|
256
|
+
files.push(...findJsonlFiles(fullPath));
|
|
257
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
258
|
+
files.push(fullPath);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
return files;
|
|
264
|
+
}
|
|
265
|
+
const jsonlFiles = findJsonlFiles(projectsDir);
|
|
266
|
+
for (const filePath of jsonlFiles) {
|
|
267
|
+
try {
|
|
268
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
269
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
270
|
+
for (const line of lines) {
|
|
271
|
+
try {
|
|
272
|
+
const event = JSON.parse(line);
|
|
273
|
+
if (event.type === "assistant" && event.message?.usage) {
|
|
274
|
+
const usage = event.message.usage;
|
|
275
|
+
totalInputTokens += usage.input_tokens || 0;
|
|
276
|
+
totalOutputTokens += usage.output_tokens || 0;
|
|
277
|
+
totalCacheRead += usage.cache_read_input_tokens || 0;
|
|
278
|
+
totalCacheWrite += usage.cache_creation_input_tokens || 0;
|
|
279
|
+
if (event.timestamp) {
|
|
280
|
+
const date = new Date(event.timestamp).toISOString().split("T")[0];
|
|
281
|
+
dates.add(date);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const totalTokens = totalInputTokens + totalOutputTokens;
|
|
291
|
+
if (totalTokens === 0) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const costPerMillion = 3;
|
|
295
|
+
const totalCost = totalTokens / 1e6 * costPerMillion;
|
|
296
|
+
return {
|
|
297
|
+
totalTokens,
|
|
298
|
+
totalCost: Math.round(totalCost * 100) / 100,
|
|
299
|
+
inputTokens: totalInputTokens,
|
|
300
|
+
outputTokens: totalOutputTokens,
|
|
301
|
+
cacheReadTokens: totalCacheRead,
|
|
302
|
+
cacheWriteTokens: totalCacheWrite,
|
|
303
|
+
daysTracked: dates.size || 1
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function formatNumber(num) {
|
|
307
|
+
return num.toLocaleString();
|
|
308
|
+
}
|
|
309
|
+
async function detectGitHubUsername() {
|
|
310
|
+
try {
|
|
311
|
+
const { execSync } = await import("child_process");
|
|
312
|
+
try {
|
|
313
|
+
const username = execSync("git config --get user.name", { encoding: "utf-8" }).trim();
|
|
314
|
+
if (username) return username;
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const remote = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
319
|
+
const match = remote.match(/github\.com[:/]([^/]+)/);
|
|
320
|
+
if (match) return match[1];
|
|
321
|
+
} catch {
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
async function submitToServer(username, data) {
|
|
328
|
+
const apiUrl = getApiUrl();
|
|
329
|
+
try {
|
|
330
|
+
const response = await fetch(`${apiUrl}/cli/submit`, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: {
|
|
333
|
+
"Content-Type": "application/json"
|
|
334
|
+
},
|
|
335
|
+
body: JSON.stringify({
|
|
336
|
+
username,
|
|
337
|
+
totalTokens: data.totalTokens,
|
|
338
|
+
totalSpent: data.totalCost,
|
|
339
|
+
inputTokens: data.inputTokens,
|
|
340
|
+
outputTokens: data.outputTokens,
|
|
341
|
+
cacheReadTokens: data.cacheReadTokens,
|
|
342
|
+
cacheWriteTokens: data.cacheWriteTokens,
|
|
343
|
+
daysTracked: data.daysTracked,
|
|
344
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
345
|
+
})
|
|
346
|
+
});
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
const errorData = await response.json().catch(() => ({}));
|
|
349
|
+
return { success: false, error: errorData.error || `HTTP ${response.status}` };
|
|
350
|
+
}
|
|
351
|
+
const result = await response.json();
|
|
352
|
+
return { success: true, profileUrl: result.profileUrl };
|
|
353
|
+
} catch (err) {
|
|
354
|
+
return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function submit(options) {
|
|
358
|
+
console.log(import_chalk.default.bold("\n\u{1F680} CCgather Submission Tool v1.0.0\n"));
|
|
359
|
+
const spinner = (0, import_ora.default)("Detecting GitHub username...").start();
|
|
360
|
+
let username = await detectGitHubUsername();
|
|
361
|
+
spinner.stop();
|
|
362
|
+
if (username) {
|
|
363
|
+
console.log(import_chalk.default.gray(`Detected GitHub username from repository: ${import_chalk.default.white(username)}`));
|
|
364
|
+
}
|
|
365
|
+
const inquirer2 = await import("inquirer");
|
|
366
|
+
const { confirmedUsername } = await inquirer2.default.prompt([
|
|
367
|
+
{
|
|
368
|
+
type: "input",
|
|
369
|
+
name: "confirmedUsername",
|
|
370
|
+
message: "GitHub username:",
|
|
371
|
+
default: username || "",
|
|
372
|
+
validate: (input) => input.trim().length > 0 || "Username is required"
|
|
373
|
+
}
|
|
374
|
+
]);
|
|
375
|
+
username = confirmedUsername.trim();
|
|
376
|
+
const ccJsonPath = findCcJson();
|
|
377
|
+
let usageData = null;
|
|
378
|
+
let dataSource = "";
|
|
379
|
+
if (ccJsonPath) {
|
|
380
|
+
const { useCcJson } = await inquirer2.default.prompt([
|
|
381
|
+
{
|
|
382
|
+
type: "confirm",
|
|
383
|
+
name: "useCcJson",
|
|
384
|
+
message: `Found existing cc.json. Use this file?`,
|
|
385
|
+
default: true
|
|
386
|
+
}
|
|
387
|
+
]);
|
|
388
|
+
if (useCcJson) {
|
|
389
|
+
usageData = parseCcJson(ccJsonPath);
|
|
390
|
+
dataSource = "cc.json";
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (!usageData) {
|
|
394
|
+
const parseSpinner = (0, import_ora.default)("Scanning Claude Code usage data...").start();
|
|
395
|
+
usageData = parseUsageFromJsonl();
|
|
396
|
+
parseSpinner.stop();
|
|
397
|
+
dataSource = "Claude Code logs";
|
|
398
|
+
}
|
|
399
|
+
if (!usageData) {
|
|
400
|
+
console.log(import_chalk.default.red("\n\u274C No usage data found."));
|
|
401
|
+
console.log(import_chalk.default.gray("Make sure you have used Claude Code or have a cc.json file."));
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
console.log(import_chalk.default.green(`\u2713 Using ${dataSource}
|
|
405
|
+
`));
|
|
406
|
+
console.log(import_chalk.default.bold("Summary:"));
|
|
407
|
+
console.log(import_chalk.default.gray(` Total Cost: ${import_chalk.default.green("$" + formatNumber(Math.round(usageData.totalCost)))}`));
|
|
408
|
+
console.log(import_chalk.default.gray(` Total Tokens: ${import_chalk.default.cyan(formatNumber(usageData.totalTokens))}`));
|
|
409
|
+
console.log(import_chalk.default.gray(` Days Tracked: ${import_chalk.default.yellow(usageData.daysTracked.toString())}`));
|
|
410
|
+
console.log();
|
|
411
|
+
if (!options.yes) {
|
|
412
|
+
const { confirmSubmit } = await inquirer2.default.prompt([
|
|
413
|
+
{
|
|
414
|
+
type: "confirm",
|
|
415
|
+
name: "confirmSubmit",
|
|
416
|
+
message: "Submit to CCgather leaderboard?",
|
|
417
|
+
default: true
|
|
418
|
+
}
|
|
419
|
+
]);
|
|
420
|
+
if (!confirmSubmit) {
|
|
421
|
+
console.log(import_chalk.default.gray("\nSubmission cancelled."));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const submitSpinner = (0, import_ora.default)("Submitting to CCgather...").start();
|
|
426
|
+
const result = await submitToServer(username, usageData);
|
|
427
|
+
if (result.success) {
|
|
428
|
+
submitSpinner.succeed(import_chalk.default.green("Successfully submitted to CCgather!"));
|
|
429
|
+
console.log();
|
|
430
|
+
console.log(import_chalk.default.gray(`View your profile at: ${import_chalk.default.cyan(result.profileUrl || `https://ccgather.dev/u/${username}`)}`));
|
|
431
|
+
console.log();
|
|
432
|
+
console.log(import_chalk.default.bold("Done! \u{1F389}"));
|
|
433
|
+
} else {
|
|
434
|
+
submitSpinner.fail(import_chalk.default.red("Failed to submit"));
|
|
435
|
+
console.log(import_chalk.default.red(result.error || "Unknown error"));
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
console.log();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/commands/status.ts
|
|
442
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
443
|
+
var import_ora2 = __toESM(require("ora"));
|
|
444
|
+
init_config();
|
|
445
|
+
|
|
446
|
+
// src/lib/api.ts
|
|
447
|
+
init_config();
|
|
448
|
+
async function fetchApi(endpoint, options = {}) {
|
|
449
|
+
const config = getConfig();
|
|
450
|
+
const apiToken = config.get("apiToken");
|
|
451
|
+
const apiUrl = getApiUrl();
|
|
452
|
+
if (!apiToken) {
|
|
453
|
+
return { success: false, error: "Not authenticated. Run: npx ccgather auth" };
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const response = await fetch(`${apiUrl}${endpoint}`, {
|
|
457
|
+
...options,
|
|
458
|
+
headers: {
|
|
459
|
+
"Content-Type": "application/json",
|
|
460
|
+
Authorization: `Bearer ${apiToken}`,
|
|
461
|
+
...options.headers
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
const data = await response.json();
|
|
465
|
+
if (!response.ok) {
|
|
466
|
+
return { success: false, error: data.error || `HTTP ${response.status}` };
|
|
467
|
+
}
|
|
468
|
+
return { success: true, data };
|
|
469
|
+
} catch (error) {
|
|
470
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
471
|
+
return { success: false, error: message };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function getStatus() {
|
|
475
|
+
return fetchApi("/cli/status");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/commands/status.ts
|
|
479
|
+
function formatNumber2(num) {
|
|
480
|
+
if (num >= 1e9) return `${(num / 1e9).toFixed(2)}B`;
|
|
481
|
+
if (num >= 1e6) return `${(num / 1e6).toFixed(2)}M`;
|
|
482
|
+
if (num >= 1e3) return `${(num / 1e3).toFixed(2)}K`;
|
|
483
|
+
return num.toString();
|
|
484
|
+
}
|
|
485
|
+
function getTierEmoji(tier) {
|
|
486
|
+
const emojis = {
|
|
487
|
+
free: "\u{1F193}",
|
|
488
|
+
pro: "\u2B50",
|
|
489
|
+
team: "\u{1F465}",
|
|
490
|
+
enterprise: "\u{1F3E2}"
|
|
491
|
+
};
|
|
492
|
+
return emojis[tier.toLowerCase()] || "\u{1F3AF}";
|
|
493
|
+
}
|
|
494
|
+
function getRankMedal(rank) {
|
|
495
|
+
if (rank === 1) return "\u{1F947}";
|
|
496
|
+
if (rank === 2) return "\u{1F948}";
|
|
497
|
+
if (rank === 3) return "\u{1F949}";
|
|
498
|
+
if (rank <= 10) return "\u{1F3C5}";
|
|
499
|
+
if (rank <= 100) return "\u{1F396}\uFE0F";
|
|
500
|
+
return "\u{1F4CA}";
|
|
501
|
+
}
|
|
502
|
+
async function status(options) {
|
|
503
|
+
if (!isAuthenticated()) {
|
|
504
|
+
if (options.json) {
|
|
505
|
+
console.log(JSON.stringify({ error: "Not authenticated" }));
|
|
506
|
+
} else {
|
|
507
|
+
console.log(import_chalk2.default.red("\nNot authenticated."));
|
|
508
|
+
console.log(import_chalk2.default.gray("Run: npx ccgather auth\n"));
|
|
509
|
+
}
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
const spinner = options.json ? null : (0, import_ora2.default)("Fetching your stats...").start();
|
|
513
|
+
const result = await getStatus();
|
|
514
|
+
if (!result.success) {
|
|
515
|
+
if (spinner) spinner.fail(import_chalk2.default.red("Failed to fetch status"));
|
|
516
|
+
if (options.json) {
|
|
517
|
+
console.log(JSON.stringify({ error: result.error }));
|
|
518
|
+
} else {
|
|
519
|
+
console.log(import_chalk2.default.red(`Error: ${result.error}
|
|
520
|
+
`));
|
|
521
|
+
}
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
const stats = result.data;
|
|
525
|
+
if (options.json) {
|
|
526
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
spinner?.succeed(import_chalk2.default.green("Status retrieved"));
|
|
530
|
+
console.log("\n" + import_chalk2.default.bold("\u2550".repeat(42)));
|
|
531
|
+
console.log(import_chalk2.default.bold.white(" \u{1F310} CCgather Status"));
|
|
532
|
+
console.log(import_chalk2.default.bold("\u2550".repeat(42)));
|
|
533
|
+
const medal = getRankMedal(stats.rank);
|
|
534
|
+
console.log(`
|
|
535
|
+
${medal} ${import_chalk2.default.bold.yellow(`Rank #${stats.rank}`)}`);
|
|
536
|
+
console.log(import_chalk2.default.gray(` Top ${stats.percentile.toFixed(1)}% of all users`));
|
|
537
|
+
console.log("\n" + import_chalk2.default.gray("\u2500".repeat(42)));
|
|
538
|
+
console.log(
|
|
539
|
+
` ${import_chalk2.default.gray("Tokens")} ${import_chalk2.default.white(formatNumber2(stats.totalTokens))}`
|
|
540
|
+
);
|
|
541
|
+
console.log(
|
|
542
|
+
` ${import_chalk2.default.gray("Spent")} ${import_chalk2.default.green("$" + stats.totalSpent.toFixed(2))}`
|
|
543
|
+
);
|
|
544
|
+
console.log(
|
|
545
|
+
` ${import_chalk2.default.gray("Tier")} ${getTierEmoji(stats.tier)} ${import_chalk2.default.white(stats.tier)}`
|
|
546
|
+
);
|
|
547
|
+
if (stats.badges && stats.badges.length > 0) {
|
|
548
|
+
console.log("\n" + import_chalk2.default.gray("\u2500".repeat(42)));
|
|
549
|
+
console.log(import_chalk2.default.gray(" Badges"));
|
|
550
|
+
console.log(` ${stats.badges.join(" ")}`);
|
|
551
|
+
}
|
|
552
|
+
console.log("\n" + import_chalk2.default.bold("\u2550".repeat(42)));
|
|
553
|
+
console.log(import_chalk2.default.gray("\n View leaderboard: https://ccgather.dev/leaderboard\n"));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/commands/setup-auto.ts
|
|
557
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
558
|
+
var import_ora4 = __toESM(require("ora"));
|
|
559
|
+
var http = __toESM(require("http"));
|
|
560
|
+
var fs3 = __toESM(require("fs"));
|
|
561
|
+
var path3 = __toESM(require("path"));
|
|
562
|
+
var os3 = __toESM(require("os"));
|
|
563
|
+
init_config();
|
|
564
|
+
init_config();
|
|
565
|
+
var CALLBACK_PORT = 9876;
|
|
566
|
+
function getClaudeSettingsDir2() {
|
|
567
|
+
return path3.join(os3.homedir(), ".claude");
|
|
568
|
+
}
|
|
569
|
+
async function openBrowser(url) {
|
|
570
|
+
const { default: open } = await import("open");
|
|
571
|
+
await open(url);
|
|
572
|
+
}
|
|
573
|
+
function createCallbackServer() {
|
|
574
|
+
return new Promise((resolve, reject) => {
|
|
575
|
+
const server = http.createServer((req, res) => {
|
|
576
|
+
const url = new URL(req.url || "", `http://localhost:${CALLBACK_PORT}`);
|
|
577
|
+
if (url.pathname === "/callback") {
|
|
578
|
+
const token = url.searchParams.get("token");
|
|
579
|
+
const userId = url.searchParams.get("userId");
|
|
580
|
+
const username = url.searchParams.get("username");
|
|
581
|
+
const error = url.searchParams.get("error");
|
|
582
|
+
if (error) {
|
|
583
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
584
|
+
res.end(`
|
|
585
|
+
<html>
|
|
586
|
+
<head><title>CCgather - Error</title></head>
|
|
587
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a1a; color: #fff;">
|
|
588
|
+
<div style="text-align: center;">
|
|
589
|
+
<h1 style="color: #ef4444;">\u274C Authentication Failed</h1>
|
|
590
|
+
<p>${error}</p>
|
|
591
|
+
<p style="color: #888;">You can close this window.</p>
|
|
592
|
+
</div>
|
|
593
|
+
</body>
|
|
594
|
+
</html>
|
|
595
|
+
`);
|
|
596
|
+
server.close();
|
|
597
|
+
reject(new Error(error));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (token && userId && username) {
|
|
601
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
602
|
+
res.end(`
|
|
603
|
+
<html>
|
|
604
|
+
<head><title>CCgather - Success</title></head>
|
|
605
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a1a; color: #fff;">
|
|
606
|
+
<div style="text-align: center;">
|
|
607
|
+
<h1 style="color: #22c55e;">\u2705 Authentication Successful!</h1>
|
|
608
|
+
<p>Welcome, <strong>${username}</strong>!</p>
|
|
609
|
+
<p style="color: #888;">You can close this window and return to your terminal.</p>
|
|
610
|
+
</div>
|
|
611
|
+
</body>
|
|
612
|
+
</html>
|
|
613
|
+
`);
|
|
614
|
+
server.close();
|
|
615
|
+
resolve({ token, userId, username });
|
|
616
|
+
} else {
|
|
617
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
618
|
+
res.end("Missing required parameters");
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
622
|
+
res.end("Not found");
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
server.listen(CALLBACK_PORT, () => {
|
|
626
|
+
});
|
|
627
|
+
server.on("error", (err) => {
|
|
628
|
+
reject(new Error(`Failed to start callback server: ${err.message}`));
|
|
629
|
+
});
|
|
630
|
+
setTimeout(() => {
|
|
631
|
+
server.close();
|
|
632
|
+
reject(new Error("Authentication timed out"));
|
|
633
|
+
}, 5 * 60 * 1e3);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
function generateSyncScript(apiUrl, apiToken) {
|
|
637
|
+
return `#!/usr/bin/env node
|
|
638
|
+
/**
|
|
639
|
+
* CCgather Auto-Sync Script
|
|
640
|
+
* This script is automatically called by Claude Code's Stop hook
|
|
641
|
+
* to sync your usage data to the leaderboard.
|
|
642
|
+
*/
|
|
643
|
+
|
|
644
|
+
const https = require('https');
|
|
645
|
+
const http = require('http');
|
|
646
|
+
const fs = require('fs');
|
|
647
|
+
const path = require('path');
|
|
648
|
+
const os = require('os');
|
|
649
|
+
|
|
650
|
+
const API_URL = '${apiUrl}';
|
|
651
|
+
const API_TOKEN = '${apiToken}';
|
|
652
|
+
|
|
653
|
+
// Get Claude Code projects directory for JSONL files
|
|
654
|
+
function getClaudeProjectsDir() {
|
|
655
|
+
const platform = os.platform();
|
|
656
|
+
if (platform === 'win32') {
|
|
657
|
+
return path.join(os.homedir(), '.claude', 'projects');
|
|
658
|
+
}
|
|
659
|
+
return path.join(os.homedir(), '.claude', 'projects');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Parse JSONL files for usage data
|
|
663
|
+
function parseUsageFromJsonl() {
|
|
664
|
+
const projectsDir = getClaudeProjectsDir();
|
|
665
|
+
|
|
666
|
+
if (!fs.existsSync(projectsDir)) {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
let totalInputTokens = 0;
|
|
671
|
+
let totalOutputTokens = 0;
|
|
672
|
+
let totalCacheRead = 0;
|
|
673
|
+
let totalCacheWrite = 0;
|
|
674
|
+
const modelBreakdown = {};
|
|
675
|
+
|
|
676
|
+
// Recursively find all .jsonl files
|
|
677
|
+
function findJsonlFiles(dir) {
|
|
678
|
+
const files = [];
|
|
679
|
+
try {
|
|
680
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
681
|
+
for (const entry of entries) {
|
|
682
|
+
const fullPath = path.join(dir, entry.name);
|
|
683
|
+
if (entry.isDirectory()) {
|
|
684
|
+
files.push(...findJsonlFiles(fullPath));
|
|
685
|
+
} else if (entry.name.endsWith('.jsonl')) {
|
|
686
|
+
files.push(fullPath);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} catch (e) {
|
|
690
|
+
// Ignore permission errors
|
|
691
|
+
}
|
|
692
|
+
return files;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const jsonlFiles = findJsonlFiles(projectsDir);
|
|
696
|
+
|
|
697
|
+
for (const filePath of jsonlFiles) {
|
|
698
|
+
try {
|
|
699
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
700
|
+
const lines = content.split('\\n').filter(line => line.trim());
|
|
701
|
+
|
|
702
|
+
for (const line of lines) {
|
|
703
|
+
try {
|
|
704
|
+
const event = JSON.parse(line);
|
|
705
|
+
|
|
706
|
+
if (event.type === 'assistant' && event.message?.usage) {
|
|
707
|
+
const usage = event.message.usage;
|
|
708
|
+
totalInputTokens += usage.input_tokens || 0;
|
|
709
|
+
totalOutputTokens += usage.output_tokens || 0;
|
|
710
|
+
totalCacheRead += usage.cache_read_input_tokens || 0;
|
|
711
|
+
totalCacheWrite += usage.cache_creation_input_tokens || 0;
|
|
712
|
+
|
|
713
|
+
const model = event.message.model || 'unknown';
|
|
714
|
+
modelBreakdown[model] = (modelBreakdown[model] || 0) +
|
|
715
|
+
(usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
716
|
+
}
|
|
717
|
+
} catch (e) {
|
|
718
|
+
// Skip invalid JSON lines
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} catch (e) {
|
|
722
|
+
// Skip unreadable files
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const totalTokens = totalInputTokens + totalOutputTokens;
|
|
727
|
+
|
|
728
|
+
if (totalTokens === 0) {
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Estimate cost (rough approximation)
|
|
733
|
+
const costPerMillion = 3; // Average cost
|
|
734
|
+
const totalCost = (totalTokens / 1000000) * costPerMillion;
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
totalTokens,
|
|
738
|
+
totalCost: Math.round(totalCost * 100) / 100,
|
|
739
|
+
inputTokens: totalInputTokens,
|
|
740
|
+
outputTokens: totalOutputTokens,
|
|
741
|
+
cacheReadTokens: totalCacheRead,
|
|
742
|
+
cacheWriteTokens: totalCacheWrite,
|
|
743
|
+
modelBreakdown
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Send data to CCgather API
|
|
748
|
+
function syncToServer(data) {
|
|
749
|
+
const urlObj = new URL(API_URL + '/cli/sync');
|
|
750
|
+
const isHttps = urlObj.protocol === 'https:';
|
|
751
|
+
const client = isHttps ? https : http;
|
|
752
|
+
|
|
753
|
+
const postData = JSON.stringify({
|
|
754
|
+
totalTokens: data.totalTokens,
|
|
755
|
+
totalSpent: data.totalCost,
|
|
756
|
+
inputTokens: data.inputTokens,
|
|
757
|
+
outputTokens: data.outputTokens,
|
|
758
|
+
cacheReadTokens: data.cacheReadTokens,
|
|
759
|
+
cacheWriteTokens: data.cacheWriteTokens,
|
|
760
|
+
modelBreakdown: data.modelBreakdown,
|
|
761
|
+
timestamp: new Date().toISOString()
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const options = {
|
|
765
|
+
hostname: urlObj.hostname,
|
|
766
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
767
|
+
path: urlObj.pathname,
|
|
768
|
+
method: 'POST',
|
|
769
|
+
headers: {
|
|
770
|
+
'Content-Type': 'application/json',
|
|
771
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
772
|
+
'Authorization': 'Bearer ' + API_TOKEN
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const req = client.request(options, (res) => {
|
|
777
|
+
// Silent success
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
req.on('error', (e) => {
|
|
781
|
+
// Silent failure - don't interrupt user's workflow
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
req.write(postData);
|
|
785
|
+
req.end();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Main execution
|
|
789
|
+
const usageData = parseUsageFromJsonl();
|
|
790
|
+
if (usageData) {
|
|
791
|
+
syncToServer(usageData);
|
|
792
|
+
}
|
|
793
|
+
`;
|
|
794
|
+
}
|
|
795
|
+
function installStopHook() {
|
|
796
|
+
const claudeDir = getClaudeSettingsDir2();
|
|
797
|
+
const settingsPath = path3.join(claudeDir, "settings.json");
|
|
798
|
+
if (!fs3.existsSync(claudeDir)) {
|
|
799
|
+
fs3.mkdirSync(claudeDir, { recursive: true });
|
|
800
|
+
}
|
|
801
|
+
let settings = {};
|
|
802
|
+
try {
|
|
803
|
+
if (fs3.existsSync(settingsPath)) {
|
|
804
|
+
const content = fs3.readFileSync(settingsPath, "utf-8");
|
|
805
|
+
settings = JSON.parse(content);
|
|
806
|
+
}
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
if (!settings.hooks || typeof settings.hooks !== "object") {
|
|
810
|
+
settings.hooks = {};
|
|
811
|
+
}
|
|
812
|
+
const hooks = settings.hooks;
|
|
813
|
+
const syncScriptPath = path3.join(claudeDir, "ccgather-sync.js");
|
|
814
|
+
const hookCommand = `node "${syncScriptPath}"`;
|
|
815
|
+
if (!hooks.Stop || !Array.isArray(hooks.Stop)) {
|
|
816
|
+
hooks.Stop = [];
|
|
817
|
+
}
|
|
818
|
+
const existingHook = hooks.Stop.find(
|
|
819
|
+
(h) => typeof h === "object" && h !== null && h.command === hookCommand
|
|
820
|
+
);
|
|
821
|
+
if (!existingHook) {
|
|
822
|
+
hooks.Stop.push({
|
|
823
|
+
command: hookCommand,
|
|
824
|
+
background: true
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
fs3.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
828
|
+
return { success: true, message: "Stop hook installed" };
|
|
829
|
+
}
|
|
830
|
+
function saveSyncScript(apiUrl, apiToken) {
|
|
831
|
+
const claudeDir = getClaudeSettingsDir2();
|
|
832
|
+
const scriptPath = path3.join(claudeDir, "ccgather-sync.js");
|
|
833
|
+
const scriptContent = generateSyncScript(apiUrl, apiToken);
|
|
834
|
+
fs3.writeFileSync(scriptPath, scriptContent);
|
|
835
|
+
if (os3.platform() !== "win32") {
|
|
836
|
+
fs3.chmodSync(scriptPath, "755");
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
async function setupAuto(options = {}) {
|
|
840
|
+
if (options.manual) {
|
|
841
|
+
console.log(import_chalk4.default.bold("\n\u{1F527} Disabling Auto-Sync\n"));
|
|
842
|
+
const { reset: reset2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
|
|
843
|
+
await reset2();
|
|
844
|
+
console.log(import_chalk4.default.green("\u2713 Auto-sync disabled. Use `npx ccg` to submit manually."));
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
console.log(import_chalk4.default.bold("\n\u26A0\uFE0F Auto-Sync Mode (Optional)\n"));
|
|
848
|
+
console.log(import_chalk4.default.gray("This will install a hook that automatically syncs"));
|
|
849
|
+
console.log(import_chalk4.default.gray("your usage data when Claude Code sessions end."));
|
|
850
|
+
console.log();
|
|
851
|
+
console.log(import_chalk4.default.yellow("Note: Manual submission (`npx ccg`) is recommended for most users."));
|
|
852
|
+
console.log();
|
|
853
|
+
const inquirer2 = await import("inquirer");
|
|
854
|
+
const { proceed } = await inquirer2.default.prompt([
|
|
855
|
+
{
|
|
856
|
+
type: "confirm",
|
|
857
|
+
name: "proceed",
|
|
858
|
+
message: "Continue with auto-sync setup?",
|
|
859
|
+
default: false
|
|
860
|
+
}
|
|
861
|
+
]);
|
|
862
|
+
if (!proceed) {
|
|
863
|
+
console.log(import_chalk4.default.gray("\nSetup cancelled. Use `npx ccg` to submit manually."));
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const config = getConfig();
|
|
867
|
+
const apiUrl = getApiUrl();
|
|
868
|
+
console.log(import_chalk4.default.bold("\n\u{1F310} CCgather Setup\n"));
|
|
869
|
+
const existingToken = config.get("apiToken");
|
|
870
|
+
if (existingToken) {
|
|
871
|
+
const inquirer3 = await import("inquirer");
|
|
872
|
+
const { reconfigure } = await inquirer3.default.prompt([
|
|
873
|
+
{
|
|
874
|
+
type: "confirm",
|
|
875
|
+
name: "reconfigure",
|
|
876
|
+
message: "You are already set up. Do you want to reconfigure?",
|
|
877
|
+
default: false
|
|
878
|
+
}
|
|
879
|
+
]);
|
|
880
|
+
if (!reconfigure) {
|
|
881
|
+
console.log(import_chalk4.default.gray("Setup cancelled."));
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
console.log(import_chalk4.default.gray("Opening browser for GitHub authentication...\n"));
|
|
886
|
+
const callbackUrl = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
887
|
+
const baseUrl = apiUrl.replace("/api", "");
|
|
888
|
+
const authUrl = `${baseUrl}/cli/auth?callback=${encodeURIComponent(callbackUrl)}`;
|
|
889
|
+
const serverPromise = createCallbackServer();
|
|
890
|
+
try {
|
|
891
|
+
await openBrowser(authUrl);
|
|
892
|
+
} catch {
|
|
893
|
+
console.log(import_chalk4.default.yellow("Could not open browser automatically."));
|
|
894
|
+
console.log(import_chalk4.default.gray("Please open this URL manually:"));
|
|
895
|
+
console.log(import_chalk4.default.cyan(authUrl));
|
|
896
|
+
console.log();
|
|
897
|
+
}
|
|
898
|
+
const spinner = (0, import_ora4.default)("Waiting for authentication...").start();
|
|
899
|
+
try {
|
|
900
|
+
const authData = await serverPromise;
|
|
901
|
+
spinner.succeed(import_chalk4.default.green("Authentication successful!"));
|
|
902
|
+
config.set("apiToken", authData.token);
|
|
903
|
+
config.set("userId", authData.userId);
|
|
904
|
+
const hookSpinner = (0, import_ora4.default)("Installing Claude Code hook...").start();
|
|
905
|
+
try {
|
|
906
|
+
saveSyncScript(apiUrl, authData.token);
|
|
907
|
+
const hookResult = installStopHook();
|
|
908
|
+
if (hookResult.success) {
|
|
909
|
+
hookSpinner.succeed(import_chalk4.default.green("Hook installed successfully!"));
|
|
910
|
+
} else {
|
|
911
|
+
hookSpinner.fail(import_chalk4.default.red("Failed to install hook"));
|
|
912
|
+
console.log(import_chalk4.default.red(hookResult.message));
|
|
913
|
+
}
|
|
914
|
+
} catch (err) {
|
|
915
|
+
hookSpinner.fail(import_chalk4.default.red("Failed to install hook"));
|
|
916
|
+
console.log(import_chalk4.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
917
|
+
}
|
|
918
|
+
console.log();
|
|
919
|
+
console.log(import_chalk4.default.green.bold("\u2705 Setup complete!"));
|
|
920
|
+
console.log();
|
|
921
|
+
console.log(import_chalk4.default.gray(`Welcome, ${import_chalk4.default.white(authData.username)}!`));
|
|
922
|
+
console.log();
|
|
923
|
+
console.log(import_chalk4.default.gray("Your Claude Code usage will now be automatically synced"));
|
|
924
|
+
console.log(import_chalk4.default.gray("to the leaderboard when each session ends."));
|
|
925
|
+
console.log();
|
|
926
|
+
console.log(import_chalk4.default.gray("View your stats:"));
|
|
927
|
+
console.log(import_chalk4.default.cyan(" npx ccg status"));
|
|
928
|
+
console.log();
|
|
929
|
+
console.log(import_chalk4.default.gray("View the leaderboard:"));
|
|
930
|
+
console.log(import_chalk4.default.cyan(" https://ccgather.dev/leaderboard"));
|
|
931
|
+
console.log();
|
|
932
|
+
} catch (err) {
|
|
933
|
+
spinner.fail(import_chalk4.default.red("Authentication failed"));
|
|
934
|
+
console.log(import_chalk4.default.red(err instanceof Error ? err.message : "Unknown error"));
|
|
935
|
+
process.exit(1);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// src/index.ts
|
|
940
|
+
var program = new import_commander.Command();
|
|
941
|
+
program.name("ccg").description("Submit your Claude Code usage to the CCgather leaderboard").version("1.0.0").option("-y, --yes", "Skip confirmation prompt").option("--auto", "Enable automatic sync on session end").option("--manual", "Disable automatic sync");
|
|
942
|
+
program.command("rank").description("View your current rank and stats").action(status);
|
|
943
|
+
program.command("reset").description("Remove auto-sync hook and clear config").action(async () => {
|
|
944
|
+
const { reset: reset2 } = await Promise.resolve().then(() => (init_reset(), reset_exports));
|
|
945
|
+
await reset2();
|
|
946
|
+
});
|
|
947
|
+
program.action(async (options) => {
|
|
948
|
+
if (options.auto) {
|
|
949
|
+
await setupAuto({ auto: true });
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (options.manual) {
|
|
953
|
+
await setupAuto({ manual: true });
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
await submit({ yes: options.yes });
|
|
957
|
+
});
|
|
958
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ccgather",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for syncing Claude Code usage data to CCgather leaderboard",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ccgather": "./dist/index.js",
|
|
7
|
+
"ccg": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup src/index.ts --format cjs --dts",
|
|
13
|
+
"dev": "tsup src/index.ts --format cjs --dts --watch",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude",
|
|
19
|
+
"anthropic",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"ccgather",
|
|
22
|
+
"leaderboard",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"author": "",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"chalk": "^5.3.0",
|
|
29
|
+
"commander": "^12.1.0",
|
|
30
|
+
"conf": "^13.0.1",
|
|
31
|
+
"inquirer": "^10.2.2",
|
|
32
|
+
"open": "^10.1.0",
|
|
33
|
+
"ora": "^8.1.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/inquirer": "^9.0.7",
|
|
37
|
+
"@types/node": "^22.10.2",
|
|
38
|
+
"tsup": "^8.3.5",
|
|
39
|
+
"typescript": "^5.7.2"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/your-username/ccgather"
|
|
50
|
+
}
|
|
51
|
+
}
|