getprismo 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/lib/prismo-dev/benchmark.js +122 -0
- package/lib/prismo-dev/report.js +37 -0
- package/lib/prismo-dev/scan-path-utils.js +203 -0
- package/lib/prismo-dev/scan.js +34 -191
- package/lib/prismo-dev/usage-log-utils.js +251 -0
- package/lib/prismo-dev/usage-watch.js +20 -226
- package/lib/prismo-dev/utils.js +85 -0
- package/lib/prismo-dev-scan.js +68 -79
- package/package.json +1 -1
package/lib/prismo-dev/scan.js
CHANGED
|
@@ -27,197 +27,13 @@ const ASSUMED_TURNS_PER_AI_SESSION = 40;
|
|
|
27
27
|
const ASSUMED_INPUT_COST_PER_1K_TOKENS = 0.003;
|
|
28
28
|
const ASSUMED_SESSIONS_PER_MONTH = 30;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const text = fs.readFileSync(filePath, "utf8");
|
|
38
|
-
return text
|
|
39
|
-
.split(/\r?\n/)
|
|
40
|
-
.map((line) => line.trim())
|
|
41
|
-
.filter((line) => line && !line.startsWith("#"))
|
|
42
|
-
.map((line) => line.replace(/^!/, ""));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function patternMatches(pattern, relPath, isDir = false) {
|
|
46
|
-
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\//, "");
|
|
47
|
-
const normalizedRel = normalizeRel(relPath);
|
|
48
|
-
const dirRel = isDir && !normalizedRel.endsWith("/") ? `${normalizedRel}/` : normalizedRel;
|
|
49
|
-
|
|
50
|
-
if (!normalizedPattern) return false;
|
|
51
|
-
if (normalizedPattern.endsWith("/")) {
|
|
52
|
-
const base = normalizedPattern.slice(0, -1);
|
|
53
|
-
return (
|
|
54
|
-
normalizedRel === base ||
|
|
55
|
-
normalizedRel.startsWith(`${base}/`) ||
|
|
56
|
-
normalizedRel.endsWith(`/${base}`) ||
|
|
57
|
-
normalizedRel.includes(`/${base}/`) ||
|
|
58
|
-
dirRel.includes(`/${base}/`)
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
if (normalizedPattern.startsWith("*.")) {
|
|
62
|
-
return normalizedRel.endsWith(normalizedPattern.slice(1));
|
|
63
|
-
}
|
|
64
|
-
if (normalizedPattern.includes("*")) {
|
|
65
|
-
const escaped = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
66
|
-
return new RegExp(`(^|/)${escaped}$`).test(normalizedRel);
|
|
67
|
-
}
|
|
68
|
-
return (
|
|
69
|
-
normalizedRel === normalizedPattern ||
|
|
70
|
-
dirRel === normalizedPattern ||
|
|
71
|
-
normalizedRel.startsWith(`${normalizedPattern}/`) ||
|
|
72
|
-
normalizedRel.endsWith(`/${normalizedPattern}`)
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function isIgnored(relPath, patterns, isDir = false) {
|
|
77
|
-
return patterns.some((pattern) => patternMatches(pattern, relPath, isDir));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function ignoreSuggestionCovered(pattern, existingPatterns) {
|
|
81
|
-
if (!pattern) return true;
|
|
82
|
-
if (existingPatterns.includes(pattern)) return true;
|
|
83
|
-
const sample = pattern
|
|
84
|
-
.replace(/^\*\//, "")
|
|
85
|
-
.replace(/^\*\*/, "sample")
|
|
86
|
-
.replace(/\*/g, "sample")
|
|
87
|
-
.replace(/\/$/, "");
|
|
88
|
-
const isDir = pattern.endsWith("/") || pattern.endsWith("/**");
|
|
89
|
-
return existingPatterns.some((existing) => {
|
|
90
|
-
if (existing === pattern) return true;
|
|
91
|
-
if (existing.endsWith("/") && pattern.startsWith(existing)) return true;
|
|
92
|
-
return patternMatches(existing, sample, isDir);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function missingIgnoreSuggestions(recommended, existingPatterns) {
|
|
97
|
-
return recommended.filter((pattern) => !ignoreSuggestionCovered(pattern, existingPatterns));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const SESSION_NOISE_DIRS = new Set([
|
|
101
|
-
".next",
|
|
102
|
-
".nuxt",
|
|
103
|
-
".prismo",
|
|
104
|
-
".pytest_cache",
|
|
105
|
-
".turbo",
|
|
106
|
-
"__pycache__",
|
|
107
|
-
"build",
|
|
108
|
-
"calendar-dumps",
|
|
109
|
-
"coverage",
|
|
110
|
-
"dist",
|
|
111
|
-
"event-dumps",
|
|
112
|
-
"events",
|
|
113
|
-
"exports",
|
|
114
|
-
"htmlcov",
|
|
115
|
-
"inbox-dumps",
|
|
116
|
-
"logs",
|
|
117
|
-
"models",
|
|
118
|
-
"node_modules",
|
|
119
|
-
"out",
|
|
120
|
-
"playwright-report",
|
|
121
|
-
"session-dumps",
|
|
122
|
-
"source-streams",
|
|
123
|
-
"state-backups",
|
|
124
|
-
"test-results",
|
|
125
|
-
"tmp",
|
|
126
|
-
"temp",
|
|
127
|
-
]);
|
|
128
|
-
|
|
129
|
-
const SESSION_NOISE_FILE_NAMES = new Set([
|
|
130
|
-
"package-lock.json",
|
|
131
|
-
"pnpm-lock.yaml",
|
|
132
|
-
"yarn.lock",
|
|
133
|
-
"bun.lockb",
|
|
134
|
-
"coverage-final.json",
|
|
135
|
-
"lcov.info",
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
const SESSION_NOISE_EXTENSIONS = new Set([
|
|
139
|
-
".db",
|
|
140
|
-
".jsonl",
|
|
141
|
-
".lock",
|
|
142
|
-
".log",
|
|
143
|
-
".sqlite",
|
|
144
|
-
".sqlite3",
|
|
145
|
-
]);
|
|
146
|
-
|
|
147
|
-
function cleanSessionPath(value) {
|
|
148
|
-
const text = String(value || "").trim().replace(/\\/g, "/");
|
|
149
|
-
if (!text || /^https?:\/\//.test(text)) return null;
|
|
150
|
-
const withoutQuotes = text.replace(/^["'`]+|["'`.,:;)\]]+$/g, "");
|
|
151
|
-
if (!withoutQuotes || withoutQuotes.includes("\n")) return null;
|
|
152
|
-
const markerIndex = withoutQuotes.indexOf("/Users/");
|
|
153
|
-
if (markerIndex > 0) return withoutQuotes.slice(markerIndex);
|
|
154
|
-
return withoutQuotes;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function sessionIgnorePatternForPath(value, root) {
|
|
158
|
-
const cleaned = cleanSessionPath(value);
|
|
159
|
-
if (!cleaned) return null;
|
|
160
|
-
const rootNormalized = normalizeRel(root);
|
|
161
|
-
let rel = cleaned;
|
|
162
|
-
if (path.isAbsolute(cleaned)) {
|
|
163
|
-
const normalized = normalizeRel(cleaned);
|
|
164
|
-
if (!normalized.startsWith(`${rootNormalized}/`)) return null;
|
|
165
|
-
rel = normalizeRel(path.relative(root, cleaned));
|
|
166
|
-
}
|
|
167
|
-
rel = normalizeRel(rel).replace(/^\.\//, "");
|
|
168
|
-
if (!rel || rel === "." || rel.startsWith("../") || rel.includes("..")) return null;
|
|
169
|
-
|
|
170
|
-
const segments = rel.split("/").filter(Boolean);
|
|
171
|
-
if (!segments.length) return null;
|
|
172
|
-
for (let index = 0; index < segments.length; index += 1) {
|
|
173
|
-
const segment = segments[index];
|
|
174
|
-
if (SESSION_NOISE_DIRS.has(segment)) {
|
|
175
|
-
return `${segments.slice(0, index + 1).join("/")}/`;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const fileName = segments[segments.length - 1];
|
|
180
|
-
const lowerName = fileName.toLowerCase();
|
|
181
|
-
const ext = path.extname(lowerName);
|
|
182
|
-
if (SESSION_NOISE_FILE_NAMES.has(lowerName)) return fileName;
|
|
183
|
-
if (SESSION_NOISE_EXTENSIONS.has(ext)) return rel;
|
|
184
|
-
if (/_state\.json$/i.test(fileName)) return "*_state.json";
|
|
185
|
-
if (/_tokens\.json$/i.test(fileName)) return "*_tokens.json";
|
|
186
|
-
if (/_export\.json$/i.test(fileName)) return "*_export.json";
|
|
187
|
-
if (/secret|credential|token/i.test(fileName) && /\.json$/i.test(fileName)) return rel;
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function buildSessionIgnoreSuggestions(realUsage, root) {
|
|
192
|
-
if (!realUsage || !Array.isArray(realUsage.sessions)) return [];
|
|
193
|
-
const byPattern = new Map();
|
|
194
|
-
const add = (pattern, item, source, reason) => {
|
|
195
|
-
if (!pattern) return;
|
|
196
|
-
const existing = byPattern.get(pattern) || {
|
|
197
|
-
pattern,
|
|
198
|
-
source,
|
|
199
|
-
reason,
|
|
200
|
-
count: 0,
|
|
201
|
-
examples: [],
|
|
202
|
-
};
|
|
203
|
-
existing.count += Number(item?.count || 1);
|
|
204
|
-
const example = item?.value || item?.path || pattern;
|
|
205
|
-
if (example && !existing.examples.includes(example) && existing.examples.length < 3) existing.examples.push(example);
|
|
206
|
-
byPattern.set(pattern, existing);
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
for (const session of realUsage.sessions) {
|
|
210
|
-
for (const item of session.generatedArtifacts || []) {
|
|
211
|
-
add(sessionIgnorePatternForPath(item.value, root), item, session.tool || "session", "Generated artifact entered local session context.");
|
|
212
|
-
}
|
|
213
|
-
for (const item of session.repeatedPathMentions || []) {
|
|
214
|
-
add(sessionIgnorePatternForPath(item.value, root), item, session.tool || "session", "Noisy path appeared repeatedly in local session context.");
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return Array.from(byPattern.values())
|
|
218
|
-
.sort((a, b) => b.count - a.count || a.pattern.localeCompare(b.pattern))
|
|
219
|
-
.slice(0, 25);
|
|
220
|
-
}
|
|
30
|
+
const {
|
|
31
|
+
buildSessionIgnoreSuggestions,
|
|
32
|
+
isIgnored,
|
|
33
|
+
missingIgnoreSuggestions,
|
|
34
|
+
normalizeRel,
|
|
35
|
+
readIgnoreFile,
|
|
36
|
+
} = require("./scan-path-utils")({ fs, path });
|
|
221
37
|
|
|
222
38
|
function getFileKind(filePath) {
|
|
223
39
|
const ext = path.extname(filePath).toLowerCase();
|
|
@@ -390,12 +206,21 @@ function detectOptimizationStack(root, claudeConfig, codexConfig) {
|
|
|
390
206
|
const projectHeadroom = fs.existsSync(path.join(root, ".headroom")) || fs.existsSync(path.join(os.homedir(), ".headroom"));
|
|
391
207
|
const projectDistill = fs.existsSync(path.join(os.homedir(), ".config", "distill")) || commandExists("distill");
|
|
392
208
|
const projectRtk = fs.existsSync(path.join(root, ".rtk")) || commandExists("rtk");
|
|
209
|
+
const packageText = readIfText(path.join(root, "package.json"), 512 * 1024) || "";
|
|
210
|
+
const readmeText = readIfText(path.join(root, "README.md"), 512 * 1024) || "";
|
|
211
|
+
const projectText = `${packageText}\n${readmeText}`.toLowerCase();
|
|
212
|
+
const hasText = (pattern) => pattern.test(projectText);
|
|
393
213
|
|
|
394
214
|
const tools = {
|
|
395
215
|
rtk: { detected: projectRtk, source: projectRtk ? "binary-or-project-config" : "not-detected" },
|
|
396
216
|
headroom: { detected: projectHeadroom || commandExists("headroom"), source: projectHeadroom ? "local-config" : commandExists("headroom") ? "binary" : "not-detected" },
|
|
397
217
|
distill: { detected: projectDistill, source: projectDistill ? "binary-or-user-config" : "not-detected" },
|
|
398
218
|
mana: { detected: projectMana || commandExists("mana"), source: projectMana ? "local-config" : commandExists("mana") ? "binary" : "not-detected" },
|
|
219
|
+
contextMode: { detected: commandExists("context-mode") || hasText(/context-mode/), source: commandExists("context-mode") ? "binary" : hasText(/context-mode/) ? "project-reference" : "not-detected" },
|
|
220
|
+
leanCtx: { detected: commandExists("lean-ctx") || hasText(/lean-ctx|lean ctx/), source: commandExists("lean-ctx") ? "binary" : hasText(/lean-ctx|lean ctx/) ? "project-reference" : "not-detected" },
|
|
221
|
+
repomix: { detected: commandExists("repomix") || hasText(/repomix/), source: commandExists("repomix") ? "binary" : hasText(/repomix/) ? "project-reference" : "not-detected" },
|
|
222
|
+
codegraph: { detected: commandExists("codegraph") || hasText(/codegraph|codebase-memory-mcp|jcodemunch|sigmap/), source: commandExists("codegraph") ? "binary" : hasText(/codegraph|codebase-memory-mcp|jcodemunch|sigmap/) ? "project-reference" : "not-detected" },
|
|
223
|
+
tokf: { detected: commandExists("tokf") || hasText(/tokf/), source: commandExists("tokf") ? "binary" : hasText(/tokf/) ? "project-reference" : "not-detected" },
|
|
399
224
|
};
|
|
400
225
|
|
|
401
226
|
return {
|
|
@@ -908,6 +733,13 @@ function buildOptimizerFit(result) {
|
|
|
908
733
|
const mcpEvidence = [];
|
|
909
734
|
let mcpScore = result.optimizationStack.mcpServerTotal >= 10 ? 70 : result.optimizationStack.mcpServerTotal >= 5 ? 45 : 10;
|
|
910
735
|
if (result.optimizationStack.mcpServerTotal) addEvidence(mcpEvidence, `${result.optimizationStack.mcpServerTotal} MCP/tool servers detected`);
|
|
736
|
+
const totalToolCalls = realUsage ? realUsage.sessions.reduce((sum, session) => sum + Number(session.toolCalls || 0), 0) : 0;
|
|
737
|
+
const repeatedCommands = realUsage ? realUsage.sessions.reduce((sum, session) => sum + (session.repeatedCommands || []).reduce((inner, item) => inner + Number(item.count || 0), 0), 0) : 0;
|
|
738
|
+
if (totalToolCalls >= 500) mcpScore += 30;
|
|
739
|
+
else if (totalToolCalls >= 100) mcpScore += 15;
|
|
740
|
+
if (repeatedCommands >= 20) mcpScore += 20;
|
|
741
|
+
if (totalToolCalls) addEvidence(mcpEvidence, `${totalToolCalls} tool calls in recent local sessions`);
|
|
742
|
+
if (repeatedCommands) addEvidence(mcpEvidence, `${repeatedCommands} repeated command/tool mentions`);
|
|
911
743
|
bottlenecks.push({
|
|
912
744
|
id: "tool-surface",
|
|
913
745
|
label: "Tool/MCP surface overhead",
|
|
@@ -1006,6 +838,17 @@ function buildOptimizerFit(result) {
|
|
|
1006
838
|
reason: "Best for one-shot repo handoff, less ideal for long live coding sessions.",
|
|
1007
839
|
},
|
|
1008
840
|
],
|
|
841
|
+
roundTripContext: {
|
|
842
|
+
level: levelFromScore(Math.max(mcpScore, repeatedSourceReads >= 50 ? 60 : repeatedSourceReads >= 12 ? 35 : 0)),
|
|
843
|
+
toolCalls: totalToolCalls,
|
|
844
|
+
repeatedCommandMentions: repeatedCommands,
|
|
845
|
+
repeatedSourceReads,
|
|
846
|
+
mcpServers: result.optimizationStack.mcpServerTotal,
|
|
847
|
+
summary: totalToolCalls || repeatedCommands || repeatedSourceReads || result.optimizationStack.mcpServerTotal
|
|
848
|
+
? "Round-trip context risk includes tool calls, repeated commands, repeated source reads, and MCP/tool surface."
|
|
849
|
+
: "No strong round-trip context signal found in local logs.",
|
|
850
|
+
recommendation: "Measure workflow-level savings, not only compressed payload size. Fewer tool round trips can beat smaller individual responses.",
|
|
851
|
+
},
|
|
1009
852
|
caveats: [
|
|
1010
853
|
"Do not stack optimizers blindly; measure one real workflow before and after.",
|
|
1011
854
|
"Payload reduction is not the same as workflow savings; repeated tool calls can erase compression wins.",
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
module.exports = function createUsageLogUtils(deps) {
|
|
2
|
+
const {
|
|
3
|
+
fs,
|
|
4
|
+
path,
|
|
5
|
+
GENERATED_ARTIFACT_PATTERNS,
|
|
6
|
+
readIfText,
|
|
7
|
+
} = deps;
|
|
8
|
+
|
|
9
|
+
function listFilesRecursive(root, predicate = () => true, limit = 300) {
|
|
10
|
+
const files = [];
|
|
11
|
+
if (!fs.existsSync(root)) return files;
|
|
12
|
+
const stack = [root];
|
|
13
|
+
while (stack.length && files.length < limit) {
|
|
14
|
+
const current = stack.pop();
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
18
|
+
} catch {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const fullPath = path.join(current, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
stack.push(fullPath);
|
|
25
|
+
} else if (entry.isFile() && predicate(fullPath)) {
|
|
26
|
+
files.push(fullPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return files.sort((a, b) => {
|
|
31
|
+
try {
|
|
32
|
+
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
|
33
|
+
} catch {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseJsonl(filePath, maxLines = 20000) {
|
|
40
|
+
const text = readIfText(filePath, 30 * 1024 * 1024);
|
|
41
|
+
if (!text) return [];
|
|
42
|
+
const rows = [];
|
|
43
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
44
|
+
for (const line of lines.slice(Math.max(0, lines.length - maxLines))) {
|
|
45
|
+
try {
|
|
46
|
+
rows.push(JSON.parse(line));
|
|
47
|
+
} catch {
|
|
48
|
+
// Local tool logs can contain partial writes while a session is active.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return rows;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectText(value, options = {}, depth = 0) {
|
|
55
|
+
if (value == null || depth > 8) return "";
|
|
56
|
+
if (typeof value === "string") return value;
|
|
57
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
58
|
+
if (Array.isArray(value)) return value.map((item) => collectText(item, options, depth + 1)).join("\n");
|
|
59
|
+
if (typeof value !== "object") return "";
|
|
60
|
+
|
|
61
|
+
const skipKeys = new Set(["signature", "encrypted_content", "image_url", "data", "auth", "api_key", "token"]);
|
|
62
|
+
const parts = [];
|
|
63
|
+
for (const [key, child] of Object.entries(value)) {
|
|
64
|
+
if (skipKeys.has(key)) continue;
|
|
65
|
+
parts.push(collectText(child, options, depth + 1));
|
|
66
|
+
}
|
|
67
|
+
return parts.filter(Boolean).join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addUsage(target, usage) {
|
|
71
|
+
if (!usage || typeof usage !== "object") return;
|
|
72
|
+
target.inputTokens += Number(usage.input_tokens || usage.prompt_tokens || 0);
|
|
73
|
+
target.outputTokens += Number(usage.output_tokens || usage.completion_tokens || 0);
|
|
74
|
+
target.cacheReadTokens += Number(usage.cache_read_input_tokens || 0);
|
|
75
|
+
target.cacheCreationTokens += Number(usage.cache_creation_input_tokens || 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function totalUsageTokens(usage) {
|
|
79
|
+
if (!usage) return 0;
|
|
80
|
+
return (
|
|
81
|
+
Number(usage.input_tokens || usage.prompt_tokens || 0) +
|
|
82
|
+
Number(usage.output_tokens || usage.completion_tokens || 0) +
|
|
83
|
+
Number(usage.cache_read_input_tokens || 0) +
|
|
84
|
+
Number(usage.cache_creation_input_tokens || 0)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function incrementMap(map, key, amount = 1) {
|
|
89
|
+
if (!key) return;
|
|
90
|
+
map[key] = (map[key] || 0) + amount;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeMentionedPath(value, cwd = "") {
|
|
94
|
+
let normalized = String(value || "")
|
|
95
|
+
.replace(/\\/g, "/")
|
|
96
|
+
.replace(/^[`'"]+|[`'",:;)\]}]+$/g, "")
|
|
97
|
+
.trim();
|
|
98
|
+
normalized = normalized.replace(/^[ MADRCU?!]{1,4}\s+(?=\/|Users\/|home\/)/, "");
|
|
99
|
+
const normalizedCwd = String(cwd || "").replace(/\\/g, "/");
|
|
100
|
+
const wasAbsolute = normalized.startsWith("/");
|
|
101
|
+
if (wasAbsolute && normalizedCwd && !normalized.startsWith(`${normalizedCwd}/`) && normalized !== normalizedCwd) {
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
if (normalizedCwd && normalized.startsWith(normalizedCwd)) {
|
|
105
|
+
normalized = normalized.slice(normalizedCwd.length);
|
|
106
|
+
}
|
|
107
|
+
normalized = normalized.replace(/^\.?\//, "");
|
|
108
|
+
if (normalizedCwd) {
|
|
109
|
+
const repoName = path.basename(normalizedCwd);
|
|
110
|
+
const repoIndex = normalized.indexOf(`${repoName}/`);
|
|
111
|
+
if (repoIndex >= 0) normalized = normalized.slice(repoIndex + repoName.length + 1);
|
|
112
|
+
}
|
|
113
|
+
return normalized;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isGeneratedArtifactPath(relPath) {
|
|
117
|
+
const normalized = normalizeMentionedPath(relPath);
|
|
118
|
+
return GENERATED_ARTIFACT_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function looksLikeUsefulPath(relPath) {
|
|
122
|
+
const normalized = normalizeMentionedPath(relPath);
|
|
123
|
+
if (!normalized || normalized.startsWith("http") || normalized.includes("://")) return false;
|
|
124
|
+
if (normalized.length < 3 || normalized.split("/").some((part) => !part || part.length > 120)) return false;
|
|
125
|
+
if (/^(Users|home|var|tmp|private|Volumes)\//i.test(normalized)) return false;
|
|
126
|
+
if (/^(Users|home|var|tmp|Downloads|Code|Projects)$/i.test(normalized)) return false;
|
|
127
|
+
if (isGeneratedArtifactPath(normalized)) return true;
|
|
128
|
+
if (/\.[A-Za-z0-9]{1,12}$/.test(normalized)) return true;
|
|
129
|
+
return /(^|\/)(src|app|lib|backend|frontend|tests|docs|scripts|components|pages|routes|api)\//.test(normalized);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function extractMentionedPaths(text, cwd = "") {
|
|
133
|
+
const found = new Set();
|
|
134
|
+
const source = String(text || "");
|
|
135
|
+
const pathPattern = /(?:^|[\s"'`])((?:\.{0,2}\/)?(?:[\w.@-]+\/)+[\w.@+-]+\.[A-Za-z0-9]{1,12})/g;
|
|
136
|
+
const filePattern = /(?:^|[\s"'`])((?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|coverage-final\.json|tsconfig\.json|pyproject\.toml|requirements\.txt|README\.md|CLAUDE\.md|AGENTS\.md))/g;
|
|
137
|
+
for (const pattern of [pathPattern, filePattern]) {
|
|
138
|
+
let match;
|
|
139
|
+
while ((match = pattern.exec(source))) {
|
|
140
|
+
const rel = normalizeMentionedPath(match[1], cwd);
|
|
141
|
+
if (!looksLikeUsefulPath(rel)) continue;
|
|
142
|
+
if (cwd && !isGeneratedArtifactPath(rel) && !fs.existsSync(path.join(cwd, rel))) continue;
|
|
143
|
+
found.add(rel);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return Array.from(found);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeCommand(value) {
|
|
150
|
+
return String(value || "")
|
|
151
|
+
.replace(/\s+/g, " ")
|
|
152
|
+
.replace(/[;|&]+$/g, "")
|
|
153
|
+
.trim()
|
|
154
|
+
.slice(0, 160);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isShellCommand(value) {
|
|
158
|
+
return /^(npm|pnpm|yarn|bun|pytest|python3?|node|npx|uv|ruff|cargo|go|make|git|cd|rm|cp|mv|sed|rg|grep|find|cat)\b/.test(String(value || "").trim());
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function extractCommandCandidates(row, text) {
|
|
162
|
+
const commands = [];
|
|
163
|
+
const directInputs = [
|
|
164
|
+
row.payload?.input,
|
|
165
|
+
row.payload?.arguments,
|
|
166
|
+
row.message?.input,
|
|
167
|
+
row.message?.arguments,
|
|
168
|
+
];
|
|
169
|
+
for (const input of directInputs) {
|
|
170
|
+
if (typeof input === "string") commands.push(input);
|
|
171
|
+
else if (input && typeof input === "object") {
|
|
172
|
+
for (const value of Object.values(input)) {
|
|
173
|
+
if (typeof value === "string") commands.push(value);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const toolItems = Array.isArray(row.message?.content) ? row.message.content : [];
|
|
178
|
+
for (const item of toolItems) {
|
|
179
|
+
if (!item || typeof item !== "object") continue;
|
|
180
|
+
if (typeof item.input === "string") commands.push(item.input);
|
|
181
|
+
if (item.input && typeof item.input === "object") {
|
|
182
|
+
for (const value of Object.values(item.input)) {
|
|
183
|
+
if (typeof value === "string") commands.push(value);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (/tool_use|function_call/i.test(row.type || row.payload?.type || "")) {
|
|
188
|
+
const commandPattern = /\b(?:npm|pnpm|yarn|bun|pytest|python3?|node|npx|uv|ruff|cargo|go test|make|git)\b[^\n\r"`']{0,140}/g;
|
|
189
|
+
for (const match of String(text || "").matchAll(commandPattern)) {
|
|
190
|
+
commands.push(match[0]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return Array.from(new Set(commands.map(normalizeCommand).filter((cmd) => cmd.length >= 3 && /\s/.test(cmd) && isShellCommand(cmd))));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function topCountEntries(map, limit = 5, minCount = 2) {
|
|
197
|
+
return Object.entries(map || {})
|
|
198
|
+
.filter(([, count]) => count >= minCount)
|
|
199
|
+
.sort((a, b) => b[1] - a[1])
|
|
200
|
+
.slice(0, limit)
|
|
201
|
+
.map(([value, count]) => ({ value, count }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isExpectedRepeatedPath(value) {
|
|
205
|
+
const normalized = normalizeMentionedPath(value).toLowerCase();
|
|
206
|
+
return ["claude.md", "agents.md", "readme.md"].includes(normalized) || normalized.endsWith("/readme.md");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getActionableRepeatedPaths(session, limit = 3) {
|
|
210
|
+
return (session.repeatedPathMentions || [])
|
|
211
|
+
.filter((item) => !isExpectedRepeatedPath(item.value))
|
|
212
|
+
.filter((item) => !isGeneratedArtifactPath(item.value))
|
|
213
|
+
.slice(0, limit);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function summarizeGeneratedArtifacts(items = [], limit = 4) {
|
|
217
|
+
const groups = new Map();
|
|
218
|
+
for (const item of items) {
|
|
219
|
+
const value = normalizeMentionedPath(item.value);
|
|
220
|
+
let key = "generated files";
|
|
221
|
+
if (value.includes("__pycache__/") || value.endsWith(".pyc")) key = "__pycache__";
|
|
222
|
+
else if (value.includes("node_modules/")) key = "node_modules";
|
|
223
|
+
else if (/package-lock\.json|pnpm-lock\.yaml|yarn\.lock$/i.test(value)) key = "lockfiles";
|
|
224
|
+
else if (value.includes("/dist/") || value.startsWith("dist/")) key = "dist";
|
|
225
|
+
else if (value.includes("/build/") || value.startsWith("build/")) key = "build";
|
|
226
|
+
else if (value.includes("/coverage/") || value.startsWith("coverage/")) key = "coverage";
|
|
227
|
+
else if (/(^|\/)assets\/[^/]+-[A-Za-z0-9_-]{6,}\.(js|css|map)$/i.test(value)) key = "hashed assets";
|
|
228
|
+
const current = groups.get(key) || { type: key, count: 0, examples: [] };
|
|
229
|
+
current.count += Number(item.count || 1);
|
|
230
|
+
if (current.examples.length < 2) current.examples.push(value);
|
|
231
|
+
groups.set(key, current);
|
|
232
|
+
}
|
|
233
|
+
return Array.from(groups.values()).sort((a, b) => b.count - a.count).slice(0, limit);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
addUsage,
|
|
238
|
+
collectText,
|
|
239
|
+
extractCommandCandidates,
|
|
240
|
+
extractMentionedPaths,
|
|
241
|
+
getActionableRepeatedPaths,
|
|
242
|
+
incrementMap,
|
|
243
|
+
isGeneratedArtifactPath,
|
|
244
|
+
listFilesRecursive,
|
|
245
|
+
normalizeMentionedPath,
|
|
246
|
+
parseJsonl,
|
|
247
|
+
summarizeGeneratedArtifacts,
|
|
248
|
+
topCountEntries,
|
|
249
|
+
totalUsageTokens,
|
|
250
|
+
};
|
|
251
|
+
};
|