ccclub 0.1.0 → 0.1.2
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 +76 -53
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import { createInterface } from "readline/promises";
|
|
8
8
|
import { stdin, stdout } from "process";
|
|
9
|
-
import
|
|
9
|
+
import chalk3 from "chalk";
|
|
10
10
|
import ora2 from "ora";
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
@@ -133,10 +133,30 @@ async function installHeartbeat() {
|
|
|
133
133
|
return true;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// src/global-install.ts
|
|
137
|
+
import { execFile as execFile2 } from "child_process";
|
|
138
|
+
import chalk from "chalk";
|
|
139
|
+
async function ensureGlobalInstall() {
|
|
140
|
+
const isInstalled = await new Promise((resolve) => {
|
|
141
|
+
execFile2("ccclub", ["--version"], (err) => resolve(!err));
|
|
142
|
+
});
|
|
143
|
+
if (isInstalled) return;
|
|
144
|
+
console.log(chalk.dim("\n Installing ccclub globally so you can run it directly..."));
|
|
145
|
+
const ok = await new Promise((resolve) => {
|
|
146
|
+
execFile2("npm", ["install", "-g", "ccclub"], (err) => resolve(!err));
|
|
147
|
+
});
|
|
148
|
+
if (ok) {
|
|
149
|
+
console.log(chalk.green(" \u2713 Installed!") + chalk.dim(" You can now use ") + chalk.white("ccclub rank") + chalk.dim(" directly."));
|
|
150
|
+
} else {
|
|
151
|
+
console.log(chalk.dim(" Could not install globally. You can do it manually:"));
|
|
152
|
+
console.log(chalk.dim(" npm install -g ccclub"));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
136
156
|
// src/commands/sync.ts
|
|
137
157
|
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
138
158
|
import { existsSync as existsSync3 } from "fs";
|
|
139
|
-
import
|
|
159
|
+
import chalk2 from "chalk";
|
|
140
160
|
import ora from "ora";
|
|
141
161
|
|
|
142
162
|
// src/collector.ts
|
|
@@ -314,7 +334,7 @@ async function doSync(firstSync = false, silent = false) {
|
|
|
314
334
|
const totalCost = blocksToSync.reduce((s, b) => s + b.costUSD, 0);
|
|
315
335
|
if (spinner) {
|
|
316
336
|
spinner.succeed(`Synced ${data.synced} blocks`);
|
|
317
|
-
log(
|
|
337
|
+
log(chalk2.dim(` Tokens: ${totalTokens.toLocaleString()} Cost: $${totalCost.toFixed(4)}`));
|
|
318
338
|
}
|
|
319
339
|
} catch (err) {
|
|
320
340
|
if (spinner) spinner.fail(`Sync error: ${err instanceof Error ? err.message : err}`);
|
|
@@ -325,17 +345,18 @@ async function doSync(firstSync = false, silent = false) {
|
|
|
325
345
|
async function initCommand() {
|
|
326
346
|
const existing = await loadConfig();
|
|
327
347
|
if (existing) {
|
|
328
|
-
console.log(
|
|
348
|
+
console.log(chalk3.yellow("Already initialized!"));
|
|
329
349
|
console.log(` User: ${existing.displayName}`);
|
|
330
350
|
console.log(` Groups: ${existing.groups.join(", ") || "(none)"}`);
|
|
331
|
-
console.log(
|
|
351
|
+
console.log(chalk3.dim('\n Run "ccclub rank" to see rankings'));
|
|
352
|
+
await ensureGlobalInstall();
|
|
332
353
|
return;
|
|
333
354
|
}
|
|
334
355
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
335
356
|
try {
|
|
336
|
-
const displayName = await rl.question(
|
|
357
|
+
const displayName = await rl.question(chalk3.bold("Your display name: "));
|
|
337
358
|
if (!displayName.trim()) {
|
|
338
|
-
console.error(
|
|
359
|
+
console.error(chalk3.red("Name cannot be empty"));
|
|
339
360
|
return;
|
|
340
361
|
}
|
|
341
362
|
const spinner = ora2("Setting up...").start();
|
|
@@ -362,18 +383,19 @@ async function initCommand() {
|
|
|
362
383
|
const heartbeatOk = await installHeartbeat();
|
|
363
384
|
spinner.succeed("CCClub initialized!");
|
|
364
385
|
console.log("");
|
|
365
|
-
console.log(
|
|
366
|
-
console.log(
|
|
386
|
+
console.log(chalk3.bold(" Your invite code:"));
|
|
387
|
+
console.log(chalk3.cyan.bold(`
|
|
367
388
|
${data.groupCode}
|
|
368
389
|
`));
|
|
369
|
-
console.log(
|
|
390
|
+
console.log(chalk3.dim(" Share with friends: ") + chalk3.white(`npx ccclub join ${data.groupCode}`));
|
|
370
391
|
if (heartbeatOk) {
|
|
371
|
-
console.log(
|
|
392
|
+
console.log(chalk3.dim(" Heartbeat: installed (syncs every hour)"));
|
|
372
393
|
} else {
|
|
373
|
-
console.log(
|
|
394
|
+
console.log(chalk3.dim(' Tip: run "ccclub sync" periodically to update data'));
|
|
374
395
|
}
|
|
375
396
|
console.log("");
|
|
376
397
|
await doSync(true);
|
|
398
|
+
await ensureGlobalInstall();
|
|
377
399
|
} finally {
|
|
378
400
|
rl.close();
|
|
379
401
|
}
|
|
@@ -382,7 +404,7 @@ async function initCommand() {
|
|
|
382
404
|
// src/commands/join.ts
|
|
383
405
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
384
406
|
import { stdin as stdin2, stdout as stdout2 } from "process";
|
|
385
|
-
import
|
|
407
|
+
import chalk4 from "chalk";
|
|
386
408
|
import ora3 from "ora";
|
|
387
409
|
async function joinCommand(inviteCode) {
|
|
388
410
|
let config = await loadConfig();
|
|
@@ -395,9 +417,9 @@ async function joinCommand(inviteCode) {
|
|
|
395
417
|
} else {
|
|
396
418
|
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
397
419
|
try {
|
|
398
|
-
displayName = await rl.question(
|
|
420
|
+
displayName = await rl.question(chalk4.bold("Your display name: "));
|
|
399
421
|
if (!displayName.trim()) {
|
|
400
|
-
console.error(
|
|
422
|
+
console.error(chalk4.red("Name cannot be empty"));
|
|
401
423
|
return;
|
|
402
424
|
}
|
|
403
425
|
displayName = displayName.trim();
|
|
@@ -434,17 +456,18 @@ async function joinCommand(inviteCode) {
|
|
|
434
456
|
await installHeartbeat();
|
|
435
457
|
}
|
|
436
458
|
spinner.succeed(`Joined "${data.groupName}"!`);
|
|
437
|
-
console.log(
|
|
459
|
+
console.log(chalk4.dim(`
|
|
438
460
|
Dashboard: ${apiUrl}/g/${data.groupCode}`));
|
|
439
|
-
console.log(
|
|
461
|
+
console.log(chalk4.dim(' Run "ccclub rank" to see rankings'));
|
|
440
462
|
if (!config) {
|
|
441
463
|
console.log("");
|
|
442
464
|
await doSync(true);
|
|
443
465
|
}
|
|
466
|
+
await ensureGlobalInstall();
|
|
444
467
|
}
|
|
445
468
|
|
|
446
469
|
// src/commands/rank.ts
|
|
447
|
-
import
|
|
470
|
+
import chalk5 from "chalk";
|
|
448
471
|
import Table from "cli-table3";
|
|
449
472
|
import ora4 from "ora";
|
|
450
473
|
async function rankCommand(options) {
|
|
@@ -467,24 +490,24 @@ async function rankCommand(options) {
|
|
|
467
490
|
const data = await res.json();
|
|
468
491
|
spinner.stop();
|
|
469
492
|
if (data.rankings.length === 0) {
|
|
470
|
-
console.log(
|
|
471
|
-
console.log(
|
|
493
|
+
console.log(chalk5.yellow("\n No rankings data for this period"));
|
|
494
|
+
console.log(chalk5.dim(' Run "ccclub sync" to upload your usage data'));
|
|
472
495
|
return;
|
|
473
496
|
}
|
|
474
|
-
console.log(
|
|
497
|
+
console.log(chalk5.bold(`
|
|
475
498
|
${data.group.name}`));
|
|
476
|
-
console.log(
|
|
499
|
+
console.log(chalk5.dim(` ${period.toUpperCase()} \xB7 ${data.start.slice(0, 10)} \u2192 ${data.end.slice(0, 10)} \xB7 ${data.group.memberCount} members
|
|
477
500
|
`));
|
|
478
501
|
const table = new Table({
|
|
479
|
-
head: ["#", "Name", "Tokens", "Cost", "Calls"].map((h) =>
|
|
502
|
+
head: ["#", "Name", "Tokens", "Cost", "Calls"].map((h) => chalk5.cyan(h)),
|
|
480
503
|
style: { head: [], border: [] },
|
|
481
504
|
colWidths: [5, 20, 16, 12, 8]
|
|
482
505
|
});
|
|
483
506
|
for (const entry of data.rankings) {
|
|
484
507
|
const isMe = entry.userId === config.userId;
|
|
485
|
-
const marker = isMe ?
|
|
486
|
-
const name = isMe ?
|
|
487
|
-
const rankColor = entry.rank <= 3 ?
|
|
508
|
+
const marker = isMe ? chalk5.green("\u2192") : " ";
|
|
509
|
+
const name = isMe ? chalk5.green.bold(entry.displayName) : entry.displayName;
|
|
510
|
+
const rankColor = entry.rank <= 3 ? chalk5.yellow : chalk5.white;
|
|
488
511
|
table.push([
|
|
489
512
|
`${marker}${rankColor(String(entry.rank))}`,
|
|
490
513
|
name,
|
|
@@ -494,7 +517,7 @@ async function rankCommand(options) {
|
|
|
494
517
|
]);
|
|
495
518
|
}
|
|
496
519
|
console.log(table.toString());
|
|
497
|
-
console.log(
|
|
520
|
+
console.log(chalk5.dim(`
|
|
498
521
|
Dashboard: ${config.apiUrl}/g/${groupCode}`));
|
|
499
522
|
} catch (err) {
|
|
500
523
|
spinner.fail(`Error: ${err instanceof Error ? err.message : err}`);
|
|
@@ -502,7 +525,7 @@ async function rankCommand(options) {
|
|
|
502
525
|
}
|
|
503
526
|
|
|
504
527
|
// src/commands/profile.ts
|
|
505
|
-
import
|
|
528
|
+
import chalk6 from "chalk";
|
|
506
529
|
import ora5 from "ora";
|
|
507
530
|
async function profileCommand(options) {
|
|
508
531
|
const config = await requireConfig();
|
|
@@ -519,10 +542,10 @@ async function profileCommand(options) {
|
|
|
519
542
|
}
|
|
520
543
|
const profile = await res.json();
|
|
521
544
|
spinner2.stop();
|
|
522
|
-
console.log(
|
|
545
|
+
console.log(chalk6.bold("\n Your Profile"));
|
|
523
546
|
console.log(` Name: ${profile.displayName}`);
|
|
524
|
-
console.log(` Avatar: ${profile.avatar ||
|
|
525
|
-
console.log(` Visibility: ${profile.visibility === "public" ?
|
|
547
|
+
console.log(` Avatar: ${profile.avatar || chalk6.dim("(default)")}`);
|
|
548
|
+
console.log(` Visibility: ${profile.visibility === "public" ? chalk6.green("public") : chalk6.dim("private")}`);
|
|
526
549
|
console.log();
|
|
527
550
|
} catch (err) {
|
|
528
551
|
spinner2.fail(`Error: ${err instanceof Error ? err.message : err}`);
|
|
@@ -554,10 +577,10 @@ async function profileCommand(options) {
|
|
|
554
577
|
config.displayName = body.displayName;
|
|
555
578
|
await saveConfig(config);
|
|
556
579
|
}
|
|
557
|
-
console.log(
|
|
580
|
+
console.log(chalk6.green("\n Profile updated!"));
|
|
558
581
|
console.log(` Name: ${profile.displayName}`);
|
|
559
|
-
console.log(` Avatar: ${profile.avatar ||
|
|
560
|
-
console.log(` Visibility: ${profile.visibility === "public" ?
|
|
582
|
+
console.log(` Avatar: ${profile.avatar || chalk6.dim("(default)")}`);
|
|
583
|
+
console.log(` Visibility: ${profile.visibility === "public" ? chalk6.green("public") : chalk6.dim("private")}`);
|
|
561
584
|
console.log();
|
|
562
585
|
} catch (err) {
|
|
563
586
|
spinner.fail(`Error: ${err instanceof Error ? err.message : err}`);
|
|
@@ -565,44 +588,44 @@ async function profileCommand(options) {
|
|
|
565
588
|
}
|
|
566
589
|
|
|
567
590
|
// src/commands/show-data.ts
|
|
568
|
-
import
|
|
591
|
+
import chalk7 from "chalk";
|
|
569
592
|
async function showDataCommand() {
|
|
570
|
-
console.log(
|
|
571
|
-
console.log(
|
|
572
|
-
console.log(
|
|
593
|
+
console.log(chalk7.bold("\n What CCClub uploads:\n"));
|
|
594
|
+
console.log(chalk7.dim(" Only aggregated 5-hour block summaries. No conversation content,"));
|
|
595
|
+
console.log(chalk7.dim(" no file paths, no project names, no session details.\n"));
|
|
573
596
|
const entries = await collectUsageEntries();
|
|
574
597
|
const blocks = aggregateToBlocks(entries);
|
|
575
598
|
if (blocks.length === 0) {
|
|
576
|
-
console.log(
|
|
599
|
+
console.log(chalk7.yellow(" No usage data found in ~/.claude/projects/"));
|
|
577
600
|
return;
|
|
578
601
|
}
|
|
579
|
-
console.log(
|
|
580
|
-
console.log(
|
|
602
|
+
console.log(chalk7.dim(` Total entries found: ${entries.length}`));
|
|
603
|
+
console.log(chalk7.dim(` Aggregated into: ${blocks.length} blocks
|
|
581
604
|
`));
|
|
582
605
|
const recent = blocks.slice(-5);
|
|
583
|
-
console.log(
|
|
606
|
+
console.log(chalk7.bold(" Last 5 blocks (this is exactly what gets uploaded):\n"));
|
|
584
607
|
for (const block of recent) {
|
|
585
|
-
console.log(
|
|
586
|
-
console.log(
|
|
608
|
+
console.log(chalk7.cyan(` ${block.blockStart.slice(0, 16)} \u2192 ${block.blockEnd.slice(11, 16)}`));
|
|
609
|
+
console.log(chalk7.dim(` tokens: ${block.totalTokens.toLocaleString()} cost: $${block.costUSD.toFixed(4)} calls: ${block.entryCount} models: ${block.models.join(", ")}`));
|
|
587
610
|
}
|
|
588
611
|
const totalTokens = blocks.reduce((s, b) => s + b.totalTokens, 0);
|
|
589
612
|
const totalCost = blocks.reduce((s, b) => s + b.costUSD, 0);
|
|
590
|
-
console.log(
|
|
613
|
+
console.log(chalk7.bold(`
|
|
591
614
|
All-time total: ${totalTokens.toLocaleString()} tokens \xB7 $${totalCost.toFixed(2)}`));
|
|
592
615
|
}
|
|
593
616
|
|
|
594
617
|
// src/commands/group.ts
|
|
595
618
|
import { createInterface as createInterface3 } from "readline/promises";
|
|
596
619
|
import { stdin as stdin3, stdout as stdout3 } from "process";
|
|
597
|
-
import
|
|
620
|
+
import chalk8 from "chalk";
|
|
598
621
|
import ora6 from "ora";
|
|
599
622
|
async function createGroupCommand() {
|
|
600
623
|
const config = await requireConfig();
|
|
601
624
|
const rl = createInterface3({ input: stdin3, output: stdout3 });
|
|
602
625
|
try {
|
|
603
|
-
const name = await rl.question(
|
|
626
|
+
const name = await rl.question(chalk8.bold("Group name: "));
|
|
604
627
|
if (!name.trim()) {
|
|
605
|
-
console.error(
|
|
628
|
+
console.error(chalk8.red("Name cannot be empty"));
|
|
606
629
|
return;
|
|
607
630
|
}
|
|
608
631
|
const spinner = ora6("Creating group...").start();
|
|
@@ -625,10 +648,10 @@ async function createGroupCommand() {
|
|
|
625
648
|
await saveConfig(config);
|
|
626
649
|
}
|
|
627
650
|
spinner.succeed(`Created "${data.groupName}"`);
|
|
628
|
-
console.log(
|
|
629
|
-
Invite code: `) +
|
|
630
|
-
console.log(
|
|
631
|
-
console.log(
|
|
651
|
+
console.log(chalk8.bold(`
|
|
652
|
+
Invite code: `) + chalk8.cyan.bold(data.groupCode));
|
|
653
|
+
console.log(chalk8.dim(` Share: npx ccclub join ${data.groupCode}`));
|
|
654
|
+
console.log(chalk8.dim(` Dashboard: ${config.apiUrl}/g/${data.groupCode}`));
|
|
632
655
|
} finally {
|
|
633
656
|
rl.close();
|
|
634
657
|
}
|
|
@@ -636,7 +659,7 @@ async function createGroupCommand() {
|
|
|
636
659
|
|
|
637
660
|
// src/index.ts
|
|
638
661
|
var program = new Command();
|
|
639
|
-
program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.1.
|
|
662
|
+
program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.1.2");
|
|
640
663
|
program.command("init").description("Initialize CCClub (one-time setup)").action(initCommand);
|
|
641
664
|
program.command("join").description("Join a friend's group").argument("<invite-code>", "6-character invite code").action(joinCommand);
|
|
642
665
|
program.command("sync").description("Sync local usage data to server").option("-s, --silent", "No output (used by heartbeat)").action(syncCommand);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccclub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "See how much Claude Code you and your friends are using",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ccclub": "
|
|
7
|
+
"ccclub": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist"
|