ccgather 1.5.0 → 2.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.
Files changed (2) hide show
  1. package/dist/index.js +143 -419
  2. package/package.json +6 -3
package/dist/index.js CHANGED
@@ -208,60 +208,9 @@ function success(message) {
208
208
  function error(message) {
209
209
  return `${colors.error("\u2717")} ${message}`;
210
210
  }
211
- function warning(message) {
212
- return `${colors.warning("\u26A0")} ${message}`;
213
- }
214
211
  function link(url) {
215
212
  return hyperlink(colors.cyan.underline(url), url);
216
213
  }
217
- function planDetectionSection(title, icon = "\u{1F50D}") {
218
- return `
219
- ${colors.dim("\u2500".repeat(50))}
220
- ${icon} ${colors.white.bold(title)}
221
- ${colors.dim("\u2500".repeat(50))}`;
222
- }
223
- function maxVerifiedMessage(opusModels) {
224
- const modelList = opusModels.length > 2 ? `${opusModels.slice(0, 2).join(", ")} +${opusModels.length - 2} more` : opusModels.join(", ");
225
- return [
226
- ``,
227
- ` ${success("Max plan verified")}`,
228
- ``,
229
- ` ${colors.muted("\u{1F4A1} Reason:")} Opus model usage detected`,
230
- ` ${colors.dim("Models:")} ${colors.cyan(modelList)}`,
231
- ``,
232
- ` ${colors.dim("All data will be recorded under Max league.")}`,
233
- ``
234
- ];
235
- }
236
- function pastDataWarningMessage(oldestDate, daysSince) {
237
- return [
238
- ``,
239
- ` ${warning(`Data older than 30 days detected`)}`,
240
- ``,
241
- ` ${colors.muted("Oldest record:")} ${colors.white(oldestDate)} ${colors.dim(`(${daysSince} days ago)`)}`,
242
- ` ${colors.muted("Opus usage:")} ${colors.dim("Not found")}`,
243
- ``,
244
- ` ${colors.dim("\u2192 Max plan cannot be verified automatically.")}`,
245
- ``
246
- ];
247
- }
248
- function trustMessage() {
249
- return [
250
- ` ${colors.muted("\u{1F4A1} For fair league placement, we trust your choice.")}`,
251
- ` ${colors.dim("Tip: Submit regularly for accurate tracking!")}`,
252
- ``
253
- ];
254
- }
255
- function currentPlanMessage(plan) {
256
- const planColor = plan === "max" ? colors.max : plan === "pro" ? colors.pro : colors.free;
257
- const planBadge = plan === "max" ? "\u{1F680} MAX" : plan === "pro" ? "\u26A1 PRO" : "\u26AA FREE";
258
- return [
259
- ``,
260
- ` ${colors.muted("\u{1F4CB} Plan:")} ${planColor(planBadge)}`,
261
- ` ${colors.dim("Current plan from credentials will be used.")}`,
262
- ``
263
- ];
264
- }
265
214
  function sleep(ms) {
266
215
  return new Promise((resolve) => setTimeout(resolve, ms));
267
216
  }
@@ -380,7 +329,7 @@ var init_ui = __esm({
380
329
  "use strict";
381
330
  import_chalk = __toESM(require("chalk"));
382
331
  import_string_width = __toESM(require("string-width"));
383
- VERSION = true ? "1.5.0" : "0.0.0";
332
+ VERSION = true ? "2.0.0" : "0.0.0";
384
333
  colors = {
385
334
  primary: import_chalk.default.hex("#DA7756"),
386
335
  // Claude coral
@@ -772,22 +721,6 @@ function hasOpusUsageInProject(dailyUsage) {
772
721
  opusDates
773
722
  };
774
723
  }
775
- function hasOldData(dailyUsage, days = 30) {
776
- if (dailyUsage.length === 0) {
777
- return { hasOldData: false, oldestDate: null, daysSinceOldest: 0 };
778
- }
779
- const sortedDates = dailyUsage.map((d) => d.date).sort((a, b) => a.localeCompare(b));
780
- const oldestDate = sortedDates[0];
781
- const oldestDateObj = new Date(oldestDate);
782
- const now = /* @__PURE__ */ new Date();
783
- const diffTime = now.getTime() - oldestDateObj.getTime();
784
- const daysSinceOldest = Math.floor(diffTime / (1e3 * 60 * 60 * 24));
785
- return {
786
- hasOldData: daysSinceOldest > days,
787
- oldestDate,
788
- daysSinceOldest
789
- };
790
- }
791
724
  var CCGATHER_JSON_VERSION = "1.2.0";
792
725
  function extractProjectName(filePath) {
793
726
  const parts = filePath.split(/[/\\]/);
@@ -856,55 +789,6 @@ function encodePathLikeClaude(inputPath) {
856
789
  return "-";
857
790
  }).join("");
858
791
  }
859
- function getCurrentProjectDir() {
860
- const projectsDirs = getClaudeProjectsDirs();
861
- if (projectsDirs.length === 0) {
862
- debugLog("No project directories found");
863
- return null;
864
- }
865
- const cwd = process.cwd();
866
- const projectName = path2.basename(cwd);
867
- const encodedCwd = encodePathLikeClaude(cwd);
868
- debugLog("Looking for project:", projectName);
869
- debugLog("Encoded CWD:", encodedCwd);
870
- for (const projectsDir of projectsDirs) {
871
- try {
872
- const entries = fs2.readdirSync(projectsDir, { withFileTypes: true });
873
- debugLog(`Scanning ${projectsDir}: ${entries.length} entries`);
874
- for (const entry of entries) {
875
- if (!entry.isDirectory()) continue;
876
- const folderName = entry.name;
877
- const folderNameLower = folderName.toLowerCase();
878
- const projectNameLower = projectName.toLowerCase();
879
- const encodedCwdLower = encodedCwd.toLowerCase();
880
- if (folderName === encodedCwd || folderNameLower === encodedCwdLower) {
881
- debugLog("Match found (exact):", folderName);
882
- return path2.join(projectsDir, entry.name);
883
- }
884
- if (folderNameLower.includes(projectNameLower)) {
885
- debugLog("Match found (contains project name):", folderName);
886
- return path2.join(projectsDir, entry.name);
887
- }
888
- const projectNameHyphenated = projectNameLower.replace(/_/g, "-");
889
- if (folderNameLower.includes(projectNameHyphenated)) {
890
- debugLog("Match found (hyphenated project name):", folderName);
891
- return path2.join(projectsDir, entry.name);
892
- }
893
- const projectNamePattern = projectNameLower.replace(/[^a-z0-9]/g, "");
894
- const folderNamePattern = folderNameLower.replace(/[^a-z0-9]/g, "");
895
- if (folderNamePattern.endsWith(projectNamePattern) && projectNamePattern.length > 3) {
896
- debugLog("Match found (ends with pattern):", folderName);
897
- return path2.join(projectsDir, entry.name);
898
- }
899
- }
900
- } catch (err) {
901
- debugLog("Error scanning directory:", err);
902
- continue;
903
- }
904
- }
905
- debugLog("No matching project directory found");
906
- return null;
907
- }
908
792
  function findJsonlFiles(dir) {
909
793
  const files = [];
910
794
  try {
@@ -969,44 +853,53 @@ function generateSessionFingerprint(sessionFiles) {
969
853
  sessionCount: sessionHashes.length
970
854
  };
971
855
  }
972
- function hasProjectSessions() {
973
- return getCurrentProjectDir() !== null;
974
- }
975
- function getProjectPath() {
976
- return getCurrentProjectDir();
977
- }
978
- function getCurrentProjectName() {
856
+ function getSessionPathDebugInfo() {
857
+ const home = os2.homedir();
979
858
  const cwd = process.cwd();
980
- return path2.basename(cwd);
981
- }
982
- function getAllProjectFolders() {
983
- const projectsDirs = getClaudeProjectsDirs();
984
- const projects = [];
985
- for (const projectsDir of projectsDirs) {
986
- try {
987
- const entries = fs2.readdirSync(projectsDir, { withFileTypes: true });
988
- for (const entry of entries) {
989
- if (!entry.isDirectory()) continue;
990
- if (entry.name.startsWith(".")) continue;
991
- const fullPath = path2.join(projectsDir, entry.name);
992
- const hasJsonl = findJsonlFiles(fullPath).length > 0;
993
- if (!hasJsonl) continue;
994
- const parts = entry.name.split("-").filter((p) => p.length > 0);
995
- const displayName = parts.length > 0 ? parts[parts.length - 1] : entry.name;
996
- projects.push({
997
- folderName: entry.name,
998
- fullPath,
999
- displayName
1000
- });
859
+ const encodedCwd = encodePathLikeClaude(cwd);
860
+ const pathsToCheck = [
861
+ path2.join(home, ".config", "claude", "projects"),
862
+ path2.join(home, ".claude", "projects")
863
+ ];
864
+ const configDir = process.env.CLAUDE_CONFIG_DIR;
865
+ if (configDir) {
866
+ pathsToCheck.unshift(path2.join(configDir, "projects"));
867
+ }
868
+ if (process.platform === "win32" && process.env.APPDATA) {
869
+ pathsToCheck.unshift(path2.join(process.env.APPDATA, "claude", "projects"));
870
+ }
871
+ const searchedPaths = pathsToCheck.map((p) => {
872
+ const exists = fs2.existsSync(p);
873
+ let matchingDirs;
874
+ if (exists) {
875
+ try {
876
+ const entries = fs2.readdirSync(p, { withFileTypes: true });
877
+ matchingDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => {
878
+ const lowerName = name.toLowerCase();
879
+ const lowerEncoded = encodedCwd.toLowerCase();
880
+ return lowerName.includes(path2.basename(cwd).toLowerCase()) || lowerEncoded.includes(lowerName) || lowerName.includes(lowerEncoded.slice(-20));
881
+ }).slice(0, 5);
882
+ } catch {
883
+ matchingDirs = void 0;
1001
884
  }
1002
- } catch {
1003
- continue;
1004
885
  }
1005
- }
1006
- return projects;
886
+ return { path: p, exists, matchingDirs };
887
+ });
888
+ return {
889
+ cwd,
890
+ encodedCwd,
891
+ searchedPaths,
892
+ platform: process.platform,
893
+ home
894
+ };
1007
895
  }
1008
- function scanUsageDataFromPath(projectPath, options = {}) {
1009
- const days = options.days ?? 30;
896
+ function scanAllProjects(options = {}) {
897
+ const projectsDirs = getClaudeProjectsDirs();
898
+ if (projectsDirs.length === 0) {
899
+ debugLog("No project directories found");
900
+ return null;
901
+ }
902
+ const days = options.days ?? 0;
1010
903
  let cutoffDate = null;
1011
904
  if (days > 0) {
1012
905
  const cutoff = /* @__PURE__ */ new Date();
@@ -1026,13 +919,31 @@ function scanUsageDataFromPath(projectPath, options = {}) {
1026
919
  const dailyData = {};
1027
920
  let firstTimestamp = null;
1028
921
  let lastTimestamp = null;
1029
- const jsonlFiles = findJsonlFiles(projectPath);
1030
- sessionsCount = jsonlFiles.length;
922
+ const allJsonlFiles = [];
923
+ for (const projectsDir of projectsDirs) {
924
+ try {
925
+ const entries = fs2.readdirSync(projectsDir, { withFileTypes: true });
926
+ for (const entry of entries) {
927
+ if (!entry.isDirectory()) continue;
928
+ if (entry.name.startsWith(".")) continue;
929
+ const projectPath = path2.join(projectsDir, entry.name);
930
+ const jsonlFiles = findJsonlFiles(projectPath);
931
+ allJsonlFiles.push(...jsonlFiles);
932
+ }
933
+ } catch {
934
+ continue;
935
+ }
936
+ }
937
+ if (allJsonlFiles.length === 0) {
938
+ debugLog("No JSONL files found in any project");
939
+ return null;
940
+ }
941
+ sessionsCount = allJsonlFiles.length;
1031
942
  const { onProgress } = options;
1032
- for (let i = 0; i < jsonlFiles.length; i++) {
1033
- const filePath = jsonlFiles[i];
943
+ for (let i = 0; i < allJsonlFiles.length; i++) {
944
+ const filePath = allJsonlFiles[i];
1034
945
  if (onProgress) {
1035
- onProgress(i + 1, jsonlFiles.length);
946
+ onProgress(i + 1, allJsonlFiles.length);
1036
947
  }
1037
948
  const projectName = extractProjectName(filePath);
1038
949
  if (!projects[projectName]) {
@@ -1133,7 +1044,7 @@ function scanUsageDataFromPath(projectPath, options = {}) {
1133
1044
  models: data.models
1134
1045
  })).sort((a, b) => a.date.localeCompare(b.date));
1135
1046
  const credentials = readCredentials();
1136
- const sessionFingerprint = generateSessionFingerprint(jsonlFiles);
1047
+ const sessionFingerprint = generateSessionFingerprint(allJsonlFiles);
1137
1048
  return {
1138
1049
  version: CCGATHER_JSON_VERSION,
1139
1050
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1164,87 +1075,32 @@ function scanUsageDataFromPath(projectPath, options = {}) {
1164
1075
  sessionFingerprint
1165
1076
  };
1166
1077
  }
1167
- function getSessionPathDebugInfo() {
1168
- const home = os2.homedir();
1169
- const cwd = process.cwd();
1170
- const encodedCwd = encodePathLikeClaude(cwd);
1171
- const pathsToCheck = [
1172
- path2.join(home, ".config", "claude", "projects"),
1173
- path2.join(home, ".claude", "projects")
1174
- ];
1175
- const configDir = process.env.CLAUDE_CONFIG_DIR;
1176
- if (configDir) {
1177
- pathsToCheck.unshift(path2.join(configDir, "projects"));
1178
- }
1179
- if (process.platform === "win32" && process.env.APPDATA) {
1180
- pathsToCheck.unshift(path2.join(process.env.APPDATA, "claude", "projects"));
1181
- }
1182
- const searchedPaths = pathsToCheck.map((p) => {
1183
- const exists = fs2.existsSync(p);
1184
- let matchingDirs;
1185
- if (exists) {
1186
- try {
1187
- const entries = fs2.readdirSync(p, { withFileTypes: true });
1188
- matchingDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => {
1189
- const lowerName = name.toLowerCase();
1190
- const lowerEncoded = encodedCwd.toLowerCase();
1191
- return lowerName.includes(path2.basename(cwd).toLowerCase()) || lowerEncoded.includes(lowerName) || lowerName.includes(lowerEncoded.slice(-20));
1192
- }).slice(0, 5);
1193
- } catch {
1194
- matchingDirs = void 0;
1078
+ function getAllSessionsCount() {
1079
+ const projectsDirs = getClaudeProjectsDirs();
1080
+ let count = 0;
1081
+ for (const projectsDir of projectsDirs) {
1082
+ try {
1083
+ const entries = fs2.readdirSync(projectsDir, { withFileTypes: true });
1084
+ for (const entry of entries) {
1085
+ if (!entry.isDirectory()) continue;
1086
+ if (entry.name.startsWith(".")) continue;
1087
+ const projectPath = path2.join(projectsDir, entry.name);
1088
+ count += findJsonlFiles(projectPath).length;
1195
1089
  }
1090
+ } catch {
1091
+ continue;
1196
1092
  }
1197
- return { path: p, exists, matchingDirs };
1198
- });
1199
- return {
1200
- cwd,
1201
- encodedCwd,
1202
- searchedPaths,
1203
- platform: process.platform,
1204
- home
1205
- };
1206
- }
1207
- var CCGATHER_FILE = ".ccgather";
1208
- function getCcgatherFilePath() {
1209
- return path2.join(process.cwd(), CCGATHER_FILE);
1210
- }
1211
- function loadProjectLink() {
1212
- try {
1213
- const filePath = getCcgatherFilePath();
1214
- if (!fs2.existsSync(filePath)) {
1215
- return null;
1216
- }
1217
- const content = fs2.readFileSync(filePath, "utf-8");
1218
- const data = JSON.parse(content);
1219
- if (!data.claudeProjectPath || !data.folderName) {
1220
- return null;
1221
- }
1222
- if (!fs2.existsSync(data.claudeProjectPath)) {
1223
- return null;
1224
- }
1225
- return data;
1226
- } catch {
1227
- return null;
1228
1093
  }
1094
+ return count;
1229
1095
  }
1230
- function saveProjectLink(claudeProjectPath, folderName) {
1231
- try {
1232
- const filePath = getCcgatherFilePath();
1233
- const data = {
1234
- claudeProjectPath,
1235
- folderName,
1236
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
1237
- };
1238
- fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
1239
- return true;
1240
- } catch {
1241
- return false;
1242
- }
1096
+ function hasAnySessions() {
1097
+ return getAllSessionsCount() > 0;
1243
1098
  }
1244
1099
 
1245
1100
  // src/commands/submit.ts
1246
1101
  init_ui();
1247
1102
  function ccgatherToUsageData(data) {
1103
+ const opusCheck = hasOpusUsageInProject(data.dailyUsage);
1248
1104
  return {
1249
1105
  totalTokens: data.usage.totalTokens,
1250
1106
  totalCost: data.usage.totalCost,
@@ -1260,7 +1116,9 @@ function ccgatherToUsageData(data) {
1260
1116
  authMethod: data.account?.authMethod || "unknown",
1261
1117
  rawSubscriptionType: data.account?.rawSubscriptionType || null,
1262
1118
  dailyUsage: data.dailyUsage || [],
1263
- sessionFingerprint: data.sessionFingerprint
1119
+ sessionFingerprint: data.sessionFingerprint,
1120
+ hasOpusUsage: opusCheck.detected,
1121
+ opusModels: opusCheck.opusModels
1264
1122
  };
1265
1123
  }
1266
1124
  async function submitToServer(data) {
@@ -1285,6 +1143,7 @@ async function submitToServer(data) {
1285
1143
  cacheReadTokens: data.cacheReadTokens,
1286
1144
  cacheWriteTokens: data.cacheWriteTokens,
1287
1145
  daysTracked: data.daysTracked,
1146
+ // Plan info for badge display (not league placement)
1288
1147
  ccplan: data.ccplan,
1289
1148
  rateLimitTier: data.rateLimitTier,
1290
1149
  authMethod: data.authMethod,
@@ -1293,9 +1152,9 @@ async function submitToServer(data) {
1293
1152
  dailyUsage: data.dailyUsage,
1294
1153
  // Session fingerprint for duplicate prevention
1295
1154
  sessionFingerprint: data.sessionFingerprint,
1296
- // League placement audit trail
1297
- leagueReason: data.leagueReason,
1298
- leagueReasonDetails: data.leagueReasonDetails
1155
+ // Opus info for badge display
1156
+ hasOpusUsage: data.hasOpusUsage,
1157
+ opusModels: data.opusModels
1299
1158
  })
1300
1159
  });
1301
1160
  if (!response.ok) {
@@ -1385,6 +1244,20 @@ async function verifyToken() {
1385
1244
  return { valid: false };
1386
1245
  }
1387
1246
  }
1247
+ function getPlanColor(plan) {
1248
+ switch (plan.toLowerCase()) {
1249
+ case "max":
1250
+ return colors.max;
1251
+ case "pro":
1252
+ return colors.pro;
1253
+ case "team":
1254
+ return colors.team;
1255
+ case "enterprise":
1256
+ return colors.team;
1257
+ default:
1258
+ return colors.free;
1259
+ }
1260
+ }
1388
1261
  async function submit(options) {
1389
1262
  console.log(header("Submit Usage Data", "\u{1F4E4}"));
1390
1263
  const config = getConfig();
@@ -1419,205 +1292,48 @@ async function submit(options) {
1419
1292
  }
1420
1293
  const username = tokenCheck.username || config.get("username");
1421
1294
  verifySpinner.succeed(colors.success(`Authenticated as ${colors.white(username || "unknown")}`));
1422
- const projectName = getCurrentProjectName();
1423
- let selectedProjectPath = null;
1424
- let selectedFolderName = null;
1425
- let linkSource = null;
1426
- const savedLink = loadProjectLink();
1427
- if (savedLink) {
1428
- selectedProjectPath = savedLink.claudeProjectPath;
1429
- selectedFolderName = savedLink.folderName;
1430
- linkSource = "saved";
1295
+ if (!hasAnySessions()) {
1431
1296
  console.log(`
1432
- ${colors.success("\u2713")} ${colors.muted("Using saved project link:")}`);
1433
- console.log(` ${colors.dim(savedLink.folderName)}`);
1434
- }
1435
- if (!selectedProjectPath && hasProjectSessions()) {
1436
- const autoMatchedPath = getProjectPath();
1437
- if (autoMatchedPath) {
1438
- selectedProjectPath = autoMatchedPath;
1439
- selectedFolderName = autoMatchedPath.split(/[/\\]/).pop() || null;
1440
- linkSource = "auto";
1441
- console.log(`
1442
- ${colors.success("\u2713")} ${colors.muted("Auto-matched project:")}`);
1443
- console.log(` ${colors.dim(selectedFolderName || projectName)}`);
1444
- if (selectedFolderName) {
1445
- saveProjectLink(selectedProjectPath, selectedFolderName);
1446
- console.log(` ${colors.dim("(saved to .ccgather)")}`);
1447
- }
1448
- }
1449
- }
1450
- if (!selectedProjectPath) {
1451
- const availableProjects = getAllProjectFolders();
1452
- if (availableProjects.length === 0) {
1453
- console.log(`
1454
1297
  ${error("No Claude Code sessions found.")}`);
1455
- console.log();
1456
- console.log(` ${colors.muted("This usually means:")}`);
1457
- console.log(` ${colors.muted(" \u2022 You haven't used Claude Code yet, or")}`);
1458
- console.log(` ${colors.muted(" \u2022 You ran Claude Code but didn't send any messages")}`);
1459
- console.log();
1460
- console.log(
1461
- ` ${colors.warning("\u{1F4A1} Tip:")} ${colors.muted("Sessions are only created after you")}`
1462
- );
1463
- console.log(` ${colors.muted(" send at least one message in Claude Code.")}`);
1464
- console.log();
1465
- const debugInfo = getSessionPathDebugInfo();
1466
- console.log(` ${colors.dim("\u2500".repeat(40))}`);
1467
- console.log(` ${colors.muted("Searched paths:")}`);
1468
- for (const pathInfo of debugInfo.searchedPaths) {
1469
- const status = pathInfo.exists ? colors.success("\u2713") : colors.error("\u2717");
1470
- console.log(` ${status} ${pathInfo.path}`);
1471
- }
1472
- console.log();
1473
- process.exit(1);
1474
- }
1475
- console.log(
1476
- `
1477
- ${colors.warning("\u26A0")} ${colors.muted("No session found for current directory:")}`
1478
- );
1479
- console.log(` ${colors.dim(projectName)}`);
1298
+ console.log();
1299
+ console.log(` ${colors.muted("This usually means:")}`);
1300
+ console.log(` ${colors.muted(" \u2022 You haven't used Claude Code yet, or")}`);
1301
+ console.log(` ${colors.muted(" \u2022 You ran Claude Code but didn't send any messages")}`);
1480
1302
  console.log();
1481
1303
  console.log(
1482
- ` ${colors.muted("Found")} ${colors.white(availableProjects.length.toString())} ${colors.muted("project(s) with Claude Code sessions:")}`
1304
+ ` ${colors.warning("\u{1F4A1} Tip:")} ${colors.muted("Sessions are only created after you")}`
1483
1305
  );
1306
+ console.log(` ${colors.muted(" send at least one message in Claude Code.")}`);
1484
1307
  console.log();
1485
- const choices = availableProjects.map((p) => ({
1486
- name: `${colors.white(p.displayName)} ${colors.dim(`(${p.folderName.slice(0, 40)}${p.folderName.length > 40 ? "..." : ""})`)}`,
1487
- value: p.fullPath,
1488
- short: p.displayName
1489
- }));
1490
- choices.push({
1491
- name: colors.dim("Cancel"),
1492
- value: "__cancel__",
1493
- short: "Cancel"
1494
- });
1495
- const { selectedProject } = await import_inquirer2.default.prompt([
1496
- {
1497
- type: "list",
1498
- name: "selectedProject",
1499
- message: "Select a project to link with this folder:",
1500
- choices,
1501
- loop: false
1502
- }
1503
- ]);
1504
- if (selectedProject === "__cancel__") {
1505
- console.log(`
1506
- ${colors.muted("Cancelled.")}
1507
- `);
1508
- process.exit(0);
1509
- }
1510
- selectedProjectPath = selectedProject;
1511
- const selected = availableProjects.find((p) => p.fullPath === selectedProject);
1512
- selectedFolderName = selected?.folderName || null;
1513
- linkSource = "manual";
1514
- if (selected && selectedProjectPath) {
1515
- saveProjectLink(selectedProjectPath, selected.folderName);
1516
- console.log(`
1517
- ${success(`Linked: ${selected.displayName}`)}`);
1518
- console.log(` ${colors.dim("(saved to .ccgather - will auto-use next time)")}`);
1308
+ const debugInfo = getSessionPathDebugInfo();
1309
+ console.log(` ${colors.dim("\u2500".repeat(40))}`);
1310
+ console.log(` ${colors.muted("Searched paths:")}`);
1311
+ for (const pathInfo of debugInfo.searchedPaths) {
1312
+ const status = pathInfo.exists ? colors.success("\u2713") : colors.error("\u2717");
1313
+ console.log(` ${status} ${pathInfo.path}`);
1519
1314
  }
1315
+ console.log();
1316
+ process.exit(1);
1520
1317
  }
1521
- let usageData = null;
1522
- const allProjects = getAllProjectFolders();
1523
- const selectedInfo = allProjects.find((p) => p.fullPath === selectedProjectPath);
1524
- const displayProjectName = selectedInfo?.displayName || selectedFolderName || projectName;
1525
1318
  console.log(`
1526
- ${colors.muted("Project:")} ${colors.white(displayProjectName)}`);
1527
- console.log(` ${colors.muted("Scanning sessions...")}`);
1528
- const scannedData = scanUsageDataFromPath(selectedProjectPath, {
1319
+ ${colors.muted("Scanning all Claude Code sessions...")}`);
1320
+ const totalSessions = getAllSessionsCount();
1321
+ console.log(` ${colors.dim(`Found ${totalSessions} session file(s)`)}`);
1322
+ const scannedData = scanAllProjects({
1529
1323
  onProgress: (current, total) => {
1530
1324
  progressBar(current, total, "Scanning");
1531
1325
  }
1532
1326
  });
1533
1327
  await sleep(200);
1534
- if (scannedData) {
1535
- usageData = ccgatherToUsageData(scannedData);
1536
- console.log(` ${success("Scan complete!")}`);
1537
- }
1538
- if (!usageData) {
1328
+ if (!scannedData) {
1539
1329
  console.log(`
1540
1330
  ${error("No usage data found.")}`);
1541
1331
  console.log(` ${colors.muted("Make sure you have used Claude Code at least once.")}
1542
1332
  `);
1543
1333
  process.exit(1);
1544
1334
  }
1545
- const opusCheck = hasOpusUsageInProject(usageData.dailyUsage);
1546
- const oldDataCheck = hasOldData(usageData.dailyUsage, 30);
1547
- let finalPlan = usageData.ccplan || "free";
1548
- let planDetectionReason = "current";
1549
- if (opusCheck.detected) {
1550
- finalPlan = "max";
1551
- planDetectionReason = "opus";
1552
- console.log(planDetectionSection("Plan Detection", "\u{1F680}"));
1553
- for (const line of maxVerifiedMessage(opusCheck.opusModels)) {
1554
- console.log(line);
1555
- }
1556
- } else if (oldDataCheck.hasOldData) {
1557
- planDetectionReason = "user_choice";
1558
- console.log(planDetectionSection("Past Data Detected", "\u{1F4C5}"));
1559
- for (const line of pastDataWarningMessage(
1560
- oldDataCheck.oldestDate || "",
1561
- oldDataCheck.daysSinceOldest
1562
- )) {
1563
- console.log(line);
1564
- }
1565
- for (const line of trustMessage()) {
1566
- console.log(line);
1567
- }
1568
- const planChoices = [
1569
- {
1570
- name: `${colors.pro("\u26A1")} Pro plan`,
1571
- value: "pro",
1572
- short: "Pro"
1573
- },
1574
- {
1575
- name: `${colors.free("\u26AA")} Free plan`,
1576
- value: "free",
1577
- short: "Free"
1578
- },
1579
- {
1580
- name: `${colors.dim("?")} Can't remember ${colors.dim("(submit as Free)")}`,
1581
- value: "free_default",
1582
- short: "Free (default)"
1583
- }
1584
- ];
1585
- const { selectedPlan } = await import_inquirer2.default.prompt([
1586
- {
1587
- type: "list",
1588
- name: "selectedPlan",
1589
- message: "Which plan were you using at that time?",
1590
- choices: planChoices,
1591
- default: 0
1592
- }
1593
- ]);
1594
- finalPlan = selectedPlan === "free_default" ? "free" : selectedPlan;
1595
- console.log();
1596
- console.log(` ${success(`Selected: ${finalPlan.toUpperCase()}`)}`);
1597
- } else {
1598
- planDetectionReason = "current";
1599
- console.log(
1600
- ` ${colors.dim("\u{1F4CB} League: reading plan info only (subscriptionType, rateLimitTier)")}`
1601
- );
1602
- if (usageData.ccplan) {
1603
- for (const line of currentPlanMessage(usageData.ccplan)) {
1604
- console.log(line);
1605
- }
1606
- }
1607
- }
1608
- usageData.ccplan = finalPlan;
1609
- usageData.leagueReason = planDetectionReason === "opus" ? "opus" : planDetectionReason === "user_choice" ? "user_choice" : "credential";
1610
- if (planDetectionReason === "opus") {
1611
- usageData.leagueReasonDetails = `Opus verified: ${opusCheck.opusModels.join(", ")}`;
1612
- } else if (planDetectionReason === "user_choice") {
1613
- usageData.leagueReasonDetails = `User selected: ${finalPlan} (data >${oldDataCheck.daysSinceOldest}d old)`;
1614
- } else {
1615
- usageData.leagueReasonDetails = `Credential: ${usageData.ccplan || "free"}`;
1616
- }
1617
- usageData.dailyUsage = usageData.dailyUsage.map((daily) => ({
1618
- ...daily,
1619
- ccplan: finalPlan
1620
- }));
1335
+ const usageData = ccgatherToUsageData(scannedData);
1336
+ console.log(` ${success("Scan complete!")}`);
1621
1337
  console.log();
1622
1338
  const formatDate = (dateStr) => {
1623
1339
  if (!dateStr) return "------";
@@ -1629,21 +1345,31 @@ async function submit(options) {
1629
1345
  };
1630
1346
  const dateRange = usageData.firstUsed && usageData.lastUsed ? `${formatDate(usageData.firstUsed)} ~ ${formatDate(usageData.lastUsed)}` : "";
1631
1347
  const daysTrackedDisplay = dateRange ? `${usageData.daysTracked} days ${colors.dim(`(${dateRange})`)}` : usageData.daysTracked.toString();
1348
+ const levelProgress = getLevelProgress(usageData.totalTokens);
1349
+ const currentLevel = levelProgress.current;
1632
1350
  const summaryLines = [
1633
1351
  `${colors.muted("Total Cost")} ${colors.success(formatCost(usageData.totalCost))}`,
1634
1352
  `${colors.muted("Total Tokens")} ${colors.primary(formatNumber(usageData.totalTokens))}`,
1635
- `${colors.muted("Period")} ${colors.warning(daysTrackedDisplay)}`
1353
+ `${colors.muted("Period")} ${colors.warning(daysTrackedDisplay)}`,
1354
+ `${colors.muted("Level")} ${currentLevel.icon} ${currentLevel.color(`${currentLevel.name}`)}`
1636
1355
  ];
1637
1356
  if (usageData.ccplan) {
1357
+ const planColor = getPlanColor(usageData.ccplan);
1358
+ summaryLines.push(
1359
+ `${colors.muted("Plan")} ${planColor(usageData.ccplan.toUpperCase())} ${colors.dim("(badge)")}`
1360
+ );
1361
+ }
1362
+ if (usageData.hasOpusUsage) {
1638
1363
  summaryLines.push(
1639
- `${colors.muted("CCplan")} ${colors.cyan(usageData.ccplan.toUpperCase())}`
1364
+ `${colors.muted("Models")} ${colors.max("\u2726")} ${colors.max("Opus User")}`
1640
1365
  );
1641
1366
  }
1642
1367
  console.log(createBox(summaryLines));
1643
1368
  console.log();
1644
- if (usageData.dailyUsage.length > 0) {
1645
- console.log(` ${colors.dim(`Daily records: ${usageData.dailyUsage.length} days`)}`);
1646
- }
1369
+ const projectCount = Object.keys(scannedData.projects).length;
1370
+ console.log(
1371
+ ` ${colors.dim(`Scanned ${projectCount} project(s), ${usageData.dailyUsage.length} day(s) of data`)}`
1372
+ );
1647
1373
  const { confirmSubmit } = await import_inquirer2.default.prompt([
1648
1374
  {
1649
1375
  type: "confirm",
@@ -1687,8 +1413,6 @@ async function submit(options) {
1687
1413
  console.log();
1688
1414
  console.log(sectionHeader("\u2B06\uFE0F", "Level Progress"));
1689
1415
  console.log();
1690
- const levelProgress = getLevelProgress(usageData.totalTokens);
1691
- const currentLevel = levelProgress.current;
1692
1416
  await sleep(200);
1693
1417
  console.log(
1694
1418
  ` ${currentLevel.icon} ${currentLevel.color(`Level ${currentLevel.level}`)} ${colors.muted("\u2022")} ${colors.white(currentLevel.name)}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccgather",
3
- "version": "1.5.0",
3
+ "version": "2.0.0",
4
4
  "description": "CLI tool for syncing Claude Code usage data to CCgather leaderboard",
5
5
  "bin": {
6
6
  "ccgather": "dist/index.js",
@@ -12,7 +12,9 @@
12
12
  "build": "tsup",
13
13
  "dev": "tsup --watch",
14
14
  "start": "node dist/index.js",
15
- "typecheck": "tsc --noEmit"
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest"
16
18
  },
17
19
  "keywords": [
18
20
  "claude",
@@ -39,7 +41,8 @@
39
41
  "@types/node": "^22.10.2",
40
42
  "@types/update-notifier": "^6.0.8",
41
43
  "tsup": "^8.3.5",
42
- "typescript": "^5.7.2"
44
+ "typescript": "^5.7.2",
45
+ "vitest": "^3.0.0"
43
46
  },
44
47
  "engines": {
45
48
  "node": ">=18"