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 +5 -1
- package/lib/prismo-dev/usage-cost.js +336 -0
- package/lib/prismo-dev/usage-sessions.js +298 -0
- package/lib/prismo-dev/usage-watch.js +59 -1198
- package/lib/prismo-dev/watch-live.js +353 -0
- package/lib/prismo-dev/watch-render.js +290 -0
- package/package.json +3 -3
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-
|
|
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
|
+
};
|