getprismo 0.1.21 → 0.1.23

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
@@ -180,6 +180,33 @@ Recommended Stack
180
180
 
181
181
  This makes PrismoDev the measure-first layer: it tells you whether you need ignore cleanup, output sandboxing, code indexing, repo packing, instruction trimming, session splitting, or MCP/tool hygiene.
182
182
 
183
+ For the short version:
184
+
185
+ ```bash
186
+ npx getprismo scan --report-card
187
+ ```
188
+
189
+ That prints the simplest decision:
190
+
191
+ ```text
192
+ PrismoDev Report Card
193
+
194
+ Biggest waste: Generated artifacts / ignore cleanup: High
195
+ Start with: npx getprismo doctor --apply-suggestions --dry-run
196
+ Then: npx getprismo shield -- <noisy command>
197
+ Code index needed: not yet
198
+ Round-trip risk: Low
199
+ ```
200
+
201
+ To benchmark a noisy command:
202
+
203
+ ```bash
204
+ npx getprismo benchmark -- npm test
205
+ npx getprismo benchmark session
206
+ ```
207
+
208
+ `benchmark -- <command>` measures raw command output tokens versus the compact shield summary. `benchmark session` summarizes recent local Claude/Codex sessions, including round-trip context signals like tool calls, repeated commands, repeated source reads, and MCP/tool surface.
209
+
183
210
  ---
184
211
 
185
212
  ## new: context shield
@@ -580,6 +607,8 @@ no install needed. npx runs it directly.
580
607
  | `cc timeline` | session reconstruction with events |
581
608
  | `scan --usage` | full repo scan with local usage data |
582
609
  | `scan --optimizer-fit` | recommend which token-optimization path fits your repo/session |
610
+ | `scan --report-card` | shortest decision-layer summary |
611
+ | `benchmark` | measure command-output reduction or recent session round-trip context |
583
612
  | `scan --simple` | plain-english summary |
584
613
  | `scan --fix` | create safe fix files |
585
614
  | `scan --ci` | fail CI when token-risk gates fail |
@@ -835,8 +864,11 @@ lib/prismo-dev/fixes.js safe ignore/template generation
835
864
  lib/prismo-dev/mcp.js local MCP server and Prismo tool bindings
836
865
  lib/prismo-dev/report.js terminal, markdown, ci reports
837
866
  lib/prismo-dev/scan.js repo scanning, scoring, readiness
867
+ lib/prismo-dev/scan-path-utils.js scan ignore/path helper logic
838
868
  lib/prismo-dev/shield.js local command shield and searchable output index
869
+ lib/prismo-dev/usage-log-utils.js local session log parsing helpers
839
870
  lib/prismo-dev/usage-watch.js local logs, watch, cost, timeline
871
+ lib/prismo-dev/utils.js shared terminal/file/token helpers
840
872
  ```
841
873
 
842
874
  ---
@@ -0,0 +1,122 @@
1
+ module.exports = function createBenchmark(deps) {
2
+ const {
3
+ NPX_COMMAND,
4
+ estimateTokens,
5
+ formatTokenCount,
6
+ getUsageSummary,
7
+ runShield,
8
+ scanRepo,
9
+ color,
10
+ } = deps;
11
+
12
+ function estimateSummaryTokens(shieldResult) {
13
+ const lines = shieldResult.output && Array.isArray(shieldResult.output.interestingLines)
14
+ ? shieldResult.output.interestingLines.join("\n")
15
+ : "";
16
+ return estimateTokens([
17
+ `Command: ${shieldResult.command}`,
18
+ `Exit: ${shieldResult.exitCode}`,
19
+ `Duration: ${shieldResult.durationMs}ms`,
20
+ lines,
21
+ ].join("\n"));
22
+ }
23
+
24
+ function runBenchmark(rootDir = process.cwd(), commandArgs = [], options = {}) {
25
+ if (options.sessionOnly || !commandArgs.length) {
26
+ const scan = scanRepo(rootDir, { includeUsage: true, usageLimit: options.limit || 5 });
27
+ const usage = getUsageSummary({ cwd: scan.root, limit: options.limit || 5, tool: "all" });
28
+ const fit = scan.optimizerFit;
29
+ return {
30
+ schemaVersion: 1,
31
+ mode: "session",
32
+ scannedPath: scan.root,
33
+ score: scan.score,
34
+ riskLevel: scan.risk,
35
+ sessions: usage.sessions.length,
36
+ tokens: usage.totals,
37
+ roundTripContext: fit.roundTripContext,
38
+ optimizerFit: fit.summary,
39
+ recommendedStack: fit.recommendedStack,
40
+ next: [`${NPX_COMMAND} scan --optimizer-fit`, `${NPX_COMMAND} watch --once`, `${NPX_COMMAND} cc timeline`],
41
+ generatedAt: new Date().toISOString(),
42
+ };
43
+ }
44
+
45
+ const shield = runShield(rootDir, commandArgs);
46
+ const rawTokens = shield.output.estimatedTokens || 0;
47
+ const summaryTokens = estimateSummaryTokens(shield);
48
+ const reductionPercent = rawTokens > 0 ? Math.max(0, Math.round(((rawTokens - summaryTokens) / rawTokens) * 100)) : 0;
49
+ return {
50
+ schemaVersion: 1,
51
+ mode: "command",
52
+ scannedPath: shield.cwd,
53
+ command: shield.command,
54
+ exitCode: shield.exitCode,
55
+ durationMs: shield.durationMs,
56
+ rawOutput: {
57
+ bytes: shield.output.totalBytes,
58
+ estimatedTokens: rawTokens,
59
+ },
60
+ shieldedSummary: {
61
+ estimatedTokens: summaryTokens,
62
+ interestingLines: shield.output.interestingLines,
63
+ },
64
+ estimatedTokenReductionPercent: reductionPercent,
65
+ stored: shield.stored,
66
+ next: [
67
+ "Give the shield summary to the agent first.",
68
+ `${NPX_COMMAND} shield search "<error text>"`,
69
+ `${NPX_COMMAND} scan --optimizer-fit`,
70
+ ],
71
+ generatedAt: new Date().toISOString(),
72
+ };
73
+ }
74
+
75
+ function renderBenchmarkTerminal(result) {
76
+ const lines = [];
77
+ lines.push("");
78
+ lines.push(color("Prismo Benchmark", "bold"));
79
+ lines.push("");
80
+ if (result.mode === "session") {
81
+ lines.push(`Mode: session`);
82
+ lines.push(`Score: ${result.score}/100 (${result.riskLevel} risk)`);
83
+ lines.push(`Sessions: ${result.sessions}`);
84
+ lines.push(`Tokens: ${formatTokenCount(result.tokens.displayTokens || 0)} (${result.tokens.exactTokens ? "exact-local-log" : "estimated/local"})`);
85
+ lines.push(`Optimizer fit: ${result.optimizerFit}`);
86
+ lines.push("");
87
+ lines.push("Round-Trip Context:");
88
+ lines.push(`- Level: ${result.roundTripContext.level}`);
89
+ lines.push(`- Tool calls: ${result.roundTripContext.toolCalls}`);
90
+ lines.push(`- Repeated commands: ${result.roundTripContext.repeatedCommandMentions}`);
91
+ lines.push(`- Repeated source reads: ${result.roundTripContext.repeatedSourceReads}`);
92
+ lines.push("");
93
+ lines.push("Next:");
94
+ result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
95
+ return lines.join("\n");
96
+ }
97
+
98
+ lines.push(`Mode: command`);
99
+ lines.push(`Command: ${result.command}`);
100
+ lines.push(`Exit: ${result.exitCode}`);
101
+ lines.push(`Duration: ${result.durationMs}ms`);
102
+ lines.push(`Raw output: ${result.rawOutput.bytes.toLocaleString()} bytes (~${result.rawOutput.estimatedTokens.toLocaleString()} tokens)`);
103
+ lines.push(`Shield summary: ~${result.shieldedSummary.estimatedTokens.toLocaleString()} tokens`);
104
+ lines.push(`Estimated reduction: ${result.estimatedTokenReductionPercent}%`);
105
+ lines.push("");
106
+ lines.push("Stored Output:");
107
+ lines.push(`- ${result.stored.stdout}`);
108
+ lines.push(`- ${result.stored.stderr}`);
109
+ lines.push("");
110
+ lines.push("Useful Lines:");
111
+ result.shieldedSummary.interestingLines.slice(0, 12).forEach((line) => lines.push(`- ${line}`));
112
+ lines.push("");
113
+ lines.push("Next:");
114
+ result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
115
+ return lines.join("\n");
116
+ }
117
+
118
+ return {
119
+ renderBenchmarkTerminal,
120
+ runBenchmark,
121
+ };
122
+ };
@@ -141,11 +141,47 @@ function renderOptimizerFitTerminal(result, options = {}) {
141
141
  lines.push(` ${item.reason}`);
142
142
  });
143
143
  lines.push("");
144
+ lines.push(color("Round-Trip Context", "bold", useColor));
145
+ lines.push(`- Level: ${fit.roundTripContext.level}`);
146
+ lines.push(`- Tool calls: ${fit.roundTripContext.toolCalls}`);
147
+ lines.push(`- Repeated command mentions: ${fit.roundTripContext.repeatedCommandMentions}`);
148
+ lines.push(`- Repeated source reads: ${fit.roundTripContext.repeatedSourceReads}`);
149
+ lines.push(`- MCP/tool servers: ${fit.roundTripContext.mcpServers}`);
150
+ lines.push(`- ${fit.roundTripContext.recommendation}`);
151
+ lines.push("");
144
152
  lines.push("Notes:");
145
153
  fit.caveats.forEach((caveat) => lines.push(`- ${caveat}`));
146
154
  return lines.join("\n");
147
155
  }
148
156
 
157
+ function renderReportCardTerminal(result, options = {}) {
158
+ const useColor = options.color !== false;
159
+ const fit = result.optimizerFit;
160
+ const top = fit.recommendedStack[0];
161
+ const second = fit.recommendedStack[1];
162
+ const lines = [];
163
+ lines.push("");
164
+ lines.push(color("PrismoDev Report Card", "bold", useColor));
165
+ lines.push("");
166
+ lines.push(`Score: ${result.score}/100 (${result.risk} risk)`);
167
+ lines.push(`Biggest waste: ${fit.summary}`);
168
+ lines.push("");
169
+ if (top) lines.push(`Start with: ${top.command}`);
170
+ if (second) lines.push(`Then: ${second.command}`);
171
+ lines.push("");
172
+ const codeIndex = fit.bottlenecks.find((item) => item.id === "code-indexing");
173
+ const output = fit.bottlenecks.find((item) => item.id === "output-sandboxing");
174
+ const ignore = fit.bottlenecks.find((item) => item.id === "ignore-cleanup");
175
+ lines.push(`Code index needed: ${codeIndex && codeIndex.level === "High" ? "yes" : codeIndex && codeIndex.level === "Medium" ? "maybe" : "not yet"}`);
176
+ lines.push(`Output sandbox needed: ${output && output.level !== "Low" ? "yes" : "not yet"}`);
177
+ lines.push(`Ignore cleanup needed: ${ignore && ignore.level !== "Low" ? "yes" : "not urgent"}`);
178
+ lines.push(`Round-trip risk: ${fit.roundTripContext.level}`);
179
+ lines.push("");
180
+ lines.push("Why:");
181
+ fit.bottlenecks.slice(0, 3).forEach((item) => lines.push(`- ${item.label}: ${item.evidence[0]}`));
182
+ return lines.join("\n");
183
+ }
184
+
149
185
  function evaluateCi(result, options = {}) {
150
186
  const minScore = Number(options.minScore || 80);
151
187
  const failures = [];
@@ -434,6 +470,7 @@ function writeReport(result) {
434
470
  renderCiReport,
435
471
  renderMarkdownReport,
436
472
  renderOptimizerFitTerminal,
473
+ renderReportCardTerminal,
437
474
  renderSimpleScanReport,
438
475
  renderTerminalReport,
439
476
  writeReport,
@@ -0,0 +1,203 @@
1
+ module.exports = function createScanPathUtils(deps) {
2
+ const { fs, path } = deps;
3
+
4
+ function normalizeRel(value) {
5
+ return value.split(path.sep).join("/");
6
+ }
7
+
8
+ function readIgnoreFile(root, fileName) {
9
+ const filePath = path.join(root, fileName);
10
+ if (!fs.existsSync(filePath)) return [];
11
+ const text = fs.readFileSync(filePath, "utf8");
12
+ return text
13
+ .split(/\r?\n/)
14
+ .map((line) => line.trim())
15
+ .filter((line) => line && !line.startsWith("#"))
16
+ .map((line) => line.replace(/^!/, ""));
17
+ }
18
+
19
+ function patternMatches(pattern, relPath, isDir = false) {
20
+ const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\//, "");
21
+ const normalizedRel = normalizeRel(relPath);
22
+ const dirRel = isDir && !normalizedRel.endsWith("/") ? `${normalizedRel}/` : normalizedRel;
23
+
24
+ if (!normalizedPattern) return false;
25
+ if (normalizedPattern.endsWith("/")) {
26
+ const base = normalizedPattern.slice(0, -1);
27
+ return (
28
+ normalizedRel === base ||
29
+ normalizedRel.startsWith(`${base}/`) ||
30
+ normalizedRel.endsWith(`/${base}`) ||
31
+ normalizedRel.includes(`/${base}/`) ||
32
+ dirRel.includes(`/${base}/`)
33
+ );
34
+ }
35
+ if (normalizedPattern.startsWith("*.")) {
36
+ return normalizedRel.endsWith(normalizedPattern.slice(1));
37
+ }
38
+ if (normalizedPattern.includes("*")) {
39
+ const escaped = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
40
+ return new RegExp(`(^|/)${escaped}$`).test(normalizedRel);
41
+ }
42
+ return (
43
+ normalizedRel === normalizedPattern ||
44
+ dirRel === normalizedPattern ||
45
+ normalizedRel.startsWith(`${normalizedPattern}/`) ||
46
+ normalizedRel.endsWith(`/${normalizedPattern}`)
47
+ );
48
+ }
49
+
50
+ function isIgnored(relPath, patterns, isDir = false) {
51
+ return patterns.some((pattern) => patternMatches(pattern, relPath, isDir));
52
+ }
53
+
54
+ function ignoreSuggestionCovered(pattern, existingPatterns) {
55
+ if (!pattern) return true;
56
+ if (existingPatterns.includes(pattern)) return true;
57
+ const sample = pattern
58
+ .replace(/^\*\//, "")
59
+ .replace(/^\*\*/, "sample")
60
+ .replace(/\*/g, "sample")
61
+ .replace(/\/$/, "");
62
+ const isDir = pattern.endsWith("/") || pattern.endsWith("/**");
63
+ return existingPatterns.some((existing) => {
64
+ if (existing === pattern) return true;
65
+ if (existing.endsWith("/") && pattern.startsWith(existing)) return true;
66
+ return patternMatches(existing, sample, isDir);
67
+ });
68
+ }
69
+
70
+ function missingIgnoreSuggestions(recommended, existingPatterns) {
71
+ return recommended.filter((pattern) => !ignoreSuggestionCovered(pattern, existingPatterns));
72
+ }
73
+
74
+ const SESSION_NOISE_DIRS = new Set([
75
+ ".next",
76
+ ".nuxt",
77
+ ".prismo",
78
+ ".pytest_cache",
79
+ ".turbo",
80
+ "__pycache__",
81
+ "build",
82
+ "calendar-dumps",
83
+ "coverage",
84
+ "dist",
85
+ "event-dumps",
86
+ "events",
87
+ "exports",
88
+ "htmlcov",
89
+ "inbox-dumps",
90
+ "logs",
91
+ "models",
92
+ "node_modules",
93
+ "out",
94
+ "playwright-report",
95
+ "session-dumps",
96
+ "source-streams",
97
+ "state-backups",
98
+ "test-results",
99
+ "tmp",
100
+ "temp",
101
+ ]);
102
+
103
+ const SESSION_NOISE_FILE_NAMES = new Set([
104
+ "package-lock.json",
105
+ "pnpm-lock.yaml",
106
+ "yarn.lock",
107
+ "bun.lockb",
108
+ "coverage-final.json",
109
+ "lcov.info",
110
+ ]);
111
+
112
+ const SESSION_NOISE_EXTENSIONS = new Set([
113
+ ".db",
114
+ ".jsonl",
115
+ ".lock",
116
+ ".log",
117
+ ".sqlite",
118
+ ".sqlite3",
119
+ ]);
120
+
121
+ function cleanSessionPath(value) {
122
+ const text = String(value || "").trim().replace(/\\/g, "/");
123
+ if (!text || /^https?:\/\//.test(text)) return null;
124
+ const withoutQuotes = text.replace(/^["'`]+|["'`.,:;)\]]+$/g, "");
125
+ if (!withoutQuotes || withoutQuotes.includes("\n")) return null;
126
+ const markerIndex = withoutQuotes.indexOf("/Users/");
127
+ if (markerIndex > 0) return withoutQuotes.slice(markerIndex);
128
+ return withoutQuotes;
129
+ }
130
+
131
+ function sessionIgnorePatternForPath(value, root) {
132
+ const cleaned = cleanSessionPath(value);
133
+ if (!cleaned) return null;
134
+ const rootNormalized = normalizeRel(root);
135
+ let rel = cleaned;
136
+ if (path.isAbsolute(cleaned)) {
137
+ const normalized = normalizeRel(cleaned);
138
+ if (!normalized.startsWith(`${rootNormalized}/`)) return null;
139
+ rel = normalizeRel(path.relative(root, cleaned));
140
+ }
141
+ rel = normalizeRel(rel).replace(/^\.\//, "");
142
+ if (!rel || rel === "." || rel.startsWith("../") || rel.includes("..")) return null;
143
+
144
+ const segments = rel.split("/").filter(Boolean);
145
+ if (!segments.length) return null;
146
+ for (let index = 0; index < segments.length; index += 1) {
147
+ const segment = segments[index];
148
+ if (SESSION_NOISE_DIRS.has(segment)) {
149
+ return `${segments.slice(0, index + 1).join("/")}/`;
150
+ }
151
+ }
152
+
153
+ const fileName = segments[segments.length - 1];
154
+ const lowerName = fileName.toLowerCase();
155
+ const ext = path.extname(lowerName);
156
+ if (SESSION_NOISE_FILE_NAMES.has(lowerName)) return fileName;
157
+ if (SESSION_NOISE_EXTENSIONS.has(ext)) return rel;
158
+ if (/_state\.json$/i.test(fileName)) return "*_state.json";
159
+ if (/_tokens\.json$/i.test(fileName)) return "*_tokens.json";
160
+ if (/_export\.json$/i.test(fileName)) return "*_export.json";
161
+ if (/secret|credential|token/i.test(fileName) && /\.json$/i.test(fileName)) return rel;
162
+ return null;
163
+ }
164
+
165
+ function buildSessionIgnoreSuggestions(realUsage, root) {
166
+ if (!realUsage || !Array.isArray(realUsage.sessions)) return [];
167
+ const byPattern = new Map();
168
+ const add = (pattern, item, source, reason) => {
169
+ if (!pattern) return;
170
+ const existing = byPattern.get(pattern) || {
171
+ pattern,
172
+ source,
173
+ reason,
174
+ count: 0,
175
+ examples: [],
176
+ };
177
+ existing.count += Number(item?.count || 1);
178
+ const example = item?.value || item?.path || pattern;
179
+ if (example && !existing.examples.includes(example) && existing.examples.length < 3) existing.examples.push(example);
180
+ byPattern.set(pattern, existing);
181
+ };
182
+
183
+ for (const session of realUsage.sessions) {
184
+ for (const item of session.generatedArtifacts || []) {
185
+ add(sessionIgnorePatternForPath(item.value, root), item, session.tool || "session", "Generated artifact entered local session context.");
186
+ }
187
+ for (const item of session.repeatedPathMentions || []) {
188
+ add(sessionIgnorePatternForPath(item.value, root), item, session.tool || "session", "Noisy path appeared repeatedly in local session context.");
189
+ }
190
+ }
191
+ return Array.from(byPattern.values())
192
+ .sort((a, b) => b.count - a.count || a.pattern.localeCompare(b.pattern))
193
+ .slice(0, 25);
194
+ }
195
+
196
+ return {
197
+ buildSessionIgnoreSuggestions,
198
+ isIgnored,
199
+ missingIgnoreSuggestions,
200
+ normalizeRel,
201
+ readIgnoreFile,
202
+ };
203
+ };