ccclub 0.2.13 → 0.2.15
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 +54 -36
- package/package.json +4 -2
- package/scripts/postinstall.cjs +42 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
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
|
-
});
|
|
8
2
|
|
|
9
3
|
// src/index.ts
|
|
10
4
|
import { Command } from "commander";
|
|
@@ -67,9 +61,9 @@ async function loadConfig() {
|
|
|
67
61
|
async function saveConfig(config) {
|
|
68
62
|
const dir = getConfigDir();
|
|
69
63
|
if (!existsSync(dir)) {
|
|
70
|
-
await mkdir(dir, { recursive: true });
|
|
64
|
+
await mkdir(dir, { recursive: true, mode: 448 });
|
|
71
65
|
}
|
|
72
|
-
await writeFile(getConfigPath(), JSON.stringify(config, null, 2));
|
|
66
|
+
await writeFile(getConfigPath(), JSON.stringify(config, null, 2), { mode: 384 });
|
|
73
67
|
}
|
|
74
68
|
function generateDeviceToken() {
|
|
75
69
|
return randomBytes(32).toString("hex");
|
|
@@ -114,7 +108,7 @@ async function requireConfig() {
|
|
|
114
108
|
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
115
109
|
import { join as join2 } from "path";
|
|
116
110
|
import { homedir as homedir2 } from "os";
|
|
117
|
-
import { existsSync as existsSync2 } from "fs";
|
|
111
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
118
112
|
var CLAUDE_SETTINGS_DIR = join2(homedir2(), ".claude");
|
|
119
113
|
var CLAUDE_SETTINGS_PATH = join2(CLAUDE_SETTINGS_DIR, "settings.json");
|
|
120
114
|
var HOOK_COMMAND = "ccclub sync --silent";
|
|
@@ -162,7 +156,7 @@ async function installHook() {
|
|
|
162
156
|
function isHookInstalled() {
|
|
163
157
|
try {
|
|
164
158
|
if (!existsSync2(CLAUDE_SETTINGS_PATH)) return false;
|
|
165
|
-
const raw =
|
|
159
|
+
const raw = readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
166
160
|
const settings = JSON.parse(raw);
|
|
167
161
|
return hasOurHook(settings);
|
|
168
162
|
} catch {
|
|
@@ -172,7 +166,7 @@ function isHookInstalled() {
|
|
|
172
166
|
|
|
173
167
|
// src/commands/sync.ts
|
|
174
168
|
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
175
|
-
import { existsSync as existsSync3 } from "fs";
|
|
169
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
176
170
|
import { join as join4 } from "path";
|
|
177
171
|
import { homedir as homedir4 } from "os";
|
|
178
172
|
import chalk from "chalk";
|
|
@@ -221,7 +215,7 @@ async function collectUsageEntries() {
|
|
|
221
215
|
const usage = parsed.message.usage;
|
|
222
216
|
const requestId = parsed.requestId || "";
|
|
223
217
|
const sessionId = parsed.sessionId || "";
|
|
224
|
-
const dedupeKey = `${sessionId}:${requestId}`;
|
|
218
|
+
const dedupeKey = requestId ? `${sessionId}:${requestId}` : `${sessionId}:${parsed.timestamp}:${usage.input_tokens}:${usage.output_tokens}`;
|
|
225
219
|
if (seen.has(dedupeKey)) continue;
|
|
226
220
|
seen.add(dedupeKey);
|
|
227
221
|
const inputTokens = usage.input_tokens || 0;
|
|
@@ -338,7 +332,7 @@ function needsFullSync() {
|
|
|
338
332
|
const path = getSyncVersionPath();
|
|
339
333
|
if (!existsSync3(path)) return true;
|
|
340
334
|
try {
|
|
341
|
-
const stored =
|
|
335
|
+
const stored = readFileSync2(path, "utf-8").trim();
|
|
342
336
|
return stored !== SYNC_FORMAT_VERSION;
|
|
343
337
|
} catch {
|
|
344
338
|
return true;
|
|
@@ -387,7 +381,8 @@ async function doSync(firstSync = false, silent = false) {
|
|
|
387
381
|
"Content-Type": "application/json",
|
|
388
382
|
Authorization: `Bearer ${config.token}`
|
|
389
383
|
},
|
|
390
|
-
body: JSON.stringify({ blocks: blocksToSync })
|
|
384
|
+
body: JSON.stringify({ blocks: blocksToSync }),
|
|
385
|
+
signal: AbortSignal.timeout(3e4)
|
|
391
386
|
});
|
|
392
387
|
if (!res.ok) {
|
|
393
388
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
@@ -457,11 +452,18 @@ async function initCommand() {
|
|
|
457
452
|
const spinner = ora2("Setting up...").start();
|
|
458
453
|
const token = generateDeviceToken();
|
|
459
454
|
const apiUrl = getApiUrl();
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
455
|
+
let res;
|
|
456
|
+
try {
|
|
457
|
+
res = await fetch(`${apiUrl}/api/init`, {
|
|
458
|
+
method: "POST",
|
|
459
|
+
headers: { "Content-Type": "application/json" },
|
|
460
|
+
body: JSON.stringify({ token, displayName }),
|
|
461
|
+
signal: AbortSignal.timeout(15e3)
|
|
462
|
+
});
|
|
463
|
+
} catch (err) {
|
|
464
|
+
spinner.fail(`Setup failed: ${err instanceof Error ? err.message : err}`);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
465
467
|
if (!res.ok) {
|
|
466
468
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
467
469
|
spinner.fail(`Setup failed: ${err.error}`);
|
|
@@ -544,11 +546,18 @@ async function joinCommand(inviteCode) {
|
|
|
544
546
|
token = generateDeviceToken();
|
|
545
547
|
}
|
|
546
548
|
const spinner = ora3("Joining group...").start();
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
let res;
|
|
550
|
+
try {
|
|
551
|
+
res = await fetch(`${apiUrl}/api/join`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
headers: { "Content-Type": "application/json" },
|
|
554
|
+
body: JSON.stringify({ token, displayName, inviteCode }),
|
|
555
|
+
signal: AbortSignal.timeout(15e3)
|
|
556
|
+
});
|
|
557
|
+
} catch (err) {
|
|
558
|
+
spinner.fail(`Join failed: ${err instanceof Error ? err.message : err}`);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
552
561
|
if (!res.ok) {
|
|
553
562
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
554
563
|
spinner.fail(`Join failed: ${err.error}`);
|
|
@@ -620,7 +629,7 @@ async function rankCommand(options) {
|
|
|
620
629
|
const code = codes[i];
|
|
621
630
|
const tz = -(/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
622
631
|
const url = `${config.apiUrl}/api/rank/${code}?period=${period}&tz=${tz}`;
|
|
623
|
-
const res = await fetch(url);
|
|
632
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15e3) });
|
|
624
633
|
if (!res.ok) {
|
|
625
634
|
if (i === 0) spinner.stop();
|
|
626
635
|
console.log(chalk5.red(`
|
|
@@ -681,7 +690,8 @@ async function profileCommand(options) {
|
|
|
681
690
|
const spinner2 = ora5("Fetching profile...").start();
|
|
682
691
|
try {
|
|
683
692
|
const res = await fetch(`${config.apiUrl}/api/profile`, {
|
|
684
|
-
headers: { Authorization: `Bearer ${config.token}` }
|
|
693
|
+
headers: { Authorization: `Bearer ${config.token}` },
|
|
694
|
+
signal: AbortSignal.timeout(15e3)
|
|
685
695
|
});
|
|
686
696
|
if (!res.ok) {
|
|
687
697
|
spinner2.fail("Failed to fetch profile");
|
|
@@ -712,7 +722,8 @@ async function profileCommand(options) {
|
|
|
712
722
|
"Content-Type": "application/json",
|
|
713
723
|
Authorization: `Bearer ${config.token}`
|
|
714
724
|
},
|
|
715
|
-
body: JSON.stringify(body)
|
|
725
|
+
body: JSON.stringify(body),
|
|
726
|
+
signal: AbortSignal.timeout(15e3)
|
|
716
727
|
});
|
|
717
728
|
if (!res.ok) {
|
|
718
729
|
spinner.fail("Failed to update profile");
|
|
@@ -776,14 +787,21 @@ async function createGroupCommand() {
|
|
|
776
787
|
return;
|
|
777
788
|
}
|
|
778
789
|
const spinner = ora6("Creating group...").start();
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
790
|
+
let res;
|
|
791
|
+
try {
|
|
792
|
+
res = await fetch(`${config.apiUrl}/api/group/create`, {
|
|
793
|
+
method: "POST",
|
|
794
|
+
headers: {
|
|
795
|
+
"Content-Type": "application/json",
|
|
796
|
+
Authorization: `Bearer ${config.token}`
|
|
797
|
+
},
|
|
798
|
+
body: JSON.stringify({ name: name.trim() }),
|
|
799
|
+
signal: AbortSignal.timeout(15e3)
|
|
800
|
+
});
|
|
801
|
+
} catch (err) {
|
|
802
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : err}`);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
787
805
|
if (!res.ok) {
|
|
788
806
|
const err = await res.json().catch(() => ({ error: res.statusText }));
|
|
789
807
|
spinner.fail(`Failed: ${err.error}`);
|
|
@@ -825,7 +843,7 @@ async function hookCommand() {
|
|
|
825
843
|
|
|
826
844
|
// src/index.ts
|
|
827
845
|
var program = new Command();
|
|
828
|
-
program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.2.
|
|
846
|
+
program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.2.15");
|
|
829
847
|
program.command("init").description("Initialize CCClub (one-time setup)").action(initCommand);
|
|
830
848
|
program.command("join").description("Join a friend's group").argument("<invite-code>", "6-character invite code").action(joinCommand);
|
|
831
849
|
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,16 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccclub",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "See how much Claude Code you and your friends are using",
|
|
6
6
|
"bin": {
|
|
7
7
|
"ccclub": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
10
|
+
"dist",
|
|
11
|
+
"scripts"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
14
|
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
15
|
+
"postinstall": "node scripts/postinstall.cjs",
|
|
14
16
|
"dev": "tsx src/index.ts",
|
|
15
17
|
"test": "vitest run"
|
|
16
18
|
},
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
6
|
+
const HOOK_COMMAND = "ccclub sync --silent";
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
// Only install hook if user has Claude Code configured
|
|
10
|
+
if (!fs.existsSync(settingsPath)) process.exit(0);
|
|
11
|
+
|
|
12
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
13
|
+
if (!settings.hooks) settings.hooks = {};
|
|
14
|
+
|
|
15
|
+
const sessionEnd = settings.hooks.SessionEnd || [];
|
|
16
|
+
const hasHook = sessionEnd.some(
|
|
17
|
+
(g) => g.matcher !== undefined && g.hooks?.some((h) => h.command === HOOK_COMMAND),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if (hasHook) process.exit(0);
|
|
21
|
+
|
|
22
|
+
// Remove old format entries (missing matcher field)
|
|
23
|
+
settings.hooks.SessionEnd = sessionEnd.filter(
|
|
24
|
+
(g) => !(g.hooks?.some((h) => h.command === HOOK_COMMAND) && g.matcher === undefined),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
settings.hooks.SessionEnd.push({
|
|
28
|
+
matcher: "",
|
|
29
|
+
hooks: [
|
|
30
|
+
{
|
|
31
|
+
type: "command",
|
|
32
|
+
command: HOOK_COMMAND,
|
|
33
|
+
async: true,
|
|
34
|
+
timeout: 30,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
40
|
+
} catch {
|
|
41
|
+
// Silent fail — hook can be installed later via `ccclub hook`
|
|
42
|
+
}
|