getprismo 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -99,6 +99,58 @@ function buildBlockedContext(ctx) {
99
99
  return uniq(blocked).slice(0, 80);
100
100
  }
101
101
 
102
+ function isGeneratedLike(value) {
103
+ const text = String(value || "").replace(/\\/g, "/").toLowerCase();
104
+ return /(^|\/)(node_modules|dist|build|coverage|\.next|__pycache__|logs?|events?|source-streams?|session-dumps?|playwright-report|test-results)(\/|$)/.test(text)
105
+ || /(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb)$/.test(text)
106
+ || /\.(log|jsonl|ndjson|map|pyc)$/.test(text);
107
+ }
108
+
109
+ function firewallPatternForPath(value) {
110
+ const text = String(value || "").replace(/\\/g, "/").replace(/\s+\(\d+x\)$/, "");
111
+ if (!text) return null;
112
+ if (/(^|\/)__pycache__(\/|$)/.test(text)) return "**/__pycache__/**";
113
+ if (/(^|\/)(node_modules|dist|build|coverage|\.next|logs?|events?|source-streams?|session-dumps?|playwright-report|test-results)\//.test(text)) {
114
+ const match = text.match(/(^|\/)(node_modules|dist|build|coverage|\.next|logs?|events?|source-streams?|session-dumps?|playwright-report|test-results)\//);
115
+ const prefix = text.slice(0, match.index + match[0].length).replace(/\/$/, "");
116
+ return `${prefix}/**`;
117
+ }
118
+ return text;
119
+ }
120
+
121
+ function timelineEventPath(event) {
122
+ const detail = String(event?.detail || "");
123
+ const match = detail.match(/^(.+?)\s+\(\d+x\)$/);
124
+ return (match ? match[1] : detail).trim();
125
+ }
126
+
127
+ function buildTimelineFirewallSuggestions(ctx, session, taskScope) {
128
+ const baseAllowed = buildAllowedContext(ctx, taskScope);
129
+ const baseBlocked = buildBlockedContext(ctx);
130
+ const timeline = session?.timeline || [];
131
+ const repeated = session?.repeatedPathMentions || [];
132
+ const artifacts = session?.generatedArtifacts || [];
133
+
134
+ const sessionAllowed = repeated
135
+ .map((item) => item.value)
136
+ .filter((value) => value && !isGeneratedLike(value))
137
+ .map(globForFile)
138
+ .slice(0, 20);
139
+
140
+ const sessionBlocked = [
141
+ ...artifacts.map((item) => item.value),
142
+ ...timeline.filter((event) => event.type === "artifact-leak").map(timelineEventPath),
143
+ ...repeated.map((item) => item.value).filter(isGeneratedLike),
144
+ ].map(firewallPatternForPath).filter(Boolean);
145
+
146
+ return {
147
+ allowed: uniq([...sessionAllowed, ...baseAllowed]).slice(0, 80),
148
+ blocked: uniq([...sessionBlocked, ...baseBlocked]).slice(0, 100),
149
+ sessionAllowed: uniq(sessionAllowed),
150
+ sessionBlocked: uniq(sessionBlocked),
151
+ };
152
+ }
153
+
102
154
  function renderLines(title, items) {
103
155
  return [`# ${title}`, "", ...items.map((item) => item)].join("\n") + "\n";
104
156
  }
@@ -187,6 +239,71 @@ function runFirewall(rootDir = process.cwd(), options = {}) {
187
239
  return result;
188
240
  }
189
241
 
242
+ function renderTimelineFirewallSuggestions(result) {
243
+ const lines = [];
244
+ lines.push("# Prismo Timeline Firewall Suggestions");
245
+ lines.push("");
246
+ lines.push(`Generated: ${result.generatedAt}`);
247
+ lines.push(`Task: ${result.task}`);
248
+ lines.push(`Scope: ${result.scope}`);
249
+ if (result.sessionId) lines.push(`Session: ${result.sessionId}`);
250
+ lines.push("");
251
+ lines.push("These suggestions came from `cc timeline` session evidence. They are safe recommendation files; Prismo does not overwrite your active firewall unless you copy/apply them.");
252
+ lines.push("");
253
+ lines.push("## Session-Derived Allowed Context");
254
+ lines.push("");
255
+ if (result.sessionAllowed.length) result.sessionAllowed.forEach((item) => lines.push(`- ${item}`));
256
+ else lines.push("- No repeated source files were strong enough to promote.");
257
+ lines.push("");
258
+ lines.push("## Session-Derived Blocked Context");
259
+ lines.push("");
260
+ if (result.sessionBlocked.length) result.sessionBlocked.forEach((item) => lines.push(`- ${item}`));
261
+ else lines.push("- No generated/noisy paths were strong enough to add.");
262
+ lines.push("");
263
+ lines.push("## Next Session Prompt");
264
+ lines.push("");
265
+ lines.push("```text");
266
+ lines.push(`Use .prismo/context-firewall.suggested.md as the starting context policy for this ${result.task} task.`);
267
+ lines.push("Start from allowed context first. Do not read blocked paths unless you explain why they are required.");
268
+ lines.push("If you need wider context, name the exact file and reason before reading.");
269
+ lines.push("```");
270
+ lines.push("");
271
+ return lines.join("\n");
272
+ }
273
+
274
+ function runTimelineFirewallSuggestions(rootDir = process.cwd(), session = null, options = {}) {
275
+ const ctx = createOptimizeContext(rootDir, options.scope || null);
276
+ const task = options.task || options.scope || "timeline-followup";
277
+ const scope = inferTaskScope(task, ctx);
278
+ const suggestions = buildTimelineFirewallSuggestions(ctx, session, scope);
279
+ const result = {
280
+ root: ctx.root,
281
+ task,
282
+ scope,
283
+ sessionId: session?.sessionId || null,
284
+ allowed: suggestions.allowed,
285
+ blocked: suggestions.blocked,
286
+ sessionAllowed: suggestions.sessionAllowed,
287
+ sessionBlocked: suggestions.sessionBlocked,
288
+ generatedAt: new Date().toISOString(),
289
+ generatedFiles: [
290
+ ".prismo/timeline-firewall-suggestions.md",
291
+ ".prismo/context-firewall.suggested.md",
292
+ ".prismo/allowed-context.suggested.txt",
293
+ ".prismo/blocked-context.suggested.txt",
294
+ ],
295
+ };
296
+
297
+ if (!options.dryRun) {
298
+ writeGeneratedFile(ctx.root, ".prismo/timeline-firewall-suggestions.md", renderTimelineFirewallSuggestions(result));
299
+ writeGeneratedFile(ctx.root, ".prismo/context-firewall.suggested.md", renderFirewallPolicy(result));
300
+ writeGeneratedFile(ctx.root, ".prismo/allowed-context.suggested.txt", renderLines("Allowed Context Suggestions", result.allowed));
301
+ writeGeneratedFile(ctx.root, ".prismo/blocked-context.suggested.txt", renderLines("Blocked Context Suggestions", result.blocked));
302
+ }
303
+ result.dryRun = Boolean(options.dryRun);
304
+ return result;
305
+ }
306
+
190
307
  function renderFirewallTerminal(result) {
191
308
  const lines = [];
192
309
  lines.push("");
@@ -218,6 +335,8 @@ function renderFirewallTerminal(result) {
218
335
  renderFirewallPolicy,
219
336
  renderFirewallPrompt,
220
337
  renderFirewallTerminal,
338
+ renderTimelineFirewallSuggestions,
221
339
  runFirewall,
340
+ runTimelineFirewallSuggestions,
222
341
  };
223
342
  };
@@ -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,