/** * Spending Guard Extension * * Tracks LLM spending per task and pauses when a configurable threshold is reached, * asking the user whether to continue, refine the prompt, or stop. * * Also maintains project-level and global spending totals that persist across * sessions and auto-update on every cost received. * * **New in 1.2:** Tracks tool executions from any extension (built-in or third-party), * capturing invocation counts and nested LLM costs incurred during tool runs. * Also tracks session compactions (manual/auto) — each compaction fires a * summary LLM call that costs money. * * ## Configuration * * Reads config from (in priority order): * 1. .pi/spending-guard.json (project-local) * 2. ~/.pi/agent/spending-guard.json (global) * * If no config file exists, defaults to **enabled** with a **$3.00 limit**. * * Config format: * { * "enabled": true, * "limit": 5.00, * "trackProject": true, * "trackGlobal": true, * "trackTools": true * } * * ## Placement * * - Global: ~/.pi/agent/extensions/spending-guard.ts * - Project: .pi/extensions/spending-guard.ts * * ## Commands * * /spending - Show current spending and limit (all totals) * /spending limit [amount] - Set or view the spending limit * /spending toggle [on|off] - Enable/disable tracking (no arg toggles) * /spending track project [on|off] - Enable/disable project tracking * /spending track global [on|off] - Enable/disable global tracking * /spending track tools [on|off] - Enable/disable tool execution tracking * /spending project - Show project-level total * /spending global - Show global total * /spending tools - Show per-tool execution stats * /spending reset [project|global|session|tools|meta] - Reset a total * /spending-limit [amount] - Shortcut: set/view limit * /spending-toggle [on|off] - Shortcut: enable/disable * /spending-status - Shortcut: show status */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; // ── Configuration ────────────────────────────────────────────────── interface GuardConfig { enabled: boolean; limit: number; /** Track cumulative project-level spending (persists across sessions). */ trackProject: boolean; /** Track cumulative global spending across all projects. */ trackGlobal: boolean; /** Track tool executions from third-party extensions (count + nested costs). */ trackTools: boolean; } interface SpendingTotal { total: number; lastUpdated: number; sessions: number; } interface ToolSpendingEntry { toolName: string; cost: number; callCount: number; lastSeen: number; } interface SessionMeta { compactionCount: number; manualCompactions: number; autoCompactions: number; } const DEFAULT_CONFIG: GuardConfig = { enabled: true, limit: 3.0, trackProject: true, trackGlobal: true, trackTools: true, }; const EMPTY_TOTAL: SpendingTotal = { total: 0, lastUpdated: 0, sessions: 0 }; // ── Config file loading ──────────────────────────────────────────── function loadConfig(cwd: string): GuardConfig { const paths = [ join(cwd, CONFIG_DIR_NAME, "spending-guard.json"), join(getAgentDir(), "spending-guard.json"), ]; for (const configPath of paths) { if (existsSync(configPath)) { try { const raw = readFileSync(configPath, "utf-8"); const parsed = JSON.parse(raw) as Partial; const config: GuardConfig = { ...DEFAULT_CONFIG }; if (typeof parsed.enabled === "boolean") { config.enabled = parsed.enabled; } if (typeof parsed.limit === "number" && parsed.limit >= 0) { config.limit = parsed.limit; } if (typeof parsed.trackProject === "boolean") { config.trackProject = parsed.trackProject; } if (typeof parsed.trackGlobal === "boolean") { config.trackGlobal = parsed.trackGlobal; } if (typeof parsed.trackTools === "boolean") { config.trackTools = parsed.trackTools; } return config; } catch { // Invalid JSON – fall through } } } return { ...DEFAULT_CONFIG }; } // ── Project & global total persistence ───────────────────────────── function projectTotalPath(cwd: string): string { return join(cwd, CONFIG_DIR_NAME, "spending-guard-project-total.json"); } function globalTotalPath(): string { return join(getAgentDir(), "spending-guard-global-total.json"); } function loadProjectTotal(cwd: string): SpendingTotal { const path = projectTotalPath(cwd); try { if (existsSync(path)) { const raw = readFileSync(path, "utf-8"); const parsed = JSON.parse(raw) as Partial; return { total: typeof parsed.total === "number" ? parsed.total : 0, lastUpdated: typeof parsed.lastUpdated === "number" ? parsed.lastUpdated : 0, sessions: typeof parsed.sessions === "number" ? parsed.sessions : 0, }; } } catch { // Invalid JSON – ignore } return { ...EMPTY_TOTAL }; } function loadGlobalTotal(): SpendingTotal { const path = globalTotalPath(); try { if (existsSync(path)) { const raw = readFileSync(path, "utf-8"); const parsed = JSON.parse(raw) as Partial; return { total: typeof parsed.total === "number" ? parsed.total : 0, lastUpdated: typeof parsed.lastUpdated === "number" ? parsed.lastUpdated : 0, sessions: typeof parsed.sessions === "number" ? parsed.sessions : 0, }; } } catch { // Invalid JSON – ignore } return { ...EMPTY_TOTAL }; } function saveProjectTotal(cwd: string, total: SpendingTotal): void { try { const dir = join(cwd, CONFIG_DIR_NAME); mkdirSync(dir, { recursive: true }); writeFileSync( projectTotalPath(cwd), JSON.stringify(total, null, 2), "utf-8", ); } catch { // Silently ignore } } function saveGlobalTotal(total: SpendingTotal): void { try { mkdirSync(getAgentDir(), { recursive: true }); writeFileSync( globalTotalPath(), JSON.stringify(total, null, 2), "utf-8", ); } catch { // Silently ignore } } // ── Helpers ──────────────────────────────────────────────────────── const ENTRY_TYPE = "spending-guard-state"; function percent(used: number, limit: number): string { return limit > 0 ? ` (${((used / limit) * 100).toFixed(1)}%)` : ""; } function statusLabel(config: GuardConfig, cost: number): string { return `💵 $${cost.toFixed(2)} / $${config.limit.toFixed(2)}`; } function serializeToolSpending(map: Map): Record { const obj: Record = {}; for (const [key, val] of map) { obj[key] = val; } return obj; } function deserializeToolSpending(data: Record): Map { const map = new Map(); for (const [key, val] of Object.entries(data)) { map.set(key, val); } return map; } // ── Extension ────────────────────────────────────────────────────── export default function (pi: ExtensionAPI) { let config: GuardConfig = { ...DEFAULT_CONFIG }; let accumulatedCost = 0; let projectTotal: SpendingTotal = { ...EMPTY_TOTAL }; let globalTotal: SpendingTotal = { ...EMPTY_TOTAL }; let toolSpending = new Map(); let sessionMeta: SessionMeta = { compactionCount: 0, manualCompactions: 0, autoCompactions: 0 }; let isPaused = false; let configLoaded = false; // ── Shared helpers ─────────────────────────────────────────────── function persist() { try { pi.appendEntry(ENTRY_TYPE, { limit: config.limit, enabled: config.enabled, trackProject: config.trackProject, trackGlobal: config.trackGlobal, trackTools: config.trackTools, accumulatedCost, toolSpending: config.trackTools ? serializeToolSpending(toolSpending) : undefined, sessionMeta, }); } catch { // Silently ignore } } function updateFooter(ctx: { ui: { setStatus: (key: string, text: string | undefined) => void }; }) { const parts: string[] = []; if (config.enabled) { parts.push(statusLabel(config, accumulatedCost)); } if (config.trackProject) { parts.push(`📁 $${projectTotal.total.toFixed(2)}`); } if (config.trackGlobal) { parts.push(`🌍 $${globalTotal.total.toFixed(2)}`); } if (sessionMeta.compactionCount > 0) { parts.push(`🗜️ ${sessionMeta.compactionCount}c`); } if (config.trackTools && toolSpending.size > 0) { let totalToolCost = 0; let totalToolCalls = 0; for (const entry of toolSpending.values()) { totalToolCost += entry.cost; totalToolCalls += entry.callCount; } if (totalToolCalls > 0) { parts.push(`🔧 ${totalToolCalls} calls $${totalToolCost.toFixed(3)}`); } } ctx.ui.setStatus( "spending-guard", parts.length > 0 ? parts.join(" ") : undefined, ); } // ── Session lifecycle ──────────────────────────────────────────── pi.on("session_start", async (event, ctx) => { if (!configLoaded) { config = loadConfig(ctx.cwd); configLoaded = true; } // Restore session state from entries for (const entry of ctx.sessionManager.getEntries()) { if (entry.type === "custom" && entry.customType === ENTRY_TYPE) { const data = entry.data as { limit?: number; enabled?: boolean; trackProject?: boolean; trackGlobal?: boolean; trackTools?: boolean; accumulatedCost?: number; toolSpending?: Record; }; if (typeof data.limit === "number") config.limit = data.limit; if (typeof data.enabled === "boolean") config.enabled = data.enabled; if (typeof data.trackProject === "boolean") config.trackProject = data.trackProject; if (typeof data.trackGlobal === "boolean") config.trackGlobal = data.trackGlobal; if (typeof data.trackTools === "boolean") config.trackTools = data.trackTools; if (typeof data.accumulatedCost === "number") accumulatedCost = data.accumulatedCost; if (data.toolSpending) { toolSpending = deserializeToolSpending(data.toolSpending); } if (data.sessionMeta) { sessionMeta = data.sessionMeta as SessionMeta; } } } // Load project & global totals from disk projectTotal = loadProjectTotal(ctx.cwd); globalTotal = loadGlobalTotal(); // Bump session counter for fresh sessions if (event.reason === "startup" || event.reason === "new") { if (config.trackProject) { projectTotal.sessions += 1; projectTotal.lastUpdated = Date.now(); saveProjectTotal(ctx.cwd, projectTotal); } if (config.trackGlobal) { globalTotal.sessions += 1; globalTotal.lastUpdated = Date.now(); saveGlobalTotal(globalTotal); } // Reset per-session state for fresh sessions accumulatedCost = 0; toolSpending = new Map(); sessionMeta = { compactionCount: 0, manualCompactions: 0, autoCompactions: 0 }; } isPaused = false; updateFooter(ctx); }); pi.on("session_shutdown", async () => { isPaused = false; }); // ── Cost tracking ──────────────────────────────────────────────── pi.on("message_end", async (event, ctx) => { if (event.message.role !== "assistant") return; const cost = event.message.usage?.cost?.total; if (cost === undefined || cost === 0) return; // Always update project & global totals (independent of session enabled) if (config.trackProject) { projectTotal.total += cost; projectTotal.lastUpdated = Date.now(); saveProjectTotal(ctx.cwd, projectTotal); } if (config.trackGlobal) { globalTotal.total += cost; globalTotal.lastUpdated = Date.now(); saveGlobalTotal(globalTotal); } // Session tracking (with threshold dialog) if (!config.enabled || isPaused) { updateFooter(ctx); return; } accumulatedCost += cost; updateFooter(ctx); persist(); if (accumulatedCost >= config.limit) { await handleThreshold(ctx); } }); // ── Tool execution tracking ────────────────────────────────────── pi.on("tool_execution_start", async (event, ctx) => { if (!config.trackTools) return; let entry = toolSpending.get(event.toolName); if (!entry) { entry = { toolName: event.toolName, cost: 0, callCount: 0, lastSeen: 0, }; } entry.callCount += 1; entry.lastSeen = Date.now(); toolSpending.set(event.toolName, entry); updateFooter(ctx); persist(); }); pi.on("tool_result", async (event, ctx) => { if (!config.trackTools) return; // Capture nested LLM costs incurred during tool execution const nestedCost = event.usage?.cost?.total; if (nestedCost === undefined || nestedCost <= 0) return; // Update per-tool stats let entry = toolSpending.get(event.toolName); if (!entry) { entry = { toolName: event.toolName, cost: 0, callCount: 0, lastSeen: Date.now(), }; } entry.cost += nestedCost; entry.lastSeen = Date.now(); toolSpending.set(event.toolName, entry); // Also add to project & global totals if (config.trackProject) { projectTotal.total += nestedCost; projectTotal.lastUpdated = Date.now(); saveProjectTotal(ctx.cwd, projectTotal); } if (config.trackGlobal) { globalTotal.total += nestedCost; globalTotal.lastUpdated = Date.now(); saveGlobalTotal(globalTotal); } // Count against session threshold if (config.enabled && !isPaused) { accumulatedCost += nestedCost; } // Persist tool spending + accumulated cost changes (always, even when paused) persist(); updateFooter(ctx); // Check threshold after persisting state if (config.enabled && !isPaused && accumulatedCost >= config.limit) { await handleThreshold(ctx); } }); // ── Compaction tracking ────────────────────────────────────────── pi.on("session_compact", async (event, ctx) => { sessionMeta.compactionCount += 1; if (event.reason === "manual") { sessionMeta.manualCompactions += 1; } else { sessionMeta.autoCompactions += 1; } updateFooter(ctx); persist(); }); // ── Threshold dialog ───────────────────────────────────────────── async function handleThreshold(ctx: { ui: { select: ( title: string, options: string[], ) => Promise; input: ( title: string, placeholder?: string, ) => Promise; notify: (message: string, level: "info" | "warning" | "error") => void; setStatus: (key: string, text: string | undefined) => void; }; hasUI: boolean; shutdown: () => void; }) { isPaused = true; if (!ctx.hasUI) { ctx.ui.notify( `⚠️ Spending limit reached: $${accumulatedCost.toFixed(2)} / $${config.limit.toFixed(2)}. Auto-doubling limit.`, "warning", ); config.limit = config.limit * 2; isPaused = false; return; } const choice = await ctx.ui.select( `⚠️ Spending limit reached: $${accumulatedCost.toFixed(2)} of $${config.limit.toFixed(2)}\n\n` + `What would you like to do?`, [ "Continue (double the limit)", "Continue (reset counter)", "Refine the prompt", "Stop the task", ], ); switch (choice) { case "Continue (double the limit)": { config.limit = config.limit * 2; ctx.ui.notify( `Limit increased to $${config.limit.toFixed(2)}`, "info", ); updateFooter(ctx); persist(); break; } case "Continue (reset counter)": { accumulatedCost = 0; ctx.ui.notify("Counter reset", "info"); updateFooter(ctx); persist(); break; } case "Refine the prompt": { accumulatedCost = 0; const newPrompt = await ctx.ui.input( "Enter your refined prompt (press Enter to stop):", "Be more concise and efficient…", ); if (newPrompt) { ctx.ui.notify("Sending refined prompt…", "info"); updateFooter(ctx); persist(); pi.sendUserMessage(newPrompt, { deliverAs: "steer" }); } else { ctx.ui.notify("Task stopped by user", "warning"); ctx.shutdown(); return; } break; } case "Stop the task": default: { ctx.ui.notify("Task stopped (spending limit)", "warning"); ctx.shutdown(); return; } } isPaused = false; } // ── General /spending command ──────────────────────────────────── pi.registerCommand("spending", { description: "Spending guard hub. Subcommands: limit, toggle, track, tools, project, global, reset, status. Run /spending alone for status.", handler: async (args, ctx) => { const parts = (args ?? "").trim().split(/\s+/); const sub = parts[0]?.toLowerCase() ?? ""; const rest = parts.slice(1).join(" "); switch (sub) { // ── /spending limit [amount] ── case "limit": { if (rest) { const parsed = parseFloat(rest); if (isNaN(parsed) || parsed < 0) { ctx.ui.notify( "Invalid amount. Usage: /spending limit 5.00", "error", ); return; } config.limit = parsed; ctx.ui.notify( `Limit set to $${config.limit.toFixed(2)}`, "info", ); updateFooter(ctx); persist(); } else { ctx.ui.notify( `Limit: $${config.limit.toFixed(2)} | Spent: $${accumulatedCost.toFixed(2)}${percent(accumulatedCost, config.limit)}`, accumulatedCost >= config.limit ? "warning" : "info", ); } return; } // ── /spending toggle [on|off] ── case "toggle": { const val = rest.toLowerCase(); if (val === "on" || val === "enable") { config.enabled = true; } else if (val === "off" || val === "disable") { config.enabled = false; } else if (val === "") { config.enabled = !config.enabled; } else { ctx.ui.notify("Usage: /spending toggle [on|off]", "error"); return; } ctx.ui.notify( `Spending guard ${config.enabled ? "enabled" : "disabled"}`, "info", ); updateFooter(ctx); persist(); return; } // ── /spending track [project|global|tools] [on|off] ── case "track": { const target = parts[1]?.toLowerCase() ?? ""; const val = parts[2]?.toLowerCase() ?? ""; if (target === "project") { if (val === "on" || val === "enable") { config.trackProject = true; } else if (val === "off" || val === "disable") { config.trackProject = false; } else if (val === "") { config.trackProject = !config.trackProject; } else { ctx.ui.notify( "Usage: /spending track project [on|off]", "error", ); return; } ctx.ui.notify( `Project tracking ${config.trackProject ? "enabled" : "disabled"}`, "info", ); updateFooter(ctx); persist(); } else if (target === "global") { if (val === "on" || val === "enable") { config.trackGlobal = true; } else if (val === "off" || val === "disable") { config.trackGlobal = false; } else if (val === "") { config.trackGlobal = !config.trackGlobal; } else { ctx.ui.notify( "Usage: /spending track global [on|off]", "error", ); return; } ctx.ui.notify( `Global tracking ${config.trackGlobal ? "enabled" : "disabled"}`, "info", ); updateFooter(ctx); persist(); } else if (target === "tools") { if (val === "on" || val === "enable") { config.trackTools = true; } else if (val === "off" || val === "disable") { config.trackTools = false; } else if (val === "") { config.trackTools = !config.trackTools; } else { ctx.ui.notify( "Usage: /spending track tools [on|off]", "error", ); return; } ctx.ui.notify( `Tool tracking ${config.trackTools ? "enabled" : "disabled"}`, "info", ); updateFooter(ctx); persist(); } else { ctx.ui.notify( "Usage: /spending track project|global|tools [on|off]", "error", ); } return; } // ── /spending tools ── case "tools": { if (!config.trackTools) { ctx.ui.notify( "Tool tracking is disabled. Enable with /spending track tools on", "warning", ); return; } if (toolSpending.size === 0) { ctx.ui.notify("🔧 No tool executions recorded yet this session.", "info"); return; } // Sort by cost descending, then by callCount descending const sorted = [...toolSpending.values()].sort( (a, b) => b.cost - a.cost || b.callCount - a.callCount, ); const lines = ["🔧 Tool Execution Stats"]; for (const entry of sorted) { const costStr = entry.cost > 0 ? ` $${entry.cost.toFixed(4)}` : ""; lines.push( ` ${entry.toolName}: ${entry.callCount} call${entry.callCount !== 1 ? "s" : ""}${costStr}`, ); } let totalCalls = 0; let totalCost = 0; for (const entry of toolSpending.values()) { totalCalls += entry.callCount; totalCost += entry.cost; } lines.push( ` ─────────────────────`, ); lines.push( ` Total: ${totalCalls} call${totalCalls !== 1 ? "s" : ""} across ${ toolSpending.size } tool${toolSpending.size !== 1 ? "s" : ""}${totalCost > 0 ? `, $${totalCost.toFixed(4)} nested cost` : ""}`, ); ctx.ui.notify(lines.join("\n"), "info"); return; } // ── /spending project ── case "project": { const info = projectTotal.total > 0 ? `📁 Project total: $${projectTotal.total.toFixed(2)} (${projectTotal.sessions} sessions)` : "📁 Project total: $0.00 (no sessions yet)"; ctx.ui.notify(info, "info"); return; } // ── /spending global ── case "global": { const info = globalTotal.total > 0 ? `🌍 Global total: $${globalTotal.total.toFixed(2)} (${globalTotal.sessions} sessions)` : "🌍 Global total: $0.00 (no sessions yet)"; ctx.ui.notify(info, "info"); return; } // ── /spending reset [project|global|session|tools] ── case "reset": { const target = rest.toLowerCase(); if (target === "project") { projectTotal = { total: 0, lastUpdated: Date.now(), sessions: 0 }; saveProjectTotal(ctx.cwd, projectTotal); ctx.ui.notify("📁 Project total reset", "info"); updateFooter(ctx); return; } if (target === "global") { globalTotal = { total: 0, lastUpdated: Date.now(), sessions: 0 }; saveGlobalTotal(globalTotal); ctx.ui.notify("🌍 Global total reset", "info"); updateFooter(ctx); return; } if (target === "tools") { toolSpending = new Map(); ctx.ui.notify("🔧 Tool execution stats reset", "info"); updateFooter(ctx); persist(); return; } if (target === "meta") { sessionMeta = { compactionCount: 0, manualCompactions: 0, autoCompactions: 0 }; ctx.ui.notify("🗜️ Session meta reset", "info"); updateFooter(ctx); persist(); return; } if (target === "session" || target === "") { accumulatedCost = 0; ctx.ui.notify("💵 Session counter reset", "info"); updateFooter(ctx); persist(); return; } ctx.ui.notify( `Unknown reset target "${target}". Try: session, project, global, tools, meta`, "error", ); return; } // ── /spending status ── case "status": case "": { const state = config.enabled ? "enabled" : "paused"; const lines: string[] = [ `💵 ${state} | $${accumulatedCost.toFixed(2)} of $${config.limit.toFixed(2)}${percent(accumulatedCost, config.limit)}`, ]; if (config.trackProject) { lines.push( `📁 Project: $${projectTotal.total.toFixed(2)} (${projectTotal.sessions} sessions)`, ); } if (config.trackGlobal) { lines.push( `🌍 Global: $${globalTotal.total.toFixed(2)} (${globalTotal.sessions} sessions)`, ); } if (config.trackTools) { let totalCalls = 0; let totalCost = 0; for (const entry of toolSpending.values()) { totalCalls += entry.callCount; totalCost += entry.cost; } lines.push( `🔧 Tools: ${totalCalls} calls across ${toolSpending.size} tool${toolSpending.size !== 1 ? "s" : ""}${totalCost > 0 ? `, $${totalCost.toFixed(4)} cost` : ""}`, ); } if (sessionMeta.compactionCount > 0) { const manual = sessionMeta.manualCompactions; const auto = sessionMeta.autoCompactions; const parts: string[] = [`🗜️ Compactions: ${sessionMeta.compactionCount}`]; if (manual > 0) parts.push(`${manual} manual`); if (auto > 0) parts.push(`${auto} auto`); lines.push(parts.join(" ")); } ctx.ui.notify( lines.join("\n"), accumulatedCost >= config.limit ? "warning" : "info", ); return; } default: { ctx.ui.notify( `Unknown subcommand "${sub}". Try: limit, toggle, track, tools, project, global, reset, status`, "error", ); } } }, }); // ── Shortcut commands ──────────────────────────────────────────── pi.registerCommand("spending-limit", { description: "Shortcut for /spending limit. Usage: /spending-limit [amount]", handler: async (args, ctx) => { const trimmed = args?.trim(); if (trimmed) { const parsed = parseFloat(trimmed); if (isNaN(parsed) || parsed < 0) { ctx.ui.notify( "Invalid amount. Usage: /spending-limit 5.00", "error", ); return; } config.limit = parsed; ctx.ui.notify(`Limit set to $${config.limit.toFixed(2)}`, "info"); updateFooter(ctx); persist(); } else { ctx.ui.notify( `Limit: $${config.limit.toFixed(2)} | Spent: $${accumulatedCost.toFixed(2)}${percent(accumulatedCost, config.limit)}`, accumulatedCost >= config.limit ? "warning" : "info", ); } }, }); pi.registerCommand("spending-toggle", { description: "Shortcut for /spending toggle. Usage: /spending-toggle [on|off]", handler: async (args, ctx) => { const val = (args ?? "").trim().toLowerCase(); if (val === "on" || val === "enable") { config.enabled = true; } else if (val === "off" || val === "disable") { config.enabled = false; } else if (val === "") { config.enabled = !config.enabled; } else { ctx.ui.notify("Usage: /spending-toggle [on|off]", "error"); return; } ctx.ui.notify( `Spending guard ${config.enabled ? "enabled" : "disabled"}`, "info", ); updateFooter(ctx); persist(); }, }); pi.registerCommand("spending-status", { description: "Shortcut for /spending status. Show current spending, all totals, tool stats, and compactions.", handler: async (_args, ctx) => { const state = config.enabled ? "enabled" : "paused"; const lines: string[] = [ `💵 ${state} | $${accumulatedCost.toFixed(2)} of $${config.limit.toFixed(2)}${percent(accumulatedCost, config.limit)}`, ]; if (config.trackProject) { lines.push( `📁 Project: $${projectTotal.total.toFixed(2)} (${projectTotal.sessions} sessions)`, ); } if (config.trackGlobal) { lines.push( `🌍 Global: $${globalTotal.total.toFixed(2)} (${globalTotal.sessions} sessions)`, ); } if (config.trackTools) { let totalCalls = 0; let totalCost = 0; for (const entry of toolSpending.values()) { totalCalls += entry.callCount; totalCost += entry.cost; } lines.push( `🔧 Tools: ${totalCalls} calls across ${toolSpending.size} tool${toolSpending.size !== 1 ? "s" : ""}${totalCost > 0 ? `, $${totalCost.toFixed(4)} cost` : ""}`, ); } if (sessionMeta.compactionCount > 0) { const manual = sessionMeta.manualCompactions; const auto = sessionMeta.autoCompactions; const parts: string[] = [`🗜️ Compactions: ${sessionMeta.compactionCount}`]; if (manual > 0) parts.push(`${manual} manual`); if (auto > 0) parts.push(`${auto} auto`); lines.push(parts.join(" ")); } ctx.ui.notify( lines.join("\n"), accumulatedCost >= config.limit ? "warning" : "info", ); }, }); }