getprismo 0.1.22 → 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 +3 -0
- package/lib/prismo-dev/scan-path-utils.js +203 -0
- package/lib/prismo-dev/scan.js +7 -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 +9 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -864,8 +864,11 @@ lib/prismo-dev/fixes.js safe ignore/template generation
|
|
|
864
864
|
lib/prismo-dev/mcp.js local MCP server and Prismo tool bindings
|
|
865
865
|
lib/prismo-dev/report.js terminal, markdown, ci reports
|
|
866
866
|
lib/prismo-dev/scan.js repo scanning, scoring, readiness
|
|
867
|
+
lib/prismo-dev/scan-path-utils.js scan ignore/path helper logic
|
|
867
868
|
lib/prismo-dev/shield.js local command shield and searchable output index
|
|
869
|
+
lib/prismo-dev/usage-log-utils.js local session log parsing helpers
|
|
868
870
|
lib/prismo-dev/usage-watch.js local logs, watch, cost, timeline
|
|
871
|
+
lib/prismo-dev/utils.js shared terminal/file/token helpers
|
|
869
872
|
```
|
|
870
873
|
|
|
871
874
|
---
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
module.exports = function createScanPathUtils(deps) {
|
|
2
|
+
const { fs, path } = deps;
|
|
3
|
+
|
|
4
|
+
function normalizeRel(value) {
|
|
5
|
+
return value.split(path.sep).join("/");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readIgnoreFile(root, fileName) {
|
|
9
|
+
const filePath = path.join(root, fileName);
|
|
10
|
+
if (!fs.existsSync(filePath)) return [];
|
|
11
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
12
|
+
return text
|
|
13
|
+
.split(/\r?\n/)
|
|
14
|
+
.map((line) => line.trim())
|
|
15
|
+
.filter((line) => line && !line.startsWith("#"))
|
|
16
|
+
.map((line) => line.replace(/^!/, ""));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function patternMatches(pattern, relPath, isDir = false) {
|
|
20
|
+
const normalizedPattern = pattern.replace(/\\/g, "/").replace(/^\//, "");
|
|
21
|
+
const normalizedRel = normalizeRel(relPath);
|
|
22
|
+
const dirRel = isDir && !normalizedRel.endsWith("/") ? `${normalizedRel}/` : normalizedRel;
|
|
23
|
+
|
|
24
|
+
if (!normalizedPattern) return false;
|
|
25
|
+
if (normalizedPattern.endsWith("/")) {
|
|
26
|
+
const base = normalizedPattern.slice(0, -1);
|
|
27
|
+
return (
|
|
28
|
+
normalizedRel === base ||
|
|
29
|
+
normalizedRel.startsWith(`${base}/`) ||
|
|
30
|
+
normalizedRel.endsWith(`/${base}`) ||
|
|
31
|
+
normalizedRel.includes(`/${base}/`) ||
|
|
32
|
+
dirRel.includes(`/${base}/`)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (normalizedPattern.startsWith("*.")) {
|
|
36
|
+
return normalizedRel.endsWith(normalizedPattern.slice(1));
|
|
37
|
+
}
|
|
38
|
+
if (normalizedPattern.includes("*")) {
|
|
39
|
+
const escaped = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
40
|
+
return new RegExp(`(^|/)${escaped}$`).test(normalizedRel);
|
|
41
|
+
}
|
|
42
|
+
return (
|
|
43
|
+
normalizedRel === normalizedPattern ||
|
|
44
|
+
dirRel === normalizedPattern ||
|
|
45
|
+
normalizedRel.startsWith(`${normalizedPattern}/`) ||
|
|
46
|
+
normalizedRel.endsWith(`/${normalizedPattern}`)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isIgnored(relPath, patterns, isDir = false) {
|
|
51
|
+
return patterns.some((pattern) => patternMatches(pattern, relPath, isDir));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ignoreSuggestionCovered(pattern, existingPatterns) {
|
|
55
|
+
if (!pattern) return true;
|
|
56
|
+
if (existingPatterns.includes(pattern)) return true;
|
|
57
|
+
const sample = pattern
|
|
58
|
+
.replace(/^\*\//, "")
|
|
59
|
+
.replace(/^\*\*/, "sample")
|
|
60
|
+
.replace(/\*/g, "sample")
|
|
61
|
+
.replace(/\/$/, "");
|
|
62
|
+
const isDir = pattern.endsWith("/") || pattern.endsWith("/**");
|
|
63
|
+
return existingPatterns.some((existing) => {
|
|
64
|
+
if (existing === pattern) return true;
|
|
65
|
+
if (existing.endsWith("/") && pattern.startsWith(existing)) return true;
|
|
66
|
+
return patternMatches(existing, sample, isDir);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function missingIgnoreSuggestions(recommended, existingPatterns) {
|
|
71
|
+
return recommended.filter((pattern) => !ignoreSuggestionCovered(pattern, existingPatterns));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const SESSION_NOISE_DIRS = new Set([
|
|
75
|
+
".next",
|
|
76
|
+
".nuxt",
|
|
77
|
+
".prismo",
|
|
78
|
+
".pytest_cache",
|
|
79
|
+
".turbo",
|
|
80
|
+
"__pycache__",
|
|
81
|
+
"build",
|
|
82
|
+
"calendar-dumps",
|
|
83
|
+
"coverage",
|
|
84
|
+
"dist",
|
|
85
|
+
"event-dumps",
|
|
86
|
+
"events",
|
|
87
|
+
"exports",
|
|
88
|
+
"htmlcov",
|
|
89
|
+
"inbox-dumps",
|
|
90
|
+
"logs",
|
|
91
|
+
"models",
|
|
92
|
+
"node_modules",
|
|
93
|
+
"out",
|
|
94
|
+
"playwright-report",
|
|
95
|
+
"session-dumps",
|
|
96
|
+
"source-streams",
|
|
97
|
+
"state-backups",
|
|
98
|
+
"test-results",
|
|
99
|
+
"tmp",
|
|
100
|
+
"temp",
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const SESSION_NOISE_FILE_NAMES = new Set([
|
|
104
|
+
"package-lock.json",
|
|
105
|
+
"pnpm-lock.yaml",
|
|
106
|
+
"yarn.lock",
|
|
107
|
+
"bun.lockb",
|
|
108
|
+
"coverage-final.json",
|
|
109
|
+
"lcov.info",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const SESSION_NOISE_EXTENSIONS = new Set([
|
|
113
|
+
".db",
|
|
114
|
+
".jsonl",
|
|
115
|
+
".lock",
|
|
116
|
+
".log",
|
|
117
|
+
".sqlite",
|
|
118
|
+
".sqlite3",
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
function cleanSessionPath(value) {
|
|
122
|
+
const text = String(value || "").trim().replace(/\\/g, "/");
|
|
123
|
+
if (!text || /^https?:\/\//.test(text)) return null;
|
|
124
|
+
const withoutQuotes = text.replace(/^["'`]+|["'`.,:;)\]]+$/g, "");
|
|
125
|
+
if (!withoutQuotes || withoutQuotes.includes("\n")) return null;
|
|
126
|
+
const markerIndex = withoutQuotes.indexOf("/Users/");
|
|
127
|
+
if (markerIndex > 0) return withoutQuotes.slice(markerIndex);
|
|
128
|
+
return withoutQuotes;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function sessionIgnorePatternForPath(value, root) {
|
|
132
|
+
const cleaned = cleanSessionPath(value);
|
|
133
|
+
if (!cleaned) return null;
|
|
134
|
+
const rootNormalized = normalizeRel(root);
|
|
135
|
+
let rel = cleaned;
|
|
136
|
+
if (path.isAbsolute(cleaned)) {
|
|
137
|
+
const normalized = normalizeRel(cleaned);
|
|
138
|
+
if (!normalized.startsWith(`${rootNormalized}/`)) return null;
|
|
139
|
+
rel = normalizeRel(path.relative(root, cleaned));
|
|
140
|
+
}
|
|
141
|
+
rel = normalizeRel(rel).replace(/^\.\//, "");
|
|
142
|
+
if (!rel || rel === "." || rel.startsWith("../") || rel.includes("..")) return null;
|
|
143
|
+
|
|
144
|
+
const segments = rel.split("/").filter(Boolean);
|
|
145
|
+
if (!segments.length) return null;
|
|
146
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
147
|
+
const segment = segments[index];
|
|
148
|
+
if (SESSION_NOISE_DIRS.has(segment)) {
|
|
149
|
+
return `${segments.slice(0, index + 1).join("/")}/`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const fileName = segments[segments.length - 1];
|
|
154
|
+
const lowerName = fileName.toLowerCase();
|
|
155
|
+
const ext = path.extname(lowerName);
|
|
156
|
+
if (SESSION_NOISE_FILE_NAMES.has(lowerName)) return fileName;
|
|
157
|
+
if (SESSION_NOISE_EXTENSIONS.has(ext)) return rel;
|
|
158
|
+
if (/_state\.json$/i.test(fileName)) return "*_state.json";
|
|
159
|
+
if (/_tokens\.json$/i.test(fileName)) return "*_tokens.json";
|
|
160
|
+
if (/_export\.json$/i.test(fileName)) return "*_export.json";
|
|
161
|
+
if (/secret|credential|token/i.test(fileName) && /\.json$/i.test(fileName)) return rel;
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildSessionIgnoreSuggestions(realUsage, root) {
|
|
166
|
+
if (!realUsage || !Array.isArray(realUsage.sessions)) return [];
|
|
167
|
+
const byPattern = new Map();
|
|
168
|
+
const add = (pattern, item, source, reason) => {
|
|
169
|
+
if (!pattern) return;
|
|
170
|
+
const existing = byPattern.get(pattern) || {
|
|
171
|
+
pattern,
|
|
172
|
+
source,
|
|
173
|
+
reason,
|
|
174
|
+
count: 0,
|
|
175
|
+
examples: [],
|
|
176
|
+
};
|
|
177
|
+
existing.count += Number(item?.count || 1);
|
|
178
|
+
const example = item?.value || item?.path || pattern;
|
|
179
|
+
if (example && !existing.examples.includes(example) && existing.examples.length < 3) existing.examples.push(example);
|
|
180
|
+
byPattern.set(pattern, existing);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
for (const session of realUsage.sessions) {
|
|
184
|
+
for (const item of session.generatedArtifacts || []) {
|
|
185
|
+
add(sessionIgnorePatternForPath(item.value, root), item, session.tool || "session", "Generated artifact entered local session context.");
|
|
186
|
+
}
|
|
187
|
+
for (const item of session.repeatedPathMentions || []) {
|
|
188
|
+
add(sessionIgnorePatternForPath(item.value, root), item, session.tool || "session", "Noisy path appeared repeatedly in local session context.");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return Array.from(byPattern.values())
|
|
192
|
+
.sort((a, b) => b.count - a.count || a.pattern.localeCompare(b.pattern))
|
|
193
|
+
.slice(0, 25);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
buildSessionIgnoreSuggestions,
|
|
198
|
+
isIgnored,
|
|
199
|
+
missingIgnoreSuggestions,
|
|
200
|
+
normalizeRel,
|
|
201
|
+
readIgnoreFile,
|
|
202
|
+
};
|
|
203
|
+
};
|
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();
|
|
@@ -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
|
+
};
|
|
@@ -13,84 +13,26 @@ module.exports = function createUsageWatch(deps) {
|
|
|
13
13
|
writeGeneratedFile,
|
|
14
14
|
} = deps;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
return files.sort((a, b) => {
|
|
38
|
-
try {
|
|
39
|
-
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
|
40
|
-
} catch {
|
|
41
|
-
return 0;
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseJsonl(filePath, maxLines = 20000) {
|
|
47
|
-
const text = readIfText(filePath, 30 * 1024 * 1024);
|
|
48
|
-
if (!text) return [];
|
|
49
|
-
const rows = [];
|
|
50
|
-
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
51
|
-
for (const line of lines.slice(Math.max(0, lines.length - maxLines))) {
|
|
52
|
-
try {
|
|
53
|
-
rows.push(JSON.parse(line));
|
|
54
|
-
} catch {
|
|
55
|
-
// Local tool logs can contain partial writes while a session is active.
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return rows;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function collectText(value, options = {}, depth = 0) {
|
|
62
|
-
if (value == null || depth > 8) return "";
|
|
63
|
-
if (typeof value === "string") return value;
|
|
64
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
65
|
-
if (Array.isArray(value)) return value.map((item) => collectText(item, options, depth + 1)).join("\n");
|
|
66
|
-
if (typeof value !== "object") return "";
|
|
67
|
-
|
|
68
|
-
const skipKeys = new Set(["signature", "encrypted_content", "image_url", "data", "auth", "api_key", "token"]);
|
|
69
|
-
const parts = [];
|
|
70
|
-
for (const [key, child] of Object.entries(value)) {
|
|
71
|
-
if (skipKeys.has(key)) continue;
|
|
72
|
-
parts.push(collectText(child, options, depth + 1));
|
|
73
|
-
}
|
|
74
|
-
return parts.filter(Boolean).join("\n");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function addUsage(target, usage) {
|
|
78
|
-
if (!usage || typeof usage !== "object") return;
|
|
79
|
-
target.inputTokens += Number(usage.input_tokens || usage.prompt_tokens || 0);
|
|
80
|
-
target.outputTokens += Number(usage.output_tokens || usage.completion_tokens || 0);
|
|
81
|
-
target.cacheReadTokens += Number(usage.cache_read_input_tokens || 0);
|
|
82
|
-
target.cacheCreationTokens += Number(usage.cache_creation_input_tokens || 0);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function totalUsageTokens(usage) {
|
|
86
|
-
if (!usage) return 0;
|
|
87
|
-
return (
|
|
88
|
-
Number(usage.input_tokens || usage.prompt_tokens || 0) +
|
|
89
|
-
Number(usage.output_tokens || usage.completion_tokens || 0) +
|
|
90
|
-
Number(usage.cache_read_input_tokens || 0) +
|
|
91
|
-
Number(usage.cache_creation_input_tokens || 0)
|
|
92
|
-
);
|
|
93
|
-
}
|
|
16
|
+
const {
|
|
17
|
+
addUsage,
|
|
18
|
+
collectText,
|
|
19
|
+
extractCommandCandidates,
|
|
20
|
+
extractMentionedPaths,
|
|
21
|
+
getActionableRepeatedPaths,
|
|
22
|
+
incrementMap,
|
|
23
|
+
isGeneratedArtifactPath,
|
|
24
|
+
listFilesRecursive,
|
|
25
|
+
normalizeMentionedPath,
|
|
26
|
+
parseJsonl,
|
|
27
|
+
summarizeGeneratedArtifacts,
|
|
28
|
+
topCountEntries,
|
|
29
|
+
totalUsageTokens,
|
|
30
|
+
} = require("./usage-log-utils")({
|
|
31
|
+
fs,
|
|
32
|
+
path,
|
|
33
|
+
GENERATED_ARTIFACT_PATTERNS,
|
|
34
|
+
readIfText,
|
|
35
|
+
});
|
|
94
36
|
|
|
95
37
|
function inferClaudePricingKey(model) {
|
|
96
38
|
const normalized = String(model || "").toLowerCase();
|
|
@@ -139,154 +81,6 @@ function getSessionRisk(tokens, toolTokens) {
|
|
|
139
81
|
return "Low";
|
|
140
82
|
}
|
|
141
83
|
|
|
142
|
-
function incrementMap(map, key, amount = 1) {
|
|
143
|
-
if (!key) return;
|
|
144
|
-
map[key] = (map[key] || 0) + amount;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function normalizeMentionedPath(value, cwd = "") {
|
|
148
|
-
let normalized = String(value || "")
|
|
149
|
-
.replace(/\\/g, "/")
|
|
150
|
-
.replace(/^[`'"]+|[`'",:;)\]}]+$/g, "")
|
|
151
|
-
.trim();
|
|
152
|
-
normalized = normalized.replace(/^[ MADRCU?!]{1,4}\s+(?=\/|Users\/|home\/)/, "");
|
|
153
|
-
const normalizedCwd = String(cwd || "").replace(/\\/g, "/");
|
|
154
|
-
const wasAbsolute = normalized.startsWith("/");
|
|
155
|
-
if (wasAbsolute && normalizedCwd && !normalized.startsWith(`${normalizedCwd}/`) && normalized !== normalizedCwd) {
|
|
156
|
-
return "";
|
|
157
|
-
}
|
|
158
|
-
if (normalizedCwd && normalized.startsWith(normalizedCwd)) {
|
|
159
|
-
normalized = normalized.slice(normalizedCwd.length);
|
|
160
|
-
}
|
|
161
|
-
normalized = normalized.replace(/^\.?\//, "");
|
|
162
|
-
if (normalizedCwd) {
|
|
163
|
-
const repoName = path.basename(normalizedCwd);
|
|
164
|
-
const repoIndex = normalized.indexOf(`${repoName}/`);
|
|
165
|
-
if (repoIndex >= 0) normalized = normalized.slice(repoIndex + repoName.length + 1);
|
|
166
|
-
}
|
|
167
|
-
return normalized;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function isGeneratedArtifactPath(relPath) {
|
|
171
|
-
const normalized = normalizeMentionedPath(relPath);
|
|
172
|
-
return GENERATED_ARTIFACT_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function looksLikeUsefulPath(relPath) {
|
|
176
|
-
const normalized = normalizeMentionedPath(relPath);
|
|
177
|
-
if (!normalized || normalized.startsWith("http") || normalized.includes("://")) return false;
|
|
178
|
-
if (normalized.length < 3 || normalized.split("/").some((part) => !part || part.length > 120)) return false;
|
|
179
|
-
if (/^(Users|home|var|tmp|private|Volumes)\//i.test(normalized)) return false;
|
|
180
|
-
if (/^(Users|home|var|tmp|Downloads|Code|Projects)$/i.test(normalized)) return false;
|
|
181
|
-
if (isGeneratedArtifactPath(normalized)) return true;
|
|
182
|
-
if (/\.[A-Za-z0-9]{1,12}$/.test(normalized)) return true;
|
|
183
|
-
return /(^|\/)(src|app|lib|backend|frontend|tests|docs|scripts|components|pages|routes|api)\//.test(normalized);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function extractMentionedPaths(text, cwd = "") {
|
|
187
|
-
const found = new Set();
|
|
188
|
-
const source = String(text || "");
|
|
189
|
-
const pathPattern = /(?:^|[\s"'`])((?:\.{0,2}\/)?(?:[\w.@-]+\/)+[\w.@+-]+\.[A-Za-z0-9]{1,12})/g;
|
|
190
|
-
const filePattern = /(?:^|[\s"'`])((?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|coverage-final\.json|tsconfig\.json|pyproject\.toml|requirements\.txt|README\.md|CLAUDE\.md|AGENTS\.md))/g;
|
|
191
|
-
for (const pattern of [pathPattern, filePattern]) {
|
|
192
|
-
let match;
|
|
193
|
-
while ((match = pattern.exec(source))) {
|
|
194
|
-
const rel = normalizeMentionedPath(match[1], cwd);
|
|
195
|
-
if (!looksLikeUsefulPath(rel)) continue;
|
|
196
|
-
if (cwd && !isGeneratedArtifactPath(rel) && !fs.existsSync(path.join(cwd, rel))) continue;
|
|
197
|
-
found.add(rel);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return Array.from(found);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function normalizeCommand(value) {
|
|
204
|
-
return String(value || "")
|
|
205
|
-
.replace(/\s+/g, " ")
|
|
206
|
-
.replace(/[;|&]+$/g, "")
|
|
207
|
-
.trim()
|
|
208
|
-
.slice(0, 160);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function isShellCommand(value) {
|
|
212
|
-
return /^(npm|pnpm|yarn|bun|pytest|python3?|node|npx|uv|ruff|cargo|go|make|git|cd|rm|cp|mv|sed|rg|grep|find|cat)\b/.test(String(value || "").trim());
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function extractCommandCandidates(row, text) {
|
|
216
|
-
const commands = [];
|
|
217
|
-
const directInputs = [
|
|
218
|
-
row.payload?.input,
|
|
219
|
-
row.payload?.arguments,
|
|
220
|
-
row.message?.input,
|
|
221
|
-
row.message?.arguments,
|
|
222
|
-
];
|
|
223
|
-
for (const input of directInputs) {
|
|
224
|
-
if (typeof input === "string") commands.push(input);
|
|
225
|
-
else if (input && typeof input === "object") {
|
|
226
|
-
for (const value of Object.values(input)) {
|
|
227
|
-
if (typeof value === "string") commands.push(value);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
const toolItems = Array.isArray(row.message?.content) ? row.message.content : [];
|
|
232
|
-
for (const item of toolItems) {
|
|
233
|
-
if (!item || typeof item !== "object") continue;
|
|
234
|
-
if (typeof item.input === "string") commands.push(item.input);
|
|
235
|
-
if (item.input && typeof item.input === "object") {
|
|
236
|
-
for (const value of Object.values(item.input)) {
|
|
237
|
-
if (typeof value === "string") commands.push(value);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (/tool_use|function_call/i.test(row.type || row.payload?.type || "")) {
|
|
242
|
-
const commandPattern = /\b(?:npm|pnpm|yarn|bun|pytest|python3?|node|npx|uv|ruff|cargo|go test|make|git)\b[^\n\r"`']{0,140}/g;
|
|
243
|
-
for (const match of String(text || "").matchAll(commandPattern)) {
|
|
244
|
-
commands.push(match[0]);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return Array.from(new Set(commands.map(normalizeCommand).filter((cmd) => cmd.length >= 3 && /\s/.test(cmd) && isShellCommand(cmd))));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function topCountEntries(map, limit = 5, minCount = 2) {
|
|
251
|
-
return Object.entries(map || {})
|
|
252
|
-
.filter(([, count]) => count >= minCount)
|
|
253
|
-
.sort((a, b) => b[1] - a[1])
|
|
254
|
-
.slice(0, limit)
|
|
255
|
-
.map(([value, count]) => ({ value, count }));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function isExpectedRepeatedPath(value) {
|
|
259
|
-
const normalized = normalizeMentionedPath(value).toLowerCase();
|
|
260
|
-
return ["claude.md", "agents.md", "readme.md"].includes(normalized) || normalized.endsWith("/readme.md");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function getActionableRepeatedPaths(session, limit = 3) {
|
|
264
|
-
return (session.repeatedPathMentions || [])
|
|
265
|
-
.filter((item) => !isExpectedRepeatedPath(item.value))
|
|
266
|
-
.filter((item) => !isGeneratedArtifactPath(item.value))
|
|
267
|
-
.slice(0, limit);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function summarizeGeneratedArtifacts(items = [], limit = 4) {
|
|
271
|
-
const groups = new Map();
|
|
272
|
-
for (const item of items) {
|
|
273
|
-
const value = normalizeMentionedPath(item.value);
|
|
274
|
-
let key = "generated files";
|
|
275
|
-
if (value.includes("__pycache__/") || value.endsWith(".pyc")) key = "__pycache__";
|
|
276
|
-
else if (value.includes("node_modules/")) key = "node_modules";
|
|
277
|
-
else if (/package-lock\.json|pnpm-lock\.yaml|yarn\.lock$/i.test(value)) key = "lockfiles";
|
|
278
|
-
else if (value.includes("/dist/") || value.startsWith("dist/")) key = "dist";
|
|
279
|
-
else if (value.includes("/build/") || value.startsWith("build/")) key = "build";
|
|
280
|
-
else if (value.includes("/coverage/") || value.startsWith("coverage/")) key = "coverage";
|
|
281
|
-
else if (/(^|\/)assets\/[^/]+-[A-Za-z0-9_-]{6,}\.(js|css|map)$/i.test(value)) key = "hashed assets";
|
|
282
|
-
const current = groups.get(key) || { type: key, count: 0, examples: [] };
|
|
283
|
-
current.count += Number(item.count || 1);
|
|
284
|
-
if (current.examples.length < 2) current.examples.push(value);
|
|
285
|
-
groups.set(key, current);
|
|
286
|
-
}
|
|
287
|
-
return Array.from(groups.values()).sort((a, b) => b.count - a.count).slice(0, limit);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
84
|
function analyzeSessionFile(filePath, tool) {
|
|
291
85
|
const rows = parseJsonl(filePath);
|
|
292
86
|
const stat = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const { BINARY_EXTENSIONS } = require("./constants");
|
|
5
|
+
|
|
6
|
+
function shouldUseColor() {
|
|
7
|
+
return process.stdout.isTTY && !process.env.NO_COLOR;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const colorCodes = {
|
|
11
|
+
reset: "\x1b[0m",
|
|
12
|
+
bold: "\x1b[1m",
|
|
13
|
+
dim: "\x1b[2m",
|
|
14
|
+
red: "\x1b[31m",
|
|
15
|
+
yellow: "\x1b[33m",
|
|
16
|
+
green: "\x1b[32m",
|
|
17
|
+
cyan: "\x1b[36m",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function color(text, tone, enabled = shouldUseColor()) {
|
|
21
|
+
if (!enabled || !colorCodes[tone]) return text;
|
|
22
|
+
return `${colorCodes[tone]}${text}${colorCodes.reset}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function severityIcon(severity) {
|
|
26
|
+
if (severity === "critical") return "[critical]";
|
|
27
|
+
if (severity === "high") return "[high]";
|
|
28
|
+
if (severity === "medium") return "[medium]";
|
|
29
|
+
return "[low]";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function severityColor(severity) {
|
|
33
|
+
if (severity === "critical" || severity === "high") return "red";
|
|
34
|
+
if (severity === "medium") return "yellow";
|
|
35
|
+
return "cyan";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function printStep(label, json = false) {
|
|
39
|
+
if (json) return () => {};
|
|
40
|
+
process.stderr.write(`${color("...", "cyan")} ${label}`);
|
|
41
|
+
return (status = "done") => {
|
|
42
|
+
process.stderr.write(` ${color(`[${status}]`, status === "done" ? "green" : "yellow")}\n`);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function estimateTokens(textOrBytes) {
|
|
47
|
+
const length = typeof textOrBytes === "string" ? textOrBytes.length : Number(textOrBytes || 0);
|
|
48
|
+
return Math.ceil(length / 4);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readIfText(filePath, maxBytes = 2 * 1024 * 1024) {
|
|
52
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
53
|
+
if (BINARY_EXTENSIONS.has(ext)) return null;
|
|
54
|
+
|
|
55
|
+
let stat;
|
|
56
|
+
try {
|
|
57
|
+
stat = fs.statSync(filePath);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
62
|
+
|
|
63
|
+
const buffer = fs.readFileSync(filePath);
|
|
64
|
+
if (buffer.includes(0)) return null;
|
|
65
|
+
return buffer.toString("utf8");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function safeReadJson(filePath) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
color,
|
|
78
|
+
estimateTokens,
|
|
79
|
+
printStep,
|
|
80
|
+
readIfText,
|
|
81
|
+
safeReadJson,
|
|
82
|
+
severityColor,
|
|
83
|
+
severityIcon,
|
|
84
|
+
shouldUseColor,
|
|
85
|
+
};
|
package/lib/prismo-dev-scan.js
CHANGED
|
@@ -19,76 +19,15 @@ const {
|
|
|
19
19
|
GENERATED_ARTIFACT_PATTERNS,
|
|
20
20
|
} = require("./prismo-dev/constants");
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
yellow: "\x1b[33m",
|
|
32
|
-
green: "\x1b[32m",
|
|
33
|
-
cyan: "\x1b[36m",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function color(text, tone, enabled = shouldUseColor()) {
|
|
37
|
-
if (!enabled || !colorCodes[tone]) return text;
|
|
38
|
-
return `${colorCodes[tone]}${text}${colorCodes.reset}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function severityIcon(severity) {
|
|
42
|
-
if (severity === "critical") return "[critical]";
|
|
43
|
-
if (severity === "high") return "[high]";
|
|
44
|
-
if (severity === "medium") return "[medium]";
|
|
45
|
-
return "[low]";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function severityColor(severity) {
|
|
49
|
-
if (severity === "critical" || severity === "high") return "red";
|
|
50
|
-
if (severity === "medium") return "yellow";
|
|
51
|
-
return "cyan";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function printStep(label, json = false) {
|
|
55
|
-
if (json) return () => {};
|
|
56
|
-
process.stderr.write(`${color("...", "cyan")} ${label}`);
|
|
57
|
-
return (status = "done") => {
|
|
58
|
-
process.stderr.write(` ${color(`[${status}]`, status === "done" ? "green" : "yellow")}\n`);
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function estimateTokens(textOrBytes) {
|
|
63
|
-
const length = typeof textOrBytes === "string" ? textOrBytes.length : Number(textOrBytes || 0);
|
|
64
|
-
return Math.ceil(length / 4);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function readIfText(filePath, maxBytes = 2 * 1024 * 1024) {
|
|
68
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
69
|
-
if (BINARY_EXTENSIONS.has(ext)) return null;
|
|
70
|
-
|
|
71
|
-
let stat;
|
|
72
|
-
try {
|
|
73
|
-
stat = fs.statSync(filePath);
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
78
|
-
|
|
79
|
-
const buffer = fs.readFileSync(filePath);
|
|
80
|
-
if (buffer.includes(0)) return null;
|
|
81
|
-
return buffer.toString("utf8");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
function safeReadJson(filePath) {
|
|
86
|
-
try {
|
|
87
|
-
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
88
|
-
} catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
22
|
+
const {
|
|
23
|
+
color,
|
|
24
|
+
estimateTokens,
|
|
25
|
+
printStep,
|
|
26
|
+
readIfText,
|
|
27
|
+
safeReadJson,
|
|
28
|
+
severityColor,
|
|
29
|
+
severityIcon,
|
|
30
|
+
} = require("./prismo-dev/utils");
|
|
92
31
|
|
|
93
32
|
let scanRepo;
|
|
94
33
|
|
package/package.json
CHANGED