claude-friends 0.4.2 → 0.4.4

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.
Files changed (3) hide show
  1. package/cli.js +6 -189
  2. package/package.json +2 -5
  3. package/statusline.js +145 -19
package/cli.js CHANGED
@@ -6,7 +6,6 @@ import { homedir } from "os";
6
6
  import { getConfig, createConnection } from "./client.js";
7
7
  import { fileURLToPath } from "url";
8
8
  import { dirname } from "path";
9
- import prompts from "prompts";
10
9
 
11
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
11
  const CONFIG_PATH = join(homedir(), ".claude-friends.json");
@@ -51,199 +50,17 @@ function run(messageType, payload, responseType, formatter) {
51
50
  if (command === "setup") {
52
51
  const existing = getConfig();
53
52
  if (existing) {
54
- console.log(`\nAlready set up as "${existing.username}".`);
55
- console.log(`Config: ${CONFIG_PATH}`);
56
- console.log(`\nTo change username, delete ${CONFIG_PATH} and run again.\n`);
57
- process.exit(0);
58
- }
59
-
60
- // Helper to check username availability
61
- async function checkUsername(name) {
62
- return new Promise((resolve) => {
63
- const ws = createConnection(name);
64
- const timer = setTimeout(() => { ws.close(); resolve(true); }, 5000);
65
- ws.addEventListener("open", () => {
66
- ws.send(JSON.stringify({ type: "check-username", username: name }));
67
- });
68
- ws.addEventListener("message", (event) => {
69
- const msg = JSON.parse(event.data);
70
- if (msg.type === "username-available") {
71
- clearTimeout(timer);
72
- ws.close();
73
- resolve(msg.available);
74
- }
75
- });
76
- ws.addEventListener("error", () => { clearTimeout(timer); resolve(true); });
77
- });
78
- }
79
-
80
- // Helper to add a friend via server
81
- async function addFriend(username, friend) {
82
- return new Promise((resolve) => {
83
- const ws = createConnection(username);
84
- const timer = setTimeout(() => { ws.close(); resolve({ type: "error", message: "Timeout connecting to server." }); }, 5000);
85
- ws.addEventListener("open", () => {
86
- ws.send(JSON.stringify({ type: "add-friend", friend }));
87
- });
88
- ws.addEventListener("message", (event) => {
89
- const msg = JSON.parse(event.data);
90
- if (msg.type === "friend-added" || msg.type === "error") {
91
- clearTimeout(timer);
92
- ws.close();
93
- resolve(msg);
94
- }
95
- });
96
- });
97
- }
98
-
99
- // --- Step 1: Welcome ---
100
- console.log(`
101
- ╔══════════════════════════════════════╗
102
- ║ Welcome to claude-friends! ║
103
- ╚══════════════════════════════════════╝
53
+ console.log(`Already set up as "${existing.username}".`);
54
+ } else {
55
+ console.log(`
56
+ To set up claude-friends, open Claude Code and type:
104
57
 
105
- See when your friends are coding in Claude Code,
106
- share status updates, and nudge each other.
58
+ /friends
107
59
 
108
- Friendship is mutual you can only see someone
109
- online if you've BOTH added each other.
60
+ This will walk you through picking a username and adding friends.
110
61
  `);
111
-
112
- // --- Step 2: Pick a username ---
113
- let username;
114
- while (true) {
115
- const { value } = await prompts({
116
- type: "text",
117
- name: "value",
118
- message: "Pick a username (this is how friends will find you)",
119
- });
120
-
121
- if (!value) { console.log("Setup cancelled."); process.exit(0); }
122
-
123
- const available = await checkUsername(value.trim());
124
- if (!available) {
125
- console.log(` "${value.trim()}" is already taken. Try another.\n`);
126
- continue;
127
- }
128
-
129
- username = value.trim();
130
- break;
131
- }
132
-
133
- console.log(`\n You're "${username}"!\n`);
134
-
135
- // Save config
136
- writeFileSync(CONFIG_PATH, JSON.stringify({ username }, null, 2));
137
-
138
- // --- Step 3: Add friends ---
139
- const { wantFriends } = await prompts({
140
- type: "confirm",
141
- name: "wantFriends",
142
- message: "Want to add some friends now?",
143
- initial: true,
144
- });
145
-
146
- if (wantFriends) {
147
- console.log("\n Tell your friends to add you back with: claude-friends add " + username + "\n");
148
-
149
- let addMore = true;
150
- while (addMore) {
151
- const { friend } = await prompts({
152
- type: "text",
153
- name: "friend",
154
- message: "Friend's username",
155
- });
156
-
157
- if (!friend || !friend.trim()) break;
158
-
159
- const result = await addFriend(username, friend.trim());
160
- if (result.type === "error") {
161
- console.log(` ${result.message}`);
162
- } else if (result.mutual) {
163
- console.log(` You and ${friend.trim()} are now friends!`);
164
- } else {
165
- console.log(` Added! They need to add you back ("${username}") to see each other online.`);
166
- }
167
-
168
- const { more } = await prompts({
169
- type: "confirm",
170
- name: "more",
171
- message: "Add another friend?",
172
- initial: false,
173
- });
174
- addMore = more;
175
- }
176
- }
177
-
178
- // --- Step 4: Install hooks & commands silently ---
179
- // Install slash commands
180
- const commandsDir = join(homedir(), ".claude", "commands");
181
- mkdirSync(commandsDir, { recursive: true });
182
-
183
- const srcCommands = join(__dirname, "commands");
184
- if (existsSync(srcCommands)) {
185
- const files = readdirSync(srcCommands).filter((f) => f.endsWith(".md"));
186
- for (const file of files) {
187
- copyFileSync(join(srcCommands, file), join(commandsDir, file));
188
- }
189
62
  }
190
63
 
191
- // Install hooks to settings.json
192
- const settingsPath = join(homedir(), ".claude", "settings.json");
193
- try {
194
- let settings = {};
195
- if (existsSync(settingsPath)) {
196
- settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
197
- }
198
- if (!settings.hooks) settings.hooks = {};
199
-
200
- // Token-sharing hook (Stop)
201
- if (!settings.hooks.Stop) settings.hooks.Stop = [];
202
- const hookCommand = `node ${join(__dirname, "hooks", "update-tokens.js")}`;
203
- const tokenHookInstalled = settings.hooks.Stop.some((h) =>
204
- h.hooks?.some((hk) => hk.command?.includes("update-tokens"))
205
- );
206
- if (!tokenHookInstalled) {
207
- settings.hooks.Stop.push({
208
- hooks: [{ type: "command", command: hookCommand, async: true }],
209
- });
210
- }
211
-
212
- // Statusline
213
- if (!settings.statusLine) {
214
- settings.statusLine = {
215
- type: "command",
216
- command: `node ${join(__dirname, "statusline.js")}`,
217
- };
218
- }
219
-
220
- // Presence daemon (SessionStart)
221
- if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
222
- const daemonCmd = `node ${join(__dirname, "daemon.js")}`;
223
- const daemonInstalled = settings.hooks.SessionStart.some((h) =>
224
- h.hooks?.some((hk) => hk.command?.includes("daemon.js"))
225
- );
226
- if (!daemonInstalled) {
227
- settings.hooks.SessionStart.push({
228
- hooks: [{ type: "command", command: daemonCmd, async: true }],
229
- });
230
- }
231
-
232
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
233
- } catch {}
234
-
235
- // --- Step 5: Done ---
236
- console.log(`
237
- You're all set! In Claude Code, try:
238
-
239
- /friends See who's online
240
- /friend <name> Add a friend
241
- /nudge <name> Nudge someone
242
- /status <message> Set your status
243
-
244
- Your friends can add you with: claude-friends add ${username}
245
- `);
246
-
247
64
  } else if (command === "check-username") {
248
65
  // Check if a username is available (for Claude Code slash commands)
249
66
  if (!args) { console.log("Usage: claude-friends check-username <username>"); process.exit(1); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-friends",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "See who's online in Claude Code. Add friends, share status, nudge each other.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,8 +9,7 @@
9
9
  "main": "mcp-server.js",
10
10
  "scripts": {
11
11
  "dev": "npx partykit dev",
12
- "deploy": "npx partykit deploy",
13
- "postinstall": "claude-friends setup"
12
+ "deploy": "npx partykit deploy"
14
13
  },
15
14
  "files": [
16
15
  "cli.js",
@@ -23,9 +22,7 @@
23
22
  ],
24
23
  "dependencies": {
25
24
  "@modelcontextprotocol/sdk": "^1.12.1",
26
- "ccstatusline": "^2.2.7",
27
25
  "partysocket": "^1.0.3",
28
- "prompts": "^2.4.2",
29
26
  "zod": "^3.24.4"
30
27
  },
31
28
  "devDependencies": {
package/statusline.js CHANGED
@@ -1,23 +1,149 @@
1
- // Lightweight status line for Claude Code
2
- // Connects, grabs friend count, prints one line, exits
3
- import { getConfig, queryFriends } from "./client.js";
1
+ #!/usr/bin/env node
2
+ // Full-featured status line for Claude Code
3
+ // Reads JSON from stdin, outputs formatted status line
4
4
 
5
- const config = getConfig();
6
- if (!config) {
7
- process.stdout.write("○ friends: run claude-friends setup");
8
- process.exit(0);
5
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs";
6
+ import { join, basename } from "path";
7
+ import { homedir } from "os";
8
+ import { execSync } from "child_process";
9
+
10
+ // Read JSON from stdin
11
+ let input = "";
12
+ try {
13
+ input = readFileSync(0, "utf-8");
14
+ } catch {}
15
+
16
+ let data = {};
17
+ try {
18
+ data = JSON.parse(input);
19
+ } catch {}
20
+
21
+ const segments = [];
22
+
23
+ // 1. Project name
24
+ const projectDir = data.workspace?.project_dir || data.cwd || "";
25
+ if (projectDir) {
26
+ segments.push(basename(projectDir));
9
27
  }
10
28
 
29
+ // 2. Git branch
11
30
  try {
12
- const friends = await queryFriends(config.username, 3000);
13
- const online = friends.filter((f) => f.online);
14
- const dot = online.length > 0 ? "🟢" : "";
15
- const names = online.slice(0, 3).map((f) => f.name).join(", ");
16
- const suffix = online.length > 3 ? "…" : "";
17
- const nameStr = names ? ` (${names}${suffix})` : "";
18
- process.stdout.write(`${dot} ${online.length} online${nameStr}`);
19
- } catch {
20
- process.stdout.write("○ friends: offline");
21
- }
22
-
23
- process.exit(0);
31
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
32
+ cwd: projectDir || undefined,
33
+ stdio: ["pipe", "pipe", "pipe"],
34
+ }).toString().trim();
35
+ if (branch) {
36
+ segments.push(`\u2387 ${branch}`);
37
+ }
38
+ } catch {}
39
+
40
+ // 3. Model
41
+ if (data.model) {
42
+ const modelName = typeof data.model === "object"
43
+ ? (data.model.display_name || data.model.id || "")
44
+ : data.model;
45
+ const short = modelName
46
+ .replace(/^claude-/, "")
47
+ .replace("opus-4-6", "Opus 4.6")
48
+ .replace("sonnet-4-6", "Sonnet 4.6")
49
+ .replace("haiku-4-5-20251001", "Haiku 4.5");
50
+ segments.push(`\u{1F916} ${short}`);
51
+ }
52
+
53
+ // 4. Tokens
54
+ const totalIn = data.context_window?.total_input_tokens;
55
+ const totalOut = data.context_window?.total_output_tokens;
56
+ if (totalIn != null || totalOut != null) {
57
+ const total = (totalIn || 0) + (totalOut || 0);
58
+ segments.push(`${formatNum(total)} tokens`);
59
+ }
60
+
61
+ // 5. Tool calls from transcript
62
+ if (data.transcript_path) {
63
+ try {
64
+ const transcript = readFileSync(data.transcript_path, "utf-8");
65
+ const toolCalls = (transcript.match(/"type"\s*:\s*"tool_use"/g) || []).length;
66
+ if (toolCalls > 0) {
67
+ segments.push(`\u{1F527} ${toolCalls}`);
68
+ }
69
+ } catch {}
70
+ }
71
+
72
+ // 6. Cost
73
+ if (data.cost?.total_cost_usd != null) {
74
+ segments.push(`$${data.cost.total_cost_usd.toFixed(2)}`);
75
+ }
76
+
77
+ // 7. Streak
78
+ segments.push(`\u{1F525} ${getStreak()}d`);
79
+
80
+ // 8. Friends online
81
+ const friends = getFriendsOnline();
82
+ const dot = friends.count > 0 ? "\u{1F7E2}" : "\u25CB";
83
+ const names = friends.names.length > 0
84
+ ? ` (${friends.names.slice(0, 3).join(", ")}${friends.names.length > 3 ? "\u2026" : ""})`
85
+ : "";
86
+ segments.push(`${dot} ${friends.count} online${names}`);
87
+
88
+ process.stdout.write(segments.join(" | "));
89
+
90
+ // --- Helpers ---
91
+
92
+ function formatNum(n) {
93
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
94
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
95
+ return `${n}`;
96
+ }
97
+
98
+ function getFriendsOnline() {
99
+ try {
100
+ const cache = JSON.parse(readFileSync(join(homedir(), ".claude-friends-online.json"), "utf-8"));
101
+ if (Date.now() - cache.timestamp > 30000) return { count: 0, names: [] };
102
+ return { count: cache.onlineCount || 0, names: cache.onlineNames || [] };
103
+ } catch {
104
+ return { count: 0, names: [] };
105
+ }
106
+ }
107
+
108
+ function getStreak() {
109
+ // Collect dates of all session file modifications
110
+ const sessionsDir = join(homedir(), ".claude", "projects");
111
+ try {
112
+ if (!existsSync(sessionsDir)) return 0;
113
+ const activeDates = new Set();
114
+ scanForDates(sessionsDir, activeDates, 0);
115
+
116
+ const today = new Date();
117
+ let streak = 0;
118
+ for (let i = 0; i < 365; i++) {
119
+ const d = new Date(today);
120
+ d.setDate(d.getDate() - i);
121
+ const dateStr = d.toISOString().slice(0, 10);
122
+ if (activeDates.has(dateStr)) {
123
+ streak++;
124
+ } else if (i > 0) {
125
+ break;
126
+ }
127
+ }
128
+ return streak;
129
+ } catch {
130
+ return 0;
131
+ }
132
+ }
133
+
134
+ function scanForDates(dir, dates, depth) {
135
+ if (depth > 4) return;
136
+ try {
137
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
138
+ const full = join(dir, entry.name);
139
+ if (entry.isDirectory()) {
140
+ scanForDates(full, dates, depth + 1);
141
+ } else if (entry.name.endsWith(".jsonl")) {
142
+ try {
143
+ const mtime = statSync(full).mtime;
144
+ dates.add(mtime.toISOString().slice(0, 10));
145
+ } catch {}
146
+ }
147
+ }
148
+ } catch {}
149
+ }