claude-friends 0.3.2 → 0.4.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.
package/cli.js CHANGED
@@ -114,21 +114,42 @@ if (command === "setup") {
114
114
  console.log("Could not install token hook (non-critical):", err.message);
115
115
  }
116
116
 
117
- // Install statusline
117
+ // Install statusline + daemon hooks (read settings once)
118
118
  try {
119
119
  const settingsPath2 = join(homedir(), ".claude", "settings.json");
120
120
  let settings = {};
121
121
  if (existsSync(settingsPath2)) {
122
122
  settings = JSON.parse(readFileSync(settingsPath2, "utf-8"));
123
123
  }
124
+
124
125
  if (!settings.statusLine) {
125
126
  settings.statusLine = {
126
127
  type: "command",
127
128
  command: `node ${join(__dirname, "statusline.js")}`,
128
129
  };
129
- writeFileSync(settingsPath2, JSON.stringify(settings, null, 2));
130
130
  console.log("Installed status line.");
131
131
  }
132
+
133
+ // SessionStart hook: spawn daemon to keep user online
134
+ if (!settings.hooks) settings.hooks = {};
135
+ const daemonCmd = `node ${join(__dirname, "daemon.js")}`;
136
+
137
+ if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
138
+ const daemonInstalled = settings.hooks.SessionStart.some((h) =>
139
+ h.hooks?.some((hk) => hk.command?.includes("daemon.js"))
140
+ );
141
+ if (!daemonInstalled) {
142
+ settings.hooks.SessionStart.push({
143
+ hooks: [{
144
+ type: "command",
145
+ command: daemonCmd,
146
+ async: true,
147
+ }],
148
+ });
149
+ console.log("Installed presence daemon (keeps you online).");
150
+ }
151
+
152
+ writeFileSync(settingsPath2, JSON.stringify(settings, null, 2));
132
153
  } catch {}
133
154
 
134
155
  console.log(`
package/daemon.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Background daemon that keeps you online in claude-friends.
4
+ // Runs as a persistent WebSocket connection.
5
+ // Started automatically by the SessionStart hook.
6
+
7
+ import { getConfig, createConnection } from "./client.js";
8
+
9
+ const config = getConfig();
10
+ if (!config) process.exit(0);
11
+
12
+ const ws = createConnection(config.username);
13
+
14
+ ws.addEventListener("open", () => {
15
+ // Silently connected — we're online
16
+ });
17
+
18
+ ws.addEventListener("close", () => {
19
+ // Reconnect after a delay (partysocket handles this automatically)
20
+ });
21
+
22
+ // Keep process alive
23
+ setInterval(() => {}, 60000);
24
+
25
+ // Clean exit
26
+ process.on("SIGINT", () => { ws.close(); process.exit(0); });
27
+ process.on("SIGTERM", () => { ws.close(); process.exit(0); });
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
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.
4
+ // Only counts tokens from today. Pushes to PartyKit.
5
5
 
6
6
  import { readFileSync, readdirSync, statSync, existsSync } from "fs";
7
7
  import { join } from "path";
@@ -17,14 +17,17 @@ try {
17
17
  process.exit(0);
18
18
  }
19
19
 
20
- // Find the most recently modified session file
21
- function getLatestSessionFile() {
22
- const projectsDir = join(homedir(), ".claude", "projects");
23
- if (!existsSync(projectsDir)) return null;
20
+ // Today at midnight
21
+ const todayStart = new Date();
22
+ todayStart.setHours(0, 0, 0, 0);
23
+ const todayISO = todayStart.toISOString();
24
24
 
25
- let latestFile = null;
26
- let latestMtime = 0;
25
+ // Find all session files modified today
26
+ function getSessionFilesModifiedToday() {
27
+ const projectsDir = join(homedir(), ".claude", "projects");
28
+ if (!existsSync(projectsDir)) return [];
27
29
 
30
+ const files = [];
28
31
  try {
29
32
  for (const dir of readdirSync(projectsDir)) {
30
33
  const dirPath = join(projectsDir, dir);
@@ -33,50 +36,48 @@ function getLatestSessionFile() {
33
36
  if (!file.endsWith(".jsonl")) continue;
34
37
  const filePath = join(dirPath, file);
35
38
  const mtime = statSync(filePath).mtimeMs;
36
- if (mtime > latestMtime) {
37
- latestMtime = mtime;
38
- latestFile = filePath;
39
+ // Only files modified today
40
+ if (mtime >= todayStart.getTime()) {
41
+ files.push(filePath);
39
42
  }
40
43
  }
41
44
  } catch {}
42
45
  }
43
46
  } catch {}
44
47
 
45
- return latestFile;
48
+ return files;
46
49
  }
47
50
 
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 {}
70
- }
71
-
72
- return totalTokens;
73
- } catch {
74
- return 0;
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 {}
75
74
  }
75
+
76
+ return totalTokens;
76
77
  }
77
78
 
78
- const sessionFile = getLatestSessionFile();
79
- const totalTokens = getTokensFromSession(sessionFile);
79
+ const files = getSessionFilesModifiedToday();
80
+ const totalTokens = getTodayTokens(files);
80
81
 
81
82
  if (totalTokens === 0) process.exit(0);
82
83
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-friends",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "See who's online in Claude Code. Add friends, share status, nudge each other.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,8 @@
17
17
  "statusline.js",
18
18
  "client.js",
19
19
  "commands/",
20
- "hooks/"
20
+ "hooks/",
21
+ "daemon.js"
21
22
  ],
22
23
  "dependencies": {
23
24
  "@modelcontextprotocol/sdk": "^1.12.1",