ccclub 0.1.13 → 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.
Files changed (2) hide show
  1. package/dist/index.js +88 -66
  2. package/package.json +1 -1
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";
@@ -98,75 +104,70 @@ async function requireConfig() {
98
104
  return config;
99
105
  }
100
106
 
101
- // src/heartbeat.ts
102
- 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";
103
109
  import { join as join2 } from "path";
104
110
  import { homedir as homedir2 } from "os";
105
111
  import { existsSync as existsSync2 } from "fs";
106
- import { execFile } from "child_process";
107
- var PLIST_NAME = "dev.ccclub.sync";
108
- var LAUNCH_AGENTS_DIR = join2(homedir2(), "Library", "LaunchAgents");
109
- var PLIST_PATH = join2(LAUNCH_AGENTS_DIR, `${PLIST_NAME}.plist`);
110
- function getPlist() {
111
- const logPath = join2(homedir2(), ".ccclub", "sync.log");
112
- return `<?xml version="1.0" encoding="UTF-8"?>
113
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
114
- <plist version="1.0">
115
- <dict>
116
- <key>Label</key>
117
- <string>${PLIST_NAME}</string>
118
- <key>ProgramArguments</key>
119
- <array>
120
- <string>/usr/bin/env</string>
121
- <string>npx</string>
122
- <string>ccclub</string>
123
- <string>sync</string>
124
- <string>--silent</string>
125
- </array>
126
- <key>StartInterval</key>
127
- <integer>3600</integer>
128
- <key>StandardOutPath</key>
129
- <string>${logPath}</string>
130
- <key>StandardErrorPath</key>
131
- <string>${logPath}</string>
132
- <key>RunAtLoad</key>
133
- <true/>
134
- <key>EnvironmentVariables</key>
135
- <dict>
136
- <key>PATH</key>
137
- <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
138
- </dict>
139
- </dict>
140
- </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
+ });
141
122
  }
142
- async function installHeartbeat() {
143
- if (process.platform !== "darwin") {
144
- return false;
145
- }
146
- if (existsSync2(PLIST_PATH)) {
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");
147
147
  return true;
148
+ } catch {
149
+ return false;
148
150
  }
149
- if (!existsSync2(LAUNCH_AGENTS_DIR)) {
150
- await mkdir2(LAUNCH_AGENTS_DIR, { recursive: true });
151
- }
152
- await writeFile2(PLIST_PATH, getPlist());
151
+ }
152
+ function isHookInstalled() {
153
153
  try {
154
- await new Promise((resolve, reject) => {
155
- execFile("launchctl", ["load", PLIST_PATH], (err) => err ? reject(err) : resolve());
156
- });
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);
157
158
  } catch {
159
+ return false;
158
160
  }
159
- return true;
160
161
  }
161
162
 
162
163
  // src/commands/sync.ts
163
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
164
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
164
165
  import { existsSync as existsSync3 } from "fs";
165
166
  import chalk from "chalk";
166
167
  import ora from "ora";
167
168
 
168
169
  // src/collector.ts
169
- import { readFile as readFile2 } from "fs/promises";
170
+ import { readFile as readFile3 } from "fs/promises";
170
171
  import { join as join3 } from "path";
171
172
  import { homedir as homedir3 } from "os";
172
173
  import { glob } from "glob";
@@ -179,7 +180,7 @@ async function collectUsageEntries() {
179
180
  const entries = [];
180
181
  const seen = /* @__PURE__ */ new Set();
181
182
  for (const file of files) {
182
- const content = await readFile2(file, "utf-8");
183
+ const content = await readFile3(file, "utf-8");
183
184
  const lines = content.split("\n").filter((l) => l.trim());
184
185
  for (const line of lines) {
185
186
  let parsed;
@@ -306,7 +307,7 @@ async function doSync(firstSync = false, silent = false) {
306
307
  const lastSyncPath = getLastSyncPath();
307
308
  let lastSync = null;
308
309
  if (existsSync3(lastSyncPath)) {
309
- lastSync = (await readFile3(lastSyncPath, "utf-8")).trim() || null;
310
+ lastSync = (await readFile4(lastSyncPath, "utf-8")).trim() || null;
310
311
  }
311
312
  let blocksToSync;
312
313
  if (lastSync && !firstSync) {
@@ -388,7 +389,7 @@ async function initCommand() {
388
389
  displayName: displayName.trim(),
389
390
  groups: [data.groupCode]
390
391
  });
391
- const heartbeatOk = await installHeartbeat();
392
+ const hookOk = await installHook();
392
393
  spinner.succeed("CCClub initialized!");
393
394
  console.log("");
394
395
  console.log(chalk2.bold(" Your invite code:"));
@@ -396,10 +397,10 @@ async function initCommand() {
396
397
  ${data.groupCode}
397
398
  `));
398
399
  console.log(chalk2.dim(" Share with friends: ") + chalk2.white(`npx ccclub join ${data.groupCode}`));
399
- if (heartbeatOk) {
400
- console.log(chalk2.dim(" Auto-sync: on (every hour)"));
400
+ if (hookOk) {
401
+ console.log(chalk2.dim(" Auto-sync: on (via Claude Code hook)"));
401
402
  } else {
402
- console.log(chalk2.dim(' Tip: run "ccclub sync" periodically to update data'));
403
+ console.log(chalk2.dim(' Tip: run "ccclub hook" to set up auto-sync'));
403
404
  }
404
405
  console.log("");
405
406
  await doSync(true);
@@ -480,7 +481,7 @@ async function joinCommand(inviteCode) {
480
481
  displayName,
481
482
  groups: [data.groupCode]
482
483
  });
483
- await installHeartbeat();
484
+ await installHook();
484
485
  }
485
486
  spinner.succeed(`Joined "${data.groupName}"!`);
486
487
  if (!config) {
@@ -507,6 +508,7 @@ import Table from "cli-table3";
507
508
  import ora4 from "ora";
508
509
  async function rankCommand(options) {
509
510
  const config = await requireConfig();
511
+ await doSync(false, true);
510
512
  const isGlobal = options.global === true;
511
513
  const period = options.period || "daily";
512
514
  let codes;
@@ -538,7 +540,7 @@ async function rankCommand(options) {
538
540
  printGroup(data, code, period, config);
539
541
  if (i < codes.length - 1) console.log("");
540
542
  }
541
- console.log(chalk4.dim("\n Data syncs automatically ") + chalk4.white("every hour") + chalk4.dim(". Run ") + chalk4.white("ccclub sync") + chalk4.dim(" to update now."));
543
+ console.log(chalk4.dim("\n Data syncs automatically when each Claude Code session ends."));
542
544
  } catch (err) {
543
545
  spinner.fail(`Error: ${err instanceof Error ? err.message : err}`);
544
546
  }
@@ -710,9 +712,28 @@ async function createGroupCommand() {
710
712
  }
711
713
  }
712
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
+
713
734
  // src/global-install.ts
714
735
  import { exec } from "child_process";
715
- import chalk8 from "chalk";
736
+ import chalk9 from "chalk";
716
737
  function run(cmd) {
717
738
  return new Promise((resolve) => {
718
739
  exec(cmd, (err, stdout4) => resolve(err ? "" : stdout4.trim()));
@@ -721,19 +742,19 @@ function run(cmd) {
721
742
  async function ensureGlobalInstall() {
722
743
  const globalList = await run("npm list -g ccclub --depth=0");
723
744
  if (globalList.includes("ccclub@")) return;
724
- console.log(chalk8.dim("\n Installing ccclub globally so you can run it directly..."));
745
+ console.log(chalk9.dim("\n Installing ccclub globally so you can run it directly..."));
725
746
  const result = await run("npm install -g ccclub");
726
747
  if (result) {
727
- console.log(chalk8.green(" Done!") + chalk8.dim(" You can now use ") + chalk8.white("ccclub") + chalk8.dim(" directly."));
748
+ console.log(chalk9.green(" Done!") + chalk9.dim(" You can now use ") + chalk9.white("ccclub") + chalk9.dim(" directly."));
728
749
  } else {
729
- console.log(chalk8.dim(" Could not auto-install. Run manually:"));
730
- console.log(chalk8.white(" npm install -g ccclub"));
750
+ console.log(chalk9.dim(" Could not auto-install. Run manually:"));
751
+ console.log(chalk9.white(" npm install -g ccclub"));
731
752
  }
732
753
  }
733
754
 
734
755
  // src/index.ts
735
756
  var program = new Command();
736
- program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.1.13");
757
+ program.name("ccclub").description("CCClub - Compare Claude Code usage with friends").version("0.2.0");
737
758
  program.hook("postAction", () => ensureGlobalInstall());
738
759
  program.command("init").description("Initialize CCClub (one-time setup)").action(initCommand);
739
760
  program.command("join").description("Join a friend's group").argument("<invite-code>", "6-character invite code").action(joinCommand);
@@ -742,4 +763,5 @@ program.command("rank", { isDefault: true }).description("Show leaderboard ranki
742
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);
743
764
  program.command("create").description("Create a new group").action(createGroupCommand);
744
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);
745
767
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccclub",
3
- "version": "0.1.13",
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": {