ccclub 0.2.9 → 0.2.10

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 +100 -77
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { Command } from "commander";
12
12
  // src/commands/init.ts
13
13
  import { createInterface } from "readline/promises";
14
14
  import { stdin, stdout } from "process";
15
- import chalk2 from "chalk";
15
+ import chalk3 from "chalk";
16
16
  import ora2 from "ora";
17
17
 
18
18
  // src/config.ts
@@ -405,24 +405,45 @@ async function doSync(firstSync = false, silent = false) {
405
405
  }
406
406
  }
407
407
 
408
+ // src/global-install.ts
409
+ import { exec } from "child_process";
410
+ import chalk2 from "chalk";
411
+ function run(cmd) {
412
+ return new Promise((resolve) => {
413
+ exec(cmd, (err, stdout4) => resolve(err ? "" : stdout4.trim()));
414
+ });
415
+ }
416
+ async function ensureGlobalInstall() {
417
+ const globalList = await run("npm list -g ccclub --depth=0");
418
+ if (globalList.includes("ccclub@")) return;
419
+ console.log(chalk2.dim("\n Installing ccclub globally so you can run it directly..."));
420
+ const result = await run("npm install -g ccclub");
421
+ if (result) {
422
+ console.log(chalk2.green(" Done!") + chalk2.dim(" You can now use ") + chalk2.white("ccclub") + chalk2.dim(" directly."));
423
+ } else {
424
+ console.log(chalk2.dim(" Could not auto-install. Run manually:"));
425
+ console.log(chalk2.white(" npm install -g ccclub"));
426
+ }
427
+ }
428
+
408
429
  // src/commands/init.ts
409
430
  async function initCommand() {
410
431
  const existing = await loadConfig();
411
432
  if (existing) {
412
- console.log(chalk2.yellow("Already initialized!"));
433
+ console.log(chalk3.yellow("Already initialized!"));
413
434
  console.log(` User: ${existing.displayName}`);
414
435
  console.log(` Groups: ${existing.groups.join(", ") || "(none)"}`);
415
- console.log(chalk2.dim('\n Run "ccclub" to see rankings'));
436
+ console.log(chalk3.dim('\n Run "ccclub" to see rankings'));
416
437
  return;
417
438
  }
418
439
  const rl = createInterface({ input: stdin, output: stdout });
419
440
  try {
420
441
  const defaultName = getDefaultDisplayName();
421
- const prompt = defaultName ? chalk2.bold(`Your display name (${defaultName}): `) : chalk2.bold("Your display name: ");
442
+ const prompt = defaultName ? chalk3.bold(`Your display name (${defaultName}): `) : chalk3.bold("Your display name: ");
422
443
  const input = await rl.question(prompt);
423
444
  const displayName = input.trim() || defaultName || "";
424
445
  if (!displayName) {
425
- console.error(chalk2.red("Name cannot be empty"));
446
+ console.error(chalk3.red("Name cannot be empty"));
426
447
  return;
427
448
  }
428
449
  const spinner = ora2("Setting up...").start();
@@ -449,18 +470,19 @@ async function initCommand() {
449
470
  const hookOk = await installHook();
450
471
  spinner.succeed("CCClub initialized!");
451
472
  console.log("");
452
- console.log(chalk2.bold(" Your invite code:"));
453
- console.log(chalk2.cyan.bold(`
473
+ console.log(chalk3.bold(" Your invite code:"));
474
+ console.log(chalk3.cyan.bold(`
454
475
  ${data.groupCode}
455
476
  `));
456
- console.log(chalk2.dim(" Share with friends: ") + chalk2.white(`npx ccclub join ${data.groupCode}`));
477
+ console.log(chalk3.dim(" Share with friends: ") + chalk3.white(`npx ccclub join ${data.groupCode}`));
457
478
  if (hookOk) {
458
- console.log(chalk2.dim(" Auto-sync: on (via Claude Code hook)"));
479
+ console.log(chalk3.dim(" Auto-sync: on (via Claude Code hook)"));
459
480
  } else {
460
- console.log(chalk2.dim(' Tip: run "ccclub hook" to set up auto-sync'));
481
+ console.log(chalk3.dim(' Tip: run "ccclub hook" to set up auto-sync'));
461
482
  }
462
483
  console.log("");
463
484
  await doSync(true);
485
+ await ensureGlobalInstall();
464
486
  printQuickStart(data.groupCode);
465
487
  } finally {
466
488
  rl.close();
@@ -468,26 +490,26 @@ async function initCommand() {
468
490
  }
469
491
  function printQuickStart(groupCode) {
470
492
  console.log("");
471
- console.log(chalk2.bold(" What's next?"));
493
+ console.log(chalk3.bold(" What's next?"));
472
494
  console.log("");
473
- console.log(chalk2.dim(" See the leaderboard:"));
474
- console.log(chalk2.white(" ccclub"));
495
+ console.log(chalk3.dim(" See the leaderboard:"));
496
+ console.log(chalk3.white(" ccclub"));
475
497
  console.log("");
476
- console.log(chalk2.dim(" This week / this month / all-time:"));
477
- console.log(chalk2.white(" ccclub -p weekly"));
498
+ console.log(chalk3.dim(" This week / this month / all-time:"));
499
+ console.log(chalk3.white(" ccclub -p weekly"));
478
500
  console.log("");
479
- console.log(chalk2.dim(" Open the dashboard in browser:"));
480
- console.log(chalk2.white(` https://ccclub.dev/g/${groupCode}`));
501
+ console.log(chalk3.dim(" Open the dashboard in browser:"));
502
+ console.log(chalk3.white(` https://ccclub.dev/g/${groupCode}`));
481
503
  console.log("");
482
- console.log(chalk2.dim(" Check what data gets uploaded:"));
483
- console.log(chalk2.white(" ccclub show-data"));
504
+ console.log(chalk3.dim(" Check what data gets uploaded:"));
505
+ console.log(chalk3.white(" ccclub show-data"));
484
506
  console.log("");
485
507
  }
486
508
 
487
509
  // src/commands/join.ts
488
510
  import { createInterface as createInterface2 } from "readline/promises";
489
511
  import { stdin as stdin2, stdout as stdout2 } from "process";
490
- import chalk3 from "chalk";
512
+ import chalk4 from "chalk";
491
513
  import ora3 from "ora";
492
514
  async function joinCommand(inviteCode) {
493
515
  let config = await loadConfig();
@@ -501,11 +523,11 @@ async function joinCommand(inviteCode) {
501
523
  const rl = createInterface2({ input: stdin2, output: stdout2 });
502
524
  try {
503
525
  const defaultName = getDefaultDisplayName();
504
- const prompt = defaultName ? chalk3.bold(`Your display name (${defaultName}): `) : chalk3.bold("Your display name: ");
526
+ const prompt = defaultName ? chalk4.bold(`Your display name (${defaultName}): `) : chalk4.bold("Your display name: ");
505
527
  const input = await rl.question(prompt);
506
528
  displayName = input.trim() || defaultName || "";
507
529
  if (!displayName) {
508
- console.error(chalk3.red("Name cannot be empty"));
530
+ console.error(chalk4.red("Name cannot be empty"));
509
531
  return;
510
532
  }
511
533
  } finally {
@@ -544,23 +566,24 @@ async function joinCommand(inviteCode) {
544
566
  if (!config) {
545
567
  console.log("");
546
568
  await doSync(true);
569
+ await ensureGlobalInstall();
547
570
  }
548
571
  console.log("");
549
- console.log(chalk3.bold(" What's next?"));
572
+ console.log(chalk4.bold(" What's next?"));
550
573
  console.log("");
551
- console.log(chalk3.dim(" See the leaderboard:"));
552
- console.log(chalk3.white(" ccclub"));
574
+ console.log(chalk4.dim(" See the leaderboard:"));
575
+ console.log(chalk4.white(" ccclub"));
553
576
  console.log("");
554
- console.log(chalk3.dim(" This week / this month / all-time:"));
555
- console.log(chalk3.white(" ccclub -p weekly"));
577
+ console.log(chalk4.dim(" This week / this month / all-time:"));
578
+ console.log(chalk4.white(" ccclub -p weekly"));
556
579
  console.log("");
557
- console.log(chalk3.dim(" Open the dashboard in browser:"));
558
- console.log(chalk3.white(` https://ccclub.dev/g/${data.groupCode}`));
580
+ console.log(chalk4.dim(" Open the dashboard in browser:"));
581
+ console.log(chalk4.white(` https://ccclub.dev/g/${data.groupCode}`));
559
582
  console.log("");
560
583
  }
561
584
 
562
585
  // src/commands/rank.ts
563
- import chalk4 from "chalk";
586
+ import chalk5 from "chalk";
564
587
  import Table from "cli-table3";
565
588
  import ora4 from "ora";
566
589
  async function rankCommand(options) {
@@ -579,7 +602,7 @@ async function rankCommand(options) {
579
602
  codes = config.groups.length > 0 ? config.groups : [];
580
603
  }
581
604
  if (codes.length === 0) {
582
- console.log(chalk4.red("No group found. Run 'ccclub init' or 'ccclub join <code>' first."));
605
+ console.log(chalk5.red("No group found. Run 'ccclub init' or 'ccclub join <code>' first."));
583
606
  return;
584
607
  }
585
608
  const spinner = ora4("Fetching rankings...").start();
@@ -591,7 +614,7 @@ async function rankCommand(options) {
591
614
  const res = await fetch(url);
592
615
  if (!res.ok) {
593
616
  if (i === 0) spinner.stop();
594
- console.log(chalk4.red(`
617
+ console.log(chalk5.red(`
595
618
  Failed to fetch rankings for ${code}`));
596
619
  continue;
597
620
  }
@@ -600,33 +623,33 @@ async function rankCommand(options) {
600
623
  printGroup(data, code, period, config);
601
624
  if (i < codes.length - 1) console.log("");
602
625
  }
603
- console.log(chalk4.dim("\n Data syncs automatically when each Claude Code session ends."));
626
+ console.log(chalk5.dim("\n Data syncs automatically when each Claude Code session ends."));
604
627
  } catch (err) {
605
628
  spinner.fail(`Error: ${err instanceof Error ? err.message : err}`);
606
629
  }
607
630
  }
608
631
  function printGroup(data, code, period, config) {
609
632
  if (data.rankings.length === 0) {
610
- console.log(chalk4.bold(`
633
+ console.log(chalk5.bold(`
611
634
  ${data.group.name}`));
612
- console.log(chalk4.yellow(" No rankings data for this period"));
613
- console.log(chalk4.dim(' Run "ccclub sync" to upload your usage data'));
635
+ console.log(chalk5.yellow(" No rankings data for this period"));
636
+ console.log(chalk5.dim(' Run "ccclub sync" to upload your usage data'));
614
637
  return;
615
638
  }
616
- console.log(chalk4.bold(`
639
+ console.log(chalk5.bold(`
617
640
  ${data.group.name}`));
618
- console.log(chalk4.dim(` ${period.toUpperCase()} \xB7 ${data.start.slice(0, 10)} \u2192 ${data.end.slice(0, 10)} \xB7 ${data.group.memberCount} members
641
+ console.log(chalk5.dim(` ${period.toUpperCase()} \xB7 ${data.start.slice(0, 10)} \u2192 ${data.end.slice(0, 10)} \xB7 ${data.group.memberCount} members
619
642
  `));
620
643
  const table = new Table({
621
- head: ["#", "Name", "Tokens", "Cost", "Chats"].map((h) => chalk4.cyan(h)),
644
+ head: ["#", "Name", "Tokens", "Cost", "Chats"].map((h) => chalk5.cyan(h)),
622
645
  style: { head: [], border: [] },
623
646
  colWidths: [5, 20, 16, 12, 8]
624
647
  });
625
648
  for (const entry of data.rankings) {
626
649
  const isMe = entry.userId === config.userId;
627
- const marker = isMe ? chalk4.green("\u2192") : " ";
628
- const name = isMe ? chalk4.green.bold(entry.displayName) : entry.displayName;
629
- const rankColor = entry.rank <= 3 ? chalk4.yellow : chalk4.white;
650
+ const marker = isMe ? chalk5.green("\u2192") : " ";
651
+ const name = isMe ? chalk5.green.bold(entry.displayName) : entry.displayName;
652
+ const rankColor = entry.rank <= 3 ? chalk5.yellow : chalk5.white;
630
653
  table.push([
631
654
  `${marker}${rankColor(String(entry.rank))}`,
632
655
  name,
@@ -636,11 +659,11 @@ function printGroup(data, code, period, config) {
636
659
  ]);
637
660
  }
638
661
  console.log(table.toString());
639
- console.log(chalk4.dim(` Dashboard: ${config.apiUrl}/g/${code}`));
662
+ console.log(chalk5.dim(` Dashboard: ${config.apiUrl}/g/${code}`));
640
663
  }
641
664
 
642
665
  // src/commands/profile.ts
643
- import chalk5 from "chalk";
666
+ import chalk6 from "chalk";
644
667
  import ora5 from "ora";
645
668
  async function profileCommand(options) {
646
669
  const config = await requireConfig();
@@ -657,10 +680,10 @@ async function profileCommand(options) {
657
680
  }
658
681
  const profile = await res.json();
659
682
  spinner2.stop();
660
- console.log(chalk5.bold("\n Your Profile"));
683
+ console.log(chalk6.bold("\n Your Profile"));
661
684
  console.log(` Name: ${profile.displayName}`);
662
- console.log(` Avatar: ${profile.avatar || chalk5.dim("(default)")}`);
663
- console.log(` Visibility: ${profile.visibility === "public" ? chalk5.green("public") : chalk5.dim("private")}`);
685
+ console.log(` Avatar: ${profile.avatar || chalk6.dim("(default)")}`);
686
+ console.log(` Visibility: ${profile.visibility === "public" ? chalk6.green("public") : chalk6.dim("private")}`);
664
687
  console.log();
665
688
  } catch (err) {
666
689
  spinner2.fail(`Error: ${err instanceof Error ? err.message : err}`);
@@ -692,10 +715,10 @@ async function profileCommand(options) {
692
715
  config.displayName = body.displayName;
693
716
  await saveConfig(config);
694
717
  }
695
- console.log(chalk5.green("\n Profile updated!"));
718
+ console.log(chalk6.green("\n Profile updated!"));
696
719
  console.log(` Name: ${profile.displayName}`);
697
- console.log(` Avatar: ${profile.avatar || chalk5.dim("(default)")}`);
698
- console.log(` Visibility: ${profile.visibility === "public" ? chalk5.green("public") : chalk5.dim("private")}`);
720
+ console.log(` Avatar: ${profile.avatar || chalk6.dim("(default)")}`);
721
+ console.log(` Visibility: ${profile.visibility === "public" ? chalk6.green("public") : chalk6.dim("private")}`);
699
722
  console.log();
700
723
  } catch (err) {
701
724
  spinner.fail(`Error: ${err instanceof Error ? err.message : err}`);
@@ -703,44 +726,44 @@ async function profileCommand(options) {
703
726
  }
704
727
 
705
728
  // src/commands/show-data.ts
706
- import chalk6 from "chalk";
729
+ import chalk7 from "chalk";
707
730
  async function showDataCommand() {
708
- console.log(chalk6.bold("\n What CCClub uploads:\n"));
709
- console.log(chalk6.dim(" Only aggregated 5-hour block summaries. No conversation content,"));
710
- console.log(chalk6.dim(" no file paths, no project names, no session details.\n"));
731
+ console.log(chalk7.bold("\n What CCClub uploads:\n"));
732
+ console.log(chalk7.dim(" Only aggregated 5-hour block summaries. No conversation content,"));
733
+ console.log(chalk7.dim(" no file paths, no project names, no session details.\n"));
711
734
  const { entries, humanTurns } = await collectUsageEntries();
712
735
  const blocks = aggregateToBlocks(entries, humanTurns);
713
736
  if (blocks.length === 0) {
714
- console.log(chalk6.yellow(" No usage data found in ~/.claude/projects/"));
737
+ console.log(chalk7.yellow(" No usage data found in ~/.claude/projects/"));
715
738
  return;
716
739
  }
717
- console.log(chalk6.dim(` Total entries found: ${entries.length}`));
718
- console.log(chalk6.dim(` Aggregated into: ${blocks.length} blocks
740
+ console.log(chalk7.dim(` Total entries found: ${entries.length}`));
741
+ console.log(chalk7.dim(` Aggregated into: ${blocks.length} blocks
719
742
  `));
720
743
  const recent = blocks.slice(-5);
721
- console.log(chalk6.bold(" Last 5 blocks (this is exactly what gets uploaded):\n"));
744
+ console.log(chalk7.bold(" Last 5 blocks (this is exactly what gets uploaded):\n"));
722
745
  for (const block of recent) {
723
- console.log(chalk6.cyan(` ${block.blockStart.slice(0, 16)} \u2192 ${block.blockEnd.slice(11, 16)}`));
724
- console.log(chalk6.dim(` tokens: ${block.totalTokens.toLocaleString()} cost: $${block.costUSD.toFixed(4)} calls: ${block.entryCount} models: ${block.models.join(", ")}`));
746
+ console.log(chalk7.cyan(` ${block.blockStart.slice(0, 16)} \u2192 ${block.blockEnd.slice(11, 16)}`));
747
+ console.log(chalk7.dim(` tokens: ${block.totalTokens.toLocaleString()} cost: $${block.costUSD.toFixed(4)} calls: ${block.entryCount} models: ${block.models.join(", ")}`));
725
748
  }
726
749
  const totalTokens = blocks.reduce((s, b) => s + b.totalTokens, 0);
727
750
  const totalCost = blocks.reduce((s, b) => s + b.costUSD, 0);
728
- console.log(chalk6.bold(`
751
+ console.log(chalk7.bold(`
729
752
  All-time total: ${totalTokens.toLocaleString()} tokens \xB7 $${totalCost.toFixed(2)}`));
730
753
  }
731
754
 
732
755
  // src/commands/group.ts
733
756
  import { createInterface as createInterface3 } from "readline/promises";
734
757
  import { stdin as stdin3, stdout as stdout3 } from "process";
735
- import chalk7 from "chalk";
758
+ import chalk8 from "chalk";
736
759
  import ora6 from "ora";
737
760
  async function createGroupCommand() {
738
761
  const config = await requireConfig();
739
762
  const rl = createInterface3({ input: stdin3, output: stdout3 });
740
763
  try {
741
- const name = await rl.question(chalk7.bold("Group name: "));
764
+ const name = await rl.question(chalk8.bold("Group name: "));
742
765
  if (!name.trim()) {
743
- console.error(chalk7.red("Name cannot be empty"));
766
+ console.error(chalk8.red("Name cannot be empty"));
744
767
  return;
745
768
  }
746
769
  const spinner = ora6("Creating group...").start();
@@ -763,37 +786,37 @@ async function createGroupCommand() {
763
786
  await saveConfig(config);
764
787
  }
765
788
  spinner.succeed(`Created "${data.groupName}"`);
766
- console.log(chalk7.bold(`
767
- Invite code: `) + chalk7.cyan.bold(data.groupCode));
768
- console.log(chalk7.dim(` Share: npx ccclub join ${data.groupCode}`));
769
- console.log(chalk7.dim(` Dashboard: ${config.apiUrl}/g/${data.groupCode}`));
789
+ console.log(chalk8.bold(`
790
+ Invite code: `) + chalk8.cyan.bold(data.groupCode));
791
+ console.log(chalk8.dim(` Share: npx ccclub join ${data.groupCode}`));
792
+ console.log(chalk8.dim(` Dashboard: ${config.apiUrl}/g/${data.groupCode}`));
770
793
  } finally {
771
794
  rl.close();
772
795
  }
773
796
  }
774
797
 
775
798
  // src/commands/hook.ts
776
- import chalk8 from "chalk";
799
+ import chalk9 from "chalk";
777
800
  async function hookCommand() {
778
801
  if (isHookInstalled()) {
779
- console.log(chalk8.green(" Claude Code hook already installed."));
780
- console.log(chalk8.dim(" Usage syncs automatically when each Claude Code session ends."));
802
+ console.log(chalk9.green(" Claude Code hook already installed."));
803
+ console.log(chalk9.dim(" Usage syncs automatically when each Claude Code session ends."));
781
804
  return;
782
805
  }
783
806
  const ok = await installHook();
784
807
  if (ok) {
785
- console.log(chalk8.green(" Claude Code hook installed!"));
786
- console.log(chalk8.dim(" Usage will sync automatically when each Claude Code session ends."));
808
+ console.log(chalk9.green(" Claude Code hook installed!"));
809
+ console.log(chalk9.dim(" Usage will sync automatically when each Claude Code session ends."));
787
810
  } else {
788
- console.log(chalk8.red(" Failed to install hook."));
789
- console.log(chalk8.dim(" You can manually add to ~/.claude/settings.json:"));
790
- console.log(chalk8.dim(' {"hooks":{"SessionEnd":[{"hooks":[{"type":"command","command":"ccclub sync --silent","async":true}]}]}}'));
811
+ console.log(chalk9.red(" Failed to install hook."));
812
+ console.log(chalk9.dim(" You can manually add to ~/.claude/settings.json:"));
813
+ console.log(chalk9.dim(' {"hooks":{"SessionEnd":[{"hooks":[{"type":"command","command":"ccclub sync --silent","async":true}]}]}}'));
791
814
  }
792
815
  }
793
816
 
794
817
  // src/index.ts
795
818
  var program = new Command();
796
- program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.2.9");
819
+ program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.2.10");
797
820
  program.command("init").description("Initialize CCClub (one-time setup)").action(initCommand);
798
821
  program.command("join").description("Join a friend's group").argument("<invite-code>", "6-character invite code").action(joinCommand);
799
822
  program.command("sync").description("Sync local usage data to server").option("-s, --silent", "No output (used by auto-sync hook)").option("-f, --full", "Force full re-sync of all data").action(syncCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccclub",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "description": "See how much Claude Code you and your friends are using",
6
6
  "bin": {