claude-friends 0.3.1 → 0.3.3

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/cli.js CHANGED
@@ -165,7 +165,11 @@ Token usage is shared automatically.
165
165
  const lines = sorted.map((f) => {
166
166
  const dot = f.online ? "🟢" : "⚫";
167
167
  const status = f.status && f.status !== "offline" && f.status !== "unknown" ? ` — ${f.status}` : "";
168
- const tokens = f.tokensUsed ? ` [${(f.tokensUsed / 1000).toFixed(1)}K tokens]` : "";
168
+ const tokens = f.tokensUsed
169
+ ? f.tokensUsed >= 1_000_000
170
+ ? ` [${(f.tokensUsed / 1_000_000).toFixed(1)}M tokens]`
171
+ : ` [${(f.tokensUsed / 1000).toFixed(1)}K tokens]`
172
+ : "";
169
173
  return `${dot} ${f.name}${status}${tokens}`;
170
174
  });
171
175
 
@@ -1,62 +1,87 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Hook script: reads token estimate from session, pushes to PartyKit
4
- // Called by Claude Code's Stop hook after each response
3
+ // Hook script: reads REAL token usage from Claude Code session files
4
+ // Only counts tokens from today. Pushes to PartyKit.
5
5
 
6
- import { readFileSync, writeFileSync, existsSync } from "fs";
6
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
7
7
  import { join } from "path";
8
8
  import { homedir } from "os";
9
9
 
10
10
  const CONFIG_PATH = join(homedir(), ".claude-friends.json");
11
- const TOKENS_PATH = join(homedir(), ".claude-friends-tokens.json");
12
11
  const PARTY_HOST = "claude-friends-app.nandinitalwar.partykit.dev";
13
12
 
14
- // Read config
15
13
  let config;
16
14
  try {
17
15
  config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
18
16
  } catch {
19
- process.exit(0); // silently exit if not set up
17
+ process.exit(0);
20
18
  }
21
19
 
22
- // Read hook input from stdin
23
- let input = "";
24
- try {
25
- input = readFileSync("/dev/stdin", "utf-8");
26
- } catch {}
20
+ // Today at midnight
21
+ const todayStart = new Date();
22
+ todayStart.setHours(0, 0, 0, 0);
23
+ const todayISO = todayStart.toISOString();
27
24
 
28
- // Parse the stop event to estimate tokens
29
- let tokensThisCall = 0;
30
- try {
31
- const event = JSON.parse(input);
32
- // Estimate based on content length — rough but functional
33
- const content = JSON.stringify(event);
34
- tokensThisCall = Math.ceil(content.length / 4); // ~4 chars per token
35
- } catch {
36
- tokensThisCall = 500; // default estimate per response
37
- }
25
+ // Find all session files modified today
26
+ function getSessionFilesModifiedToday() {
27
+ const projectsDir = join(homedir(), ".claude", "projects");
28
+ if (!existsSync(projectsDir)) return [];
38
29
 
39
- // Accumulate session tokens
40
- let sessionTokens = 0;
41
- try {
42
- if (existsSync(TOKENS_PATH)) {
43
- const data = JSON.parse(readFileSync(TOKENS_PATH, "utf-8"));
44
- // Reset if older than 4 hours (new session)
45
- if (Date.now() - data.lastUpdate < 4 * 60 * 60 * 1000) {
46
- sessionTokens = data.tokens || 0;
30
+ const files = [];
31
+ try {
32
+ for (const dir of readdirSync(projectsDir)) {
33
+ const dirPath = join(projectsDir, dir);
34
+ try {
35
+ for (const file of readdirSync(dirPath)) {
36
+ if (!file.endsWith(".jsonl")) continue;
37
+ const filePath = join(dirPath, file);
38
+ const mtime = statSync(filePath).mtimeMs;
39
+ // Only files modified today
40
+ if (mtime >= todayStart.getTime()) {
41
+ files.push(filePath);
42
+ }
43
+ }
44
+ } catch {}
47
45
  }
46
+ } catch {}
47
+
48
+ return files;
49
+ }
50
+
51
+ // Sum today's token usage from session files
52
+ function getTodayTokens(files) {
53
+ let totalTokens = 0;
54
+
55
+ for (const filePath of files) {
56
+ try {
57
+ const content = readFileSync(filePath, "utf-8");
58
+ for (const line of content.trim().split("\n")) {
59
+ try {
60
+ const entry = JSON.parse(line);
61
+ const usage = entry?.message?.usage;
62
+ const timestamp = entry?.timestamp;
63
+
64
+ // Only count entries from today
65
+ if (usage && timestamp && timestamp >= todayISO) {
66
+ totalTokens +=
67
+ (usage.input_tokens || 0) +
68
+ (usage.cache_creation_input_tokens || 0) +
69
+ (usage.output_tokens || 0);
70
+ }
71
+ } catch {}
72
+ }
73
+ } catch {}
48
74
  }
49
- } catch {}
50
75
 
51
- sessionTokens += tokensThisCall;
76
+ return totalTokens;
77
+ }
78
+
79
+ const files = getSessionFilesModifiedToday();
80
+ const totalTokens = getTodayTokens(files);
52
81
 
53
- writeFileSync(TOKENS_PATH, JSON.stringify({
54
- tokens: sessionTokens,
55
- lastUpdate: Date.now(),
56
- }));
82
+ if (totalTokens === 0) process.exit(0);
57
83
 
58
- // Push to PartyKit via HTTP (faster than WebSocket for one-shot)
59
- // Use the WebSocket approach since PartyKit is WS-only
84
+ // Push to PartyKit
60
85
  try {
61
86
  const { default: PartySocket } = await import("partysocket");
62
87
  const ws = new PartySocket({
@@ -66,13 +91,11 @@ try {
66
91
  });
67
92
 
68
93
  ws.addEventListener("open", () => {
69
- ws.send(JSON.stringify({ type: "share-tokens", tokens: sessionTokens }));
94
+ ws.send(JSON.stringify({ type: "share-tokens", tokens: totalTokens }));
70
95
  setTimeout(() => { ws.close(); process.exit(0); }, 500);
71
96
  });
72
97
 
73
98
  ws.addEventListener("error", () => process.exit(0));
74
-
75
- // Don't hang
76
99
  setTimeout(() => process.exit(0), 3000);
77
100
  } catch {
78
101
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-friends",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "See who's online in Claude Code. Add friends, share status, nudge each other.",
5
5
  "type": "module",
6
6
  "bin": {