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