claude-friends 0.2.0 → 0.3.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/README.md +26 -34
- package/cli.js +150 -20
- package/commands/friend.md +1 -1
- package/commands/friends.md +1 -1
- package/commands/nudge.md +1 -1
- package/commands/status.md +1 -1
- package/commands/unfriend.md +1 -1
- package/hooks/update-tokens.js +79 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -11,55 +11,47 @@ See who's online in Claude Code. Add friends, share what you're working on, nudg
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install -g claude-friends
|
|
13
13
|
claude-friends setup
|
|
14
|
-
claude mcp add claude-friends -- claude-friends serve
|
|
15
14
|
```
|
|
16
15
|
|
|
17
|
-
That's it. No database, no API keys.
|
|
16
|
+
That's it. No database, no API keys, no MCP server to configure.
|
|
18
17
|
|
|
19
|
-
##
|
|
18
|
+
## Usage
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
### Slash commands (inside Claude Code)
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
| "check nudges" | See if anyone poked you |
|
|
31
|
-
|
|
32
|
-
## Status line
|
|
22
|
+
```
|
|
23
|
+
/friend alice Add a friend
|
|
24
|
+
/friends See who's online
|
|
25
|
+
/nudge bob hey! Nudge someone
|
|
26
|
+
/status debugging Set your status
|
|
27
|
+
/unfriend alice Remove a friend
|
|
28
|
+
```
|
|
33
29
|
|
|
34
|
-
|
|
30
|
+
### CLI (from any terminal)
|
|
35
31
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
32
|
+
```bash
|
|
33
|
+
claude-friends add alice
|
|
34
|
+
claude-friends online
|
|
35
|
+
claude-friends nudge bob "ship it!"
|
|
36
|
+
claude-friends status "pair programming"
|
|
37
|
+
claude-friends remove alice
|
|
43
38
|
```
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
## Features
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
```
|
|
42
|
+
- **Online presence** — see who's in Claude Code right now
|
|
43
|
+
- **Status messages** — share what you're working on
|
|
44
|
+
- **Nudges** — poke a friend with a message
|
|
45
|
+
- **Token usage** — automatically shared with friends (opt-in via hook)
|
|
46
|
+
- **Status line** — friend count shown in Claude Code's bottom bar
|
|
55
47
|
|
|
56
48
|
## How it works
|
|
57
49
|
|
|
58
50
|
- **PartyKit** handles real-time presence via WebSockets
|
|
59
|
-
- When you
|
|
60
|
-
-
|
|
61
|
-
- Friend lists and nudges are stored in-memory on the server
|
|
51
|
+
- When you use Claude Code → you appear online
|
|
52
|
+
- Friend lists and nudges are stored on the server
|
|
62
53
|
- No accounts, no passwords — just a username
|
|
54
|
+
- Setup auto-installs slash commands, status line, and token-sharing hook
|
|
63
55
|
|
|
64
56
|
## Self-hosting
|
|
65
57
|
|
package/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { writeFileSync, existsSync, mkdirSync, copyFileSync, readdirSync } from "fs";
|
|
3
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, copyFileSync, readdirSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { homedir } from "os";
|
|
6
6
|
import { createInterface } from "readline";
|
|
7
|
-
import { getConfig } from "./client.js";
|
|
7
|
+
import { getConfig, createConnection } from "./client.js";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import { dirname } from "path";
|
|
10
10
|
|
|
@@ -12,6 +12,37 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
12
12
|
const CONFIG_PATH = join(homedir(), ".claude-friends.json");
|
|
13
13
|
|
|
14
14
|
const command = process.argv[2];
|
|
15
|
+
const args = process.argv.slice(3).join(" ").trim();
|
|
16
|
+
|
|
17
|
+
// Helper: connect, send a message, wait for response, print, exit
|
|
18
|
+
function run(messageType, payload, responseType, formatter) {
|
|
19
|
+
const config = getConfig();
|
|
20
|
+
if (!config) {
|
|
21
|
+
console.log("Not set up. Run: claude-friends setup");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ws = createConnection(config.username);
|
|
26
|
+
|
|
27
|
+
ws.addEventListener("open", () => {
|
|
28
|
+
ws.send(JSON.stringify({ type: messageType, ...payload }));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
ws.addEventListener("message", (event) => {
|
|
32
|
+
const msg = JSON.parse(event.data);
|
|
33
|
+
if (msg.type === responseType || msg.type === "error") {
|
|
34
|
+
if (msg.type === "error") {
|
|
35
|
+
console.log("Error:", msg.message);
|
|
36
|
+
} else {
|
|
37
|
+
console.log(formatter(msg));
|
|
38
|
+
}
|
|
39
|
+
ws.close();
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
setTimeout(() => { console.log("Timeout connecting to server."); process.exit(1); }, 5000);
|
|
45
|
+
}
|
|
15
46
|
|
|
16
47
|
if (command === "setup") {
|
|
17
48
|
const existing = getConfig();
|
|
@@ -53,43 +84,142 @@ if (command === "setup") {
|
|
|
53
84
|
console.log(`\nInstalled slash commands: ${files.map((f) => "/" + f.replace(".md", "")).join(", ")}`);
|
|
54
85
|
}
|
|
55
86
|
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
// Install token-sharing hook to ~/.claude/settings.json
|
|
88
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
89
|
+
const hookCommand = `node ${join(__dirname, "hooks", "update-tokens.js")}`;
|
|
90
|
+
try {
|
|
91
|
+
let settings = {};
|
|
92
|
+
if (existsSync(settingsPath)) {
|
|
93
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
94
|
+
}
|
|
95
|
+
if (!settings.hooks) settings.hooks = {};
|
|
96
|
+
if (!settings.hooks.Stop) settings.hooks.Stop = [];
|
|
97
|
+
|
|
98
|
+
const alreadyInstalled = settings.hooks.Stop.some((h) =>
|
|
99
|
+
h.hooks?.some((hk) => hk.command?.includes("update-tokens"))
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (!alreadyInstalled) {
|
|
103
|
+
settings.hooks.Stop.push({
|
|
104
|
+
hooks: [{
|
|
105
|
+
type: "command",
|
|
106
|
+
command: hookCommand,
|
|
107
|
+
async: true,
|
|
108
|
+
}],
|
|
109
|
+
});
|
|
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
|
+
}
|
|
58
116
|
|
|
59
|
-
|
|
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
|
+
}
|
|
124
|
+
if (!settings.statusLine) {
|
|
125
|
+
settings.statusLine = {
|
|
126
|
+
type: "command",
|
|
127
|
+
command: `node ${join(__dirname, "statusline.js")}`,
|
|
128
|
+
};
|
|
129
|
+
writeFileSync(settingsPath2, JSON.stringify(settings, null, 2));
|
|
130
|
+
console.log("Installed status line.");
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
60
133
|
|
|
61
|
-
|
|
134
|
+
console.log(`
|
|
135
|
+
Done! You're "${username.trim()}".
|
|
62
136
|
|
|
63
|
-
|
|
137
|
+
In Claude Code:
|
|
64
138
|
/friend alice Add a friend
|
|
65
139
|
/friends See who's online
|
|
66
|
-
/nudge bob
|
|
140
|
+
/nudge bob hey! Nudge someone
|
|
67
141
|
/status debugging Set your status
|
|
68
142
|
/unfriend alice Remove a friend
|
|
143
|
+
|
|
144
|
+
Token usage is shared automatically.
|
|
69
145
|
`);
|
|
70
146
|
|
|
71
147
|
rl.close();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
148
|
+
|
|
149
|
+
} else if (command === "add") {
|
|
150
|
+
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!`);
|
|
152
|
+
|
|
153
|
+
} else if (command === "remove") {
|
|
154
|
+
if (!args) { console.log("Usage: claude-friends remove <username>"); process.exit(1); }
|
|
155
|
+
run("remove-friend", { friend: args }, "friend-removed", () => `Removed ${args}.`);
|
|
156
|
+
|
|
157
|
+
} else if (command === "online" || command === "list") {
|
|
158
|
+
run("get-friends", {}, "friends-list", (msg) => {
|
|
159
|
+
const friends = msg.friends || [];
|
|
160
|
+
if (friends.length === 0) return "No friends yet. Run: claude-friends add <username>";
|
|
161
|
+
|
|
162
|
+
const sorted = [...friends].sort((a, b) => (b.online ? 1 : 0) - (a.online ? 1 : 0));
|
|
163
|
+
const onlineCount = sorted.filter((f) => f.online).length;
|
|
164
|
+
|
|
165
|
+
const lines = sorted.map((f) => {
|
|
166
|
+
const dot = f.online ? "🟢" : "⚫";
|
|
167
|
+
const status = f.status && f.status !== "offline" && f.status !== "unknown" ? ` — ${f.status}` : "";
|
|
168
|
+
const tokens = f.tokensUsed ? ` [${(f.tokensUsed / 1000).toFixed(1)}K tokens]` : "";
|
|
169
|
+
return `${dot} ${f.name}${status}${tokens}`;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return `Friends (${onlineCount}/${friends.length} online):\n${lines.join("\n")}`;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
} else if (command === "status") {
|
|
176
|
+
if (!args) { console.log("Usage: claude-friends status <message>"); process.exit(1); }
|
|
177
|
+
const config = getConfig();
|
|
178
|
+
if (!config) { console.log("Not set up. Run: claude-friends setup"); process.exit(1); }
|
|
179
|
+
const ws = createConnection(config.username);
|
|
180
|
+
ws.addEventListener("open", () => {
|
|
181
|
+
ws.send(JSON.stringify({ type: "set-status", status: args }));
|
|
182
|
+
console.log(`Status set: "${args}"`);
|
|
183
|
+
setTimeout(() => { ws.close(); process.exit(0); }, 500);
|
|
184
|
+
});
|
|
185
|
+
setTimeout(() => process.exit(1), 5000);
|
|
186
|
+
|
|
187
|
+
} else if (command === "nudge") {
|
|
188
|
+
const parts = args.split(" ");
|
|
189
|
+
const friend = parts[0];
|
|
190
|
+
const message = parts.slice(1).join(" ") || undefined;
|
|
191
|
+
if (!friend) { console.log("Usage: claude-friends nudge <username> [message]"); process.exit(1); }
|
|
192
|
+
run("nudge", { friend, message }, "nudge-sent", () => `Nudge sent to ${friend}!`);
|
|
193
|
+
|
|
75
194
|
} else if (command === "whoami") {
|
|
76
195
|
const config = getConfig();
|
|
77
|
-
if (!config) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
196
|
+
if (!config) { console.log("Not set up. Run: claude-friends setup"); process.exit(1); }
|
|
197
|
+
console.log(config.username);
|
|
198
|
+
|
|
199
|
+
} else if (command === "serve") {
|
|
200
|
+
// Keep for backwards compat with anyone who set up MCP
|
|
201
|
+
await import("./mcp-server.js");
|
|
202
|
+
|
|
82
203
|
} else {
|
|
83
204
|
console.log(`
|
|
84
205
|
claude-friends — social presence for Claude Code
|
|
85
206
|
|
|
86
207
|
Commands:
|
|
87
|
-
setup
|
|
88
|
-
|
|
89
|
-
|
|
208
|
+
setup Pick a username (one-time)
|
|
209
|
+
add <username> Add a friend
|
|
210
|
+
remove <username> Remove a friend
|
|
211
|
+
online See who's online
|
|
212
|
+
status <message> Set your status
|
|
213
|
+
nudge <user> [msg] Nudge a friend
|
|
214
|
+
whoami Show your username
|
|
90
215
|
|
|
91
216
|
Quick start:
|
|
92
217
|
claude-friends setup
|
|
93
|
-
claude
|
|
218
|
+
claude-friends add alice
|
|
219
|
+
|
|
220
|
+
In Claude Code:
|
|
221
|
+
/friend alice
|
|
222
|
+
/friends
|
|
223
|
+
/nudge bob hey!
|
|
94
224
|
`);
|
|
95
225
|
}
|
package/commands/friend.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Run `claude-friends add $ARGUMENTS` and show the output. If no username is provided, ask for one first.
|
package/commands/friends.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Run `claude-friends online` and show the output.
|
package/commands/nudge.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Run `claude-friends nudge $ARGUMENTS` and show the output. If no username is provided, ask for one first.
|
package/commands/status.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Run `claude-friends status $ARGUMENTS` and show the output. If no status message is provided, ask for one first.
|
package/commands/unfriend.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Run `claude-friends remove $ARGUMENTS` and show the output. If no username is provided, ask for one first.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Hook script: reads token estimate from session, pushes to PartyKit
|
|
4
|
+
// Called by Claude Code's Stop hook after each response
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
|
|
10
|
+
const CONFIG_PATH = join(homedir(), ".claude-friends.json");
|
|
11
|
+
const TOKENS_PATH = join(homedir(), ".claude-friends-tokens.json");
|
|
12
|
+
const PARTY_HOST = "claude-friends-app.nandinitalwar.partykit.dev";
|
|
13
|
+
|
|
14
|
+
// Read config
|
|
15
|
+
let config;
|
|
16
|
+
try {
|
|
17
|
+
config = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
18
|
+
} catch {
|
|
19
|
+
process.exit(0); // silently exit if not set up
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Read hook input from stdin
|
|
23
|
+
let input = "";
|
|
24
|
+
try {
|
|
25
|
+
input = readFileSync("/dev/stdin", "utf-8");
|
|
26
|
+
} catch {}
|
|
27
|
+
|
|
28
|
+
// Parse the stop event to estimate tokens
|
|
29
|
+
let tokensThisCall = 0;
|
|
30
|
+
try {
|
|
31
|
+
const event = JSON.parse(input);
|
|
32
|
+
// Estimate based on content length — rough but functional
|
|
33
|
+
const content = JSON.stringify(event);
|
|
34
|
+
tokensThisCall = Math.ceil(content.length / 4); // ~4 chars per token
|
|
35
|
+
} catch {
|
|
36
|
+
tokensThisCall = 500; // default estimate per response
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Accumulate session tokens
|
|
40
|
+
let sessionTokens = 0;
|
|
41
|
+
try {
|
|
42
|
+
if (existsSync(TOKENS_PATH)) {
|
|
43
|
+
const data = JSON.parse(readFileSync(TOKENS_PATH, "utf-8"));
|
|
44
|
+
// Reset if older than 4 hours (new session)
|
|
45
|
+
if (Date.now() - data.lastUpdate < 4 * 60 * 60 * 1000) {
|
|
46
|
+
sessionTokens = data.tokens || 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
|
|
51
|
+
sessionTokens += tokensThisCall;
|
|
52
|
+
|
|
53
|
+
writeFileSync(TOKENS_PATH, JSON.stringify({
|
|
54
|
+
tokens: sessionTokens,
|
|
55
|
+
lastUpdate: Date.now(),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// Push to PartyKit via HTTP (faster than WebSocket for one-shot)
|
|
59
|
+
// Use the WebSocket approach since PartyKit is WS-only
|
|
60
|
+
try {
|
|
61
|
+
const { default: PartySocket } = await import("partysocket");
|
|
62
|
+
const ws = new PartySocket({
|
|
63
|
+
host: PARTY_HOST,
|
|
64
|
+
room: "lobby",
|
|
65
|
+
query: { username: config.username },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
ws.addEventListener("open", () => {
|
|
69
|
+
ws.send(JSON.stringify({ type: "share-tokens", tokens: sessionTokens }));
|
|
70
|
+
setTimeout(() => { ws.close(); process.exit(0); }, 500);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
ws.addEventListener("error", () => process.exit(0));
|
|
74
|
+
|
|
75
|
+
// Don't hang
|
|
76
|
+
setTimeout(() => process.exit(0), 3000);
|
|
77
|
+
} catch {
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-friends",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "See who's online in Claude Code. Add friends, share status, nudge each other.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"mcp-server.js",
|
|
17
17
|
"statusline.js",
|
|
18
18
|
"client.js",
|
|
19
|
-
"commands/"
|
|
19
|
+
"commands/",
|
|
20
|
+
"hooks/"
|
|
20
21
|
],
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"@modelcontextprotocol/sdk": "^1.12.1",
|