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.
- package/dist/index.js +100 -77
- 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
|
|
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(
|
|
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(
|
|
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 ?
|
|
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(
|
|
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(
|
|
453
|
-
console.log(
|
|
473
|
+
console.log(chalk3.bold(" Your invite code:"));
|
|
474
|
+
console.log(chalk3.cyan.bold(`
|
|
454
475
|
${data.groupCode}
|
|
455
476
|
`));
|
|
456
|
-
console.log(
|
|
477
|
+
console.log(chalk3.dim(" Share with friends: ") + chalk3.white(`npx ccclub join ${data.groupCode}`));
|
|
457
478
|
if (hookOk) {
|
|
458
|
-
console.log(
|
|
479
|
+
console.log(chalk3.dim(" Auto-sync: on (via Claude Code hook)"));
|
|
459
480
|
} else {
|
|
460
|
-
console.log(
|
|
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(
|
|
493
|
+
console.log(chalk3.bold(" What's next?"));
|
|
472
494
|
console.log("");
|
|
473
|
-
console.log(
|
|
474
|
-
console.log(
|
|
495
|
+
console.log(chalk3.dim(" See the leaderboard:"));
|
|
496
|
+
console.log(chalk3.white(" ccclub"));
|
|
475
497
|
console.log("");
|
|
476
|
-
console.log(
|
|
477
|
-
console.log(
|
|
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(
|
|
480
|
-
console.log(
|
|
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(
|
|
483
|
-
console.log(
|
|
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
|
|
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 ?
|
|
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(
|
|
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(
|
|
572
|
+
console.log(chalk4.bold(" What's next?"));
|
|
550
573
|
console.log("");
|
|
551
|
-
console.log(
|
|
552
|
-
console.log(
|
|
574
|
+
console.log(chalk4.dim(" See the leaderboard:"));
|
|
575
|
+
console.log(chalk4.white(" ccclub"));
|
|
553
576
|
console.log("");
|
|
554
|
-
console.log(
|
|
555
|
-
console.log(
|
|
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(
|
|
558
|
-
console.log(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
633
|
+
console.log(chalk5.bold(`
|
|
611
634
|
${data.group.name}`));
|
|
612
|
-
console.log(
|
|
613
|
-
console.log(
|
|
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(
|
|
639
|
+
console.log(chalk5.bold(`
|
|
617
640
|
${data.group.name}`));
|
|
618
|
-
console.log(
|
|
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) =>
|
|
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 ?
|
|
628
|
-
const name = isMe ?
|
|
629
|
-
const rankColor = entry.rank <= 3 ?
|
|
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(
|
|
662
|
+
console.log(chalk5.dim(` Dashboard: ${config.apiUrl}/g/${code}`));
|
|
640
663
|
}
|
|
641
664
|
|
|
642
665
|
// src/commands/profile.ts
|
|
643
|
-
import
|
|
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(
|
|
683
|
+
console.log(chalk6.bold("\n Your Profile"));
|
|
661
684
|
console.log(` Name: ${profile.displayName}`);
|
|
662
|
-
console.log(` Avatar: ${profile.avatar ||
|
|
663
|
-
console.log(` Visibility: ${profile.visibility === "public" ?
|
|
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(
|
|
718
|
+
console.log(chalk6.green("\n Profile updated!"));
|
|
696
719
|
console.log(` Name: ${profile.displayName}`);
|
|
697
|
-
console.log(` Avatar: ${profile.avatar ||
|
|
698
|
-
console.log(` Visibility: ${profile.visibility === "public" ?
|
|
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
|
|
729
|
+
import chalk7 from "chalk";
|
|
707
730
|
async function showDataCommand() {
|
|
708
|
-
console.log(
|
|
709
|
-
console.log(
|
|
710
|
-
console.log(
|
|
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(
|
|
737
|
+
console.log(chalk7.yellow(" No usage data found in ~/.claude/projects/"));
|
|
715
738
|
return;
|
|
716
739
|
}
|
|
717
|
-
console.log(
|
|
718
|
-
console.log(
|
|
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(
|
|
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(
|
|
724
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
764
|
+
const name = await rl.question(chalk8.bold("Group name: "));
|
|
742
765
|
if (!name.trim()) {
|
|
743
|
-
console.error(
|
|
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(
|
|
767
|
-
Invite code: `) +
|
|
768
|
-
console.log(
|
|
769
|
-
console.log(
|
|
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
|
|
799
|
+
import chalk9 from "chalk";
|
|
777
800
|
async function hookCommand() {
|
|
778
801
|
if (isHookInstalled()) {
|
|
779
|
-
console.log(
|
|
780
|
-
console.log(
|
|
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(
|
|
786
|
-
console.log(
|
|
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(
|
|
789
|
-
console.log(
|
|
790
|
-
console.log(
|
|
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.
|
|
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);
|