getprismo 0.1.27 → 0.1.29

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/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # prismodev
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/getprismo.svg)](https://www.npmjs.com/package/getprismo)
4
+ [![npm downloads](https://img.shields.io/npm/dw/getprismo.svg)](https://www.npmjs.com/package/getprismo)
5
+ [![license: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
6
+
3
7
  local ai coding cost control. one command to diagnose token waste, fix it, and prove the improvement.
4
8
 
5
9
  ```bash
@@ -545,9 +549,49 @@ it only creates new files and recommendations. you decide what to apply.
545
549
 
546
550
  ---
547
551
 
552
+ ## cursor session tracking
553
+
554
+ prismodev now reads cursor's local sqlite databases directly. cursor stores data differently from claude code and codex, no jsonl session logs, but it has its own tracking databases with unique data.
555
+
556
+ ```bash
557
+ npx getprismo cursor # summary of all cursor sessions
558
+ npx getprismo cursor list # list composer sessions with modes and models
559
+ npx getprismo cursor authorship # ai vs human code authorship from scored commits
560
+ npx getprismo cursor timeline # timeline of ai activity across commits and files
561
+ npx getprismo cursor files # ai-generated and ai-deleted file tracking
562
+ npx getprismo cursor --json # machine-readable output
563
+ ```
564
+
565
+ cursor tracks something claude code and codex can't: per-commit ai authorship. every commit is scored with how many lines came from composer (agent), tab completions, and human typing. prismodev surfaces this as an authorship percentage.
566
+
567
+ ```
568
+ AI Authorship (from Cursor scored commits)
569
+
570
+ Commits analyzed: 47
571
+ Total lines added: 3812
572
+
573
+ Composer (agent): 2104 lines
574
+ Tab completions: 891 lines
575
+ Human: 817 lines
576
+ --------------------------------------------------
577
+ AI authorship: 78%
578
+ ```
579
+
580
+ prismodev also tracks ai-generated files cursor is watching, files cursor deleted, conversation summaries, and model usage distribution across sessions.
581
+
582
+ what cursor can't do vs claude code: cursor doesn't expose per-message token counts, exact api costs, or full conversation transcripts in its local data. that means live context pressure, loop detection, exact cost breakdowns, cache savings analysis, and shield don't apply the same way. this is a cursor limitation. prismodev gets about 60-65% feature parity with cursor compared to claude code/codex.
583
+
584
+ what cursor gives you that the others don't: ai authorship percentages per commit, tab vs composer vs human line counts, conversation summaries with tldr, and ai-generated file tracking with churn detection.
585
+
586
+ the `prismo_cursor_sessions` mcp tool exposes all of this to compatible agents.
587
+
588
+ `scan` and `doctor` now detect cursor's tracking database automatically and flag ai-generated files still present in the repo.
589
+
590
+ ---
591
+
548
592
  ## how watch catches waste live
549
593
 
550
- watch reads local session logs from codex and claude code. it detects:
594
+ watch reads local session logs from codex, claude code, and cursor. it detects:
551
595
 
552
596
  | signal | what it means |
553
597
  |--------|--------------|
@@ -623,6 +667,7 @@ no install needed. npx runs it directly.
623
667
  | `watch` | live session monitoring with warnings |
624
668
  | `cc` | claude code cost breakdown |
625
669
  | `cc timeline` | session reconstruction with events |
670
+ | `cursor` | cursor session tracking and ai authorship |
626
671
  | `scan --usage` | full repo scan with local usage data |
627
672
  | `scan --optimizer-fit` | recommend which token-optimization path fits your repo/session |
628
673
  | `scan --report-card` | shortest decision-layer summary |
@@ -677,6 +722,7 @@ npx getprismo watch --rescue --json # include rescuePrompt in JSON
677
722
  npx getprismo watch --once --redact-paths # hide local paths
678
723
  npx getprismo watch codex # only codex sessions
679
724
  npx getprismo watch claude # only claude code sessions
725
+ npx getprismo watch cursor # only cursor sessions
680
726
  ```
681
727
 
682
728
  ### shield mode
@@ -708,6 +754,7 @@ npx getprismo mcp /path/to/repo
708
754
  - `prismo_context_pack`
709
755
  - `prismo_firewall`
710
756
  - `prismo_cc_timeline`
757
+ - `prismo_cursor_sessions`
711
758
 
712
759
  This lets an MCP-compatible agent search prior shielded test/build output, request scoped context packs, inspect token-waste signals, or coordinate multiple local agents without pasting giant logs into the conversation.
713
760
 
@@ -802,9 +849,10 @@ local logs exact when codex/claude session logs expose token fields
802
849
  prismo proxy exact usage/cost when traffic routes through prismo base url
803
850
  ```
804
851
 
805
- prismodev reads local session logs from:
852
+ prismodev reads local session data from:
806
853
  - codex: `~/.codex/sessions/**/*.jsonl`
807
854
  - claude code: `~/.claude/projects/**/*.jsonl`
855
+ - cursor: `~/.cursor/ai-tracking/ai-code-tracking.db` and `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`
808
856
 
809
857
  no api keys. no intercepted prompts. no data uploaded.
810
858
 
@@ -890,7 +938,8 @@ lib/prismo-dev/scan-path-utils.js scan ignore/path helper logic
890
938
  lib/prismo-dev/shield.js local command shield and searchable output index
891
939
  lib/prismo-dev/usage-cost.js Claude Code cost and timeline analysis
892
940
  lib/prismo-dev/usage-log-utils.js local session log parsing helpers
893
- lib/prismo-dev/usage-sessions.js local Codex/Claude session discovery
941
+ lib/prismo-dev/cursor-sessions.js Cursor SQLite session and authorship tracking
942
+ lib/prismo-dev/usage-sessions.js local Codex/Claude/Cursor session discovery
894
943
  lib/prismo-dev/usage-watch.js watch orchestration, JSON payloads, live files
895
944
  lib/prismo-dev/utils.js shared terminal/file/token helpers
896
945
  lib/prismo-dev/watch-live.js live context-pressure decisions
@@ -910,6 +959,7 @@ npx getprismo shield --help
910
959
  npx getprismo mcp --help
911
960
  npx getprismo mcp doctor
912
961
  npx getprismo cc --help
962
+ npx getprismo cursor --help
913
963
  npx getprismo scan --help
914
964
  ```
915
965
 
@@ -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
+ };
@@ -31,6 +31,7 @@ function createMcpTools(deps) {
31
31
  toDoctorJsonPayload,
32
32
  getUsageSummary,
33
33
  getClaudeCodeCostSummary,
34
+ getCursorSessionSummary,
34
35
  runOptimize,
35
36
  createOptimizeContext,
36
37
  renderStarterPrompt,
@@ -106,6 +107,11 @@ function createMcpTools(deps) {
106
107
  path: pathProperty,
107
108
  limit: limitProperty,
108
109
  }),
110
+ makeTool("prismo_cursor_sessions", "Return Cursor session data including AI authorship, conversations, and AI-generated file tracking.", {
111
+ path: pathProperty,
112
+ limit: limitProperty,
113
+ command: { type: "string", enum: ["latest", "list", "authorship", "timeline", "files"], description: "Subcommand: latest (summary), list (sessions), authorship (AI%), timeline (events), files (AI-generated)." },
114
+ }),
109
115
  ];
110
116
 
111
117
  function resolveRoot(args) {
@@ -195,6 +201,15 @@ function createMcpTools(deps) {
195
201
  }));
196
202
  }
197
203
 
204
+ if (name === "prismo_cursor_sessions") {
205
+ if (!getCursorSessionSummary) throw new Error("Cursor session support not available");
206
+ return createTextResult(getCursorSessionSummary({
207
+ cwd: target,
208
+ limit: Number(args.limit) || 20,
209
+ mode: args.command || "latest",
210
+ }));
211
+ }
212
+
198
213
  throw new Error(`Unknown MCP tool: ${name}`);
199
214
  }
200
215
 
@@ -293,6 +308,7 @@ async function runMcpDoctor(deps) {
293
308
  "prismo_context_pack",
294
309
  "prismo_firewall",
295
310
  "prismo_cc_timeline",
311
+ "prismo_cursor_sessions",
296
312
  ];
297
313
  const toolNames = tools.map((tool) => tool.name);
298
314
  const missingTools = requiredTools.filter((name) => !toolNames.includes(name));
@@ -1,6 +1,14 @@
1
1
  module.exports = function createScanDetect(deps) {
2
2
  const { fs, http, https, os, path, readIfText, estimateTokens, getClaudeSessionFiles, getCodexSessionFiles, normalizeRel } = deps;
3
3
 
4
+ let cursorSessionsModule = null;
5
+ function getCursorModule() {
6
+ if (!cursorSessionsModule) {
7
+ cursorSessionsModule = require("./cursor-sessions")({ fs, os, path, estimateTokens });
8
+ }
9
+ return cursorSessionsModule;
10
+ }
11
+
4
12
  function countJsonObjectKeys(value, keyName) {
5
13
  if (!value || typeof value !== "object") return 0;
6
14
  let count = 0;
@@ -111,10 +119,11 @@ module.exports = function createScanDetect(deps) {
111
119
  function detectAgentReadiness(root, claudeConfig, codexConfig, realUsage) {
112
120
  const claudeHome = process.env.PRISMO_CLAUDE_HOME || path.join(os.homedir(), ".claude");
113
121
  const codexHome = process.env.PRISMO_CODEX_HOME || path.join(os.homedir(), ".codex");
122
+ const cursorHome = process.env.PRISMO_CURSOR_HOME || path.join(os.homedir(), ".cursor");
114
123
  const cursorPaths = [
115
124
  path.join(root, ".cursor"),
116
125
  path.join(root, ".cursorrules"),
117
- path.join(os.homedir(), ".cursor"),
126
+ cursorHome,
118
127
  path.join(os.homedir(), ".config", "Cursor"),
119
128
  ];
120
129
  const usageSources = new Set(realUsage && realUsage.sources ? realUsage.sources : []);
@@ -140,13 +149,26 @@ module.exports = function createScanDetect(deps) {
140
149
  exactProxyTracking: "available-when-using-api-key-base-url-mode",
141
150
  recommendedMode: "prismo-proxy-for-api-mode-or-local-log-watch",
142
151
  },
143
- cursor: {
144
- detected: pathExistsAny(cursorPaths),
145
- configFiles: cursorPaths.filter((candidate) => fs.existsSync(candidate)),
146
- localLogsFound: false,
147
- exactProxyTracking: "available-only-if-configured-for-openai-compatible-base-url",
148
- recommendedMode: "repo-scan-and-prismo-proxy-when-supported",
149
- },
152
+ cursor: (() => {
153
+ const detected = pathExistsAny(cursorPaths);
154
+ const cursorMod = getCursorModule();
155
+ const dbAvailable = cursorMod.isSqlite3Available() && fs.existsSync(path.join(cursorHome, "ai-tracking", "ai-code-tracking.db"));
156
+ const dbStats = dbAvailable ? cursorMod.getAiTrackingDbStats() : null;
157
+ const workspace = detected ? cursorMod.getCursorWorkspaceForProject(root) : null;
158
+ const composers = dbAvailable ? cursorMod.getCursorComposerHeaders() : [];
159
+ return {
160
+ detected,
161
+ configFiles: cursorPaths.filter((candidate) => fs.existsSync(candidate)),
162
+ localLogsFound: dbAvailable && (dbStats ? (dbStats.ai_code_hashes + dbStats.scored_commits + dbStats.conversation_summaries) > 0 : false),
163
+ dbAvailable,
164
+ dbStats,
165
+ workspace,
166
+ totalSessions: composers.length,
167
+ activeSessions: composers.filter((c) => !c.isArchived).length,
168
+ exactProxyTracking: "available-only-if-configured-for-openai-compatible-base-url",
169
+ recommendedMode: dbAvailable ? "cursor-tracking-db-and-repo-scan" : "repo-scan-and-prismo-proxy-when-supported",
170
+ };
171
+ })(),
150
172
  localUsageLogsAvailable: Boolean((realUsage && realUsage.sessions.length) || claudeSessionFiles.length || codexSessionFiles.length),
151
173
  exactProxyTrackingAvailable: true,
152
174
  notes: [
@@ -20,6 +20,7 @@ module.exports = function createScan(deps) {
20
20
  getCodexSessionFiles,
21
21
  compactUsageSummary,
22
22
  formatTokenCount,
23
+ color,
23
24
  } = deps;
24
25
 
25
26
  const {
@@ -229,7 +230,11 @@ function renderSetupTerminal(result) {
229
230
  lines.push("Detected:");
230
231
  lines.push(`- Claude Code: ${result.detected.claudeCode.detected ? "detected" : "not detected"}; logs: ${result.detected.claudeCode.localLogsFound ? "found" : "not found"}; MCP: ${result.detected.claudeCode.mcpServers}; hooks: ${result.detected.claudeCode.hooks}`);
231
232
  lines.push(`- Codex: ${result.detected.codex.detected ? "detected" : "not detected"}; logs: ${result.detected.codex.localLogsFound ? "found" : "not found"}; MCP: ${result.detected.codex.mcpServers}`);
232
- lines.push(`- Cursor: ${result.detected.cursor.detected ? "detected" : "not detected"}`);
233
+ const cursorInfo = result.detected.cursor;
234
+ lines.push(`- Cursor: ${cursorInfo.detected ? "detected" : "not detected"}${cursorInfo.dbAvailable ? `; tracking DB: found (${cursorInfo.totalSessions} sessions, ${cursorInfo.activeSessions} active)` : ""}${cursorInfo.workspace ? `; workspace: matched` : ""}`);
235
+ if (cursorInfo.dbStats) {
236
+ lines.push(` Tracking: ${cursorInfo.dbStats.scored_commits} scored commits, ${cursorInfo.dbStats.ai_code_hashes} code hashes, ${cursorInfo.dbStats.conversation_summaries} conversation summaries`);
237
+ }
233
238
  const detectedTools = result.detected.optimizationStack.detectedTools;
234
239
  lines.push(`- Optimization tools: ${detectedTools.length ? detectedTools.join(", ") : "none detected"}`);
235
240
  lines.push("");
@@ -562,6 +567,31 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
562
567
  }
563
568
  const optimizationStack = detectOptimizationStack(root, claudeConfig, codexConfig);
564
569
  const agentReadiness = detectAgentReadiness(root, claudeConfig, codexConfig, realUsage);
570
+
571
+ if (agentReadiness.cursor.dbAvailable) {
572
+ try {
573
+ const cursorMod = require("./cursor-sessions")({ fs, os, path, estimateTokens });
574
+ const tracked = cursorMod.getCursorTrackedFileContent(20);
575
+ const cursorAiFiles = tracked.filter((f) => {
576
+ const fullPath = path.join(root, f.gitPath);
577
+ return fs.existsSync(fullPath);
578
+ });
579
+ if (cursorAiFiles.length >= 3) {
580
+ addIssue(
581
+ issues,
582
+ "medium",
583
+ "ai_generated_files",
584
+ `${cursorAiFiles.length} AI-generated files still in repo (tracked by Cursor)`,
585
+ cursorAiFiles.slice(0, 5).map((f) => `${f.gitPath} (${f.model || "unknown model"})`).join(", "),
586
+ "Review AI-generated files for quality and whether they should remain in the project.",
587
+ `AI-generated content may inflate context if agents re-read it. Run: npx getprismo cursor files`
588
+ );
589
+ }
590
+ } catch {
591
+ // Cursor data unavailable
592
+ }
593
+ }
594
+
565
595
  const toolOutputRisk = detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRiskDirs });
566
596
  const operationalNoise = detectOperationalNoise(files);
567
597
  const proxyTrackingReadiness = buildProxyTrackingReadiness({ codexConfig, claudeConfig, realUsage });
@@ -9,6 +9,10 @@ module.exports = function createUsageSessions(deps) {
9
9
  readIfText,
10
10
  } = deps;
11
11
 
12
+ const {
13
+ analyzeCursorSessions,
14
+ } = require("./cursor-sessions")({ fs, os, path, estimateTokens });
15
+
12
16
  const {
13
17
  addUsage,
14
18
  collectText,
@@ -261,6 +265,69 @@ function getUsageSummary(options = {}) {
261
265
  sessions.push(analyzeSessionFile(file, "claude-code"));
262
266
  }
263
267
  }
268
+ if (tool === "all" || tool === "cursor") {
269
+ try {
270
+ const cursorData = analyzeCursorSessions({ limit, cwd });
271
+ if (cursorData.sessions.length) {
272
+ for (const cursorSession of cursorData.sessions.slice(0, limit)) {
273
+ sessions.push({
274
+ tool: "cursor",
275
+ filePath: null,
276
+ sessionId: cursorSession.sessionId,
277
+ title: cursorSession.title,
278
+ cwd: cwd,
279
+ model: cursorSession.model,
280
+ startedAt: cursorSession.createdAt,
281
+ updatedAt: cursorSession.updatedAt || cursorSession.createdAt,
282
+ turns: 0,
283
+ userMessages: 0,
284
+ assistantMessages: 0,
285
+ toolCalls: 0,
286
+ toolResults: 0,
287
+ estimatedInputTokens: 0,
288
+ estimatedOutputTokens: 0,
289
+ estimatedToolTokens: 0,
290
+ inputTokens: 0,
291
+ outputTokens: 0,
292
+ cacheReadTokens: 0,
293
+ cacheCreationTokens: 0,
294
+ exactInputTokens: 0,
295
+ exactOutputTokens: 0,
296
+ exactCacheReadTokens: 0,
297
+ exactCacheCreationTokens: 0,
298
+ exactTotalTokens: 0,
299
+ exactAvailable: false,
300
+ confidence: "cursor-metadata",
301
+ largestTextBlobs: [],
302
+ toolNames: {},
303
+ pathMentions: {},
304
+ generatedArtifactMentions: {},
305
+ commandMentions: {},
306
+ failureMentions: 0,
307
+ eventTokenDeltas: [],
308
+ exactTokenTimeline: [],
309
+ estimatedTotalTokens: 0,
310
+ exactActiveTokens: 0,
311
+ contextTokens: 0,
312
+ displayTokens: 0,
313
+ contextRisk: "Low",
314
+ recentContextGrowth: 0,
315
+ repeatedPathMentions: [],
316
+ generatedArtifacts: [],
317
+ repeatedCommands: [],
318
+ loopSuspicion: false,
319
+ loopConfidence: "low",
320
+ cost: null,
321
+ cursorMode: cursorSession.mode,
322
+ cursorLinesAdded: cursorSession.linesAdded,
323
+ cursorLinesRemoved: cursorSession.linesRemoved,
324
+ });
325
+ }
326
+ }
327
+ } catch {
328
+ // Cursor data unavailable
329
+ }
330
+ }
264
331
  sessions.sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0));
265
332
  const selected = sessions.slice(0, limit);
266
333
  const totals = selected.reduce(
@@ -290,6 +357,7 @@ function getUsageSummary(options = {}) {
290
357
 
291
358
  return {
292
359
  analyzeSessionFile,
360
+ analyzeCursorSessions,
293
361
  getAllClaudeSessionFiles,
294
362
  getClaudeSessionFiles,
295
363
  getCodexSessionFiles,
@@ -63,6 +63,7 @@ const {
63
63
 
64
64
  const {
65
65
  analyzeSessionFile,
66
+ analyzeCursorSessions,
66
67
  getAllClaudeSessionFiles,
67
68
  getClaudeSessionFiles,
68
69
  getCodexSessionFiles,
@@ -77,6 +78,12 @@ const {
77
78
  readIfText,
78
79
  });
79
80
 
81
+ const {
82
+ buildCursorDiagnosis,
83
+ buildCursorSessionTimeline,
84
+ renderCursorTerminal,
85
+ } = require("./cursor-sessions")({ fs, os, path, estimateTokens });
86
+
80
87
  function getUsageSummary(options = {}) {
81
88
  const summary = getBaseUsageSummary(options);
82
89
  if ((summary.sessions || []).length > 1) {
@@ -176,6 +183,17 @@ function getClaudeCodeCostSummary(options = {}) {
176
183
  };
177
184
  }
178
185
 
186
+ function getCursorSessionSummary(options = {}) {
187
+ const cwd = options.cwd || process.cwd();
188
+ const limit = options.limit || 20;
189
+ const mode = options.mode || "latest";
190
+ const cursorData = analyzeCursorSessions({ limit, cwd });
191
+ cursorData.command = mode;
192
+ cursorData.timeline = buildCursorSessionTimeline(cursorData);
193
+ cursorData.diagnosis = buildCursorDiagnosis(cursorData);
194
+ return cursorData;
195
+ }
196
+
179
197
  function parsePositiveInt(value, fallback) {
180
198
  const parsed = Number.parseInt(value, 10);
181
199
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
@@ -550,6 +568,9 @@ async function watchUsage(options = {}) {
550
568
 
551
569
  return {
552
570
  analyzeSessionFile,
571
+ analyzeCursorSessions,
572
+ buildCursorDiagnosis,
573
+ buildCursorSessionTimeline,
553
574
  calculateClaudeCost,
554
575
  compactUsageSummary,
555
576
  formatMoney,
@@ -557,11 +578,13 @@ async function watchUsage(options = {}) {
557
578
  getClaudeCodeCostSummary,
558
579
  getClaudeSessionFiles,
559
580
  getCodexSessionFiles,
581
+ getCursorSessionSummary,
560
582
  getUsageSummary,
561
583
  getPositionals,
562
584
  parsePositiveInt,
563
585
  parseScopeAndTarget,
564
586
  renderClaudeCostTerminal,
587
+ renderCursorTerminal,
565
588
  renderUsageTerminal,
566
589
  renderContextThrottle,
567
590
  buildWatchEvent,
@@ -53,6 +53,9 @@ const {
53
53
 
54
54
  const {
55
55
  analyzeSessionFile,
56
+ analyzeCursorSessions,
57
+ buildCursorDiagnosis,
58
+ buildCursorSessionTimeline,
56
59
  calculateClaudeCost,
57
60
  compactUsageSummary,
58
61
  formatMoney,
@@ -60,11 +63,13 @@ const {
60
63
  getClaudeCodeCostSummary,
61
64
  getClaudeSessionFiles,
62
65
  getCodexSessionFiles,
66
+ getCursorSessionSummary,
63
67
  getUsageSummary,
64
68
  getPositionals,
65
69
  parsePositiveInt,
66
70
  parseScopeAndTarget,
67
71
  renderClaudeCostTerminal,
72
+ renderCursorTerminal,
68
73
  renderUsageTerminal,
69
74
  renderContextThrottle,
70
75
  renderRescuePrompt,
@@ -112,6 +117,7 @@ const scanApi = require("./prismo-dev/scan")({
112
117
  getCodexSessionFiles,
113
118
  compactUsageSummary,
114
119
  formatTokenCount,
120
+ color,
115
121
  });
116
122
 
117
123
  ({ scanRepo } = scanApi);
@@ -265,8 +271,9 @@ Usage:
265
271
  prismo optimize [scope] [--json] [path]
266
272
  prismo context [scope] [--json] [path]
267
273
  prismo cc [list|last N|all|timeline] [--json] [--limit N] [--firewall] [--task TASK] [path]
268
- prismo usage [codex|claude|all] [--json] [--limit N] [path]
269
- prismo watch [codex|claude|all] [--json] [--once] [--agents] [--report] [--rescue] [--guardrails] [--throttle] [--events] [--no-events] [--auto] [--budget N] [--redact-paths] [--interval N] [path]
274
+ prismo cursor [list|authorship|timeline|files|all] [--json] [--limit N] [path]
275
+ prismo usage [codex|claude|cursor|all] [--json] [--limit N] [path]
276
+ prismo watch [codex|claude|cursor|all] [--json] [--once] [--agents] [--report] [--rescue] [--guardrails] [--throttle] [--events] [--no-events] [--auto] [--budget N] [--redact-paths] [--interval N] [path]
270
277
  prismo demo
271
278
 
272
279
  Commands:
@@ -281,7 +288,8 @@ Commands:
281
288
  optimize Generate lightweight AI-readable project context files in .prismo/.
282
289
  context Print a copy-pasteable compact context prompt for AI coding tools.
283
290
  cc Show Claude Code token cost, cache cost, and all-time totals.
284
- usage Read local Codex/Claude Code session logs and summarize token usage.
291
+ cursor Show Cursor session data, AI authorship, and AI-generated file tracking.
292
+ usage Read local Codex/Claude Code/Cursor session logs and summarize token usage.
285
293
  watch Refresh local session usage in the terminal.
286
294
  demo Show sample output without needing a messy repo.
287
295
  setup Detect coding tools, tracking modes, local logs, and Prismo proxy readiness.
@@ -399,22 +407,45 @@ Examples:
399
407
  timeline shows context spikes, repeated commands, artifact leaks, and tool-output pressure for the latest session.
400
408
  --firewall writes .prismo/timeline-firewall-suggestions.md and suggested allow/block files from timeline evidence.
401
409
  Without a path, cc commands read all Claude Code projects. Passing a path filters to that project.`,
410
+ cursor: `Prismo Cursor Sessions
411
+
412
+ Usage:
413
+ prismo cursor [--json] [--limit N] [path]
414
+ prismo cursor list [--json] [--limit N] [path]
415
+ prismo cursor authorship [--json] [--limit N] [path]
416
+ prismo cursor timeline [--json] [--limit N] [path]
417
+ prismo cursor files [--json] [--limit N] [path]
418
+ prismo cursor all [--json] [--limit N] [path]
419
+
420
+ Examples:
421
+ prismo cursor
422
+ prismo cursor authorship
423
+ prismo cursor list --limit 20
424
+ prismo cursor timeline --json
425
+ prismo cursor files
426
+
427
+ Output:
428
+ Reads ~/.cursor/ai-tracking/ai-code-tracking.db and Cursor workspace state databases.
429
+ Shows Cursor composer sessions, AI authorship percentages from scored commits, AI-generated file tracking, and churn analysis.
430
+ Requires sqlite3 to be installed on the system.`,
402
431
  usage: `Prismo Usage
403
432
 
404
433
  Usage:
405
- prismo usage [codex|claude|all] [--json] [--limit N] [path]
434
+ prismo usage [codex|claude|cursor|all] [--json] [--limit N] [path]
406
435
 
407
436
  Examples:
408
437
  prismo usage
409
438
  prismo usage codex --json
439
+ prismo usage cursor
410
440
  prismo usage claude --limit 3`,
411
441
  watch: `Prismo Watch
412
442
 
413
443
  Usage:
414
- prismo watch [codex|claude|all] [--json] [--once] [--agents] [--report] [--rescue] [--guardrails] [--throttle] [--events] [--no-events] [--auto] [--budget N] [--redact-paths] [--interval N] [path]
444
+ prismo watch [codex|claude|cursor|all] [--json] [--once] [--agents] [--report] [--rescue] [--guardrails] [--throttle] [--events] [--no-events] [--auto] [--budget N] [--redact-paths] [--interval N] [path]
415
445
 
416
446
  Examples:
417
447
  prismo watch codex
448
+ prismo watch cursor --once --json
418
449
  prismo watch claude --once --json
419
450
  prismo watch --agents --once
420
451
  prismo watch --once --report
@@ -519,6 +550,7 @@ Tools exposed:
519
550
  prismo_context_pack
520
551
  prismo_firewall
521
552
  prismo_cc_timeline
553
+ prismo_cursor_sessions
522
554
 
523
555
  Output:
524
556
  Starts a local JSON-RPC MCP server over stdio. Use it from MCP-compatible clients so agents can scan context waste, search shielded command output, and request scoped context without loading huge logs into chat.
@@ -574,8 +606,8 @@ async function runCli(argv) {
574
606
  printCommandHelp(command);
575
607
  return;
576
608
  }
577
- if (!["dev", "init", "doctor", "firewall", "benchmark", "shield", "mcp", "setup", "scan", "optimize", "context", "cc", "usage", "watch", "demo"].includes(command)) {
578
- throw new Error(`Unknown command: ${command}. Try: prismo doctor, prismo watch, prismo benchmark, prismo shield, prismo mcp, prismo firewall, prismo init, prismo scan, prismo optimize, prismo context, prismo cc, or prismo usage`);
609
+ if (!["dev", "init", "doctor", "firewall", "benchmark", "shield", "mcp", "setup", "scan", "optimize", "context", "cc", "cursor", "usage", "watch", "demo"].includes(command)) {
610
+ throw new Error(`Unknown command: ${command}. Try: prismo doctor, prismo watch, prismo benchmark, prismo shield, prismo mcp, prismo firewall, prismo init, prismo scan, prismo optimize, prismo context, prismo cc, prismo cursor, or prismo usage`);
579
611
  }
580
612
 
581
613
  if (command === "demo") {
@@ -725,6 +757,7 @@ async function runCli(argv) {
725
757
  toDoctorJsonPayload,
726
758
  getUsageSummary,
727
759
  getClaudeCodeCostSummary,
760
+ getCursorSessionSummary,
728
761
  runOptimize,
729
762
  createOptimizeContext,
730
763
  renderStarterPrompt,
@@ -839,9 +872,31 @@ async function runCli(argv) {
839
872
  return;
840
873
  }
841
874
 
875
+ if (command === "cursor") {
876
+ const json = rest.includes("--json");
877
+ const limitIndex = rest.indexOf("--limit");
878
+ const positional = getPositionals(rest, new Set(["--limit"]));
879
+ const subcommand = positional[0] && ["list", "authorship", "timeline", "files", "all"].includes(positional[0].toLowerCase())
880
+ ? positional[0].toLowerCase()
881
+ : "latest";
882
+ const target = (subcommand !== "latest" ? positional[1] : positional[0]) || process.cwd();
883
+ const limit = parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, subcommand === "all" ? 200 : 20);
884
+ const summary = getCursorSessionSummary({
885
+ cwd: path.resolve(target),
886
+ limit,
887
+ mode: subcommand,
888
+ });
889
+ if (json) {
890
+ console.log(JSON.stringify(summary, null, 2));
891
+ return;
892
+ }
893
+ console.log(renderCursorTerminal(summary, subcommand));
894
+ return;
895
+ }
896
+
842
897
  if (command === "usage" || command === "watch") {
843
898
  const json = rest.includes("--json");
844
- const knownTools = new Set(["codex", "claude", "all"]);
899
+ const knownTools = new Set(["codex", "claude", "cursor", "all"]);
845
900
  const positional = getPositionals(rest, new Set(["--limit", "--interval", "--budget"]));
846
901
  const explicitTool = positional[0] && knownTools.has(positional[0].toLowerCase());
847
902
  const tool = explicitTool ? positional[0].toLowerCase() : "all";
@@ -1021,6 +1076,7 @@ module.exports = {
1021
1076
  renderMarkdownReport,
1022
1077
  renderSimpleScanReport,
1023
1078
  renderClaudeCostTerminal,
1079
+ renderCursorTerminal,
1024
1080
  renderUsageTerminal,
1025
1081
  renderContextThrottle,
1026
1082
  renderRescuePrompt,
@@ -1040,7 +1096,9 @@ module.exports = {
1040
1096
  runCli,
1041
1097
  scanRepo,
1042
1098
  getClaudeCodeCostSummary,
1099
+ getCursorSessionSummary,
1043
1100
  getUsageSummary,
1101
+ analyzeCursorSessions,
1044
1102
  analyzeSessionFile,
1045
1103
  calculateClaudeCost,
1046
1104
  toDoctorJsonPayload,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Local AI coding workflow scanner for Codex, Claude Code, Cursor, and token-waste diagnostics.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/shanirsh/prismodev#readme",