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