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.
Files changed (2) hide show
  1. package/package.json +1 -2
  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.2",
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
- // Lightweight status line for Claude Code
2
- // Connects, grabs friend count, prints one line, exits
3
- import { getConfig, queryFriends } from "./client.js";
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
- const config = getConfig();
6
- if (!config) {
7
- process.stdout.write("○ friends: run claude-friends setup");
8
- process.exit(0);
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 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);
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
+ }