standout 0.1.0 → 0.2.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 +2 -1
- package/dist/ai-usage.d.ts +164 -0
- package/dist/ai-usage.js +1302 -0
- package/dist/api.d.ts +10 -0
- package/dist/api.js +86 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +351 -0
- package/dist/cursor.d.ts +54 -0
- package/dist/cursor.js +487 -0
- package/dist/dev-env.d.ts +28 -0
- package/dist/dev-env.js +264 -0
- package/dist/gather.d.ts +81 -0
- package/dist/gather.js +869 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +25 -0
- package/dist/prompt.d.ts +1 -0
- package/dist/prompt.js +247 -0
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +37 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +182 -0
- package/dist/twitter-scrape.d.ts +1 -0
- package/dist/twitter-scrape.js +267 -0
- package/dist/wrapped/aggregate.d.ts +6 -0
- package/dist/wrapped/aggregate.js +791 -0
- package/dist/wrapped/chrono-critters.d.ts +8 -0
- package/dist/wrapped/chrono-critters.js +32 -0
- package/dist/wrapped/index.d.ts +3 -0
- package/dist/wrapped/index.js +2 -0
- package/dist/wrapped/mascots.d.ts +2 -0
- package/dist/wrapped/mascots.js +35 -0
- package/dist/wrapped/render.d.ts +13 -0
- package/dist/wrapped/render.js +1003 -0
- package/dist/wrapped/types.d.ts +183 -0
- package/dist/wrapped/types.js +4 -0
- package/dist/wrapped-client.d.ts +10 -0
- package/dist/wrapped-client.js +36 -0
- package/dist/wrapped-share.d.ts +3 -0
- package/dist/wrapped-share.js +27 -0
- package/package.json +34 -8
- package/bin/cli.mjs +0 -30
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
const SOURCES = ["claude_code", "codex", "cursor"];
|
|
2
|
+
const TOOL_LABELS = {
|
|
3
|
+
claude_code: "Claude Code",
|
|
4
|
+
codex: "Codex",
|
|
5
|
+
cursor: "Cursor",
|
|
6
|
+
};
|
|
7
|
+
const RETAIL_RATES = [
|
|
8
|
+
{
|
|
9
|
+
match: /claude.*opus/i,
|
|
10
|
+
input: 15,
|
|
11
|
+
output: 75,
|
|
12
|
+
cacheRead: 1.5,
|
|
13
|
+
cacheWrite: 18.75,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
match: /claude.*sonnet/i,
|
|
17
|
+
input: 3,
|
|
18
|
+
output: 15,
|
|
19
|
+
cacheRead: 0.3,
|
|
20
|
+
cacheWrite: 3.75,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
match: /claude.*haiku/i,
|
|
24
|
+
input: 1,
|
|
25
|
+
output: 5,
|
|
26
|
+
cacheRead: 0.1,
|
|
27
|
+
cacheWrite: 1.25,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
match: /gpt-?5(-mini|-nano)?/i,
|
|
31
|
+
input: 1.25,
|
|
32
|
+
output: 10,
|
|
33
|
+
cacheRead: 0.125,
|
|
34
|
+
cacheWrite: 1.25,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
match: /gpt-?4/i,
|
|
38
|
+
input: 2.5,
|
|
39
|
+
output: 10,
|
|
40
|
+
cacheRead: 0.25,
|
|
41
|
+
cacheWrite: 2.5,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
function rateForModel(model) {
|
|
45
|
+
if (model) {
|
|
46
|
+
for (const r of RETAIL_RATES) {
|
|
47
|
+
if (r.match.test(model))
|
|
48
|
+
return {
|
|
49
|
+
input: r.input,
|
|
50
|
+
output: r.output,
|
|
51
|
+
cacheRead: r.cacheRead,
|
|
52
|
+
cacheWrite: r.cacheWrite,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }; // generic fallback
|
|
57
|
+
}
|
|
58
|
+
function classifyProvider(model) {
|
|
59
|
+
const m = model.toLowerCase();
|
|
60
|
+
if (m.startsWith("claude") || m.includes("anthropic"))
|
|
61
|
+
return "anthropic";
|
|
62
|
+
if (m.startsWith("gpt") ||
|
|
63
|
+
m.includes("openai") ||
|
|
64
|
+
m.startsWith("o1") ||
|
|
65
|
+
m.startsWith("o3"))
|
|
66
|
+
return "openai";
|
|
67
|
+
return "other";
|
|
68
|
+
}
|
|
69
|
+
function asObject(v) {
|
|
70
|
+
return v && typeof v === "object" ? v : {};
|
|
71
|
+
}
|
|
72
|
+
function str(v) {
|
|
73
|
+
return typeof v === "string" ? v : "";
|
|
74
|
+
}
|
|
75
|
+
function strOrNull(v) {
|
|
76
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
77
|
+
}
|
|
78
|
+
// Defensive parse of computed.collaboration (background-generated; absent on the
|
|
79
|
+
// first run). Returns null unless the core fields are present.
|
|
80
|
+
function parseCollaboration(v) {
|
|
81
|
+
const o = asObject(v);
|
|
82
|
+
if (!o.style_label || !o.summary)
|
|
83
|
+
return null;
|
|
84
|
+
const signals = asObject(o.signals);
|
|
85
|
+
const funFacts = asObject(o.fun_facts);
|
|
86
|
+
return {
|
|
87
|
+
style_label: str(o.style_label),
|
|
88
|
+
summary: str(o.summary),
|
|
89
|
+
signals: {
|
|
90
|
+
fact_checking: str(signals.fact_checking),
|
|
91
|
+
depth: str(signals.depth),
|
|
92
|
+
autonomy: str(signals.autonomy),
|
|
93
|
+
complexity: str(signals.complexity),
|
|
94
|
+
},
|
|
95
|
+
fun_facts: {
|
|
96
|
+
most_common_ask: strOrNull(funFacts.most_common_ask),
|
|
97
|
+
weirdest_prompt: strOrNull(funFacts.weirdest_prompt),
|
|
98
|
+
},
|
|
99
|
+
generated_by: o.generated_by === "llm" ? "llm" : "heuristic",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function parseProficiency(v) {
|
|
103
|
+
const o = asObject(v);
|
|
104
|
+
if (typeof o.score !== "number")
|
|
105
|
+
return null;
|
|
106
|
+
const num = (x) => typeof x === "number" && Number.isFinite(x) ? x : null;
|
|
107
|
+
const dim = o.top_dimension === "intensity" ||
|
|
108
|
+
o.top_dimension === "consistency" ||
|
|
109
|
+
o.top_dimension === "craft"
|
|
110
|
+
? o.top_dimension
|
|
111
|
+
: "intensity";
|
|
112
|
+
return {
|
|
113
|
+
score: o.score,
|
|
114
|
+
intensity: num(o.intensity),
|
|
115
|
+
consistency: num(o.consistency) ?? 0,
|
|
116
|
+
craft: num(o.craft),
|
|
117
|
+
base: num(o.base) ?? o.score,
|
|
118
|
+
craft_bonus: num(o.craft_bonus) ?? 0,
|
|
119
|
+
top_dimension: dim,
|
|
120
|
+
comment: strOrNull(o.comment) ?? "",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function parseContributions(v) {
|
|
124
|
+
const o = asObject(v);
|
|
125
|
+
if (typeof o.one_liner !== "string" || o.one_liner.length === 0)
|
|
126
|
+
return null;
|
|
127
|
+
const themes = Array.isArray(o.themes)
|
|
128
|
+
? o.themes.filter((t) => typeof t === "string")
|
|
129
|
+
: [];
|
|
130
|
+
return { one_liner: o.one_liner, themes };
|
|
131
|
+
}
|
|
132
|
+
function parseProjectBlurbs(v) {
|
|
133
|
+
const out = new Map();
|
|
134
|
+
if (!Array.isArray(v))
|
|
135
|
+
return out;
|
|
136
|
+
for (const item of v) {
|
|
137
|
+
const o = asObject(item);
|
|
138
|
+
if (typeof o.name === "string" && typeof o.blurb === "string" && o.blurb) {
|
|
139
|
+
out.set(o.name, o.blurb);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function asReadinessStatus(v, fallback) {
|
|
145
|
+
return v === "ready" ||
|
|
146
|
+
v === "scanning" ||
|
|
147
|
+
v === "enriching" ||
|
|
148
|
+
v === "unavailable" ||
|
|
149
|
+
v === "error"
|
|
150
|
+
? v
|
|
151
|
+
: fallback;
|
|
152
|
+
}
|
|
153
|
+
function compactCwd(cwd) {
|
|
154
|
+
// Collapse parallel worktrees of one repo to a single project label. The CLI
|
|
155
|
+
// already canonicalizes these, but older/stored profiles may carry raw paths.
|
|
156
|
+
// ~/work/standout/.worktrees/feat-x → work/standout
|
|
157
|
+
// ~/work/standout/.claude/worktrees/foo → work/standout
|
|
158
|
+
const base = cwd
|
|
159
|
+
.replace(/\/\.claude\/worktrees\/[^/]+.*$/, "")
|
|
160
|
+
.replace(/\/\.worktrees\/[^/]+.*$/, "");
|
|
161
|
+
const segments = base.split("/").filter(Boolean);
|
|
162
|
+
// Conductor parallel workspaces ~/conductor/workspaces/<repo>/<leaf> → <repo>.
|
|
163
|
+
const ci = segments.indexOf("conductor");
|
|
164
|
+
if (ci >= 0 && segments[ci + 1] === "workspaces" && segments[ci + 2]) {
|
|
165
|
+
return segments[ci + 2];
|
|
166
|
+
}
|
|
167
|
+
return segments.slice(-2).join("/") || base;
|
|
168
|
+
}
|
|
169
|
+
export function aggregateView(args) {
|
|
170
|
+
const { profile, computed, share_url } = args;
|
|
171
|
+
const identity = asObject(profile.identity);
|
|
172
|
+
const aiUsage = asObject(profile.ai_usage);
|
|
173
|
+
const aiCoding = asObject(profile.ai_coding);
|
|
174
|
+
const explicitReadiness = asObject(profile.readiness);
|
|
175
|
+
let totalHours = 0;
|
|
176
|
+
let totalSessions = 0;
|
|
177
|
+
let activeDays = 0;
|
|
178
|
+
let streakDays = 0;
|
|
179
|
+
let weekendWeightedSum = 0;
|
|
180
|
+
let weekendWeightTotal = 0;
|
|
181
|
+
const toolCounts = {};
|
|
182
|
+
let hourBuckets = [];
|
|
183
|
+
let peakHour = null;
|
|
184
|
+
let conductorWorkspaces = 0;
|
|
185
|
+
const frameworkCounts = new Map();
|
|
186
|
+
const promptSamples = [];
|
|
187
|
+
const promptsSeen = new Set();
|
|
188
|
+
const askMap = new Map();
|
|
189
|
+
const projectMap = new Map();
|
|
190
|
+
let usageStatsSeen = false;
|
|
191
|
+
let allTimeHours = 0;
|
|
192
|
+
let allTimeSessions = 0;
|
|
193
|
+
let allTimeActiveDays = 0;
|
|
194
|
+
let allTimeFirstSession = null;
|
|
195
|
+
let allTimeLastSession = null;
|
|
196
|
+
const allTimeMonthly = new Map();
|
|
197
|
+
// Peak hour + longest streak are computed over the FULL history (all-time),
|
|
198
|
+
// not the rolling 30-day window — otherwise they wobble day to day.
|
|
199
|
+
const allTimeHourBuckets = new Array(24).fill(0);
|
|
200
|
+
let allTimeStreak = 0;
|
|
201
|
+
for (const src of SOURCES) {
|
|
202
|
+
const stats = aiUsage[src];
|
|
203
|
+
if (!stats)
|
|
204
|
+
continue;
|
|
205
|
+
usageStatsSeen = true;
|
|
206
|
+
const allTime = stats.all_time;
|
|
207
|
+
if (allTime) {
|
|
208
|
+
allTimeHours += allTime.total_duration_hours ?? 0;
|
|
209
|
+
allTimeSessions += allTime.total_sessions ?? 0;
|
|
210
|
+
allTimeActiveDays = Math.max(allTimeActiveDays, allTime.active_days ?? 0);
|
|
211
|
+
if (allTime.first_session &&
|
|
212
|
+
(!allTimeFirstSession || allTime.first_session < allTimeFirstSession)) {
|
|
213
|
+
allTimeFirstSession = allTime.first_session;
|
|
214
|
+
}
|
|
215
|
+
if (allTime.last_session &&
|
|
216
|
+
(!allTimeLastSession || allTime.last_session > allTimeLastSession)) {
|
|
217
|
+
allTimeLastSession = allTime.last_session;
|
|
218
|
+
}
|
|
219
|
+
for (const b of allTime.monthly_buckets ?? []) {
|
|
220
|
+
const month = allTimeMonthly.get(b.month) ?? {
|
|
221
|
+
sessions: 0,
|
|
222
|
+
duration_hours: 0,
|
|
223
|
+
};
|
|
224
|
+
month.sessions += b.sessions ?? 0;
|
|
225
|
+
month.duration_hours += b.duration_hours ?? 0;
|
|
226
|
+
allTimeMonthly.set(b.month, month);
|
|
227
|
+
}
|
|
228
|
+
if (allTime.hour_buckets && allTime.hour_buckets.length === 24) {
|
|
229
|
+
for (let i = 0; i < 24; i++)
|
|
230
|
+
allTimeHourBuckets[i] += allTime.hour_buckets[i] ?? 0;
|
|
231
|
+
}
|
|
232
|
+
allTimeStreak = Math.max(allTimeStreak, allTime.longest_streak_days ?? 0);
|
|
233
|
+
}
|
|
234
|
+
totalHours += stats.total_duration_hours ?? 0;
|
|
235
|
+
totalSessions += stats.total_sessions ?? 0;
|
|
236
|
+
activeDays = Math.max(activeDays, stats.active_days ?? 0);
|
|
237
|
+
streakDays = Math.max(streakDays, stats.longest_streak_days ?? 0);
|
|
238
|
+
if (typeof stats.weekend_ratio === "number") {
|
|
239
|
+
const weight = stats.total_sessions ?? 0;
|
|
240
|
+
weekendWeightedSum += stats.weekend_ratio * weight;
|
|
241
|
+
weekendWeightTotal += weight;
|
|
242
|
+
}
|
|
243
|
+
for (const [name, count] of Object.entries(stats.tool_call_counts ?? {})) {
|
|
244
|
+
toolCounts[name] = (toolCounts[name] ?? 0) + count;
|
|
245
|
+
}
|
|
246
|
+
if (stats.hour_buckets && stats.hour_buckets.length === 24) {
|
|
247
|
+
if (hourBuckets.length === 0)
|
|
248
|
+
hourBuckets = stats.hour_buckets.slice();
|
|
249
|
+
else
|
|
250
|
+
hourBuckets = hourBuckets.map((v, i) => v + (stats.hour_buckets?.[i] ?? 0));
|
|
251
|
+
}
|
|
252
|
+
// Merge per-framework usage counts so the stack orders by what's used most;
|
|
253
|
+
// fall back to presence (count 1) for older profiles without counts.
|
|
254
|
+
if (stats.framework_counts) {
|
|
255
|
+
for (const [fw, n] of Object.entries(stats.framework_counts))
|
|
256
|
+
frameworkCounts.set(fw, (frameworkCounts.get(fw) ?? 0) + n);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
for (const fw of stats.frameworks ?? [])
|
|
260
|
+
frameworkCounts.set(fw, (frameworkCounts.get(fw) ?? 0) + 1);
|
|
261
|
+
}
|
|
262
|
+
for (const p of stats.prompt_samples ?? []) {
|
|
263
|
+
if (!p || p.length < 16)
|
|
264
|
+
continue;
|
|
265
|
+
const key = p
|
|
266
|
+
.toLowerCase()
|
|
267
|
+
.replace(/[\s\p{P}]+$/u, "")
|
|
268
|
+
.slice(0, 160);
|
|
269
|
+
if (promptsSeen.has(key))
|
|
270
|
+
continue;
|
|
271
|
+
promptsSeen.add(key);
|
|
272
|
+
if (promptSamples.length < 6)
|
|
273
|
+
promptSamples.push(p);
|
|
274
|
+
}
|
|
275
|
+
for (const ask of stats.prompt_frequency ?? []) {
|
|
276
|
+
if (!ask?.text)
|
|
277
|
+
continue;
|
|
278
|
+
const key = ask.text.toLowerCase().slice(0, 120);
|
|
279
|
+
const existing = askMap.get(key);
|
|
280
|
+
if (existing)
|
|
281
|
+
existing.count += ask.count;
|
|
282
|
+
else
|
|
283
|
+
askMap.set(key, { text: ask.text, count: ask.count });
|
|
284
|
+
}
|
|
285
|
+
for (const proj of stats.projects ?? []) {
|
|
286
|
+
if (proj.cwd?.includes("/conductor/")) {
|
|
287
|
+
conductorWorkspaces += proj.session_count;
|
|
288
|
+
}
|
|
289
|
+
const label = compactCwd(proj.cwd);
|
|
290
|
+
projectMap.set(label, (projectMap.get(label) ?? 0) + proj.session_count);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// "WHEN YOU CODE" + peak are computed over the full history when available
|
|
294
|
+
// (stable), falling back to the 30-day window only if all-time is missing.
|
|
295
|
+
const allTimeHasHours = allTimeHourBuckets.some((v) => v > 0);
|
|
296
|
+
let histogram = allTimeHasHours ? allTimeHourBuckets : hourBuckets;
|
|
297
|
+
if (histogram.length === 0)
|
|
298
|
+
histogram = new Array(24).fill(0);
|
|
299
|
+
// Peak hour = argmax of that merged histogram (consistent with the bars drawn).
|
|
300
|
+
let peakVal = 0;
|
|
301
|
+
histogram.forEach((count, hour) => {
|
|
302
|
+
if (count > peakVal) {
|
|
303
|
+
peakVal = count;
|
|
304
|
+
peakHour = hour;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
// Longest streak over the full history (only grows over time), falling back to
|
|
308
|
+
// the 30-day streak if all-time isn't available.
|
|
309
|
+
if (allTimeStreak > 0)
|
|
310
|
+
streakDays = allTimeStreak;
|
|
311
|
+
const topTools = Object.entries(toolCounts)
|
|
312
|
+
.sort((a, b) => b[1] - a[1])
|
|
313
|
+
.slice(0, 5)
|
|
314
|
+
.map(([name, count]) => ({ name, count }));
|
|
315
|
+
// Merge git repos you committed to (local.repos, all-time commits) with the
|
|
316
|
+
// AI-session cwds, so projects worked on outside Claude Code/Codex appear too.
|
|
317
|
+
const localRepos = Array.isArray(asObject(profile.local).repos)
|
|
318
|
+
? asObject(profile.local).repos
|
|
319
|
+
: [];
|
|
320
|
+
const projByName = new Map();
|
|
321
|
+
for (const r of localRepos) {
|
|
322
|
+
const name = typeof r.name === "string" ? r.name : null;
|
|
323
|
+
if (!name)
|
|
324
|
+
continue;
|
|
325
|
+
const commits = typeof r.commit_count === "number" ? r.commit_count : 0;
|
|
326
|
+
const existing = projByName.get(name);
|
|
327
|
+
// Two local clones can share a repo name (e.g. a stray 1-commit checkout and
|
|
328
|
+
// the real one with hundreds). Keep the fuller clone's count so the stray
|
|
329
|
+
// can't overwrite and bury the real project.
|
|
330
|
+
if (existing) {
|
|
331
|
+
if (commits > existing.commits)
|
|
332
|
+
existing.commits = commits;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
projByName.set(name, { label: name, name, commits, sessions: 0 });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const [label, sessions] of projectMap) {
|
|
339
|
+
const name = label.split("/").filter(Boolean).pop() ?? label;
|
|
340
|
+
const existing = projByName.get(name);
|
|
341
|
+
if (existing) {
|
|
342
|
+
existing.sessions += sessions;
|
|
343
|
+
existing.label = label;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
projByName.set(name, { label, name, commits: 0, sessions });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const topProjects = [
|
|
350
|
+
...projByName.values(),
|
|
351
|
+
]
|
|
352
|
+
.sort((a, b) => b.commits - a.commits || b.sessions - a.sessions)
|
|
353
|
+
.slice(0, 12);
|
|
354
|
+
const totalCommits = aiCoding.total_commits_since_2024 ?? 0;
|
|
355
|
+
const aiAssistedCommits = aiCoding.ai_assisted_commits ?? 0;
|
|
356
|
+
const aiAssistedPct = totalCommits > 0 ? aiAssistedCommits / totalCommits : 0;
|
|
357
|
+
// ───────────────────── tokens, tool relationship, models ─────────────────────
|
|
358
|
+
const tokens = computeTokens(aiUsage);
|
|
359
|
+
const toolRelationship = computeToolRelationship(aiUsage);
|
|
360
|
+
const models = computeModels(aiUsage);
|
|
361
|
+
const computedObj = asObject(computed);
|
|
362
|
+
const archetypeLabel = typeof computedObj.archetype_label === "string"
|
|
363
|
+
? computedObj.archetype_label
|
|
364
|
+
: "Builder";
|
|
365
|
+
const zinger = typeof computedObj.zinger === "string" && computedObj.zinger.length > 0
|
|
366
|
+
? computedObj.zinger
|
|
367
|
+
: "just keeps building";
|
|
368
|
+
const mascotPose = (typeof computedObj.mascot_pose === "string"
|
|
369
|
+
? computedObj.mascot_pose
|
|
370
|
+
: "default");
|
|
371
|
+
const earlyAdopter = computedObj.early_adopter !== false;
|
|
372
|
+
const cohortSize = computedObj.cohort_size ?? 0;
|
|
373
|
+
const percentileBands = asObject(computedObj.percentile_bands);
|
|
374
|
+
const rankSummary = asObject(computedObj.rank_summary);
|
|
375
|
+
const collaboration = parseCollaboration(computedObj.collaboration);
|
|
376
|
+
const contributions = parseContributions(computedObj.contributions);
|
|
377
|
+
const cardComments = asObject(computedObj.card_comments);
|
|
378
|
+
const talkThemes = Array.isArray(computedObj.talk_themes)
|
|
379
|
+
? computedObj.talk_themes.filter((t) => typeof t === "string")
|
|
380
|
+
: [];
|
|
381
|
+
const projectBlurbs = parseProjectBlurbs(computedObj.project_blurbs);
|
|
382
|
+
if (projectBlurbs.size > 0) {
|
|
383
|
+
for (const p of topProjects) {
|
|
384
|
+
const repoName = p.label.split("/").filter(Boolean).pop() ?? p.label;
|
|
385
|
+
const blurb = projectBlurbs.get(repoName) ?? projectBlurbs.get(p.label);
|
|
386
|
+
if (blurb)
|
|
387
|
+
p.blurb = blurb;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const topAsks = [...askMap.values()]
|
|
391
|
+
.sort((a, b) => b.count - a.count)
|
|
392
|
+
.slice(0, 5);
|
|
393
|
+
const name = identity.name || identity.email || "you";
|
|
394
|
+
const readiness = {
|
|
395
|
+
usage_stats_status: asReadinessStatus(explicitReadiness.usage_stats_status, usageStatsSeen ? "ready" : "unavailable"),
|
|
396
|
+
profile_basics_status: asReadinessStatus(explicitReadiness.profile_basics_status, identity.name || identity.email || identity.github_username
|
|
397
|
+
? "ready"
|
|
398
|
+
: "unavailable"),
|
|
399
|
+
enrichment_status: asReadinessStatus(explicitReadiness.enrichment_status, "unavailable"),
|
|
400
|
+
};
|
|
401
|
+
return {
|
|
402
|
+
readiness,
|
|
403
|
+
name,
|
|
404
|
+
archetype_label: archetypeLabel,
|
|
405
|
+
zinger,
|
|
406
|
+
mascot_pose: mascotPose,
|
|
407
|
+
total_hours: totalHours,
|
|
408
|
+
total_sessions: totalSessions,
|
|
409
|
+
active_days: activeDays,
|
|
410
|
+
streak_days: streakDays,
|
|
411
|
+
weekend_ratio: weekendWeightTotal > 0 ? weekendWeightedSum / weekendWeightTotal : 0,
|
|
412
|
+
top_tools: topTools,
|
|
413
|
+
hour_histogram: histogram,
|
|
414
|
+
peak_hour: peakHour,
|
|
415
|
+
conductor_workspaces: conductorWorkspaces,
|
|
416
|
+
top_projects: topProjects,
|
|
417
|
+
contributions,
|
|
418
|
+
all_time: {
|
|
419
|
+
total_hours: +allTimeHours.toFixed(1),
|
|
420
|
+
total_sessions: allTimeSessions,
|
|
421
|
+
active_days: allTimeActiveDays,
|
|
422
|
+
first_session: allTimeFirstSession,
|
|
423
|
+
last_session: allTimeLastSession,
|
|
424
|
+
monthly_buckets: [...allTimeMonthly.entries()]
|
|
425
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
426
|
+
.map(([month, b]) => ({
|
|
427
|
+
month,
|
|
428
|
+
sessions: b.sessions,
|
|
429
|
+
duration_hours: +b.duration_hours.toFixed(1),
|
|
430
|
+
})),
|
|
431
|
+
},
|
|
432
|
+
frameworks: [...frameworkCounts.entries()]
|
|
433
|
+
.sort((a, b) => b[1] - a[1])
|
|
434
|
+
.map(([fw]) => fw),
|
|
435
|
+
prompt_samples: promptSamples,
|
|
436
|
+
top_asks: topAsks,
|
|
437
|
+
collaboration,
|
|
438
|
+
total_commits: totalCommits,
|
|
439
|
+
ai_assisted_commits: aiAssistedCommits,
|
|
440
|
+
ai_assisted_pct: aiAssistedPct,
|
|
441
|
+
code_footprint: computeCodeFootprint(aiCoding, totalCommits, strOrNull(cardComments.footprint)),
|
|
442
|
+
concurrency: computeConcurrency(aiUsage, strOrNull(cardComments.concurrency)),
|
|
443
|
+
conversation: computeConversation(aiUsage, strOrNull(cardComments.talk), talkThemes),
|
|
444
|
+
rhythm_comment: strOrNull(cardComments.rhythm),
|
|
445
|
+
stack_comment: strOrNull(cardComments.stack),
|
|
446
|
+
early_adopter: earlyAdopter,
|
|
447
|
+
cohort_size: cohortSize,
|
|
448
|
+
percentile_bands: {
|
|
449
|
+
typescript_shippers: percentileBands.typescript_shippers ?? null,
|
|
450
|
+
ai_native: percentileBands.ai_native ?? null,
|
|
451
|
+
open_source: percentileBands.open_source ?? null,
|
|
452
|
+
},
|
|
453
|
+
rank_summary: {
|
|
454
|
+
sample_size: rankSummary.sample_size ?? 0,
|
|
455
|
+
hours_percentile: rankSummary.hours_percentile ?? null,
|
|
456
|
+
sessions_percentile: rankSummary.sessions_percentile ?? null,
|
|
457
|
+
active_days_percentile: rankSummary.active_days_percentile ?? null,
|
|
458
|
+
tokens_percentile: rankSummary.tokens_percentile ?? null,
|
|
459
|
+
commit_days_percentile: rankSummary.commit_days_percentile ?? null,
|
|
460
|
+
},
|
|
461
|
+
proficiency: parseProficiency(computedObj.proficiency),
|
|
462
|
+
tokens,
|
|
463
|
+
tool_relationship: toolRelationship,
|
|
464
|
+
models,
|
|
465
|
+
share_url,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
// ───────────────────── helpers ─────────────────────
|
|
469
|
+
function computeTokens(aiUsage) {
|
|
470
|
+
// The scanner feeds this view a rolling 30-day window. Sum all retained
|
|
471
|
+
// monthly buckets because a rolling window often spans two calendar months.
|
|
472
|
+
const displayMonths = new Map();
|
|
473
|
+
const byTool = {};
|
|
474
|
+
let totalCost = 0;
|
|
475
|
+
let workTokens = 0;
|
|
476
|
+
let cacheTokens = 0;
|
|
477
|
+
let workPrior30d = 0;
|
|
478
|
+
// Headline = "work" tokens the model actually generated/read fresh
|
|
479
|
+
// (input + output). Cache is reported separately because it's mostly the same
|
|
480
|
+
// context re-read every turn and would otherwise dwarf the number 100x.
|
|
481
|
+
const workTotal = (b) => (b.input_tokens ?? 0) + (b.output_tokens ?? 0);
|
|
482
|
+
const cacheTotal = (b) => b.cache_read_tokens !== undefined || b.cache_write_tokens !== undefined
|
|
483
|
+
? (b.cache_read_tokens ?? 0) + (b.cache_write_tokens ?? 0)
|
|
484
|
+
: (b.cache_tokens ?? 0);
|
|
485
|
+
// Cursor exposes no reliable token counts — flag it so the card can say so,
|
|
486
|
+
// but only when it was actually used in THIS window (the card is "last 30
|
|
487
|
+
// days"). All-time Cursor sessions don't count: claiming "Cursor used too" when
|
|
488
|
+
// they haven't opened it in a month is just wrong.
|
|
489
|
+
const CURSOR_MIN_SESSIONS = 3;
|
|
490
|
+
const cursorStats = aiUsage["cursor"];
|
|
491
|
+
const cursorUntracked = !!(cursorStats && (cursorStats.total_sessions ?? 0) >= CURSOR_MIN_SESSIONS);
|
|
492
|
+
for (const src of SOURCES) {
|
|
493
|
+
const stats = aiUsage[src];
|
|
494
|
+
if (!stats)
|
|
495
|
+
continue;
|
|
496
|
+
const buckets = stats.monthly_buckets ?? [];
|
|
497
|
+
for (const b of buckets) {
|
|
498
|
+
const work = workTotal(b);
|
|
499
|
+
const cache = cacheTotal(b);
|
|
500
|
+
workTokens += work;
|
|
501
|
+
cacheTokens += cache;
|
|
502
|
+
// The per-tool bars + sparkline show the cache-inclusive total (matches the
|
|
503
|
+
// rainbow headline); the "generated" callout is the work-only number.
|
|
504
|
+
byTool[TOOL_LABELS[src] ?? src] =
|
|
505
|
+
(byTool[TOOL_LABELS[src] ?? src] ?? 0) + work + cache;
|
|
506
|
+
const rate = rateForModel(b.primary_model);
|
|
507
|
+
const cacheRead = b.cache_read_tokens ??
|
|
508
|
+
(b.cache_write_tokens === undefined ? (b.cache_tokens ?? 0) : 0);
|
|
509
|
+
const cacheWrite = b.cache_write_tokens ?? 0;
|
|
510
|
+
totalCost +=
|
|
511
|
+
((b.input_tokens ?? 0) * rate.input) / 1_000_000 +
|
|
512
|
+
((b.output_tokens ?? 0) * rate.output) / 1_000_000 +
|
|
513
|
+
(cacheRead * rate.cacheRead) / 1_000_000 +
|
|
514
|
+
(cacheWrite * rate.cacheWrite) / 1_000_000;
|
|
515
|
+
}
|
|
516
|
+
const priorBuckets = stats.previous_30d?.monthly_buckets ?? [];
|
|
517
|
+
workPrior30d +=
|
|
518
|
+
priorBuckets.reduce((sum, b) => sum + workTotal(b), 0) ||
|
|
519
|
+
(stats.previous_30d?.total_input_tokens ?? 0) +
|
|
520
|
+
(stats.previous_30d?.total_output_tokens ?? 0);
|
|
521
|
+
const monthSource = stats.all_time?.monthly_buckets && stats.all_time.monthly_buckets.length
|
|
522
|
+
? stats.all_time.monthly_buckets
|
|
523
|
+
: buckets;
|
|
524
|
+
for (const b of monthSource) {
|
|
525
|
+
const m = displayMonths.get(b.month) ?? { tokens: 0 };
|
|
526
|
+
m.tokens += workTotal(b) + cacheTotal(b);
|
|
527
|
+
displayMonths.set(b.month, m);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const sortedMonths = [...displayMonths.keys()].sort();
|
|
531
|
+
const thisMonth = sortedMonths[sortedMonths.length - 1] ?? null;
|
|
532
|
+
// Omit tools with no token data (e.g. Cursor, which we can't read tokens for)
|
|
533
|
+
// so the card never renders a dead "Cursor ░░░░ 0" row.
|
|
534
|
+
const sortedByTool = Object.entries(byTool)
|
|
535
|
+
.filter(([, tokens]) => tokens > 0)
|
|
536
|
+
.sort(([, a], [, b]) => b - a)
|
|
537
|
+
.map(([tool, tokens]) => ({ tool, tokens }));
|
|
538
|
+
// 6-month sparkline
|
|
539
|
+
const sixMonth = [];
|
|
540
|
+
if (thisMonth) {
|
|
541
|
+
const [year, month] = thisMonth.split("-").map(Number);
|
|
542
|
+
for (let i = 5; i >= 0; i--) {
|
|
543
|
+
const d = new Date(Date.UTC(year, month - 1 - i, 1));
|
|
544
|
+
const key = d.toISOString().slice(0, 7);
|
|
545
|
+
sixMonth.push({
|
|
546
|
+
month: key,
|
|
547
|
+
tokens: displayMonths.get(key)?.tokens ?? 0,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
work_tokens: workTokens,
|
|
553
|
+
cache_tokens: cacheTokens,
|
|
554
|
+
total_30d: workTokens + cacheTokens,
|
|
555
|
+
total_prior_30d: workPrior30d,
|
|
556
|
+
multiplier_vs_prior: workTokens > 0 && workPrior30d > 0 ? workTokens / workPrior30d : null,
|
|
557
|
+
by_tool: sortedByTool,
|
|
558
|
+
six_month_buckets: sixMonth,
|
|
559
|
+
estimated_retail_cost_usd: Math.round(totalCost),
|
|
560
|
+
cursor_untracked: cursorUntracked,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function computeToolRelationship(aiUsage) {
|
|
564
|
+
// Build a per-month, per-tool session map across the trailing 6 months
|
|
565
|
+
const monthMap = new Map();
|
|
566
|
+
for (const src of SOURCES) {
|
|
567
|
+
const stats = aiUsage[src];
|
|
568
|
+
if (!stats)
|
|
569
|
+
continue;
|
|
570
|
+
for (const b of stats.monthly_buckets ?? []) {
|
|
571
|
+
let row = monthMap.get(b.month);
|
|
572
|
+
if (!row) {
|
|
573
|
+
row = new Map();
|
|
574
|
+
monthMap.set(b.month, row);
|
|
575
|
+
}
|
|
576
|
+
row.set(src, (row.get(src) ?? 0) + b.sessions);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const months = [...monthMap.keys()].sort();
|
|
580
|
+
if (months.length < 2) {
|
|
581
|
+
// Not enough data to detect a relationship — fall back to "loyalist" if there's
|
|
582
|
+
// any single-tool usage, otherwise insufficient.
|
|
583
|
+
if (months.length === 1) {
|
|
584
|
+
const row = monthMap.get(months[0]);
|
|
585
|
+
const best = [...row.entries()].sort(([, a], [, b]) => b - a)[0];
|
|
586
|
+
if (best) {
|
|
587
|
+
return {
|
|
588
|
+
kind: "loyalist",
|
|
589
|
+
tool: TOOL_LABELS[best[0]] ?? best[0],
|
|
590
|
+
months_count: 1,
|
|
591
|
+
sessions_count: best[1],
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return { kind: "insufficient" };
|
|
596
|
+
}
|
|
597
|
+
const timeline = [];
|
|
598
|
+
for (const month of months) {
|
|
599
|
+
const row = monthMap.get(month);
|
|
600
|
+
const total = [...row.values()].reduce((a, b) => a + b, 0);
|
|
601
|
+
if (total === 0)
|
|
602
|
+
continue;
|
|
603
|
+
const sorted = [...row.entries()].sort(([, a], [, b]) => b - a);
|
|
604
|
+
const [topTool, topCount] = sorted[0];
|
|
605
|
+
timeline.push({
|
|
606
|
+
month,
|
|
607
|
+
dominant: topTool,
|
|
608
|
+
share: topCount / total,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
// Detect a switch — find the longest run of consistent dominance, then look for
|
|
612
|
+
// a transition at the end. If the last 2+ months differ from the first 2+ months,
|
|
613
|
+
// that's a switch.
|
|
614
|
+
const dominants = timeline.map((t) => t.dominant);
|
|
615
|
+
const lastDominant = dominants[dominants.length - 1];
|
|
616
|
+
const firstDominant = dominants[0];
|
|
617
|
+
if (firstDominant && lastDominant && firstDominant !== lastDominant) {
|
|
618
|
+
// Find the month where the switch became permanent
|
|
619
|
+
let switchIdx = -1;
|
|
620
|
+
for (let i = dominants.length - 1; i >= 1; i--) {
|
|
621
|
+
if (dominants[i] === lastDominant && dominants[i - 1] !== lastDominant) {
|
|
622
|
+
switchIdx = i;
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (switchIdx > 0) {
|
|
627
|
+
return {
|
|
628
|
+
kind: "switch",
|
|
629
|
+
from_tool: TOOL_LABELS[firstDominant] ?? firstDominant,
|
|
630
|
+
to_tool: TOOL_LABELS[lastDominant] ?? lastDominant,
|
|
631
|
+
switch_month: timeline[switchIdx].month,
|
|
632
|
+
timeline: timeline.map((t) => ({
|
|
633
|
+
month: t.month,
|
|
634
|
+
dominant: TOOL_LABELS[t.dominant] ?? t.dominant,
|
|
635
|
+
share: t.share,
|
|
636
|
+
})),
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// No switch detected — check if all months had the same dominant tool (loyalist)
|
|
641
|
+
// or if it's been a mix (polyglot)
|
|
642
|
+
const uniqueDominants = new Set(dominants);
|
|
643
|
+
if (uniqueDominants.size === 1) {
|
|
644
|
+
let totalSessions = 0;
|
|
645
|
+
for (const row of monthMap.values()) {
|
|
646
|
+
totalSessions += row.get(firstDominant) ?? 0;
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
kind: "loyalist",
|
|
650
|
+
tool: TOOL_LABELS[firstDominant] ?? firstDominant,
|
|
651
|
+
months_count: months.length,
|
|
652
|
+
sessions_count: totalSessions,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
// Polyglot — bouncing between tools across months
|
|
656
|
+
const totalsByTool = new Map();
|
|
657
|
+
for (const row of monthMap.values()) {
|
|
658
|
+
for (const [tool, n] of row) {
|
|
659
|
+
totalsByTool.set(tool, (totalsByTool.get(tool) ?? 0) + n);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
const grandTotal = [...totalsByTool.values()].reduce((a, b) => a + b, 0);
|
|
663
|
+
const tools = [...totalsByTool.entries()]
|
|
664
|
+
.sort(([, a], [, b]) => b - a)
|
|
665
|
+
.map(([tool, n]) => ({
|
|
666
|
+
tool: TOOL_LABELS[tool] ?? tool,
|
|
667
|
+
share: grandTotal > 0 ? n / grandTotal : 0,
|
|
668
|
+
}));
|
|
669
|
+
return { kind: "polyglot", tools };
|
|
670
|
+
}
|
|
671
|
+
function computeModels(aiUsage) {
|
|
672
|
+
const allModelCounts = new Map();
|
|
673
|
+
for (const src of SOURCES) {
|
|
674
|
+
const stats = aiUsage[src];
|
|
675
|
+
if (!stats?.model_session_counts)
|
|
676
|
+
continue;
|
|
677
|
+
for (const [model, n] of Object.entries(stats.model_session_counts)) {
|
|
678
|
+
allModelCounts.set(model, (allModelCounts.get(model) ?? 0) + n);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const totalClassified = [...allModelCounts.values()].reduce((a, b) => a + b, 0);
|
|
682
|
+
if (totalClassified === 0) {
|
|
683
|
+
return { by_provider: [], top_model: null };
|
|
684
|
+
}
|
|
685
|
+
const byProvider = new Map();
|
|
686
|
+
for (const [model, sessions] of allModelCounts) {
|
|
687
|
+
const provider = classifyProvider(model);
|
|
688
|
+
const entry = byProvider.get(provider) ?? { sessions: 0, models: [] };
|
|
689
|
+
entry.sessions += sessions;
|
|
690
|
+
entry.models.push({ name: model, sessions });
|
|
691
|
+
byProvider.set(provider, entry);
|
|
692
|
+
}
|
|
693
|
+
const providers = [
|
|
694
|
+
...byProvider.entries(),
|
|
695
|
+
]
|
|
696
|
+
.sort(([, a], [, b]) => b.sessions - a.sessions)
|
|
697
|
+
.map(([provider, entry]) => ({
|
|
698
|
+
provider,
|
|
699
|
+
sessions: entry.sessions,
|
|
700
|
+
share: entry.sessions / totalClassified,
|
|
701
|
+
models: entry.models.sort((a, b) => b.sessions - a.sessions).slice(0, 4),
|
|
702
|
+
}));
|
|
703
|
+
let topModel = null;
|
|
704
|
+
let topCount = 0;
|
|
705
|
+
for (const [name, sessions] of allModelCounts) {
|
|
706
|
+
if (sessions > topCount) {
|
|
707
|
+
topCount = sessions;
|
|
708
|
+
topModel = {
|
|
709
|
+
name,
|
|
710
|
+
sessions,
|
|
711
|
+
share: sessions / totalClassified,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return { by_provider: providers, top_model: topModel };
|
|
716
|
+
}
|
|
717
|
+
function footprintLabel(net, additiveRatio) {
|
|
718
|
+
if (net <= 0)
|
|
719
|
+
return "Cleaner";
|
|
720
|
+
if (additiveRatio >= 0.7)
|
|
721
|
+
return "Shipper";
|
|
722
|
+
return "Refactorer";
|
|
723
|
+
}
|
|
724
|
+
function computeCodeFootprint(aiCoding, totalCommits, line) {
|
|
725
|
+
const added = aiCoding.lines_added_since_2024 ?? 0;
|
|
726
|
+
const deleted = aiCoding.lines_deleted_since_2024 ?? 0;
|
|
727
|
+
if (added === 0 && deleted === 0)
|
|
728
|
+
return null;
|
|
729
|
+
const net = added - deleted;
|
|
730
|
+
const additiveRatio = added + deleted > 0 ? added / (added + deleted) : 0;
|
|
731
|
+
return {
|
|
732
|
+
lines_added: added,
|
|
733
|
+
lines_deleted: deleted,
|
|
734
|
+
net,
|
|
735
|
+
additive_ratio: +additiveRatio.toFixed(2),
|
|
736
|
+
median_commit_size: aiCoding.median_commit_size ?? 0,
|
|
737
|
+
biggest_commit_size: aiCoding.biggest_commit_size ?? 0,
|
|
738
|
+
lines_per_commit: totalCommits > 0 ? Math.round((added + deleted) / totalCommits) : 0,
|
|
739
|
+
label: footprintLabel(net, additiveRatio),
|
|
740
|
+
line,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
// Concurrency comes from the tool with the most sessions (the user's main
|
|
744
|
+
// driver) — open-tab overlap isn't additive across tools.
|
|
745
|
+
function computeConcurrency(aiUsage, line) {
|
|
746
|
+
let best = null;
|
|
747
|
+
let bestSessions = -1;
|
|
748
|
+
for (const src of SOURCES) {
|
|
749
|
+
const stats = aiUsage[src];
|
|
750
|
+
if (!stats?.concurrency)
|
|
751
|
+
continue;
|
|
752
|
+
const sessions = stats.total_sessions ?? 0;
|
|
753
|
+
if (sessions > bestSessions) {
|
|
754
|
+
bestSessions = sessions;
|
|
755
|
+
best = stats.concurrency;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (!best)
|
|
759
|
+
return null;
|
|
760
|
+
return {
|
|
761
|
+
open_tab_avg: best.open_tab_avg ?? 0,
|
|
762
|
+
open_tab_peak: best.open_tab_peak ?? 0,
|
|
763
|
+
longest_session_hours: best.longest_session_hours ?? 0,
|
|
764
|
+
active_juggle_pct: best.active_juggle_pct ?? 0,
|
|
765
|
+
line,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function computeConversation(aiUsage, line, themes) {
|
|
769
|
+
let totalPrompts = 0;
|
|
770
|
+
let questionWeighted = 0;
|
|
771
|
+
let charsWeighted = 0;
|
|
772
|
+
for (const src of SOURCES) {
|
|
773
|
+
const stats = aiUsage[src];
|
|
774
|
+
const c = stats?.conversation;
|
|
775
|
+
if (!c)
|
|
776
|
+
continue;
|
|
777
|
+
const n = c.total_prompts ?? 0;
|
|
778
|
+
totalPrompts += n;
|
|
779
|
+
questionWeighted += (c.question_ratio ?? 0) * n;
|
|
780
|
+
charsWeighted += (c.avg_prompt_chars ?? 0) * n;
|
|
781
|
+
}
|
|
782
|
+
if (totalPrompts === 0)
|
|
783
|
+
return null;
|
|
784
|
+
return {
|
|
785
|
+
total_prompts: totalPrompts,
|
|
786
|
+
question_ratio: +(questionWeighted / totalPrompts).toFixed(2),
|
|
787
|
+
avg_prompt_chars: Math.round(charsWeighted / totalPrompts),
|
|
788
|
+
line,
|
|
789
|
+
themes,
|
|
790
|
+
};
|
|
791
|
+
}
|