claude-friends 0.3.0 → 0.3.2

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,86 @@
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
+ // and pushes it to PartyKit. Called by the Stop hook after each response.
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
+ // Find the most recently modified session file
21
+ function getLatestSessionFile() {
22
+ const projectsDir = join(homedir(), ".claude", "projects");
23
+ if (!existsSync(projectsDir)) return null;
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
25
+ let latestFile = null;
26
+ let latestMtime = 0;
27
+
28
+ try {
29
+ for (const dir of readdirSync(projectsDir)) {
30
+ const dirPath = join(projectsDir, dir);
31
+ try {
32
+ for (const file of readdirSync(dirPath)) {
33
+ if (!file.endsWith(".jsonl")) continue;
34
+ const filePath = join(dirPath, file);
35
+ const mtime = statSync(filePath).mtimeMs;
36
+ if (mtime > latestMtime) {
37
+ latestMtime = mtime;
38
+ latestFile = filePath;
39
+ }
40
+ }
41
+ } catch {}
42
+ }
43
+ } catch {}
44
+
45
+ return latestFile;
37
46
  }
38
47
 
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;
48
+ // Sum token usage from session file
49
+ function getTokensFromSession(filePath) {
50
+ if (!filePath) return 0;
51
+
52
+ try {
53
+ const content = readFileSync(filePath, "utf-8");
54
+ const lines = content.trim().split("\n");
55
+
56
+ let totalTokens = 0;
57
+ for (const line of lines) {
58
+ try {
59
+ const entry = JSON.parse(line);
60
+ const usage = entry?.message?.usage;
61
+ if (usage) {
62
+ // Count input + output + cache writes (real cost)
63
+ // Exclude cache_read — those are nearly free and inflate the number
64
+ totalTokens +=
65
+ (usage.input_tokens || 0) +
66
+ (usage.cache_creation_input_tokens || 0) +
67
+ (usage.output_tokens || 0);
68
+ }
69
+ } catch {}
47
70
  }
71
+
72
+ return totalTokens;
73
+ } catch {
74
+ return 0;
48
75
  }
49
- } catch {}
76
+ }
50
77
 
51
- sessionTokens += tokensThisCall;
78
+ const sessionFile = getLatestSessionFile();
79
+ const totalTokens = getTokensFromSession(sessionFile);
52
80
 
53
- writeFileSync(TOKENS_PATH, JSON.stringify({
54
- tokens: sessionTokens,
55
- lastUpdate: Date.now(),
56
- }));
81
+ if (totalTokens === 0) process.exit(0);
57
82
 
58
- // Push to PartyKit via HTTP (faster than WebSocket for one-shot)
59
- // Use the WebSocket approach since PartyKit is WS-only
83
+ // Push to PartyKit
60
84
  try {
61
85
  const { default: PartySocket } = await import("partysocket");
62
86
  const ws = new PartySocket({
@@ -66,13 +90,11 @@ try {
66
90
  });
67
91
 
68
92
  ws.addEventListener("open", () => {
69
- ws.send(JSON.stringify({ type: "share-tokens", tokens: sessionTokens }));
93
+ ws.send(JSON.stringify({ type: "share-tokens", tokens: totalTokens }));
70
94
  setTimeout(() => { ws.close(); process.exit(0); }, 500);
71
95
  });
72
96
 
73
97
  ws.addEventListener("error", () => process.exit(0));
74
-
75
- // Don't hang
76
98
  setTimeout(() => process.exit(0), 3000);
77
99
  } catch {
78
100
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-friends",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "See who's online in Claude Code. Add friends, share status, nudge each other.",
5
5
  "type": "module",
6
6
  "bin": {