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.
@@ -13,84 +13,26 @@ module.exports = function createUsageWatch(deps) {
13
13
  writeGeneratedFile,
14
14
  } = deps;
15
15
 
16
- function listFilesRecursive(root, predicate = () => true, limit = 300) {
17
- const files = [];
18
- if (!fs.existsSync(root)) return files;
19
- const stack = [root];
20
- while (stack.length && files.length < limit) {
21
- const current = stack.pop();
22
- let entries;
23
- try {
24
- entries = fs.readdirSync(current, { withFileTypes: true });
25
- } catch {
26
- continue;
27
- }
28
- for (const entry of entries) {
29
- const fullPath = path.join(current, entry.name);
30
- if (entry.isDirectory()) {
31
- stack.push(fullPath);
32
- } else if (entry.isFile() && predicate(fullPath)) {
33
- files.push(fullPath);
34
- }
35
- }
36
- }
37
- return files.sort((a, b) => {
38
- try {
39
- return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
40
- } catch {
41
- return 0;
42
- }
43
- });
44
- }
45
-
46
- function parseJsonl(filePath, maxLines = 20000) {
47
- const text = readIfText(filePath, 30 * 1024 * 1024);
48
- if (!text) return [];
49
- const rows = [];
50
- const lines = text.split(/\r?\n/).filter(Boolean);
51
- for (const line of lines.slice(Math.max(0, lines.length - maxLines))) {
52
- try {
53
- rows.push(JSON.parse(line));
54
- } catch {
55
- // Local tool logs can contain partial writes while a session is active.
56
- }
57
- }
58
- return rows;
59
- }
60
-
61
- function collectText(value, options = {}, depth = 0) {
62
- if (value == null || depth > 8) return "";
63
- if (typeof value === "string") return value;
64
- if (typeof value === "number" || typeof value === "boolean") return String(value);
65
- if (Array.isArray(value)) return value.map((item) => collectText(item, options, depth + 1)).join("\n");
66
- if (typeof value !== "object") return "";
67
-
68
- const skipKeys = new Set(["signature", "encrypted_content", "image_url", "data", "auth", "api_key", "token"]);
69
- const parts = [];
70
- for (const [key, child] of Object.entries(value)) {
71
- if (skipKeys.has(key)) continue;
72
- parts.push(collectText(child, options, depth + 1));
73
- }
74
- return parts.filter(Boolean).join("\n");
75
- }
76
-
77
- function addUsage(target, usage) {
78
- if (!usage || typeof usage !== "object") return;
79
- target.inputTokens += Number(usage.input_tokens || usage.prompt_tokens || 0);
80
- target.outputTokens += Number(usage.output_tokens || usage.completion_tokens || 0);
81
- target.cacheReadTokens += Number(usage.cache_read_input_tokens || 0);
82
- target.cacheCreationTokens += Number(usage.cache_creation_input_tokens || 0);
83
- }
84
-
85
- function totalUsageTokens(usage) {
86
- if (!usage) return 0;
87
- return (
88
- Number(usage.input_tokens || usage.prompt_tokens || 0) +
89
- Number(usage.output_tokens || usage.completion_tokens || 0) +
90
- Number(usage.cache_read_input_tokens || 0) +
91
- Number(usage.cache_creation_input_tokens || 0)
92
- );
93
- }
16
+ const {
17
+ addUsage,
18
+ collectText,
19
+ extractCommandCandidates,
20
+ extractMentionedPaths,
21
+ getActionableRepeatedPaths,
22
+ incrementMap,
23
+ isGeneratedArtifactPath,
24
+ listFilesRecursive,
25
+ normalizeMentionedPath,
26
+ parseJsonl,
27
+ summarizeGeneratedArtifacts,
28
+ topCountEntries,
29
+ totalUsageTokens,
30
+ } = require("./usage-log-utils")({
31
+ fs,
32
+ path,
33
+ GENERATED_ARTIFACT_PATTERNS,
34
+ readIfText,
35
+ });
94
36
 
95
37
  function inferClaudePricingKey(model) {
96
38
  const normalized = String(model || "").toLowerCase();
@@ -139,154 +81,6 @@ function getSessionRisk(tokens, toolTokens) {
139
81
  return "Low";
140
82
  }
141
83
 
142
- function incrementMap(map, key, amount = 1) {
143
- if (!key) return;
144
- map[key] = (map[key] || 0) + amount;
145
- }
146
-
147
- function normalizeMentionedPath(value, cwd = "") {
148
- let normalized = String(value || "")
149
- .replace(/\\/g, "/")
150
- .replace(/^[`'"]+|[`'",:;)\]}]+$/g, "")
151
- .trim();
152
- normalized = normalized.replace(/^[ MADRCU?!]{1,4}\s+(?=\/|Users\/|home\/)/, "");
153
- const normalizedCwd = String(cwd || "").replace(/\\/g, "/");
154
- const wasAbsolute = normalized.startsWith("/");
155
- if (wasAbsolute && normalizedCwd && !normalized.startsWith(`${normalizedCwd}/`) && normalized !== normalizedCwd) {
156
- return "";
157
- }
158
- if (normalizedCwd && normalized.startsWith(normalizedCwd)) {
159
- normalized = normalized.slice(normalizedCwd.length);
160
- }
161
- normalized = normalized.replace(/^\.?\//, "");
162
- if (normalizedCwd) {
163
- const repoName = path.basename(normalizedCwd);
164
- const repoIndex = normalized.indexOf(`${repoName}/`);
165
- if (repoIndex >= 0) normalized = normalized.slice(repoIndex + repoName.length + 1);
166
- }
167
- return normalized;
168
- }
169
-
170
- function isGeneratedArtifactPath(relPath) {
171
- const normalized = normalizeMentionedPath(relPath);
172
- return GENERATED_ARTIFACT_PATTERNS.some((pattern) => pattern.test(normalized));
173
- }
174
-
175
- function looksLikeUsefulPath(relPath) {
176
- const normalized = normalizeMentionedPath(relPath);
177
- if (!normalized || normalized.startsWith("http") || normalized.includes("://")) return false;
178
- if (normalized.length < 3 || normalized.split("/").some((part) => !part || part.length > 120)) return false;
179
- if (/^(Users|home|var|tmp|private|Volumes)\//i.test(normalized)) return false;
180
- if (/^(Users|home|var|tmp|Downloads|Code|Projects)$/i.test(normalized)) return false;
181
- if (isGeneratedArtifactPath(normalized)) return true;
182
- if (/\.[A-Za-z0-9]{1,12}$/.test(normalized)) return true;
183
- return /(^|\/)(src|app|lib|backend|frontend|tests|docs|scripts|components|pages|routes|api)\//.test(normalized);
184
- }
185
-
186
- function extractMentionedPaths(text, cwd = "") {
187
- const found = new Set();
188
- const source = String(text || "");
189
- const pathPattern = /(?:^|[\s"'`])((?:\.{0,2}\/)?(?:[\w.@-]+\/)+[\w.@+-]+\.[A-Za-z0-9]{1,12})/g;
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
- for (const pattern of [pathPattern, filePattern]) {
192
- let match;
193
- while ((match = pattern.exec(source))) {
194
- const rel = normalizeMentionedPath(match[1], cwd);
195
- if (!looksLikeUsefulPath(rel)) continue;
196
- if (cwd && !isGeneratedArtifactPath(rel) && !fs.existsSync(path.join(cwd, rel))) continue;
197
- found.add(rel);
198
- }
199
- }
200
- return Array.from(found);
201
- }
202
-
203
- function normalizeCommand(value) {
204
- return String(value || "")
205
- .replace(/\s+/g, " ")
206
- .replace(/[;|&]+$/g, "")
207
- .trim()
208
- .slice(0, 160);
209
- }
210
-
211
- function isShellCommand(value) {
212
- return /^(npm|pnpm|yarn|bun|pytest|python3?|node|npx|uv|ruff|cargo|go|make|git|cd|rm|cp|mv|sed|rg|grep|find|cat)\b/.test(String(value || "").trim());
213
- }
214
-
215
- function extractCommandCandidates(row, text) {
216
- const commands = [];
217
- const directInputs = [
218
- row.payload?.input,
219
- row.payload?.arguments,
220
- row.message?.input,
221
- row.message?.arguments,
222
- ];
223
- for (const input of directInputs) {
224
- if (typeof input === "string") commands.push(input);
225
- else if (input && typeof input === "object") {
226
- for (const value of Object.values(input)) {
227
- if (typeof value === "string") commands.push(value);
228
- }
229
- }
230
- }
231
- const toolItems = Array.isArray(row.message?.content) ? row.message.content : [];
232
- for (const item of toolItems) {
233
- if (!item || typeof item !== "object") continue;
234
- if (typeof item.input === "string") commands.push(item.input);
235
- if (item.input && typeof item.input === "object") {
236
- for (const value of Object.values(item.input)) {
237
- if (typeof value === "string") commands.push(value);
238
- }
239
- }
240
- }
241
- if (/tool_use|function_call/i.test(row.type || row.payload?.type || "")) {
242
- const commandPattern = /\b(?:npm|pnpm|yarn|bun|pytest|python3?|node|npx|uv|ruff|cargo|go test|make|git)\b[^\n\r"`']{0,140}/g;
243
- for (const match of String(text || "").matchAll(commandPattern)) {
244
- commands.push(match[0]);
245
- }
246
- }
247
- return Array.from(new Set(commands.map(normalizeCommand).filter((cmd) => cmd.length >= 3 && /\s/.test(cmd) && isShellCommand(cmd))));
248
- }
249
-
250
- function topCountEntries(map, limit = 5, minCount = 2) {
251
- return Object.entries(map || {})
252
- .filter(([, count]) => count >= minCount)
253
- .sort((a, b) => b[1] - a[1])
254
- .slice(0, limit)
255
- .map(([value, count]) => ({ value, count }));
256
- }
257
-
258
- function isExpectedRepeatedPath(value) {
259
- const normalized = normalizeMentionedPath(value).toLowerCase();
260
- return ["claude.md", "agents.md", "readme.md"].includes(normalized) || normalized.endsWith("/readme.md");
261
- }
262
-
263
- function getActionableRepeatedPaths(session, limit = 3) {
264
- return (session.repeatedPathMentions || [])
265
- .filter((item) => !isExpectedRepeatedPath(item.value))
266
- .filter((item) => !isGeneratedArtifactPath(item.value))
267
- .slice(0, limit);
268
- }
269
-
270
- function summarizeGeneratedArtifacts(items = [], limit = 4) {
271
- const groups = new Map();
272
- for (const item of items) {
273
- const value = normalizeMentionedPath(item.value);
274
- let key = "generated files";
275
- if (value.includes("__pycache__/") || value.endsWith(".pyc")) key = "__pycache__";
276
- else if (value.includes("node_modules/")) key = "node_modules";
277
- else if (/package-lock\.json|pnpm-lock\.yaml|yarn\.lock$/i.test(value)) key = "lockfiles";
278
- else if (value.includes("/dist/") || value.startsWith("dist/")) key = "dist";
279
- else if (value.includes("/build/") || value.startsWith("build/")) key = "build";
280
- else if (value.includes("/coverage/") || value.startsWith("coverage/")) key = "coverage";
281
- else if (/(^|\/)assets\/[^/]+-[A-Za-z0-9_-]{6,}\.(js|css|map)$/i.test(value)) key = "hashed assets";
282
- const current = groups.get(key) || { type: key, count: 0, examples: [] };
283
- current.count += Number(item.count || 1);
284
- if (current.examples.length < 2) current.examples.push(value);
285
- groups.set(key, current);
286
- }
287
- return Array.from(groups.values()).sort((a, b) => b.count - a.count).slice(0, limit);
288
- }
289
-
290
84
  function analyzeSessionFile(filePath, tool) {
291
85
  const rows = parseJsonl(filePath);
292
86
  const stat = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
@@ -0,0 +1,85 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const { BINARY_EXTENSIONS } = require("./constants");
5
+
6
+ function shouldUseColor() {
7
+ return process.stdout.isTTY && !process.env.NO_COLOR;
8
+ }
9
+
10
+ const colorCodes = {
11
+ reset: "\x1b[0m",
12
+ bold: "\x1b[1m",
13
+ dim: "\x1b[2m",
14
+ red: "\x1b[31m",
15
+ yellow: "\x1b[33m",
16
+ green: "\x1b[32m",
17
+ cyan: "\x1b[36m",
18
+ };
19
+
20
+ function color(text, tone, enabled = shouldUseColor()) {
21
+ if (!enabled || !colorCodes[tone]) return text;
22
+ return `${colorCodes[tone]}${text}${colorCodes.reset}`;
23
+ }
24
+
25
+ function severityIcon(severity) {
26
+ if (severity === "critical") return "[critical]";
27
+ if (severity === "high") return "[high]";
28
+ if (severity === "medium") return "[medium]";
29
+ return "[low]";
30
+ }
31
+
32
+ function severityColor(severity) {
33
+ if (severity === "critical" || severity === "high") return "red";
34
+ if (severity === "medium") return "yellow";
35
+ return "cyan";
36
+ }
37
+
38
+ function printStep(label, json = false) {
39
+ if (json) return () => {};
40
+ process.stderr.write(`${color("...", "cyan")} ${label}`);
41
+ return (status = "done") => {
42
+ process.stderr.write(` ${color(`[${status}]`, status === "done" ? "green" : "yellow")}\n`);
43
+ };
44
+ }
45
+
46
+ function estimateTokens(textOrBytes) {
47
+ const length = typeof textOrBytes === "string" ? textOrBytes.length : Number(textOrBytes || 0);
48
+ return Math.ceil(length / 4);
49
+ }
50
+
51
+ function readIfText(filePath, maxBytes = 2 * 1024 * 1024) {
52
+ const ext = path.extname(filePath).toLowerCase();
53
+ if (BINARY_EXTENSIONS.has(ext)) return null;
54
+
55
+ let stat;
56
+ try {
57
+ stat = fs.statSync(filePath);
58
+ } catch {
59
+ return null;
60
+ }
61
+ if (!stat.isFile() || stat.size > maxBytes) return null;
62
+
63
+ const buffer = fs.readFileSync(filePath);
64
+ if (buffer.includes(0)) return null;
65
+ return buffer.toString("utf8");
66
+ }
67
+
68
+ function safeReadJson(filePath) {
69
+ try {
70
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ module.exports = {
77
+ color,
78
+ estimateTokens,
79
+ printStep,
80
+ readIfText,
81
+ safeReadJson,
82
+ severityColor,
83
+ severityIcon,
84
+ shouldUseColor,
85
+ };
@@ -19,76 +19,15 @@ const {
19
19
  GENERATED_ARTIFACT_PATTERNS,
20
20
  } = require("./prismo-dev/constants");
21
21
 
22
- function shouldUseColor() {
23
- return process.stdout.isTTY && !process.env.NO_COLOR;
24
- }
25
-
26
- const colorCodes = {
27
- reset: "\x1b[0m",
28
- bold: "\x1b[1m",
29
- dim: "\x1b[2m",
30
- red: "\x1b[31m",
31
- yellow: "\x1b[33m",
32
- green: "\x1b[32m",
33
- cyan: "\x1b[36m",
34
- };
35
-
36
- function color(text, tone, enabled = shouldUseColor()) {
37
- if (!enabled || !colorCodes[tone]) return text;
38
- return `${colorCodes[tone]}${text}${colorCodes.reset}`;
39
- }
40
-
41
- function severityIcon(severity) {
42
- if (severity === "critical") return "[critical]";
43
- if (severity === "high") return "[high]";
44
- if (severity === "medium") return "[medium]";
45
- return "[low]";
46
- }
47
-
48
- function severityColor(severity) {
49
- if (severity === "critical" || severity === "high") return "red";
50
- if (severity === "medium") return "yellow";
51
- return "cyan";
52
- }
53
-
54
- function printStep(label, json = false) {
55
- if (json) return () => {};
56
- process.stderr.write(`${color("...", "cyan")} ${label}`);
57
- return (status = "done") => {
58
- process.stderr.write(` ${color(`[${status}]`, status === "done" ? "green" : "yellow")}\n`);
59
- };
60
- }
61
-
62
- function estimateTokens(textOrBytes) {
63
- const length = typeof textOrBytes === "string" ? textOrBytes.length : Number(textOrBytes || 0);
64
- return Math.ceil(length / 4);
65
- }
66
-
67
- function readIfText(filePath, maxBytes = 2 * 1024 * 1024) {
68
- const ext = path.extname(filePath).toLowerCase();
69
- if (BINARY_EXTENSIONS.has(ext)) return null;
70
-
71
- let stat;
72
- try {
73
- stat = fs.statSync(filePath);
74
- } catch {
75
- return null;
76
- }
77
- if (!stat.isFile() || stat.size > maxBytes) return null;
78
-
79
- const buffer = fs.readFileSync(filePath);
80
- if (buffer.includes(0)) return null;
81
- return buffer.toString("utf8");
82
- }
83
-
84
-
85
- function safeReadJson(filePath) {
86
- try {
87
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
88
- } catch {
89
- return null;
90
- }
91
- }
22
+ const {
23
+ color,
24
+ estimateTokens,
25
+ printStep,
26
+ readIfText,
27
+ safeReadJson,
28
+ severityColor,
29
+ severityIcon,
30
+ } = require("./prismo-dev/utils");
92
31
 
93
32
  let scanRepo;
94
33
 
@@ -194,6 +133,7 @@ const {
194
133
  renderCiReport,
195
134
  renderMarkdownReport,
196
135
  renderOptimizerFitTerminal,
136
+ renderReportCardTerminal,
197
137
  renderSimpleScanReport,
198
138
  renderTerminalReport,
199
139
  writeReport,
@@ -285,6 +225,19 @@ const {
285
225
  color,
286
226
  });
287
227
 
228
+ const {
229
+ renderBenchmarkTerminal,
230
+ runBenchmark,
231
+ } = require("./prismo-dev/benchmark")({
232
+ NPX_COMMAND,
233
+ estimateTokens,
234
+ formatTokenCount,
235
+ getUsageSummary,
236
+ runShield,
237
+ scanRepo: (...args) => scanRepo(...args),
238
+ color,
239
+ });
240
+
288
241
  const {
289
242
  renderMcpDoctorTerminal,
290
243
  runMcpDoctor,
@@ -300,13 +253,14 @@ Usage:
300
253
  prismo init [--json] [--dry-run] [path]
301
254
  prismo doctor [--json] [--dry-run] [--apply-ignores-only] [--apply-suggestions] [--no-context-packs] [--limit N] [path]
302
255
  prismo firewall [task] [--json] [--dry-run] [path]
256
+ prismo benchmark [session] [--json] [--limit N] [path] [-- <command ...>]
303
257
  prismo shield [--json] [path] -- <command ...>
304
258
  prismo shield last [--json] [--limit N] [path]
305
259
  prismo shield search <query> [--json] [--limit N] [path]
306
260
  prismo mcp [path]
307
261
  prismo mcp doctor [--json] [path]
308
262
  prismo setup [--json] [--proxy-url URL] [path]
309
- prismo scan [--fix] [--ci] [--json] [--usage] [--optimizer-fit] [--simple] [--no-report] [path]
263
+ prismo scan [--fix] [--ci] [--json] [--usage] [--optimizer-fit] [--report-card] [--simple] [--no-report] [path]
310
264
  prismo optimize [scope] [--json] [path]
311
265
  prismo context [scope] [--json] [path]
312
266
  prismo cc [list|last N|all] [--json] [--limit N] [path]
@@ -319,6 +273,7 @@ Commands:
319
273
  init Add local PrismoDev helper docs and npm scripts when package.json exists.
320
274
  doctor Diagnose, safely optimize, re-scan, and show before/after payoff.
321
275
  firewall Generate allowed/blocked context policy files for an AI coding task.
276
+ benchmark Measure command-output savings or recent session round-trip context.
322
277
  shield Run a noisy command, store full output locally, and return a compact summary.
323
278
  mcp Start a local MCP server exposing Prismo tools over stdio.
324
279
  scan Run PrismoDev for Claude Code, Codex, Cursor, and AI coding workflows.
@@ -336,6 +291,7 @@ Options:
336
291
  --json Output valid JSON only for CI or future dashboard ingestion.
337
292
  --usage Include real local Codex/Claude Code session usage in scan diagnostics.
338
293
  --optimizer-fit Recommend the right optimization path for this repo/session.
294
+ --report-card Print a short plain-English optimization report card.
339
295
  --simple Print a plain-English scan summary for first-time or non-technical users.
340
296
  --no-report Do not write .prismo/prismo-dev-report.md.
341
297
  --limit N Number of recent local sessions to show.
@@ -375,12 +331,13 @@ function printCommandHelp(command) {
375
331
  scan: `PrismoDev
376
332
 
377
333
  Usage:
378
- prismo scan [--fix] [--ci] [--json] [--usage] [--optimizer-fit] [--simple] [--no-report] [--limit N] [path]
334
+ prismo scan [--fix] [--ci] [--json] [--usage] [--optimizer-fit] [--report-card] [--simple] [--no-report] [--limit N] [path]
379
335
 
380
336
  Examples:
381
337
  prismo scan
382
338
  prismo scan --usage
383
339
  prismo scan --optimizer-fit
340
+ prismo scan --report-card
384
341
  prismo scan --simple
385
342
  prismo scan --fix
386
343
  prismo scan --ci
@@ -390,6 +347,7 @@ Examples:
390
347
  Notes:
391
348
  --usage reads local Codex/Claude Code logs when present.
392
349
  --optimizer-fit explains whether ignore cleanup, output sandboxing, code indexing, repo packing, instruction trimming, or session splitting fits this repo best.
350
+ --report-card prints the shortest decision-layer summary.
393
351
  --simple keeps the output short and does not write a report unless combined with --fix.
394
352
  --fix creates safe recommendation files and never overwrites CLAUDE.md or AGENTS.md.`,
395
353
  optimize: `Prismo Optimize
@@ -608,8 +566,8 @@ async function runCli(argv) {
608
566
  printCommandHelp(command);
609
567
  return;
610
568
  }
611
- if (!["dev", "init", "doctor", "firewall", "shield", "mcp", "setup", "scan", "optimize", "context", "cc", "usage", "watch", "demo"].includes(command)) {
612
- 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`);
569
+ if (!["dev", "init", "doctor", "firewall", "benchmark", "shield", "mcp", "setup", "scan", "optimize", "context", "cc", "usage", "watch", "demo"].includes(command)) {
570
+ 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`);
613
571
  }
614
572
 
615
573
  if (command === "demo") {
@@ -692,6 +650,25 @@ async function runCli(argv) {
692
650
  return;
693
651
  }
694
652
 
653
+ if (command === "benchmark") {
654
+ const json = rest.includes("--json");
655
+ const limitIndex = rest.indexOf("--limit");
656
+ const separatorIndex = rest.indexOf("--");
657
+ const beforeSeparator = separatorIndex >= 0 ? rest.slice(0, separatorIndex) : rest;
658
+ const commandArgs = separatorIndex >= 0 ? rest.slice(separatorIndex + 1) : [];
659
+ const positional = getPositionals(beforeSeparator, new Set(["--limit"]));
660
+ const sessionOnly = positional[0] === "session" || commandArgs.length === 0;
661
+ const target = positional[0] === "session" ? positional[1] || process.cwd() : positional[0] || process.cwd();
662
+ const result = runBenchmark(target, commandArgs, {
663
+ sessionOnly,
664
+ limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5),
665
+ });
666
+ if (json) console.log(JSON.stringify(result, null, 2));
667
+ else console.log(renderBenchmarkTerminal(result));
668
+ if (result.mode === "command") process.exitCode = result.exitCode;
669
+ return;
670
+ }
671
+
695
672
  if (command === "shield") {
696
673
  const json = rest.includes("--json");
697
674
  const limitIndex = rest.indexOf("--limit");
@@ -926,12 +903,13 @@ async function runCli(argv) {
926
903
  const json = rest.includes("--json");
927
904
  const simple = rest.includes("--simple");
928
905
  const optimizerFit = rest.includes("--optimizer-fit");
906
+ const reportCard = rest.includes("--report-card");
929
907
  const ciMode = rest.includes("--ci");
930
- const includeUsage = rest.includes("--usage") || optimizerFit;
908
+ const includeUsage = rest.includes("--usage") || optimizerFit || reportCard;
931
909
  const limitIndex = rest.indexOf("--limit");
932
910
  const usageToolIndex = rest.indexOf("--usage-tool");
933
911
  const target = getPositionals(rest, new Set(["--limit", "--usage-tool"]))[0] || process.cwd();
934
- const scanDone = printStep(includeUsage ? "Scanning repo and local usage" : "Scanning repo", json || simple || optimizerFit);
912
+ const scanDone = printStep(includeUsage ? "Scanning repo and local usage" : "Scanning repo", json || simple || optimizerFit || reportCard);
935
913
  const result = scanRepo(target, {
936
914
  includeUsage,
937
915
  usageLimit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5),
@@ -952,13 +930,19 @@ async function runCli(argv) {
952
930
  payload.ci = evaluateCi(result);
953
931
  if (!payload.ci.passed) process.exitCode = 1;
954
932
  }
955
- if (optimizerFit) {
933
+ if (optimizerFit || reportCard) {
956
934
  console.log(JSON.stringify({
957
935
  schemaVersion: 1,
958
936
  scannedPath: result.root,
959
937
  score: result.score,
960
938
  riskLevel: result.risk,
961
939
  optimizerFit: result.optimizerFit,
940
+ reportCard: reportCard ? {
941
+ biggestWaste: result.optimizerFit.summary,
942
+ startWith: result.optimizerFit.recommendedStack[0]?.command || null,
943
+ then: result.optimizerFit.recommendedStack[1]?.command || null,
944
+ roundTripRisk: result.optimizerFit.roundTripContext.level,
945
+ } : undefined,
962
946
  generatedAt: result.generatedAt,
963
947
  }, null, 2));
964
948
  return;
@@ -969,7 +953,9 @@ async function runCli(argv) {
969
953
  return;
970
954
  }
971
955
 
972
- if (optimizerFit) {
956
+ if (reportCard) {
957
+ console.log(renderReportCardTerminal(result));
958
+ } else if (optimizerFit) {
973
959
  console.log(renderOptimizerFitTerminal(result));
974
960
  } else if (simple) {
975
961
  console.log(renderSimpleScanReport(result));
@@ -985,7 +971,7 @@ async function runCli(argv) {
985
971
  const actions = applyFixes(result);
986
972
  console.log("\nFix Mode:");
987
973
  actions.forEach((action) => console.log(`- ${action}`));
988
- } else if (!noReport && !simple && !optimizerFit) {
974
+ } else if (!noReport && !simple && !optimizerFit && !reportCard) {
989
975
  const report = writeReport(result);
990
976
  if (report.backupPath) {
991
977
  console.log(`\nExisting report backed up to ${path.basename(report.backupPath)}.`);
@@ -1006,10 +992,13 @@ module.exports = {
1006
992
  renderWatchTerminal,
1007
993
  renderWatchReport,
1008
994
  renderTerminalReport,
995
+ renderOptimizerFitTerminal,
996
+ renderReportCardTerminal,
1009
997
  renderDoctorTerminal,
1010
998
  renderInitTerminal,
1011
999
  runSetup,
1012
1000
  runOptimize,
1001
+ runBenchmark,
1013
1002
  runDoctor,
1014
1003
  runInit,
1015
1004
  runCli,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getprismo",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
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",