ccclub 0.1.12 → 0.2.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.
- package/dist/index.js +125 -73
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
2
8
|
|
|
3
9
|
// src/index.ts
|
|
4
10
|
import { Command } from "commander";
|
|
@@ -12,9 +18,10 @@ import ora2 from "ora";
|
|
|
12
18
|
// src/config.ts
|
|
13
19
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
14
20
|
import { join } from "path";
|
|
15
|
-
import { homedir } from "os";
|
|
21
|
+
import { homedir, userInfo } from "os";
|
|
16
22
|
import { existsSync } from "fs";
|
|
17
23
|
import { randomBytes } from "crypto";
|
|
24
|
+
import { execSync } from "child_process";
|
|
18
25
|
|
|
19
26
|
// ../shared/dist/constants.js
|
|
20
27
|
var BLOCK_DURATION_HOURS = 5;
|
|
@@ -64,6 +71,30 @@ function generateDeviceToken() {
|
|
|
64
71
|
function getApiUrl() {
|
|
65
72
|
return process.env.CCCLUB_API_URL || DEFAULT_API_URL;
|
|
66
73
|
}
|
|
74
|
+
function getDefaultDisplayName() {
|
|
75
|
+
try {
|
|
76
|
+
const name = execSync("git config --global user.name", {
|
|
77
|
+
encoding: "utf-8",
|
|
78
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
79
|
+
}).trim();
|
|
80
|
+
if (name) return name;
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const name = execSync("id -F", {
|
|
85
|
+
encoding: "utf-8",
|
|
86
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
87
|
+
}).trim();
|
|
88
|
+
if (name) return name;
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const name = userInfo().username;
|
|
93
|
+
if (name) return name;
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
67
98
|
async function requireConfig() {
|
|
68
99
|
const config = await loadConfig();
|
|
69
100
|
if (!config) {
|
|
@@ -73,75 +104,70 @@ async function requireConfig() {
|
|
|
73
104
|
return config;
|
|
74
105
|
}
|
|
75
106
|
|
|
76
|
-
// src/
|
|
77
|
-
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
107
|
+
// src/hook.ts
|
|
108
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
78
109
|
import { join as join2 } from "path";
|
|
79
110
|
import { homedir as homedir2 } from "os";
|
|
80
111
|
import { existsSync as existsSync2 } from "fs";
|
|
81
|
-
|
|
82
|
-
var
|
|
83
|
-
var
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<key>Label</key>
|
|
92
|
-
<string>${PLIST_NAME}</string>
|
|
93
|
-
<key>ProgramArguments</key>
|
|
94
|
-
<array>
|
|
95
|
-
<string>/usr/bin/env</string>
|
|
96
|
-
<string>npx</string>
|
|
97
|
-
<string>ccclub</string>
|
|
98
|
-
<string>sync</string>
|
|
99
|
-
<string>--silent</string>
|
|
100
|
-
</array>
|
|
101
|
-
<key>StartInterval</key>
|
|
102
|
-
<integer>3600</integer>
|
|
103
|
-
<key>StandardOutPath</key>
|
|
104
|
-
<string>${logPath}</string>
|
|
105
|
-
<key>StandardErrorPath</key>
|
|
106
|
-
<string>${logPath}</string>
|
|
107
|
-
<key>RunAtLoad</key>
|
|
108
|
-
<true/>
|
|
109
|
-
<key>EnvironmentVariables</key>
|
|
110
|
-
<dict>
|
|
111
|
-
<key>PATH</key>
|
|
112
|
-
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
113
|
-
</dict>
|
|
114
|
-
</dict>
|
|
115
|
-
</plist>`;
|
|
112
|
+
var CLAUDE_SETTINGS_DIR = join2(homedir2(), ".claude");
|
|
113
|
+
var CLAUDE_SETTINGS_PATH = join2(CLAUDE_SETTINGS_DIR, "settings.json");
|
|
114
|
+
var HOOK_COMMAND = "ccclub sync --silent";
|
|
115
|
+
function hasOurHook(settings) {
|
|
116
|
+
const sessionEndHooks = settings.hooks?.SessionEnd;
|
|
117
|
+
if (!Array.isArray(sessionEndHooks)) return false;
|
|
118
|
+
return sessionEndHooks.some((group) => {
|
|
119
|
+
const g = group;
|
|
120
|
+
return g.hooks?.some((h) => h.command === HOOK_COMMAND);
|
|
121
|
+
});
|
|
116
122
|
}
|
|
117
|
-
async function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
async function installHook() {
|
|
124
|
+
try {
|
|
125
|
+
if (!existsSync2(CLAUDE_SETTINGS_DIR)) {
|
|
126
|
+
await mkdir2(CLAUDE_SETTINGS_DIR, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
let settings = {};
|
|
129
|
+
if (existsSync2(CLAUDE_SETTINGS_PATH)) {
|
|
130
|
+
const raw = await readFile2(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
131
|
+
settings = JSON.parse(raw);
|
|
132
|
+
}
|
|
133
|
+
if (hasOurHook(settings)) return true;
|
|
134
|
+
if (!settings.hooks) settings.hooks = {};
|
|
135
|
+
if (!Array.isArray(settings.hooks.SessionEnd)) settings.hooks.SessionEnd = [];
|
|
136
|
+
settings.hooks.SessionEnd.push({
|
|
137
|
+
hooks: [
|
|
138
|
+
{
|
|
139
|
+
type: "command",
|
|
140
|
+
command: HOOK_COMMAND,
|
|
141
|
+
async: true,
|
|
142
|
+
timeout: 30
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
await writeFile2(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
122
147
|
return true;
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
123
150
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
await writeFile2(PLIST_PATH, getPlist());
|
|
151
|
+
}
|
|
152
|
+
function isHookInstalled() {
|
|
128
153
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
154
|
+
if (!existsSync2(CLAUDE_SETTINGS_PATH)) return false;
|
|
155
|
+
const raw = __require("fs").readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
156
|
+
const settings = JSON.parse(raw);
|
|
157
|
+
return hasOurHook(settings);
|
|
132
158
|
} catch {
|
|
159
|
+
return false;
|
|
133
160
|
}
|
|
134
|
-
return true;
|
|
135
161
|
}
|
|
136
162
|
|
|
137
163
|
// src/commands/sync.ts
|
|
138
|
-
import { readFile as
|
|
164
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
139
165
|
import { existsSync as existsSync3 } from "fs";
|
|
140
166
|
import chalk from "chalk";
|
|
141
167
|
import ora from "ora";
|
|
142
168
|
|
|
143
169
|
// src/collector.ts
|
|
144
|
-
import { readFile as
|
|
170
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
145
171
|
import { join as join3 } from "path";
|
|
146
172
|
import { homedir as homedir3 } from "os";
|
|
147
173
|
import { glob } from "glob";
|
|
@@ -154,7 +180,7 @@ async function collectUsageEntries() {
|
|
|
154
180
|
const entries = [];
|
|
155
181
|
const seen = /* @__PURE__ */ new Set();
|
|
156
182
|
for (const file of files) {
|
|
157
|
-
const content = await
|
|
183
|
+
const content = await readFile3(file, "utf-8");
|
|
158
184
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
159
185
|
for (const line of lines) {
|
|
160
186
|
let parsed;
|
|
@@ -281,7 +307,7 @@ async function doSync(firstSync = false, silent = false) {
|
|
|
281
307
|
const lastSyncPath = getLastSyncPath();
|
|
282
308
|
let lastSync = null;
|
|
283
309
|
if (existsSync3(lastSyncPath)) {
|
|
284
|
-
lastSync = (await
|
|
310
|
+
lastSync = (await readFile4(lastSyncPath, "utf-8")).trim() || null;
|
|
285
311
|
}
|
|
286
312
|
let blocksToSync;
|
|
287
313
|
if (lastSync && !firstSync) {
|
|
@@ -334,8 +360,11 @@ async function initCommand() {
|
|
|
334
360
|
}
|
|
335
361
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
336
362
|
try {
|
|
337
|
-
const
|
|
338
|
-
|
|
363
|
+
const defaultName = getDefaultDisplayName();
|
|
364
|
+
const prompt = defaultName ? chalk2.bold(`Your display name (${defaultName}): `) : chalk2.bold("Your display name: ");
|
|
365
|
+
const input = await rl.question(prompt);
|
|
366
|
+
const displayName = input.trim() || defaultName || "";
|
|
367
|
+
if (!displayName) {
|
|
339
368
|
console.error(chalk2.red("Name cannot be empty"));
|
|
340
369
|
return;
|
|
341
370
|
}
|
|
@@ -345,7 +374,7 @@ async function initCommand() {
|
|
|
345
374
|
const res = await fetch(`${apiUrl}/api/init`, {
|
|
346
375
|
method: "POST",
|
|
347
376
|
headers: { "Content-Type": "application/json" },
|
|
348
|
-
body: JSON.stringify({ token, displayName
|
|
377
|
+
body: JSON.stringify({ token, displayName })
|
|
349
378
|
});
|
|
350
379
|
if (!res.ok) {
|
|
351
380
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
@@ -360,7 +389,7 @@ async function initCommand() {
|
|
|
360
389
|
displayName: displayName.trim(),
|
|
361
390
|
groups: [data.groupCode]
|
|
362
391
|
});
|
|
363
|
-
const
|
|
392
|
+
const hookOk = await installHook();
|
|
364
393
|
spinner.succeed("CCClub initialized!");
|
|
365
394
|
console.log("");
|
|
366
395
|
console.log(chalk2.bold(" Your invite code:"));
|
|
@@ -368,10 +397,10 @@ async function initCommand() {
|
|
|
368
397
|
${data.groupCode}
|
|
369
398
|
`));
|
|
370
399
|
console.log(chalk2.dim(" Share with friends: ") + chalk2.white(`npx ccclub join ${data.groupCode}`));
|
|
371
|
-
if (
|
|
372
|
-
console.log(chalk2.dim(" Auto-sync: on (
|
|
400
|
+
if (hookOk) {
|
|
401
|
+
console.log(chalk2.dim(" Auto-sync: on (via Claude Code hook)"));
|
|
373
402
|
} else {
|
|
374
|
-
console.log(chalk2.dim(' Tip: run "ccclub
|
|
403
|
+
console.log(chalk2.dim(' Tip: run "ccclub hook" to set up auto-sync'));
|
|
375
404
|
}
|
|
376
405
|
console.log("");
|
|
377
406
|
await doSync(true);
|
|
@@ -414,12 +443,14 @@ async function joinCommand(inviteCode) {
|
|
|
414
443
|
} else {
|
|
415
444
|
const rl = createInterface2({ input: stdin2, output: stdout2 });
|
|
416
445
|
try {
|
|
417
|
-
|
|
418
|
-
|
|
446
|
+
const defaultName = getDefaultDisplayName();
|
|
447
|
+
const prompt = defaultName ? chalk3.bold(`Your display name (${defaultName}): `) : chalk3.bold("Your display name: ");
|
|
448
|
+
const input = await rl.question(prompt);
|
|
449
|
+
displayName = input.trim() || defaultName || "";
|
|
450
|
+
if (!displayName) {
|
|
419
451
|
console.error(chalk3.red("Name cannot be empty"));
|
|
420
452
|
return;
|
|
421
453
|
}
|
|
422
|
-
displayName = displayName.trim();
|
|
423
454
|
} finally {
|
|
424
455
|
rl.close();
|
|
425
456
|
}
|
|
@@ -450,7 +481,7 @@ async function joinCommand(inviteCode) {
|
|
|
450
481
|
displayName,
|
|
451
482
|
groups: [data.groupCode]
|
|
452
483
|
});
|
|
453
|
-
await
|
|
484
|
+
await installHook();
|
|
454
485
|
}
|
|
455
486
|
spinner.succeed(`Joined "${data.groupName}"!`);
|
|
456
487
|
if (!config) {
|
|
@@ -477,6 +508,7 @@ import Table from "cli-table3";
|
|
|
477
508
|
import ora4 from "ora";
|
|
478
509
|
async function rankCommand(options) {
|
|
479
510
|
const config = await requireConfig();
|
|
511
|
+
await doSync(false, true);
|
|
480
512
|
const isGlobal = options.global === true;
|
|
481
513
|
const period = options.period || "daily";
|
|
482
514
|
let codes;
|
|
@@ -508,7 +540,7 @@ async function rankCommand(options) {
|
|
|
508
540
|
printGroup(data, code, period, config);
|
|
509
541
|
if (i < codes.length - 1) console.log("");
|
|
510
542
|
}
|
|
511
|
-
console.log(chalk4.dim("\n Data syncs automatically
|
|
543
|
+
console.log(chalk4.dim("\n Data syncs automatically when each Claude Code session ends."));
|
|
512
544
|
} catch (err) {
|
|
513
545
|
spinner.fail(`Error: ${err instanceof Error ? err.message : err}`);
|
|
514
546
|
}
|
|
@@ -680,9 +712,28 @@ async function createGroupCommand() {
|
|
|
680
712
|
}
|
|
681
713
|
}
|
|
682
714
|
|
|
715
|
+
// src/commands/hook.ts
|
|
716
|
+
import chalk8 from "chalk";
|
|
717
|
+
async function hookCommand() {
|
|
718
|
+
if (isHookInstalled()) {
|
|
719
|
+
console.log(chalk8.green(" Claude Code hook already installed."));
|
|
720
|
+
console.log(chalk8.dim(" Usage syncs automatically when each Claude Code session ends."));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const ok = await installHook();
|
|
724
|
+
if (ok) {
|
|
725
|
+
console.log(chalk8.green(" Claude Code hook installed!"));
|
|
726
|
+
console.log(chalk8.dim(" Usage will sync automatically when each Claude Code session ends."));
|
|
727
|
+
} else {
|
|
728
|
+
console.log(chalk8.red(" Failed to install hook."));
|
|
729
|
+
console.log(chalk8.dim(" You can manually add to ~/.claude/settings.json:"));
|
|
730
|
+
console.log(chalk8.dim(' {"hooks":{"SessionEnd":[{"hooks":[{"type":"command","command":"ccclub sync --silent","async":true}]}]}}'));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
683
734
|
// src/global-install.ts
|
|
684
735
|
import { exec } from "child_process";
|
|
685
|
-
import
|
|
736
|
+
import chalk9 from "chalk";
|
|
686
737
|
function run(cmd) {
|
|
687
738
|
return new Promise((resolve) => {
|
|
688
739
|
exec(cmd, (err, stdout4) => resolve(err ? "" : stdout4.trim()));
|
|
@@ -691,19 +742,19 @@ function run(cmd) {
|
|
|
691
742
|
async function ensureGlobalInstall() {
|
|
692
743
|
const globalList = await run("npm list -g ccclub --depth=0");
|
|
693
744
|
if (globalList.includes("ccclub@")) return;
|
|
694
|
-
console.log(
|
|
745
|
+
console.log(chalk9.dim("\n Installing ccclub globally so you can run it directly..."));
|
|
695
746
|
const result = await run("npm install -g ccclub");
|
|
696
747
|
if (result) {
|
|
697
|
-
console.log(
|
|
748
|
+
console.log(chalk9.green(" Done!") + chalk9.dim(" You can now use ") + chalk9.white("ccclub") + chalk9.dim(" directly."));
|
|
698
749
|
} else {
|
|
699
|
-
console.log(
|
|
700
|
-
console.log(
|
|
750
|
+
console.log(chalk9.dim(" Could not auto-install. Run manually:"));
|
|
751
|
+
console.log(chalk9.white(" npm install -g ccclub"));
|
|
701
752
|
}
|
|
702
753
|
}
|
|
703
754
|
|
|
704
755
|
// src/index.ts
|
|
705
756
|
var program = new Command();
|
|
706
|
-
program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.
|
|
757
|
+
program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.2.0");
|
|
707
758
|
program.hook("postAction", () => ensureGlobalInstall());
|
|
708
759
|
program.command("init").description("Initialize CCClub (one-time setup)").action(initCommand);
|
|
709
760
|
program.command("join").description("Join a friend's group").argument("<invite-code>", "6-character invite code").action(joinCommand);
|
|
@@ -712,4 +763,5 @@ program.command("rank", { isDefault: true }).description("Show leaderboard ranki
|
|
|
712
763
|
program.command("profile").description("View or update your profile").option("-n, --name <name>", "Set display name").option("--avatar <url>", "Set avatar URL (empty string to reset)").option("--public", "Set profile visibility to public").option("--private", "Set profile visibility to private").action(profileCommand);
|
|
713
764
|
program.command("create").description("Create a new group").action(createGroupCommand);
|
|
714
765
|
program.command("show-data").description("Show exactly what data CCClub uploads (privacy audit)").action(showDataCommand);
|
|
766
|
+
program.command("hook").description("Set up Claude Code hook for auto-sync on session end").action(hookCommand);
|
|
715
767
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccclub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "See how much Claude Code you and your friends are using",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
14
|
-
"dev": "tsx src/index.ts"
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"test": "vitest run"
|
|
15
16
|
},
|
|
16
17
|
"dependencies": {
|
|
17
18
|
"chalk": "^5.3.0",
|
|
@@ -22,10 +23,11 @@
|
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@ccclub/shared": "workspace:*",
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
25
27
|
"tsup": "^8.3.0",
|
|
26
28
|
"tsx": "^4.19.0",
|
|
27
29
|
"typescript": "^5.7.0",
|
|
28
|
-
"
|
|
30
|
+
"vitest": "^4.0.18"
|
|
29
31
|
},
|
|
30
32
|
"keywords": [
|
|
31
33
|
"claude",
|