getprismo 0.1.26 → 0.1.28

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.
@@ -0,0 +1,641 @@
1
+ module.exports = function createCursorSessions(deps) {
2
+ const { fs, os, path, estimateTokens } = deps;
3
+
4
+ function getCursorHome() {
5
+ return process.env.PRISMO_CURSOR_HOME || path.join(os.homedir(), ".cursor");
6
+ }
7
+ function getCursorAppSupport() {
8
+ return process.env.PRISMO_CURSOR_APP_SUPPORT || path.join(os.homedir(), "Library", "Application Support", "Cursor");
9
+ }
10
+ function getAiTrackingDbPath() {
11
+ return path.join(getCursorHome(), "ai-tracking", "ai-code-tracking.db");
12
+ }
13
+ function getGlobalStateDbPath() {
14
+ return path.join(getCursorAppSupport(), "User", "globalStorage", "state.vscdb");
15
+ }
16
+ function getIdeStatePath() {
17
+ return path.join(getCursorHome(), "ide_state.json");
18
+ }
19
+
20
+ let sqlite3Available = null;
21
+ let spawnSyncFn = null;
22
+
23
+ function getSpawnSync() {
24
+ if (!spawnSyncFn) {
25
+ spawnSyncFn = require("child_process").spawnSync;
26
+ }
27
+ return spawnSyncFn;
28
+ }
29
+
30
+ function isSqlite3Available() {
31
+ if (sqlite3Available !== null) return sqlite3Available;
32
+ try {
33
+ const result = getSpawnSync()("sqlite3", ["--version"], { timeout: 3000, stdio: "pipe" });
34
+ sqlite3Available = result.status === 0;
35
+ } catch {
36
+ sqlite3Available = false;
37
+ }
38
+ return sqlite3Available;
39
+ }
40
+
41
+ function querySqlite(dbPath, sql) {
42
+ if (!isSqlite3Available()) return [];
43
+ if (!fs.existsSync(dbPath)) return [];
44
+ try {
45
+ const result = getSpawnSync()("sqlite3", ["-json", dbPath, sql], {
46
+ timeout: 10000,
47
+ stdio: "pipe",
48
+ maxBuffer: 8 * 1024 * 1024,
49
+ });
50
+ if (result.status !== 0) return [];
51
+ const output = (result.stdout || "").toString().trim();
52
+ if (!output) return [];
53
+ return JSON.parse(output);
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ function querySqliteCsv(dbPath, sql) {
60
+ if (!isSqlite3Available()) return [];
61
+ if (!fs.existsSync(dbPath)) return [];
62
+ try {
63
+ const result = getSpawnSync()("sqlite3", ["-header", "-csv", dbPath, sql], {
64
+ timeout: 10000,
65
+ stdio: "pipe",
66
+ maxBuffer: 8 * 1024 * 1024,
67
+ });
68
+ if (result.status !== 0) return [];
69
+ const output = (result.stdout || "").toString().trim();
70
+ if (!output) return [];
71
+ const lines = output.split(/\r?\n/);
72
+ if (lines.length < 2) return [];
73
+ const headers = parseCsvLine(lines[0]);
74
+ return lines.slice(1).map((line) => {
75
+ const values = parseCsvLine(line);
76
+ const row = {};
77
+ headers.forEach((header, i) => { row[header] = values[i] || ""; });
78
+ return row;
79
+ });
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ function parseCsvLine(line) {
86
+ const values = [];
87
+ let current = "";
88
+ let inQuotes = false;
89
+ for (let i = 0; i < line.length; i++) {
90
+ const ch = line[i];
91
+ if (inQuotes) {
92
+ if (ch === '"' && line[i + 1] === '"') {
93
+ current += '"';
94
+ i++;
95
+ } else if (ch === '"') {
96
+ inQuotes = false;
97
+ } else {
98
+ current += ch;
99
+ }
100
+ } else if (ch === '"') {
101
+ inQuotes = true;
102
+ } else if (ch === ",") {
103
+ values.push(current);
104
+ current = "";
105
+ } else {
106
+ current += ch;
107
+ }
108
+ }
109
+ values.push(current);
110
+ return values;
111
+ }
112
+
113
+ function queryDb(dbPath, sql) {
114
+ const rows = querySqlite(dbPath, sql);
115
+ if (rows.length) return rows;
116
+ return querySqliteCsv(dbPath, sql);
117
+ }
118
+
119
+ function getCursorScoredCommits(limit = 50) {
120
+ const sql = `SELECT branchName, commitHash, commitMessage, commitDate,
121
+ linesAdded, linesDeleted, tabLinesAdded, tabLinesDeleted,
122
+ composerLinesAdded, composerLinesDeleted,
123
+ humanLinesAdded, humanLinesDeleted,
124
+ blankLinesAdded, blankLinesDeleted,
125
+ v1AiPercentage, v2AiPercentage, scoredAt
126
+ FROM scored_commits ORDER BY scoredAt DESC LIMIT ${limit}`;
127
+ return queryDb(getAiTrackingDbPath(), sql).map((row) => ({
128
+ branchName: row.branchName || "",
129
+ commitHash: row.commitHash || "",
130
+ commitMessage: row.commitMessage || "",
131
+ commitDate: row.commitDate || "",
132
+ linesAdded: Number(row.linesAdded) || 0,
133
+ linesDeleted: Number(row.linesDeleted) || 0,
134
+ tabLinesAdded: Number(row.tabLinesAdded) || 0,
135
+ tabLinesDeleted: Number(row.tabLinesDeleted) || 0,
136
+ composerLinesAdded: Number(row.composerLinesAdded) || 0,
137
+ composerLinesDeleted: Number(row.composerLinesDeleted) || 0,
138
+ humanLinesAdded: Number(row.humanLinesAdded) || 0,
139
+ humanLinesDeleted: Number(row.humanLinesDeleted) || 0,
140
+ blankLinesAdded: Number(row.blankLinesAdded) || 0,
141
+ blankLinesDeleted: Number(row.blankLinesDeleted) || 0,
142
+ v1AiPercentage: row.v1AiPercentage || "",
143
+ v2AiPercentage: row.v2AiPercentage || "",
144
+ scoredAt: Number(row.scoredAt) || 0,
145
+ }));
146
+ }
147
+
148
+ function getCursorAiCodeHashes(limit = 100) {
149
+ const sql = `SELECT hash, source, fileExtension, fileName, requestId,
150
+ conversationId, timestamp, model, createdAt
151
+ FROM ai_code_hashes ORDER BY createdAt DESC LIMIT ${limit}`;
152
+ return queryDb(getAiTrackingDbPath(), sql).map((row) => ({
153
+ hash: row.hash || "",
154
+ source: row.source || "",
155
+ fileExtension: row.fileExtension || "",
156
+ fileName: row.fileName || "",
157
+ requestId: row.requestId || "",
158
+ conversationId: row.conversationId || "",
159
+ timestamp: Number(row.timestamp) || 0,
160
+ model: row.model || "",
161
+ createdAt: Number(row.createdAt) || 0,
162
+ }));
163
+ }
164
+
165
+ function getCursorConversationSummaries(limit = 50) {
166
+ const sql = `SELECT conversationId, title, tldr, overview, summaryBullets,
167
+ model, mode, updatedAt
168
+ FROM conversation_summaries ORDER BY updatedAt DESC LIMIT ${limit}`;
169
+ return queryDb(getAiTrackingDbPath(), sql).map((row) => ({
170
+ conversationId: row.conversationId || "",
171
+ title: row.title || "",
172
+ tldr: row.tldr || "",
173
+ overview: row.overview || "",
174
+ summaryBullets: row.summaryBullets || "",
175
+ model: row.model || "",
176
+ mode: row.mode || "",
177
+ updatedAt: Number(row.updatedAt) || 0,
178
+ }));
179
+ }
180
+
181
+ function getCursorTrackedFileContent(limit = 50) {
182
+ const sql = `SELECT gitPath, conversationId, model, fileExtension, createdAt,
183
+ length(content) as contentLength
184
+ FROM tracked_file_content ORDER BY createdAt DESC LIMIT ${limit}`;
185
+ return queryDb(getAiTrackingDbPath(), sql).map((row) => ({
186
+ gitPath: row.gitPath || "",
187
+ conversationId: row.conversationId || "",
188
+ model: row.model || "",
189
+ fileExtension: row.fileExtension || "",
190
+ createdAt: Number(row.createdAt) || 0,
191
+ contentLength: Number(row.contentLength) || 0,
192
+ }));
193
+ }
194
+
195
+ function getCursorDeletedFiles(limit = 50) {
196
+ const sql = `SELECT gitPath, composerId, conversationId, model, deletedAt
197
+ FROM ai_deleted_files ORDER BY deletedAt DESC LIMIT ${limit}`;
198
+ return queryDb(getAiTrackingDbPath(), sql).map((row) => ({
199
+ gitPath: row.gitPath || "",
200
+ composerId: row.composerId || "",
201
+ conversationId: row.conversationId || "",
202
+ model: row.model || "",
203
+ deletedAt: Number(row.deletedAt) || 0,
204
+ }));
205
+ }
206
+
207
+ function getCursorComposerHeaders() {
208
+ const rows = queryDb(getGlobalStateDbPath(),
209
+ "SELECT value FROM ItemTable WHERE key = 'composer.composerHeaders'");
210
+ if (!rows.length) return [];
211
+ try {
212
+ const raw = rows[0].value || rows[0];
213
+ const data = typeof raw === "string" ? JSON.parse(raw) : raw;
214
+ return (data.allComposers || []).map((c) => ({
215
+ composerId: c.composerId || "",
216
+ createdAt: Number(c.createdAt) || 0,
217
+ mode: c.unifiedMode || c.forceMode || "",
218
+ linesAdded: Number(c.totalLinesAdded) || 0,
219
+ linesRemoved: Number(c.totalLinesRemoved) || 0,
220
+ isArchived: Boolean(c.isArchived),
221
+ isDraft: Boolean(c.isDraft),
222
+ isWorktree: Boolean(c.isWorktree),
223
+ isSpec: Boolean(c.isSpec),
224
+ numSubComposers: Number(c.numSubComposers) || 0,
225
+ workspaceId: c.workspaceIdentifier?.id || "",
226
+ }));
227
+ } catch {
228
+ return [];
229
+ }
230
+ }
231
+
232
+ function getCursorWorkspaceForProject(projectPath) {
233
+ const wsDir = path.join(getCursorAppSupport(), "User", "workspaceStorage");
234
+ if (!fs.existsSync(wsDir)) return null;
235
+ try {
236
+ const entries = fs.readdirSync(wsDir);
237
+ for (const entry of entries) {
238
+ const wsJson = path.join(wsDir, entry, "workspace.json");
239
+ if (!fs.existsSync(wsJson)) continue;
240
+ try {
241
+ const data = JSON.parse(fs.readFileSync(wsJson, "utf8"));
242
+ const folder = decodeURIComponent((data.folder || "").replace("file://", ""));
243
+ if (folder && path.resolve(folder) === path.resolve(projectPath)) {
244
+ return { workspaceId: entry, folder };
245
+ }
246
+ } catch {
247
+ continue;
248
+ }
249
+ }
250
+ } catch {
251
+ // workspace dir not readable
252
+ }
253
+ return null;
254
+ }
255
+
256
+ function getCursorIdeState() {
257
+ if (!fs.existsSync(getIdeStatePath())) return null;
258
+ try {
259
+ return JSON.parse(fs.readFileSync(getIdeStatePath(), "utf8"));
260
+ } catch {
261
+ return null;
262
+ }
263
+ }
264
+
265
+ function getAiTrackingDbStats() {
266
+ if (!fs.existsSync(getAiTrackingDbPath())) return null;
267
+ const counts = {};
268
+ const tables = ["ai_code_hashes", "conversation_summaries", "scored_commits", "tracked_file_content", "ai_deleted_files"];
269
+ for (const table of tables) {
270
+ const rows = queryDb(getAiTrackingDbPath(), `SELECT COUNT(*) as count FROM ${table}`);
271
+ counts[table] = rows.length ? Number(rows[0].count) || 0 : 0;
272
+ }
273
+ return counts;
274
+ }
275
+
276
+ function analyzeCursorSessions(options = {}) {
277
+ const limit = options.limit || 20;
278
+ const cwd = options.cwd || process.cwd();
279
+
280
+ const composers = getCursorComposerHeaders();
281
+ const summaries = getCursorConversationSummaries(limit);
282
+ const scoredCommits = getCursorScoredCommits(limit);
283
+ const aiHashes = getCursorAiCodeHashes(limit);
284
+ const trackedFiles = getCursorTrackedFileContent(limit);
285
+ const deletedFiles = getCursorDeletedFiles(limit);
286
+ const dbStats = getAiTrackingDbStats();
287
+ const workspace = getCursorWorkspaceForProject(cwd);
288
+
289
+ const summaryMap = new Map();
290
+ for (const s of summaries) {
291
+ summaryMap.set(s.conversationId, s);
292
+ }
293
+
294
+ const sessions = composers
295
+ .sort((a, b) => b.createdAt - a.createdAt)
296
+ .slice(0, limit)
297
+ .map((composer) => {
298
+ const summary = summaryMap.get(composer.composerId);
299
+ return {
300
+ tool: "cursor",
301
+ sessionId: composer.composerId,
302
+ title: summary?.title || "",
303
+ tldr: summary?.tldr || "",
304
+ model: summary?.model || "",
305
+ mode: composer.mode || summary?.mode || "",
306
+ createdAt: composer.createdAt ? new Date(composer.createdAt).toISOString() : null,
307
+ updatedAt: summary?.updatedAt ? new Date(summary.updatedAt).toISOString() : null,
308
+ linesAdded: composer.linesAdded,
309
+ linesRemoved: composer.linesRemoved,
310
+ isArchived: composer.isArchived,
311
+ isDraft: composer.isDraft,
312
+ isWorktree: composer.isWorktree,
313
+ numSubComposers: composer.numSubComposers,
314
+ workspaceId: composer.workspaceId,
315
+ confidence: "cursor-metadata",
316
+ };
317
+ });
318
+
319
+ const totalLinesAdded = scoredCommits.reduce((sum, c) => sum + c.linesAdded, 0);
320
+ const totalComposerLinesAdded = scoredCommits.reduce((sum, c) => sum + c.composerLinesAdded, 0);
321
+ const totalTabLinesAdded = scoredCommits.reduce((sum, c) => sum + c.tabLinesAdded, 0);
322
+ const totalHumanLinesAdded = scoredCommits.reduce((sum, c) => sum + c.humanLinesAdded, 0);
323
+ const aiLinesAdded = totalComposerLinesAdded + totalTabLinesAdded;
324
+ const aiAuthorshipPercent = totalLinesAdded > 0 ? Math.round((aiLinesAdded / totalLinesAdded) * 100) : 0;
325
+
326
+ const modelDistribution = {};
327
+ for (const h of aiHashes) {
328
+ if (h.model) modelDistribution[h.model] = (modelDistribution[h.model] || 0) + 1;
329
+ }
330
+ const modeDistribution = {};
331
+ for (const c of composers) {
332
+ if (c.mode) modeDistribution[c.mode] = (modeDistribution[c.mode] || 0) + 1;
333
+ }
334
+
335
+ const aiGeneratedFiles = trackedFiles.map((f) => ({
336
+ gitPath: f.gitPath,
337
+ model: f.model,
338
+ contentLength: f.contentLength,
339
+ estimatedTokens: estimateTokens(f.contentLength),
340
+ createdAt: f.createdAt ? new Date(f.createdAt).toISOString() : null,
341
+ }));
342
+
343
+ const aiDeletedFileList = deletedFiles.map((f) => ({
344
+ gitPath: f.gitPath,
345
+ model: f.model,
346
+ deletedAt: f.deletedAt ? new Date(f.deletedAt).toISOString() : null,
347
+ }));
348
+
349
+ return {
350
+ generatedAt: new Date().toISOString(),
351
+ scannedPath: cwd,
352
+ tool: "cursor",
353
+ dbAvailable: fs.existsSync(getAiTrackingDbPath()),
354
+ sqlite3Available: isSqlite3Available(),
355
+ dbStats,
356
+ workspace,
357
+ sessions,
358
+ scoredCommits: scoredCommits.map((c) => ({
359
+ ...c,
360
+ scoredAt: c.scoredAt ? new Date(c.scoredAt).toISOString() : null,
361
+ })),
362
+ aiAuthorship: {
363
+ totalCommits: scoredCommits.length,
364
+ totalLinesAdded,
365
+ aiLinesAdded,
366
+ humanLinesAdded: totalHumanLinesAdded,
367
+ composerLinesAdded: totalComposerLinesAdded,
368
+ tabLinesAdded: totalTabLinesAdded,
369
+ aiAuthorshipPercent,
370
+ },
371
+ aiGeneratedFiles,
372
+ aiDeletedFiles: aiDeletedFileList,
373
+ modelDistribution,
374
+ modeDistribution,
375
+ totalSessions: composers.length,
376
+ activeSessions: composers.filter((c) => !c.isArchived).length,
377
+ };
378
+ }
379
+
380
+ function buildCursorSessionTimeline(cursorData) {
381
+ const events = [];
382
+
383
+ for (const commit of (cursorData.scoredCommits || []).slice(0, 10)) {
384
+ const aiLines = (commit.composerLinesAdded || 0) + (commit.tabLinesAdded || 0);
385
+ const totalLines = commit.linesAdded || 0;
386
+ const pct = totalLines > 0 ? Math.round((aiLines / totalLines) * 100) : 0;
387
+ if (totalLines > 0) {
388
+ events.push({
389
+ timestamp: commit.scoredAt || commit.commitDate || null,
390
+ type: pct >= 80 ? "high-ai-authorship" : pct >= 40 ? "mixed-authorship" : "human-authorship",
391
+ label: `Commit: ${(commit.commitMessage || commit.commitHash || "").slice(0, 60)}`,
392
+ detail: `+${totalLines}/-${commit.linesDeleted || 0} lines, ${pct}% AI (composer: ${commit.composerLinesAdded}, tab: ${commit.tabLinesAdded}, human: ${commit.humanLinesAdded})`,
393
+ });
394
+ }
395
+ }
396
+
397
+ for (const file of (cursorData.aiGeneratedFiles || []).slice(0, 5)) {
398
+ events.push({
399
+ timestamp: file.createdAt || null,
400
+ type: "ai-generated-file",
401
+ label: `AI-generated file tracked`,
402
+ detail: `${file.gitPath} (${file.model || "unknown model"}, ~${file.estimatedTokens} tokens)`,
403
+ });
404
+ }
405
+
406
+ for (const file of (cursorData.aiDeletedFiles || []).slice(0, 5)) {
407
+ events.push({
408
+ timestamp: file.deletedAt || null,
409
+ type: "ai-deleted-file",
410
+ label: `AI-generated file deleted`,
411
+ detail: `${file.gitPath} (${file.model || "unknown model"})`,
412
+ });
413
+ }
414
+
415
+ return events.sort((a, b) => {
416
+ const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
417
+ const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
418
+ return ta - tb;
419
+ }).slice(-20);
420
+ }
421
+
422
+ function buildCursorDiagnosis(cursorData) {
423
+ const drivers = [];
424
+ const recommendations = [];
425
+
426
+ const authorship = cursorData.aiAuthorship || {};
427
+ if (authorship.aiAuthorshipPercent >= 80) {
428
+ drivers.push({
429
+ type: "high-ai-authorship",
430
+ value: `${authorship.aiAuthorshipPercent}%`,
431
+ message: "Most committed code is AI-generated; review coverage may need extra attention.",
432
+ });
433
+ }
434
+ if (authorship.tabLinesAdded > authorship.composerLinesAdded && authorship.tabLinesAdded > 50) {
435
+ drivers.push({
436
+ type: "tab-completion-heavy",
437
+ value: `${authorship.tabLinesAdded} lines`,
438
+ message: "Tab completions contribute more code than composer/agent sessions.",
439
+ });
440
+ }
441
+
442
+ const totalSessions = cursorData.totalSessions || 0;
443
+ const activeSessions = cursorData.activeSessions || 0;
444
+ if (totalSessions > 50 && activeSessions > 40) {
445
+ drivers.push({
446
+ type: "session-accumulation",
447
+ value: `${totalSessions} total, ${activeSessions} active`,
448
+ message: "Many non-archived sessions; old context may accumulate.",
449
+ });
450
+ recommendations.push("Archive old Cursor composer sessions to reduce context clutter.");
451
+ }
452
+
453
+ const deletedCount = (cursorData.aiDeletedFiles || []).length;
454
+ const generatedCount = (cursorData.aiGeneratedFiles || []).length;
455
+ if (deletedCount > 3) {
456
+ drivers.push({
457
+ type: "ai-churn",
458
+ value: `${deletedCount} files deleted`,
459
+ message: "AI-generated files are being created and deleted, suggesting trial-and-error patterns.",
460
+ });
461
+ recommendations.push("Use context packs and firewall rules to scope AI tasks more precisely.");
462
+ }
463
+
464
+ if (generatedCount > 0) {
465
+ recommendations.push("Review AI-generated tracked files — some may be leaking into context.");
466
+ }
467
+ if (authorship.totalCommits > 0) {
468
+ recommendations.push("Use npx getprismo cursor authorship to track AI vs human code ratios over time.");
469
+ }
470
+ if (!recommendations.length) {
471
+ recommendations.push("npx getprismo doctor to optimize repo for Cursor sessions.");
472
+ }
473
+
474
+ return {
475
+ drivers: drivers.slice(0, 5),
476
+ recommendations: Array.from(new Set(recommendations)).slice(0, 4),
477
+ };
478
+ }
479
+
480
+ function renderCursorTerminal(cursorData, command) {
481
+ const lines = [];
482
+ lines.push("");
483
+ lines.push("Prismo Cursor Sessions");
484
+ lines.push("");
485
+
486
+ if (!cursorData.dbAvailable) {
487
+ lines.push("No Cursor AI tracking database found.");
488
+ lines.push("Cursor stores tracking data at ~/.cursor/ai-tracking/ai-code-tracking.db");
489
+ lines.push("Make sure Cursor is installed and has been used at least once.");
490
+ return lines.join("\n");
491
+ }
492
+ if (!cursorData.sqlite3Available) {
493
+ lines.push("sqlite3 command not found. Install sqlite3 to read Cursor tracking data.");
494
+ return lines.join("\n");
495
+ }
496
+
497
+ if (command === "list") {
498
+ lines.push(`Total sessions: ${cursorData.totalSessions} (${cursorData.activeSessions} active)`);
499
+ lines.push(`Mode: ${Object.entries(cursorData.modeDistribution).map(([k, v]) => `${k}: ${v}`).join(", ") || "unknown"}`);
500
+ lines.push("");
501
+ const sessions = cursorData.sessions.slice(0, 15);
502
+ sessions.forEach((session, i) => {
503
+ const title = session.title ? ` "${session.title.slice(0, 50)}"` : "";
504
+ const model = session.model ? ` ${session.model}` : "";
505
+ lines.push(`${i + 1}. [${session.mode || "?"}]${title}${model}`);
506
+ lines.push(` +${session.linesAdded}/-${session.linesRemoved} ${session.createdAt || "unknown date"}${session.isArchived ? " (archived)" : ""}`);
507
+ });
508
+ return lines.join("\n");
509
+ }
510
+
511
+ if (command === "authorship") {
512
+ const auth = cursorData.aiAuthorship;
513
+ lines.push("AI Authorship (from Cursor scored commits)");
514
+ lines.push("");
515
+ lines.push(`Commits analyzed: ${auth.totalCommits}`);
516
+ lines.push(`Total lines added: ${auth.totalLinesAdded}`);
517
+ lines.push("");
518
+ lines.push(` Composer (agent): ${auth.composerLinesAdded} lines`);
519
+ lines.push(` Tab completions: ${auth.tabLinesAdded} lines`);
520
+ lines.push(` Human: ${auth.humanLinesAdded} lines`);
521
+ lines.push("--------------------------------------------------");
522
+ lines.push(` AI authorship: ${auth.aiAuthorshipPercent}%`);
523
+ lines.push("");
524
+ if (cursorData.scoredCommits.length) {
525
+ lines.push("Recent commits:");
526
+ cursorData.scoredCommits.slice(0, 8).forEach((c) => {
527
+ const aiLines = (c.composerLinesAdded || 0) + (c.tabLinesAdded || 0);
528
+ const pct = c.linesAdded > 0 ? Math.round((aiLines / c.linesAdded) * 100) : 0;
529
+ const msg = (c.commitMessage || c.commitHash || "").slice(0, 50);
530
+ lines.push(` ${pct}% AI +${c.linesAdded}/-${c.linesDeleted} ${msg}`);
531
+ });
532
+ }
533
+ return lines.join("\n");
534
+ }
535
+
536
+ if (command === "timeline") {
537
+ const timeline = buildCursorSessionTimeline(cursorData);
538
+ lines.push("Cursor Session Timeline");
539
+ lines.push("");
540
+ if (!timeline.length) {
541
+ lines.push("No timeline events found. Cursor needs scored commits or tracked files for timeline data.");
542
+ } else {
543
+ timeline.forEach((event) => {
544
+ const when = event.timestamp ? new Date(event.timestamp).toLocaleString() : "unknown";
545
+ lines.push(`${when} [${event.type}]`);
546
+ lines.push(` ${event.label}`);
547
+ lines.push(` ${event.detail}`);
548
+ lines.push("");
549
+ });
550
+ }
551
+ const diagnosis = buildCursorDiagnosis(cursorData);
552
+ if (diagnosis.drivers.length) {
553
+ lines.push("Signals:");
554
+ diagnosis.drivers.forEach((d) => lines.push(`- ${d.message}`));
555
+ }
556
+ lines.push("");
557
+ lines.push("Suggested Actions:");
558
+ diagnosis.recommendations.forEach((r) => lines.push(`- ${r}`));
559
+ return lines.join("\n");
560
+ }
561
+
562
+ if (command === "files") {
563
+ lines.push("AI-Generated Files (tracked by Cursor)");
564
+ lines.push("");
565
+ if (!cursorData.aiGeneratedFiles.length && !cursorData.aiDeletedFiles.length) {
566
+ lines.push("No AI-generated files tracked yet.");
567
+ return lines.join("\n");
568
+ }
569
+ if (cursorData.aiGeneratedFiles.length) {
570
+ lines.push(`Tracked: ${cursorData.aiGeneratedFiles.length}`);
571
+ cursorData.aiGeneratedFiles.forEach((f) => {
572
+ lines.push(` ${f.gitPath} (${f.model || "?"}, ~${f.estimatedTokens} tokens)`);
573
+ });
574
+ }
575
+ if (cursorData.aiDeletedFiles.length) {
576
+ lines.push("");
577
+ lines.push(`Deleted: ${cursorData.aiDeletedFiles.length}`);
578
+ cursorData.aiDeletedFiles.forEach((f) => {
579
+ lines.push(` ${f.gitPath} (${f.model || "?"}, ${f.deletedAt || "?"})`);
580
+ });
581
+ }
582
+ return lines.join("\n");
583
+ }
584
+
585
+ // Default: summary view
586
+ lines.push(`Sessions: ${cursorData.totalSessions} total (${cursorData.activeSessions} active)`);
587
+ lines.push(`Modes: ${Object.entries(cursorData.modeDistribution).map(([k, v]) => `${k}: ${v}`).join(", ") || "none"}`);
588
+ if (Object.keys(cursorData.modelDistribution).length) {
589
+ lines.push(`Models: ${Object.entries(cursorData.modelDistribution).map(([k, v]) => `${k}: ${v}`).join(", ")}`);
590
+ }
591
+ lines.push("");
592
+ const auth = cursorData.aiAuthorship;
593
+ if (auth.totalCommits > 0) {
594
+ lines.push("AI Authorship");
595
+ lines.push(` ${auth.totalCommits} commits analyzed, ${auth.aiAuthorshipPercent}% AI-authored`);
596
+ lines.push(` Composer: ${auth.composerLinesAdded} Tab: ${auth.tabLinesAdded} Human: ${auth.humanLinesAdded} lines`);
597
+ } else {
598
+ lines.push("AI Authorship: no scored commits found yet");
599
+ }
600
+ lines.push("");
601
+
602
+ if (cursorData.aiGeneratedFiles.length) {
603
+ lines.push(`AI-generated files: ${cursorData.aiGeneratedFiles.length} tracked`);
604
+ }
605
+ if (cursorData.aiDeletedFiles.length) {
606
+ lines.push(`AI-deleted files: ${cursorData.aiDeletedFiles.length} (churn)`);
607
+ }
608
+ if (cursorData.dbStats) {
609
+ lines.push("");
610
+ lines.push("Tracking DB:");
611
+ lines.push(` Code hashes: ${cursorData.dbStats.ai_code_hashes} Conversations: ${cursorData.dbStats.conversation_summaries} Commits: ${cursorData.dbStats.scored_commits}`);
612
+ }
613
+ lines.push("");
614
+ const diagnosis = buildCursorDiagnosis(cursorData);
615
+ if (diagnosis.drivers.length) {
616
+ lines.push("Signals:");
617
+ diagnosis.drivers.forEach((d) => lines.push(`- ${d.message}`));
618
+ lines.push("");
619
+ }
620
+ lines.push("Next:");
621
+ diagnosis.recommendations.slice(0, 3).forEach((r) => lines.push(`- ${r}`));
622
+ return lines.join("\n");
623
+ }
624
+
625
+ return {
626
+ analyzeCursorSessions,
627
+ buildCursorDiagnosis,
628
+ buildCursorSessionTimeline,
629
+ getAiTrackingDbStats,
630
+ getCursorAiCodeHashes,
631
+ getCursorComposerHeaders,
632
+ getCursorConversationSummaries,
633
+ getCursorDeletedFiles,
634
+ getCursorIdeState,
635
+ getCursorScoredCommits,
636
+ getCursorTrackedFileContent,
637
+ getCursorWorkspaceForProject,
638
+ isSqlite3Available,
639
+ renderCursorTerminal,
640
+ };
641
+ };