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 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);