claude-friends 0.1.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/LICENSE +21 -0
- package/README.md +76 -0
- package/cli.js +81 -0
- package/client.js +58 -0
- package/mcp-server.js +216 -0
- package/package.json +34 -0
- package/statusline.js +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nandini Talwar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# claude-friends
|
|
2
|
+
|
|
3
|
+
See who's online in Claude Code. Add friends, share what you're working on, nudge each other.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
● 2 online (alice, bob)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g claude-friends
|
|
13
|
+
claude-friends setup
|
|
14
|
+
claude mcp add claude-friends -- claude-friends serve
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That's it. No database, no API keys.
|
|
18
|
+
|
|
19
|
+
## What you get
|
|
20
|
+
|
|
21
|
+
A status line in Claude Code showing online friends, plus these tools:
|
|
22
|
+
|
|
23
|
+
| Command | What it does |
|
|
24
|
+
|---|---|
|
|
25
|
+
| "who's online?" | See friends with 🟢/⚫ indicators |
|
|
26
|
+
| "add friend alice" | Add someone by username |
|
|
27
|
+
| "set my status to debugging auth" | Share what you're working on |
|
|
28
|
+
| "nudge bob" | Poke a friend with a message |
|
|
29
|
+
| "share my token usage: 45000" | Let friends see your token count |
|
|
30
|
+
| "check nudges" | See if anyone poked you |
|
|
31
|
+
|
|
32
|
+
## Status line
|
|
33
|
+
|
|
34
|
+
Add to your Claude Code settings (`~/.claude/settings.json`):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"statusLine": {
|
|
39
|
+
"type": "command",
|
|
40
|
+
"command": "node /path/to/claude-friends/statusline.js"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or after global install:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"statusLine": {
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "claude-friends statusline"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## How it works
|
|
57
|
+
|
|
58
|
+
- **PartyKit** handles real-time presence via WebSockets
|
|
59
|
+
- When you open Claude Code → you go online
|
|
60
|
+
- When you close it → PartyKit detects the disconnect → you go offline
|
|
61
|
+
- Friend lists and nudges are stored in-memory on the server
|
|
62
|
+
- No accounts, no passwords — just a username
|
|
63
|
+
|
|
64
|
+
## Self-hosting
|
|
65
|
+
|
|
66
|
+
Want to run your own server? Fork this repo, then:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx partykit deploy
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Update the `PARTY_HOST` in `client.js` to point to your deployment.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/cli.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { writeFileSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { createInterface } from "readline";
|
|
7
|
+
import { getConfig } from "./client.js";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { dirname } from "path";
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const CONFIG_PATH = join(homedir(), ".claude-friends.json");
|
|
13
|
+
|
|
14
|
+
const command = process.argv[2];
|
|
15
|
+
|
|
16
|
+
if (command === "setup") {
|
|
17
|
+
const existing = getConfig();
|
|
18
|
+
if (existing) {
|
|
19
|
+
console.log(`\nAlready set up as "${existing.username}".`);
|
|
20
|
+
console.log(`Config: ${CONFIG_PATH}`);
|
|
21
|
+
console.log(`\nTo change username, delete ${CONFIG_PATH} and run again.\n`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
26
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
27
|
+
|
|
28
|
+
console.log(`
|
|
29
|
+
╔══════════════════════════════════════╗
|
|
30
|
+
║ claude-friends setup ║
|
|
31
|
+
╚══════════════════════════════════════╝
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
const username = await ask("Choose a username: ");
|
|
35
|
+
|
|
36
|
+
if (!username.trim()) {
|
|
37
|
+
console.log("Username can't be empty.");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
writeFileSync(CONFIG_PATH, JSON.stringify({ username: username.trim() }, null, 2));
|
|
42
|
+
|
|
43
|
+
console.log(`
|
|
44
|
+
Done! You're "${username.trim()}".
|
|
45
|
+
|
|
46
|
+
Now add the MCP server to Claude Code:
|
|
47
|
+
|
|
48
|
+
claude mcp add claude-friends -- node ${join(__dirname, "mcp-server.js")}
|
|
49
|
+
|
|
50
|
+
Then in Claude Code, try:
|
|
51
|
+
"who's online?"
|
|
52
|
+
"add friend alice"
|
|
53
|
+
"set my status to debugging auth"
|
|
54
|
+
"nudge bob"
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
rl.close();
|
|
58
|
+
} else if (command === "serve") {
|
|
59
|
+
// Start the MCP server directly
|
|
60
|
+
await import("./mcp-server.js");
|
|
61
|
+
} else if (command === "whoami") {
|
|
62
|
+
const config = getConfig();
|
|
63
|
+
if (!config) {
|
|
64
|
+
console.log("Not set up yet. Run: claude-friends setup");
|
|
65
|
+
} else {
|
|
66
|
+
console.log(config.username);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
console.log(`
|
|
70
|
+
claude-friends — social presence for Claude Code
|
|
71
|
+
|
|
72
|
+
Commands:
|
|
73
|
+
setup Pick a username (one-time)
|
|
74
|
+
serve Start the MCP server (used by Claude Code)
|
|
75
|
+
whoami Show your username
|
|
76
|
+
|
|
77
|
+
Quick start:
|
|
78
|
+
claude-friends setup
|
|
79
|
+
claude mcp add claude-friends -- claude-friends serve
|
|
80
|
+
`);
|
|
81
|
+
}
|
package/client.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Shared client for connecting to the PartyKit server
|
|
2
|
+
import PartySocket from "partysocket";
|
|
3
|
+
import { readFileSync, existsSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
|
|
8
|
+
const CONFIG_PATH = join(homedir(), ".claude-friends.json");
|
|
9
|
+
|
|
10
|
+
// TODO: replace with your deployed PartyKit URL after `npx partykit deploy`
|
|
11
|
+
const PARTY_HOST = "claude-friends-app.nandinitalwar.partykit.dev";
|
|
12
|
+
|
|
13
|
+
export function getConfig() {
|
|
14
|
+
if (!existsSync(CONFIG_PATH)) return null;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createConnection(username) {
|
|
23
|
+
const ws = new PartySocket({
|
|
24
|
+
host: PARTY_HOST,
|
|
25
|
+
room: "lobby",
|
|
26
|
+
query: { username },
|
|
27
|
+
});
|
|
28
|
+
return ws;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// One-shot: connect, request data, disconnect
|
|
32
|
+
export async function queryFriends(username, timeout = 5000) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const ws = createConnection(username);
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
ws.close();
|
|
37
|
+
reject(new Error("timeout"));
|
|
38
|
+
}, timeout);
|
|
39
|
+
|
|
40
|
+
ws.addEventListener("open", () => {
|
|
41
|
+
ws.send(JSON.stringify({ type: "get-friends" }));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
ws.addEventListener("message", (event) => {
|
|
45
|
+
const msg = JSON.parse(event.data);
|
|
46
|
+
if (msg.type === "friends-list") {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
ws.close();
|
|
49
|
+
resolve(msg.friends);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
ws.addEventListener("error", (err) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
reject(err);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
package/mcp-server.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getConfig, createConnection } from "./client.js";
|
|
5
|
+
|
|
6
|
+
const config = getConfig();
|
|
7
|
+
if (!config) {
|
|
8
|
+
console.error("Not set up. Run: claude-friends setup");
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const username = config.username;
|
|
13
|
+
|
|
14
|
+
// Persistent WebSocket connection — stays open while Claude Code is running
|
|
15
|
+
const ws = createConnection(username);
|
|
16
|
+
|
|
17
|
+
// Local cache of state, updated via WebSocket messages
|
|
18
|
+
let friendsList = [];
|
|
19
|
+
let pendingNudges = [];
|
|
20
|
+
let lastError = null;
|
|
21
|
+
|
|
22
|
+
// Promise-based request/response helper
|
|
23
|
+
function request(msg, responseType, timeout = 5000) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const timer = setTimeout(() => reject(new Error("timeout")), timeout);
|
|
26
|
+
const handler = (event) => {
|
|
27
|
+
const data = JSON.parse(event.data);
|
|
28
|
+
if (data.type === responseType) {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
ws.removeEventListener("message", handler);
|
|
31
|
+
resolve(data);
|
|
32
|
+
} else if (data.type === "error") {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
ws.removeEventListener("message", handler);
|
|
35
|
+
resolve(data);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
ws.addEventListener("message", handler);
|
|
39
|
+
ws.send(JSON.stringify(msg));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Listen for incoming messages
|
|
44
|
+
ws.addEventListener("message", (event) => {
|
|
45
|
+
const msg = JSON.parse(event.data);
|
|
46
|
+
|
|
47
|
+
switch (msg.type) {
|
|
48
|
+
case "state":
|
|
49
|
+
friendsList = msg.friends || [];
|
|
50
|
+
pendingNudges = msg.nudges || [];
|
|
51
|
+
break;
|
|
52
|
+
case "nudge":
|
|
53
|
+
pendingNudges.push({ from: msg.from, message: msg.message });
|
|
54
|
+
break;
|
|
55
|
+
case "friend-added":
|
|
56
|
+
case "friend-removed":
|
|
57
|
+
// Refresh friends list
|
|
58
|
+
ws.send(JSON.stringify({ type: "get-friends" }));
|
|
59
|
+
break;
|
|
60
|
+
case "friends-list":
|
|
61
|
+
friendsList = msg.friends || [];
|
|
62
|
+
break;
|
|
63
|
+
case "error":
|
|
64
|
+
lastError = msg.message;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Wait for connection before starting MCP
|
|
70
|
+
await new Promise((resolve) => {
|
|
71
|
+
if (ws.readyState === 1) return resolve();
|
|
72
|
+
ws.addEventListener("open", resolve, { once: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// --- MCP Server ---
|
|
76
|
+
|
|
77
|
+
const server = new McpServer({
|
|
78
|
+
name: "claude-friends",
|
|
79
|
+
version: "0.1.0",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
server.tool(
|
|
83
|
+
"friends-online",
|
|
84
|
+
"See which of your friends are currently online in Claude Code.",
|
|
85
|
+
{},
|
|
86
|
+
async () => {
|
|
87
|
+
const resp = await request({ type: "get-friends" }, "friends-list");
|
|
88
|
+
const friends = resp.friends || [];
|
|
89
|
+
|
|
90
|
+
if (friends.length === 0) {
|
|
91
|
+
return { content: [{ type: "text", text: "No friends yet. Use add-friend to add someone!" }] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const sorted = [...friends].sort((a, b) => (b.online ? 1 : 0) - (a.online ? 1 : 0));
|
|
95
|
+
const onlineCount = sorted.filter((f) => f.online).length;
|
|
96
|
+
|
|
97
|
+
const lines = sorted.map((f) => {
|
|
98
|
+
const dot = f.online ? "🟢" : "⚫";
|
|
99
|
+
const status = f.status && f.status !== "offline" && f.status !== "unknown" ? ` — ${f.status}` : "";
|
|
100
|
+
const tokens = f.tokensUsed ? ` [${(f.tokensUsed / 1000).toFixed(1)}K tokens]` : "";
|
|
101
|
+
return `${dot} ${f.name}${status}${tokens}`;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: `Friends (${onlineCount}/${friends.length} online):\n${lines.join("\n")}`,
|
|
108
|
+
}],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
server.tool(
|
|
114
|
+
"add-friend",
|
|
115
|
+
"Add a friend by their username.",
|
|
116
|
+
{ username: z.string().describe("The friend's username") },
|
|
117
|
+
async ({ username: friend }) => {
|
|
118
|
+
const resp = await request({ type: "add-friend", friend }, "friend-added");
|
|
119
|
+
if (resp.type === "error") {
|
|
120
|
+
return { content: [{ type: "text", text: resp.message }] };
|
|
121
|
+
}
|
|
122
|
+
return { content: [{ type: "text", text: `Added ${friend} as a friend!` }] };
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
"remove-friend",
|
|
128
|
+
"Remove a friend.",
|
|
129
|
+
{ username: z.string().describe("The friend's username") },
|
|
130
|
+
async ({ username: friend }) => {
|
|
131
|
+
const resp = await request({ type: "remove-friend", friend }, "friend-removed");
|
|
132
|
+
return { content: [{ type: "text", text: `Removed ${friend}.` }] };
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
server.tool(
|
|
137
|
+
"set-status",
|
|
138
|
+
"Set your status so friends can see what you're working on.",
|
|
139
|
+
{ status: z.string().describe("Your status, e.g. 'debugging auth flow'") },
|
|
140
|
+
async ({ status }) => {
|
|
141
|
+
ws.send(JSON.stringify({ type: "set-status", status }));
|
|
142
|
+
return { content: [{ type: "text", text: `Status set: "${status}"` }] };
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
server.tool(
|
|
147
|
+
"share-tokens",
|
|
148
|
+
"Share your token usage with friends.",
|
|
149
|
+
{ tokens: z.number().describe("Tokens used this session") },
|
|
150
|
+
async ({ tokens }) => {
|
|
151
|
+
ws.send(JSON.stringify({ type: "share-tokens", tokens }));
|
|
152
|
+
return { content: [{ type: "text", text: `Sharing: ${tokens.toLocaleString()} tokens` }] };
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
server.tool(
|
|
157
|
+
"hide-tokens",
|
|
158
|
+
"Stop sharing token usage.",
|
|
159
|
+
{},
|
|
160
|
+
async () => {
|
|
161
|
+
ws.send(JSON.stringify({ type: "hide-tokens" }));
|
|
162
|
+
return { content: [{ type: "text", text: "Token usage hidden." }] };
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
server.tool(
|
|
167
|
+
"nudge",
|
|
168
|
+
"Send a nudge/message to a friend.",
|
|
169
|
+
{
|
|
170
|
+
username: z.string().describe("Friend to nudge"),
|
|
171
|
+
message: z.string().optional().describe("Optional message"),
|
|
172
|
+
},
|
|
173
|
+
async ({ username: friend, message }) => {
|
|
174
|
+
const resp = await request(
|
|
175
|
+
{ type: "nudge", friend, message },
|
|
176
|
+
"nudge-sent"
|
|
177
|
+
);
|
|
178
|
+
if (resp.type === "error") {
|
|
179
|
+
return { content: [{ type: "text", text: resp.message }] };
|
|
180
|
+
}
|
|
181
|
+
return { content: [{ type: "text", text: `Nudge sent to ${friend}!` }] };
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
server.tool(
|
|
186
|
+
"check-nudges",
|
|
187
|
+
"Check if anyone has nudged you.",
|
|
188
|
+
{},
|
|
189
|
+
async () => {
|
|
190
|
+
const resp = await request({ type: "get-nudges" }, "nudges-list");
|
|
191
|
+
const nudges = resp.nudges || [];
|
|
192
|
+
if (nudges.length === 0) {
|
|
193
|
+
return { content: [{ type: "text", text: "No new nudges." }] };
|
|
194
|
+
}
|
|
195
|
+
const lines = nudges.map((n) => `💬 ${n.from}: ${n.message}`);
|
|
196
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
server.tool(
|
|
201
|
+
"my-profile",
|
|
202
|
+
"Show your username and status.",
|
|
203
|
+
{},
|
|
204
|
+
async () => {
|
|
205
|
+
return {
|
|
206
|
+
content: [{ type: "text", text: `🟢 ${username} (that's you)` }],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const transport = new StdioServerTransport();
|
|
212
|
+
await server.connect(transport);
|
|
213
|
+
|
|
214
|
+
// Cleanup
|
|
215
|
+
process.on("SIGINT", () => { ws.close(); process.exit(0); });
|
|
216
|
+
process.on("SIGTERM", () => { ws.close(); process.exit(0); });
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-friends",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "See who's online in Claude Code. Add friends, share status, nudge each other.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-friends": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "mcp-server.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "npx partykit dev",
|
|
12
|
+
"deploy": "npx partykit deploy"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"cli.js",
|
|
16
|
+
"mcp-server.js",
|
|
17
|
+
"statusline.js",
|
|
18
|
+
"client.js"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
22
|
+
"partysocket": "^1.0.3",
|
|
23
|
+
"zod": "^3.24.4"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"partykit": "^0.0.111"
|
|
27
|
+
},
|
|
28
|
+
"keywords": ["claude", "claude-code", "mcp", "social", "presence", "friends"],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/nandinitalwar/claude-friends"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/statusline.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Lightweight status line for Claude Code
|
|
2
|
+
// Connects, grabs friend count, prints one line, exits
|
|
3
|
+
import { getConfig, queryFriends } from "./client.js";
|
|
4
|
+
|
|
5
|
+
const config = getConfig();
|
|
6
|
+
if (!config) {
|
|
7
|
+
process.stdout.write("○ friends: run claude-friends setup");
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
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);
|