standout 0.5.25 → 0.5.26

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/dist/cursor.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface CursorStats {
11
11
  total: number;
12
12
  };
13
13
  models: string[];
14
+ model_session_counts: Record<string, number>;
15
+ tool_call_counts: Record<string, number>;
14
16
  hour_buckets: number[];
15
17
  weekend_ratio: number;
16
18
  languages: Record<string, number>;
@@ -18,15 +20,12 @@ export interface CursorStats {
18
20
  prompt_frequency: PromptFrequency[];
19
21
  exchanges: ConversationExchange[];
20
22
  interaction: InteractionSignals;
21
- monthly_buckets: {
22
- month: string;
23
- sessions: number;
24
- duration_hours: number;
25
- input_tokens: number;
26
- output_tokens: number;
27
- cache_tokens: number;
28
- primary_model: string | null;
29
- }[];
23
+ total_input_tokens: number;
24
+ total_output_tokens: number;
25
+ total_cache_read_tokens: number;
26
+ total_cache_write_tokens: number;
27
+ tokens_estimated: true;
28
+ monthly_buckets: CursorMonthlyBucket[];
30
29
  all_time?: {
31
30
  total_sessions: number;
32
31
  active_days: number;
@@ -37,18 +36,22 @@ export interface CursorStats {
37
36
  total_output_tokens: number;
38
37
  total_cache_read_tokens: number;
39
38
  total_cache_write_tokens: number;
40
- monthly_buckets: {
41
- month: string;
42
- sessions: number;
43
- duration_hours: number;
44
- input_tokens: number;
45
- output_tokens: number;
46
- cache_tokens: number;
47
- primary_model: string | null;
48
- }[];
39
+ monthly_buckets: CursorMonthlyBucket[];
49
40
  };
50
41
  }
42
+ interface CursorMonthlyBucket {
43
+ month: string;
44
+ sessions: number;
45
+ duration_hours: number;
46
+ input_tokens: number;
47
+ output_tokens: number;
48
+ cache_read_tokens: number;
49
+ cache_write_tokens: number;
50
+ cache_tokens: number;
51
+ primary_model: string | null;
52
+ }
51
53
  export declare function gatherCursor(opts?: {
52
54
  dbPath?: string;
53
55
  windowStartMs?: number;
54
56
  }): Promise<CursorStats | null>;
57
+ export {};
package/dist/cursor.js CHANGED
@@ -16,6 +16,27 @@ const CORRECTION_RE = /\b(no|nope|actually|wrong|incorrect|instead|revert|undo|d
16
16
  // Mirrors ai-usage.ts — cannot import (would create a module cycle).
17
17
  const TRIVIAL_ASK_RE = /^(y|n|yes|yep|yeah|ya|ok|okay|k|sure|go|go ahead|do it|continue|please continue|next|thanks|thank you|thx|ty|nice|cool|perfect|great|good|done|ship it|lgtm|👍|\.|\?)\b[\s.!]*$/i;
18
18
  const SHELL_COMMAND_RE = /^(npx|npm|pnpm|yarn|bun|git|cd|ls|cat|node|python|pip|brew|sudo|docker|curl|echo|mkdir|rm|mv|cp)\b/i;
19
+ // Cursor persists no usable token/cost data locally — bubble `tokenCount` is
20
+ // zeroed and composer `usageData` is empty. We estimate both from active
21
+ // duration using the economics of our Claude Code cohort (Cursor runs the same
22
+ // Claude models). The per-hour token split is calibrated so that, priced at the
23
+ // generic retail fallback rate, it totals ~$20/active-hour — the cohort's
24
+ // blended retail $/hour. Surfaced with an "estimated" disclaimer on the cards.
25
+ const CURSOR_EST_TOKENS_PER_HOUR = {
26
+ input: 38_000,
27
+ output: 110_000,
28
+ cacheRead: 24_750_000,
29
+ cacheWrite: 750_000,
30
+ };
31
+ function estimateCursorTokens(hours) {
32
+ const h = hours > 0 ? hours : 0;
33
+ return {
34
+ input: Math.round(CURSOR_EST_TOKENS_PER_HOUR.input * h),
35
+ output: Math.round(CURSOR_EST_TOKENS_PER_HOUR.output * h),
36
+ cacheRead: Math.round(CURSOR_EST_TOKENS_PER_HOUR.cacheRead * h),
37
+ cacheWrite: Math.round(CURSOR_EST_TOKENS_PER_HOUR.cacheWrite * h),
38
+ };
39
+ }
19
40
  function isLowSignalAsk(sample) {
20
41
  const t = sample.trim();
21
42
  return t.length < 12 || TRIVIAL_ASK_RE.test(t) || SHELL_COMMAND_RE.test(t);
@@ -159,13 +180,20 @@ async function readState(dbPath, Driver) {
159
180
  continue;
160
181
  const composerId = parsed.composerId ??
161
182
  row.key.slice("composerData:".length);
183
+ const modelConfig = parsed.modelConfig && typeof parsed.modelConfig === "object"
184
+ ? parsed.modelConfig
185
+ : null;
186
+ const modelName = (modelConfig && typeof modelConfig.modelName === "string"
187
+ ? modelConfig.modelName
188
+ : null) ??
189
+ (typeof parsed.latestSelectedModel === "string"
190
+ ? parsed.latestSelectedModel
191
+ : null);
162
192
  composers.set(composerId, {
163
193
  composerId,
164
194
  createdAt: numericTs(parsed.createdAt),
165
195
  lastUpdatedAt: numericTs(parsed.lastUpdatedAt),
166
- modelConfig: typeof parsed.latestSelectedModel === "string"
167
- ? parsed.latestSelectedModel
168
- : null,
196
+ modelName: modelName && modelName !== "default" ? modelName : null,
169
197
  });
170
198
  }
171
199
  if (rows.length < PAGE)
@@ -196,12 +224,22 @@ async function readState(dbPath, Driver) {
196
224
  const text = typeof parsed.text === "string" ? parsed.text : null;
197
225
  const createdAt = numericTs(parsed.createdAt);
198
226
  const filePaths = extractFilePaths(parsed);
227
+ const tool = parsed.toolFormerData && typeof parsed.toolFormerData === "object"
228
+ ? parsed.toolFormerData
229
+ : null;
230
+ const toolName = tool &&
231
+ (typeof tool.name === "string"
232
+ ? tool.name
233
+ : typeof tool.tool === "string"
234
+ ? tool.tool
235
+ : null);
199
236
  bubbles.set(parts[2], {
200
237
  conversationId,
201
238
  type,
202
239
  text,
203
240
  createdAt,
204
241
  filePaths,
242
+ toolName: toolName || null,
205
243
  });
206
244
  }
207
245
  if (rows.length < PAGE)
@@ -294,10 +332,10 @@ export async function gatherCursor(opts = {}) {
294
332
  first_session: allTime.first_session,
295
333
  last_session: allTime.last_session,
296
334
  total_duration_hours: allTime.total_duration_hours,
297
- total_input_tokens: 0,
298
- total_output_tokens: 0,
299
- total_cache_read_tokens: 0,
300
- total_cache_write_tokens: 0,
335
+ total_input_tokens: allTime.total_input_tokens,
336
+ total_output_tokens: allTime.total_output_tokens,
337
+ total_cache_read_tokens: allTime.total_cache_read_tokens,
338
+ total_cache_write_tokens: allTime.total_cache_write_tokens,
301
339
  monthly_buckets: allTime.monthly_buckets,
302
340
  };
303
341
  return stats.total_sessions > 0 || allTime.total_sessions > 0 ? stats : null;
@@ -335,6 +373,9 @@ function finalize(raw, opts = {}) {
335
373
  let userTurns = 0;
336
374
  let correctionTurns = 0;
337
375
  let countedSessions = 0;
376
+ let toolCalls = 0;
377
+ const modelSessionCounts = {};
378
+ const toolCallCounts = {};
338
379
  const monthly = new Map();
339
380
  for (const [composerId, composer] of raw.composers.entries()) {
340
381
  const entry = sessionBubbles.get(composerId);
@@ -366,6 +407,10 @@ function finalize(raw, opts = {}) {
366
407
  month.sessions += 1;
367
408
  month.durationMs += durationMs;
368
409
  monthly.set(monthKey, month);
410
+ if (composer.modelName) {
411
+ modelSessionCounts[composer.modelName] =
412
+ (modelSessionCounts[composer.modelName] ?? 0) + 1;
413
+ }
369
414
  for (const ts of windowTs) {
370
415
  const d = new Date(ts);
371
416
  if (Number.isNaN(d.getTime()))
@@ -385,6 +430,11 @@ function finalize(raw, opts = {}) {
385
430
  if (allFiles.size < MAX_FILE_PATHS)
386
431
  allFiles.add(fp);
387
432
  }
433
+ if (bubble.toolName) {
434
+ toolCalls += 1;
435
+ toolCallCounts[bubble.toolName] =
436
+ (toolCallCounts[bubble.toolName] ?? 0) + 1;
437
+ }
388
438
  }
389
439
  const windowSamples = windowBubbles
390
440
  .filter((bubble) => bubble.type === 1 && bubble.text)
@@ -443,6 +493,34 @@ function finalize(raw, opts = {}) {
443
493
  languages[lang] = (languages[lang] || 0) + 1;
444
494
  }
445
495
  }
496
+ const monthlyBuckets = [...monthly.entries()]
497
+ .sort(([a], [b]) => a.localeCompare(b))
498
+ .map(([month, b]) => {
499
+ const hours = +(b.durationMs / 1000 / 3600).toFixed(1);
500
+ const est = estimateCursorTokens(hours);
501
+ return {
502
+ month,
503
+ sessions: b.sessions,
504
+ duration_hours: hours,
505
+ input_tokens: est.input,
506
+ output_tokens: est.output,
507
+ cache_read_tokens: est.cacheRead,
508
+ cache_write_tokens: est.cacheWrite,
509
+ cache_tokens: est.cacheRead + est.cacheWrite,
510
+ primary_model: null,
511
+ };
512
+ });
513
+ // Totals are the exact sum of the (rounded-per-month) buckets so the top-level
514
+ // figures metrics.ts reads match what the card sums from monthly_buckets.
515
+ const totals = monthlyBuckets.reduce((acc, b) => ({
516
+ input: acc.input + b.input_tokens,
517
+ output: acc.output + b.output_tokens,
518
+ cacheRead: acc.cacheRead + b.cache_read_tokens,
519
+ cacheWrite: acc.cacheWrite + b.cache_write_tokens,
520
+ }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
521
+ const models = Object.entries(modelSessionCounts)
522
+ .sort((a, b) => b[1] - a[1])
523
+ .map(([model]) => model);
446
524
  return {
447
525
  total_sessions: countedSessions,
448
526
  active_days: activeDays.size,
@@ -458,7 +536,9 @@ function finalize(raw, opts = {}) {
458
536
  assistant: assistantTotal,
459
537
  total: userTotal + assistantTotal,
460
538
  },
461
- models: [],
539
+ models,
540
+ model_session_counts: modelSessionCounts,
541
+ tool_call_counts: toolCallCounts,
462
542
  hour_buckets: hourBuckets,
463
543
  weekend_ratio: totalEvents > 0 ? +(weekendEvents / totalEvents).toFixed(2) : 0,
464
544
  languages,
@@ -470,18 +550,13 @@ function finalize(raw, opts = {}) {
470
550
  interaction: {
471
551
  user_turns: userTurns,
472
552
  correction_turns: correctionTurns,
473
- tool_calls: 0,
553
+ tool_calls: toolCalls,
474
554
  },
475
- monthly_buckets: [...monthly.entries()]
476
- .sort(([a], [b]) => a.localeCompare(b))
477
- .map(([month, b]) => ({
478
- month,
479
- sessions: b.sessions,
480
- duration_hours: +(b.durationMs / 1000 / 3600).toFixed(1),
481
- input_tokens: 0,
482
- output_tokens: 0,
483
- cache_tokens: 0,
484
- primary_model: null,
485
- })),
555
+ total_input_tokens: totals.input,
556
+ total_output_tokens: totals.output,
557
+ total_cache_read_tokens: totals.cacheRead,
558
+ total_cache_write_tokens: totals.cacheWrite,
559
+ tokens_estimated: true,
560
+ monthly_buckets: monthlyBuckets,
486
561
  };
487
562
  }
@@ -495,13 +495,15 @@ function computeTokens(aiUsage) {
495
495
  const cacheTotal = (b) => b.cache_read_tokens !== undefined || b.cache_write_tokens !== undefined
496
496
  ? (b.cache_read_tokens ?? 0) + (b.cache_write_tokens ?? 0)
497
497
  : (b.cache_tokens ?? 0);
498
- // Cursor exposes no reliable token counts — flag it so the card can say so,
499
- // but only when it was actually used in THIS window (the card is "last 30
500
- // days"). All-time Cursor sessions don't count: claiming "Cursor used too" when
501
- // they haven't opened it in a month is just wrong.
498
+ // Cursor exposes no local token counts — its tokens/cost are estimated from
499
+ // active time (see cursor.ts). Flag it so the card discloses the estimate, but
500
+ // only when it was actually used in THIS window (the card is "last 30 days").
501
+ // All-time Cursor sessions don't count: claiming "Cursor too" when they haven't
502
+ // opened it in a month is just wrong.
502
503
  const CURSOR_MIN_SESSIONS = 3;
503
504
  const cursorStats = aiUsage["cursor"];
504
- const cursorUntracked = !!(cursorStats && (cursorStats.total_sessions ?? 0) >= CURSOR_MIN_SESSIONS);
505
+ const cursorEstimated = !!(cursorStats && (cursorStats.total_sessions ?? 0) >= CURSOR_MIN_SESSIONS);
506
+ let cursorTokens = 0;
505
507
  for (const src of SOURCES) {
506
508
  const stats = aiUsage[src];
507
509
  if (!stats)
@@ -512,6 +514,8 @@ function computeTokens(aiUsage) {
512
514
  const cache = cacheTotal(b);
513
515
  workTokens += work;
514
516
  cacheTokens += cache;
517
+ if (src === "cursor")
518
+ cursorTokens += work + cache;
515
519
  // The per-tool bars + sparkline show the cache-inclusive total (matches the
516
520
  // rainbow headline); the "generated" callout is the work-only number.
517
521
  byTool[TOOL_LABELS[src] ?? src] =
@@ -570,7 +574,10 @@ function computeTokens(aiUsage) {
570
574
  by_tool: sortedByTool,
571
575
  six_month_buckets: sixMonth,
572
576
  estimated_retail_cost_usd: Math.round(totalCost),
573
- cursor_untracked: cursorUntracked,
577
+ cursor_estimated: cursorEstimated,
578
+ cursor_token_share: cursorEstimated && workTokens + cacheTokens > 0
579
+ ? cursorTokens / (workTokens + cacheTokens)
580
+ : 0,
574
581
  };
575
582
  }
576
583
  function computeToolRelationship(aiUsage) {
@@ -453,9 +453,9 @@ function cardRankSummary(view) {
453
453
  .filter((stat) => stat.topNumber !== null)
454
454
  .sort((a, b) => a.topNumber - b.topNumber)[0];
455
455
  const cohort = rank.sample_size > 0
456
- ? `across ${rank.sample_size.toLocaleString()} Standout users`
456
+ ? `across ${rank.sample_size.toLocaleString()} users`
457
457
  : view.cohort_size > 0
458
- ? `among ${view.cohort_size.toLocaleString()} Standout users so far`
458
+ ? `among ${view.cohort_size.toLocaleString()} users so far`
459
459
  : "Standout profile summary";
460
460
  const row = (stat) => {
461
461
  const top = "top" in stat && stat.top
@@ -517,6 +517,14 @@ function cardTokens(view) {
517
517
  const costLine = cost > 0
518
518
  ? ` ${chalk.dim("would have cost")} ${TITLE_GRADIENT(`~$${cost.toLocaleString()}`)} ${chalk.dim("at retail API rates")}`
519
519
  : "";
520
+ // Cursor stores no token counts locally, so its tokens + cost are estimated
521
+ // from active time. Disclose it — wording scales with how much of this card is
522
+ // Cursor (cursor-only users: the whole number is an estimate).
523
+ const cursorNote = t.cursor_estimated
524
+ ? t.cursor_token_share >= 0.9
525
+ ? chalk.dim(" ≈ estimated from time spent in Cursor (no local token data)")
526
+ : chalk.dim(" includes Cursor (tokens + cost estimated from active time)")
527
+ : "";
520
528
  const cachedNote = t.cache_tokens > t.work_tokens
521
529
  ? chalk.dim(" thankfully, most of them were cached :)")
522
530
  : t.cache_tokens > 0
@@ -533,12 +541,10 @@ function cardTokens(view) {
533
541
  cachedNote,
534
542
  generatedLine,
535
543
  costLine,
544
+ cursorNote,
536
545
  "",
537
546
  " last 30 days, across:",
538
547
  toolRows,
539
- view.tokens.cursor_untracked
540
- ? chalk.dim(" Cursor used too (its tokens aren't tracked locally)")
541
- : "",
542
548
  "",
543
549
  deltaLine,
544
550
  ]
@@ -875,9 +881,9 @@ export function cardProficiency(view) {
875
881
  const scoreTop = topPercentNumber(p.score_percentile ?? null);
876
882
  const sampleSize = view.rank_summary.sample_size;
877
883
  const cohort = scoreTop != null && sampleSize > 0
878
- ? `top ${scoreTop}% of ${sampleSize.toLocaleString()} Standout users`
884
+ ? `top ${scoreTop}% of ${sampleSize.toLocaleString()} users`
879
885
  : view.cohort_size > 0
880
- ? `among ${view.cohort_size.toLocaleString()} Standout users so far`
886
+ ? `among ${view.cohort_size.toLocaleString()} users so far`
881
887
  : "";
882
888
  const metricRow = (label, value) => value === null
883
889
  ? null
@@ -115,7 +115,8 @@ export interface WrappedAggregateView {
115
115
  tokens: number;
116
116
  }[];
117
117
  estimated_retail_cost_usd: number;
118
- cursor_untracked: boolean;
118
+ cursor_estimated: boolean;
119
+ cursor_token_share: number;
119
120
  };
120
121
  tool_relationship: {
121
122
  kind: "switch";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "standout",
3
- "version": "0.5.25",
3
+ "version": "0.5.26",
4
4
  "description": "Build your developer profile with AI. One command, zero friction.",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",