getprismo 0.1.0
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 +110 -0
- package/bin/prismo.js +8 -0
- package/lib/prismo-dev-scan.js +2461 -0
- package/package.json +28 -0
|
@@ -0,0 +1,2461 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const HIGH_RISK_DIRS = [
|
|
6
|
+
"node_modules",
|
|
7
|
+
".next",
|
|
8
|
+
"dist",
|
|
9
|
+
"build",
|
|
10
|
+
"coverage",
|
|
11
|
+
".turbo",
|
|
12
|
+
".venv",
|
|
13
|
+
"venv",
|
|
14
|
+
"__pycache__",
|
|
15
|
+
".pytest_cache",
|
|
16
|
+
".cache",
|
|
17
|
+
"logs",
|
|
18
|
+
"test-results",
|
|
19
|
+
"playwright-report",
|
|
20
|
+
"tmp",
|
|
21
|
+
"temp",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const HIGH_RISK_FILE_NAMES = new Set([
|
|
25
|
+
"package-lock.json",
|
|
26
|
+
"yarn.lock",
|
|
27
|
+
"pnpm-lock.yaml",
|
|
28
|
+
"npm-shrinkwrap.json",
|
|
29
|
+
"coverage-final.json",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const BINARY_EXTENSIONS = new Set([
|
|
33
|
+
".png",
|
|
34
|
+
".jpg",
|
|
35
|
+
".jpeg",
|
|
36
|
+
".gif",
|
|
37
|
+
".webp",
|
|
38
|
+
".ico",
|
|
39
|
+
".svg",
|
|
40
|
+
".pdf",
|
|
41
|
+
".zip",
|
|
42
|
+
".gz",
|
|
43
|
+
".tar",
|
|
44
|
+
".tgz",
|
|
45
|
+
".mp4",
|
|
46
|
+
".mov",
|
|
47
|
+
".mp3",
|
|
48
|
+
".wav",
|
|
49
|
+
".woff",
|
|
50
|
+
".woff2",
|
|
51
|
+
".ttf",
|
|
52
|
+
".otf",
|
|
53
|
+
".pyc",
|
|
54
|
+
".class",
|
|
55
|
+
".wasm",
|
|
56
|
+
".sqlite",
|
|
57
|
+
".sqlite3",
|
|
58
|
+
".db",
|
|
59
|
+
".bin",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
63
|
+
".js",
|
|
64
|
+
".jsx",
|
|
65
|
+
".ts",
|
|
66
|
+
".tsx",
|
|
67
|
+
".py",
|
|
68
|
+
".go",
|
|
69
|
+
".rs",
|
|
70
|
+
".java",
|
|
71
|
+
".c",
|
|
72
|
+
".cc",
|
|
73
|
+
".cpp",
|
|
74
|
+
".h",
|
|
75
|
+
".hpp",
|
|
76
|
+
".cs",
|
|
77
|
+
".rb",
|
|
78
|
+
".php",
|
|
79
|
+
".swift",
|
|
80
|
+
".kt",
|
|
81
|
+
".m",
|
|
82
|
+
".mm",
|
|
83
|
+
".scala",
|
|
84
|
+
".sh",
|
|
85
|
+
".sql",
|
|
86
|
+
".css",
|
|
87
|
+
".scss",
|
|
88
|
+
".html",
|
|
89
|
+
".vue",
|
|
90
|
+
".svelte",
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const INSTRUCTION_FILES = [
|
|
94
|
+
"CLAUDE.md",
|
|
95
|
+
"AGENTS.md",
|
|
96
|
+
"README.md",
|
|
97
|
+
"README",
|
|
98
|
+
".openai/instructions.md",
|
|
99
|
+
".codex/AGENTS.md",
|
|
100
|
+
".codex/instructions.md",
|
|
101
|
+
".codex/config.toml",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const DEFAULT_CLAUDEIGNORE = [
|
|
105
|
+
"node_modules/",
|
|
106
|
+
".next/",
|
|
107
|
+
"dist/",
|
|
108
|
+
"build/",
|
|
109
|
+
"coverage/",
|
|
110
|
+
".turbo/",
|
|
111
|
+
".venv/",
|
|
112
|
+
"venv/",
|
|
113
|
+
"__pycache__/",
|
|
114
|
+
"pycache/",
|
|
115
|
+
".pytest_cache/",
|
|
116
|
+
".cache/",
|
|
117
|
+
"logs/",
|
|
118
|
+
"*.log",
|
|
119
|
+
"*.lock",
|
|
120
|
+
"*.tmp",
|
|
121
|
+
"*.min.js",
|
|
122
|
+
"*.min.css",
|
|
123
|
+
"coverage-final.json",
|
|
124
|
+
"package-lock.json",
|
|
125
|
+
"yarn.lock",
|
|
126
|
+
"pnpm-lock.yaml",
|
|
127
|
+
"test-results/",
|
|
128
|
+
"playwright-report/",
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const NPX_COMMAND = "npx getprismo";
|
|
132
|
+
|
|
133
|
+
function shouldUseColor() {
|
|
134
|
+
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const colorCodes = {
|
|
138
|
+
reset: "\x1b[0m",
|
|
139
|
+
bold: "\x1b[1m",
|
|
140
|
+
dim: "\x1b[2m",
|
|
141
|
+
red: "\x1b[31m",
|
|
142
|
+
yellow: "\x1b[33m",
|
|
143
|
+
green: "\x1b[32m",
|
|
144
|
+
cyan: "\x1b[36m",
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function color(text, tone, enabled = shouldUseColor()) {
|
|
148
|
+
if (!enabled || !colorCodes[tone]) return text;
|
|
149
|
+
return `${colorCodes[tone]}${text}${colorCodes.reset}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function severityIcon(severity) {
|
|
153
|
+
if (severity === "critical") return "[critical]";
|
|
154
|
+
if (severity === "high") return "[high]";
|
|
155
|
+
if (severity === "medium") return "[medium]";
|
|
156
|
+
return "[low]";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function severityColor(severity) {
|
|
160
|
+
if (severity === "critical" || severity === "high") return "red";
|
|
161
|
+
if (severity === "medium") return "yellow";
|
|
162
|
+
return "cyan";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function printStep(label, json = false) {
|
|
166
|
+
if (json) return () => {};
|
|
167
|
+
process.stderr.write(`${color("...", "cyan")} ${label}`);
|
|
168
|
+
return (status = "done") => {
|
|
169
|
+
process.stderr.write(` ${color(`[${status}]`, status === "done" ? "green" : "yellow")}\n`);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function estimateTokens(textOrBytes) {
|
|
174
|
+
const length = typeof textOrBytes === "string" ? textOrBytes.length : Number(textOrBytes || 0);
|
|
175
|
+
return Math.ceil(length / 4);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readIfText(filePath, maxBytes = 2 * 1024 * 1024) {
|
|
179
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
180
|
+
if (BINARY_EXTENSIONS.has(ext)) return null;
|
|
181
|
+
|
|
182
|
+
let stat;
|
|
183
|
+
try {
|
|
184
|
+
stat = fs.statSync(filePath);
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
189
|
+
|
|
190
|
+
const buffer = fs.readFileSync(filePath);
|
|
191
|
+
if (buffer.includes(0)) return null;
|
|
192
|
+
return buffer.toString("utf8");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeRel(value) {
|
|
196
|
+
return value.split(path.sep).join("/");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function readIgnoreFile(root, fileName) {
|
|
200
|
+
const filePath = path.join(root, fileName);
|
|
201
|
+
if (!fs.existsSync(filePath)) return [];
|
|
202
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
203
|
+
return text
|
|
204
|
+
.split(/\r?\n/)
|
|
205
|
+
.map((line) => line.trim())
|
|
206
|
+
.filter((line) => line && !line.startsWith("#"))
|
|
207
|
+
.map((line) => line.replace(/^!/, ""));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function patternMatches(pattern, relPath, isDir = false) {
|
|
211
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\//, "");
|
|
212
|
+
const normalizedRel = normalizeRel(relPath);
|
|
213
|
+
const dirRel = isDir && !normalizedRel.endsWith("/") ? `${normalizedRel}/` : normalizedRel;
|
|
214
|
+
|
|
215
|
+
if (!normalizedPattern) return false;
|
|
216
|
+
if (normalizedPattern.endsWith("/")) {
|
|
217
|
+
const base = normalizedPattern.slice(0, -1);
|
|
218
|
+
return (
|
|
219
|
+
normalizedRel === base ||
|
|
220
|
+
normalizedRel.startsWith(`${base}/`) ||
|
|
221
|
+
normalizedRel.endsWith(`/${base}`) ||
|
|
222
|
+
normalizedRel.includes(`/${base}/`) ||
|
|
223
|
+
dirRel.includes(`/${base}/`)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (normalizedPattern.startsWith("*.")) {
|
|
227
|
+
return normalizedRel.endsWith(normalizedPattern.slice(1));
|
|
228
|
+
}
|
|
229
|
+
if (normalizedPattern.includes("*")) {
|
|
230
|
+
const escaped = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
231
|
+
return new RegExp(`(^|/)${escaped}$`).test(normalizedRel);
|
|
232
|
+
}
|
|
233
|
+
return (
|
|
234
|
+
normalizedRel === normalizedPattern ||
|
|
235
|
+
dirRel === normalizedPattern ||
|
|
236
|
+
normalizedRel.startsWith(`${normalizedPattern}/`) ||
|
|
237
|
+
normalizedRel.endsWith(`/${normalizedPattern}`)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isIgnored(relPath, patterns, isDir = false) {
|
|
242
|
+
return patterns.some((pattern) => patternMatches(pattern, relPath, isDir));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getFileKind(filePath) {
|
|
246
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
247
|
+
const name = path.basename(filePath).toLowerCase();
|
|
248
|
+
if (BINARY_EXTENSIONS.has(ext)) return "binary";
|
|
249
|
+
if (name.endsWith(".log") || name.includes("log")) return "log";
|
|
250
|
+
if (ext === ".json") return "json";
|
|
251
|
+
if (name.endsWith(".min.js") || name.endsWith(".min.css")) return "minified";
|
|
252
|
+
if (HIGH_RISK_FILE_NAMES.has(name)) return "lock/generated";
|
|
253
|
+
return SOURCE_EXTENSIONS.has(ext) ? "source" : "text";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function walkRepo(root, ignorePatterns) {
|
|
257
|
+
const files = [];
|
|
258
|
+
const highRiskDirs = [];
|
|
259
|
+
const stack = [root];
|
|
260
|
+
const rootReal = fs.realpathSync(root);
|
|
261
|
+
|
|
262
|
+
while (stack.length) {
|
|
263
|
+
const current = stack.pop();
|
|
264
|
+
let entries;
|
|
265
|
+
try {
|
|
266
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
267
|
+
} catch {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
const fullPath = path.join(current, entry.name);
|
|
273
|
+
const relPath = normalizeRel(path.relative(root, fullPath));
|
|
274
|
+
if (!relPath || relPath === ".git") continue;
|
|
275
|
+
if (relPath.startsWith(".git/")) continue;
|
|
276
|
+
|
|
277
|
+
if (entry.isSymbolicLink()) continue;
|
|
278
|
+
|
|
279
|
+
if (entry.isDirectory()) {
|
|
280
|
+
const exposed = !isIgnored(relPath, ignorePatterns, true);
|
|
281
|
+
if (HIGH_RISK_DIRS.includes(entry.name)) {
|
|
282
|
+
highRiskDirs.push({ path: relPath, exposed });
|
|
283
|
+
// Never descend into bulky generated/cache folders; existence is enough.
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
stack.push(fullPath);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!entry.isFile()) continue;
|
|
291
|
+
let stat;
|
|
292
|
+
try {
|
|
293
|
+
stat = fs.statSync(fullPath);
|
|
294
|
+
} catch {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (!fs.realpathSync(fullPath).startsWith(rootReal)) continue;
|
|
298
|
+
|
|
299
|
+
const kind = getFileKind(fullPath);
|
|
300
|
+
files.push({
|
|
301
|
+
path: relPath,
|
|
302
|
+
fullPath,
|
|
303
|
+
size: stat.size,
|
|
304
|
+
kind,
|
|
305
|
+
ignored: isIgnored(relPath, ignorePatterns, false),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { files, highRiskDirs };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function scanInstructionFiles(root) {
|
|
314
|
+
const results = [];
|
|
315
|
+
for (const rel of INSTRUCTION_FILES) {
|
|
316
|
+
const filePath = path.join(root, rel);
|
|
317
|
+
if (!fs.existsSync(filePath)) continue;
|
|
318
|
+
const stat = fs.statSync(filePath);
|
|
319
|
+
if (!stat.isFile()) continue;
|
|
320
|
+
const text = readIfText(filePath) || "";
|
|
321
|
+
results.push({
|
|
322
|
+
path: rel,
|
|
323
|
+
size: stat.size,
|
|
324
|
+
tokens: estimateTokens(text || stat.size),
|
|
325
|
+
isClaude: path.basename(rel).toLowerCase() === "claude.md",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return results;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function countJsonObjectKeys(value, keyName) {
|
|
332
|
+
if (!value || typeof value !== "object") return 0;
|
|
333
|
+
let count = 0;
|
|
334
|
+
if (value[keyName] && typeof value[keyName] === "object") {
|
|
335
|
+
count += Array.isArray(value[keyName]) ? value[keyName].length : Object.keys(value[keyName]).length;
|
|
336
|
+
}
|
|
337
|
+
for (const child of Object.values(value)) {
|
|
338
|
+
if (child && typeof child === "object") count += countJsonObjectKeys(child, keyName);
|
|
339
|
+
}
|
|
340
|
+
return count;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function scanClaudeConfig(root) {
|
|
344
|
+
const candidates = [
|
|
345
|
+
path.join(os.homedir(), ".claude", "settings.json"),
|
|
346
|
+
path.join(os.homedir(), ".claude.json"),
|
|
347
|
+
path.join(root, ".claude", "settings.json"),
|
|
348
|
+
path.join(root, ".claude.json"),
|
|
349
|
+
];
|
|
350
|
+
const found = [];
|
|
351
|
+
let mcpServers = 0;
|
|
352
|
+
let hooks = 0;
|
|
353
|
+
let pluginRefs = 0;
|
|
354
|
+
|
|
355
|
+
for (const filePath of candidates) {
|
|
356
|
+
if (!fs.existsSync(filePath)) continue;
|
|
357
|
+
const text = readIfText(filePath);
|
|
358
|
+
if (!text) continue;
|
|
359
|
+
const rel = filePath.startsWith(root) ? normalizeRel(path.relative(root, filePath)) : filePath;
|
|
360
|
+
found.push(rel);
|
|
361
|
+
try {
|
|
362
|
+
const json = JSON.parse(text);
|
|
363
|
+
mcpServers += countJsonObjectKeys(json, "mcpServers");
|
|
364
|
+
hooks += countJsonObjectKeys(json, "hooks");
|
|
365
|
+
} catch {
|
|
366
|
+
mcpServers += (text.match(/mcpServers|mcp_servers|mcp-server/g) || []).length;
|
|
367
|
+
hooks += (text.match(/hooks|hook/g) || []).length;
|
|
368
|
+
}
|
|
369
|
+
pluginRefs += (text.match(/plugin|skill/gi) || []).length;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { files: found, mcpServers, hooks, pluginRefs };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function scanCodexConfig(root) {
|
|
376
|
+
const candidates = [
|
|
377
|
+
path.join(root, ".codex", "config.toml"),
|
|
378
|
+
path.join(os.homedir(), ".codex", "config.toml"),
|
|
379
|
+
path.join(root, "AGENTS.md"),
|
|
380
|
+
];
|
|
381
|
+
const found = [];
|
|
382
|
+
let mcpServers = 0;
|
|
383
|
+
|
|
384
|
+
for (const filePath of candidates) {
|
|
385
|
+
if (!fs.existsSync(filePath)) continue;
|
|
386
|
+
const text = readIfText(filePath);
|
|
387
|
+
if (!text) continue;
|
|
388
|
+
found.push(filePath.startsWith(root) ? normalizeRel(path.relative(root, filePath)) : filePath);
|
|
389
|
+
mcpServers += (text.match(/\[mcp|mcp_servers|mcp-server|server\]/gi) || []).length;
|
|
390
|
+
}
|
|
391
|
+
return { files: found, mcpServers };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function classifyLargeFiles(files) {
|
|
395
|
+
return files
|
|
396
|
+
.filter((file) => file.kind !== "binary" && file.size >= 500 * 1024)
|
|
397
|
+
.sort((a, b) => b.size - a.size)
|
|
398
|
+
.map((file) => ({
|
|
399
|
+
path: file.path,
|
|
400
|
+
size: file.size,
|
|
401
|
+
mb: file.size / (1024 * 1024),
|
|
402
|
+
kind: file.kind,
|
|
403
|
+
ignored: file.ignored,
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function formatBytes(bytes) {
|
|
408
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
409
|
+
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
|
|
410
|
+
return `${bytes} B`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function estimateClaudeInstructionImpact(tokens) {
|
|
414
|
+
if (!tokens || tokens <= 500) return null;
|
|
415
|
+
const extra = Math.max(0, tokens - 500);
|
|
416
|
+
return `Potential savings estimate: about ${extra.toLocaleString()} persistent instruction tokens per turn if trimmed near 500 tokens.`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function estimateLargeFileImpact(files) {
|
|
420
|
+
if (!files.length) return null;
|
|
421
|
+
const totalTokens = files.reduce((sum, file) => sum + estimateTokens(file.size), 0);
|
|
422
|
+
return `Likely avoidable token exposure: up to ~${totalTokens.toLocaleString()} tokens if these files are read into agent context.`;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function estimateRiskyDirImpact(dirs) {
|
|
426
|
+
if (!dirs.length) return null;
|
|
427
|
+
return "Likely avoidable token exposure: generated/cache directories can create high repo-read risk when agents explore broadly.";
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function estimateMcpImpact(count) {
|
|
431
|
+
if (!count || count < 5) return null;
|
|
432
|
+
return "Possible baseline/tool overhead: many MCP servers can expand tool choice and produce extra tool traffic.";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function severityWeight(severity) {
|
|
436
|
+
return severity === "critical" ? 22 : severity === "high" ? 14 : severity === "medium" ? 8 : 4;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function severityRank(severity) {
|
|
440
|
+
return { critical: 0, high: 1, medium: 2, low: 3 }[severity] ?? 4;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function addIssue(issues, severity, category, title, description, recommendation, estimatedTokenImpact = null) {
|
|
444
|
+
issues.push({
|
|
445
|
+
severity,
|
|
446
|
+
category,
|
|
447
|
+
title,
|
|
448
|
+
description,
|
|
449
|
+
recommendation,
|
|
450
|
+
estimatedTokenImpact,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function buildRecommendations({ hasClaudeIgnore, gitignorePatterns, exposedHighRiskDirs, largeFiles, instructionFiles, claudeConfig }) {
|
|
455
|
+
const recs = [];
|
|
456
|
+
if (!hasClaudeIgnore) {
|
|
457
|
+
recs.push("Create .claudeignore with generated/cache folders and large artifacts excluded.");
|
|
458
|
+
}
|
|
459
|
+
if (gitignorePatterns.length && !hasClaudeIgnore) {
|
|
460
|
+
recs.push("Use .gitignore as the baseline for .claudeignore, then add AI-specific exclusions.");
|
|
461
|
+
}
|
|
462
|
+
if (exposedHighRiskDirs.length) {
|
|
463
|
+
recs.push(`Ignore generated/cache folders: ${exposedHighRiskDirs.slice(0, 8).map((d) => `${d.path}/`).join(", ")}.`);
|
|
464
|
+
}
|
|
465
|
+
if (largeFiles.some((file) => !file.ignored)) {
|
|
466
|
+
recs.push("Avoid loading large logs, JSON dumps, coverage reports, and minified assets into coding-agent context.");
|
|
467
|
+
}
|
|
468
|
+
if (instructionFiles.some((file) => file.isClaude && file.tokens > 500)) {
|
|
469
|
+
recs.push("Trim CLAUDE.md to project rules only; move long implementation notes into docs referenced on demand.");
|
|
470
|
+
}
|
|
471
|
+
if (claudeConfig.mcpServers >= 5) {
|
|
472
|
+
recs.push("Disable MCP servers that are not needed for the current project or task.");
|
|
473
|
+
}
|
|
474
|
+
recs.push("Start fresh sessions for unrelated tasks and compact long sessions when context growth accelerates.");
|
|
475
|
+
recs.push("Use cheaper/faster models for mechanical edits, formatting, and low-risk refactors.");
|
|
476
|
+
return Array.from(new Set(recs));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function scoreScan(issues, stats) {
|
|
480
|
+
let score = 100;
|
|
481
|
+
for (const issue of issues) {
|
|
482
|
+
score -= severityWeight(issue.severity);
|
|
483
|
+
}
|
|
484
|
+
if (stats.totalFiles > 2500) score -= 8;
|
|
485
|
+
if (stats.exposedLargeFiles > 10) score -= 8;
|
|
486
|
+
if (stats.exposedHighRiskDirs > 4) score -= 8;
|
|
487
|
+
score = Math.max(0, Math.min(100, score));
|
|
488
|
+
|
|
489
|
+
const risk = score >= 80 ? "Low" : score >= 55 ? "Medium" : "High";
|
|
490
|
+
const avoidableWaste = risk === "Low" ? "5-15%" : risk === "Medium" ? "20-40%" : "40-65%";
|
|
491
|
+
return { score, risk, avoidableWaste };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function getTopTokenLeaks(issues, limit = 5) {
|
|
495
|
+
return [...issues]
|
|
496
|
+
.sort((a, b) => severityRank(a.severity) - severityRank(b.severity))
|
|
497
|
+
.slice(0, limit)
|
|
498
|
+
.map((issue) => issue.title);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function getNextCommands(result, scope = null) {
|
|
502
|
+
const commands = [];
|
|
503
|
+
if (!result.hasClaudeIgnore || result.issues.some((issue) => ["instruction_file", "codex_config"].includes(issue.category))) {
|
|
504
|
+
commands.push(`${NPX_COMMAND} scan --fix`);
|
|
505
|
+
}
|
|
506
|
+
commands.push(`${NPX_COMMAND} optimize`);
|
|
507
|
+
if (scope) commands.push(`${NPX_COMMAND} context ${scope}`);
|
|
508
|
+
else commands.push(result.stats.sourceFiles ? `${NPX_COMMAND} context` : `${NPX_COMMAND} --help`);
|
|
509
|
+
if (result.realUsage && result.realUsage.sessions.length) commands.push(`${NPX_COMMAND} usage --limit 3`);
|
|
510
|
+
else commands.push(`${NPX_COMMAND} scan --usage`);
|
|
511
|
+
return Array.from(new Set(commands)).slice(0, 4);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function toJsonPayload(result) {
|
|
515
|
+
const payload = {
|
|
516
|
+
score: result.score,
|
|
517
|
+
riskLevel: result.risk,
|
|
518
|
+
estimatedAvoidableWasteRange: result.avoidableWaste,
|
|
519
|
+
issues: result.issues,
|
|
520
|
+
recommendations: result.recommendations,
|
|
521
|
+
largeFiles: result.largeFiles.map((file) => ({
|
|
522
|
+
path: file.path,
|
|
523
|
+
sizeBytes: file.size,
|
|
524
|
+
sizeLabel: formatBytes(file.size),
|
|
525
|
+
kind: file.kind,
|
|
526
|
+
ignored: file.ignored,
|
|
527
|
+
estimatedTokensIfRead: estimateTokens(file.size),
|
|
528
|
+
})),
|
|
529
|
+
riskyDirectories: result.highRiskDirs.map((dir) => ({
|
|
530
|
+
path: dir.path,
|
|
531
|
+
exposed: dir.exposed,
|
|
532
|
+
})),
|
|
533
|
+
instructionFiles: result.instructionFiles.map((file) => ({
|
|
534
|
+
path: file.path,
|
|
535
|
+
sizeBytes: file.size,
|
|
536
|
+
estimatedTokens: file.tokens,
|
|
537
|
+
type: file.isClaude ? "claude" : file.path === "AGENTS.md" || file.path.startsWith(".codex/") ? "codex" : "general",
|
|
538
|
+
})),
|
|
539
|
+
claudeFindings: {
|
|
540
|
+
hasClaudeMd: result.instructionFiles.some((file) => file.isClaude),
|
|
541
|
+
hasClaudeIgnore: result.hasClaudeIgnore,
|
|
542
|
+
configFiles: result.claudeConfig.files,
|
|
543
|
+
mcpServers: result.claudeConfig.mcpServers,
|
|
544
|
+
hooks: result.claudeConfig.hooks,
|
|
545
|
+
pluginRefs: result.claudeConfig.pluginRefs,
|
|
546
|
+
},
|
|
547
|
+
codexFindings: {
|
|
548
|
+
configFiles: result.codexConfig.files,
|
|
549
|
+
mcpServers: result.codexConfig.mcpServers,
|
|
550
|
+
hasAgentsMd: result.instructionFiles.some((file) => file.path === "AGENTS.md"),
|
|
551
|
+
hasCodexDirectory: fs.existsSync(path.join(result.root, ".codex")),
|
|
552
|
+
hasOpenAiDirectory: fs.existsSync(path.join(result.root, ".openai")),
|
|
553
|
+
},
|
|
554
|
+
suggestedClaudeIgnore: result.recommendedClaudeIgnore,
|
|
555
|
+
nextCommands: getNextCommands(result),
|
|
556
|
+
generatedAt: result.generatedAt,
|
|
557
|
+
scannedPath: result.root,
|
|
558
|
+
};
|
|
559
|
+
if (result.realUsage) payload.realUsage = result.realUsage;
|
|
560
|
+
return payload;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function addRealUsageIssues(issues, usage) {
|
|
564
|
+
if (!usage || !usage.sessions.length) return;
|
|
565
|
+
const total = usage.totals.displayTokens || 0;
|
|
566
|
+
const exact = usage.totals.exactTokens || 0;
|
|
567
|
+
const toolTokens = usage.totals.toolTokens || 0;
|
|
568
|
+
const highRiskSessions = usage.sessions.filter((session) => session.contextRisk === "High");
|
|
569
|
+
|
|
570
|
+
if (total >= 1000000) {
|
|
571
|
+
addIssue(
|
|
572
|
+
issues,
|
|
573
|
+
total >= 5000000 ? "critical" : "high",
|
|
574
|
+
"repo_size",
|
|
575
|
+
`Recent local AI sessions used ${formatTokenCount(total)} tokens`,
|
|
576
|
+
exact ? "Prismo found exact token counts in local Codex/Claude Code session logs." : "Prismo estimated usage from local session text because exact token fields were unavailable.",
|
|
577
|
+
"Use Prismo Optimize context packs, compact long sessions, and start fresh sessions for unrelated tasks.",
|
|
578
|
+
exact
|
|
579
|
+
? `Actual local usage observed: ${total.toLocaleString()} tokens across ${usage.sessions.length} recent session(s).`
|
|
580
|
+
: `Estimated local usage observed: ${total.toLocaleString()} tokens across ${usage.sessions.length} recent session(s).`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (toolTokens >= 50000) {
|
|
585
|
+
addIssue(
|
|
586
|
+
issues,
|
|
587
|
+
toolTokens >= 150000 ? "high" : "medium",
|
|
588
|
+
"mcp_tooling",
|
|
589
|
+
`Tool output/context contributed about ${formatTokenCount(toolTokens)} tokens`,
|
|
590
|
+
"Large tool results and repeated command output can dominate coding-agent context.",
|
|
591
|
+
"Prefer targeted file reads and summarize long logs before pasting or loading them.",
|
|
592
|
+
`Local session estimate: about ${toolTokens.toLocaleString()} tool/output tokens in recent sessions.`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (highRiskSessions.length) {
|
|
597
|
+
addIssue(
|
|
598
|
+
issues,
|
|
599
|
+
"medium",
|
|
600
|
+
"repo_size",
|
|
601
|
+
`${highRiskSessions.length} recent session${highRiskSessions.length === 1 ? "" : "s"} reached high context risk`,
|
|
602
|
+
"Long-running sessions tend to accumulate stale context, repeated reads, and tool output.",
|
|
603
|
+
"Start a new session after major task boundaries and use scoped `.prismo/*-context.md` files.",
|
|
604
|
+
"Actual/observed local sessions crossed Prismo's high context-risk threshold."
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function buildRealUsageRecommendations(usage) {
|
|
610
|
+
if (!usage || !usage.sessions.length) return [];
|
|
611
|
+
const recs = [];
|
|
612
|
+
if (usage.totals.displayTokens >= 1000000) {
|
|
613
|
+
recs.push("Use real session usage as the primary optimization signal; prioritize reducing the largest recent sessions first.");
|
|
614
|
+
recs.push("Run `prismo optimize` and start coding sessions from `.prismo/architecture-summary.md` instead of broad repo exploration.");
|
|
615
|
+
}
|
|
616
|
+
if (usage.totals.toolTokens >= 50000) {
|
|
617
|
+
recs.push("Reduce large tool outputs by narrowing commands, reading smaller file ranges, and summarizing logs before loading them.");
|
|
618
|
+
}
|
|
619
|
+
if (usage.sessions.some((session) => session.turns >= 25)) {
|
|
620
|
+
recs.push("Split long-running coding sessions at task boundaries to prevent context accumulation.");
|
|
621
|
+
}
|
|
622
|
+
return recs;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function scanRepo(rootDir = process.cwd(), options = {}) {
|
|
626
|
+
const root = path.resolve(rootDir);
|
|
627
|
+
if (!fs.existsSync(root)) {
|
|
628
|
+
throw new Error(`Path not found: ${root}`);
|
|
629
|
+
}
|
|
630
|
+
let rootStat;
|
|
631
|
+
try {
|
|
632
|
+
rootStat = fs.statSync(root);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
throw new Error(`Cannot access path: ${root}. ${error.message}`);
|
|
635
|
+
}
|
|
636
|
+
if (!rootStat.isDirectory()) {
|
|
637
|
+
throw new Error(`Expected a directory to scan, got: ${root}`);
|
|
638
|
+
}
|
|
639
|
+
const hasGitignore = fs.existsSync(path.join(root, ".gitignore"));
|
|
640
|
+
const hasClaudeIgnore = fs.existsSync(path.join(root, ".claudeignore"));
|
|
641
|
+
const repoDetected = fs.existsSync(path.join(root, ".git")) || fs.existsSync(path.join(root, "package.json")) || fs.existsSync(path.join(root, "pyproject.toml")) || fs.existsSync(path.join(root, "go.mod")) || fs.existsSync(path.join(root, "Cargo.toml"));
|
|
642
|
+
const gitignorePatterns = readIgnoreFile(root, ".gitignore");
|
|
643
|
+
const claudeIgnorePatterns = readIgnoreFile(root, ".claudeignore");
|
|
644
|
+
const combinedIgnorePatterns = Array.from(new Set([...gitignorePatterns, ...claudeIgnorePatterns]));
|
|
645
|
+
|
|
646
|
+
const { files, highRiskDirs } = walkRepo(root, combinedIgnorePatterns);
|
|
647
|
+
const instructionFiles = scanInstructionFiles(root);
|
|
648
|
+
const largeFiles = classifyLargeFiles(files);
|
|
649
|
+
const exposedLargeFiles = largeFiles.filter((file) => !file.ignored);
|
|
650
|
+
const exposedHighRiskDirs = highRiskDirs.filter((dir) => dir.exposed);
|
|
651
|
+
const ignoredHighRiskDirs = highRiskDirs.filter((dir) => !dir.exposed);
|
|
652
|
+
const claudeConfig = scanClaudeConfig(root);
|
|
653
|
+
const codexConfig = scanCodexConfig(root);
|
|
654
|
+
|
|
655
|
+
const issues = [];
|
|
656
|
+
|
|
657
|
+
const claudeFile = instructionFiles.find((file) => file.isClaude);
|
|
658
|
+
if (claudeFile && claudeFile.tokens > 2000) {
|
|
659
|
+
addIssue(
|
|
660
|
+
issues,
|
|
661
|
+
"high",
|
|
662
|
+
"instruction_file",
|
|
663
|
+
`CLAUDE.md is ~${claudeFile.tokens.toLocaleString()} tokens`,
|
|
664
|
+
"Large persistent instruction files can raise baseline token usage in Claude Code-style workflows.",
|
|
665
|
+
"Trim CLAUDE.md under 500 tokens and link to longer docs only when needed.",
|
|
666
|
+
estimateClaudeInstructionImpact(claudeFile.tokens)
|
|
667
|
+
);
|
|
668
|
+
} else if (claudeFile && claudeFile.tokens > 500) {
|
|
669
|
+
addIssue(
|
|
670
|
+
issues,
|
|
671
|
+
"medium",
|
|
672
|
+
"instruction_file",
|
|
673
|
+
`CLAUDE.md is ~${claudeFile.tokens.toLocaleString()} tokens`,
|
|
674
|
+
"This is above the recommended persistent-instruction budget.",
|
|
675
|
+
"Keep CLAUDE.md focused on durable project rules.",
|
|
676
|
+
estimateClaudeInstructionImpact(claudeFile.tokens)
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
for (const file of instructionFiles.filter((item) => !item.isClaude && item.tokens > 2000)) {
|
|
681
|
+
addIssue(
|
|
682
|
+
issues,
|
|
683
|
+
"medium",
|
|
684
|
+
file.path.toLowerCase().includes("codex") || file.path === "AGENTS.md" ? "codex_config" : "instruction_file",
|
|
685
|
+
`${file.path} is ~${file.tokens.toLocaleString()} tokens`,
|
|
686
|
+
"Large instruction/readme files may be repeatedly loaded by coding agents.",
|
|
687
|
+
"Split long context into task-specific docs and reference only what is needed.",
|
|
688
|
+
`Potential savings estimate: reduce repeated baseline context by trimming or splitting this ~${file.tokens.toLocaleString()} token file.`
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
for (const file of instructionFiles.filter((item) => !item.isClaude && item.tokens > 500 && item.tokens <= 2000)) {
|
|
693
|
+
addIssue(
|
|
694
|
+
issues,
|
|
695
|
+
"low",
|
|
696
|
+
file.path.toLowerCase().includes("codex") || file.path === "AGENTS.md" ? "codex_config" : "instruction_file",
|
|
697
|
+
`${file.path} is ~${file.tokens.toLocaleString()} tokens`,
|
|
698
|
+
"Moderately large project instructions can become recurring baseline context in coding-agent workflows.",
|
|
699
|
+
"Keep persistent instructions concise and move task-specific notes into separate docs.",
|
|
700
|
+
`Potential savings estimate: review this ~${file.tokens.toLocaleString()} token file for repeated context bloat.`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (!hasClaudeIgnore) {
|
|
705
|
+
addIssue(
|
|
706
|
+
issues,
|
|
707
|
+
exposedHighRiskDirs.length || exposedLargeFiles.length ? "critical" : "high",
|
|
708
|
+
"ignore_file",
|
|
709
|
+
".claudeignore not found",
|
|
710
|
+
"Claude Code-style workflows may expose generated files, caches, and logs unless they are ignored.",
|
|
711
|
+
"Create .claudeignore using the generated suggestions.",
|
|
712
|
+
exposedHighRiskDirs.length || exposedLargeFiles.length
|
|
713
|
+
? "Likely avoidable token exposure: missing ignore coverage plus exposed large/generated files increases broad repo-read risk."
|
|
714
|
+
: "Potential savings estimate: prevents generated files and logs from entering future agent context."
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (!hasGitignore) {
|
|
719
|
+
addIssue(
|
|
720
|
+
issues,
|
|
721
|
+
"medium",
|
|
722
|
+
"ignore_file",
|
|
723
|
+
".gitignore not found",
|
|
724
|
+
"Missing .gitignore makes it harder to infer generated or irrelevant files.",
|
|
725
|
+
"Create .gitignore and mirror relevant entries into .claudeignore.",
|
|
726
|
+
"Potential savings estimate: better ignore baselines reduce accidental generated-file exposure."
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (exposedHighRiskDirs.length) {
|
|
731
|
+
addIssue(
|
|
732
|
+
issues,
|
|
733
|
+
exposedHighRiskDirs.length > 5 && !hasClaudeIgnore ? "critical" : exposedHighRiskDirs.length > 3 ? "high" : "medium",
|
|
734
|
+
"risky_directory",
|
|
735
|
+
`${exposedHighRiskDirs.length} token-bloat director${exposedHighRiskDirs.length === 1 ? "y" : "ies"} may be visible`,
|
|
736
|
+
exposedHighRiskDirs.slice(0, 8).map((dir) => `${dir.path}/`).join(", "),
|
|
737
|
+
"Ignore generated/cache/build folders for coding-agent workflows.",
|
|
738
|
+
estimateRiskyDirImpact(exposedHighRiskDirs)
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (exposedLargeFiles.length) {
|
|
743
|
+
addIssue(
|
|
744
|
+
issues,
|
|
745
|
+
exposedLargeFiles.some((file) => file.size >= 1024 * 1024) ? "high" : "medium",
|
|
746
|
+
"large_file",
|
|
747
|
+
`${exposedLargeFiles.length} exposed large file${exposedLargeFiles.length === 1 ? "" : "s"} detected`,
|
|
748
|
+
exposedLargeFiles.slice(0, 6).map((file) => `${file.path} (${formatBytes(file.size)})`).join(", "),
|
|
749
|
+
"Avoid loading large artifacts directly; add generated/log files to .claudeignore.",
|
|
750
|
+
estimateLargeFileImpact(exposedLargeFiles)
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (claudeConfig.mcpServers >= 5) {
|
|
755
|
+
addIssue(
|
|
756
|
+
issues,
|
|
757
|
+
"medium",
|
|
758
|
+
"mcp_tooling",
|
|
759
|
+
`${claudeConfig.mcpServers} MCP servers detected in Claude config`,
|
|
760
|
+
"Many active MCP servers can increase tool overhead and agent search space.",
|
|
761
|
+
"Disable MCP servers not needed for the current repo.",
|
|
762
|
+
estimateMcpImpact(claudeConfig.mcpServers)
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (claudeConfig.hooks >= 5) {
|
|
767
|
+
addIssue(
|
|
768
|
+
issues,
|
|
769
|
+
"low",
|
|
770
|
+
"claude_config",
|
|
771
|
+
`${claudeConfig.hooks} Claude hooks detected`,
|
|
772
|
+
"Large hook setups can add workflow overhead or noisy tool results.",
|
|
773
|
+
"Keep hooks scoped to the current workflow.",
|
|
774
|
+
"Possible workflow overhead: hook output can add noisy tool results if it is too broad."
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (codexConfig.mcpServers >= 5) {
|
|
779
|
+
addIssue(
|
|
780
|
+
issues,
|
|
781
|
+
"medium",
|
|
782
|
+
"codex_config",
|
|
783
|
+
`${codexConfig.mcpServers} MCP/tool references detected in Codex config`,
|
|
784
|
+
"Large tool surfaces can add overhead in OpenAI/Codex workflows.",
|
|
785
|
+
"Keep Codex tools scoped to the task.",
|
|
786
|
+
estimateMcpImpact(codexConfig.mcpServers)
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (!repoDetected) {
|
|
791
|
+
addIssue(
|
|
792
|
+
issues,
|
|
793
|
+
"low",
|
|
794
|
+
"repo_size",
|
|
795
|
+
"No common repo marker detected",
|
|
796
|
+
"Prismo did not find .git, package.json, pyproject.toml, go.mod, or Cargo.toml at the scan root.",
|
|
797
|
+
"Run Prismo from the repository root for the most useful results.",
|
|
798
|
+
"No token estimate; this is an onboarding/setup warning."
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const realUsage = options.includeUsage ? getUsageSummary({ tool: options.usageTool || "all", cwd: root, limit: options.usageLimit || 5 }) : null;
|
|
803
|
+
addRealUsageIssues(issues, realUsage);
|
|
804
|
+
|
|
805
|
+
const sourceFiles = files.filter((file) => file.kind === "source").length;
|
|
806
|
+
const stats = {
|
|
807
|
+
totalFiles: files.length,
|
|
808
|
+
sourceFiles,
|
|
809
|
+
largeFiles: largeFiles.length,
|
|
810
|
+
exposedLargeFiles: exposedLargeFiles.length,
|
|
811
|
+
highRiskDirs: highRiskDirs.length,
|
|
812
|
+
exposedHighRiskDirs: exposedHighRiskDirs.length,
|
|
813
|
+
ignoredHighRiskDirs: ignoredHighRiskDirs.length,
|
|
814
|
+
};
|
|
815
|
+
if (stats.totalFiles === 0) {
|
|
816
|
+
addIssue(
|
|
817
|
+
issues,
|
|
818
|
+
"low",
|
|
819
|
+
"repo_size",
|
|
820
|
+
"Folder is empty",
|
|
821
|
+
"There are no files to scan, so Prismo can only provide setup guidance.",
|
|
822
|
+
"Run Prismo inside a project after files have been added.",
|
|
823
|
+
"No token estimate; no AI-readable files were found."
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
if (stats.totalFiles > 10000) {
|
|
827
|
+
addIssue(
|
|
828
|
+
issues,
|
|
829
|
+
"medium",
|
|
830
|
+
"repo_size",
|
|
831
|
+
`Huge repo surface detected (${stats.totalFiles.toLocaleString()} files)`,
|
|
832
|
+
"Very large repos increase broad exploration risk for coding agents.",
|
|
833
|
+
"Use scoped context packs and ignore generated/vendor folders aggressively.",
|
|
834
|
+
"Likely avoidable token exposure: large repos make repeated discovery more expensive."
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
const score = scoreScan(issues, stats);
|
|
838
|
+
const largeFileSuggestions = exposedLargeFiles
|
|
839
|
+
.filter((file) => file.size >= 1024 * 1024 || ["log", "json", "minified", "lock/generated"].includes(file.kind))
|
|
840
|
+
.map((file) => file.path);
|
|
841
|
+
const recommendedClaudeIgnore = Array.from(new Set([
|
|
842
|
+
...DEFAULT_CLAUDEIGNORE,
|
|
843
|
+
...gitignorePatterns.filter((line) => !line.startsWith("!")),
|
|
844
|
+
...largeFileSuggestions,
|
|
845
|
+
]));
|
|
846
|
+
const recommendations = buildRecommendations({
|
|
847
|
+
hasClaudeIgnore,
|
|
848
|
+
gitignorePatterns,
|
|
849
|
+
exposedHighRiskDirs,
|
|
850
|
+
largeFiles,
|
|
851
|
+
instructionFiles,
|
|
852
|
+
claudeConfig,
|
|
853
|
+
});
|
|
854
|
+
buildRealUsageRecommendations(realUsage).forEach((rec) => recommendations.push(rec));
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
root,
|
|
858
|
+
score: score.score,
|
|
859
|
+
risk: score.risk,
|
|
860
|
+
avoidableWaste: score.avoidableWaste,
|
|
861
|
+
issues,
|
|
862
|
+
recommendations,
|
|
863
|
+
realUsage,
|
|
864
|
+
files,
|
|
865
|
+
instructionFiles,
|
|
866
|
+
largeFiles,
|
|
867
|
+
exposedLargeFiles,
|
|
868
|
+
highRiskDirs,
|
|
869
|
+
exposedHighRiskDirs,
|
|
870
|
+
ignoredHighRiskDirs,
|
|
871
|
+
claudeConfig,
|
|
872
|
+
codexConfig,
|
|
873
|
+
stats,
|
|
874
|
+
hasGitignore,
|
|
875
|
+
hasClaudeIgnore,
|
|
876
|
+
repoDetected,
|
|
877
|
+
recommendedClaudeIgnore,
|
|
878
|
+
topTokenLeaks: getTopTokenLeaks(issues),
|
|
879
|
+
generatedAt: new Date().toISOString(),
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function renderTerminalReport(result, options = {}) {
|
|
884
|
+
const reportEnabled = options.reportEnabled !== false;
|
|
885
|
+
const useColor = options.color !== false;
|
|
886
|
+
const riskTone = result.risk === "High" ? "red" : result.risk === "Medium" ? "yellow" : "green";
|
|
887
|
+
const lines = [];
|
|
888
|
+
lines.push("");
|
|
889
|
+
lines.push(color("Prismo Dev Scan", "bold", useColor));
|
|
890
|
+
lines.push("");
|
|
891
|
+
lines.push(`Score: ${color(`${result.score}/100`, riskTone, useColor)} | Risk: ${color(result.risk, riskTone, useColor)} | Token leaks: ${result.issues.length}`);
|
|
892
|
+
lines.push(`Estimated avoidable waste: ${result.avoidableWaste}`);
|
|
893
|
+
lines.push("");
|
|
894
|
+
lines.push(color("Top Token Leaks", "bold", useColor));
|
|
895
|
+
if (!result.topTokenLeaks.length) {
|
|
896
|
+
lines.push("1. [ok] No major token leaks detected");
|
|
897
|
+
} else {
|
|
898
|
+
result.topTokenLeaks.forEach((leak, index) => lines.push(`${index + 1}. ${leak}`));
|
|
899
|
+
}
|
|
900
|
+
lines.push("");
|
|
901
|
+
const nextCommands = getNextCommands(result, options.scope);
|
|
902
|
+
lines.push(color("Top Fix", "bold", useColor));
|
|
903
|
+
lines.push(`Run: ${nextCommands[0]}`);
|
|
904
|
+
if (nextCommands[1]) lines.push(`Then: ${nextCommands[1]}`);
|
|
905
|
+
if (nextCommands[2]) lines.push(`Then: ${nextCommands[2]}`);
|
|
906
|
+
lines.push("");
|
|
907
|
+
lines.push(color("Scan Context", "bold", useColor));
|
|
908
|
+
lines.push(`- Files scanned: ${result.stats.totalFiles.toLocaleString()}`);
|
|
909
|
+
lines.push(`- Source files: ${result.stats.sourceFiles.toLocaleString()}`);
|
|
910
|
+
lines.push(`- Large files: ${result.stats.largeFiles.toLocaleString()} (${result.stats.exposedLargeFiles} exposed)`);
|
|
911
|
+
lines.push(`- Risky directories: ${result.stats.highRiskDirs} (${result.stats.exposedHighRiskDirs} exposed)`);
|
|
912
|
+
lines.push(`- Repo detected: ${result.repoDetected ? "yes" : "no"}`);
|
|
913
|
+
if (result.realUsage && result.realUsage.sessions.length) {
|
|
914
|
+
lines.push(`- Real local usage: ${formatTokenCount(result.realUsage.totals.displayTokens)} tokens across ${result.realUsage.sessions.length} session(s)`);
|
|
915
|
+
lines.push(`- Usage confidence: ${result.realUsage.confidence}`);
|
|
916
|
+
} else if (result.realUsage) {
|
|
917
|
+
lines.push("- Real local usage: no matching local Codex/Claude Code sessions found for this repo");
|
|
918
|
+
}
|
|
919
|
+
lines.push("");
|
|
920
|
+
lines.push(color("Issues", "bold", useColor));
|
|
921
|
+
if (!result.issues.length) {
|
|
922
|
+
lines.push("- [ok] No major token-waste risks detected.");
|
|
923
|
+
} else {
|
|
924
|
+
for (const issue of result.issues.slice(0, 8)) {
|
|
925
|
+
const icon = color(severityIcon(issue.severity), severityColor(issue.severity), useColor);
|
|
926
|
+
lines.push(`- ${icon} ${issue.title}. ${issue.description}`);
|
|
927
|
+
if (issue.estimatedTokenImpact) lines.push(` ${issue.estimatedTokenImpact}`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
lines.push("");
|
|
931
|
+
lines.push(color("Recommended Fixes", "bold", useColor));
|
|
932
|
+
result.recommendations.forEach((rec, index) => lines.push(`${index + 1}. ${rec}`));
|
|
933
|
+
lines.push("");
|
|
934
|
+
lines.push(result.realUsage && result.realUsage.sessions.length
|
|
935
|
+
? "Usage findings come from local Codex/Claude Code logs when exact token fields are available; repo-risk estimates remain heuristic."
|
|
936
|
+
: result.realUsage
|
|
937
|
+
? "No matching local usage sessions were found; repo-risk estimates remain heuristic."
|
|
938
|
+
: "Potential savings estimates are heuristic and local-only, not provider billing data.");
|
|
939
|
+
lines.push("");
|
|
940
|
+
lines.push(reportEnabled ? "Report: prismo-dev-report.md" : "Report: skipped (--no-report)");
|
|
941
|
+
return lines.join("\n");
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function renderMarkdownReport(result) {
|
|
945
|
+
const lines = [];
|
|
946
|
+
lines.push("# Prismo Dev Scan Report");
|
|
947
|
+
lines.push("");
|
|
948
|
+
lines.push("## Executive Summary");
|
|
949
|
+
lines.push("");
|
|
950
|
+
lines.push(`- **Score:** ${result.score}/100`);
|
|
951
|
+
lines.push(`- **Risk Level:** ${result.risk}`);
|
|
952
|
+
lines.push(`- **Token Leaks Found:** ${result.issues.length}`);
|
|
953
|
+
lines.push(`- **Estimated Avoidable Waste:** ${result.avoidableWaste}`);
|
|
954
|
+
lines.push(`- **Repo:** \`${result.root}\``);
|
|
955
|
+
lines.push(`- **Generated At:** ${result.generatedAt}`);
|
|
956
|
+
if (result.realUsage) {
|
|
957
|
+
lines.push(`- **Real Local Usage:** ${result.realUsage.totals.displayTokens.toLocaleString()} tokens across ${result.realUsage.sessions.length} session(s)`);
|
|
958
|
+
lines.push(`- **Usage Confidence:** ${result.realUsage.confidence}`);
|
|
959
|
+
}
|
|
960
|
+
lines.push("");
|
|
961
|
+
lines.push("Estimates are based on local file-size and configuration heuristics. They are not provider billing data and are not guaranteed savings.");
|
|
962
|
+
lines.push("");
|
|
963
|
+
lines.push("## Top Token Leaks");
|
|
964
|
+
lines.push("");
|
|
965
|
+
if (!result.topTokenLeaks.length) {
|
|
966
|
+
lines.push("1. No major token leaks detected.");
|
|
967
|
+
} else {
|
|
968
|
+
result.topTokenLeaks.forEach((leak, index) => lines.push(`${index + 1}. ${leak}`));
|
|
969
|
+
}
|
|
970
|
+
lines.push("");
|
|
971
|
+
lines.push("## Repo Context");
|
|
972
|
+
lines.push("");
|
|
973
|
+
lines.push(`- Total files scanned: ${result.stats.totalFiles}`);
|
|
974
|
+
lines.push(`- Source files: ${result.stats.sourceFiles}`);
|
|
975
|
+
lines.push(`- Large files: ${result.stats.largeFiles}`);
|
|
976
|
+
lines.push(`- Exposed large files: ${result.stats.exposedLargeFiles}`);
|
|
977
|
+
lines.push(`- Token-bloat directories: ${result.stats.highRiskDirs}`);
|
|
978
|
+
lines.push(`- Exposed token-bloat directories: ${result.stats.exposedHighRiskDirs}`);
|
|
979
|
+
lines.push("");
|
|
980
|
+
if (result.realUsage) {
|
|
981
|
+
lines.push("## Real Local Usage");
|
|
982
|
+
lines.push("");
|
|
983
|
+
lines.push(`- Tool scope: ${result.realUsage.tool}`);
|
|
984
|
+
lines.push(`- Displayed tokens: ${result.realUsage.totals.displayTokens.toLocaleString()}`);
|
|
985
|
+
lines.push(`- Exact local-log tokens: ${result.realUsage.totals.exactTokens.toLocaleString()}`);
|
|
986
|
+
lines.push(`- Estimated tool/output tokens: ${result.realUsage.totals.toolTokens.toLocaleString()}`);
|
|
987
|
+
lines.push(`- Confidence: ${result.realUsage.confidence}`);
|
|
988
|
+
lines.push("");
|
|
989
|
+
result.realUsage.sessions.slice(0, 5).forEach((session, index) => {
|
|
990
|
+
lines.push(`${index + 1}. ${session.tool} - ${session.title || session.sessionId}`);
|
|
991
|
+
lines.push(` - Tokens: ${session.displayTokens.toLocaleString()} (${session.confidence})`);
|
|
992
|
+
lines.push(` - Risk: ${session.contextRisk}; turns: ${session.turns}; tools: ${session.toolCalls}`);
|
|
993
|
+
if (session.cwd) lines.push(` - CWD: \`${session.cwd}\``);
|
|
994
|
+
});
|
|
995
|
+
lines.push("");
|
|
996
|
+
}
|
|
997
|
+
lines.push("## Issues");
|
|
998
|
+
lines.push("");
|
|
999
|
+
if (!result.issues.length) {
|
|
1000
|
+
lines.push("- No major token-waste risks detected.");
|
|
1001
|
+
} else {
|
|
1002
|
+
for (const issue of result.issues) {
|
|
1003
|
+
lines.push(`- **${issue.severity.toUpperCase()}** / \`${issue.category}\`: ${issue.title}`);
|
|
1004
|
+
lines.push(` - ${issue.description}`);
|
|
1005
|
+
lines.push(` - Fix: ${issue.recommendation}`);
|
|
1006
|
+
if (issue.estimatedTokenImpact) lines.push(` - ${issue.estimatedTokenImpact}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
lines.push("");
|
|
1010
|
+
lines.push("## Claude Code Findings");
|
|
1011
|
+
lines.push("");
|
|
1012
|
+
lines.push(`- CLAUDE.md found: ${result.instructionFiles.some((file) => file.isClaude) ? "yes" : "no"}`);
|
|
1013
|
+
lines.push(`- .claudeignore found: ${result.hasClaudeIgnore ? "yes" : "no"}`);
|
|
1014
|
+
lines.push(`- Claude config files found: ${result.claudeConfig.files.length ? result.claudeConfig.files.map((file) => `\`${file}\``).join(", ") : "none"}.`);
|
|
1015
|
+
lines.push(`- MCP servers detected: ${result.claudeConfig.mcpServers}. Hooks detected: ${result.claudeConfig.hooks}. Plugin/skill references: ${result.claudeConfig.pluginRefs}.`);
|
|
1016
|
+
lines.push("- Keep `CLAUDE.md` under 500 tokens when possible.");
|
|
1017
|
+
lines.push("- Move long implementation notes into separate docs and reference them only when needed.");
|
|
1018
|
+
lines.push("- Create `.claudeignore` to hide generated files, caches, logs, coverage, and lock files.");
|
|
1019
|
+
lines.push("");
|
|
1020
|
+
lines.push("## OpenAI/Codex Findings");
|
|
1021
|
+
lines.push("");
|
|
1022
|
+
lines.push(`- AGENTS.md found: ${result.instructionFiles.some((file) => file.path === "AGENTS.md") ? "yes" : "no"}`);
|
|
1023
|
+
lines.push(`- Codex config files found: ${result.codexConfig.files.length ? result.codexConfig.files.map((file) => `\`${file}\``).join(", ") : "none"}.`);
|
|
1024
|
+
lines.push(`- Codex MCP/tool references detected: ${result.codexConfig.mcpServers}.`);
|
|
1025
|
+
lines.push("- Keep `AGENTS.md` and `.codex/` instructions concise and task-oriented.");
|
|
1026
|
+
lines.push("- Avoid pasting giant logs or generated JSON into Codex sessions.");
|
|
1027
|
+
lines.push("- Use cheaper/faster models for mechanical edits and low-risk refactors.");
|
|
1028
|
+
lines.push("");
|
|
1029
|
+
lines.push("## Large Files");
|
|
1030
|
+
lines.push("");
|
|
1031
|
+
if (!result.largeFiles.length) {
|
|
1032
|
+
lines.push("- No large text-like files over 500 KB detected.");
|
|
1033
|
+
} else {
|
|
1034
|
+
result.largeFiles.slice(0, 40).forEach((file) => {
|
|
1035
|
+
lines.push(`- \`${file.path}\` - ${formatBytes(file.size)} - ${file.kind}${file.ignored ? " - ignored" : ""}`);
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
lines.push("");
|
|
1039
|
+
lines.push("## Risky Directories");
|
|
1040
|
+
lines.push("");
|
|
1041
|
+
if (!result.highRiskDirs.length) {
|
|
1042
|
+
lines.push("- No common token-bloat directories detected.");
|
|
1043
|
+
} else {
|
|
1044
|
+
result.highRiskDirs.forEach((dir) => {
|
|
1045
|
+
lines.push(`- \`${dir.path}/\`${dir.exposed ? " - exposed" : " - ignored"}`);
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
lines.push("");
|
|
1049
|
+
lines.push("## Recommended .claudeignore");
|
|
1050
|
+
lines.push("");
|
|
1051
|
+
lines.push("```gitignore");
|
|
1052
|
+
result.recommendedClaudeIgnore.forEach((line) => lines.push(line));
|
|
1053
|
+
lines.push("```");
|
|
1054
|
+
lines.push("");
|
|
1055
|
+
lines.push("## Recommended Next Steps");
|
|
1056
|
+
lines.push("");
|
|
1057
|
+
result.recommendations.forEach((rec, index) => lines.push(`${index + 1}. ${rec}`));
|
|
1058
|
+
lines.push("");
|
|
1059
|
+
lines.push("## Disclaimer");
|
|
1060
|
+
lines.push("");
|
|
1061
|
+
lines.push("Prismo Dev Scan is a fast local scanner. It does not connect to Anthropic, OpenAI, Claude Code, Codex, Cursor, or billing accounts. Token and savings estimates are heuristic and should be treated as directional diagnostics only.");
|
|
1062
|
+
lines.push("");
|
|
1063
|
+
return lines.join("\n");
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function backupIfExists(filePath) {
|
|
1067
|
+
if (!fs.existsSync(filePath)) return null;
|
|
1068
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1069
|
+
const backupPath = `${filePath}.${stamp}.bak`;
|
|
1070
|
+
fs.copyFileSync(filePath, backupPath);
|
|
1071
|
+
return backupPath;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function writeReport(result) {
|
|
1075
|
+
const reportPath = path.join(result.root, "prismo-dev-report.md");
|
|
1076
|
+
const backupPath = backupIfExists(reportPath);
|
|
1077
|
+
fs.writeFileSync(reportPath, renderMarkdownReport(result), "utf8");
|
|
1078
|
+
return { reportPath, backupPath };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function renderClaudeTemplate(result) {
|
|
1082
|
+
const claudeFile = result.instructionFiles.find((file) => file.isClaude);
|
|
1083
|
+
return [
|
|
1084
|
+
"# Prismo Optimized CLAUDE.md Template",
|
|
1085
|
+
"",
|
|
1086
|
+
"Use this as a concise replacement draft. Review manually before changing your real CLAUDE.md.",
|
|
1087
|
+
"",
|
|
1088
|
+
"## Project Rules",
|
|
1089
|
+
"",
|
|
1090
|
+
"- Keep changes scoped to the requested task.",
|
|
1091
|
+
"- Prefer existing project patterns and tests.",
|
|
1092
|
+
"- Do not load generated files, logs, coverage reports, or build artifacts unless explicitly needed.",
|
|
1093
|
+
"- Reference long docs only when the task requires them.",
|
|
1094
|
+
"",
|
|
1095
|
+
"## Token Hygiene",
|
|
1096
|
+
"",
|
|
1097
|
+
"- Keep persistent instructions under roughly 500 tokens.",
|
|
1098
|
+
"- Move long implementation notes into separate docs.",
|
|
1099
|
+
"- Start a fresh session for unrelated work.",
|
|
1100
|
+
"",
|
|
1101
|
+
claudeFile ? `Original CLAUDE.md estimate: ~${claudeFile.tokens.toLocaleString()} tokens.` : "",
|
|
1102
|
+
"",
|
|
1103
|
+
].join("\n");
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function renderAgentsRecommendations(result) {
|
|
1107
|
+
const codexIssues = result.issues.filter((issue) => issue.category === "codex_config" || issue.title.includes("AGENTS.md"));
|
|
1108
|
+
return [
|
|
1109
|
+
"# Prismo AGENTS.md / Codex Recommendations",
|
|
1110
|
+
"",
|
|
1111
|
+
"Review these suggestions manually before changing AGENTS.md or .codex configuration.",
|
|
1112
|
+
"",
|
|
1113
|
+
"## Findings",
|
|
1114
|
+
"",
|
|
1115
|
+
...(codexIssues.length
|
|
1116
|
+
? codexIssues.map((issue) => `- ${issue.title}: ${issue.recommendation}`)
|
|
1117
|
+
: ["- No major Codex-specific risks detected, but keeping persistent instructions concise is still recommended."]),
|
|
1118
|
+
"",
|
|
1119
|
+
"## Suggested Practices",
|
|
1120
|
+
"",
|
|
1121
|
+
"- Keep AGENTS.md focused on durable project rules.",
|
|
1122
|
+
"- Move task-specific context into separate docs.",
|
|
1123
|
+
"- Avoid pasting giant logs, generated JSON, lockfiles, and coverage reports into Codex sessions.",
|
|
1124
|
+
"- Scope MCP/tool configuration to the current project.",
|
|
1125
|
+
"",
|
|
1126
|
+
].join("\n");
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function safeReadJson(filePath) {
|
|
1130
|
+
try {
|
|
1131
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1132
|
+
} catch {
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function findRepoFiles(result, predicate, limit = 40) {
|
|
1138
|
+
return result.files
|
|
1139
|
+
? result.files.filter((file) => !file.ignored && file.kind !== "binary" && predicate(file.path, file)).slice(0, limit)
|
|
1140
|
+
: [];
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function detectFrameworks(root, result) {
|
|
1144
|
+
const frameworks = new Set();
|
|
1145
|
+
const packageFiles = findRepoFiles(result, (rel) => path.basename(rel) === "package.json" && !rel.includes("node_modules/"), 12);
|
|
1146
|
+
for (const file of packageFiles) {
|
|
1147
|
+
const pkg = safeReadJson(path.join(root, file.path));
|
|
1148
|
+
const deps = { ...(pkg && pkg.dependencies), ...(pkg && pkg.devDependencies) };
|
|
1149
|
+
if (deps.next) frameworks.add("Next.js");
|
|
1150
|
+
if (deps.react) frameworks.add("React");
|
|
1151
|
+
if (deps.express) frameworks.add("Express");
|
|
1152
|
+
if (deps["@nestjs/core"]) frameworks.add("NestJS");
|
|
1153
|
+
if (deps.prisma || deps["@prisma/client"]) frameworks.add("Prisma");
|
|
1154
|
+
if (deps.tailwindcss) frameworks.add("Tailwind");
|
|
1155
|
+
if (deps.typescript) frameworks.add("TypeScript");
|
|
1156
|
+
if (pkg) frameworks.add("Node.js");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const textFiles = new Map(result.files.map((file) => [file.path, file]));
|
|
1160
|
+
const requirements = [...textFiles.keys()].filter((rel) => rel.endsWith("requirements.txt"));
|
|
1161
|
+
for (const rel of requirements) {
|
|
1162
|
+
const text = readIfText(path.join(root, rel)) || "";
|
|
1163
|
+
if (/fastapi/i.test(text)) frameworks.add("FastAPI");
|
|
1164
|
+
if (/django/i.test(text)) frameworks.add("Django");
|
|
1165
|
+
if (/flask/i.test(text)) frameworks.add("Flask");
|
|
1166
|
+
if (/psycopg2|asyncpg|sqlalchemy/i.test(text)) frameworks.add("PostgreSQL");
|
|
1167
|
+
if (/redis/i.test(text)) frameworks.add("Redis");
|
|
1168
|
+
frameworks.add("Python");
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith("pyproject.toml"))) frameworks.add("Python");
|
|
1172
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith("Cargo.toml"))) frameworks.add("Rust");
|
|
1173
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith("go.mod"))) frameworks.add("Go");
|
|
1174
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith("docker-compose.yml") || rel.endsWith("docker-compose.yaml"))) frameworks.add("Docker");
|
|
1175
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("prisma/schema.prisma"))) frameworks.add("Prisma");
|
|
1176
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("next.config."))) frameworks.add("Next.js");
|
|
1177
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("vite.config."))) frameworks.add("Vite");
|
|
1178
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("tailwind.config."))) frameworks.add("Tailwind");
|
|
1179
|
+
if ([...textFiles.keys()].some((rel) => rel.endsWith("tsconfig.json"))) frameworks.add("TypeScript");
|
|
1180
|
+
if ([...textFiles.keys()].some((rel) => rel.includes("alembic/") || rel.endsWith("alembic.ini"))) frameworks.add("Alembic");
|
|
1181
|
+
if ([...textFiles.keys()].some((rel) => /postgres|postgresql/i.test(rel))) frameworks.add("PostgreSQL");
|
|
1182
|
+
return Array.from(frameworks).sort();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function topLevelDirectories(root) {
|
|
1186
|
+
try {
|
|
1187
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
1188
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules")
|
|
1189
|
+
.map((entry) => entry.name)
|
|
1190
|
+
.sort();
|
|
1191
|
+
} catch {
|
|
1192
|
+
return [];
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function hasPath(result, matcher) {
|
|
1197
|
+
return result.files.some((file) => matcher(file.path));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function detectEntrypoints(result) {
|
|
1201
|
+
const candidates = [
|
|
1202
|
+
"backend/app/main.py",
|
|
1203
|
+
"backend/main.py",
|
|
1204
|
+
"app/main.py",
|
|
1205
|
+
"main.py",
|
|
1206
|
+
"frontend/src/app/page.tsx",
|
|
1207
|
+
"frontend/src/app/layout.tsx",
|
|
1208
|
+
"src/app/page.tsx",
|
|
1209
|
+
"src/main.tsx",
|
|
1210
|
+
"src/index.tsx",
|
|
1211
|
+
"server.js",
|
|
1212
|
+
"index.js",
|
|
1213
|
+
"docker/docker-compose.yml",
|
|
1214
|
+
"docker-compose.yml",
|
|
1215
|
+
];
|
|
1216
|
+
const paths = new Set(result.files.map((file) => file.path));
|
|
1217
|
+
return candidates.filter((candidate) => paths.has(candidate));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function detectBackendPaths(result) {
|
|
1221
|
+
return {
|
|
1222
|
+
api: findRepoFiles(result, (rel) => /backend\/.*(router|routes|api)\//.test(rel) || /backend\/.*(router|routes).*\.py$/.test(rel), 20).map((f) => f.path),
|
|
1223
|
+
services: findRepoFiles(result, (rel) => /backend\/.*(service|services|application)/.test(rel), 20).map((f) => f.path),
|
|
1224
|
+
models: findRepoFiles(result, (rel) => /backend\/.*models\.py$/.test(rel) || /backend\/.*schema/.test(rel), 20).map((f) => f.path),
|
|
1225
|
+
db: findRepoFiles(result, (rel) => /backend\/.*(db|database|alembic|migrations)/.test(rel), 20).map((f) => f.path),
|
|
1226
|
+
config: findRepoFiles(result, (rel) => /backend\/.*(config|settings|env).*\.py$/.test(rel) || rel.endsWith("backend/requirements.txt"), 20).map((f) => f.path),
|
|
1227
|
+
auth: findRepoFiles(result, (rel) => /backend\/.*auth/.test(rel), 20).map((f) => f.path),
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function detectFrontendPaths(result) {
|
|
1232
|
+
return {
|
|
1233
|
+
app: findRepoFiles(result, (rel) => /frontend\/src\/app\//.test(rel) || /src\/app\//.test(rel), 24).map((f) => f.path),
|
|
1234
|
+
components: findRepoFiles(result, (rel) => /frontend\/src\/components\//.test(rel) || /src\/components\//.test(rel), 20).map((f) => f.path),
|
|
1235
|
+
apiClient: findRepoFiles(result, (rel) => /frontend\/src\/(lib|hooks)\/.*(api|client|query|finops)/.test(rel), 20).map((f) => f.path),
|
|
1236
|
+
styling: findRepoFiles(result, (rel) => /tailwind\.config|globals\.css|\.module\.css|frontend\/src\/app\/globals/.test(rel), 20).map((f) => f.path),
|
|
1237
|
+
state: findRepoFiles(result, (rel) => /providers\.tsx|react-query|use[A-Z].*\.ts/.test(rel), 20).map((f) => f.path),
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function createOptimizeContext(rootDir = process.cwd(), scope = null) {
|
|
1242
|
+
const scan = scanRepo(rootDir);
|
|
1243
|
+
const root = scan.root;
|
|
1244
|
+
const frameworks = detectFrameworks(root, scan);
|
|
1245
|
+
const folders = topLevelDirectories(root);
|
|
1246
|
+
const entrypoints = detectEntrypoints(scan);
|
|
1247
|
+
const backendDetected = folders.includes("backend") || frameworks.some((name) => ["FastAPI", "Django", "Flask"].includes(name));
|
|
1248
|
+
const frontendDetected = folders.includes("frontend") || frameworks.some((name) => ["Next.js", "React", "Vite"].includes(name));
|
|
1249
|
+
const backend = detectBackendPaths(scan);
|
|
1250
|
+
const frontend = detectFrontendPaths(scan);
|
|
1251
|
+
const warnings = [];
|
|
1252
|
+
if (scan.exposedLargeFiles.length) warnings.push(`${scan.exposedLargeFiles.length} exposed large file(s) may bloat AI context.`);
|
|
1253
|
+
if (!scan.hasClaudeIgnore) warnings.push(".claudeignore is missing.");
|
|
1254
|
+
if (scan.exposedHighRiskDirs.length) warnings.push(`${scan.exposedHighRiskDirs.length} generated/cache directories may be visible.`);
|
|
1255
|
+
|
|
1256
|
+
const suggestions = [
|
|
1257
|
+
"Use .prismo/architecture-summary.md as first-pass repo context instead of asking agents to explore broadly.",
|
|
1258
|
+
"Keep CLAUDE.md and AGENTS.md concise; link to generated summaries when deeper context is needed.",
|
|
1259
|
+
"Use scoped context packs for focused work, especially frontend/backend/auth tasks.",
|
|
1260
|
+
"Keep generated files, logs, coverage, build output, and lockfiles out of coding-agent context.",
|
|
1261
|
+
];
|
|
1262
|
+
|
|
1263
|
+
return {
|
|
1264
|
+
root,
|
|
1265
|
+
scope,
|
|
1266
|
+
scan,
|
|
1267
|
+
frameworks,
|
|
1268
|
+
folders,
|
|
1269
|
+
entrypoints,
|
|
1270
|
+
backendDetected,
|
|
1271
|
+
frontendDetected,
|
|
1272
|
+
backend,
|
|
1273
|
+
frontend,
|
|
1274
|
+
warnings,
|
|
1275
|
+
suggestions,
|
|
1276
|
+
estimatedContextReduction: scan.avoidableWaste,
|
|
1277
|
+
generatedAt: new Date().toISOString(),
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function mdList(items, empty = "None detected.") {
|
|
1282
|
+
if (!items || !items.length) return `- ${empty}`;
|
|
1283
|
+
return items.map((item) => `- \`${item}\``).join("\n");
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function proseList(items, empty = "none detected") {
|
|
1287
|
+
return items && items.length ? items.join(", ") : empty;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function renderArchitectureSummary(ctx) {
|
|
1291
|
+
const apiLayer = ctx.backend.api.slice(0, 6);
|
|
1292
|
+
const dbLayer = ctx.backend.db.slice(0, 6);
|
|
1293
|
+
const frontendLayer = ctx.frontend.app.slice(0, 6);
|
|
1294
|
+
return [
|
|
1295
|
+
"# Architecture Summary",
|
|
1296
|
+
"",
|
|
1297
|
+
"Use this file as the first repo-context attachment for Claude Code, Codex, Cursor, and similar tools. It is intentionally concise so agents do not have to rediscover the project from scratch.",
|
|
1298
|
+
"",
|
|
1299
|
+
"## Detected Frameworks",
|
|
1300
|
+
"",
|
|
1301
|
+
mdList(ctx.frameworks, "No common framework markers detected."),
|
|
1302
|
+
"",
|
|
1303
|
+
"## Major Folders",
|
|
1304
|
+
"",
|
|
1305
|
+
mdList(ctx.folders),
|
|
1306
|
+
"",
|
|
1307
|
+
"## Likely Architecture",
|
|
1308
|
+
"",
|
|
1309
|
+
`- Frontend detected: ${ctx.frontendDetected ? "yes" : "no"}`,
|
|
1310
|
+
`- Backend detected: ${ctx.backendDetected ? "yes" : "no"}`,
|
|
1311
|
+
`- API layer likely lives in: ${apiLayer.map((p) => `\`${p}\``).join(", ") || "not detected"}`,
|
|
1312
|
+
`- Database/migration layer likely lives in: ${dbLayer.map((p) => `\`${p}\``).join(", ") || "not detected"}`,
|
|
1313
|
+
`- Frontend routes/app surface likely lives in: ${frontendLayer.map((p) => `\`${p}\``).join(", ") || "not detected"}`,
|
|
1314
|
+
"",
|
|
1315
|
+
"## Recommended Read Order",
|
|
1316
|
+
"",
|
|
1317
|
+
"- Start here.",
|
|
1318
|
+
ctx.backendDetected ? "- For backend work, read `.prismo/backend-summary.md` next." : "",
|
|
1319
|
+
ctx.frontendDetected ? "- For frontend work, read `.prismo/frontend-summary.md` next." : "",
|
|
1320
|
+
"- Then inspect only the files directly relevant to the task.",
|
|
1321
|
+
"- Avoid broad recursive reads unless the task truly needs a repo-wide audit.",
|
|
1322
|
+
"",
|
|
1323
|
+
"## Key Entrypoints",
|
|
1324
|
+
"",
|
|
1325
|
+
mdList(ctx.entrypoints),
|
|
1326
|
+
"",
|
|
1327
|
+
"## Context Risks",
|
|
1328
|
+
"",
|
|
1329
|
+
ctx.warnings.length ? ctx.warnings.map((warning) => `- ${warning}`).join("\n") : "- No major local context risks detected.",
|
|
1330
|
+
"",
|
|
1331
|
+
"## AI Workflow Notes",
|
|
1332
|
+
"",
|
|
1333
|
+
"- Prefer this summary before broad repo reads.",
|
|
1334
|
+
"- Use scoped context files for focused tasks.",
|
|
1335
|
+
"- Avoid generated folders, caches, logs, coverage output, and large analysis files.",
|
|
1336
|
+
"",
|
|
1337
|
+
].join("\n");
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function renderBackendSummary(ctx) {
|
|
1341
|
+
return [
|
|
1342
|
+
"# Backend Summary",
|
|
1343
|
+
"",
|
|
1344
|
+
"Only reasonably inferable backend structure is listed here.",
|
|
1345
|
+
"",
|
|
1346
|
+
"## API / Router Files",
|
|
1347
|
+
"",
|
|
1348
|
+
mdList(ctx.backend.api),
|
|
1349
|
+
"",
|
|
1350
|
+
"## Services / Application Logic",
|
|
1351
|
+
"",
|
|
1352
|
+
mdList(ctx.backend.services),
|
|
1353
|
+
"",
|
|
1354
|
+
"## Models / Schemas",
|
|
1355
|
+
"",
|
|
1356
|
+
mdList(ctx.backend.models),
|
|
1357
|
+
"",
|
|
1358
|
+
"## Database / Migration Layer",
|
|
1359
|
+
"",
|
|
1360
|
+
mdList(ctx.backend.db),
|
|
1361
|
+
"",
|
|
1362
|
+
"## Auth-Related Paths",
|
|
1363
|
+
"",
|
|
1364
|
+
mdList(ctx.backend.auth),
|
|
1365
|
+
"",
|
|
1366
|
+
"## Config / Environment Hints",
|
|
1367
|
+
"",
|
|
1368
|
+
mdList(ctx.backend.config),
|
|
1369
|
+
"",
|
|
1370
|
+
].join("\n");
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function renderFrontendSummary(ctx) {
|
|
1374
|
+
return [
|
|
1375
|
+
"# Frontend Summary",
|
|
1376
|
+
"",
|
|
1377
|
+
"Only reasonably inferable frontend structure is listed here.",
|
|
1378
|
+
"",
|
|
1379
|
+
"## App / Routing Surface",
|
|
1380
|
+
"",
|
|
1381
|
+
mdList(ctx.frontend.app),
|
|
1382
|
+
"",
|
|
1383
|
+
"## Components",
|
|
1384
|
+
"",
|
|
1385
|
+
mdList(ctx.frontend.components),
|
|
1386
|
+
"",
|
|
1387
|
+
"## API Clients / Data Hooks",
|
|
1388
|
+
"",
|
|
1389
|
+
mdList(ctx.frontend.apiClient),
|
|
1390
|
+
"",
|
|
1391
|
+
"## State / Providers",
|
|
1392
|
+
"",
|
|
1393
|
+
mdList(ctx.frontend.state),
|
|
1394
|
+
"",
|
|
1395
|
+
"## Styling",
|
|
1396
|
+
"",
|
|
1397
|
+
mdList(ctx.frontend.styling),
|
|
1398
|
+
"",
|
|
1399
|
+
].join("\n");
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function renderRecommendedClaude(ctx) {
|
|
1403
|
+
const commands = [];
|
|
1404
|
+
if (hasPath(ctx.scan, (rel) => rel === "package.json")) {
|
|
1405
|
+
commands.push("npm run scan");
|
|
1406
|
+
commands.push("npm run test:scan");
|
|
1407
|
+
}
|
|
1408
|
+
if (hasPath(ctx.scan, (rel) => rel === "frontend/package.json")) commands.push("cd frontend && npm run test");
|
|
1409
|
+
if (hasPath(ctx.scan, (rel) => rel === "backend/pytest.ini" || rel.startsWith("backend/tests/"))) commands.push("cd backend && pytest");
|
|
1410
|
+
return [
|
|
1411
|
+
"# CLAUDE.md",
|
|
1412
|
+
"",
|
|
1413
|
+
"Keep context small. Start with `.prismo/architecture-summary.md`; use scoped `.prismo/*-summary.md` files only when relevant.",
|
|
1414
|
+
"",
|
|
1415
|
+
"## Commands",
|
|
1416
|
+
"",
|
|
1417
|
+
...(commands.length ? commands.map((cmd) => `- \`${cmd}\``) : ["- Check package-specific scripts before running tests."]),
|
|
1418
|
+
"",
|
|
1419
|
+
"## Architecture",
|
|
1420
|
+
"",
|
|
1421
|
+
`- Frameworks: ${ctx.frameworks.join(", ") || "not detected"}.`,
|
|
1422
|
+
`- Backend: ${ctx.backendDetected ? "see `.prismo/backend-summary.md`" : "not detected"}.`,
|
|
1423
|
+
`- Frontend: ${ctx.frontendDetected ? "see `.prismo/frontend-summary.md`" : "not detected"}.`,
|
|
1424
|
+
`- Entrypoints: ${proseList(ctx.entrypoints)}.`,
|
|
1425
|
+
"",
|
|
1426
|
+
"## Rules",
|
|
1427
|
+
"",
|
|
1428
|
+
"- Do not read generated folders, logs, coverage reports, build output, or lockfiles unless explicitly needed.",
|
|
1429
|
+
"- Prefer existing project patterns and narrow edits.",
|
|
1430
|
+
"- Use focused context packs for auth/frontend/backend tasks.",
|
|
1431
|
+
"- Keep long implementation notes out of persistent instructions.",
|
|
1432
|
+
"- Summarize any extra files opened before making broad changes.",
|
|
1433
|
+
"",
|
|
1434
|
+
"## Important Paths",
|
|
1435
|
+
"",
|
|
1436
|
+
"- `.prismo/architecture-summary.md`",
|
|
1437
|
+
ctx.backendDetected ? "- `.prismo/backend-summary.md`" : "",
|
|
1438
|
+
ctx.frontendDetected ? "- `.prismo/frontend-summary.md`" : "",
|
|
1439
|
+
"",
|
|
1440
|
+
].join("\n");
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function renderRecommendedAgents(ctx) {
|
|
1444
|
+
return [
|
|
1445
|
+
"# AGENTS.md",
|
|
1446
|
+
"",
|
|
1447
|
+
"Use `.prismo/architecture-summary.md` first to avoid repeated broad repo exploration. Keep this file durable and short; task-specific details belong in the prompt or scoped context files.",
|
|
1448
|
+
"",
|
|
1449
|
+
"## Repo Structure",
|
|
1450
|
+
"",
|
|
1451
|
+
mdList(ctx.folders),
|
|
1452
|
+
"",
|
|
1453
|
+
"## Conventions",
|
|
1454
|
+
"",
|
|
1455
|
+
"- Keep changes scoped and follow nearby patterns.",
|
|
1456
|
+
"- Use generated `.prismo/*-summary.md` files as compact context.",
|
|
1457
|
+
"- Do not load generated artifacts, logs, coverage, caches, binary/media files, or lockfiles by default.",
|
|
1458
|
+
"- For focused work, request or attach the relevant scoped context pack.",
|
|
1459
|
+
"- Prefer small file reads and targeted searches before opening large documents.",
|
|
1460
|
+
"- Call out uncertainty instead of inferring architecture that is not present in the repo.",
|
|
1461
|
+
"",
|
|
1462
|
+
"## Suggested Workflow",
|
|
1463
|
+
"",
|
|
1464
|
+
"1. Read `.prismo/architecture-summary.md`.",
|
|
1465
|
+
"2. Read the scoped context file for the task, if one exists.",
|
|
1466
|
+
"3. Inspect only relevant source files.",
|
|
1467
|
+
"4. Run the narrowest useful tests.",
|
|
1468
|
+
"",
|
|
1469
|
+
"## Important Paths",
|
|
1470
|
+
"",
|
|
1471
|
+
mdList([".prismo/architecture-summary.md", ".prismo/recommended-.claudeignore", ".prismo/optimize-report.md"]),
|
|
1472
|
+
"",
|
|
1473
|
+
].join("\n");
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function renderGitignoreAdditions(ctx) {
|
|
1477
|
+
const additions = [
|
|
1478
|
+
".prismo/*.bak",
|
|
1479
|
+
"logs/",
|
|
1480
|
+
"test-results/",
|
|
1481
|
+
"playwright-report/",
|
|
1482
|
+
"*.tmp",
|
|
1483
|
+
"*.bak",
|
|
1484
|
+
];
|
|
1485
|
+
for (const file of ctx.scan.exposedLargeFiles) {
|
|
1486
|
+
if (file.size >= 1024 * 1024) additions.push(file.path);
|
|
1487
|
+
}
|
|
1488
|
+
return Array.from(new Set(additions)).join("\n") + "\n";
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function renderOptimizeReport(ctx, generatedFiles) {
|
|
1492
|
+
return [
|
|
1493
|
+
"# Prismo Optimize Report",
|
|
1494
|
+
"",
|
|
1495
|
+
"## Executive Summary",
|
|
1496
|
+
"",
|
|
1497
|
+
`- Estimated context reduction: ${ctx.estimatedContextReduction}`,
|
|
1498
|
+
`- Frameworks detected: ${ctx.frameworks.join(", ") || "none"}`,
|
|
1499
|
+
`- Generated at: ${ctx.generatedAt}`,
|
|
1500
|
+
"",
|
|
1501
|
+
"## AI Context Risk Areas",
|
|
1502
|
+
"",
|
|
1503
|
+
ctx.warnings.length ? ctx.warnings.map((warning) => `- ${warning}`).join("\n") : "- No major local context risks detected.",
|
|
1504
|
+
"",
|
|
1505
|
+
"## Token-Heavy Directories",
|
|
1506
|
+
"",
|
|
1507
|
+
mdList(ctx.scan.highRiskDirs.map((dir) => `${dir.path}/${dir.exposed ? " (exposed)" : " (ignored)"}`)),
|
|
1508
|
+
"",
|
|
1509
|
+
"## Optimization Suggestions",
|
|
1510
|
+
"",
|
|
1511
|
+
ctx.suggestions.map((suggestion, index) => `${index + 1}. ${suggestion}`).join("\n"),
|
|
1512
|
+
"",
|
|
1513
|
+
"## Generated Files",
|
|
1514
|
+
"",
|
|
1515
|
+
mdList(generatedFiles),
|
|
1516
|
+
"",
|
|
1517
|
+
"## Workflow Improvements",
|
|
1518
|
+
"",
|
|
1519
|
+
"- Start Claude Code/Codex with architecture-summary.md instead of asking for a broad repo scan.",
|
|
1520
|
+
"- Use frontend/backend/auth context packs for scoped tasks.",
|
|
1521
|
+
"- Keep persistent instruction files under roughly 500 tokens.",
|
|
1522
|
+
"- Avoid pasting giant logs directly into AI coding sessions.",
|
|
1523
|
+
"",
|
|
1524
|
+
].join("\n");
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function renderScopedContext(ctx, scope) {
|
|
1528
|
+
const scopeLower = scope.toLowerCase();
|
|
1529
|
+
const relevant = ctx.scan.files
|
|
1530
|
+
.filter((file) => {
|
|
1531
|
+
const rel = file.path.toLowerCase();
|
|
1532
|
+
if (scopeLower === "frontend") return rel.includes("frontend/") || rel.includes("src/app/") || rel.includes("src/components/");
|
|
1533
|
+
if (scopeLower === "backend") return rel.includes("backend/") || rel.includes("app/modules/") || rel.includes("app/shared/");
|
|
1534
|
+
if (scopeLower === "auth") return rel.includes("auth") || rel.includes("supabase") || rel.includes("login") || rel.includes("signup");
|
|
1535
|
+
return rel.includes(scopeLower);
|
|
1536
|
+
})
|
|
1537
|
+
.filter((file) => file.kind !== "binary")
|
|
1538
|
+
.slice(0, 60)
|
|
1539
|
+
.map((file) => file.path);
|
|
1540
|
+
|
|
1541
|
+
return [
|
|
1542
|
+
`# ${scope.charAt(0).toUpperCase()}${scope.slice(1)} Context`,
|
|
1543
|
+
"",
|
|
1544
|
+
"Use this as a focused context pack for AI coding workflows.",
|
|
1545
|
+
"",
|
|
1546
|
+
"## Relevant Files",
|
|
1547
|
+
"",
|
|
1548
|
+
mdList(relevant),
|
|
1549
|
+
"",
|
|
1550
|
+
"## Notes",
|
|
1551
|
+
"",
|
|
1552
|
+
"- This file is generated from deterministic path heuristics.",
|
|
1553
|
+
"- Verify flow details in source before making behavioral changes.",
|
|
1554
|
+
"- Keep follow-up context narrow; do not attach generated files or logs unless needed.",
|
|
1555
|
+
"- If this context pack is too broad, search within the listed files before opening all of them.",
|
|
1556
|
+
"",
|
|
1557
|
+
].join("\n");
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function getContextFileForScope(ctx, scope) {
|
|
1561
|
+
if (!scope) return ".prismo/architecture-summary.md";
|
|
1562
|
+
const normalized = scope.toLowerCase();
|
|
1563
|
+
if (normalized === "frontend") return ".prismo/frontend-context.md";
|
|
1564
|
+
if (normalized === "backend") return ".prismo/backend-context.md";
|
|
1565
|
+
if (normalized === "auth") return ".prismo/auth-context.md";
|
|
1566
|
+
return `.prismo/${normalized}-context.md`;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function renderStarterPrompt(ctx, scope = null) {
|
|
1570
|
+
const contextFile = getContextFileForScope(ctx, scope);
|
|
1571
|
+
const supporting = [];
|
|
1572
|
+
if (!scope) {
|
|
1573
|
+
if (ctx.backendDetected) supporting.push(".prismo/backend-summary.md");
|
|
1574
|
+
if (ctx.frontendDetected) supporting.push(".prismo/frontend-summary.md");
|
|
1575
|
+
} else if (scope === "frontend") {
|
|
1576
|
+
supporting.push(".prismo/frontend-summary.md");
|
|
1577
|
+
} else if (scope === "backend") {
|
|
1578
|
+
supporting.push(".prismo/backend-summary.md");
|
|
1579
|
+
} else if (scope === "auth") {
|
|
1580
|
+
supporting.push(".prismo/architecture-summary.md");
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
const lines = [
|
|
1584
|
+
"Use Prismo's compact repo context before exploring files.",
|
|
1585
|
+
`Start with ${contextFile}.`,
|
|
1586
|
+
];
|
|
1587
|
+
if (supporting.length) lines.push(`Also use ${supporting.join(" and ")} if needed.`);
|
|
1588
|
+
lines.push("Only inspect files directly relevant to the task.");
|
|
1589
|
+
lines.push("Do not read generated folders, logs, coverage reports, node_modules, .next, dist, build, cache folders, lockfiles, or large analysis files unless I explicitly ask.");
|
|
1590
|
+
lines.push("Before editing, summarize the small set of files you actually inspected and why.");
|
|
1591
|
+
return lines.join(" ");
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function renderContextCommand(ctx, scope = null) {
|
|
1595
|
+
const label = scope ? `${scope.charAt(0).toUpperCase()}${scope.slice(1)} Context Prompt` : "Project Context Prompt";
|
|
1596
|
+
const contextFile = getContextFileForScope(ctx, scope);
|
|
1597
|
+
const existing = fs.existsSync(path.join(ctx.root, contextFile));
|
|
1598
|
+
return [
|
|
1599
|
+
`# Prismo ${label}`,
|
|
1600
|
+
"",
|
|
1601
|
+
renderStarterPrompt(ctx, scope),
|
|
1602
|
+
"",
|
|
1603
|
+
"## Context Files",
|
|
1604
|
+
"",
|
|
1605
|
+
`- ${contextFile}${existing ? "" : " (run `prismo optimize" + (scope ? ` ${scope}` : "") + "` to generate)"}`,
|
|
1606
|
+
!scope && ctx.backendDetected ? "- .prismo/backend-summary.md" : "",
|
|
1607
|
+
!scope && ctx.frontendDetected ? "- .prismo/frontend-summary.md" : "",
|
|
1608
|
+
scope === "frontend" ? "- .prismo/frontend-summary.md" : "",
|
|
1609
|
+
scope === "backend" ? "- .prismo/backend-summary.md" : "",
|
|
1610
|
+
"",
|
|
1611
|
+
"## Copy/Paste Task Wrapper",
|
|
1612
|
+
"",
|
|
1613
|
+
"```text",
|
|
1614
|
+
`${renderStarterPrompt(ctx, scope)}\n\nTask: <describe the change here>`,
|
|
1615
|
+
"```",
|
|
1616
|
+
"",
|
|
1617
|
+
].filter(Boolean).join("\n");
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function listFilesRecursive(root, predicate = () => true, limit = 300) {
|
|
1621
|
+
const files = [];
|
|
1622
|
+
if (!fs.existsSync(root)) return files;
|
|
1623
|
+
const stack = [root];
|
|
1624
|
+
while (stack.length && files.length < limit) {
|
|
1625
|
+
const current = stack.pop();
|
|
1626
|
+
let entries;
|
|
1627
|
+
try {
|
|
1628
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
1629
|
+
} catch {
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
for (const entry of entries) {
|
|
1633
|
+
const fullPath = path.join(current, entry.name);
|
|
1634
|
+
if (entry.isDirectory()) {
|
|
1635
|
+
stack.push(fullPath);
|
|
1636
|
+
} else if (entry.isFile() && predicate(fullPath)) {
|
|
1637
|
+
files.push(fullPath);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return files.sort((a, b) => {
|
|
1642
|
+
try {
|
|
1643
|
+
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
|
1644
|
+
} catch {
|
|
1645
|
+
return 0;
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function parseJsonl(filePath, maxLines = 20000) {
|
|
1651
|
+
const text = readIfText(filePath, 30 * 1024 * 1024);
|
|
1652
|
+
if (!text) return [];
|
|
1653
|
+
const rows = [];
|
|
1654
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
1655
|
+
for (const line of lines.slice(Math.max(0, lines.length - maxLines))) {
|
|
1656
|
+
try {
|
|
1657
|
+
rows.push(JSON.parse(line));
|
|
1658
|
+
} catch {
|
|
1659
|
+
// Local tool logs can contain partial writes while a session is active.
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
return rows;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function collectText(value, options = {}, depth = 0) {
|
|
1666
|
+
if (value == null || depth > 8) return "";
|
|
1667
|
+
if (typeof value === "string") return value;
|
|
1668
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1669
|
+
if (Array.isArray(value)) return value.map((item) => collectText(item, options, depth + 1)).join("\n");
|
|
1670
|
+
if (typeof value !== "object") return "";
|
|
1671
|
+
|
|
1672
|
+
const skipKeys = new Set(["signature", "encrypted_content", "image_url", "data", "auth", "api_key", "token"]);
|
|
1673
|
+
const parts = [];
|
|
1674
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1675
|
+
if (skipKeys.has(key)) continue;
|
|
1676
|
+
parts.push(collectText(child, options, depth + 1));
|
|
1677
|
+
}
|
|
1678
|
+
return parts.filter(Boolean).join("\n");
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function addUsage(target, usage) {
|
|
1682
|
+
if (!usage || typeof usage !== "object") return;
|
|
1683
|
+
target.inputTokens += Number(usage.input_tokens || usage.prompt_tokens || 0);
|
|
1684
|
+
target.outputTokens += Number(usage.output_tokens || usage.completion_tokens || 0);
|
|
1685
|
+
target.cacheReadTokens += Number(usage.cache_read_input_tokens || 0);
|
|
1686
|
+
target.cacheCreationTokens += Number(usage.cache_creation_input_tokens || 0);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function totalUsageTokens(usage) {
|
|
1690
|
+
if (!usage) return 0;
|
|
1691
|
+
return (
|
|
1692
|
+
Number(usage.input_tokens || usage.prompt_tokens || 0) +
|
|
1693
|
+
Number(usage.output_tokens || usage.completion_tokens || 0) +
|
|
1694
|
+
Number(usage.cache_read_input_tokens || 0) +
|
|
1695
|
+
Number(usage.cache_creation_input_tokens || 0)
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function getSessionRisk(tokens, toolTokens) {
|
|
1700
|
+
if (tokens >= 200000 || toolTokens >= 75000) return "High";
|
|
1701
|
+
if (tokens >= 60000 || toolTokens >= 20000) return "Medium";
|
|
1702
|
+
return "Low";
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
function analyzeSessionFile(filePath, tool) {
|
|
1706
|
+
const rows = parseJsonl(filePath);
|
|
1707
|
+
const stat = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
|
|
1708
|
+
const session = {
|
|
1709
|
+
tool,
|
|
1710
|
+
filePath,
|
|
1711
|
+
sessionId: path.basename(filePath).replace(/\.jsonl$/, ""),
|
|
1712
|
+
title: "",
|
|
1713
|
+
cwd: "",
|
|
1714
|
+
model: "",
|
|
1715
|
+
startedAt: null,
|
|
1716
|
+
updatedAt: stat ? new Date(stat.mtimeMs).toISOString() : null,
|
|
1717
|
+
turns: 0,
|
|
1718
|
+
userMessages: 0,
|
|
1719
|
+
assistantMessages: 0,
|
|
1720
|
+
toolCalls: 0,
|
|
1721
|
+
toolResults: 0,
|
|
1722
|
+
estimatedInputTokens: 0,
|
|
1723
|
+
estimatedOutputTokens: 0,
|
|
1724
|
+
estimatedToolTokens: 0,
|
|
1725
|
+
inputTokens: 0,
|
|
1726
|
+
outputTokens: 0,
|
|
1727
|
+
cacheReadTokens: 0,
|
|
1728
|
+
cacheCreationTokens: 0,
|
|
1729
|
+
exactInputTokens: 0,
|
|
1730
|
+
exactOutputTokens: 0,
|
|
1731
|
+
exactCacheReadTokens: 0,
|
|
1732
|
+
exactCacheCreationTokens: 0,
|
|
1733
|
+
exactTotalTokens: 0,
|
|
1734
|
+
exactAvailable: false,
|
|
1735
|
+
confidence: "estimated",
|
|
1736
|
+
largestTextBlobs: [],
|
|
1737
|
+
toolNames: {},
|
|
1738
|
+
};
|
|
1739
|
+
const seenUsage = new Set();
|
|
1740
|
+
let codexCumulative = null;
|
|
1741
|
+
|
|
1742
|
+
for (const row of rows) {
|
|
1743
|
+
const timestamp = row.timestamp || row.payload?.started_at || row.message?.timestamp;
|
|
1744
|
+
if (timestamp && !session.startedAt) session.startedAt = timestamp;
|
|
1745
|
+
if (timestamp) session.updatedAt = timestamp;
|
|
1746
|
+
|
|
1747
|
+
const meta = row.payload?.type === "session_meta" ? row.payload : row.type === "session_meta" ? row.payload : null;
|
|
1748
|
+
if (meta) {
|
|
1749
|
+
session.sessionId = meta.id || session.sessionId;
|
|
1750
|
+
session.cwd = meta.cwd || session.cwd;
|
|
1751
|
+
session.model = meta.model || meta.model_slug || session.model;
|
|
1752
|
+
}
|
|
1753
|
+
if (row.payload?.type === "token_count" && row.payload?.info?.total_token_usage) {
|
|
1754
|
+
codexCumulative = row.payload.info.total_token_usage;
|
|
1755
|
+
}
|
|
1756
|
+
if (row.type === "event_msg" && row.payload?.type === "token_count" && row.payload?.info?.total_token_usage) {
|
|
1757
|
+
codexCumulative = row.payload.info.total_token_usage;
|
|
1758
|
+
}
|
|
1759
|
+
if (row.type === "ai-title" && row.aiTitle) session.title = row.aiTitle;
|
|
1760
|
+
|
|
1761
|
+
const msg = row.message || row.payload;
|
|
1762
|
+
const role = msg?.role || row.payload?.role;
|
|
1763
|
+
const text = collectText(msg);
|
|
1764
|
+
const tokens = estimateTokens(text);
|
|
1765
|
+
if (tokens > 0) {
|
|
1766
|
+
session.largestTextBlobs.push({
|
|
1767
|
+
label: row.type || row.payload?.type || "event",
|
|
1768
|
+
tokens,
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
if (role === "user" || row.type === "user" || row.payload?.role === "user") {
|
|
1772
|
+
session.userMessages += 1;
|
|
1773
|
+
session.estimatedInputTokens += tokens;
|
|
1774
|
+
} else if (role === "assistant" || row.type === "assistant" || row.payload?.role === "assistant") {
|
|
1775
|
+
session.assistantMessages += 1;
|
|
1776
|
+
session.estimatedOutputTokens += tokens;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
const rowText = JSON.stringify(row);
|
|
1780
|
+
const toolUseMatches = rowText.match(/"tool_use"|function_call|"name":"([^"]+)"/g) || [];
|
|
1781
|
+
const toolResultMatches = rowText.match(/"tool_result"|function_call_output/g) || [];
|
|
1782
|
+
if (toolUseMatches.length) session.toolCalls += toolUseMatches.length;
|
|
1783
|
+
if (toolResultMatches.length) {
|
|
1784
|
+
session.toolResults += toolResultMatches.length;
|
|
1785
|
+
session.estimatedToolTokens += tokens;
|
|
1786
|
+
}
|
|
1787
|
+
const toolName = row.message?.content?.find?.((item) => item && item.type === "tool_use")?.name || row.payload?.name;
|
|
1788
|
+
if (toolName) session.toolNames[toolName] = (session.toolNames[toolName] || 0) + 1;
|
|
1789
|
+
|
|
1790
|
+
const usage = row.message?.usage || row.payload?.usage;
|
|
1791
|
+
if (usage) {
|
|
1792
|
+
const key = `${row.requestId || ""}:${row.message?.id || ""}:${totalUsageTokens(usage)}`;
|
|
1793
|
+
if (!seenUsage.has(key)) {
|
|
1794
|
+
seenUsage.add(key);
|
|
1795
|
+
addUsage(session, usage);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if (codexCumulative) {
|
|
1801
|
+
session.exactInputTokens = Number(codexCumulative.input_tokens || 0);
|
|
1802
|
+
session.exactOutputTokens = Number(codexCumulative.output_tokens || 0);
|
|
1803
|
+
session.exactCacheReadTokens = Number(codexCumulative.cached_input_tokens || 0);
|
|
1804
|
+
session.exactTotalTokens = Number(codexCumulative.total_tokens || 0);
|
|
1805
|
+
session.exactAvailable = session.exactTotalTokens > 0;
|
|
1806
|
+
} else {
|
|
1807
|
+
session.exactInputTokens = session.inputTokens || 0;
|
|
1808
|
+
session.exactOutputTokens = session.outputTokens || 0;
|
|
1809
|
+
session.exactCacheReadTokens = session.cacheReadTokens || 0;
|
|
1810
|
+
session.exactCacheCreationTokens = session.cacheCreationTokens || 0;
|
|
1811
|
+
session.exactTotalTokens =
|
|
1812
|
+
session.exactInputTokens + session.exactOutputTokens + session.exactCacheReadTokens + session.exactCacheCreationTokens;
|
|
1813
|
+
session.exactAvailable = session.exactTotalTokens > 0;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
session.turns = Math.max(session.userMessages, session.assistantMessages);
|
|
1817
|
+
session.estimatedTotalTokens = session.estimatedInputTokens + session.estimatedOutputTokens + session.estimatedToolTokens;
|
|
1818
|
+
session.displayTokens = session.exactAvailable ? session.exactTotalTokens : session.estimatedTotalTokens;
|
|
1819
|
+
session.confidence = session.exactAvailable ? "exact-local-log" : "estimated-local-log";
|
|
1820
|
+
session.contextRisk = getSessionRisk(session.displayTokens, session.estimatedToolTokens);
|
|
1821
|
+
session.largestTextBlobs = session.largestTextBlobs.sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
1822
|
+
return session;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
function getCodexSessionFiles() {
|
|
1826
|
+
const codexHome = process.env.PRISMO_CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
1827
|
+
return listFilesRecursive(path.join(codexHome, "sessions"), (file) => file.endsWith(".jsonl"), 200);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function getClaudeSessionFiles(cwd = process.cwd()) {
|
|
1831
|
+
const claudeHome = process.env.PRISMO_CLAUDE_HOME || path.join(os.homedir(), ".claude");
|
|
1832
|
+
const safeProject = cwd.replace(/[\/\\:]/g, "-").replace(/^-/, "-");
|
|
1833
|
+
const projectDir = path.join(claudeHome, "projects", safeProject);
|
|
1834
|
+
return listFilesRecursive(projectDir, (file) => file.endsWith(".jsonl"), 200);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function sameResolvedPath(a, b) {
|
|
1838
|
+
if (!a || !b) return false;
|
|
1839
|
+
try {
|
|
1840
|
+
return path.resolve(a) === path.resolve(b);
|
|
1841
|
+
} catch {
|
|
1842
|
+
return false;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function getUsageSummary(options = {}) {
|
|
1847
|
+
const tool = options.tool || "all";
|
|
1848
|
+
const limit = options.limit || 5;
|
|
1849
|
+
const cwd = options.cwd || process.cwd();
|
|
1850
|
+
const sessions = [];
|
|
1851
|
+
if (tool === "all" || tool === "codex") {
|
|
1852
|
+
for (const file of getCodexSessionFiles().slice(0, Math.max(limit * 8, 20))) {
|
|
1853
|
+
const session = analyzeSessionFile(file, "codex");
|
|
1854
|
+
if (!session.cwd || sameResolvedPath(session.cwd, cwd)) sessions.push(session);
|
|
1855
|
+
if (sessions.filter((item) => item.tool === "codex").length >= limit) break;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
if (tool === "all" || tool === "claude") {
|
|
1859
|
+
for (const file of getClaudeSessionFiles(cwd).slice(0, limit)) {
|
|
1860
|
+
sessions.push(analyzeSessionFile(file, "claude-code"));
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
sessions.sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0));
|
|
1864
|
+
const selected = sessions.slice(0, limit);
|
|
1865
|
+
const totals = selected.reduce(
|
|
1866
|
+
(acc, session) => {
|
|
1867
|
+
acc.displayTokens += session.displayTokens || 0;
|
|
1868
|
+
acc.estimatedTokens += session.estimatedTotalTokens || 0;
|
|
1869
|
+
acc.exactTokens += session.exactAvailable ? session.exactTotalTokens : 0;
|
|
1870
|
+
acc.toolTokens += session.estimatedToolTokens || 0;
|
|
1871
|
+
acc.sessions += 1;
|
|
1872
|
+
return acc;
|
|
1873
|
+
},
|
|
1874
|
+
{ sessions: 0, displayTokens: 0, estimatedTokens: 0, exactTokens: 0, toolTokens: 0 }
|
|
1875
|
+
);
|
|
1876
|
+
return {
|
|
1877
|
+
generatedAt: new Date().toISOString(),
|
|
1878
|
+
scannedPath: cwd,
|
|
1879
|
+
tool,
|
|
1880
|
+
confidence: selected.every((session) => session.exactAvailable) && selected.length ? "exact-local-log" : "mixed-or-estimated",
|
|
1881
|
+
totals,
|
|
1882
|
+
sessions: selected,
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function parsePositiveInt(value, fallback) {
|
|
1887
|
+
const parsed = Number.parseInt(value, 10);
|
|
1888
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function getPositionals(args, valueFlags = new Set()) {
|
|
1892
|
+
const values = [];
|
|
1893
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1894
|
+
const arg = args[i];
|
|
1895
|
+
if (valueFlags.has(arg)) {
|
|
1896
|
+
i += 1;
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
if (!arg.startsWith("-")) values.push(arg);
|
|
1900
|
+
}
|
|
1901
|
+
return values;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function formatTokenCount(value) {
|
|
1905
|
+
const n = Number(value || 0);
|
|
1906
|
+
if (n >= 1000000) return `${(n / 1000000).toFixed(2)}M`;
|
|
1907
|
+
if (n >= 1000) return `${Math.round(n / 1000)}k`;
|
|
1908
|
+
return String(Math.round(n));
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function renderUsageTerminal(summary, title = "Prismo Usage") {
|
|
1912
|
+
const lines = [];
|
|
1913
|
+
lines.push("");
|
|
1914
|
+
lines.push(title);
|
|
1915
|
+
lines.push("");
|
|
1916
|
+
lines.push(`Tool scope: ${summary.tool}`);
|
|
1917
|
+
lines.push(`Sessions shown: ${summary.sessions.length}`);
|
|
1918
|
+
lines.push(`Total displayed tokens: ${formatTokenCount(summary.totals.displayTokens)}`);
|
|
1919
|
+
if (summary.totals.exactTokens) lines.push(`Exact local-log tokens: ${formatTokenCount(summary.totals.exactTokens)}`);
|
|
1920
|
+
if (summary.totals.toolTokens) lines.push(`Estimated tool/output tokens: ${formatTokenCount(summary.totals.toolTokens)}`);
|
|
1921
|
+
lines.push(`Confidence: ${summary.confidence}`);
|
|
1922
|
+
lines.push("");
|
|
1923
|
+
lines.push("Recent Sessions:");
|
|
1924
|
+
if (!summary.sessions.length) {
|
|
1925
|
+
lines.push("- No local sessions detected.");
|
|
1926
|
+
} else {
|
|
1927
|
+
summary.sessions.forEach((session, index) => {
|
|
1928
|
+
lines.push(`${index + 1}. ${session.tool} - ${session.title || session.sessionId}`);
|
|
1929
|
+
lines.push(` tokens: ${formatTokenCount(session.displayTokens)} (${session.confidence}), risk: ${session.contextRisk}, turns: ${session.turns}, tools: ${session.toolCalls}`);
|
|
1930
|
+
if (session.model) lines.push(` model: ${session.model}`);
|
|
1931
|
+
if (session.cwd) lines.push(` cwd: ${session.cwd}`);
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
lines.push("");
|
|
1935
|
+
lines.push("Notes: exact means the local tool log exposed token fields. Estimated means Prismo used local text size heuristics only.");
|
|
1936
|
+
return lines.join("\n");
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
async function watchUsage(options = {}) {
|
|
1940
|
+
const intervalMs = options.intervalMs || 3000;
|
|
1941
|
+
const iterations = options.once ? 1 : Number.POSITIVE_INFINITY;
|
|
1942
|
+
for (let i = 0; i < iterations; i += 1) {
|
|
1943
|
+
const summary = getUsageSummary(options);
|
|
1944
|
+
if (options.json) {
|
|
1945
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1946
|
+
} else {
|
|
1947
|
+
console.clear();
|
|
1948
|
+
console.log(renderUsageTerminal(summary, "Prismo Watch"));
|
|
1949
|
+
console.log(`\nRefreshing every ${Math.round(intervalMs / 1000)}s. Press Ctrl+C to stop.`);
|
|
1950
|
+
}
|
|
1951
|
+
if (i + 1 >= iterations) break;
|
|
1952
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
function writeGeneratedFile(root, relPath, contents) {
|
|
1957
|
+
const fullPath = path.join(root, relPath);
|
|
1958
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
1959
|
+
const backupPath = backupIfExists(fullPath);
|
|
1960
|
+
fs.writeFileSync(fullPath, contents, "utf8");
|
|
1961
|
+
return { path: relPath, backupPath };
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function runOptimize(rootDir = process.cwd(), options = {}) {
|
|
1965
|
+
const ctx = createOptimizeContext(rootDir, options.scope || null);
|
|
1966
|
+
const generated = [];
|
|
1967
|
+
const pending = [
|
|
1968
|
+
["architecture-summary.md", renderArchitectureSummary(ctx)],
|
|
1969
|
+
["recommended-CLAUDE.md", renderRecommendedClaude(ctx)],
|
|
1970
|
+
["recommended-AGENTS.md", renderRecommendedAgents(ctx)],
|
|
1971
|
+
["recommended-.claudeignore", `${ctx.scan.recommendedClaudeIgnore.join("\n")}\n`],
|
|
1972
|
+
["recommended-.gitignore-additions", renderGitignoreAdditions(ctx)],
|
|
1973
|
+
];
|
|
1974
|
+
if (ctx.backendDetected) pending.push(["backend-summary.md", renderBackendSummary(ctx)]);
|
|
1975
|
+
if (ctx.frontendDetected) pending.push(["frontend-summary.md", renderFrontendSummary(ctx)]);
|
|
1976
|
+
if (ctx.scope) pending.push([`${ctx.scope.toLowerCase()}-context.md`, renderScopedContext(ctx, ctx.scope)]);
|
|
1977
|
+
|
|
1978
|
+
for (const [name, contents] of pending) {
|
|
1979
|
+
const written = writeGeneratedFile(ctx.root, path.join(".prismo", name), contents);
|
|
1980
|
+
generated.push(written.path);
|
|
1981
|
+
}
|
|
1982
|
+
const report = renderOptimizeReport(ctx, [...generated, ".prismo/optimize-report.md"]);
|
|
1983
|
+
const writtenReport = writeGeneratedFile(ctx.root, path.join(".prismo", "optimize-report.md"), report);
|
|
1984
|
+
generated.push(writtenReport.path);
|
|
1985
|
+
|
|
1986
|
+
return {
|
|
1987
|
+
root: ctx.root,
|
|
1988
|
+
scope: ctx.scope,
|
|
1989
|
+
frameworks: ctx.frameworks,
|
|
1990
|
+
generatedFiles: generated,
|
|
1991
|
+
warnings: ctx.warnings,
|
|
1992
|
+
riskScore: ctx.scan.score,
|
|
1993
|
+
estimatedContextReduction: ctx.estimatedContextReduction,
|
|
1994
|
+
optimizationSuggestions: ctx.suggestions,
|
|
1995
|
+
starterPrompt: renderStarterPrompt(ctx, ctx.scope),
|
|
1996
|
+
generatedAt: ctx.generatedAt,
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function renderOptimizeTerminal(result) {
|
|
2001
|
+
const lines = [];
|
|
2002
|
+
lines.push("");
|
|
2003
|
+
lines.push(color("Prismo Optimize", "bold"));
|
|
2004
|
+
lines.push("");
|
|
2005
|
+
lines.push("Detected:");
|
|
2006
|
+
if (result.frameworks.length) result.frameworks.forEach((name) => lines.push(`- ${name}`));
|
|
2007
|
+
else lines.push("- No common framework markers detected");
|
|
2008
|
+
lines.push("");
|
|
2009
|
+
lines.push("Generated:");
|
|
2010
|
+
result.generatedFiles.forEach((file) => lines.push(`- [ok] ${file}`));
|
|
2011
|
+
lines.push("");
|
|
2012
|
+
lines.push("Optimization Opportunities:");
|
|
2013
|
+
if (result.warnings.length) result.warnings.forEach((warning) => lines.push(`- ${warning}`));
|
|
2014
|
+
else lines.push("- Use generated context packs to reduce repeated repo exploration");
|
|
2015
|
+
result.optimizationSuggestions.slice(0, 4).forEach((suggestion) => lines.push(`- ${suggestion}`));
|
|
2016
|
+
lines.push("");
|
|
2017
|
+
lines.push(`Estimated Context Reduction: ${result.estimatedContextReduction}`);
|
|
2018
|
+
lines.push("");
|
|
2019
|
+
lines.push("Next Command:");
|
|
2020
|
+
lines.push(`${NPX_COMMAND} context${result.scope ? ` ${result.scope}` : ""}`);
|
|
2021
|
+
lines.push("");
|
|
2022
|
+
lines.push("Starter Prompt:");
|
|
2023
|
+
lines.push(result.starterPrompt);
|
|
2024
|
+
lines.push("");
|
|
2025
|
+
lines.push("Files are recommendations/templates only. No CLAUDE.md, AGENTS.md, .gitignore, or .claudeignore files were overwritten.");
|
|
2026
|
+
return lines.join("\n");
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function createDemoResult() {
|
|
2030
|
+
const issues = [
|
|
2031
|
+
{
|
|
2032
|
+
severity: "critical",
|
|
2033
|
+
category: "repo_size",
|
|
2034
|
+
title: "Recent local AI sessions used 2.40M tokens",
|
|
2035
|
+
description: "Prismo found exact token counts in local coding-agent logs.",
|
|
2036
|
+
recommendation: "Use compact context packs and split long sessions at task boundaries.",
|
|
2037
|
+
estimatedTokenImpact: "Actual local usage observed: 2,400,000 tokens across 3 recent sessions.",
|
|
2038
|
+
},
|
|
2039
|
+
{
|
|
2040
|
+
severity: "high",
|
|
2041
|
+
category: "large_file",
|
|
2042
|
+
title: "Large exposed file detected",
|
|
2043
|
+
description: "logs/debug-output.json (6.8 MB)",
|
|
2044
|
+
recommendation: "Ignore or summarize large logs before loading them into an agent.",
|
|
2045
|
+
estimatedTokenImpact: "Likely avoidable token exposure: up to ~1,700,000 tokens if read into context.",
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
severity: "medium",
|
|
2049
|
+
category: "instruction_file",
|
|
2050
|
+
title: "CLAUDE.md is ~1,900 tokens",
|
|
2051
|
+
description: "Persistent instructions are larger than the recommended baseline.",
|
|
2052
|
+
recommendation: "Trim CLAUDE.md under 500 tokens and link to details only when needed.",
|
|
2053
|
+
estimatedTokenImpact: "Potential savings estimate: about 1,400 persistent instruction tokens per turn.",
|
|
2054
|
+
},
|
|
2055
|
+
];
|
|
2056
|
+
return {
|
|
2057
|
+
root: "/demo/prismo-app",
|
|
2058
|
+
score: 58,
|
|
2059
|
+
risk: "Medium",
|
|
2060
|
+
avoidableWaste: "20-40%",
|
|
2061
|
+
issues,
|
|
2062
|
+
recommendations: [
|
|
2063
|
+
"Create .claudeignore with generated/cache folders and large artifacts excluded.",
|
|
2064
|
+
"Run `prismo optimize` to generate compact context files.",
|
|
2065
|
+
"Start coding sessions from `.prismo/architecture-summary.md` instead of broad repo exploration.",
|
|
2066
|
+
"Split long-running coding sessions at task boundaries.",
|
|
2067
|
+
],
|
|
2068
|
+
realUsage: {
|
|
2069
|
+
tool: "all",
|
|
2070
|
+
confidence: "exact-local-log",
|
|
2071
|
+
totals: { sessions: 3, displayTokens: 2400000, estimatedTokens: 180000, exactTokens: 2400000, toolTokens: 95000 },
|
|
2072
|
+
sessions: [{}, {}, {}],
|
|
2073
|
+
},
|
|
2074
|
+
stats: { totalFiles: 842, sourceFiles: 318, largeFiles: 4, exposedLargeFiles: 2, highRiskDirs: 7, exposedHighRiskDirs: 3 },
|
|
2075
|
+
topTokenLeaks: getTopTokenLeaks(issues),
|
|
2076
|
+
hasClaudeIgnore: false,
|
|
2077
|
+
repoDetected: true,
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
function renderDemoTerminal() {
|
|
2082
|
+
return [
|
|
2083
|
+
renderTerminalReport(createDemoResult(), { reportEnabled: false }),
|
|
2084
|
+
"",
|
|
2085
|
+
"Try it on your repo:",
|
|
2086
|
+
`1. ${NPX_COMMAND} scan --usage`,
|
|
2087
|
+
`2. ${NPX_COMMAND} scan --fix`,
|
|
2088
|
+
`3. ${NPX_COMMAND} optimize`,
|
|
2089
|
+
`4. ${NPX_COMMAND} context frontend`,
|
|
2090
|
+
].join("\n");
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function runDevFlow(rootDir = process.cwd(), options = {}) {
|
|
2094
|
+
const root = path.resolve(rootDir);
|
|
2095
|
+
const scanDone = printStep("Scanning repo and local usage", options.json);
|
|
2096
|
+
const scan = scanRepo(root, { includeUsage: true, usageLimit: options.limit || 3 });
|
|
2097
|
+
scanDone();
|
|
2098
|
+
const optimizeDone = printStep("Generating compact context files", options.json);
|
|
2099
|
+
const optimize = runOptimize(root);
|
|
2100
|
+
optimizeDone();
|
|
2101
|
+
const scope = optimize.frameworks.some((name) => ["Next.js", "React", "Vite"].includes(name)) ? "frontend" : null;
|
|
2102
|
+
const ctx = createOptimizeContext(root, scope);
|
|
2103
|
+
const prompt = renderContextCommand(ctx, scope);
|
|
2104
|
+
return {
|
|
2105
|
+
scan,
|
|
2106
|
+
optimize,
|
|
2107
|
+
scope,
|
|
2108
|
+
prompt,
|
|
2109
|
+
nextCommands: getNextCommands(scan, scope),
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
function renderDevTerminal(result) {
|
|
2114
|
+
const lines = [];
|
|
2115
|
+
lines.push("");
|
|
2116
|
+
lines.push(color("Prismo Dev", "bold"));
|
|
2117
|
+
lines.push("");
|
|
2118
|
+
lines.push(`Score: ${result.scan.score}/100 | Risk: ${result.scan.risk} | Token leaks: ${result.scan.issues.length}`);
|
|
2119
|
+
if (result.scan.realUsage && result.scan.realUsage.sessions.length) {
|
|
2120
|
+
lines.push(`Real local usage: ${formatTokenCount(result.scan.realUsage.totals.displayTokens)} tokens (${result.scan.realUsage.confidence})`);
|
|
2121
|
+
} else if (result.scan.realUsage) {
|
|
2122
|
+
lines.push("Real local usage: no matching local sessions found for this repo");
|
|
2123
|
+
}
|
|
2124
|
+
lines.push("");
|
|
2125
|
+
lines.push("Generated:");
|
|
2126
|
+
result.optimize.generatedFiles.slice(0, 8).forEach((file) => lines.push(`- ${file}`));
|
|
2127
|
+
lines.push("");
|
|
2128
|
+
lines.push("Next Commands:");
|
|
2129
|
+
result.nextCommands.forEach((cmd, index) => lines.push(`${index + 1}. ${cmd}`));
|
|
2130
|
+
lines.push("");
|
|
2131
|
+
lines.push("Paste-ready prompt:");
|
|
2132
|
+
lines.push(renderStarterPrompt(createOptimizeContext(result.optimize.root, result.scope), result.scope));
|
|
2133
|
+
lines.push("");
|
|
2134
|
+
return lines.join("\n");
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function applyFixes(result) {
|
|
2138
|
+
const actions = [];
|
|
2139
|
+
const claudeIgnorePath = path.join(result.root, ".claudeignore");
|
|
2140
|
+
const suggestedClaudeIgnorePath = path.join(result.root, ".claudeignore.prismo-suggested");
|
|
2141
|
+
if (!result.hasClaudeIgnore) {
|
|
2142
|
+
fs.writeFileSync(claudeIgnorePath, `${result.recommendedClaudeIgnore.join("\n")}\n`, "utf8");
|
|
2143
|
+
actions.push("Created .claudeignore");
|
|
2144
|
+
} else {
|
|
2145
|
+
const backupPath = backupIfExists(suggestedClaudeIgnorePath);
|
|
2146
|
+
fs.writeFileSync(suggestedClaudeIgnorePath, `${result.recommendedClaudeIgnore.join("\n")}\n`, "utf8");
|
|
2147
|
+
actions.push("Created .claudeignore.prismo-suggested because .claudeignore already exists");
|
|
2148
|
+
if (backupPath) actions.push(`Backed up existing .claudeignore.prismo-suggested to ${path.basename(backupPath)}`);
|
|
2149
|
+
}
|
|
2150
|
+
const report = writeReport(result);
|
|
2151
|
+
actions.push(`Generated ${path.basename(report.reportPath)}`);
|
|
2152
|
+
if (report.backupPath) actions.push(`Backed up existing report to ${path.basename(report.backupPath)}`);
|
|
2153
|
+
|
|
2154
|
+
const claudeFile = result.instructionFiles.find((file) => file.isClaude);
|
|
2155
|
+
if (claudeFile && claudeFile.tokens > 500) {
|
|
2156
|
+
const templatePath = path.join(result.root, "prismo-optimized-CLAUDE.template.md");
|
|
2157
|
+
const backupPath = backupIfExists(templatePath);
|
|
2158
|
+
fs.writeFileSync(templatePath, renderClaudeTemplate(result), "utf8");
|
|
2159
|
+
actions.push("Generated prismo-optimized-CLAUDE.template.md");
|
|
2160
|
+
if (backupPath) actions.push(`Backed up existing CLAUDE template to ${path.basename(backupPath)}`);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
const hasCodexRisk = result.issues.some((issue) => issue.category === "codex_config");
|
|
2164
|
+
if (hasCodexRisk || result.instructionFiles.some((file) => file.path === "AGENTS.md" || file.path.startsWith(".codex/"))) {
|
|
2165
|
+
const codexPath = path.join(result.root, "prismo-AGENTS-recommendations.md");
|
|
2166
|
+
const backupPath = backupIfExists(codexPath);
|
|
2167
|
+
fs.writeFileSync(codexPath, renderAgentsRecommendations(result), "utf8");
|
|
2168
|
+
actions.push("Generated prismo-AGENTS-recommendations.md");
|
|
2169
|
+
if (backupPath) actions.push(`Backed up existing AGENTS recommendations to ${path.basename(backupPath)}`);
|
|
2170
|
+
}
|
|
2171
|
+
return actions;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
function printHelp() {
|
|
2175
|
+
console.log(`Prismo CLI
|
|
2176
|
+
|
|
2177
|
+
Usage:
|
|
2178
|
+
prismo dev [path]
|
|
2179
|
+
prismo scan [--fix] [--json] [--usage] [--no-report] [path]
|
|
2180
|
+
prismo optimize [scope] [--json] [path]
|
|
2181
|
+
prismo context [scope] [--json] [path]
|
|
2182
|
+
prismo usage [codex|claude|all] [--json] [--limit N] [path]
|
|
2183
|
+
prismo watch [codex|claude|all] [--json] [--once] [--interval N] [path]
|
|
2184
|
+
prismo demo
|
|
2185
|
+
|
|
2186
|
+
Commands:
|
|
2187
|
+
dev Guided flow: scan, optimize, and print a paste-ready context prompt.
|
|
2188
|
+
scan Run Prismo Dev Scan for Claude Code, Codex, Cursor, and AI coding workflows.
|
|
2189
|
+
optimize Generate lightweight AI-readable project context files in .prismo/.
|
|
2190
|
+
context Print a copy-pasteable compact context prompt for AI coding tools.
|
|
2191
|
+
usage Read local Codex/Claude Code session logs and summarize token usage.
|
|
2192
|
+
watch Refresh local session usage in the terminal.
|
|
2193
|
+
demo Show sample output without needing a messy repo.
|
|
2194
|
+
|
|
2195
|
+
Options:
|
|
2196
|
+
--fix Safely create .claudeignore if missing and generate the report.
|
|
2197
|
+
--json Output valid JSON only for CI or future dashboard ingestion.
|
|
2198
|
+
--usage Include real local Codex/Claude Code session usage in scan diagnostics.
|
|
2199
|
+
--no-report Do not write prismo-dev-report.md.
|
|
2200
|
+
--limit N Number of recent local sessions to show.
|
|
2201
|
+
--interval N Refresh interval in seconds for watch mode.
|
|
2202
|
+
--once Run watch mode once, useful for tests and scripts.
|
|
2203
|
+
--help Show this help.
|
|
2204
|
+
`);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
function printCommandHelp(command) {
|
|
2208
|
+
const help = {
|
|
2209
|
+
scan: `Prismo Scan
|
|
2210
|
+
|
|
2211
|
+
Usage:
|
|
2212
|
+
prismo scan [--fix] [--json] [--usage] [--no-report] [--limit N] [path]
|
|
2213
|
+
|
|
2214
|
+
Examples:
|
|
2215
|
+
prismo scan
|
|
2216
|
+
prismo scan --usage
|
|
2217
|
+
prismo scan --fix
|
|
2218
|
+
prismo scan --usage --json --no-report
|
|
2219
|
+
|
|
2220
|
+
Notes:
|
|
2221
|
+
--usage reads local Codex/Claude Code logs when present.
|
|
2222
|
+
--fix creates safe recommendation files and never overwrites CLAUDE.md or AGENTS.md.`,
|
|
2223
|
+
optimize: `Prismo Optimize
|
|
2224
|
+
|
|
2225
|
+
Usage:
|
|
2226
|
+
prismo optimize [auth|frontend|backend] [--json] [path]
|
|
2227
|
+
|
|
2228
|
+
Examples:
|
|
2229
|
+
prismo optimize
|
|
2230
|
+
prismo optimize frontend
|
|
2231
|
+
|
|
2232
|
+
Output:
|
|
2233
|
+
Generates compact AI context files in .prismo/.`,
|
|
2234
|
+
context: `Prismo Context
|
|
2235
|
+
|
|
2236
|
+
Usage:
|
|
2237
|
+
prismo context [auth|frontend|backend] [--json] [path]
|
|
2238
|
+
|
|
2239
|
+
Examples:
|
|
2240
|
+
prismo context
|
|
2241
|
+
prismo context frontend
|
|
2242
|
+
|
|
2243
|
+
Output:
|
|
2244
|
+
Prints a paste-ready prompt for Codex, Claude Code, Cursor, and similar tools.`,
|
|
2245
|
+
usage: `Prismo Usage
|
|
2246
|
+
|
|
2247
|
+
Usage:
|
|
2248
|
+
prismo usage [codex|claude|all] [--json] [--limit N] [path]
|
|
2249
|
+
|
|
2250
|
+
Examples:
|
|
2251
|
+
prismo usage
|
|
2252
|
+
prismo usage codex --json
|
|
2253
|
+
prismo usage claude --limit 3`,
|
|
2254
|
+
watch: `Prismo Watch
|
|
2255
|
+
|
|
2256
|
+
Usage:
|
|
2257
|
+
prismo watch [codex|claude|all] [--json] [--once] [--interval N] [path]
|
|
2258
|
+
|
|
2259
|
+
Examples:
|
|
2260
|
+
prismo watch codex
|
|
2261
|
+
prismo watch claude --once --json`,
|
|
2262
|
+
dev: `Prismo Dev
|
|
2263
|
+
|
|
2264
|
+
Usage:
|
|
2265
|
+
prismo dev [--json] [--limit N] [path]
|
|
2266
|
+
|
|
2267
|
+
Flow:
|
|
2268
|
+
1. Scan repo and local usage.
|
|
2269
|
+
2. Generate .prismo/ optimized context files.
|
|
2270
|
+
3. Print a paste-ready prompt.`,
|
|
2271
|
+
demo: `Prismo Demo
|
|
2272
|
+
|
|
2273
|
+
Usage:
|
|
2274
|
+
prismo demo
|
|
2275
|
+
|
|
2276
|
+
Shows sample Prismo Dev Scan output without reading local files.`,
|
|
2277
|
+
};
|
|
2278
|
+
console.log(help[command] || "Unknown command. Try: prismo --help");
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
async function runCli(argv) {
|
|
2282
|
+
const [command, ...rest] = argv;
|
|
2283
|
+
if (!command || command === "--help" || command === "-h") {
|
|
2284
|
+
printHelp();
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
2288
|
+
printCommandHelp(command);
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
if (!["dev", "scan", "optimize", "context", "usage", "watch", "demo"].includes(command)) {
|
|
2292
|
+
throw new Error(`Unknown command: ${command}. Try: prismo dev, prismo scan, prismo optimize, prismo context, or prismo usage`);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
if (command === "demo") {
|
|
2296
|
+
console.log(renderDemoTerminal());
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
if (command === "dev") {
|
|
2301
|
+
const json = rest.includes("--json");
|
|
2302
|
+
const limitIndex = rest.indexOf("--limit");
|
|
2303
|
+
const target = getPositionals(rest, new Set(["--limit"]))[0] || process.cwd();
|
|
2304
|
+
const result = runDevFlow(target, {
|
|
2305
|
+
json,
|
|
2306
|
+
limit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 3),
|
|
2307
|
+
});
|
|
2308
|
+
if (json) {
|
|
2309
|
+
console.log(JSON.stringify({
|
|
2310
|
+
score: result.scan.score,
|
|
2311
|
+
riskLevel: result.scan.risk,
|
|
2312
|
+
tokenLeaks: result.scan.issues.length,
|
|
2313
|
+
realUsage: result.scan.realUsage,
|
|
2314
|
+
generatedFiles: result.optimize.generatedFiles,
|
|
2315
|
+
nextCommands: result.nextCommands,
|
|
2316
|
+
prompt: renderStarterPrompt(createOptimizeContext(result.optimize.root, result.scope), result.scope),
|
|
2317
|
+
scannedPath: result.scan.root,
|
|
2318
|
+
generatedAt: result.scan.generatedAt,
|
|
2319
|
+
}, null, 2));
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
console.log(renderDevTerminal(result));
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
if (command === "usage" || command === "watch") {
|
|
2327
|
+
const json = rest.includes("--json");
|
|
2328
|
+
const knownTools = new Set(["codex", "claude", "all"]);
|
|
2329
|
+
const positional = getPositionals(rest, new Set(["--limit", "--interval"]));
|
|
2330
|
+
const tool = positional[0] && knownTools.has(positional[0].toLowerCase()) ? positional[0].toLowerCase() : "all";
|
|
2331
|
+
const target = tool === "all" ? positional[0] || process.cwd() : positional[1] || process.cwd();
|
|
2332
|
+
const limitIndex = rest.indexOf("--limit");
|
|
2333
|
+
const intervalIndex = rest.indexOf("--interval");
|
|
2334
|
+
const limit = parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5);
|
|
2335
|
+
const intervalMs = parsePositiveInt(intervalIndex >= 0 ? rest[intervalIndex + 1] : null, 3) * 1000;
|
|
2336
|
+
const usageOptions = {
|
|
2337
|
+
tool,
|
|
2338
|
+
cwd: path.resolve(target),
|
|
2339
|
+
limit,
|
|
2340
|
+
json,
|
|
2341
|
+
once: rest.includes("--once"),
|
|
2342
|
+
intervalMs,
|
|
2343
|
+
};
|
|
2344
|
+
|
|
2345
|
+
if (command === "watch") {
|
|
2346
|
+
await watchUsage(usageOptions);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
const summary = getUsageSummary(usageOptions);
|
|
2351
|
+
if (json) {
|
|
2352
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
console.log(renderUsageTerminal(summary));
|
|
2356
|
+
return;
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
if (command === "context") {
|
|
2360
|
+
const json = rest.includes("--json");
|
|
2361
|
+
const positional = rest.filter((arg) => !arg.startsWith("-"));
|
|
2362
|
+
const knownScopes = new Set(["auth", "frontend", "backend"]);
|
|
2363
|
+
const scope = positional[0] && knownScopes.has(positional[0].toLowerCase()) ? positional[0].toLowerCase() : null;
|
|
2364
|
+
const target = scope ? positional[1] || process.cwd() : positional[0] || process.cwd();
|
|
2365
|
+
const ctx = createOptimizeContext(target, scope);
|
|
2366
|
+
const prompt = renderStarterPrompt(ctx, scope);
|
|
2367
|
+
const output = renderContextCommand(ctx, scope);
|
|
2368
|
+
if (json) {
|
|
2369
|
+
console.log(JSON.stringify({
|
|
2370
|
+
scope,
|
|
2371
|
+
prompt,
|
|
2372
|
+
contextFile: getContextFileForScope(ctx, scope),
|
|
2373
|
+
supportingFiles: [
|
|
2374
|
+
!scope && ctx.backendDetected ? ".prismo/backend-summary.md" : null,
|
|
2375
|
+
!scope && ctx.frontendDetected ? ".prismo/frontend-summary.md" : null,
|
|
2376
|
+
scope === "frontend" ? ".prismo/frontend-summary.md" : null,
|
|
2377
|
+
scope === "backend" ? ".prismo/backend-summary.md" : null,
|
|
2378
|
+
].filter(Boolean),
|
|
2379
|
+
scannedPath: ctx.root,
|
|
2380
|
+
generatedAt: ctx.generatedAt,
|
|
2381
|
+
}, null, 2));
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
console.log(output);
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
if (command === "optimize") {
|
|
2389
|
+
const json = rest.includes("--json");
|
|
2390
|
+
const positional = rest.filter((arg) => !arg.startsWith("-"));
|
|
2391
|
+
const knownScopes = new Set(["auth", "frontend", "backend"]);
|
|
2392
|
+
const scope = positional[0] && knownScopes.has(positional[0].toLowerCase()) ? positional[0].toLowerCase() : null;
|
|
2393
|
+
const target = scope ? positional[1] || process.cwd() : positional[0] || process.cwd();
|
|
2394
|
+
const result = runOptimize(target, { scope });
|
|
2395
|
+
if (json) {
|
|
2396
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
console.log(renderOptimizeTerminal(result));
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
const fix = rest.includes("--fix");
|
|
2404
|
+
const noReport = rest.includes("--no-report");
|
|
2405
|
+
const json = rest.includes("--json");
|
|
2406
|
+
const includeUsage = rest.includes("--usage");
|
|
2407
|
+
const limitIndex = rest.indexOf("--limit");
|
|
2408
|
+
const usageToolIndex = rest.indexOf("--usage-tool");
|
|
2409
|
+
const target = getPositionals(rest, new Set(["--limit", "--usage-tool"]))[0] || process.cwd();
|
|
2410
|
+
const scanDone = printStep(includeUsage ? "Scanning repo and local usage" : "Scanning repo", json);
|
|
2411
|
+
const result = scanRepo(target, {
|
|
2412
|
+
includeUsage,
|
|
2413
|
+
usageLimit: parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5),
|
|
2414
|
+
usageTool: usageToolIndex >= 0 ? rest[usageToolIndex + 1] : "all",
|
|
2415
|
+
});
|
|
2416
|
+
scanDone();
|
|
2417
|
+
|
|
2418
|
+
if (json) {
|
|
2419
|
+
let fixActions = [];
|
|
2420
|
+
let report = null;
|
|
2421
|
+
if (fix) {
|
|
2422
|
+
fixActions = applyFixes(result);
|
|
2423
|
+
} else if (!noReport) {
|
|
2424
|
+
report = writeReport(result);
|
|
2425
|
+
}
|
|
2426
|
+
const payload = toJsonPayload(result);
|
|
2427
|
+
if (fixActions.length) payload.fixActions = fixActions;
|
|
2428
|
+
if (report) payload.reportPath = report.reportPath;
|
|
2429
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
console.log(renderTerminalReport(result, { reportEnabled: !noReport || fix }));
|
|
2434
|
+
|
|
2435
|
+
if (fix) {
|
|
2436
|
+
const actions = applyFixes(result);
|
|
2437
|
+
console.log("\nFix Mode:");
|
|
2438
|
+
actions.forEach((action) => console.log(`- ${action}`));
|
|
2439
|
+
} else if (!noReport) {
|
|
2440
|
+
const report = writeReport(result);
|
|
2441
|
+
if (report.backupPath) {
|
|
2442
|
+
console.log(`\nExisting report backed up to ${path.basename(report.backupPath)}.`);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
module.exports = {
|
|
2448
|
+
applyFixes,
|
|
2449
|
+
estimateTokens,
|
|
2450
|
+
renderMarkdownReport,
|
|
2451
|
+
renderUsageTerminal,
|
|
2452
|
+
renderTerminalReport,
|
|
2453
|
+
runOptimize,
|
|
2454
|
+
runCli,
|
|
2455
|
+
scanRepo,
|
|
2456
|
+
getUsageSummary,
|
|
2457
|
+
analyzeSessionFile,
|
|
2458
|
+
toJsonPayload,
|
|
2459
|
+
watchUsage,
|
|
2460
|
+
writeReport,
|
|
2461
|
+
};
|