claude-friends 0.4.0 → 0.4.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
@@ -1,12 +1,12 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --no-warnings
2
2
 
3
3
  import { writeFileSync, readFileSync, existsSync, mkdirSync, copyFileSync, readdirSync } from "fs";
4
4
  import { join } from "path";
5
5
  import { homedir } from "os";
6
- import { createInterface } from "readline";
7
6
  import { getConfig, createConnection } from "./client.js";
8
7
  import { fileURLToPath } from "url";
9
8
  import { dirname } from "path";
9
+ import prompts from "prompts";
10
10
 
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
12
  const CONFIG_PATH = join(homedir(), ".claude-friends.json");
@@ -14,6 +14,10 @@ const CONFIG_PATH = join(homedir(), ".claude-friends.json");
14
14
  const command = process.argv[2];
15
15
  const args = process.argv.slice(3).join(" ").trim();
16
16
 
17
+ main().catch((err) => { console.error(err.message); process.exit(1); });
18
+
19
+ async function main() {
20
+
17
21
  // Helper: connect, send a message, wait for response, print, exit
18
22
  function run(messageType, payload, responseType, formatter) {
19
23
  const config = getConfig();
@@ -53,25 +57,126 @@ if (command === "setup") {
53
57
  process.exit(0);
54
58
  }
55
59
 
56
- const rl = createInterface({ input: process.stdin, output: process.stdout });
57
- const ask = (q) => new Promise((res) => rl.question(q, res));
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
+ }
58
98
 
99
+ // --- Step 1: Welcome ---
59
100
  console.log(`
60
101
  ╔══════════════════════════════════════╗
61
- claude-friends setup
102
+ Welcome to claude-friends!
62
103
  ╚══════════════════════════════════════╝
104
+
105
+ See when your friends are coding in Claude Code,
106
+ share status updates, and nudge each other.
107
+
108
+ Friendship is mutual — you can only see someone
109
+ online if you've BOTH added each other.
63
110
  `);
64
111
 
65
- const username = await ask("Choose a username: ");
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); }
66
122
 
67
- if (!username.trim()) {
68
- console.log("Username can't be empty.");
69
- process.exit(1);
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;
70
131
  }
71
132
 
72
- writeFileSync(CONFIG_PATH, JSON.stringify({ username: username.trim() }, null, 2));
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
+ }
73
167
 
74
- // Install slash commands to ~/.claude/commands/
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
75
180
  const commandsDir = join(homedir(), ".claude", "commands");
76
181
  mkdirSync(commandsDir, { recursive: true });
77
182
 
@@ -81,95 +186,186 @@ if (command === "setup") {
81
186
  for (const file of files) {
82
187
  copyFileSync(join(srcCommands, file), join(commandsDir, file));
83
188
  }
84
- console.log(`\nInstalled slash commands: ${files.map((f) => "/" + f.replace(".md", "")).join(", ")}`);
85
189
  }
86
190
 
87
- // Install token-sharing hook to ~/.claude/settings.json
191
+ // Install hooks to settings.json
88
192
  const settingsPath = join(homedir(), ".claude", "settings.json");
89
- const hookCommand = `node ${join(__dirname, "hooks", "update-tokens.js")}`;
90
193
  try {
91
194
  let settings = {};
92
195
  if (existsSync(settingsPath)) {
93
196
  settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
94
197
  }
95
198
  if (!settings.hooks) settings.hooks = {};
96
- if (!settings.hooks.Stop) settings.hooks.Stop = [];
97
199
 
98
- const alreadyInstalled = settings.hooks.Stop.some((h) =>
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) =>
99
204
  h.hooks?.some((hk) => hk.command?.includes("update-tokens"))
100
205
  );
101
-
102
- if (!alreadyInstalled) {
206
+ if (!tokenHookInstalled) {
103
207
  settings.hooks.Stop.push({
104
- hooks: [{
105
- type: "command",
106
- command: hookCommand,
107
- async: true,
108
- }],
208
+ hooks: [{ type: "command", command: hookCommand, async: true }],
109
209
  });
110
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
111
- console.log("Installed auto token-sharing hook.");
112
- }
113
- } catch (err) {
114
- console.log("Could not install token hook (non-critical):", err.message);
115
- }
116
-
117
- // Install statusline + daemon hooks (read settings once)
118
- try {
119
- const settingsPath2 = join(homedir(), ".claude", "settings.json");
120
- let settings = {};
121
- if (existsSync(settingsPath2)) {
122
- settings = JSON.parse(readFileSync(settingsPath2, "utf-8"));
123
210
  }
124
211
 
212
+ // Statusline
125
213
  if (!settings.statusLine) {
126
214
  settings.statusLine = {
127
215
  type: "command",
128
216
  command: `node ${join(__dirname, "statusline.js")}`,
129
217
  };
130
- console.log("Installed status line.");
131
218
  }
132
219
 
133
- // SessionStart hook: spawn daemon to keep user online
134
- if (!settings.hooks) settings.hooks = {};
135
- const daemonCmd = `node ${join(__dirname, "daemon.js")}`;
136
-
220
+ // Presence daemon (SessionStart)
137
221
  if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
222
+ const daemonCmd = `node ${join(__dirname, "daemon.js")}`;
138
223
  const daemonInstalled = settings.hooks.SessionStart.some((h) =>
139
224
  h.hooks?.some((hk) => hk.command?.includes("daemon.js"))
140
225
  );
141
226
  if (!daemonInstalled) {
142
227
  settings.hooks.SessionStart.push({
143
- hooks: [{
144
- type: "command",
145
- command: daemonCmd,
146
- async: true,
147
- }],
228
+ hooks: [{ type: "command", command: daemonCmd, async: true }],
148
229
  });
149
- console.log("Installed presence daemon (keeps you online).");
150
230
  }
151
231
 
152
- writeFileSync(settingsPath2, JSON.stringify(settings, null, 2));
232
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
153
233
  } catch {}
154
234
 
235
+ // --- Step 5: Done ---
155
236
  console.log(`
156
- Done! You're "${username.trim()}".
237
+ You're all set! In Claude Code, try:
157
238
 
158
- In Claude Code:
159
- /friend alice Add a friend
160
239
  /friends See who's online
161
- /nudge bob hey! Nudge someone
162
- /status debugging Set your status
163
- /unfriend alice Remove a friend
240
+ /friend <name> Add a friend
241
+ /nudge <name> Nudge someone
242
+ /status <message> Set your status
164
243
 
165
- Token usage is shared automatically.
244
+ Your friends can add you with: claude-friends add ${username}
166
245
  `);
167
246
 
168
- rl.close();
247
+ } else if (command === "check-username") {
248
+ // Check if a username is available (for Claude Code slash commands)
249
+ if (!args) { console.log("Usage: claude-friends check-username <username>"); process.exit(1); }
250
+ const name = args.trim();
251
+ const ws = createConnection(name);
252
+ const timer = setTimeout(() => { ws.close(); console.log("available"); process.exit(0); }, 5000);
253
+ ws.addEventListener("open", () => {
254
+ ws.send(JSON.stringify({ type: "check-username", username: name }));
255
+ });
256
+ ws.addEventListener("message", (event) => {
257
+ const msg = JSON.parse(event.data);
258
+ if (msg.type === "username-available") {
259
+ clearTimeout(timer);
260
+ ws.close();
261
+ console.log(msg.available ? "available" : "taken");
262
+ process.exit(msg.available ? 0 : 1);
263
+ }
264
+ });
265
+
266
+ } else if (command === "setup-noninteractive") {
267
+ // Non-interactive setup for use by Claude Code slash commands
268
+ if (!args) { console.log("Usage: claude-friends setup-noninteractive <username>"); process.exit(1); }
269
+
270
+ const username = args.trim();
271
+
272
+ // Check if already set up
273
+ const existing = getConfig();
274
+ if (existing) {
275
+ console.log(`Already set up as "${existing.username}".`);
276
+ process.exit(0);
277
+ }
278
+
279
+ // Check username availability
280
+ const available = await new Promise((resolve) => {
281
+ const ws = createConnection(username);
282
+ const timer = setTimeout(() => { ws.close(); resolve(true); }, 5000);
283
+ ws.addEventListener("open", () => {
284
+ ws.send(JSON.stringify({ type: "check-username", username }));
285
+ });
286
+ ws.addEventListener("message", (event) => {
287
+ const msg = JSON.parse(event.data);
288
+ if (msg.type === "username-available") {
289
+ clearTimeout(timer);
290
+ ws.close();
291
+ resolve(msg.available);
292
+ }
293
+ });
294
+ ws.addEventListener("error", () => { clearTimeout(timer); resolve(true); });
295
+ });
296
+
297
+ if (!available) {
298
+ console.log(`Username "${username}" is already taken.`);
299
+ process.exit(1);
300
+ }
301
+
302
+ // Register on server
303
+ await new Promise((resolve) => {
304
+ const ws = createConnection(username);
305
+ const timer = setTimeout(() => { ws.close(); resolve(); }, 5000);
306
+ ws.addEventListener("open", () => {
307
+ ws.send(JSON.stringify({ type: "register" }));
308
+ });
309
+ ws.addEventListener("message", (event) => {
310
+ const msg = JSON.parse(event.data);
311
+ if (msg.type === "register-result") {
312
+ clearTimeout(timer);
313
+ ws.close();
314
+ resolve();
315
+ }
316
+ });
317
+ });
318
+
319
+ // Save config
320
+ writeFileSync(CONFIG_PATH, JSON.stringify({ username }, null, 2));
321
+
322
+ // Install slash commands
323
+ const commandsDir = join(homedir(), ".claude", "commands");
324
+ mkdirSync(commandsDir, { recursive: true });
325
+ const srcCommands = join(__dirname, "commands");
326
+ if (existsSync(srcCommands)) {
327
+ for (const file of readdirSync(srcCommands).filter((f) => f.endsWith(".md"))) {
328
+ copyFileSync(join(srcCommands, file), join(commandsDir, file));
329
+ }
330
+ }
331
+
332
+ // Install hooks
333
+ const settingsPath = join(homedir(), ".claude", "settings.json");
334
+ try {
335
+ let settings = {};
336
+ if (existsSync(settingsPath)) {
337
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
338
+ }
339
+ if (!settings.hooks) settings.hooks = {};
340
+
341
+ if (!settings.hooks.Stop) settings.hooks.Stop = [];
342
+ const hookCommand = `node ${join(__dirname, "hooks", "update-tokens.js")}`;
343
+ if (!settings.hooks.Stop.some((h) => h.hooks?.some((hk) => hk.command?.includes("update-tokens")))) {
344
+ settings.hooks.Stop.push({ hooks: [{ type: "command", command: hookCommand, async: true }] });
345
+ }
346
+
347
+ if (!settings.statusLine) {
348
+ settings.statusLine = { type: "command", command: `node ${join(__dirname, "statusline.js")}` };
349
+ }
350
+
351
+ if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
352
+ const daemonCmd = `node ${join(__dirname, "daemon.js")}`;
353
+ if (!settings.hooks.SessionStart.some((h) => h.hooks?.some((hk) => hk.command?.includes("daemon.js")))) {
354
+ settings.hooks.SessionStart.push({ hooks: [{ type: "command", command: daemonCmd, async: true }] });
355
+ }
356
+
357
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
358
+ } catch {}
359
+
360
+ console.log(`Set up as "${username}".`);
169
361
 
170
362
  } else if (command === "add") {
171
363
  if (!args) { console.log("Usage: claude-friends add <username>"); process.exit(1); }
172
- run("add-friend", { friend: args }, "friend-added", () => `Added ${args} as a friend!`);
364
+ run("add-friend", { friend: args }, "friend-added", (msg) =>
365
+ msg.mutual
366
+ ? `You and ${args} are now friends!`
367
+ : `Added ${args}! They need to add you back to see each other online.`
368
+ );
173
369
 
174
370
  } else if (command === "remove") {
175
371
  if (!args) { console.log("Usage: claude-friends remove <username>"); process.exit(1); }
@@ -180,21 +376,44 @@ Token usage is shared automatically.
180
376
  const friends = msg.friends || [];
181
377
  if (friends.length === 0) return "No friends yet. Run: claude-friends add <username>";
182
378
 
183
- const sorted = [...friends].sort((a, b) => (b.online ? 1 : 0) - (a.online ? 1 : 0));
379
+ const mutual = friends.filter((f) => f.mutual);
380
+ const pending = friends.filter((f) => !f.mutual);
381
+ const sorted = [...mutual].sort((a, b) => (b.online ? 1 : 0) - (a.online ? 1 : 0));
184
382
  const onlineCount = sorted.filter((f) => f.online).length;
185
383
 
186
384
  const lines = sorted.map((f) => {
187
385
  const dot = f.online ? "🟢" : "⚫";
188
386
  const status = f.status && f.status !== "offline" && f.status !== "unknown" ? ` — ${f.status}` : "";
189
- const tokens = f.tokensUsed
190
- ? f.tokensUsed >= 1_000_000
191
- ? ` [${(f.tokensUsed / 1_000_000).toFixed(1)}M tokens]`
192
- : ` [${(f.tokensUsed / 1000).toFixed(1)}K tokens]`
193
- : "";
194
- return `${dot} ${f.name}${status}${tokens}`;
387
+ return `${dot} ${f.name}${status}`;
195
388
  });
196
389
 
197
- return `Friends (${onlineCount}/${friends.length} online):\n${lines.join("\n")}`;
390
+ let output = `Friends (${onlineCount}/${mutual.length} online):\n${lines.join("\n")}`;
391
+
392
+ if (pending.length > 0) {
393
+ output += `\n\nPending (waiting for them to add you back):\n${pending.map((f) => `⏳ ${f.name}`).join("\n")}`;
394
+ }
395
+
396
+ // Token usage graph
397
+ const withTokens = sorted.filter((f) => f.tokensUsed > 0);
398
+ if (withTokens.length > 0) {
399
+ const maxTokens = Math.max(...withTokens.map((f) => f.tokensUsed));
400
+ const maxNameLen = Math.max(...withTokens.map((f) => f.name.length));
401
+ const barWidth = 20;
402
+
403
+ output += "\n\nToken usage today:";
404
+ for (const f of withTokens) {
405
+ const name = f.name.padEnd(maxNameLen);
406
+ const filled = Math.max(1, Math.round((f.tokensUsed / maxTokens) * barWidth));
407
+ const bar = "█".repeat(filled);
408
+ let tokenStr;
409
+ if (f.tokensUsed >= 1_000_000) tokenStr = `${(f.tokensUsed / 1_000_000).toFixed(1)}M`;
410
+ else if (f.tokensUsed >= 1_000) tokenStr = `${(f.tokensUsed / 1_000).toFixed(1)}K`;
411
+ else tokenStr = `${f.tokensUsed}`;
412
+ output += `\n ${name} ${bar} ${tokenStr}`;
413
+ }
414
+ }
415
+
416
+ return output;
198
417
  });
199
418
 
200
419
  } else if (command === "status") {
@@ -248,3 +467,5 @@ In Claude Code:
248
467
  /nudge bob hey!
249
468
  `);
250
469
  }
470
+
471
+ } // end main
@@ -1 +1,7 @@
1
- Run `claude-friends add $ARGUMENTS` and show the output. If no username is provided, ask for one first.
1
+ Add a friend by username.
2
+
3
+ First, check if claude-friends is set up by running `claude-friends whoami`. If it returns "Not set up", run the onboarding flow described in the /friends command BEFORE doing anything else.
4
+
5
+ If set up: run `claude-friends add $ARGUMENTS` and show the output. If no username is provided, ask for one using AskUserQuestion.
6
+
7
+ Remind the user that friendship is mutual — their friend needs to add them back to see each other online.
@@ -1 +1,32 @@
1
- Run `claude-friends online` and show the output.
1
+ See who's online.
2
+
3
+ First, check if claude-friends is set up by running `claude-friends whoami`. If it returns "Not set up", run the onboarding flow below BEFORE doing anything else. If it IS set up, skip to "Show friends".
4
+
5
+ ## Onboarding (only if not set up)
6
+
7
+ Walk the user through setup using AskUserQuestion:
8
+
9
+ 1. Explain: "claude-friends lets you see when your friends are coding in Claude Code. Friendship is mutual — you can only see each other online if you've BOTH added each other."
10
+
11
+ 2. Before asking the user to pick a username, generate 2-3 suggestions based on their system username (e.g. first name, initials, short nickname). Run `claude-friends check-username <name>` for EACH suggestion IN PARALLEL to check availability. Only include suggestions that return "available" as options in the AskUserQuestion. This way every option shown is guaranteed available. If the user types their own via "Other", run the check after — if taken, tell them immediately and re-ask.
12
+
13
+ 3. Run `claude-friends setup-noninteractive <username>` to register the chosen username.
14
+
15
+ 4. Ask "Want to add some friends?" (Yes / No). If yes, go DIRECTLY to asking for a username — do NOT show an intermediate screen. Use AskUserQuestion with two options like "Skip" and "Done adding" so the user types their friend's username via "Other". After each add:
16
+ - Run `claude-friends add <friend>` and show the result
17
+ - Ask "Add another?" (Yes / No) — repeat until they say no
18
+ - Remind them: "Tell your friends to add you back with: claude-friends add <their-username>"
19
+
20
+ 5. Say "You're all set!" and continue to Show friends below.
21
+
22
+ ## Show friends
23
+
24
+ Run `claude-friends online` and show the output.
25
+
26
+ If any friends have token usage data, also show a bar chart comparing their usage. Use block characters (█) to draw horizontal bars, scaled relative to the highest usage. Example:
27
+
28
+ ```
29
+ tej ██████████████████ 245.3K
30
+ alice ████████ 102.1K
31
+ bob ███ 38.5K
32
+ ```
package/daemon.js CHANGED
@@ -2,23 +2,56 @@
2
2
 
3
3
  // Background daemon that keeps you online in claude-friends.
4
4
  // Runs as a persistent WebSocket connection.
5
- // Started automatically by the SessionStart hook.
5
+ // Also periodically writes friend status to a cache file
6
+ // so the statusline can read it synchronously.
6
7
 
8
+ import { writeFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
7
11
  import { getConfig, createConnection } from "./client.js";
8
12
 
9
13
  const config = getConfig();
10
14
  if (!config) process.exit(0);
11
15
 
16
+ const CACHE_PATH = join(homedir(), ".claude-friends-online.json");
12
17
  const ws = createConnection(config.username);
13
18
 
19
+ function updateCache(friends) {
20
+ const onlineNames = friends.filter((f) => f.online).map((f) => f.name);
21
+ try {
22
+ writeFileSync(CACHE_PATH, JSON.stringify({
23
+ onlineCount: onlineNames.length,
24
+ onlineNames,
25
+ lastUpdate: Date.now(),
26
+ }));
27
+ } catch {}
28
+ }
29
+
14
30
  ws.addEventListener("open", () => {
15
- // Silently connected we're online
31
+ // Request friends list immediately and periodically
32
+ ws.send(JSON.stringify({ type: "get-friends" }));
16
33
  });
17
34
 
18
- ws.addEventListener("close", () => {
19
- // Reconnect after a delay (partysocket handles this automatically)
35
+ ws.addEventListener("message", (event) => {
36
+ try {
37
+ const msg = JSON.parse(event.data);
38
+ if (msg.type === "friends-list") {
39
+ updateCache(msg.friends || []);
40
+ }
41
+ // Also update on presence changes
42
+ if (msg.type === "presence" || msg.type === "state") {
43
+ ws.send(JSON.stringify({ type: "get-friends" }));
44
+ }
45
+ } catch {}
20
46
  });
21
47
 
48
+ // Poll friends every 15 seconds
49
+ setInterval(() => {
50
+ if (ws.readyState === 1) {
51
+ ws.send(JSON.stringify({ type: "get-friends" }));
52
+ }
53
+ }, 15000);
54
+
22
55
  // Keep process alive
23
56
  setInterval(() => {}, 60000);
24
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-friends",
3
- "version": "0.4.0",
3
+ "version": "0.4.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": {
@@ -9,7 +9,8 @@
9
9
  "main": "mcp-server.js",
10
10
  "scripts": {
11
11
  "dev": "npx partykit dev",
12
- "deploy": "npx partykit deploy"
12
+ "deploy": "npx partykit deploy",
13
+ "postinstall": "claude-friends setup"
13
14
  },
14
15
  "files": [
15
16
  "cli.js",
@@ -22,7 +23,9 @@
22
23
  ],
23
24
  "dependencies": {
24
25
  "@modelcontextprotocol/sdk": "^1.12.1",
26
+ "ccstatusline": "^2.2.7",
25
27
  "partysocket": "^1.0.3",
28
+ "prompts": "^2.4.2",
26
29
  "zod": "^3.24.4"
27
30
  },
28
31
  "devDependencies": {
package/statusline.js CHANGED
@@ -11,7 +11,7 @@ if (!config) {
11
11
  try {
12
12
  const friends = await queryFriends(config.username, 3000);
13
13
  const online = friends.filter((f) => f.online);
14
- const dot = online.length > 0 ? "" : "";
14
+ const dot = online.length > 0 ? "🟢" : "";
15
15
  const names = online.slice(0, 3).map((f) => f.name).join(", ");
16
16
  const suffix = online.length > 3 ? "…" : "";
17
17
  const nameStr = names ? ` (${names}${suffix})` : "";
package/commands/nudge.md DELETED
@@ -1 +0,0 @@
1
- Run `claude-friends nudge $ARGUMENTS` and show the output. If no username is provided, ask for one first.
@@ -1 +0,0 @@
1
- Run `claude-friends status $ARGUMENTS` and show the output. If no status message is provided, ask for one first.
@@ -1 +0,0 @@
1
- Run `claude-friends remove $ARGUMENTS` and show the output. If no username is provided, ask for one first.