getprismo 0.1.23 → 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.
package/README.md CHANGED
@@ -866,9 +866,13 @@ lib/prismo-dev/report.js terminal, markdown, ci reports
866
866
  lib/prismo-dev/scan.js repo scanning, scoring, readiness
867
867
  lib/prismo-dev/scan-path-utils.js scan ignore/path helper logic
868
868
  lib/prismo-dev/shield.js local command shield and searchable output index
869
+ lib/prismo-dev/usage-cost.js Claude Code cost and timeline analysis
869
870
  lib/prismo-dev/usage-log-utils.js local session log parsing helpers
870
- lib/prismo-dev/usage-watch.js local logs, watch, cost, timeline
871
+ lib/prismo-dev/usage-sessions.js local Codex/Claude session discovery
872
+ lib/prismo-dev/usage-watch.js watch orchestration, JSON payloads, live files
871
873
  lib/prismo-dev/utils.js shared terminal/file/token helpers
874
+ lib/prismo-dev/watch-live.js live context-pressure decisions
875
+ lib/prismo-dev/watch-render.js watch terminal and guardrail renderers
872
876
  ```
873
877
 
874
878
  ---
@@ -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,298 @@
1
+ module.exports = function createUsageSessions(deps) {
2
+ const {
3
+ fs,
4
+ os,
5
+ path,
6
+ GENERATED_ARTIFACT_PATTERNS,
7
+ calculateClaudeCost,
8
+ estimateTokens,
9
+ readIfText,
10
+ } = deps;
11
+
12
+ const {
13
+ addUsage,
14
+ collectText,
15
+ extractCommandCandidates,
16
+ extractMentionedPaths,
17
+ incrementMap,
18
+ isGeneratedArtifactPath,
19
+ listFilesRecursive,
20
+ parseJsonl,
21
+ topCountEntries,
22
+ totalUsageTokens,
23
+ } = require("./usage-log-utils")({
24
+ fs,
25
+ path,
26
+ GENERATED_ARTIFACT_PATTERNS,
27
+ readIfText,
28
+ });
29
+
30
+ function getSessionRisk(tokens, toolTokens) {
31
+ if (tokens >= 200000 || toolTokens >= 75000) return "High";
32
+ if (tokens >= 60000 || toolTokens >= 20000) return "Medium";
33
+ return "Low";
34
+ }
35
+ function analyzeSessionFile(filePath, tool) {
36
+ const rows = parseJsonl(filePath);
37
+ const stat = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
38
+ const session = {
39
+ tool,
40
+ filePath,
41
+ sessionId: path.basename(filePath).replace(/\.jsonl$/, ""),
42
+ title: "",
43
+ cwd: "",
44
+ model: "",
45
+ startedAt: null,
46
+ updatedAt: stat ? new Date(stat.mtimeMs).toISOString() : null,
47
+ turns: 0,
48
+ userMessages: 0,
49
+ assistantMessages: 0,
50
+ toolCalls: 0,
51
+ toolResults: 0,
52
+ estimatedInputTokens: 0,
53
+ estimatedOutputTokens: 0,
54
+ estimatedToolTokens: 0,
55
+ inputTokens: 0,
56
+ outputTokens: 0,
57
+ cacheReadTokens: 0,
58
+ cacheCreationTokens: 0,
59
+ exactInputTokens: 0,
60
+ exactOutputTokens: 0,
61
+ exactCacheReadTokens: 0,
62
+ exactCacheCreationTokens: 0,
63
+ exactTotalTokens: 0,
64
+ exactAvailable: false,
65
+ confidence: "estimated",
66
+ largestTextBlobs: [],
67
+ toolNames: {},
68
+ pathMentions: {},
69
+ generatedArtifactMentions: {},
70
+ commandMentions: {},
71
+ failureMentions: 0,
72
+ eventTokenDeltas: [],
73
+ exactTokenTimeline: [],
74
+ };
75
+ const seenUsage = new Set();
76
+ let codexCumulative = null;
77
+
78
+ for (const row of rows) {
79
+ const timestamp = row.timestamp || row.payload?.started_at || row.message?.timestamp;
80
+ if (timestamp && !session.startedAt) session.startedAt = timestamp;
81
+ if (timestamp) session.updatedAt = timestamp;
82
+ if (row.cwd && !session.cwd) session.cwd = row.cwd;
83
+
84
+ const meta = row.payload?.type === "session_meta" ? row.payload : row.type === "session_meta" ? row.payload : null;
85
+ if (meta) {
86
+ session.sessionId = meta.id || session.sessionId;
87
+ session.cwd = meta.cwd || session.cwd;
88
+ session.model = meta.model || meta.model_slug || session.model;
89
+ }
90
+ if (row.payload?.type === "token_count" && row.payload?.info?.total_token_usage) {
91
+ codexCumulative = row.payload.info.total_token_usage;
92
+ session.exactTokenTimeline.push({
93
+ total: Number(codexCumulative.total_tokens || 0),
94
+ timestamp: timestamp || null,
95
+ });
96
+ }
97
+ if (row.type === "event_msg" && row.payload?.type === "token_count" && row.payload?.info?.total_token_usage) {
98
+ codexCumulative = row.payload.info.total_token_usage;
99
+ session.exactTokenTimeline.push({
100
+ total: Number(codexCumulative.total_tokens || 0),
101
+ timestamp: timestamp || null,
102
+ });
103
+ }
104
+ if (row.type === "ai-title" && row.aiTitle) session.title = row.aiTitle;
105
+
106
+ const msg = row.message || row.payload;
107
+ if (msg?.model && !session.model) session.model = msg.model;
108
+ const role = msg?.role || row.payload?.role;
109
+ const text = collectText(msg);
110
+ const tokens = estimateTokens(text);
111
+ if (tokens > 0) {
112
+ session.largestTextBlobs.push({
113
+ label: row.type || row.payload?.type || "event",
114
+ tokens,
115
+ });
116
+ session.eventTokenDeltas.push({
117
+ label: row.type || row.payload?.type || "event",
118
+ tokens,
119
+ timestamp: timestamp || null,
120
+ });
121
+ }
122
+ for (const mentionedPath of extractMentionedPaths(text, session.cwd)) {
123
+ incrementMap(session.pathMentions, mentionedPath);
124
+ if (isGeneratedArtifactPath(mentionedPath)) incrementMap(session.generatedArtifactMentions, mentionedPath);
125
+ }
126
+ for (const command of extractCommandCandidates(row, text)) {
127
+ incrementMap(session.commandMentions, command);
128
+ }
129
+ if (/\b(error|failed|failure|traceback|exception|exit code|non-zero|tests? failed)\b/i.test(text)) {
130
+ session.failureMentions += 1;
131
+ }
132
+ if (role === "user" || row.type === "user" || row.payload?.role === "user") {
133
+ session.userMessages += 1;
134
+ session.estimatedInputTokens += tokens;
135
+ } else if (role === "assistant" || row.type === "assistant" || row.payload?.role === "assistant") {
136
+ session.assistantMessages += 1;
137
+ session.estimatedOutputTokens += tokens;
138
+ }
139
+
140
+ const rowText = JSON.stringify(row);
141
+ const toolUseMatches = rowText.match(/"tool_use"|function_call|"name":"([^"]+)"/g) || [];
142
+ const toolResultMatches = rowText.match(/"tool_result"|function_call_output/g) || [];
143
+ if (toolUseMatches.length) session.toolCalls += toolUseMatches.length;
144
+ if (toolResultMatches.length) {
145
+ session.toolResults += toolResultMatches.length;
146
+ session.estimatedToolTokens += tokens;
147
+ }
148
+ const toolName = row.message?.content?.find?.((item) => item && item.type === "tool_use")?.name || row.payload?.name;
149
+ if (toolName) session.toolNames[toolName] = (session.toolNames[toolName] || 0) + 1;
150
+
151
+ const usage = row.message?.usage || row.payload?.usage;
152
+ if (usage) {
153
+ const key = `${row.requestId || ""}:${row.message?.id || ""}:${totalUsageTokens(usage)}`;
154
+ if (!seenUsage.has(key)) {
155
+ seenUsage.add(key);
156
+ addUsage(session, usage);
157
+ }
158
+ }
159
+ }
160
+
161
+ if (codexCumulative) {
162
+ session.exactInputTokens = Number(codexCumulative.input_tokens || 0);
163
+ session.exactOutputTokens = Number(codexCumulative.output_tokens || 0);
164
+ session.exactCacheReadTokens = Number(codexCumulative.cached_input_tokens || 0);
165
+ session.exactTotalTokens = Number(codexCumulative.total_tokens || 0);
166
+ session.exactAvailable = session.exactTotalTokens > 0;
167
+ } else {
168
+ session.exactInputTokens = session.inputTokens || 0;
169
+ session.exactOutputTokens = session.outputTokens || 0;
170
+ session.exactCacheReadTokens = session.cacheReadTokens || 0;
171
+ session.exactCacheCreationTokens = session.cacheCreationTokens || 0;
172
+ session.exactTotalTokens =
173
+ session.exactInputTokens + session.exactOutputTokens + session.exactCacheReadTokens + session.exactCacheCreationTokens;
174
+ session.exactAvailable = session.exactTotalTokens > 0;
175
+ }
176
+
177
+ session.turns = Math.max(session.userMessages, session.assistantMessages);
178
+ session.estimatedTotalTokens = session.estimatedInputTokens + session.estimatedOutputTokens + session.estimatedToolTokens;
179
+ session.exactActiveTokens = session.exactAvailable
180
+ ? Math.max(session.exactInputTokens - session.exactCacheReadTokens, 0) + session.exactOutputTokens + (session.exactCacheCreationTokens || 0)
181
+ : 0;
182
+ session.contextTokens = session.exactAvailable ? session.exactTotalTokens : session.estimatedTotalTokens;
183
+ session.displayTokens = session.exactAvailable ? session.exactActiveTokens : session.estimatedTotalTokens;
184
+ session.confidence = session.exactAvailable ? "exact-local-log" : "estimated-local-log";
185
+ session.contextRisk = getSessionRisk(session.displayTokens, session.estimatedToolTokens);
186
+ if (session.exactTokenTimeline.length >= 2) {
187
+ const last = session.exactTokenTimeline[session.exactTokenTimeline.length - 1];
188
+ const prev = session.exactTokenTimeline[session.exactTokenTimeline.length - 2];
189
+ session.recentContextGrowth = Math.max(0, (last.total || 0) - (prev.total || 0));
190
+ } else {
191
+ session.recentContextGrowth = session.eventTokenDeltas.slice(-3).reduce((sum, item) => sum + (item.tokens || 0), 0);
192
+ }
193
+ session.repeatedPathMentions = topCountEntries(session.pathMentions, 5, 4);
194
+ session.generatedArtifacts = topCountEntries(session.generatedArtifactMentions, 5, 1);
195
+ session.repeatedCommands = topCountEntries(session.commandMentions, 5, 3);
196
+ session.loopSuspicion = session.repeatedCommands.length > 0 && (session.failureMentions >= 2 || session.toolResults >= 4 || session.turns >= 12);
197
+ session.loopConfidence = !session.loopSuspicion
198
+ ? "low"
199
+ : session.failureMentions >= 2 && session.repeatedCommands[0]?.count >= 5
200
+ ? "high"
201
+ : "medium";
202
+ session.cost = tool === "claude-code"
203
+ ? calculateClaudeCost({
204
+ inputTokens: session.exactInputTokens,
205
+ outputTokens: session.exactOutputTokens,
206
+ cacheCreationTokens: session.exactCacheCreationTokens,
207
+ cacheReadTokens: session.exactCacheReadTokens,
208
+ }, session.model)
209
+ : null;
210
+ session.largestTextBlobs = session.largestTextBlobs.sort((a, b) => b.tokens - a.tokens).slice(0, 5);
211
+ return session;
212
+ }
213
+ function getCodexSessionFiles() {
214
+ const codexHome = process.env.PRISMO_CODEX_HOME || path.join(os.homedir(), ".codex");
215
+ return listFilesRecursive(path.join(codexHome, "sessions"), (file) => file.endsWith(".jsonl"), 200);
216
+ }
217
+ function getClaudeSessionFiles(cwd = process.cwd()) {
218
+ const claudeHome = process.env.PRISMO_CLAUDE_HOME || path.join(os.homedir(), ".claude");
219
+ const candidates = [cwd];
220
+ try {
221
+ candidates.push(fs.realpathSync(cwd));
222
+ } catch {
223
+ // Keep the original cwd candidate when realpath is unavailable.
224
+ }
225
+ const files = [];
226
+ for (const candidate of Array.from(new Set(candidates))) {
227
+ const safeProject = candidate.replace(/[\/\\:]/g, "-").replace(/^-/, "-");
228
+ const projectDir = path.join(claudeHome, "projects", safeProject);
229
+ files.push(...listFilesRecursive(projectDir, (file) => file.endsWith(".jsonl"), 200));
230
+ }
231
+ return Array.from(new Set(files));
232
+ }
233
+ function getAllClaudeSessionFiles() {
234
+ const claudeHome = process.env.PRISMO_CLAUDE_HOME || path.join(os.homedir(), ".claude");
235
+ return listFilesRecursive(path.join(claudeHome, "projects"), (file) => file.endsWith(".jsonl"), 1000);
236
+ }
237
+ function sameResolvedPath(a, b) {
238
+ if (!a || !b) return false;
239
+ try {
240
+ const resolvedA = fs.existsSync(a) ? fs.realpathSync(a) : path.resolve(a);
241
+ const resolvedB = fs.existsSync(b) ? fs.realpathSync(b) : path.resolve(b);
242
+ return resolvedA === resolvedB;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+ function getUsageSummary(options = {}) {
248
+ const tool = options.tool || "all";
249
+ const limit = options.limit || 5;
250
+ const cwd = options.cwd || process.cwd();
251
+ const sessions = [];
252
+ if (tool === "all" || tool === "codex") {
253
+ for (const file of getCodexSessionFiles().slice(0, Math.max(limit * 8, 20))) {
254
+ const session = analyzeSessionFile(file, "codex");
255
+ if (!session.cwd || sameResolvedPath(session.cwd, cwd)) sessions.push(session);
256
+ if (sessions.filter((item) => item.tool === "codex").length >= limit) break;
257
+ }
258
+ }
259
+ if (tool === "all" || tool === "claude") {
260
+ for (const file of getClaudeSessionFiles(cwd).slice(0, limit)) {
261
+ sessions.push(analyzeSessionFile(file, "claude-code"));
262
+ }
263
+ }
264
+ sessions.sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0));
265
+ const selected = sessions.slice(0, limit);
266
+ const totals = selected.reduce(
267
+ (acc, session) => {
268
+ acc.displayTokens += session.displayTokens || 0;
269
+ acc.contextTokens += session.contextTokens || 0;
270
+ acc.estimatedTokens += session.estimatedTotalTokens || 0;
271
+ acc.exactTokens += session.exactAvailable ? session.exactTotalTokens : 0;
272
+ acc.toolTokens += session.estimatedToolTokens || 0;
273
+ acc.sessions += 1;
274
+ return acc;
275
+ },
276
+ { sessions: 0, displayTokens: 0, contextTokens: 0, estimatedTokens: 0, exactTokens: 0, toolTokens: 0 }
277
+ );
278
+ const sources = Array.from(new Set(selected.map((session) => session.tool).filter(Boolean)));
279
+ return {
280
+ generatedAt: new Date().toISOString(),
281
+ scannedPath: cwd,
282
+ tool,
283
+ tokenBudget: options.tokenBudget || null,
284
+ confidence: selected.every((session) => session.exactAvailable) && selected.length ? "exact-local-log" : "mixed-or-estimated",
285
+ totals,
286
+ sources,
287
+ sessions: selected,
288
+ };
289
+ }
290
+
291
+ return {
292
+ analyzeSessionFile,
293
+ getAllClaudeSessionFiles,
294
+ getClaudeSessionFiles,
295
+ getCodexSessionFiles,
296
+ getUsageSummary,
297
+ };
298
+ };