claude-friends 0.4.2 → 0.4.3
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/package.json +1 -2
- package/statusline.js +145 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-friends",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "See who's online in Claude Code. Add friends, share status, nudge each other.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
26
|
-
"ccstatusline": "^2.2.7",
|
|
27
26
|
"partysocket": "^1.0.3",
|
|
28
27
|
"prompts": "^2.4.2",
|
|
29
28
|
"zod": "^3.24.4"
|
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
|
+
}
|