standout 0.1.0 → 0.5.15

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 (45) hide show
  1. package/README.md +2 -1
  2. package/dist/ai-usage.d.ts +164 -0
  3. package/dist/ai-usage.js +1303 -0
  4. package/dist/api.d.ts +10 -0
  5. package/dist/api.js +86 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +351 -0
  8. package/dist/cursor.d.ts +54 -0
  9. package/dist/cursor.js +487 -0
  10. package/dist/dev-env.d.ts +28 -0
  11. package/dist/dev-env.js +264 -0
  12. package/dist/gather.d.ts +81 -0
  13. package/dist/gather.js +885 -0
  14. package/dist/mcp.d.ts +1 -0
  15. package/dist/mcp.js +25 -0
  16. package/dist/prompt.d.ts +1 -0
  17. package/dist/prompt.js +247 -0
  18. package/dist/redact.d.ts +1 -0
  19. package/dist/redact.js +37 -0
  20. package/dist/tools.d.ts +4 -0
  21. package/dist/tools.js +182 -0
  22. package/dist/twitter-scrape.d.ts +1 -0
  23. package/dist/twitter-scrape.js +267 -0
  24. package/dist/wrapped/aggregate.d.ts +6 -0
  25. package/dist/wrapped/aggregate.js +792 -0
  26. package/dist/wrapped/chrono-critters.d.ts +8 -0
  27. package/dist/wrapped/chrono-critters.js +32 -0
  28. package/dist/wrapped/index.d.ts +3 -0
  29. package/dist/wrapped/index.js +2 -0
  30. package/dist/wrapped/mascots.d.ts +2 -0
  31. package/dist/wrapped/mascots.js +35 -0
  32. package/dist/wrapped/preview.d.ts +1 -0
  33. package/dist/wrapped/preview.js +93 -0
  34. package/dist/wrapped/render.d.ts +14 -0
  35. package/dist/wrapped/render.js +996 -0
  36. package/dist/wrapped/tiers.d.ts +9 -0
  37. package/dist/wrapped/tiers.js +73 -0
  38. package/dist/wrapped/types.d.ts +184 -0
  39. package/dist/wrapped/types.js +4 -0
  40. package/dist/wrapped-client.d.ts +10 -0
  41. package/dist/wrapped-client.js +36 -0
  42. package/dist/wrapped-share.d.ts +3 -0
  43. package/dist/wrapped-share.js +27 -0
  44. package/package.json +35 -8
  45. package/bin/cli.mjs +0 -30
package/dist/cursor.js ADDED
@@ -0,0 +1,487 @@
1
+ import { existsSync } from "fs";
2
+ import { homedir, platform } from "os";
3
+ import { join } from "path";
4
+ import { redactSecrets } from "./redact.js";
5
+ const MAX_PROMPT_SAMPLES = 30;
6
+ const MAX_PROMPT_LEN = 200;
7
+ const MAX_SESSION_HOURS = 4;
8
+ const MAX_ACTIVE_GAP_MINUTES = 15;
9
+ const SINGLE_EVENT_SESSION_MINUTES = 1;
10
+ const MAX_FILE_PATHS = 5000;
11
+ const MAX_EXCHANGES = 500;
12
+ const MAX_ASSISTANT_LEN = 800;
13
+ const MAX_EXCHANGE_PROMPT_LEN = 2000;
14
+ const MAX_PROMPT_FREQUENCY = 12;
15
+ const CORRECTION_RE = /\b(no|nope|actually|wrong|incorrect|instead|revert|undo|don'?t|stop|not what|that'?s not)\b/i;
16
+ // Mirrors ai-usage.ts — cannot import (would create a module cycle).
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
+ 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
+ function isLowSignalAsk(sample) {
20
+ const t = sample.trim();
21
+ return t.length < 12 || TRIVIAL_ASK_RE.test(t) || SHELL_COMMAND_RE.test(t);
22
+ }
23
+ function cursorDedupKey(s) {
24
+ return s
25
+ .toLowerCase()
26
+ .replace(/[\s\p{P}]+$/u, "")
27
+ .slice(0, 160);
28
+ }
29
+ function sanitizeAssistantText(text) {
30
+ let out = text
31
+ .trim()
32
+ .replace(/```[\s\S]*?```/g, " ")
33
+ .replace(/\s+/g, " ")
34
+ .trim();
35
+ if (out.length < 8)
36
+ return null;
37
+ out = redactSecrets(out);
38
+ if (out.length > MAX_ASSISTANT_LEN)
39
+ out = out.slice(0, MAX_ASSISTANT_LEN) + "…";
40
+ return out;
41
+ }
42
+ const EXT_TO_LANG = {
43
+ ts: "typescript",
44
+ tsx: "typescript",
45
+ js: "javascript",
46
+ jsx: "javascript",
47
+ mjs: "javascript",
48
+ cjs: "javascript",
49
+ py: "python",
50
+ go: "go",
51
+ rs: "rust",
52
+ java: "java",
53
+ kt: "kotlin",
54
+ swift: "swift",
55
+ rb: "ruby",
56
+ php: "php",
57
+ cpp: "cpp",
58
+ cc: "cpp",
59
+ c: "c",
60
+ cs: "csharp",
61
+ sh: "shell",
62
+ bash: "shell",
63
+ zsh: "shell",
64
+ sql: "sql",
65
+ yaml: "yaml",
66
+ yml: "yaml",
67
+ json: "json",
68
+ md: "markdown",
69
+ mdx: "markdown",
70
+ css: "css",
71
+ scss: "sass",
72
+ vue: "vue",
73
+ svelte: "svelte",
74
+ };
75
+ function findCursorDb() {
76
+ const home = homedir();
77
+ const os = platform();
78
+ const candidates = os === "darwin"
79
+ ? [
80
+ join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb"),
81
+ ]
82
+ : os === "linux"
83
+ ? [
84
+ join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb"),
85
+ ]
86
+ : os === "win32"
87
+ ? [
88
+ join(home, "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb"),
89
+ ]
90
+ : [];
91
+ for (const p of candidates) {
92
+ if (existsSync(p))
93
+ return p;
94
+ }
95
+ return null;
96
+ }
97
+ async function loadSqliteDriver() {
98
+ try {
99
+ // Optional dep — better-sqlite3 ships prebuilds for common platforms but
100
+ // can fail to install in some environments. We must not crash gather().
101
+ const mod = (await import("better-sqlite3"));
102
+ return mod.default;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ function sanitizePrompt(raw, maxLen = MAX_PROMPT_LEN) {
109
+ if (!raw)
110
+ return null;
111
+ let text = raw.trim();
112
+ text = text
113
+ .replace(/```[\s\S]*?```/g, " ")
114
+ .replace(/^#+\s*/gm, "")
115
+ .replace(/\s+/g, " ")
116
+ .trim();
117
+ if (!text || text.length < 8)
118
+ return null;
119
+ text = redactSecrets(text);
120
+ if (text.length > maxLen) {
121
+ text = text.slice(0, maxLen) + "…";
122
+ }
123
+ return text;
124
+ }
125
+ async function readState(dbPath, Driver) {
126
+ const yieldMacro = () => new Promise((r) => setImmediate(r));
127
+ const PAGE = 500;
128
+ const D = Driver;
129
+ let db;
130
+ try {
131
+ db = new D(dbPath, { readonly: true });
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ try {
137
+ const composers = new Map();
138
+ const bubbles = new Map();
139
+ // Page through rows (better-sqlite3 is synchronous; reading + JSON.parsing
140
+ // everything at once blocks the event loop for seconds and freezes the
141
+ // loading spinner). Yield a macrotask between pages so it keeps animating.
142
+ // Keyset pagination (key is the indexed PK) — O(n) range scan, unlike
143
+ // LIMIT/OFFSET which re-scans from the start each page.
144
+ const composerStmt = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%' AND key > ? ORDER BY key LIMIT ?");
145
+ for (let lastKey = "";;) {
146
+ const rows = composerStmt.all(lastKey, PAGE);
147
+ if (rows.length === 0)
148
+ break;
149
+ for (const row of rows) {
150
+ lastKey = row.key;
151
+ let parsed;
152
+ try {
153
+ parsed = JSON.parse(row.value);
154
+ }
155
+ catch {
156
+ continue;
157
+ }
158
+ if (!parsed || typeof parsed !== "object")
159
+ continue;
160
+ const composerId = parsed.composerId ??
161
+ row.key.slice("composerData:".length);
162
+ composers.set(composerId, {
163
+ composerId,
164
+ createdAt: numericTs(parsed.createdAt),
165
+ lastUpdatedAt: numericTs(parsed.lastUpdatedAt),
166
+ modelConfig: typeof parsed.latestSelectedModel === "string"
167
+ ? parsed.latestSelectedModel
168
+ : null,
169
+ });
170
+ }
171
+ if (rows.length < PAGE)
172
+ break;
173
+ await yieldMacro();
174
+ }
175
+ const bubbleStmt = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' AND key > ? ORDER BY key LIMIT ?");
176
+ for (let lastKey = "";;) {
177
+ const rows = bubbleStmt.all(lastKey, PAGE);
178
+ if (rows.length === 0)
179
+ break;
180
+ for (const row of rows) {
181
+ lastKey = row.key;
182
+ const parts = row.key.split(":");
183
+ if (parts.length < 3)
184
+ continue;
185
+ const conversationId = parts[1];
186
+ let parsed;
187
+ try {
188
+ parsed = JSON.parse(row.value);
189
+ }
190
+ catch {
191
+ continue;
192
+ }
193
+ if (!parsed || typeof parsed !== "object")
194
+ continue;
195
+ const type = typeof parsed.type === "number" ? parsed.type : null;
196
+ const text = typeof parsed.text === "string" ? parsed.text : null;
197
+ const createdAt = numericTs(parsed.createdAt);
198
+ const filePaths = extractFilePaths(parsed);
199
+ bubbles.set(parts[2], {
200
+ conversationId,
201
+ type,
202
+ text,
203
+ createdAt,
204
+ filePaths,
205
+ });
206
+ }
207
+ if (rows.length < PAGE)
208
+ break;
209
+ await yieldMacro();
210
+ }
211
+ return { composers, bubbles };
212
+ }
213
+ finally {
214
+ try {
215
+ db.close();
216
+ }
217
+ catch {
218
+ // skip
219
+ }
220
+ }
221
+ }
222
+ function numericTs(v) {
223
+ if (typeof v === "number" && Number.isFinite(v)) {
224
+ // Cursor timestamps are sometimes ms since epoch (composers) or ISO string (bubbles).
225
+ return v > 1e12 ? v : v * 1000;
226
+ }
227
+ if (typeof v === "string") {
228
+ const t = Date.parse(v);
229
+ return Number.isNaN(t) ? null : t;
230
+ }
231
+ return null;
232
+ }
233
+ function extractFilePaths(bubble) {
234
+ const out = [];
235
+ const grab = (arr) => {
236
+ if (!Array.isArray(arr))
237
+ return;
238
+ for (const item of arr) {
239
+ if (typeof item === "string") {
240
+ out.push(item);
241
+ }
242
+ else if (item && typeof item === "object") {
243
+ const o = item;
244
+ if (typeof o.fsPath === "string")
245
+ out.push(o.fsPath);
246
+ else if (typeof o.path === "string")
247
+ out.push(o.path);
248
+ else if (typeof o.uri === "string")
249
+ out.push(o.uri);
250
+ else if (typeof o.relativeWorkspacePath === "string")
251
+ out.push(o.relativeWorkspacePath);
252
+ }
253
+ }
254
+ };
255
+ grab(bubble.relevantFiles);
256
+ grab(bubble.attachedFileCodeChunksMetadataOnly);
257
+ grab(bubble.attachedCodeChunks);
258
+ grab(bubble.recentlyViewedFiles);
259
+ return out.slice(0, 20);
260
+ }
261
+ function estimateActiveDurationMs(timestamps) {
262
+ const sorted = [...new Set(timestamps)]
263
+ .filter((ts) => Number.isFinite(ts))
264
+ .sort((a, b) => a - b);
265
+ if (sorted.length === 0)
266
+ return 0;
267
+ if (sorted.length === 1)
268
+ return SINGLE_EVENT_SESSION_MINUTES * 60 * 1000;
269
+ const maxGapMs = MAX_ACTIVE_GAP_MINUTES * 60 * 1000;
270
+ let total = 0;
271
+ for (let i = 1; i < sorted.length; i++) {
272
+ const gap = sorted[i] - sorted[i - 1];
273
+ if (gap <= 0)
274
+ continue;
275
+ total += Math.min(gap, maxGapMs);
276
+ }
277
+ return Math.min(total, MAX_SESSION_HOURS * 3600 * 1000);
278
+ }
279
+ export async function gatherCursor(opts = {}) {
280
+ const dbPath = opts.dbPath ?? findCursorDb();
281
+ if (!dbPath)
282
+ return null;
283
+ const Driver = await loadSqliteDriver();
284
+ if (!Driver)
285
+ return null;
286
+ const raw = await readState(dbPath, Driver);
287
+ if (!raw || raw.composers.size === 0)
288
+ return null;
289
+ const allTime = finalize(raw);
290
+ const stats = finalize(raw, { windowStartMs: opts.windowStartMs });
291
+ stats.all_time = {
292
+ total_sessions: allTime.total_sessions,
293
+ active_days: allTime.active_days,
294
+ first_session: allTime.first_session,
295
+ last_session: allTime.last_session,
296
+ 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,
301
+ monthly_buckets: allTime.monthly_buckets,
302
+ };
303
+ return stats.total_sessions > 0 || allTime.total_sessions > 0 ? stats : null;
304
+ }
305
+ function finalize(raw, opts = {}) {
306
+ // Group bubbles by conversationId so we know how many messages per session.
307
+ const sessionBubbles = new Map();
308
+ for (const bubble of raw.bubbles.values()) {
309
+ let entry = sessionBubbles.get(bubble.conversationId);
310
+ if (!entry) {
311
+ entry = {
312
+ timestamps: [],
313
+ bubbles: [],
314
+ };
315
+ sessionBubbles.set(bubble.conversationId, entry);
316
+ }
317
+ if (bubble.createdAt != null)
318
+ entry.timestamps.push(bubble.createdAt);
319
+ entry.bubbles.push(bubble);
320
+ }
321
+ const hourBuckets = new Array(24).fill(0);
322
+ let weekendEvents = 0;
323
+ let totalEvents = 0;
324
+ const activeDays = new Set();
325
+ let firstTs = Infinity;
326
+ let lastTs = -Infinity;
327
+ let totalDurationMs = 0;
328
+ let userTotal = 0;
329
+ let assistantTotal = 0;
330
+ const allFiles = new Set();
331
+ const promptSamples = [];
332
+ const promptSamplesSeen = new Set();
333
+ const promptCounts = new Map();
334
+ const exchanges = [];
335
+ let userTurns = 0;
336
+ let correctionTurns = 0;
337
+ let countedSessions = 0;
338
+ const monthly = new Map();
339
+ for (const [composerId, composer] of raw.composers.entries()) {
340
+ const entry = sessionBubbles.get(composerId);
341
+ const windowBubbles = opts.windowStartMs === undefined
342
+ ? (entry?.bubbles ?? [])
343
+ : (entry?.bubbles ?? []).filter((bubble) => bubble.createdAt != null &&
344
+ bubble.createdAt >= opts.windowStartMs);
345
+ const composerTs = composer.createdAt;
346
+ const bubbleTimestamps = entry?.timestamps ?? [];
347
+ const allTs = composerTs
348
+ ? [composerTs, ...bubbleTimestamps]
349
+ : bubbleTimestamps;
350
+ const windowTs = opts.windowStartMs === undefined
351
+ ? allTs
352
+ : allTs.filter((ts) => ts >= opts.windowStartMs);
353
+ if (windowTs.length === 0)
354
+ continue;
355
+ countedSessions += 1;
356
+ const minTs = Math.min(...windowTs);
357
+ const maxTs = Math.max(...windowTs);
358
+ const durationMs = estimateActiveDurationMs(windowTs);
359
+ if (minTs < firstTs)
360
+ firstTs = minTs;
361
+ if (maxTs > lastTs)
362
+ lastTs = maxTs;
363
+ totalDurationMs += durationMs;
364
+ const monthKey = new Date(minTs).toISOString().slice(0, 7);
365
+ const month = monthly.get(monthKey) ?? { sessions: 0, durationMs: 0 };
366
+ month.sessions += 1;
367
+ month.durationMs += durationMs;
368
+ monthly.set(monthKey, month);
369
+ for (const ts of windowTs) {
370
+ const d = new Date(ts);
371
+ if (Number.isNaN(d.getTime()))
372
+ continue;
373
+ hourBuckets[d.getHours()] += 1;
374
+ const day = d.getDay();
375
+ if (day === 0 || day === 6)
376
+ weekendEvents += 1;
377
+ totalEvents += 1;
378
+ activeDays.add(d.toISOString().slice(0, 10));
379
+ }
380
+ if (entry) {
381
+ userTotal += windowBubbles.filter((bubble) => bubble.type === 1).length;
382
+ assistantTotal += windowBubbles.filter((bubble) => bubble.type === 2).length;
383
+ for (const bubble of windowBubbles) {
384
+ for (const fp of bubble.filePaths) {
385
+ if (allFiles.size < MAX_FILE_PATHS)
386
+ allFiles.add(fp);
387
+ }
388
+ }
389
+ const windowSamples = windowBubbles
390
+ .filter((bubble) => bubble.type === 1 && bubble.text)
391
+ .map((bubble) => sanitizePrompt(bubble.text))
392
+ .filter((sample) => Boolean(sample));
393
+ for (const sample of windowSamples) {
394
+ if (promptSamples.length >= MAX_PROMPT_SAMPLES)
395
+ break;
396
+ const key = cursorDedupKey(sample);
397
+ if (promptSamplesSeen.has(key))
398
+ continue;
399
+ promptSamplesSeen.add(key);
400
+ promptSamples.push(sample);
401
+ }
402
+ // Conversation corpus: walk bubbles in chronological order, count prompt
403
+ // frequency + correction signal, and pair each user prompt with the next
404
+ // assistant reply into a truncated exchange for collaboration analysis.
405
+ const ordered = [...windowBubbles].sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
406
+ let pendingUser = null;
407
+ for (const bubble of ordered) {
408
+ if (bubble.type === 1 && bubble.text) {
409
+ const sample = sanitizePrompt(bubble.text);
410
+ if (!sample)
411
+ continue;
412
+ if (!isLowSignalAsk(sample)) {
413
+ const key = cursorDedupKey(sample);
414
+ const existing = promptCounts.get(key);
415
+ if (existing)
416
+ existing.count += 1;
417
+ else
418
+ promptCounts.set(key, { text: sample, count: 1 });
419
+ }
420
+ userTurns += 1;
421
+ if (CORRECTION_RE.test(sample))
422
+ correctionTurns += 1;
423
+ pendingUser =
424
+ sanitizePrompt(bubble.text, MAX_EXCHANGE_PROMPT_LEN) ?? sample;
425
+ }
426
+ else if (bubble.type === 2 && bubble.text && pendingUser) {
427
+ if (exchanges.length >= MAX_EXCHANGES)
428
+ continue;
429
+ const reply = sanitizeAssistantText(bubble.text);
430
+ if (!reply)
431
+ continue;
432
+ exchanges.push({ user: pendingUser, assistant: reply });
433
+ pendingUser = null;
434
+ }
435
+ }
436
+ }
437
+ }
438
+ const languages = {};
439
+ for (const fp of allFiles) {
440
+ const ext = fp.toLowerCase().split(".").pop();
441
+ if (ext && EXT_TO_LANG[ext]) {
442
+ const lang = EXT_TO_LANG[ext];
443
+ languages[lang] = (languages[lang] || 0) + 1;
444
+ }
445
+ }
446
+ return {
447
+ total_sessions: countedSessions,
448
+ active_days: activeDays.size,
449
+ first_session: Number.isFinite(firstTs)
450
+ ? new Date(firstTs).toISOString()
451
+ : null,
452
+ last_session: Number.isFinite(lastTs)
453
+ ? new Date(lastTs).toISOString()
454
+ : null,
455
+ total_duration_hours: +(totalDurationMs / 1000 / 3600).toFixed(1),
456
+ message_counts: {
457
+ user: userTotal,
458
+ assistant: assistantTotal,
459
+ total: userTotal + assistantTotal,
460
+ },
461
+ models: [],
462
+ hour_buckets: hourBuckets,
463
+ weekend_ratio: totalEvents > 0 ? +(weekendEvents / totalEvents).toFixed(2) : 0,
464
+ languages,
465
+ prompt_samples: promptSamples,
466
+ prompt_frequency: [...promptCounts.values()]
467
+ .sort((a, b) => b.count - a.count)
468
+ .slice(0, MAX_PROMPT_FREQUENCY),
469
+ exchanges,
470
+ interaction: {
471
+ user_turns: userTurns,
472
+ correction_turns: correctionTurns,
473
+ tool_calls: 0,
474
+ },
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
+ })),
486
+ };
487
+ }
@@ -0,0 +1,28 @@
1
+ export interface ClaudeSettingsSummary {
2
+ path: string;
3
+ hooks: string[];
4
+ mcp_servers: string[];
5
+ permissions_count: number;
6
+ }
7
+ export interface DevEnvironment {
8
+ agents_md: {
9
+ path: string;
10
+ contents: string;
11
+ }[];
12
+ claude_settings: ClaudeSettingsSummary[];
13
+ claude_commands: string[];
14
+ claude_skills: string[];
15
+ cursor_rules: {
16
+ path: string;
17
+ contents: string;
18
+ }[];
19
+ vscode_extensions: string[];
20
+ global_npm_packages: string[];
21
+ global_brew_packages: string[];
22
+ global_pip_packages: string[];
23
+ tools: {
24
+ name: string;
25
+ version: string;
26
+ }[];
27
+ }
28
+ export declare function gatherDevEnvironment(): Promise<DevEnvironment>;