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 +5 -1
- package/hooks/update-tokens.js +61 -39
- package/package.json +1 -1
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
|
|
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
|
|
package/hooks/update-tokens.js
CHANGED
|
@@ -1,62 +1,86 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Hook script: reads token
|
|
4
|
-
// Called by
|
|
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,
|
|
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);
|
|
17
|
+
process.exit(0);
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
let
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
}
|
|
76
|
+
}
|
|
50
77
|
|
|
51
|
-
|
|
78
|
+
const sessionFile = getLatestSessionFile();
|
|
79
|
+
const totalTokens = getTokensFromSession(sessionFile);
|
|
52
80
|
|
|
53
|
-
|
|
54
|
-
tokens: sessionTokens,
|
|
55
|
-
lastUpdate: Date.now(),
|
|
56
|
-
}));
|
|
81
|
+
if (totalTokens === 0) process.exit(0);
|
|
57
82
|
|
|
58
|
-
// Push to PartyKit
|
|
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:
|
|
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);
|