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