claude-friends 0.3.3 → 0.4.1

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
@@ -3,10 +3,10 @@
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
+ });
66
120
 
67
- if (!username.trim()) {
68
- console.log("Username can't be empty.");
69
- process.exit(1);
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;
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
+ }
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
+ }
73
177
 
74
- // Install slash commands to ~/.claude/commands/
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,74 +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
210
  }
113
- } catch (err) {
114
- console.log("Could not install token hook (non-critical):", err.message);
115
- }
116
211
 
117
- // Install statusline
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
- }
212
+ // Statusline
124
213
  if (!settings.statusLine) {
125
214
  settings.statusLine = {
126
215
  type: "command",
127
216
  command: `node ${join(__dirname, "statusline.js")}`,
128
217
  };
129
- writeFileSync(settingsPath2, JSON.stringify(settings, null, 2));
130
- console.log("Installed status line.");
131
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));
132
233
  } catch {}
133
234
 
235
+ // --- Step 5: Done ---
134
236
  console.log(`
135
- Done! You're "${username.trim()}".
237
+ You're all set! In Claude Code, try:
136
238
 
137
- In Claude Code:
138
- /friend alice Add a friend
139
239
  /friends See who's online
140
- /nudge bob hey! Nudge someone
141
- /status debugging Set your status
142
- /unfriend alice Remove a friend
240
+ /friend <name> Add a friend
241
+ /nudge <name> Nudge someone
242
+ /status <message> Set your status
143
243
 
144
- Token usage is shared automatically.
244
+ Your friends can add you with: claude-friends add ${username}
145
245
  `);
146
246
 
147
- 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}".`);
148
361
 
149
362
  } else if (command === "add") {
150
363
  if (!args) { console.log("Usage: claude-friends add <username>"); process.exit(1); }
151
- 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
+ );
152
369
 
153
370
  } else if (command === "remove") {
154
371
  if (!args) { console.log("Usage: claude-friends remove <username>"); process.exit(1); }
@@ -159,21 +376,44 @@ Token usage is shared automatically.
159
376
  const friends = msg.friends || [];
160
377
  if (friends.length === 0) return "No friends yet. Run: claude-friends add <username>";
161
378
 
162
- 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));
163
382
  const onlineCount = sorted.filter((f) => f.online).length;
164
383
 
165
384
  const lines = sorted.map((f) => {
166
385
  const dot = f.online ? "🟢" : "⚫";
167
386
  const status = f.status && f.status !== "offline" && f.status !== "unknown" ? ` — ${f.status}` : "";
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
- : "";
173
- return `${dot} ${f.name}${status}${tokens}`;
387
+ return `${dot} ${f.name}${status}`;
174
388
  });
175
389
 
176
- 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;
177
417
  });
178
418
 
179
419
  } else if (command === "status") {
@@ -227,3 +467,5 @@ In Claude Code:
227
467
  /nudge bob hey!
228
468
  `);
229
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 ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Background daemon that keeps you online in claude-friends.
4
+ // Runs as a persistent WebSocket connection.
5
+ // Also periodically writes friend status to a cache file
6
+ // so the statusline can read it synchronously.
7
+
8
+ import { writeFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ import { getConfig, createConnection } from "./client.js";
12
+
13
+ const config = getConfig();
14
+ if (!config) process.exit(0);
15
+
16
+ const CACHE_PATH = join(homedir(), ".claude-friends-online.json");
17
+ const ws = createConnection(config.username);
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
+
30
+ ws.addEventListener("open", () => {
31
+ // Request friends list immediately and periodically
32
+ ws.send(JSON.stringify({ type: "get-friends" }));
33
+ });
34
+
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 {}
46
+ });
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
+
55
+ // Keep process alive
56
+ setInterval(() => {}, 60000);
57
+
58
+ // Clean exit
59
+ process.on("SIGINT", () => { ws.close(); process.exit(0); });
60
+ process.on("SIGTERM", () => { ws.close(); process.exit(0); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-friends",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
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",
@@ -17,11 +18,14 @@
17
18
  "statusline.js",
18
19
  "client.js",
19
20
  "commands/",
20
- "hooks/"
21
+ "hooks/",
22
+ "daemon.js"
21
23
  ],
22
24
  "dependencies": {
23
25
  "@modelcontextprotocol/sdk": "^1.12.1",
26
+ "ccstatusline": "^2.2.7",
24
27
  "partysocket": "^1.0.3",
28
+ "prompts": "^2.4.2",
25
29
  "zod": "^3.24.4"
26
30
  },
27
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.