getprismo 0.1.20 → 0.1.22

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
@@ -146,6 +146,69 @@ watch caught lockfiles entering context, a file being read 286 times, and tool o
146
146
 
147
147
  ---
148
148
 
149
+ ## new: optimizer fit
150
+
151
+ not every token optimizer solves the same bottleneck. before stacking compression proxies, repo packers, code indexes, and MCP tools, run:
152
+
153
+ ```bash
154
+ npx getprismo scan --optimizer-fit
155
+ ```
156
+
157
+ PrismoDev scores your actual repo/session signals and recommends the right path:
158
+
159
+ ```text
160
+ Prismo Optimizer Fit
161
+
162
+ Primary bottleneck: Generated artifacts / ignore cleanup: HIGH
163
+
164
+ Bottlenecks
165
+ - Generated artifacts / ignore cleanup: High
166
+ .claudeignore is missing
167
+ - Oversized command/tool output: Medium
168
+ 237k tool/output tokens found in local sessions
169
+ - Repeated source exploration: Low
170
+ Repo/source exploration does not look like the main bottleneck
171
+
172
+ Recommended Stack
173
+ 1. Apply safe ignore/context fixes first.
174
+ Run: npx getprismo doctor --apply-suggestions --dry-run
175
+ Category: ignore cleanup (.claudeignore, .cursorignore)
176
+ 2. Sandbox noisy command output before adding more code-indexing tools.
177
+ Run: npx getprismo shield -- <noisy command>
178
+ Category: output sandboxing (Prismo shield, context-mode, RTK, tokf, distill)
179
+ ```
180
+
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
+
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
+
210
+ ---
211
+
149
212
  ## new: context shield
150
213
 
151
214
  if you know a command may dump huge output, run it through prismo:
@@ -543,6 +606,9 @@ no install needed. npx runs it directly.
543
606
  | `cc` | claude code cost breakdown |
544
607
  | `cc timeline` | session reconstruction with events |
545
608
  | `scan --usage` | full repo scan with local usage data |
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 |
546
612
  | `scan --simple` | plain-english summary |
547
613
  | `scan --fix` | create safe fix files |
548
614
  | `scan --ci` | fail CI when token-risk gates fail |
@@ -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
+ };
@@ -105,6 +105,83 @@ function renderTerminalReport(result, options = {}) {
105
105
  return lines.join("\n");
106
106
  }
107
107
 
108
+ function renderOptimizerFitTerminal(result, options = {}) {
109
+ const useColor = options.color !== false;
110
+ const fit = result.optimizerFit;
111
+ const tone = fit.summary.includes("High") ? "red" : fit.summary.includes("Medium") ? "yellow" : "green";
112
+ const lines = [];
113
+ lines.push("");
114
+ lines.push(color("Prismo Optimizer Fit", "bold", useColor));
115
+ lines.push("");
116
+ lines.push(`Primary bottleneck: ${color(fit.summary, tone, useColor)}`);
117
+ if (result.realUsage && result.realUsage.sessions.length) {
118
+ lines.push(`Local usage: ${formatTokenCount(result.realUsage.totals.displayTokens)} tokens across ${result.realUsage.sessions.length} session(s)`);
119
+ } else if (result.realUsage) {
120
+ lines.push("Local usage: no matching local Claude/Codex sessions found");
121
+ }
122
+ lines.push("");
123
+ lines.push(color("Bottlenecks", "bold", useColor));
124
+ fit.bottlenecks.forEach((item) => {
125
+ lines.push(`- ${item.label}: ${item.level}`);
126
+ item.evidence.slice(0, 2).forEach((evidence) => lines.push(` ${evidence}`));
127
+ });
128
+ lines.push("");
129
+ lines.push(color("Recommended Stack", "bold", useColor));
130
+ fit.recommendedStack.forEach((item) => {
131
+ lines.push(`${item.rank}. ${item.action}`);
132
+ lines.push(` Run: ${item.command}`);
133
+ lines.push(` Why: ${item.why}`);
134
+ lines.push(` Category: ${item.category} (${item.examples.join(", ")})`);
135
+ });
136
+ lines.push("");
137
+ lines.push(color("Tool Fit", "bold", useColor));
138
+ fit.toolFit.forEach((item) => {
139
+ lines.push(`- ${item.category}: ${item.fit}`);
140
+ lines.push(` Examples: ${item.examples.join(", ")}`);
141
+ lines.push(` ${item.reason}`);
142
+ });
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("");
152
+ lines.push("Notes:");
153
+ fit.caveats.forEach((caveat) => lines.push(`- ${caveat}`));
154
+ return lines.join("\n");
155
+ }
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
+
108
185
  function evaluateCi(result, options = {}) {
109
186
  const minScore = Number(options.minScore || 80);
110
187
  const failures = [];
@@ -392,6 +469,8 @@ function writeReport(result) {
392
469
  evaluateCi,
393
470
  renderCiReport,
394
471
  renderMarkdownReport,
472
+ renderOptimizerFitTerminal,
473
+ renderReportCardTerminal,
395
474
  renderSimpleScanReport,
396
475
  renderTerminalReport,
397
476
  writeReport,
@@ -390,12 +390,21 @@ function detectOptimizationStack(root, claudeConfig, codexConfig) {
390
390
  const projectHeadroom = fs.existsSync(path.join(root, ".headroom")) || fs.existsSync(path.join(os.homedir(), ".headroom"));
391
391
  const projectDistill = fs.existsSync(path.join(os.homedir(), ".config", "distill")) || commandExists("distill");
392
392
  const projectRtk = fs.existsSync(path.join(root, ".rtk")) || commandExists("rtk");
393
+ const packageText = readIfText(path.join(root, "package.json"), 512 * 1024) || "";
394
+ const readmeText = readIfText(path.join(root, "README.md"), 512 * 1024) || "";
395
+ const projectText = `${packageText}\n${readmeText}`.toLowerCase();
396
+ const hasText = (pattern) => pattern.test(projectText);
393
397
 
394
398
  const tools = {
395
399
  rtk: { detected: projectRtk, source: projectRtk ? "binary-or-project-config" : "not-detected" },
396
400
  headroom: { detected: projectHeadroom || commandExists("headroom"), source: projectHeadroom ? "local-config" : commandExists("headroom") ? "binary" : "not-detected" },
397
401
  distill: { detected: projectDistill, source: projectDistill ? "binary-or-user-config" : "not-detected" },
398
402
  mana: { detected: projectMana || commandExists("mana"), source: projectMana ? "local-config" : commandExists("mana") ? "binary" : "not-detected" },
403
+ contextMode: { detected: commandExists("context-mode") || hasText(/context-mode/), source: commandExists("context-mode") ? "binary" : hasText(/context-mode/) ? "project-reference" : "not-detected" },
404
+ leanCtx: { detected: commandExists("lean-ctx") || hasText(/lean-ctx|lean ctx/), source: commandExists("lean-ctx") ? "binary" : hasText(/lean-ctx|lean ctx/) ? "project-reference" : "not-detected" },
405
+ repomix: { detected: commandExists("repomix") || hasText(/repomix/), source: commandExists("repomix") ? "binary" : hasText(/repomix/) ? "project-reference" : "not-detected" },
406
+ codegraph: { detected: commandExists("codegraph") || hasText(/codegraph|codebase-memory-mcp|jcodemunch|sigmap/), source: commandExists("codegraph") ? "binary" : hasText(/codegraph|codebase-memory-mcp|jcodemunch|sigmap/) ? "project-reference" : "not-detected" },
407
+ tokf: { detected: commandExists("tokf") || hasText(/tokf/), source: commandExists("tokf") ? "binary" : hasText(/tokf/) ? "project-reference" : "not-detected" },
399
408
  };
400
409
 
401
410
  return {
@@ -786,6 +795,253 @@ function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighR
786
795
  return Array.from(new Set(recs));
787
796
  }
788
797
 
798
+ function levelFromScore(score) {
799
+ if (score >= 70) return "High";
800
+ if (score >= 35) return "Medium";
801
+ return "Low";
802
+ }
803
+
804
+ function addEvidence(evidence, text) {
805
+ if (text && !evidence.includes(text)) evidence.push(text);
806
+ }
807
+
808
+ function countRepeatedSourceReads(realUsage) {
809
+ if (!realUsage || !Array.isArray(realUsage.sessions)) return 0;
810
+ const generatedPattern = /(^|\/)(node_modules|dist|build|coverage|\.next|__pycache__|logs|test-results|playwright-report)\//;
811
+ return realUsage.sessions.reduce((sum, session) => {
812
+ return sum + (session.repeatedPathMentions || []).filter((item) => {
813
+ const value = String(item.value || "");
814
+ if (!value || generatedPattern.test(value)) return false;
815
+ return /\.(js|jsx|ts|tsx|py|go|rs|java|kt|swift|rb|php|cs|svelte|vue|astro|md|json|toml|yaml|yml)$/i.test(value);
816
+ }).reduce((inner, item) => inner + Number(item.count || 0), 0);
817
+ }, 0);
818
+ }
819
+
820
+ function buildOptimizerFit(result) {
821
+ const bottlenecks = [];
822
+ const realUsage = result.realUsage;
823
+ const toolTokens = realUsage ? Number(realUsage.totals.toolTokens || 0) : 0;
824
+ const displayTokens = realUsage ? Number(realUsage.totals.displayTokens || 0) : 0;
825
+ const highRiskSessions = realUsage ? realUsage.sessions.filter((session) => session.contextRisk === "High").length : 0;
826
+ const repeatedSourceReads = countRepeatedSourceReads(realUsage);
827
+
828
+ const ignoreEvidence = [];
829
+ let ignoreScore = 0;
830
+ if (!result.hasClaudeIgnore) {
831
+ ignoreScore += 35;
832
+ addEvidence(ignoreEvidence, ".claudeignore is missing");
833
+ }
834
+ if (!result.hasCursorIgnore) {
835
+ ignoreScore += 20;
836
+ addEvidence(ignoreEvidence, ".cursorignore is missing");
837
+ }
838
+ if (result.exposedHighRiskDirs.length) {
839
+ ignoreScore += Math.min(35, result.exposedHighRiskDirs.length * 8);
840
+ addEvidence(ignoreEvidence, `${result.exposedHighRiskDirs.length} generated/cache directories are exposed`);
841
+ }
842
+ if ((result.sessionIgnoreSuggestions || []).length) {
843
+ ignoreScore += 25;
844
+ addEvidence(ignoreEvidence, `${result.sessionIgnoreSuggestions.length} ignore rules came from actual session leaks`);
845
+ }
846
+ bottlenecks.push({
847
+ id: "ignore-cleanup",
848
+ label: "Generated artifacts / ignore cleanup",
849
+ score: Math.min(100, ignoreScore),
850
+ level: levelFromScore(ignoreScore),
851
+ evidence: ignoreEvidence.length ? ignoreEvidence : ["No major ignore-file leak detected"],
852
+ });
853
+
854
+ const outputEvidence = [];
855
+ let outputScore = result.toolOutputRisk.level === "High" ? 70 : result.toolOutputRisk.level === "Medium" ? 45 : 10;
856
+ if (toolTokens >= 150000) outputScore += 25;
857
+ else if (toolTokens >= 50000) outputScore += 15;
858
+ if (result.toolOutputRisk.exposedNoisyFiles.length) addEvidence(outputEvidence, `${result.toolOutputRisk.exposedNoisyFiles.length} noisy files are exposed`);
859
+ if (result.toolOutputRisk.exposedNoisyDirectories.length) addEvidence(outputEvidence, `${result.toolOutputRisk.exposedNoisyDirectories.length} noisy directories are exposed`);
860
+ if (toolTokens) addEvidence(outputEvidence, `${formatTokenCount(toolTokens)} tool/output tokens found in local sessions`);
861
+ bottlenecks.push({
862
+ id: "output-sandboxing",
863
+ label: "Oversized command/tool output",
864
+ score: Math.min(100, outputScore),
865
+ level: levelFromScore(outputScore),
866
+ evidence: outputEvidence.length ? outputEvidence : ["No dominant command-output flood detected"],
867
+ });
868
+
869
+ const indexEvidence = [];
870
+ let indexScore = 0;
871
+ if (result.stats.sourceFiles >= 1000) indexScore += 35;
872
+ else if (result.stats.sourceFiles >= 250) indexScore += 20;
873
+ if (result.stats.totalFiles >= 5000) indexScore += 25;
874
+ else if (result.stats.totalFiles >= 1000) indexScore += 15;
875
+ if (repeatedSourceReads >= 50) indexScore += 35;
876
+ else if (repeatedSourceReads >= 12) indexScore += 20;
877
+ if (result.stats.sourceFiles >= 250) addEvidence(indexEvidence, `${result.stats.sourceFiles.toLocaleString()} source files`);
878
+ if (repeatedSourceReads) addEvidence(indexEvidence, `${repeatedSourceReads} repeated source-file mentions in local sessions`);
879
+ bottlenecks.push({
880
+ id: "code-indexing",
881
+ label: "Repeated source exploration",
882
+ score: Math.min(100, indexScore),
883
+ level: levelFromScore(indexScore),
884
+ evidence: indexEvidence.length ? indexEvidence : ["Repo/source exploration does not look like the main bottleneck"],
885
+ });
886
+
887
+ const instructionEvidence = [];
888
+ const instructionTokens = result.instructionFiles.reduce((sum, file) => sum + Math.max(0, (file.tokens || 0) - 500), 0);
889
+ let instructionScore = instructionTokens >= 3000 ? 80 : instructionTokens >= 1000 ? 55 : instructionTokens > 0 ? 30 : 0;
890
+ result.instructionFiles
891
+ .filter((file) => file.tokens > 500)
892
+ .slice(0, 3)
893
+ .forEach((file) => addEvidence(instructionEvidence, `${file.path} is ~${(file.tokens || 0).toLocaleString()} tokens`));
894
+ bottlenecks.push({
895
+ id: "instruction-trim",
896
+ label: "Persistent instruction bloat",
897
+ score: Math.min(100, instructionScore),
898
+ level: levelFromScore(instructionScore),
899
+ evidence: instructionEvidence.length ? instructionEvidence : ["Persistent instruction files look manageable"],
900
+ });
901
+
902
+ const sessionEvidence = [];
903
+ let sessionScore = 0;
904
+ if (displayTokens >= 2000000) sessionScore += 60;
905
+ else if (displayTokens >= 500000) sessionScore += 35;
906
+ if (highRiskSessions) sessionScore += Math.min(35, highRiskSessions * 18);
907
+ if (displayTokens) addEvidence(sessionEvidence, `${formatTokenCount(displayTokens)} tokens across recent local sessions`);
908
+ if (highRiskSessions) addEvidence(sessionEvidence, `${highRiskSessions} high-context-risk session${highRiskSessions === 1 ? "" : "s"}`);
909
+ bottlenecks.push({
910
+ id: "session-splitting",
911
+ label: "Long-session context buildup",
912
+ score: Math.min(100, sessionScore),
913
+ level: levelFromScore(sessionScore),
914
+ evidence: sessionEvidence.length ? sessionEvidence : ["No matching high-growth local sessions found"],
915
+ });
916
+
917
+ const mcpEvidence = [];
918
+ let mcpScore = result.optimizationStack.mcpServerTotal >= 10 ? 70 : result.optimizationStack.mcpServerTotal >= 5 ? 45 : 10;
919
+ if (result.optimizationStack.mcpServerTotal) addEvidence(mcpEvidence, `${result.optimizationStack.mcpServerTotal} MCP/tool servers detected`);
920
+ const totalToolCalls = realUsage ? realUsage.sessions.reduce((sum, session) => sum + Number(session.toolCalls || 0), 0) : 0;
921
+ const repeatedCommands = realUsage ? realUsage.sessions.reduce((sum, session) => sum + (session.repeatedCommands || []).reduce((inner, item) => inner + Number(item.count || 0), 0), 0) : 0;
922
+ if (totalToolCalls >= 500) mcpScore += 30;
923
+ else if (totalToolCalls >= 100) mcpScore += 15;
924
+ if (repeatedCommands >= 20) mcpScore += 20;
925
+ if (totalToolCalls) addEvidence(mcpEvidence, `${totalToolCalls} tool calls in recent local sessions`);
926
+ if (repeatedCommands) addEvidence(mcpEvidence, `${repeatedCommands} repeated command/tool mentions`);
927
+ bottlenecks.push({
928
+ id: "tool-surface",
929
+ label: "Tool/MCP surface overhead",
930
+ score: Math.min(100, mcpScore),
931
+ level: levelFromScore(mcpScore),
932
+ evidence: mcpEvidence.length ? mcpEvidence : ["Tool surface does not look unusually large"],
933
+ });
934
+
935
+ const ranked = bottlenecks.sort((a, b) => b.score - a.score || a.label.localeCompare(b.label));
936
+ const primary = ranked[0];
937
+ const actionById = {
938
+ "ignore-cleanup": {
939
+ action: "Apply safe ignore/context fixes first.",
940
+ command: `${NPX_COMMAND} doctor --apply-suggestions --dry-run`,
941
+ category: "ignore cleanup",
942
+ examples: ["Prismo doctor", ".claudeignore", ".cursorignore"],
943
+ },
944
+ "output-sandboxing": {
945
+ action: "Sandbox noisy command output before adding more code-indexing tools.",
946
+ command: `${NPX_COMMAND} shield -- <noisy command>`,
947
+ category: "output sandboxing",
948
+ examples: ["Prismo shield", "context-mode", "RTK", "tokf", "distill"],
949
+ },
950
+ "code-indexing": {
951
+ action: "Use a code indexer if repeated source exploration keeps happening.",
952
+ command: `${NPX_COMMAND} context`,
953
+ category: "code indexing",
954
+ examples: ["codegraph", "jcodemunch", "codebase-memory-mcp", "sigmap"],
955
+ },
956
+ "instruction-trim": {
957
+ action: "Trim persistent instructions before adding runtime compression.",
958
+ command: `${NPX_COMMAND} doctor`,
959
+ category: "instruction quality",
960
+ examples: ["CLAUDE.md cleanup", "AGENTS.md cleanup", "caveman-style concise responses"],
961
+ },
962
+ "session-splitting": {
963
+ action: "Split long sessions and recover from context pressure while working.",
964
+ command: `${NPX_COMMAND} watch --auto`,
965
+ category: "session control",
966
+ examples: ["Prismo watch", "Prismo rescue", "fresh task sessions"],
967
+ },
968
+ "tool-surface": {
969
+ action: "Reduce unused MCP/tool surface for the current task.",
970
+ command: `${NPX_COMMAND} mcp doctor`,
971
+ category: "tool hygiene",
972
+ examples: ["disable unused MCP servers", "strict task-scoped tool config"],
973
+ },
974
+ };
975
+
976
+ const recommendedStack = ranked
977
+ .filter((item) => item.level !== "Low")
978
+ .slice(0, 4)
979
+ .map((item, index) => ({ rank: index + 1, bottleneck: item.id, ...actionById[item.id], why: item.evidence[0] }));
980
+
981
+ if (!recommendedStack.length) {
982
+ recommendedStack.push({
983
+ rank: 1,
984
+ bottleneck: "baseline",
985
+ action: "Keep the stack simple; no major optimizer fit signal was detected.",
986
+ command: `${NPX_COMMAND} watch --once`,
987
+ category: "baseline monitoring",
988
+ examples: ["Prismo watch", "Prismo cc timeline"],
989
+ why: "Repo scan did not find a dominant token-waste source.",
990
+ });
991
+ }
992
+
993
+ return {
994
+ schemaVersion: 1,
995
+ primaryBottleneck: primary.id,
996
+ summary: `${primary.label}: ${primary.level}`,
997
+ bottlenecks: ranked,
998
+ recommendedStack,
999
+ toolFit: [
1000
+ {
1001
+ category: "PrismoDev workflow",
1002
+ fit: "High",
1003
+ examples: ["doctor", "watch", "shield", "cc timeline"],
1004
+ reason: "Use this first to diagnose repo/session waste and verify before stacking optimizers.",
1005
+ },
1006
+ {
1007
+ category: "Output compression/sandboxing",
1008
+ fit: ranked.find((item) => item.id === "output-sandboxing").level,
1009
+ examples: ["Prismo shield", "context-mode", "RTK", "tokf", "distill", "headroom"],
1010
+ reason: "Best when shell/test/log output is the dominant waste source.",
1011
+ },
1012
+ {
1013
+ category: "Code indexing / AST graph",
1014
+ fit: ranked.find((item) => item.id === "code-indexing").level,
1015
+ examples: ["codegraph", "jcodemunch", "codebase-memory-mcp", "sigmap"],
1016
+ reason: "Best when the agent repeatedly greps/reads source files to orient itself.",
1017
+ },
1018
+ {
1019
+ category: "Repo packing",
1020
+ fit: result.stats.sourceFiles && result.stats.sourceFiles <= 250 && result.toolOutputRisk.level === "Low" ? "Medium" : "Low",
1021
+ examples: ["repomix", "Prismo context packs"],
1022
+ reason: "Best for one-shot repo handoff, less ideal for long live coding sessions.",
1023
+ },
1024
+ ],
1025
+ roundTripContext: {
1026
+ level: levelFromScore(Math.max(mcpScore, repeatedSourceReads >= 50 ? 60 : repeatedSourceReads >= 12 ? 35 : 0)),
1027
+ toolCalls: totalToolCalls,
1028
+ repeatedCommandMentions: repeatedCommands,
1029
+ repeatedSourceReads,
1030
+ mcpServers: result.optimizationStack.mcpServerTotal,
1031
+ summary: totalToolCalls || repeatedCommands || repeatedSourceReads || result.optimizationStack.mcpServerTotal
1032
+ ? "Round-trip context risk includes tool calls, repeated commands, repeated source reads, and MCP/tool surface."
1033
+ : "No strong round-trip context signal found in local logs.",
1034
+ recommendation: "Measure workflow-level savings, not only compressed payload size. Fewer tool round trips can beat smaller individual responses.",
1035
+ },
1036
+ caveats: [
1037
+ "Do not stack optimizers blindly; measure one real workflow before and after.",
1038
+ "Payload reduction is not the same as workflow savings; repeated tool calls can erase compression wins.",
1039
+ "Token savings are only useful if the agent still finds the right files and produces accepted changes.",
1040
+ ],
1041
+ nextCommands: recommendedStack.map((item) => item.command),
1042
+ };
1043
+ }
1044
+
789
1045
  function scoreScan(issues, stats, context = {}) {
790
1046
  const issuePenalty = issues.reduce((sum, issue) => sum + severityWeight(issue.severity), 0);
791
1047
  const repoPenalty =
@@ -897,6 +1153,7 @@ function toJsonPayload(result) {
897
1153
  optimizationStack: result.optimizationStack,
898
1154
  toolOutputRisk: result.toolOutputRisk,
899
1155
  operationalNoise: result.operationalNoise,
1156
+ optimizerFit: result.optimizerFit,
900
1157
  sessionIgnoreSuggestions: result.sessionIgnoreSuggestions || [],
901
1158
  proxyTrackingReadiness: result.proxyTrackingReadiness,
902
1159
  suggestedClaudeIgnore: result.recommendedClaudeIgnore,
@@ -1308,7 +1565,7 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
1308
1565
  });
1309
1566
  buildRealUsageRecommendations(realUsage).forEach((rec) => recommendations.push(rec));
1310
1567
 
1311
- return {
1568
+ const scanResult = {
1312
1569
  root,
1313
1570
  score: score.score,
1314
1571
  risk: score.risk,
@@ -1344,6 +1601,8 @@ function scanRepo(rootDir = process.cwd(), options = {}) {
1344
1601
  topTokenLeaks: getTopTokenLeaks(issues),
1345
1602
  generatedAt: new Date().toISOString(),
1346
1603
  };
1604
+ scanResult.optimizerFit = buildOptimizerFit(scanResult);
1605
+ return scanResult;
1347
1606
  }
1348
1607
 
1349
1608
  return {
@@ -193,6 +193,8 @@ const {
193
193
  evaluateCi,
194
194
  renderCiReport,
195
195
  renderMarkdownReport,
196
+ renderOptimizerFitTerminal,
197
+ renderReportCardTerminal,
196
198
  renderSimpleScanReport,
197
199
  renderTerminalReport,
198
200
  writeReport,
@@ -284,6 +286,19 @@ const {
284
286
  color,
285
287
  });
286
288
 
289
+ const {
290
+ renderBenchmarkTerminal,
291
+ runBenchmark,
292
+ } = require("./prismo-dev/benchmark")({
293
+ NPX_COMMAND,
294
+ estimateTokens,
295
+ formatTokenCount,
296
+ getUsageSummary,
297
+ runShield,
298
+ scanRepo: (...args) => scanRepo(...args),
299
+ color,
300
+ });
301
+
287
302
  const {
288
303
  renderMcpDoctorTerminal,
289
304
  runMcpDoctor,
@@ -299,13 +314,14 @@ Usage:
299
314
  prismo init [--json] [--dry-run] [path]
300
315
  prismo doctor [--json] [--dry-run] [--apply-ignores-only] [--apply-suggestions] [--no-context-packs] [--limit N] [path]
301
316
  prismo firewall [task] [--json] [--dry-run] [path]
317
+ prismo benchmark [session] [--json] [--limit N] [path] [-- <command ...>]
302
318
  prismo shield [--json] [path] -- <command ...>
303
319
  prismo shield last [--json] [--limit N] [path]
304
320
  prismo shield search <query> [--json] [--limit N] [path]
305
321
  prismo mcp [path]
306
322
  prismo mcp doctor [--json] [path]
307
323
  prismo setup [--json] [--proxy-url URL] [path]
308
- prismo scan [--fix] [--ci] [--json] [--usage] [--simple] [--no-report] [path]
324
+ prismo scan [--fix] [--ci] [--json] [--usage] [--optimizer-fit] [--report-card] [--simple] [--no-report] [path]
309
325
  prismo optimize [scope] [--json] [path]
310
326
  prismo context [scope] [--json] [path]
311
327
  prismo cc [list|last N|all] [--json] [--limit N] [path]
@@ -318,6 +334,7 @@ Commands:
318
334
  init Add local PrismoDev helper docs and npm scripts when package.json exists.
319
335
  doctor Diagnose, safely optimize, re-scan, and show before/after payoff.
320
336
  firewall Generate allowed/blocked context policy files for an AI coding task.
337
+ benchmark Measure command-output savings or recent session round-trip context.
321
338
  shield Run a noisy command, store full output locally, and return a compact summary.
322
339
  mcp Start a local MCP server exposing Prismo tools over stdio.
323
340
  scan Run PrismoDev for Claude Code, Codex, Cursor, and AI coding workflows.
@@ -334,6 +351,8 @@ Options:
334
351
  --ci Fail with exit code 1 when token-risk gates fail.
335
352
  --json Output valid JSON only for CI or future dashboard ingestion.
336
353
  --usage Include real local Codex/Claude Code session usage in scan diagnostics.
354
+ --optimizer-fit Recommend the right optimization path for this repo/session.
355
+ --report-card Print a short plain-English optimization report card.
337
356
  --simple Print a plain-English scan summary for first-time or non-technical users.
338
357
  --no-report Do not write .prismo/prismo-dev-report.md.
339
358
  --limit N Number of recent local sessions to show.
@@ -373,18 +392,23 @@ function printCommandHelp(command) {
373
392
  scan: `PrismoDev
374
393
 
375
394
  Usage:
376
- prismo scan [--fix] [--ci] [--json] [--usage] [--simple] [--no-report] [--limit N] [path]
395
+ prismo scan [--fix] [--ci] [--json] [--usage] [--optimizer-fit] [--report-card] [--simple] [--no-report] [--limit N] [path]
377
396
 
378
397
  Examples:
379
398
  prismo scan
380
399
  prismo scan --usage
400
+ prismo scan --optimizer-fit
401
+ prismo scan --report-card
381
402
  prismo scan --simple
382
403
  prismo scan --fix
383
404
  prismo scan --ci
384
405
  prismo scan --usage --json --no-report
406
+ prismo scan --optimizer-fit --json
385
407
 
386
408
  Notes:
387
409
  --usage reads local Codex/Claude Code logs when present.
410
+ --optimizer-fit explains whether ignore cleanup, output sandboxing, code indexing, repo packing, instruction trimming, or session splitting fits this repo best.
411
+ --report-card prints the shortest decision-layer summary.
388
412
  --simple keeps the output short and does not write a report unless combined with --fix.
389
413
  --fix creates safe recommendation files and never overwrites CLAUDE.md or AGENTS.md.`,
390
414
  optimize: `Prismo Optimize
@@ -603,8 +627,8 @@ async function runCli(argv) {
603
627
  printCommandHelp(command);
604
628
  return;
605
629
  }
606
- if (!["dev", "init", "doctor", "firewall", "shield", "mcp", "setup", "scan", "optimize", "context", "cc", "usage", "watch", "demo"].includes(command)) {
607
- throw new Error(`Unknown command: ${command}. Try: prismo doctor, prismo watch, prismo shield, prismo mcp, prismo firewall, prismo init, prismo scan, prismo optimize, prismo context, prismo cc, or prismo usage`);
630
+ if (!["dev", "init", "doctor", "firewall", "benchmark", "shield", "mcp", "setup", "scan", "optimize", "context", "cc", "usage", "watch", "demo"].includes(command)) {
631
+ 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
632
  }
609
633
 
610
634
  if (command === "demo") {
@@ -687,6 +711,25 @@ async function runCli(argv) {
687
711
  return;
688
712
  }
689
713
 
714
+ if (command === "benchmark") {
715
+ const json = rest.includes("--json");
716
+ const limitIndex = rest.indexOf("--limit");
717
+ const separatorIndex = rest.indexOf("--");
718
+ const beforeSeparator = separatorIndex >= 0 ? rest.slice(0, separatorIndex) : rest;
719
+ const commandArgs = separatorIndex >= 0 ? rest.slice(separatorIndex + 1) : [];
720
+ const positional = getPositionals(beforeSeparator, new Set(["--limit"]));
721
+ const sessionOnly = positional[0] === "session" || commandArgs.length === 0;
722
+ const target = positional[0] === "session" ? positional[1] || process.cwd() : positional[0] || process.cwd();
723
+ const result = runBenchmark(target, commandArgs, {
724
+ sessionOnly,
725
+ limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5),
726
+ });
727
+ if (json) console.log(JSON.stringify(result, null, 2));
728
+ else console.log(renderBenchmarkTerminal(result));
729
+ if (result.mode === "command") process.exitCode = result.exitCode;
730
+ return;
731
+ }
732
+
690
733
  if (command === "shield") {
691
734
  const json = rest.includes("--json");
692
735
  const limitIndex = rest.indexOf("--limit");
@@ -920,12 +963,14 @@ async function runCli(argv) {
920
963
  const noReport = rest.includes("--no-report");
921
964
  const json = rest.includes("--json");
922
965
  const simple = rest.includes("--simple");
966
+ const optimizerFit = rest.includes("--optimizer-fit");
967
+ const reportCard = rest.includes("--report-card");
923
968
  const ciMode = rest.includes("--ci");
924
- const includeUsage = rest.includes("--usage");
969
+ const includeUsage = rest.includes("--usage") || optimizerFit || reportCard;
925
970
  const limitIndex = rest.indexOf("--limit");
926
971
  const usageToolIndex = rest.indexOf("--usage-tool");
927
972
  const target = getPositionals(rest, new Set(["--limit", "--usage-tool"]))[0] || process.cwd();
928
- const scanDone = printStep(includeUsage ? "Scanning repo and local usage" : "Scanning repo", json || simple);
973
+ const scanDone = printStep(includeUsage ? "Scanning repo and local usage" : "Scanning repo", json || simple || optimizerFit || reportCard);
929
974
  const result = scanRepo(target, {
930
975
  includeUsage,
931
976
  usageLimit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5),
@@ -938,7 +983,7 @@ async function runCli(argv) {
938
983
  let report = null;
939
984
  if (fix) {
940
985
  fixActions = applyFixes(result);
941
- } else if (!noReport) {
986
+ } else if (!noReport && !optimizerFit) {
942
987
  report = writeReport(result);
943
988
  }
944
989
  const payload = toJsonPayload(result);
@@ -946,13 +991,34 @@ async function runCli(argv) {
946
991
  payload.ci = evaluateCi(result);
947
992
  if (!payload.ci.passed) process.exitCode = 1;
948
993
  }
994
+ if (optimizerFit || reportCard) {
995
+ console.log(JSON.stringify({
996
+ schemaVersion: 1,
997
+ scannedPath: result.root,
998
+ score: result.score,
999
+ riskLevel: result.risk,
1000
+ optimizerFit: result.optimizerFit,
1001
+ reportCard: reportCard ? {
1002
+ biggestWaste: result.optimizerFit.summary,
1003
+ startWith: result.optimizerFit.recommendedStack[0]?.command || null,
1004
+ then: result.optimizerFit.recommendedStack[1]?.command || null,
1005
+ roundTripRisk: result.optimizerFit.roundTripContext.level,
1006
+ } : undefined,
1007
+ generatedAt: result.generatedAt,
1008
+ }, null, 2));
1009
+ return;
1010
+ }
949
1011
  if (fixActions.length) payload.fixActions = fixActions;
950
1012
  if (report) payload.reportPath = report.reportPath;
951
1013
  console.log(JSON.stringify(payload, null, 2));
952
1014
  return;
953
1015
  }
954
1016
 
955
- if (simple) {
1017
+ if (reportCard) {
1018
+ console.log(renderReportCardTerminal(result));
1019
+ } else if (optimizerFit) {
1020
+ console.log(renderOptimizerFitTerminal(result));
1021
+ } else if (simple) {
956
1022
  console.log(renderSimpleScanReport(result));
957
1023
  } else if (ciMode) {
958
1024
  const ci = evaluateCi(result);
@@ -966,7 +1032,7 @@ async function runCli(argv) {
966
1032
  const actions = applyFixes(result);
967
1033
  console.log("\nFix Mode:");
968
1034
  actions.forEach((action) => console.log(`- ${action}`));
969
- } else if (!noReport && !simple) {
1035
+ } else if (!noReport && !simple && !optimizerFit && !reportCard) {
970
1036
  const report = writeReport(result);
971
1037
  if (report.backupPath) {
972
1038
  console.log(`\nExisting report backed up to ${path.basename(report.backupPath)}.`);
@@ -987,10 +1053,13 @@ module.exports = {
987
1053
  renderWatchTerminal,
988
1054
  renderWatchReport,
989
1055
  renderTerminalReport,
1056
+ renderOptimizerFitTerminal,
1057
+ renderReportCardTerminal,
990
1058
  renderDoctorTerminal,
991
1059
  renderInitTerminal,
992
1060
  runSetup,
993
1061
  runOptimize,
1062
+ runBenchmark,
994
1063
  runDoctor,
995
1064
  runInit,
996
1065
  runCli,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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",