getprismo 0.1.6 → 0.1.8

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
@@ -68,8 +68,8 @@ Fixed:
68
68
  - Created .cursorignore
69
69
  - Generated prismo-dev-report.md
70
70
  - Generated .prismo/architecture-summary.md
71
- - Generated .prismo/recommended-CLAUDE.md
72
- - Generated .prismo/recommended-AGENTS.md
71
+ - Generated .prismo/recommended-CLAUDE.boilerplate.md
72
+ - Generated .prismo/recommended-AGENTS.boilerplate.md
73
73
  - Generated .prismo/recommended-.claudeignore
74
74
  - Generated .prismo/recommended-.cursorignore
75
75
  - Generated .prismo/recommended-.gitignore-additions
@@ -384,14 +384,18 @@ what doctor creates:
384
384
  .prismo/architecture-summary.md compact project overview for agents
385
385
  .prismo/backend-summary.md backend-specific context
386
386
  .prismo/frontend-summary.md frontend-specific context
387
- .prismo/recommended-CLAUDE.md optimized CLAUDE.md template
388
- .prismo/recommended-AGENTS.md optimized AGENTS.md template
387
+ .prismo/recommended-CLAUDE.boilerplate.md CLAUDE.md boilerplate reference; do not overwrite curated files
388
+ .prismo/recommended-AGENTS.boilerplate.md AGENTS.md boilerplate reference; do not overwrite curated files
389
389
  .prismo/recommended-.claudeignore full recommended ignore list
390
390
  .prismo/recommended-.cursorignore full recommended ignore list
391
391
  .prismo/recommended-.gitignore-additions things your gitignore might be missing
392
392
  prismo-dev-report.md full diagnostic report
393
393
  ```
394
394
 
395
+ if an existing `.claudeignore` or `.cursorignore` already covers prismo's recommendations, doctor skips the suggested ignore file instead of creating redundant noise. the default recommendations include common project state, local db, export, credential, and token patterns such as `*_state.json`, `*_tokens.json`, `*_export.json`, `*.sqlite`, `models/`, and `state-backups/`.
396
+
397
+ backend and frontend summaries include load-bearing candidates ranked by lightweight reference signals plus file size, not just directory listings.
398
+
395
399
  what doctor never touches:
396
400
 
397
401
  - your real `CLAUDE.md`
@@ -600,8 +604,8 @@ no api keys. no intercepted prompts. no data uploaded.
600
604
  ├── frontend-summary.md
601
605
  ├── frontend-context.md
602
606
  ├── backend-context.md
603
- ├── recommended-CLAUDE.md
604
- ├── recommended-AGENTS.md
607
+ ├── recommended-CLAUDE.boilerplate.md
608
+ ├── recommended-AGENTS.boilerplate.md
605
609
  ├── recommended-.claudeignore
606
610
  ├── recommended-.cursorignore
607
611
  ├── recommended-.gitignore-additions
@@ -122,6 +122,19 @@ const DEFAULT_CLAUDEIGNORE = [
122
122
  "pnpm-lock.yaml",
123
123
  "test-results/",
124
124
  "playwright-report/",
125
+ "models/",
126
+ "state-backups/",
127
+ "backups/",
128
+ "*.sqlite",
129
+ "*.sqlite3",
130
+ "*.db",
131
+ "*_state.json",
132
+ "*_tokens.json",
133
+ "*_export.json",
134
+ "*secret*.json",
135
+ "*credential*.json",
136
+ ".env",
137
+ ".env.*",
125
138
  ];
126
139
 
127
140
  const NPX_COMMAND = "npx getprismo";
@@ -122,18 +122,49 @@ function isNonSourcePath(rel) {
122
122
  }
123
123
 
124
124
  function detectBackendPaths(result) {
125
- const api = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/(backend|app|src)\/.*(router|routes|api)\//.test(rel) || /(backend|app|src)\/.*(router|routes).*\.py$/.test(rel) || /\/(routing|routers?)\.py$/.test(rel)), 20).map((f) => f.path);
126
- const services = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/(backend|app|src)\/.*(service|services|application)/.test(rel) || /\/applications?\.py$/.test(rel)), 20).map((f) => f.path);
127
- const models = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/(backend|app|src)\/.*models\.py$/.test(rel) || /(backend|app|src)\/.*schema/.test(rel) || /\/models\.py$/.test(rel)), 20).map((f) => f.path);
128
- const db = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && /(backend|app|src)\/.*\/(db|database|alembic|migrations)[/.]/.test(rel), 20).map((f) => f.path);
129
- const config = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/(backend|app|src)\/.*(config|settings|env).*\.py$/.test(rel) || rel.endsWith("requirements.txt") || rel === "pyproject.toml"), 20).map((f) => f.path);
130
- const auth = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/(backend|app|src)\/.*auth/.test(rel) || /\/security\.py$/.test(rel)), 20).map((f) => f.path);
125
+ const pythonText = (file) => readIfText(path.join(result.root, file.path), 256 * 1024) || "";
126
+ const isRootPython = (rel) => /^[^/]+\.py$/.test(rel);
127
+ const isPython = (rel) => rel.endsWith(".py");
128
+ const hasFastApiSignal = (file) => /FastAPI\s*\(|APIRouter\s*\(|@app\.(get|post|put|patch|delete)|@router\.(get|post|put|patch|delete)/.test(pythonText(file));
129
+ const api = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
130
+ /(backend|app|src)\/.*(router|routes|api)\//.test(rel) ||
131
+ /(backend|app|src)\/.*(router|routes).*\.py$/.test(rel) ||
132
+ /\/(routing|routers?)\.py$/.test(rel) ||
133
+ (isRootPython(rel) && hasFastApiSignal(file))
134
+ ), 30).map((f) => f.path);
135
+ const services = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
136
+ /(backend|app|src)\/.*(service|services|application)/.test(rel) ||
137
+ /\/applications?\.py$/.test(rel) ||
138
+ (isRootPython(rel) && /(service|worker|manager|client|heartbeat|memory|chat|tool|orchestrator|pipeline)/i.test(rel))
139
+ ), 40).map((f) => f.path);
140
+ const models = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
141
+ /(backend|app|src)\/.*models\.py$/.test(rel) ||
142
+ /(backend|app|src)\/.*schema/.test(rel) ||
143
+ /\/models\.py$/.test(rel) ||
144
+ (isRootPython(rel) && /(model|schema|entity|types?)/i.test(rel))
145
+ ), 30).map((f) => f.path);
146
+ const db = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
147
+ /(backend|app|src)\/.*\/(db|database|alembic|migrations)[/.]/.test(rel) ||
148
+ (isPython(rel) && /(sqlite|qdrant|neo4j|database|storage|repository|vector|graph|migration)/i.test(rel))
149
+ ), 30).map((f) => f.path);
150
+ const config = findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (
151
+ /(backend|app|src)\/.*(config|settings|env).*\.py$/.test(rel) ||
152
+ (isRootPython(rel) && /(config|settings|env)/i.test(rel)) ||
153
+ rel.endsWith("requirements.txt") ||
154
+ rel === "pyproject.toml"
155
+ ), 20).map((f) => f.path);
156
+ const auth = findRepoFiles(result, (rel, file) => !isNonSourcePath(rel) && (
157
+ /(backend|app|src)\/.*auth/.test(rel) ||
158
+ /\/security\.py$/.test(rel) ||
159
+ (isPython(rel) && /(auth|security|permission|token|session|oauth|jwt|middleware)/i.test(rel)) ||
160
+ (isRootPython(rel) && /(Depends|HTTPBearer|OAuth2|JWT|Authorization)/.test(pythonText(file)))
161
+ ), 30).map((f) => f.path);
131
162
  return { api, services, models, db, config, auth };
132
163
  }
133
164
 
134
165
  function detectFrontendPaths(result) {
135
166
  return {
136
- app: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/frontend\/src\/app\//.test(rel) || /src\/app\//.test(rel) || /apps\/[^/]+\/app\//.test(rel) || /apps\/[^/]+\/src\/app\//.test(rel)), 24).map((f) => f.path),
167
+ app: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/frontend\/src\/app\//.test(rel) || /frontend\/src\/App\.[jt]sx?$/.test(rel) || /src\/App\.[jt]sx?$/.test(rel) || /src\/app\//.test(rel) || /apps\/[^/]+\/app\//.test(rel) || /apps\/[^/]+\/src\/app\//.test(rel)), 24).map((f) => f.path),
137
168
  components: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/frontend\/src\/components\//.test(rel) || /src\/components\//.test(rel) || /apps\/[^/]+\/.*components\//.test(rel) || /packages\/[^/]+\/.*components\//.test(rel)), 20).map((f) => f.path),
138
169
  apiClient: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/frontend\/src\/(lib|hooks)\/.*(api|client|query|finops)/.test(rel) || /src\/(lib|hooks)\/.*(api|client|query)/.test(rel)), 20).map((f) => f.path),
139
170
  styling: findRepoFiles(result, (rel) => !isNonSourcePath(rel) && (/tailwind\.config|globals\.css|\.module\.css|frontend\/src\/app\/globals/.test(rel)), 20).map((f) => f.path),
@@ -186,6 +217,68 @@ function mdList(items, empty = "None detected.") {
186
217
  return items.map((item) => `- \`${item}\``).join("\n");
187
218
  }
188
219
 
220
+ function topLoadBearing(root, files, allFiles, limit = 8) {
221
+ const textFiles = (allFiles || []).filter((file) => !file.ignored && file.kind !== "binary" && /\.(py|tsx?|jsx?)$/.test(file.path));
222
+ const corpus = textFiles.map((file) => `${file.path}\n${readIfText(path.join(root, file.path)) || ""}`);
223
+ return [...(files || [])]
224
+ .filter((file) => !file.ignored && file.kind !== "binary")
225
+ .map((file) => {
226
+ const base = path.basename(file.path).replace(/\.[^.]+$/, "");
227
+ const importName = base.replace(/[-.]/g, "_");
228
+ const refs = corpus.reduce((sum, text) => sum + (text.includes(base) || text.includes(importName) ? 1 : 0), 0);
229
+ return { file, refs };
230
+ })
231
+ .sort((a, b) => b.refs - a.refs || b.file.size - a.file.size)
232
+ .slice(0, limit)
233
+ .map(({ file, refs }) => `${file.path} (${refs} reference signal${refs === 1 ? "" : "s"}, ${formatBytes(file.size)})`);
234
+ }
235
+
236
+ function inferGaps(ctx) {
237
+ const gaps = [];
238
+ if (ctx.backendDetected && !ctx.backend.api.length) {
239
+ gaps.push("No conventional backend API layout detected; review root-level Python files or custom service modules for FastAPI/APIRouter usage.");
240
+ }
241
+ if (ctx.backendDetected && !ctx.backend.services.length) {
242
+ gaps.push("No conventional service directory detected; backend services may use flat files or project-specific naming.");
243
+ }
244
+ if (ctx.frontendDetected && !ctx.frontend.app.length) {
245
+ gaps.push("No conventional frontend routing surface detected; Vite/React routes may live in App.tsx or custom tab components.");
246
+ }
247
+ return gaps;
248
+ }
249
+
250
+ function summarizeRiskyDirectories(dirs) {
251
+ const groups = new Map();
252
+ for (const dir of dirs || []) {
253
+ const normalized = dir.path.replace(/\\/g, "/");
254
+ const parts = normalized.split("/");
255
+ let key = `${normalized}/`;
256
+ if (parts.includes("__pycache__")) {
257
+ const before = parts.slice(0, parts.indexOf("__pycache__"));
258
+ const prefix = before.length ? `${before[0]}/**` : "**";
259
+ key = `${prefix}/__pycache__/`;
260
+ } else if (parts.includes("node_modules")) {
261
+ key = `${parts.slice(0, parts.indexOf("node_modules") + 1).join("/")}/**`;
262
+ } else if (parts.length > 2) {
263
+ key = `${parts.slice(0, 2).join("/")}/**/${parts[parts.length - 1]}/`;
264
+ }
265
+ const existing = groups.get(key) || { key, count: 0, exposed: 0, ignored: 0, examples: [] };
266
+ existing.count += 1;
267
+ if (dir.exposed) existing.exposed += 1;
268
+ else existing.ignored += 1;
269
+ if (existing.examples.length < 2) existing.examples.push(`${normalized}/`);
270
+ groups.set(key, existing);
271
+ }
272
+ return Array.from(groups.values())
273
+ .sort((a, b) => b.exposed - a.exposed || b.count - a.count)
274
+ .slice(0, 25)
275
+ .map((group) => {
276
+ const state = group.exposed ? `${group.exposed} exposed` : `${group.ignored} ignored`;
277
+ const example = group.examples[0] && group.examples[0] !== group.key ? `; e.g. ${group.examples[0]}` : "";
278
+ return `${group.key} (${group.count} director${group.count === 1 ? "y" : "ies"}, ${state}${example})`;
279
+ });
280
+ }
281
+
189
282
  function proseList(items, empty = "none detected") {
190
283
  return items && items.length ? items.join(", ") : empty;
191
284
  }
@@ -194,6 +287,7 @@ function renderArchitectureSummary(ctx) {
194
287
  const apiLayer = ctx.backend.api.slice(0, 6);
195
288
  const dbLayer = ctx.backend.db.slice(0, 6);
196
289
  const frontendLayer = ctx.frontend.app.slice(0, 6);
290
+ const gaps = inferGaps(ctx);
197
291
  const readOrder = [
198
292
  "- Start here.",
199
293
  ctx.backendDetected ? "- For backend work, read `.prismo/backend-summary.md` next." : null,
@@ -234,6 +328,10 @@ function renderArchitectureSummary(ctx) {
234
328
  "",
235
329
  ctx.warnings.length ? ctx.warnings.map((warning) => `- ${warning}`).join("\n") : "- No major local context risks detected.",
236
330
  "",
331
+ "## Detection Gaps",
332
+ "",
333
+ gaps.length ? gaps.map((gap) => `- ${gap}`).join("\n") : "- No major architecture-detection gaps surfaced.",
334
+ "",
237
335
  "## AI Workflow Notes",
238
336
  "",
239
337
  "- Prefer this summary before broad repo reads.",
@@ -244,6 +342,7 @@ function renderArchitectureSummary(ctx) {
244
342
  }
245
343
 
246
344
  function renderBackendSummary(ctx) {
345
+ const backendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => file.path.endsWith(".py") && !isNonSourcePath(file.path)), ctx.scan.files, 8);
247
346
  return [
248
347
  "# Backend Summary",
249
348
  "",
@@ -273,10 +372,21 @@ function renderBackendSummary(ctx) {
273
372
  "",
274
373
  mdList(ctx.backend.config),
275
374
  "",
375
+ "## Load-Bearing Candidates",
376
+ "",
377
+ mdList(backendCandidates, "No Python source candidates detected."),
378
+ "",
379
+ "## Detection Notes",
380
+ "",
381
+ ctx.backend.api.length || ctx.backend.services.length
382
+ ? "- Backend paths were inferred from conventional directories plus root-level Python/FastAPI/service filename signals."
383
+ : "- No conventional backend paths were detected; inspect root-level Python modules and custom service naming manually.",
384
+ "",
276
385
  ].join("\n");
277
386
  }
278
387
 
279
388
  function renderFrontendSummary(ctx) {
389
+ const frontendCandidates = topLoadBearing(ctx.root, ctx.scan.files.filter((file) => /\.(tsx?|jsx?)$/.test(file.path) && /(^|\/)(frontend|src|app|components|hooks)\//.test(file.path)), ctx.scan.files, 8);
280
390
  return [
281
391
  "# Frontend Summary",
282
392
  "",
@@ -302,6 +412,10 @@ function renderFrontendSummary(ctx) {
302
412
  "",
303
413
  mdList(ctx.frontend.styling),
304
414
  "",
415
+ "## Load-Bearing Candidates",
416
+ "",
417
+ mdList(frontendCandidates, "No frontend source candidates detected."),
418
+ "",
305
419
  ].join("\n");
306
420
  }
307
421
 
@@ -319,7 +433,9 @@ function renderRecommendedClaude(ctx) {
319
433
  if (hasPath(ctx.scan, (rel) => rel === "frontend/package.json")) commands.push("cd frontend && npm run test");
320
434
  if (hasPath(ctx.scan, (rel) => rel === "backend/pytest.ini" || rel.startsWith("backend/tests/"))) commands.push("cd backend && pytest");
321
435
  return [
322
- "# CLAUDE.md",
436
+ "# CLAUDE.md Boilerplate",
437
+ "",
438
+ "Do not overwrite an existing curated CLAUDE.md with this file. Use it as a diff/reference for missing compact-context guidance only.",
323
439
  "",
324
440
  "Keep context small. Start with `.prismo/architecture-summary.md`; use scoped `.prismo/*-summary.md` files only when relevant.",
325
441
  "",
@@ -351,7 +467,9 @@ function renderRecommendedClaude(ctx) {
351
467
 
352
468
  function renderRecommendedAgents(ctx) {
353
469
  return [
354
- "# AGENTS.md",
470
+ "# AGENTS.md Boilerplate",
471
+ "",
472
+ "Do not overwrite an existing curated AGENTS.md with this file. Use it as a diff/reference for missing compact-context guidance only.",
355
473
  "",
356
474
  "Use `.prismo/architecture-summary.md` first to avoid repeated broad repo exploration. Keep this file durable and short; task-specific details belong in the prompt or scoped context files.",
357
475
  "",
@@ -413,7 +531,7 @@ function renderOptimizeReport(ctx, generatedFiles) {
413
531
  "",
414
532
  "## Token-Heavy Directories",
415
533
  "",
416
- mdList(ctx.scan.highRiskDirs.map((dir) => `${dir.path}/${dir.exposed ? " (exposed)" : " (ignored)"}`)),
534
+ mdList(summarizeRiskyDirectories(ctx.scan.highRiskDirs)),
417
535
  "",
418
536
  "## Optimization Suggestions",
419
537
  "",
@@ -530,8 +648,8 @@ function renderContextCommand(ctx, scope = null) {
530
648
  function getOptimizePendingFiles(ctx) {
531
649
  const pending = [
532
650
  ["architecture-summary.md", renderArchitectureSummary(ctx)],
533
- ["recommended-CLAUDE.md", renderRecommendedClaude(ctx)],
534
- ["recommended-AGENTS.md", renderRecommendedAgents(ctx)],
651
+ ["recommended-CLAUDE.boilerplate.md", renderRecommendedClaude(ctx)],
652
+ ["recommended-AGENTS.boilerplate.md", renderRecommendedAgents(ctx)],
535
653
  ["recommended-.claudeignore", `${ctx.scan.recommendedClaudeIgnore.join("\n")}\n`],
536
654
  ["recommended-.cursorignore", `${ctx.scan.recommendedCursorIgnore.join("\n")}\n`],
537
655
  ["recommended-.gitignore-additions", renderGitignoreAdditions(ctx)],
@@ -60,25 +60,31 @@ function applyFixes(result, options = {}) {
60
60
  if (!result.hasClaudeIgnore) {
61
61
  if (!options.dryRun) fs.writeFileSync(claudeIgnorePath, `${result.recommendedClaudeIgnore.join("\n")}\n`, "utf8");
62
62
  actions.push(`${dryRunPrefix} .claudeignore`);
63
+ } else if (result.missingClaudeIgnoreSuggestions && result.missingClaudeIgnoreSuggestions.length === 0) {
64
+ actions.push("Skipped .claudeignore.prismo-suggested because existing .claudeignore already covers Prismo recommendations");
63
65
  } else {
64
66
  const backupPath = options.dryRun ? null : backupIfExists(suggestedClaudeIgnorePath);
65
- if (!options.dryRun) fs.writeFileSync(suggestedClaudeIgnorePath, `${result.recommendedClaudeIgnore.join("\n")}\n`, "utf8");
67
+ const suggestions = result.missingClaudeIgnoreSuggestions || result.recommendedClaudeIgnore;
68
+ if (!options.dryRun) fs.writeFileSync(suggestedClaudeIgnorePath, `${suggestions.join("\n")}\n`, "utf8");
66
69
  actions.push(`${dryRunPrefix} .claudeignore.prismo-suggested because .claudeignore already exists`);
67
70
  if (backupPath) actions.push(`Backed up existing .claudeignore.prismo-suggested to ${path.basename(backupPath)}`);
68
71
  }
69
72
  if (!result.hasCursorIgnore) {
70
73
  if (!options.dryRun) fs.writeFileSync(cursorIgnorePath, `${result.recommendedCursorIgnore.join("\n")}\n`, "utf8");
71
74
  actions.push(`${dryRunPrefix} .cursorignore`);
75
+ } else if (result.missingCursorIgnoreSuggestions && result.missingCursorIgnoreSuggestions.length === 0) {
76
+ actions.push("Skipped .cursorignore.prismo-suggested because existing .cursorignore already covers Prismo recommendations");
72
77
  } else {
73
78
  const backupPath = options.dryRun ? null : backupIfExists(suggestedCursorIgnorePath);
74
- if (!options.dryRun) fs.writeFileSync(suggestedCursorIgnorePath, `${result.recommendedCursorIgnore.join("\n")}\n`, "utf8");
79
+ const suggestions = result.missingCursorIgnoreSuggestions || result.recommendedCursorIgnore;
80
+ if (!options.dryRun) fs.writeFileSync(suggestedCursorIgnorePath, `${suggestions.join("\n")}\n`, "utf8");
75
81
  actions.push(`${dryRunPrefix} .cursorignore.prismo-suggested because .cursorignore already exists`);
76
82
  if (backupPath) actions.push(`Backed up existing .cursorignore.prismo-suggested to ${path.basename(backupPath)}`);
77
83
  }
78
84
  if (ignoresOnly) return actions;
79
85
 
80
- const report = options.dryRun ? { reportPath: path.join(result.root, "prismo-dev-report.md"), backupPath: null } : writeReport(result);
81
- actions.push(`${options.dryRun ? "Would generate" : "Generated"} ${path.basename(report.reportPath)}`);
86
+ const report = options.dryRun ? { reportPath: path.join(result.root, ".prismo", "prismo-dev-report.md"), backupPath: null } : writeReport(result);
87
+ actions.push(`${options.dryRun ? "Would generate" : "Generated"} .prismo/${path.basename(report.reportPath)}`);
82
88
  if (report.backupPath) actions.push(`Backed up existing report to ${path.basename(report.backupPath)}`);
83
89
 
84
90
  const claudeFile = result.instructionFiles.find((file) => file.isClaude);
@@ -92,10 +98,12 @@ function applyFixes(result, options = {}) {
92
98
 
93
99
  const hasCodexRisk = result.issues.some((issue) => issue.category === "codex_config");
94
100
  if (hasCodexRisk || result.instructionFiles.some((file) => file.path === "AGENTS.md" || file.path.startsWith(".codex/"))) {
95
- const codexPath = path.join(result.root, "prismo-AGENTS-recommendations.md");
101
+ const prismoDir = path.join(result.root, ".prismo");
102
+ if (!options.dryRun) fs.mkdirSync(prismoDir, { recursive: true });
103
+ const codexPath = path.join(prismoDir, "prismo-AGENTS-recommendations.md");
96
104
  const backupPath = options.dryRun ? null : backupIfExists(codexPath);
97
105
  if (!options.dryRun) fs.writeFileSync(codexPath, renderAgentsRecommendations(result), "utf8");
98
- actions.push(`${options.dryRun ? "Would generate" : "Generated"} prismo-AGENTS-recommendations.md`);
106
+ actions.push(`${options.dryRun ? "Would generate" : "Generated"} .prismo/prismo-AGENTS-recommendations.md`);
99
107
  if (backupPath) actions.push(`Backed up existing AGENTS recommendations to ${path.basename(backupPath)}`);
100
108
  }
101
109
  return actions;
@@ -94,7 +94,7 @@ function renderTerminalReport(result, options = {}) {
94
94
  ? "No matching local usage sessions were found; repo-risk estimates remain heuristic."
95
95
  : "Potential savings estimates are heuristic and local-only, not provider billing data.");
96
96
  lines.push("");
97
- lines.push(reportEnabled ? "Report: prismo-dev-report.md" : "Report: skipped (--no-report)");
97
+ lines.push(reportEnabled ? "Report: .prismo/prismo-dev-report.md" : "Report: skipped (--no-report)");
98
98
  return lines.join("\n");
99
99
  }
100
100
 
@@ -312,14 +312,22 @@ function renderMarkdownReport(result) {
312
312
  lines.push("");
313
313
  lines.push("## Recommended .claudeignore");
314
314
  lines.push("");
315
+ if (result.hasClaudeIgnore && result.missingClaudeIgnoreSuggestions && !result.missingClaudeIgnoreSuggestions.length) {
316
+ lines.push("Existing .claudeignore already covers Prismo recommendations.");
317
+ lines.push("");
318
+ }
315
319
  lines.push("```gitignore");
316
- result.recommendedClaudeIgnore.forEach((line) => lines.push(line));
320
+ (result.hasClaudeIgnore && result.missingClaudeIgnoreSuggestions ? result.missingClaudeIgnoreSuggestions : result.recommendedClaudeIgnore).forEach((line) => lines.push(line));
317
321
  lines.push("```");
318
322
  lines.push("");
319
323
  lines.push("## Recommended .cursorignore");
320
324
  lines.push("");
325
+ if (result.hasCursorIgnore && result.missingCursorIgnoreSuggestions && !result.missingCursorIgnoreSuggestions.length) {
326
+ lines.push("Existing .cursorignore already covers Prismo recommendations.");
327
+ lines.push("");
328
+ }
321
329
  lines.push("```gitignore");
322
- result.recommendedCursorIgnore.forEach((line) => lines.push(line));
330
+ (result.hasCursorIgnore && result.missingCursorIgnoreSuggestions ? result.missingCursorIgnoreSuggestions : result.recommendedCursorIgnore).forEach((line) => lines.push(line));
323
331
  lines.push("```");
324
332
  lines.push("");
325
333
  lines.push("## Recommended Next Steps");
@@ -342,7 +350,9 @@ function backupIfExists(filePath) {
342
350
  }
343
351
 
344
352
  function writeReport(result) {
345
- const reportPath = path.join(result.root, "prismo-dev-report.md");
353
+ const prismoDir = path.join(result.root, ".prismo");
354
+ fs.mkdirSync(prismoDir, { recursive: true });
355
+ const reportPath = path.join(prismoDir, "prismo-dev-report.md");
346
356
  const backupPath = backupIfExists(reportPath);
347
357
  fs.writeFileSync(reportPath, renderMarkdownReport(result), "utf8");
348
358
  return { reportPath, backupPath };
@@ -77,6 +77,26 @@ function isIgnored(relPath, patterns, isDir = false) {
77
77
  return patterns.some((pattern) => patternMatches(pattern, relPath, isDir));
78
78
  }
79
79
 
80
+ function ignoreSuggestionCovered(pattern, existingPatterns) {
81
+ if (!pattern) return true;
82
+ if (existingPatterns.includes(pattern)) return true;
83
+ const sample = pattern
84
+ .replace(/^\*\//, "")
85
+ .replace(/^\*\*/, "sample")
86
+ .replace(/\*/g, "sample")
87
+ .replace(/\/$/, "");
88
+ const isDir = pattern.endsWith("/") || pattern.endsWith("/**");
89
+ return existingPatterns.some((existing) => {
90
+ if (existing === pattern) return true;
91
+ if (existing.endsWith("/") && pattern.startsWith(existing)) return true;
92
+ return patternMatches(existing, sample, isDir);
93
+ });
94
+ }
95
+
96
+ function missingIgnoreSuggestions(recommended, existingPatterns) {
97
+ return recommended.filter((pattern) => !ignoreSuggestionCovered(pattern, existingPatterns));
98
+ }
99
+
80
100
  function getFileKind(filePath) {
81
101
  const ext = path.extname(filePath).toLowerCase();
82
102
  const name = path.basename(filePath).toLowerCase();
@@ -703,6 +723,8 @@ function toJsonPayload(result) {
703
723
  proxyTrackingReadiness: result.proxyTrackingReadiness,
704
724
  suggestedClaudeIgnore: result.recommendedClaudeIgnore,
705
725
  suggestedCursorIgnore: result.recommendedCursorIgnore,
726
+ missingClaudeIgnoreSuggestions: result.missingClaudeIgnoreSuggestions || [],
727
+ missingCursorIgnoreSuggestions: result.missingCursorIgnoreSuggestions || [],
706
728
  nextCommands: getNextCommands(result),
707
729
  generatedAt: result.generatedAt,
708
730
  scannedPath: result.root,
@@ -1060,10 +1082,10 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
1060
1082
  const recommendedCursorIgnore = Array.from(new Set([
1061
1083
  ...recommendedClaudeIgnore,
1062
1084
  ".prismo/",
1063
- "prismo-dev-report.md",
1064
1085
  "prismo-optimized-CLAUDE.template.md",
1065
- "prismo-AGENTS-recommendations.md",
1066
1086
  ]));
1087
+ const missingClaudeIgnoreSuggestions = hasClaudeIgnore ? missingIgnoreSuggestions(recommendedClaudeIgnore, claudeIgnorePatterns) : recommendedClaudeIgnore;
1088
+ const missingCursorIgnoreSuggestions = hasCursorIgnore ? missingIgnoreSuggestions(recommendedCursorIgnore, cursorIgnorePatterns) : recommendedCursorIgnore;
1067
1089
  const recommendations = buildRecommendations({
1068
1090
  hasClaudeIgnore,
1069
1091
  gitignorePatterns,
@@ -1105,6 +1127,8 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
1105
1127
  repoDetected,
1106
1128
  recommendedClaudeIgnore,
1107
1129
  recommendedCursorIgnore,
1130
+ missingClaudeIgnoreSuggestions,
1131
+ missingCursorIgnoreSuggestions,
1108
1132
  topTokenLeaks: getTopTokenLeaks(issues),
1109
1133
  generatedAt: new Date().toISOString(),
1110
1134
  };
@@ -304,7 +304,7 @@ Options:
304
304
  --json Output valid JSON only for CI or future dashboard ingestion.
305
305
  --usage Include real local Codex/Claude Code session usage in scan diagnostics.
306
306
  --simple Print a plain-English scan summary for first-time or non-technical users.
307
- --no-report Do not write prismo-dev-report.md.
307
+ --no-report Do not write .prismo/prismo-dev-report.md.
308
308
  --limit N Number of recent local sessions to show.
309
309
  --interval N Refresh interval in seconds for watch mode.
310
310
  --dry-run Preview doctor/fix actions without writing files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",