claude-friends 0.4.2 → 0.4.4
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 +6 -189
- package/package.json +2 -5
- package/statusline.js +145 -19
package/cli.js
CHANGED
|
@@ -6,7 +6,6 @@ import { homedir } from "os";
|
|
|
6
6
|
import { getConfig, createConnection } from "./client.js";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { dirname } from "path";
|
|
9
|
-
import prompts from "prompts";
|
|
10
9
|
|
|
11
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
11
|
const CONFIG_PATH = join(homedir(), ".claude-friends.json");
|
|
@@ -51,199 +50,17 @@ function run(messageType, payload, responseType, formatter) {
|
|
|
51
50
|
if (command === "setup") {
|
|
52
51
|
const existing = getConfig();
|
|
53
52
|
if (existing) {
|
|
54
|
-
console.log(
|
|
55
|
-
|
|
56
|
-
console.log(
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
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
|
-
}
|
|
98
|
-
|
|
99
|
-
// --- Step 1: Welcome ---
|
|
100
|
-
console.log(`
|
|
101
|
-
╔══════════════════════════════════════╗
|
|
102
|
-
║ Welcome to claude-friends! ║
|
|
103
|
-
╚══════════════════════════════════════╝
|
|
53
|
+
console.log(`Already set up as "${existing.username}".`);
|
|
54
|
+
} else {
|
|
55
|
+
console.log(`
|
|
56
|
+
To set up claude-friends, open Claude Code and type:
|
|
104
57
|
|
|
105
|
-
|
|
106
|
-
share status updates, and nudge each other.
|
|
58
|
+
/friends
|
|
107
59
|
|
|
108
|
-
|
|
109
|
-
online if you've BOTH added each other.
|
|
60
|
+
This will walk you through picking a username and adding friends.
|
|
110
61
|
`);
|
|
111
|
-
|
|
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); }
|
|
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;
|
|
131
|
-
}
|
|
132
|
-
|
|
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
|
-
}
|
|
177
|
-
|
|
178
|
-
// --- Step 4: Install hooks & commands silently ---
|
|
179
|
-
// Install slash commands
|
|
180
|
-
const commandsDir = join(homedir(), ".claude", "commands");
|
|
181
|
-
mkdirSync(commandsDir, { recursive: true });
|
|
182
|
-
|
|
183
|
-
const srcCommands = join(__dirname, "commands");
|
|
184
|
-
if (existsSync(srcCommands)) {
|
|
185
|
-
const files = readdirSync(srcCommands).filter((f) => f.endsWith(".md"));
|
|
186
|
-
for (const file of files) {
|
|
187
|
-
copyFileSync(join(srcCommands, file), join(commandsDir, file));
|
|
188
|
-
}
|
|
189
62
|
}
|
|
190
63
|
|
|
191
|
-
// Install hooks to settings.json
|
|
192
|
-
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
193
|
-
try {
|
|
194
|
-
let settings = {};
|
|
195
|
-
if (existsSync(settingsPath)) {
|
|
196
|
-
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
197
|
-
}
|
|
198
|
-
if (!settings.hooks) settings.hooks = {};
|
|
199
|
-
|
|
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) =>
|
|
204
|
-
h.hooks?.some((hk) => hk.command?.includes("update-tokens"))
|
|
205
|
-
);
|
|
206
|
-
if (!tokenHookInstalled) {
|
|
207
|
-
settings.hooks.Stop.push({
|
|
208
|
-
hooks: [{ type: "command", command: hookCommand, async: true }],
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Statusline
|
|
213
|
-
if (!settings.statusLine) {
|
|
214
|
-
settings.statusLine = {
|
|
215
|
-
type: "command",
|
|
216
|
-
command: `node ${join(__dirname, "statusline.js")}`,
|
|
217
|
-
};
|
|
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));
|
|
233
|
-
} catch {}
|
|
234
|
-
|
|
235
|
-
// --- Step 5: Done ---
|
|
236
|
-
console.log(`
|
|
237
|
-
You're all set! In Claude Code, try:
|
|
238
|
-
|
|
239
|
-
/friends See who's online
|
|
240
|
-
/friend <name> Add a friend
|
|
241
|
-
/nudge <name> Nudge someone
|
|
242
|
-
/status <message> Set your status
|
|
243
|
-
|
|
244
|
-
Your friends can add you with: claude-friends add ${username}
|
|
245
|
-
`);
|
|
246
|
-
|
|
247
64
|
} else if (command === "check-username") {
|
|
248
65
|
// Check if a username is available (for Claude Code slash commands)
|
|
249
66
|
if (!args) { console.log("Usage: claude-friends check-username <username>"); process.exit(1); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-friends",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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,8 +9,7 @@
|
|
|
9
9
|
"main": "mcp-server.js",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "npx partykit dev",
|
|
12
|
-
"deploy": "npx partykit deploy"
|
|
13
|
-
"postinstall": "claude-friends setup"
|
|
12
|
+
"deploy": "npx partykit deploy"
|
|
14
13
|
},
|
|
15
14
|
"files": [
|
|
16
15
|
"cli.js",
|
|
@@ -23,9 +22,7 @@
|
|
|
23
22
|
],
|
|
24
23
|
"dependencies": {
|
|
25
24
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
|
-
"ccstatusline": "^2.2.7",
|
|
27
25
|
"partysocket": "^1.0.3",
|
|
28
|
-
"prompts": "^2.4.2",
|
|
29
26
|
"zod": "^3.24.4"
|
|
30
27
|
},
|
|
31
28
|
"devDependencies": {
|
package/statusline.js
CHANGED
|
@@ -1,23 +1,149 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Full-featured status line for Claude Code
|
|
3
|
+
// Reads JSON from stdin, outputs formatted status line
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "fs";
|
|
6
|
+
import { join, basename } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { execSync } from "child_process";
|
|
9
|
+
|
|
10
|
+
// Read JSON from stdin
|
|
11
|
+
let input = "";
|
|
12
|
+
try {
|
|
13
|
+
input = readFileSync(0, "utf-8");
|
|
14
|
+
} catch {}
|
|
15
|
+
|
|
16
|
+
let data = {};
|
|
17
|
+
try {
|
|
18
|
+
data = JSON.parse(input);
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
21
|
+
const segments = [];
|
|
22
|
+
|
|
23
|
+
// 1. Project name
|
|
24
|
+
const projectDir = data.workspace?.project_dir || data.cwd || "";
|
|
25
|
+
if (projectDir) {
|
|
26
|
+
segments.push(basename(projectDir));
|
|
9
27
|
}
|
|
10
28
|
|
|
29
|
+
// 2. Git branch
|
|
11
30
|
try {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} catch {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
32
|
+
cwd: projectDir || undefined,
|
|
33
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
34
|
+
}).toString().trim();
|
|
35
|
+
if (branch) {
|
|
36
|
+
segments.push(`\u2387 ${branch}`);
|
|
37
|
+
}
|
|
38
|
+
} catch {}
|
|
39
|
+
|
|
40
|
+
// 3. Model
|
|
41
|
+
if (data.model) {
|
|
42
|
+
const modelName = typeof data.model === "object"
|
|
43
|
+
? (data.model.display_name || data.model.id || "")
|
|
44
|
+
: data.model;
|
|
45
|
+
const short = modelName
|
|
46
|
+
.replace(/^claude-/, "")
|
|
47
|
+
.replace("opus-4-6", "Opus 4.6")
|
|
48
|
+
.replace("sonnet-4-6", "Sonnet 4.6")
|
|
49
|
+
.replace("haiku-4-5-20251001", "Haiku 4.5");
|
|
50
|
+
segments.push(`\u{1F916} ${short}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 4. Tokens
|
|
54
|
+
const totalIn = data.context_window?.total_input_tokens;
|
|
55
|
+
const totalOut = data.context_window?.total_output_tokens;
|
|
56
|
+
if (totalIn != null || totalOut != null) {
|
|
57
|
+
const total = (totalIn || 0) + (totalOut || 0);
|
|
58
|
+
segments.push(`${formatNum(total)} tokens`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 5. Tool calls from transcript
|
|
62
|
+
if (data.transcript_path) {
|
|
63
|
+
try {
|
|
64
|
+
const transcript = readFileSync(data.transcript_path, "utf-8");
|
|
65
|
+
const toolCalls = (transcript.match(/"type"\s*:\s*"tool_use"/g) || []).length;
|
|
66
|
+
if (toolCalls > 0) {
|
|
67
|
+
segments.push(`\u{1F527} ${toolCalls}`);
|
|
68
|
+
}
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 6. Cost
|
|
73
|
+
if (data.cost?.total_cost_usd != null) {
|
|
74
|
+
segments.push(`$${data.cost.total_cost_usd.toFixed(2)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 7. Streak
|
|
78
|
+
segments.push(`\u{1F525} ${getStreak()}d`);
|
|
79
|
+
|
|
80
|
+
// 8. Friends online
|
|
81
|
+
const friends = getFriendsOnline();
|
|
82
|
+
const dot = friends.count > 0 ? "\u{1F7E2}" : "\u25CB";
|
|
83
|
+
const names = friends.names.length > 0
|
|
84
|
+
? ` (${friends.names.slice(0, 3).join(", ")}${friends.names.length > 3 ? "\u2026" : ""})`
|
|
85
|
+
: "";
|
|
86
|
+
segments.push(`${dot} ${friends.count} online${names}`);
|
|
87
|
+
|
|
88
|
+
process.stdout.write(segments.join(" | "));
|
|
89
|
+
|
|
90
|
+
// --- Helpers ---
|
|
91
|
+
|
|
92
|
+
function formatNum(n) {
|
|
93
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
94
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
95
|
+
return `${n}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getFriendsOnline() {
|
|
99
|
+
try {
|
|
100
|
+
const cache = JSON.parse(readFileSync(join(homedir(), ".claude-friends-online.json"), "utf-8"));
|
|
101
|
+
if (Date.now() - cache.timestamp > 30000) return { count: 0, names: [] };
|
|
102
|
+
return { count: cache.onlineCount || 0, names: cache.onlineNames || [] };
|
|
103
|
+
} catch {
|
|
104
|
+
return { count: 0, names: [] };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getStreak() {
|
|
109
|
+
// Collect dates of all session file modifications
|
|
110
|
+
const sessionsDir = join(homedir(), ".claude", "projects");
|
|
111
|
+
try {
|
|
112
|
+
if (!existsSync(sessionsDir)) return 0;
|
|
113
|
+
const activeDates = new Set();
|
|
114
|
+
scanForDates(sessionsDir, activeDates, 0);
|
|
115
|
+
|
|
116
|
+
const today = new Date();
|
|
117
|
+
let streak = 0;
|
|
118
|
+
for (let i = 0; i < 365; i++) {
|
|
119
|
+
const d = new Date(today);
|
|
120
|
+
d.setDate(d.getDate() - i);
|
|
121
|
+
const dateStr = d.toISOString().slice(0, 10);
|
|
122
|
+
if (activeDates.has(dateStr)) {
|
|
123
|
+
streak++;
|
|
124
|
+
} else if (i > 0) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return streak;
|
|
129
|
+
} catch {
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function scanForDates(dir, dates, depth) {
|
|
135
|
+
if (depth > 4) return;
|
|
136
|
+
try {
|
|
137
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
138
|
+
const full = join(dir, entry.name);
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
scanForDates(full, dates, depth + 1);
|
|
141
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
142
|
+
try {
|
|
143
|
+
const mtime = statSync(full).mtime;
|
|
144
|
+
dates.add(mtime.toISOString().slice(0, 10));
|
|
145
|
+
} catch {}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch {}
|
|
149
|
+
}
|