@aixle/insights 0.1.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/dist/auth/credentials.d.ts +23 -0
  4. package/dist/auth/credentials.js +174 -0
  5. package/dist/auth/exchange.d.ts +25 -0
  6. package/dist/auth/exchange.js +87 -0
  7. package/dist/auth/flow.d.ts +24 -0
  8. package/dist/auth/flow.js +66 -0
  9. package/dist/auth/keycloak.d.ts +35 -0
  10. package/dist/auth/keycloak.js +170 -0
  11. package/dist/cli.d.ts +51 -0
  12. package/dist/cli.js +426 -0
  13. package/dist/client.d.ts +28 -0
  14. package/dist/client.js +102 -0
  15. package/dist/collect-cursor-payloads.d.ts +57 -0
  16. package/dist/collect-cursor-payloads.js +134 -0
  17. package/dist/credentials.d.ts +2 -0
  18. package/dist/credentials.js +1 -0
  19. package/dist/cursor-checkpoints.d.ts +12 -0
  20. package/dist/cursor-checkpoints.js +28 -0
  21. package/dist/cursor-config.d.ts +5 -0
  22. package/dist/cursor-config.js +34 -0
  23. package/dist/cursor-payload-contract.d.ts +17 -0
  24. package/dist/cursor-payload-contract.js +258 -0
  25. package/dist/cursor-settings.d.ts +6 -0
  26. package/dist/cursor-settings.js +38 -0
  27. package/dist/cursor-store-audit.d.ts +48 -0
  28. package/dist/cursor-store-audit.js +155 -0
  29. package/dist/daily-stats-versions.d.ts +31 -0
  30. package/dist/daily-stats-versions.js +170 -0
  31. package/dist/health.d.ts +31 -0
  32. package/dist/health.js +195 -0
  33. package/dist/hooks/cursor-hooks-mapper.d.ts +22 -0
  34. package/dist/hooks/cursor-hooks-mapper.js +84 -0
  35. package/dist/hooks/cursor-hooks-reader.d.ts +30 -0
  36. package/dist/hooks/cursor-hooks-reader.js +117 -0
  37. package/dist/hooks/hook-forwarder.mjs +110 -0
  38. package/dist/hooks/hooks-config.d.ts +92 -0
  39. package/dist/hooks/hooks-config.js +235 -0
  40. package/dist/install/claude.d.ts +37 -0
  41. package/dist/install/claude.js +144 -0
  42. package/dist/install/index.d.ts +8 -0
  43. package/dist/install/index.js +11 -0
  44. package/dist/lib/args.d.ts +26 -0
  45. package/dist/lib/args.js +17 -0
  46. package/dist/lib/client.d.ts +33 -0
  47. package/dist/lib/client.js +52 -0
  48. package/dist/lib/config.d.ts +26 -0
  49. package/dist/lib/config.js +39 -0
  50. package/dist/lib/index.d.ts +4 -0
  51. package/dist/lib/index.js +4 -0
  52. package/dist/lib/project-resolver.d.ts +48 -0
  53. package/dist/lib/project-resolver.js +203 -0
  54. package/dist/lock.d.ts +9 -0
  55. package/dist/lock.js +84 -0
  56. package/dist/log.d.ts +14 -0
  57. package/dist/log.js +81 -0
  58. package/dist/pricing.d.ts +40 -0
  59. package/dist/pricing.js +149 -0
  60. package/dist/readers/claude.d.ts +83 -0
  61. package/dist/readers/claude.js +317 -0
  62. package/dist/readers/cursor.d.ts +134 -0
  63. package/dist/readers/cursor.js +900 -0
  64. package/dist/risk-scanner.d.ts +8 -0
  65. package/dist/risk-scanner.js +59 -0
  66. package/dist/server.d.ts +14 -0
  67. package/dist/server.js +234 -0
  68. package/dist/state.d.ts +69 -0
  69. package/dist/state.js +155 -0
  70. package/dist/sync.d.ts +74 -0
  71. package/dist/sync.js +679 -0
  72. package/package.json +66 -0
@@ -0,0 +1,900 @@
1
+ /**
2
+ * Consolidated SQLite reader + ingest mapper for Cursor telemetry.
3
+ * Not imported from the package — public export is `./sync`.
4
+ */
5
+ import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
6
+ import { finished } from "node:stream/promises";
7
+ import { createInterface } from "node:readline";
8
+ import { createHash } from "node:crypto";
9
+ import { basename, dirname, join } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { homedir } from "node:os";
12
+ import { glob } from "glob";
13
+ import Database from "better-sqlite3";
14
+ import { scanText } from "../risk-scanner.js";
15
+ // ─── Reader: paths & SQLite ──────────────────────────────────────────────────
16
+ export function cursorUserDir() {
17
+ switch (process.platform) {
18
+ case "darwin":
19
+ return join(homedir(), "Library", "Application Support", "Cursor", "User");
20
+ case "win32":
21
+ return join(process.env.APPDATA ?? homedir(), "Cursor", "User");
22
+ default:
23
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "Cursor", "User");
24
+ }
25
+ }
26
+ function cursorProjectsDir() {
27
+ return join(homedir(), ".cursor", "projects");
28
+ }
29
+ const LEGACY_TABLE = "CursorRequestFeedback";
30
+ const STATE_TABLE = "ItemTable";
31
+ function getTableNames(db) {
32
+ return db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name);
33
+ }
34
+ function tableExists(db, tableName) {
35
+ const row = db
36
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
37
+ .get(tableName);
38
+ return row !== undefined;
39
+ }
40
+ function logDbTables(db, dbPath, label) {
41
+ const tables = getTableNames(db);
42
+ console.log(` [${label}] ${dbPath}`);
43
+ console.log(` tables: ${tables.join(", ") || "(none)"}`);
44
+ }
45
+ /** Smoke-test better-sqlite3 against the global Cursor state DB (CUR-V02 / verify scripts). */
46
+ export function probeCursorGlobalStateDb(verbose = false) {
47
+ const dbPath = join(cursorUserDir(), "globalStorage", "state.vscdb");
48
+ try {
49
+ const db = new Database(dbPath, { readonly: true });
50
+ const row = db
51
+ .prepare(`SELECT count(*) AS c FROM ${STATE_TABLE} WHERE key LIKE 'aiCodeTracking.dailyStats%'`)
52
+ .get();
53
+ db.close();
54
+ if (verbose) {
55
+ console.log(` [probe] global state.vscdb OK — ${row.c} dailyStats key(s)`);
56
+ }
57
+ return true;
58
+ }
59
+ catch (err) {
60
+ const msg = err instanceof Error ? err.message : String(err);
61
+ console.error(` [probe] failed to read ${dbPath}: ${msg}`);
62
+ return false;
63
+ }
64
+ }
65
+ // ─── Legacy: cursor.db / CursorRequestFeedback ─────────────────────────────────
66
+ export function findCursorDbs(baseDir) {
67
+ const dir = join(baseDir ?? cursorUserDir(), "workspaceStorage");
68
+ try {
69
+ return glob.sync(join(dir, "**", "cursor.db"));
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ function readLegacyFromDb(dbPath, since, workspacePath, verbose) {
76
+ let db = null;
77
+ try {
78
+ db = new Database(dbPath, { readonly: true });
79
+ if (verbose)
80
+ logDbTables(db, dbPath, "cursor.db");
81
+ if (!tableExists(db, LEGACY_TABLE))
82
+ return [];
83
+ const params = [workspacePath];
84
+ let query = `SELECT *, ? as _workspacePath FROM ${LEGACY_TABLE}`;
85
+ if (since != null) {
86
+ query += " WHERE timestamp > ?";
87
+ params.push(since.getTime() / 1000 - 1);
88
+ }
89
+ query += " ORDER BY timestamp ASC";
90
+ const rows = db.prepare(query).all(...params);
91
+ if (since != null) {
92
+ const sinceMs = since.getTime();
93
+ return rows.filter((row) => {
94
+ const ms = toEpochMs(row.timestamp);
95
+ return ms !== null && ms > sinceMs;
96
+ });
97
+ }
98
+ return rows;
99
+ }
100
+ catch {
101
+ return [];
102
+ }
103
+ finally {
104
+ db?.close();
105
+ }
106
+ }
107
+ export function readLegacyEvents(since, baseDir, verbose = false) {
108
+ const dbPaths = findCursorDbs(baseDir);
109
+ if (verbose)
110
+ console.log(`Found ${dbPaths.length} legacy cursor.db file(s)`);
111
+ const results = [];
112
+ for (const dbPath of dbPaths) {
113
+ const workspacePath = dbPath.replace(/[\\/]cursor\.db$/, "");
114
+ for (const row of readLegacyFromDb(dbPath, since, workspacePath, verbose)) {
115
+ results.push({ row, workspacePath });
116
+ }
117
+ }
118
+ return results;
119
+ }
120
+ export function isGlobalStateDbPath(dbPath) {
121
+ return dbPath.replace(/\\/g, "/").includes("/globalStorage/state.vscdb");
122
+ }
123
+ function fileUriToPath(uri) {
124
+ try {
125
+ if (uri.startsWith("file://"))
126
+ return fileURLToPath(uri);
127
+ return uri;
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
133
+ function readWorkspaceFolderFromJson(workspaceStorageDir) {
134
+ const jsonPath = join(workspaceStorageDir, "workspace.json");
135
+ if (!existsSync(jsonPath))
136
+ return null;
137
+ try {
138
+ const parsed = JSON.parse(readFileSync(jsonPath, "utf-8"));
139
+ if (typeof parsed.folder === "string")
140
+ return fileUriToPath(parsed.folder);
141
+ const folders = parsed.folders;
142
+ if (Array.isArray(folders) && folders.length > 0) {
143
+ const first = folders[0];
144
+ if (typeof first.path === "string")
145
+ return fileUriToPath(first.path);
146
+ }
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ return null;
152
+ }
153
+ function resolveCursorWorkspaceFolder(dbPathOrWorkspaceDir) {
154
+ if (isGlobalStateDbPath(dbPathOrWorkspaceDir))
155
+ return null;
156
+ const normalized = dbPathOrWorkspaceDir.replace(/\\/g, "/");
157
+ const wsDir = normalized.endsWith("/state.vscdb")
158
+ ? dirname(dbPathOrWorkspaceDir)
159
+ : dbPathOrWorkspaceDir;
160
+ return readWorkspaceFolderFromJson(wsDir);
161
+ }
162
+ function cursorWorkspaceMetadata(dbPath) {
163
+ const workspace_scope = isGlobalStateDbPath(dbPath) ? "global" : "workspace";
164
+ const folder = workspace_scope === "workspace" ? resolveCursorWorkspaceFolder(dbPath) : null;
165
+ return {
166
+ workspace: dbPath,
167
+ workspace_scope,
168
+ ...(folder ? { workspace_folder: folder } : {}),
169
+ };
170
+ }
171
+ function cursorWorkspaceMetadataFromStorageDir(workspaceStorageDir) {
172
+ const folder = resolveCursorWorkspaceFolder(workspaceStorageDir);
173
+ return {
174
+ workspace: workspaceStorageDir,
175
+ workspace_scope: "workspace",
176
+ ...(folder ? { workspace_folder: folder } : {}),
177
+ };
178
+ }
179
+ function dailyStatsActivityTotal(value) {
180
+ if (typeof value !== "object" || value === null)
181
+ return 0;
182
+ const o = value;
183
+ const fields = [
184
+ "tabSuggestedLines",
185
+ "tabAcceptedLines",
186
+ "composerSuggestedLines",
187
+ "composerAcceptedLines",
188
+ ];
189
+ return fields.reduce((sum, field) => {
190
+ const n = o[field];
191
+ return sum + (typeof n === "number" && n > 0 ? n : 0);
192
+ }, 0);
193
+ }
194
+ function dailyStatsValuesEqual(a, b) {
195
+ return JSON.stringify(a) === JSON.stringify(b);
196
+ }
197
+ export function dedupeDailyStatsEntries(entries) {
198
+ const byDate = new Map();
199
+ for (const entry of entries) {
200
+ const group = byDate.get(entry.date) ?? [];
201
+ group.push(entry);
202
+ byDate.set(entry.date, group);
203
+ }
204
+ const deduped = [];
205
+ for (const group of byDate.values()) {
206
+ if (group.length === 1) {
207
+ deduped.push(group[0]);
208
+ continue;
209
+ }
210
+ const fromGlobal = group.filter((e) => isGlobalStateDbPath(e.dbPath));
211
+ if (fromGlobal.length > 0) {
212
+ deduped.push(fromGlobal[0]);
213
+ continue;
214
+ }
215
+ const distinct = [];
216
+ for (const entry of group) {
217
+ if (distinct.some((d) => dailyStatsValuesEqual(d.value, entry.value)))
218
+ continue;
219
+ distinct.push(entry);
220
+ }
221
+ if (distinct.length === 1) {
222
+ deduped.push(distinct[0]);
223
+ continue;
224
+ }
225
+ distinct.sort((a, b) => dailyStatsActivityTotal(b.value) - dailyStatsActivityTotal(a.value));
226
+ deduped.push(distinct[0]);
227
+ }
228
+ return deduped.sort((a, b) => a.date.localeCompare(b.date));
229
+ }
230
+ export function findStateVscDbs(baseDir) {
231
+ const userDir = baseDir ?? cursorUserDir();
232
+ const results = [];
233
+ results.push(join(userDir, "globalStorage", "state.vscdb"));
234
+ try {
235
+ results.push(...glob.sync(join(userDir, "workspaceStorage", "**", "state.vscdb")));
236
+ }
237
+ catch {
238
+ /* ignore */
239
+ }
240
+ return results;
241
+ }
242
+ function readDailyStatsFromDb(dbPath, since, verbose) {
243
+ let db = null;
244
+ try {
245
+ db = new Database(dbPath, { readonly: true });
246
+ if (verbose)
247
+ logDbTables(db, dbPath, "state.vscdb");
248
+ if (!tableExists(db, STATE_TABLE))
249
+ return [];
250
+ const rows = db
251
+ .prepare(`SELECT key, value FROM ${STATE_TABLE} WHERE key LIKE 'aiCodeTracking.%'`)
252
+ .all();
253
+ if (verbose && rows.length > 0) {
254
+ console.log(` aiCodeTracking keys (${rows.length}):`);
255
+ for (const r of rows.slice(0, 10)) {
256
+ console.log(` ${r.key} → ${r.value}`);
257
+ }
258
+ if (rows.length > 10)
259
+ console.log(` … and ${rows.length - 10} more`);
260
+ }
261
+ const entries = [];
262
+ for (const { key, value: rawValue } of rows) {
263
+ const dateMatch = key.match(/(\d{4}-\d{2}-\d{2})$/);
264
+ if (!dateMatch)
265
+ continue;
266
+ const date = dateMatch[1];
267
+ if (since && date <= since.toISOString().slice(0, 10))
268
+ continue;
269
+ let parsed;
270
+ try {
271
+ parsed = JSON.parse(rawValue);
272
+ }
273
+ catch {
274
+ continue;
275
+ }
276
+ entries.push({ date, value: parsed, dbPath });
277
+ }
278
+ return entries;
279
+ }
280
+ catch {
281
+ return [];
282
+ }
283
+ finally {
284
+ db?.close();
285
+ }
286
+ }
287
+ function readDailyStatsRaw(since, baseDir, verbose) {
288
+ const dbPaths = findStateVscDbs(baseDir);
289
+ if (verbose) {
290
+ console.log(`Searching: ${baseDir ?? cursorUserDir()}`);
291
+ console.log(`Found ${dbPaths.length} state.vscdb file(s)`);
292
+ }
293
+ const raw = [];
294
+ for (const dbPath of dbPaths) {
295
+ raw.push(...readDailyStatsFromDb(dbPath, since, verbose));
296
+ }
297
+ return raw;
298
+ }
299
+ export function readDailyStatsWithDedupe(since, baseDir, verbose = false) {
300
+ const raw = readDailyStatsRaw(since, baseDir, verbose);
301
+ const deduped = dedupeDailyStatsEntries(raw);
302
+ if (verbose && raw.length !== deduped.length) {
303
+ console.log(` dailyStats dedupe: ${raw.length} raw row(s) → ${deduped.length} after preferring globalStorage per date`);
304
+ }
305
+ return { raw, deduped };
306
+ }
307
+ export function readDailyStats(since, baseDir, verbose = false) {
308
+ return readDailyStatsWithDedupe(since, baseDir, verbose).deduped;
309
+ }
310
+ // ─── recentCommit ────────────────────────────────────────────────────────────
311
+ const RECENT_COMMIT_KEY = "aiCodeTracking.recentCommit";
312
+ function toTimestampMs(raw) {
313
+ if (typeof raw === "number" && !isNaN(raw) && raw > 0)
314
+ return raw;
315
+ if (typeof raw === "string") {
316
+ const n = Number(raw);
317
+ if (!isNaN(n) && n > 0)
318
+ return n;
319
+ }
320
+ return null;
321
+ }
322
+ function dedupeRecentCommitSnapshots(entries) {
323
+ const byKey = new Map();
324
+ for (const e of entries) {
325
+ const h = typeof e.value.commitHash === "string" && e.value.commitHash.length > 0
326
+ ? e.value.commitHash
327
+ : `${e.dbPath}:${String(e.value.timestamp ?? "")}`;
328
+ const prev = byKey.get(h);
329
+ const tE = toTimestampMs(e.value.timestamp);
330
+ if (tE === null)
331
+ continue;
332
+ if (!prev) {
333
+ byKey.set(h, e);
334
+ continue;
335
+ }
336
+ const tP = toTimestampMs(prev.value.timestamp);
337
+ if (tP === null || tE > tP)
338
+ byKey.set(h, e);
339
+ }
340
+ return [...byKey.values()];
341
+ }
342
+ function readRecentCommitFromDb(dbPath, since, verbose) {
343
+ let db = null;
344
+ try {
345
+ db = new Database(dbPath, { readonly: true });
346
+ if (!tableExists(db, STATE_TABLE))
347
+ return [];
348
+ const row = db
349
+ .prepare(`SELECT value FROM ${STATE_TABLE} WHERE key = ?`)
350
+ .get(RECENT_COMMIT_KEY);
351
+ if (!row)
352
+ return [];
353
+ let parsed;
354
+ try {
355
+ parsed = JSON.parse(row.value);
356
+ }
357
+ catch {
358
+ return [];
359
+ }
360
+ if (typeof parsed !== "object" || parsed === null)
361
+ return [];
362
+ const obj = parsed;
363
+ const tMs = toTimestampMs(obj.timestamp);
364
+ if (tMs === null)
365
+ return [];
366
+ if (since !== null) {
367
+ const sinceMs = since.getTime();
368
+ if (tMs <= sinceMs)
369
+ return [];
370
+ }
371
+ if (verbose) {
372
+ const ch = typeof obj.commitHash === "string" ? obj.commitHash : "";
373
+ console.log(` [recentCommit] ${dbPath}`);
374
+ if (ch)
375
+ console.log(` commitHash → ${ch.slice(0, 12)}…`);
376
+ }
377
+ return [{ value: obj, dbPath }];
378
+ }
379
+ catch {
380
+ return [];
381
+ }
382
+ finally {
383
+ db?.close();
384
+ }
385
+ }
386
+ export function readRecentCommitSnapshots(since, baseDir, verbose = false) {
387
+ const dbPaths = findStateVscDbs(baseDir);
388
+ if (verbose) {
389
+ console.log(`Searching recentCommit: ${baseDir ?? cursorUserDir()}`);
390
+ }
391
+ const found = [];
392
+ for (const dbPath of dbPaths) {
393
+ found.push(...readRecentCommitFromDb(dbPath, since, verbose));
394
+ }
395
+ return dedupeRecentCommitSnapshots(found);
396
+ }
397
+ export function readEvents(since, baseDir, verbose = false) {
398
+ return readLegacyEvents(since, baseDir, verbose);
399
+ }
400
+ function extractContentText(content) {
401
+ if (typeof content === "string")
402
+ return [content];
403
+ if (!Array.isArray(content))
404
+ return [];
405
+ return content.flatMap((block) => {
406
+ if (typeof block !== "object" || block === null)
407
+ return [];
408
+ const { type, text } = block;
409
+ return type === "text" && typeof text === "string" ? [text] : [];
410
+ });
411
+ }
412
+ function stripUserQueryWrapper(text) {
413
+ return text
414
+ .replace(/<user_query>\s*/g, "")
415
+ .replace(/\s*<\/user_query>/g, "")
416
+ .trim();
417
+ }
418
+ function estimateTokens(text) {
419
+ const trimmed = text.trim();
420
+ if (!trimmed)
421
+ return 0;
422
+ return Math.max(1, Math.ceil(trimmed.length / 4));
423
+ }
424
+ function toIsoFromMs(value) {
425
+ if (typeof value !== "number" || Number.isNaN(value) || value <= 0)
426
+ return null;
427
+ const date = new Date(value);
428
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
429
+ }
430
+ function readComposerHeaders(baseDir) {
431
+ const userDir = baseDir ?? cursorUserDir();
432
+ const dbPath = join(userDir, "globalStorage", "state.vscdb");
433
+ let db = null;
434
+ try {
435
+ db = new Database(dbPath, { readonly: true });
436
+ if (!tableExists(db, STATE_TABLE))
437
+ return new Map();
438
+ const row = db
439
+ .prepare(`SELECT value FROM ${STATE_TABLE} WHERE key = ?`)
440
+ .get("composer.composerHeaders");
441
+ if (!row)
442
+ return new Map();
443
+ const parsed = JSON.parse(row.value);
444
+ if (typeof parsed !== "object" || parsed === null)
445
+ return new Map();
446
+ const allComposers = parsed.allComposers;
447
+ if (!Array.isArray(allComposers))
448
+ return new Map();
449
+ const headers = new Map();
450
+ for (const entry of allComposers) {
451
+ if (typeof entry !== "object" || entry === null)
452
+ continue;
453
+ const composer = entry;
454
+ const composerId = typeof composer.composerId === "string" && composer.composerId.length > 0
455
+ ? composer.composerId
456
+ : null;
457
+ if (!composerId)
458
+ continue;
459
+ const workspaceIdentifier = typeof composer.workspaceIdentifier === "object" && composer.workspaceIdentifier !== null
460
+ ? composer.workspaceIdentifier
461
+ : null;
462
+ const uri = workspaceIdentifier &&
463
+ typeof workspaceIdentifier.uri === "object" &&
464
+ workspaceIdentifier.uri !== null
465
+ ? workspaceIdentifier.uri
466
+ : null;
467
+ headers.set(composerId, {
468
+ composerId,
469
+ name: typeof composer.name === "string" ? composer.name : null,
470
+ workspacePath: typeof uri?.fsPath === "string" ? uri.fsPath : null,
471
+ lastUpdatedAt: toIsoFromMs(composer.lastUpdatedAt),
472
+ });
473
+ }
474
+ return headers;
475
+ }
476
+ catch {
477
+ return new Map();
478
+ }
479
+ finally {
480
+ db?.close();
481
+ }
482
+ }
483
+ export function findCursorTranscriptFiles(projectDirs) {
484
+ const dirs = projectDirs ?? [cursorProjectsDir()];
485
+ const files = [];
486
+ for (const dir of dirs) {
487
+ try {
488
+ files.push(...glob.sync("**/agent-transcripts/*/*.jsonl", {
489
+ cwd: dir,
490
+ absolute: true,
491
+ }));
492
+ }
493
+ catch {
494
+ // directory missing — skip
495
+ }
496
+ }
497
+ return [...new Set(files)];
498
+ }
499
+ /**
500
+ * Decode a Cursor project directory name back to the original workspace path.
501
+ * Cursor encodes workspace paths as `absolutePath.slice(1).replace(/\//g, '-')`,
502
+ * which is ambiguous when directories contain literal hyphens. We resolve the
503
+ * ambiguity by backtracking: at each '-', try treating it as a path separator
504
+ * first (only if the current prefix exists on disk), then as a literal hyphen.
505
+ */
506
+ function decodeProjectDirName(encodedName) {
507
+ function bt(pos, current) {
508
+ if (pos === encodedName.length) {
509
+ return existsSync("/" + current) ? "/" + current : null;
510
+ }
511
+ const ch = encodedName[pos];
512
+ if (ch !== "-")
513
+ return bt(pos + 1, current + ch);
514
+ if (existsSync("/" + current)) {
515
+ const res = bt(pos + 1, current + "/");
516
+ if (res)
517
+ return res;
518
+ }
519
+ return bt(pos + 1, current + "-");
520
+ }
521
+ return bt(0, "");
522
+ }
523
+ /**
524
+ * Derive the workspace path from a transcript file path when the composer
525
+ * header doesn't carry a workspacePath. The transcript lives at:
526
+ * ~/.cursor/projects/<encoded-workspace>/agent-transcripts/<session>/<session>.jsonl
527
+ * Going up three directories yields the encoded project dir, which we decode.
528
+ * Returns null on Windows (encoding differs) or if the path can't be resolved.
529
+ */
530
+ function workspaceFromTranscriptFile(filePath) {
531
+ if (process.platform === "win32")
532
+ return null;
533
+ const projectDir = dirname(dirname(dirname(filePath)));
534
+ const projectsDir = cursorProjectsDir();
535
+ if (!projectDir.startsWith(projectsDir))
536
+ return null;
537
+ return decodeProjectDirName(basename(projectDir));
538
+ }
539
+ /** Skip JSONL files larger than this to avoid memory pressure on extremely long sessions. */
540
+ const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024; // 50 MB
541
+ export async function parseCursorTranscriptFile(filePath, composerHeaders, verbose = false) {
542
+ let fileSize = 0;
543
+ let occurredAt = new Date().toISOString();
544
+ try {
545
+ const stat = statSync(filePath);
546
+ fileSize = stat.size;
547
+ occurredAt = stat.mtime.toISOString();
548
+ }
549
+ catch {
550
+ return [];
551
+ }
552
+ if (fileSize > MAX_TRANSCRIPT_BYTES) {
553
+ if (verbose) {
554
+ console.warn(`[warn][cursor] ${filePath} exceeds ${MAX_TRANSCRIPT_BYTES / 1024 / 1024} MB limit — skipping`);
555
+ }
556
+ return [];
557
+ }
558
+ const sessionId = basename(filePath, ".jsonl");
559
+ const header = composerHeaders.get(sessionId);
560
+ if (header?.lastUpdatedAt)
561
+ occurredAt = header.lastUpdatedAt;
562
+ const turns = [];
563
+ let currentPromptParts = [];
564
+ let currentAssistantParts = [];
565
+ let turnIndex = 0;
566
+ const hasher = createHash("sha256");
567
+ const stream = createReadStream(filePath, { encoding: "utf-8" });
568
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
569
+ const finalizeTurn = () => {
570
+ const promptText = currentPromptParts.join("\n\n").trim();
571
+ const assistantText = currentAssistantParts.join("\n\n").trim();
572
+ if (!promptText && !assistantText)
573
+ return;
574
+ const risk = scanText(promptText);
575
+ turnIndex += 1;
576
+ turns.push({
577
+ turnId: `${sessionId}:${turnIndex}`,
578
+ sessionId,
579
+ filePath,
580
+ fileSize,
581
+ workspacePath: header?.workspacePath ?? workspaceFromTranscriptFile(filePath),
582
+ composerName: header?.name ?? null,
583
+ occurredAt,
584
+ promptText,
585
+ assistantText,
586
+ tokensIn: estimateTokens(promptText),
587
+ tokensOut: estimateTokens(assistantText),
588
+ riskLevel: risk.risk_level,
589
+ riskScore: risk.risk_score,
590
+ riskCategories: risk.risk_categories,
591
+ });
592
+ };
593
+ let lineNumber = 0;
594
+ try {
595
+ for await (const line of rl) {
596
+ lineNumber++;
597
+ hasher.update(line + "\n");
598
+ const trimmed = line.trim();
599
+ if (!trimmed)
600
+ continue;
601
+ let entry;
602
+ try {
603
+ entry = JSON.parse(trimmed);
604
+ }
605
+ catch {
606
+ if (verbose) {
607
+ console.warn(`[warn] ${filePath}:${lineNumber} — invalid JSON, skipping`);
608
+ }
609
+ continue;
610
+ }
611
+ const texts = extractContentText(entry.message?.content);
612
+ if (texts.length === 0)
613
+ continue;
614
+ if (entry.role === "user") {
615
+ if (currentPromptParts.length > 0 || currentAssistantParts.length > 0) {
616
+ finalizeTurn();
617
+ currentPromptParts = [];
618
+ currentAssistantParts = [];
619
+ }
620
+ currentPromptParts.push(...texts.map(stripUserQueryWrapper).filter((text) => text.length > 0));
621
+ }
622
+ else if (entry.role === "assistant") {
623
+ currentAssistantParts.push(...texts.map((text) => text.trim()).filter((text) => text.length > 0));
624
+ }
625
+ }
626
+ }
627
+ catch (err) {
628
+ if (verbose) {
629
+ const message = err instanceof Error ? err.message : String(err);
630
+ console.warn(`[warn] ${filePath} — stream error, skipping file: ${message}`);
631
+ }
632
+ rl.close();
633
+ stream.destroy();
634
+ await finished(stream).catch(() => undefined);
635
+ return [];
636
+ }
637
+ finalizeTurn();
638
+ const contentHash = hasher.digest("hex").slice(0, 32);
639
+ for (const t of turns)
640
+ t.contentHash = contentHash;
641
+ return turns;
642
+ }
643
+ export async function readCursorTranscriptSessions(cursorUserBaseDir, transcriptProjectDirs, verbose = false) {
644
+ const composerHeaders = readComposerHeaders(cursorUserBaseDir);
645
+ const files = findCursorTranscriptFiles(transcriptProjectDirs);
646
+ const sessions = await Promise.all(files.map((filePath) => parseCursorTranscriptFile(filePath, composerHeaders, verbose)));
647
+ return sessions.flat();
648
+ }
649
+ // ─── Mapper ──────────────────────────────────────────────────────────────────
650
+ const LINE_COST_MODEL = "estimated_line_count";
651
+ const TOKEN_COST_MODEL = "token_count";
652
+ const TRANSCRIPT_COST_MODEL = "estimated_transcript_text";
653
+ export const HOOK_COST_MODEL = "cursor_hook";
654
+ export const DEFAULT_CURSOR_PRICING = {
655
+ tokens_per_line: 15,
656
+ completion_output_per_mtok: 0.6,
657
+ chat_input_per_mtok: 3.0,
658
+ chat_output_per_mtok: 15.0,
659
+ };
660
+ const EPOCH_SECONDS_THRESHOLD = 1e12;
661
+ export function toEpochMs(timestamp) {
662
+ if (timestamp == null)
663
+ return null;
664
+ const num = typeof timestamp === "string" ? Number(timestamp) : timestamp;
665
+ if (isNaN(num))
666
+ return null;
667
+ return num < EPOCH_SECONDS_THRESHOLD ? num * 1000 : num;
668
+ }
669
+ function toIsoString(timestamp) {
670
+ const ms = toEpochMs(timestamp);
671
+ if (ms === null)
672
+ return null;
673
+ const date = new Date(ms);
674
+ if (isNaN(date.getTime()))
675
+ return null;
676
+ return date.toISOString();
677
+ }
678
+ const nn = (n) => (n > 0 ? n : 0);
679
+ function computeLineCost(eventType, lines, pricing) {
680
+ const safeLines = Math.max(0, lines);
681
+ const tokensPerLine = nn(pricing.tokens_per_line);
682
+ if (eventType === "completion") {
683
+ return (safeLines * tokensPerLine * nn(pricing.completion_output_per_mtok)) / 1_000_000;
684
+ }
685
+ return ((safeLines * tokensPerLine * (nn(pricing.chat_output_per_mtok) + nn(pricing.chat_input_per_mtok) * 2)) /
686
+ 1_000_000);
687
+ }
688
+ function computeTokenCost(eventType, tokensIn, tokensOut, pricing) {
689
+ const safeIn = Math.max(0, tokensIn);
690
+ const safeOut = Math.max(0, tokensOut);
691
+ if (eventType === "completion") {
692
+ return (safeOut * nn(pricing.completion_output_per_mtok)) / 1_000_000;
693
+ }
694
+ return (safeIn * nn(pricing.chat_input_per_mtok) + safeOut * nn(pricing.chat_output_per_mtok)) / 1_000_000;
695
+ }
696
+ function pick(obj, ...keys) {
697
+ let cur = obj;
698
+ for (const k of keys) {
699
+ if (typeof cur !== "object" || cur === null)
700
+ return null;
701
+ cur = cur[k];
702
+ }
703
+ return typeof cur === "number" ? cur : null;
704
+ }
705
+ function buildPayload(opts) {
706
+ const { eventType, tokensIn, tokensOut, costUsd, occurredAt, dbPath, model = "unknown", projectId, costModel = LINE_COST_MODEL, } = opts;
707
+ const payload = {
708
+ tool_name: "cursor",
709
+ event_type: eventType,
710
+ model,
711
+ tokens_in: tokensIn,
712
+ tokens_out: tokensOut,
713
+ cost_usd: costUsd,
714
+ occurred_at: occurredAt,
715
+ metadata: {
716
+ cursor_session_id: null,
717
+ ...cursorWorkspaceMetadata(dbPath),
718
+ cost_model: costModel,
719
+ scannable: false,
720
+ risk_level: "none",
721
+ },
722
+ };
723
+ if (projectId)
724
+ payload.project_id = projectId;
725
+ return payload;
726
+ }
727
+ export function mapDailyStats(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model) {
728
+ const { date, value, dbPath } = entry;
729
+ const occurredAt = `${date}T00:00:00.000Z`;
730
+ const results = [];
731
+ if (typeof value !== "object" || value === null)
732
+ return results;
733
+ const obj = value;
734
+ const tabSuggested = pick(obj, "tabSuggestedLines") ?? 0;
735
+ const tabAccepted = pick(obj, "tabAcceptedLines") ?? 0;
736
+ const composerSuggested = pick(obj, "composerSuggestedLines") ?? 0;
737
+ const composerAccepted = pick(obj, "composerAcceptedLines") ?? 0;
738
+ if (tabSuggested > 0 || tabAccepted > 0) {
739
+ results.push(buildPayload({
740
+ eventType: "completion",
741
+ tokensIn: tabSuggested,
742
+ tokensOut: tabAccepted,
743
+ costUsd: computeLineCost("completion", tabSuggested, pricing),
744
+ occurredAt,
745
+ dbPath,
746
+ model,
747
+ projectId,
748
+ }));
749
+ }
750
+ if (composerSuggested > 0 || composerAccepted > 0) {
751
+ results.push(buildPayload({
752
+ eventType: "chat",
753
+ tokensIn: composerSuggested,
754
+ tokensOut: composerAccepted,
755
+ costUsd: computeLineCost("chat", composerSuggested, pricing),
756
+ occurredAt,
757
+ dbPath,
758
+ model,
759
+ projectId,
760
+ }));
761
+ }
762
+ if (results.length > 0)
763
+ return results;
764
+ const KNOWN_NON_MODEL_KEYS = new Set(["tab", "composer", "chat", "date", "inputTokens", "outputTokens"]);
765
+ for (const [model, stats] of Object.entries(obj)) {
766
+ if (KNOWN_NON_MODEL_KEYS.has(model) || typeof stats !== "object" || stats === null)
767
+ continue;
768
+ const tokensIn = pick(stats, "inputTokens") ?? pick(stats, "promptTokens") ?? 0;
769
+ const tokensOut = pick(stats, "outputTokens") ?? pick(stats, "generatedTokens") ?? 0;
770
+ if (tokensIn === 0 && tokensOut === 0)
771
+ continue;
772
+ results.push(buildPayload({
773
+ eventType: "chat",
774
+ tokensIn,
775
+ tokensOut,
776
+ costUsd: computeTokenCost("chat", tokensIn, tokensOut, pricing),
777
+ occurredAt,
778
+ dbPath,
779
+ model,
780
+ projectId,
781
+ costModel: TOKEN_COST_MODEL,
782
+ }));
783
+ }
784
+ return results;
785
+ }
786
+ /**
787
+ * Maps Cursor’s latest-commit snapshot (`aiCodeTracking.recentCommit`) to a single commit-classified event.
788
+ * Cursor only keeps one recent commit row (overwritten on each new commit).
789
+ * Line-cost math still follows the chat-style line proxy (`computeLineCost("chat", …)`); only `event_type` differs.
790
+ */
791
+ export function mapRecentCommit(entry, projectId, pricing = DEFAULT_CURSOR_PRICING, model) {
792
+ const { value: obj, dbPath } = entry;
793
+ const occurredAt = toIsoString(obj.timestamp);
794
+ if (!occurredAt)
795
+ return null;
796
+ const la = Number(obj.linesAdded) || 0;
797
+ const ld = Number(obj.linesDeleted) || 0;
798
+ const tla = Number(obj.tabLinesAdded) || 0;
799
+ const tld = Number(obj.tabLinesDeleted) || 0;
800
+ const cla = Number(obj.composerLinesAdded) || 0;
801
+ const cld = Number(obj.composerLinesDeleted) || 0;
802
+ const linesAddedProxy = la + tla + cla;
803
+ const linesDeletedProxy = ld + tld + cld;
804
+ if (linesAddedProxy === 0 && linesDeletedProxy === 0)
805
+ return null;
806
+ const lineForCost = linesAddedProxy + linesDeletedProxy;
807
+ const costUsd = computeLineCost("chat", Math.max(lineForCost, 0), pricing);
808
+ const commitHash = obj.commitHash;
809
+ const commitMessage = obj.commitMessage;
810
+ const repoName = obj.repoName;
811
+ const branchName = obj.branchName;
812
+ const aiPct = obj.aiPercentage;
813
+ const payload = {
814
+ tool_name: "cursor",
815
+ event_type: "commit",
816
+ model: model ?? "unknown",
817
+ tokens_in: linesAddedProxy,
818
+ tokens_out: linesDeletedProxy,
819
+ cost_usd: costUsd,
820
+ occurred_at: occurredAt,
821
+ metadata: {
822
+ cursor_session_id: null,
823
+ ...cursorWorkspaceMetadata(dbPath),
824
+ cost_model: LINE_COST_MODEL,
825
+ source: "recent_commit",
826
+ commit_hash: typeof commitHash === "string" ? commitHash : undefined,
827
+ commit_message: typeof commitMessage === "string" ? commitMessage : undefined,
828
+ repo_name: typeof repoName === "string" ? repoName : undefined,
829
+ branch_name: typeof branchName === "string" ? branchName : undefined,
830
+ ai_percentage: typeof aiPct === "number"
831
+ ? aiPct
832
+ : typeof aiPct === "string"
833
+ ? parseFloat(aiPct) || undefined
834
+ : undefined,
835
+ scannable: false,
836
+ risk_level: "none",
837
+ },
838
+ };
839
+ if (projectId)
840
+ payload.project_id = projectId;
841
+ return payload;
842
+ }
843
+ export function mapEvent(row, workspacePath, projectId, pricing = DEFAULT_CURSOR_PRICING) {
844
+ const occurredAt = toIsoString(row.timestamp);
845
+ if (!occurredAt)
846
+ return null;
847
+ const model = row.model;
848
+ if (!model)
849
+ return null;
850
+ const eventType = row.type === 1 ? "chat" : "completion";
851
+ const tokensIn = row.promptTokens ?? 0;
852
+ const tokensOut = row.generatedTokens ?? 0;
853
+ const payload = {
854
+ tool_name: "cursor",
855
+ event_type: eventType,
856
+ model,
857
+ tokens_in: tokensIn,
858
+ tokens_out: tokensOut,
859
+ cost_usd: computeTokenCost(eventType, tokensIn, tokensOut, pricing),
860
+ occurred_at: occurredAt,
861
+ metadata: {
862
+ cursor_session_id: row.sessionId ?? row.requestId ?? null,
863
+ ...cursorWorkspaceMetadataFromStorageDir(workspacePath),
864
+ cost_model: TOKEN_COST_MODEL,
865
+ scannable: false,
866
+ risk_level: "none",
867
+ },
868
+ };
869
+ if (projectId)
870
+ payload.project_id = projectId;
871
+ return payload;
872
+ }
873
+ export function mapTranscriptTurn(turn, projectId, pricing = DEFAULT_CURSOR_PRICING, model) {
874
+ const payload = {
875
+ tool_name: "cursor",
876
+ event_type: "chat",
877
+ model: model ?? "unknown",
878
+ tokens_in: turn.tokensIn,
879
+ tokens_out: turn.tokensOut,
880
+ cost_usd: computeTokenCost("chat", turn.tokensIn, turn.tokensOut, pricing),
881
+ occurred_at: turn.occurredAt,
882
+ metadata: {
883
+ session_id: turn.turnId,
884
+ cursor_session_id: turn.sessionId,
885
+ workspace: turn.workspacePath ?? turn.filePath,
886
+ cost_model: TRANSCRIPT_COST_MODEL,
887
+ scannable: true,
888
+ risk_level: turn.riskLevel,
889
+ risk_categories: turn.riskCategories,
890
+ risk_score: turn.riskScore,
891
+ transcript_source: "agent_transcript",
892
+ composer_name: turn.composerName ?? undefined,
893
+ prompt_text: turn.promptText || undefined,
894
+ assistant_text: turn.assistantText || undefined,
895
+ },
896
+ };
897
+ if (projectId)
898
+ payload.project_id = projectId;
899
+ return payload;
900
+ }