getprismo 0.1.16 → 0.1.18

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
@@ -52,6 +52,7 @@ agent-native npx getprismo mcp
52
52
  - repeated file reads (same file loaded 100+ times in one session)
53
53
  - repeated commands (agent running the same command in a loop)
54
54
  - high context risk sessions that should have been split at task boundaries
55
+ - session-derived ignore candidates from actual Claude/Codex logs (`logs/debug.log`, `dist/app.js`, `package-lock.json`, source-stream dumps)
55
56
 
56
57
  ---
57
58
 
@@ -96,6 +97,8 @@ Next:
96
97
 
97
98
  doctor went from 79 to 91 in one run. the repo now has proper ignore files, compact context packs, and a clear starting point for the next coding session.
98
99
 
100
+ `scan --usage` and `doctor` can also turn real session leaks into concrete ignore suggestions. if local Claude/Codex logs show `logs/debug.log`, `dist/app.js`, `package-lock.json`, source-stream dumps, or other noisy files repeatedly entering context, prismodev adds conservative `.claudeignore` / `.cursorignore` candidate rules instead of only reporting the problem.
101
+
99
102
  ---
100
103
 
101
104
  ## real output: watch
@@ -806,6 +809,7 @@ npx getprismo doctor --help
806
809
  npx getprismo watch --help
807
810
  npx getprismo shield --help
808
811
  npx getprismo mcp --help
812
+ npx getprismo mcp doctor
809
813
  npx getprismo cc --help
810
814
  npx getprismo scan --help
811
815
  ```
package/docs/live-demo.md CHANGED
@@ -12,6 +12,7 @@ Shows:
12
12
 
13
13
  - before/after repo score
14
14
  - missing `.claudeignore` / `.cursorignore`
15
+ - ignore suggestions derived from actual local Claude/Codex session leaks
15
16
  - generated artifacts exposed to AI context
16
17
  - compact `.prismo/` context packs
17
18
  - recommended next starting context
package/docs/mcp.md CHANGED
@@ -8,6 +8,16 @@ PrismoDev can run as a local MCP server so compatible coding agents can inspect
8
8
  npx getprismo mcp /path/to/your/repo
9
9
  ```
10
10
 
11
+ ## Doctor
12
+
13
+ Before adding PrismoDev to a client, validate the local MCP surface:
14
+
15
+ ```bash
16
+ npx getprismo mcp doctor /path/to/your/repo
17
+ ```
18
+
19
+ This checks that the MCP server can expose all Prismo tools, runs a scan smoke test, and prints a ready-to-use client config snippet.
20
+
11
21
  ## Generic MCP Config
12
22
 
13
23
  ```json
@@ -258,7 +258,96 @@ function runMcpServer(deps) {
258
258
  });
259
259
  }
260
260
 
261
+ async function runMcpDoctor(deps) {
262
+ const { rootDir, packageVersion = "0.0.0" } = deps;
263
+ const { tools, callTool } = createMcpTools(deps);
264
+ const requiredTools = [
265
+ "prismo_scan",
266
+ "prismo_doctor_dry_run",
267
+ "prismo_watch_snapshot",
268
+ "prismo_shield_run",
269
+ "prismo_shield_search",
270
+ "prismo_shield_last",
271
+ "prismo_context_pack",
272
+ "prismo_firewall",
273
+ "prismo_cc_timeline",
274
+ ];
275
+ const toolNames = tools.map((tool) => tool.name);
276
+ const missingTools = requiredTools.filter((name) => !toolNames.includes(name));
277
+ const scanResult = await callTool("prismo_scan", { path: rootDir, includeUsage: false, limit: 1 });
278
+ let scanPayload = {};
279
+ try {
280
+ scanPayload = JSON.parse(scanResult.content?.[0]?.text || "{}");
281
+ } catch {
282
+ scanPayload = {};
283
+ }
284
+ const config = {
285
+ mcpServers: {
286
+ prismodev: {
287
+ command: "npx",
288
+ args: ["-y", "getprismo", "mcp", rootDir],
289
+ },
290
+ },
291
+ };
292
+ return {
293
+ schemaVersion: 1,
294
+ ok: missingTools.length === 0 && Boolean(scanPayload.schemaVersion),
295
+ server: {
296
+ name: "prismodev",
297
+ version: packageVersion,
298
+ transport: "stdio",
299
+ root: rootDir,
300
+ },
301
+ tools: {
302
+ count: tools.length,
303
+ required: requiredTools,
304
+ missing: missingTools,
305
+ hasShield: toolNames.includes("prismo_shield_run") && toolNames.includes("prismo_shield_search"),
306
+ },
307
+ smoke: {
308
+ scan: {
309
+ ok: Boolean(scanPayload.schemaVersion),
310
+ score: scanPayload.score ?? null,
311
+ riskLevel: scanPayload.riskLevel ?? null,
312
+ },
313
+ },
314
+ config,
315
+ next: [
316
+ "Add the config snippet to your MCP-compatible client.",
317
+ "Restart the client and confirm prismodev appears in the MCP tool list.",
318
+ "Ask the agent to call prismo_scan or prismo_shield_run.",
319
+ ],
320
+ };
321
+ }
322
+
323
+ function renderMcpDoctorTerminal(result) {
324
+ const lines = [];
325
+ lines.push("");
326
+ lines.push("Prismo MCP Doctor");
327
+ lines.push("");
328
+ lines.push(`Status: ${result.ok ? "ready" : "needs attention"}`);
329
+ lines.push(`Server: ${result.server.name}@${result.server.version}`);
330
+ lines.push(`Transport: ${result.server.transport}`);
331
+ lines.push(`Repo: ${result.server.root}`);
332
+ lines.push("");
333
+ lines.push("Checks");
334
+ lines.push(`- Tools exposed: ${result.tools.count}`);
335
+ lines.push(`- Required tools missing: ${result.tools.missing.length ? result.tools.missing.join(", ") : "none"}`);
336
+ lines.push(`- Shield tools: ${result.tools.hasShield ? "ready" : "missing"}`);
337
+ lines.push(`- Scan smoke: ${result.smoke.scan.ok ? `ok (${result.smoke.scan.score}/100, ${result.smoke.scan.riskLevel})` : "failed"}`);
338
+ lines.push("");
339
+ lines.push("MCP config");
340
+ lines.push("");
341
+ lines.push(JSON.stringify(result.config, null, 2));
342
+ lines.push("");
343
+ lines.push("Next");
344
+ result.next.forEach((step, index) => lines.push(`${index + 1}. ${step}`));
345
+ return lines.join("\n");
346
+ }
347
+
261
348
  module.exports = {
262
349
  createMcpTools,
350
+ renderMcpDoctorTerminal,
351
+ runMcpDoctor,
263
352
  runMcpServer,
264
353
  };
@@ -277,6 +277,16 @@ function renderMarkdownReport(result) {
277
277
  });
278
278
  lines.push("");
279
279
  }
280
+ if (result.sessionIgnoreSuggestions && result.sessionIgnoreSuggestions.length) {
281
+ lines.push("## Session-Derived Ignore Suggestions");
282
+ lines.push("");
283
+ lines.push("These rules came from local Claude/Codex session logs where noisy paths already entered context.");
284
+ lines.push("");
285
+ result.sessionIgnoreSuggestions.slice(0, 20).forEach((item) => {
286
+ lines.push(`- \`${item.pattern}\` - ${item.reason} (${item.count} mention${item.count === 1 ? "" : "s"}${item.examples.length ? `; e.g. ${item.examples[0]}` : ""})`);
287
+ });
288
+ lines.push("");
289
+ }
280
290
  lines.push("## Issues");
281
291
  lines.push("");
282
292
  if (!result.issues.length) {
@@ -97,6 +97,128 @@ function missingIgnoreSuggestions(recommended, existingPatterns) {
97
97
  return recommended.filter((pattern) => !ignoreSuggestionCovered(pattern, existingPatterns));
98
98
  }
99
99
 
100
+ const SESSION_NOISE_DIRS = new Set([
101
+ ".next",
102
+ ".nuxt",
103
+ ".prismo",
104
+ ".pytest_cache",
105
+ ".turbo",
106
+ "__pycache__",
107
+ "build",
108
+ "calendar-dumps",
109
+ "coverage",
110
+ "dist",
111
+ "event-dumps",
112
+ "events",
113
+ "exports",
114
+ "htmlcov",
115
+ "inbox-dumps",
116
+ "logs",
117
+ "models",
118
+ "node_modules",
119
+ "out",
120
+ "playwright-report",
121
+ "session-dumps",
122
+ "source-streams",
123
+ "state-backups",
124
+ "test-results",
125
+ "tmp",
126
+ "temp",
127
+ ]);
128
+
129
+ const SESSION_NOISE_FILE_NAMES = new Set([
130
+ "package-lock.json",
131
+ "pnpm-lock.yaml",
132
+ "yarn.lock",
133
+ "bun.lockb",
134
+ "coverage-final.json",
135
+ "lcov.info",
136
+ ]);
137
+
138
+ const SESSION_NOISE_EXTENSIONS = new Set([
139
+ ".db",
140
+ ".jsonl",
141
+ ".lock",
142
+ ".log",
143
+ ".sqlite",
144
+ ".sqlite3",
145
+ ]);
146
+
147
+ function cleanSessionPath(value) {
148
+ const text = String(value || "").trim().replace(/\\/g, "/");
149
+ if (!text || /^https?:\/\//.test(text)) return null;
150
+ const withoutQuotes = text.replace(/^["'`]+|["'`.,:;)\]]+$/g, "");
151
+ if (!withoutQuotes || withoutQuotes.includes("\n")) return null;
152
+ const markerIndex = withoutQuotes.indexOf("/Users/");
153
+ if (markerIndex > 0) return withoutQuotes.slice(markerIndex);
154
+ return withoutQuotes;
155
+ }
156
+
157
+ function sessionIgnorePatternForPath(value, root) {
158
+ const cleaned = cleanSessionPath(value);
159
+ if (!cleaned) return null;
160
+ const rootNormalized = normalizeRel(root);
161
+ let rel = cleaned;
162
+ if (path.isAbsolute(cleaned)) {
163
+ const normalized = normalizeRel(cleaned);
164
+ if (!normalized.startsWith(`${rootNormalized}/`)) return null;
165
+ rel = normalizeRel(path.relative(root, cleaned));
166
+ }
167
+ rel = normalizeRel(rel).replace(/^\.\//, "");
168
+ if (!rel || rel === "." || rel.startsWith("../") || rel.includes("..")) return null;
169
+
170
+ const segments = rel.split("/").filter(Boolean);
171
+ if (!segments.length) return null;
172
+ for (let index = 0; index < segments.length; index += 1) {
173
+ const segment = segments[index];
174
+ if (SESSION_NOISE_DIRS.has(segment)) {
175
+ return `${segments.slice(0, index + 1).join("/")}/`;
176
+ }
177
+ }
178
+
179
+ const fileName = segments[segments.length - 1];
180
+ const lowerName = fileName.toLowerCase();
181
+ const ext = path.extname(lowerName);
182
+ if (SESSION_NOISE_FILE_NAMES.has(lowerName)) return fileName;
183
+ if (SESSION_NOISE_EXTENSIONS.has(ext)) return rel;
184
+ if (/_state\.json$/i.test(fileName)) return "*_state.json";
185
+ if (/_tokens\.json$/i.test(fileName)) return "*_tokens.json";
186
+ if (/_export\.json$/i.test(fileName)) return "*_export.json";
187
+ if (/secret|credential|token/i.test(fileName) && /\.json$/i.test(fileName)) return rel;
188
+ return null;
189
+ }
190
+
191
+ function buildSessionIgnoreSuggestions(realUsage, root) {
192
+ if (!realUsage || !Array.isArray(realUsage.sessions)) return [];
193
+ const byPattern = new Map();
194
+ const add = (pattern, item, source, reason) => {
195
+ if (!pattern) return;
196
+ const existing = byPattern.get(pattern) || {
197
+ pattern,
198
+ source,
199
+ reason,
200
+ count: 0,
201
+ examples: [],
202
+ };
203
+ existing.count += Number(item?.count || 1);
204
+ const example = item?.value || item?.path || pattern;
205
+ if (example && !existing.examples.includes(example) && existing.examples.length < 3) existing.examples.push(example);
206
+ byPattern.set(pattern, existing);
207
+ };
208
+
209
+ for (const session of realUsage.sessions) {
210
+ for (const item of session.generatedArtifacts || []) {
211
+ add(sessionIgnorePatternForPath(item.value, root), item, session.tool || "session", "Generated artifact entered local session context.");
212
+ }
213
+ for (const item of session.repeatedPathMentions || []) {
214
+ add(sessionIgnorePatternForPath(item.value, root), item, session.tool || "session", "Noisy path appeared repeatedly in local session context.");
215
+ }
216
+ }
217
+ return Array.from(byPattern.values())
218
+ .sort((a, b) => b.count - a.count || a.pattern.localeCompare(b.pattern))
219
+ .slice(0, 25);
220
+ }
221
+
100
222
  function getFileKind(filePath) {
101
223
  const ext = path.extname(filePath).toLowerCase();
102
224
  const name = path.basename(filePath).toLowerCase();
@@ -775,6 +897,7 @@ function toJsonPayload(result) {
775
897
  optimizationStack: result.optimizationStack,
776
898
  toolOutputRisk: result.toolOutputRisk,
777
899
  operationalNoise: result.operationalNoise,
900
+ sessionIgnoreSuggestions: result.sessionIgnoreSuggestions || [],
778
901
  proxyTrackingReadiness: result.proxyTrackingReadiness,
779
902
  suggestedClaudeIgnore: result.recommendedClaudeIgnore,
780
903
  suggestedCursorIgnore: result.recommendedCursorIgnore,
@@ -1061,7 +1184,19 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
1061
1184
  }
1062
1185
 
1063
1186
  const realUsage = options.includeUsage ? getUsageSummary({ tool: options.usageTool || "all", cwd: root, limit: options.usageLimit || 5 }) : null;
1187
+ const sessionIgnoreSuggestions = buildSessionIgnoreSuggestions(realUsage, root);
1064
1188
  addRealUsageIssues(issues, realUsage);
1189
+ if (sessionIgnoreSuggestions.length) {
1190
+ addIssue(
1191
+ issues,
1192
+ "medium",
1193
+ "ignore_file",
1194
+ `${sessionIgnoreSuggestions.length} session-derived ignore suggestion${sessionIgnoreSuggestions.length === 1 ? "" : "s"}`,
1195
+ sessionIgnoreSuggestions.slice(0, 5).map((item) => `${item.pattern} (${item.count}x)`).join(", "),
1196
+ "Review the generated .claudeignore/.cursorignore suggestions from actual local session context.",
1197
+ "Likely avoidable token exposure: these paths already appeared in local coding-agent context."
1198
+ );
1199
+ }
1065
1200
  const optimizationStack = detectOptimizationStack(root, claudeConfig, codexConfig);
1066
1201
  const agentReadiness = detectAgentReadiness(root, claudeConfig, codexConfig, realUsage);
1067
1202
  const toolOutputRisk = detectToolOutputRisk({ exposedLargeFiles, exposedHighRiskDirs, highRiskDirs });
@@ -1145,11 +1280,13 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
1145
1280
  .filter((file) => file.size >= 1024 * 1024 || ["log", "json", "minified", "lock/generated"].includes(file.kind))
1146
1281
  .map((file) => file.path);
1147
1282
  const operationalNoiseSuggestions = operationalNoise.files.map((file) => file.path);
1283
+ const sessionIgnorePatterns = sessionIgnoreSuggestions.map((item) => item.pattern);
1148
1284
  const recommendedClaudeIgnore = Array.from(new Set([
1149
1285
  ...DEFAULT_CLAUDEIGNORE,
1150
1286
  ...gitignorePatterns.filter((line) => !line.startsWith("!")),
1151
1287
  ...largeFileSuggestions,
1152
1288
  ...operationalNoiseSuggestions,
1289
+ ...sessionIgnorePatterns,
1153
1290
  ]));
1154
1291
  const recommendedCursorIgnore = Array.from(new Set([
1155
1292
  ...recommendedClaudeIgnore,
@@ -1203,6 +1340,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
1203
1340
  recommendedCursorIgnore,
1204
1341
  missingClaudeIgnoreSuggestions,
1205
1342
  missingCursorIgnoreSuggestions,
1343
+ sessionIgnoreSuggestions,
1206
1344
  topTokenLeaks: getTopTokenLeaks(issues),
1207
1345
  generatedAt: new Date().toISOString(),
1208
1346
  };
@@ -186,7 +186,7 @@ function looksLikeUsefulPath(relPath) {
186
186
  function extractMentionedPaths(text, cwd = "") {
187
187
  const found = new Set();
188
188
  const source = String(text || "");
189
- const pathPattern = /(?:^|[\s"'`])((?:\.{0,2}\/)?(?:[\w .@-]+\/)+[\w .@+-]+\.[A-Za-z0-9]{1,12})/g;
189
+ const pathPattern = /(?:^|[\s"'`])((?:\.{0,2}\/)?(?:[\w.@-]+\/)+[\w.@+-]+\.[A-Za-z0-9]{1,12})/g;
190
190
  const filePattern = /(?:^|[\s"'`])((?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|coverage-final\.json|tsconfig\.json|pyproject\.toml|requirements\.txt|README\.md|CLAUDE\.md|AGENTS\.md))/g;
191
191
  for (const pattern of [pathPattern, filePattern]) {
192
192
  let match;
@@ -283,7 +283,11 @@ const {
283
283
  color,
284
284
  });
285
285
 
286
- const { runMcpServer } = require("./prismo-dev/mcp");
286
+ const {
287
+ renderMcpDoctorTerminal,
288
+ runMcpDoctor,
289
+ runMcpServer,
290
+ } = require("./prismo-dev/mcp");
287
291
 
288
292
  function printHelp() {
289
293
  console.log(`Prismo CLI
@@ -298,6 +302,7 @@ Usage:
298
302
  prismo shield last [--json] [--limit N] [path]
299
303
  prismo shield search <query> [--json] [--limit N] [path]
300
304
  prismo mcp [path]
305
+ prismo mcp doctor [--json] [path]
301
306
  prismo setup [--json] [--proxy-url URL] [path]
302
307
  prismo scan [--fix] [--ci] [--json] [--usage] [--simple] [--no-report] [path]
303
308
  prismo optimize [scope] [--json] [path]
@@ -521,10 +526,13 @@ Output:
521
526
 
522
527
  Usage:
523
528
  prismo mcp [path]
529
+ prismo mcp doctor [--json] [path]
524
530
 
525
531
  Examples:
526
532
  prismo mcp
527
533
  prismo mcp /path/to/repo
534
+ prismo mcp doctor
535
+ prismo mcp doctor --json
528
536
 
529
537
  Tools exposed:
530
538
  prismo_scan
@@ -538,7 +546,9 @@ Tools exposed:
538
546
  prismo_cc_timeline
539
547
 
540
548
  Output:
541
- 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.`,
549
+ 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.
550
+
551
+ prismo mcp doctor validates the local MCP tool surface and prints a ready-to-use client config snippet.`,
542
552
  ci: `Prismo CI
543
553
 
544
554
  Usage:
@@ -707,8 +717,11 @@ async function runCli(argv) {
707
717
  }
708
718
 
709
719
  if (command === "mcp") {
710
- const target = getPositionals(rest)[0] || process.cwd();
711
- runMcpServer({
720
+ const json = rest.includes("--json");
721
+ const positional = getPositionals(rest);
722
+ const subcommand = positional[0] === "doctor" ? "doctor" : "server";
723
+ const target = subcommand === "doctor" ? positional[1] || process.cwd() : positional[0] || process.cwd();
724
+ const mcpDeps = {
712
725
  rootDir: path.resolve(target),
713
726
  packageVersion: PACKAGE_VERSION,
714
727
  scanRepo,
@@ -724,7 +737,14 @@ async function runCli(argv) {
724
737
  runShield,
725
738
  runShieldLast,
726
739
  runShieldSearch,
727
- });
740
+ };
741
+ if (subcommand === "doctor") {
742
+ const result = await runMcpDoctor(mcpDeps);
743
+ if (json) console.log(JSON.stringify(result, null, 2));
744
+ else console.log(renderMcpDoctorTerminal(result));
745
+ return;
746
+ }
747
+ runMcpServer(mcpDeps);
728
748
  return;
729
749
  }
730
750
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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",