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 +32 -0
- package/lib/prismo-dev/benchmark.js +122 -0
- package/lib/prismo-dev/report.js +37 -0
- package/lib/prismo-dev/scan-path-utils.js +203 -0
- package/lib/prismo-dev/scan.js +34 -191
- package/lib/prismo-dev/usage-log-utils.js +251 -0
- package/lib/prismo-dev/usage-watch.js +20 -226
- package/lib/prismo-dev/utils.js +85 -0
- package/lib/prismo-dev-scan.js +68 -79
- package/package.json +1 -1
|
@@ -13,84 +13,26 @@ module.exports = function createUsageWatch(deps) {
|
|
|
13
13
|
writeGeneratedFile,
|
|
14
14
|
} = deps;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
};
|
package/lib/prismo-dev-scan.js
CHANGED
|
@@ -19,76 +19,15 @@ const {
|
|
|
19
19
|
GENERATED_ARTIFACT_PATTERNS,
|
|
20
20
|
} = require("./prismo-dev/constants");
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 (
|
|
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