getprismo 0.1.22 → 0.1.24

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.
@@ -0,0 +1,336 @@
1
+ module.exports = function createUsageCost(deps) {
2
+ const {
3
+ CLAUDE_PRICING,
4
+ DEFAULT_CLAUDE_PRICING_KEY,
5
+ NPX_COMMAND,
6
+ formatMoney,
7
+ formatTokenCount,
8
+ } = deps;
9
+
10
+ function percentOf(part, total) {
11
+ if (!total) return 0;
12
+ return Math.round((Number(part || 0) / total) * 100);
13
+ }
14
+
15
+ function inferClaudePricingKey(model) {
16
+ const normalized = String(model || "").toLowerCase();
17
+ if (normalized.includes("opus") && normalized.includes("4-1")) return "opus-4.1";
18
+ if (normalized.includes("opus") && normalized.includes("4.1")) return "opus-4.1";
19
+ if (normalized.includes("opus") && normalized.includes("4")) return "opus-4";
20
+ if (normalized.includes("sonnet") && normalized.includes("4")) return "sonnet-4";
21
+ if (normalized.includes("sonnet") && (normalized.includes("3-7") || normalized.includes("3.7"))) return "sonnet-3.7";
22
+ if (normalized.includes("sonnet") && (normalized.includes("3-5") || normalized.includes("3.5"))) return "sonnet-3.5";
23
+ if (normalized.includes("haiku") && (normalized.includes("3-5") || normalized.includes("3.5"))) return "haiku-3.5";
24
+ if (normalized.includes("haiku") && normalized.includes("3")) return "haiku-3";
25
+ if (normalized.includes("opus") && normalized.includes("3")) return "opus-3";
26
+ return DEFAULT_CLAUDE_PRICING_KEY;
27
+ }
28
+
29
+ function calculateClaudeCost(tokens, model) {
30
+ const pricingKey = inferClaudePricingKey(model);
31
+ const pricing = CLAUDE_PRICING[pricingKey] || CLAUDE_PRICING[DEFAULT_CLAUDE_PRICING_KEY];
32
+ const inputTokens = Number(tokens.inputTokens || 0);
33
+ const outputTokens = Number(tokens.outputTokens || 0);
34
+ const cacheWriteTokens = Number(tokens.cacheCreationTokens || tokens.cacheWriteTokens || 0);
35
+ const cacheReadTokens = Number(tokens.cacheReadTokens || 0);
36
+ const input = (inputTokens / 1000000) * pricing.input;
37
+ const output = (outputTokens / 1000000) * pricing.output;
38
+ const cacheWrite = (cacheWriteTokens / 1000000) * pricing.cacheWrite;
39
+ const cacheRead = (cacheReadTokens / 1000000) * pricing.cacheRead;
40
+ const total = input + output + cacheWrite + cacheRead;
41
+ const noCache = ((inputTokens + cacheWriteTokens + cacheReadTokens) / 1000000) * pricing.input + output;
42
+ return {
43
+ model: pricing.name,
44
+ pricingKey,
45
+ pricing,
46
+ input,
47
+ output,
48
+ cacheWrite,
49
+ cacheRead,
50
+ total,
51
+ noCache,
52
+ cacheSavings: Math.max(noCache - total, 0),
53
+ };
54
+ }
55
+
56
+ function buildClaudeSessionDiagnosis(session) {
57
+ const totalCost = session.cost ? session.cost.total : 0;
58
+ const drivers = [];
59
+ if (session.cost) {
60
+ const costParts = [
61
+ ["output", session.cost.output, "Assistant output is the largest cost driver."],
62
+ ["cache-read", session.cost.cacheRead, "Repeated cached context reads are driving spend."],
63
+ ["cache-write", session.cost.cacheWrite, "Large context cache writes are adding upfront cost."],
64
+ ["input", session.cost.input, "Fresh input/context tokens are driving spend."],
65
+ ].sort((a, b) => b[1] - a[1]);
66
+ for (const [name, cost, message] of costParts) {
67
+ if (cost > 0) {
68
+ drivers.push({ type: name, cost, share: percentOf(cost, totalCost), message });
69
+ }
70
+ }
71
+ }
72
+ if (session.estimatedToolTokens >= 75000) {
73
+ drivers.push({
74
+ type: "tool-output",
75
+ tokens: session.estimatedToolTokens,
76
+ share: null,
77
+ message: "Tool output looks heavy; test logs, shell output, or file dumps may be inflating context.",
78
+ });
79
+ }
80
+ if (session.turns >= 30) {
81
+ drivers.push({
82
+ type: "long-session",
83
+ turns: session.turns,
84
+ share: null,
85
+ message: "Long session detected; unrelated follow-up work is likely riding old context.",
86
+ });
87
+ }
88
+ if (session.contextRisk === "High") {
89
+ drivers.push({
90
+ type: "context-risk",
91
+ tokens: session.displayTokens,
92
+ share: null,
93
+ message: "Session context is high enough that splitting work or using context packs should matter.",
94
+ });
95
+ }
96
+
97
+ const recommendations = [];
98
+ if (drivers.some((driver) => driver.type === "tool-output")) {
99
+ recommendations.push("Summarize long command output before pasting or re-reading it.");
100
+ }
101
+ if (drivers.some((driver) => driver.type === "cache-read" || driver.type === "cache-write" || driver.type === "context-risk")) {
102
+ recommendations.push(`Run ${NPX_COMMAND} optimize, then start from .prismo/architecture-summary.md.`);
103
+ }
104
+ if (drivers.some((driver) => driver.type === "long-session")) {
105
+ recommendations.push("Start a fresh Claude Code session for the next unrelated task.");
106
+ }
107
+ if (drivers.some((driver) => driver.type === "output")) {
108
+ recommendations.push("Ask for concise diffs, file paths, and verification results instead of full prose dumps.");
109
+ }
110
+ if (!recommendations.length) {
111
+ recommendations.push(`${NPX_COMMAND} scan --usage can tie this spend back to repo-level token waste.`);
112
+ }
113
+
114
+ const avoidableRate =
115
+ session.contextRisk === "High" ? 0.28 :
116
+ session.contextRisk === "Medium" ? 0.16 :
117
+ session.turns >= 20 || session.estimatedToolTokens >= 30000 ? 0.1 : 0.04;
118
+ return {
119
+ wasteScore: session.contextRisk === "High" ? 85 : session.contextRisk === "Medium" ? 55 : session.turns >= 20 ? 40 : 20,
120
+ estimatedAvoidableCost: totalCost * avoidableRate,
121
+ estimatedAvoidableRate: avoidableRate,
122
+ drivers: drivers.slice(0, 5),
123
+ recommendations: Array.from(new Set(recommendations)).slice(0, 4),
124
+ };
125
+ }
126
+
127
+ function buildSessionTimeline(session) {
128
+ const events = [];
129
+ const timeline = session.exactTokenTimeline || [];
130
+ for (let i = 1; i < timeline.length; i += 1) {
131
+ const previous = timeline[i - 1];
132
+ const current = timeline[i];
133
+ const delta = Math.max(0, (current.total || 0) - (previous.total || 0));
134
+ if (delta >= 60000) {
135
+ events.push({
136
+ timestamp: current.timestamp || session.updatedAt,
137
+ type: delta >= 250000 ? "context-spike" : "context-growth",
138
+ label: delta >= 250000 ? "Context spike likely" : "Context growth",
139
+ tokens: delta,
140
+ detail: `+${formatTokenCount(delta)} tokens`,
141
+ });
142
+ }
143
+ }
144
+ for (const item of (session.generatedArtifacts || []).slice(0, 5)) {
145
+ events.push({
146
+ timestamp: session.updatedAt,
147
+ type: "artifact-leak",
148
+ label: "Generated artifact likely entered context",
149
+ tokens: null,
150
+ detail: `${item.value} (${item.count}x)`,
151
+ });
152
+ }
153
+ for (const item of (session.repeatedCommands || []).slice(0, 5)) {
154
+ events.push({
155
+ timestamp: session.updatedAt,
156
+ type: "repeated-command",
157
+ label: session.loopSuspicion ? "Repeated command loop possible" : "Repeated command",
158
+ tokens: null,
159
+ detail: `${item.value} (${item.count}x)`,
160
+ });
161
+ }
162
+ for (const item of (session.repeatedPathMentions || []).slice(0, 3)) {
163
+ events.push({
164
+ timestamp: session.updatedAt,
165
+ type: "repeated-file",
166
+ label: "Repeated file/path context",
167
+ tokens: null,
168
+ detail: `${item.value} (${item.count}x)`,
169
+ });
170
+ }
171
+ if (session.estimatedToolTokens >= 75000) {
172
+ events.push({
173
+ timestamp: session.updatedAt,
174
+ type: "tool-output",
175
+ label: "Large tool output",
176
+ tokens: session.estimatedToolTokens,
177
+ detail: `${formatTokenCount(session.estimatedToolTokens)} estimated tool/output tokens`,
178
+ });
179
+ }
180
+ return events
181
+ .sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0))
182
+ .slice(-20);
183
+ }
184
+
185
+ function buildClaudeCostInsights(sessions, totals) {
186
+ const highestCostSessions = sessions
187
+ .slice()
188
+ .sort((a, b) => (b.cost?.total || 0) - (a.cost?.total || 0))
189
+ .slice(0, 3)
190
+ .map((session) => ({
191
+ sessionId: session.sessionId,
192
+ updatedAt: session.updatedAt,
193
+ model: session.cost?.model || session.model,
194
+ cost: session.cost?.total || 0,
195
+ risk: session.contextRisk,
196
+ topDriver: session.prismo?.drivers?.[0] || null,
197
+ }));
198
+ const costDrivers = [
199
+ { type: "output", cost: totals.outputCost, share: percentOf(totals.outputCost, totals.totalCost) },
200
+ { type: "cache-read", cost: totals.cacheReadCost, share: percentOf(totals.cacheReadCost, totals.totalCost) },
201
+ { type: "cache-write", cost: totals.cacheWriteCost, share: percentOf(totals.cacheWriteCost, totals.totalCost) },
202
+ { type: "input", cost: totals.inputCost, share: percentOf(totals.inputCost, totals.totalCost) },
203
+ ].filter((driver) => driver.cost > 0).sort((a, b) => b.cost - a.cost);
204
+ const estimatedAvoidableCost = sessions.reduce((sum, session) => sum + (session.prismo?.estimatedAvoidableCost || 0), 0);
205
+ const recommendations = [];
206
+ if (costDrivers[0]?.type === "cache-read" || costDrivers[0]?.type === "cache-write") {
207
+ recommendations.push("Repeated context is the main spend driver; generate context packs and avoid broad repo reads.");
208
+ }
209
+ if (costDrivers[0]?.type === "output") {
210
+ recommendations.push("Output cost is leading; ask the agent for concise diffs and summaries by default.");
211
+ }
212
+ if (sessions.some((session) => session.estimatedToolTokens >= 75000)) {
213
+ recommendations.push("Tool output is bloating at least one session; keep shell output narrow and summarize logs.");
214
+ }
215
+ if (sessions.some((session) => session.turns >= 30)) {
216
+ recommendations.push("At least one session is long; split unrelated tasks into fresh sessions.");
217
+ }
218
+ recommendations.push(`${NPX_COMMAND} scan --usage`);
219
+ recommendations.push(`${NPX_COMMAND} optimize`);
220
+ return {
221
+ estimatedAvoidableCost,
222
+ estimatedAvoidableRate: totals.totalCost ? estimatedAvoidableCost / totals.totalCost : 0,
223
+ costDrivers,
224
+ highestCostSessions,
225
+ recommendations: Array.from(new Set(recommendations)).slice(0, 5),
226
+ };
227
+ }
228
+
229
+ function renderClaudeCostTerminal(summary) {
230
+ const lines = [];
231
+ const latest = summary.sessions[0] || null;
232
+ lines.push("");
233
+ lines.push("Prismo Claude Code Cost");
234
+ lines.push("");
235
+ if (!summary.sessions.length) {
236
+ lines.push(summary.scope === "all-claude-projects" ? "No Claude Code sessions found." : "No Claude Code sessions found for this repo.");
237
+ lines.push("");
238
+ lines.push("Tip: run Claude Code inside this project, then try `npx getprismo cc` again.");
239
+ return lines.join("\n");
240
+ }
241
+
242
+ if (summary.command === "list") {
243
+ lines.push(`Recent sessions: ${summary.sessions.length}`);
244
+ lines.push("");
245
+ summary.sessions.forEach((session, index) => {
246
+ lines.push(`${index + 1}. ${session.model || session.cost.model} ${session.updatedAt || "unknown date"}`);
247
+ lines.push(` ${formatTokenCount(session.exactTotalTokens || session.contextTokens)} tokens -> ${formatMoney(session.cost.total)} (${session.sessionId})`);
248
+ if (session.prismo?.drivers?.[0]) lines.push(` driver: ${session.prismo.drivers[0].message}`);
249
+ });
250
+ lines.push("");
251
+ lines.push(`Estimated avoidable spend: ${formatMoney(summary.insights.estimatedAvoidableCost)}`);
252
+ lines.push(`Next: ${summary.insights.recommendations.slice(0, 2).join(" -> ")}`);
253
+ return lines.join("\n");
254
+ }
255
+
256
+ if (summary.command === "all") {
257
+ lines.push(`Sessions: ${summary.totals.sessions}`);
258
+ lines.push("");
259
+ lines.push(`Input: ${formatTokenCount(summary.totals.inputTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.inputCost)}`);
260
+ lines.push(`Output: ${formatTokenCount(summary.totals.outputTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.outputCost)}`);
261
+ lines.push(`Cache write: ${formatTokenCount(summary.totals.cacheCreationTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.cacheWriteCost)}`);
262
+ lines.push(`Cache read: ${formatTokenCount(summary.totals.cacheReadTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.cacheReadCost)}`);
263
+ lines.push("--------------------------------------------------");
264
+ lines.push(`Total: ${formatTokenCount(summary.totals.totalTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.totalCost)}`);
265
+ if (summary.totals.cacheSavings > 0) lines.push("");
266
+ if (summary.totals.cacheSavings > 0) lines.push(`Cache saved you ${formatMoney(summary.totals.cacheSavings)} (vs no caching)`);
267
+ lines.push("");
268
+ lines.push("Prismo Diagnosis");
269
+ lines.push(`Estimated avoidable spend: ${formatMoney(summary.insights.estimatedAvoidableCost)} (${Math.round(summary.insights.estimatedAvoidableRate * 100)}%)`);
270
+ if (summary.insights.costDrivers.length) {
271
+ lines.push(`Main cost driver: ${summary.insights.costDrivers[0].type} (${summary.insights.costDrivers[0].share}%)`);
272
+ }
273
+ summary.insights.recommendations.slice(0, 3).forEach((recommendation) => lines.push(`- ${recommendation}`));
274
+ return lines.join("\n");
275
+ }
276
+
277
+ if (summary.command === "timeline") {
278
+ lines.push(`Session: ${latest.sessionId}`);
279
+ if (latest.model || latest.cost?.model) lines.push(`Model: ${latest.model || latest.cost.model}`);
280
+ lines.push(`Updated: ${latest.updatedAt || "unknown date"}`);
281
+ lines.push("");
282
+ lines.push("Timeline");
283
+ if (!latest.timeline || !latest.timeline.length) {
284
+ lines.push("- No major context spikes, repeated commands, or artifact leaks detected in this session.");
285
+ } else {
286
+ latest.timeline.forEach((event) => {
287
+ const when = event.timestamp ? new Date(event.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "unknown";
288
+ lines.push(`${when} ${event.label} ${event.detail}`);
289
+ });
290
+ }
291
+ lines.push("");
292
+ lines.push("Suggested Action");
293
+ lines.push(latest.prismo?.recommendations?.[0] || `${NPX_COMMAND} doctor`);
294
+ return lines.join("\n");
295
+ }
296
+
297
+ const sessions = summary.command === "last" ? summary.sessions : [latest];
298
+ sessions.forEach((session, index) => {
299
+ if (index > 0) lines.push("");
300
+ lines.push(`${session.cost.model} ${session.updatedAt || "unknown date"}`);
301
+ if (session.sessionId) lines.push(`Session: ${session.sessionId}`);
302
+ if (session.exactAvailable) lines.push(`Confidence: ${session.confidence}`);
303
+ else lines.push("Confidence: estimated; exact token usage was not present in the local log.");
304
+ lines.push("");
305
+ lines.push(`Input: ${formatTokenCount(session.exactInputTokens).padStart(8)} tokens -> ${formatMoney(session.cost.input)}`);
306
+ lines.push(`Output: ${formatTokenCount(session.exactOutputTokens).padStart(8)} tokens -> ${formatMoney(session.cost.output)}`);
307
+ lines.push(`Cache write: ${formatTokenCount(session.exactCacheCreationTokens).padStart(8)} tokens -> ${formatMoney(session.cost.cacheWrite)}`);
308
+ lines.push(`Cache read: ${formatTokenCount(session.exactCacheReadTokens).padStart(8)} tokens -> ${formatMoney(session.cost.cacheRead)}`);
309
+ lines.push("--------------------------------------------------");
310
+ lines.push(`Total: ${formatTokenCount(session.exactTotalTokens || session.contextTokens).padStart(8)} tokens -> ${formatMoney(session.cost.total)}`);
311
+ if (session.cost.cacheSavings > 0) lines.push("");
312
+ if (session.cost.cacheSavings > 0) lines.push(`Cache saved you ${formatMoney(session.cost.cacheSavings)} (vs no caching)`);
313
+ lines.push("");
314
+ lines.push("Prismo Diagnosis");
315
+ lines.push(`Waste score: ${session.prismo.wasteScore}/100`);
316
+ lines.push(`Estimated avoidable spend: ${formatMoney(session.prismo.estimatedAvoidableCost)} (${Math.round(session.prismo.estimatedAvoidableRate * 100)}%)`);
317
+ if (session.prismo.drivers.length) {
318
+ lines.push("Cost Drivers:");
319
+ session.prismo.drivers.slice(0, 3).forEach((driver) => lines.push(`- ${driver.message}`));
320
+ }
321
+ lines.push("Better Next Actions:");
322
+ session.prismo.recommendations.forEach((recommendation) => lines.push(`- ${recommendation}`));
323
+ });
324
+ lines.push("");
325
+ lines.push(`Next: ${NPX_COMMAND} scan --usage to connect spend back to repo token waste.`);
326
+ return lines.join("\n");
327
+ }
328
+
329
+ return {
330
+ buildClaudeCostInsights,
331
+ buildClaudeSessionDiagnosis,
332
+ buildSessionTimeline,
333
+ calculateClaudeCost,
334
+ renderClaudeCostTerminal,
335
+ };
336
+ };
@@ -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
+ };