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 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 = __require("fs").readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
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 = __require("fs").readFileSync(path, "utf-8").trim();
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
- const res = await fetch(`${apiUrl}/api/init`, {
461
- method: "POST",
462
- headers: { "Content-Type": "application/json" },
463
- body: JSON.stringify({ token, displayName })
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
- const res = await fetch(`${apiUrl}/api/join`, {
548
- method: "POST",
549
- headers: { "Content-Type": "application/json" },
550
- body: JSON.stringify({ token, displayName, inviteCode })
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
- const res = await fetch(`${config.apiUrl}/api/group/create`, {
780
- method: "POST",
781
- headers: {
782
- "Content-Type": "application/json",
783
- Authorization: `Bearer ${config.token}`
784
- },
785
- body: JSON.stringify({ name: name.trim() })
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.13");
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.13",
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
+ }