getprismo 0.1.4 → 0.1.6

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,1852 @@
1
+ module.exports = function createUsageWatch(deps) {
2
+ const {
3
+ fs,
4
+ os,
5
+ path,
6
+ NPX_COMMAND,
7
+ CLAUDE_PRICING,
8
+ DEFAULT_CLAUDE_PRICING_KEY,
9
+ GENERATED_ARTIFACT_PATTERNS,
10
+ readIfText,
11
+ estimateTokens,
12
+ color,
13
+ writeGeneratedFile,
14
+ } = deps;
15
+
16
+ function listFilesRecursive(root, predicate = () => true, limit = 300) {
17
+ const files = [];
18
+ if (!fs.existsSync(root)) return files;
19
+ const stack = [root];
20
+ while (stack.length && files.length < limit) {
21
+ const current = stack.pop();
22
+ let entries;
23
+ try {
24
+ entries = fs.readdirSync(current, { withFileTypes: true });
25
+ } catch {
26
+ continue;
27
+ }
28
+ for (const entry of entries) {
29
+ const fullPath = path.join(current, entry.name);
30
+ if (entry.isDirectory()) {
31
+ stack.push(fullPath);
32
+ } else if (entry.isFile() && predicate(fullPath)) {
33
+ files.push(fullPath);
34
+ }
35
+ }
36
+ }
37
+ return files.sort((a, b) => {
38
+ try {
39
+ return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
40
+ } catch {
41
+ return 0;
42
+ }
43
+ });
44
+ }
45
+
46
+ function parseJsonl(filePath, maxLines = 20000) {
47
+ const text = readIfText(filePath, 30 * 1024 * 1024);
48
+ if (!text) return [];
49
+ const rows = [];
50
+ const lines = text.split(/\r?\n/).filter(Boolean);
51
+ for (const line of lines.slice(Math.max(0, lines.length - maxLines))) {
52
+ try {
53
+ rows.push(JSON.parse(line));
54
+ } catch {
55
+ // Local tool logs can contain partial writes while a session is active.
56
+ }
57
+ }
58
+ return rows;
59
+ }
60
+
61
+ function collectText(value, options = {}, depth = 0) {
62
+ if (value == null || depth > 8) return "";
63
+ if (typeof value === "string") return value;
64
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
65
+ if (Array.isArray(value)) return value.map((item) => collectText(item, options, depth + 1)).join("\n");
66
+ if (typeof value !== "object") return "";
67
+
68
+ const skipKeys = new Set(["signature", "encrypted_content", "image_url", "data", "auth", "api_key", "token"]);
69
+ const parts = [];
70
+ for (const [key, child] of Object.entries(value)) {
71
+ if (skipKeys.has(key)) continue;
72
+ parts.push(collectText(child, options, depth + 1));
73
+ }
74
+ return parts.filter(Boolean).join("\n");
75
+ }
76
+
77
+ function addUsage(target, usage) {
78
+ if (!usage || typeof usage !== "object") return;
79
+ target.inputTokens += Number(usage.input_tokens || usage.prompt_tokens || 0);
80
+ target.outputTokens += Number(usage.output_tokens || usage.completion_tokens || 0);
81
+ target.cacheReadTokens += Number(usage.cache_read_input_tokens || 0);
82
+ target.cacheCreationTokens += Number(usage.cache_creation_input_tokens || 0);
83
+ }
84
+
85
+ function totalUsageTokens(usage) {
86
+ if (!usage) return 0;
87
+ return (
88
+ Number(usage.input_tokens || usage.prompt_tokens || 0) +
89
+ Number(usage.output_tokens || usage.completion_tokens || 0) +
90
+ Number(usage.cache_read_input_tokens || 0) +
91
+ Number(usage.cache_creation_input_tokens || 0)
92
+ );
93
+ }
94
+
95
+ function inferClaudePricingKey(model) {
96
+ const normalized = String(model || "").toLowerCase();
97
+ if (normalized.includes("opus") && normalized.includes("4-1")) return "opus-4.1";
98
+ if (normalized.includes("opus") && normalized.includes("4.1")) return "opus-4.1";
99
+ if (normalized.includes("opus") && normalized.includes("4")) return "opus-4";
100
+ if (normalized.includes("sonnet") && normalized.includes("4")) return "sonnet-4";
101
+ if (normalized.includes("sonnet") && (normalized.includes("3-7") || normalized.includes("3.7"))) return "sonnet-3.7";
102
+ if (normalized.includes("sonnet") && (normalized.includes("3-5") || normalized.includes("3.5"))) return "sonnet-3.5";
103
+ if (normalized.includes("haiku") && (normalized.includes("3-5") || normalized.includes("3.5"))) return "haiku-3.5";
104
+ if (normalized.includes("haiku") && normalized.includes("3")) return "haiku-3";
105
+ if (normalized.includes("opus") && normalized.includes("3")) return "opus-3";
106
+ return DEFAULT_CLAUDE_PRICING_KEY;
107
+ }
108
+
109
+ function calculateClaudeCost(tokens, model) {
110
+ const pricingKey = inferClaudePricingKey(model);
111
+ const pricing = CLAUDE_PRICING[pricingKey] || CLAUDE_PRICING[DEFAULT_CLAUDE_PRICING_KEY];
112
+ const inputTokens = Number(tokens.inputTokens || 0);
113
+ const outputTokens = Number(tokens.outputTokens || 0);
114
+ const cacheWriteTokens = Number(tokens.cacheCreationTokens || tokens.cacheWriteTokens || 0);
115
+ const cacheReadTokens = Number(tokens.cacheReadTokens || 0);
116
+ const input = (inputTokens / 1000000) * pricing.input;
117
+ const output = (outputTokens / 1000000) * pricing.output;
118
+ const cacheWrite = (cacheWriteTokens / 1000000) * pricing.cacheWrite;
119
+ const cacheRead = (cacheReadTokens / 1000000) * pricing.cacheRead;
120
+ const total = input + output + cacheWrite + cacheRead;
121
+ const noCache = ((inputTokens + cacheWriteTokens + cacheReadTokens) / 1000000) * pricing.input + output;
122
+ return {
123
+ model: pricing.name,
124
+ pricingKey,
125
+ pricing,
126
+ input,
127
+ output,
128
+ cacheWrite,
129
+ cacheRead,
130
+ total,
131
+ noCache,
132
+ cacheSavings: Math.max(noCache - total, 0),
133
+ };
134
+ }
135
+
136
+ function getSessionRisk(tokens, toolTokens) {
137
+ if (tokens >= 200000 || toolTokens >= 75000) return "High";
138
+ if (tokens >= 60000 || toolTokens >= 20000) return "Medium";
139
+ return "Low";
140
+ }
141
+
142
+ function incrementMap(map, key, amount = 1) {
143
+ if (!key) return;
144
+ map[key] = (map[key] || 0) + amount;
145
+ }
146
+
147
+ function normalizeMentionedPath(value, cwd = "") {
148
+ let normalized = String(value || "")
149
+ .replace(/\\/g, "/")
150
+ .replace(/^[`'"]+|[`'",:;)\]}]+$/g, "")
151
+ .trim();
152
+ normalized = normalized.replace(/^[ MADRCU?!]{1,4}\s+(?=\/|Users\/|home\/)/, "");
153
+ const normalizedCwd = String(cwd || "").replace(/\\/g, "/");
154
+ const wasAbsolute = normalized.startsWith("/");
155
+ if (wasAbsolute && normalizedCwd && !normalized.startsWith(`${normalizedCwd}/`) && normalized !== normalizedCwd) {
156
+ return "";
157
+ }
158
+ if (normalizedCwd && normalized.startsWith(normalizedCwd)) {
159
+ normalized = normalized.slice(normalizedCwd.length);
160
+ }
161
+ normalized = normalized.replace(/^\.?\//, "");
162
+ if (normalizedCwd) {
163
+ const repoName = path.basename(normalizedCwd);
164
+ const repoIndex = normalized.indexOf(`${repoName}/`);
165
+ if (repoIndex >= 0) normalized = normalized.slice(repoIndex + repoName.length + 1);
166
+ }
167
+ return normalized;
168
+ }
169
+
170
+ function isGeneratedArtifactPath(relPath) {
171
+ const normalized = normalizeMentionedPath(relPath);
172
+ return GENERATED_ARTIFACT_PATTERNS.some((pattern) => pattern.test(normalized));
173
+ }
174
+
175
+ function looksLikeUsefulPath(relPath) {
176
+ const normalized = normalizeMentionedPath(relPath);
177
+ if (!normalized || normalized.startsWith("http") || normalized.includes("://")) return false;
178
+ if (normalized.length < 3 || normalized.split("/").some((part) => !part || part.length > 120)) return false;
179
+ if (/^(Users|home|var|tmp|private|Volumes)\//i.test(normalized)) return false;
180
+ if (/^(Users|home|var|tmp|Downloads|Code|Projects)$/i.test(normalized)) return false;
181
+ if (isGeneratedArtifactPath(normalized)) return true;
182
+ if (/\.[A-Za-z0-9]{1,12}$/.test(normalized)) return true;
183
+ return /(^|\/)(src|app|lib|backend|frontend|tests|docs|scripts|components|pages|routes|api)\//.test(normalized);
184
+ }
185
+
186
+ function extractMentionedPaths(text, cwd = "") {
187
+ const found = new Set();
188
+ const source = String(text || "");
189
+ const pathPattern = /(?:^|[\s"'`])((?:\.{0,2}\/)?(?:[\w .@-]+\/)+[\w .@+-]+\.[A-Za-z0-9]{1,12})/g;
190
+ const filePattern = /(?:^|[\s"'`])((?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|coverage-final\.json|tsconfig\.json|pyproject\.toml|requirements\.txt|README\.md|CLAUDE\.md|AGENTS\.md))/g;
191
+ for (const pattern of [pathPattern, filePattern]) {
192
+ let match;
193
+ while ((match = pattern.exec(source))) {
194
+ const rel = normalizeMentionedPath(match[1], cwd);
195
+ if (!looksLikeUsefulPath(rel)) continue;
196
+ if (cwd && !isGeneratedArtifactPath(rel) && !fs.existsSync(path.join(cwd, rel))) continue;
197
+ found.add(rel);
198
+ }
199
+ }
200
+ return Array.from(found);
201
+ }
202
+
203
+ function normalizeCommand(value) {
204
+ return String(value || "")
205
+ .replace(/\s+/g, " ")
206
+ .replace(/[;|&]+$/g, "")
207
+ .trim()
208
+ .slice(0, 160);
209
+ }
210
+
211
+ function isShellCommand(value) {
212
+ return /^(npm|pnpm|yarn|bun|pytest|python3?|node|npx|uv|ruff|cargo|go|make|git|cd|rm|cp|mv|sed|rg|grep|find|cat)\b/.test(String(value || "").trim());
213
+ }
214
+
215
+ function extractCommandCandidates(row, text) {
216
+ const commands = [];
217
+ const directInputs = [
218
+ row.payload?.input,
219
+ row.payload?.arguments,
220
+ row.message?.input,
221
+ row.message?.arguments,
222
+ ];
223
+ for (const input of directInputs) {
224
+ if (typeof input === "string") commands.push(input);
225
+ else if (input && typeof input === "object") {
226
+ for (const value of Object.values(input)) {
227
+ if (typeof value === "string") commands.push(value);
228
+ }
229
+ }
230
+ }
231
+ const toolItems = Array.isArray(row.message?.content) ? row.message.content : [];
232
+ for (const item of toolItems) {
233
+ if (!item || typeof item !== "object") continue;
234
+ if (typeof item.input === "string") commands.push(item.input);
235
+ if (item.input && typeof item.input === "object") {
236
+ for (const value of Object.values(item.input)) {
237
+ if (typeof value === "string") commands.push(value);
238
+ }
239
+ }
240
+ }
241
+ if (/tool_use|function_call/i.test(row.type || row.payload?.type || "")) {
242
+ const commandPattern = /\b(?:npm|pnpm|yarn|bun|pytest|python3?|node|npx|uv|ruff|cargo|go test|make|git)\b[^\n\r"`']{0,140}/g;
243
+ for (const match of String(text || "").matchAll(commandPattern)) {
244
+ commands.push(match[0]);
245
+ }
246
+ }
247
+ return Array.from(new Set(commands.map(normalizeCommand).filter((cmd) => cmd.length >= 3 && /\s/.test(cmd) && isShellCommand(cmd))));
248
+ }
249
+
250
+ function topCountEntries(map, limit = 5, minCount = 2) {
251
+ return Object.entries(map || {})
252
+ .filter(([, count]) => count >= minCount)
253
+ .sort((a, b) => b[1] - a[1])
254
+ .slice(0, limit)
255
+ .map(([value, count]) => ({ value, count }));
256
+ }
257
+
258
+ function isExpectedRepeatedPath(value) {
259
+ const normalized = normalizeMentionedPath(value).toLowerCase();
260
+ return ["claude.md", "agents.md", "readme.md"].includes(normalized) || normalized.endsWith("/readme.md");
261
+ }
262
+
263
+ function getActionableRepeatedPaths(session, limit = 3) {
264
+ return (session.repeatedPathMentions || [])
265
+ .filter((item) => !isExpectedRepeatedPath(item.value))
266
+ .filter((item) => !isGeneratedArtifactPath(item.value))
267
+ .slice(0, limit);
268
+ }
269
+
270
+ function summarizeGeneratedArtifacts(items = [], limit = 4) {
271
+ const groups = new Map();
272
+ for (const item of items) {
273
+ const value = normalizeMentionedPath(item.value);
274
+ let key = "generated files";
275
+ if (value.includes("__pycache__/") || value.endsWith(".pyc")) key = "__pycache__";
276
+ else if (value.includes("node_modules/")) key = "node_modules";
277
+ else if (/package-lock\.json|pnpm-lock\.yaml|yarn\.lock$/i.test(value)) key = "lockfiles";
278
+ else if (value.includes("/dist/") || value.startsWith("dist/")) key = "dist";
279
+ else if (value.includes("/build/") || value.startsWith("build/")) key = "build";
280
+ else if (value.includes("/coverage/") || value.startsWith("coverage/")) key = "coverage";
281
+ else if (/(^|\/)assets\/[^/]+-[A-Za-z0-9_-]{6,}\.(js|css|map)$/i.test(value)) key = "hashed assets";
282
+ const current = groups.get(key) || { type: key, count: 0, examples: [] };
283
+ current.count += Number(item.count || 1);
284
+ if (current.examples.length < 2) current.examples.push(value);
285
+ groups.set(key, current);
286
+ }
287
+ return Array.from(groups.values()).sort((a, b) => b.count - a.count).slice(0, limit);
288
+ }
289
+
290
+ function analyzeSessionFile(filePath, tool) {
291
+ const rows = parseJsonl(filePath);
292
+ const stat = fs.existsSync(filePath) ? fs.statSync(filePath) : null;
293
+ const session = {
294
+ tool,
295
+ filePath,
296
+ sessionId: path.basename(filePath).replace(/\.jsonl$/, ""),
297
+ title: "",
298
+ cwd: "",
299
+ model: "",
300
+ startedAt: null,
301
+ updatedAt: stat ? new Date(stat.mtimeMs).toISOString() : null,
302
+ turns: 0,
303
+ userMessages: 0,
304
+ assistantMessages: 0,
305
+ toolCalls: 0,
306
+ toolResults: 0,
307
+ estimatedInputTokens: 0,
308
+ estimatedOutputTokens: 0,
309
+ estimatedToolTokens: 0,
310
+ inputTokens: 0,
311
+ outputTokens: 0,
312
+ cacheReadTokens: 0,
313
+ cacheCreationTokens: 0,
314
+ exactInputTokens: 0,
315
+ exactOutputTokens: 0,
316
+ exactCacheReadTokens: 0,
317
+ exactCacheCreationTokens: 0,
318
+ exactTotalTokens: 0,
319
+ exactAvailable: false,
320
+ confidence: "estimated",
321
+ largestTextBlobs: [],
322
+ toolNames: {},
323
+ pathMentions: {},
324
+ generatedArtifactMentions: {},
325
+ commandMentions: {},
326
+ failureMentions: 0,
327
+ eventTokenDeltas: [],
328
+ exactTokenTimeline: [],
329
+ };
330
+ const seenUsage = new Set();
331
+ let codexCumulative = null;
332
+
333
+ for (const row of rows) {
334
+ const timestamp = row.timestamp || row.payload?.started_at || row.message?.timestamp;
335
+ if (timestamp && !session.startedAt) session.startedAt = timestamp;
336
+ if (timestamp) session.updatedAt = timestamp;
337
+ if (row.cwd && !session.cwd) session.cwd = row.cwd;
338
+
339
+ const meta = row.payload?.type === "session_meta" ? row.payload : row.type === "session_meta" ? row.payload : null;
340
+ if (meta) {
341
+ session.sessionId = meta.id || session.sessionId;
342
+ session.cwd = meta.cwd || session.cwd;
343
+ session.model = meta.model || meta.model_slug || session.model;
344
+ }
345
+ if (row.payload?.type === "token_count" && row.payload?.info?.total_token_usage) {
346
+ codexCumulative = row.payload.info.total_token_usage;
347
+ session.exactTokenTimeline.push({
348
+ total: Number(codexCumulative.total_tokens || 0),
349
+ timestamp: timestamp || null,
350
+ });
351
+ }
352
+ if (row.type === "event_msg" && row.payload?.type === "token_count" && row.payload?.info?.total_token_usage) {
353
+ codexCumulative = row.payload.info.total_token_usage;
354
+ session.exactTokenTimeline.push({
355
+ total: Number(codexCumulative.total_tokens || 0),
356
+ timestamp: timestamp || null,
357
+ });
358
+ }
359
+ if (row.type === "ai-title" && row.aiTitle) session.title = row.aiTitle;
360
+
361
+ const msg = row.message || row.payload;
362
+ if (msg?.model && !session.model) session.model = msg.model;
363
+ const role = msg?.role || row.payload?.role;
364
+ const text = collectText(msg);
365
+ const tokens = estimateTokens(text);
366
+ if (tokens > 0) {
367
+ session.largestTextBlobs.push({
368
+ label: row.type || row.payload?.type || "event",
369
+ tokens,
370
+ });
371
+ session.eventTokenDeltas.push({
372
+ label: row.type || row.payload?.type || "event",
373
+ tokens,
374
+ timestamp: timestamp || null,
375
+ });
376
+ }
377
+ for (const mentionedPath of extractMentionedPaths(text, session.cwd)) {
378
+ incrementMap(session.pathMentions, mentionedPath);
379
+ if (isGeneratedArtifactPath(mentionedPath)) incrementMap(session.generatedArtifactMentions, mentionedPath);
380
+ }
381
+ for (const command of extractCommandCandidates(row, text)) {
382
+ incrementMap(session.commandMentions, command);
383
+ }
384
+ if (/\b(error|failed|failure|traceback|exception|exit code|non-zero|tests? failed)\b/i.test(text)) {
385
+ session.failureMentions += 1;
386
+ }
387
+ if (role === "user" || row.type === "user" || row.payload?.role === "user") {
388
+ session.userMessages += 1;
389
+ session.estimatedInputTokens += tokens;
390
+ } else if (role === "assistant" || row.type === "assistant" || row.payload?.role === "assistant") {
391
+ session.assistantMessages += 1;
392
+ session.estimatedOutputTokens += tokens;
393
+ }
394
+
395
+ const rowText = JSON.stringify(row);
396
+ const toolUseMatches = rowText.match(/"tool_use"|function_call|"name":"([^"]+)"/g) || [];
397
+ const toolResultMatches = rowText.match(/"tool_result"|function_call_output/g) || [];
398
+ if (toolUseMatches.length) session.toolCalls += toolUseMatches.length;
399
+ if (toolResultMatches.length) {
400
+ session.toolResults += toolResultMatches.length;
401
+ session.estimatedToolTokens += tokens;
402
+ }
403
+ const toolName = row.message?.content?.find?.((item) => item && item.type === "tool_use")?.name || row.payload?.name;
404
+ if (toolName) session.toolNames[toolName] = (session.toolNames[toolName] || 0) + 1;
405
+
406
+ const usage = row.message?.usage || row.payload?.usage;
407
+ if (usage) {
408
+ const key = `${row.requestId || ""}:${row.message?.id || ""}:${totalUsageTokens(usage)}`;
409
+ if (!seenUsage.has(key)) {
410
+ seenUsage.add(key);
411
+ addUsage(session, usage);
412
+ }
413
+ }
414
+ }
415
+
416
+ if (codexCumulative) {
417
+ session.exactInputTokens = Number(codexCumulative.input_tokens || 0);
418
+ session.exactOutputTokens = Number(codexCumulative.output_tokens || 0);
419
+ session.exactCacheReadTokens = Number(codexCumulative.cached_input_tokens || 0);
420
+ session.exactTotalTokens = Number(codexCumulative.total_tokens || 0);
421
+ session.exactAvailable = session.exactTotalTokens > 0;
422
+ } else {
423
+ session.exactInputTokens = session.inputTokens || 0;
424
+ session.exactOutputTokens = session.outputTokens || 0;
425
+ session.exactCacheReadTokens = session.cacheReadTokens || 0;
426
+ session.exactCacheCreationTokens = session.cacheCreationTokens || 0;
427
+ session.exactTotalTokens =
428
+ session.exactInputTokens + session.exactOutputTokens + session.exactCacheReadTokens + session.exactCacheCreationTokens;
429
+ session.exactAvailable = session.exactTotalTokens > 0;
430
+ }
431
+
432
+ session.turns = Math.max(session.userMessages, session.assistantMessages);
433
+ session.estimatedTotalTokens = session.estimatedInputTokens + session.estimatedOutputTokens + session.estimatedToolTokens;
434
+ session.exactActiveTokens = session.exactAvailable
435
+ ? Math.max(session.exactInputTokens - session.exactCacheReadTokens, 0) + session.exactOutputTokens + (session.exactCacheCreationTokens || 0)
436
+ : 0;
437
+ session.contextTokens = session.exactAvailable ? session.exactTotalTokens : session.estimatedTotalTokens;
438
+ session.displayTokens = session.exactAvailable ? session.exactActiveTokens : session.estimatedTotalTokens;
439
+ session.confidence = session.exactAvailable ? "exact-local-log" : "estimated-local-log";
440
+ session.contextRisk = getSessionRisk(session.displayTokens, session.estimatedToolTokens);
441
+ if (session.exactTokenTimeline.length >= 2) {
442
+ const last = session.exactTokenTimeline[session.exactTokenTimeline.length - 1];
443
+ const prev = session.exactTokenTimeline[session.exactTokenTimeline.length - 2];
444
+ session.recentContextGrowth = Math.max(0, (last.total || 0) - (prev.total || 0));
445
+ } else {
446
+ session.recentContextGrowth = session.eventTokenDeltas.slice(-3).reduce((sum, item) => sum + (item.tokens || 0), 0);
447
+ }
448
+ session.repeatedPathMentions = topCountEntries(session.pathMentions, 5, 4);
449
+ session.generatedArtifacts = topCountEntries(session.generatedArtifactMentions, 5, 1);
450
+ session.repeatedCommands = topCountEntries(session.commandMentions, 5, 3);
451
+ session.loopSuspicion = session.repeatedCommands.length > 0 && (session.failureMentions >= 2 || session.toolResults >= 4 || session.turns >= 12);
452
+ session.loopConfidence = !session.loopSuspicion
453
+ ? "low"
454
+ : session.failureMentions >= 2 && session.repeatedCommands[0]?.count >= 5
455
+ ? "high"
456
+ : "medium";
457
+ session.cost = tool === "claude-code"
458
+ ? calculateClaudeCost({
459
+ inputTokens: session.exactInputTokens,
460
+ outputTokens: session.exactOutputTokens,
461
+ cacheCreationTokens: session.exactCacheCreationTokens,
462
+ cacheReadTokens: session.exactCacheReadTokens,
463
+ }, session.model)
464
+ : null;
465
+ session.largestTextBlobs = session.largestTextBlobs.sort((a, b) => b.tokens - a.tokens).slice(0, 5);
466
+ return session;
467
+ }
468
+
469
+ function getCodexSessionFiles() {
470
+ const codexHome = process.env.PRISMO_CODEX_HOME || path.join(os.homedir(), ".codex");
471
+ return listFilesRecursive(path.join(codexHome, "sessions"), (file) => file.endsWith(".jsonl"), 200);
472
+ }
473
+
474
+ function getClaudeSessionFiles(cwd = process.cwd()) {
475
+ const claudeHome = process.env.PRISMO_CLAUDE_HOME || path.join(os.homedir(), ".claude");
476
+ const candidates = [cwd];
477
+ try {
478
+ candidates.push(fs.realpathSync(cwd));
479
+ } catch {
480
+ // Keep the original cwd candidate when realpath is unavailable.
481
+ }
482
+ const files = [];
483
+ for (const candidate of Array.from(new Set(candidates))) {
484
+ const safeProject = candidate.replace(/[\/\\:]/g, "-").replace(/^-/, "-");
485
+ const projectDir = path.join(claudeHome, "projects", safeProject);
486
+ files.push(...listFilesRecursive(projectDir, (file) => file.endsWith(".jsonl"), 200));
487
+ }
488
+ return Array.from(new Set(files));
489
+ }
490
+
491
+ function getAllClaudeSessionFiles() {
492
+ const claudeHome = process.env.PRISMO_CLAUDE_HOME || path.join(os.homedir(), ".claude");
493
+ return listFilesRecursive(path.join(claudeHome, "projects"), (file) => file.endsWith(".jsonl"), 1000);
494
+ }
495
+
496
+ function sameResolvedPath(a, b) {
497
+ if (!a || !b) return false;
498
+ try {
499
+ const resolvedA = fs.existsSync(a) ? fs.realpathSync(a) : path.resolve(a);
500
+ const resolvedB = fs.existsSync(b) ? fs.realpathSync(b) : path.resolve(b);
501
+ return resolvedA === resolvedB;
502
+ } catch {
503
+ return false;
504
+ }
505
+ }
506
+
507
+ function getUsageSummary(options = {}) {
508
+ const tool = options.tool || "all";
509
+ const limit = options.limit || 5;
510
+ const cwd = options.cwd || process.cwd();
511
+ const sessions = [];
512
+ if (tool === "all" || tool === "codex") {
513
+ for (const file of getCodexSessionFiles().slice(0, Math.max(limit * 8, 20))) {
514
+ const session = analyzeSessionFile(file, "codex");
515
+ if (!session.cwd || sameResolvedPath(session.cwd, cwd)) sessions.push(session);
516
+ if (sessions.filter((item) => item.tool === "codex").length >= limit) break;
517
+ }
518
+ }
519
+ if (tool === "all" || tool === "claude") {
520
+ for (const file of getClaudeSessionFiles(cwd).slice(0, limit)) {
521
+ sessions.push(analyzeSessionFile(file, "claude-code"));
522
+ }
523
+ }
524
+ sessions.sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0));
525
+ const selected = sessions.slice(0, limit);
526
+ const totals = selected.reduce(
527
+ (acc, session) => {
528
+ acc.displayTokens += session.displayTokens || 0;
529
+ acc.contextTokens += session.contextTokens || 0;
530
+ acc.estimatedTokens += session.estimatedTotalTokens || 0;
531
+ acc.exactTokens += session.exactAvailable ? session.exactTotalTokens : 0;
532
+ acc.toolTokens += session.estimatedToolTokens || 0;
533
+ acc.sessions += 1;
534
+ return acc;
535
+ },
536
+ { sessions: 0, displayTokens: 0, contextTokens: 0, estimatedTokens: 0, exactTokens: 0, toolTokens: 0 }
537
+ );
538
+ const sources = Array.from(new Set(selected.map((session) => session.tool).filter(Boolean)));
539
+ return {
540
+ generatedAt: new Date().toISOString(),
541
+ scannedPath: cwd,
542
+ tool,
543
+ tokenBudget: options.tokenBudget || null,
544
+ confidence: selected.every((session) => session.exactAvailable) && selected.length ? "exact-local-log" : "mixed-or-estimated",
545
+ totals,
546
+ sources,
547
+ sessions: selected,
548
+ };
549
+ }
550
+
551
+ function percentOf(part, total) {
552
+ if (!total) return 0;
553
+ return Math.round((Number(part || 0) / total) * 100);
554
+ }
555
+
556
+ function buildClaudeSessionDiagnosis(session) {
557
+ const totalCost = session.cost ? session.cost.total : 0;
558
+ const drivers = [];
559
+ if (session.cost) {
560
+ const costParts = [
561
+ ["output", session.cost.output, "Assistant output is the largest cost driver."],
562
+ ["cache-read", session.cost.cacheRead, "Repeated cached context reads are driving spend."],
563
+ ["cache-write", session.cost.cacheWrite, "Large context cache writes are adding upfront cost."],
564
+ ["input", session.cost.input, "Fresh input/context tokens are driving spend."],
565
+ ].sort((a, b) => b[1] - a[1]);
566
+ for (const [name, cost, message] of costParts) {
567
+ if (cost > 0) {
568
+ drivers.push({ type: name, cost, share: percentOf(cost, totalCost), message });
569
+ }
570
+ }
571
+ }
572
+ if (session.estimatedToolTokens >= 75000) {
573
+ drivers.push({
574
+ type: "tool-output",
575
+ tokens: session.estimatedToolTokens,
576
+ share: null,
577
+ message: "Tool output looks heavy; test logs, shell output, or file dumps may be inflating context.",
578
+ });
579
+ }
580
+ if (session.turns >= 30) {
581
+ drivers.push({
582
+ type: "long-session",
583
+ turns: session.turns,
584
+ share: null,
585
+ message: "Long session detected; unrelated follow-up work is likely riding old context.",
586
+ });
587
+ }
588
+ if (session.contextRisk === "High") {
589
+ drivers.push({
590
+ type: "context-risk",
591
+ tokens: session.displayTokens,
592
+ share: null,
593
+ message: "Session context is high enough that splitting work or using context packs should matter.",
594
+ });
595
+ }
596
+
597
+ const recommendations = [];
598
+ if (drivers.some((driver) => driver.type === "tool-output")) {
599
+ recommendations.push("Summarize long command output before pasting or re-reading it.");
600
+ }
601
+ if (drivers.some((driver) => driver.type === "cache-read" || driver.type === "cache-write" || driver.type === "context-risk")) {
602
+ recommendations.push(`Run ${NPX_COMMAND} optimize, then start from .prismo/architecture-summary.md.`);
603
+ }
604
+ if (drivers.some((driver) => driver.type === "long-session")) {
605
+ recommendations.push("Start a fresh Claude Code session for the next unrelated task.");
606
+ }
607
+ if (drivers.some((driver) => driver.type === "output")) {
608
+ recommendations.push("Ask for concise diffs, file paths, and verification results instead of full prose dumps.");
609
+ }
610
+ if (!recommendations.length) {
611
+ recommendations.push(`${NPX_COMMAND} scan --usage can tie this spend back to repo-level token waste.`);
612
+ }
613
+
614
+ const avoidableRate =
615
+ session.contextRisk === "High" ? 0.28 :
616
+ session.contextRisk === "Medium" ? 0.16 :
617
+ session.turns >= 20 || session.estimatedToolTokens >= 30000 ? 0.1 : 0.04;
618
+ return {
619
+ wasteScore: session.contextRisk === "High" ? 85 : session.contextRisk === "Medium" ? 55 : session.turns >= 20 ? 40 : 20,
620
+ estimatedAvoidableCost: totalCost * avoidableRate,
621
+ estimatedAvoidableRate: avoidableRate,
622
+ drivers: drivers.slice(0, 5),
623
+ recommendations: Array.from(new Set(recommendations)).slice(0, 4),
624
+ };
625
+ }
626
+
627
+ function buildSessionTimeline(session) {
628
+ const events = [];
629
+ const timeline = session.exactTokenTimeline || [];
630
+ for (let i = 1; i < timeline.length; i += 1) {
631
+ const previous = timeline[i - 1];
632
+ const current = timeline[i];
633
+ const delta = Math.max(0, (current.total || 0) - (previous.total || 0));
634
+ if (delta >= 60000) {
635
+ events.push({
636
+ timestamp: current.timestamp || session.updatedAt,
637
+ type: delta >= 250000 ? "context-spike" : "context-growth",
638
+ label: delta >= 250000 ? "Context spike likely" : "Context growth",
639
+ tokens: delta,
640
+ detail: `+${formatTokenCount(delta)} tokens`,
641
+ });
642
+ }
643
+ }
644
+ for (const item of (session.generatedArtifacts || []).slice(0, 5)) {
645
+ events.push({
646
+ timestamp: session.updatedAt,
647
+ type: "artifact-leak",
648
+ label: "Generated artifact likely entered context",
649
+ tokens: null,
650
+ detail: `${item.value} (${item.count}x)`,
651
+ });
652
+ }
653
+ for (const item of (session.repeatedCommands || []).slice(0, 5)) {
654
+ events.push({
655
+ timestamp: session.updatedAt,
656
+ type: "repeated-command",
657
+ label: session.loopSuspicion ? "Repeated command loop possible" : "Repeated command",
658
+ tokens: null,
659
+ detail: `${item.value} (${item.count}x)`,
660
+ });
661
+ }
662
+ for (const item of (session.repeatedPathMentions || []).slice(0, 3)) {
663
+ events.push({
664
+ timestamp: session.updatedAt,
665
+ type: "repeated-file",
666
+ label: "Repeated file/path context",
667
+ tokens: null,
668
+ detail: `${item.value} (${item.count}x)`,
669
+ });
670
+ }
671
+ if (session.estimatedToolTokens >= 75000) {
672
+ events.push({
673
+ timestamp: session.updatedAt,
674
+ type: "tool-output",
675
+ label: "Large tool output",
676
+ tokens: session.estimatedToolTokens,
677
+ detail: `${formatTokenCount(session.estimatedToolTokens)} estimated tool/output tokens`,
678
+ });
679
+ }
680
+ return events
681
+ .sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0))
682
+ .slice(-20);
683
+ }
684
+
685
+ function buildClaudeCostInsights(sessions, totals) {
686
+ const highestCostSessions = sessions
687
+ .slice()
688
+ .sort((a, b) => (b.cost?.total || 0) - (a.cost?.total || 0))
689
+ .slice(0, 3)
690
+ .map((session) => ({
691
+ sessionId: session.sessionId,
692
+ updatedAt: session.updatedAt,
693
+ model: session.cost?.model || session.model,
694
+ cost: session.cost?.total || 0,
695
+ risk: session.contextRisk,
696
+ topDriver: session.prismo?.drivers?.[0] || null,
697
+ }));
698
+ const costDrivers = [
699
+ { type: "output", cost: totals.outputCost, share: percentOf(totals.outputCost, totals.totalCost) },
700
+ { type: "cache-read", cost: totals.cacheReadCost, share: percentOf(totals.cacheReadCost, totals.totalCost) },
701
+ { type: "cache-write", cost: totals.cacheWriteCost, share: percentOf(totals.cacheWriteCost, totals.totalCost) },
702
+ { type: "input", cost: totals.inputCost, share: percentOf(totals.inputCost, totals.totalCost) },
703
+ ].filter((driver) => driver.cost > 0).sort((a, b) => b.cost - a.cost);
704
+ const estimatedAvoidableCost = sessions.reduce((sum, session) => sum + (session.prismo?.estimatedAvoidableCost || 0), 0);
705
+ const recommendations = [];
706
+ if (costDrivers[0]?.type === "cache-read" || costDrivers[0]?.type === "cache-write") {
707
+ recommendations.push("Repeated context is the main spend driver; generate context packs and avoid broad repo reads.");
708
+ }
709
+ if (costDrivers[0]?.type === "output") {
710
+ recommendations.push("Output cost is leading; ask the agent for concise diffs and summaries by default.");
711
+ }
712
+ if (sessions.some((session) => session.estimatedToolTokens >= 75000)) {
713
+ recommendations.push("Tool output is bloating at least one session; keep shell output narrow and summarize logs.");
714
+ }
715
+ if (sessions.some((session) => session.turns >= 30)) {
716
+ recommendations.push("At least one session is long; split unrelated tasks into fresh sessions.");
717
+ }
718
+ recommendations.push(`${NPX_COMMAND} scan --usage`);
719
+ recommendations.push(`${NPX_COMMAND} optimize`);
720
+ return {
721
+ estimatedAvoidableCost,
722
+ estimatedAvoidableRate: totals.totalCost ? estimatedAvoidableCost / totals.totalCost : 0,
723
+ costDrivers,
724
+ highestCostSessions,
725
+ recommendations: Array.from(new Set(recommendations)).slice(0, 5),
726
+ };
727
+ }
728
+
729
+ function getClaudeCodeCostSummary(options = {}) {
730
+ const limit = options.all ? 200 : options.limit || 1;
731
+ const cwd = options.cwd || process.cwd();
732
+ const files = options.allProjects ? getAllClaudeSessionFiles() : getClaudeSessionFiles(cwd);
733
+ const sessions = files
734
+ .slice(0, limit)
735
+ .map((file) => analyzeSessionFile(file, "claude-code"))
736
+ .sort((a, b) => new Date(b.updatedAt || 0) - new Date(a.updatedAt || 0))
737
+ .slice(0, limit)
738
+ .map((session) => {
739
+ if (session.cost) return session;
740
+ return {
741
+ ...session,
742
+ cost: calculateClaudeCost({
743
+ inputTokens: session.exactInputTokens,
744
+ outputTokens: session.exactOutputTokens,
745
+ cacheCreationTokens: session.exactCacheCreationTokens,
746
+ cacheReadTokens: session.exactCacheReadTokens,
747
+ }, session.model),
748
+ };
749
+ })
750
+ .map((session) => ({
751
+ ...session,
752
+ prismo: buildClaudeSessionDiagnosis(session),
753
+ timeline: buildSessionTimeline(session),
754
+ }));
755
+ const totals = sessions.reduce(
756
+ (acc, session) => {
757
+ acc.sessions += 1;
758
+ acc.inputTokens += session.exactInputTokens || 0;
759
+ acc.outputTokens += session.exactOutputTokens || 0;
760
+ acc.cacheCreationTokens += session.exactCacheCreationTokens || 0;
761
+ acc.cacheReadTokens += session.exactCacheReadTokens || 0;
762
+ acc.totalTokens += session.exactTotalTokens || session.contextTokens || 0;
763
+ acc.inputCost += session.cost ? session.cost.input : 0;
764
+ acc.outputCost += session.cost ? session.cost.output : 0;
765
+ acc.cacheWriteCost += session.cost ? session.cost.cacheWrite : 0;
766
+ acc.cacheReadCost += session.cost ? session.cost.cacheRead : 0;
767
+ acc.totalCost += session.cost ? session.cost.total : 0;
768
+ acc.noCacheCost += session.cost ? session.cost.noCache : 0;
769
+ acc.cacheSavings += session.cost ? session.cost.cacheSavings : 0;
770
+ acc.estimatedAvoidableCost += session.prismo ? session.prismo.estimatedAvoidableCost : 0;
771
+ return acc;
772
+ },
773
+ {
774
+ sessions: 0,
775
+ inputTokens: 0,
776
+ outputTokens: 0,
777
+ cacheCreationTokens: 0,
778
+ cacheReadTokens: 0,
779
+ totalTokens: 0,
780
+ inputCost: 0,
781
+ outputCost: 0,
782
+ cacheWriteCost: 0,
783
+ cacheReadCost: 0,
784
+ totalCost: 0,
785
+ noCacheCost: 0,
786
+ cacheSavings: 0,
787
+ estimatedAvoidableCost: 0,
788
+ }
789
+ );
790
+ const insights = buildClaudeCostInsights(sessions, totals);
791
+ return {
792
+ generatedAt: new Date().toISOString(),
793
+ scannedPath: options.allProjects ? null : cwd,
794
+ scope: options.allProjects ? "all-claude-projects" : "project",
795
+ command: options.mode || "latest",
796
+ pricingSource: "Anthropic public API pricing, USD per 1M tokens; defaults to Claude Sonnet 4 when a model cannot be inferred.",
797
+ totals,
798
+ insights,
799
+ sessions,
800
+ };
801
+ }
802
+
803
+ function parsePositiveInt(value, fallback) {
804
+ const parsed = Number.parseInt(value, 10);
805
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
806
+ }
807
+
808
+ function getPositionals(args, valueFlags = new Set()) {
809
+ const values = [];
810
+ for (let i = 0; i < args.length; i += 1) {
811
+ const arg = args[i];
812
+ if (valueFlags.has(arg)) {
813
+ i += 1;
814
+ continue;
815
+ }
816
+ if (!arg.startsWith("-")) values.push(arg);
817
+ }
818
+ return values;
819
+ }
820
+
821
+ function isScopeToken(value) {
822
+ return /^[a-zA-Z0-9][a-zA-Z0-9_-]{1,40}$/.test(String(value || ""));
823
+ }
824
+
825
+ function parseScopeAndTarget(args, valueFlags = new Set()) {
826
+ const positional = getPositionals(args, valueFlags);
827
+ if (!positional.length) return { scope: null, target: process.cwd() };
828
+ if (positional.length >= 2 && isScopeToken(positional[0])) {
829
+ return { scope: positional[0].toLowerCase(), target: positional[1] || process.cwd() };
830
+ }
831
+ if (isScopeToken(positional[0]) && ![".", ".."].includes(positional[0])) {
832
+ return { scope: positional[0].toLowerCase(), target: process.cwd() };
833
+ }
834
+ return { scope: null, target: positional[0] || process.cwd() };
835
+ }
836
+
837
+ function formatTokenCount(value) {
838
+ const n = Number(value || 0);
839
+ if (n >= 1000000) return `${(n / 1000000).toFixed(2)}M`;
840
+ if (n >= 1000) return `${Math.round(n / 1000)}k`;
841
+ return String(Math.round(n));
842
+ }
843
+
844
+ function formatMoney(value) {
845
+ const n = Number(value || 0);
846
+ if (n >= 1) return `$${n.toFixed(2)}`;
847
+ return `$${n.toFixed(4)}`;
848
+ }
849
+
850
+ function getRiskRank(risk) {
851
+ return { High: 3, Medium: 2, Low: 1 }[risk] || 0;
852
+ }
853
+
854
+ function getTopToolNames(session, limit = 4) {
855
+ return Object.entries(session && session.toolNames ? session.toolNames : {})
856
+ .sort((a, b) => b[1] - a[1])
857
+ .slice(0, limit)
858
+ .map(([name, count]) => ({ name, count }));
859
+ }
860
+
861
+ function getContextPressure(activeSession, warnings = []) {
862
+ if (!activeSession) return "Low";
863
+ const highSignals = [
864
+ activeSession.contextRisk === "High",
865
+ activeSession.recentContextGrowth >= 250000,
866
+ activeSession.estimatedToolTokens >= 150000,
867
+ activeSession.loopSuspicion,
868
+ warnings.some((warning) => warning.includes("exceeded the live token budget")),
869
+ warnings.length >= 4,
870
+ ].filter(Boolean).length;
871
+ if (highSignals) return "High";
872
+ const mediumSignals = [
873
+ activeSession.contextRisk === "Medium",
874
+ activeSession.recentContextGrowth >= 60000,
875
+ activeSession.estimatedToolTokens >= 50000,
876
+ (activeSession.repeatedPathMentions || []).length > 0,
877
+ (activeSession.generatedArtifacts || []).length > 0,
878
+ activeSession.turns >= 20,
879
+ ].filter(Boolean).length;
880
+ return mediumSignals ? "Medium" : "Low";
881
+ }
882
+
883
+ function buildLiveWarnings(activeSession, summary) {
884
+ const warnings = [];
885
+ if (!activeSession) {
886
+ warnings.push("No active local session detected for this repo yet.");
887
+ return warnings;
888
+ }
889
+ const budgetStatus = getTokenBudgetStatus(activeSession, summary.tokenBudget);
890
+ if (budgetStatus?.exceeded) {
891
+ warnings.push(`Session exceeded the live token budget by about ${formatTokenCount(budgetStatus.overBy)} tokens.`);
892
+ } else if (budgetStatus?.nearLimit) {
893
+ warnings.push(`Session is near the live token budget (${budgetStatus.percent}% used).`);
894
+ }
895
+ if (activeSession.contextRisk === "High") {
896
+ warnings.push("Context risk is high; consider starting a fresh session at the next task boundary.");
897
+ } else if (activeSession.contextRisk === "Medium") {
898
+ warnings.push("Context risk is rising; keep reads and shell output narrow.");
899
+ }
900
+ if (activeSession.estimatedToolTokens >= 150000) {
901
+ warnings.push("Tool/output tokens are dominating this session; summarize logs and test output before loading more.");
902
+ } else if (activeSession.estimatedToolTokens >= 50000) {
903
+ warnings.push("Tool/output tokens are elevated; prefer targeted commands and smaller file reads.");
904
+ }
905
+ if (activeSession.recentContextGrowth >= 250000) {
906
+ warnings.push(`Context jumped by about ${formatTokenCount(activeSession.recentContextGrowth)} tokens recently.`);
907
+ } else if (activeSession.recentContextGrowth >= 60000) {
908
+ warnings.push(`Recent context growth is about ${formatTokenCount(activeSession.recentContextGrowth)} tokens.`);
909
+ }
910
+ for (const item of getActionableRepeatedPaths(activeSession, 2)) {
911
+ warnings.push(`${item.value} appears repeatedly in context (${item.count}x).`);
912
+ }
913
+ for (const group of summarizeGeneratedArtifacts(activeSession.generatedArtifacts || [], 2)) {
914
+ warnings.push(`${group.type} likely entered active context (${group.count} mentions; e.g. ${group.examples[0]}).`);
915
+ }
916
+ if (activeSession.loopSuspicion) {
917
+ const command = activeSession.repeatedCommands?.[0]?.value;
918
+ warnings.push(command ? `Possible repeated command loop (${activeSession.loopConfidence} confidence) around "${command}".` : `Possible repeated tool loop (${activeSession.loopConfidence} confidence).`);
919
+ }
920
+ if (activeSession.turns >= 30) {
921
+ warnings.push("Long session detected; split unrelated follow-up work into a new session.");
922
+ }
923
+ if (!activeSession.exactAvailable) {
924
+ warnings.push("Exact token fields were not found; usage is estimated from local session text.");
925
+ }
926
+ if (summary.totals.displayTokens >= 1000000) {
927
+ warnings.push("Recent local usage is above 1M tokens; prioritize the largest session for cleanup.");
928
+ }
929
+ return Array.from(new Set(warnings)).slice(0, 5);
930
+ }
931
+
932
+ function getTokenBudgetStatus(activeSession, tokenBudget) {
933
+ const budget = Number(tokenBudget || 0);
934
+ if (!activeSession || !Number.isFinite(budget) || budget <= 0) return null;
935
+ const used = Number(activeSession.displayTokens || activeSession.tokens || 0);
936
+ const percent = budget > 0 ? Math.round((used / budget) * 100) : 0;
937
+ return {
938
+ budget,
939
+ used,
940
+ remaining: Math.max(0, budget - used),
941
+ overBy: Math.max(0, used - budget),
942
+ percent,
943
+ nearLimit: percent >= 80 && percent < 100,
944
+ exceeded: used >= budget,
945
+ };
946
+ }
947
+
948
+ function getRecommendedWatchAction(activeSession, warnings) {
949
+ if (!activeSession) return `${NPX_COMMAND} setup`;
950
+ if (activeSession.loopSuspicion) return "Pause the loop, summarize the failure once, then start a fresh scoped session.";
951
+ if ((activeSession.generatedArtifacts || []).length || getActionableRepeatedPaths(activeSession, 1).length) return `${NPX_COMMAND} doctor`;
952
+ if (activeSession.recentContextGrowth >= 250000) return "Start a fresh coding-agent session with a scoped Prismo context prompt.";
953
+ if (warnings.some((warning) => warning.includes("Tool/output"))) return `${NPX_COMMAND} context`;
954
+ if (activeSession.contextRisk === "High") return "Start a fresh coding-agent session before the next unrelated task.";
955
+ if (activeSession.turns >= 20) return `${NPX_COMMAND} optimize`;
956
+ return `${NPX_COMMAND} scan --usage`;
957
+ }
958
+
959
+ function buildLiveAction(activeSession, warnings, budgetStatus = null) {
960
+ if (!activeSession) {
961
+ return {
962
+ cause: "no-active-session",
963
+ confidence: "low",
964
+ summary: "No matching live coding-agent session is visible yet.",
965
+ now: [
966
+ "Start Codex or Claude Code from this repo.",
967
+ `Run ${NPX_COMMAND} watch --once after the first tool call.`,
968
+ ],
969
+ rescueAvailable: false,
970
+ rescueCommand: null,
971
+ };
972
+ }
973
+
974
+ if (budgetStatus?.exceeded) {
975
+ return {
976
+ cause: "token-budget-exceeded",
977
+ confidence: "high",
978
+ summary: `Session crossed the live token budget (${formatTokenCount(budgetStatus.used)} used / ${formatTokenCount(budgetStatus.budget)} budget).`,
979
+ now: [
980
+ "Stop broad exploration and finish only the current smallest step.",
981
+ "Ask the agent for a compact state summary before any more file reads.",
982
+ "Start a fresh scoped session for the next task boundary.",
983
+ ],
984
+ rescueAvailable: true,
985
+ rescueCommand: `${NPX_COMMAND} watch --rescue`,
986
+ };
987
+ }
988
+
989
+ if (activeSession.loopSuspicion) {
990
+ const command = activeSession.repeatedCommands?.[0];
991
+ return {
992
+ cause: "possible-loop",
993
+ confidence: activeSession.loopConfidence || "medium",
994
+ summary: command
995
+ ? `Repeated command loop around "${command.value}" (${command.count}x).`
996
+ : "Repeated tool loop likely.",
997
+ now: [
998
+ "Stop rerunning the same command.",
999
+ "Ask the agent to summarize the exact failure, changed files, and next smallest fix.",
1000
+ "Start a fresh scoped session if the summary is longer than one screen.",
1001
+ ],
1002
+ rescueAvailable: true,
1003
+ rescueCommand: `${NPX_COMMAND} watch --rescue`,
1004
+ };
1005
+ }
1006
+
1007
+ if (activeSession.estimatedToolTokens >= 150000) {
1008
+ return {
1009
+ cause: "tool-output-flood",
1010
+ confidence: "high",
1011
+ summary: `Tool/output tokens are dominating this session (${formatTokenCount(activeSession.estimatedToolTokens)} tokens).`,
1012
+ now: [
1013
+ "Stop loading full logs or broad command output.",
1014
+ "Rerun failing commands with tight filters or short ranges.",
1015
+ "Ask the agent to summarize current errors before reading more files.",
1016
+ ],
1017
+ rescueAvailable: true,
1018
+ rescueCommand: `${NPX_COMMAND} watch --rescue`,
1019
+ };
1020
+ }
1021
+
1022
+ const artifactGroup = summarizeGeneratedArtifacts(activeSession.generatedArtifacts || [], 1)[0];
1023
+ if (artifactGroup) {
1024
+ return {
1025
+ cause: "artifact-leak",
1026
+ confidence: "high",
1027
+ summary: `${artifactGroup.type} entered active context (${artifactGroup.count} mentions).`,
1028
+ now: [
1029
+ `Add or verify ignore coverage with ${NPX_COMMAND} doctor.`,
1030
+ "Tell the agent not to read generated artifacts, lockfiles, caches, build output, or coverage.",
1031
+ "Continue from the smallest relevant source files only.",
1032
+ ],
1033
+ rescueAvailable: true,
1034
+ rescueCommand: `${NPX_COMMAND} watch --rescue`,
1035
+ };
1036
+ }
1037
+
1038
+ const repeatedPath = getActionableRepeatedPaths(activeSession, 1)[0];
1039
+ if (repeatedPath) {
1040
+ return {
1041
+ cause: "repeated-file-read",
1042
+ confidence: repeatedPath.count >= 20 ? "high" : "medium",
1043
+ summary: `${repeatedPath.value} is repeatedly entering context (${repeatedPath.count}x).`,
1044
+ now: [
1045
+ "Ask the agent to summarize what it learned from that file.",
1046
+ "Stop re-reading it unless a new edit changed it.",
1047
+ "Move to the next smallest relevant file or test.",
1048
+ ],
1049
+ rescueAvailable: true,
1050
+ rescueCommand: `${NPX_COMMAND} watch --rescue`,
1051
+ };
1052
+ }
1053
+
1054
+ if (activeSession.recentContextGrowth >= 250000) {
1055
+ return {
1056
+ cause: "context-spike",
1057
+ confidence: "high",
1058
+ summary: `Context jumped by about ${formatTokenCount(activeSession.recentContextGrowth)} tokens.`,
1059
+ now: [
1060
+ "Pause broad exploration.",
1061
+ "Ask for a compact state summary.",
1062
+ "Start a fresh session with a scoped context prompt.",
1063
+ ],
1064
+ rescueAvailable: true,
1065
+ rescueCommand: `${NPX_COMMAND} watch --rescue`,
1066
+ };
1067
+ }
1068
+
1069
+ if (activeSession.contextRisk === "High") {
1070
+ return {
1071
+ cause: "high-context-pressure",
1072
+ confidence: "medium",
1073
+ summary: "This session is carrying high context pressure.",
1074
+ now: [
1075
+ "Finish the current small step only.",
1076
+ "Start a fresh session at the next task boundary.",
1077
+ `Use ${NPX_COMMAND} context for a scoped restart prompt.`,
1078
+ ],
1079
+ rescueAvailable: true,
1080
+ rescueCommand: `${NPX_COMMAND} watch --rescue`,
1081
+ };
1082
+ }
1083
+
1084
+ return {
1085
+ cause: warnings.length ? "context-pressure" : "healthy",
1086
+ confidence: warnings.length ? "medium" : "low",
1087
+ summary: warnings.length ? warnings[0] : "No major live waste signal detected.",
1088
+ now: warnings.length
1089
+ ? ["Keep file reads narrow.", "Avoid broad logs and generated artifacts.", "Continue watching for spikes."]
1090
+ : ["Keep working.", "Use scoped reads and concise command output.", "Run watch again after major tool calls."],
1091
+ rescueAvailable: warnings.length > 0,
1092
+ rescueCommand: warnings.length ? `${NPX_COMMAND} watch --rescue` : null,
1093
+ };
1094
+ }
1095
+
1096
+ function buildLiveSessionView(summary) {
1097
+ const activeSession = summary.sessions[0] || null;
1098
+ const warnings = buildLiveWarnings(activeSession, summary);
1099
+ const contextPressure = getContextPressure(activeSession, warnings);
1100
+ const budgetStatus = getTokenBudgetStatus(activeSession, summary.tokenBudget);
1101
+ const topTools = getTopToolNames(activeSession);
1102
+ const largestTextBlobs = activeSession ? activeSession.largestTextBlobs || [] : [];
1103
+ const actionableRepeatedPaths = activeSession ? getActionableRepeatedPaths(activeSession, 5) : [];
1104
+ const generatedArtifactGroups = activeSession ? summarizeGeneratedArtifacts(activeSession.generatedArtifacts || [], 5) : [];
1105
+ const liveAction = buildLiveAction(activeSession, warnings, budgetStatus);
1106
+ return {
1107
+ activeSession: activeSession
1108
+ ? {
1109
+ tool: activeSession.tool,
1110
+ sessionId: activeSession.sessionId,
1111
+ title: activeSession.title,
1112
+ model: activeSession.model,
1113
+ cwd: activeSession.cwd,
1114
+ updatedAt: activeSession.updatedAt,
1115
+ tokens: activeSession.displayTokens,
1116
+ contextTokens: activeSession.contextTokens,
1117
+ exactAvailable: activeSession.exactAvailable,
1118
+ confidence: activeSession.confidence,
1119
+ contextRisk: activeSession.contextRisk,
1120
+ turns: activeSession.turns,
1121
+ toolCalls: activeSession.toolCalls,
1122
+ toolResults: activeSession.toolResults,
1123
+ estimatedToolTokens: activeSession.estimatedToolTokens,
1124
+ recentContextGrowth: activeSession.recentContextGrowth || 0,
1125
+ repeatedPathMentions: activeSession.repeatedPathMentions || [],
1126
+ actionableRepeatedPaths,
1127
+ generatedArtifacts: activeSession.generatedArtifacts || [],
1128
+ generatedArtifactGroups,
1129
+ repeatedCommands: activeSession.repeatedCommands || [],
1130
+ loopSuspicion: Boolean(activeSession.loopSuspicion),
1131
+ loopConfidence: activeSession.loopConfidence || "low",
1132
+ topTools,
1133
+ largestTextBlobs,
1134
+ }
1135
+ : null,
1136
+ contextPressure,
1137
+ highestRisk: summary.sessions.reduce((risk, session) => (getRiskRank(session.contextRisk) > getRiskRank(risk) ? session.contextRisk : risk), "Low"),
1138
+ budget: budgetStatus,
1139
+ warnings,
1140
+ liveAction,
1141
+ recommendedAction: getRecommendedWatchAction(activeSession, warnings),
1142
+ nextCommands: Array.from(new Set([
1143
+ `${NPX_COMMAND} doctor`,
1144
+ `${NPX_COMMAND} context`,
1145
+ `${NPX_COMMAND} optimize`,
1146
+ ])).slice(0, 3),
1147
+ };
1148
+ }
1149
+
1150
+ function renderUsageTerminal(summary, title = "Prismo Usage") {
1151
+ const lines = [];
1152
+ lines.push("");
1153
+ lines.push(title);
1154
+ lines.push("");
1155
+ lines.push(`Tool scope: ${summary.tool}`);
1156
+ lines.push(`Sessions shown: ${summary.sessions.length}`);
1157
+ lines.push(`Total displayed tokens: ${formatTokenCount(summary.totals.displayTokens)}`);
1158
+ if (summary.totals.exactTokens) lines.push(`Exact local-log tokens: ${formatTokenCount(summary.totals.exactTokens)}`);
1159
+ if (summary.totals.toolTokens) lines.push(`Estimated tool/output tokens: ${formatTokenCount(summary.totals.toolTokens)}`);
1160
+ lines.push(`Confidence: ${summary.confidence}`);
1161
+ lines.push("");
1162
+ lines.push("Recent Sessions:");
1163
+ if (!summary.sessions.length) {
1164
+ lines.push("- No local sessions detected.");
1165
+ } else {
1166
+ summary.sessions.forEach((session, index) => {
1167
+ lines.push(`${index + 1}. ${session.tool} - ${session.title || session.sessionId}`);
1168
+ lines.push(` tokens: ${formatTokenCount(session.displayTokens)} (${session.confidence}), risk: ${session.contextRisk}, turns: ${session.turns}, tools: ${session.toolCalls}`);
1169
+ if (session.model) lines.push(` model: ${session.model}`);
1170
+ if (session.cwd) lines.push(` cwd: ${session.cwd}`);
1171
+ });
1172
+ }
1173
+ lines.push("");
1174
+ lines.push("Notes: exact means the local tool log exposed token fields. Estimated means Prismo used local text size heuristics only.");
1175
+ return lines.join("\n");
1176
+ }
1177
+
1178
+ function renderClaudeCostTerminal(summary) {
1179
+ const lines = [];
1180
+ const latest = summary.sessions[0] || null;
1181
+ lines.push("");
1182
+ lines.push("Prismo Claude Code Cost");
1183
+ lines.push("");
1184
+ if (!summary.sessions.length) {
1185
+ lines.push(summary.scope === "all-claude-projects" ? "No Claude Code sessions found." : "No Claude Code sessions found for this repo.");
1186
+ lines.push("");
1187
+ lines.push("Tip: run Claude Code inside this project, then try `npx getprismo cc` again.");
1188
+ return lines.join("\n");
1189
+ }
1190
+
1191
+ if (summary.command === "list") {
1192
+ lines.push(`Recent sessions: ${summary.sessions.length}`);
1193
+ lines.push("");
1194
+ summary.sessions.forEach((session, index) => {
1195
+ lines.push(`${index + 1}. ${session.model || session.cost.model} ${session.updatedAt || "unknown date"}`);
1196
+ lines.push(` ${formatTokenCount(session.exactTotalTokens || session.contextTokens)} tokens -> ${formatMoney(session.cost.total)} (${session.sessionId})`);
1197
+ if (session.prismo?.drivers?.[0]) lines.push(` driver: ${session.prismo.drivers[0].message}`);
1198
+ });
1199
+ lines.push("");
1200
+ lines.push(`Estimated avoidable spend: ${formatMoney(summary.insights.estimatedAvoidableCost)}`);
1201
+ lines.push(`Next: ${summary.insights.recommendations.slice(0, 2).join(" -> ")}`);
1202
+ return lines.join("\n");
1203
+ }
1204
+
1205
+ if (summary.command === "all") {
1206
+ lines.push(`Sessions: ${summary.totals.sessions}`);
1207
+ lines.push("");
1208
+ lines.push(`Input: ${formatTokenCount(summary.totals.inputTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.inputCost)}`);
1209
+ lines.push(`Output: ${formatTokenCount(summary.totals.outputTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.outputCost)}`);
1210
+ lines.push(`Cache write: ${formatTokenCount(summary.totals.cacheCreationTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.cacheWriteCost)}`);
1211
+ lines.push(`Cache read: ${formatTokenCount(summary.totals.cacheReadTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.cacheReadCost)}`);
1212
+ lines.push("--------------------------------------------------");
1213
+ lines.push(`Total: ${formatTokenCount(summary.totals.totalTokens).padStart(8)} tokens -> ${formatMoney(summary.totals.totalCost)}`);
1214
+ if (summary.totals.cacheSavings > 0) lines.push("");
1215
+ if (summary.totals.cacheSavings > 0) lines.push(`Cache saved you ${formatMoney(summary.totals.cacheSavings)} (vs no caching)`);
1216
+ lines.push("");
1217
+ lines.push("Prismo Diagnosis");
1218
+ lines.push(`Estimated avoidable spend: ${formatMoney(summary.insights.estimatedAvoidableCost)} (${Math.round(summary.insights.estimatedAvoidableRate * 100)}%)`);
1219
+ if (summary.insights.costDrivers.length) {
1220
+ lines.push(`Main cost driver: ${summary.insights.costDrivers[0].type} (${summary.insights.costDrivers[0].share}%)`);
1221
+ }
1222
+ summary.insights.recommendations.slice(0, 3).forEach((recommendation) => lines.push(`- ${recommendation}`));
1223
+ return lines.join("\n");
1224
+ }
1225
+
1226
+ if (summary.command === "timeline") {
1227
+ lines.push(`Session: ${latest.sessionId}`);
1228
+ if (latest.model || latest.cost?.model) lines.push(`Model: ${latest.model || latest.cost.model}`);
1229
+ lines.push(`Updated: ${latest.updatedAt || "unknown date"}`);
1230
+ lines.push("");
1231
+ lines.push("Timeline");
1232
+ if (!latest.timeline || !latest.timeline.length) {
1233
+ lines.push("- No major context spikes, repeated commands, or artifact leaks detected in this session.");
1234
+ } else {
1235
+ latest.timeline.forEach((event) => {
1236
+ const when = event.timestamp ? new Date(event.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "unknown";
1237
+ lines.push(`${when} ${event.label} ${event.detail}`);
1238
+ });
1239
+ }
1240
+ lines.push("");
1241
+ lines.push("Suggested Action");
1242
+ lines.push(latest.prismo?.recommendations?.[0] || `${NPX_COMMAND} doctor`);
1243
+ return lines.join("\n");
1244
+ }
1245
+
1246
+ const sessions = summary.command === "last" ? summary.sessions : [latest];
1247
+ sessions.forEach((session, index) => {
1248
+ if (index > 0) lines.push("");
1249
+ lines.push(`${session.cost.model} ${session.updatedAt || "unknown date"}`);
1250
+ if (session.sessionId) lines.push(`Session: ${session.sessionId}`);
1251
+ if (session.exactAvailable) lines.push(`Confidence: ${session.confidence}`);
1252
+ else lines.push("Confidence: estimated; exact token usage was not present in the local log.");
1253
+ lines.push("");
1254
+ lines.push(`Input: ${formatTokenCount(session.exactInputTokens).padStart(8)} tokens -> ${formatMoney(session.cost.input)}`);
1255
+ lines.push(`Output: ${formatTokenCount(session.exactOutputTokens).padStart(8)} tokens -> ${formatMoney(session.cost.output)}`);
1256
+ lines.push(`Cache write: ${formatTokenCount(session.exactCacheCreationTokens).padStart(8)} tokens -> ${formatMoney(session.cost.cacheWrite)}`);
1257
+ lines.push(`Cache read: ${formatTokenCount(session.exactCacheReadTokens).padStart(8)} tokens -> ${formatMoney(session.cost.cacheRead)}`);
1258
+ lines.push("--------------------------------------------------");
1259
+ lines.push(`Total: ${formatTokenCount(session.exactTotalTokens || session.contextTokens).padStart(8)} tokens -> ${formatMoney(session.cost.total)}`);
1260
+ if (session.cost.cacheSavings > 0) lines.push("");
1261
+ if (session.cost.cacheSavings > 0) lines.push(`Cache saved you ${formatMoney(session.cost.cacheSavings)} (vs no caching)`);
1262
+ lines.push("");
1263
+ lines.push("Prismo Diagnosis");
1264
+ lines.push(`Waste score: ${session.prismo.wasteScore}/100`);
1265
+ lines.push(`Estimated avoidable spend: ${formatMoney(session.prismo.estimatedAvoidableCost)} (${Math.round(session.prismo.estimatedAvoidableRate * 100)}%)`);
1266
+ if (session.prismo.drivers.length) {
1267
+ lines.push("Cost Drivers:");
1268
+ session.prismo.drivers.slice(0, 3).forEach((driver) => lines.push(`- ${driver.message}`));
1269
+ }
1270
+ lines.push("Better Next Actions:");
1271
+ session.prismo.recommendations.forEach((recommendation) => lines.push(`- ${recommendation}`));
1272
+ });
1273
+ lines.push("");
1274
+ lines.push(`Next: ${NPX_COMMAND} scan --usage to connect spend back to repo token waste.`);
1275
+ return lines.join("\n");
1276
+ }
1277
+
1278
+ function renderWatchTerminal(summary) {
1279
+ const live = summary.live || buildLiveSessionView(summary);
1280
+ const active = live.activeSession;
1281
+ const lines = [];
1282
+ const pressureTone = live.contextPressure === "High" ? "red" : live.contextPressure === "Medium" ? "yellow" : "green";
1283
+ lines.push("");
1284
+ lines.push(color("Prismo Watch", "bold"));
1285
+ lines.push("");
1286
+ if (!active) {
1287
+ lines.push("Context Pressure: Low");
1288
+ lines.push("- No local Codex/Claude Code session detected for this repo yet.");
1289
+ lines.push("");
1290
+ lines.push("Suggested Action");
1291
+ lines.push(`Run: ${live.recommendedAction}`);
1292
+ lines.push("");
1293
+ lines.push("Tip: start Codex or Claude Code in this repo, then keep this watch open.");
1294
+ return lines.join("\n");
1295
+ }
1296
+ lines.push(`Context Pressure: ${color(live.contextPressure.toUpperCase(), pressureTone)}`);
1297
+ lines.push(`Session Size: ${formatTokenCount(active.tokens)} tokens (${active.confidence})`);
1298
+ if (live.budget) {
1299
+ const budgetTone = live.budget.exceeded ? "red" : live.budget.nearLimit ? "yellow" : "green";
1300
+ lines.push(`Token Budget: ${color(`${live.budget.percent}%`, budgetTone)} of ${formatTokenCount(live.budget.budget)}`);
1301
+ }
1302
+ lines.push(`Recent Growth: +${formatTokenCount(active.recentContextGrowth || 0)} tokens`);
1303
+ lines.push(`Tool Output: ${formatTokenCount(active.estimatedToolTokens)} tokens`);
1304
+ lines.push(`Turns: ${active.turns} | Tool calls: ${active.toolCalls}`);
1305
+ lines.push(`Source: ${active.tool}`);
1306
+ if (active.contextTokens && active.contextTokens !== active.tokens) lines.push(`Context tokens observed: ${formatTokenCount(active.contextTokens)}`);
1307
+ if (active.model) lines.push(`Model: ${active.model}`);
1308
+ if (active.updatedAt) lines.push(`Updated: ${active.updatedAt}`);
1309
+ lines.push("");
1310
+ lines.push("Warnings");
1311
+ if (live.warnings.length) live.warnings.slice(0, 6).forEach((warning) => lines.push(`- ${warning}`));
1312
+ else lines.push("- No major context-pressure signals detected.");
1313
+ lines.push("");
1314
+ lines.push("Do This Now");
1315
+ lines.push(`Cause: ${live.liveAction.cause} (${live.liveAction.confidence} confidence)`);
1316
+ lines.push(live.liveAction.summary);
1317
+ live.liveAction.now.slice(0, 4).forEach((step, index) => lines.push(`${index + 1}. ${step}`));
1318
+ if (live.liveAction.rescueAvailable) lines.push(`Rescue: ${live.liveAction.rescueCommand}`);
1319
+ lines.push("");
1320
+ if (active.actionableRepeatedPaths?.length || active.generatedArtifactGroups?.length || active.repeatedCommands.length) {
1321
+ lines.push("Signals");
1322
+ (active.actionableRepeatedPaths || []).slice(0, 3).forEach((item) => lines.push(`- Repeated file: ${item.value} (${item.count}x)`));
1323
+ (active.generatedArtifactGroups || []).slice(0, 3).forEach((group) => lines.push(`- Generated artifacts: ${group.type} (${group.count} mentions; e.g. ${group.examples[0]})`));
1324
+ active.repeatedCommands.slice(0, 2).forEach((item) => lines.push(`- Repeated command: ${item.value} (${item.count}x${active.loopSuspicion ? `, ${active.loopConfidence} loop confidence` : ""})`));
1325
+ lines.push("");
1326
+ }
1327
+ if (active.largestTextBlobs.length) {
1328
+ lines.push("Largest Context Sources");
1329
+ active.largestTextBlobs.slice(0, 3).forEach((blob) => lines.push(`- ${blob.label}: ~${blob.tokens.toLocaleString()} tokens`));
1330
+ lines.push("");
1331
+ }
1332
+ lines.push("Suggested Action");
1333
+ lines.push(`Run: ${live.recommendedAction}`);
1334
+ lines.push("");
1335
+ lines.push("Useful Commands");
1336
+ live.nextCommands.forEach((command) => lines.push(`- ${command}`));
1337
+ lines.push("");
1338
+ lines.push("Signals are local estimates from available coding-agent logs. Prismo uses likely/suspicious language when log visibility is limited.");
1339
+ lines.push("Expected project instruction files are muted unless they combine with stronger context-pressure signals.");
1340
+ return lines.join("\n");
1341
+ }
1342
+
1343
+ function renderRescuePrompt(summary) {
1344
+ const live = summary.live || buildLiveSessionView(summary);
1345
+ const active = live.activeSession;
1346
+ const lines = [];
1347
+ lines.push("Prismo Rescue Prompt");
1348
+ lines.push("");
1349
+ if (!active) {
1350
+ lines.push("No active local Codex/Claude Code session was found for this repo.");
1351
+ lines.push("");
1352
+ lines.push("Start a coding-agent session from the repo root, then rerun:");
1353
+ lines.push(`${NPX_COMMAND} watch --rescue`);
1354
+ return lines.join("\n");
1355
+ }
1356
+
1357
+ lines.push("Paste this into the current AI coding session:");
1358
+ lines.push("");
1359
+ lines.push("```text");
1360
+ lines.push("We are in a high-context AI coding session. Stop broad exploration and recover state before doing more work.");
1361
+ lines.push("");
1362
+ lines.push(`Current Prismo signal: ${live.liveAction.cause} (${live.liveAction.confidence} confidence).`);
1363
+ lines.push(`Summary: ${live.liveAction.summary}`);
1364
+ lines.push(`Context pressure: ${live.contextPressure}. Session size: ${formatTokenCount(active.tokens)} tokens. Tool output: ${formatTokenCount(active.estimatedToolTokens)} tokens.`);
1365
+ if (active.recentContextGrowth) lines.push(`Recent context growth: +${formatTokenCount(active.recentContextGrowth)} tokens.`);
1366
+ lines.push("");
1367
+ lines.push("Do this now:");
1368
+ live.liveAction.now.forEach((step, index) => lines.push(`${index + 1}. ${step}`));
1369
+ lines.push("");
1370
+ lines.push("Before reading or editing anything else, summarize:");
1371
+ lines.push("- files changed so far");
1372
+ lines.push("- exact failing command or error");
1373
+ lines.push("- current hypothesis");
1374
+ lines.push("- next smallest file/test to inspect");
1375
+ lines.push("");
1376
+ const repeated = active.actionableRepeatedPaths || [];
1377
+ if (repeated.length) {
1378
+ lines.push("Do not re-read these files unless they changed:");
1379
+ repeated.slice(0, 5).forEach((item) => lines.push(`- ${item.value} (${item.count}x already)`));
1380
+ lines.push("");
1381
+ }
1382
+ const groups = active.generatedArtifactGroups || [];
1383
+ lines.push("Do not read generated/noisy artifacts unless explicitly required:");
1384
+ const avoids = groups.length
1385
+ ? groups.map((group) => `${group.type}${group.examples?.[0] ? `, e.g. ${group.examples[0]}` : ""}`)
1386
+ : ["node_modules/", ".next/", "dist/", "build/", "coverage/", "package-lock.json", "logs/"];
1387
+ avoids.slice(0, 8).forEach((item) => lines.push(`- ${item}`));
1388
+ lines.push("");
1389
+ lines.push("Keep the next response under 20 lines unless a code diff is required.");
1390
+ lines.push("```");
1391
+ return lines.join("\n");
1392
+ }
1393
+
1394
+ function renderLiveGuardrails(summary) {
1395
+ const live = summary.live || buildLiveSessionView(summary);
1396
+ const active = live.activeSession;
1397
+ const lines = [];
1398
+ lines.push("# Prismo Live Guardrails");
1399
+ lines.push("");
1400
+ lines.push(`Generated: ${summary.generatedAt}`);
1401
+ lines.push(`Context pressure: ${live.contextPressure}`);
1402
+ lines.push(`Current issue: ${live.liveAction.cause}`);
1403
+ lines.push(`Confidence: ${live.liveAction.confidence}`);
1404
+ lines.push("");
1405
+ lines.push("## Effective Immediately");
1406
+ lines.push("");
1407
+ live.liveAction.now.forEach((step) => lines.push(`- ${step}`));
1408
+ lines.push("- Keep command output short; prefer filtered errors, small ranges, and summaries over full logs.");
1409
+ lines.push("- Do not read generated artifacts, lockfiles, caches, build output, coverage, or logs unless explicitly required.");
1410
+ lines.push("- Before broad exploration, summarize the current task, changed files, current failure, and next smallest useful file/test.");
1411
+ if (active) {
1412
+ lines.push("");
1413
+ lines.push("## Current Session");
1414
+ lines.push("");
1415
+ lines.push(`- Tool: ${active.tool}`);
1416
+ lines.push(`- Session size: ${formatTokenCount(active.tokens)} tokens`);
1417
+ lines.push(`- Tool/output tokens: ${formatTokenCount(active.estimatedToolTokens)}`);
1418
+ lines.push(`- Turns: ${active.turns}`);
1419
+ if (active.recentContextGrowth) lines.push(`- Recent context growth: +${formatTokenCount(active.recentContextGrowth)} tokens`);
1420
+ const repeated = active.actionableRepeatedPaths || [];
1421
+ if (repeated.length) {
1422
+ lines.push("");
1423
+ lines.push("## Do Not Re-Read Unless Changed");
1424
+ lines.push("");
1425
+ repeated.slice(0, 8).forEach((item) => lines.push(`- ${item.value} (${item.count}x already)`));
1426
+ }
1427
+ const groups = active.generatedArtifactGroups || [];
1428
+ if (groups.length) {
1429
+ lines.push("");
1430
+ lines.push("## Noisy Artifacts To Avoid");
1431
+ lines.push("");
1432
+ groups.slice(0, 8).forEach((group) => {
1433
+ lines.push(`- ${group.type}${group.examples?.[0] ? `, e.g. ${group.examples[0]}` : ""}`);
1434
+ });
1435
+ }
1436
+ } else {
1437
+ lines.push("");
1438
+ lines.push("No active local session was detected for this repo yet. Keep this file referenced once the coding-agent session starts.");
1439
+ }
1440
+ lines.push("");
1441
+ lines.push("## Agent Instruction");
1442
+ lines.push("");
1443
+ lines.push("Follow this file during the current session. If it changes, adapt immediately before reading more files or running more tools.");
1444
+ lines.push("");
1445
+ return lines.join("\n");
1446
+ }
1447
+
1448
+ function renderContextThrottle(summary) {
1449
+ const live = summary.live || buildLiveSessionView(summary);
1450
+ const active = live.activeSession;
1451
+ const budget = live.budget;
1452
+ const lines = [];
1453
+ lines.push("# Prismo Live Context Throttle");
1454
+ lines.push("");
1455
+ lines.push(`Generated: ${summary.generatedAt}`);
1456
+ lines.push(`Mode: ${budget?.exceeded ? "hard-throttle" : budget?.nearLimit ? "soft-throttle" : live.contextPressure === "High" ? "pressure-throttle" : "watch"}`);
1457
+ lines.push(`Context pressure: ${live.contextPressure}`);
1458
+ lines.push(`Current issue: ${live.liveAction.cause}`);
1459
+ if (budget) {
1460
+ lines.push(`Token budget: ${formatTokenCount(budget.budget)}`);
1461
+ lines.push(`Current session: ${formatTokenCount(budget.used)} (${budget.percent}% used)`);
1462
+ }
1463
+ lines.push("");
1464
+ lines.push("## Stop");
1465
+ lines.push("");
1466
+ lines.push("- Do not run broad search commands without a tight pattern and path.");
1467
+ lines.push("- Do not paste or read full logs, lockfiles, generated files, cache folders, coverage, build output, or minified bundles.");
1468
+ lines.push("- Do not re-open files listed below unless they changed after this file was generated.");
1469
+ lines.push("");
1470
+ lines.push("## Allowed Next");
1471
+ lines.push("");
1472
+ if (budget?.exceeded) {
1473
+ lines.push("- Produce a compact state summary first.");
1474
+ lines.push("- Finish only the smallest current fix or stop at a clear handoff.");
1475
+ lines.push("- Move the next unrelated task into a fresh session.");
1476
+ } else {
1477
+ live.liveAction.now.slice(0, 4).forEach((step) => lines.push(`- ${step}`));
1478
+ lines.push("- Keep the next tool call narrow enough to summarize in one screen.");
1479
+ }
1480
+ if (active?.actionableRepeatedPaths?.length) {
1481
+ lines.push("");
1482
+ lines.push("## Blocked Re-Reads");
1483
+ lines.push("");
1484
+ active.actionableRepeatedPaths.slice(0, 10).forEach((item) => lines.push(`- ${item.value} (${item.count}x already)`));
1485
+ }
1486
+ if (active?.generatedArtifactGroups?.length) {
1487
+ lines.push("");
1488
+ lines.push("## Blocked Artifact Classes");
1489
+ lines.push("");
1490
+ active.generatedArtifactGroups.slice(0, 10).forEach((group) => {
1491
+ lines.push(`- ${group.type}${group.examples?.[0] ? `, e.g. ${group.examples[0]}` : ""}`);
1492
+ });
1493
+ }
1494
+ lines.push("");
1495
+ lines.push("## Agent Instruction");
1496
+ lines.push("");
1497
+ lines.push("Treat this as the active context budget. Prefer summaries over reads, targeted commands over broad output, and fresh sessions over dragging old context forward.");
1498
+ lines.push("");
1499
+ return lines.join("\n");
1500
+ }
1501
+
1502
+ function writeLiveFile(root, relPath, contents) {
1503
+ const fullPath = path.join(root || process.cwd(), relPath);
1504
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
1505
+ fs.writeFileSync(fullPath, contents, "utf8");
1506
+ return relPath;
1507
+ }
1508
+
1509
+ function writeLiveGuardrails(summary) {
1510
+ const root = summary.scannedPath || process.cwd();
1511
+ const guardrailsPath = writeLiveFile(root, path.join(".prismo", "live-guardrails.md"), renderLiveGuardrails(summary));
1512
+ const rescuePath = writeLiveFile(root, path.join(".prismo", "live-rescue-prompt.md"), renderRescuePrompt(summary));
1513
+ return { guardrailsPath, rescuePath };
1514
+ }
1515
+
1516
+ function writeContextThrottle(summary) {
1517
+ const root = summary.scannedPath || process.cwd();
1518
+ return writeLiveFile(root, path.join(".prismo", "live-context-throttle.md"), renderContextThrottle(summary));
1519
+ }
1520
+
1521
+ function buildWatchEvent(summary) {
1522
+ const live = summary.live || buildLiveSessionView(summary);
1523
+ const active = live.activeSession;
1524
+ if (!active || live.liveAction.cause === "healthy" || live.liveAction.cause === "no-active-session") return null;
1525
+ const repeated = active.actionableRepeatedPaths || [];
1526
+ const artifacts = active.generatedArtifactGroups || [];
1527
+ const budget = live.budget || null;
1528
+ const signatureParts = [
1529
+ live.liveAction.cause,
1530
+ live.contextPressure,
1531
+ budget?.exceeded ? "budget-exceeded" : budget?.nearLimit ? "budget-near" : "budget-ok",
1532
+ repeated.slice(0, 3).map((item) => `${item.value}:${item.count}`).join("|"),
1533
+ artifacts.slice(0, 3).map((item) => `${item.type}:${item.count}`).join("|"),
1534
+ ];
1535
+ return {
1536
+ schemaVersion: 1,
1537
+ timestamp: summary.generatedAt,
1538
+ repo: summary.scannedPath,
1539
+ tool: active.tool,
1540
+ sessionId: active.sessionId,
1541
+ pressure: live.contextPressure,
1542
+ cause: live.liveAction.cause,
1543
+ confidence: live.liveAction.confidence,
1544
+ summary: live.liveAction.summary,
1545
+ tokens: active.tokens,
1546
+ recentContextGrowth: active.recentContextGrowth || 0,
1547
+ toolOutputTokens: active.estimatedToolTokens || 0,
1548
+ budget,
1549
+ warnings: live.warnings || [],
1550
+ repeatedFiles: repeated.slice(0, 5),
1551
+ artifactGroups: artifacts.slice(0, 5),
1552
+ signature: signatureParts.join("::"),
1553
+ };
1554
+ }
1555
+
1556
+ function readLastJsonLine(filePath) {
1557
+ try {
1558
+ if (!fs.existsSync(filePath)) return null;
1559
+ const lines = fs.readFileSync(filePath, "utf8").trim().split(/\r?\n/).filter(Boolean);
1560
+ if (!lines.length) return null;
1561
+ return JSON.parse(lines[lines.length - 1]);
1562
+ } catch {
1563
+ return null;
1564
+ }
1565
+ }
1566
+
1567
+ function writeWatchEvent(summary) {
1568
+ const event = buildWatchEvent(summary);
1569
+ if (!event) return null;
1570
+ const root = summary.scannedPath || process.cwd();
1571
+ const relPath = path.join(".prismo", "watch-events.jsonl");
1572
+ const fullPath = path.join(root, relPath);
1573
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
1574
+ const last = readLastJsonLine(fullPath);
1575
+ if (last?.signature === event.signature) return relPath;
1576
+ fs.appendFileSync(fullPath, `${JSON.stringify(event)}\n`, "utf8");
1577
+ return relPath;
1578
+ }
1579
+
1580
+ function renderWatchReport(summary) {
1581
+ if (summary.redactPaths) {
1582
+ const redacted = redactWatchPayload(toWatchJsonPayload(summary));
1583
+ return renderWatchReport({
1584
+ ...summary,
1585
+ scannedPath: redacted.scannedPath,
1586
+ sessions: redacted.sessions,
1587
+ live: redacted.live,
1588
+ redactPaths: false,
1589
+ });
1590
+ }
1591
+ const live = summary.live || buildLiveSessionView(summary);
1592
+ const active = live.activeSession;
1593
+ const lines = [];
1594
+ lines.push("# Prismo Watch Report");
1595
+ lines.push("");
1596
+ lines.push(`Generated: ${summary.generatedAt}`);
1597
+ lines.push(`Repo: ${summary.scannedPath}`);
1598
+ lines.push("");
1599
+ lines.push("## Context Pressure");
1600
+ lines.push("");
1601
+ lines.push(`- Pressure: ${live.contextPressure}`);
1602
+ if (active) {
1603
+ lines.push(`- Session size: ${formatTokenCount(active.tokens)} tokens (${active.confidence})`);
1604
+ lines.push(`- Recent growth: +${formatTokenCount(active.recentContextGrowth || 0)} tokens`);
1605
+ lines.push(`- Tool/output tokens: ${formatTokenCount(active.estimatedToolTokens)}`);
1606
+ lines.push(`- Turns: ${active.turns}`);
1607
+ lines.push(`- Tool calls: ${active.toolCalls}`);
1608
+ if (active.model) lines.push(`- Model: ${active.model}`);
1609
+ if (active.updatedAt) lines.push(`- Updated: ${active.updatedAt}`);
1610
+ } else {
1611
+ lines.push("- No active local Codex/Claude Code session detected for this repo.");
1612
+ }
1613
+ lines.push("");
1614
+ lines.push("## Warnings");
1615
+ lines.push("");
1616
+ if (live.warnings.length) live.warnings.forEach((warning) => lines.push(`- ${warning}`));
1617
+ else lines.push("- No major context-pressure signals detected.");
1618
+ lines.push("");
1619
+ lines.push("## Do This Now");
1620
+ lines.push("");
1621
+ lines.push(`- Cause: ${live.liveAction.cause} (${live.liveAction.confidence} confidence)`);
1622
+ lines.push(`- ${live.liveAction.summary}`);
1623
+ live.liveAction.now.forEach((step) => lines.push(`- ${step}`));
1624
+ if (summary.guardrailsPath) {
1625
+ lines.push(`- Guardrails: ${summary.guardrailsPath}`);
1626
+ lines.push(`- Rescue prompt: ${summary.rescuePath}`);
1627
+ }
1628
+ if (summary.throttlePath) lines.push(`- Context throttle: ${summary.throttlePath}`);
1629
+ if (summary.firewallPath) lines.push(`- Context firewall: ${summary.firewallPath}`);
1630
+ if (summary.eventsPath) lines.push(`- Event log: ${summary.eventsPath}`);
1631
+ if (active) {
1632
+ lines.push("");
1633
+ lines.push("## Signals");
1634
+ lines.push("");
1635
+ const signals = [
1636
+ ...((active.actionableRepeatedPaths || getActionableRepeatedPaths(active)) || []).map((item) => `Repeated file: ${item.value} (${item.count}x)`),
1637
+ ...((active.generatedArtifactGroups || summarizeGeneratedArtifacts(active.generatedArtifacts || [])) || []).map((group) => `Generated artifacts likely entered context: ${group.type} (${group.count} mentions)`),
1638
+ ...(active.repeatedCommands || []).map((item) => `Repeated command: ${item.value} (${item.count}x)`),
1639
+ ];
1640
+ if (signals.length) signals.forEach((signal) => lines.push(`- ${signal}`));
1641
+ else lines.push("- No repeated path, generated artifact, or loop signals detected.");
1642
+ if (active.largestTextBlobs.length) {
1643
+ lines.push("");
1644
+ lines.push("## Largest Context Sources");
1645
+ lines.push("");
1646
+ active.largestTextBlobs.forEach((blob) => lines.push(`- ${blob.label}: ~${blob.tokens.toLocaleString()} tokens`));
1647
+ }
1648
+ }
1649
+ lines.push("");
1650
+ lines.push("## Suggested Action");
1651
+ lines.push("");
1652
+ lines.push(`Run: ${live.recommendedAction}`);
1653
+ lines.push("");
1654
+ lines.push("## Notes");
1655
+ lines.push("");
1656
+ lines.push("Prismo Watch uses local coding-agent logs. Exactness depends on which fields Codex or Claude Code wrote to disk, so warnings use likely/suspicious language when visibility is limited.");
1657
+ lines.push("");
1658
+ return lines.join("\n");
1659
+ }
1660
+
1661
+ function writeWatchReport(summary) {
1662
+ const written = writeGeneratedFile(summary.scannedPath || process.cwd(), path.join(".prismo", "watch-report.md"), renderWatchReport(summary));
1663
+ return written.path;
1664
+ }
1665
+
1666
+ function compactWatchSession(session) {
1667
+ return {
1668
+ tool: session.tool,
1669
+ sessionId: session.sessionId,
1670
+ title: session.title,
1671
+ cwd: session.cwd,
1672
+ model: session.model,
1673
+ startedAt: session.startedAt,
1674
+ updatedAt: session.updatedAt,
1675
+ turns: session.turns,
1676
+ toolCalls: session.toolCalls,
1677
+ toolResults: session.toolResults,
1678
+ displayTokens: session.displayTokens,
1679
+ contextTokens: session.contextTokens,
1680
+ estimatedToolTokens: session.estimatedToolTokens,
1681
+ exactInputTokens: session.exactInputTokens || 0,
1682
+ exactOutputTokens: session.exactOutputTokens || 0,
1683
+ exactCacheReadTokens: session.exactCacheReadTokens || 0,
1684
+ exactCacheCreationTokens: session.exactCacheCreationTokens || 0,
1685
+ exactTotalTokens: session.exactTotalTokens || 0,
1686
+ exactAvailable: session.exactAvailable,
1687
+ confidence: session.confidence,
1688
+ contextRisk: session.contextRisk,
1689
+ recentContextGrowth: session.recentContextGrowth || 0,
1690
+ repeatedPathMentions: session.repeatedPathMentions || [],
1691
+ actionableRepeatedPaths: getActionableRepeatedPaths(session, 5),
1692
+ generatedArtifacts: session.generatedArtifacts || [],
1693
+ generatedArtifactGroups: summarizeGeneratedArtifacts(session.generatedArtifacts || [], 5),
1694
+ repeatedCommands: session.repeatedCommands || [],
1695
+ loopSuspicion: Boolean(session.loopSuspicion),
1696
+ loopConfidence: session.loopConfidence || "low",
1697
+ largestTextBlobs: session.largestTextBlobs || [],
1698
+ };
1699
+ }
1700
+
1701
+ function compactUsageSummary(summary) {
1702
+ if (!summary) return null;
1703
+ return {
1704
+ generatedAt: summary.generatedAt,
1705
+ scannedPath: summary.scannedPath,
1706
+ tool: summary.tool,
1707
+ confidence: summary.confidence,
1708
+ totals: summary.totals,
1709
+ sources: summary.sources,
1710
+ sessions: (summary.sessions || []).map(compactWatchSession),
1711
+ };
1712
+ }
1713
+
1714
+ function redactPathValue(value) {
1715
+ const text = String(value || "");
1716
+ if (!text) return text;
1717
+ const normalized = text.replace(/\\/g, "/");
1718
+ const cwd = String(process.cwd() || "").replace(/\\/g, "/");
1719
+ if (cwd && normalized.startsWith(`${cwd}/`)) return normalized.slice(cwd.length + 1);
1720
+ if (normalized.startsWith("/")) return path.basename(normalized);
1721
+ return normalized.replace(/^Users\/[^/]+\//, "").replace(/^home\/[^/]+\//, "");
1722
+ }
1723
+
1724
+ function redactWatchPayload(payload) {
1725
+ const clone = JSON.parse(JSON.stringify(payload));
1726
+ clone.scannedPath = clone.scannedPath ? "[repo]" : clone.scannedPath;
1727
+ const redactItems = (items) => (items || []).map((item) => ({ ...item, value: redactPathValue(item.value) }));
1728
+ const redactGroups = (groups) => (groups || []).map((group) => ({
1729
+ ...group,
1730
+ examples: (group.examples || []).map(redactPathValue),
1731
+ }));
1732
+ for (const session of clone.sessions || []) {
1733
+ session.cwd = session.cwd ? "[repo]" : session.cwd;
1734
+ session.repeatedPathMentions = redactItems(session.repeatedPathMentions);
1735
+ session.actionableRepeatedPaths = redactItems(session.actionableRepeatedPaths);
1736
+ session.generatedArtifacts = redactItems(session.generatedArtifacts);
1737
+ session.generatedArtifactGroups = redactGroups(session.generatedArtifactGroups);
1738
+ }
1739
+ if (clone.live?.activeSession) {
1740
+ clone.live.activeSession.cwd = clone.live.activeSession.cwd ? "[repo]" : clone.live.activeSession.cwd;
1741
+ clone.live.activeSession.repeatedPathMentions = redactItems(clone.live.activeSession.repeatedPathMentions);
1742
+ clone.live.activeSession.actionableRepeatedPaths = redactItems(clone.live.activeSession.actionableRepeatedPaths);
1743
+ clone.live.activeSession.generatedArtifacts = redactItems(clone.live.activeSession.generatedArtifacts);
1744
+ clone.live.activeSession.generatedArtifactGroups = redactGroups(clone.live.activeSession.generatedArtifactGroups);
1745
+ }
1746
+ if (clone.live?.warnings) {
1747
+ clone.live.warnings = clone.live.warnings.map((warning) => warning.replace(/Users\/[^ ]+/g, "[path]").replace(/\/Users\/[^ ]+/g, "[path]"));
1748
+ }
1749
+ return clone;
1750
+ }
1751
+
1752
+ function toWatchJsonPayload(summary) {
1753
+ const payload = {
1754
+ schemaVersion: 1,
1755
+ generatedAt: summary.generatedAt,
1756
+ scannedPath: summary.scannedPath,
1757
+ tool: summary.tool,
1758
+ confidence: summary.confidence,
1759
+ totals: summary.totals,
1760
+ sources: summary.sources,
1761
+ sessions: summary.sessions.map(compactWatchSession),
1762
+ live: summary.live || buildLiveSessionView(summary),
1763
+ auto: Boolean(summary.auto),
1764
+ rescuePrompt: summary.includeRescuePrompt ? renderRescuePrompt(summary) : null,
1765
+ guardrailsPath: summary.guardrailsPath || null,
1766
+ rescuePath: summary.rescuePath || null,
1767
+ throttlePath: summary.throttlePath || null,
1768
+ firewallPath: summary.firewallPath || null,
1769
+ eventsPath: summary.eventsPath || null,
1770
+ reportPath: summary.reportPath || null,
1771
+ };
1772
+ return summary.redactPaths ? redactWatchPayload(payload) : payload;
1773
+ }
1774
+
1775
+ async function watchUsage(options = {}) {
1776
+ const intervalMs = options.intervalMs || 3000;
1777
+ const iterations = options.once ? 1 : Number.POSITIVE_INFINITY;
1778
+ for (let i = 0; i < iterations; i += 1) {
1779
+ const summary = getUsageSummary(options);
1780
+ summary.auto = Boolean(options.auto);
1781
+ summary.live = buildLiveSessionView(summary);
1782
+ if (options.guardrails) {
1783
+ const written = writeLiveGuardrails(summary);
1784
+ summary.guardrailsPath = written.guardrailsPath;
1785
+ summary.rescuePath = written.rescuePath;
1786
+ }
1787
+ if (options.throttle) {
1788
+ summary.throttlePath = writeContextThrottle(summary);
1789
+ }
1790
+ if (options.updateFirewall) {
1791
+ const firewall = options.updateFirewall(summary);
1792
+ if (firewall && firewall.generatedFiles) summary.firewallPath = ".prismo/context-firewall.md";
1793
+ }
1794
+ if (options.events) {
1795
+ summary.eventsPath = writeWatchEvent(summary);
1796
+ }
1797
+ if (options.report) {
1798
+ summary.redactPaths = Boolean(options.redactPaths);
1799
+ summary.reportPath = writeWatchReport(summary);
1800
+ }
1801
+ summary.redactPaths = Boolean(options.redactPaths);
1802
+ summary.includeRescuePrompt = Boolean(options.rescue);
1803
+ if (options.rescue && !options.json) {
1804
+ console.log(renderRescuePrompt(summary));
1805
+ } else if (options.json) {
1806
+ console.log(JSON.stringify(toWatchJsonPayload(summary), null, 2));
1807
+ } else {
1808
+ console.clear();
1809
+ const terminalSummary = options.redactPaths ? { ...summary, ...redactWatchPayload(toWatchJsonPayload(summary)) } : summary;
1810
+ console.log(renderWatchTerminal(terminalSummary));
1811
+ if (options.guardrails) console.log(`\nGuardrails: ${summary.guardrailsPath}\nRescue prompt: ${summary.rescuePath}`);
1812
+ if (options.throttle) console.log(`\nContext throttle: ${summary.throttlePath}`);
1813
+ if (summary.firewallPath) console.log(`\nContext firewall: ${summary.firewallPath}`);
1814
+ if (options.events && summary.eventsPath) console.log(`\nEvent log: ${summary.eventsPath}`);
1815
+ if (options.report) console.log(`\nReport: ${summary.reportPath}`);
1816
+ if (!options.once) console.log(`\nRefreshing every ${Math.round(intervalMs / 1000)}s. Press Ctrl+C to stop.`);
1817
+ }
1818
+ if (i + 1 >= iterations) break;
1819
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1820
+ }
1821
+ }
1822
+
1823
+
1824
+ return {
1825
+ analyzeSessionFile,
1826
+ calculateClaudeCost,
1827
+ compactUsageSummary,
1828
+ formatMoney,
1829
+ formatTokenCount,
1830
+ getClaudeCodeCostSummary,
1831
+ getClaudeSessionFiles,
1832
+ getCodexSessionFiles,
1833
+ getUsageSummary,
1834
+ getPositionals,
1835
+ parsePositiveInt,
1836
+ parseScopeAndTarget,
1837
+ renderClaudeCostTerminal,
1838
+ renderUsageTerminal,
1839
+ renderContextThrottle,
1840
+ buildWatchEvent,
1841
+ renderRescuePrompt,
1842
+ renderLiveGuardrails,
1843
+ renderWatchReport,
1844
+ renderWatchTerminal,
1845
+ toWatchJsonPayload,
1846
+ watchUsage,
1847
+ writeContextThrottle,
1848
+ writeLiveGuardrails,
1849
+ writeWatchEvent,
1850
+ writeWatchReport,
1851
+ };
1852
+ };