ccgather 2.0.31 → 2.0.33

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 +262 -163
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -107,19 +107,15 @@ function createProfessionalHeader() {
107
107
  );
108
108
  return lines;
109
109
  }
110
- async function printAnimatedBox(lines, width = 47, lineDelay = 60) {
111
- const top = colors.dim(` ${box.topLeft}${box.horizontal.repeat(width)}${box.topRight}`);
112
- const bottom = colors.dim(` ${box.bottomLeft}${box.horizontal.repeat(width)}${box.bottomRight}`);
113
- console.log(top);
114
- await sleep(lineDelay / 2);
115
- for (const line of lines) {
110
+ function createBox(lines, width = 47) {
111
+ const paddedLines = lines.map((line) => {
116
112
  const visibleLength = getDisplayWidth(line);
117
113
  const padding = width - 2 - visibleLength;
118
- const paddedLine = `${box.vertical} ${line}${" ".repeat(Math.max(0, padding))} ${box.vertical}`;
119
- console.log(colors.dim(" ") + paddedLine);
120
- await sleep(lineDelay);
121
- }
122
- console.log(bottom);
114
+ return `${box.vertical} ${line}${" ".repeat(Math.max(0, padding))} ${box.vertical}`;
115
+ });
116
+ const top = colors.dim(` ${box.topLeft}${box.horizontal.repeat(width)}${box.topRight}`);
117
+ const bottom = colors.dim(` ${box.bottomLeft}${box.horizontal.repeat(width)}${box.bottomRight}`);
118
+ return [top, ...paddedLines.map((l) => colors.dim(" ") + l), bottom].join("\n");
123
119
  }
124
120
  function _stripAnsi(str) {
125
121
  return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
@@ -247,57 +243,6 @@ async function printAnimatedHeader() {
247
243
  }
248
244
  console.log();
249
245
  }
250
- async function slotMachineRank(finalRank, label, medal, iterations = 12, previousRank) {
251
- const maxRank = Math.max(finalRank * 3, 100);
252
- for (let i = 0; i < iterations; i++) {
253
- const fakeRank = Math.floor(Math.random() * maxRank) + 1;
254
- const speed = Math.min(30 + i * 15, 150);
255
- process.stdout.write(
256
- `\r ${medal} ${colors.muted(label)} ${colors.dim(`#${fakeRank}`)} `
257
- );
258
- await sleep(speed);
259
- }
260
- let changeText = "";
261
- if (previousRank && previousRank !== finalRank) {
262
- const change = previousRank - finalRank;
263
- if (change > 0) {
264
- changeText = ` ${colors.success(`\u2191${change}`)}`;
265
- } else if (change < 0) {
266
- changeText = ` ${colors.error(`\u2193${Math.abs(change)}`)}`;
267
- }
268
- }
269
- process.stdout.write(
270
- `\r ${medal} ${colors.muted(label)} ${colors.primary.bold(`#${finalRank}`)}${changeText}
271
- `
272
- );
273
- }
274
- async function animatedProgressBar(targetPercent, barWidth = 20, stepDelay = 25) {
275
- const steps = Math.min(targetPercent, 20);
276
- const stepSize = targetPercent / steps;
277
- for (let i = 0; i <= steps; i++) {
278
- const currentPercent = Math.round(i * stepSize);
279
- const filled = Math.round(currentPercent / 100 * barWidth);
280
- const empty = barWidth - filled;
281
- const bar = colors.primary("\u2588".repeat(filled)) + colors.dim("\u2591".repeat(empty));
282
- process.stdout.write(`\r [${bar}] ${colors.white(`${currentPercent}%`)} `);
283
- await sleep(stepDelay);
284
- }
285
- const finalFilled = Math.round(targetPercent / 100 * barWidth);
286
- const finalEmpty = barWidth - finalFilled;
287
- return colors.primary("\u2588".repeat(finalFilled)) + colors.dim("\u2591".repeat(finalEmpty));
288
- }
289
- async function suspenseDots(message, durationMs = 600) {
290
- const frames = ["", ".", "..", "..."];
291
- const frameDelay = 100;
292
- const iterations = Math.ceil(durationMs / (frames.length * frameDelay));
293
- for (let i = 0; i < iterations; i++) {
294
- for (const frame of frames) {
295
- process.stdout.write(`\r ${colors.muted(message)}${colors.primary(frame)} `);
296
- await sleep(frameDelay);
297
- }
298
- }
299
- process.stdout.write("\r" + " ".repeat(50) + "\r");
300
- }
301
246
  async function printAnimatedWelcomeBox(user) {
302
247
  console.log(` \u{1F44B} ${colors.white.bold(`Welcome back, ${user.username}!`)}`);
303
248
  await sleep(50);
@@ -308,7 +253,7 @@ var init_ui = __esm({
308
253
  "use strict";
309
254
  import_chalk = __toESM(require("chalk"));
310
255
  import_string_width = __toESM(require("string-width"));
311
- VERSION = true ? "2.0.31" : "0.0.0";
256
+ VERSION = true ? "2.0.33" : "0.0.0";
312
257
  colors = {
313
258
  primary: import_chalk.default.hex("#DA7756"),
314
259
  // Claude coral
@@ -545,9 +490,9 @@ var import_inquirer2 = __toESM(require("inquirer"));
545
490
  init_config();
546
491
 
547
492
  // src/lib/ccgather-json.ts
548
- var fs2 = __toESM(require("fs"));
549
- var path2 = __toESM(require("path"));
550
- var os2 = __toESM(require("os"));
493
+ var fs3 = __toESM(require("fs"));
494
+ var path3 = __toESM(require("path"));
495
+ var os3 = __toESM(require("os"));
551
496
  var crypto = __toESM(require("crypto"));
552
497
 
553
498
  // src/lib/credentials.ts
@@ -671,6 +616,154 @@ function readCredentials() {
671
616
  };
672
617
  }
673
618
 
619
+ // src/lib/pricing.ts
620
+ var fs2 = __toESM(require("fs"));
621
+ var path2 = __toESM(require("path"));
622
+ var os2 = __toESM(require("os"));
623
+ var LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
624
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
625
+ var FETCH_TIMEOUT_MS = 5e3;
626
+ var FALLBACK_PRICING = {
627
+ "opus-4": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
628
+ "sonnet-4": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
629
+ haiku: { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
630
+ default: { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 }
631
+ };
632
+ var pricingData = null;
633
+ function getCacheFilePath() {
634
+ const configDir = process.platform === "win32" ? path2.join(
635
+ process.env.APPDATA || path2.join(os2.homedir(), "AppData", "Roaming"),
636
+ "ccgather-nodejs"
637
+ ) : path2.join(os2.homedir(), ".config", "ccgather-nodejs");
638
+ return path2.join(configDir, "pricing-cache.json");
639
+ }
640
+ function loadCache() {
641
+ try {
642
+ const cachePath = getCacheFilePath();
643
+ if (!fs2.existsSync(cachePath)) return null;
644
+ const raw = fs2.readFileSync(cachePath, "utf-8");
645
+ const cache = JSON.parse(raw);
646
+ if (Date.now() - cache.fetchedAt > CACHE_TTL_MS) return null;
647
+ if (!cache.models || Object.keys(cache.models).length === 0) return null;
648
+ return cache;
649
+ } catch {
650
+ return null;
651
+ }
652
+ }
653
+ function saveCache(models) {
654
+ try {
655
+ const cachePath = getCacheFilePath();
656
+ const cacheDir = path2.dirname(cachePath);
657
+ if (!fs2.existsSync(cacheDir)) {
658
+ fs2.mkdirSync(cacheDir, { recursive: true });
659
+ }
660
+ const cache = {
661
+ fetchedAt: Date.now(),
662
+ models
663
+ };
664
+ fs2.writeFileSync(cachePath, JSON.stringify(cache), "utf-8");
665
+ } catch {
666
+ }
667
+ }
668
+ function extractClaudePricing(rawData) {
669
+ const result = {};
670
+ for (const [key, value] of Object.entries(rawData)) {
671
+ if (!key.startsWith("claude-")) continue;
672
+ const inputCostPerToken = value.input_cost_per_token;
673
+ const outputCostPerToken = value.output_cost_per_token;
674
+ if (inputCostPerToken == null || outputCostPerToken == null) continue;
675
+ const input = inputCostPerToken * 1e6;
676
+ const output = outputCostPerToken * 1e6;
677
+ const cacheWritePerToken = value.cache_creation_input_token_cost;
678
+ const cacheReadPerToken = value.cache_read_input_token_cost;
679
+ const cacheWrite = cacheWritePerToken != null ? cacheWritePerToken * 1e6 : input * 1.25;
680
+ const cacheRead = cacheReadPerToken != null ? cacheReadPerToken * 1e6 : input * 0.1;
681
+ const pricing = {
682
+ input: Math.round(input * 1e3) / 1e3,
683
+ output: Math.round(output * 1e3) / 1e3,
684
+ cacheWrite: Math.round(cacheWrite * 1e3) / 1e3,
685
+ cacheRead: Math.round(cacheRead * 1e3) / 1e3
686
+ };
687
+ result[key] = pricing;
688
+ const withoutDate = key.replace(/-\d{8}$/, "");
689
+ if (withoutDate !== key && !result[withoutDate]) {
690
+ result[withoutDate] = pricing;
691
+ }
692
+ const withoutVersion = key.replace(/-v\d+:\d+$/, "").replace(/-\d{8}$/, "");
693
+ if (withoutVersion !== key && withoutVersion !== withoutDate && !result[withoutVersion]) {
694
+ result[withoutVersion] = pricing;
695
+ }
696
+ }
697
+ return result;
698
+ }
699
+ async function initPricing() {
700
+ if (pricingData) return;
701
+ const cached = loadCache();
702
+ if (cached) {
703
+ pricingData = cached.models;
704
+ return;
705
+ }
706
+ try {
707
+ const controller = new AbortController();
708
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
709
+ const response = await fetch(LITELLM_PRICING_URL, {
710
+ signal: controller.signal
711
+ });
712
+ clearTimeout(timeout);
713
+ if (response.ok) {
714
+ const rawData = await response.json();
715
+ const extracted = extractClaudePricing(rawData);
716
+ if (Object.keys(extracted).length > 0) {
717
+ pricingData = extracted;
718
+ saveCache(extracted);
719
+ return;
720
+ }
721
+ }
722
+ } catch {
723
+ }
724
+ pricingData = null;
725
+ }
726
+ function matchModel(model) {
727
+ if (pricingData) {
728
+ if (pricingData[model]) {
729
+ return pricingData[model];
730
+ }
731
+ const withoutDate = model.replace(/-\d{8}$/, "");
732
+ if (pricingData[withoutDate]) {
733
+ return pricingData[withoutDate];
734
+ }
735
+ const withoutVersion = model.replace(/-v\d+:\d+$/, "").replace(/-\d{8}$/, "");
736
+ if (pricingData[withoutVersion]) {
737
+ return pricingData[withoutVersion];
738
+ }
739
+ const modelLower2 = model.toLowerCase();
740
+ let bestMatch = null;
741
+ for (const [key, pricing] of Object.entries(pricingData)) {
742
+ if (modelLower2.startsWith(key.toLowerCase()) || key.toLowerCase().startsWith(modelLower2.replace(/-\d{8}$/, ""))) {
743
+ if (!bestMatch || key.length > bestMatch.key.length) {
744
+ bestMatch = { key, pricing };
745
+ }
746
+ }
747
+ }
748
+ if (bestMatch) {
749
+ return bestMatch.pricing;
750
+ }
751
+ }
752
+ const modelLower = model.toLowerCase();
753
+ if (modelLower.includes("opus")) return FALLBACK_PRICING["opus-4"];
754
+ if (modelLower.includes("haiku")) return FALLBACK_PRICING["haiku"];
755
+ if (modelLower.includes("sonnet")) return FALLBACK_PRICING["sonnet-4"];
756
+ return FALLBACK_PRICING["default"];
757
+ }
758
+ function estimateCost(model, inputTokens, outputTokens, cacheWriteTokens = 0, cacheReadTokens = 0) {
759
+ const price = matchModel(model);
760
+ const inputCost = inputTokens / 1e6 * price.input;
761
+ const outputCost = outputTokens / 1e6 * price.output;
762
+ const cacheWriteCost = cacheWriteTokens / 1e6 * price.cacheWrite;
763
+ const cacheReadCost = cacheReadTokens / 1e6 * price.cacheRead;
764
+ return Math.round((inputCost + outputCost + cacheWriteCost + cacheReadCost) * 100) / 100;
765
+ }
766
+
674
767
  // src/lib/ccgather-json.ts
675
768
  function hasOpusUsageInProject(dailyUsage) {
676
769
  const opusModels = /* @__PURE__ */ new Set();
@@ -707,26 +800,26 @@ function extractProjectName(filePath) {
707
800
  }
708
801
  function getClaudeProjectsDirs() {
709
802
  const dirs = [];
710
- const home = os2.homedir();
803
+ const home = os3.homedir();
711
804
  const configDir = process.env.CLAUDE_CONFIG_DIR;
712
805
  if (configDir) {
713
- const envPath = path2.join(configDir, "projects");
806
+ const envPath = path3.join(configDir, "projects");
714
807
  dirs.push(envPath);
715
808
  }
716
809
  if (process.platform === "win32") {
717
810
  const appData = process.env.APPDATA;
718
811
  if (appData) {
719
- const appDataPath = path2.join(appData, "claude", "projects");
812
+ const appDataPath = path3.join(appData, "claude", "projects");
720
813
  dirs.push(appDataPath);
721
814
  }
722
815
  }
723
- const xdgPath = path2.join(home, ".config", "claude", "projects");
816
+ const xdgPath = path3.join(home, ".config", "claude", "projects");
724
817
  dirs.push(xdgPath);
725
- const legacyPath = path2.join(home, ".claude", "projects");
818
+ const legacyPath = path3.join(home, ".claude", "projects");
726
819
  dirs.push(legacyPath);
727
820
  const uniqueDirs = [...new Set(dirs)];
728
821
  const existingDirs = uniqueDirs.filter((dir) => {
729
- return fs2.existsSync(dir);
822
+ return fs3.existsSync(dir);
730
823
  });
731
824
  return existingDirs;
732
825
  }
@@ -745,9 +838,9 @@ function encodePathLikeClaude(inputPath) {
745
838
  function findJsonlFiles(dir) {
746
839
  const files = [];
747
840
  try {
748
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
841
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
749
842
  for (const entry of entries) {
750
- const fullPath = path2.join(dir, entry.name);
843
+ const fullPath = path3.join(dir, entry.name);
751
844
  if (entry.isDirectory()) {
752
845
  files.push(...findJsonlFiles(fullPath));
753
846
  } else if (entry.name.endsWith(".jsonl")) {
@@ -758,32 +851,11 @@ function findJsonlFiles(dir) {
758
851
  }
759
852
  return files;
760
853
  }
761
- function estimateCost(model, inputTokens, outputTokens, cacheWriteTokens = 0, cacheReadTokens = 0) {
762
- const pricing = {
763
- "claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.5 },
764
- "claude-sonnet-4": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
765
- "claude-haiku": { input: 0.25, output: 1.25, cacheWrite: 0.3125, cacheRead: 0.025 },
766
- default: { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 }
767
- };
768
- let modelKey = "default";
769
- for (const key of Object.keys(pricing)) {
770
- if (model.includes(key.replace("claude-", ""))) {
771
- modelKey = key;
772
- break;
773
- }
774
- }
775
- const price = pricing[modelKey];
776
- const inputCost = inputTokens / 1e6 * price.input;
777
- const outputCost = outputTokens / 1e6 * price.output;
778
- const cacheWriteCost = cacheWriteTokens / 1e6 * price.cacheWrite;
779
- const cacheReadCost = cacheReadTokens / 1e6 * price.cacheRead;
780
- return Math.round((inputCost + outputCost + cacheWriteCost + cacheReadCost) * 100) / 100;
781
- }
782
854
  function generateSessionHash(filePath, maxLines = 50) {
783
855
  try {
784
- const content = fs2.readFileSync(filePath, "utf-8");
856
+ const content = fs3.readFileSync(filePath, "utf-8");
785
857
  const lines = content.split("\n").slice(0, maxLines).join("\n");
786
- const fileName = path2.basename(filePath);
858
+ const fileName = path3.basename(filePath);
787
859
  const hashInput = `${fileName}:${lines}`;
788
860
  return crypto.createHash("sha256").update(hashInput).digest("hex");
789
861
  } catch {
@@ -807,30 +879,30 @@ function generateSessionFingerprint(sessionFiles) {
807
879
  };
808
880
  }
809
881
  function getSessionPathDebugInfo() {
810
- const home = os2.homedir();
882
+ const home = os3.homedir();
811
883
  const cwd = process.cwd();
812
884
  const encodedCwd = encodePathLikeClaude(cwd);
813
885
  const pathsToCheck = [
814
- path2.join(home, ".config", "claude", "projects"),
815
- path2.join(home, ".claude", "projects")
886
+ path3.join(home, ".config", "claude", "projects"),
887
+ path3.join(home, ".claude", "projects")
816
888
  ];
817
889
  const configDir = process.env.CLAUDE_CONFIG_DIR;
818
890
  if (configDir) {
819
- pathsToCheck.unshift(path2.join(configDir, "projects"));
891
+ pathsToCheck.unshift(path3.join(configDir, "projects"));
820
892
  }
821
893
  if (process.platform === "win32" && process.env.APPDATA) {
822
- pathsToCheck.unshift(path2.join(process.env.APPDATA, "claude", "projects"));
894
+ pathsToCheck.unshift(path3.join(process.env.APPDATA, "claude", "projects"));
823
895
  }
824
896
  const searchedPaths = pathsToCheck.map((p) => {
825
- const exists = fs2.existsSync(p);
897
+ const exists = fs3.existsSync(p);
826
898
  let matchingDirs;
827
899
  if (exists) {
828
900
  try {
829
- const entries = fs2.readdirSync(p, { withFileTypes: true });
901
+ const entries = fs3.readdirSync(p, { withFileTypes: true });
830
902
  matchingDirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => {
831
903
  const lowerName = name.toLowerCase();
832
904
  const lowerEncoded = encodedCwd.toLowerCase();
833
- return lowerName.includes(path2.basename(cwd).toLowerCase()) || lowerEncoded.includes(lowerName) || lowerName.includes(lowerEncoded.slice(-20));
905
+ return lowerName.includes(path3.basename(cwd).toLowerCase()) || lowerEncoded.includes(lowerName) || lowerName.includes(lowerEncoded.slice(-20));
834
906
  }).slice(0, 5);
835
907
  } catch {
836
908
  matchingDirs = void 0;
@@ -874,11 +946,11 @@ function scanAllProjects(options = {}) {
874
946
  const allJsonlFiles = [];
875
947
  for (const projectsDir of projectsDirs) {
876
948
  try {
877
- const entries = fs2.readdirSync(projectsDir, { withFileTypes: true });
949
+ const entries = fs3.readdirSync(projectsDir, { withFileTypes: true });
878
950
  for (const entry of entries) {
879
951
  if (!entry.isDirectory()) continue;
880
952
  if (entry.name.startsWith(".")) continue;
881
- const projectPath = path2.join(projectsDir, entry.name);
953
+ const projectPath = path3.join(projectsDir, entry.name);
882
954
  const jsonlFiles = findJsonlFiles(projectPath);
883
955
  allJsonlFiles.push(...jsonlFiles);
884
956
  }
@@ -893,9 +965,6 @@ function scanAllProjects(options = {}) {
893
965
  const { onProgress } = options;
894
966
  for (let i = 0; i < allJsonlFiles.length; i++) {
895
967
  const filePath = allJsonlFiles[i];
896
- if (onProgress) {
897
- onProgress(i + 1, allJsonlFiles.length);
898
- }
899
968
  const projectName = extractProjectName(filePath);
900
969
  if (!projects[projectName]) {
901
970
  projects[projectName] = {
@@ -907,7 +976,7 @@ function scanAllProjects(options = {}) {
907
976
  }
908
977
  projects[projectName].sessions++;
909
978
  try {
910
- const content = fs2.readFileSync(filePath, "utf-8");
979
+ const content = fs3.readFileSync(filePath, "utf-8");
911
980
  const lines = content.split("\n").filter((line) => line.trim());
912
981
  for (const line of lines) {
913
982
  try {
@@ -975,6 +1044,9 @@ function scanAllProjects(options = {}) {
975
1044
  }
976
1045
  } catch {
977
1046
  }
1047
+ if (onProgress) {
1048
+ onProgress(i + 1, allJsonlFiles.length);
1049
+ }
978
1050
  }
979
1051
  const totalTokens = totalInputTokens + totalOutputTokens + totalCacheWrite + totalCacheRead;
980
1052
  if (totalTokens === 0) {
@@ -1031,11 +1103,11 @@ function getAllSessionsCount() {
1031
1103
  let count = 0;
1032
1104
  for (const projectsDir of projectsDirs) {
1033
1105
  try {
1034
- const entries = fs2.readdirSync(projectsDir, { withFileTypes: true });
1106
+ const entries = fs3.readdirSync(projectsDir, { withFileTypes: true });
1035
1107
  for (const entry of entries) {
1036
1108
  if (!entry.isDirectory()) continue;
1037
1109
  if (entry.name.startsWith(".")) continue;
1038
- const projectPath = path2.join(projectsDir, entry.name);
1110
+ const projectPath = path3.join(projectsDir, entry.name);
1039
1111
  count += findJsonlFiles(projectPath).length;
1040
1112
  }
1041
1113
  } catch {
@@ -1050,6 +1122,27 @@ function hasAnySessions() {
1050
1122
 
1051
1123
  // src/commands/submit.ts
1052
1124
  init_ui();
1125
+ async function reportSubmitAttempt(reason, debugInfo) {
1126
+ try {
1127
+ const config = getConfig();
1128
+ const apiUrl = getApiUrl();
1129
+ const token = config.get("apiToken");
1130
+ await fetch(`${apiUrl}/cli/submit-attempt`, {
1131
+ method: "POST",
1132
+ headers: {
1133
+ "Content-Type": "application/json",
1134
+ ...token && { Authorization: `Bearer ${token}` }
1135
+ },
1136
+ body: JSON.stringify({
1137
+ reason,
1138
+ debugInfo,
1139
+ cliVersion: process.env.npm_package_version || "unknown",
1140
+ platform: process.platform
1141
+ })
1142
+ });
1143
+ } catch {
1144
+ }
1145
+ }
1053
1146
  function ccgatherToUsageData(data) {
1054
1147
  const opusCheck = hasOpusUsageInProject(data.dailyUsage);
1055
1148
  return {
@@ -1144,30 +1237,21 @@ function formatBadgeDate(dateStr) {
1144
1237
  if (!dateStr) return (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, ".");
1145
1238
  return dateStr.split("T")[0].replace(/-/g, ".");
1146
1239
  }
1147
- async function displayNewBadges(badges) {
1240
+ function displayNewBadges(badges) {
1148
1241
  if (badges.length === 0) return;
1149
1242
  console.log();
1150
- await suspenseDots("Checking achievements", 800);
1151
- for (let i = 0; i < badges.length; i++) {
1152
- const badge = badges[i];
1243
+ for (const badge of badges) {
1153
1244
  const rarityColor = getRarityColor(badge.rarity);
1154
1245
  const rarityLabel = badge.rarity.toUpperCase();
1155
- if (i > 0) {
1156
- await sleep(300);
1157
- }
1158
1246
  console.log(
1159
1247
  ` \u2728 ${badge.icon} ${colors.white.bold(badge.name)} ${rarityColor(`[${rarityLabel}]`)}`
1160
1248
  );
1161
- await sleep(100);
1162
1249
  console.log(` ${colors.muted(badge.description)}`);
1163
- await sleep(80);
1164
1250
  if (badge.praise) {
1165
1251
  console.log(` ${colors.cyan(`"${badge.praise}"`)}`);
1166
- await sleep(80);
1167
1252
  }
1168
1253
  if (badge.category === "rank") {
1169
1254
  console.log(` ${colors.dim(`\u{1F4C5} Achieved: ${formatBadgeDate(badge.earnedAt)}`)}`);
1170
- await sleep(80);
1171
1255
  }
1172
1256
  }
1173
1257
  }
@@ -1243,6 +1327,7 @@ async function submit(options) {
1243
1327
  }
1244
1328
  const username = tokenCheck.username || config.get("username");
1245
1329
  verifySpinner.succeed(colors.success(`Authenticated as ${colors.white(username || "unknown")}`));
1330
+ await initPricing();
1246
1331
  if (!hasAnySessions()) {
1247
1332
  console.log(`
1248
1333
  ${error("No Claude Code sessions found.")}`);
@@ -1264,6 +1349,7 @@ async function submit(options) {
1264
1349
  console.log(` ${status} ${pathInfo.path}`);
1265
1350
  }
1266
1351
  console.log();
1352
+ await reportSubmitAttempt("no_sessions", debugInfo);
1267
1353
  process.exit(1);
1268
1354
  }
1269
1355
  console.log(`
@@ -1280,15 +1366,22 @@ async function submit(options) {
1280
1366
  if (lastProgress > 0) {
1281
1367
  process.stdout.write("\r" + " ".repeat(60) + "\r");
1282
1368
  }
1369
+ process.stdout.write(` ${colors.muted("Processing...")}`);
1283
1370
  if (!scannedData) {
1371
+ process.stdout.write("\r" + " ".repeat(40) + "\r");
1284
1372
  console.log(` ${colors.error("\u2717")} ${colors.error("No usage data found.")}`);
1285
1373
  console.log(` ${colors.muted("Make sure you have used Claude Code at least once.")}
1286
1374
  `);
1375
+ await reportSubmitAttempt("no_data");
1287
1376
  process.exit(1);
1288
1377
  }
1289
1378
  const usageData = ccgatherToUsageData(scannedData);
1290
- console.log(` ${colors.success("\u2714")} ${colors.success("Scan complete!")}`);
1291
- console.log();
1379
+ process.stdout.write("\r" + " ".repeat(40) + "\r");
1380
+ const submitSpinner = (0, import_ora2.default)({
1381
+ text: "Submitting to CCgather...",
1382
+ color: "cyan"
1383
+ }).start();
1384
+ const result = await submitToServer(usageData);
1292
1385
  const formatDate = (dateStr) => {
1293
1386
  if (!dateStr) return "------";
1294
1387
  const d = new Date(dateStr);
@@ -1299,13 +1392,10 @@ async function submit(options) {
1299
1392
  };
1300
1393
  const dateRange = usageData.firstUsed && usageData.lastUsed ? `${formatDate(usageData.firstUsed)} ~ ${formatDate(usageData.lastUsed)}` : "";
1301
1394
  const daysTrackedDisplay = dateRange ? `${usageData.daysTracked} days ${colors.dim(`(${dateRange})`)}` : usageData.daysTracked.toString();
1302
- const levelProgress = getLevelProgress(usageData.totalTokens);
1303
- const currentLevel = levelProgress.current;
1304
1395
  const summaryLines = [
1305
1396
  `${colors.muted("Total Cost")} \u{1F4B0} ${colors.warning(formatCost(usageData.totalCost))}`,
1306
1397
  `${colors.muted("Total Tokens")} \u26A1 ${colors.primary(formatNumber(usageData.totalTokens))}`,
1307
- `${colors.muted("Period")} \u{1F4C5} ${colors.white(daysTrackedDisplay)}`,
1308
- `${colors.muted("Level")} ${currentLevel.icon} ${currentLevel.color(`${currentLevel.name}`)}`
1398
+ `${colors.muted("Period")} \u{1F4C5} ${colors.white(daysTrackedDisplay)}`
1309
1399
  ];
1310
1400
  if (usageData.ccplan) {
1311
1401
  const planColor = getPlanColor(usageData.ccplan);
@@ -1317,18 +1407,7 @@ async function submit(options) {
1317
1407
  if (usageData.hasOpusUsage) {
1318
1408
  summaryLines.push(`${colors.muted("Models")} ${colors.max("\u2726 Opus User")}`);
1319
1409
  }
1320
- await printAnimatedBox(summaryLines, 52, 60);
1321
- console.log();
1322
1410
  const projectCount = Object.keys(scannedData.projects).length;
1323
- console.log(
1324
- ` ${colors.dim(`Scanned ${projectCount} project(s), ${usageData.dailyUsage.length} day(s) of data`)}`
1325
- );
1326
- console.log();
1327
- const submitSpinner = (0, import_ora2.default)({
1328
- text: "Submitting to CCgather...",
1329
- color: "cyan"
1330
- }).start();
1331
- const result = await submitToServer(usageData);
1332
1411
  if (result.success) {
1333
1412
  submitSpinner.succeed(colors.success("Successfully submitted!"));
1334
1413
  console.log();
@@ -1337,25 +1416,26 @@ async function submit(options) {
1337
1416
  const lineLength = 40 - text.length;
1338
1417
  return ` ${colors.white.bold(text)}${colors.dim("\u2500".repeat(Math.max(0, lineLength)))}`;
1339
1418
  };
1419
+ const accumulatedTokens = result.previous?.totalTokens || usageData.totalTokens;
1420
+ const levelProgress = getLevelProgress(accumulatedTokens);
1421
+ const currentLevel = levelProgress.current;
1422
+ summaryLines.push(
1423
+ `${colors.muted("Level")} ${currentLevel.icon} ${currentLevel.color(`${currentLevel.name}`)}`
1424
+ );
1425
+ console.log(createBox(summaryLines, 52));
1426
+ console.log(
1427
+ ` ${colors.dim(`Scanned ${projectCount} project(s), ${usageData.dailyUsage.length} day(s) of data`)}`
1428
+ );
1340
1429
  if (result.previous) {
1341
1430
  const prev = result.previous;
1431
+ console.log();
1342
1432
  console.log(sectionHeader("\u{1F4E6}", "Server Records"));
1343
- await sleep(40);
1344
1433
  console.log();
1345
1434
  const prevTokens = prev.totalTokens || 0;
1346
1435
  const prevCost = prev.totalCost || 0;
1347
1436
  console.log(
1348
1437
  ` ${colors.muted("Accumulated")} \u26A1 ${colors.primary(formatNumber(prevTokens))} ${colors.dim("\u2502")} \u{1F4B0} ${colors.warning(formatCost(prevCost))}`
1349
1438
  );
1350
- await sleep(50);
1351
- console.log();
1352
- await sleep(60);
1353
- const rankSpinner = (0, import_ora2.default)({
1354
- text: colors.dim("Calculating ranking..."),
1355
- color: "cyan"
1356
- }).start();
1357
- await sleep(400);
1358
- rankSpinner.stop();
1359
1439
  }
1360
1440
  if (result.rank || result.countryRank) {
1361
1441
  console.log();
@@ -1363,37 +1443,47 @@ async function submit(options) {
1363
1443
  console.log();
1364
1444
  const prevGlobalRank = result.previous?.previousGlobalRank;
1365
1445
  const prevCountryRank = result.previous?.previousCountryRank;
1446
+ const formatRankChange = (current, previous) => {
1447
+ if (!previous || previous === current) return "";
1448
+ const change = previous - current;
1449
+ if (change > 0) return ` ${colors.success(`\u2191${change}`)}`;
1450
+ return ` ${colors.error(`\u2193${Math.abs(change)}`)}`;
1451
+ };
1366
1452
  if (result.rank) {
1367
1453
  const medal = result.rank === 1 ? "\u{1F947}" : result.rank === 2 ? "\u{1F948}" : result.rank === 3 ? "\u{1F949}" : result.rank <= 10 ? "\u{1F3C5}" : "\u{1F30D}";
1368
- await slotMachineRank(result.rank, "Global:", medal, 12, prevGlobalRank);
1454
+ console.log(
1455
+ ` ${medal} ${colors.muted("Global:")} ${colors.primary.bold(`#${result.rank}`)}${formatRankChange(result.rank, prevGlobalRank)}`
1456
+ );
1369
1457
  }
1370
1458
  if (result.countryRank) {
1371
1459
  const countryMedal = result.countryRank === 1 ? "\u{1F947}" : result.countryRank <= 3 ? "\u{1F3C6}" : "\u{1F3E0}";
1372
- await slotMachineRank(result.countryRank, "Country:", countryMedal, 10, prevCountryRank);
1460
+ console.log(
1461
+ ` ${countryMedal} ${colors.muted("Country:")} ${colors.primary.bold(`#${result.countryRank}`)}${formatRankChange(result.countryRank, prevCountryRank)}`
1462
+ );
1373
1463
  }
1374
1464
  }
1375
1465
  console.log();
1376
1466
  console.log(sectionHeader("\u2B06\uFE0F", "Level Progress"));
1377
1467
  console.log();
1378
- await sleep(200);
1379
1468
  console.log(
1380
1469
  ` ${colors.muted("Lv.")}${colors.white(String(currentLevel.level))} ${currentLevel.icon} ${currentLevel.color(currentLevel.name)}`
1381
1470
  );
1382
1471
  if (!levelProgress.isMaxLevel && levelProgress.next) {
1383
- await animatedProgressBar(levelProgress.progress, 20, 30);
1384
- console.log();
1385
- await sleep(150);
1472
+ const barWidth = 20;
1473
+ const filled = Math.round(levelProgress.progress / 100 * barWidth);
1474
+ const empty = barWidth - filled;
1475
+ const bar = colors.primary("\u2588".repeat(filled)) + colors.dim("\u2591".repeat(empty));
1476
+ console.log(` [${bar}] ${colors.white(`${levelProgress.progress}%`)}`);
1386
1477
  console.log(
1387
1478
  ` ${colors.dim("\u2192")} ${levelProgress.next.icon} ${colors.white(levelProgress.next.name)} ${colors.muted("in")} ${colors.primary(formatNumber(levelProgress.tokensToNext))}`
1388
1479
  );
1389
1480
  } else {
1390
- await sleep(300);
1391
1481
  console.log(` ${colors.max("\u2605")} ${colors.max("MAX LEVEL ACHIEVED!")}`);
1392
1482
  }
1393
1483
  if (result.newBadges && result.newBadges.length > 0) {
1394
1484
  console.log();
1395
1485
  console.log(sectionHeader("\u{1F389}", "New Badge Unlocked"));
1396
- await displayNewBadges(result.newBadges);
1486
+ displayNewBadges(result.newBadges);
1397
1487
  }
1398
1488
  console.log();
1399
1489
  const leaderboardUrl = `https://ccgather.com/leaderboard?u=${username}`;
@@ -1406,14 +1496,23 @@ async function submit(options) {
1406
1496
  console.log(` ${colors.muted("Submit regularly to preserve your full history!")}`);
1407
1497
  console.log();
1408
1498
  } else {
1409
- submitSpinner.fail(colors.error("Failed to submit"));
1410
- console.log(`
1411
- ${error(result.error || "Unknown error")}`);
1412
1499
  if (result.retryAfterMinutes) {
1500
+ submitSpinner.fail(colors.warning("Submission limit reached"));
1501
+ console.log();
1502
+ console.log(
1503
+ ` ${colors.muted("To keep our service stable, you can submit up to 2 times per hour.")}`
1504
+ );
1505
+ console.log(
1506
+ ` ${colors.muted("Your data is safely stored locally - submit anytime later!")}`
1507
+ );
1413
1508
  console.log();
1414
1509
  console.log(
1415
- ` ${colors.warning("\u23F3")} ${colors.muted("Try again in")} ${colors.white(`${result.retryAfterMinutes} minute${result.retryAfterMinutes !== 1 ? "s" : ""}`)}`
1510
+ ` ${colors.warning("\u23F3")} ${colors.white("Ready to submit again in")} ${colors.primary(`${result.retryAfterMinutes} minute${result.retryAfterMinutes !== 1 ? "s" : ""}`)}`
1416
1511
  );
1512
+ } else {
1513
+ submitSpinner.fail(colors.error("Failed to submit"));
1514
+ console.log(`
1515
+ ${error(result.error || "Unknown error")}`);
1417
1516
  }
1418
1517
  console.log();
1419
1518
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccgather",
3
- "version": "2.0.31",
3
+ "version": "2.0.33",
4
4
  "description": "CLI tool for syncing Claude Code usage data to CCgather leaderboard",
5
5
  "bin": {
6
6
  "ccgather": "dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "cli"
26
26
  ],
27
27
  "author": "",
28
- "license": "MIT",
28
+ "license": "Apache-2.0",
29
29
  "dependencies": {
30
30
  "chalk": "^5.3.0",
31
31
  "commander": "^12.1.0",
@@ -50,6 +50,6 @@
50
50
  ],
51
51
  "repository": {
52
52
  "type": "git",
53
- "url": "git+https://github.com/DHxYoon/CCgather.git"
53
+ "url": "git+https://github.com/DHxWhy/CCgather.git"
54
54
  }
55
55
  }