getprismo 0.1.27 → 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.
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: [
@@ -229,7 +229,11 @@ function renderSetupTerminal(result) {
229
229
  lines.push("Detected:");
230
230
  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
231
  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"}`);
232
+ const cursorInfo = result.detected.cursor;
233
+ lines.push(`- Cursor: ${cursorInfo.detected ? "detected" : "not detected"}${cursorInfo.dbAvailable ? `; tracking DB: found (${cursorInfo.totalSessions} sessions, ${cursorInfo.activeSessions} active)` : ""}${cursorInfo.workspace ? `; workspace: matched` : ""}`);
234
+ if (cursorInfo.dbStats) {
235
+ lines.push(` Tracking: ${cursorInfo.dbStats.scored_commits} scored commits, ${cursorInfo.dbStats.ai_code_hashes} code hashes, ${cursorInfo.dbStats.conversation_summaries} conversation summaries`);
236
+ }
233
237
  const detectedTools = result.detected.optimizationStack.detectedTools;
234
238
  lines.push(`- Optimization tools: ${detectedTools.length ? detectedTools.join(", ") : "none detected"}`);
235
239
  lines.push("");
@@ -562,6 +566,31 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
562
566
  }
563
567
  const optimizationStack = detectOptimizationStack(root, claudeConfig, codexConfig);
564
568
  const agentReadiness = detectAgentReadiness(root, claudeConfig, codexConfig, realUsage);
569
+
570
+ if (agentReadiness.cursor.dbAvailable) {
571
+ try {
572
+ const cursorMod = require("./cursor-sessions")({ fs, os, path, estimateTokens });
573
+ const tracked = cursorMod.getCursorTrackedFileContent(20);
574
+ const cursorAiFiles = tracked.filter((f) => {
575
+ const fullPath = path.join(root, f.gitPath);
576
+ return fs.existsSync(fullPath);
577
+ });
578
+ if (cursorAiFiles.length >= 3) {
579
+ addIssue(
580
+ issues,
581
+ "medium",
582
+ "ai_generated_files",
583
+ `${cursorAiFiles.length} AI-generated files still in repo (tracked by Cursor)`,
584
+ cursorAiFiles.slice(0, 5).map((f) => `${f.gitPath} (${f.model || "unknown model"})`).join(", "),
585
+ "Review AI-generated files for quality and whether they should remain in the project.",
586
+ `AI-generated content may inflate context if agents re-read it. Run: npx getprismo cursor files`
587
+ );
588
+ }
589
+ } catch {
590
+ // Cursor data unavailable
591
+ }
592
+ }
593
+
565
594
  const toolOutputRisk = detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRiskDirs });
566
595
  const operationalNoise = detectOperationalNoise(files);
567
596
  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,
@@ -265,8 +270,9 @@ Usage:
265
270
  prismo optimize [scope] [--json] [path]
266
271
  prismo context [scope] [--json] [path]
267
272
  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]
273
+ prismo cursor [list|authorship|timeline|files|all] [--json] [--limit N] [path]
274
+ prismo usage [codex|claude|cursor|all] [--json] [--limit N] [path]
275
+ 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
276
  prismo demo
271
277
 
272
278
  Commands:
@@ -281,7 +287,8 @@ Commands:
281
287
  optimize Generate lightweight AI-readable project context files in .prismo/.
282
288
  context Print a copy-pasteable compact context prompt for AI coding tools.
283
289
  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.
290
+ cursor Show Cursor session data, AI authorship, and AI-generated file tracking.
291
+ usage Read local Codex/Claude Code/Cursor session logs and summarize token usage.
285
292
  watch Refresh local session usage in the terminal.
286
293
  demo Show sample output without needing a messy repo.
287
294
  setup Detect coding tools, tracking modes, local logs, and Prismo proxy readiness.
@@ -399,22 +406,45 @@ Examples:
399
406
  timeline shows context spikes, repeated commands, artifact leaks, and tool-output pressure for the latest session.
400
407
  --firewall writes .prismo/timeline-firewall-suggestions.md and suggested allow/block files from timeline evidence.
401
408
  Without a path, cc commands read all Claude Code projects. Passing a path filters to that project.`,
409
+ cursor: `Prismo Cursor Sessions
410
+
411
+ Usage:
412
+ prismo cursor [--json] [--limit N] [path]
413
+ prismo cursor list [--json] [--limit N] [path]
414
+ prismo cursor authorship [--json] [--limit N] [path]
415
+ prismo cursor timeline [--json] [--limit N] [path]
416
+ prismo cursor files [--json] [--limit N] [path]
417
+ prismo cursor all [--json] [--limit N] [path]
418
+
419
+ Examples:
420
+ prismo cursor
421
+ prismo cursor authorship
422
+ prismo cursor list --limit 20
423
+ prismo cursor timeline --json
424
+ prismo cursor files
425
+
426
+ Output:
427
+ Reads ~/.cursor/ai-tracking/ai-code-tracking.db and Cursor workspace state databases.
428
+ Shows Cursor composer sessions, AI authorship percentages from scored commits, AI-generated file tracking, and churn analysis.
429
+ Requires sqlite3 to be installed on the system.`,
402
430
  usage: `Prismo Usage
403
431
 
404
432
  Usage:
405
- prismo usage [codex|claude|all] [--json] [--limit N] [path]
433
+ prismo usage [codex|claude|cursor|all] [--json] [--limit N] [path]
406
434
 
407
435
  Examples:
408
436
  prismo usage
409
437
  prismo usage codex --json
438
+ prismo usage cursor
410
439
  prismo usage claude --limit 3`,
411
440
  watch: `Prismo Watch
412
441
 
413
442
  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]
443
+ 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
444
 
416
445
  Examples:
417
446
  prismo watch codex
447
+ prismo watch cursor --once --json
418
448
  prismo watch claude --once --json
419
449
  prismo watch --agents --once
420
450
  prismo watch --once --report
@@ -519,6 +549,7 @@ Tools exposed:
519
549
  prismo_context_pack
520
550
  prismo_firewall
521
551
  prismo_cc_timeline
552
+ prismo_cursor_sessions
522
553
 
523
554
  Output:
524
555
  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 +605,8 @@ async function runCli(argv) {
574
605
  printCommandHelp(command);
575
606
  return;
576
607
  }
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`);
608
+ if (!["dev", "init", "doctor", "firewall", "benchmark", "shield", "mcp", "setup", "scan", "optimize", "context", "cc", "cursor", "usage", "watch", "demo"].includes(command)) {
609
+ 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
610
  }
580
611
 
581
612
  if (command === "demo") {
@@ -725,6 +756,7 @@ async function runCli(argv) {
725
756
  toDoctorJsonPayload,
726
757
  getUsageSummary,
727
758
  getClaudeCodeCostSummary,
759
+ getCursorSessionSummary,
728
760
  runOptimize,
729
761
  createOptimizeContext,
730
762
  renderStarterPrompt,
@@ -839,9 +871,31 @@ async function runCli(argv) {
839
871
  return;
840
872
  }
841
873
 
874
+ if (command === "cursor") {
875
+ const json = rest.includes("--json");
876
+ const limitIndex = rest.indexOf("--limit");
877
+ const positional = getPositionals(rest, new Set(["--limit"]));
878
+ const subcommand = positional[0] && ["list", "authorship", "timeline", "files", "all"].includes(positional[0].toLowerCase())
879
+ ? positional[0].toLowerCase()
880
+ : "latest";
881
+ const target = (subcommand !== "latest" ? positional[1] : positional[0]) || process.cwd();
882
+ const limit = parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, subcommand === "all" ? 200 : 20);
883
+ const summary = getCursorSessionSummary({
884
+ cwd: path.resolve(target),
885
+ limit,
886
+ mode: subcommand,
887
+ });
888
+ if (json) {
889
+ console.log(JSON.stringify(summary, null, 2));
890
+ return;
891
+ }
892
+ console.log(renderCursorTerminal(summary, subcommand));
893
+ return;
894
+ }
895
+
842
896
  if (command === "usage" || command === "watch") {
843
897
  const json = rest.includes("--json");
844
- const knownTools = new Set(["codex", "claude", "all"]);
898
+ const knownTools = new Set(["codex", "claude", "cursor", "all"]);
845
899
  const positional = getPositionals(rest, new Set(["--limit", "--interval", "--budget"]));
846
900
  const explicitTool = positional[0] && knownTools.has(positional[0].toLowerCase());
847
901
  const tool = explicitTool ? positional[0].toLowerCase() : "all";
@@ -1021,6 +1075,7 @@ module.exports = {
1021
1075
  renderMarkdownReport,
1022
1076
  renderSimpleScanReport,
1023
1077
  renderClaudeCostTerminal,
1078
+ renderCursorTerminal,
1024
1079
  renderUsageTerminal,
1025
1080
  renderContextThrottle,
1026
1081
  renderRescuePrompt,
@@ -1040,7 +1095,9 @@ module.exports = {
1040
1095
  runCli,
1041
1096
  scanRepo,
1042
1097
  getClaudeCodeCostSummary,
1098
+ getCursorSessionSummary,
1043
1099
  getUsageSummary,
1100
+ analyzeCursorSessions,
1044
1101
  analyzeSessionFile,
1045
1102
  calculateClaudeCost,
1046
1103
  toDoctorJsonPayload,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
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",