claude-rpc 0.22.1 → 0.24.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 +4 -2
- package/package.json +1 -1
- package/src/cli.js +17 -9
- package/src/insights.js +215 -44
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -146,13 +146,15 @@ claude-rpc readme # prints paste-ready README badge markdown
|
|
|
146
146
|
claude-rpc readme --raw | pbcopy # straight to your clipboard
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
**Live badges, paste once.** With a public profile (`claude-rpc profile set --handle <you> && claude-rpc profile on`), your stats are served as always-current badges from the community worker — no `gh`, no gist, nothing to re-run:
|
|
149
|
+
**Live card + badges, paste once.** With a public profile (`claude-rpc profile set --handle <you> && claude-rpc profile on`), your stats are served as an always-current card (and badges) from the community worker — no `gh`, no gist, nothing to re-run:
|
|
150
150
|
|
|
151
151
|
```md
|
|
152
|
+
[](https://claude-rpc.vercel.app/u/<you>)
|
|
153
|
+
|
|
152
154
|
[](https://claude-rpc.vercel.app/?ref=badge)
|
|
153
155
|
```
|
|
154
156
|
|
|
155
|
-
`metric=` is one of `tokens · sessions · hours · streak` (optional `&label=` to retitle). Your profile page at `/u/<you>` has a one-click "copy" for the
|
|
157
|
+
The card shows tokens, sessions, active hours and streak; badge `metric=` is one of `tokens · sessions · hours · streak` (optional `&label=` to retitle). Both refresh themselves as the daemon flushes your profile (~every 30 min). Your profile page at `/u/<you>` has a one-click "copy" for the whole block.
|
|
156
158
|
|
|
157
159
|
Prefer to render locally? `badge`/`card`/`calendar`/`github-stat` all write SVG, and `--gist` self-hosts a live one:
|
|
158
160
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1075,35 +1075,43 @@ function doReadme(argv) {
|
|
|
1075
1075
|
['streak', 'Claude Code streak'],
|
|
1076
1076
|
['tokens', 'Claude Code tokens'],
|
|
1077
1077
|
];
|
|
1078
|
-
|
|
1078
|
+
// The hero: a live stat card that auto-refreshes from your published profile.
|
|
1079
|
+
const cardMd = (h) => `[](${site}/u/${h})`;
|
|
1080
|
+
const badgesMd = (h) => metrics
|
|
1079
1081
|
.map(([m, alt]) => `[](${site}/?ref=badge)`)
|
|
1080
1082
|
.join('\n');
|
|
1083
|
+
const liveMd = (h) => `${cardMd(h)}\n\n${badgesMd(h)}`;
|
|
1081
1084
|
|
|
1082
1085
|
// Raw mode: just the markdown, for `claude-rpc readme --raw | pbcopy`.
|
|
1083
1086
|
if (raw) {
|
|
1084
1087
|
process.stdout.write(live
|
|
1085
1088
|
? liveMd(handle) + '\n'
|
|
1086
|
-
: '<!-- live badges need a public profile: claude-rpc profile set --handle <name> && claude-rpc profile on -->\n');
|
|
1089
|
+
: '<!-- live card/badges need a public profile: claude-rpc profile set --handle <name> && claude-rpc profile on -->\n');
|
|
1087
1090
|
return;
|
|
1088
1091
|
}
|
|
1089
1092
|
|
|
1090
1093
|
console.log('');
|
|
1091
|
-
console.log(` ${c.bold}${c.magenta}◆ README badges${c.reset}`);
|
|
1094
|
+
console.log(` ${c.bold}${c.magenta}◆ README card + badges${c.reset}`);
|
|
1092
1095
|
console.log('');
|
|
1093
1096
|
|
|
1094
1097
|
if (live) {
|
|
1095
|
-
console.log(` ${c.dim}Live
|
|
1098
|
+
console.log(` ${c.dim}Live stat card for ${c.reset}${c.cyan}@${handle}${c.reset}${c.dim} — paste once; it auto-updates as you work:${c.reset}`);
|
|
1096
1099
|
console.log('');
|
|
1097
|
-
|
|
1100
|
+
console.log(` ${cardMd(handle)}`);
|
|
1098
1101
|
console.log('');
|
|
1099
|
-
console.log(` ${c.dim}
|
|
1102
|
+
console.log(` ${c.dim}…or compact badges:${c.reset}`);
|
|
1103
|
+
console.log('');
|
|
1104
|
+
for (const line of badgesMd(handle).split('\n')) console.log(` ${line}`);
|
|
1105
|
+
console.log('');
|
|
1106
|
+
console.log(` ${c.dim}Badge metrics:${c.reset} ${c.cyan}tokens · sessions · hours · streak${c.reset}`);
|
|
1100
1107
|
console.log(` ${c.dim}One-click copy + shareable page:${c.reset} ${c.cyan}${site}/u/${handle}${c.reset}`);
|
|
1101
1108
|
console.log(` ${c.dim}Straight to clipboard:${c.reset} ${c.cyan}claude-rpc readme --raw${c.reset}${c.dim} | pbcopy${c.reset}`);
|
|
1102
1109
|
} else {
|
|
1103
|
-
console.log(` ${c.dim}A live, always-current
|
|
1110
|
+
console.log(` ${c.dim}A live, always-current card + badges need a public profile. Turn one on:${c.reset}`);
|
|
1104
1111
|
console.log(` ${c.cyan}claude-rpc profile set --handle <name> && claude-rpc profile on${c.reset}`);
|
|
1105
1112
|
console.log('');
|
|
1106
|
-
console.log(` ${c.dim}Then your badges live here (self-refreshing — paste once):${c.reset}`);
|
|
1113
|
+
console.log(` ${c.dim}Then your card + badges live here (self-refreshing — paste once):${c.reset}`);
|
|
1114
|
+
console.log(` ${c.cyan}${endpoint}/card/<handle>.svg${c.reset}`);
|
|
1107
1115
|
console.log(` ${c.cyan}${endpoint}/badge/<handle>.svg?metric=hours${c.reset}`);
|
|
1108
1116
|
}
|
|
1109
1117
|
|
|
@@ -2003,7 +2011,7 @@ function help() {
|
|
|
2003
2011
|
['statusline', 'One-line status for tmux/shell prompts (--template)'],
|
|
2004
2012
|
['calendar', 'Year activity heatmap SVG (--out --gist)'],
|
|
2005
2013
|
['session-card', 'Recap card for the current session (--out)'],
|
|
2006
|
-
['readme', 'Paste-ready README badges for your profile (--raw to pipe)'],
|
|
2014
|
+
['readme', 'Paste-ready README card + badges for your profile (--raw to pipe)'],
|
|
2007
2015
|
['mcp install', 'Wire the stats MCP server into Claude Code (one command)'],
|
|
2008
2016
|
['mcp uninstall', 'Remove the stats MCP server from Claude Code'],
|
|
2009
2017
|
['mcp', 'Run the MCP server (stdio) — exposes your stats to Claude'],
|
package/src/insights.js
CHANGED
|
@@ -1,18 +1,64 @@
|
|
|
1
|
-
// Generate
|
|
1
|
+
// Generate short, contextual insight lines from an aggregate.json snapshot.
|
|
2
2
|
// Used by `claude-rpc insights`, the web `/api/insights` route, and the TUI.
|
|
3
3
|
// Pure functions — no I/O, no globals beyond Date.
|
|
4
|
+
//
|
|
5
|
+
// Each generator contributes 0+ candidate lines with a priority weight. The
|
|
6
|
+
// final pick is weight + a seeded jitter, so the strongest signals lead but the
|
|
7
|
+
// mid-tier ROTATES — a fresh mix each run instead of the same five forever.
|
|
8
|
+
// Pass opts.seed for a deterministic order (tests); it defaults to the clock.
|
|
4
9
|
|
|
5
|
-
import { dayKey
|
|
10
|
+
import { dayKey } from './scanner.js';
|
|
6
11
|
import { fmtCost } from './pricing.js';
|
|
7
12
|
import { fmtNum, fmtHours } from './fmt.js';
|
|
8
13
|
|
|
9
14
|
const WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
15
|
+
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
10
16
|
|
|
11
17
|
function pct(n) {
|
|
12
18
|
const sign = n >= 0 ? '+' : '−';
|
|
13
19
|
return `${sign}${Math.round(Math.abs(n) * 100)}%`;
|
|
14
20
|
}
|
|
15
21
|
|
|
22
|
+
// Plain thousands separator — fmtNum compacts to "1.7k", which reads oddly for
|
|
23
|
+
// page/file counts where the exact number is the point.
|
|
24
|
+
function commas(n) {
|
|
25
|
+
return String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hourLabel(h) {
|
|
29
|
+
const ampm = h < 12 ? 'am' : 'pm';
|
|
30
|
+
const hr = h % 12 === 0 ? 12 : h % 12;
|
|
31
|
+
return `${hr}${ampm}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function monthDay(s) {
|
|
35
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(s || ''));
|
|
36
|
+
return m ? `${MON[+m[2] - 1]} ${+m[3]}` : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function daysAgo(s) {
|
|
40
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(s || ''));
|
|
41
|
+
if (!m) return null;
|
|
42
|
+
const d = new Date(+m[1], +m[2] - 1, +m[3]); d.setHours(0, 0, 0, 0);
|
|
43
|
+
const t = new Date(); t.setHours(0, 0, 0, 0);
|
|
44
|
+
return Math.round((t - d) / 86_400_000);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// "opus-4-8" → "Opus 4.8", "fable-5" → "Fable 5".
|
|
48
|
+
function prettyModel(m) {
|
|
49
|
+
if (!m) return '';
|
|
50
|
+
const p = String(m).split('-');
|
|
51
|
+
const fam = p[0].charAt(0).toUpperCase() + p[0].slice(1);
|
|
52
|
+
const ver = p.slice(1).filter((x) => /^\d+$/.test(x)).join('.');
|
|
53
|
+
return ver ? `${fam} ${ver}` : fam;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Tiny seeded LCG so the rotation is deterministic given a seed.
|
|
57
|
+
function rng(seed) {
|
|
58
|
+
let s = (Math.floor(seed) >>> 0) || 1;
|
|
59
|
+
return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; };
|
|
60
|
+
}
|
|
61
|
+
|
|
16
62
|
// Sum activeMs across the last `days` days (inclusive of today).
|
|
17
63
|
function windowActive(byDay, days, offset = 0) {
|
|
18
64
|
let total = 0;
|
|
@@ -26,13 +72,16 @@ function windowActive(byDay, days, offset = 0) {
|
|
|
26
72
|
|
|
27
73
|
export function generateInsights(aggregate, opts = {}) {
|
|
28
74
|
const a = aggregate || {};
|
|
29
|
-
const out = [];
|
|
30
|
-
|
|
31
75
|
if (!a.byDay || !Object.keys(a.byDay).length) {
|
|
32
76
|
return ['Not enough data yet — keep coding and check back tomorrow.'];
|
|
33
77
|
}
|
|
34
78
|
|
|
35
|
-
|
|
79
|
+
const C = [];
|
|
80
|
+
// topic (optional) groups near-duplicates so at most one survives selection
|
|
81
|
+
// (e.g. two streak lines, or peak-weekday + weekday-rhythm, never together).
|
|
82
|
+
const push = (w, text, topic) => { if (text) C.push({ w, text, topic }); };
|
|
83
|
+
|
|
84
|
+
// ── 1. Week-over-week trend (headline) ─────────────────────────────────
|
|
36
85
|
const last7 = windowActive(a.byDay, 7, 0);
|
|
37
86
|
const prev7 = windowActive(a.byDay, 7, 7);
|
|
38
87
|
if (last7 || prev7) {
|
|
@@ -40,85 +89,207 @@ export function generateInsights(aggregate, opts = {}) {
|
|
|
40
89
|
const delta = (last7 - prev7) / prev7;
|
|
41
90
|
if (Math.abs(delta) >= 0.10) {
|
|
42
91
|
const dir = delta > 0 ? 'above' : 'below';
|
|
43
|
-
|
|
92
|
+
push(Math.abs(delta) >= 0.25 ? 95 : 78,
|
|
93
|
+
`You're ${pct(delta)} ${dir} last week — ${fmtHours(last7)} vs ${fmtHours(prev7)}.`);
|
|
44
94
|
} else {
|
|
45
|
-
|
|
95
|
+
push(64, `Steady week — ${fmtHours(last7)} active, on par with the previous 7 days.`);
|
|
46
96
|
}
|
|
47
97
|
} else if (last7 > 0) {
|
|
48
|
-
|
|
98
|
+
push(80, `Fresh momentum — ${fmtHours(last7)} active this past week.`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── 2. Chronotype (peak hour / night owl / early bird) ─────────────────
|
|
103
|
+
if (a.byHour && Object.keys(a.byHour).length) {
|
|
104
|
+
let total = 0, night = 0;
|
|
105
|
+
for (const [h, d] of Object.entries(a.byHour)) {
|
|
106
|
+
const hr = +h; const ms = d.activeMs || 0;
|
|
107
|
+
total += ms;
|
|
108
|
+
if (hr >= 22 || hr <= 4) night += ms;
|
|
109
|
+
}
|
|
110
|
+
const ph = a.peakHour && a.peakHour.hour != null ? +a.peakHour.hour : null;
|
|
111
|
+
if (total > 0) {
|
|
112
|
+
const ns = night / total;
|
|
113
|
+
if (ns >= 0.33 || (ph != null && (ph >= 22 || ph <= 4))) {
|
|
114
|
+
push(64, `Night owl — ${Math.round(ns * 100)}% of your coding is between 10pm and 5am${ph != null ? ` (peak ${hourLabel(ph)})` : ''}.`);
|
|
115
|
+
} else if (ph != null && ph >= 5 && ph <= 9) {
|
|
116
|
+
push(62, `Early bird — your focus peaks around ${hourLabel(ph)}.`);
|
|
117
|
+
} else if (ph != null) {
|
|
118
|
+
push(54, `Your most productive hour is ${hourLabel(ph)}.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 3. Best day on record ──────────────────────────────────────────────
|
|
124
|
+
if (a.bestDay && a.bestDay.day) {
|
|
125
|
+
const md = monthDay(a.bestDay.day);
|
|
126
|
+
const ago = daysAgo(a.bestDay.day);
|
|
127
|
+
if (md) {
|
|
128
|
+
const recent = ago != null && ago <= 1;
|
|
129
|
+
const ln = a.bestDay.linesAdded || 0;
|
|
130
|
+
const extra = ln > 0 ? ` and ${commas(ln)} lines` : '';
|
|
131
|
+
push(recent ? 93 : 60,
|
|
132
|
+
`${recent ? 'New personal best! ' : 'Biggest day so far: '}${md} — ${fmtHours(a.bestDay.activeMs || 0)}${extra}.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── 4. Weekend vs weekday rhythm ───────────────────────────────────────
|
|
137
|
+
if (a.byWeekday) {
|
|
138
|
+
let wd = 0, wdN = 0, we = 0, weN = 0;
|
|
139
|
+
for (const [k, d] of Object.entries(a.byWeekday)) {
|
|
140
|
+
const n = +k; const ms = d.activeMs || 0;
|
|
141
|
+
if (n === 0 || n === 6) { we += ms; weN += 1; } else if (n >= 1 && n <= 5) { wd += ms; wdN += 1; }
|
|
142
|
+
}
|
|
143
|
+
if (wdN && weN && wd > 0 && we > 0) {
|
|
144
|
+
const aWd = wd / wdN, aWe = we / weN;
|
|
145
|
+
if (aWd >= aWe * 1.6) push(55, `Weekday grinder — you average ${(aWd / aWe).toFixed(1)}× more on weekdays than weekends.`, 'weekday');
|
|
146
|
+
else if (aWe >= aWd * 1.4) push(57, `Weekend warrior — your Saturdays and Sundays out-pace the work week.`, 'weekday');
|
|
49
147
|
}
|
|
50
148
|
}
|
|
51
149
|
|
|
52
|
-
//
|
|
150
|
+
// ── 5. Peak weekday ────────────────────────────────────────────────────
|
|
53
151
|
if (a.byWeekday && Object.keys(a.byWeekday).length) {
|
|
54
152
|
let best = null;
|
|
55
|
-
for (const [
|
|
56
|
-
if (!best ||
|
|
57
|
-
}
|
|
58
|
-
if (best && best.ms > 0) {
|
|
59
|
-
out.push(`Peak weekday is ${WEEKDAYS[best.wd]} — ${fmtHours(best.ms)} all-time.`);
|
|
153
|
+
for (const [w, d] of Object.entries(a.byWeekday)) {
|
|
154
|
+
if (!best || (d.activeMs || 0) > best.ms) best = { wd: +w, ms: d.activeMs || 0 };
|
|
60
155
|
}
|
|
156
|
+
if (best && best.ms > 0) push(52, `Peak weekday is ${WEEKDAYS[best.wd]} — ${fmtHours(best.ms)} all-time.`, 'weekday');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── 6. Output as book pages ────────────────────────────────────────────
|
|
160
|
+
if ((a.linesAdded || 0) >= 500) {
|
|
161
|
+
const pages = Math.max(1, Math.round(a.linesAdded / 55));
|
|
162
|
+
push(48, `${commas(a.linesAdded)} lines written — about ${commas(pages)} pages of a paperback.`);
|
|
61
163
|
}
|
|
62
164
|
|
|
63
|
-
//
|
|
165
|
+
// ── 7. Cost pace (month-to-date forecast) ──────────────────────────────
|
|
64
166
|
if (a.estimatedCost && a.byDay) {
|
|
65
167
|
const now = new Date();
|
|
66
|
-
const
|
|
168
|
+
const ym = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
67
169
|
let mtd = 0;
|
|
68
|
-
for (const [k, day] of Object.entries(a.byDay))
|
|
69
|
-
if (k.startsWith(yearMonth)) mtd += day.cost || 0;
|
|
70
|
-
}
|
|
170
|
+
for (const [k, day] of Object.entries(a.byDay)) if (k.startsWith(ym)) mtd += day.cost || 0;
|
|
71
171
|
if (mtd > 0) {
|
|
72
172
|
const daysIn = now.getDate();
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
out.push(`Month-to-date estimate: ${fmtCost(mtd)} — pace projects ${fmtCost(forecast)} for the month.`);
|
|
173
|
+
const dim = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
174
|
+
push(60, `Month-to-date estimate: ${fmtCost(mtd)} — pace projects ${fmtCost((mtd / daysIn) * dim)} for the month.`);
|
|
76
175
|
}
|
|
77
176
|
}
|
|
78
177
|
|
|
79
|
-
//
|
|
178
|
+
// ── 8. Hotspot file ────────────────────────────────────────────────────
|
|
80
179
|
if (a.topEditedFiles && a.topEditedFiles.length) {
|
|
81
180
|
const top = a.topEditedFiles[0];
|
|
82
181
|
const name = top.path.split(/[\\/]/).filter(Boolean).pop();
|
|
83
|
-
|
|
182
|
+
push(54, `Hotspot: ${name} with ${fmtNum(top.count)} edits.`);
|
|
84
183
|
}
|
|
85
184
|
|
|
86
|
-
//
|
|
185
|
+
// ── 9. Streak — milestone tease + best-ever flag ───────────────────────
|
|
87
186
|
if (a.streak >= 3) {
|
|
88
|
-
const
|
|
89
|
-
const next = targets.find((t) => t > a.streak);
|
|
187
|
+
const next = [7, 14, 30, 60, 100, 365].find((t) => t > a.streak);
|
|
90
188
|
if (next) {
|
|
91
|
-
const
|
|
92
|
-
|
|
189
|
+
const rem = next - a.streak;
|
|
190
|
+
push(rem <= 2 ? 90 : 64, `${a.streak}-day streak — ${rem} ${rem === 1 ? 'day' : 'days'} to ${next}.`, 'streak');
|
|
93
191
|
} else {
|
|
94
|
-
|
|
192
|
+
push(82, `${a.streak}-day streak — beyond every milestone we track. Incredible.`, 'streak');
|
|
95
193
|
}
|
|
96
194
|
} else if (a.longestStreak >= 7) {
|
|
97
|
-
|
|
195
|
+
push(58, `Longest streak so far: ${a.longestStreak} days. Today could start the next one.`, 'streak');
|
|
196
|
+
}
|
|
197
|
+
if (a.streak && a.streak === a.longestStreak && a.streak >= 5) {
|
|
198
|
+
push(72, `You're on your best-ever streak — ${a.streak} days and counting.`, 'streak');
|
|
98
199
|
}
|
|
99
200
|
|
|
100
|
-
//
|
|
201
|
+
// ── 10. Top language ───────────────────────────────────────────────────
|
|
101
202
|
if (a.languages) {
|
|
102
203
|
const top = Object.entries(a.languages).sort((x, y) => y[1].edits - x[1].edits)[0];
|
|
103
|
-
if (top) {
|
|
104
|
-
out.push(`Most edits land in ${top[0]} — ${fmtNum(top[1].edits)} across ${fmtNum(top[1].files)} files.`);
|
|
105
|
-
}
|
|
204
|
+
if (top) push(53, `Most edits land in ${top[0]} — ${fmtNum(top[1].edits)} across ${fmtNum(top[1].files)} files.`);
|
|
106
205
|
}
|
|
107
206
|
|
|
108
|
-
//
|
|
109
|
-
if (a.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
207
|
+
// ── 11. Top project + breadth ──────────────────────────────────────────
|
|
208
|
+
if (a.projects && Object.keys(a.projects).length) {
|
|
209
|
+
let best = null;
|
|
210
|
+
for (const [name, d] of Object.entries(a.projects)) {
|
|
211
|
+
if (!best || (d.activeMs || 0) > best.ms) best = { name, ms: d.activeMs || 0, s: d.sessions || 0 };
|
|
113
212
|
}
|
|
213
|
+
if (best && best.ms > 0) push(56, `Most of your time goes to ${best.name} — ${fmtHours(best.ms)} across ${fmtNum(best.s)} sessions.`, 'project');
|
|
214
|
+
const n = Object.keys(a.projects).length;
|
|
215
|
+
if (n >= 5) push(40, `You've worked across ${n} projects with Claude Code.`, 'project');
|
|
114
216
|
}
|
|
115
217
|
|
|
116
|
-
//
|
|
218
|
+
// ── 12. Top tool + tool volume ─────────────────────────────────────────
|
|
219
|
+
if (a.toolBreakdown && Object.keys(a.toolBreakdown).length) {
|
|
220
|
+
const t = Object.entries(a.toolBreakdown).sort((x, y) => y[1] - x[1])[0];
|
|
221
|
+
if (t && t[1] >= 10) push(44, `Your most-used tool is ${t[0]} — ${fmtNum(t[1])} calls.`);
|
|
222
|
+
}
|
|
223
|
+
if ((a.uniqueFiles || 0) >= 50) push(42, `${commas(a.uniqueFiles)} unique files touched.`);
|
|
224
|
+
|
|
225
|
+
// ── 13. Top shell command ──────────────────────────────────────────────
|
|
226
|
+
if (a.bashCommands && Object.keys(a.bashCommands).length) {
|
|
227
|
+
const b = Object.entries(a.bashCommands).sort((x, y) => y[1] - x[1])[0];
|
|
228
|
+
if (b && b[1] >= 5) push(40, `Your go-to shell command: ${b[0]} (${fmtNum(b[1])}×).`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── 14. Model mix ──────────────────────────────────────────────────────
|
|
232
|
+
if (Array.isArray(a.modelSplit) && a.modelSplit.length) {
|
|
233
|
+
const m = a.modelSplit[0];
|
|
234
|
+
if (m && m.tokenPct > 0) push(50, `${prettyModel(m.model)} does most of your work — ${Math.round(m.tokenPct * 100)}% of tokens.`, 'model');
|
|
235
|
+
if (a.modelSplit.length >= 3) push(38, `${a.modelSplit.length} models in your rotation.`, 'model');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── 15. Subagents ──────────────────────────────────────────────────────
|
|
239
|
+
if ((a.subagentRuns || 0) >= 5) {
|
|
240
|
+
let topName = null, topN = 0;
|
|
241
|
+
for (const [name, n] of Object.entries(a.subagents || {})) if (n > topN) { topN = n; topName = name; }
|
|
242
|
+
push(46, `${fmtNum(a.subagentRuns)} subagent runs${topName ? ` — favourite is ${topName}` : ''}.`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── 16. Cache efficiency ───────────────────────────────────────────────
|
|
246
|
+
const totalTok = (a.inputTokens || 0) + (a.outputTokens || 0) + (a.cacheReadTokens || 0) + (a.cacheWriteTokens || 0);
|
|
247
|
+
if (totalTok > 0 && (a.cacheReadTokens || 0) / totalTok >= 0.5) {
|
|
248
|
+
push(46, `${Math.round((a.cacheReadTokens / totalTok) * 100)}% of your tokens come from cache — heavy context reuse.`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── 17. Tenure / anniversary ───────────────────────────────────────────
|
|
252
|
+
if (a.daysSinceFirst >= 2) {
|
|
253
|
+
const near = [7, 30, 100, 182, 365, 730].find((t) => t >= a.daysSinceFirst && t - a.daysSinceFirst <= 3);
|
|
254
|
+
if (near && near === a.daysSinceFirst) push(78, `Day ${near} with Claude Code — milestone unlocked.`);
|
|
255
|
+
else if (near) push(70, `${near - a.daysSinceFirst} ${near - a.daysSinceFirst === 1 ? 'day' : 'days'} to your ${near}-day mark with Claude Code.`);
|
|
256
|
+
else push(44, `Day ${a.daysSinceFirst} with Claude Code.`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── 18. Cadence ────────────────────────────────────────────────────────
|
|
260
|
+
if (a.daysSinceFirst >= 7 && a.sessions > 0) {
|
|
261
|
+
const per = a.sessions / a.daysSinceFirst;
|
|
262
|
+
if (per >= 1) push(40, `~${per.toFixed(1)} sessions a day since you started.`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── 19. Web research breadth ───────────────────────────────────────────
|
|
266
|
+
if (a.webDomains && Object.keys(a.webDomains).length >= 10) {
|
|
267
|
+
push(36, `Researched across ${Object.keys(a.webDomains).length} web domains.`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── 20. Notifications ──────────────────────────────────────────────────
|
|
117
271
|
if (a.notifications && a.notifications > 20) {
|
|
118
|
-
|
|
272
|
+
push(38, `${fmtNum(a.notifications)} notifications — Claude has paused for you that many times.`);
|
|
119
273
|
}
|
|
120
274
|
|
|
121
|
-
|
|
275
|
+
if (!C.length) return ['Not enough data yet — keep coding and check back tomorrow.'];
|
|
276
|
+
|
|
277
|
+
// Weight + seeded jitter: headlines (≥90) reliably lead; the mid-tier rotates
|
|
278
|
+
// so repeat runs surface a fresh mix. Default seed = clock → fresh each run.
|
|
279
|
+
const rand = rng(opts.seed != null ? opts.seed : Date.now());
|
|
280
|
+
for (const c of C) c.score = c.w + rand() * 22;
|
|
281
|
+
C.sort((x, y) => y.score - x.score);
|
|
282
|
+
|
|
283
|
+
// Take the top `limit`, but at most one line per topic so near-duplicates
|
|
284
|
+
// (two streak lines, peak-weekday + weekday-rhythm) never appear together.
|
|
122
285
|
const limit = opts.limit ?? 5;
|
|
123
|
-
|
|
286
|
+
const seen = new Set();
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const c of C) {
|
|
289
|
+
if (c.topic && seen.has(c.topic)) continue;
|
|
290
|
+
if (c.topic) seen.add(c.topic);
|
|
291
|
+
out.push(c.text);
|
|
292
|
+
if (out.length >= limit) break;
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
124
295
|
}
|