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 +5 -1
- package/hooks/update-tokens.js +63 -40
- 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,87 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// Hook script: reads token
|
|
4
|
-
//
|
|
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,
|
|
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
|
+
// Today at midnight
|
|
21
|
+
const todayStart = new Date();
|
|
22
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
23
|
+
const todayISO = todayStart.toISOString();
|
|
27
24
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
76
|
+
return totalTokens;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const files = getSessionFilesModifiedToday();
|
|
80
|
+
const totalTokens = getTodayTokens(files);
|
|
52
81
|
|
|
53
|
-
|
|
54
|
-
tokens: sessionTokens,
|
|
55
|
-
lastUpdate: Date.now(),
|
|
56
|
-
}));
|
|
82
|
+
if (totalTokens === 0) process.exit(0);
|
|
57
83
|
|
|
58
|
-
// Push to PartyKit
|
|
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:
|
|
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);
|