@cocaxcode/token-optimizer-mcp
Advanced tools
| #!/usr/bin/env node | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-DK5JDNWQ.js"; | ||
| import "./chunk-OIZX7KF2.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| projectHash, | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/budget.ts | ||
| function runBudgetCli(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const db = getDb(dbPath); | ||
| const mgr = new BudgetManager(db); | ||
| const hash = projectHash(projectDir); | ||
| const sub = args[0]; | ||
| if (sub === "set") { | ||
| const scope = args[1]; | ||
| const limitRaw = args[2]; | ||
| const limit = limitRaw ? parseInt(limitRaw, 10) : NaN; | ||
| if (scope !== "session" && scope !== "project" || !Number.isFinite(limit)) { | ||
| print("Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| try { | ||
| const budget = mgr.setBudget({ | ||
| scope, | ||
| scope_key: scopeKey, | ||
| limit_tokens: limit | ||
| }); | ||
| print( | ||
| `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}` | ||
| ); | ||
| return 0; | ||
| } catch (e) { | ||
| print(`Error: ${e instanceof Error ? e.message : String(e)}`); | ||
| return 1; | ||
| } | ||
| } | ||
| if (sub === "get") { | ||
| const status = mgr.checkBudget("default", hash); | ||
| if (!status.active) { | ||
| print("Sin presupuesto activo"); | ||
| return 0; | ||
| } | ||
| const pct = (status.percent_used * 100).toFixed(1); | ||
| print( | ||
| `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? "n/a"}` | ||
| ); | ||
| return 0; | ||
| } | ||
| if (sub === "clear") { | ||
| const scope = args[1]; | ||
| if (scope !== "session" && scope !== "project") { | ||
| print("Uso: token-optimizer-mcp budget clear <session|project>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| const removed = mgr.clearBudget(scope, scopeKey); | ||
| print(removed ? `Eliminado (${scope})` : "No habia presupuesto para este scope"); | ||
| return 0; | ||
| } | ||
| print("Uso: token-optimizer-mcp budget <set|get|clear> [args]"); | ||
| return 1; | ||
| } | ||
| export { | ||
| runBudgetCli | ||
| }; | ||
| //# sourceMappingURL=budget-NQVRS4O7.js.map |
| {"version":3,"sources":["../src/cli/budget.ts"],"sourcesContent":["// Budget CLI — Phase 4.15\n// Thin wrapper delegating to BudgetManager. Subcommands: set / get / clear.\n\nimport { getDb } from '../db/connection.js'\nimport {\n resolveProjectDir,\n resolveAnalyticsDbPath,\n projectHash,\n} from '../lib/paths.js'\nimport { BudgetManager } from '../services/budget-manager.js'\nimport type { BudgetScope } from '../lib/types.js'\n\nexport interface BudgetCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runBudgetCli(args: string[] = [], opts: BudgetCliOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n const db = getDb(dbPath)\n const mgr = new BudgetManager(db)\n const hash = projectHash(projectDir)\n\n const sub = args[0]\n\n if (sub === 'set') {\n const scope = args[1] as BudgetScope | undefined\n const limitRaw = args[2]\n const limit = limitRaw ? parseInt(limitRaw, 10) : NaN\n if ((scope !== 'session' && scope !== 'project') || !Number.isFinite(limit)) {\n print('Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>')\n return 1\n }\n const scopeKey = scope === 'session' ? 'default' : hash\n try {\n const budget = mgr.setBudget({\n scope,\n scope_key: scopeKey,\n limit_tokens: limit,\n })\n print(\n `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}`,\n )\n return 0\n } catch (e) {\n print(`Error: ${e instanceof Error ? e.message : String(e)}`)\n return 1\n }\n }\n\n if (sub === 'get') {\n const status = mgr.checkBudget('default', hash)\n if (!status.active) {\n print('Sin presupuesto activo')\n return 0\n }\n const pct = (status.percent_used * 100).toFixed(1)\n print(\n `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? 'n/a'}`,\n )\n return 0\n }\n\n if (sub === 'clear') {\n const scope = args[1] as BudgetScope | undefined\n if (scope !== 'session' && scope !== 'project') {\n print('Uso: token-optimizer-mcp budget clear <session|project>')\n return 1\n }\n const scopeKey = scope === 'session' ? 'default' : hash\n const removed = mgr.clearBudget(scope, scopeKey)\n print(removed ? `Eliminado (${scope})` : 'No habia presupuesto para este scope')\n return 0\n }\n\n print('Uso: token-optimizer-mcp budget <set|get|clear> [args]')\n return 1\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBO,SAAS,aAAa,OAAiB,CAAC,GAAG,OAAyB,CAAC,GAAW;AACrF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,QAAM,KAAK,MAAM,MAAM;AACvB,QAAM,MAAM,IAAI,cAAc,EAAE;AAChC,QAAM,OAAO,YAAY,UAAU;AAEnC,QAAM,MAAM,KAAK,CAAC;AAElB,MAAI,QAAQ,OAAO;AACjB,UAAM,QAAQ,KAAK,CAAC;AACpB,UAAM,WAAW,KAAK,CAAC;AACvB,UAAM,QAAQ,WAAW,SAAS,UAAU,EAAE,IAAI;AAClD,QAAK,UAAU,aAAa,UAAU,aAAc,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3E,YAAM,sEAAsE;AAC5E,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,QAAI;AACF,YAAM,SAAS,IAAI,UAAU;AAAA,QAC3B;AAAA,QACA,WAAW;AAAA,QACX,cAAc;AAAA,MAChB,CAAC;AACD;AAAA,QACE,yBAAyB,OAAO,KAAK,IAAI,OAAO,SAAS,UAAU,OAAO,YAAY,SAAS,OAAO,IAAI;AAAA,MAC5G;AACA,aAAO;AAAA,IACT,SAAS,GAAG;AACV,YAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AAC5D,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,QAAQ,OAAO;AACjB,UAAM,SAAS,IAAI,YAAY,WAAW,IAAI;AAC9C,QAAI,CAAC,OAAO,QAAQ;AAClB,YAAM,wBAAwB;AAC9B,aAAO;AAAA,IACT;AACA,UAAM,OAAO,OAAO,eAAe,KAAK,QAAQ,CAAC;AACjD;AAAA,MACE,WAAW,OAAO,KAAK,aAAa,OAAO,SAAS,QAAQ,GAAG,UAAU,OAAO,QAAQ,KAAK;AAAA,IAC/F;AACA,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,SAAS;AACnB,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,UAAU,aAAa,UAAU,WAAW;AAC9C,YAAM,yDAAyD;AAC/D,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,UAAM,UAAU,IAAI,YAAY,OAAO,QAAQ;AAC/C,UAAM,UAAU,cAAc,KAAK,MAAM,sCAAsC;AAC/E,WAAO;AAAA,EACT;AAEA,QAAM,wDAAwD;AAC9D,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveXrayUrl | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-OIZX7KF2.js"; | ||
| import { | ||
| resolveTranscriptPath | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/coach/knowledge-base.ts | ||
| var KNOWLEDGE_BASE = [ | ||
| { | ||
| id: "use-opusplan", | ||
| title: "Usa /opusplan para planificar con Opus y ejecutar con Sonnet", | ||
| description: "opusplan usa Opus durante plan mode para razonamiento complejo y vuelve a Sonnet para implementacion. Solo pagas Opus en la fase de planning.", | ||
| savings_estimate: "60-80% de reduccion de coste en sesiones con planning intensivo", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "/model opusplan", | ||
| when_applicable: "Sesiones con razonamiento largo antes de codigo", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-long-reasoning-no-code" | ||
| }, | ||
| { | ||
| id: "use-plan-mode", | ||
| title: "Activa plan mode para exploracion sin escribir codigo", | ||
| description: "EnterPlanMode permite razonar y explorar sin hacer ediciones, reduciendo iteraciones costosas.", | ||
| savings_estimate: "Variable segun tarea", | ||
| savings_source: "internal", | ||
| how_to_invoke: "EnterPlanMode tool", | ||
| when_applicable: "Tareas no triviales antes de escribir codigo", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-long-reasoning-no-code" | ||
| }, | ||
| { | ||
| id: "use-fast-mode", | ||
| title: "Activa /fast para respuestas mas directas", | ||
| description: "Modo rapido mantiene el modelo pero reduce el detalle de las respuestas.", | ||
| savings_estimate: "Reduce tiempo principalmente", | ||
| savings_source: "internal", | ||
| how_to_invoke: "/fast", | ||
| when_applicable: "Cuando quieres respuestas mas concisas", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "default-to-sonnet", | ||
| title: "Arranca cada sesion con Sonnet y sube a Opus solo cuando haga falta", | ||
| description: "Sonnet resuelve ~80% de tareas de coding bien. El switching tactico a Opus solo en razonamiento complejo ahorra el grueso del coste.", | ||
| savings_estimate: "60-80% reduccion de coste total", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "/model sonnet (inicio) \u2192 /model opus (cuando sea necesario)", | ||
| when_applicable: "Siempre como default", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-opus-for-simple-task" | ||
| }, | ||
| { | ||
| id: "use-haiku-for-simple", | ||
| title: "Usa Haiku para formato, Q&A simple y tareas de alto volumen", | ||
| description: "Haiku es mucho mas barato y rapido. Para formateo, preguntas puntuales o tareas repetitivas es el modelo adecuado.", | ||
| savings_estimate: "~90% reduccion vs Opus en tareas simples", | ||
| savings_source: "anthropic-docs", | ||
| how_to_invoke: "/model haiku", | ||
| when_applicable: "Formateo, Q&A simple, alto volumen", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-opus-for-simple-task" | ||
| }, | ||
| { | ||
| id: "use-compact-long-session", | ||
| title: "Corre /compact cuando el contexto supere el 75%", | ||
| description: "/compact genera un resumen del contexto actual liberando ~60-80% de la ventana sin perder continuidad.", | ||
| savings_estimate: "60-80% de contexto liberado", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "/compact", | ||
| when_applicable: "Contexto > 75% de la ventana", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-context-threshold" | ||
| }, | ||
| { | ||
| id: "use-clear-rename-resume", | ||
| title: "Usa /rename \u2192 /clear \u2192 /resume para pivotes de tema", | ||
| description: "Al cambiar a un tema no relacionado, renombra la sesion, haz /clear para empezar limpio, y resume cuando vuelvas.", | ||
| savings_estimate: "Variable segun contexto descartado", | ||
| savings_source: "internal", | ||
| how_to_invoke: "/rename <nombre> \u2192 /clear \u2192 (trabajar) \u2192 /resume <nombre>", | ||
| when_applicable: "Pivote total a tema no relacionado", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-clear-opportunity" | ||
| }, | ||
| { | ||
| id: "use-sessionstart-compact-hook", | ||
| title: "Activa el hook SessionStart:compact de token-optimizer", | ||
| description: "Cuando Claude Code compacta el contexto, token-optimizer inyecta un resumen con archivos, comandos y presupuesto.", | ||
| savings_estimate: "Evita re-lectura tras compactacion", | ||
| savings_source: "internal", | ||
| how_to_invoke: "token-optimizer-mcp install (ya lo configura)", | ||
| when_applicable: "Siempre como parte del install", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-memory-save", | ||
| title: "Guarda decisiones con mem_save antes de compactar", | ||
| description: "Persistir decisiones arquitectonicas en engram evita tener que re-derivarlas cuando el contexto se compacta.", | ||
| savings_estimate: "Variable", | ||
| savings_source: "internal", | ||
| how_to_invoke: "mem_save (via engram MCP)", | ||
| when_applicable: "Antes de /compact o cambiar de sesion", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-agent-explore", | ||
| title: "Delega busquedas amplias al subagente Explore", | ||
| description: "El subagente Explore tiene su propio contexto y no consume el de la sesion principal. Ideal para buscar en muchos archivos.", | ||
| savings_estimate: "Aisla contexto al subagente", | ||
| savings_source: "internal", | ||
| how_to_invoke: 'Agent tool con subagent_type="Explore"', | ||
| when_applicable: "3+ busquedas Grep/Glob similares", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-repeated-searches" | ||
| }, | ||
| { | ||
| id: "use-todowrite-long-task", | ||
| title: "Usa TodoWrite para tareas multi-paso", | ||
| description: "TodoWrite mantiene el estado de la tarea sin re-leer archivos, reduciendo redundancia.", | ||
| savings_estimate: "Evita re-lectura de estado", | ||
| savings_source: "internal", | ||
| how_to_invoke: "TodoWrite", | ||
| when_applicable: "3+ pasos independientes", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-skill-trigger", | ||
| title: "Invoca skills en lugar de re-derivar instrucciones", | ||
| description: "Los skills cargan instrucciones especializadas solo cuando se invocan. Mejor que un CLAUDE.md monolitico.", | ||
| savings_estimate: "~15k tokens/sesion con progressive disclosure", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "Skill tool con nombre del skill", | ||
| when_applicable: "Tareas que matchean un skill disponible", | ||
| source_type: "skill", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-skill-trigger-ignored" | ||
| }, | ||
| { | ||
| id: "install-serena", | ||
| title: "Instala serena-mcp para lecturas simbolicas", | ||
| description: "serena usa LSP para leer solo los simbolos que necesitas en lugar del archivo completo. Nota: incluye execute_shell_command.", | ||
| savings_estimate: "20-30% en lecturas de archivos grandes", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "uvx --from git+https://github.com/oraios/serena serena start-mcp-server", | ||
| when_applicable: "Proyectos con archivos >50k tokens", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-huge-file-reads" | ||
| }, | ||
| { | ||
| id: "prefer-serena-reads", | ||
| title: "Usa Serena en vez de Read para archivos de codigo", | ||
| description: "Serena lee simbolos (funciones, clases) sin cargar el archivo completo. Usa get_symbols_overview para explorar y find_symbol con include_body para leer solo lo que necesitas. Ahorro tipico: 60-90% vs Read.", | ||
| savings_estimate: "60-90% en lecturas de codigo", | ||
| savings_source: "internal", | ||
| how_to_invoke: "get_symbols_overview(path) \u2192 find_symbol(name, include_body=true)", | ||
| when_applicable: "Archivos .ts/.js/.py/.java >50 lineas donde solo necesitas 1-2 funciones", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-12", | ||
| detector_id: "detect-read-over-serena" | ||
| }, | ||
| { | ||
| id: "install-rtk", | ||
| title: "Instala RTK para filtrar salida ruidosa de Bash", | ||
| description: "RTK filtra output de builds/tests antes de llegar a Claude Code. Publica releases firmadas con GPG.", | ||
| savings_estimate: "15-25% en ciclos build/test", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "brew install standard-input/tap/rtk (macOS) o binario firmado en github.com/standard-input/rtk", | ||
| when_applicable: "Proyectos con builds/tests ruidosos", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-many-bash-commands" | ||
| }, | ||
| { | ||
| id: "use-mcp-prune", | ||
| title: "Aplica un allowlist de MCPs por proyecto", | ||
| description: "Reduce el coste del tool-schema excluyendo MCPs que no usas en este proyecto. ~5-12% adicional sobre Tool Search.", | ||
| savings_estimate: "5-12% por turno sobre Tool Search nativo", | ||
| savings_source: "internal", | ||
| how_to_invoke: "mcp_prune_suggest \u2192 mcp_prune_apply", | ||
| when_applicable: "MCPs registrados pero no usados en el proyecto", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-unused-mcp-servers" | ||
| }, | ||
| { | ||
| id: "migrate-claudemd-to-skills", | ||
| title: "Migra CLAUDE.md grande a skills con progressive disclosure", | ||
| description: "Un CLAUDE.md monolitico se carga en cada sesion. Los skills solo cargan cuando se invocan. ~15k tokens recuperados.", | ||
| savings_estimate: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "Crear skills en .claude/skills/ con triggers especificos", | ||
| when_applicable: "CLAUDE.md > 10k tokens con uso parcial", | ||
| source_type: "skill", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-claudemd-bloat" | ||
| }, | ||
| { | ||
| id: "use-settings-local", | ||
| title: "Configuracion personal en settings.local.json", | ||
| description: "Evita contaminar settings.json del equipo. settings.local.json es personal y gitignored por defecto.", | ||
| savings_estimate: "Higiene, no tokens", | ||
| savings_source: "internal", | ||
| how_to_invoke: "Editar .claude/settings.local.json", | ||
| when_applicable: "Configuracion personal no compartible", | ||
| source_type: "settings", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-serena-overview-first", | ||
| title: "Usa get_symbols_overview antes de find_symbol", | ||
| description: "Llamar get_symbols_overview una vez da el mapa del archivo. Las llamadas sucesivas find_symbol sin overview previo leen el mismo archivo repetidamente.", | ||
| savings_estimate: "30-50% menos llamadas Serena por sesion", | ||
| savings_source: "internal", | ||
| how_to_invoke: "mcp__serena__get_symbols_overview con relative_path antes de find_symbol", | ||
| when_applicable: "Al explorar un archivo por primera vez en la sesion", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-15", | ||
| detector_id: "detect-serena-read-cascade" | ||
| }, | ||
| { | ||
| id: "use-prompt-caching", | ||
| title: "Estructura prompts para maximizar cache hits", | ||
| description: "Los tokens leidos del cache cuestan 10x menos. Mantener el prefijo estable (system, CLAUDE.md) aprovecha el cache.", | ||
| savings_estimate: "10x mas barato en reads cacheados", | ||
| savings_source: "anthropic-docs", | ||
| how_to_invoke: "Mantener prefijo estable entre turns", | ||
| when_applicable: "Siempre", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| } | ||
| ]; | ||
| // src/coach/rules.ts | ||
| function countMatching(events, predicate) { | ||
| let c = 0; | ||
| for (const e of events) if (predicate(e)) c++; | ||
| return c; | ||
| } | ||
| var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]); | ||
| var DETECTION_RULES = [ | ||
| // 1. detect-context-threshold | ||
| { | ||
| id: "detect-context-threshold", | ||
| tip_ids: ["use-compact-long-session"], | ||
| run(ctx) { | ||
| if (ctx.session_token_total === null || ctx.session_token_limit <= 0) return null; | ||
| const percent = ctx.session_token_total / ctx.session_token_limit; | ||
| if (percent < 0.5) return null; | ||
| let severity = "info"; | ||
| if (percent >= 0.9) severity = "critical"; | ||
| else if (percent >= 0.75) severity = "warn"; | ||
| return { | ||
| rule_id: "detect-context-threshold", | ||
| tip_ids: ["use-compact-long-session"], | ||
| severity, | ||
| evidence: `Contexto: ${(percent * 100).toFixed(1)}% usado (${ctx.session_token_total}/${ctx.session_token_limit} tokens)`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| }, | ||
| // 2. detect-long-reasoning-no-code | ||
| { | ||
| id: "detect-long-reasoning-no-code", | ||
| tip_ids: ["use-plan-mode", "use-opusplan"], | ||
| run(ctx) { | ||
| const recent = ctx.events.slice(0, 10); | ||
| if (recent.length < 10) return null; | ||
| const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name)); | ||
| if (edits > 0) return null; | ||
| return { | ||
| rule_id: "detect-long-reasoning-no-code", | ||
| tip_ids: ["use-plan-mode", "use-opusplan"], | ||
| severity: "info", | ||
| evidence: "10 eventos recientes sin ediciones de codigo", | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 3. detect-repeated-searches | ||
| { | ||
| id: "detect-repeated-searches", | ||
| tip_ids: ["use-agent-explore"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 20); | ||
| const searches = countMatching(window, (e) => e.tool_name === "Grep" || e.tool_name === "Glob"); | ||
| if (searches < 3) return null; | ||
| return { | ||
| rule_id: "detect-repeated-searches", | ||
| tip_ids: ["use-agent-explore"], | ||
| severity: "info", | ||
| evidence: `${searches} busquedas Grep/Glob en los ultimos 20 eventos`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 4. detect-huge-file-reads | ||
| { | ||
| id: "detect-huge-file-reads", | ||
| tip_ids: ["install-serena"], | ||
| run(ctx) { | ||
| const huge = ctx.events.find((e) => e.tool_name === "Read" && e.tokens_estimated > 5e4); | ||
| if (!huge) return null; | ||
| return { | ||
| rule_id: "detect-huge-file-reads", | ||
| tip_ids: ["install-serena"], | ||
| severity: "warn", | ||
| evidence: `Read consumio ${huge.tokens_estimated} tokens (umbral 50k)`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 5. detect-many-bash-commands | ||
| { | ||
| id: "detect-many-bash-commands", | ||
| tip_ids: ["install-rtk"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 100); | ||
| const bash = countMatching(window, (e) => e.tool_name === "Bash"); | ||
| if (bash <= 10) return null; | ||
| return { | ||
| rule_id: "detect-many-bash-commands", | ||
| tip_ids: ["install-rtk"], | ||
| severity: "info", | ||
| evidence: `${bash} comandos Bash en los ultimos ${window.length} eventos`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 6. detect-clear-opportunity (was #7 — detect-unused-mcp-servers stub removed) | ||
| { | ||
| id: "detect-clear-opportunity", | ||
| tip_ids: ["use-clear-rename-resume"], | ||
| run(ctx) { | ||
| if (ctx.events.length < 40) return null; | ||
| const recentTools = new Set(ctx.events.slice(0, 20).map((e) => e.tool_name)); | ||
| const priorTools = new Set(ctx.events.slice(20, 40).map((e) => e.tool_name)); | ||
| if (recentTools.size === 0) return null; | ||
| let overlap = 0; | ||
| for (const t of recentTools) if (priorTools.has(t)) overlap++; | ||
| const ratio = overlap / recentTools.size; | ||
| if (ratio >= 0.3) return null; | ||
| return { | ||
| rule_id: "detect-clear-opportunity", | ||
| tip_ids: ["use-clear-rename-resume"], | ||
| severity: "info", | ||
| evidence: `Solapamiento de herramientas ${(ratio * 100).toFixed(0)}% \u2014 posible pivote de tema`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 8. detect-opus-for-simple-task | ||
| { | ||
| id: "detect-opus-for-simple-task", | ||
| tip_ids: ["default-to-sonnet", "use-haiku-for-simple"], | ||
| run(ctx) { | ||
| if (!ctx.active_model || !/opus/i.test(ctx.active_model)) return null; | ||
| const recent = ctx.events.slice(0, 20); | ||
| if (recent.length < 6) return null; | ||
| const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name)); | ||
| const bash = countMatching(recent, (e) => e.tool_name === "Bash"); | ||
| if (edits + bash < 6) return null; | ||
| return { | ||
| rule_id: "detect-opus-for-simple-task", | ||
| tip_ids: ["default-to-sonnet", "use-haiku-for-simple"], | ||
| severity: "info", | ||
| evidence: `Opus ejecutando trabajo mecanico: ${edits} edits + ${bash} Bash en ultimos 20 eventos. Sonnet haria lo mismo un 80% mas barato.`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 9. detect-claudemd-bloat (stub — requires filesystem stat at runtime) | ||
| { | ||
| id: "detect-claudemd-bloat", | ||
| tip_ids: ["migrate-claudemd-to-skills"], | ||
| run() { | ||
| return null; | ||
| } | ||
| }, | ||
| // 10. detect-post-milestone-opportunity | ||
| { | ||
| id: "detect-post-milestone-opportunity", | ||
| tip_ids: ["use-compact-long-session"], | ||
| run(ctx) { | ||
| const recent = ctx.events.slice(0, 20); | ||
| const edits = countMatching(recent, (e) => e.tool_name === "Edit" || e.tool_name === "Write"); | ||
| const hasBash = countMatching(recent, (e) => e.tool_name === "Bash") > 0; | ||
| if (edits < 5 || !hasBash) return null; | ||
| if (ctx.session_token_total === null) return null; | ||
| const percent = ctx.session_token_total / ctx.session_token_limit; | ||
| if (percent < 0.4) return null; | ||
| return { | ||
| rule_id: "detect-post-milestone-opportunity", | ||
| tip_ids: ["use-compact-long-session"], | ||
| severity: "info", | ||
| evidence: `${edits} ediciones + Bash reciente + contexto ${(percent * 100).toFixed(0)}%`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| }, | ||
| // 11. detect-skill-trigger-ignored (stub — requires skill registry) | ||
| { | ||
| id: "detect-skill-trigger-ignored", | ||
| tip_ids: ["use-skill-trigger"], | ||
| run() { | ||
| return null; | ||
| } | ||
| }, | ||
| // 12. detect-serena-read-cascade | ||
| // Fires when the agent makes ≥5 find_symbol calls without a get_symbols_overview | ||
| // in the same window — suggests starting with an overview first. | ||
| { | ||
| id: "detect-serena-read-cascade", | ||
| tip_ids: ["use-serena-overview-first"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 15); | ||
| const findSymbolCount = countMatching( | ||
| window, | ||
| (e) => e.tool_name === "mcp__serena__find_symbol" | ||
| ); | ||
| if (findSymbolCount < 5) return null; | ||
| const hasOverview = window.some( | ||
| (e) => e.tool_name === "mcp__serena__get_symbols_overview" | ||
| ); | ||
| if (hasOverview) return null; | ||
| return { | ||
| rule_id: "detect-serena-read-cascade", | ||
| tip_ids: ["use-serena-overview-first"], | ||
| severity: "info", | ||
| evidence: `${findSymbolCount} llamadas find_symbol sin get_symbols_overview en los ultimos 15 eventos.`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| }, | ||
| // 13. detect-read-over-serena | ||
| { | ||
| id: "detect-read-over-serena", | ||
| tip_ids: ["prefer-serena-reads"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 30); | ||
| const largeReads = window.filter( | ||
| (e) => e.tool_name === "Read" && e.tokens_estimated > 2e3 | ||
| ); | ||
| if (largeReads.length < 3) return null; | ||
| const totalTokens = largeReads.reduce((sum, e) => sum + e.tokens_estimated, 0); | ||
| const estimatedSaving = Math.round(totalTokens * 0.7); | ||
| const severity = largeReads.length >= 6 ? "warn" : "info"; | ||
| return { | ||
| rule_id: "detect-read-over-serena", | ||
| tip_ids: ["prefer-serena-reads"], | ||
| severity, | ||
| evidence: `${largeReads.length} lecturas Read >2k tokens (total: ${totalTokens}). Serena ahorraria ~${estimatedSaving} tokens (~70%).`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| } | ||
| ]; | ||
| // src/coach/detector.ts | ||
| var SEVERITY_ORDER = { critical: 0, warn: 1, info: 2 }; | ||
| function runRules(ctx) { | ||
| const hits = []; | ||
| for (const rule of DETECTION_RULES) { | ||
| try { | ||
| const hit = rule.run(ctx); | ||
| if (hit) hits.push(hit); | ||
| } catch { | ||
| } | ||
| } | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| const unique = []; | ||
| for (const h of hits) { | ||
| if (seen.has(h.rule_id)) continue; | ||
| seen.add(h.rule_id); | ||
| unique.push(h); | ||
| } | ||
| unique.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99)); | ||
| return unique; | ||
| } | ||
| // src/coach/context-meter.ts | ||
| import fs from "fs"; | ||
| // src/services/xray-client.ts | ||
| var POST_TIMEOUT_MS = 500; | ||
| var GET_TIMEOUT_MS = 300; | ||
| var DEFAULT_LIMIT = 2e5; | ||
| var VERSION = true ? "0.4.21" : "0.1.0"; | ||
| async function postToXray(event, opts = {}) { | ||
| const xrayUrl = opts.xrayUrl ?? resolveXrayUrl(); | ||
| if (!xrayUrl) return false; | ||
| const fetchFn = opts.fetchImpl ?? fetch; | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? POST_TIMEOUT_MS); | ||
| try { | ||
| await fetchFn(`${xrayUrl}/hooks/token-optimizer`, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| source: "token-optimizer-mcp", | ||
| version: VERSION, | ||
| event | ||
| }), | ||
| signal: controller.signal | ||
| }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } | ||
| var SUMMARY_TIMEOUT_MS = 2e3; | ||
| async function postSummaryToXray(summary, opts = {}) { | ||
| const xrayUrl = opts.xrayUrl ?? resolveXrayUrl(); | ||
| if (!xrayUrl) return false; | ||
| const fetchFn = opts.fetchImpl ?? fetch; | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUMMARY_TIMEOUT_MS); | ||
| try { | ||
| await fetchFn(`${xrayUrl}/hooks/token-optimizer/summary`, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| source: "token-optimizer-mcp", | ||
| version: VERSION, | ||
| summary | ||
| }), | ||
| signal: controller.signal | ||
| }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } | ||
| async function getSessionTokens(sessionId, opts = {}) { | ||
| const xrayUrl = opts.xrayUrl ?? resolveXrayUrl(); | ||
| if (!xrayUrl) return null; | ||
| const fetchFn = opts.fetchImpl ?? fetch; | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? GET_TIMEOUT_MS); | ||
| try { | ||
| const res = await fetchFn( | ||
| `${xrayUrl}/sessions/${encodeURIComponent(sessionId)}/tokens`, | ||
| { signal: controller.signal } | ||
| ); | ||
| if (!res.ok) return null; | ||
| const data = await res.json(); | ||
| const tokens = data.tokens ?? 0; | ||
| const limit = data.limit ?? DEFAULT_LIMIT; | ||
| return { | ||
| tokens, | ||
| limit, | ||
| percent: limit > 0 ? tokens / limit : 0, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } | ||
| // src/coach/context-meter.ts | ||
| var DEFAULT_LIMIT2 = 2e5; | ||
| var OPUS_1M_LIMIT = 1e6; | ||
| var BASELINE_TOKENS = 15e3; | ||
| async function measureContextSize(sessionId, opts = {}) { | ||
| if (opts.projectDir) { | ||
| const transcript = readTranscript(opts.projectDir, sessionId); | ||
| if (transcript) return transcript; | ||
| } | ||
| const xrayResult = await tryXray(sessionId, opts.fetchImpl); | ||
| if (xrayResult) return xrayResult; | ||
| const limit = resolveLimit(opts.activeModel); | ||
| if (opts.db) { | ||
| return cumulativeEstimate(opts.db, sessionId, limit); | ||
| } | ||
| return { tokens: 0, limit, percent: 0, estimation_method: "unknown" }; | ||
| } | ||
| function readTranscript(projectDir, sessionId) { | ||
| try { | ||
| const p = resolveTranscriptPath(projectDir, sessionId); | ||
| if (!fs.existsSync(p)) return null; | ||
| const content = fs.readFileSync(p, "utf8"); | ||
| const lines = content.split("\n").filter((l) => l.trim().length > 0); | ||
| let totalTokens = 0; | ||
| let limit = DEFAULT_LIMIT2; | ||
| for (const line of lines) { | ||
| try { | ||
| const turn = JSON.parse(line); | ||
| if (turn.usage) { | ||
| totalTokens += (turn.usage.input_tokens ?? 0) + (turn.usage.output_tokens ?? 0) + (turn.usage.cache_read_input_tokens ?? 0); | ||
| } | ||
| if (turn.model && /1m/i.test(turn.model)) limit = OPUS_1M_LIMIT; | ||
| } catch { | ||
| } | ||
| } | ||
| return { | ||
| tokens: totalTokens, | ||
| limit, | ||
| percent: limit > 0 ? totalTokens / limit : 0, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| async function tryXray(sessionId, fetchImpl) { | ||
| const opts = {}; | ||
| if (fetchImpl !== void 0) opts.fetchImpl = fetchImpl; | ||
| return getSessionTokens(sessionId, opts); | ||
| } | ||
| function resolveLimit(activeModel) { | ||
| if (activeModel && /1m|opus/i.test(activeModel)) return OPUS_1M_LIMIT; | ||
| return DEFAULT_LIMIT2; | ||
| } | ||
| function cumulativeEstimate(db, sessionId, limit = DEFAULT_LIMIT2) { | ||
| const queries = buildQueries(db); | ||
| const sessionTokens = queries.sumTokensBySession(sessionId); | ||
| const total = sessionTokens + BASELINE_TOKENS; | ||
| return { | ||
| tokens: total, | ||
| limit, | ||
| percent: total / limit, | ||
| estimation_method: "estimated_cumulative" | ||
| }; | ||
| } | ||
| function measureContextSizeFromDbSync(db, sessionId, activeModel) { | ||
| const limit = resolveLimit(activeModel); | ||
| return cumulativeEstimate(db, sessionId, limit); | ||
| } | ||
| // src/coach/surface.ts | ||
| function checkDedupe(db, sessionId, ruleId, tipId, windowSeconds) { | ||
| const row = db.prepare( | ||
| `SELECT 1 FROM coach_surface_log | ||
| WHERE session_id = ? AND rule_id = ? AND tip_id = ? | ||
| AND created_at > datetime('now', ?) | ||
| LIMIT 1` | ||
| ).get(sessionId, ruleId, tipId, `-${windowSeconds} seconds`); | ||
| return row !== void 0 && row !== null; | ||
| } | ||
| function logSurface(db, sessionId, hit, via) { | ||
| db.prepare(`INSERT OR IGNORE INTO sessions (id) VALUES (?)`).run(sessionId); | ||
| const stmt = db.prepare( | ||
| `INSERT INTO coach_surface_log (session_id, rule_id, tip_id, surfaced_via, severity) | ||
| VALUES (?, ?, ?, ?, ?)` | ||
| ); | ||
| for (const tipId of hit.tip_ids) { | ||
| stmt.run(sessionId, hit.rule_id, tipId, via, hit.severity); | ||
| } | ||
| } | ||
| function surfaceWithDedupe(db, sessionId, hits, via, windowSeconds) { | ||
| const surfaced = []; | ||
| for (const hit of hits) { | ||
| const anyFresh = hit.tip_ids.some( | ||
| (tipId) => !checkDedupe(db, sessionId, hit.rule_id, tipId, windowSeconds) | ||
| ); | ||
| if (anyFresh) { | ||
| logSurface(db, sessionId, hit, via); | ||
| surfaced.push(hit); | ||
| } | ||
| } | ||
| return surfaced; | ||
| } | ||
| function getCoachSurfaceLog(db, sessionId) { | ||
| const rows = db.prepare( | ||
| `SELECT rule_id, tip_id, severity | ||
| FROM coach_surface_log | ||
| WHERE session_id = ? | ||
| ORDER BY created_at` | ||
| ).all(sessionId); | ||
| const map = /* @__PURE__ */ new Map(); | ||
| for (const row of rows) { | ||
| if (!map.has(row.rule_id)) { | ||
| map.set(row.rule_id, { tip_ids: [], severity: row.severity }); | ||
| } | ||
| const entry = map.get(row.rule_id); | ||
| if (!entry.tip_ids.includes(row.tip_id)) { | ||
| entry.tip_ids.push(row.tip_id); | ||
| } | ||
| } | ||
| return Array.from(map.entries()).map(([rule_id, v]) => ({ | ||
| rule_id, | ||
| tip_ids: v.tip_ids, | ||
| severity: v.severity | ||
| })); | ||
| } | ||
| function clearSurfaceLog(db, sessionId) { | ||
| if (sessionId) { | ||
| const info2 = db.prepare(`DELETE FROM coach_surface_log WHERE session_id = ?`).run(sessionId); | ||
| return info2.changes; | ||
| } | ||
| const info = db.prepare(`DELETE FROM coach_surface_log`).run(); | ||
| return info.changes; | ||
| } | ||
| export { | ||
| postToXray, | ||
| postSummaryToXray, | ||
| KNOWLEDGE_BASE, | ||
| runRules, | ||
| measureContextSize, | ||
| measureContextSizeFromDbSync, | ||
| surfaceWithDedupe, | ||
| getCoachSurfaceLog, | ||
| clearSurfaceLog | ||
| }; | ||
| //# sourceMappingURL=chunk-22BLF7AQ.js.map |
| {"version":3,"sources":["../src/coach/knowledge-base.ts","../src/coach/rules.ts","../src/coach/detector.ts","../src/coach/context-meter.ts","../src/services/xray-client.ts","../src/coach/surface.ts"],"sourcesContent":["// Static catalog of 18 CoachTip entries — Phase 4.40\n// See sdd/token-optimizer-v0.1/coach-layer-addendum CO-1\n\nimport type { CoachTip } from '../lib/types.js'\n\nexport const KNOWLEDGE_BASE: readonly CoachTip[] = [\n {\n id: 'use-opusplan',\n title: 'Usa /opusplan para planificar con Opus y ejecutar con Sonnet',\n description:\n 'opusplan usa Opus durante plan mode para razonamiento complejo y vuelve a Sonnet para implementacion. Solo pagas Opus en la fase de planning.',\n savings_estimate: '60-80% de reduccion de coste en sesiones con planning intensivo',\n savings_source: 'community-measured',\n how_to_invoke: '/model opusplan',\n when_applicable: 'Sesiones con razonamiento largo antes de codigo',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-long-reasoning-no-code',\n },\n {\n id: 'use-plan-mode',\n title: 'Activa plan mode para exploracion sin escribir codigo',\n description:\n 'EnterPlanMode permite razonar y explorar sin hacer ediciones, reduciendo iteraciones costosas.',\n savings_estimate: 'Variable segun tarea',\n savings_source: 'internal',\n how_to_invoke: 'EnterPlanMode tool',\n when_applicable: 'Tareas no triviales antes de escribir codigo',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-long-reasoning-no-code',\n },\n {\n id: 'use-fast-mode',\n title: 'Activa /fast para respuestas mas directas',\n description: 'Modo rapido mantiene el modelo pero reduce el detalle de las respuestas.',\n savings_estimate: 'Reduce tiempo principalmente',\n savings_source: 'internal',\n how_to_invoke: '/fast',\n when_applicable: 'Cuando quieres respuestas mas concisas',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'default-to-sonnet',\n title: 'Arranca cada sesion con Sonnet y sube a Opus solo cuando haga falta',\n description:\n 'Sonnet resuelve ~80% de tareas de coding bien. El switching tactico a Opus solo en razonamiento complejo ahorra el grueso del coste.',\n savings_estimate: '60-80% reduccion de coste total',\n savings_source: 'community-measured',\n how_to_invoke: '/model sonnet (inicio) → /model opus (cuando sea necesario)',\n when_applicable: 'Siempre como default',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-opus-for-simple-task',\n },\n {\n id: 'use-haiku-for-simple',\n title: 'Usa Haiku para formato, Q&A simple y tareas de alto volumen',\n description:\n 'Haiku es mucho mas barato y rapido. Para formateo, preguntas puntuales o tareas repetitivas es el modelo adecuado.',\n savings_estimate: '~90% reduccion vs Opus en tareas simples',\n savings_source: 'anthropic-docs',\n how_to_invoke: '/model haiku',\n when_applicable: 'Formateo, Q&A simple, alto volumen',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-opus-for-simple-task',\n },\n {\n id: 'use-compact-long-session',\n title: 'Corre /compact cuando el contexto supere el 75%',\n description:\n '/compact genera un resumen del contexto actual liberando ~60-80% de la ventana sin perder continuidad.',\n savings_estimate: '60-80% de contexto liberado',\n savings_source: 'community-measured',\n how_to_invoke: '/compact',\n when_applicable: 'Contexto > 75% de la ventana',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-context-threshold',\n },\n {\n id: 'use-clear-rename-resume',\n title: 'Usa /rename → /clear → /resume para pivotes de tema',\n description:\n 'Al cambiar a un tema no relacionado, renombra la sesion, haz /clear para empezar limpio, y resume cuando vuelvas.',\n savings_estimate: 'Variable segun contexto descartado',\n savings_source: 'internal',\n how_to_invoke: '/rename <nombre> → /clear → (trabajar) → /resume <nombre>',\n when_applicable: 'Pivote total a tema no relacionado',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-clear-opportunity',\n },\n {\n id: 'use-sessionstart-compact-hook',\n title: 'Activa el hook SessionStart:compact de token-optimizer',\n description:\n 'Cuando Claude Code compacta el contexto, token-optimizer inyecta un resumen con archivos, comandos y presupuesto.',\n savings_estimate: 'Evita re-lectura tras compactacion',\n savings_source: 'internal',\n how_to_invoke: 'token-optimizer-mcp install (ya lo configura)',\n when_applicable: 'Siempre como parte del install',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-memory-save',\n title: 'Guarda decisiones con mem_save antes de compactar',\n description:\n 'Persistir decisiones arquitectonicas en engram evita tener que re-derivarlas cuando el contexto se compacta.',\n savings_estimate: 'Variable',\n savings_source: 'internal',\n how_to_invoke: 'mem_save (via engram MCP)',\n when_applicable: 'Antes de /compact o cambiar de sesion',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-agent-explore',\n title: 'Delega busquedas amplias al subagente Explore',\n description:\n 'El subagente Explore tiene su propio contexto y no consume el de la sesion principal. Ideal para buscar en muchos archivos.',\n savings_estimate: 'Aisla contexto al subagente',\n savings_source: 'internal',\n how_to_invoke: 'Agent tool con subagent_type=\"Explore\"',\n when_applicable: '3+ busquedas Grep/Glob similares',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-repeated-searches',\n },\n {\n id: 'use-todowrite-long-task',\n title: 'Usa TodoWrite para tareas multi-paso',\n description:\n 'TodoWrite mantiene el estado de la tarea sin re-leer archivos, reduciendo redundancia.',\n savings_estimate: 'Evita re-lectura de estado',\n savings_source: 'internal',\n how_to_invoke: 'TodoWrite',\n when_applicable: '3+ pasos independientes',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-skill-trigger',\n title: 'Invoca skills en lugar de re-derivar instrucciones',\n description:\n 'Los skills cargan instrucciones especializadas solo cuando se invocan. Mejor que un CLAUDE.md monolitico.',\n savings_estimate: '~15k tokens/sesion con progressive disclosure',\n savings_source: 'community-measured',\n how_to_invoke: 'Skill tool con nombre del skill',\n when_applicable: 'Tareas que matchean un skill disponible',\n source_type: 'skill',\n verified_at: '2026-04-11',\n detector_id: 'detect-skill-trigger-ignored',\n },\n {\n id: 'install-serena',\n title: 'Instala serena-mcp para lecturas simbolicas',\n description:\n 'serena usa LSP para leer solo los simbolos que necesitas en lugar del archivo completo. Nota: incluye execute_shell_command.',\n savings_estimate: '20-30% en lecturas de archivos grandes',\n savings_source: 'community-measured',\n how_to_invoke: 'uvx --from git+https://github.com/oraios/serena serena start-mcp-server',\n when_applicable: 'Proyectos con archivos >50k tokens',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: 'detect-huge-file-reads',\n },\n {\n id: 'prefer-serena-reads',\n title: 'Usa Serena en vez de Read para archivos de codigo',\n description:\n 'Serena lee simbolos (funciones, clases) sin cargar el archivo completo. Usa get_symbols_overview para explorar y find_symbol con include_body para leer solo lo que necesitas. Ahorro tipico: 60-90% vs Read.',\n savings_estimate: '60-90% en lecturas de codigo',\n savings_source: 'internal',\n how_to_invoke: 'get_symbols_overview(path) → find_symbol(name, include_body=true)',\n when_applicable: 'Archivos .ts/.js/.py/.java >50 lineas donde solo necesitas 1-2 funciones',\n source_type: 'mcp',\n verified_at: '2026-04-12',\n detector_id: 'detect-read-over-serena',\n },\n {\n id: 'install-rtk',\n title: 'Instala RTK para filtrar salida ruidosa de Bash',\n description:\n 'RTK filtra output de builds/tests antes de llegar a Claude Code. Publica releases firmadas con GPG.',\n savings_estimate: '15-25% en ciclos build/test',\n savings_source: 'community-measured',\n how_to_invoke: 'brew install standard-input/tap/rtk (macOS) o binario firmado en github.com/standard-input/rtk',\n when_applicable: 'Proyectos con builds/tests ruidosos',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: 'detect-many-bash-commands',\n },\n {\n id: 'use-mcp-prune',\n title: 'Aplica un allowlist de MCPs por proyecto',\n description:\n 'Reduce el coste del tool-schema excluyendo MCPs que no usas en este proyecto. ~5-12% adicional sobre Tool Search.',\n savings_estimate: '5-12% por turno sobre Tool Search nativo',\n savings_source: 'internal',\n how_to_invoke: 'mcp_prune_suggest → mcp_prune_apply',\n when_applicable: 'MCPs registrados pero no usados en el proyecto',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: 'detect-unused-mcp-servers',\n },\n {\n id: 'migrate-claudemd-to-skills',\n title: 'Migra CLAUDE.md grande a skills con progressive disclosure',\n description:\n 'Un CLAUDE.md monolitico se carga en cada sesion. Los skills solo cargan cuando se invocan. ~15k tokens recuperados.',\n savings_estimate: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n savings_source: 'community-measured',\n how_to_invoke: 'Crear skills en .claude/skills/ con triggers especificos',\n when_applicable: 'CLAUDE.md > 10k tokens con uso parcial',\n source_type: 'skill',\n verified_at: '2026-04-11',\n detector_id: 'detect-claudemd-bloat',\n },\n {\n id: 'use-settings-local',\n title: 'Configuracion personal en settings.local.json',\n description:\n 'Evita contaminar settings.json del equipo. settings.local.json es personal y gitignored por defecto.',\n savings_estimate: 'Higiene, no tokens',\n savings_source: 'internal',\n how_to_invoke: 'Editar .claude/settings.local.json',\n when_applicable: 'Configuracion personal no compartible',\n source_type: 'settings',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-serena-overview-first',\n title: 'Usa get_symbols_overview antes de find_symbol',\n description:\n 'Llamar get_symbols_overview una vez da el mapa del archivo. Las llamadas sucesivas find_symbol sin overview previo leen el mismo archivo repetidamente.',\n savings_estimate: '30-50% menos llamadas Serena por sesion',\n savings_source: 'internal',\n how_to_invoke: 'mcp__serena__get_symbols_overview con relative_path antes de find_symbol',\n when_applicable: 'Al explorar un archivo por primera vez en la sesion',\n source_type: 'mcp',\n verified_at: '2026-04-15',\n detector_id: 'detect-serena-read-cascade',\n },\n {\n id: 'use-prompt-caching',\n title: 'Estructura prompts para maximizar cache hits',\n description:\n 'Los tokens leidos del cache cuestan 10x menos. Mantener el prefijo estable (system, CLAUDE.md) aprovecha el cache.',\n savings_estimate: '10x mas barato en reads cacheados',\n savings_source: 'anthropic-docs',\n how_to_invoke: 'Mantener prefijo estable entre turns',\n when_applicable: 'Siempre',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n]\n","// Detection rules registry (11 rules) — Phase 4.43\n// Each rule is a pure function over EventContext returning DetectionHit | null.\n// Rules MUST NOT throw; the orchestrator catches everything.\n\nimport type { DetectionRule, DetectionSeverity, ToolEvent } from '../lib/types.js'\n\nfunction countMatching(events: readonly ToolEvent[], predicate: (e: ToolEvent) => boolean): number {\n let c = 0\n for (const e of events) if (predicate(e)) c++\n return c\n}\n\nconst EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])\n\nexport const DETECTION_RULES: readonly DetectionRule[] = [\n // 1. detect-context-threshold\n {\n id: 'detect-context-threshold',\n tip_ids: ['use-compact-long-session'],\n run(ctx) {\n if (ctx.session_token_total === null || ctx.session_token_limit <= 0) return null\n const percent = ctx.session_token_total / ctx.session_token_limit\n if (percent < 0.5) return null\n let severity: DetectionSeverity = 'info'\n if (percent >= 0.9) severity = 'critical'\n else if (percent >= 0.75) severity = 'warn'\n return {\n rule_id: 'detect-context-threshold',\n tip_ids: ['use-compact-long-session'],\n severity,\n evidence: `Contexto: ${(percent * 100).toFixed(1)}% usado (${ctx.session_token_total}/${ctx.session_token_limit} tokens)`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n\n // 2. detect-long-reasoning-no-code\n {\n id: 'detect-long-reasoning-no-code',\n tip_ids: ['use-plan-mode', 'use-opusplan'],\n run(ctx) {\n const recent = ctx.events.slice(0, 10)\n if (recent.length < 10) return null\n const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name))\n if (edits > 0) return null\n return {\n rule_id: 'detect-long-reasoning-no-code',\n tip_ids: ['use-plan-mode', 'use-opusplan'],\n severity: 'info',\n evidence: '10 eventos recientes sin ediciones de codigo',\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 3. detect-repeated-searches\n {\n id: 'detect-repeated-searches',\n tip_ids: ['use-agent-explore'],\n run(ctx) {\n const window = ctx.events.slice(0, 20)\n const searches = countMatching(window, (e) => e.tool_name === 'Grep' || e.tool_name === 'Glob')\n if (searches < 3) return null\n return {\n rule_id: 'detect-repeated-searches',\n tip_ids: ['use-agent-explore'],\n severity: 'info',\n evidence: `${searches} busquedas Grep/Glob en los ultimos 20 eventos`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 4. detect-huge-file-reads\n {\n id: 'detect-huge-file-reads',\n tip_ids: ['install-serena'],\n run(ctx) {\n const huge = ctx.events.find((e) => e.tool_name === 'Read' && e.tokens_estimated > 50_000)\n if (!huge) return null\n return {\n rule_id: 'detect-huge-file-reads',\n tip_ids: ['install-serena'],\n severity: 'warn',\n evidence: `Read consumio ${huge.tokens_estimated} tokens (umbral 50k)`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 5. detect-many-bash-commands\n {\n id: 'detect-many-bash-commands',\n tip_ids: ['install-rtk'],\n run(ctx) {\n const window = ctx.events.slice(0, 100)\n const bash = countMatching(window, (e) => e.tool_name === 'Bash')\n if (bash <= 10) return null\n return {\n rule_id: 'detect-many-bash-commands',\n tip_ids: ['install-rtk'],\n severity: 'info',\n evidence: `${bash} comandos Bash en los ultimos ${window.length} eventos`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 6. detect-clear-opportunity (was #7 — detect-unused-mcp-servers stub removed)\n {\n id: 'detect-clear-opportunity',\n tip_ids: ['use-clear-rename-resume'],\n run(ctx) {\n if (ctx.events.length < 40) return null\n const recentTools = new Set(ctx.events.slice(0, 20).map((e) => e.tool_name))\n const priorTools = new Set(ctx.events.slice(20, 40).map((e) => e.tool_name))\n if (recentTools.size === 0) return null\n let overlap = 0\n for (const t of recentTools) if (priorTools.has(t)) overlap++\n const ratio = overlap / recentTools.size\n if (ratio >= 0.3) return null\n return {\n rule_id: 'detect-clear-opportunity',\n tip_ids: ['use-clear-rename-resume'],\n severity: 'info',\n evidence: `Solapamiento de herramientas ${(ratio * 100).toFixed(0)}% — posible pivote de tema`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 8. detect-opus-for-simple-task\n {\n id: 'detect-opus-for-simple-task',\n tip_ids: ['default-to-sonnet', 'use-haiku-for-simple'],\n run(ctx) {\n if (!ctx.active_model || !/opus/i.test(ctx.active_model)) return null\n const recent = ctx.events.slice(0, 20)\n if (recent.length < 6) return null\n const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name))\n const bash = countMatching(recent, (e) => e.tool_name === 'Bash')\n // Opus es correcto para planificar/preguntar — solo avisar cuando está ejecutando código\n if (edits + bash < 6) return null\n return {\n rule_id: 'detect-opus-for-simple-task',\n tip_ids: ['default-to-sonnet', 'use-haiku-for-simple'],\n severity: 'info',\n evidence: `Opus ejecutando trabajo mecanico: ${edits} edits + ${bash} Bash en ultimos 20 eventos. Sonnet haria lo mismo un 80% mas barato.`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 9. detect-claudemd-bloat (stub — requires filesystem stat at runtime)\n {\n id: 'detect-claudemd-bloat',\n tip_ids: ['migrate-claudemd-to-skills'],\n run() {\n return null\n },\n },\n\n // 10. detect-post-milestone-opportunity\n {\n id: 'detect-post-milestone-opportunity',\n tip_ids: ['use-compact-long-session'],\n run(ctx) {\n const recent = ctx.events.slice(0, 20)\n const edits = countMatching(recent, (e) => e.tool_name === 'Edit' || e.tool_name === 'Write')\n const hasBash = countMatching(recent, (e) => e.tool_name === 'Bash') > 0\n if (edits < 5 || !hasBash) return null\n if (ctx.session_token_total === null) return null\n const percent = ctx.session_token_total / ctx.session_token_limit\n if (percent < 0.4) return null\n return {\n rule_id: 'detect-post-milestone-opportunity',\n tip_ids: ['use-compact-long-session'],\n severity: 'info',\n evidence: `${edits} ediciones + Bash reciente + contexto ${(percent * 100).toFixed(0)}%`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n\n // 11. detect-skill-trigger-ignored (stub — requires skill registry)\n {\n id: 'detect-skill-trigger-ignored',\n tip_ids: ['use-skill-trigger'],\n run() {\n return null\n },\n },\n\n // 12. detect-serena-read-cascade\n // Fires when the agent makes ≥5 find_symbol calls without a get_symbols_overview\n // in the same window — suggests starting with an overview first.\n {\n id: 'detect-serena-read-cascade',\n tip_ids: ['use-serena-overview-first'],\n run(ctx) {\n const window = ctx.events.slice(0, 15)\n const findSymbolCount = countMatching(\n window,\n (e) => e.tool_name === 'mcp__serena__find_symbol',\n )\n if (findSymbolCount < 5) return null\n const hasOverview = window.some(\n (e) => e.tool_name === 'mcp__serena__get_symbols_overview',\n )\n if (hasOverview) return null\n return {\n rule_id: 'detect-serena-read-cascade',\n tip_ids: ['use-serena-overview-first'],\n severity: 'info' as DetectionSeverity,\n evidence: `${findSymbolCount} llamadas find_symbol sin get_symbols_overview en los ultimos 15 eventos.`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n\n // 13. detect-read-over-serena\n {\n id: 'detect-read-over-serena',\n tip_ids: ['prefer-serena-reads'],\n run(ctx) {\n const window = ctx.events.slice(0, 30)\n const largeReads = window.filter(\n (e) => e.tool_name === 'Read' && e.tokens_estimated > 2_000,\n )\n if (largeReads.length < 3) return null\n const totalTokens = largeReads.reduce((sum, e) => sum + e.tokens_estimated, 0)\n const estimatedSaving = Math.round(totalTokens * 0.7)\n const severity: DetectionSeverity = largeReads.length >= 6 ? 'warn' : 'info'\n return {\n rule_id: 'detect-read-over-serena',\n tip_ids: ['prefer-serena-reads'],\n severity,\n evidence: `${largeReads.length} lecturas Read >2k tokens (total: ${totalTokens}). Serena ahorraria ~${estimatedSaving} tokens (~70%).`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n]\n","// Rules orchestrator — Phase 4.44\n// Runs all detection rules, dedupes by (rule_id, tip_id), sorts by severity desc.\n\nimport type { DetectionHit, EventContext } from '../lib/types.js'\nimport { DETECTION_RULES } from './rules.js'\n\nconst SEVERITY_ORDER: Record<string, number> = { critical: 0, warn: 1, info: 2 }\n\nexport function runRules(ctx: EventContext): DetectionHit[] {\n const hits: DetectionHit[] = []\n for (const rule of DETECTION_RULES) {\n try {\n const hit = rule.run(ctx)\n if (hit) hits.push(hit)\n } catch {\n // swallow — rules must never crash the caller\n }\n }\n // Dedupe by rule_id\n const seen = new Set<string>()\n const unique: DetectionHit[] = []\n for (const h of hits) {\n if (seen.has(h.rule_id)) continue\n seen.add(h.rule_id)\n unique.push(h)\n }\n unique.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99))\n return unique\n}\n","// Context size meter with 3-source fallback — Phase 4.42\n// (1) transcript JSONL → (2) xray HTTP → (3) cumulative DB estimate\n\nimport fs from 'node:fs'\nimport type Database from 'better-sqlite3'\nimport type { ContextMeasurement, EstimationMethod } from '../lib/types.js'\nimport { resolveTranscriptPath } from '../lib/paths.js'\nimport { buildQueries } from '../db/queries.js'\nimport { getSessionTokens } from '../services/xray-client.js'\n\ntype DB = Database.Database\n\nconst DEFAULT_LIMIT = 200_000\nconst OPUS_1M_LIMIT = 1_000_000\nconst BASELINE_TOKENS = 15_000\n\nexport interface ContextMeterOptions {\n projectDir?: string\n db?: DB\n fetchImpl?: typeof fetch\n activeModel?: string\n}\n\nexport async function measureContextSize(\n sessionId: string,\n opts: ContextMeterOptions = {},\n): Promise<ContextMeasurement> {\n // Strategy 1: transcript JSONL (measured_exact)\n if (opts.projectDir) {\n const transcript = readTranscript(opts.projectDir, sessionId)\n if (transcript) return transcript\n }\n\n // Strategy 2: xray HTTP (measured_exact)\n const xrayResult = await tryXray(sessionId, opts.fetchImpl)\n if (xrayResult) return xrayResult\n\n // Strategy 3: cumulative estimate from our DB (estimated_cumulative)\n const limit = resolveLimit(opts.activeModel)\n if (opts.db) {\n return cumulativeEstimate(opts.db, sessionId, limit)\n }\n\n return { tokens: 0, limit, percent: 0, estimation_method: 'unknown' }\n}\n\nfunction readTranscript(projectDir: string, sessionId: string): ContextMeasurement | null {\n try {\n const p = resolveTranscriptPath(projectDir, sessionId)\n if (!fs.existsSync(p)) return null\n const content = fs.readFileSync(p, 'utf8')\n const lines = content.split('\\n').filter((l) => l.trim().length > 0)\n let totalTokens = 0\n let limit = DEFAULT_LIMIT\n for (const line of lines) {\n try {\n const turn = JSON.parse(line) as {\n usage?: {\n input_tokens?: number\n output_tokens?: number\n cache_read_input_tokens?: number\n }\n model?: string\n }\n if (turn.usage) {\n totalTokens +=\n (turn.usage.input_tokens ?? 0) +\n (turn.usage.output_tokens ?? 0) +\n (turn.usage.cache_read_input_tokens ?? 0)\n }\n if (turn.model && /1m/i.test(turn.model)) limit = OPUS_1M_LIMIT\n } catch {\n // skip unparseable line\n }\n }\n return {\n tokens: totalTokens,\n limit,\n percent: limit > 0 ? totalTokens / limit : 0,\n estimation_method: 'measured_exact' as EstimationMethod,\n }\n } catch {\n return null\n }\n}\n\nasync function tryXray(\n sessionId: string,\n fetchImpl?: typeof fetch,\n): Promise<ContextMeasurement | null> {\n const opts: Parameters<typeof getSessionTokens>[1] = {}\n if (fetchImpl !== undefined) opts.fetchImpl = fetchImpl\n return getSessionTokens(sessionId, opts)\n}\n\nfunction resolveLimit(activeModel?: string): number {\n if (activeModel && /1m|opus/i.test(activeModel)) return OPUS_1M_LIMIT\n return DEFAULT_LIMIT\n}\n\nfunction cumulativeEstimate(\n db: DB,\n sessionId: string,\n limit: number = DEFAULT_LIMIT,\n): ContextMeasurement {\n const queries = buildQueries(db)\n const sessionTokens = queries.sumTokensBySession(sessionId)\n const total = sessionTokens + BASELINE_TOKENS\n return {\n tokens: total,\n limit,\n percent: total / limit,\n estimation_method: 'estimated_cumulative',\n }\n}\n\n/**\n * Synchronous DB-only context measurement for hot paths (PostToolUse).\n * Skips transcript + xray strategies to stay under the 5ms budget. The\n * estimation_method returned ('estimated_cumulative') is surfaced verbatim\n * in tips so the agent knows this is a fast approximation.\n */\nexport function measureContextSizeFromDbSync(\n db: DB,\n sessionId: string,\n activeModel?: string,\n): ContextMeasurement {\n const limit = resolveLimit(activeModel)\n return cumulativeEstimate(db, sessionId, limit)\n}\n","// xray client — Phase 5.1\n// Fire-and-forget POST for tool events + GET for session tokens (used by coach context meter).\n// Silent on all failures: no stderr, no throw. Timeout 500ms for post, 300ms for get.\n\nimport type { ContextMeasurement } from '../lib/types.js'\nimport { resolveXrayUrl } from '../cli/config.js'\n\nconst POST_TIMEOUT_MS = 500\nconst GET_TIMEOUT_MS = 300\nconst DEFAULT_LIMIT = 200_000\n\ndeclare const __PKG_VERSION__: string\nconst VERSION = typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : '0.1.0'\n\nexport interface PostToXrayOptions {\n xrayUrl?: string\n fetchImpl?: typeof fetch\n timeoutMs?: number\n}\n\n/**\n * Fire-and-forget POST of a tool event to an xray server.\n * Returns true if the request was attempted, false if skipped (no URL).\n * Any network/parse error is swallowed silently.\n */\nexport async function postToXray(\n event: Record<string, unknown>,\n opts: PostToXrayOptions = {},\n): Promise<boolean> {\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\n if (!xrayUrl) return false\n const fetchFn = opts.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? POST_TIMEOUT_MS)\n try {\n await fetchFn(`${xrayUrl}/hooks/token-optimizer`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n source: 'token-optimizer-mcp',\n version: VERSION,\n event,\n }),\n signal: controller.signal,\n })\n return true\n } catch {\n return false\n } finally {\n clearTimeout(timer)\n }\n}\n\nconst SUMMARY_TIMEOUT_MS = 2000\n\n/**\n * Fire-and-forget POST of session summary to xray.\n * Only called once per session (not in hot path), so allows longer timeout.\n */\nexport async function postSummaryToXray(\n summary: Record<string, unknown>,\n opts: PostToXrayOptions = {},\n): Promise<boolean> {\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\n if (!xrayUrl) return false\n const fetchFn = opts.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUMMARY_TIMEOUT_MS)\n try {\n await fetchFn(`${xrayUrl}/hooks/token-optimizer/summary`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n source: 'token-optimizer-mcp',\n version: VERSION,\n summary,\n }),\n signal: controller.signal,\n })\n return true\n } catch {\n return false\n } finally {\n clearTimeout(timer)\n }\n}\n\nexport interface GetSessionTokensOptions {\n xrayUrl?: string\n fetchImpl?: typeof fetch\n timeoutMs?: number\n}\n\n/**\n * Read real token counts from xray for a given session.\n * Returns null on any failure or when XRAY_URL is unset.\n */\nexport async function getSessionTokens(\n sessionId: string,\n opts: GetSessionTokensOptions = {},\n): Promise<ContextMeasurement | null> {\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\n if (!xrayUrl) return null\n const fetchFn = opts.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? GET_TIMEOUT_MS)\n try {\n const res = await fetchFn(\n `${xrayUrl}/sessions/${encodeURIComponent(sessionId)}/tokens`,\n { signal: controller.signal },\n )\n if (!res.ok) return null\n const data = (await res.json()) as { tokens?: number; limit?: number }\n const tokens = data.tokens ?? 0\n const limit = data.limit ?? DEFAULT_LIMIT\n return {\n tokens,\n limit,\n percent: limit > 0 ? tokens / limit : 0,\n estimation_method: 'measured_exact',\n }\n } catch {\n return null\n } finally {\n clearTimeout(timer)\n }\n}\n","// Surfacing dedupe + log writer — Phase 4.45\n// Writes to coach_surface_log with session+rule+tip+via+severity.\n\nimport type Database from 'better-sqlite3'\nimport type { DetectionHit } from '../lib/types.js'\n\ntype DB = Database.Database\n\nexport type SurfacedVia = 'sessionstart' | 'posttooluse' | 'mcp' | 'cli'\n\n/**\n * Returns true if this (session, rule, tip) was surfaced within the last\n * `windowSeconds` seconds. Used by the PostToolUse throttle path.\n */\nexport function checkDedupe(\n db: DB,\n sessionId: string,\n ruleId: string,\n tipId: string,\n windowSeconds: number,\n): boolean {\n const row = db\n .prepare(\n `SELECT 1 FROM coach_surface_log\n WHERE session_id = ? AND rule_id = ? AND tip_id = ?\n AND created_at > datetime('now', ?)\n LIMIT 1`,\n )\n .get(sessionId, ruleId, tipId, `-${windowSeconds} seconds`) as unknown\n return row !== undefined && row !== null\n}\n\nexport function logSurface(\n db: DB,\n sessionId: string,\n hit: DetectionHit,\n via: SurfacedVia,\n): void {\n // Ensure session exists so FK succeeds\n db.prepare(`INSERT OR IGNORE INTO sessions (id) VALUES (?)`).run(sessionId)\n const stmt = db.prepare(\n `INSERT INTO coach_surface_log (session_id, rule_id, tip_id, surfaced_via, severity)\n VALUES (?, ?, ?, ?, ?)`,\n )\n for (const tipId of hit.tip_ids) {\n stmt.run(sessionId, hit.rule_id, tipId, via, hit.severity)\n }\n}\n\n/**\n * Log the list of hits under dedupe. A hit is considered \"fresh\" (to be\n * logged and returned to the caller) if at least one of its tip_ids was NOT\n * surfaced within `windowSeconds`. Returns only the surfaced hits.\n */\nexport function surfaceWithDedupe(\n db: DB,\n sessionId: string,\n hits: DetectionHit[],\n via: SurfacedVia,\n windowSeconds: number,\n): DetectionHit[] {\n const surfaced: DetectionHit[] = []\n for (const hit of hits) {\n const anyFresh = hit.tip_ids.some(\n (tipId) => !checkDedupe(db, sessionId, hit.rule_id, tipId, windowSeconds),\n )\n if (anyFresh) {\n logSurface(db, sessionId, hit, via)\n surfaced.push(hit)\n }\n }\n return surfaced\n}\n\n/**\n * Read all coach tips surfaced during a session, grouped by rule.\n * Used by session-summary-builder for xray integration.\n */\nexport function getCoachSurfaceLog(\n db: DB,\n sessionId: string,\n): Array<{ rule_id: string; tip_ids: string[]; severity: string }> {\n const rows = db\n .prepare(\n `SELECT rule_id, tip_id, severity\n FROM coach_surface_log\n WHERE session_id = ?\n ORDER BY created_at`,\n )\n .all(sessionId) as Array<{ rule_id: string; tip_id: string; severity: string }>\n\n // Group tip_ids by rule_id\n const map = new Map<string, { tip_ids: string[]; severity: string }>()\n for (const row of rows) {\n if (!map.has(row.rule_id)) {\n map.set(row.rule_id, { tip_ids: [], severity: row.severity })\n }\n const entry = map.get(row.rule_id)!\n if (!entry.tip_ids.includes(row.tip_id)) {\n entry.tip_ids.push(row.tip_id)\n }\n }\n\n return Array.from(map.entries()).map(([rule_id, v]) => ({\n rule_id,\n tip_ids: v.tip_ids,\n severity: v.severity,\n }))\n}\n\nexport function clearSurfaceLog(db: DB, sessionId?: string): number {\n if (sessionId) {\n const info = db.prepare(`DELETE FROM coach_surface_log WHERE session_id = ?`).run(sessionId)\n return info.changes as number\n }\n const info = db.prepare(`DELETE FROM coach_surface_log`).run()\n return info.changes as number\n}\n"],"mappings":"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","names":["DEFAULT_LIMIT","info"]} |
| #!/usr/bin/env node | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-L5Z32XXL.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/prune-mcp.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/; | ||
| function extractServerFromToolName(toolName) { | ||
| const m = MCP_TOOL_RE.exec(toolName); | ||
| return m ? m[1] : null; | ||
| } | ||
| function settingsLocalPath(cwd) { | ||
| return path.join(cwd, ".claude", "settings.local.json"); | ||
| } | ||
| function readJsonSafe(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return {}; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| function writeJson(p, data) { | ||
| fs.mkdirSync(path.dirname(p), { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(data, null, 2)); | ||
| } | ||
| function generateFromHistory(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const days = opts.days ?? 14; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const serverCounts = {}; | ||
| if (fs.existsSync(dbPath)) { | ||
| const db = getDb(dbPath); | ||
| const rows = db.prepare( | ||
| `SELECT tool_name, COUNT(*) as count | ||
| FROM tool_calls | ||
| WHERE created_at >= ? AND tool_name LIKE 'mcp__%' | ||
| GROUP BY tool_name` | ||
| ).all(since); | ||
| for (const row of rows) { | ||
| const server = extractServerFromToolName(row.tool_name); | ||
| if (server) { | ||
| serverCounts[server] = (serverCounts[server] ?? 0) + row.count; | ||
| } | ||
| } | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd, home: opts.home }); | ||
| const registered = new Set(schema.mcp_servers); | ||
| const used = new Set(Object.keys(serverCounts)); | ||
| const inactive = [...registered].filter((s) => !used.has(s)); | ||
| return { | ||
| proposed_allowlist: [...used], | ||
| inactive_servers: inactive, | ||
| analysis_days: days, | ||
| total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0), | ||
| server_counts: serverCounts | ||
| }; | ||
| } | ||
| function timestampForBackup() { | ||
| return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); | ||
| } | ||
| function insertSnapshot(cwd, method, details) { | ||
| try { | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) return; | ||
| const db = getDb(dbPath); | ||
| db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run( | ||
| method, | ||
| JSON.stringify(details) | ||
| ); | ||
| } catch { | ||
| } | ||
| } | ||
| function applyAllowlist(allowlist, opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const source = opts.source ?? "cli"; | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| if (fs.existsSync(settingsPath)) { | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| } else { | ||
| fs.mkdirSync(path.dirname(backupPath), { recursive: true }); | ||
| fs.writeFileSync(backupPath, "{}"); | ||
| } | ||
| const current = readJsonSafe(settingsPath); | ||
| current.enabledMcpjsonServers = allowlist; | ||
| writeJson(settingsPath, current); | ||
| insertSnapshot(cwd, source === "mcp" ? "allowlist_generated_via_mcp" : "allowlist_generated", { | ||
| allowlist, | ||
| target: settingsPath, | ||
| backup: backupPath | ||
| }); | ||
| return { settings_path: settingsPath, backup_path: backupPath }; | ||
| } | ||
| function rollback(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const dir = path.dirname(settingsPath); | ||
| if (!fs.existsSync(dir)) return { restored: false, from: null }; | ||
| const backups = fs.readdirSync(dir).filter((f) => f.startsWith("settings.local.json.backup-")).sort(); | ||
| if (backups.length === 0) return { restored: false, from: null }; | ||
| const target = opts.to ? backups.find((b) => b.includes(opts.to)) : backups[backups.length - 1]; | ||
| if (!target) return { restored: false, from: null }; | ||
| const backupPath = path.join(dir, target); | ||
| fs.copyFileSync(backupPath, settingsPath); | ||
| insertSnapshot(cwd, "rollback", { from: backupPath, to: settingsPath }); | ||
| return { restored: true, from: backupPath }; | ||
| } | ||
| function clearAllowlist(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }; | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| const json = readJsonSafe(settingsPath); | ||
| delete json.enabledMcpjsonServers; | ||
| writeJson(settingsPath, json); | ||
| insertSnapshot(cwd, "allowlist_cleared", { backup: backupPath }); | ||
| return { cleared: true, backup_path: backupPath }; | ||
| } | ||
| function impact(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const db = getDb(dbPath); | ||
| const snapshot = db.prepare( | ||
| `SELECT created_at FROM optimization_snapshots | ||
| WHERE method LIKE 'allowlist_%' | ||
| ORDER BY created_at DESC LIMIT 1` | ||
| ).get(); | ||
| if (!snapshot) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const before = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const after = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const beforeAvg = before.avg; | ||
| const afterAvg = after.avg; | ||
| const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null; | ||
| const percent = beforeAvg !== null && beforeAvg > 0 && afterAvg !== null ? (afterAvg - beforeAvg) / beforeAvg : null; | ||
| return { | ||
| before_avg: beforeAvg, | ||
| after_avg: afterAvg, | ||
| delta, | ||
| percent, | ||
| snapshot_at: snapshot.created_at | ||
| }; | ||
| } | ||
| function runPruneMcp(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| if (args.includes("--generate-from-history")) { | ||
| const daysFlag = args.find((a) => a.startsWith("--days=")); | ||
| const days = daysFlag ? parseInt(daysFlag.split("=")[1], 10) : 14; | ||
| const result = generateFromHistory({ cwd, days }); | ||
| print(`Propuesta de allowlist (${days} dias de historial):`); | ||
| print(` Usados: ${result.proposed_allowlist.join(", ") || "(ninguno)"}`); | ||
| print(` Inactivos: ${result.inactive_servers.join(", ") || "(ninguno)"}`); | ||
| print(` Eventos MCP totales: ${result.total_mcp_events}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--apply")) { | ||
| const generated = generateFromHistory({ cwd }); | ||
| if (generated.proposed_allowlist.length === 0) { | ||
| print("No hay MCPs activos en el historial. Nada que aplicar."); | ||
| return 1; | ||
| } | ||
| const applied = applyAllowlist(generated.proposed_allowlist, { cwd }); | ||
| print(`Allowlist aplicado a ${applied.settings_path}`); | ||
| print(`Backup: ${applied.backup_path}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--rollback")) { | ||
| const toFlag = args.find((a) => a.startsWith("--to=")); | ||
| const to = toFlag ? toFlag.split("=")[1] : void 0; | ||
| const result = rollback({ cwd, to }); | ||
| if (result.restored) { | ||
| print(`Restaurado desde ${result.from}`); | ||
| return 0; | ||
| } | ||
| print("No hay backups disponibles."); | ||
| return 1; | ||
| } | ||
| if (args.includes("--clear")) { | ||
| const result = clearAllowlist({ cwd }); | ||
| print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : "Nada que eliminar"); | ||
| return 0; | ||
| } | ||
| if (args.includes("--impact")) { | ||
| const result = impact({ cwd }); | ||
| if (result.snapshot_at === null) { | ||
| print("No hay snapshots de allowlist todavia."); | ||
| return 0; | ||
| } | ||
| print(`Snapshot mas reciente: ${result.snapshot_at}`); | ||
| print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? "n/a"}`); | ||
| print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? "n/a"}`); | ||
| if (result.percent !== null) { | ||
| print(`Delta: ${(result.percent * 100).toFixed(1)}%`); | ||
| } | ||
| return 0; | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd }); | ||
| print(`MCPs registrados (${schema.mcp_servers.length}):`); | ||
| for (const s of schema.mcp_servers) { | ||
| print(` ${s}`); | ||
| } | ||
| print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`); | ||
| print(""); | ||
| print("Flags:"); | ||
| print(" --generate-from-history [--days N] Propone allowlist (read-only)"); | ||
| print(" --apply Aplica el allowlist generado"); | ||
| print(" --rollback [--to TIMESTAMP] Restaura el ultimo backup"); | ||
| print(" --clear Elimina allowlist actual"); | ||
| print(" --impact Compara antes/despues del ultimo snapshot"); | ||
| return 0; | ||
| } | ||
| export { | ||
| settingsLocalPath, | ||
| generateFromHistory, | ||
| applyAllowlist, | ||
| rollback, | ||
| clearAllowlist, | ||
| impact, | ||
| runPruneMcp | ||
| }; | ||
| //# sourceMappingURL=chunk-3CAOEPYE.js.map |
| {"version":3,"sources":["../src/cli/prune-mcp.ts"],"sourcesContent":["// prune-mcp CLI + service — Phase 4.16-4.22\n// Generate allowlist from history, apply/rollback/clear, compute impact.\n// Writes to .claude/settings.local.json (NOT settings.json).\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\n\nconst MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/\n\nfunction extractServerFromToolName(toolName: string): string | null {\n const m = MCP_TOOL_RE.exec(toolName)\n return m ? m[1] : null\n}\n\nexport function settingsLocalPath(cwd: string): string {\n return path.join(cwd, '.claude', 'settings.local.json')\n}\n\nfunction readJsonSafe(p: string): Record<string, unknown> {\n try {\n if (!fs.existsSync(p)) return {}\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\n } catch {\n return {}\n }\n}\n\nfunction writeJson(p: string, data: Record<string, unknown>): void {\n fs.mkdirSync(path.dirname(p), { recursive: true })\n fs.writeFileSync(p, JSON.stringify(data, null, 2))\n}\n\nexport interface GeneratedAllowlist {\n proposed_allowlist: string[]\n inactive_servers: string[]\n analysis_days: number\n total_mcp_events: number\n server_counts: Record<string, number>\n}\n\nexport interface GenerateOptions {\n cwd?: string\n days?: number\n home?: string\n}\n\nexport function generateFromHistory(opts: GenerateOptions = {}): GeneratedAllowlist {\n const cwd = opts.cwd ?? process.cwd()\n const days = opts.days ?? 14\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\n\n const serverCounts: Record<string, number> = {}\n if (fs.existsSync(dbPath)) {\n const db = getDb(dbPath)\n const rows = db\n .prepare(\n `SELECT tool_name, COUNT(*) as count\n FROM tool_calls\n WHERE created_at >= ? AND tool_name LIKE 'mcp__%'\n GROUP BY tool_name`,\n )\n .all(since) as Array<{ tool_name: string; count: number }>\n for (const row of rows) {\n const server = extractServerFromToolName(row.tool_name)\n if (server) {\n serverCounts[server] = (serverCounts[server] ?? 0) + row.count\n }\n }\n }\n\n const schema = measureCurrentSchemaBytes({ cwd, home: opts.home })\n const registered = new Set(schema.mcp_servers)\n const used = new Set(Object.keys(serverCounts))\n const inactive = [...registered].filter((s) => !used.has(s))\n\n return {\n proposed_allowlist: [...used],\n inactive_servers: inactive,\n analysis_days: days,\n total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0),\n server_counts: serverCounts,\n }\n}\n\nexport interface ApplyOptions {\n cwd?: string\n source?: 'cli' | 'mcp'\n}\n\nexport interface ApplyResult {\n settings_path: string\n backup_path: string\n}\n\nfunction timestampForBackup(): string {\n return new Date().toISOString().replace(/[:.]/g, '-')\n}\n\nfunction insertSnapshot(cwd: string, method: string, details: Record<string, unknown>): void {\n try {\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) return\n const db = getDb(dbPath)\n db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run(\n method,\n JSON.stringify(details),\n )\n } catch {\n // swallow\n }\n}\n\nexport function applyAllowlist(allowlist: string[], opts: ApplyOptions = {}): ApplyResult {\n const cwd = opts.cwd ?? process.cwd()\n const source = opts.source ?? 'cli'\n const settingsPath = settingsLocalPath(cwd)\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\n\n if (fs.existsSync(settingsPath)) {\n fs.copyFileSync(settingsPath, backupPath)\n } else {\n fs.mkdirSync(path.dirname(backupPath), { recursive: true })\n fs.writeFileSync(backupPath, '{}')\n }\n\n const current = readJsonSafe(settingsPath)\n current.enabledMcpjsonServers = allowlist\n writeJson(settingsPath, current)\n\n insertSnapshot(cwd, source === 'mcp' ? 'allowlist_generated_via_mcp' : 'allowlist_generated', {\n allowlist,\n target: settingsPath,\n backup: backupPath,\n })\n\n return { settings_path: settingsPath, backup_path: backupPath }\n}\n\nexport interface RollbackOptions {\n cwd?: string\n to?: string\n}\n\nexport interface RollbackResult {\n restored: boolean\n from: string | null\n}\n\nexport function rollback(opts: RollbackOptions = {}): RollbackResult {\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = settingsLocalPath(cwd)\n const dir = path.dirname(settingsPath)\n if (!fs.existsSync(dir)) return { restored: false, from: null }\n\n const backups = fs\n .readdirSync(dir)\n .filter((f) => f.startsWith('settings.local.json.backup-'))\n .sort()\n if (backups.length === 0) return { restored: false, from: null }\n\n const target = opts.to ? backups.find((b) => b.includes(opts.to!)) : backups[backups.length - 1]\n if (!target) return { restored: false, from: null }\n\n const backupPath = path.join(dir, target)\n fs.copyFileSync(backupPath, settingsPath)\n\n insertSnapshot(cwd, 'rollback', { from: backupPath, to: settingsPath })\n return { restored: true, from: backupPath }\n}\n\nexport function clearAllowlist(opts: { cwd?: string } = {}): { cleared: boolean; backup_path: string | null } {\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = settingsLocalPath(cwd)\n if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }\n\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\n fs.copyFileSync(settingsPath, backupPath)\n\n const json = readJsonSafe(settingsPath)\n delete json.enabledMcpjsonServers\n writeJson(settingsPath, json)\n\n insertSnapshot(cwd, 'allowlist_cleared', { backup: backupPath })\n return { cleared: true, backup_path: backupPath }\n}\n\nexport interface ImpactResult {\n before_avg: number | null\n after_avg: number | null\n delta: number | null\n percent: number | null\n snapshot_at: string | null\n}\n\nexport function impact(opts: { cwd?: string } = {}): ImpactResult {\n const cwd = opts.cwd ?? process.cwd()\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) {\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\n }\n const db = getDb(dbPath)\n const snapshot = db\n .prepare(\n `SELECT created_at FROM optimization_snapshots\n WHERE method LIKE 'allowlist_%'\n ORDER BY created_at DESC LIMIT 1`,\n )\n .get() as { created_at: string } | undefined\n if (!snapshot) {\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\n }\n\n const before = db\n .prepare(\n `SELECT AVG(tokens_estimated) as avg FROM (\n SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100\n )`,\n )\n .get(snapshot.created_at) as { avg: number | null }\n const after = db\n .prepare(\n `SELECT AVG(tokens_estimated) as avg FROM (\n SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100\n )`,\n )\n .get(snapshot.created_at) as { avg: number | null }\n\n const beforeAvg = before.avg\n const afterAvg = after.avg\n const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null\n const percent =\n beforeAvg !== null && beforeAvg > 0 && afterAvg !== null\n ? (afterAvg - beforeAvg) / beforeAvg\n : null\n\n return {\n before_avg: beforeAvg,\n after_avg: afterAvg,\n delta,\n percent,\n snapshot_at: snapshot.created_at,\n }\n}\n\nexport interface PruneMcpCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runPruneMcp(args: string[] = [], opts: PruneMcpCliOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n\n if (args.includes('--generate-from-history')) {\n const daysFlag = args.find((a) => a.startsWith('--days='))\n const days = daysFlag ? parseInt(daysFlag.split('=')[1], 10) : 14\n const result = generateFromHistory({ cwd, days })\n print(`Propuesta de allowlist (${days} dias de historial):`)\n print(` Usados: ${result.proposed_allowlist.join(', ') || '(ninguno)'}`)\n print(` Inactivos: ${result.inactive_servers.join(', ') || '(ninguno)'}`)\n print(` Eventos MCP totales: ${result.total_mcp_events}`)\n return 0\n }\n\n if (args.includes('--apply')) {\n const generated = generateFromHistory({ cwd })\n if (generated.proposed_allowlist.length === 0) {\n print('No hay MCPs activos en el historial. Nada que aplicar.')\n return 1\n }\n const applied = applyAllowlist(generated.proposed_allowlist, { cwd })\n print(`Allowlist aplicado a ${applied.settings_path}`)\n print(`Backup: ${applied.backup_path}`)\n return 0\n }\n\n if (args.includes('--rollback')) {\n const toFlag = args.find((a) => a.startsWith('--to='))\n const to = toFlag ? toFlag.split('=')[1] : undefined\n const result = rollback({ cwd, to })\n if (result.restored) {\n print(`Restaurado desde ${result.from}`)\n return 0\n }\n print('No hay backups disponibles.')\n return 1\n }\n\n if (args.includes('--clear')) {\n const result = clearAllowlist({ cwd })\n print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar')\n return 0\n }\n\n if (args.includes('--impact')) {\n const result = impact({ cwd })\n if (result.snapshot_at === null) {\n print('No hay snapshots de allowlist todavia.')\n return 0\n }\n print(`Snapshot mas reciente: ${result.snapshot_at}`)\n print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? 'n/a'}`)\n print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? 'n/a'}`)\n if (result.percent !== null) {\n print(`Delta: ${(result.percent * 100).toFixed(1)}%`)\n }\n return 0\n }\n\n // Default: list registered MCPs with estimated cost\n const schema = measureCurrentSchemaBytes({ cwd })\n print(`MCPs registrados (${schema.mcp_servers.length}):`)\n for (const s of schema.mcp_servers) {\n print(` ${s}`)\n }\n print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`)\n print('')\n print('Flags:')\n print(' --generate-from-history [--days N] Propone allowlist (read-only)')\n print(' --apply Aplica el allowlist generado')\n print(' --rollback [--to TIMESTAMP] Restaura el ultimo backup')\n print(' --clear Elimina allowlist actual')\n print(' --impact Compara antes/despues del ultimo snapshot')\n return 0\n}\n"],"mappings":";;;;;;;;;;;;;AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AAKjB,IAAM,cAAc;AAEpB,SAAS,0BAA0B,UAAiC;AAClE,QAAM,IAAI,YAAY,KAAK,QAAQ;AACnC,SAAO,IAAI,EAAE,CAAC,IAAI;AACpB;AAEO,SAAS,kBAAkB,KAAqB;AACrD,SAAO,KAAK,KAAK,KAAK,WAAW,qBAAqB;AACxD;AAEA,SAAS,aAAa,GAAoC;AACxD,MAAI;AACF,QAAI,CAAC,GAAG,WAAW,CAAC,EAAG,QAAO,CAAC;AAC/B,WAAO,KAAK,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;AAAA,EAC9C,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAS,UAAU,GAAW,MAAqC;AACjE,KAAG,UAAU,KAAK,QAAQ,CAAC,GAAG,EAAE,WAAW,KAAK,CAAC;AACjD,KAAG,cAAc,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AACnD;AAgBO,SAAS,oBAAoB,OAAwB,CAAC,GAAuB;AAClF,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,OAAO,KAAK,QAAQ;AAC1B,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAChD,QAAM,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAU,EAAE,YAAY;AAEnE,QAAM,eAAuC,CAAC;AAC9C,MAAI,GAAG,WAAW,MAAM,GAAG;AACzB,UAAM,KAAK,MAAM,MAAM;AACvB,UAAM,OAAO,GACV;AAAA,MACC;AAAA;AAAA;AAAA;AAAA,IAIF,EACC,IAAI,KAAK;AACZ,eAAW,OAAO,MAAM;AACtB,YAAM,SAAS,0BAA0B,IAAI,SAAS;AACtD,UAAI,QAAQ;AACV,qBAAa,MAAM,KAAK,aAAa,MAAM,KAAK,KAAK,IAAI;AAAA,MAC3D;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,0BAA0B,EAAE,KAAK,MAAM,KAAK,KAAK,CAAC;AACjE,QAAM,aAAa,IAAI,IAAI,OAAO,WAAW;AAC7C,QAAM,OAAO,IAAI,IAAI,OAAO,KAAK,YAAY,CAAC;AAC9C,QAAM,WAAW,CAAC,GAAG,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,CAAC;AAE3D,SAAO;AAAA,IACL,oBAAoB,CAAC,GAAG,IAAI;AAAA,IAC5B,kBAAkB;AAAA,IAClB,eAAe;AAAA,IACf,kBAAkB,OAAO,OAAO,YAAY,EAAE,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC;AAAA,IACvE,eAAe;AAAA,EACjB;AACF;AAYA,SAAS,qBAA6B;AACpC,UAAO,oBAAI,KAAK,GAAE,YAAY,EAAE,QAAQ,SAAS,GAAG;AACtD;AAEA,SAAS,eAAe,KAAa,QAAgB,SAAwC;AAC3F,MAAI;AACF,UAAM,aAAa,kBAAkB,GAAG;AACxC,UAAM,SAAS,uBAAuB,UAAU;AAChD,QAAI,CAAC,GAAG,WAAW,MAAM,EAAG;AAC5B,UAAM,KAAK,MAAM,MAAM;AACvB,OAAG,QAAQ,oEAAoE,EAAE;AAAA,MAC/E;AAAA,MACA,KAAK,UAAU,OAAO;AAAA,IACxB;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,eAAe,WAAqB,OAAqB,CAAC,GAAgB;AACxF,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,eAAe,kBAAkB,GAAG;AAC1C,QAAM,aAAa,GAAG,YAAY,WAAW,mBAAmB,CAAC;AAEjE,MAAI,GAAG,WAAW,YAAY,GAAG;AAC/B,OAAG,aAAa,cAAc,UAAU;AAAA,EAC1C,OAAO;AACL,OAAG,UAAU,KAAK,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAC1D,OAAG,cAAc,YAAY,IAAI;AAAA,EACnC;AAEA,QAAM,UAAU,aAAa,YAAY;AACzC,UAAQ,wBAAwB;AAChC,YAAU,cAAc,OAAO;AAE/B,iBAAe,KAAK,WAAW,QAAQ,gCAAgC,uBAAuB;AAAA,IAC5F;AAAA,IACA,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV,CAAC;AAED,SAAO,EAAE,eAAe,cAAc,aAAa,WAAW;AAChE;AAYO,SAAS,SAAS,OAAwB,CAAC,GAAmB;AACnE,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,eAAe,kBAAkB,GAAG;AAC1C,QAAM,MAAM,KAAK,QAAQ,YAAY;AACrC,MAAI,CAAC,GAAG,WAAW,GAAG,EAAG,QAAO,EAAE,UAAU,OAAO,MAAM,KAAK;AAE9D,QAAM,UAAU,GACb,YAAY,GAAG,EACf,OAAO,CAAC,MAAM,EAAE,WAAW,6BAA6B,CAAC,EACzD,KAAK;AACR,MAAI,QAAQ,WAAW,EAAG,QAAO,EAAE,UAAU,OAAO,MAAM,KAAK;AAE/D,QAAM,SAAS,KAAK,KAAK,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK,EAAG,CAAC,IAAI,QAAQ,QAAQ,SAAS,CAAC;AAC/F,MAAI,CAAC,OAAQ,QAAO,EAAE,UAAU,OAAO,MAAM,KAAK;AAElD,QAAM,aAAa,KAAK,KAAK,KAAK,MAAM;AACxC,KAAG,aAAa,YAAY,YAAY;AAExC,iBAAe,KAAK,YAAY,EAAE,MAAM,YAAY,IAAI,aAAa,CAAC;AACtE,SAAO,EAAE,UAAU,MAAM,MAAM,WAAW;AAC5C;AAEO,SAAS,eAAe,OAAyB,CAAC,GAAqD;AAC5G,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,eAAe,kBAAkB,GAAG;AAC1C,MAAI,CAAC,GAAG,WAAW,YAAY,EAAG,QAAO,EAAE,SAAS,OAAO,aAAa,KAAK;AAE7E,QAAM,aAAa,GAAG,YAAY,WAAW,mBAAmB,CAAC;AACjE,KAAG,aAAa,cAAc,UAAU;AAExC,QAAM,OAAO,aAAa,YAAY;AACtC,SAAO,KAAK;AACZ,YAAU,cAAc,IAAI;AAE5B,iBAAe,KAAK,qBAAqB,EAAE,QAAQ,WAAW,CAAC;AAC/D,SAAO,EAAE,SAAS,MAAM,aAAa,WAAW;AAClD;AAUO,SAAS,OAAO,OAAyB,CAAC,GAAiB;AAChE,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAChD,MAAI,CAAC,GAAG,WAAW,MAAM,GAAG;AAC1B,WAAO,EAAE,YAAY,MAAM,WAAW,MAAM,OAAO,MAAM,SAAS,MAAM,aAAa,KAAK;AAAA,EAC5F;AACA,QAAM,KAAK,MAAM,MAAM;AACvB,QAAM,WAAW,GACd;AAAA,IACC;AAAA;AAAA;AAAA,EAGF,EACC,IAAI;AACP,MAAI,CAAC,UAAU;AACb,WAAO,EAAE,YAAY,MAAM,WAAW,MAAM,OAAO,MAAM,SAAS,MAAM,aAAa,KAAK;AAAA,EAC5F;AAEA,QAAM,SAAS,GACZ;AAAA,IACC;AAAA;AAAA;AAAA,EAGF,EACC,IAAI,SAAS,UAAU;AAC1B,QAAM,QAAQ,GACX;AAAA,IACC;AAAA;AAAA;AAAA,EAGF,EACC,IAAI,SAAS,UAAU;AAE1B,QAAM,YAAY,OAAO;AACzB,QAAM,WAAW,MAAM;AACvB,QAAM,QAAQ,cAAc,QAAQ,aAAa,OAAO,WAAW,YAAY;AAC/E,QAAM,UACJ,cAAc,QAAQ,YAAY,KAAK,aAAa,QAC/C,WAAW,aAAa,YACzB;AAEN,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,WAAW;AAAA,IACX;AAAA,IACA;AAAA,IACA,aAAa,SAAS;AAAA,EACxB;AACF;AAOO,SAAS,YAAY,OAAiB,CAAC,GAAG,OAA2B,CAAC,GAAW;AACtF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AAEpC,MAAI,KAAK,SAAS,yBAAyB,GAAG;AAC5C,UAAM,WAAW,KAAK,KAAK,CAAC,MAAM,EAAE,WAAW,SAAS,CAAC;AACzD,UAAM,OAAO,WAAW,SAAS,SAAS,MAAM,GAAG,EAAE,CAAC,GAAG,EAAE,IAAI;AAC/D,UAAM,SAAS,oBAAoB,EAAE,KAAK,KAAK,CAAC;AAChD,UAAM,2BAA2B,IAAI,sBAAsB;AAC3D,UAAM,gBAAgB,OAAO,mBAAmB,KAAK,IAAI,KAAK,WAAW,EAAE;AAC3E,UAAM,gBAAgB,OAAO,iBAAiB,KAAK,IAAI,KAAK,WAAW,EAAE;AACzE,UAAM,0BAA0B,OAAO,gBAAgB,EAAE;AACzD,WAAO;AAAA,EACT;AAEA,MAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,UAAM,YAAY,oBAAoB,EAAE,IAAI,CAAC;AAC7C,QAAI,UAAU,mBAAmB,WAAW,GAAG;AAC7C,YAAM,wDAAwD;AAC9D,aAAO;AAAA,IACT;AACA,UAAM,UAAU,eAAe,UAAU,oBAAoB,EAAE,IAAI,CAAC;AACpE,UAAM,wBAAwB,QAAQ,aAAa,EAAE;AACrD,UAAM,WAAW,QAAQ,WAAW,EAAE;AACtC,WAAO;AAAA,EACT;AAEA,MAAI,KAAK,SAAS,YAAY,GAAG;AAC/B,UAAM,SAAS,KAAK,KAAK,CAAC,MAAM,EAAE,WAAW,OAAO,CAAC;AACrD,UAAM,KAAK,SAAS,OAAO,MAAM,GAAG,EAAE,CAAC,IAAI;AAC3C,UAAM,SAAS,SAAS,EAAE,KAAK,GAAG,CAAC;AACnC,QAAI,OAAO,UAAU;AACnB,YAAM,oBAAoB,OAAO,IAAI,EAAE;AACvC,aAAO;AAAA,IACT;AACA,UAAM,6BAA6B;AACnC,WAAO;AAAA,EACT;AAEA,MAAI,KAAK,SAAS,SAAS,GAAG;AAC5B,UAAM,SAAS,eAAe,EAAE,IAAI,CAAC;AACrC,UAAM,OAAO,UAAU,gCAAgC,OAAO,WAAW,MAAM,mBAAmB;AAClG,WAAO;AAAA,EACT;AAEA,MAAI,KAAK,SAAS,UAAU,GAAG;AAC7B,UAAM,SAAS,OAAO,EAAE,IAAI,CAAC;AAC7B,QAAI,OAAO,gBAAgB,MAAM;AAC/B,YAAM,wCAAwC;AAC9C,aAAO;AAAA,IACT;AACA,UAAM,0BAA0B,OAAO,WAAW,EAAE;AACpD,UAAM,iCAAiC,OAAO,YAAY,QAAQ,CAAC,KAAK,KAAK,EAAE;AAC/E,UAAM,mCAAmC,OAAO,WAAW,QAAQ,CAAC,KAAK,KAAK,EAAE;AAChF,QAAI,OAAO,YAAY,MAAM;AAC3B,YAAM,WAAW,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,GAAG;AAAA,IACtD;AACA,WAAO;AAAA,EACT;AAGA,QAAM,SAAS,0BAA0B,EAAE,IAAI,CAAC;AAChD,QAAM,qBAAqB,OAAO,YAAY,MAAM,IAAI;AACxD,aAAW,KAAK,OAAO,aAAa;AAClC,UAAM,KAAK,CAAC,EAAE;AAAA,EAChB;AACA,QAAM,iCAAiC,OAAO,kBAAkB,SAAS;AACzE,QAAM,EAAE;AACR,QAAM,QAAQ;AACd,QAAM,sEAAsE;AAC5E,QAAM,sEAAsE;AAC5E,QAAM,mEAAmE;AACzE,QAAM,kEAAkE;AACxE,QAAM,mFAAmF;AACzF,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-DK5JDNWQ.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-OIZX7KF2.js"; | ||
| // src/services/stats.ts | ||
| var DAY_MS = 864e5; | ||
| var HAIKU_INPUT_PER_MTOK = 1; | ||
| var SONNET_INPUT_PER_MTOK = 3; | ||
| var OPUS_INPUT_PER_MTOK = 5; | ||
| function sinceDays(days) { | ||
| return new Date(Date.now() - days * DAY_MS).toISOString(); | ||
| } | ||
| function getUsageStats(db, days = 7) { | ||
| const queries = buildQueries(db); | ||
| const since = sinceDays(days); | ||
| const byTool = queries.countToolCallsByTool(since); | ||
| const bySource = queries.countToolCallsBySource(since); | ||
| const totalTokens = bySource.reduce((sum, r) => sum + r.tokens, 0); | ||
| const totalEvents = bySource.reduce((sum, r) => sum + r.count, 0); | ||
| return { | ||
| period_days: days, | ||
| period_since: since, | ||
| by_tool: byTool, | ||
| by_source: bySource, | ||
| total_tokens: totalTokens, | ||
| total_events: totalEvents | ||
| }; | ||
| } | ||
| function getCostReport(db, days = 7) { | ||
| const usage = getUsageStats(db, days); | ||
| const mtok = usage.total_tokens / 1e6; | ||
| const haiku = Number((mtok * HAIKU_INPUT_PER_MTOK).toFixed(4)); | ||
| const sonnet = Number((mtok * SONNET_INPUT_PER_MTOK).toFixed(4)); | ||
| const opus = Number((mtok * OPUS_INPUT_PER_MTOK).toFixed(4)); | ||
| return { | ||
| period_days: days, | ||
| total_tokens: usage.total_tokens, | ||
| estimated_cost_usd_haiku: haiku, | ||
| estimated_cost_usd_sonnet: sonnet, | ||
| estimated_cost_usd_opus: opus, | ||
| estimated_cost_usd_min: haiku, | ||
| estimated_cost_usd_max: opus, | ||
| by_source: usage.by_source, | ||
| disclaimer: "Coste estimado de tokens de herramientas (input al modelo). Haiku $1, Sonnet $3, Opus $5 por MTok. Revisa tu factura Anthropic para el coste real." | ||
| }; | ||
| } | ||
| function getActiveBudgetSummary(db, sessionId, projectHash) { | ||
| const mgr = new BudgetManager(db); | ||
| return mgr.checkBudget(sessionId, projectHash); | ||
| } | ||
| export { | ||
| getUsageStats, | ||
| getCostReport, | ||
| getActiveBudgetSummary | ||
| }; | ||
| //# sourceMappingURL=chunk-3LSC7CFQ.js.map |
| {"version":3,"sources":["../src/services/stats.ts"],"sourcesContent":["// Shared stats service — Phase 4.4\n// Read-only aggregates used by CLI and MCP tools.\n\nimport type Database from 'better-sqlite3'\nimport { buildQueries, type ToolCountRow, type SourceCountRow } from '../db/queries.js'\nimport { BudgetManager } from './budget-manager.js'\nimport type { BudgetStatus } from '../lib/types.js'\n\ntype DB = Database.Database\n\nconst DAY_MS = 86_400_000\n// Pricing April 2026 — input tokens (tool output → model input)\n// Haiku 4.5: $1/$5, Sonnet 4.6: $3/$15, Opus 4.6: $5/$25 per MTok (input/output)\n// token-optimizer tracks tool output (= model input), so we use input pricing.\nconst HAIKU_INPUT_PER_MTOK = 1\nconst SONNET_INPUT_PER_MTOK = 3\nconst OPUS_INPUT_PER_MTOK = 5\n\nfunction sinceDays(days: number): string {\n return new Date(Date.now() - days * DAY_MS).toISOString()\n}\n\nexport interface UsageStats {\n period_days: number\n period_since: string\n by_tool: ToolCountRow[]\n by_source: SourceCountRow[]\n total_tokens: number\n total_events: number\n}\n\nexport interface CostReport {\n period_days: number\n total_tokens: number\n estimated_cost_usd_haiku: number\n estimated_cost_usd_sonnet: number\n estimated_cost_usd_opus: number\n /** @deprecated Use estimated_cost_usd_haiku */\n estimated_cost_usd_min: number\n /** @deprecated Use estimated_cost_usd_opus */\n estimated_cost_usd_max: number\n by_source: SourceCountRow[]\n disclaimer: string\n}\n\nexport interface SavingsToday {\n date: string\n by_source: SourceCountRow[]\n total_tokens: number\n note: string\n}\n\nexport function getUsageStats(db: DB, days = 7): UsageStats {\n const queries = buildQueries(db)\n const since = sinceDays(days)\n const byTool = queries.countToolCallsByTool(since)\n const bySource = queries.countToolCallsBySource(since)\n const totalTokens = bySource.reduce((sum, r) => sum + r.tokens, 0)\n const totalEvents = bySource.reduce((sum, r) => sum + r.count, 0)\n return {\n period_days: days,\n period_since: since,\n by_tool: byTool,\n by_source: bySource,\n total_tokens: totalTokens,\n total_events: totalEvents,\n }\n}\n\nexport function getCostReport(db: DB, days = 7): CostReport {\n const usage = getUsageStats(db, days)\n const mtok = usage.total_tokens / 1_000_000\n const haiku = Number((mtok * HAIKU_INPUT_PER_MTOK).toFixed(4))\n const sonnet = Number((mtok * SONNET_INPUT_PER_MTOK).toFixed(4))\n const opus = Number((mtok * OPUS_INPUT_PER_MTOK).toFixed(4))\n return {\n period_days: days,\n total_tokens: usage.total_tokens,\n estimated_cost_usd_haiku: haiku,\n estimated_cost_usd_sonnet: sonnet,\n estimated_cost_usd_opus: opus,\n estimated_cost_usd_min: haiku,\n estimated_cost_usd_max: opus,\n by_source: usage.by_source,\n disclaimer:\n 'Coste estimado de tokens de herramientas (input al modelo). Haiku $1, Sonnet $3, Opus $5 por MTok. Revisa tu factura Anthropic para el coste real.',\n }\n}\n\nexport function getActiveBudgetSummary(\n db: DB,\n sessionId: string,\n projectHash: string | null,\n): BudgetStatus {\n const mgr = new BudgetManager(db)\n return mgr.checkBudget(sessionId, projectHash)\n}\n\nexport function getSavingsToday(db: DB): SavingsToday {\n const queries = buildQueries(db)\n const since = sinceDays(1)\n const bySource = queries.countToolCallsBySource(since)\n const total = bySource.reduce((sum, r) => sum + r.tokens, 0)\n return {\n date: new Date().toISOString().slice(0, 10),\n by_source: bySource,\n total_tokens: total,\n note: 'Ahorros por fuente no medidos directamente; revisa el reporte para el split Medido/Estimado.',\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAM,SAAS;AAIf,IAAM,uBAAuB;AAC7B,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB;AAE5B,SAAS,UAAU,MAAsB;AACvC,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE,YAAY;AAC1D;AAgCO,SAAS,cAAc,IAAQ,OAAO,GAAe;AAC1D,QAAM,UAAU,aAAa,EAAE;AAC/B,QAAM,QAAQ,UAAU,IAAI;AAC5B,QAAM,SAAS,QAAQ,qBAAqB,KAAK;AACjD,QAAM,WAAW,QAAQ,uBAAuB,KAAK;AACrD,QAAM,cAAc,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,QAAQ,CAAC;AACjE,QAAM,cAAc,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AAChE,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc;AAAA,IACd,SAAS;AAAA,IACT,WAAW;AAAA,IACX,cAAc;AAAA,IACd,cAAc;AAAA,EAChB;AACF;AAEO,SAAS,cAAc,IAAQ,OAAO,GAAe;AAC1D,QAAM,QAAQ,cAAc,IAAI,IAAI;AACpC,QAAM,OAAO,MAAM,eAAe;AAClC,QAAM,QAAQ,QAAQ,OAAO,sBAAsB,QAAQ,CAAC,CAAC;AAC7D,QAAM,SAAS,QAAQ,OAAO,uBAAuB,QAAQ,CAAC,CAAC;AAC/D,QAAM,OAAO,QAAQ,OAAO,qBAAqB,QAAQ,CAAC,CAAC;AAC3D,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc,MAAM;AAAA,IACpB,0BAA0B;AAAA,IAC1B,2BAA2B;AAAA,IAC3B,yBAAyB;AAAA,IACzB,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,IACxB,WAAW,MAAM;AAAA,IACjB,YACE;AAAA,EACJ;AACF;AAEO,SAAS,uBACd,IACA,WACA,aACc;AACd,QAAM,MAAM,IAAI,cAAc,EAAE;AAChC,SAAO,IAAI,YAAY,WAAW,WAAW;AAC/C;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-OIZX7KF2.js"; | ||
| // src/services/budget-manager.ts | ||
| var MAX_TOKENS = 1e7; | ||
| var BudgetManager = class { | ||
| queries; | ||
| constructor(db) { | ||
| this.queries = buildQueries(db); | ||
| } | ||
| setBudget(input) { | ||
| if (!Number.isInteger(input.limit_tokens)) { | ||
| throw new Error("limit_tokens debe ser un entero"); | ||
| } | ||
| if (input.limit_tokens <= 0) { | ||
| throw new Error("limit_tokens debe ser mayor que 0"); | ||
| } | ||
| if (input.limit_tokens > MAX_TOKENS) { | ||
| throw new Error(`limit_tokens no puede exceder ${MAX_TOKENS}`); | ||
| } | ||
| if (input.scope !== "session" && input.scope !== "project") { | ||
| throw new Error(`scope invalido: ${String(input.scope)}`); | ||
| } | ||
| const mode = "warn"; | ||
| this.queries.upsertBudget(input.scope, input.scope_key, input.limit_tokens, mode); | ||
| const budget = this.queries.getBudgetByScope(input.scope, input.scope_key); | ||
| if (!budget) { | ||
| throw new Error("no se pudo leer el budget recien creado"); | ||
| } | ||
| return budget; | ||
| } | ||
| /** | ||
| * Resolve the active budget for a given session + project. | ||
| * Returns the session-scoped budget if present, else the project-scoped one, else null. | ||
| */ | ||
| getActiveBudget(sessionId, projectHash) { | ||
| const sessionBudget = this.queries.getBudgetByScope("session", sessionId); | ||
| if (sessionBudget) return sessionBudget; | ||
| if (projectHash) { | ||
| const projectBudget = this.queries.getBudgetByScope("project", projectHash); | ||
| if (projectBudget) return projectBudget; | ||
| } | ||
| return null; | ||
| } | ||
| /** Compute spent tokens against a budget from tool_calls since the budget creation timestamp. */ | ||
| computeSpent(budget) { | ||
| if (budget.scope === "session") { | ||
| return this.queries.sumTokensBySessionSince(budget.scope_key, budget.created_at); | ||
| } | ||
| return this.queries.sumTokensByProjectSince(budget.scope_key, budget.created_at); | ||
| } | ||
| checkBudget(sessionId, projectHash) { | ||
| const active = this.getActiveBudget(sessionId, projectHash); | ||
| if (!active) { | ||
| return { active: false, spent: 0, remaining: 0, percent_used: 0, mode: null }; | ||
| } | ||
| const spent = this.computeSpent(active); | ||
| const remaining = Math.max(0, active.limit_tokens - spent); | ||
| const percent = active.limit_tokens > 0 ? spent / active.limit_tokens : 0; | ||
| return { | ||
| active: true, | ||
| spent, | ||
| remaining, | ||
| percent_used: percent, | ||
| mode: active.mode | ||
| }; | ||
| } | ||
| clearBudget(scope, scopeKey) { | ||
| return this.queries.deleteBudgetByScope(scope, scopeKey) > 0; | ||
| } | ||
| getBudgetReport(since) { | ||
| return { | ||
| by_tool: this.queries.countToolCallsByTool(since), | ||
| by_source: this.queries.countToolCallsBySource(since), | ||
| period_since: since | ||
| }; | ||
| } | ||
| recordBudgetEvent(budgetId, eventType, tokens) { | ||
| this.queries.insertBudgetEvent(budgetId, eventType, tokens); | ||
| } | ||
| }; | ||
| export { | ||
| BudgetManager | ||
| }; | ||
| //# sourceMappingURL=chunk-DK5JDNWQ.js.map |
| {"version":3,"sources":["../src/services/budget-manager.ts"],"sourcesContent":["// Budget manager — Phase 2.1 + 2.3\n// Precedence: session > project. Spent tokens computed from tool_calls since budget.created_at.\n\nimport type Database from 'better-sqlite3'\nimport type {\n Budget,\n BudgetScope,\n BudgetMode,\n BudgetStatus,\n} from '../lib/types.js'\nimport { buildQueries, type Queries, type ToolCountRow, type SourceCountRow } from '../db/queries.js'\n\ntype DB = Database.Database\n\nexport interface SetBudgetInput {\n scope: BudgetScope\n scope_key: string\n limit_tokens: number\n mode?: BudgetMode\n}\n\nexport interface BudgetReport {\n by_tool: ToolCountRow[]\n by_source: SourceCountRow[]\n period_since: string\n}\n\nconst MAX_TOKENS = 1e7\n\nexport class BudgetManager {\n private queries: Queries\n\n constructor(db: DB) {\n this.queries = buildQueries(db)\n }\n\n setBudget(input: SetBudgetInput): Budget {\n if (!Number.isInteger(input.limit_tokens)) {\n throw new Error('limit_tokens debe ser un entero')\n }\n if (input.limit_tokens <= 0) {\n throw new Error('limit_tokens debe ser mayor que 0')\n }\n if (input.limit_tokens > MAX_TOKENS) {\n throw new Error(`limit_tokens no puede exceder ${MAX_TOKENS}`)\n }\n if (input.scope !== 'session' && input.scope !== 'project') {\n throw new Error(`scope invalido: ${String(input.scope)}`)\n }\n const mode: BudgetMode = 'warn'\n this.queries.upsertBudget(input.scope, input.scope_key, input.limit_tokens, mode)\n const budget = this.queries.getBudgetByScope(input.scope, input.scope_key)\n if (!budget) {\n throw new Error('no se pudo leer el budget recien creado')\n }\n return budget\n }\n\n /**\n * Resolve the active budget for a given session + project.\n * Returns the session-scoped budget if present, else the project-scoped one, else null.\n */\n getActiveBudget(sessionId: string, projectHash: string | null): Budget | null {\n const sessionBudget = this.queries.getBudgetByScope('session', sessionId)\n if (sessionBudget) return sessionBudget\n if (projectHash) {\n const projectBudget = this.queries.getBudgetByScope('project', projectHash)\n if (projectBudget) return projectBudget\n }\n return null\n }\n\n /** Compute spent tokens against a budget from tool_calls since the budget creation timestamp. */\n computeSpent(budget: Budget): number {\n if (budget.scope === 'session') {\n return this.queries.sumTokensBySessionSince(budget.scope_key, budget.created_at)\n }\n return this.queries.sumTokensByProjectSince(budget.scope_key, budget.created_at)\n }\n\n checkBudget(sessionId: string, projectHash: string | null): BudgetStatus {\n const active = this.getActiveBudget(sessionId, projectHash)\n if (!active) {\n return { active: false, spent: 0, remaining: 0, percent_used: 0, mode: null }\n }\n const spent = this.computeSpent(active)\n const remaining = Math.max(0, active.limit_tokens - spent)\n const percent = active.limit_tokens > 0 ? spent / active.limit_tokens : 0\n return {\n active: true,\n spent,\n remaining,\n percent_used: percent,\n mode: active.mode,\n }\n }\n\n clearBudget(scope: BudgetScope, scopeKey: string): boolean {\n return this.queries.deleteBudgetByScope(scope, scopeKey) > 0\n }\n\n getBudgetReport(since: string): BudgetReport {\n return {\n by_tool: this.queries.countToolCallsByTool(since),\n by_source: this.queries.countToolCallsBySource(since),\n period_since: since,\n }\n }\n\n recordBudgetEvent(\n budgetId: number,\n eventType: 'spend' | 'warn' | 'block' | 'reset',\n tokens: number | null,\n ): void {\n this.queries.insertBudgetEvent(budgetId, eventType, tokens)\n }\n}\n"],"mappings":";;;;;;AA2BA,IAAM,aAAa;AAEZ,IAAM,gBAAN,MAAoB;AAAA,EACjB;AAAA,EAER,YAAY,IAAQ;AAClB,SAAK,UAAU,aAAa,EAAE;AAAA,EAChC;AAAA,EAEA,UAAU,OAA+B;AACvC,QAAI,CAAC,OAAO,UAAU,MAAM,YAAY,GAAG;AACzC,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AACA,QAAI,MAAM,gBAAgB,GAAG;AAC3B,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AACA,QAAI,MAAM,eAAe,YAAY;AACnC,YAAM,IAAI,MAAM,iCAAiC,UAAU,EAAE;AAAA,IAC/D;AACA,QAAI,MAAM,UAAU,aAAa,MAAM,UAAU,WAAW;AAC1D,YAAM,IAAI,MAAM,mBAAmB,OAAO,MAAM,KAAK,CAAC,EAAE;AAAA,IAC1D;AACA,UAAM,OAAmB;AACzB,SAAK,QAAQ,aAAa,MAAM,OAAO,MAAM,WAAW,MAAM,cAAc,IAAI;AAChF,UAAM,SAAS,KAAK,QAAQ,iBAAiB,MAAM,OAAO,MAAM,SAAS;AACzE,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB,WAAmB,aAA2C;AAC5E,UAAM,gBAAgB,KAAK,QAAQ,iBAAiB,WAAW,SAAS;AACxE,QAAI,cAAe,QAAO;AAC1B,QAAI,aAAa;AACf,YAAM,gBAAgB,KAAK,QAAQ,iBAAiB,WAAW,WAAW;AAC1E,UAAI,cAAe,QAAO;AAAA,IAC5B;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,aAAa,QAAwB;AACnC,QAAI,OAAO,UAAU,WAAW;AAC9B,aAAO,KAAK,QAAQ,wBAAwB,OAAO,WAAW,OAAO,UAAU;AAAA,IACjF;AACA,WAAO,KAAK,QAAQ,wBAAwB,OAAO,WAAW,OAAO,UAAU;AAAA,EACjF;AAAA,EAEA,YAAY,WAAmB,aAA0C;AACvE,UAAM,SAAS,KAAK,gBAAgB,WAAW,WAAW;AAC1D,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,QAAQ,OAAO,OAAO,GAAG,WAAW,GAAG,cAAc,GAAG,MAAM,KAAK;AAAA,IAC9E;AACA,UAAM,QAAQ,KAAK,aAAa,MAAM;AACtC,UAAM,YAAY,KAAK,IAAI,GAAG,OAAO,eAAe,KAAK;AACzD,UAAM,UAAU,OAAO,eAAe,IAAI,QAAQ,OAAO,eAAe;AACxE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,cAAc;AAAA,MACd,MAAM,OAAO;AAAA,IACf;AAAA,EACF;AAAA,EAEA,YAAY,OAAoB,UAA2B;AACzD,WAAO,KAAK,QAAQ,oBAAoB,OAAO,QAAQ,IAAI;AAAA,EAC7D;AAAA,EAEA,gBAAgB,OAA6B;AAC3C,WAAO;AAAA,MACL,SAAS,KAAK,QAAQ,qBAAqB,KAAK;AAAA,MAChD,WAAW,KAAK,QAAQ,uBAAuB,KAAK;AAAA,MACpD,cAAc;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,kBACE,UACA,WACA,QACM;AACN,SAAK,QAAQ,kBAAkB,UAAU,WAAW,MAAM;AAAA,EAC5D;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| // src/db/queries.ts | ||
| function buildQueries(db) { | ||
| const insertSession = db.prepare( | ||
| `INSERT OR IGNORE INTO sessions (id, project_hash) VALUES (?, ?)` | ||
| ); | ||
| const insertToolCall = db.prepare( | ||
| `INSERT INTO tool_calls ( | ||
| session_id, tool_name, source, output_bytes, | ||
| tokens_estimated, tokens_actual, duration_ms, estimation_method, | ||
| shadow_delta_tokens, command_preview, created_at | ||
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` | ||
| ); | ||
| const getToolCallsSince = db.prepare( | ||
| `SELECT * FROM tool_calls WHERE created_at >= ? ORDER BY created_at DESC` | ||
| ); | ||
| const countToolCallsByTool = db.prepare( | ||
| `SELECT tool_name, COUNT(*) as count, COALESCE(SUM(tokens_estimated), 0) as tokens | ||
| FROM tool_calls WHERE created_at >= ? | ||
| GROUP BY tool_name ORDER BY tokens DESC` | ||
| ); | ||
| const countToolCallsBySource = db.prepare( | ||
| `SELECT source, COUNT(*) as count, COALESCE(SUM(tokens_estimated), 0) as tokens | ||
| FROM tool_calls WHERE created_at >= ? | ||
| GROUP BY source ORDER BY tokens DESC` | ||
| ); | ||
| const sumTokensBySession = db.prepare( | ||
| `SELECT COALESCE(SUM(tokens_estimated), 0) as total | ||
| FROM tool_calls WHERE session_id = ?` | ||
| ); | ||
| const countToolCallsBySessionStmt = db.prepare( | ||
| `SELECT COUNT(*) as c FROM tool_calls WHERE session_id = ?` | ||
| ); | ||
| const sumTokensBySessionSince = db.prepare( | ||
| `SELECT COALESCE(SUM(tokens_estimated), 0) as total | ||
| FROM tool_calls WHERE session_id = ? AND created_at >= ?` | ||
| ); | ||
| const sumTokensByProjectSince = db.prepare( | ||
| `SELECT COALESCE(SUM(tc.tokens_estimated), 0) as total | ||
| FROM tool_calls tc | ||
| JOIN sessions s ON tc.session_id = s.id | ||
| WHERE s.project_hash = ? AND tc.created_at >= ?` | ||
| ); | ||
| const insertToolCallMany = db.transaction((events) => { | ||
| for (const e of events) { | ||
| insertToolCall.run( | ||
| e.session_id, | ||
| e.tool_name, | ||
| e.source, | ||
| e.output_bytes, | ||
| e.tokens_estimated, | ||
| e.tokens_actual, | ||
| e.duration_ms, | ||
| e.estimation_method, | ||
| e.shadow_delta_tokens ?? null, | ||
| e.created_at | ||
| ); | ||
| } | ||
| }); | ||
| const insertSerenaTouch = db.prepare( | ||
| `INSERT INTO serena_symbol_touches (session_id, tool_name, relative_path, name_path) | ||
| VALUES (?, ?, ?, ?)` | ||
| ); | ||
| const getRecentSerenaSymbols = db.prepare( | ||
| `SELECT tool_name, relative_path, name_path, created_at | ||
| FROM serena_symbol_touches | ||
| WHERE session_id = ? | ||
| ORDER BY created_at DESC LIMIT ?` | ||
| ); | ||
| const insertRtkRewrite = db.prepare( | ||
| `INSERT INTO rtk_rewrites (session_id, command_hash, rewritten_to) VALUES (?, ?, ?)` | ||
| ); | ||
| const findRtkMark = db.prepare( | ||
| `SELECT id, rewritten_to FROM rtk_rewrites | ||
| WHERE session_id = ? AND command_hash = ? | ||
| AND created_at >= datetime('now', '-60 seconds') | ||
| ORDER BY id DESC LIMIT 1` | ||
| ); | ||
| const deleteRtkMarkById = db.prepare(`DELETE FROM rtk_rewrites WHERE id = ?`); | ||
| const purgeStaleRtkMarks = db.prepare( | ||
| `DELETE FROM rtk_rewrites WHERE created_at < datetime('now', '-60 seconds')` | ||
| ); | ||
| const upsertMetaCounter = db.prepare( | ||
| `INSERT INTO meta (key, value) VALUES (?, '1') | ||
| ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1` | ||
| ); | ||
| const getMeta = db.prepare(`SELECT value FROM meta WHERE key = ?`); | ||
| const upsertBudget = db.prepare( | ||
| `INSERT INTO budgets (scope, scope_key, limit_tokens, mode) | ||
| VALUES (?, ?, ?, ?) | ||
| ON CONFLICT(scope, scope_key) DO UPDATE SET | ||
| limit_tokens = excluded.limit_tokens, | ||
| mode = excluded.mode` | ||
| ); | ||
| const selectBudgetByScope = db.prepare( | ||
| `SELECT id, scope, scope_key, limit_tokens, spent_tokens, mode, created_at | ||
| FROM budgets WHERE scope = ? AND scope_key = ?` | ||
| ); | ||
| const deleteBudgetByScope = db.prepare( | ||
| `DELETE FROM budgets WHERE scope = ? AND scope_key = ?` | ||
| ); | ||
| const listBudgets = db.prepare( | ||
| `SELECT id, scope, scope_key, limit_tokens, spent_tokens, mode, created_at | ||
| FROM budgets ORDER BY created_at DESC` | ||
| ); | ||
| const insertBudgetEvent = db.prepare( | ||
| `INSERT INTO budget_events (budget_id, event_type, tokens) VALUES (?, ?, ?)` | ||
| ); | ||
| return { | ||
| // sessions | ||
| insertSession(id, projectHash) { | ||
| insertSession.run(id, projectHash); | ||
| }, | ||
| // tool_calls | ||
| insertToolCall(event) { | ||
| insertToolCall.run( | ||
| event.session_id, | ||
| event.tool_name, | ||
| event.source, | ||
| event.output_bytes, | ||
| event.tokens_estimated, | ||
| event.tokens_actual, | ||
| event.duration_ms, | ||
| event.estimation_method, | ||
| event.shadow_delta_tokens ?? null, | ||
| event.command_preview ?? null, | ||
| event.created_at | ||
| ); | ||
| }, | ||
| insertToolCallMany(events) { | ||
| insertToolCallMany(events); | ||
| }, | ||
| getToolCallsSince(timestamp) { | ||
| return getToolCallsSince.all(timestamp); | ||
| }, | ||
| countToolCallsByTool(since) { | ||
| return countToolCallsByTool.all(since); | ||
| }, | ||
| countToolCallsBySource(since) { | ||
| return countToolCallsBySource.all(since); | ||
| }, | ||
| sumTokensBySession(sessionId) { | ||
| const row = sumTokensBySession.get(sessionId); | ||
| return row?.total ?? 0; | ||
| }, | ||
| countToolCallsBySession(sessionId) { | ||
| const row = countToolCallsBySessionStmt.get(sessionId); | ||
| return row?.c ?? 0; | ||
| }, | ||
| sumTokensBySessionSince(sessionId, since) { | ||
| const row = sumTokensBySessionSince.get(sessionId, since); | ||
| return row?.total ?? 0; | ||
| }, | ||
| sumTokensByProjectSince(projectHash, since) { | ||
| const row = sumTokensByProjectSince.get(projectHash, since); | ||
| return row?.total ?? 0; | ||
| }, | ||
| // meta | ||
| upsertMetaCounter(key) { | ||
| upsertMetaCounter.run(key); | ||
| }, | ||
| getMeta(key) { | ||
| const row = getMeta.get(key); | ||
| return row?.value ?? null; | ||
| }, | ||
| // budgets | ||
| upsertBudget(scope, scopeKey, limitTokens, mode) { | ||
| upsertBudget.run(scope, scopeKey, limitTokens, mode); | ||
| }, | ||
| getBudgetByScope(scope, scopeKey) { | ||
| const row = selectBudgetByScope.get(scope, scopeKey); | ||
| return row ?? null; | ||
| }, | ||
| deleteBudgetByScope(scope, scopeKey) { | ||
| const info = deleteBudgetByScope.run(scope, scopeKey); | ||
| return info.changes; | ||
| }, | ||
| listBudgets() { | ||
| return listBudgets.all(); | ||
| }, | ||
| insertBudgetEvent(budgetId, eventType, tokens) { | ||
| insertBudgetEvent.run(budgetId, eventType, tokens); | ||
| }, | ||
| // rtk rewrites | ||
| insertRtkRewrite(sessionId, commandHash, rewrittenTo) { | ||
| insertRtkRewrite.run(sessionId, commandHash, rewrittenTo); | ||
| }, | ||
| /** | ||
| * Look up and consume an RTK rewrite mark. Returns the rewritten command if | ||
| * there was a mark for this (session, hash) within the last 60 s, or null. | ||
| * The row is deleted on match so it cannot be double-consumed. | ||
| */ | ||
| consumeRtkRewrite(sessionId, commandHash) { | ||
| const row = findRtkMark.get(sessionId, commandHash); | ||
| if (!row) return null; | ||
| deleteRtkMarkById.run(row.id); | ||
| return row.rewritten_to; | ||
| }, | ||
| purgeStaleRtkMarks() { | ||
| const info = purgeStaleRtkMarks.run(); | ||
| return info.changes; | ||
| }, | ||
| // serena symbol touches | ||
| insertSerenaTouch(sessionId, toolName, relativePath, namePath) { | ||
| insertSerenaTouch.run(sessionId, toolName, relativePath, namePath); | ||
| }, | ||
| getRecentSerenaSymbols(sessionId, limit) { | ||
| return getRecentSerenaSymbols.all(sessionId, limit); | ||
| } | ||
| }; | ||
| } | ||
| export { | ||
| buildQueries | ||
| }; | ||
| //# sourceMappingURL=chunk-OIZX7KF2.js.map |
| {"version":3,"sources":["../src/db/queries.ts"],"sourcesContent":["// Prepared statement factory — Phase 1.7 + 2.2\r\n\r\nimport type Database from 'better-sqlite3'\r\nimport type { ToolEvent, Budget, BudgetScope, BudgetMode } from '../lib/types.js'\r\n\r\ntype DB = Database.Database\r\n\r\nexport interface ToolCountRow {\r\n tool_name: string\r\n count: number\r\n tokens: number\r\n}\r\n\r\nexport interface SourceCountRow {\r\n source: string\r\n count: number\r\n tokens: number\r\n}\r\n\r\nexport function buildQueries(db: DB) {\r\n // ── sessions ──\r\n const insertSession = db.prepare(\r\n `INSERT OR IGNORE INTO sessions (id, project_hash) VALUES (?, ?)`,\r\n )\r\n\r\n // ── tool_calls ──\r\n const insertToolCall = db.prepare(\r\n `INSERT INTO tool_calls (\r\n session_id, tool_name, source, output_bytes,\r\n tokens_estimated, tokens_actual, duration_ms, estimation_method,\r\n shadow_delta_tokens, command_preview, created_at\r\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\r\n )\r\n\r\n const getToolCallsSince = db.prepare(\r\n `SELECT * FROM tool_calls WHERE created_at >= ? ORDER BY created_at DESC`,\r\n )\r\n\r\n const countToolCallsByTool = db.prepare(\r\n `SELECT tool_name, COUNT(*) as count, COALESCE(SUM(tokens_estimated), 0) as tokens\r\n FROM tool_calls WHERE created_at >= ?\r\n GROUP BY tool_name ORDER BY tokens DESC`,\r\n )\r\n\r\n const countToolCallsBySource = db.prepare(\r\n `SELECT source, COUNT(*) as count, COALESCE(SUM(tokens_estimated), 0) as tokens\r\n FROM tool_calls WHERE created_at >= ?\r\n GROUP BY source ORDER BY tokens DESC`,\r\n )\r\n\r\n const sumTokensBySession = db.prepare(\r\n `SELECT COALESCE(SUM(tokens_estimated), 0) as total\r\n FROM tool_calls WHERE session_id = ?`,\r\n )\r\n\r\n const countToolCallsBySessionStmt = db.prepare(\r\n `SELECT COUNT(*) as c FROM tool_calls WHERE session_id = ?`,\r\n )\r\n\r\n const sumTokensBySessionSince = db.prepare(\r\n `SELECT COALESCE(SUM(tokens_estimated), 0) as total\r\n FROM tool_calls WHERE session_id = ? AND created_at >= ?`,\r\n )\r\n\r\n const sumTokensByProjectSince = db.prepare(\r\n `SELECT COALESCE(SUM(tc.tokens_estimated), 0) as total\r\n FROM tool_calls tc\r\n JOIN sessions s ON tc.session_id = s.id\r\n WHERE s.project_hash = ? AND tc.created_at >= ?`,\r\n )\r\n\r\n const insertToolCallMany = db.transaction((events: readonly ToolEvent[]) => {\r\n for (const e of events) {\r\n insertToolCall.run(\r\n e.session_id,\r\n e.tool_name,\r\n e.source,\r\n e.output_bytes,\r\n e.tokens_estimated,\r\n e.tokens_actual,\r\n e.duration_ms,\r\n e.estimation_method,\r\n e.shadow_delta_tokens ?? null,\r\n e.created_at,\r\n )\r\n }\r\n })\r\n\r\n // ── serena_symbol_touches ──\r\n const insertSerenaTouch = db.prepare(\r\n `INSERT INTO serena_symbol_touches (session_id, tool_name, relative_path, name_path)\r\n VALUES (?, ?, ?, ?)`,\r\n )\r\n\r\n const getRecentSerenaSymbols = db.prepare(\r\n `SELECT tool_name, relative_path, name_path, created_at\r\n FROM serena_symbol_touches\r\n WHERE session_id = ?\r\n ORDER BY created_at DESC LIMIT ?`,\r\n )\r\n\r\n // ── meta ──\r\n // ── rtk_rewrites ──\r\n const insertRtkRewrite = db.prepare(\r\n `INSERT INTO rtk_rewrites (session_id, command_hash, rewritten_to) VALUES (?, ?, ?)`,\r\n )\r\n // Consume (look up AND delete) the most recent matching mark. If the PostToolUse\r\n // hook finds a mark for this (session, command_hash), the call was rewritten by\r\n // rtk in PreToolUse and must be reclassified as source=rtk. We bound the lookup\r\n // to 60 s so stale marks never poison future events.\r\n const findRtkMark = db.prepare(\r\n `SELECT id, rewritten_to FROM rtk_rewrites\r\n WHERE session_id = ? AND command_hash = ?\r\n AND created_at >= datetime('now', '-60 seconds')\r\n ORDER BY id DESC LIMIT 1`,\r\n )\r\n const deleteRtkMarkById = db.prepare(`DELETE FROM rtk_rewrites WHERE id = ?`)\r\n const purgeStaleRtkMarks = db.prepare(\r\n `DELETE FROM rtk_rewrites WHERE created_at < datetime('now', '-60 seconds')`,\r\n )\r\n\r\n const upsertMetaCounter = db.prepare(\r\n `INSERT INTO meta (key, value) VALUES (?, '1')\r\n ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1`,\r\n )\r\n\r\n const getMeta = db.prepare(`SELECT value FROM meta WHERE key = ?`)\r\n\r\n // ── budgets ──\r\n const upsertBudget = db.prepare(\r\n `INSERT INTO budgets (scope, scope_key, limit_tokens, mode)\r\n VALUES (?, ?, ?, ?)\r\n ON CONFLICT(scope, scope_key) DO UPDATE SET\r\n limit_tokens = excluded.limit_tokens,\r\n mode = excluded.mode`,\r\n )\r\n\r\n const selectBudgetByScope = db.prepare(\r\n `SELECT id, scope, scope_key, limit_tokens, spent_tokens, mode, created_at\r\n FROM budgets WHERE scope = ? AND scope_key = ?`,\r\n )\r\n\r\n const deleteBudgetByScope = db.prepare(\r\n `DELETE FROM budgets WHERE scope = ? AND scope_key = ?`,\r\n )\r\n\r\n const listBudgets = db.prepare(\r\n `SELECT id, scope, scope_key, limit_tokens, spent_tokens, mode, created_at\r\n FROM budgets ORDER BY created_at DESC`,\r\n )\r\n\r\n const insertBudgetEvent = db.prepare(\r\n `INSERT INTO budget_events (budget_id, event_type, tokens) VALUES (?, ?, ?)`,\r\n )\r\n\r\n return {\r\n // sessions\r\n insertSession(id: string, projectHash: string | null) {\r\n insertSession.run(id, projectHash)\r\n },\r\n\r\n // tool_calls\r\n insertToolCall(event: ToolEvent) {\r\n insertToolCall.run(\r\n event.session_id,\r\n event.tool_name,\r\n event.source,\r\n event.output_bytes,\r\n event.tokens_estimated,\r\n event.tokens_actual,\r\n event.duration_ms,\r\n event.estimation_method,\r\n event.shadow_delta_tokens ?? null,\r\n event.command_preview ?? null,\r\n event.created_at,\r\n )\r\n },\r\n insertToolCallMany(events: readonly ToolEvent[]) {\r\n insertToolCallMany(events)\r\n },\r\n getToolCallsSince(timestamp: string) {\r\n return getToolCallsSince.all(timestamp)\r\n },\r\n countToolCallsByTool(since: string): ToolCountRow[] {\r\n return countToolCallsByTool.all(since) as ToolCountRow[]\r\n },\r\n countToolCallsBySource(since: string): SourceCountRow[] {\r\n return countToolCallsBySource.all(since) as SourceCountRow[]\r\n },\r\n sumTokensBySession(sessionId: string): number {\r\n const row = sumTokensBySession.get(sessionId) as { total: number } | undefined\r\n return row?.total ?? 0\r\n },\r\n countToolCallsBySession(sessionId: string): number {\r\n const row = countToolCallsBySessionStmt.get(sessionId) as { c: number } | undefined\r\n return row?.c ?? 0\r\n },\r\n sumTokensBySessionSince(sessionId: string, since: string): number {\r\n const row = sumTokensBySessionSince.get(sessionId, since) as { total: number } | undefined\r\n return row?.total ?? 0\r\n },\r\n sumTokensByProjectSince(projectHash: string, since: string): number {\r\n const row = sumTokensByProjectSince.get(projectHash, since) as { total: number } | undefined\r\n return row?.total ?? 0\r\n },\r\n\r\n // meta\r\n upsertMetaCounter(key: string) {\r\n upsertMetaCounter.run(key)\r\n },\r\n getMeta(key: string): string | null {\r\n const row = getMeta.get(key) as { value: string } | undefined\r\n return row?.value ?? null\r\n },\r\n\r\n // budgets\r\n upsertBudget(scope: BudgetScope, scopeKey: string, limitTokens: number, mode: BudgetMode) {\r\n upsertBudget.run(scope, scopeKey, limitTokens, mode)\r\n },\r\n getBudgetByScope(scope: BudgetScope, scopeKey: string): Budget | null {\r\n const row = selectBudgetByScope.get(scope, scopeKey) as Budget | undefined\r\n return row ?? null\r\n },\r\n deleteBudgetByScope(scope: BudgetScope, scopeKey: string): number {\r\n const info = deleteBudgetByScope.run(scope, scopeKey)\r\n return info.changes as number\r\n },\r\n listBudgets(): Budget[] {\r\n return listBudgets.all() as Budget[]\r\n },\r\n insertBudgetEvent(budgetId: number, eventType: string, tokens: number | null) {\r\n insertBudgetEvent.run(budgetId, eventType, tokens)\r\n },\r\n\r\n // rtk rewrites\r\n insertRtkRewrite(sessionId: string, commandHash: string, rewrittenTo: string) {\r\n insertRtkRewrite.run(sessionId, commandHash, rewrittenTo)\r\n },\r\n /**\r\n * Look up and consume an RTK rewrite mark. Returns the rewritten command if\r\n * there was a mark for this (session, hash) within the last 60 s, or null.\r\n * The row is deleted on match so it cannot be double-consumed.\r\n */\r\n consumeRtkRewrite(sessionId: string, commandHash: string): string | null {\r\n const row = findRtkMark.get(sessionId, commandHash) as\r\n | { id: number; rewritten_to: string }\r\n | undefined\r\n if (!row) return null\r\n deleteRtkMarkById.run(row.id)\r\n return row.rewritten_to\r\n },\r\n purgeStaleRtkMarks(): number {\r\n const info = purgeStaleRtkMarks.run()\r\n return info.changes as number\r\n },\r\n\r\n // serena symbol touches\r\n insertSerenaTouch(\r\n sessionId: string,\r\n toolName: string,\r\n relativePath: string,\r\n namePath: string | null,\r\n ) {\r\n insertSerenaTouch.run(sessionId, toolName, relativePath, namePath)\r\n },\r\n getRecentSerenaSymbols(\r\n sessionId: string,\r\n limit: number,\r\n ): { tool_name: string; relative_path: string; name_path: string | null; created_at: string }[] {\r\n return getRecentSerenaSymbols.all(sessionId, limit) as {\r\n tool_name: string\r\n relative_path: string\r\n name_path: string | null\r\n created_at: string\r\n }[]\r\n },\r\n }\r\n}\r\n\r\nexport type Queries = ReturnType<typeof buildQueries>\r\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| // src/db/connection.ts | ||
| import Database from "better-sqlite3"; | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| // src/db/schema.ts | ||
| var SCHEMA_SQL = ` | ||
| CREATE TABLE IF NOT EXISTS sessions ( | ||
| id TEXT PRIMARY KEY, | ||
| project_hash TEXT, | ||
| started_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS tool_calls ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, | ||
| tool_name TEXT NOT NULL, | ||
| source TEXT NOT NULL, | ||
| output_bytes INTEGER NOT NULL, | ||
| tokens_estimated INTEGER NOT NULL, | ||
| tokens_actual INTEGER, | ||
| duration_ms INTEGER, | ||
| estimation_method TEXT, | ||
| shadow_delta_tokens INTEGER, | ||
| command_preview TEXT, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, created_at DESC); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_source ON tool_calls(source); | ||
| CREATE TABLE IF NOT EXISTS budgets ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| scope TEXT NOT NULL CHECK(scope IN ('session', 'project')), | ||
| scope_key TEXT NOT NULL, | ||
| limit_tokens INTEGER NOT NULL CHECK(limit_tokens > 0), | ||
| spent_tokens INTEGER NOT NULL DEFAULT 0, | ||
| mode TEXT NOT NULL CHECK(mode IN ('warn')), | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')), | ||
| UNIQUE(scope, scope_key) | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS budget_events ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| budget_id INTEGER NOT NULL REFERENCES budgets(id) ON DELETE CASCADE, | ||
| event_type TEXT NOT NULL CHECK(event_type IN ('spend', 'warn', 'reset')), | ||
| tokens INTEGER, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS optimization_snapshots ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| method TEXT NOT NULL, | ||
| details TEXT, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS coach_surface_log ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, | ||
| rule_id TEXT NOT NULL, | ||
| tip_id TEXT NOT NULL, | ||
| surfaced_via TEXT NOT NULL, | ||
| severity TEXT NOT NULL, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_coach_surface_log_session | ||
| ON coach_surface_log(session_id, created_at DESC); | ||
| CREATE TABLE IF NOT EXISTS meta ( | ||
| key TEXT PRIMARY KEY, | ||
| value TEXT NOT NULL | ||
| ); | ||
| -- Pending RTK rewrites. PreToolUse writes a row when it rewrites a Bash command | ||
| -- via rtk rewrite; PostToolUse consumes it to reclassify the event as source=rtk. | ||
| -- The PostToolUse hook only sees the ORIGINAL command (what Claude asked for), | ||
| -- not the command Claude Code actually executed after PreToolUse mutation. This | ||
| -- table bridges the gap so stats reflect the real RTK activity. | ||
| CREATE TABLE IF NOT EXISTS rtk_rewrites ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL, | ||
| command_hash TEXT NOT NULL, | ||
| rewritten_to TEXT NOT NULL, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_rtk_rewrites_lookup | ||
| ON rtk_rewrites(session_id, command_hash, created_at DESC); | ||
| -- Serena symbol touches. PostToolUse writes a row for each Serena read tool call | ||
| -- that includes a relative_path, so SessionStart:compact can re-inject relevant | ||
| -- symbols when context is reset. | ||
| CREATE TABLE IF NOT EXISTS serena_symbol_touches ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, | ||
| tool_name TEXT NOT NULL, | ||
| relative_path TEXT NOT NULL, | ||
| name_path TEXT, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_serena_symbol_touches_session | ||
| ON serena_symbol_touches(session_id, created_at DESC); | ||
| `; | ||
| // src/db/connection.ts | ||
| var dbInstance = null; | ||
| var currentPath = null; | ||
| function migrateLegacyToolCallsSchema(db) { | ||
| const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all(); | ||
| if (cols.length === 0) return; | ||
| const hasInputHash = cols.some((c) => c.name === "input_hash"); | ||
| if (!hasInputHash) return; | ||
| db.exec(` | ||
| DROP TRIGGER IF EXISTS tool_calls_ai; | ||
| DROP TRIGGER IF EXISTS tool_calls_ad; | ||
| DROP TABLE IF EXISTS events_fts; | ||
| `); | ||
| const migrate = db.transaction(() => { | ||
| db.exec(` | ||
| CREATE TABLE tool_calls_new ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, | ||
| tool_name TEXT NOT NULL, | ||
| source TEXT NOT NULL, | ||
| output_bytes INTEGER NOT NULL, | ||
| tokens_estimated INTEGER NOT NULL, | ||
| tokens_actual INTEGER, | ||
| duration_ms INTEGER, | ||
| estimation_method TEXT, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| INSERT INTO tool_calls_new ( | ||
| id, session_id, tool_name, source, output_bytes, | ||
| tokens_estimated, tokens_actual, duration_ms, estimation_method, created_at | ||
| ) | ||
| SELECT id, session_id, tool_name, source, output_bytes, | ||
| tokens_estimated, tokens_actual, duration_ms, estimation_method, created_at | ||
| FROM tool_calls; | ||
| DROP TABLE tool_calls; | ||
| ALTER TABLE tool_calls_new RENAME TO tool_calls; | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, created_at DESC); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_source ON tool_calls(source); | ||
| `); | ||
| }); | ||
| migrate(); | ||
| } | ||
| function migrateAddShadowDeltaTokens(db) { | ||
| const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all(); | ||
| if (cols.some((c) => c.name === "shadow_delta_tokens")) return; | ||
| db.exec(`ALTER TABLE tool_calls ADD COLUMN shadow_delta_tokens INTEGER`); | ||
| } | ||
| function migrateAddCommandPreview(db) { | ||
| const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all(); | ||
| if (cols.some((c) => c.name === "command_preview")) return; | ||
| db.exec(`ALTER TABLE tool_calls ADD COLUMN command_preview TEXT`); | ||
| } | ||
| function getDb(dbPath) { | ||
| const resolvedPath = dbPath ?? ":memory:"; | ||
| if (dbInstance && currentPath === resolvedPath) { | ||
| return dbInstance; | ||
| } | ||
| if (dbInstance && currentPath !== resolvedPath) { | ||
| try { | ||
| dbInstance.close(); | ||
| } catch { | ||
| } | ||
| dbInstance = null; | ||
| } | ||
| if (resolvedPath !== ":memory:") { | ||
| const dir = path.dirname(resolvedPath); | ||
| if (!fs.existsSync(dir)) { | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| } | ||
| } | ||
| const db = new Database(resolvedPath); | ||
| db.pragma("journal_mode = WAL"); | ||
| db.pragma("foreign_keys = ON"); | ||
| db.exec(SCHEMA_SQL); | ||
| migrateLegacyToolCallsSchema(db); | ||
| migrateAddShadowDeltaTokens(db); | ||
| migrateAddCommandPreview(db); | ||
| dbInstance = db; | ||
| currentPath = resolvedPath; | ||
| return db; | ||
| } | ||
| export { | ||
| getDb | ||
| }; | ||
| //# sourceMappingURL=chunk-TOEPQYR3.js.map |
| {"version":3,"sources":["../src/db/connection.ts","../src/db/schema.ts"],"sourcesContent":["// getDb() singleton with WAL + FK — Phase 1.3\r\n\r\nimport Database from 'better-sqlite3'\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport { SCHEMA_SQL } from './schema.js'\r\n\r\ntype DB = Database.Database\r\n\r\nlet dbInstance: DB | null = null\r\nlet currentPath: string | null = null\r\n\r\n/**\r\n * Migrate legacy tool_calls schema to the current simplified one.\r\n *\r\n * DBs created before Phase 1.2 had: input_hash TEXT NOT NULL, tool_input_summary TEXT, content TEXT,\r\n * plus FTS5 virtual table events_fts with AFTER INSERT/DELETE triggers referencing `content`.\r\n *\r\n * The current code no longer passes input_hash or content, so inserts on legacy DBs fail silently\r\n * with \"NOT NULL constraint failed: tool_calls.input_hash\" — data is lost, droppedEvents counter grows.\r\n *\r\n * This migration rebuilds tool_calls with the current schema, preserving historical rows.\r\n */\r\nfunction migrateLegacyToolCallsSchema(db: DB): void {\r\n const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all() as Array<{ name: string }>\r\n if (cols.length === 0) return // table didn't exist yet, SCHEMA_SQL just created it clean\r\n const hasInputHash = cols.some((c) => c.name === 'input_hash')\r\n if (!hasInputHash) return // already migrated\r\n\r\n // Drop FTS5 triggers that reference `content` before we touch the table\r\n db.exec(`\r\n DROP TRIGGER IF EXISTS tool_calls_ai;\r\n DROP TRIGGER IF EXISTS tool_calls_ad;\r\n DROP TABLE IF EXISTS events_fts;\r\n `)\r\n\r\n // Rebuild tool_calls with the new schema. Wrap in a transaction for atomicity.\r\n const migrate = db.transaction(() => {\r\n db.exec(`\r\n CREATE TABLE tool_calls_new (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\r\n tool_name TEXT NOT NULL,\r\n source TEXT NOT NULL,\r\n output_bytes INTEGER NOT NULL,\r\n tokens_estimated INTEGER NOT NULL,\r\n tokens_actual INTEGER,\r\n duration_ms INTEGER,\r\n estimation_method TEXT,\r\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\r\n );\r\n INSERT INTO tool_calls_new (\r\n id, session_id, tool_name, source, output_bytes,\r\n tokens_estimated, tokens_actual, duration_ms, estimation_method, created_at\r\n )\r\n SELECT id, session_id, tool_name, source, output_bytes,\r\n tokens_estimated, tokens_actual, duration_ms, estimation_method, created_at\r\n FROM tool_calls;\r\n DROP TABLE tool_calls;\r\n ALTER TABLE tool_calls_new RENAME TO tool_calls;\r\n CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, created_at DESC);\r\n CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name);\r\n CREATE INDEX IF NOT EXISTS idx_tool_calls_source ON tool_calls(source);\r\n `)\r\n })\r\n migrate()\r\n}\r\n\r\n/** Add shadow_delta_tokens column to existing DBs that were created before Sprint C. */\r\nfunction migrateAddShadowDeltaTokens(db: DB): void {\r\n const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all() as Array<{ name: string }>\r\n if (cols.some((c) => c.name === 'shadow_delta_tokens')) return // already present\r\n db.exec(`ALTER TABLE tool_calls ADD COLUMN shadow_delta_tokens INTEGER`)\r\n}\r\n\r\nfunction migrateAddCommandPreview(db: DB): void {\r\n const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all() as Array<{ name: string }>\r\n if (cols.some((c) => c.name === 'command_preview')) return // already present\r\n db.exec(`ALTER TABLE tool_calls ADD COLUMN command_preview TEXT`)\r\n}\r\n\r\nexport function getDb(dbPath?: string): DB {\r\n const resolvedPath = dbPath ?? ':memory:'\r\n if (dbInstance && currentPath === resolvedPath) {\r\n return dbInstance\r\n }\r\n if (dbInstance && currentPath !== resolvedPath) {\r\n try {\r\n dbInstance.close()\r\n } catch {\r\n // swallow\r\n }\r\n dbInstance = null\r\n }\r\n if (resolvedPath !== ':memory:') {\r\n const dir = path.dirname(resolvedPath)\r\n if (!fs.existsSync(dir)) {\r\n fs.mkdirSync(dir, { recursive: true })\r\n }\r\n }\r\n const db = new Database(resolvedPath)\r\n db.pragma('journal_mode = WAL')\r\n db.pragma('foreign_keys = ON')\r\n db.exec(SCHEMA_SQL)\r\n migrateLegacyToolCallsSchema(db)\r\n migrateAddShadowDeltaTokens(db)\r\n migrateAddCommandPreview(db)\r\n dbInstance = db\r\n currentPath = resolvedPath\r\n return db\r\n}\r\n\r\nexport function closeDb(): void {\r\n if (dbInstance) {\r\n try {\r\n dbInstance.close()\r\n } catch {\r\n // swallow\r\n }\r\n dbInstance = null\r\n currentPath = null\r\n }\r\n}\r\n","// SQLite schema — Phase 1.2 (simplified: removed FTS5, input_hash, tool_input_summary, content)\r\n// Covers: sessions, tool_calls, budgets, budget_events, optimization_snapshots, coach_surface_log, meta\r\n\r\nexport const SCHEMA_SQL = `\r\nCREATE TABLE IF NOT EXISTS sessions (\r\n id TEXT PRIMARY KEY,\r\n project_hash TEXT,\r\n started_at TEXT NOT NULL DEFAULT (datetime('now'))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS tool_calls (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\r\n tool_name TEXT NOT NULL,\r\n source TEXT NOT NULL,\r\n output_bytes INTEGER NOT NULL,\r\n tokens_estimated INTEGER NOT NULL,\r\n tokens_actual INTEGER,\r\n duration_ms INTEGER,\r\n estimation_method TEXT,\r\n shadow_delta_tokens INTEGER,\r\n command_preview TEXT,\r\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, created_at DESC);\r\nCREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name);\r\nCREATE INDEX IF NOT EXISTS idx_tool_calls_source ON tool_calls(source);\r\n\r\nCREATE TABLE IF NOT EXISTS budgets (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n scope TEXT NOT NULL CHECK(scope IN ('session', 'project')),\r\n scope_key TEXT NOT NULL,\r\n limit_tokens INTEGER NOT NULL CHECK(limit_tokens > 0),\r\n spent_tokens INTEGER NOT NULL DEFAULT 0,\r\n mode TEXT NOT NULL CHECK(mode IN ('warn')),\r\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\r\n UNIQUE(scope, scope_key)\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS budget_events (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n budget_id INTEGER NOT NULL REFERENCES budgets(id) ON DELETE CASCADE,\r\n event_type TEXT NOT NULL CHECK(event_type IN ('spend', 'warn', 'reset')),\r\n tokens INTEGER,\r\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS optimization_snapshots (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n method TEXT NOT NULL,\r\n details TEXT,\r\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\r\n);\r\n\r\nCREATE TABLE IF NOT EXISTS coach_surface_log (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\r\n rule_id TEXT NOT NULL,\r\n tip_id TEXT NOT NULL,\r\n surfaced_via TEXT NOT NULL,\r\n severity TEXT NOT NULL,\r\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_coach_surface_log_session\r\n ON coach_surface_log(session_id, created_at DESC);\r\n\r\nCREATE TABLE IF NOT EXISTS meta (\r\n key TEXT PRIMARY KEY,\r\n value TEXT NOT NULL\r\n);\r\n\r\n-- Pending RTK rewrites. PreToolUse writes a row when it rewrites a Bash command\r\n-- via rtk rewrite; PostToolUse consumes it to reclassify the event as source=rtk.\r\n-- The PostToolUse hook only sees the ORIGINAL command (what Claude asked for),\r\n-- not the command Claude Code actually executed after PreToolUse mutation. This\r\n-- table bridges the gap so stats reflect the real RTK activity.\r\nCREATE TABLE IF NOT EXISTS rtk_rewrites (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n session_id TEXT NOT NULL,\r\n command_hash TEXT NOT NULL,\r\n rewritten_to TEXT NOT NULL,\r\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_rtk_rewrites_lookup\r\n ON rtk_rewrites(session_id, command_hash, created_at DESC);\r\n\r\n-- Serena symbol touches. PostToolUse writes a row for each Serena read tool call\r\n-- that includes a relative_path, so SessionStart:compact can re-inject relevant\r\n-- symbols when context is reset.\r\nCREATE TABLE IF NOT EXISTS serena_symbol_touches (\r\n id INTEGER PRIMARY KEY AUTOINCREMENT,\r\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\r\n tool_name TEXT NOT NULL,\r\n relative_path TEXT NOT NULL,\r\n name_path TEXT,\r\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\r\n);\r\n\r\nCREATE INDEX IF NOT EXISTS idx_serena_symbol_touches_session\r\n ON serena_symbol_touches(session_id, created_at DESC);\r\n`\r\n"],"mappings":";;;AAEA,OAAO,cAAc;AACrB,OAAO,QAAQ;AACf,OAAO,UAAU;;;ACDV,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ADM1B,IAAI,aAAwB;AAC5B,IAAI,cAA6B;AAajC,SAAS,6BAA6B,IAAc;AAClD,QAAM,OAAO,GAAG,QAAQ,iCAAiC,EAAE,IAAI;AAC/D,MAAI,KAAK,WAAW,EAAG;AACvB,QAAM,eAAe,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,YAAY;AAC7D,MAAI,CAAC,aAAc;AAGnB,KAAG,KAAK;AAAA;AAAA;AAAA;AAAA,GAIP;AAGD,QAAM,UAAU,GAAG,YAAY,MAAM;AACnC,OAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAyBP;AAAA,EACH,CAAC;AACD,UAAQ;AACV;AAGA,SAAS,4BAA4B,IAAc;AACjD,QAAM,OAAO,GAAG,QAAQ,iCAAiC,EAAE,IAAI;AAC/D,MAAI,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,qBAAqB,EAAG;AACxD,KAAG,KAAK,+DAA+D;AACzE;AAEA,SAAS,yBAAyB,IAAc;AAC9C,QAAM,OAAO,GAAG,QAAQ,iCAAiC,EAAE,IAAI;AAC/D,MAAI,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAG;AACpD,KAAG,KAAK,wDAAwD;AAClE;AAEO,SAAS,MAAM,QAAqB;AACzC,QAAM,eAAe,UAAU;AAC/B,MAAI,cAAc,gBAAgB,cAAc;AAC9C,WAAO;AAAA,EACT;AACA,MAAI,cAAc,gBAAgB,cAAc;AAC9C,QAAI;AACF,iBAAW,MAAM;AAAA,IACnB,QAAQ;AAAA,IAER;AACA,iBAAa;AAAA,EACf;AACA,MAAI,iBAAiB,YAAY;AAC/B,UAAM,MAAM,KAAK,QAAQ,YAAY;AACrC,QAAI,CAAC,GAAG,WAAW,GAAG,GAAG;AACvB,SAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,IACvC;AAAA,EACF;AACA,QAAM,KAAK,IAAI,SAAS,YAAY;AACpC,KAAG,OAAO,oBAAoB;AAC9B,KAAG,OAAO,mBAAmB;AAC7B,KAAG,KAAK,UAAU;AAClB,+BAA6B,EAAE;AAC/B,8BAA4B,EAAE;AAC9B,2BAAyB,EAAE;AAC3B,eAAa;AACb,gBAAc;AACd,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| clearSurfaceLog, | ||
| measureContextSize, | ||
| runRules | ||
| } from "./chunk-22BLF7AQ.js"; | ||
| import "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-OIZX7KF2.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/coach.ts | ||
| import fs from "fs"; | ||
| async function runCoachCli(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const sub = args[0] ?? "status"; | ||
| if (sub === "list") { | ||
| print(`Knowledge base (${KNOWLEDGE_BASE.length} tips):`); | ||
| for (const tip of KNOWLEDGE_BASE) { | ||
| print(` \u2022 ${tip.id.padEnd(32)} ${tip.title}`); | ||
| } | ||
| return 0; | ||
| } | ||
| if (sub === "explain") { | ||
| const tipId = args[1]; | ||
| if (!tipId) { | ||
| print("Uso: token-optimizer-mcp coach explain <tip_id>"); | ||
| return 1; | ||
| } | ||
| const tip = KNOWLEDGE_BASE.find((t) => t.id === tipId); | ||
| if (!tip) { | ||
| print(`Tip no encontrado: ${tipId}`); | ||
| return 1; | ||
| } | ||
| print(tip.title); | ||
| print(""); | ||
| print(tip.description); | ||
| print(""); | ||
| print(`Como usarlo: ${tip.how_to_invoke}`); | ||
| print(`Cuando: ${tip.when_applicable}`); | ||
| print(`Ahorro: ${tip.savings_estimate}`); | ||
| print(`Fuente: ${tip.savings_source} \xB7 verificado: ${tip.verified_at}`); | ||
| return 0; | ||
| } | ||
| if (sub === "reset") { | ||
| const projectDir2 = resolveProjectDir(cwd); | ||
| const dbPath2 = resolveAnalyticsDbPath(projectDir2); | ||
| if (!fs.existsSync(dbPath2)) { | ||
| print("Sin DB; nada que resetear."); | ||
| return 0; | ||
| } | ||
| const db2 = getDb(dbPath2); | ||
| const deleted = clearSurfaceLog(db2); | ||
| print(`Log de coach reseteado (${deleted} entradas eliminadas)`); | ||
| return 0; | ||
| } | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) { | ||
| print("Coach status: sin datos. Ejecuta el hook posttooluse al menos una vez."); | ||
| return 0; | ||
| } | ||
| const db = getDb(dbPath); | ||
| const contextOpts = { db }; | ||
| if (projectDir) contextOpts.projectDir = projectDir; | ||
| const context = await measureContextSize("default", contextOpts); | ||
| const queries = buildQueries(db); | ||
| const since = new Date(Date.now() - 864e5).toISOString(); | ||
| const rawRows = queries.getToolCallsSince(since); | ||
| const ctx = { | ||
| session_id: "default", | ||
| events: rawRows.slice(0, 100), | ||
| session_token_total: context.tokens, | ||
| session_token_method: context.estimation_method, | ||
| session_token_limit: context.limit, | ||
| active_model: null | ||
| }; | ||
| const hits = runRules(ctx); | ||
| print("token-optimizer-mcp coach status"); | ||
| print(""); | ||
| print( | ||
| `Contexto: ${(context.percent * 100).toFixed(1)}% (${context.tokens}/${context.limit} tokens, ${context.estimation_method})` | ||
| ); | ||
| print(`Tips activos: ${hits.length}`); | ||
| if (hits.length === 0) { | ||
| print(" (sin tips disparados en este momento)"); | ||
| } else { | ||
| for (const h of hits) { | ||
| print(` [${h.severity}] ${h.rule_id}: ${h.evidence}`); | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
| export { | ||
| runCoachCli | ||
| }; | ||
| //# sourceMappingURL=coach-RGJZHRQK.js.map |
| {"version":3,"sources":["../src/cli/coach.ts"],"sourcesContent":["// Coach CLI — Phase 4.50\n// Subcommands: status | list | explain <tip_id> | reset\n\nimport fs from 'node:fs'\nimport { KNOWLEDGE_BASE } from '../coach/knowledge-base.js'\nimport { runRules } from '../coach/detector.js'\nimport { measureContextSize } from '../coach/context-meter.js'\nimport { clearSurfaceLog } from '../coach/surface.js'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\nimport { buildQueries } from '../db/queries.js'\nimport type { EventContext, ToolEvent } from '../lib/types.js'\n\nexport interface CoachCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport async function runCoachCli(\n args: string[] = [],\n opts: CoachCliOptions = {},\n): Promise<number> {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const sub = args[0] ?? 'status'\n\n if (sub === 'list') {\n print(`Knowledge base (${KNOWLEDGE_BASE.length} tips):`)\n for (const tip of KNOWLEDGE_BASE) {\n print(` • ${tip.id.padEnd(32)} ${tip.title}`)\n }\n return 0\n }\n\n if (sub === 'explain') {\n const tipId = args[1]\n if (!tipId) {\n print('Uso: token-optimizer-mcp coach explain <tip_id>')\n return 1\n }\n const tip = KNOWLEDGE_BASE.find((t) => t.id === tipId)\n if (!tip) {\n print(`Tip no encontrado: ${tipId}`)\n return 1\n }\n print(tip.title)\n print('')\n print(tip.description)\n print('')\n print(`Como usarlo: ${tip.how_to_invoke}`)\n print(`Cuando: ${tip.when_applicable}`)\n print(`Ahorro: ${tip.savings_estimate}`)\n print(`Fuente: ${tip.savings_source} · verificado: ${tip.verified_at}`)\n return 0\n }\n\n if (sub === 'reset') {\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) {\n print('Sin DB; nada que resetear.')\n return 0\n }\n const db = getDb(dbPath)\n const deleted = clearSurfaceLog(db)\n print(`Log de coach reseteado (${deleted} entradas eliminadas)`)\n return 0\n }\n\n // Default: status\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) {\n print('Coach status: sin datos. Ejecuta el hook posttooluse al menos una vez.')\n return 0\n }\n const db = getDb(dbPath)\n const contextOpts: {\n db: typeof db\n projectDir?: string\n } = { db }\n if (projectDir) contextOpts.projectDir = projectDir\n const context = await measureContextSize('default', contextOpts)\n\n const queries = buildQueries(db)\n const since = new Date(Date.now() - 86_400_000).toISOString()\n const rawRows = queries.getToolCallsSince(since) as ToolEvent[]\n const ctx: EventContext = {\n session_id: 'default',\n events: rawRows.slice(0, 100),\n session_token_total: context.tokens,\n session_token_method: context.estimation_method,\n session_token_limit: context.limit,\n active_model: null,\n }\n const hits = runRules(ctx)\n\n print('token-optimizer-mcp coach status')\n print('')\n print(\n `Contexto: ${(context.percent * 100).toFixed(1)}% (${context.tokens}/${context.limit} tokens, ${context.estimation_method})`,\n )\n print(`Tips activos: ${hits.length}`)\n if (hits.length === 0) {\n print(' (sin tips disparados en este momento)')\n } else {\n for (const h of hits) {\n print(` [${h.severity}] ${h.rule_id}: ${h.evidence}`)\n }\n }\n return 0\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAGA,OAAO,QAAQ;AAef,eAAsB,YACpB,OAAiB,CAAC,GAClB,OAAwB,CAAC,GACR;AACjB,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,MAAM,KAAK,CAAC,KAAK;AAEvB,MAAI,QAAQ,QAAQ;AAClB,UAAM,mBAAmB,eAAe,MAAM,SAAS;AACvD,eAAW,OAAO,gBAAgB;AAChC,YAAM,YAAO,IAAI,GAAG,OAAO,EAAE,CAAC,IAAI,IAAI,KAAK,EAAE;AAAA,IAC/C;AACA,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,WAAW;AACrB,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,CAAC,OAAO;AACV,YAAM,iDAAiD;AACvD,aAAO;AAAA,IACT;AACA,UAAM,MAAM,eAAe,KAAK,CAAC,MAAM,EAAE,OAAO,KAAK;AACrD,QAAI,CAAC,KAAK;AACR,YAAM,sBAAsB,KAAK,EAAE;AACnC,aAAO;AAAA,IACT;AACA,UAAM,IAAI,KAAK;AACf,UAAM,EAAE;AACR,UAAM,IAAI,WAAW;AACrB,UAAM,EAAE;AACR,UAAM,gBAAgB,IAAI,aAAa,EAAE;AACzC,UAAM,gBAAgB,IAAI,eAAe,EAAE;AAC3C,UAAM,gBAAgB,IAAI,gBAAgB,EAAE;AAC5C,UAAM,gBAAgB,IAAI,cAAc,qBAAkB,IAAI,WAAW,EAAE;AAC3E,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,SAAS;AACnB,UAAMA,cAAa,kBAAkB,GAAG;AACxC,UAAMC,UAAS,uBAAuBD,WAAU;AAChD,QAAI,CAAC,GAAG,WAAWC,OAAM,GAAG;AAC1B,YAAM,4BAA4B;AAClC,aAAO;AAAA,IACT;AACA,UAAMC,MAAK,MAAMD,OAAM;AACvB,UAAM,UAAU,gBAAgBC,GAAE;AAClC,UAAM,2BAA2B,OAAO,uBAAuB;AAC/D,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAChD,MAAI,CAAC,GAAG,WAAW,MAAM,GAAG;AAC1B,UAAM,wEAAwE;AAC9E,WAAO;AAAA,EACT;AACA,QAAM,KAAK,MAAM,MAAM;AACvB,QAAM,cAGF,EAAE,GAAG;AACT,MAAI,WAAY,aAAY,aAAa;AACzC,QAAM,UAAU,MAAM,mBAAmB,WAAW,WAAW;AAE/D,QAAM,UAAU,aAAa,EAAE;AAC/B,QAAM,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAU,EAAE,YAAY;AAC5D,QAAM,UAAU,QAAQ,kBAAkB,KAAK;AAC/C,QAAM,MAAoB;AAAA,IACxB,YAAY;AAAA,IACZ,QAAQ,QAAQ,MAAM,GAAG,GAAG;AAAA,IAC5B,qBAAqB,QAAQ;AAAA,IAC7B,sBAAsB,QAAQ;AAAA,IAC9B,qBAAqB,QAAQ;AAAA,IAC7B,cAAc;AAAA,EAChB;AACA,QAAM,OAAO,SAAS,GAAG;AAEzB,QAAM,kCAAkC;AACxC,QAAM,EAAE;AACR;AAAA,IACE,cAAc,QAAQ,UAAU,KAAK,QAAQ,CAAC,CAAC,MAAM,QAAQ,MAAM,IAAI,QAAQ,KAAK,YAAY,QAAQ,iBAAiB;AAAA,EAC3H;AACA,QAAM,iBAAiB,KAAK,MAAM,EAAE;AACpC,MAAI,KAAK,WAAW,GAAG;AACrB,UAAM,yCAAyC;AAAA,EACjD,OAAO;AACL,eAAW,KAAK,MAAM;AACpB,YAAM,MAAM,EAAE,QAAQ,KAAK,EAAE,OAAO,KAAK,EAAE,QAAQ,EAAE;AAAA,IACvD;AAAA,EACF;AACA,SAAO;AACT;","names":["projectDir","dbPath","db"]} |
| #!/usr/bin/env node | ||
| // src/cli/dispatcher.ts | ||
| async function dispatchCli(argv) { | ||
| const [sub, ...rest] = argv; | ||
| switch (sub) { | ||
| case "install": { | ||
| const mod = await import("./install-TNLZRW5R.js"); | ||
| return mod.runInstall(rest); | ||
| } | ||
| case "uninstall": { | ||
| const mod = await import("./uninstall-UK255POR.js"); | ||
| return mod.runUninstall(rest); | ||
| } | ||
| case "doctor": { | ||
| const mod = await import("./doctor-C3UG64HT.js"); | ||
| return mod.runDoctor(rest); | ||
| } | ||
| case "status": { | ||
| const mod = await import("./status-ZCOK7BWN.js"); | ||
| return mod.runStatus(rest); | ||
| } | ||
| case "report": { | ||
| const mod = await import("./report-JX5CQ6FO.js"); | ||
| return mod.runReport(rest); | ||
| } | ||
| case "budget": { | ||
| const mod = await import("./budget-NQVRS4O7.js"); | ||
| return mod.runBudgetCli(rest); | ||
| } | ||
| case "config": { | ||
| const mod = await import("./config-ETWSZHVJ.js"); | ||
| return mod.runConfigCommand(rest); | ||
| } | ||
| case "prune-mcp": { | ||
| const mod = await import("./prune-mcp-TGHEFZ3X.js"); | ||
| return mod.runPruneMcp(rest); | ||
| } | ||
| case "coach": { | ||
| const mod = await import("./coach-RGJZHRQK.js"); | ||
| return mod.runCoachCli(rest); | ||
| } | ||
| case "sync-xray": { | ||
| const mod = await import("./sync-xray-U6ORS5MP.js"); | ||
| return mod.runSyncXray(rest); | ||
| } | ||
| default: | ||
| console.error(`Subcomando desconocido: ${sub ?? "(ninguno)"}`); | ||
| console.error( | ||
| "Disponibles: install, uninstall, doctor, status, report, budget, prune-mcp, coach, config, sync-xray" | ||
| ); | ||
| return 1; | ||
| } | ||
| } | ||
| export { | ||
| dispatchCli | ||
| }; | ||
| //# sourceMappingURL=dispatcher-SW3I5LZR.js.map |
| {"version":3,"sources":["../src/cli/dispatcher.ts"],"sourcesContent":["// CLI subcommand dispatcher — Phase 4.9\n// Routes argv[0] to the appropriate subcommand module (lazy-imported).\n\nexport async function dispatchCli(argv: string[]): Promise<number> {\n const [sub, ...rest] = argv\n switch (sub) {\n case 'install': {\n const mod = await import('./install.js')\n return mod.runInstall(rest)\n }\n case 'uninstall': {\n const mod = await import('./uninstall.js')\n return mod.runUninstall(rest)\n }\n case 'doctor': {\n const mod = await import('./doctor.js')\n return mod.runDoctor(rest)\n }\n case 'status': {\n const mod = await import('./status.js')\n return mod.runStatus(rest)\n }\n case 'report': {\n const mod = await import('./report.js')\n return mod.runReport(rest)\n }\n case 'budget': {\n const mod = await import('./budget.js')\n return mod.runBudgetCli(rest)\n }\n case 'config': {\n const mod = await import('./config.js')\n return mod.runConfigCommand(rest)\n }\n case 'prune-mcp': {\n const mod = await import('./prune-mcp.js')\n return mod.runPruneMcp(rest)\n }\n case 'coach': {\n const mod = await import('./coach.js')\n return mod.runCoachCli(rest)\n }\n case 'sync-xray': {\n const mod = await import('./sync-xray.js')\n return mod.runSyncXray(rest)\n }\n default:\n console.error(`Subcomando desconocido: ${sub ?? '(ninguno)'}`)\n console.error(\n 'Disponibles: install, uninstall, doctor, status, report, budget, prune-mcp, coach, config, sync-xray',\n )\n return 1\n }\n}\n"],"mappings":";;;AAGA,eAAsB,YAAY,MAAiC;AACjE,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI;AACvB,UAAQ,KAAK;AAAA,IACX,KAAK,WAAW;AACd,YAAM,MAAM,MAAM,OAAO,uBAAc;AACvC,aAAO,IAAI,WAAW,IAAI;AAAA,IAC5B;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,MAAM,MAAM,OAAO,yBAAgB;AACzC,aAAO,IAAI,aAAa,IAAI;AAAA,IAC9B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,aAAa,IAAI;AAAA,IAC9B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,iBAAiB,IAAI;AAAA,IAClC;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,MAAM,MAAM,OAAO,yBAAgB;AACzC,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,MAAM,MAAM,OAAO,qBAAY;AACrC,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,MAAM,MAAM,OAAO,yBAAgB;AACzC,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA;AACE,cAAQ,MAAM,2BAA2B,OAAO,WAAW,EAAE;AAC7D,cAAQ;AAAA,QACN;AAAA,MACF;AACA,aAAO;AAAA,EACX;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| } from "./chunk-3CAOEPYE.js"; | ||
| import "./chunk-L5Z32XXL.js"; | ||
| import "./chunk-TOEPQYR3.js"; | ||
| import "./chunk-V4PINTCV.js"; | ||
| export { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| }; | ||
| //# sourceMappingURL=prune-mcp-TGHEFZ3X.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/report.ts | ||
| import fs from "fs"; | ||
| var PERIOD_DAYS = { | ||
| session: 3650, | ||
| day: 1, | ||
| week: 7, | ||
| month: 30 | ||
| }; | ||
| function isMeasured(method) { | ||
| return method === "measured_exact" || method === "measured_delta"; | ||
| } | ||
| function queryBySourceAndMethod(db, sinceIso) { | ||
| return db.prepare( | ||
| `SELECT source, estimation_method, | ||
| COUNT(*) as count, | ||
| COALESCE(SUM(tokens_estimated), 0) as tokens | ||
| FROM tool_calls | ||
| WHERE created_at >= ? | ||
| GROUP BY source, estimation_method | ||
| ORDER BY tokens DESC` | ||
| ).all(sinceIso); | ||
| } | ||
| var REFERENCE_DATA = [ | ||
| { | ||
| feature: "Model switching (opusplan / default-to-sonnet)", | ||
| saving: "60-80% reduccion de coste", | ||
| source: "mindstudio.ai, verdent.ai, claudelab.net", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Progressive disclosure skills", | ||
| saving: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| source: "claudefast.com", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Prompt caching read hit", | ||
| saving: "10x mas barato que uncached", | ||
| source: "Anthropic docs", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Claude Code Tool Search", | ||
| saving: "~85% schema reduction (77k \u2192 8.7k)", | ||
| source: "observado en sesion", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "MCP pruning sobre Tool Search", | ||
| saving: "~5-12% adicional por turno", | ||
| source: "estimacion interna", | ||
| verified_at: "2026-04-11" | ||
| } | ||
| ]; | ||
| function resolvePeriod(args, fallback) { | ||
| const flag = args.find((a) => a.startsWith("--period=")); | ||
| if (flag) { | ||
| const value = flag.split("=")[1]; | ||
| if (value && value in PERIOD_DAYS) return value; | ||
| } | ||
| return fallback; | ||
| } | ||
| function runReport(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const period = opts.period ?? resolvePeriod(args, "day"); | ||
| const days = PERIOD_DAYS[period]; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const lines = []; | ||
| lines.push(`token-optimizer-mcp reporte \u2014 periodo: ${period} (${days} dia(s))`); | ||
| lines.push(""); | ||
| if (!fs.existsSync(dbPath)) { | ||
| lines.push("No hay datos registrados todavia."); | ||
| } else { | ||
| const db = getDb(dbPath); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const rows = queryBySourceAndMethod(db, since); | ||
| let medidoTotal = 0; | ||
| let estimadoTotal = 0; | ||
| lines.push("Por fuente y metodo de estimacion:"); | ||
| if (rows.length === 0) { | ||
| lines.push(" (sin eventos en este periodo)"); | ||
| } else { | ||
| for (const row of rows) { | ||
| const method = row.estimation_method ?? "unknown"; | ||
| lines.push( | ||
| ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens` | ||
| ); | ||
| if (isMeasured(method)) medidoTotal += row.tokens; | ||
| else estimadoTotal += row.tokens; | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push(`Resumen: Medido: ${medidoTotal} tokens \xB7 Estimado: ${estimadoTotal} tokens`); | ||
| lines.push(""); | ||
| } | ||
| lines.push("Coach activity:"); | ||
| lines.push(" (sin tips surfaceados todavia \u2014 coach layer se activa en Phase 4.H)"); | ||
| lines.push(""); | ||
| printReference(lines); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| function printReference(lines) { | ||
| lines.push("Referencia (datos publicos verificables):"); | ||
| for (const row of REFERENCE_DATA) { | ||
| lines.push(` \u2022 ${row.feature}`); | ||
| lines.push(` ahorro: ${row.saving}`); | ||
| lines.push(` fuente: ${row.source} \xB7 verificado: ${row.verified_at}`); | ||
| } | ||
| } | ||
| export { | ||
| runReport | ||
| }; | ||
| //# sourceMappingURL=report-JX5CQ6FO.js.map |
| {"version":3,"sources":["../src/cli/report.ts"],"sourcesContent":["// Report CLI — Phase 4.14\n// Per-source breakdown WITH estimation_method label + Medido/Estimado split +\n// reference-data table (coach-layer addendum CO-4). Spanish.\n\nimport fs from 'node:fs'\nimport type Database from 'better-sqlite3'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\n\ntype DB = Database.Database\n\ntype Period = 'session' | 'day' | 'week' | 'month'\n\nconst PERIOD_DAYS: Record<Period, number> = {\n session: 3650,\n day: 1,\n week: 7,\n month: 30,\n}\n\ninterface SourceMethodRow {\n source: string\n estimation_method: string | null\n count: number\n tokens: number\n}\n\nfunction isMeasured(method: string | null): boolean {\n return method === 'measured_exact' || method === 'measured_delta'\n}\n\nfunction queryBySourceAndMethod(db: DB, sinceIso: string): SourceMethodRow[] {\n return db\n .prepare(\n `SELECT source, estimation_method,\n COUNT(*) as count,\n COALESCE(SUM(tokens_estimated), 0) as tokens\n FROM tool_calls\n WHERE created_at >= ?\n GROUP BY source, estimation_method\n ORDER BY tokens DESC`,\n )\n .all(sinceIso) as SourceMethodRow[]\n}\n\nexport interface ReportOptions {\n cwd?: string\n period?: Period\n print?: (msg: string) => void\n}\n\nconst REFERENCE_DATA: Array<{\n feature: string\n saving: string\n source: string\n verified_at: string\n}> = [\n {\n feature: 'Model switching (opusplan / default-to-sonnet)',\n saving: '60-80% reduccion de coste',\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Progressive disclosure skills',\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n source: 'claudefast.com',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Prompt caching read hit',\n saving: '10x mas barato que uncached',\n source: 'Anthropic docs',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Claude Code Tool Search',\n saving: '~85% schema reduction (77k → 8.7k)',\n source: 'observado en sesion',\n verified_at: '2026-04-11',\n },\n {\n feature: 'MCP pruning sobre Tool Search',\n saving: '~5-12% adicional por turno',\n source: 'estimacion interna',\n verified_at: '2026-04-11',\n },\n]\n\nfunction resolvePeriod(args: string[], fallback: Period): Period {\n const flag = args.find((a) => a.startsWith('--period='))\n if (flag) {\n const value = flag.split('=')[1] as Period | undefined\n if (value && value in PERIOD_DAYS) return value\n }\n return fallback\n}\n\nexport function runReport(args: string[] = [], opts: ReportOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const period: Period = opts.period ?? resolvePeriod(args, 'day')\n const days = PERIOD_DAYS[period]\n\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n const lines: string[] = []\n lines.push(`token-optimizer-mcp reporte — periodo: ${period} (${days} dia(s))`)\n lines.push('')\n\n if (!fs.existsSync(dbPath)) {\n lines.push('No hay datos registrados todavia.')\n } else {\n const db = getDb(dbPath)\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\n const rows = queryBySourceAndMethod(db, since)\n\n let medidoTotal = 0\n let estimadoTotal = 0\n\n lines.push('Por fuente y metodo de estimacion:')\n if (rows.length === 0) {\n lines.push(' (sin eventos en este periodo)')\n } else {\n for (const row of rows) {\n const method = row.estimation_method ?? 'unknown'\n lines.push(\n ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens`,\n )\n if (isMeasured(method)) medidoTotal += row.tokens\n else estimadoTotal += row.tokens\n }\n }\n lines.push('')\n lines.push(`Resumen: Medido: ${medidoTotal} tokens · Estimado: ${estimadoTotal} tokens`)\n lines.push('')\n }\n\n // Coach activity section (always present; filled in Phase 4.H with real data)\n lines.push('Coach activity:')\n lines.push(' (sin tips surfaceados todavia — coach layer se activa en Phase 4.H)')\n lines.push('')\n\n printReference(lines)\n print(lines.join('\\n'))\n return 0\n}\n\nfunction printReference(lines: string[]): void {\n lines.push('Referencia (datos publicos verificables):')\n for (const row of REFERENCE_DATA) {\n lines.push(` • ${row.feature}`)\n lines.push(` ahorro: ${row.saving}`)\n lines.push(` fuente: ${row.source} · verificado: ${row.verified_at}`)\n }\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| getCoachSurfaceLog, | ||
| measureContextSize, | ||
| postSummaryToXray, | ||
| runRules | ||
| } from "./chunk-22BLF7AQ.js"; | ||
| import "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| rollback | ||
| } from "./chunk-3CAOEPYE.js"; | ||
| import { | ||
| ensureStorageDir | ||
| } from "./chunk-VHP52B3J.js"; | ||
| import { | ||
| buildSuggestions, | ||
| checkSerenaHealth, | ||
| probeMcpPruning, | ||
| probePromptCaching, | ||
| probeRtk, | ||
| probeSerena | ||
| } from "./chunk-DSMHUGNS.js"; | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-L5Z32XXL.js"; | ||
| import { | ||
| getCostReport, | ||
| getUsageStats | ||
| } from "./chunk-3LSC7CFQ.js"; | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-DK5JDNWQ.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-OIZX7KF2.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/server.ts | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| // src/tools/budget.ts | ||
| import { z } from "zod"; | ||
| // src/lib/response.ts | ||
| var text = (t) => ({ | ||
| content: [{ type: "text", text: t }] | ||
| }); | ||
| var error = (t) => ({ | ||
| content: [{ type: "text", text: `Error: ${t}` }], | ||
| isError: true | ||
| }); | ||
| // src/tools/budget.ts | ||
| var DAY_MS = 864e5; | ||
| function sinceForPeriod(period) { | ||
| const now = Date.now(); | ||
| switch (period) { | ||
| case "day": | ||
| return new Date(now - DAY_MS).toISOString(); | ||
| case "week": | ||
| return new Date(now - 7 * DAY_MS).toISOString(); | ||
| case "month": | ||
| return new Date(now - 30 * DAY_MS).toISOString(); | ||
| case "session": | ||
| default: | ||
| return "1970-01-01T00:00:00.000Z"; | ||
| } | ||
| } | ||
| function registerBudgetTools(server, db) { | ||
| const manager = new BudgetManager(db); | ||
| server.tool( | ||
| "budget_set", | ||
| "Define o actualiza un presupuesto de tokens. Precedencia: session > project. Modo warn avisa al exceder.", | ||
| { | ||
| scope: z.enum(["session", "project"]).describe("Ambito del presupuesto"), | ||
| scope_key: z.string().min(1).describe("Clave del scope (sessionId o projectHash)"), | ||
| limit_tokens: z.number().int().positive().max(1e7).describe("Limite en tokens (1..10_000_000)") | ||
| }, | ||
| async ({ scope, scope_key, limit_tokens }) => { | ||
| try { | ||
| const budget = manager.setBudget({ scope, scope_key, limit_tokens }); | ||
| return text( | ||
| [ | ||
| "Presupuesto guardado:", | ||
| "", | ||
| ` scope: ${budget.scope}`, | ||
| ` scope_key: ${budget.scope_key}`, | ||
| ` limit_tokens: ${budget.limit_tokens}`, | ||
| ` mode: ${budget.mode}` | ||
| ].join("\n") | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "budget_check", | ||
| "Consulta el estado del presupuesto activo (gasto actual, restante y porcentaje).", | ||
| { | ||
| session_id: z.string().optional().describe('ID de sesion (default: "default")'), | ||
| project_hash: z.string().optional().describe("Hash del proyecto para fallback a scope project") | ||
| }, | ||
| async ({ session_id, project_hash }) => { | ||
| try { | ||
| const status = manager.checkBudget(session_id ?? "default", project_hash ?? null); | ||
| if (!status.active) { | ||
| return text("Sin presupuesto activo para la sesion/proyecto actual."); | ||
| } | ||
| const percent = (status.percent_used * 100).toFixed(1); | ||
| return text( | ||
| [ | ||
| "Estado del presupuesto:", | ||
| "", | ||
| ` gastado: ${status.spent} tokens`, | ||
| ` restante: ${status.remaining} tokens`, | ||
| ` uso: ${percent}%`, | ||
| ` modo: ${status.mode ?? "n/a"}` | ||
| ].join("\n") | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "budget_report", | ||
| "Muestra el consumo de tokens agrupado por herramienta y por fuente durante un periodo.", | ||
| { | ||
| period: z.enum(["session", "day", "week", "month"]).optional().describe("Periodo del reporte (default: day)") | ||
| }, | ||
| async ({ period }) => { | ||
| try { | ||
| const since = sinceForPeriod(period ?? "day"); | ||
| const report = manager.getBudgetReport(since); | ||
| const lines = [`Reporte de consumo (desde ${report.period_since}):`, ""]; | ||
| lines.push("Por herramienta:"); | ||
| if (report.by_tool.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of report.by_tool) { | ||
| lines.push(` ${row.tool_name}: ${row.count} llamadas, ${row.tokens} tokens`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push("Por fuente:"); | ||
| if (report.by_source.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of report.by_source) { | ||
| lines.push(` ${row.source}: ${row.count} llamadas, ${row.tokens} tokens`); | ||
| } | ||
| } | ||
| return text(lines.join("\n")); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/tools/session.ts | ||
| function registerSessionTools(_server, _db) { | ||
| } | ||
| // src/tools/orchestration.ts | ||
| import { z as z2 } from "zod"; | ||
| // src/services/session-summary-builder.ts | ||
| function buildSessionSummary(db, sessionId, version) { | ||
| const usage = getUsageStats(db, 1); | ||
| const cost = getCostReport(db, 1); | ||
| const serena = probeSerena(); | ||
| const rtk = probeRtk(); | ||
| const mcpPruning = probeMcpPruning(); | ||
| const promptCaching = probePromptCaching(); | ||
| const schema = measureCurrentSchemaBytes(); | ||
| const coachTips = getCoachSurfaceLog(db, sessionId); | ||
| const projDir = resolveProjectDir(); | ||
| const projName = projDir.split(/[\\/]/).filter(Boolean).pop() ?? "unknown"; | ||
| return { | ||
| session_id: sessionId, | ||
| project_path: projDir, | ||
| project_name: projName, | ||
| total_tokens: usage.total_tokens, | ||
| total_events: usage.total_events, | ||
| by_source: usage.by_source, | ||
| by_tool: usage.by_tool.map((t) => ({ | ||
| tool_name: t.tool_name, | ||
| count: t.count, | ||
| tokens: t.tokens | ||
| })), | ||
| cost_haiku: cost.estimated_cost_usd_haiku, | ||
| cost_sonnet: cost.estimated_cost_usd_sonnet, | ||
| cost_opus: cost.estimated_cost_usd_opus, | ||
| probes: { | ||
| serena: { present: serena.present, confidence: serena.confidence, signals: serena.signals }, | ||
| rtk: { present: rtk.present, confidence: rtk.confidence, signals: rtk.signals }, | ||
| mcp_pruning: { | ||
| present: mcpPruning.present, | ||
| confidence: mcpPruning.confidence, | ||
| signals: mcpPruning.signals | ||
| }, | ||
| prompt_caching: { present: promptCaching.present, confidence: promptCaching.confidence } | ||
| }, | ||
| coach_tips_surfaced: coachTips, | ||
| schema_measurement: { | ||
| tool_schema_tokens: schema.tool_schema_tokens, | ||
| mcp_servers: schema.mcp_servers | ||
| }, | ||
| optimizer_version: version | ||
| }; | ||
| } | ||
| // src/tools/orchestration.ts | ||
| function registerOrchestrationTools(server, db) { | ||
| server.tool( | ||
| "mcp_usage_stats", | ||
| "Estadisticas de uso de tokens por herramienta y fuente en un periodo.", | ||
| { | ||
| days: z2.number().int().positive().max(365).optional().describe("Dias a analizar (default: 7)") | ||
| }, | ||
| async ({ days }) => { | ||
| try { | ||
| const stats = getUsageStats(db, days ?? 7); | ||
| const lines = [ | ||
| `Uso en los ultimos ${stats.period_days} dia(s):`, | ||
| "", | ||
| `Total: ${stats.total_tokens} tokens, ${stats.total_events} eventos`, | ||
| "", | ||
| "Por fuente:" | ||
| ]; | ||
| if (stats.by_source.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of stats.by_source) { | ||
| lines.push(` ${row.source}: ${row.tokens} tokens, ${row.count} llamadas`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push("Top herramientas:"); | ||
| if (stats.by_tool.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of stats.by_tool.slice(0, 10)) { | ||
| lines.push(` ${row.tool_name}: ${row.tokens} tokens, ${row.count} llamadas`); | ||
| } | ||
| } | ||
| return text(lines.join("\n")); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_cost_report", | ||
| "Reporte de coste estimado con rango Haiku-Sonnet-Opus y disclaimer honesto.", | ||
| { | ||
| days: z2.number().int().positive().max(365).optional().describe("Dias a analizar (default: 7)") | ||
| }, | ||
| async ({ days }) => { | ||
| try { | ||
| const cost = getCostReport(db, days ?? 7); | ||
| const lines = [ | ||
| `Reporte de coste (${cost.period_days} dia(s)):`, | ||
| "", | ||
| `Tokens totales: ${cost.total_tokens}`, | ||
| `Coste estimado (input pricing):`, | ||
| ` Haiku 4.5: $${cost.estimated_cost_usd_haiku.toFixed(4)} ($1/MTok)`, | ||
| ` Sonnet 4.6: $${cost.estimated_cost_usd_sonnet.toFixed(4)} ($3/MTok)`, | ||
| ` Opus 4.6: $${cost.estimated_cost_usd_opus.toFixed(4)} ($5/MTok)`, | ||
| "", | ||
| `Nota: ${cost.disclaimer}` | ||
| ]; | ||
| return text(lines.join("\n")); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "optimization_status", | ||
| "Estado de las optimizaciones detectadas: serena, RTK, MCP pruning, prompt caching, schema size.", | ||
| {}, | ||
| async () => { | ||
| try { | ||
| const serena = probeSerena(); | ||
| const rtk = probeRtk(); | ||
| const pruning = probeMcpPruning(); | ||
| const pcProbe = probePromptCaching(); | ||
| void pcProbe; | ||
| const schema = measureCurrentSchemaBytes(); | ||
| const status = { | ||
| serena, | ||
| rtk, | ||
| mcp_pruning: pruning, | ||
| prompt_caching: { | ||
| active_by_default: true, | ||
| savings_tokens: null, | ||
| estimation_method: "unknown", | ||
| note: "Revisa tu factura Anthropic para confirmar el ahorro real" | ||
| }, | ||
| schema_bytes: { | ||
| tool_schema_bytes: schema.tool_schema_bytes, | ||
| measurement_method: schema.measurement_method | ||
| } | ||
| }; | ||
| const serenaHealth = serena.present ? checkSerenaHealth() : []; | ||
| const suggestions = buildSuggestions(status); | ||
| try { | ||
| const lastSession = db.prepare("SELECT id FROM sessions ORDER BY started_at DESC LIMIT 1").get(); | ||
| if (lastSession) { | ||
| const summary = buildSessionSummary(db, lastSession.id, "0.2.6"); | ||
| void postSummaryToXray(summary).catch(() => { | ||
| }); | ||
| } | ||
| } catch { | ||
| } | ||
| return text(JSON.stringify({ status, serena_health: serenaHealth, suggestions }, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_suggest", | ||
| "Genera un allowlist de MCPs basandose en el historial (NO modifica archivos).", | ||
| { | ||
| days: z2.number().int().positive().max(365).optional().describe("Dias de historial a analizar (default: 14)") | ||
| }, | ||
| async ({ days }) => { | ||
| try { | ||
| const proposal = generateFromHistory({ days: days ?? 14 }); | ||
| return text(JSON.stringify(proposal, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_apply", | ||
| "Restringe los MCPs activos escribiendo enabledMcpjsonServers en .claude/settings.local.json. Requiere confirm:true. Acepta dos formas equivalentes: allowlist (lista blanca, los que SI quieres) o exclude (lista negra, los que NO quieres). Se debe pasar exactamente una de las dos.", | ||
| { | ||
| allowlist: z2.array(z2.string()).optional().describe("Nombres de MCPs a permitir (lista blanca). Exclusivo con exclude."), | ||
| exclude: z2.array(z2.string()).optional().describe( | ||
| "Nombres de MCPs a desactivar (lista negra). Internamente se traduce a allowlist = registrados - exclude. Exclusivo con allowlist." | ||
| ), | ||
| confirm: z2.boolean().describe("Debe ser true para confirmar la escritura") | ||
| }, | ||
| async ({ allowlist, exclude, confirm }) => { | ||
| try { | ||
| if (confirm !== true) { | ||
| return error( | ||
| "Operacion destructiva: requiere confirm:true. Revisa el allowlist antes de aplicar." | ||
| ); | ||
| } | ||
| const hasAllow = Array.isArray(allowlist); | ||
| const hasExclude = Array.isArray(exclude); | ||
| if (hasAllow === hasExclude) { | ||
| return error( | ||
| "Debes pasar exactamente uno: allowlist (los que SI quieres) o exclude (los que NO quieres)." | ||
| ); | ||
| } | ||
| const schema = measureCurrentSchemaBytes(); | ||
| const registered = new Set(schema.mcp_servers); | ||
| let effective; | ||
| let translationNote = ""; | ||
| if (hasAllow) { | ||
| effective = allowlist; | ||
| if (registered.size > 0) { | ||
| const invalid = effective.filter((s) => !registered.has(s)); | ||
| if (invalid.length > 0) { | ||
| return error( | ||
| `Allowlist contiene MCPs no registrados en settings: ${invalid.join(", ")}` | ||
| ); | ||
| } | ||
| } | ||
| } else { | ||
| const excludeSet = new Set(exclude); | ||
| if (registered.size > 0) { | ||
| const invalid = exclude.filter((s) => !registered.has(s)); | ||
| if (invalid.length > 0) { | ||
| return error( | ||
| `Exclude contiene MCPs no registrados en settings: ${invalid.join(", ")}` | ||
| ); | ||
| } | ||
| } | ||
| effective = [...registered].filter((s) => !excludeSet.has(s)); | ||
| translationNote = ` | ||
| exclude: [${exclude.join(", ")}] | ||
| \u2192 allowlist efectivo: [${effective.join(", ")}]`; | ||
| } | ||
| const applied = applyAllowlist(effective, { source: "mcp" }); | ||
| return text( | ||
| `Allowlist aplicado.${translationNote} | ||
| settings: ${applied.settings_path} | ||
| backup: ${applied.backup_path}` | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_rollback", | ||
| "Restaura el backup mas reciente de settings.local.json. Requiere confirm:true.", | ||
| { | ||
| confirm: z2.boolean(), | ||
| to: z2.string().optional().describe("Timestamp opcional del backup a restaurar") | ||
| }, | ||
| async ({ confirm, to }) => { | ||
| try { | ||
| if (confirm !== true) { | ||
| return error("Operacion destructiva: requiere confirm:true."); | ||
| } | ||
| const result = rollback(to !== void 0 ? { to } : {}); | ||
| if (!result.restored) return error("No hay backups disponibles."); | ||
| return text(`Restaurado desde ${result.from}`); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_clear", | ||
| "Elimina el allowlist de settings.local.json (crea backup). Requiere confirm:true.", | ||
| { | ||
| confirm: z2.boolean() | ||
| }, | ||
| async ({ confirm }) => { | ||
| try { | ||
| if (confirm !== true) { | ||
| return error("Operacion destructiva: requiere confirm:true."); | ||
| } | ||
| const result = clearAllowlist(); | ||
| return text( | ||
| result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : "Nada que eliminar" | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/tools/coach.ts | ||
| import { z as z3 } from "zod"; | ||
| // src/coach/reference-data.ts | ||
| var REFERENCE_DATA = [ | ||
| { | ||
| feature: "Model switching (opusplan / default-to-sonnet)", | ||
| saving: "60-80% reduccion de coste", | ||
| source: "mindstudio.ai, verdent.ai, claudelab.net", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "Progressive disclosure skills", | ||
| saving: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| source: "claudefast.com", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "Prompt caching read hit", | ||
| saving: "10x mas barato que uncached", | ||
| source: "Anthropic docs", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "Claude Code Tool Search", | ||
| saving: "~85% schema reduction (77k \u2192 8.7k tokens)", | ||
| source: "observado en sesion", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "MCP pruning sobre Tool Search", | ||
| saving: "~5-12% adicional por turno", | ||
| source: "estimacion interna", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| } | ||
| ]; | ||
| var DAY_MS2 = 864e5; | ||
| function getStaleRows(daysThreshold = 90, today = /* @__PURE__ */ new Date()) { | ||
| const cutoff = today.getTime() - daysThreshold * DAY_MS2; | ||
| return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() < cutoff); | ||
| } | ||
| // src/coach/tips-payload.ts | ||
| async function computeCoachTipsPayload(opts) { | ||
| const { db } = opts; | ||
| const sessionId = opts.sessionId ?? "default"; | ||
| const contextOpts = { db }; | ||
| if (opts.projectDir !== void 0) contextOpts.projectDir = opts.projectDir; | ||
| if (opts.activeModel !== void 0) contextOpts.activeModel = opts.activeModel; | ||
| const context = await measureContextSize(sessionId, contextOpts); | ||
| const queries = buildQueries(db); | ||
| const since = new Date(Date.now() - 864e5).toISOString(); | ||
| const rawRows = queries.getToolCallsSince(since); | ||
| const events = rawRows.slice(0, 100); | ||
| const ctx = { | ||
| session_id: sessionId, | ||
| events, | ||
| session_token_total: context.tokens, | ||
| session_token_method: context.estimation_method, | ||
| session_token_limit: context.limit, | ||
| active_model: opts.activeModel ?? null | ||
| }; | ||
| const hits = runRules(ctx); | ||
| const staleTips = getStaleRows(); | ||
| return { | ||
| current: hits, | ||
| known_tricks: KNOWLEDGE_BASE, | ||
| context, | ||
| reference_data: REFERENCE_DATA, | ||
| stale_reference_count: staleTips.length, | ||
| last_computed_at: (/* @__PURE__ */ new Date()).toISOString() | ||
| }; | ||
| } | ||
| // src/tools/coach.ts | ||
| function registerCoachTools(server, db) { | ||
| server.tool( | ||
| "coach_tips", | ||
| "Devuelve tips activos (rules disparadas) y medicion de contexto. Por defecto modo compacto (~500 tokens). Usa verbose=true para incluir el catalogo completo de 18 tips y la tabla de referencia (~3.5k tokens).", | ||
| { | ||
| session_id: z3.string().optional().describe('ID de la sesion (default: "default")'), | ||
| project_dir: z3.string().optional().describe("Directorio del proyecto para medir contexto desde transcript JSONL"), | ||
| active_model: z3.string().optional().describe("Modelo activo (opcional, habilita regla detect-opus-for-simple-task)"), | ||
| verbose: z3.boolean().optional().describe( | ||
| "Si true, incluye el knowledge base completo (18 tips) y la reference data. Default false = solo hits activos + contexto. Ahorra ~3.3k tokens por llamada en modo compacto." | ||
| ) | ||
| }, | ||
| async ({ session_id, project_dir, active_model, verbose }) => { | ||
| try { | ||
| const payloadOpts = { db }; | ||
| if (session_id !== void 0) payloadOpts.sessionId = session_id; | ||
| if (project_dir !== void 0) payloadOpts.projectDir = project_dir; | ||
| if (active_model !== void 0) payloadOpts.activeModel = active_model; | ||
| const response = await computeCoachTipsPayload(payloadOpts); | ||
| if (verbose !== true) { | ||
| const { known_tricks: _kb, reference_data: _ref, ...compact } = response; | ||
| return text(JSON.stringify(compact, null, 2)); | ||
| } | ||
| return text(JSON.stringify(response, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/tools/toon.ts | ||
| import { z as z4 } from "zod"; | ||
| function compactEncode(data) { | ||
| try { | ||
| return JSON.stringify(data); | ||
| } catch (e) { | ||
| const msg = e instanceof Error ? e.message : String(e); | ||
| if (/circular|cyclic/i.test(msg)) { | ||
| throw new Error("Referencia circular detectada: TOON no soporta objetos ciclicos"); | ||
| } | ||
| throw new Error(`No se pudo codificar a TOON: ${msg}`); | ||
| } | ||
| } | ||
| function compactDecode(toon) { | ||
| try { | ||
| return JSON.parse(toon); | ||
| } catch (e) { | ||
| throw new Error(`TOON invalido: ${e instanceof Error ? e.message : String(e)}`); | ||
| } | ||
| } | ||
| function registerToonTools(server) { | ||
| server.tool( | ||
| "toon_encode", | ||
| "Codifica un objeto JSON a formato TOON (JSON compacto token-eficiente, round-trip lossless).", | ||
| { | ||
| data: z4.unknown().describe("Valor a codificar (objeto, array, primitivo)") | ||
| }, | ||
| async ({ data }) => { | ||
| try { | ||
| const encoded = compactEncode(data); | ||
| return text(encoded); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "toon_decode", | ||
| "Decodifica una cadena TOON a JSON. Devuelve el objeto formateado para lectura.", | ||
| { | ||
| toon: z4.string().min(1).describe("Cadena TOON a decodificar") | ||
| }, | ||
| async ({ toon }) => { | ||
| try { | ||
| const decoded = compactDecode(toon); | ||
| return text(JSON.stringify(decoded, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/resources/coach-tips.ts | ||
| var COACH_TIPS_URI = "token-optimizer://coach/tips"; | ||
| function registerCoachTipsResource(server, db) { | ||
| server.resource( | ||
| "coach-tips", | ||
| COACH_TIPS_URI, | ||
| { | ||
| description: "Tips activos del coach, catalogo completo de trucos, medicion de contexto y tabla de referencia.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri) => { | ||
| try { | ||
| const payload = await computeCoachTipsPayload({ db }); | ||
| return { | ||
| contents: [ | ||
| { | ||
| uri: uri.href, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify(payload, null, 2) | ||
| } | ||
| ] | ||
| }; | ||
| } catch (e) { | ||
| const message = e instanceof Error ? e.message : String(e); | ||
| return { | ||
| contents: [ | ||
| { | ||
| uri: uri.href, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify({ error: message }) | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/server.ts | ||
| var VERSION = true ? "0.4.21" : "0.1.0"; | ||
| var INSTRUCTIONS = `token-optimizer-mcp: orchestration + observability + coach layer for Claude Code. | ||
| Measures tool usage, enforces token budgets, advises on complementary tools (serena, RTK), | ||
| and proactively surfaces savings tips. Coach layer detects inefficiencies and suggests | ||
| optimizations like opusplan, /compact, plan mode, and more. | ||
| Does NOT replace serena (symbolic file reads) or RTK (Bash output filtering) \u2014 | ||
| coordinates with them and adds measurements, budgets, compact recovery, and coaching.`; | ||
| function createServer(options = {}) { | ||
| const resolvedProject = options.projectDir ?? resolveProjectDir(); | ||
| const dbPath = options.dbPath ?? (options.storageDir === ":memory:" ? ":memory:" : (ensureStorageDir(resolvedProject), resolveAnalyticsDbPath(resolvedProject))); | ||
| const db = getDb(dbPath); | ||
| const server = new McpServer( | ||
| { | ||
| name: "token-optimizer-mcp", | ||
| version: VERSION | ||
| }, | ||
| { | ||
| instructions: INSTRUCTIONS | ||
| } | ||
| ); | ||
| registerBudgetTools(server, db); | ||
| registerSessionTools(server, db); | ||
| registerOrchestrationTools(server, db); | ||
| registerCoachTools(server, db); | ||
| registerCoachTipsResource(server, db); | ||
| registerToonTools(server); | ||
| return server; | ||
| } | ||
| export { | ||
| createServer | ||
| }; | ||
| //# sourceMappingURL=server-GY6E6HFP.js.map |
| {"version":3,"sources":["../src/server.ts","../src/tools/budget.ts","../src/lib/response.ts","../src/tools/session.ts","../src/tools/orchestration.ts","../src/services/session-summary-builder.ts","../src/tools/coach.ts","../src/coach/reference-data.ts","../src/coach/tips-payload.ts","../src/tools/toon.ts","../src/resources/coach-tips.ts"],"sourcesContent":["// createServer factory — Phase 1.13\n// Returns a configured McpServer. Tools are registered in later phases.\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { getDb } from './db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from './lib/paths.js'\nimport { ensureStorageDir } from './lib/storage.js'\nimport { registerBudgetTools } from './tools/budget.js'\nimport { registerSessionTools } from './tools/session.js'\nimport { registerOrchestrationTools } from './tools/orchestration.js'\nimport { registerCoachTools } from './tools/coach.js'\nimport { registerToonTools } from './tools/toon.js'\nimport { registerCoachTipsResource } from './resources/coach-tips.js'\n\ndeclare const __PKG_VERSION__: string\nconst VERSION = typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : '0.1.0'\n\nconst INSTRUCTIONS = `token-optimizer-mcp: orchestration + observability + coach layer for Claude Code.\n\nMeasures tool usage, enforces token budgets, advises on complementary tools (serena, RTK),\nand proactively surfaces savings tips. Coach layer detects inefficiencies and suggests\noptimizations like opusplan, /compact, plan mode, and more.\n\nDoes NOT replace serena (symbolic file reads) or RTK (Bash output filtering) —\ncoordinates with them and adds measurements, budgets, compact recovery, and coaching.`\n\nexport interface CreateServerOptions {\n storageDir?: string\n projectDir?: string\n dbPath?: string\n}\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const resolvedProject = options.projectDir ?? resolveProjectDir()\n const dbPath =\n options.dbPath ??\n (options.storageDir === ':memory:'\n ? ':memory:'\n : (ensureStorageDir(resolvedProject), resolveAnalyticsDbPath(resolvedProject)))\n\n // Initialize DB (schema created by getDb)\n const db = getDb(dbPath)\n\n const server = new McpServer(\n {\n name: 'token-optimizer-mcp',\n version: VERSION,\n },\n {\n instructions: INSTRUCTIONS,\n },\n )\n\n // Phase 2 tools\n registerBudgetTools(server, db)\n // Phase 3 tools\n registerSessionTools(server, db)\n // Phase 4 tools\n registerOrchestrationTools(server, db)\n registerCoachTools(server, db)\n registerCoachTipsResource(server, db)\n // Phase 5 tools\n registerToonTools(server)\n\n return server\n}\n","// Budget MCP tools — Phase 2.4\n// budget_set, budget_check, budget_report\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { BudgetManager } from '../services/budget-manager.js'\nimport { text, error } from '../lib/response.js'\n\ntype DB = Database.Database\n\nconst DAY_MS = 86_400_000\n\nfunction sinceForPeriod(period: 'session' | 'day' | 'week' | 'month'): string {\n const now = Date.now()\n switch (period) {\n case 'day':\n return new Date(now - DAY_MS).toISOString()\n case 'week':\n return new Date(now - 7 * DAY_MS).toISOString()\n case 'month':\n return new Date(now - 30 * DAY_MS).toISOString()\n case 'session':\n default:\n return '1970-01-01T00:00:00.000Z'\n }\n}\n\nexport function registerBudgetTools(server: McpServer, db: DB): void {\n const manager = new BudgetManager(db)\n\n // ── budget_set ──\n server.tool(\n 'budget_set',\n 'Define o actualiza un presupuesto de tokens. Precedencia: session > project. Modo warn avisa al exceder.',\n {\n scope: z.enum(['session', 'project']).describe('Ambito del presupuesto'),\n scope_key: z.string().min(1).describe('Clave del scope (sessionId o projectHash)'),\n limit_tokens: z\n .number()\n .int()\n .positive()\n .max(10_000_000)\n .describe('Limite en tokens (1..10_000_000)'),\n },\n async ({ scope, scope_key, limit_tokens }) => {\n try {\n const budget = manager.setBudget({ scope, scope_key, limit_tokens })\n return text(\n [\n 'Presupuesto guardado:',\n '',\n ` scope: ${budget.scope}`,\n ` scope_key: ${budget.scope_key}`,\n ` limit_tokens: ${budget.limit_tokens}`,\n ` mode: ${budget.mode}`,\n ].join('\\n'),\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── budget_check ──\n server.tool(\n 'budget_check',\n 'Consulta el estado del presupuesto activo (gasto actual, restante y porcentaje).',\n {\n session_id: z.string().optional().describe('ID de sesion (default: \"default\")'),\n project_hash: z\n .string()\n .optional()\n .describe('Hash del proyecto para fallback a scope project'),\n },\n async ({ session_id, project_hash }) => {\n try {\n const status = manager.checkBudget(session_id ?? 'default', project_hash ?? null)\n if (!status.active) {\n return text('Sin presupuesto activo para la sesion/proyecto actual.')\n }\n const percent = (status.percent_used * 100).toFixed(1)\n return text(\n [\n 'Estado del presupuesto:',\n '',\n ` gastado: ${status.spent} tokens`,\n ` restante: ${status.remaining} tokens`,\n ` uso: ${percent}%`,\n ` modo: ${status.mode ?? 'n/a'}`,\n ].join('\\n'),\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── budget_report ──\n server.tool(\n 'budget_report',\n 'Muestra el consumo de tokens agrupado por herramienta y por fuente durante un periodo.',\n {\n period: z\n .enum(['session', 'day', 'week', 'month'])\n .optional()\n .describe('Periodo del reporte (default: day)'),\n },\n async ({ period }) => {\n try {\n const since = sinceForPeriod(period ?? 'day')\n const report = manager.getBudgetReport(since)\n const lines = [`Reporte de consumo (desde ${report.period_since}):`, '']\n lines.push('Por herramienta:')\n if (report.by_tool.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of report.by_tool) {\n lines.push(` ${row.tool_name}: ${row.count} llamadas, ${row.tokens} tokens`)\n }\n }\n lines.push('')\n lines.push('Por fuente:')\n if (report.by_source.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of report.by_source) {\n lines.push(` ${row.source}: ${row.count} llamadas, ${row.tokens} tokens`)\n }\n }\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Shared MCP tool response helpers — Phase 2\n\nexport const text = (t: string) => ({\n content: [{ type: 'text' as const, text: t }],\n})\n\nexport const error = (t: string) => ({\n content: [{ type: 'text' as const, text: `Error: ${t}` }],\n isError: true as const,\n})\n","// Session tools — Phase 3.3 (simplified: session_search removed, FTS5 no longer available)\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\n\ntype DB = Database.Database\n\n// No session tools registered after FTS5 removal.\n// Keeping the function signature for backwards compatibility with server.ts imports.\nexport function registerSessionTools(_server: McpServer, _db: DB): void {\n // noop\n}\n","// Orchestration MCP tools — Phase 4.23-4.28\n// mcp_usage_stats, mcp_cost_report, optimization_status,\n// mcp_prune_suggest, mcp_prune_apply, mcp_prune_rollback, mcp_prune_clear\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { text, error } from '../lib/response.js'\nimport { getUsageStats, getCostReport } from '../services/stats.js'\nimport {\n probeSerena,\n probeRtk,\n probeMcpPruning,\n probePromptCaching,\n checkSerenaHealth,\n} from '../orchestration/detector.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\nimport { buildSuggestions } from '../orchestration/advisor.js'\nimport {\n generateFromHistory,\n applyAllowlist,\n rollback,\n clearAllowlist,\n} from '../cli/prune-mcp.js'\nimport type { OptimizationStatus } from '../lib/types.js'\nimport { buildSessionSummary } from '../services/session-summary-builder.js'\nimport { postSummaryToXray } from '../services/xray-client.js'\n\ntype DB = Database.Database\n\nexport function registerOrchestrationTools(server: McpServer, db: DB): void {\n // ── mcp_usage_stats ──\n server.tool(\n 'mcp_usage_stats',\n 'Estadisticas de uso de tokens por herramienta y fuente en un periodo.',\n {\n days: z.number().int().positive().max(365).optional().describe('Dias a analizar (default: 7)'),\n },\n async ({ days }) => {\n try {\n const stats = getUsageStats(db, days ?? 7)\n const lines = [\n `Uso en los ultimos ${stats.period_days} dia(s):`,\n '',\n `Total: ${stats.total_tokens} tokens, ${stats.total_events} eventos`,\n '',\n 'Por fuente:',\n ]\n if (stats.by_source.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of stats.by_source) {\n lines.push(` ${row.source}: ${row.tokens} tokens, ${row.count} llamadas`)\n }\n }\n lines.push('')\n lines.push('Top herramientas:')\n if (stats.by_tool.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of stats.by_tool.slice(0, 10)) {\n lines.push(` ${row.tool_name}: ${row.tokens} tokens, ${row.count} llamadas`)\n }\n }\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_cost_report ──\n server.tool(\n 'mcp_cost_report',\n 'Reporte de coste estimado con rango Haiku-Sonnet-Opus y disclaimer honesto.',\n {\n days: z\n .number()\n .int()\n .positive()\n .max(365)\n .optional()\n .describe('Dias a analizar (default: 7)'),\n },\n async ({ days }) => {\n try {\n const cost = getCostReport(db, days ?? 7)\n const lines = [\n `Reporte de coste (${cost.period_days} dia(s)):`,\n '',\n `Tokens totales: ${cost.total_tokens}`,\n `Coste estimado (input pricing):`,\n ` Haiku 4.5: $${cost.estimated_cost_usd_haiku.toFixed(4)} ($1/MTok)`,\n ` Sonnet 4.6: $${cost.estimated_cost_usd_sonnet.toFixed(4)} ($3/MTok)`,\n ` Opus 4.6: $${cost.estimated_cost_usd_opus.toFixed(4)} ($5/MTok)`,\n '',\n `Nota: ${cost.disclaimer}`,\n ]\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── optimization_status ──\n server.tool(\n 'optimization_status',\n 'Estado de las optimizaciones detectadas: serena, RTK, MCP pruning, prompt caching, schema size.',\n {},\n async () => {\n try {\n const serena = probeSerena()\n const rtk = probeRtk()\n const pruning = probeMcpPruning()\n const pcProbe = probePromptCaching()\n void pcProbe\n const schema = measureCurrentSchemaBytes()\n // Always include prompt_caching with explicit estimation_method per measurement-honesty spec\n const status: OptimizationStatus = {\n serena,\n rtk,\n mcp_pruning: pruning,\n prompt_caching: {\n active_by_default: true,\n savings_tokens: null,\n estimation_method: 'unknown',\n note: 'Revisa tu factura Anthropic para confirmar el ahorro real',\n },\n schema_bytes: {\n tool_schema_bytes: schema.tool_schema_bytes,\n measurement_method: schema.measurement_method,\n },\n }\n const serenaHealth = serena.present ? checkSerenaHealth() : []\n const suggestions = buildSuggestions(status)\n\n // Fire-and-forget summary to xray (if XRAY_URL is set)\n try {\n const lastSession = db\n .prepare('SELECT id FROM sessions ORDER BY started_at DESC LIMIT 1')\n .get() as { id: string } | undefined\n if (lastSession) {\n const summary = buildSessionSummary(db, lastSession.id, '0.2.6')\n void postSummaryToXray(summary as unknown as Record<string, unknown>).catch(() => {})\n }\n } catch {\n // Silent — xray is optional\n }\n\n return text(JSON.stringify({ status, serena_health: serenaHealth, suggestions }, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_suggest ──\n server.tool(\n 'mcp_prune_suggest',\n 'Genera un allowlist de MCPs basandose en el historial (NO modifica archivos).',\n {\n days: z\n .number()\n .int()\n .positive()\n .max(365)\n .optional()\n .describe('Dias de historial a analizar (default: 14)'),\n },\n async ({ days }) => {\n try {\n const proposal = generateFromHistory({ days: days ?? 14 })\n return text(JSON.stringify(proposal, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_apply ──\n server.tool(\n 'mcp_prune_apply',\n 'Restringe los MCPs activos escribiendo enabledMcpjsonServers en .claude/settings.local.json. Requiere confirm:true. Acepta dos formas equivalentes: allowlist (lista blanca, los que SI quieres) o exclude (lista negra, los que NO quieres). Se debe pasar exactamente una de las dos.',\n {\n allowlist: z\n .array(z.string())\n .optional()\n .describe('Nombres de MCPs a permitir (lista blanca). Exclusivo con exclude.'),\n exclude: z\n .array(z.string())\n .optional()\n .describe(\n 'Nombres de MCPs a desactivar (lista negra). Internamente se traduce a allowlist = registrados - exclude. Exclusivo con allowlist.',\n ),\n confirm: z.boolean().describe('Debe ser true para confirmar la escritura'),\n },\n async ({ allowlist, exclude, confirm }) => {\n try {\n if (confirm !== true) {\n return error(\n 'Operacion destructiva: requiere confirm:true. Revisa el allowlist antes de aplicar.',\n )\n }\n const hasAllow = Array.isArray(allowlist)\n const hasExclude = Array.isArray(exclude)\n if (hasAllow === hasExclude) {\n return error(\n 'Debes pasar exactamente uno: allowlist (los que SI quieres) o exclude (los que NO quieres).',\n )\n }\n\n const schema = measureCurrentSchemaBytes()\n const registered = new Set(schema.mcp_servers)\n\n let effective: string[]\n let translationNote = ''\n\n if (hasAllow) {\n effective = allowlist as string[]\n if (registered.size > 0) {\n const invalid = effective.filter((s) => !registered.has(s))\n if (invalid.length > 0) {\n return error(\n `Allowlist contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\n )\n }\n }\n } else {\n const excludeSet = new Set(exclude as string[])\n if (registered.size > 0) {\n const invalid = (exclude as string[]).filter((s) => !registered.has(s))\n if (invalid.length > 0) {\n return error(\n `Exclude contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\n )\n }\n }\n effective = [...registered].filter((s) => !excludeSet.has(s))\n translationNote = `\\n exclude: [${(exclude as string[]).join(', ')}]\\n → allowlist efectivo: [${effective.join(', ')}]`\n }\n\n const applied = applyAllowlist(effective, { source: 'mcp' })\n return text(\n `Allowlist aplicado.${translationNote}\\n settings: ${applied.settings_path}\\n backup: ${applied.backup_path}`,\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_rollback ──\n server.tool(\n 'mcp_prune_rollback',\n 'Restaura el backup mas reciente de settings.local.json. Requiere confirm:true.',\n {\n confirm: z.boolean(),\n to: z.string().optional().describe('Timestamp opcional del backup a restaurar'),\n },\n async ({ confirm, to }) => {\n try {\n if (confirm !== true) {\n return error('Operacion destructiva: requiere confirm:true.')\n }\n const result = rollback(to !== undefined ? { to } : {})\n if (!result.restored) return error('No hay backups disponibles.')\n return text(`Restaurado desde ${result.from}`)\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_clear ──\n server.tool(\n 'mcp_prune_clear',\n 'Elimina el allowlist de settings.local.json (crea backup). Requiere confirm:true.',\n {\n confirm: z.boolean(),\n },\n async ({ confirm }) => {\n try {\n if (confirm !== true) {\n return error('Operacion destructiva: requiere confirm:true.')\n }\n const result = clearAllowlist()\n return text(\n result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar',\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Session summary builder for xray integration.\n// Aggregates all local data sources into a single payload for xray.\n// Only called once per session (not in PostToolUse hot path).\n\nimport type Database from 'better-sqlite3'\nimport { getUsageStats, getCostReport } from './stats.js'\nimport {\n probeSerena,\n probeRtk,\n probeMcpPruning,\n probePromptCaching,\n} from '../orchestration/detector.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\nimport { getCoachSurfaceLog } from '../coach/surface.js'\nimport { resolveProjectDir } from '../lib/paths.js'\n\ntype DB = Database.Database\n\nexport interface XraySummaryPayload {\n session_id: string\n project_path: string\n project_name: string\n total_tokens: number\n total_events: number\n by_source: Array<{ source: string; count: number; tokens: number }>\n by_tool: Array<{ tool_name: string; count: number; tokens: number }>\n cost_haiku: number\n cost_sonnet: number\n cost_opus: number\n probes: {\n serena: { present: boolean; confidence: number; signals: string[] }\n rtk: { present: boolean; confidence: number; signals: string[] }\n mcp_pruning: { present: boolean; confidence: number; signals: string[] }\n prompt_caching: { present: boolean; confidence: number }\n }\n coach_tips_surfaced: Array<{ rule_id: string; tip_ids: string[]; severity: string }>\n schema_measurement: { tool_schema_tokens: number; mcp_servers: string[] }\n optimizer_version: string\n}\n\nexport function buildSessionSummary(\n db: DB,\n sessionId: string,\n version: string,\n): XraySummaryPayload {\n // Usage stats for last 24h (covers the session)\n const usage = getUsageStats(db, 1)\n const cost = getCostReport(db, 1)\n\n // Detection probes (reads local files, no network)\n const serena = probeSerena()\n const rtk = probeRtk()\n const mcpPruning = probeMcpPruning()\n const promptCaching = probePromptCaching()\n\n // Schema measurement (reads settings files, no network)\n const schema = measureCurrentSchemaBytes()\n\n // Coach tips surfaced during this session\n const coachTips = getCoachSurfaceLog(db, sessionId)\n\n const projDir = resolveProjectDir()\n const projName = projDir.split(/[\\\\/]/).filter(Boolean).pop() ?? 'unknown'\n\n return {\n session_id: sessionId,\n project_path: projDir,\n project_name: projName,\n total_tokens: usage.total_tokens,\n total_events: usage.total_events,\n by_source: usage.by_source,\n by_tool: usage.by_tool.map((t) => ({\n tool_name: t.tool_name,\n count: t.count,\n tokens: t.tokens,\n })),\n cost_haiku: cost.estimated_cost_usd_haiku,\n cost_sonnet: cost.estimated_cost_usd_sonnet,\n cost_opus: cost.estimated_cost_usd_opus,\n probes: {\n serena: { present: serena.present, confidence: serena.confidence, signals: serena.signals },\n rtk: { present: rtk.present, confidence: rtk.confidence, signals: rtk.signals },\n mcp_pruning: {\n present: mcpPruning.present,\n confidence: mcpPruning.confidence,\n signals: mcpPruning.signals,\n },\n prompt_caching: { present: promptCaching.present, confidence: promptCaching.confidence },\n },\n coach_tips_surfaced: coachTips,\n schema_measurement: {\n tool_schema_tokens: schema.tool_schema_tokens,\n mcp_servers: schema.mcp_servers,\n },\n optimizer_version: version,\n }\n}\n","// Coach MCP tool — Phase 4.46\n// coach_tips: returns active hits + full knowledge base + context measurement + reference table\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { text, error } from '../lib/response.js'\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\n\ntype DB = Database.Database\n\nexport function registerCoachTools(server: McpServer, db: DB): void {\n server.tool(\n 'coach_tips',\n 'Devuelve tips activos (rules disparadas) y medicion de contexto. Por defecto modo compacto (~500 tokens). Usa verbose=true para incluir el catalogo completo de 18 tips y la tabla de referencia (~3.5k tokens).',\n {\n session_id: z.string().optional().describe('ID de la sesion (default: \"default\")'),\n project_dir: z\n .string()\n .optional()\n .describe('Directorio del proyecto para medir contexto desde transcript JSONL'),\n active_model: z\n .string()\n .optional()\n .describe('Modelo activo (opcional, habilita regla detect-opus-for-simple-task)'),\n verbose: z\n .boolean()\n .optional()\n .describe(\n 'Si true, incluye el knowledge base completo (18 tips) y la reference data. Default false = solo hits activos + contexto. Ahorra ~3.3k tokens por llamada en modo compacto.',\n ),\n },\n async ({ session_id, project_dir, active_model, verbose }) => {\n try {\n const payloadOpts: Parameters<typeof computeCoachTipsPayload>[0] = { db }\n if (session_id !== undefined) payloadOpts.sessionId = session_id\n if (project_dir !== undefined) payloadOpts.projectDir = project_dir\n if (active_model !== undefined) payloadOpts.activeModel = active_model\n const response = await computeCoachTipsPayload(payloadOpts)\n\n // Default compact mode: strip the heavy knowledge_base + reference_data\n // to avoid burning ~3.3k tokens per call. verbose=true restores them.\n if (verbose !== true) {\n const { known_tricks: _kb, reference_data: _ref, ...compact } = response\n return text(JSON.stringify(compact, null, 2))\n }\n return text(JSON.stringify(response, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Reference data table with publicly-verifiable savings numbers — Phase 4.41\n// Every row tagged estimation_method: 'reference_measured'\n\nimport type { EstimationMethod } from '../lib/types.js'\n\nexport interface ReferenceDataRow {\n feature: string\n saving: string\n source: string\n verified_at: string\n estimation_method: EstimationMethod\n}\n\nexport const REFERENCE_DATA: readonly ReferenceDataRow[] = [\n {\n feature: 'Model switching (opusplan / default-to-sonnet)',\n saving: '60-80% reduccion de coste',\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Progressive disclosure skills',\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n source: 'claudefast.com',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Prompt caching read hit',\n saving: '10x mas barato que uncached',\n source: 'Anthropic docs',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Claude Code Tool Search',\n saving: '~85% schema reduction (77k → 8.7k tokens)',\n source: 'observado en sesion',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'MCP pruning sobre Tool Search',\n saving: '~5-12% adicional por turno',\n source: 'estimacion interna',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n]\n\nconst DAY_MS = 86_400_000\n\nexport function getFreshRows(\n daysThreshold = 90,\n today: Date = new Date(),\n): ReferenceDataRow[] {\n const cutoff = today.getTime() - daysThreshold * DAY_MS\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() >= cutoff)\n}\n\nexport function getStaleRows(\n daysThreshold = 90,\n today: Date = new Date(),\n): ReferenceDataRow[] {\n const cutoff = today.getTime() - daysThreshold * DAY_MS\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() < cutoff)\n}\n","// Shared payload builder for coach_tips MCP tool + token-optimizer://coach/tips\n// resource. Keeps tool/resource outputs identical — Phase 4.H.\n\nimport type Database from 'better-sqlite3'\nimport type { ContextMeasurement, EventContext, ToolEvent, DetectionHit, CoachTip } from '../lib/types.js'\nimport { KNOWLEDGE_BASE } from './knowledge-base.js'\nimport { REFERENCE_DATA, getStaleRows } from './reference-data.js'\nimport { runRules } from './detector.js'\nimport { measureContextSize } from './context-meter.js'\nimport { buildQueries } from '../db/queries.js'\n\ntype DB = Database.Database\n\nexport interface CoachTipsPayload {\n current: DetectionHit[]\n known_tricks: readonly CoachTip[]\n context: ContextMeasurement\n reference_data: typeof REFERENCE_DATA\n stale_reference_count: number\n last_computed_at: string\n}\n\nexport interface ComputeCoachTipsPayloadOptions {\n db: DB\n sessionId?: string\n projectDir?: string\n activeModel?: string\n}\n\nexport async function computeCoachTipsPayload(\n opts: ComputeCoachTipsPayloadOptions,\n): Promise<CoachTipsPayload> {\n const { db } = opts\n const sessionId = opts.sessionId ?? 'default'\n\n const contextOpts: Parameters<typeof measureContextSize>[1] = { db }\n if (opts.projectDir !== undefined) contextOpts.projectDir = opts.projectDir\n if (opts.activeModel !== undefined) contextOpts.activeModel = opts.activeModel\n const context = await measureContextSize(sessionId, contextOpts)\n\n const queries = buildQueries(db)\n const since = new Date(Date.now() - 86_400_000).toISOString()\n const rawRows = queries.getToolCallsSince(since) as ToolEvent[]\n const events = rawRows.slice(0, 100)\n\n const ctx: EventContext = {\n session_id: sessionId,\n events,\n session_token_total: context.tokens,\n session_token_method: context.estimation_method,\n session_token_limit: context.limit,\n active_model: opts.activeModel ?? null,\n }\n\n const hits = runRules(ctx)\n const staleTips = getStaleRows()\n\n return {\n current: hits,\n known_tricks: KNOWLEDGE_BASE,\n context,\n reference_data: REFERENCE_DATA,\n stale_reference_count: staleTips.length,\n last_computed_at: new Date().toISOString(),\n }\n}\n","// TOON encoding tools — Phase 5.3\n// toon_encode: data -> compact JSON (no whitespace) = token-efficient\n// toon_decode: toon string -> JSON object\n//\n// Note: the original `toon-format` npm package was deferred during Phase 0\n// due to package-name uncertainty. This implementation uses compact JSON under\n// the hood, which is round-trip lossless and ~30-40% cheaper in tokens than\n// pretty-printed JSON. The tool names are preserved so a real TOON impl can\n// drop in later without changing the MCP API.\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { text, error } from '../lib/response.js'\n\nfunction compactEncode(data: unknown): string {\n try {\n return JSON.stringify(data)\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e)\n if (/circular|cyclic/i.test(msg)) {\n throw new Error('Referencia circular detectada: TOON no soporta objetos ciclicos')\n }\n throw new Error(`No se pudo codificar a TOON: ${msg}`)\n }\n}\n\nfunction compactDecode(toon: string): unknown {\n try {\n return JSON.parse(toon)\n } catch (e) {\n throw new Error(`TOON invalido: ${e instanceof Error ? e.message : String(e)}`)\n }\n}\n\nexport function registerToonTools(server: McpServer): void {\n // ── toon_encode ──\n server.tool(\n 'toon_encode',\n 'Codifica un objeto JSON a formato TOON (JSON compacto token-eficiente, round-trip lossless).',\n {\n data: z.unknown().describe('Valor a codificar (objeto, array, primitivo)'),\n },\n async ({ data }) => {\n try {\n const encoded = compactEncode(data)\n return text(encoded)\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── toon_decode ──\n server.tool(\n 'toon_decode',\n 'Decodifica una cadena TOON a JSON. Devuelve el objeto formateado para lectura.',\n {\n toon: z.string().min(1).describe('Cadena TOON a decodificar'),\n },\n async ({ toon }) => {\n try {\n const decoded = compactDecode(toon)\n return text(JSON.stringify(decoded, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n\n// Exported for tests\nexport const _internal = { compactEncode, compactDecode }\n","// token-optimizer://coach/tips resource — Phase 4.H\n// Mirrors coach_tips() tool payload, readable without a tool call.\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\n\ntype DB = Database.Database\n\nexport const COACH_TIPS_URI = 'token-optimizer://coach/tips'\n\nexport function registerCoachTipsResource(server: McpServer, db: DB): void {\n server.resource(\n 'coach-tips',\n COACH_TIPS_URI,\n {\n description:\n 'Tips activos del coach, catalogo completo de trucos, medicion de contexto y tabla de referencia.',\n mimeType: 'application/json',\n },\n async (uri: URL) => {\n try {\n const payload = await computeCoachTipsPayload({ db })\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify(payload, null, 2),\n },\n ],\n }\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e)\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify({ error: message }),\n },\n ],\n }\n }\n },\n )\n}\n"],"mappings":"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","names":["z","z","z","DAY_MS","DAY_MS","z","z","z"]} |
| #!/usr/bin/env node | ||
| import { | ||
| getActiveBudgetSummary, | ||
| getUsageStats | ||
| } from "./chunk-3LSC7CFQ.js"; | ||
| import "./chunk-DK5JDNWQ.js"; | ||
| import "./chunk-OIZX7KF2.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| projectHash, | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/status.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| function runStatus(_args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = path.join(home, ".claude", "settings.json"); | ||
| const installed = (() => { | ||
| try { | ||
| if (!fs.existsSync(settingsPath)) return false; | ||
| const json = JSON.parse(fs.readFileSync(settingsPath, "utf8")); | ||
| const mcp = json.mcpServers ?? {}; | ||
| return "token-optimizer" in mcp; | ||
| } catch { | ||
| return false; | ||
| } | ||
| })(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| let eventsToday = 0; | ||
| let tokensBySource = []; | ||
| let budgetLine = "sin presupuesto activo"; | ||
| if (fs.existsSync(dbPath)) { | ||
| try { | ||
| const db = getDb(dbPath); | ||
| const usage = getUsageStats(db, 1); | ||
| eventsToday = usage.total_events; | ||
| tokensBySource = usage.by_source.map((r) => ({ source: r.source, tokens: r.tokens })); | ||
| const budget = getActiveBudgetSummary(db, "default", projectHash(projectDir)); | ||
| if (budget.active) { | ||
| const pct = (budget.percent_used * 100).toFixed(1); | ||
| budgetLine = `gastado=${budget.spent} restante=${budget.remaining} uso=${pct}% modo=${budget.mode}`; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| const lines = []; | ||
| lines.push("token-optimizer-mcp status"); | ||
| lines.push(""); | ||
| lines.push(`Instalado: ${installed ? "\u2713" : "\u2717"} (${settingsPath})`); | ||
| lines.push(`Storage DB: ${dbPath}${fs.existsSync(dbPath) ? "" : " (no existe aun)"}`); | ||
| lines.push(`Eventos hoy: ${eventsToday}`); | ||
| lines.push( | ||
| `Tokens por fuente: ${tokensBySource.length > 0 ? tokensBySource.map((s) => `${s.source}=${s.tokens}`).join(", ") : "(sin datos)"}` | ||
| ); | ||
| lines.push(`Presupuesto: ${budgetLine}`); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| export { | ||
| runStatus | ||
| }; | ||
| //# sourceMappingURL=status-ZCOK7BWN.js.map |
| {"version":3,"sources":["../src/cli/status.ts"],"sourcesContent":["// Status CLI — Phase 4.13\n// Prints install detection, storage DB, events today, tokens by source, active budget.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport os from 'node:os'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath, projectHash } from '../lib/paths.js'\nimport { getUsageStats, getActiveBudgetSummary } from '../services/stats.js'\n\nexport interface StatusOptions {\n home?: string\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runStatus(_args: string[] = [], opts: StatusOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const home = opts.home ?? os.homedir()\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = path.join(home, '.claude', 'settings.json')\n\n const installed = (() => {\n try {\n if (!fs.existsSync(settingsPath)) return false\n const json = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as Record<string, unknown>\n const mcp = (json.mcpServers ?? {}) as Record<string, unknown>\n return 'token-optimizer' in mcp\n } catch {\n return false\n }\n })()\n\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n let eventsToday = 0\n let tokensBySource: Array<{ source: string; tokens: number }> = []\n let budgetLine = 'sin presupuesto activo'\n\n if (fs.existsSync(dbPath)) {\n try {\n const db = getDb(dbPath)\n const usage = getUsageStats(db, 1)\n eventsToday = usage.total_events\n tokensBySource = usage.by_source.map((r) => ({ source: r.source, tokens: r.tokens }))\n const budget = getActiveBudgetSummary(db, 'default', projectHash(projectDir))\n if (budget.active) {\n const pct = (budget.percent_used * 100).toFixed(1)\n budgetLine = `gastado=${budget.spent} restante=${budget.remaining} uso=${pct}% modo=${budget.mode}`\n }\n } catch {\n // swallow\n }\n }\n\n const lines: string[] = []\n lines.push('token-optimizer-mcp status')\n lines.push('')\n lines.push(`Instalado: ${installed ? '✓' : '✗'} (${settingsPath})`)\n lines.push(`Storage DB: ${dbPath}${fs.existsSync(dbPath) ? '' : ' (no existe aun)'}`)\n lines.push(`Eventos hoy: ${eventsToday}`)\n lines.push(\n `Tokens por fuente: ${\n tokensBySource.length > 0\n ? tokensBySource.map((s) => `${s.source}=${s.tokens}`).join(', ')\n : '(sin datos)'\n }`,\n )\n lines.push(`Presupuesto: ${budgetLine}`)\n\n print(lines.join('\\n'))\n return 0\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAGA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAWR,SAAS,UAAU,QAAkB,CAAC,GAAG,OAAsB,CAAC,GAAW;AAChF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ;AACrC,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,eAAe,KAAK,KAAK,MAAM,WAAW,eAAe;AAE/D,QAAM,aAAa,MAAM;AACvB,QAAI;AACF,UAAI,CAAC,GAAG,WAAW,YAAY,EAAG,QAAO;AACzC,YAAM,OAAO,KAAK,MAAM,GAAG,aAAa,cAAc,MAAM,CAAC;AAC7D,YAAM,MAAO,KAAK,cAAc,CAAC;AACjC,aAAO,qBAAqB;AAAA,IAC9B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,MAAI,cAAc;AAClB,MAAI,iBAA4D,CAAC;AACjE,MAAI,aAAa;AAEjB,MAAI,GAAG,WAAW,MAAM,GAAG;AACzB,QAAI;AACF,YAAM,KAAK,MAAM,MAAM;AACvB,YAAM,QAAQ,cAAc,IAAI,CAAC;AACjC,oBAAc,MAAM;AACpB,uBAAiB,MAAM,UAAU,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,QAAQ,EAAE,OAAO,EAAE;AACpF,YAAM,SAAS,uBAAuB,IAAI,WAAW,YAAY,UAAU,CAAC;AAC5E,UAAI,OAAO,QAAQ;AACjB,cAAM,OAAO,OAAO,eAAe,KAAK,QAAQ,CAAC;AACjD,qBAAa,WAAW,OAAO,KAAK,aAAa,OAAO,SAAS,QAAQ,GAAG,UAAU,OAAO,IAAI;AAAA,MACnG;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,4BAA4B;AACvC,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,uBAAuB,YAAY,WAAM,QAAG,KAAK,YAAY,GAAG;AAC3E,QAAM,KAAK,uBAAuB,MAAM,GAAG,GAAG,WAAW,MAAM,IAAI,KAAK,kBAAkB,EAAE;AAC5F,QAAM,KAAK,uBAAuB,WAAW,EAAE;AAC/C,QAAM;AAAA,IACJ,uBACE,eAAe,SAAS,IACpB,eAAe,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,IAAI,IAC9D,aACN;AAAA,EACF;AACA,QAAM,KAAK,uBAAuB,UAAU,EAAE;AAE9C,QAAM,MAAM,KAAK,IAAI,CAAC;AACtB,SAAO;AACT;","names":[]} |
+12
-12
@@ -9,3 +9,3 @@ #!/usr/bin/env node | ||
| surfaceWithDedupe | ||
| } from "./chunk-4PA7MRTS.js"; | ||
| } from "./chunk-22BLF7AQ.js"; | ||
| import { | ||
@@ -22,9 +22,9 @@ loadConfig | ||
| BudgetManager | ||
| } from "./chunk-XAYSOAZV.js"; | ||
| } from "./chunk-DK5JDNWQ.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FT4EJUIT.js"; | ||
| } from "./chunk-OIZX7KF2.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-L4GG4274.js"; | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
@@ -325,3 +325,3 @@ normalizePath, | ||
| var PREVIEW_FIELDS = ["command", "file_path", "pattern", "relative_path", "name_path", "path"]; | ||
| function extractCommandPreview(toolName, input) { | ||
| function extractCommandPreview(_toolName, input) { | ||
| if (input == null || typeof input !== "object") return void 0; | ||
@@ -361,2 +361,3 @@ const obj = input; | ||
| let estimationMethod = tagEstimationMethod(source); | ||
| const commandPreview = extractCommandPreview(toolName, parsed.tool_input); | ||
| const event = { | ||
@@ -371,3 +372,4 @@ session_id: sessionId, | ||
| estimation_method: estimationMethod, | ||
| created_at: (/* @__PURE__ */ new Date()).toISOString() | ||
| created_at: (/* @__PURE__ */ new Date()).toISOString(), | ||
| command_preview: commandPreview ?? null | ||
| }; | ||
@@ -456,9 +458,7 @@ let additionalContext = null; | ||
| const projDir = opts.projectDir ?? resolveProjectDir(); | ||
| const commandPreview = extractCommandPreview(toolName, parsed.tool_input); | ||
| const enriched = { | ||
| ...event, | ||
| project_path: projDir, | ||
| project_name: projDir.split(/[\\/]/).filter(Boolean).pop() ?? "unknown", | ||
| project_hash: projectHash(projDir), | ||
| ...commandPreview !== void 0 && { command_preview: commandPreview } | ||
| project_name: projDir.split(/[\\\/]/).filter(Boolean).pop() ?? "unknown", | ||
| project_hash: projectHash(projDir) | ||
| }; | ||
@@ -894,3 +894,3 @@ void postToXray(enriched).catch(() => { | ||
| const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js"); | ||
| const { createServer } = await import("./server-KUXDBXVT.js"); | ||
| const { createServer } = await import("./server-GY6E6HFP.js"); | ||
| const server = createServer(); | ||
@@ -905,5 +905,5 @@ const transport = new StdioServerTransport(); | ||
| } | ||
| var { dispatchCli } = await import("./dispatcher-XGDCQYQI.js"); | ||
| var { dispatchCli } = await import("./dispatcher-SW3I5LZR.js"); | ||
| var exitCode = await dispatchCli(args); | ||
| process.exit(exitCode); | ||
| //# sourceMappingURL=index.js.map |
+1
-1
| { | ||
| "name": "@cocaxcode/token-optimizer-mcp", | ||
| "version": "0.4.20", | ||
| "version": "0.4.21", | ||
| "mcpName": "io.github.cocaxcode/token-optimizer-mcp", | ||
@@ -5,0 +5,0 @@ "description": "Orchestration + observability + coach layer for Claude Code token optimization. Measures tool usage, enforces budgets, advises on complementary tools (serena, RTK), and proactively surfaces savings tips.", |
| #!/usr/bin/env node | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-XAYSOAZV.js"; | ||
| import "./chunk-FT4EJUIT.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-L4GG4274.js"; | ||
| import { | ||
| projectHash, | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/budget.ts | ||
| function runBudgetCli(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const db = getDb(dbPath); | ||
| const mgr = new BudgetManager(db); | ||
| const hash = projectHash(projectDir); | ||
| const sub = args[0]; | ||
| if (sub === "set") { | ||
| const scope = args[1]; | ||
| const limitRaw = args[2]; | ||
| const limit = limitRaw ? parseInt(limitRaw, 10) : NaN; | ||
| if (scope !== "session" && scope !== "project" || !Number.isFinite(limit)) { | ||
| print("Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| try { | ||
| const budget = mgr.setBudget({ | ||
| scope, | ||
| scope_key: scopeKey, | ||
| limit_tokens: limit | ||
| }); | ||
| print( | ||
| `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}` | ||
| ); | ||
| return 0; | ||
| } catch (e) { | ||
| print(`Error: ${e instanceof Error ? e.message : String(e)}`); | ||
| return 1; | ||
| } | ||
| } | ||
| if (sub === "get") { | ||
| const status = mgr.checkBudget("default", hash); | ||
| if (!status.active) { | ||
| print("Sin presupuesto activo"); | ||
| return 0; | ||
| } | ||
| const pct = (status.percent_used * 100).toFixed(1); | ||
| print( | ||
| `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? "n/a"}` | ||
| ); | ||
| return 0; | ||
| } | ||
| if (sub === "clear") { | ||
| const scope = args[1]; | ||
| if (scope !== "session" && scope !== "project") { | ||
| print("Uso: token-optimizer-mcp budget clear <session|project>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| const removed = mgr.clearBudget(scope, scopeKey); | ||
| print(removed ? `Eliminado (${scope})` : "No habia presupuesto para este scope"); | ||
| return 0; | ||
| } | ||
| print("Uso: token-optimizer-mcp budget <set|get|clear> [args]"); | ||
| return 1; | ||
| } | ||
| export { | ||
| runBudgetCli | ||
| }; | ||
| //# sourceMappingURL=budget-ZAT64F3R.js.map |
| {"version":3,"sources":["../src/cli/budget.ts"],"sourcesContent":["// Budget CLI — Phase 4.15\n// Thin wrapper delegating to BudgetManager. Subcommands: set / get / clear.\n\nimport { getDb } from '../db/connection.js'\nimport {\n resolveProjectDir,\n resolveAnalyticsDbPath,\n projectHash,\n} from '../lib/paths.js'\nimport { BudgetManager } from '../services/budget-manager.js'\nimport type { BudgetScope } from '../lib/types.js'\n\nexport interface BudgetCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runBudgetCli(args: string[] = [], opts: BudgetCliOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n const db = getDb(dbPath)\n const mgr = new BudgetManager(db)\n const hash = projectHash(projectDir)\n\n const sub = args[0]\n\n if (sub === 'set') {\n const scope = args[1] as BudgetScope | undefined\n const limitRaw = args[2]\n const limit = limitRaw ? parseInt(limitRaw, 10) : NaN\n if ((scope !== 'session' && scope !== 'project') || !Number.isFinite(limit)) {\n print('Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>')\n return 1\n }\n const scopeKey = scope === 'session' ? 'default' : hash\n try {\n const budget = mgr.setBudget({\n scope,\n scope_key: scopeKey,\n limit_tokens: limit,\n })\n print(\n `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}`,\n )\n return 0\n } catch (e) {\n print(`Error: ${e instanceof Error ? e.message : String(e)}`)\n return 1\n }\n }\n\n if (sub === 'get') {\n const status = mgr.checkBudget('default', hash)\n if (!status.active) {\n print('Sin presupuesto activo')\n return 0\n }\n const pct = (status.percent_used * 100).toFixed(1)\n print(\n `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? 'n/a'}`,\n )\n return 0\n }\n\n if (sub === 'clear') {\n const scope = args[1] as BudgetScope | undefined\n if (scope !== 'session' && scope !== 'project') {\n print('Uso: token-optimizer-mcp budget clear <session|project>')\n return 1\n }\n const scopeKey = scope === 'session' ? 'default' : hash\n const removed = mgr.clearBudget(scope, scopeKey)\n print(removed ? `Eliminado (${scope})` : 'No habia presupuesto para este scope')\n return 0\n }\n\n print('Uso: token-optimizer-mcp budget <set|get|clear> [args]')\n return 1\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBO,SAAS,aAAa,OAAiB,CAAC,GAAG,OAAyB,CAAC,GAAW;AACrF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,QAAM,KAAK,MAAM,MAAM;AACvB,QAAM,MAAM,IAAI,cAAc,EAAE;AAChC,QAAM,OAAO,YAAY,UAAU;AAEnC,QAAM,MAAM,KAAK,CAAC;AAElB,MAAI,QAAQ,OAAO;AACjB,UAAM,QAAQ,KAAK,CAAC;AACpB,UAAM,WAAW,KAAK,CAAC;AACvB,UAAM,QAAQ,WAAW,SAAS,UAAU,EAAE,IAAI;AAClD,QAAK,UAAU,aAAa,UAAU,aAAc,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3E,YAAM,sEAAsE;AAC5E,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,QAAI;AACF,YAAM,SAAS,IAAI,UAAU;AAAA,QAC3B;AAAA,QACA,WAAW;AAAA,QACX,cAAc;AAAA,MAChB,CAAC;AACD;AAAA,QACE,yBAAyB,OAAO,KAAK,IAAI,OAAO,SAAS,UAAU,OAAO,YAAY,SAAS,OAAO,IAAI;AAAA,MAC5G;AACA,aAAO;AAAA,IACT,SAAS,GAAG;AACV,YAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AAC5D,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,QAAQ,OAAO;AACjB,UAAM,SAAS,IAAI,YAAY,WAAW,IAAI;AAC9C,QAAI,CAAC,OAAO,QAAQ;AAClB,YAAM,wBAAwB;AAC9B,aAAO;AAAA,IACT;AACA,UAAM,OAAO,OAAO,eAAe,KAAK,QAAQ,CAAC;AACjD;AAAA,MACE,WAAW,OAAO,KAAK,aAAa,OAAO,SAAS,QAAQ,GAAG,UAAU,OAAO,QAAQ,KAAK;AAAA,IAC/F;AACA,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,SAAS;AACnB,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,UAAU,aAAa,UAAU,WAAW;AAC9C,YAAM,yDAAyD;AAC/D,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,UAAM,UAAU,IAAI,YAAY,OAAO,QAAQ;AAC/C,UAAM,UAAU,cAAc,KAAK,MAAM,sCAAsC;AAC/E,WAAO;AAAA,EACT;AAEA,QAAM,wDAAwD;AAC9D,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-L5Z32XXL.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-L4GG4274.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/prune-mcp.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/; | ||
| function extractServerFromToolName(toolName) { | ||
| const m = MCP_TOOL_RE.exec(toolName); | ||
| return m ? m[1] : null; | ||
| } | ||
| function settingsLocalPath(cwd) { | ||
| return path.join(cwd, ".claude", "settings.local.json"); | ||
| } | ||
| function readJsonSafe(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return {}; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| function writeJson(p, data) { | ||
| fs.mkdirSync(path.dirname(p), { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(data, null, 2)); | ||
| } | ||
| function generateFromHistory(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const days = opts.days ?? 14; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const serverCounts = {}; | ||
| if (fs.existsSync(dbPath)) { | ||
| const db = getDb(dbPath); | ||
| const rows = db.prepare( | ||
| `SELECT tool_name, COUNT(*) as count | ||
| FROM tool_calls | ||
| WHERE created_at >= ? AND tool_name LIKE 'mcp__%' | ||
| GROUP BY tool_name` | ||
| ).all(since); | ||
| for (const row of rows) { | ||
| const server = extractServerFromToolName(row.tool_name); | ||
| if (server) { | ||
| serverCounts[server] = (serverCounts[server] ?? 0) + row.count; | ||
| } | ||
| } | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd, home: opts.home }); | ||
| const registered = new Set(schema.mcp_servers); | ||
| const used = new Set(Object.keys(serverCounts)); | ||
| const inactive = [...registered].filter((s) => !used.has(s)); | ||
| return { | ||
| proposed_allowlist: [...used], | ||
| inactive_servers: inactive, | ||
| analysis_days: days, | ||
| total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0), | ||
| server_counts: serverCounts | ||
| }; | ||
| } | ||
| function timestampForBackup() { | ||
| return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); | ||
| } | ||
| function insertSnapshot(cwd, method, details) { | ||
| try { | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) return; | ||
| const db = getDb(dbPath); | ||
| db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run( | ||
| method, | ||
| JSON.stringify(details) | ||
| ); | ||
| } catch { | ||
| } | ||
| } | ||
| function applyAllowlist(allowlist, opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const source = opts.source ?? "cli"; | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| if (fs.existsSync(settingsPath)) { | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| } else { | ||
| fs.mkdirSync(path.dirname(backupPath), { recursive: true }); | ||
| fs.writeFileSync(backupPath, "{}"); | ||
| } | ||
| const current = readJsonSafe(settingsPath); | ||
| current.enabledMcpjsonServers = allowlist; | ||
| writeJson(settingsPath, current); | ||
| insertSnapshot(cwd, source === "mcp" ? "allowlist_generated_via_mcp" : "allowlist_generated", { | ||
| allowlist, | ||
| target: settingsPath, | ||
| backup: backupPath | ||
| }); | ||
| return { settings_path: settingsPath, backup_path: backupPath }; | ||
| } | ||
| function rollback(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const dir = path.dirname(settingsPath); | ||
| if (!fs.existsSync(dir)) return { restored: false, from: null }; | ||
| const backups = fs.readdirSync(dir).filter((f) => f.startsWith("settings.local.json.backup-")).sort(); | ||
| if (backups.length === 0) return { restored: false, from: null }; | ||
| const target = opts.to ? backups.find((b) => b.includes(opts.to)) : backups[backups.length - 1]; | ||
| if (!target) return { restored: false, from: null }; | ||
| const backupPath = path.join(dir, target); | ||
| fs.copyFileSync(backupPath, settingsPath); | ||
| insertSnapshot(cwd, "rollback", { from: backupPath, to: settingsPath }); | ||
| return { restored: true, from: backupPath }; | ||
| } | ||
| function clearAllowlist(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }; | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| const json = readJsonSafe(settingsPath); | ||
| delete json.enabledMcpjsonServers; | ||
| writeJson(settingsPath, json); | ||
| insertSnapshot(cwd, "allowlist_cleared", { backup: backupPath }); | ||
| return { cleared: true, backup_path: backupPath }; | ||
| } | ||
| function impact(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const db = getDb(dbPath); | ||
| const snapshot = db.prepare( | ||
| `SELECT created_at FROM optimization_snapshots | ||
| WHERE method LIKE 'allowlist_%' | ||
| ORDER BY created_at DESC LIMIT 1` | ||
| ).get(); | ||
| if (!snapshot) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const before = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const after = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const beforeAvg = before.avg; | ||
| const afterAvg = after.avg; | ||
| const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null; | ||
| const percent = beforeAvg !== null && beforeAvg > 0 && afterAvg !== null ? (afterAvg - beforeAvg) / beforeAvg : null; | ||
| return { | ||
| before_avg: beforeAvg, | ||
| after_avg: afterAvg, | ||
| delta, | ||
| percent, | ||
| snapshot_at: snapshot.created_at | ||
| }; | ||
| } | ||
| function runPruneMcp(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| if (args.includes("--generate-from-history")) { | ||
| const daysFlag = args.find((a) => a.startsWith("--days=")); | ||
| const days = daysFlag ? parseInt(daysFlag.split("=")[1], 10) : 14; | ||
| const result = generateFromHistory({ cwd, days }); | ||
| print(`Propuesta de allowlist (${days} dias de historial):`); | ||
| print(` Usados: ${result.proposed_allowlist.join(", ") || "(ninguno)"}`); | ||
| print(` Inactivos: ${result.inactive_servers.join(", ") || "(ninguno)"}`); | ||
| print(` Eventos MCP totales: ${result.total_mcp_events}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--apply")) { | ||
| const generated = generateFromHistory({ cwd }); | ||
| if (generated.proposed_allowlist.length === 0) { | ||
| print("No hay MCPs activos en el historial. Nada que aplicar."); | ||
| return 1; | ||
| } | ||
| const applied = applyAllowlist(generated.proposed_allowlist, { cwd }); | ||
| print(`Allowlist aplicado a ${applied.settings_path}`); | ||
| print(`Backup: ${applied.backup_path}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--rollback")) { | ||
| const toFlag = args.find((a) => a.startsWith("--to=")); | ||
| const to = toFlag ? toFlag.split("=")[1] : void 0; | ||
| const result = rollback({ cwd, to }); | ||
| if (result.restored) { | ||
| print(`Restaurado desde ${result.from}`); | ||
| return 0; | ||
| } | ||
| print("No hay backups disponibles."); | ||
| return 1; | ||
| } | ||
| if (args.includes("--clear")) { | ||
| const result = clearAllowlist({ cwd }); | ||
| print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : "Nada que eliminar"); | ||
| return 0; | ||
| } | ||
| if (args.includes("--impact")) { | ||
| const result = impact({ cwd }); | ||
| if (result.snapshot_at === null) { | ||
| print("No hay snapshots de allowlist todavia."); | ||
| return 0; | ||
| } | ||
| print(`Snapshot mas reciente: ${result.snapshot_at}`); | ||
| print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? "n/a"}`); | ||
| print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? "n/a"}`); | ||
| if (result.percent !== null) { | ||
| print(`Delta: ${(result.percent * 100).toFixed(1)}%`); | ||
| } | ||
| return 0; | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd }); | ||
| print(`MCPs registrados (${schema.mcp_servers.length}):`); | ||
| for (const s of schema.mcp_servers) { | ||
| print(` ${s}`); | ||
| } | ||
| print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`); | ||
| print(""); | ||
| print("Flags:"); | ||
| print(" --generate-from-history [--days N] Propone allowlist (read-only)"); | ||
| print(" --apply Aplica el allowlist generado"); | ||
| print(" --rollback [--to TIMESTAMP] Restaura el ultimo backup"); | ||
| print(" --clear Elimina allowlist actual"); | ||
| print(" --impact Compara antes/despues del ultimo snapshot"); | ||
| return 0; | ||
| } | ||
| export { | ||
| settingsLocalPath, | ||
| generateFromHistory, | ||
| applyAllowlist, | ||
| rollback, | ||
| clearAllowlist, | ||
| impact, | ||
| runPruneMcp | ||
| }; | ||
| //# sourceMappingURL=chunk-3DLEPM6P.js.map |
| {"version":3,"sources":["../src/cli/prune-mcp.ts"],"sourcesContent":["// prune-mcp CLI + service — Phase 4.16-4.22\n// Generate allowlist from history, apply/rollback/clear, compute impact.\n// Writes to .claude/settings.local.json (NOT settings.json).\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\n\nconst MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/\n\nfunction extractServerFromToolName(toolName: string): string | null {\n const m = MCP_TOOL_RE.exec(toolName)\n return m ? m[1] : null\n}\n\nexport function settingsLocalPath(cwd: string): string {\n return path.join(cwd, '.claude', 'settings.local.json')\n}\n\nfunction readJsonSafe(p: string): Record<string, unknown> {\n try {\n if (!fs.existsSync(p)) return {}\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\n } catch {\n return {}\n }\n}\n\nfunction writeJson(p: string, data: Record<string, unknown>): void {\n fs.mkdirSync(path.dirname(p), { recursive: true })\n fs.writeFileSync(p, JSON.stringify(data, null, 2))\n}\n\nexport interface GeneratedAllowlist {\n proposed_allowlist: string[]\n inactive_servers: string[]\n analysis_days: number\n total_mcp_events: number\n server_counts: Record<string, number>\n}\n\nexport interface GenerateOptions {\n cwd?: string\n days?: number\n home?: string\n}\n\nexport function generateFromHistory(opts: GenerateOptions = {}): GeneratedAllowlist {\n const cwd = opts.cwd ?? process.cwd()\n const days = opts.days ?? 14\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\n\n const serverCounts: Record<string, number> = {}\n if (fs.existsSync(dbPath)) {\n const db = getDb(dbPath)\n const rows = db\n .prepare(\n `SELECT tool_name, COUNT(*) as count\n FROM tool_calls\n WHERE created_at >= ? AND tool_name LIKE 'mcp__%'\n GROUP BY tool_name`,\n )\n .all(since) as Array<{ tool_name: string; count: number }>\n for (const row of rows) {\n const server = extractServerFromToolName(row.tool_name)\n if (server) {\n serverCounts[server] = (serverCounts[server] ?? 0) + row.count\n }\n }\n }\n\n const schema = measureCurrentSchemaBytes({ cwd, home: opts.home })\n const registered = new Set(schema.mcp_servers)\n const used = new Set(Object.keys(serverCounts))\n const inactive = [...registered].filter((s) => !used.has(s))\n\n return {\n proposed_allowlist: [...used],\n inactive_servers: inactive,\n analysis_days: days,\n total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0),\n server_counts: serverCounts,\n }\n}\n\nexport interface ApplyOptions {\n cwd?: string\n source?: 'cli' | 'mcp'\n}\n\nexport interface ApplyResult {\n settings_path: string\n backup_path: string\n}\n\nfunction timestampForBackup(): string {\n return new Date().toISOString().replace(/[:.]/g, '-')\n}\n\nfunction insertSnapshot(cwd: string, method: string, details: Record<string, unknown>): void {\n try {\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) return\n const db = getDb(dbPath)\n db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run(\n method,\n JSON.stringify(details),\n )\n } catch {\n // swallow\n }\n}\n\nexport function applyAllowlist(allowlist: string[], opts: ApplyOptions = {}): ApplyResult {\n const cwd = opts.cwd ?? process.cwd()\n const source = opts.source ?? 'cli'\n const settingsPath = settingsLocalPath(cwd)\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\n\n if (fs.existsSync(settingsPath)) {\n fs.copyFileSync(settingsPath, backupPath)\n } else {\n fs.mkdirSync(path.dirname(backupPath), { recursive: true })\n fs.writeFileSync(backupPath, '{}')\n }\n\n const current = readJsonSafe(settingsPath)\n current.enabledMcpjsonServers = allowlist\n writeJson(settingsPath, current)\n\n insertSnapshot(cwd, source === 'mcp' ? 'allowlist_generated_via_mcp' : 'allowlist_generated', {\n allowlist,\n target: settingsPath,\n backup: backupPath,\n })\n\n return { settings_path: settingsPath, backup_path: backupPath }\n}\n\nexport interface RollbackOptions {\n cwd?: string\n to?: string\n}\n\nexport interface RollbackResult {\n restored: boolean\n from: string | null\n}\n\nexport function rollback(opts: RollbackOptions = {}): RollbackResult {\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = settingsLocalPath(cwd)\n const dir = path.dirname(settingsPath)\n if (!fs.existsSync(dir)) return { restored: false, from: null }\n\n const backups = fs\n .readdirSync(dir)\n .filter((f) => f.startsWith('settings.local.json.backup-'))\n .sort()\n if (backups.length === 0) return { restored: false, from: null }\n\n const target = opts.to ? backups.find((b) => b.includes(opts.to!)) : backups[backups.length - 1]\n if (!target) return { restored: false, from: null }\n\n const backupPath = path.join(dir, target)\n fs.copyFileSync(backupPath, settingsPath)\n\n insertSnapshot(cwd, 'rollback', { from: backupPath, to: settingsPath })\n return { restored: true, from: backupPath }\n}\n\nexport function clearAllowlist(opts: { cwd?: string } = {}): { cleared: boolean; backup_path: string | null } {\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = settingsLocalPath(cwd)\n if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }\n\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\n fs.copyFileSync(settingsPath, backupPath)\n\n const json = readJsonSafe(settingsPath)\n delete json.enabledMcpjsonServers\n writeJson(settingsPath, json)\n\n insertSnapshot(cwd, 'allowlist_cleared', { backup: backupPath })\n return { cleared: true, backup_path: backupPath }\n}\n\nexport interface ImpactResult {\n before_avg: number | null\n after_avg: number | null\n delta: number | null\n percent: number | null\n snapshot_at: string | null\n}\n\nexport function impact(opts: { cwd?: string } = {}): ImpactResult {\n const cwd = opts.cwd ?? process.cwd()\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) {\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\n }\n const db = getDb(dbPath)\n const snapshot = db\n .prepare(\n `SELECT created_at FROM optimization_snapshots\n WHERE method LIKE 'allowlist_%'\n ORDER BY created_at DESC LIMIT 1`,\n )\n .get() as { created_at: string } | undefined\n if (!snapshot) {\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\n }\n\n const before = db\n .prepare(\n `SELECT AVG(tokens_estimated) as avg FROM (\n SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100\n )`,\n )\n .get(snapshot.created_at) as { avg: number | null }\n const after = db\n .prepare(\n `SELECT AVG(tokens_estimated) as avg FROM (\n SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100\n )`,\n )\n .get(snapshot.created_at) as { avg: number | null }\n\n const beforeAvg = before.avg\n const afterAvg = after.avg\n const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null\n const percent =\n beforeAvg !== null && beforeAvg > 0 && afterAvg !== null\n ? (afterAvg - beforeAvg) / beforeAvg\n : null\n\n return {\n before_avg: beforeAvg,\n after_avg: afterAvg,\n delta,\n percent,\n snapshot_at: snapshot.created_at,\n }\n}\n\nexport interface PruneMcpCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runPruneMcp(args: string[] = [], opts: PruneMcpCliOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n\n if (args.includes('--generate-from-history')) {\n const daysFlag = args.find((a) => a.startsWith('--days='))\n const days = daysFlag ? parseInt(daysFlag.split('=')[1], 10) : 14\n const result = generateFromHistory({ cwd, days })\n print(`Propuesta de allowlist (${days} dias de historial):`)\n print(` Usados: ${result.proposed_allowlist.join(', ') || '(ninguno)'}`)\n print(` Inactivos: ${result.inactive_servers.join(', ') || '(ninguno)'}`)\n print(` Eventos MCP totales: ${result.total_mcp_events}`)\n return 0\n }\n\n if (args.includes('--apply')) {\n const generated = generateFromHistory({ cwd })\n if (generated.proposed_allowlist.length === 0) {\n print('No hay MCPs activos en el historial. Nada que aplicar.')\n return 1\n }\n const applied = applyAllowlist(generated.proposed_allowlist, { cwd })\n print(`Allowlist aplicado a ${applied.settings_path}`)\n print(`Backup: ${applied.backup_path}`)\n return 0\n }\n\n if (args.includes('--rollback')) {\n const toFlag = args.find((a) => a.startsWith('--to='))\n const to = toFlag ? toFlag.split('=')[1] : undefined\n const result = rollback({ cwd, to })\n if (result.restored) {\n print(`Restaurado desde ${result.from}`)\n return 0\n }\n print('No hay backups disponibles.')\n return 1\n }\n\n if (args.includes('--clear')) {\n const result = clearAllowlist({ cwd })\n print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar')\n return 0\n }\n\n if (args.includes('--impact')) {\n const result = impact({ cwd })\n if (result.snapshot_at === null) {\n print('No hay snapshots de allowlist todavia.')\n return 0\n }\n print(`Snapshot mas reciente: ${result.snapshot_at}`)\n print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? 'n/a'}`)\n print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? 'n/a'}`)\n if (result.percent !== null) {\n print(`Delta: ${(result.percent * 100).toFixed(1)}%`)\n }\n return 0\n }\n\n // Default: list registered MCPs with estimated cost\n const schema = measureCurrentSchemaBytes({ cwd })\n print(`MCPs registrados (${schema.mcp_servers.length}):`)\n for (const s of schema.mcp_servers) {\n print(` ${s}`)\n }\n print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`)\n print('')\n print('Flags:')\n print(' --generate-from-history [--days N] Propone allowlist (read-only)')\n print(' --apply Aplica el allowlist generado')\n print(' --rollback [--to TIMESTAMP] Restaura el ultimo backup')\n print(' --clear Elimina allowlist actual')\n print(' --impact Compara antes/despues del ultimo snapshot')\n return 0\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveXrayUrl | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FT4EJUIT.js"; | ||
| import { | ||
| resolveTranscriptPath | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/coach/knowledge-base.ts | ||
| var KNOWLEDGE_BASE = [ | ||
| { | ||
| id: "use-opusplan", | ||
| title: "Usa /opusplan para planificar con Opus y ejecutar con Sonnet", | ||
| description: "opusplan usa Opus durante plan mode para razonamiento complejo y vuelve a Sonnet para implementacion. Solo pagas Opus en la fase de planning.", | ||
| savings_estimate: "60-80% de reduccion de coste en sesiones con planning intensivo", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "/model opusplan", | ||
| when_applicable: "Sesiones con razonamiento largo antes de codigo", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-long-reasoning-no-code" | ||
| }, | ||
| { | ||
| id: "use-plan-mode", | ||
| title: "Activa plan mode para exploracion sin escribir codigo", | ||
| description: "EnterPlanMode permite razonar y explorar sin hacer ediciones, reduciendo iteraciones costosas.", | ||
| savings_estimate: "Variable segun tarea", | ||
| savings_source: "internal", | ||
| how_to_invoke: "EnterPlanMode tool", | ||
| when_applicable: "Tareas no triviales antes de escribir codigo", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-long-reasoning-no-code" | ||
| }, | ||
| { | ||
| id: "use-fast-mode", | ||
| title: "Activa /fast para respuestas mas directas", | ||
| description: "Modo rapido mantiene el modelo pero reduce el detalle de las respuestas.", | ||
| savings_estimate: "Reduce tiempo principalmente", | ||
| savings_source: "internal", | ||
| how_to_invoke: "/fast", | ||
| when_applicable: "Cuando quieres respuestas mas concisas", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "default-to-sonnet", | ||
| title: "Arranca cada sesion con Sonnet y sube a Opus solo cuando haga falta", | ||
| description: "Sonnet resuelve ~80% de tareas de coding bien. El switching tactico a Opus solo en razonamiento complejo ahorra el grueso del coste.", | ||
| savings_estimate: "60-80% reduccion de coste total", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "/model sonnet (inicio) \u2192 /model opus (cuando sea necesario)", | ||
| when_applicable: "Siempre como default", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-opus-for-simple-task" | ||
| }, | ||
| { | ||
| id: "use-haiku-for-simple", | ||
| title: "Usa Haiku para formato, Q&A simple y tareas de alto volumen", | ||
| description: "Haiku es mucho mas barato y rapido. Para formateo, preguntas puntuales o tareas repetitivas es el modelo adecuado.", | ||
| savings_estimate: "~90% reduccion vs Opus en tareas simples", | ||
| savings_source: "anthropic-docs", | ||
| how_to_invoke: "/model haiku", | ||
| when_applicable: "Formateo, Q&A simple, alto volumen", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-opus-for-simple-task" | ||
| }, | ||
| { | ||
| id: "use-compact-long-session", | ||
| title: "Corre /compact cuando el contexto supere el 75%", | ||
| description: "/compact genera un resumen del contexto actual liberando ~60-80% de la ventana sin perder continuidad.", | ||
| savings_estimate: "60-80% de contexto liberado", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "/compact", | ||
| when_applicable: "Contexto > 75% de la ventana", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-context-threshold" | ||
| }, | ||
| { | ||
| id: "use-clear-rename-resume", | ||
| title: "Usa /rename \u2192 /clear \u2192 /resume para pivotes de tema", | ||
| description: "Al cambiar a un tema no relacionado, renombra la sesion, haz /clear para empezar limpio, y resume cuando vuelvas.", | ||
| savings_estimate: "Variable segun contexto descartado", | ||
| savings_source: "internal", | ||
| how_to_invoke: "/rename <nombre> \u2192 /clear \u2192 (trabajar) \u2192 /resume <nombre>", | ||
| when_applicable: "Pivote total a tema no relacionado", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-clear-opportunity" | ||
| }, | ||
| { | ||
| id: "use-sessionstart-compact-hook", | ||
| title: "Activa el hook SessionStart:compact de token-optimizer", | ||
| description: "Cuando Claude Code compacta el contexto, token-optimizer inyecta un resumen con archivos, comandos y presupuesto.", | ||
| savings_estimate: "Evita re-lectura tras compactacion", | ||
| savings_source: "internal", | ||
| how_to_invoke: "token-optimizer-mcp install (ya lo configura)", | ||
| when_applicable: "Siempre como parte del install", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-memory-save", | ||
| title: "Guarda decisiones con mem_save antes de compactar", | ||
| description: "Persistir decisiones arquitectonicas en engram evita tener que re-derivarlas cuando el contexto se compacta.", | ||
| savings_estimate: "Variable", | ||
| savings_source: "internal", | ||
| how_to_invoke: "mem_save (via engram MCP)", | ||
| when_applicable: "Antes de /compact o cambiar de sesion", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-agent-explore", | ||
| title: "Delega busquedas amplias al subagente Explore", | ||
| description: "El subagente Explore tiene su propio contexto y no consume el de la sesion principal. Ideal para buscar en muchos archivos.", | ||
| savings_estimate: "Aisla contexto al subagente", | ||
| savings_source: "internal", | ||
| how_to_invoke: 'Agent tool con subagent_type="Explore"', | ||
| when_applicable: "3+ busquedas Grep/Glob similares", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-repeated-searches" | ||
| }, | ||
| { | ||
| id: "use-todowrite-long-task", | ||
| title: "Usa TodoWrite para tareas multi-paso", | ||
| description: "TodoWrite mantiene el estado de la tarea sin re-leer archivos, reduciendo redundancia.", | ||
| savings_estimate: "Evita re-lectura de estado", | ||
| savings_source: "internal", | ||
| how_to_invoke: "TodoWrite", | ||
| when_applicable: "3+ pasos independientes", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-skill-trigger", | ||
| title: "Invoca skills en lugar de re-derivar instrucciones", | ||
| description: "Los skills cargan instrucciones especializadas solo cuando se invocan. Mejor que un CLAUDE.md monolitico.", | ||
| savings_estimate: "~15k tokens/sesion con progressive disclosure", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "Skill tool con nombre del skill", | ||
| when_applicable: "Tareas que matchean un skill disponible", | ||
| source_type: "skill", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-skill-trigger-ignored" | ||
| }, | ||
| { | ||
| id: "install-serena", | ||
| title: "Instala serena-mcp para lecturas simbolicas", | ||
| description: "serena usa LSP para leer solo los simbolos que necesitas en lugar del archivo completo. Nota: incluye execute_shell_command.", | ||
| savings_estimate: "20-30% en lecturas de archivos grandes", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "uvx --from git+https://github.com/oraios/serena serena start-mcp-server", | ||
| when_applicable: "Proyectos con archivos >50k tokens", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-huge-file-reads" | ||
| }, | ||
| { | ||
| id: "prefer-serena-reads", | ||
| title: "Usa Serena en vez de Read para archivos de codigo", | ||
| description: "Serena lee simbolos (funciones, clases) sin cargar el archivo completo. Usa get_symbols_overview para explorar y find_symbol con include_body para leer solo lo que necesitas. Ahorro tipico: 60-90% vs Read.", | ||
| savings_estimate: "60-90% en lecturas de codigo", | ||
| savings_source: "internal", | ||
| how_to_invoke: "get_symbols_overview(path) \u2192 find_symbol(name, include_body=true)", | ||
| when_applicable: "Archivos .ts/.js/.py/.java >50 lineas donde solo necesitas 1-2 funciones", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-12", | ||
| detector_id: "detect-read-over-serena" | ||
| }, | ||
| { | ||
| id: "install-rtk", | ||
| title: "Instala RTK para filtrar salida ruidosa de Bash", | ||
| description: "RTK filtra output de builds/tests antes de llegar a Claude Code. Publica releases firmadas con GPG.", | ||
| savings_estimate: "15-25% en ciclos build/test", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "brew install standard-input/tap/rtk (macOS) o binario firmado en github.com/standard-input/rtk", | ||
| when_applicable: "Proyectos con builds/tests ruidosos", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-many-bash-commands" | ||
| }, | ||
| { | ||
| id: "use-mcp-prune", | ||
| title: "Aplica un allowlist de MCPs por proyecto", | ||
| description: "Reduce el coste del tool-schema excluyendo MCPs que no usas en este proyecto. ~5-12% adicional sobre Tool Search.", | ||
| savings_estimate: "5-12% por turno sobre Tool Search nativo", | ||
| savings_source: "internal", | ||
| how_to_invoke: "mcp_prune_suggest \u2192 mcp_prune_apply", | ||
| when_applicable: "MCPs registrados pero no usados en el proyecto", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-unused-mcp-servers" | ||
| }, | ||
| { | ||
| id: "migrate-claudemd-to-skills", | ||
| title: "Migra CLAUDE.md grande a skills con progressive disclosure", | ||
| description: "Un CLAUDE.md monolitico se carga en cada sesion. Los skills solo cargan cuando se invocan. ~15k tokens recuperados.", | ||
| savings_estimate: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "Crear skills en .claude/skills/ con triggers especificos", | ||
| when_applicable: "CLAUDE.md > 10k tokens con uso parcial", | ||
| source_type: "skill", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-claudemd-bloat" | ||
| }, | ||
| { | ||
| id: "use-settings-local", | ||
| title: "Configuracion personal en settings.local.json", | ||
| description: "Evita contaminar settings.json del equipo. settings.local.json es personal y gitignored por defecto.", | ||
| savings_estimate: "Higiene, no tokens", | ||
| savings_source: "internal", | ||
| how_to_invoke: "Editar .claude/settings.local.json", | ||
| when_applicable: "Configuracion personal no compartible", | ||
| source_type: "settings", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-serena-overview-first", | ||
| title: "Usa get_symbols_overview antes de find_symbol", | ||
| description: "Llamar get_symbols_overview una vez da el mapa del archivo. Las llamadas sucesivas find_symbol sin overview previo leen el mismo archivo repetidamente.", | ||
| savings_estimate: "30-50% menos llamadas Serena por sesion", | ||
| savings_source: "internal", | ||
| how_to_invoke: "mcp__serena__get_symbols_overview con relative_path antes de find_symbol", | ||
| when_applicable: "Al explorar un archivo por primera vez en la sesion", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-15", | ||
| detector_id: "detect-serena-read-cascade" | ||
| }, | ||
| { | ||
| id: "use-prompt-caching", | ||
| title: "Estructura prompts para maximizar cache hits", | ||
| description: "Los tokens leidos del cache cuestan 10x menos. Mantener el prefijo estable (system, CLAUDE.md) aprovecha el cache.", | ||
| savings_estimate: "10x mas barato en reads cacheados", | ||
| savings_source: "anthropic-docs", | ||
| how_to_invoke: "Mantener prefijo estable entre turns", | ||
| when_applicable: "Siempre", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| } | ||
| ]; | ||
| // src/coach/rules.ts | ||
| function countMatching(events, predicate) { | ||
| let c = 0; | ||
| for (const e of events) if (predicate(e)) c++; | ||
| return c; | ||
| } | ||
| var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]); | ||
| var DETECTION_RULES = [ | ||
| // 1. detect-context-threshold | ||
| { | ||
| id: "detect-context-threshold", | ||
| tip_ids: ["use-compact-long-session"], | ||
| run(ctx) { | ||
| if (ctx.session_token_total === null || ctx.session_token_limit <= 0) return null; | ||
| const percent = ctx.session_token_total / ctx.session_token_limit; | ||
| if (percent < 0.5) return null; | ||
| let severity = "info"; | ||
| if (percent >= 0.9) severity = "critical"; | ||
| else if (percent >= 0.75) severity = "warn"; | ||
| return { | ||
| rule_id: "detect-context-threshold", | ||
| tip_ids: ["use-compact-long-session"], | ||
| severity, | ||
| evidence: `Contexto: ${(percent * 100).toFixed(1)}% usado (${ctx.session_token_total}/${ctx.session_token_limit} tokens)`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| }, | ||
| // 2. detect-long-reasoning-no-code | ||
| { | ||
| id: "detect-long-reasoning-no-code", | ||
| tip_ids: ["use-plan-mode", "use-opusplan"], | ||
| run(ctx) { | ||
| const recent = ctx.events.slice(0, 10); | ||
| if (recent.length < 10) return null; | ||
| const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name)); | ||
| if (edits > 0) return null; | ||
| return { | ||
| rule_id: "detect-long-reasoning-no-code", | ||
| tip_ids: ["use-plan-mode", "use-opusplan"], | ||
| severity: "info", | ||
| evidence: "10 eventos recientes sin ediciones de codigo", | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 3. detect-repeated-searches | ||
| { | ||
| id: "detect-repeated-searches", | ||
| tip_ids: ["use-agent-explore"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 20); | ||
| const searches = countMatching(window, (e) => e.tool_name === "Grep" || e.tool_name === "Glob"); | ||
| if (searches < 3) return null; | ||
| return { | ||
| rule_id: "detect-repeated-searches", | ||
| tip_ids: ["use-agent-explore"], | ||
| severity: "info", | ||
| evidence: `${searches} busquedas Grep/Glob en los ultimos 20 eventos`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 4. detect-huge-file-reads | ||
| { | ||
| id: "detect-huge-file-reads", | ||
| tip_ids: ["install-serena"], | ||
| run(ctx) { | ||
| const huge = ctx.events.find((e) => e.tool_name === "Read" && e.tokens_estimated > 5e4); | ||
| if (!huge) return null; | ||
| return { | ||
| rule_id: "detect-huge-file-reads", | ||
| tip_ids: ["install-serena"], | ||
| severity: "warn", | ||
| evidence: `Read consumio ${huge.tokens_estimated} tokens (umbral 50k)`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 5. detect-many-bash-commands | ||
| { | ||
| id: "detect-many-bash-commands", | ||
| tip_ids: ["install-rtk"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 100); | ||
| const bash = countMatching(window, (e) => e.tool_name === "Bash"); | ||
| if (bash <= 10) return null; | ||
| return { | ||
| rule_id: "detect-many-bash-commands", | ||
| tip_ids: ["install-rtk"], | ||
| severity: "info", | ||
| evidence: `${bash} comandos Bash en los ultimos ${window.length} eventos`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 6. detect-clear-opportunity (was #7 — detect-unused-mcp-servers stub removed) | ||
| { | ||
| id: "detect-clear-opportunity", | ||
| tip_ids: ["use-clear-rename-resume"], | ||
| run(ctx) { | ||
| if (ctx.events.length < 40) return null; | ||
| const recentTools = new Set(ctx.events.slice(0, 20).map((e) => e.tool_name)); | ||
| const priorTools = new Set(ctx.events.slice(20, 40).map((e) => e.tool_name)); | ||
| if (recentTools.size === 0) return null; | ||
| let overlap = 0; | ||
| for (const t of recentTools) if (priorTools.has(t)) overlap++; | ||
| const ratio = overlap / recentTools.size; | ||
| if (ratio >= 0.3) return null; | ||
| return { | ||
| rule_id: "detect-clear-opportunity", | ||
| tip_ids: ["use-clear-rename-resume"], | ||
| severity: "info", | ||
| evidence: `Solapamiento de herramientas ${(ratio * 100).toFixed(0)}% \u2014 posible pivote de tema`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 8. detect-opus-for-simple-task | ||
| { | ||
| id: "detect-opus-for-simple-task", | ||
| tip_ids: ["default-to-sonnet", "use-haiku-for-simple"], | ||
| run(ctx) { | ||
| if (!ctx.active_model || !/opus/i.test(ctx.active_model)) return null; | ||
| const recent = ctx.events.slice(0, 20); | ||
| if (recent.length < 6) return null; | ||
| const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name)); | ||
| const bash = countMatching(recent, (e) => e.tool_name === "Bash"); | ||
| if (edits + bash < 6) return null; | ||
| return { | ||
| rule_id: "detect-opus-for-simple-task", | ||
| tip_ids: ["default-to-sonnet", "use-haiku-for-simple"], | ||
| severity: "info", | ||
| evidence: `Opus ejecutando trabajo mecanico: ${edits} edits + ${bash} Bash en ultimos 20 eventos. Sonnet haria lo mismo un 80% mas barato.`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 9. detect-claudemd-bloat (stub — requires filesystem stat at runtime) | ||
| { | ||
| id: "detect-claudemd-bloat", | ||
| tip_ids: ["migrate-claudemd-to-skills"], | ||
| run() { | ||
| return null; | ||
| } | ||
| }, | ||
| // 10. detect-post-milestone-opportunity | ||
| { | ||
| id: "detect-post-milestone-opportunity", | ||
| tip_ids: ["use-compact-long-session"], | ||
| run(ctx) { | ||
| const recent = ctx.events.slice(0, 20); | ||
| const edits = countMatching(recent, (e) => e.tool_name === "Edit" || e.tool_name === "Write"); | ||
| const hasBash = countMatching(recent, (e) => e.tool_name === "Bash") > 0; | ||
| if (edits < 5 || !hasBash) return null; | ||
| if (ctx.session_token_total === null) return null; | ||
| const percent = ctx.session_token_total / ctx.session_token_limit; | ||
| if (percent < 0.4) return null; | ||
| return { | ||
| rule_id: "detect-post-milestone-opportunity", | ||
| tip_ids: ["use-compact-long-session"], | ||
| severity: "info", | ||
| evidence: `${edits} ediciones + Bash reciente + contexto ${(percent * 100).toFixed(0)}%`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| }, | ||
| // 11. detect-skill-trigger-ignored (stub — requires skill registry) | ||
| { | ||
| id: "detect-skill-trigger-ignored", | ||
| tip_ids: ["use-skill-trigger"], | ||
| run() { | ||
| return null; | ||
| } | ||
| }, | ||
| // 12. detect-serena-read-cascade | ||
| // Fires when the agent makes ≥5 find_symbol calls without a get_symbols_overview | ||
| // in the same window — suggests starting with an overview first. | ||
| { | ||
| id: "detect-serena-read-cascade", | ||
| tip_ids: ["use-serena-overview-first"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 15); | ||
| const findSymbolCount = countMatching( | ||
| window, | ||
| (e) => e.tool_name === "mcp__serena__find_symbol" | ||
| ); | ||
| if (findSymbolCount < 5) return null; | ||
| const hasOverview = window.some( | ||
| (e) => e.tool_name === "mcp__serena__get_symbols_overview" | ||
| ); | ||
| if (hasOverview) return null; | ||
| return { | ||
| rule_id: "detect-serena-read-cascade", | ||
| tip_ids: ["use-serena-overview-first"], | ||
| severity: "info", | ||
| evidence: `${findSymbolCount} llamadas find_symbol sin get_symbols_overview en los ultimos 15 eventos.`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| }, | ||
| // 13. detect-read-over-serena | ||
| { | ||
| id: "detect-read-over-serena", | ||
| tip_ids: ["prefer-serena-reads"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 30); | ||
| const largeReads = window.filter( | ||
| (e) => e.tool_name === "Read" && e.tokens_estimated > 2e3 | ||
| ); | ||
| if (largeReads.length < 3) return null; | ||
| const totalTokens = largeReads.reduce((sum, e) => sum + e.tokens_estimated, 0); | ||
| const estimatedSaving = Math.round(totalTokens * 0.7); | ||
| const severity = largeReads.length >= 6 ? "warn" : "info"; | ||
| return { | ||
| rule_id: "detect-read-over-serena", | ||
| tip_ids: ["prefer-serena-reads"], | ||
| severity, | ||
| evidence: `${largeReads.length} lecturas Read >2k tokens (total: ${totalTokens}). Serena ahorraria ~${estimatedSaving} tokens (~70%).`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| } | ||
| ]; | ||
| // src/coach/detector.ts | ||
| var SEVERITY_ORDER = { critical: 0, warn: 1, info: 2 }; | ||
| function runRules(ctx) { | ||
| const hits = []; | ||
| for (const rule of DETECTION_RULES) { | ||
| try { | ||
| const hit = rule.run(ctx); | ||
| if (hit) hits.push(hit); | ||
| } catch { | ||
| } | ||
| } | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| const unique = []; | ||
| for (const h of hits) { | ||
| if (seen.has(h.rule_id)) continue; | ||
| seen.add(h.rule_id); | ||
| unique.push(h); | ||
| } | ||
| unique.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99)); | ||
| return unique; | ||
| } | ||
| // src/coach/context-meter.ts | ||
| import fs from "fs"; | ||
| // src/services/xray-client.ts | ||
| var POST_TIMEOUT_MS = 500; | ||
| var GET_TIMEOUT_MS = 300; | ||
| var DEFAULT_LIMIT = 2e5; | ||
| var VERSION = true ? "0.4.20" : "0.1.0"; | ||
| async function postToXray(event, opts = {}) { | ||
| const xrayUrl = opts.xrayUrl ?? resolveXrayUrl(); | ||
| if (!xrayUrl) return false; | ||
| const fetchFn = opts.fetchImpl ?? fetch; | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? POST_TIMEOUT_MS); | ||
| try { | ||
| await fetchFn(`${xrayUrl}/hooks/token-optimizer`, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| source: "token-optimizer-mcp", | ||
| version: VERSION, | ||
| event | ||
| }), | ||
| signal: controller.signal | ||
| }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } | ||
| var SUMMARY_TIMEOUT_MS = 2e3; | ||
| async function postSummaryToXray(summary, opts = {}) { | ||
| const xrayUrl = opts.xrayUrl ?? resolveXrayUrl(); | ||
| if (!xrayUrl) return false; | ||
| const fetchFn = opts.fetchImpl ?? fetch; | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUMMARY_TIMEOUT_MS); | ||
| try { | ||
| await fetchFn(`${xrayUrl}/hooks/token-optimizer/summary`, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| source: "token-optimizer-mcp", | ||
| version: VERSION, | ||
| summary | ||
| }), | ||
| signal: controller.signal | ||
| }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } | ||
| async function getSessionTokens(sessionId, opts = {}) { | ||
| const xrayUrl = opts.xrayUrl ?? resolveXrayUrl(); | ||
| if (!xrayUrl) return null; | ||
| const fetchFn = opts.fetchImpl ?? fetch; | ||
| const controller = new AbortController(); | ||
| const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? GET_TIMEOUT_MS); | ||
| try { | ||
| const res = await fetchFn( | ||
| `${xrayUrl}/sessions/${encodeURIComponent(sessionId)}/tokens`, | ||
| { signal: controller.signal } | ||
| ); | ||
| if (!res.ok) return null; | ||
| const data = await res.json(); | ||
| const tokens = data.tokens ?? 0; | ||
| const limit = data.limit ?? DEFAULT_LIMIT; | ||
| return { | ||
| tokens, | ||
| limit, | ||
| percent: limit > 0 ? tokens / limit : 0, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } finally { | ||
| clearTimeout(timer); | ||
| } | ||
| } | ||
| // src/coach/context-meter.ts | ||
| var DEFAULT_LIMIT2 = 2e5; | ||
| var OPUS_1M_LIMIT = 1e6; | ||
| var BASELINE_TOKENS = 15e3; | ||
| async function measureContextSize(sessionId, opts = {}) { | ||
| if (opts.projectDir) { | ||
| const transcript = readTranscript(opts.projectDir, sessionId); | ||
| if (transcript) return transcript; | ||
| } | ||
| const xrayResult = await tryXray(sessionId, opts.fetchImpl); | ||
| if (xrayResult) return xrayResult; | ||
| const limit = resolveLimit(opts.activeModel); | ||
| if (opts.db) { | ||
| return cumulativeEstimate(opts.db, sessionId, limit); | ||
| } | ||
| return { tokens: 0, limit, percent: 0, estimation_method: "unknown" }; | ||
| } | ||
| function readTranscript(projectDir, sessionId) { | ||
| try { | ||
| const p = resolveTranscriptPath(projectDir, sessionId); | ||
| if (!fs.existsSync(p)) return null; | ||
| const content = fs.readFileSync(p, "utf8"); | ||
| const lines = content.split("\n").filter((l) => l.trim().length > 0); | ||
| let totalTokens = 0; | ||
| let limit = DEFAULT_LIMIT2; | ||
| for (const line of lines) { | ||
| try { | ||
| const turn = JSON.parse(line); | ||
| if (turn.usage) { | ||
| totalTokens += (turn.usage.input_tokens ?? 0) + (turn.usage.output_tokens ?? 0) + (turn.usage.cache_read_input_tokens ?? 0); | ||
| } | ||
| if (turn.model && /1m/i.test(turn.model)) limit = OPUS_1M_LIMIT; | ||
| } catch { | ||
| } | ||
| } | ||
| return { | ||
| tokens: totalTokens, | ||
| limit, | ||
| percent: limit > 0 ? totalTokens / limit : 0, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| async function tryXray(sessionId, fetchImpl) { | ||
| const opts = {}; | ||
| if (fetchImpl !== void 0) opts.fetchImpl = fetchImpl; | ||
| return getSessionTokens(sessionId, opts); | ||
| } | ||
| function resolveLimit(activeModel) { | ||
| if (activeModel && /1m|opus/i.test(activeModel)) return OPUS_1M_LIMIT; | ||
| return DEFAULT_LIMIT2; | ||
| } | ||
| function cumulativeEstimate(db, sessionId, limit = DEFAULT_LIMIT2) { | ||
| const queries = buildQueries(db); | ||
| const sessionTokens = queries.sumTokensBySession(sessionId); | ||
| const total = sessionTokens + BASELINE_TOKENS; | ||
| return { | ||
| tokens: total, | ||
| limit, | ||
| percent: total / limit, | ||
| estimation_method: "estimated_cumulative" | ||
| }; | ||
| } | ||
| function measureContextSizeFromDbSync(db, sessionId, activeModel) { | ||
| const limit = resolveLimit(activeModel); | ||
| return cumulativeEstimate(db, sessionId, limit); | ||
| } | ||
| // src/coach/surface.ts | ||
| function checkDedupe(db, sessionId, ruleId, tipId, windowSeconds) { | ||
| const row = db.prepare( | ||
| `SELECT 1 FROM coach_surface_log | ||
| WHERE session_id = ? AND rule_id = ? AND tip_id = ? | ||
| AND created_at > datetime('now', ?) | ||
| LIMIT 1` | ||
| ).get(sessionId, ruleId, tipId, `-${windowSeconds} seconds`); | ||
| return row !== void 0 && row !== null; | ||
| } | ||
| function logSurface(db, sessionId, hit, via) { | ||
| db.prepare(`INSERT OR IGNORE INTO sessions (id) VALUES (?)`).run(sessionId); | ||
| const stmt = db.prepare( | ||
| `INSERT INTO coach_surface_log (session_id, rule_id, tip_id, surfaced_via, severity) | ||
| VALUES (?, ?, ?, ?, ?)` | ||
| ); | ||
| for (const tipId of hit.tip_ids) { | ||
| stmt.run(sessionId, hit.rule_id, tipId, via, hit.severity); | ||
| } | ||
| } | ||
| function surfaceWithDedupe(db, sessionId, hits, via, windowSeconds) { | ||
| const surfaced = []; | ||
| for (const hit of hits) { | ||
| const anyFresh = hit.tip_ids.some( | ||
| (tipId) => !checkDedupe(db, sessionId, hit.rule_id, tipId, windowSeconds) | ||
| ); | ||
| if (anyFresh) { | ||
| logSurface(db, sessionId, hit, via); | ||
| surfaced.push(hit); | ||
| } | ||
| } | ||
| return surfaced; | ||
| } | ||
| function getCoachSurfaceLog(db, sessionId) { | ||
| const rows = db.prepare( | ||
| `SELECT rule_id, tip_id, severity | ||
| FROM coach_surface_log | ||
| WHERE session_id = ? | ||
| ORDER BY created_at` | ||
| ).all(sessionId); | ||
| const map = /* @__PURE__ */ new Map(); | ||
| for (const row of rows) { | ||
| if (!map.has(row.rule_id)) { | ||
| map.set(row.rule_id, { tip_ids: [], severity: row.severity }); | ||
| } | ||
| const entry = map.get(row.rule_id); | ||
| if (!entry.tip_ids.includes(row.tip_id)) { | ||
| entry.tip_ids.push(row.tip_id); | ||
| } | ||
| } | ||
| return Array.from(map.entries()).map(([rule_id, v]) => ({ | ||
| rule_id, | ||
| tip_ids: v.tip_ids, | ||
| severity: v.severity | ||
| })); | ||
| } | ||
| function clearSurfaceLog(db, sessionId) { | ||
| if (sessionId) { | ||
| const info2 = db.prepare(`DELETE FROM coach_surface_log WHERE session_id = ?`).run(sessionId); | ||
| return info2.changes; | ||
| } | ||
| const info = db.prepare(`DELETE FROM coach_surface_log`).run(); | ||
| return info.changes; | ||
| } | ||
| export { | ||
| postToXray, | ||
| postSummaryToXray, | ||
| KNOWLEDGE_BASE, | ||
| runRules, | ||
| measureContextSize, | ||
| measureContextSizeFromDbSync, | ||
| surfaceWithDedupe, | ||
| getCoachSurfaceLog, | ||
| clearSurfaceLog | ||
| }; | ||
| //# sourceMappingURL=chunk-4PA7MRTS.js.map |
| {"version":3,"sources":["../src/coach/knowledge-base.ts","../src/coach/rules.ts","../src/coach/detector.ts","../src/coach/context-meter.ts","../src/services/xray-client.ts","../src/coach/surface.ts"],"sourcesContent":["// Static catalog of 18 CoachTip entries — Phase 4.40\n// See sdd/token-optimizer-v0.1/coach-layer-addendum CO-1\n\nimport type { CoachTip } from '../lib/types.js'\n\nexport const KNOWLEDGE_BASE: readonly CoachTip[] = [\n {\n id: 'use-opusplan',\n title: 'Usa /opusplan para planificar con Opus y ejecutar con Sonnet',\n description:\n 'opusplan usa Opus durante plan mode para razonamiento complejo y vuelve a Sonnet para implementacion. Solo pagas Opus en la fase de planning.',\n savings_estimate: '60-80% de reduccion de coste en sesiones con planning intensivo',\n savings_source: 'community-measured',\n how_to_invoke: '/model opusplan',\n when_applicable: 'Sesiones con razonamiento largo antes de codigo',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-long-reasoning-no-code',\n },\n {\n id: 'use-plan-mode',\n title: 'Activa plan mode para exploracion sin escribir codigo',\n description:\n 'EnterPlanMode permite razonar y explorar sin hacer ediciones, reduciendo iteraciones costosas.',\n savings_estimate: 'Variable segun tarea',\n savings_source: 'internal',\n how_to_invoke: 'EnterPlanMode tool',\n when_applicable: 'Tareas no triviales antes de escribir codigo',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-long-reasoning-no-code',\n },\n {\n id: 'use-fast-mode',\n title: 'Activa /fast para respuestas mas directas',\n description: 'Modo rapido mantiene el modelo pero reduce el detalle de las respuestas.',\n savings_estimate: 'Reduce tiempo principalmente',\n savings_source: 'internal',\n how_to_invoke: '/fast',\n when_applicable: 'Cuando quieres respuestas mas concisas',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'default-to-sonnet',\n title: 'Arranca cada sesion con Sonnet y sube a Opus solo cuando haga falta',\n description:\n 'Sonnet resuelve ~80% de tareas de coding bien. El switching tactico a Opus solo en razonamiento complejo ahorra el grueso del coste.',\n savings_estimate: '60-80% reduccion de coste total',\n savings_source: 'community-measured',\n how_to_invoke: '/model sonnet (inicio) → /model opus (cuando sea necesario)',\n when_applicable: 'Siempre como default',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-opus-for-simple-task',\n },\n {\n id: 'use-haiku-for-simple',\n title: 'Usa Haiku para formato, Q&A simple y tareas de alto volumen',\n description:\n 'Haiku es mucho mas barato y rapido. Para formateo, preguntas puntuales o tareas repetitivas es el modelo adecuado.',\n savings_estimate: '~90% reduccion vs Opus en tareas simples',\n savings_source: 'anthropic-docs',\n how_to_invoke: '/model haiku',\n when_applicable: 'Formateo, Q&A simple, alto volumen',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-opus-for-simple-task',\n },\n {\n id: 'use-compact-long-session',\n title: 'Corre /compact cuando el contexto supere el 75%',\n description:\n '/compact genera un resumen del contexto actual liberando ~60-80% de la ventana sin perder continuidad.',\n savings_estimate: '60-80% de contexto liberado',\n savings_source: 'community-measured',\n how_to_invoke: '/compact',\n when_applicable: 'Contexto > 75% de la ventana',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-context-threshold',\n },\n {\n id: 'use-clear-rename-resume',\n title: 'Usa /rename → /clear → /resume para pivotes de tema',\n description:\n 'Al cambiar a un tema no relacionado, renombra la sesion, haz /clear para empezar limpio, y resume cuando vuelvas.',\n savings_estimate: 'Variable segun contexto descartado',\n savings_source: 'internal',\n how_to_invoke: '/rename <nombre> → /clear → (trabajar) → /resume <nombre>',\n when_applicable: 'Pivote total a tema no relacionado',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-clear-opportunity',\n },\n {\n id: 'use-sessionstart-compact-hook',\n title: 'Activa el hook SessionStart:compact de token-optimizer',\n description:\n 'Cuando Claude Code compacta el contexto, token-optimizer inyecta un resumen con archivos, comandos y presupuesto.',\n savings_estimate: 'Evita re-lectura tras compactacion',\n savings_source: 'internal',\n how_to_invoke: 'token-optimizer-mcp install (ya lo configura)',\n when_applicable: 'Siempre como parte del install',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-memory-save',\n title: 'Guarda decisiones con mem_save antes de compactar',\n description:\n 'Persistir decisiones arquitectonicas en engram evita tener que re-derivarlas cuando el contexto se compacta.',\n savings_estimate: 'Variable',\n savings_source: 'internal',\n how_to_invoke: 'mem_save (via engram MCP)',\n when_applicable: 'Antes de /compact o cambiar de sesion',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-agent-explore',\n title: 'Delega busquedas amplias al subagente Explore',\n description:\n 'El subagente Explore tiene su propio contexto y no consume el de la sesion principal. Ideal para buscar en muchos archivos.',\n savings_estimate: 'Aisla contexto al subagente',\n savings_source: 'internal',\n how_to_invoke: 'Agent tool con subagent_type=\"Explore\"',\n when_applicable: '3+ busquedas Grep/Glob similares',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-repeated-searches',\n },\n {\n id: 'use-todowrite-long-task',\n title: 'Usa TodoWrite para tareas multi-paso',\n description:\n 'TodoWrite mantiene el estado de la tarea sin re-leer archivos, reduciendo redundancia.',\n savings_estimate: 'Evita re-lectura de estado',\n savings_source: 'internal',\n how_to_invoke: 'TodoWrite',\n when_applicable: '3+ pasos independientes',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-skill-trigger',\n title: 'Invoca skills en lugar de re-derivar instrucciones',\n description:\n 'Los skills cargan instrucciones especializadas solo cuando se invocan. Mejor que un CLAUDE.md monolitico.',\n savings_estimate: '~15k tokens/sesion con progressive disclosure',\n savings_source: 'community-measured',\n how_to_invoke: 'Skill tool con nombre del skill',\n when_applicable: 'Tareas que matchean un skill disponible',\n source_type: 'skill',\n verified_at: '2026-04-11',\n detector_id: 'detect-skill-trigger-ignored',\n },\n {\n id: 'install-serena',\n title: 'Instala serena-mcp para lecturas simbolicas',\n description:\n 'serena usa LSP para leer solo los simbolos que necesitas en lugar del archivo completo. Nota: incluye execute_shell_command.',\n savings_estimate: '20-30% en lecturas de archivos grandes',\n savings_source: 'community-measured',\n how_to_invoke: 'uvx --from git+https://github.com/oraios/serena serena start-mcp-server',\n when_applicable: 'Proyectos con archivos >50k tokens',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: 'detect-huge-file-reads',\n },\n {\n id: 'prefer-serena-reads',\n title: 'Usa Serena en vez de Read para archivos de codigo',\n description:\n 'Serena lee simbolos (funciones, clases) sin cargar el archivo completo. Usa get_symbols_overview para explorar y find_symbol con include_body para leer solo lo que necesitas. Ahorro tipico: 60-90% vs Read.',\n savings_estimate: '60-90% en lecturas de codigo',\n savings_source: 'internal',\n how_to_invoke: 'get_symbols_overview(path) → find_symbol(name, include_body=true)',\n when_applicable: 'Archivos .ts/.js/.py/.java >50 lineas donde solo necesitas 1-2 funciones',\n source_type: 'mcp',\n verified_at: '2026-04-12',\n detector_id: 'detect-read-over-serena',\n },\n {\n id: 'install-rtk',\n title: 'Instala RTK para filtrar salida ruidosa de Bash',\n description:\n 'RTK filtra output de builds/tests antes de llegar a Claude Code. Publica releases firmadas con GPG.',\n savings_estimate: '15-25% en ciclos build/test',\n savings_source: 'community-measured',\n how_to_invoke: 'brew install standard-input/tap/rtk (macOS) o binario firmado en github.com/standard-input/rtk',\n when_applicable: 'Proyectos con builds/tests ruidosos',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: 'detect-many-bash-commands',\n },\n {\n id: 'use-mcp-prune',\n title: 'Aplica un allowlist de MCPs por proyecto',\n description:\n 'Reduce el coste del tool-schema excluyendo MCPs que no usas en este proyecto. ~5-12% adicional sobre Tool Search.',\n savings_estimate: '5-12% por turno sobre Tool Search nativo',\n savings_source: 'internal',\n how_to_invoke: 'mcp_prune_suggest → mcp_prune_apply',\n when_applicable: 'MCPs registrados pero no usados en el proyecto',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: 'detect-unused-mcp-servers',\n },\n {\n id: 'migrate-claudemd-to-skills',\n title: 'Migra CLAUDE.md grande a skills con progressive disclosure',\n description:\n 'Un CLAUDE.md monolitico se carga en cada sesion. Los skills solo cargan cuando se invocan. ~15k tokens recuperados.',\n savings_estimate: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n savings_source: 'community-measured',\n how_to_invoke: 'Crear skills en .claude/skills/ con triggers especificos',\n when_applicable: 'CLAUDE.md > 10k tokens con uso parcial',\n source_type: 'skill',\n verified_at: '2026-04-11',\n detector_id: 'detect-claudemd-bloat',\n },\n {\n id: 'use-settings-local',\n title: 'Configuracion personal en settings.local.json',\n description:\n 'Evita contaminar settings.json del equipo. settings.local.json es personal y gitignored por defecto.',\n savings_estimate: 'Higiene, no tokens',\n savings_source: 'internal',\n how_to_invoke: 'Editar .claude/settings.local.json',\n when_applicable: 'Configuracion personal no compartible',\n source_type: 'settings',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-serena-overview-first',\n title: 'Usa get_symbols_overview antes de find_symbol',\n description:\n 'Llamar get_symbols_overview una vez da el mapa del archivo. Las llamadas sucesivas find_symbol sin overview previo leen el mismo archivo repetidamente.',\n savings_estimate: '30-50% menos llamadas Serena por sesion',\n savings_source: 'internal',\n how_to_invoke: 'mcp__serena__get_symbols_overview con relative_path antes de find_symbol',\n when_applicable: 'Al explorar un archivo por primera vez en la sesion',\n source_type: 'mcp',\n verified_at: '2026-04-15',\n detector_id: 'detect-serena-read-cascade',\n },\n {\n id: 'use-prompt-caching',\n title: 'Estructura prompts para maximizar cache hits',\n description:\n 'Los tokens leidos del cache cuestan 10x menos. Mantener el prefijo estable (system, CLAUDE.md) aprovecha el cache.',\n savings_estimate: '10x mas barato en reads cacheados',\n savings_source: 'anthropic-docs',\n how_to_invoke: 'Mantener prefijo estable entre turns',\n when_applicable: 'Siempre',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n]\n","// Detection rules registry (11 rules) — Phase 4.43\n// Each rule is a pure function over EventContext returning DetectionHit | null.\n// Rules MUST NOT throw; the orchestrator catches everything.\n\nimport type { DetectionRule, DetectionSeverity, ToolEvent } from '../lib/types.js'\n\nfunction countMatching(events: readonly ToolEvent[], predicate: (e: ToolEvent) => boolean): number {\n let c = 0\n for (const e of events) if (predicate(e)) c++\n return c\n}\n\nconst EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])\n\nexport const DETECTION_RULES: readonly DetectionRule[] = [\n // 1. detect-context-threshold\n {\n id: 'detect-context-threshold',\n tip_ids: ['use-compact-long-session'],\n run(ctx) {\n if (ctx.session_token_total === null || ctx.session_token_limit <= 0) return null\n const percent = ctx.session_token_total / ctx.session_token_limit\n if (percent < 0.5) return null\n let severity: DetectionSeverity = 'info'\n if (percent >= 0.9) severity = 'critical'\n else if (percent >= 0.75) severity = 'warn'\n return {\n rule_id: 'detect-context-threshold',\n tip_ids: ['use-compact-long-session'],\n severity,\n evidence: `Contexto: ${(percent * 100).toFixed(1)}% usado (${ctx.session_token_total}/${ctx.session_token_limit} tokens)`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n\n // 2. detect-long-reasoning-no-code\n {\n id: 'detect-long-reasoning-no-code',\n tip_ids: ['use-plan-mode', 'use-opusplan'],\n run(ctx) {\n const recent = ctx.events.slice(0, 10)\n if (recent.length < 10) return null\n const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name))\n if (edits > 0) return null\n return {\n rule_id: 'detect-long-reasoning-no-code',\n tip_ids: ['use-plan-mode', 'use-opusplan'],\n severity: 'info',\n evidence: '10 eventos recientes sin ediciones de codigo',\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 3. detect-repeated-searches\n {\n id: 'detect-repeated-searches',\n tip_ids: ['use-agent-explore'],\n run(ctx) {\n const window = ctx.events.slice(0, 20)\n const searches = countMatching(window, (e) => e.tool_name === 'Grep' || e.tool_name === 'Glob')\n if (searches < 3) return null\n return {\n rule_id: 'detect-repeated-searches',\n tip_ids: ['use-agent-explore'],\n severity: 'info',\n evidence: `${searches} busquedas Grep/Glob en los ultimos 20 eventos`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 4. detect-huge-file-reads\n {\n id: 'detect-huge-file-reads',\n tip_ids: ['install-serena'],\n run(ctx) {\n const huge = ctx.events.find((e) => e.tool_name === 'Read' && e.tokens_estimated > 50_000)\n if (!huge) return null\n return {\n rule_id: 'detect-huge-file-reads',\n tip_ids: ['install-serena'],\n severity: 'warn',\n evidence: `Read consumio ${huge.tokens_estimated} tokens (umbral 50k)`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 5. detect-many-bash-commands\n {\n id: 'detect-many-bash-commands',\n tip_ids: ['install-rtk'],\n run(ctx) {\n const window = ctx.events.slice(0, 100)\n const bash = countMatching(window, (e) => e.tool_name === 'Bash')\n if (bash <= 10) return null\n return {\n rule_id: 'detect-many-bash-commands',\n tip_ids: ['install-rtk'],\n severity: 'info',\n evidence: `${bash} comandos Bash en los ultimos ${window.length} eventos`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 6. detect-clear-opportunity (was #7 — detect-unused-mcp-servers stub removed)\n {\n id: 'detect-clear-opportunity',\n tip_ids: ['use-clear-rename-resume'],\n run(ctx) {\n if (ctx.events.length < 40) return null\n const recentTools = new Set(ctx.events.slice(0, 20).map((e) => e.tool_name))\n const priorTools = new Set(ctx.events.slice(20, 40).map((e) => e.tool_name))\n if (recentTools.size === 0) return null\n let overlap = 0\n for (const t of recentTools) if (priorTools.has(t)) overlap++\n const ratio = overlap / recentTools.size\n if (ratio >= 0.3) return null\n return {\n rule_id: 'detect-clear-opportunity',\n tip_ids: ['use-clear-rename-resume'],\n severity: 'info',\n evidence: `Solapamiento de herramientas ${(ratio * 100).toFixed(0)}% — posible pivote de tema`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 8. detect-opus-for-simple-task\n {\n id: 'detect-opus-for-simple-task',\n tip_ids: ['default-to-sonnet', 'use-haiku-for-simple'],\n run(ctx) {\n if (!ctx.active_model || !/opus/i.test(ctx.active_model)) return null\n const recent = ctx.events.slice(0, 20)\n if (recent.length < 6) return null\n const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name))\n const bash = countMatching(recent, (e) => e.tool_name === 'Bash')\n // Opus es correcto para planificar/preguntar — solo avisar cuando está ejecutando código\n if (edits + bash < 6) return null\n return {\n rule_id: 'detect-opus-for-simple-task',\n tip_ids: ['default-to-sonnet', 'use-haiku-for-simple'],\n severity: 'info',\n evidence: `Opus ejecutando trabajo mecanico: ${edits} edits + ${bash} Bash en ultimos 20 eventos. Sonnet haria lo mismo un 80% mas barato.`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 9. detect-claudemd-bloat (stub — requires filesystem stat at runtime)\n {\n id: 'detect-claudemd-bloat',\n tip_ids: ['migrate-claudemd-to-skills'],\n run() {\n return null\n },\n },\n\n // 10. detect-post-milestone-opportunity\n {\n id: 'detect-post-milestone-opportunity',\n tip_ids: ['use-compact-long-session'],\n run(ctx) {\n const recent = ctx.events.slice(0, 20)\n const edits = countMatching(recent, (e) => e.tool_name === 'Edit' || e.tool_name === 'Write')\n const hasBash = countMatching(recent, (e) => e.tool_name === 'Bash') > 0\n if (edits < 5 || !hasBash) return null\n if (ctx.session_token_total === null) return null\n const percent = ctx.session_token_total / ctx.session_token_limit\n if (percent < 0.4) return null\n return {\n rule_id: 'detect-post-milestone-opportunity',\n tip_ids: ['use-compact-long-session'],\n severity: 'info',\n evidence: `${edits} ediciones + Bash reciente + contexto ${(percent * 100).toFixed(0)}%`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n\n // 11. detect-skill-trigger-ignored (stub — requires skill registry)\n {\n id: 'detect-skill-trigger-ignored',\n tip_ids: ['use-skill-trigger'],\n run() {\n return null\n },\n },\n\n // 12. detect-serena-read-cascade\n // Fires when the agent makes ≥5 find_symbol calls without a get_symbols_overview\n // in the same window — suggests starting with an overview first.\n {\n id: 'detect-serena-read-cascade',\n tip_ids: ['use-serena-overview-first'],\n run(ctx) {\n const window = ctx.events.slice(0, 15)\n const findSymbolCount = countMatching(\n window,\n (e) => e.tool_name === 'mcp__serena__find_symbol',\n )\n if (findSymbolCount < 5) return null\n const hasOverview = window.some(\n (e) => e.tool_name === 'mcp__serena__get_symbols_overview',\n )\n if (hasOverview) return null\n return {\n rule_id: 'detect-serena-read-cascade',\n tip_ids: ['use-serena-overview-first'],\n severity: 'info' as DetectionSeverity,\n evidence: `${findSymbolCount} llamadas find_symbol sin get_symbols_overview en los ultimos 15 eventos.`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n\n // 13. detect-read-over-serena\n {\n id: 'detect-read-over-serena',\n tip_ids: ['prefer-serena-reads'],\n run(ctx) {\n const window = ctx.events.slice(0, 30)\n const largeReads = window.filter(\n (e) => e.tool_name === 'Read' && e.tokens_estimated > 2_000,\n )\n if (largeReads.length < 3) return null\n const totalTokens = largeReads.reduce((sum, e) => sum + e.tokens_estimated, 0)\n const estimatedSaving = Math.round(totalTokens * 0.7)\n const severity: DetectionSeverity = largeReads.length >= 6 ? 'warn' : 'info'\n return {\n rule_id: 'detect-read-over-serena',\n tip_ids: ['prefer-serena-reads'],\n severity,\n evidence: `${largeReads.length} lecturas Read >2k tokens (total: ${totalTokens}). Serena ahorraria ~${estimatedSaving} tokens (~70%).`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n]\n","// Rules orchestrator — Phase 4.44\n// Runs all detection rules, dedupes by (rule_id, tip_id), sorts by severity desc.\n\nimport type { DetectionHit, EventContext } from '../lib/types.js'\nimport { DETECTION_RULES } from './rules.js'\n\nconst SEVERITY_ORDER: Record<string, number> = { critical: 0, warn: 1, info: 2 }\n\nexport function runRules(ctx: EventContext): DetectionHit[] {\n const hits: DetectionHit[] = []\n for (const rule of DETECTION_RULES) {\n try {\n const hit = rule.run(ctx)\n if (hit) hits.push(hit)\n } catch {\n // swallow — rules must never crash the caller\n }\n }\n // Dedupe by rule_id\n const seen = new Set<string>()\n const unique: DetectionHit[] = []\n for (const h of hits) {\n if (seen.has(h.rule_id)) continue\n seen.add(h.rule_id)\n unique.push(h)\n }\n unique.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99))\n return unique\n}\n","// Context size meter with 3-source fallback — Phase 4.42\n// (1) transcript JSONL → (2) xray HTTP → (3) cumulative DB estimate\n\nimport fs from 'node:fs'\nimport type Database from 'better-sqlite3'\nimport type { ContextMeasurement, EstimationMethod } from '../lib/types.js'\nimport { resolveTranscriptPath } from '../lib/paths.js'\nimport { buildQueries } from '../db/queries.js'\nimport { getSessionTokens } from '../services/xray-client.js'\n\ntype DB = Database.Database\n\nconst DEFAULT_LIMIT = 200_000\nconst OPUS_1M_LIMIT = 1_000_000\nconst BASELINE_TOKENS = 15_000\n\nexport interface ContextMeterOptions {\n projectDir?: string\n db?: DB\n fetchImpl?: typeof fetch\n activeModel?: string\n}\n\nexport async function measureContextSize(\n sessionId: string,\n opts: ContextMeterOptions = {},\n): Promise<ContextMeasurement> {\n // Strategy 1: transcript JSONL (measured_exact)\n if (opts.projectDir) {\n const transcript = readTranscript(opts.projectDir, sessionId)\n if (transcript) return transcript\n }\n\n // Strategy 2: xray HTTP (measured_exact)\n const xrayResult = await tryXray(sessionId, opts.fetchImpl)\n if (xrayResult) return xrayResult\n\n // Strategy 3: cumulative estimate from our DB (estimated_cumulative)\n const limit = resolveLimit(opts.activeModel)\n if (opts.db) {\n return cumulativeEstimate(opts.db, sessionId, limit)\n }\n\n return { tokens: 0, limit, percent: 0, estimation_method: 'unknown' }\n}\n\nfunction readTranscript(projectDir: string, sessionId: string): ContextMeasurement | null {\n try {\n const p = resolveTranscriptPath(projectDir, sessionId)\n if (!fs.existsSync(p)) return null\n const content = fs.readFileSync(p, 'utf8')\n const lines = content.split('\\n').filter((l) => l.trim().length > 0)\n let totalTokens = 0\n let limit = DEFAULT_LIMIT\n for (const line of lines) {\n try {\n const turn = JSON.parse(line) as {\n usage?: {\n input_tokens?: number\n output_tokens?: number\n cache_read_input_tokens?: number\n }\n model?: string\n }\n if (turn.usage) {\n totalTokens +=\n (turn.usage.input_tokens ?? 0) +\n (turn.usage.output_tokens ?? 0) +\n (turn.usage.cache_read_input_tokens ?? 0)\n }\n if (turn.model && /1m/i.test(turn.model)) limit = OPUS_1M_LIMIT\n } catch {\n // skip unparseable line\n }\n }\n return {\n tokens: totalTokens,\n limit,\n percent: limit > 0 ? totalTokens / limit : 0,\n estimation_method: 'measured_exact' as EstimationMethod,\n }\n } catch {\n return null\n }\n}\n\nasync function tryXray(\n sessionId: string,\n fetchImpl?: typeof fetch,\n): Promise<ContextMeasurement | null> {\n const opts: Parameters<typeof getSessionTokens>[1] = {}\n if (fetchImpl !== undefined) opts.fetchImpl = fetchImpl\n return getSessionTokens(sessionId, opts)\n}\n\nfunction resolveLimit(activeModel?: string): number {\n if (activeModel && /1m|opus/i.test(activeModel)) return OPUS_1M_LIMIT\n return DEFAULT_LIMIT\n}\n\nfunction cumulativeEstimate(\n db: DB,\n sessionId: string,\n limit: number = DEFAULT_LIMIT,\n): ContextMeasurement {\n const queries = buildQueries(db)\n const sessionTokens = queries.sumTokensBySession(sessionId)\n const total = sessionTokens + BASELINE_TOKENS\n return {\n tokens: total,\n limit,\n percent: total / limit,\n estimation_method: 'estimated_cumulative',\n }\n}\n\n/**\n * Synchronous DB-only context measurement for hot paths (PostToolUse).\n * Skips transcript + xray strategies to stay under the 5ms budget. The\n * estimation_method returned ('estimated_cumulative') is surfaced verbatim\n * in tips so the agent knows this is a fast approximation.\n */\nexport function measureContextSizeFromDbSync(\n db: DB,\n sessionId: string,\n activeModel?: string,\n): ContextMeasurement {\n const limit = resolveLimit(activeModel)\n return cumulativeEstimate(db, sessionId, limit)\n}\n","// xray client — Phase 5.1\n// Fire-and-forget POST for tool events + GET for session tokens (used by coach context meter).\n// Silent on all failures: no stderr, no throw. Timeout 500ms for post, 300ms for get.\n\nimport type { ContextMeasurement } from '../lib/types.js'\nimport { resolveXrayUrl } from '../cli/config.js'\n\nconst POST_TIMEOUT_MS = 500\nconst GET_TIMEOUT_MS = 300\nconst DEFAULT_LIMIT = 200_000\n\ndeclare const __PKG_VERSION__: string\nconst VERSION = typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : '0.1.0'\n\nexport interface PostToXrayOptions {\n xrayUrl?: string\n fetchImpl?: typeof fetch\n timeoutMs?: number\n}\n\n/**\n * Fire-and-forget POST of a tool event to an xray server.\n * Returns true if the request was attempted, false if skipped (no URL).\n * Any network/parse error is swallowed silently.\n */\nexport async function postToXray(\n event: Record<string, unknown>,\n opts: PostToXrayOptions = {},\n): Promise<boolean> {\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\n if (!xrayUrl) return false\n const fetchFn = opts.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? POST_TIMEOUT_MS)\n try {\n await fetchFn(`${xrayUrl}/hooks/token-optimizer`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n source: 'token-optimizer-mcp',\n version: VERSION,\n event,\n }),\n signal: controller.signal,\n })\n return true\n } catch {\n return false\n } finally {\n clearTimeout(timer)\n }\n}\n\nconst SUMMARY_TIMEOUT_MS = 2000\n\n/**\n * Fire-and-forget POST of session summary to xray.\n * Only called once per session (not in hot path), so allows longer timeout.\n */\nexport async function postSummaryToXray(\n summary: Record<string, unknown>,\n opts: PostToXrayOptions = {},\n): Promise<boolean> {\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\n if (!xrayUrl) return false\n const fetchFn = opts.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUMMARY_TIMEOUT_MS)\n try {\n await fetchFn(`${xrayUrl}/hooks/token-optimizer/summary`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n source: 'token-optimizer-mcp',\n version: VERSION,\n summary,\n }),\n signal: controller.signal,\n })\n return true\n } catch {\n return false\n } finally {\n clearTimeout(timer)\n }\n}\n\nexport interface GetSessionTokensOptions {\n xrayUrl?: string\n fetchImpl?: typeof fetch\n timeoutMs?: number\n}\n\n/**\n * Read real token counts from xray for a given session.\n * Returns null on any failure or when XRAY_URL is unset.\n */\nexport async function getSessionTokens(\n sessionId: string,\n opts: GetSessionTokensOptions = {},\n): Promise<ContextMeasurement | null> {\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\n if (!xrayUrl) return null\n const fetchFn = opts.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? GET_TIMEOUT_MS)\n try {\n const res = await fetchFn(\n `${xrayUrl}/sessions/${encodeURIComponent(sessionId)}/tokens`,\n { signal: controller.signal },\n )\n if (!res.ok) return null\n const data = (await res.json()) as { tokens?: number; limit?: number }\n const tokens = data.tokens ?? 0\n const limit = data.limit ?? DEFAULT_LIMIT\n return {\n tokens,\n limit,\n percent: limit > 0 ? tokens / limit : 0,\n estimation_method: 'measured_exact',\n }\n } catch {\n return null\n } finally {\n clearTimeout(timer)\n }\n}\n","// Surfacing dedupe + log writer — Phase 4.45\n// Writes to coach_surface_log with session+rule+tip+via+severity.\n\nimport type Database from 'better-sqlite3'\nimport type { DetectionHit } from '../lib/types.js'\n\ntype DB = Database.Database\n\nexport type SurfacedVia = 'sessionstart' | 'posttooluse' | 'mcp' | 'cli'\n\n/**\n * Returns true if this (session, rule, tip) was surfaced within the last\n * `windowSeconds` seconds. Used by the PostToolUse throttle path.\n */\nexport function checkDedupe(\n db: DB,\n sessionId: string,\n ruleId: string,\n tipId: string,\n windowSeconds: number,\n): boolean {\n const row = db\n .prepare(\n `SELECT 1 FROM coach_surface_log\n WHERE session_id = ? AND rule_id = ? AND tip_id = ?\n AND created_at > datetime('now', ?)\n LIMIT 1`,\n )\n .get(sessionId, ruleId, tipId, `-${windowSeconds} seconds`) as unknown\n return row !== undefined && row !== null\n}\n\nexport function logSurface(\n db: DB,\n sessionId: string,\n hit: DetectionHit,\n via: SurfacedVia,\n): void {\n // Ensure session exists so FK succeeds\n db.prepare(`INSERT OR IGNORE INTO sessions (id) VALUES (?)`).run(sessionId)\n const stmt = db.prepare(\n `INSERT INTO coach_surface_log (session_id, rule_id, tip_id, surfaced_via, severity)\n VALUES (?, ?, ?, ?, ?)`,\n )\n for (const tipId of hit.tip_ids) {\n stmt.run(sessionId, hit.rule_id, tipId, via, hit.severity)\n }\n}\n\n/**\n * Log the list of hits under dedupe. A hit is considered \"fresh\" (to be\n * logged and returned to the caller) if at least one of its tip_ids was NOT\n * surfaced within `windowSeconds`. Returns only the surfaced hits.\n */\nexport function surfaceWithDedupe(\n db: DB,\n sessionId: string,\n hits: DetectionHit[],\n via: SurfacedVia,\n windowSeconds: number,\n): DetectionHit[] {\n const surfaced: DetectionHit[] = []\n for (const hit of hits) {\n const anyFresh = hit.tip_ids.some(\n (tipId) => !checkDedupe(db, sessionId, hit.rule_id, tipId, windowSeconds),\n )\n if (anyFresh) {\n logSurface(db, sessionId, hit, via)\n surfaced.push(hit)\n }\n }\n return surfaced\n}\n\n/**\n * Read all coach tips surfaced during a session, grouped by rule.\n * Used by session-summary-builder for xray integration.\n */\nexport function getCoachSurfaceLog(\n db: DB,\n sessionId: string,\n): Array<{ rule_id: string; tip_ids: string[]; severity: string }> {\n const rows = db\n .prepare(\n `SELECT rule_id, tip_id, severity\n FROM coach_surface_log\n WHERE session_id = ?\n ORDER BY created_at`,\n )\n .all(sessionId) as Array<{ rule_id: string; tip_id: string; severity: string }>\n\n // Group tip_ids by rule_id\n const map = new Map<string, { tip_ids: string[]; severity: string }>()\n for (const row of rows) {\n if (!map.has(row.rule_id)) {\n map.set(row.rule_id, { tip_ids: [], severity: row.severity })\n }\n const entry = map.get(row.rule_id)!\n if (!entry.tip_ids.includes(row.tip_id)) {\n entry.tip_ids.push(row.tip_id)\n }\n }\n\n return Array.from(map.entries()).map(([rule_id, v]) => ({\n rule_id,\n tip_ids: v.tip_ids,\n severity: v.severity,\n }))\n}\n\nexport function clearSurfaceLog(db: DB, sessionId?: string): number {\n if (sessionId) {\n const info = db.prepare(`DELETE FROM coach_surface_log WHERE session_id = ?`).run(sessionId)\n return info.changes as number\n }\n const info = db.prepare(`DELETE FROM coach_surface_log`).run()\n return info.changes as number\n}\n"],"mappings":"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","names":["DEFAULT_LIMIT","info"]} |
| #!/usr/bin/env node | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-XAYSOAZV.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FT4EJUIT.js"; | ||
| // src/services/stats.ts | ||
| var DAY_MS = 864e5; | ||
| var HAIKU_INPUT_PER_MTOK = 1; | ||
| var SONNET_INPUT_PER_MTOK = 3; | ||
| var OPUS_INPUT_PER_MTOK = 5; | ||
| function sinceDays(days) { | ||
| return new Date(Date.now() - days * DAY_MS).toISOString(); | ||
| } | ||
| function getUsageStats(db, days = 7) { | ||
| const queries = buildQueries(db); | ||
| const since = sinceDays(days); | ||
| const byTool = queries.countToolCallsByTool(since); | ||
| const bySource = queries.countToolCallsBySource(since); | ||
| const totalTokens = bySource.reduce((sum, r) => sum + r.tokens, 0); | ||
| const totalEvents = bySource.reduce((sum, r) => sum + r.count, 0); | ||
| return { | ||
| period_days: days, | ||
| period_since: since, | ||
| by_tool: byTool, | ||
| by_source: bySource, | ||
| total_tokens: totalTokens, | ||
| total_events: totalEvents | ||
| }; | ||
| } | ||
| function getCostReport(db, days = 7) { | ||
| const usage = getUsageStats(db, days); | ||
| const mtok = usage.total_tokens / 1e6; | ||
| const haiku = Number((mtok * HAIKU_INPUT_PER_MTOK).toFixed(4)); | ||
| const sonnet = Number((mtok * SONNET_INPUT_PER_MTOK).toFixed(4)); | ||
| const opus = Number((mtok * OPUS_INPUT_PER_MTOK).toFixed(4)); | ||
| return { | ||
| period_days: days, | ||
| total_tokens: usage.total_tokens, | ||
| estimated_cost_usd_haiku: haiku, | ||
| estimated_cost_usd_sonnet: sonnet, | ||
| estimated_cost_usd_opus: opus, | ||
| estimated_cost_usd_min: haiku, | ||
| estimated_cost_usd_max: opus, | ||
| by_source: usage.by_source, | ||
| disclaimer: "Coste estimado de tokens de herramientas (input al modelo). Haiku $1, Sonnet $3, Opus $5 por MTok. Revisa tu factura Anthropic para el coste real." | ||
| }; | ||
| } | ||
| function getActiveBudgetSummary(db, sessionId, projectHash) { | ||
| const mgr = new BudgetManager(db); | ||
| return mgr.checkBudget(sessionId, projectHash); | ||
| } | ||
| export { | ||
| getUsageStats, | ||
| getCostReport, | ||
| getActiveBudgetSummary | ||
| }; | ||
| //# sourceMappingURL=chunk-BJYHIS4U.js.map |
| {"version":3,"sources":["../src/services/stats.ts"],"sourcesContent":["// Shared stats service — Phase 4.4\n// Read-only aggregates used by CLI and MCP tools.\n\nimport type Database from 'better-sqlite3'\nimport { buildQueries, type ToolCountRow, type SourceCountRow } from '../db/queries.js'\nimport { BudgetManager } from './budget-manager.js'\nimport type { BudgetStatus } from '../lib/types.js'\n\ntype DB = Database.Database\n\nconst DAY_MS = 86_400_000\n// Pricing April 2026 — input tokens (tool output → model input)\n// Haiku 4.5: $1/$5, Sonnet 4.6: $3/$15, Opus 4.6: $5/$25 per MTok (input/output)\n// token-optimizer tracks tool output (= model input), so we use input pricing.\nconst HAIKU_INPUT_PER_MTOK = 1\nconst SONNET_INPUT_PER_MTOK = 3\nconst OPUS_INPUT_PER_MTOK = 5\n\nfunction sinceDays(days: number): string {\n return new Date(Date.now() - days * DAY_MS).toISOString()\n}\n\nexport interface UsageStats {\n period_days: number\n period_since: string\n by_tool: ToolCountRow[]\n by_source: SourceCountRow[]\n total_tokens: number\n total_events: number\n}\n\nexport interface CostReport {\n period_days: number\n total_tokens: number\n estimated_cost_usd_haiku: number\n estimated_cost_usd_sonnet: number\n estimated_cost_usd_opus: number\n /** @deprecated Use estimated_cost_usd_haiku */\n estimated_cost_usd_min: number\n /** @deprecated Use estimated_cost_usd_opus */\n estimated_cost_usd_max: number\n by_source: SourceCountRow[]\n disclaimer: string\n}\n\nexport interface SavingsToday {\n date: string\n by_source: SourceCountRow[]\n total_tokens: number\n note: string\n}\n\nexport function getUsageStats(db: DB, days = 7): UsageStats {\n const queries = buildQueries(db)\n const since = sinceDays(days)\n const byTool = queries.countToolCallsByTool(since)\n const bySource = queries.countToolCallsBySource(since)\n const totalTokens = bySource.reduce((sum, r) => sum + r.tokens, 0)\n const totalEvents = bySource.reduce((sum, r) => sum + r.count, 0)\n return {\n period_days: days,\n period_since: since,\n by_tool: byTool,\n by_source: bySource,\n total_tokens: totalTokens,\n total_events: totalEvents,\n }\n}\n\nexport function getCostReport(db: DB, days = 7): CostReport {\n const usage = getUsageStats(db, days)\n const mtok = usage.total_tokens / 1_000_000\n const haiku = Number((mtok * HAIKU_INPUT_PER_MTOK).toFixed(4))\n const sonnet = Number((mtok * SONNET_INPUT_PER_MTOK).toFixed(4))\n const opus = Number((mtok * OPUS_INPUT_PER_MTOK).toFixed(4))\n return {\n period_days: days,\n total_tokens: usage.total_tokens,\n estimated_cost_usd_haiku: haiku,\n estimated_cost_usd_sonnet: sonnet,\n estimated_cost_usd_opus: opus,\n estimated_cost_usd_min: haiku,\n estimated_cost_usd_max: opus,\n by_source: usage.by_source,\n disclaimer:\n 'Coste estimado de tokens de herramientas (input al modelo). Haiku $1, Sonnet $3, Opus $5 por MTok. Revisa tu factura Anthropic para el coste real.',\n }\n}\n\nexport function getActiveBudgetSummary(\n db: DB,\n sessionId: string,\n projectHash: string | null,\n): BudgetStatus {\n const mgr = new BudgetManager(db)\n return mgr.checkBudget(sessionId, projectHash)\n}\n\nexport function getSavingsToday(db: DB): SavingsToday {\n const queries = buildQueries(db)\n const since = sinceDays(1)\n const bySource = queries.countToolCallsBySource(since)\n const total = bySource.reduce((sum, r) => sum + r.tokens, 0)\n return {\n date: new Date().toISOString().slice(0, 10),\n by_source: bySource,\n total_tokens: total,\n note: 'Ahorros por fuente no medidos directamente; revisa el reporte para el split Medido/Estimado.',\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAM,SAAS;AAIf,IAAM,uBAAuB;AAC7B,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB;AAE5B,SAAS,UAAU,MAAsB;AACvC,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE,YAAY;AAC1D;AAgCO,SAAS,cAAc,IAAQ,OAAO,GAAe;AAC1D,QAAM,UAAU,aAAa,EAAE;AAC/B,QAAM,QAAQ,UAAU,IAAI;AAC5B,QAAM,SAAS,QAAQ,qBAAqB,KAAK;AACjD,QAAM,WAAW,QAAQ,uBAAuB,KAAK;AACrD,QAAM,cAAc,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,QAAQ,CAAC;AACjE,QAAM,cAAc,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AAChE,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc;AAAA,IACd,SAAS;AAAA,IACT,WAAW;AAAA,IACX,cAAc;AAAA,IACd,cAAc;AAAA,EAChB;AACF;AAEO,SAAS,cAAc,IAAQ,OAAO,GAAe;AAC1D,QAAM,QAAQ,cAAc,IAAI,IAAI;AACpC,QAAM,OAAO,MAAM,eAAe;AAClC,QAAM,QAAQ,QAAQ,OAAO,sBAAsB,QAAQ,CAAC,CAAC;AAC7D,QAAM,SAAS,QAAQ,OAAO,uBAAuB,QAAQ,CAAC,CAAC;AAC/D,QAAM,OAAO,QAAQ,OAAO,qBAAqB,QAAQ,CAAC,CAAC;AAC3D,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc,MAAM;AAAA,IACpB,0BAA0B;AAAA,IAC1B,2BAA2B;AAAA,IAC3B,yBAAyB;AAAA,IACzB,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,IACxB,WAAW,MAAM;AAAA,IACjB,YACE;AAAA,EACJ;AACF;AAEO,SAAS,uBACd,IACA,WACA,aACc;AACd,QAAM,MAAM,IAAI,cAAc,EAAE;AAChC,SAAO,IAAI,YAAY,WAAW,WAAW;AAC/C;","names":[]} |
| #!/usr/bin/env node | ||
| // src/db/queries.ts | ||
| function buildQueries(db) { | ||
| const insertSession = db.prepare( | ||
| `INSERT OR IGNORE INTO sessions (id, project_hash) VALUES (?, ?)` | ||
| ); | ||
| const insertToolCall = db.prepare( | ||
| `INSERT INTO tool_calls ( | ||
| session_id, tool_name, source, output_bytes, | ||
| tokens_estimated, tokens_actual, duration_ms, estimation_method, | ||
| shadow_delta_tokens, created_at | ||
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` | ||
| ); | ||
| const getToolCallsSince = db.prepare( | ||
| `SELECT * FROM tool_calls WHERE created_at >= ? ORDER BY created_at DESC` | ||
| ); | ||
| const countToolCallsByTool = db.prepare( | ||
| `SELECT tool_name, COUNT(*) as count, COALESCE(SUM(tokens_estimated), 0) as tokens | ||
| FROM tool_calls WHERE created_at >= ? | ||
| GROUP BY tool_name ORDER BY tokens DESC` | ||
| ); | ||
| const countToolCallsBySource = db.prepare( | ||
| `SELECT source, COUNT(*) as count, COALESCE(SUM(tokens_estimated), 0) as tokens | ||
| FROM tool_calls WHERE created_at >= ? | ||
| GROUP BY source ORDER BY tokens DESC` | ||
| ); | ||
| const sumTokensBySession = db.prepare( | ||
| `SELECT COALESCE(SUM(tokens_estimated), 0) as total | ||
| FROM tool_calls WHERE session_id = ?` | ||
| ); | ||
| const countToolCallsBySessionStmt = db.prepare( | ||
| `SELECT COUNT(*) as c FROM tool_calls WHERE session_id = ?` | ||
| ); | ||
| const sumTokensBySessionSince = db.prepare( | ||
| `SELECT COALESCE(SUM(tokens_estimated), 0) as total | ||
| FROM tool_calls WHERE session_id = ? AND created_at >= ?` | ||
| ); | ||
| const sumTokensByProjectSince = db.prepare( | ||
| `SELECT COALESCE(SUM(tc.tokens_estimated), 0) as total | ||
| FROM tool_calls tc | ||
| JOIN sessions s ON tc.session_id = s.id | ||
| WHERE s.project_hash = ? AND tc.created_at >= ?` | ||
| ); | ||
| const insertToolCallMany = db.transaction((events) => { | ||
| for (const e of events) { | ||
| insertToolCall.run( | ||
| e.session_id, | ||
| e.tool_name, | ||
| e.source, | ||
| e.output_bytes, | ||
| e.tokens_estimated, | ||
| e.tokens_actual, | ||
| e.duration_ms, | ||
| e.estimation_method, | ||
| e.shadow_delta_tokens ?? null, | ||
| e.created_at | ||
| ); | ||
| } | ||
| }); | ||
| const insertSerenaTouch = db.prepare( | ||
| `INSERT INTO serena_symbol_touches (session_id, tool_name, relative_path, name_path) | ||
| VALUES (?, ?, ?, ?)` | ||
| ); | ||
| const getRecentSerenaSymbols = db.prepare( | ||
| `SELECT tool_name, relative_path, name_path, created_at | ||
| FROM serena_symbol_touches | ||
| WHERE session_id = ? | ||
| ORDER BY created_at DESC LIMIT ?` | ||
| ); | ||
| const insertRtkRewrite = db.prepare( | ||
| `INSERT INTO rtk_rewrites (session_id, command_hash, rewritten_to) VALUES (?, ?, ?)` | ||
| ); | ||
| const findRtkMark = db.prepare( | ||
| `SELECT id, rewritten_to FROM rtk_rewrites | ||
| WHERE session_id = ? AND command_hash = ? | ||
| AND created_at >= datetime('now', '-60 seconds') | ||
| ORDER BY id DESC LIMIT 1` | ||
| ); | ||
| const deleteRtkMarkById = db.prepare(`DELETE FROM rtk_rewrites WHERE id = ?`); | ||
| const purgeStaleRtkMarks = db.prepare( | ||
| `DELETE FROM rtk_rewrites WHERE created_at < datetime('now', '-60 seconds')` | ||
| ); | ||
| const upsertMetaCounter = db.prepare( | ||
| `INSERT INTO meta (key, value) VALUES (?, '1') | ||
| ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1` | ||
| ); | ||
| const getMeta = db.prepare(`SELECT value FROM meta WHERE key = ?`); | ||
| const upsertBudget = db.prepare( | ||
| `INSERT INTO budgets (scope, scope_key, limit_tokens, mode) | ||
| VALUES (?, ?, ?, ?) | ||
| ON CONFLICT(scope, scope_key) DO UPDATE SET | ||
| limit_tokens = excluded.limit_tokens, | ||
| mode = excluded.mode` | ||
| ); | ||
| const selectBudgetByScope = db.prepare( | ||
| `SELECT id, scope, scope_key, limit_tokens, spent_tokens, mode, created_at | ||
| FROM budgets WHERE scope = ? AND scope_key = ?` | ||
| ); | ||
| const deleteBudgetByScope = db.prepare( | ||
| `DELETE FROM budgets WHERE scope = ? AND scope_key = ?` | ||
| ); | ||
| const listBudgets = db.prepare( | ||
| `SELECT id, scope, scope_key, limit_tokens, spent_tokens, mode, created_at | ||
| FROM budgets ORDER BY created_at DESC` | ||
| ); | ||
| const insertBudgetEvent = db.prepare( | ||
| `INSERT INTO budget_events (budget_id, event_type, tokens) VALUES (?, ?, ?)` | ||
| ); | ||
| return { | ||
| // sessions | ||
| insertSession(id, projectHash) { | ||
| insertSession.run(id, projectHash); | ||
| }, | ||
| // tool_calls | ||
| insertToolCall(event) { | ||
| insertToolCall.run( | ||
| event.session_id, | ||
| event.tool_name, | ||
| event.source, | ||
| event.output_bytes, | ||
| event.tokens_estimated, | ||
| event.tokens_actual, | ||
| event.duration_ms, | ||
| event.estimation_method, | ||
| event.shadow_delta_tokens ?? null, | ||
| event.created_at | ||
| ); | ||
| }, | ||
| insertToolCallMany(events) { | ||
| insertToolCallMany(events); | ||
| }, | ||
| getToolCallsSince(timestamp) { | ||
| return getToolCallsSince.all(timestamp); | ||
| }, | ||
| countToolCallsByTool(since) { | ||
| return countToolCallsByTool.all(since); | ||
| }, | ||
| countToolCallsBySource(since) { | ||
| return countToolCallsBySource.all(since); | ||
| }, | ||
| sumTokensBySession(sessionId) { | ||
| const row = sumTokensBySession.get(sessionId); | ||
| return row?.total ?? 0; | ||
| }, | ||
| countToolCallsBySession(sessionId) { | ||
| const row = countToolCallsBySessionStmt.get(sessionId); | ||
| return row?.c ?? 0; | ||
| }, | ||
| sumTokensBySessionSince(sessionId, since) { | ||
| const row = sumTokensBySessionSince.get(sessionId, since); | ||
| return row?.total ?? 0; | ||
| }, | ||
| sumTokensByProjectSince(projectHash, since) { | ||
| const row = sumTokensByProjectSince.get(projectHash, since); | ||
| return row?.total ?? 0; | ||
| }, | ||
| // meta | ||
| upsertMetaCounter(key) { | ||
| upsertMetaCounter.run(key); | ||
| }, | ||
| getMeta(key) { | ||
| const row = getMeta.get(key); | ||
| return row?.value ?? null; | ||
| }, | ||
| // budgets | ||
| upsertBudget(scope, scopeKey, limitTokens, mode) { | ||
| upsertBudget.run(scope, scopeKey, limitTokens, mode); | ||
| }, | ||
| getBudgetByScope(scope, scopeKey) { | ||
| const row = selectBudgetByScope.get(scope, scopeKey); | ||
| return row ?? null; | ||
| }, | ||
| deleteBudgetByScope(scope, scopeKey) { | ||
| const info = deleteBudgetByScope.run(scope, scopeKey); | ||
| return info.changes; | ||
| }, | ||
| listBudgets() { | ||
| return listBudgets.all(); | ||
| }, | ||
| insertBudgetEvent(budgetId, eventType, tokens) { | ||
| insertBudgetEvent.run(budgetId, eventType, tokens); | ||
| }, | ||
| // rtk rewrites | ||
| insertRtkRewrite(sessionId, commandHash, rewrittenTo) { | ||
| insertRtkRewrite.run(sessionId, commandHash, rewrittenTo); | ||
| }, | ||
| /** | ||
| * Look up and consume an RTK rewrite mark. Returns the rewritten command if | ||
| * there was a mark for this (session, hash) within the last 60 s, or null. | ||
| * The row is deleted on match so it cannot be double-consumed. | ||
| */ | ||
| consumeRtkRewrite(sessionId, commandHash) { | ||
| const row = findRtkMark.get(sessionId, commandHash); | ||
| if (!row) return null; | ||
| deleteRtkMarkById.run(row.id); | ||
| return row.rewritten_to; | ||
| }, | ||
| purgeStaleRtkMarks() { | ||
| const info = purgeStaleRtkMarks.run(); | ||
| return info.changes; | ||
| }, | ||
| // serena symbol touches | ||
| insertSerenaTouch(sessionId, toolName, relativePath, namePath) { | ||
| insertSerenaTouch.run(sessionId, toolName, relativePath, namePath); | ||
| }, | ||
| getRecentSerenaSymbols(sessionId, limit) { | ||
| return getRecentSerenaSymbols.all(sessionId, limit); | ||
| } | ||
| }; | ||
| } | ||
| export { | ||
| buildQueries | ||
| }; | ||
| //# sourceMappingURL=chunk-FT4EJUIT.js.map |
| {"version":3,"sources":["../src/db/queries.ts"],"sourcesContent":["// Prepared statement factory — Phase 1.7 + 2.2\n\nimport type Database from 'better-sqlite3'\nimport type { ToolEvent, Budget, BudgetScope, BudgetMode } from '../lib/types.js'\n\ntype DB = Database.Database\n\nexport interface ToolCountRow {\n tool_name: string\n count: number\n tokens: number\n}\n\nexport interface SourceCountRow {\n source: string\n count: number\n tokens: number\n}\n\nexport function buildQueries(db: DB) {\n // ── sessions ──\n const insertSession = db.prepare(\n `INSERT OR IGNORE INTO sessions (id, project_hash) VALUES (?, ?)`,\n )\n\n // ── tool_calls ──\n const insertToolCall = db.prepare(\n `INSERT INTO tool_calls (\n session_id, tool_name, source, output_bytes,\n tokens_estimated, tokens_actual, duration_ms, estimation_method,\n shadow_delta_tokens, created_at\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n )\n\n const getToolCallsSince = db.prepare(\n `SELECT * FROM tool_calls WHERE created_at >= ? ORDER BY created_at DESC`,\n )\n\n const countToolCallsByTool = db.prepare(\n `SELECT tool_name, COUNT(*) as count, COALESCE(SUM(tokens_estimated), 0) as tokens\n FROM tool_calls WHERE created_at >= ?\n GROUP BY tool_name ORDER BY tokens DESC`,\n )\n\n const countToolCallsBySource = db.prepare(\n `SELECT source, COUNT(*) as count, COALESCE(SUM(tokens_estimated), 0) as tokens\n FROM tool_calls WHERE created_at >= ?\n GROUP BY source ORDER BY tokens DESC`,\n )\n\n const sumTokensBySession = db.prepare(\n `SELECT COALESCE(SUM(tokens_estimated), 0) as total\n FROM tool_calls WHERE session_id = ?`,\n )\n\n const countToolCallsBySessionStmt = db.prepare(\n `SELECT COUNT(*) as c FROM tool_calls WHERE session_id = ?`,\n )\n\n const sumTokensBySessionSince = db.prepare(\n `SELECT COALESCE(SUM(tokens_estimated), 0) as total\n FROM tool_calls WHERE session_id = ? AND created_at >= ?`,\n )\n\n const sumTokensByProjectSince = db.prepare(\n `SELECT COALESCE(SUM(tc.tokens_estimated), 0) as total\n FROM tool_calls tc\n JOIN sessions s ON tc.session_id = s.id\n WHERE s.project_hash = ? AND tc.created_at >= ?`,\n )\n\n const insertToolCallMany = db.transaction((events: readonly ToolEvent[]) => {\n for (const e of events) {\n insertToolCall.run(\n e.session_id,\n e.tool_name,\n e.source,\n e.output_bytes,\n e.tokens_estimated,\n e.tokens_actual,\n e.duration_ms,\n e.estimation_method,\n e.shadow_delta_tokens ?? null,\n e.created_at,\n )\n }\n })\n\n // ── serena_symbol_touches ──\n const insertSerenaTouch = db.prepare(\n `INSERT INTO serena_symbol_touches (session_id, tool_name, relative_path, name_path)\n VALUES (?, ?, ?, ?)`,\n )\n\n const getRecentSerenaSymbols = db.prepare(\n `SELECT tool_name, relative_path, name_path, created_at\n FROM serena_symbol_touches\n WHERE session_id = ?\n ORDER BY created_at DESC LIMIT ?`,\n )\n\n // ── meta ──\n // ── rtk_rewrites ──\n const insertRtkRewrite = db.prepare(\n `INSERT INTO rtk_rewrites (session_id, command_hash, rewritten_to) VALUES (?, ?, ?)`,\n )\n // Consume (look up AND delete) the most recent matching mark. If the PostToolUse\n // hook finds a mark for this (session, command_hash), the call was rewritten by\n // rtk in PreToolUse and must be reclassified as source=rtk. We bound the lookup\n // to 60 s so stale marks never poison future events.\n const findRtkMark = db.prepare(\n `SELECT id, rewritten_to FROM rtk_rewrites\n WHERE session_id = ? AND command_hash = ?\n AND created_at >= datetime('now', '-60 seconds')\n ORDER BY id DESC LIMIT 1`,\n )\n const deleteRtkMarkById = db.prepare(`DELETE FROM rtk_rewrites WHERE id = ?`)\n const purgeStaleRtkMarks = db.prepare(\n `DELETE FROM rtk_rewrites WHERE created_at < datetime('now', '-60 seconds')`,\n )\n\n const upsertMetaCounter = db.prepare(\n `INSERT INTO meta (key, value) VALUES (?, '1')\n ON CONFLICT(key) DO UPDATE SET value = CAST(value AS INTEGER) + 1`,\n )\n\n const getMeta = db.prepare(`SELECT value FROM meta WHERE key = ?`)\n\n // ── budgets ──\n const upsertBudget = db.prepare(\n `INSERT INTO budgets (scope, scope_key, limit_tokens, mode)\n VALUES (?, ?, ?, ?)\n ON CONFLICT(scope, scope_key) DO UPDATE SET\n limit_tokens = excluded.limit_tokens,\n mode = excluded.mode`,\n )\n\n const selectBudgetByScope = db.prepare(\n `SELECT id, scope, scope_key, limit_tokens, spent_tokens, mode, created_at\n FROM budgets WHERE scope = ? AND scope_key = ?`,\n )\n\n const deleteBudgetByScope = db.prepare(\n `DELETE FROM budgets WHERE scope = ? AND scope_key = ?`,\n )\n\n const listBudgets = db.prepare(\n `SELECT id, scope, scope_key, limit_tokens, spent_tokens, mode, created_at\n FROM budgets ORDER BY created_at DESC`,\n )\n\n const insertBudgetEvent = db.prepare(\n `INSERT INTO budget_events (budget_id, event_type, tokens) VALUES (?, ?, ?)`,\n )\n\n return {\n // sessions\n insertSession(id: string, projectHash: string | null) {\n insertSession.run(id, projectHash)\n },\n\n // tool_calls\n insertToolCall(event: ToolEvent) {\n insertToolCall.run(\n event.session_id,\n event.tool_name,\n event.source,\n event.output_bytes,\n event.tokens_estimated,\n event.tokens_actual,\n event.duration_ms,\n event.estimation_method,\n event.shadow_delta_tokens ?? null,\n event.created_at,\n )\n },\n insertToolCallMany(events: readonly ToolEvent[]) {\n insertToolCallMany(events)\n },\n getToolCallsSince(timestamp: string) {\n return getToolCallsSince.all(timestamp)\n },\n countToolCallsByTool(since: string): ToolCountRow[] {\n return countToolCallsByTool.all(since) as ToolCountRow[]\n },\n countToolCallsBySource(since: string): SourceCountRow[] {\n return countToolCallsBySource.all(since) as SourceCountRow[]\n },\n sumTokensBySession(sessionId: string): number {\n const row = sumTokensBySession.get(sessionId) as { total: number } | undefined\n return row?.total ?? 0\n },\n countToolCallsBySession(sessionId: string): number {\n const row = countToolCallsBySessionStmt.get(sessionId) as { c: number } | undefined\n return row?.c ?? 0\n },\n sumTokensBySessionSince(sessionId: string, since: string): number {\n const row = sumTokensBySessionSince.get(sessionId, since) as { total: number } | undefined\n return row?.total ?? 0\n },\n sumTokensByProjectSince(projectHash: string, since: string): number {\n const row = sumTokensByProjectSince.get(projectHash, since) as { total: number } | undefined\n return row?.total ?? 0\n },\n\n // meta\n upsertMetaCounter(key: string) {\n upsertMetaCounter.run(key)\n },\n getMeta(key: string): string | null {\n const row = getMeta.get(key) as { value: string } | undefined\n return row?.value ?? null\n },\n\n // budgets\n upsertBudget(scope: BudgetScope, scopeKey: string, limitTokens: number, mode: BudgetMode) {\n upsertBudget.run(scope, scopeKey, limitTokens, mode)\n },\n getBudgetByScope(scope: BudgetScope, scopeKey: string): Budget | null {\n const row = selectBudgetByScope.get(scope, scopeKey) as Budget | undefined\n return row ?? null\n },\n deleteBudgetByScope(scope: BudgetScope, scopeKey: string): number {\n const info = deleteBudgetByScope.run(scope, scopeKey)\n return info.changes as number\n },\n listBudgets(): Budget[] {\n return listBudgets.all() as Budget[]\n },\n insertBudgetEvent(budgetId: number, eventType: string, tokens: number | null) {\n insertBudgetEvent.run(budgetId, eventType, tokens)\n },\n\n // rtk rewrites\n insertRtkRewrite(sessionId: string, commandHash: string, rewrittenTo: string) {\n insertRtkRewrite.run(sessionId, commandHash, rewrittenTo)\n },\n /**\n * Look up and consume an RTK rewrite mark. Returns the rewritten command if\n * there was a mark for this (session, hash) within the last 60 s, or null.\n * The row is deleted on match so it cannot be double-consumed.\n */\n consumeRtkRewrite(sessionId: string, commandHash: string): string | null {\n const row = findRtkMark.get(sessionId, commandHash) as\n | { id: number; rewritten_to: string }\n | undefined\n if (!row) return null\n deleteRtkMarkById.run(row.id)\n return row.rewritten_to\n },\n purgeStaleRtkMarks(): number {\n const info = purgeStaleRtkMarks.run()\n return info.changes as number\n },\n\n // serena symbol touches\n insertSerenaTouch(\n sessionId: string,\n toolName: string,\n relativePath: string,\n namePath: string | null,\n ) {\n insertSerenaTouch.run(sessionId, toolName, relativePath, namePath)\n },\n getRecentSerenaSymbols(\n sessionId: string,\n limit: number,\n ): { tool_name: string; relative_path: string; name_path: string | null; created_at: string }[] {\n return getRecentSerenaSymbols.all(sessionId, limit) as {\n tool_name: string\n relative_path: string\n name_path: string | null\n created_at: string\n }[]\n },\n }\n}\n\nexport type Queries = ReturnType<typeof buildQueries>\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| // src/db/connection.ts | ||
| import Database from "better-sqlite3"; | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| // src/db/schema.ts | ||
| var SCHEMA_SQL = ` | ||
| CREATE TABLE IF NOT EXISTS sessions ( | ||
| id TEXT PRIMARY KEY, | ||
| project_hash TEXT, | ||
| started_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS tool_calls ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, | ||
| tool_name TEXT NOT NULL, | ||
| source TEXT NOT NULL, | ||
| output_bytes INTEGER NOT NULL, | ||
| tokens_estimated INTEGER NOT NULL, | ||
| tokens_actual INTEGER, | ||
| duration_ms INTEGER, | ||
| estimation_method TEXT, | ||
| shadow_delta_tokens INTEGER, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, created_at DESC); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_source ON tool_calls(source); | ||
| CREATE TABLE IF NOT EXISTS budgets ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| scope TEXT NOT NULL CHECK(scope IN ('session', 'project')), | ||
| scope_key TEXT NOT NULL, | ||
| limit_tokens INTEGER NOT NULL CHECK(limit_tokens > 0), | ||
| spent_tokens INTEGER NOT NULL DEFAULT 0, | ||
| mode TEXT NOT NULL CHECK(mode IN ('warn')), | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')), | ||
| UNIQUE(scope, scope_key) | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS budget_events ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| budget_id INTEGER NOT NULL REFERENCES budgets(id) ON DELETE CASCADE, | ||
| event_type TEXT NOT NULL CHECK(event_type IN ('spend', 'warn', 'reset')), | ||
| tokens INTEGER, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS optimization_snapshots ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| method TEXT NOT NULL, | ||
| details TEXT, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE TABLE IF NOT EXISTS coach_surface_log ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, | ||
| rule_id TEXT NOT NULL, | ||
| tip_id TEXT NOT NULL, | ||
| surfaced_via TEXT NOT NULL, | ||
| severity TEXT NOT NULL, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_coach_surface_log_session | ||
| ON coach_surface_log(session_id, created_at DESC); | ||
| CREATE TABLE IF NOT EXISTS meta ( | ||
| key TEXT PRIMARY KEY, | ||
| value TEXT NOT NULL | ||
| ); | ||
| -- Pending RTK rewrites. PreToolUse writes a row when it rewrites a Bash command | ||
| -- via rtk rewrite; PostToolUse consumes it to reclassify the event as source=rtk. | ||
| -- The PostToolUse hook only sees the ORIGINAL command (what Claude asked for), | ||
| -- not the command Claude Code actually executed after PreToolUse mutation. This | ||
| -- table bridges the gap so stats reflect the real RTK activity. | ||
| CREATE TABLE IF NOT EXISTS rtk_rewrites ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL, | ||
| command_hash TEXT NOT NULL, | ||
| rewritten_to TEXT NOT NULL, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_rtk_rewrites_lookup | ||
| ON rtk_rewrites(session_id, command_hash, created_at DESC); | ||
| -- Serena symbol touches. PostToolUse writes a row for each Serena read tool call | ||
| -- that includes a relative_path, so SessionStart:compact can re-inject relevant | ||
| -- symbols when context is reset. | ||
| CREATE TABLE IF NOT EXISTS serena_symbol_touches ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, | ||
| tool_name TEXT NOT NULL, | ||
| relative_path TEXT NOT NULL, | ||
| name_path TEXT, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| CREATE INDEX IF NOT EXISTS idx_serena_symbol_touches_session | ||
| ON serena_symbol_touches(session_id, created_at DESC); | ||
| `; | ||
| // src/db/connection.ts | ||
| var dbInstance = null; | ||
| var currentPath = null; | ||
| function migrateLegacyToolCallsSchema(db) { | ||
| const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all(); | ||
| if (cols.length === 0) return; | ||
| const hasInputHash = cols.some((c) => c.name === "input_hash"); | ||
| if (!hasInputHash) return; | ||
| db.exec(` | ||
| DROP TRIGGER IF EXISTS tool_calls_ai; | ||
| DROP TRIGGER IF EXISTS tool_calls_ad; | ||
| DROP TABLE IF EXISTS events_fts; | ||
| `); | ||
| const migrate = db.transaction(() => { | ||
| db.exec(` | ||
| CREATE TABLE tool_calls_new ( | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, | ||
| tool_name TEXT NOT NULL, | ||
| source TEXT NOT NULL, | ||
| output_bytes INTEGER NOT NULL, | ||
| tokens_estimated INTEGER NOT NULL, | ||
| tokens_actual INTEGER, | ||
| duration_ms INTEGER, | ||
| estimation_method TEXT, | ||
| created_at TEXT NOT NULL DEFAULT (datetime('now')) | ||
| ); | ||
| INSERT INTO tool_calls_new ( | ||
| id, session_id, tool_name, source, output_bytes, | ||
| tokens_estimated, tokens_actual, duration_ms, estimation_method, created_at | ||
| ) | ||
| SELECT id, session_id, tool_name, source, output_bytes, | ||
| tokens_estimated, tokens_actual, duration_ms, estimation_method, created_at | ||
| FROM tool_calls; | ||
| DROP TABLE tool_calls; | ||
| ALTER TABLE tool_calls_new RENAME TO tool_calls; | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, created_at DESC); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name); | ||
| CREATE INDEX IF NOT EXISTS idx_tool_calls_source ON tool_calls(source); | ||
| `); | ||
| }); | ||
| migrate(); | ||
| } | ||
| function migrateAddShadowDeltaTokens(db) { | ||
| const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all(); | ||
| if (cols.some((c) => c.name === "shadow_delta_tokens")) return; | ||
| db.exec(`ALTER TABLE tool_calls ADD COLUMN shadow_delta_tokens INTEGER`); | ||
| } | ||
| function getDb(dbPath) { | ||
| const resolvedPath = dbPath ?? ":memory:"; | ||
| if (dbInstance && currentPath === resolvedPath) { | ||
| return dbInstance; | ||
| } | ||
| if (dbInstance && currentPath !== resolvedPath) { | ||
| try { | ||
| dbInstance.close(); | ||
| } catch { | ||
| } | ||
| dbInstance = null; | ||
| } | ||
| if (resolvedPath !== ":memory:") { | ||
| const dir = path.dirname(resolvedPath); | ||
| if (!fs.existsSync(dir)) { | ||
| fs.mkdirSync(dir, { recursive: true }); | ||
| } | ||
| } | ||
| const db = new Database(resolvedPath); | ||
| db.pragma("journal_mode = WAL"); | ||
| db.pragma("foreign_keys = ON"); | ||
| db.exec(SCHEMA_SQL); | ||
| migrateLegacyToolCallsSchema(db); | ||
| migrateAddShadowDeltaTokens(db); | ||
| dbInstance = db; | ||
| currentPath = resolvedPath; | ||
| return db; | ||
| } | ||
| export { | ||
| getDb | ||
| }; | ||
| //# sourceMappingURL=chunk-L4GG4274.js.map |
| {"version":3,"sources":["../src/db/connection.ts","../src/db/schema.ts"],"sourcesContent":["// getDb() singleton with WAL + FK — Phase 1.3\n\nimport Database from 'better-sqlite3'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { SCHEMA_SQL } from './schema.js'\n\ntype DB = Database.Database\n\nlet dbInstance: DB | null = null\nlet currentPath: string | null = null\n\n/**\n * Migrate legacy tool_calls schema to the current simplified one.\n *\n * DBs created before Phase 1.2 had: input_hash TEXT NOT NULL, tool_input_summary TEXT, content TEXT,\n * plus FTS5 virtual table events_fts with AFTER INSERT/DELETE triggers referencing `content`.\n *\n * The current code no longer passes input_hash or content, so inserts on legacy DBs fail silently\n * with \"NOT NULL constraint failed: tool_calls.input_hash\" — data is lost, droppedEvents counter grows.\n *\n * This migration rebuilds tool_calls with the current schema, preserving historical rows.\n */\nfunction migrateLegacyToolCallsSchema(db: DB): void {\n const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all() as Array<{ name: string }>\n if (cols.length === 0) return // table didn't exist yet, SCHEMA_SQL just created it clean\n const hasInputHash = cols.some((c) => c.name === 'input_hash')\n if (!hasInputHash) return // already migrated\n\n // Drop FTS5 triggers that reference `content` before we touch the table\n db.exec(`\n DROP TRIGGER IF EXISTS tool_calls_ai;\n DROP TRIGGER IF EXISTS tool_calls_ad;\n DROP TABLE IF EXISTS events_fts;\n `)\n\n // Rebuild tool_calls with the new schema. Wrap in a transaction for atomicity.\n const migrate = db.transaction(() => {\n db.exec(`\n CREATE TABLE tool_calls_new (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\n tool_name TEXT NOT NULL,\n source TEXT NOT NULL,\n output_bytes INTEGER NOT NULL,\n tokens_estimated INTEGER NOT NULL,\n tokens_actual INTEGER,\n duration_ms INTEGER,\n estimation_method TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n );\n INSERT INTO tool_calls_new (\n id, session_id, tool_name, source, output_bytes,\n tokens_estimated, tokens_actual, duration_ms, estimation_method, created_at\n )\n SELECT id, session_id, tool_name, source, output_bytes,\n tokens_estimated, tokens_actual, duration_ms, estimation_method, created_at\n FROM tool_calls;\n DROP TABLE tool_calls;\n ALTER TABLE tool_calls_new RENAME TO tool_calls;\n CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, created_at DESC);\n CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name);\n CREATE INDEX IF NOT EXISTS idx_tool_calls_source ON tool_calls(source);\n `)\n })\n migrate()\n}\n\n/** Add shadow_delta_tokens column to existing DBs that were created before Sprint C. */\nfunction migrateAddShadowDeltaTokens(db: DB): void {\n const cols = db.prepare(`PRAGMA table_info('tool_calls')`).all() as Array<{ name: string }>\n if (cols.some((c) => c.name === 'shadow_delta_tokens')) return // already present\n db.exec(`ALTER TABLE tool_calls ADD COLUMN shadow_delta_tokens INTEGER`)\n}\n\nexport function getDb(dbPath?: string): DB {\n const resolvedPath = dbPath ?? ':memory:'\n if (dbInstance && currentPath === resolvedPath) {\n return dbInstance\n }\n if (dbInstance && currentPath !== resolvedPath) {\n try {\n dbInstance.close()\n } catch {\n // swallow\n }\n dbInstance = null\n }\n if (resolvedPath !== ':memory:') {\n const dir = path.dirname(resolvedPath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n }\n const db = new Database(resolvedPath)\n db.pragma('journal_mode = WAL')\n db.pragma('foreign_keys = ON')\n db.exec(SCHEMA_SQL)\n migrateLegacyToolCallsSchema(db)\n migrateAddShadowDeltaTokens(db)\n dbInstance = db\n currentPath = resolvedPath\n return db\n}\n\nexport function closeDb(): void {\n if (dbInstance) {\n try {\n dbInstance.close()\n } catch {\n // swallow\n }\n dbInstance = null\n currentPath = null\n }\n}\n","// SQLite schema — Phase 1.2 (simplified: removed FTS5, input_hash, tool_input_summary, content)\n// Covers: sessions, tool_calls, budgets, budget_events, optimization_snapshots, coach_surface_log, meta\n\nexport const SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n project_hash TEXT,\n started_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS tool_calls (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\n tool_name TEXT NOT NULL,\n source TEXT NOT NULL,\n output_bytes INTEGER NOT NULL,\n tokens_estimated INTEGER NOT NULL,\n tokens_actual INTEGER,\n duration_ms INTEGER,\n estimation_method TEXT,\n shadow_delta_tokens INTEGER,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool_name);\nCREATE INDEX IF NOT EXISTS idx_tool_calls_source ON tool_calls(source);\n\nCREATE TABLE IF NOT EXISTS budgets (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n scope TEXT NOT NULL CHECK(scope IN ('session', 'project')),\n scope_key TEXT NOT NULL,\n limit_tokens INTEGER NOT NULL CHECK(limit_tokens > 0),\n spent_tokens INTEGER NOT NULL DEFAULT 0,\n mode TEXT NOT NULL CHECK(mode IN ('warn')),\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n UNIQUE(scope, scope_key)\n);\n\nCREATE TABLE IF NOT EXISTS budget_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n budget_id INTEGER NOT NULL REFERENCES budgets(id) ON DELETE CASCADE,\n event_type TEXT NOT NULL CHECK(event_type IN ('spend', 'warn', 'reset')),\n tokens INTEGER,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS optimization_snapshots (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n method TEXT NOT NULL,\n details TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS coach_surface_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\n rule_id TEXT NOT NULL,\n tip_id TEXT NOT NULL,\n surfaced_via TEXT NOT NULL,\n severity TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_coach_surface_log_session\n ON coach_surface_log(session_id, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\n-- Pending RTK rewrites. PreToolUse writes a row when it rewrites a Bash command\n-- via rtk rewrite; PostToolUse consumes it to reclassify the event as source=rtk.\n-- The PostToolUse hook only sees the ORIGINAL command (what Claude asked for),\n-- not the command Claude Code actually executed after PreToolUse mutation. This\n-- table bridges the gap so stats reflect the real RTK activity.\nCREATE TABLE IF NOT EXISTS rtk_rewrites (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n command_hash TEXT NOT NULL,\n rewritten_to TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_rtk_rewrites_lookup\n ON rtk_rewrites(session_id, command_hash, created_at DESC);\n\n-- Serena symbol touches. PostToolUse writes a row for each Serena read tool call\n-- that includes a relative_path, so SessionStart:compact can re-inject relevant\n-- symbols when context is reset.\nCREATE TABLE IF NOT EXISTS serena_symbol_touches (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,\n tool_name TEXT NOT NULL,\n relative_path TEXT NOT NULL,\n name_path TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE INDEX IF NOT EXISTS idx_serena_symbol_touches_session\n ON serena_symbol_touches(session_id, created_at DESC);\n`\n"],"mappings":";;;AAEA,OAAO,cAAc;AACrB,OAAO,QAAQ;AACf,OAAO,UAAU;;;ACDV,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ADM1B,IAAI,aAAwB;AAC5B,IAAI,cAA6B;AAajC,SAAS,6BAA6B,IAAc;AAClD,QAAM,OAAO,GAAG,QAAQ,iCAAiC,EAAE,IAAI;AAC/D,MAAI,KAAK,WAAW,EAAG;AACvB,QAAM,eAAe,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,YAAY;AAC7D,MAAI,CAAC,aAAc;AAGnB,KAAG,KAAK;AAAA;AAAA;AAAA;AAAA,GAIP;AAGD,QAAM,UAAU,GAAG,YAAY,MAAM;AACnC,OAAG,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAyBP;AAAA,EACH,CAAC;AACD,UAAQ;AACV;AAGA,SAAS,4BAA4B,IAAc;AACjD,QAAM,OAAO,GAAG,QAAQ,iCAAiC,EAAE,IAAI;AAC/D,MAAI,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,qBAAqB,EAAG;AACxD,KAAG,KAAK,+DAA+D;AACzE;AAEO,SAAS,MAAM,QAAqB;AACzC,QAAM,eAAe,UAAU;AAC/B,MAAI,cAAc,gBAAgB,cAAc;AAC9C,WAAO;AAAA,EACT;AACA,MAAI,cAAc,gBAAgB,cAAc;AAC9C,QAAI;AACF,iBAAW,MAAM;AAAA,IACnB,QAAQ;AAAA,IAER;AACA,iBAAa;AAAA,EACf;AACA,MAAI,iBAAiB,YAAY;AAC/B,UAAM,MAAM,KAAK,QAAQ,YAAY;AACrC,QAAI,CAAC,GAAG,WAAW,GAAG,GAAG;AACvB,SAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,IACvC;AAAA,EACF;AACA,QAAM,KAAK,IAAI,SAAS,YAAY;AACpC,KAAG,OAAO,oBAAoB;AAC9B,KAAG,OAAO,mBAAmB;AAC7B,KAAG,KAAK,UAAU;AAClB,+BAA6B,EAAE;AAC/B,8BAA4B,EAAE;AAC9B,eAAa;AACb,gBAAc;AACd,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FT4EJUIT.js"; | ||
| // src/services/budget-manager.ts | ||
| var MAX_TOKENS = 1e7; | ||
| var BudgetManager = class { | ||
| queries; | ||
| constructor(db) { | ||
| this.queries = buildQueries(db); | ||
| } | ||
| setBudget(input) { | ||
| if (!Number.isInteger(input.limit_tokens)) { | ||
| throw new Error("limit_tokens debe ser un entero"); | ||
| } | ||
| if (input.limit_tokens <= 0) { | ||
| throw new Error("limit_tokens debe ser mayor que 0"); | ||
| } | ||
| if (input.limit_tokens > MAX_TOKENS) { | ||
| throw new Error(`limit_tokens no puede exceder ${MAX_TOKENS}`); | ||
| } | ||
| if (input.scope !== "session" && input.scope !== "project") { | ||
| throw new Error(`scope invalido: ${String(input.scope)}`); | ||
| } | ||
| const mode = "warn"; | ||
| this.queries.upsertBudget(input.scope, input.scope_key, input.limit_tokens, mode); | ||
| const budget = this.queries.getBudgetByScope(input.scope, input.scope_key); | ||
| if (!budget) { | ||
| throw new Error("no se pudo leer el budget recien creado"); | ||
| } | ||
| return budget; | ||
| } | ||
| /** | ||
| * Resolve the active budget for a given session + project. | ||
| * Returns the session-scoped budget if present, else the project-scoped one, else null. | ||
| */ | ||
| getActiveBudget(sessionId, projectHash) { | ||
| const sessionBudget = this.queries.getBudgetByScope("session", sessionId); | ||
| if (sessionBudget) return sessionBudget; | ||
| if (projectHash) { | ||
| const projectBudget = this.queries.getBudgetByScope("project", projectHash); | ||
| if (projectBudget) return projectBudget; | ||
| } | ||
| return null; | ||
| } | ||
| /** Compute spent tokens against a budget from tool_calls since the budget creation timestamp. */ | ||
| computeSpent(budget) { | ||
| if (budget.scope === "session") { | ||
| return this.queries.sumTokensBySessionSince(budget.scope_key, budget.created_at); | ||
| } | ||
| return this.queries.sumTokensByProjectSince(budget.scope_key, budget.created_at); | ||
| } | ||
| checkBudget(sessionId, projectHash) { | ||
| const active = this.getActiveBudget(sessionId, projectHash); | ||
| if (!active) { | ||
| return { active: false, spent: 0, remaining: 0, percent_used: 0, mode: null }; | ||
| } | ||
| const spent = this.computeSpent(active); | ||
| const remaining = Math.max(0, active.limit_tokens - spent); | ||
| const percent = active.limit_tokens > 0 ? spent / active.limit_tokens : 0; | ||
| return { | ||
| active: true, | ||
| spent, | ||
| remaining, | ||
| percent_used: percent, | ||
| mode: active.mode | ||
| }; | ||
| } | ||
| clearBudget(scope, scopeKey) { | ||
| return this.queries.deleteBudgetByScope(scope, scopeKey) > 0; | ||
| } | ||
| getBudgetReport(since) { | ||
| return { | ||
| by_tool: this.queries.countToolCallsByTool(since), | ||
| by_source: this.queries.countToolCallsBySource(since), | ||
| period_since: since | ||
| }; | ||
| } | ||
| recordBudgetEvent(budgetId, eventType, tokens) { | ||
| this.queries.insertBudgetEvent(budgetId, eventType, tokens); | ||
| } | ||
| }; | ||
| export { | ||
| BudgetManager | ||
| }; | ||
| //# sourceMappingURL=chunk-XAYSOAZV.js.map |
| {"version":3,"sources":["../src/services/budget-manager.ts"],"sourcesContent":["// Budget manager — Phase 2.1 + 2.3\n// Precedence: session > project. Spent tokens computed from tool_calls since budget.created_at.\n\nimport type Database from 'better-sqlite3'\nimport type {\n Budget,\n BudgetScope,\n BudgetMode,\n BudgetStatus,\n} from '../lib/types.js'\nimport { buildQueries, type Queries, type ToolCountRow, type SourceCountRow } from '../db/queries.js'\n\ntype DB = Database.Database\n\nexport interface SetBudgetInput {\n scope: BudgetScope\n scope_key: string\n limit_tokens: number\n mode?: BudgetMode\n}\n\nexport interface BudgetReport {\n by_tool: ToolCountRow[]\n by_source: SourceCountRow[]\n period_since: string\n}\n\nconst MAX_TOKENS = 1e7\n\nexport class BudgetManager {\n private queries: Queries\n\n constructor(db: DB) {\n this.queries = buildQueries(db)\n }\n\n setBudget(input: SetBudgetInput): Budget {\n if (!Number.isInteger(input.limit_tokens)) {\n throw new Error('limit_tokens debe ser un entero')\n }\n if (input.limit_tokens <= 0) {\n throw new Error('limit_tokens debe ser mayor que 0')\n }\n if (input.limit_tokens > MAX_TOKENS) {\n throw new Error(`limit_tokens no puede exceder ${MAX_TOKENS}`)\n }\n if (input.scope !== 'session' && input.scope !== 'project') {\n throw new Error(`scope invalido: ${String(input.scope)}`)\n }\n const mode: BudgetMode = 'warn'\n this.queries.upsertBudget(input.scope, input.scope_key, input.limit_tokens, mode)\n const budget = this.queries.getBudgetByScope(input.scope, input.scope_key)\n if (!budget) {\n throw new Error('no se pudo leer el budget recien creado')\n }\n return budget\n }\n\n /**\n * Resolve the active budget for a given session + project.\n * Returns the session-scoped budget if present, else the project-scoped one, else null.\n */\n getActiveBudget(sessionId: string, projectHash: string | null): Budget | null {\n const sessionBudget = this.queries.getBudgetByScope('session', sessionId)\n if (sessionBudget) return sessionBudget\n if (projectHash) {\n const projectBudget = this.queries.getBudgetByScope('project', projectHash)\n if (projectBudget) return projectBudget\n }\n return null\n }\n\n /** Compute spent tokens against a budget from tool_calls since the budget creation timestamp. */\n computeSpent(budget: Budget): number {\n if (budget.scope === 'session') {\n return this.queries.sumTokensBySessionSince(budget.scope_key, budget.created_at)\n }\n return this.queries.sumTokensByProjectSince(budget.scope_key, budget.created_at)\n }\n\n checkBudget(sessionId: string, projectHash: string | null): BudgetStatus {\n const active = this.getActiveBudget(sessionId, projectHash)\n if (!active) {\n return { active: false, spent: 0, remaining: 0, percent_used: 0, mode: null }\n }\n const spent = this.computeSpent(active)\n const remaining = Math.max(0, active.limit_tokens - spent)\n const percent = active.limit_tokens > 0 ? spent / active.limit_tokens : 0\n return {\n active: true,\n spent,\n remaining,\n percent_used: percent,\n mode: active.mode,\n }\n }\n\n clearBudget(scope: BudgetScope, scopeKey: string): boolean {\n return this.queries.deleteBudgetByScope(scope, scopeKey) > 0\n }\n\n getBudgetReport(since: string): BudgetReport {\n return {\n by_tool: this.queries.countToolCallsByTool(since),\n by_source: this.queries.countToolCallsBySource(since),\n period_since: since,\n }\n }\n\n recordBudgetEvent(\n budgetId: number,\n eventType: 'spend' | 'warn' | 'block' | 'reset',\n tokens: number | null,\n ): void {\n this.queries.insertBudgetEvent(budgetId, eventType, tokens)\n }\n}\n"],"mappings":";;;;;;AA2BA,IAAM,aAAa;AAEZ,IAAM,gBAAN,MAAoB;AAAA,EACjB;AAAA,EAER,YAAY,IAAQ;AAClB,SAAK,UAAU,aAAa,EAAE;AAAA,EAChC;AAAA,EAEA,UAAU,OAA+B;AACvC,QAAI,CAAC,OAAO,UAAU,MAAM,YAAY,GAAG;AACzC,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AACA,QAAI,MAAM,gBAAgB,GAAG;AAC3B,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AACA,QAAI,MAAM,eAAe,YAAY;AACnC,YAAM,IAAI,MAAM,iCAAiC,UAAU,EAAE;AAAA,IAC/D;AACA,QAAI,MAAM,UAAU,aAAa,MAAM,UAAU,WAAW;AAC1D,YAAM,IAAI,MAAM,mBAAmB,OAAO,MAAM,KAAK,CAAC,EAAE;AAAA,IAC1D;AACA,UAAM,OAAmB;AACzB,SAAK,QAAQ,aAAa,MAAM,OAAO,MAAM,WAAW,MAAM,cAAc,IAAI;AAChF,UAAM,SAAS,KAAK,QAAQ,iBAAiB,MAAM,OAAO,MAAM,SAAS;AACzE,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB,WAAmB,aAA2C;AAC5E,UAAM,gBAAgB,KAAK,QAAQ,iBAAiB,WAAW,SAAS;AACxE,QAAI,cAAe,QAAO;AAC1B,QAAI,aAAa;AACf,YAAM,gBAAgB,KAAK,QAAQ,iBAAiB,WAAW,WAAW;AAC1E,UAAI,cAAe,QAAO;AAAA,IAC5B;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,aAAa,QAAwB;AACnC,QAAI,OAAO,UAAU,WAAW;AAC9B,aAAO,KAAK,QAAQ,wBAAwB,OAAO,WAAW,OAAO,UAAU;AAAA,IACjF;AACA,WAAO,KAAK,QAAQ,wBAAwB,OAAO,WAAW,OAAO,UAAU;AAAA,EACjF;AAAA,EAEA,YAAY,WAAmB,aAA0C;AACvE,UAAM,SAAS,KAAK,gBAAgB,WAAW,WAAW;AAC1D,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,QAAQ,OAAO,OAAO,GAAG,WAAW,GAAG,cAAc,GAAG,MAAM,KAAK;AAAA,IAC9E;AACA,UAAM,QAAQ,KAAK,aAAa,MAAM;AACtC,UAAM,YAAY,KAAK,IAAI,GAAG,OAAO,eAAe,KAAK;AACzD,UAAM,UAAU,OAAO,eAAe,IAAI,QAAQ,OAAO,eAAe;AACxE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,cAAc;AAAA,MACd,MAAM,OAAO;AAAA,IACf;AAAA,EACF;AAAA,EAEA,YAAY,OAAoB,UAA2B;AACzD,WAAO,KAAK,QAAQ,oBAAoB,OAAO,QAAQ,IAAI;AAAA,EAC7D;AAAA,EAEA,gBAAgB,OAA6B;AAC3C,WAAO;AAAA,MACL,SAAS,KAAK,QAAQ,qBAAqB,KAAK;AAAA,MAChD,WAAW,KAAK,QAAQ,uBAAuB,KAAK;AAAA,MACpD,cAAc;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,kBACE,UACA,WACA,QACM;AACN,SAAK,QAAQ,kBAAkB,UAAU,WAAW,MAAM;AAAA,EAC5D;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| clearSurfaceLog, | ||
| measureContextSize, | ||
| runRules | ||
| } from "./chunk-4PA7MRTS.js"; | ||
| import "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FT4EJUIT.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-L4GG4274.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/coach.ts | ||
| import fs from "fs"; | ||
| async function runCoachCli(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const sub = args[0] ?? "status"; | ||
| if (sub === "list") { | ||
| print(`Knowledge base (${KNOWLEDGE_BASE.length} tips):`); | ||
| for (const tip of KNOWLEDGE_BASE) { | ||
| print(` \u2022 ${tip.id.padEnd(32)} ${tip.title}`); | ||
| } | ||
| return 0; | ||
| } | ||
| if (sub === "explain") { | ||
| const tipId = args[1]; | ||
| if (!tipId) { | ||
| print("Uso: token-optimizer-mcp coach explain <tip_id>"); | ||
| return 1; | ||
| } | ||
| const tip = KNOWLEDGE_BASE.find((t) => t.id === tipId); | ||
| if (!tip) { | ||
| print(`Tip no encontrado: ${tipId}`); | ||
| return 1; | ||
| } | ||
| print(tip.title); | ||
| print(""); | ||
| print(tip.description); | ||
| print(""); | ||
| print(`Como usarlo: ${tip.how_to_invoke}`); | ||
| print(`Cuando: ${tip.when_applicable}`); | ||
| print(`Ahorro: ${tip.savings_estimate}`); | ||
| print(`Fuente: ${tip.savings_source} \xB7 verificado: ${tip.verified_at}`); | ||
| return 0; | ||
| } | ||
| if (sub === "reset") { | ||
| const projectDir2 = resolveProjectDir(cwd); | ||
| const dbPath2 = resolveAnalyticsDbPath(projectDir2); | ||
| if (!fs.existsSync(dbPath2)) { | ||
| print("Sin DB; nada que resetear."); | ||
| return 0; | ||
| } | ||
| const db2 = getDb(dbPath2); | ||
| const deleted = clearSurfaceLog(db2); | ||
| print(`Log de coach reseteado (${deleted} entradas eliminadas)`); | ||
| return 0; | ||
| } | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) { | ||
| print("Coach status: sin datos. Ejecuta el hook posttooluse al menos una vez."); | ||
| return 0; | ||
| } | ||
| const db = getDb(dbPath); | ||
| const contextOpts = { db }; | ||
| if (projectDir) contextOpts.projectDir = projectDir; | ||
| const context = await measureContextSize("default", contextOpts); | ||
| const queries = buildQueries(db); | ||
| const since = new Date(Date.now() - 864e5).toISOString(); | ||
| const rawRows = queries.getToolCallsSince(since); | ||
| const ctx = { | ||
| session_id: "default", | ||
| events: rawRows.slice(0, 100), | ||
| session_token_total: context.tokens, | ||
| session_token_method: context.estimation_method, | ||
| session_token_limit: context.limit, | ||
| active_model: null | ||
| }; | ||
| const hits = runRules(ctx); | ||
| print("token-optimizer-mcp coach status"); | ||
| print(""); | ||
| print( | ||
| `Contexto: ${(context.percent * 100).toFixed(1)}% (${context.tokens}/${context.limit} tokens, ${context.estimation_method})` | ||
| ); | ||
| print(`Tips activos: ${hits.length}`); | ||
| if (hits.length === 0) { | ||
| print(" (sin tips disparados en este momento)"); | ||
| } else { | ||
| for (const h of hits) { | ||
| print(` [${h.severity}] ${h.rule_id}: ${h.evidence}`); | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
| export { | ||
| runCoachCli | ||
| }; | ||
| //# sourceMappingURL=coach-6OD6QT3Y.js.map |
| {"version":3,"sources":["../src/cli/coach.ts"],"sourcesContent":["// Coach CLI — Phase 4.50\n// Subcommands: status | list | explain <tip_id> | reset\n\nimport fs from 'node:fs'\nimport { KNOWLEDGE_BASE } from '../coach/knowledge-base.js'\nimport { runRules } from '../coach/detector.js'\nimport { measureContextSize } from '../coach/context-meter.js'\nimport { clearSurfaceLog } from '../coach/surface.js'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\nimport { buildQueries } from '../db/queries.js'\nimport type { EventContext, ToolEvent } from '../lib/types.js'\n\nexport interface CoachCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport async function runCoachCli(\n args: string[] = [],\n opts: CoachCliOptions = {},\n): Promise<number> {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const sub = args[0] ?? 'status'\n\n if (sub === 'list') {\n print(`Knowledge base (${KNOWLEDGE_BASE.length} tips):`)\n for (const tip of KNOWLEDGE_BASE) {\n print(` • ${tip.id.padEnd(32)} ${tip.title}`)\n }\n return 0\n }\n\n if (sub === 'explain') {\n const tipId = args[1]\n if (!tipId) {\n print('Uso: token-optimizer-mcp coach explain <tip_id>')\n return 1\n }\n const tip = KNOWLEDGE_BASE.find((t) => t.id === tipId)\n if (!tip) {\n print(`Tip no encontrado: ${tipId}`)\n return 1\n }\n print(tip.title)\n print('')\n print(tip.description)\n print('')\n print(`Como usarlo: ${tip.how_to_invoke}`)\n print(`Cuando: ${tip.when_applicable}`)\n print(`Ahorro: ${tip.savings_estimate}`)\n print(`Fuente: ${tip.savings_source} · verificado: ${tip.verified_at}`)\n return 0\n }\n\n if (sub === 'reset') {\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) {\n print('Sin DB; nada que resetear.')\n return 0\n }\n const db = getDb(dbPath)\n const deleted = clearSurfaceLog(db)\n print(`Log de coach reseteado (${deleted} entradas eliminadas)`)\n return 0\n }\n\n // Default: status\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) {\n print('Coach status: sin datos. Ejecuta el hook posttooluse al menos una vez.')\n return 0\n }\n const db = getDb(dbPath)\n const contextOpts: {\n db: typeof db\n projectDir?: string\n } = { db }\n if (projectDir) contextOpts.projectDir = projectDir\n const context = await measureContextSize('default', contextOpts)\n\n const queries = buildQueries(db)\n const since = new Date(Date.now() - 86_400_000).toISOString()\n const rawRows = queries.getToolCallsSince(since) as ToolEvent[]\n const ctx: EventContext = {\n session_id: 'default',\n events: rawRows.slice(0, 100),\n session_token_total: context.tokens,\n session_token_method: context.estimation_method,\n session_token_limit: context.limit,\n active_model: null,\n }\n const hits = runRules(ctx)\n\n print('token-optimizer-mcp coach status')\n print('')\n print(\n `Contexto: ${(context.percent * 100).toFixed(1)}% (${context.tokens}/${context.limit} tokens, ${context.estimation_method})`,\n )\n print(`Tips activos: ${hits.length}`)\n if (hits.length === 0) {\n print(' (sin tips disparados en este momento)')\n } else {\n for (const h of hits) {\n print(` [${h.severity}] ${h.rule_id}: ${h.evidence}`)\n }\n }\n return 0\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAGA,OAAO,QAAQ;AAef,eAAsB,YACpB,OAAiB,CAAC,GAClB,OAAwB,CAAC,GACR;AACjB,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,MAAM,KAAK,CAAC,KAAK;AAEvB,MAAI,QAAQ,QAAQ;AAClB,UAAM,mBAAmB,eAAe,MAAM,SAAS;AACvD,eAAW,OAAO,gBAAgB;AAChC,YAAM,YAAO,IAAI,GAAG,OAAO,EAAE,CAAC,IAAI,IAAI,KAAK,EAAE;AAAA,IAC/C;AACA,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,WAAW;AACrB,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,CAAC,OAAO;AACV,YAAM,iDAAiD;AACvD,aAAO;AAAA,IACT;AACA,UAAM,MAAM,eAAe,KAAK,CAAC,MAAM,EAAE,OAAO,KAAK;AACrD,QAAI,CAAC,KAAK;AACR,YAAM,sBAAsB,KAAK,EAAE;AACnC,aAAO;AAAA,IACT;AACA,UAAM,IAAI,KAAK;AACf,UAAM,EAAE;AACR,UAAM,IAAI,WAAW;AACrB,UAAM,EAAE;AACR,UAAM,gBAAgB,IAAI,aAAa,EAAE;AACzC,UAAM,gBAAgB,IAAI,eAAe,EAAE;AAC3C,UAAM,gBAAgB,IAAI,gBAAgB,EAAE;AAC5C,UAAM,gBAAgB,IAAI,cAAc,qBAAkB,IAAI,WAAW,EAAE;AAC3E,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,SAAS;AACnB,UAAMA,cAAa,kBAAkB,GAAG;AACxC,UAAMC,UAAS,uBAAuBD,WAAU;AAChD,QAAI,CAAC,GAAG,WAAWC,OAAM,GAAG;AAC1B,YAAM,4BAA4B;AAClC,aAAO;AAAA,IACT;AACA,UAAMC,MAAK,MAAMD,OAAM;AACvB,UAAM,UAAU,gBAAgBC,GAAE;AAClC,UAAM,2BAA2B,OAAO,uBAAuB;AAC/D,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAChD,MAAI,CAAC,GAAG,WAAW,MAAM,GAAG;AAC1B,UAAM,wEAAwE;AAC9E,WAAO;AAAA,EACT;AACA,QAAM,KAAK,MAAM,MAAM;AACvB,QAAM,cAGF,EAAE,GAAG;AACT,MAAI,WAAY,aAAY,aAAa;AACzC,QAAM,UAAU,MAAM,mBAAmB,WAAW,WAAW;AAE/D,QAAM,UAAU,aAAa,EAAE;AAC/B,QAAM,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAU,EAAE,YAAY;AAC5D,QAAM,UAAU,QAAQ,kBAAkB,KAAK;AAC/C,QAAM,MAAoB;AAAA,IACxB,YAAY;AAAA,IACZ,QAAQ,QAAQ,MAAM,GAAG,GAAG;AAAA,IAC5B,qBAAqB,QAAQ;AAAA,IAC7B,sBAAsB,QAAQ;AAAA,IAC9B,qBAAqB,QAAQ;AAAA,IAC7B,cAAc;AAAA,EAChB;AACA,QAAM,OAAO,SAAS,GAAG;AAEzB,QAAM,kCAAkC;AACxC,QAAM,EAAE;AACR;AAAA,IACE,cAAc,QAAQ,UAAU,KAAK,QAAQ,CAAC,CAAC,MAAM,QAAQ,MAAM,IAAI,QAAQ,KAAK,YAAY,QAAQ,iBAAiB;AAAA,EAC3H;AACA,QAAM,iBAAiB,KAAK,MAAM,EAAE;AACpC,MAAI,KAAK,WAAW,GAAG;AACrB,UAAM,yCAAyC;AAAA,EACjD,OAAO;AACL,eAAW,KAAK,MAAM;AACpB,YAAM,MAAM,EAAE,QAAQ,KAAK,EAAE,OAAO,KAAK,EAAE,QAAQ,EAAE;AAAA,IACvD;AAAA,EACF;AACA,SAAO;AACT;","names":["projectDir","dbPath","db"]} |
| #!/usr/bin/env node | ||
| // src/cli/dispatcher.ts | ||
| async function dispatchCli(argv) { | ||
| const [sub, ...rest] = argv; | ||
| switch (sub) { | ||
| case "install": { | ||
| const mod = await import("./install-TNLZRW5R.js"); | ||
| return mod.runInstall(rest); | ||
| } | ||
| case "uninstall": { | ||
| const mod = await import("./uninstall-UK255POR.js"); | ||
| return mod.runUninstall(rest); | ||
| } | ||
| case "doctor": { | ||
| const mod = await import("./doctor-C3UG64HT.js"); | ||
| return mod.runDoctor(rest); | ||
| } | ||
| case "status": { | ||
| const mod = await import("./status-ATJQRVRE.js"); | ||
| return mod.runStatus(rest); | ||
| } | ||
| case "report": { | ||
| const mod = await import("./report-O4KKLVNJ.js"); | ||
| return mod.runReport(rest); | ||
| } | ||
| case "budget": { | ||
| const mod = await import("./budget-ZAT64F3R.js"); | ||
| return mod.runBudgetCli(rest); | ||
| } | ||
| case "config": { | ||
| const mod = await import("./config-ETWSZHVJ.js"); | ||
| return mod.runConfigCommand(rest); | ||
| } | ||
| case "prune-mcp": { | ||
| const mod = await import("./prune-mcp-5SNZEYYL.js"); | ||
| return mod.runPruneMcp(rest); | ||
| } | ||
| case "coach": { | ||
| const mod = await import("./coach-6OD6QT3Y.js"); | ||
| return mod.runCoachCli(rest); | ||
| } | ||
| case "sync-xray": { | ||
| const mod = await import("./sync-xray-U6ORS5MP.js"); | ||
| return mod.runSyncXray(rest); | ||
| } | ||
| default: | ||
| console.error(`Subcomando desconocido: ${sub ?? "(ninguno)"}`); | ||
| console.error( | ||
| "Disponibles: install, uninstall, doctor, status, report, budget, prune-mcp, coach, config, sync-xray" | ||
| ); | ||
| return 1; | ||
| } | ||
| } | ||
| export { | ||
| dispatchCli | ||
| }; | ||
| //# sourceMappingURL=dispatcher-XGDCQYQI.js.map |
| {"version":3,"sources":["../src/cli/dispatcher.ts"],"sourcesContent":["// CLI subcommand dispatcher — Phase 4.9\n// Routes argv[0] to the appropriate subcommand module (lazy-imported).\n\nexport async function dispatchCli(argv: string[]): Promise<number> {\n const [sub, ...rest] = argv\n switch (sub) {\n case 'install': {\n const mod = await import('./install.js')\n return mod.runInstall(rest)\n }\n case 'uninstall': {\n const mod = await import('./uninstall.js')\n return mod.runUninstall(rest)\n }\n case 'doctor': {\n const mod = await import('./doctor.js')\n return mod.runDoctor(rest)\n }\n case 'status': {\n const mod = await import('./status.js')\n return mod.runStatus(rest)\n }\n case 'report': {\n const mod = await import('./report.js')\n return mod.runReport(rest)\n }\n case 'budget': {\n const mod = await import('./budget.js')\n return mod.runBudgetCli(rest)\n }\n case 'config': {\n const mod = await import('./config.js')\n return mod.runConfigCommand(rest)\n }\n case 'prune-mcp': {\n const mod = await import('./prune-mcp.js')\n return mod.runPruneMcp(rest)\n }\n case 'coach': {\n const mod = await import('./coach.js')\n return mod.runCoachCli(rest)\n }\n case 'sync-xray': {\n const mod = await import('./sync-xray.js')\n return mod.runSyncXray(rest)\n }\n default:\n console.error(`Subcomando desconocido: ${sub ?? '(ninguno)'}`)\n console.error(\n 'Disponibles: install, uninstall, doctor, status, report, budget, prune-mcp, coach, config, sync-xray',\n )\n return 1\n }\n}\n"],"mappings":";;;AAGA,eAAsB,YAAY,MAAiC;AACjE,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI;AACvB,UAAQ,KAAK;AAAA,IACX,KAAK,WAAW;AACd,YAAM,MAAM,MAAM,OAAO,uBAAc;AACvC,aAAO,IAAI,WAAW,IAAI;AAAA,IAC5B;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,MAAM,MAAM,OAAO,yBAAgB;AACzC,aAAO,IAAI,aAAa,IAAI;AAAA,IAC9B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,aAAa,IAAI;AAAA,IAC9B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,iBAAiB,IAAI;AAAA,IAClC;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,MAAM,MAAM,OAAO,yBAAgB;AACzC,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,MAAM,MAAM,OAAO,qBAAY;AACrC,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,MAAM,MAAM,OAAO,yBAAgB;AACzC,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA;AACE,cAAQ,MAAM,2BAA2B,OAAO,WAAW,EAAE;AAC7D,cAAQ;AAAA,QACN;AAAA,MACF;AACA,aAAO;AAAA,EACX;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| } from "./chunk-3DLEPM6P.js"; | ||
| import "./chunk-L5Z32XXL.js"; | ||
| import "./chunk-L4GG4274.js"; | ||
| import "./chunk-V4PINTCV.js"; | ||
| export { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| }; | ||
| //# sourceMappingURL=prune-mcp-5SNZEYYL.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| getDb | ||
| } from "./chunk-L4GG4274.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/report.ts | ||
| import fs from "fs"; | ||
| var PERIOD_DAYS = { | ||
| session: 3650, | ||
| day: 1, | ||
| week: 7, | ||
| month: 30 | ||
| }; | ||
| function isMeasured(method) { | ||
| return method === "measured_exact" || method === "measured_delta"; | ||
| } | ||
| function queryBySourceAndMethod(db, sinceIso) { | ||
| return db.prepare( | ||
| `SELECT source, estimation_method, | ||
| COUNT(*) as count, | ||
| COALESCE(SUM(tokens_estimated), 0) as tokens | ||
| FROM tool_calls | ||
| WHERE created_at >= ? | ||
| GROUP BY source, estimation_method | ||
| ORDER BY tokens DESC` | ||
| ).all(sinceIso); | ||
| } | ||
| var REFERENCE_DATA = [ | ||
| { | ||
| feature: "Model switching (opusplan / default-to-sonnet)", | ||
| saving: "60-80% reduccion de coste", | ||
| source: "mindstudio.ai, verdent.ai, claudelab.net", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Progressive disclosure skills", | ||
| saving: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| source: "claudefast.com", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Prompt caching read hit", | ||
| saving: "10x mas barato que uncached", | ||
| source: "Anthropic docs", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Claude Code Tool Search", | ||
| saving: "~85% schema reduction (77k \u2192 8.7k)", | ||
| source: "observado en sesion", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "MCP pruning sobre Tool Search", | ||
| saving: "~5-12% adicional por turno", | ||
| source: "estimacion interna", | ||
| verified_at: "2026-04-11" | ||
| } | ||
| ]; | ||
| function resolvePeriod(args, fallback) { | ||
| const flag = args.find((a) => a.startsWith("--period=")); | ||
| if (flag) { | ||
| const value = flag.split("=")[1]; | ||
| if (value && value in PERIOD_DAYS) return value; | ||
| } | ||
| return fallback; | ||
| } | ||
| function runReport(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const period = opts.period ?? resolvePeriod(args, "day"); | ||
| const days = PERIOD_DAYS[period]; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const lines = []; | ||
| lines.push(`token-optimizer-mcp reporte \u2014 periodo: ${period} (${days} dia(s))`); | ||
| lines.push(""); | ||
| if (!fs.existsSync(dbPath)) { | ||
| lines.push("No hay datos registrados todavia."); | ||
| } else { | ||
| const db = getDb(dbPath); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const rows = queryBySourceAndMethod(db, since); | ||
| let medidoTotal = 0; | ||
| let estimadoTotal = 0; | ||
| lines.push("Por fuente y metodo de estimacion:"); | ||
| if (rows.length === 0) { | ||
| lines.push(" (sin eventos en este periodo)"); | ||
| } else { | ||
| for (const row of rows) { | ||
| const method = row.estimation_method ?? "unknown"; | ||
| lines.push( | ||
| ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens` | ||
| ); | ||
| if (isMeasured(method)) medidoTotal += row.tokens; | ||
| else estimadoTotal += row.tokens; | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push(`Resumen: Medido: ${medidoTotal} tokens \xB7 Estimado: ${estimadoTotal} tokens`); | ||
| lines.push(""); | ||
| } | ||
| lines.push("Coach activity:"); | ||
| lines.push(" (sin tips surfaceados todavia \u2014 coach layer se activa en Phase 4.H)"); | ||
| lines.push(""); | ||
| printReference(lines); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| function printReference(lines) { | ||
| lines.push("Referencia (datos publicos verificables):"); | ||
| for (const row of REFERENCE_DATA) { | ||
| lines.push(` \u2022 ${row.feature}`); | ||
| lines.push(` ahorro: ${row.saving}`); | ||
| lines.push(` fuente: ${row.source} \xB7 verificado: ${row.verified_at}`); | ||
| } | ||
| } | ||
| export { | ||
| runReport | ||
| }; | ||
| //# sourceMappingURL=report-O4KKLVNJ.js.map |
| {"version":3,"sources":["../src/cli/report.ts"],"sourcesContent":["// Report CLI — Phase 4.14\n// Per-source breakdown WITH estimation_method label + Medido/Estimado split +\n// reference-data table (coach-layer addendum CO-4). Spanish.\n\nimport fs from 'node:fs'\nimport type Database from 'better-sqlite3'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\n\ntype DB = Database.Database\n\ntype Period = 'session' | 'day' | 'week' | 'month'\n\nconst PERIOD_DAYS: Record<Period, number> = {\n session: 3650,\n day: 1,\n week: 7,\n month: 30,\n}\n\ninterface SourceMethodRow {\n source: string\n estimation_method: string | null\n count: number\n tokens: number\n}\n\nfunction isMeasured(method: string | null): boolean {\n return method === 'measured_exact' || method === 'measured_delta'\n}\n\nfunction queryBySourceAndMethod(db: DB, sinceIso: string): SourceMethodRow[] {\n return db\n .prepare(\n `SELECT source, estimation_method,\n COUNT(*) as count,\n COALESCE(SUM(tokens_estimated), 0) as tokens\n FROM tool_calls\n WHERE created_at >= ?\n GROUP BY source, estimation_method\n ORDER BY tokens DESC`,\n )\n .all(sinceIso) as SourceMethodRow[]\n}\n\nexport interface ReportOptions {\n cwd?: string\n period?: Period\n print?: (msg: string) => void\n}\n\nconst REFERENCE_DATA: Array<{\n feature: string\n saving: string\n source: string\n verified_at: string\n}> = [\n {\n feature: 'Model switching (opusplan / default-to-sonnet)',\n saving: '60-80% reduccion de coste',\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Progressive disclosure skills',\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n source: 'claudefast.com',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Prompt caching read hit',\n saving: '10x mas barato que uncached',\n source: 'Anthropic docs',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Claude Code Tool Search',\n saving: '~85% schema reduction (77k → 8.7k)',\n source: 'observado en sesion',\n verified_at: '2026-04-11',\n },\n {\n feature: 'MCP pruning sobre Tool Search',\n saving: '~5-12% adicional por turno',\n source: 'estimacion interna',\n verified_at: '2026-04-11',\n },\n]\n\nfunction resolvePeriod(args: string[], fallback: Period): Period {\n const flag = args.find((a) => a.startsWith('--period='))\n if (flag) {\n const value = flag.split('=')[1] as Period | undefined\n if (value && value in PERIOD_DAYS) return value\n }\n return fallback\n}\n\nexport function runReport(args: string[] = [], opts: ReportOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const period: Period = opts.period ?? resolvePeriod(args, 'day')\n const days = PERIOD_DAYS[period]\n\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n const lines: string[] = []\n lines.push(`token-optimizer-mcp reporte — periodo: ${period} (${days} dia(s))`)\n lines.push('')\n\n if (!fs.existsSync(dbPath)) {\n lines.push('No hay datos registrados todavia.')\n } else {\n const db = getDb(dbPath)\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\n const rows = queryBySourceAndMethod(db, since)\n\n let medidoTotal = 0\n let estimadoTotal = 0\n\n lines.push('Por fuente y metodo de estimacion:')\n if (rows.length === 0) {\n lines.push(' (sin eventos en este periodo)')\n } else {\n for (const row of rows) {\n const method = row.estimation_method ?? 'unknown'\n lines.push(\n ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens`,\n )\n if (isMeasured(method)) medidoTotal += row.tokens\n else estimadoTotal += row.tokens\n }\n }\n lines.push('')\n lines.push(`Resumen: Medido: ${medidoTotal} tokens · Estimado: ${estimadoTotal} tokens`)\n lines.push('')\n }\n\n // Coach activity section (always present; filled in Phase 4.H with real data)\n lines.push('Coach activity:')\n lines.push(' (sin tips surfaceados todavia — coach layer se activa en Phase 4.H)')\n lines.push('')\n\n printReference(lines)\n print(lines.join('\\n'))\n return 0\n}\n\nfunction printReference(lines: string[]): void {\n lines.push('Referencia (datos publicos verificables):')\n for (const row of REFERENCE_DATA) {\n lines.push(` • ${row.feature}`)\n lines.push(` ahorro: ${row.saving}`)\n lines.push(` fuente: ${row.source} · verificado: ${row.verified_at}`)\n }\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| getCoachSurfaceLog, | ||
| measureContextSize, | ||
| postSummaryToXray, | ||
| runRules | ||
| } from "./chunk-4PA7MRTS.js"; | ||
| import "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| rollback | ||
| } from "./chunk-3DLEPM6P.js"; | ||
| import { | ||
| ensureStorageDir | ||
| } from "./chunk-VHP52B3J.js"; | ||
| import { | ||
| buildSuggestions, | ||
| checkSerenaHealth, | ||
| probeMcpPruning, | ||
| probePromptCaching, | ||
| probeRtk, | ||
| probeSerena | ||
| } from "./chunk-DSMHUGNS.js"; | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-L5Z32XXL.js"; | ||
| import { | ||
| getCostReport, | ||
| getUsageStats | ||
| } from "./chunk-BJYHIS4U.js"; | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-XAYSOAZV.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FT4EJUIT.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-L4GG4274.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/server.ts | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| // src/tools/budget.ts | ||
| import { z } from "zod"; | ||
| // src/lib/response.ts | ||
| var text = (t) => ({ | ||
| content: [{ type: "text", text: t }] | ||
| }); | ||
| var error = (t) => ({ | ||
| content: [{ type: "text", text: `Error: ${t}` }], | ||
| isError: true | ||
| }); | ||
| // src/tools/budget.ts | ||
| var DAY_MS = 864e5; | ||
| function sinceForPeriod(period) { | ||
| const now = Date.now(); | ||
| switch (period) { | ||
| case "day": | ||
| return new Date(now - DAY_MS).toISOString(); | ||
| case "week": | ||
| return new Date(now - 7 * DAY_MS).toISOString(); | ||
| case "month": | ||
| return new Date(now - 30 * DAY_MS).toISOString(); | ||
| case "session": | ||
| default: | ||
| return "1970-01-01T00:00:00.000Z"; | ||
| } | ||
| } | ||
| function registerBudgetTools(server, db) { | ||
| const manager = new BudgetManager(db); | ||
| server.tool( | ||
| "budget_set", | ||
| "Define o actualiza un presupuesto de tokens. Precedencia: session > project. Modo warn avisa al exceder.", | ||
| { | ||
| scope: z.enum(["session", "project"]).describe("Ambito del presupuesto"), | ||
| scope_key: z.string().min(1).describe("Clave del scope (sessionId o projectHash)"), | ||
| limit_tokens: z.number().int().positive().max(1e7).describe("Limite en tokens (1..10_000_000)") | ||
| }, | ||
| async ({ scope, scope_key, limit_tokens }) => { | ||
| try { | ||
| const budget = manager.setBudget({ scope, scope_key, limit_tokens }); | ||
| return text( | ||
| [ | ||
| "Presupuesto guardado:", | ||
| "", | ||
| ` scope: ${budget.scope}`, | ||
| ` scope_key: ${budget.scope_key}`, | ||
| ` limit_tokens: ${budget.limit_tokens}`, | ||
| ` mode: ${budget.mode}` | ||
| ].join("\n") | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "budget_check", | ||
| "Consulta el estado del presupuesto activo (gasto actual, restante y porcentaje).", | ||
| { | ||
| session_id: z.string().optional().describe('ID de sesion (default: "default")'), | ||
| project_hash: z.string().optional().describe("Hash del proyecto para fallback a scope project") | ||
| }, | ||
| async ({ session_id, project_hash }) => { | ||
| try { | ||
| const status = manager.checkBudget(session_id ?? "default", project_hash ?? null); | ||
| if (!status.active) { | ||
| return text("Sin presupuesto activo para la sesion/proyecto actual."); | ||
| } | ||
| const percent = (status.percent_used * 100).toFixed(1); | ||
| return text( | ||
| [ | ||
| "Estado del presupuesto:", | ||
| "", | ||
| ` gastado: ${status.spent} tokens`, | ||
| ` restante: ${status.remaining} tokens`, | ||
| ` uso: ${percent}%`, | ||
| ` modo: ${status.mode ?? "n/a"}` | ||
| ].join("\n") | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "budget_report", | ||
| "Muestra el consumo de tokens agrupado por herramienta y por fuente durante un periodo.", | ||
| { | ||
| period: z.enum(["session", "day", "week", "month"]).optional().describe("Periodo del reporte (default: day)") | ||
| }, | ||
| async ({ period }) => { | ||
| try { | ||
| const since = sinceForPeriod(period ?? "day"); | ||
| const report = manager.getBudgetReport(since); | ||
| const lines = [`Reporte de consumo (desde ${report.period_since}):`, ""]; | ||
| lines.push("Por herramienta:"); | ||
| if (report.by_tool.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of report.by_tool) { | ||
| lines.push(` ${row.tool_name}: ${row.count} llamadas, ${row.tokens} tokens`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push("Por fuente:"); | ||
| if (report.by_source.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of report.by_source) { | ||
| lines.push(` ${row.source}: ${row.count} llamadas, ${row.tokens} tokens`); | ||
| } | ||
| } | ||
| return text(lines.join("\n")); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/tools/session.ts | ||
| function registerSessionTools(_server, _db) { | ||
| } | ||
| // src/tools/orchestration.ts | ||
| import { z as z2 } from "zod"; | ||
| // src/services/session-summary-builder.ts | ||
| function buildSessionSummary(db, sessionId, version) { | ||
| const usage = getUsageStats(db, 1); | ||
| const cost = getCostReport(db, 1); | ||
| const serena = probeSerena(); | ||
| const rtk = probeRtk(); | ||
| const mcpPruning = probeMcpPruning(); | ||
| const promptCaching = probePromptCaching(); | ||
| const schema = measureCurrentSchemaBytes(); | ||
| const coachTips = getCoachSurfaceLog(db, sessionId); | ||
| const projDir = resolveProjectDir(); | ||
| const projName = projDir.split(/[\\/]/).filter(Boolean).pop() ?? "unknown"; | ||
| return { | ||
| session_id: sessionId, | ||
| project_path: projDir, | ||
| project_name: projName, | ||
| total_tokens: usage.total_tokens, | ||
| total_events: usage.total_events, | ||
| by_source: usage.by_source, | ||
| by_tool: usage.by_tool.map((t) => ({ | ||
| tool_name: t.tool_name, | ||
| count: t.count, | ||
| tokens: t.tokens | ||
| })), | ||
| cost_haiku: cost.estimated_cost_usd_haiku, | ||
| cost_sonnet: cost.estimated_cost_usd_sonnet, | ||
| cost_opus: cost.estimated_cost_usd_opus, | ||
| probes: { | ||
| serena: { present: serena.present, confidence: serena.confidence, signals: serena.signals }, | ||
| rtk: { present: rtk.present, confidence: rtk.confidence, signals: rtk.signals }, | ||
| mcp_pruning: { | ||
| present: mcpPruning.present, | ||
| confidence: mcpPruning.confidence, | ||
| signals: mcpPruning.signals | ||
| }, | ||
| prompt_caching: { present: promptCaching.present, confidence: promptCaching.confidence } | ||
| }, | ||
| coach_tips_surfaced: coachTips, | ||
| schema_measurement: { | ||
| tool_schema_tokens: schema.tool_schema_tokens, | ||
| mcp_servers: schema.mcp_servers | ||
| }, | ||
| optimizer_version: version | ||
| }; | ||
| } | ||
| // src/tools/orchestration.ts | ||
| function registerOrchestrationTools(server, db) { | ||
| server.tool( | ||
| "mcp_usage_stats", | ||
| "Estadisticas de uso de tokens por herramienta y fuente en un periodo.", | ||
| { | ||
| days: z2.number().int().positive().max(365).optional().describe("Dias a analizar (default: 7)") | ||
| }, | ||
| async ({ days }) => { | ||
| try { | ||
| const stats = getUsageStats(db, days ?? 7); | ||
| const lines = [ | ||
| `Uso en los ultimos ${stats.period_days} dia(s):`, | ||
| "", | ||
| `Total: ${stats.total_tokens} tokens, ${stats.total_events} eventos`, | ||
| "", | ||
| "Por fuente:" | ||
| ]; | ||
| if (stats.by_source.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of stats.by_source) { | ||
| lines.push(` ${row.source}: ${row.tokens} tokens, ${row.count} llamadas`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push("Top herramientas:"); | ||
| if (stats.by_tool.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of stats.by_tool.slice(0, 10)) { | ||
| lines.push(` ${row.tool_name}: ${row.tokens} tokens, ${row.count} llamadas`); | ||
| } | ||
| } | ||
| return text(lines.join("\n")); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_cost_report", | ||
| "Reporte de coste estimado con rango Haiku-Sonnet-Opus y disclaimer honesto.", | ||
| { | ||
| days: z2.number().int().positive().max(365).optional().describe("Dias a analizar (default: 7)") | ||
| }, | ||
| async ({ days }) => { | ||
| try { | ||
| const cost = getCostReport(db, days ?? 7); | ||
| const lines = [ | ||
| `Reporte de coste (${cost.period_days} dia(s)):`, | ||
| "", | ||
| `Tokens totales: ${cost.total_tokens}`, | ||
| `Coste estimado (input pricing):`, | ||
| ` Haiku 4.5: $${cost.estimated_cost_usd_haiku.toFixed(4)} ($1/MTok)`, | ||
| ` Sonnet 4.6: $${cost.estimated_cost_usd_sonnet.toFixed(4)} ($3/MTok)`, | ||
| ` Opus 4.6: $${cost.estimated_cost_usd_opus.toFixed(4)} ($5/MTok)`, | ||
| "", | ||
| `Nota: ${cost.disclaimer}` | ||
| ]; | ||
| return text(lines.join("\n")); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "optimization_status", | ||
| "Estado de las optimizaciones detectadas: serena, RTK, MCP pruning, prompt caching, schema size.", | ||
| {}, | ||
| async () => { | ||
| try { | ||
| const serena = probeSerena(); | ||
| const rtk = probeRtk(); | ||
| const pruning = probeMcpPruning(); | ||
| const pcProbe = probePromptCaching(); | ||
| void pcProbe; | ||
| const schema = measureCurrentSchemaBytes(); | ||
| const status = { | ||
| serena, | ||
| rtk, | ||
| mcp_pruning: pruning, | ||
| prompt_caching: { | ||
| active_by_default: true, | ||
| savings_tokens: null, | ||
| estimation_method: "unknown", | ||
| note: "Revisa tu factura Anthropic para confirmar el ahorro real" | ||
| }, | ||
| schema_bytes: { | ||
| tool_schema_bytes: schema.tool_schema_bytes, | ||
| measurement_method: schema.measurement_method | ||
| } | ||
| }; | ||
| const serenaHealth = serena.present ? checkSerenaHealth() : []; | ||
| const suggestions = buildSuggestions(status); | ||
| try { | ||
| const lastSession = db.prepare("SELECT id FROM sessions ORDER BY started_at DESC LIMIT 1").get(); | ||
| if (lastSession) { | ||
| const summary = buildSessionSummary(db, lastSession.id, "0.2.6"); | ||
| void postSummaryToXray(summary).catch(() => { | ||
| }); | ||
| } | ||
| } catch { | ||
| } | ||
| return text(JSON.stringify({ status, serena_health: serenaHealth, suggestions }, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_suggest", | ||
| "Genera un allowlist de MCPs basandose en el historial (NO modifica archivos).", | ||
| { | ||
| days: z2.number().int().positive().max(365).optional().describe("Dias de historial a analizar (default: 14)") | ||
| }, | ||
| async ({ days }) => { | ||
| try { | ||
| const proposal = generateFromHistory({ days: days ?? 14 }); | ||
| return text(JSON.stringify(proposal, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_apply", | ||
| "Restringe los MCPs activos escribiendo enabledMcpjsonServers en .claude/settings.local.json. Requiere confirm:true. Acepta dos formas equivalentes: allowlist (lista blanca, los que SI quieres) o exclude (lista negra, los que NO quieres). Se debe pasar exactamente una de las dos.", | ||
| { | ||
| allowlist: z2.array(z2.string()).optional().describe("Nombres de MCPs a permitir (lista blanca). Exclusivo con exclude."), | ||
| exclude: z2.array(z2.string()).optional().describe( | ||
| "Nombres de MCPs a desactivar (lista negra). Internamente se traduce a allowlist = registrados - exclude. Exclusivo con allowlist." | ||
| ), | ||
| confirm: z2.boolean().describe("Debe ser true para confirmar la escritura") | ||
| }, | ||
| async ({ allowlist, exclude, confirm }) => { | ||
| try { | ||
| if (confirm !== true) { | ||
| return error( | ||
| "Operacion destructiva: requiere confirm:true. Revisa el allowlist antes de aplicar." | ||
| ); | ||
| } | ||
| const hasAllow = Array.isArray(allowlist); | ||
| const hasExclude = Array.isArray(exclude); | ||
| if (hasAllow === hasExclude) { | ||
| return error( | ||
| "Debes pasar exactamente uno: allowlist (los que SI quieres) o exclude (los que NO quieres)." | ||
| ); | ||
| } | ||
| const schema = measureCurrentSchemaBytes(); | ||
| const registered = new Set(schema.mcp_servers); | ||
| let effective; | ||
| let translationNote = ""; | ||
| if (hasAllow) { | ||
| effective = allowlist; | ||
| if (registered.size > 0) { | ||
| const invalid = effective.filter((s) => !registered.has(s)); | ||
| if (invalid.length > 0) { | ||
| return error( | ||
| `Allowlist contiene MCPs no registrados en settings: ${invalid.join(", ")}` | ||
| ); | ||
| } | ||
| } | ||
| } else { | ||
| const excludeSet = new Set(exclude); | ||
| if (registered.size > 0) { | ||
| const invalid = exclude.filter((s) => !registered.has(s)); | ||
| if (invalid.length > 0) { | ||
| return error( | ||
| `Exclude contiene MCPs no registrados en settings: ${invalid.join(", ")}` | ||
| ); | ||
| } | ||
| } | ||
| effective = [...registered].filter((s) => !excludeSet.has(s)); | ||
| translationNote = ` | ||
| exclude: [${exclude.join(", ")}] | ||
| \u2192 allowlist efectivo: [${effective.join(", ")}]`; | ||
| } | ||
| const applied = applyAllowlist(effective, { source: "mcp" }); | ||
| return text( | ||
| `Allowlist aplicado.${translationNote} | ||
| settings: ${applied.settings_path} | ||
| backup: ${applied.backup_path}` | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_rollback", | ||
| "Restaura el backup mas reciente de settings.local.json. Requiere confirm:true.", | ||
| { | ||
| confirm: z2.boolean(), | ||
| to: z2.string().optional().describe("Timestamp opcional del backup a restaurar") | ||
| }, | ||
| async ({ confirm, to }) => { | ||
| try { | ||
| if (confirm !== true) { | ||
| return error("Operacion destructiva: requiere confirm:true."); | ||
| } | ||
| const result = rollback(to !== void 0 ? { to } : {}); | ||
| if (!result.restored) return error("No hay backups disponibles."); | ||
| return text(`Restaurado desde ${result.from}`); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_clear", | ||
| "Elimina el allowlist de settings.local.json (crea backup). Requiere confirm:true.", | ||
| { | ||
| confirm: z2.boolean() | ||
| }, | ||
| async ({ confirm }) => { | ||
| try { | ||
| if (confirm !== true) { | ||
| return error("Operacion destructiva: requiere confirm:true."); | ||
| } | ||
| const result = clearAllowlist(); | ||
| return text( | ||
| result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : "Nada que eliminar" | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/tools/coach.ts | ||
| import { z as z3 } from "zod"; | ||
| // src/coach/reference-data.ts | ||
| var REFERENCE_DATA = [ | ||
| { | ||
| feature: "Model switching (opusplan / default-to-sonnet)", | ||
| saving: "60-80% reduccion de coste", | ||
| source: "mindstudio.ai, verdent.ai, claudelab.net", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "Progressive disclosure skills", | ||
| saving: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| source: "claudefast.com", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "Prompt caching read hit", | ||
| saving: "10x mas barato que uncached", | ||
| source: "Anthropic docs", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "Claude Code Tool Search", | ||
| saving: "~85% schema reduction (77k \u2192 8.7k tokens)", | ||
| source: "observado en sesion", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "MCP pruning sobre Tool Search", | ||
| saving: "~5-12% adicional por turno", | ||
| source: "estimacion interna", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| } | ||
| ]; | ||
| var DAY_MS2 = 864e5; | ||
| function getStaleRows(daysThreshold = 90, today = /* @__PURE__ */ new Date()) { | ||
| const cutoff = today.getTime() - daysThreshold * DAY_MS2; | ||
| return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() < cutoff); | ||
| } | ||
| // src/coach/tips-payload.ts | ||
| async function computeCoachTipsPayload(opts) { | ||
| const { db } = opts; | ||
| const sessionId = opts.sessionId ?? "default"; | ||
| const contextOpts = { db }; | ||
| if (opts.projectDir !== void 0) contextOpts.projectDir = opts.projectDir; | ||
| if (opts.activeModel !== void 0) contextOpts.activeModel = opts.activeModel; | ||
| const context = await measureContextSize(sessionId, contextOpts); | ||
| const queries = buildQueries(db); | ||
| const since = new Date(Date.now() - 864e5).toISOString(); | ||
| const rawRows = queries.getToolCallsSince(since); | ||
| const events = rawRows.slice(0, 100); | ||
| const ctx = { | ||
| session_id: sessionId, | ||
| events, | ||
| session_token_total: context.tokens, | ||
| session_token_method: context.estimation_method, | ||
| session_token_limit: context.limit, | ||
| active_model: opts.activeModel ?? null | ||
| }; | ||
| const hits = runRules(ctx); | ||
| const staleTips = getStaleRows(); | ||
| return { | ||
| current: hits, | ||
| known_tricks: KNOWLEDGE_BASE, | ||
| context, | ||
| reference_data: REFERENCE_DATA, | ||
| stale_reference_count: staleTips.length, | ||
| last_computed_at: (/* @__PURE__ */ new Date()).toISOString() | ||
| }; | ||
| } | ||
| // src/tools/coach.ts | ||
| function registerCoachTools(server, db) { | ||
| server.tool( | ||
| "coach_tips", | ||
| "Devuelve tips activos (rules disparadas) y medicion de contexto. Por defecto modo compacto (~500 tokens). Usa verbose=true para incluir el catalogo completo de 18 tips y la tabla de referencia (~3.5k tokens).", | ||
| { | ||
| session_id: z3.string().optional().describe('ID de la sesion (default: "default")'), | ||
| project_dir: z3.string().optional().describe("Directorio del proyecto para medir contexto desde transcript JSONL"), | ||
| active_model: z3.string().optional().describe("Modelo activo (opcional, habilita regla detect-opus-for-simple-task)"), | ||
| verbose: z3.boolean().optional().describe( | ||
| "Si true, incluye el knowledge base completo (18 tips) y la reference data. Default false = solo hits activos + contexto. Ahorra ~3.3k tokens por llamada en modo compacto." | ||
| ) | ||
| }, | ||
| async ({ session_id, project_dir, active_model, verbose }) => { | ||
| try { | ||
| const payloadOpts = { db }; | ||
| if (session_id !== void 0) payloadOpts.sessionId = session_id; | ||
| if (project_dir !== void 0) payloadOpts.projectDir = project_dir; | ||
| if (active_model !== void 0) payloadOpts.activeModel = active_model; | ||
| const response = await computeCoachTipsPayload(payloadOpts); | ||
| if (verbose !== true) { | ||
| const { known_tricks: _kb, reference_data: _ref, ...compact } = response; | ||
| return text(JSON.stringify(compact, null, 2)); | ||
| } | ||
| return text(JSON.stringify(response, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/tools/toon.ts | ||
| import { z as z4 } from "zod"; | ||
| function compactEncode(data) { | ||
| try { | ||
| return JSON.stringify(data); | ||
| } catch (e) { | ||
| const msg = e instanceof Error ? e.message : String(e); | ||
| if (/circular|cyclic/i.test(msg)) { | ||
| throw new Error("Referencia circular detectada: TOON no soporta objetos ciclicos"); | ||
| } | ||
| throw new Error(`No se pudo codificar a TOON: ${msg}`); | ||
| } | ||
| } | ||
| function compactDecode(toon) { | ||
| try { | ||
| return JSON.parse(toon); | ||
| } catch (e) { | ||
| throw new Error(`TOON invalido: ${e instanceof Error ? e.message : String(e)}`); | ||
| } | ||
| } | ||
| function registerToonTools(server) { | ||
| server.tool( | ||
| "toon_encode", | ||
| "Codifica un objeto JSON a formato TOON (JSON compacto token-eficiente, round-trip lossless).", | ||
| { | ||
| data: z4.unknown().describe("Valor a codificar (objeto, array, primitivo)") | ||
| }, | ||
| async ({ data }) => { | ||
| try { | ||
| const encoded = compactEncode(data); | ||
| return text(encoded); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "toon_decode", | ||
| "Decodifica una cadena TOON a JSON. Devuelve el objeto formateado para lectura.", | ||
| { | ||
| toon: z4.string().min(1).describe("Cadena TOON a decodificar") | ||
| }, | ||
| async ({ toon }) => { | ||
| try { | ||
| const decoded = compactDecode(toon); | ||
| return text(JSON.stringify(decoded, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/resources/coach-tips.ts | ||
| var COACH_TIPS_URI = "token-optimizer://coach/tips"; | ||
| function registerCoachTipsResource(server, db) { | ||
| server.resource( | ||
| "coach-tips", | ||
| COACH_TIPS_URI, | ||
| { | ||
| description: "Tips activos del coach, catalogo completo de trucos, medicion de contexto y tabla de referencia.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri) => { | ||
| try { | ||
| const payload = await computeCoachTipsPayload({ db }); | ||
| return { | ||
| contents: [ | ||
| { | ||
| uri: uri.href, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify(payload, null, 2) | ||
| } | ||
| ] | ||
| }; | ||
| } catch (e) { | ||
| const message = e instanceof Error ? e.message : String(e); | ||
| return { | ||
| contents: [ | ||
| { | ||
| uri: uri.href, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify({ error: message }) | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/server.ts | ||
| var VERSION = true ? "0.4.20" : "0.1.0"; | ||
| var INSTRUCTIONS = `token-optimizer-mcp: orchestration + observability + coach layer for Claude Code. | ||
| Measures tool usage, enforces token budgets, advises on complementary tools (serena, RTK), | ||
| and proactively surfaces savings tips. Coach layer detects inefficiencies and suggests | ||
| optimizations like opusplan, /compact, plan mode, and more. | ||
| Does NOT replace serena (symbolic file reads) or RTK (Bash output filtering) \u2014 | ||
| coordinates with them and adds measurements, budgets, compact recovery, and coaching.`; | ||
| function createServer(options = {}) { | ||
| const resolvedProject = options.projectDir ?? resolveProjectDir(); | ||
| const dbPath = options.dbPath ?? (options.storageDir === ":memory:" ? ":memory:" : (ensureStorageDir(resolvedProject), resolveAnalyticsDbPath(resolvedProject))); | ||
| const db = getDb(dbPath); | ||
| const server = new McpServer( | ||
| { | ||
| name: "token-optimizer-mcp", | ||
| version: VERSION | ||
| }, | ||
| { | ||
| instructions: INSTRUCTIONS | ||
| } | ||
| ); | ||
| registerBudgetTools(server, db); | ||
| registerSessionTools(server, db); | ||
| registerOrchestrationTools(server, db); | ||
| registerCoachTools(server, db); | ||
| registerCoachTipsResource(server, db); | ||
| registerToonTools(server); | ||
| return server; | ||
| } | ||
| export { | ||
| createServer | ||
| }; | ||
| //# sourceMappingURL=server-KUXDBXVT.js.map |
| {"version":3,"sources":["../src/server.ts","../src/tools/budget.ts","../src/lib/response.ts","../src/tools/session.ts","../src/tools/orchestration.ts","../src/services/session-summary-builder.ts","../src/tools/coach.ts","../src/coach/reference-data.ts","../src/coach/tips-payload.ts","../src/tools/toon.ts","../src/resources/coach-tips.ts"],"sourcesContent":["// createServer factory — Phase 1.13\n// Returns a configured McpServer. Tools are registered in later phases.\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { getDb } from './db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from './lib/paths.js'\nimport { ensureStorageDir } from './lib/storage.js'\nimport { registerBudgetTools } from './tools/budget.js'\nimport { registerSessionTools } from './tools/session.js'\nimport { registerOrchestrationTools } from './tools/orchestration.js'\nimport { registerCoachTools } from './tools/coach.js'\nimport { registerToonTools } from './tools/toon.js'\nimport { registerCoachTipsResource } from './resources/coach-tips.js'\n\ndeclare const __PKG_VERSION__: string\nconst VERSION = typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : '0.1.0'\n\nconst INSTRUCTIONS = `token-optimizer-mcp: orchestration + observability + coach layer for Claude Code.\n\nMeasures tool usage, enforces token budgets, advises on complementary tools (serena, RTK),\nand proactively surfaces savings tips. Coach layer detects inefficiencies and suggests\noptimizations like opusplan, /compact, plan mode, and more.\n\nDoes NOT replace serena (symbolic file reads) or RTK (Bash output filtering) —\ncoordinates with them and adds measurements, budgets, compact recovery, and coaching.`\n\nexport interface CreateServerOptions {\n storageDir?: string\n projectDir?: string\n dbPath?: string\n}\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const resolvedProject = options.projectDir ?? resolveProjectDir()\n const dbPath =\n options.dbPath ??\n (options.storageDir === ':memory:'\n ? ':memory:'\n : (ensureStorageDir(resolvedProject), resolveAnalyticsDbPath(resolvedProject)))\n\n // Initialize DB (schema created by getDb)\n const db = getDb(dbPath)\n\n const server = new McpServer(\n {\n name: 'token-optimizer-mcp',\n version: VERSION,\n },\n {\n instructions: INSTRUCTIONS,\n },\n )\n\n // Phase 2 tools\n registerBudgetTools(server, db)\n // Phase 3 tools\n registerSessionTools(server, db)\n // Phase 4 tools\n registerOrchestrationTools(server, db)\n registerCoachTools(server, db)\n registerCoachTipsResource(server, db)\n // Phase 5 tools\n registerToonTools(server)\n\n return server\n}\n","// Budget MCP tools — Phase 2.4\n// budget_set, budget_check, budget_report\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { BudgetManager } from '../services/budget-manager.js'\nimport { text, error } from '../lib/response.js'\n\ntype DB = Database.Database\n\nconst DAY_MS = 86_400_000\n\nfunction sinceForPeriod(period: 'session' | 'day' | 'week' | 'month'): string {\n const now = Date.now()\n switch (period) {\n case 'day':\n return new Date(now - DAY_MS).toISOString()\n case 'week':\n return new Date(now - 7 * DAY_MS).toISOString()\n case 'month':\n return new Date(now - 30 * DAY_MS).toISOString()\n case 'session':\n default:\n return '1970-01-01T00:00:00.000Z'\n }\n}\n\nexport function registerBudgetTools(server: McpServer, db: DB): void {\n const manager = new BudgetManager(db)\n\n // ── budget_set ──\n server.tool(\n 'budget_set',\n 'Define o actualiza un presupuesto de tokens. Precedencia: session > project. Modo warn avisa al exceder.',\n {\n scope: z.enum(['session', 'project']).describe('Ambito del presupuesto'),\n scope_key: z.string().min(1).describe('Clave del scope (sessionId o projectHash)'),\n limit_tokens: z\n .number()\n .int()\n .positive()\n .max(10_000_000)\n .describe('Limite en tokens (1..10_000_000)'),\n },\n async ({ scope, scope_key, limit_tokens }) => {\n try {\n const budget = manager.setBudget({ scope, scope_key, limit_tokens })\n return text(\n [\n 'Presupuesto guardado:',\n '',\n ` scope: ${budget.scope}`,\n ` scope_key: ${budget.scope_key}`,\n ` limit_tokens: ${budget.limit_tokens}`,\n ` mode: ${budget.mode}`,\n ].join('\\n'),\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── budget_check ──\n server.tool(\n 'budget_check',\n 'Consulta el estado del presupuesto activo (gasto actual, restante y porcentaje).',\n {\n session_id: z.string().optional().describe('ID de sesion (default: \"default\")'),\n project_hash: z\n .string()\n .optional()\n .describe('Hash del proyecto para fallback a scope project'),\n },\n async ({ session_id, project_hash }) => {\n try {\n const status = manager.checkBudget(session_id ?? 'default', project_hash ?? null)\n if (!status.active) {\n return text('Sin presupuesto activo para la sesion/proyecto actual.')\n }\n const percent = (status.percent_used * 100).toFixed(1)\n return text(\n [\n 'Estado del presupuesto:',\n '',\n ` gastado: ${status.spent} tokens`,\n ` restante: ${status.remaining} tokens`,\n ` uso: ${percent}%`,\n ` modo: ${status.mode ?? 'n/a'}`,\n ].join('\\n'),\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── budget_report ──\n server.tool(\n 'budget_report',\n 'Muestra el consumo de tokens agrupado por herramienta y por fuente durante un periodo.',\n {\n period: z\n .enum(['session', 'day', 'week', 'month'])\n .optional()\n .describe('Periodo del reporte (default: day)'),\n },\n async ({ period }) => {\n try {\n const since = sinceForPeriod(period ?? 'day')\n const report = manager.getBudgetReport(since)\n const lines = [`Reporte de consumo (desde ${report.period_since}):`, '']\n lines.push('Por herramienta:')\n if (report.by_tool.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of report.by_tool) {\n lines.push(` ${row.tool_name}: ${row.count} llamadas, ${row.tokens} tokens`)\n }\n }\n lines.push('')\n lines.push('Por fuente:')\n if (report.by_source.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of report.by_source) {\n lines.push(` ${row.source}: ${row.count} llamadas, ${row.tokens} tokens`)\n }\n }\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Shared MCP tool response helpers — Phase 2\n\nexport const text = (t: string) => ({\n content: [{ type: 'text' as const, text: t }],\n})\n\nexport const error = (t: string) => ({\n content: [{ type: 'text' as const, text: `Error: ${t}` }],\n isError: true as const,\n})\n","// Session tools — Phase 3.3 (simplified: session_search removed, FTS5 no longer available)\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\n\ntype DB = Database.Database\n\n// No session tools registered after FTS5 removal.\n// Keeping the function signature for backwards compatibility with server.ts imports.\nexport function registerSessionTools(_server: McpServer, _db: DB): void {\n // noop\n}\n","// Orchestration MCP tools — Phase 4.23-4.28\n// mcp_usage_stats, mcp_cost_report, optimization_status,\n// mcp_prune_suggest, mcp_prune_apply, mcp_prune_rollback, mcp_prune_clear\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { text, error } from '../lib/response.js'\nimport { getUsageStats, getCostReport } from '../services/stats.js'\nimport {\n probeSerena,\n probeRtk,\n probeMcpPruning,\n probePromptCaching,\n checkSerenaHealth,\n} from '../orchestration/detector.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\nimport { buildSuggestions } from '../orchestration/advisor.js'\nimport {\n generateFromHistory,\n applyAllowlist,\n rollback,\n clearAllowlist,\n} from '../cli/prune-mcp.js'\nimport type { OptimizationStatus } from '../lib/types.js'\nimport { buildSessionSummary } from '../services/session-summary-builder.js'\nimport { postSummaryToXray } from '../services/xray-client.js'\n\ntype DB = Database.Database\n\nexport function registerOrchestrationTools(server: McpServer, db: DB): void {\n // ── mcp_usage_stats ──\n server.tool(\n 'mcp_usage_stats',\n 'Estadisticas de uso de tokens por herramienta y fuente en un periodo.',\n {\n days: z.number().int().positive().max(365).optional().describe('Dias a analizar (default: 7)'),\n },\n async ({ days }) => {\n try {\n const stats = getUsageStats(db, days ?? 7)\n const lines = [\n `Uso en los ultimos ${stats.period_days} dia(s):`,\n '',\n `Total: ${stats.total_tokens} tokens, ${stats.total_events} eventos`,\n '',\n 'Por fuente:',\n ]\n if (stats.by_source.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of stats.by_source) {\n lines.push(` ${row.source}: ${row.tokens} tokens, ${row.count} llamadas`)\n }\n }\n lines.push('')\n lines.push('Top herramientas:')\n if (stats.by_tool.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of stats.by_tool.slice(0, 10)) {\n lines.push(` ${row.tool_name}: ${row.tokens} tokens, ${row.count} llamadas`)\n }\n }\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_cost_report ──\n server.tool(\n 'mcp_cost_report',\n 'Reporte de coste estimado con rango Haiku-Sonnet-Opus y disclaimer honesto.',\n {\n days: z\n .number()\n .int()\n .positive()\n .max(365)\n .optional()\n .describe('Dias a analizar (default: 7)'),\n },\n async ({ days }) => {\n try {\n const cost = getCostReport(db, days ?? 7)\n const lines = [\n `Reporte de coste (${cost.period_days} dia(s)):`,\n '',\n `Tokens totales: ${cost.total_tokens}`,\n `Coste estimado (input pricing):`,\n ` Haiku 4.5: $${cost.estimated_cost_usd_haiku.toFixed(4)} ($1/MTok)`,\n ` Sonnet 4.6: $${cost.estimated_cost_usd_sonnet.toFixed(4)} ($3/MTok)`,\n ` Opus 4.6: $${cost.estimated_cost_usd_opus.toFixed(4)} ($5/MTok)`,\n '',\n `Nota: ${cost.disclaimer}`,\n ]\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── optimization_status ──\n server.tool(\n 'optimization_status',\n 'Estado de las optimizaciones detectadas: serena, RTK, MCP pruning, prompt caching, schema size.',\n {},\n async () => {\n try {\n const serena = probeSerena()\n const rtk = probeRtk()\n const pruning = probeMcpPruning()\n const pcProbe = probePromptCaching()\n void pcProbe\n const schema = measureCurrentSchemaBytes()\n // Always include prompt_caching with explicit estimation_method per measurement-honesty spec\n const status: OptimizationStatus = {\n serena,\n rtk,\n mcp_pruning: pruning,\n prompt_caching: {\n active_by_default: true,\n savings_tokens: null,\n estimation_method: 'unknown',\n note: 'Revisa tu factura Anthropic para confirmar el ahorro real',\n },\n schema_bytes: {\n tool_schema_bytes: schema.tool_schema_bytes,\n measurement_method: schema.measurement_method,\n },\n }\n const serenaHealth = serena.present ? checkSerenaHealth() : []\n const suggestions = buildSuggestions(status)\n\n // Fire-and-forget summary to xray (if XRAY_URL is set)\n try {\n const lastSession = db\n .prepare('SELECT id FROM sessions ORDER BY started_at DESC LIMIT 1')\n .get() as { id: string } | undefined\n if (lastSession) {\n const summary = buildSessionSummary(db, lastSession.id, '0.2.6')\n void postSummaryToXray(summary as unknown as Record<string, unknown>).catch(() => {})\n }\n } catch {\n // Silent — xray is optional\n }\n\n return text(JSON.stringify({ status, serena_health: serenaHealth, suggestions }, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_suggest ──\n server.tool(\n 'mcp_prune_suggest',\n 'Genera un allowlist de MCPs basandose en el historial (NO modifica archivos).',\n {\n days: z\n .number()\n .int()\n .positive()\n .max(365)\n .optional()\n .describe('Dias de historial a analizar (default: 14)'),\n },\n async ({ days }) => {\n try {\n const proposal = generateFromHistory({ days: days ?? 14 })\n return text(JSON.stringify(proposal, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_apply ──\n server.tool(\n 'mcp_prune_apply',\n 'Restringe los MCPs activos escribiendo enabledMcpjsonServers en .claude/settings.local.json. Requiere confirm:true. Acepta dos formas equivalentes: allowlist (lista blanca, los que SI quieres) o exclude (lista negra, los que NO quieres). Se debe pasar exactamente una de las dos.',\n {\n allowlist: z\n .array(z.string())\n .optional()\n .describe('Nombres de MCPs a permitir (lista blanca). Exclusivo con exclude.'),\n exclude: z\n .array(z.string())\n .optional()\n .describe(\n 'Nombres de MCPs a desactivar (lista negra). Internamente se traduce a allowlist = registrados - exclude. Exclusivo con allowlist.',\n ),\n confirm: z.boolean().describe('Debe ser true para confirmar la escritura'),\n },\n async ({ allowlist, exclude, confirm }) => {\n try {\n if (confirm !== true) {\n return error(\n 'Operacion destructiva: requiere confirm:true. Revisa el allowlist antes de aplicar.',\n )\n }\n const hasAllow = Array.isArray(allowlist)\n const hasExclude = Array.isArray(exclude)\n if (hasAllow === hasExclude) {\n return error(\n 'Debes pasar exactamente uno: allowlist (los que SI quieres) o exclude (los que NO quieres).',\n )\n }\n\n const schema = measureCurrentSchemaBytes()\n const registered = new Set(schema.mcp_servers)\n\n let effective: string[]\n let translationNote = ''\n\n if (hasAllow) {\n effective = allowlist as string[]\n if (registered.size > 0) {\n const invalid = effective.filter((s) => !registered.has(s))\n if (invalid.length > 0) {\n return error(\n `Allowlist contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\n )\n }\n }\n } else {\n const excludeSet = new Set(exclude as string[])\n if (registered.size > 0) {\n const invalid = (exclude as string[]).filter((s) => !registered.has(s))\n if (invalid.length > 0) {\n return error(\n `Exclude contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\n )\n }\n }\n effective = [...registered].filter((s) => !excludeSet.has(s))\n translationNote = `\\n exclude: [${(exclude as string[]).join(', ')}]\\n → allowlist efectivo: [${effective.join(', ')}]`\n }\n\n const applied = applyAllowlist(effective, { source: 'mcp' })\n return text(\n `Allowlist aplicado.${translationNote}\\n settings: ${applied.settings_path}\\n backup: ${applied.backup_path}`,\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_rollback ──\n server.tool(\n 'mcp_prune_rollback',\n 'Restaura el backup mas reciente de settings.local.json. Requiere confirm:true.',\n {\n confirm: z.boolean(),\n to: z.string().optional().describe('Timestamp opcional del backup a restaurar'),\n },\n async ({ confirm, to }) => {\n try {\n if (confirm !== true) {\n return error('Operacion destructiva: requiere confirm:true.')\n }\n const result = rollback(to !== undefined ? { to } : {})\n if (!result.restored) return error('No hay backups disponibles.')\n return text(`Restaurado desde ${result.from}`)\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_clear ──\n server.tool(\n 'mcp_prune_clear',\n 'Elimina el allowlist de settings.local.json (crea backup). Requiere confirm:true.',\n {\n confirm: z.boolean(),\n },\n async ({ confirm }) => {\n try {\n if (confirm !== true) {\n return error('Operacion destructiva: requiere confirm:true.')\n }\n const result = clearAllowlist()\n return text(\n result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar',\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Session summary builder for xray integration.\n// Aggregates all local data sources into a single payload for xray.\n// Only called once per session (not in PostToolUse hot path).\n\nimport type Database from 'better-sqlite3'\nimport { getUsageStats, getCostReport } from './stats.js'\nimport {\n probeSerena,\n probeRtk,\n probeMcpPruning,\n probePromptCaching,\n} from '../orchestration/detector.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\nimport { getCoachSurfaceLog } from '../coach/surface.js'\nimport { resolveProjectDir } from '../lib/paths.js'\n\ntype DB = Database.Database\n\nexport interface XraySummaryPayload {\n session_id: string\n project_path: string\n project_name: string\n total_tokens: number\n total_events: number\n by_source: Array<{ source: string; count: number; tokens: number }>\n by_tool: Array<{ tool_name: string; count: number; tokens: number }>\n cost_haiku: number\n cost_sonnet: number\n cost_opus: number\n probes: {\n serena: { present: boolean; confidence: number; signals: string[] }\n rtk: { present: boolean; confidence: number; signals: string[] }\n mcp_pruning: { present: boolean; confidence: number; signals: string[] }\n prompt_caching: { present: boolean; confidence: number }\n }\n coach_tips_surfaced: Array<{ rule_id: string; tip_ids: string[]; severity: string }>\n schema_measurement: { tool_schema_tokens: number; mcp_servers: string[] }\n optimizer_version: string\n}\n\nexport function buildSessionSummary(\n db: DB,\n sessionId: string,\n version: string,\n): XraySummaryPayload {\n // Usage stats for last 24h (covers the session)\n const usage = getUsageStats(db, 1)\n const cost = getCostReport(db, 1)\n\n // Detection probes (reads local files, no network)\n const serena = probeSerena()\n const rtk = probeRtk()\n const mcpPruning = probeMcpPruning()\n const promptCaching = probePromptCaching()\n\n // Schema measurement (reads settings files, no network)\n const schema = measureCurrentSchemaBytes()\n\n // Coach tips surfaced during this session\n const coachTips = getCoachSurfaceLog(db, sessionId)\n\n const projDir = resolveProjectDir()\n const projName = projDir.split(/[\\\\/]/).filter(Boolean).pop() ?? 'unknown'\n\n return {\n session_id: sessionId,\n project_path: projDir,\n project_name: projName,\n total_tokens: usage.total_tokens,\n total_events: usage.total_events,\n by_source: usage.by_source,\n by_tool: usage.by_tool.map((t) => ({\n tool_name: t.tool_name,\n count: t.count,\n tokens: t.tokens,\n })),\n cost_haiku: cost.estimated_cost_usd_haiku,\n cost_sonnet: cost.estimated_cost_usd_sonnet,\n cost_opus: cost.estimated_cost_usd_opus,\n probes: {\n serena: { present: serena.present, confidence: serena.confidence, signals: serena.signals },\n rtk: { present: rtk.present, confidence: rtk.confidence, signals: rtk.signals },\n mcp_pruning: {\n present: mcpPruning.present,\n confidence: mcpPruning.confidence,\n signals: mcpPruning.signals,\n },\n prompt_caching: { present: promptCaching.present, confidence: promptCaching.confidence },\n },\n coach_tips_surfaced: coachTips,\n schema_measurement: {\n tool_schema_tokens: schema.tool_schema_tokens,\n mcp_servers: schema.mcp_servers,\n },\n optimizer_version: version,\n }\n}\n","// Coach MCP tool — Phase 4.46\n// coach_tips: returns active hits + full knowledge base + context measurement + reference table\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { text, error } from '../lib/response.js'\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\n\ntype DB = Database.Database\n\nexport function registerCoachTools(server: McpServer, db: DB): void {\n server.tool(\n 'coach_tips',\n 'Devuelve tips activos (rules disparadas) y medicion de contexto. Por defecto modo compacto (~500 tokens). Usa verbose=true para incluir el catalogo completo de 18 tips y la tabla de referencia (~3.5k tokens).',\n {\n session_id: z.string().optional().describe('ID de la sesion (default: \"default\")'),\n project_dir: z\n .string()\n .optional()\n .describe('Directorio del proyecto para medir contexto desde transcript JSONL'),\n active_model: z\n .string()\n .optional()\n .describe('Modelo activo (opcional, habilita regla detect-opus-for-simple-task)'),\n verbose: z\n .boolean()\n .optional()\n .describe(\n 'Si true, incluye el knowledge base completo (18 tips) y la reference data. Default false = solo hits activos + contexto. Ahorra ~3.3k tokens por llamada en modo compacto.',\n ),\n },\n async ({ session_id, project_dir, active_model, verbose }) => {\n try {\n const payloadOpts: Parameters<typeof computeCoachTipsPayload>[0] = { db }\n if (session_id !== undefined) payloadOpts.sessionId = session_id\n if (project_dir !== undefined) payloadOpts.projectDir = project_dir\n if (active_model !== undefined) payloadOpts.activeModel = active_model\n const response = await computeCoachTipsPayload(payloadOpts)\n\n // Default compact mode: strip the heavy knowledge_base + reference_data\n // to avoid burning ~3.3k tokens per call. verbose=true restores them.\n if (verbose !== true) {\n const { known_tricks: _kb, reference_data: _ref, ...compact } = response\n return text(JSON.stringify(compact, null, 2))\n }\n return text(JSON.stringify(response, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Reference data table with publicly-verifiable savings numbers — Phase 4.41\n// Every row tagged estimation_method: 'reference_measured'\n\nimport type { EstimationMethod } from '../lib/types.js'\n\nexport interface ReferenceDataRow {\n feature: string\n saving: string\n source: string\n verified_at: string\n estimation_method: EstimationMethod\n}\n\nexport const REFERENCE_DATA: readonly ReferenceDataRow[] = [\n {\n feature: 'Model switching (opusplan / default-to-sonnet)',\n saving: '60-80% reduccion de coste',\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Progressive disclosure skills',\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n source: 'claudefast.com',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Prompt caching read hit',\n saving: '10x mas barato que uncached',\n source: 'Anthropic docs',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Claude Code Tool Search',\n saving: '~85% schema reduction (77k → 8.7k tokens)',\n source: 'observado en sesion',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'MCP pruning sobre Tool Search',\n saving: '~5-12% adicional por turno',\n source: 'estimacion interna',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n]\n\nconst DAY_MS = 86_400_000\n\nexport function getFreshRows(\n daysThreshold = 90,\n today: Date = new Date(),\n): ReferenceDataRow[] {\n const cutoff = today.getTime() - daysThreshold * DAY_MS\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() >= cutoff)\n}\n\nexport function getStaleRows(\n daysThreshold = 90,\n today: Date = new Date(),\n): ReferenceDataRow[] {\n const cutoff = today.getTime() - daysThreshold * DAY_MS\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() < cutoff)\n}\n","// Shared payload builder for coach_tips MCP tool + token-optimizer://coach/tips\n// resource. Keeps tool/resource outputs identical — Phase 4.H.\n\nimport type Database from 'better-sqlite3'\nimport type { ContextMeasurement, EventContext, ToolEvent, DetectionHit, CoachTip } from '../lib/types.js'\nimport { KNOWLEDGE_BASE } from './knowledge-base.js'\nimport { REFERENCE_DATA, getStaleRows } from './reference-data.js'\nimport { runRules } from './detector.js'\nimport { measureContextSize } from './context-meter.js'\nimport { buildQueries } from '../db/queries.js'\n\ntype DB = Database.Database\n\nexport interface CoachTipsPayload {\n current: DetectionHit[]\n known_tricks: readonly CoachTip[]\n context: ContextMeasurement\n reference_data: typeof REFERENCE_DATA\n stale_reference_count: number\n last_computed_at: string\n}\n\nexport interface ComputeCoachTipsPayloadOptions {\n db: DB\n sessionId?: string\n projectDir?: string\n activeModel?: string\n}\n\nexport async function computeCoachTipsPayload(\n opts: ComputeCoachTipsPayloadOptions,\n): Promise<CoachTipsPayload> {\n const { db } = opts\n const sessionId = opts.sessionId ?? 'default'\n\n const contextOpts: Parameters<typeof measureContextSize>[1] = { db }\n if (opts.projectDir !== undefined) contextOpts.projectDir = opts.projectDir\n if (opts.activeModel !== undefined) contextOpts.activeModel = opts.activeModel\n const context = await measureContextSize(sessionId, contextOpts)\n\n const queries = buildQueries(db)\n const since = new Date(Date.now() - 86_400_000).toISOString()\n const rawRows = queries.getToolCallsSince(since) as ToolEvent[]\n const events = rawRows.slice(0, 100)\n\n const ctx: EventContext = {\n session_id: sessionId,\n events,\n session_token_total: context.tokens,\n session_token_method: context.estimation_method,\n session_token_limit: context.limit,\n active_model: opts.activeModel ?? null,\n }\n\n const hits = runRules(ctx)\n const staleTips = getStaleRows()\n\n return {\n current: hits,\n known_tricks: KNOWLEDGE_BASE,\n context,\n reference_data: REFERENCE_DATA,\n stale_reference_count: staleTips.length,\n last_computed_at: new Date().toISOString(),\n }\n}\n","// TOON encoding tools — Phase 5.3\n// toon_encode: data -> compact JSON (no whitespace) = token-efficient\n// toon_decode: toon string -> JSON object\n//\n// Note: the original `toon-format` npm package was deferred during Phase 0\n// due to package-name uncertainty. This implementation uses compact JSON under\n// the hood, which is round-trip lossless and ~30-40% cheaper in tokens than\n// pretty-printed JSON. The tool names are preserved so a real TOON impl can\n// drop in later without changing the MCP API.\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { text, error } from '../lib/response.js'\n\nfunction compactEncode(data: unknown): string {\n try {\n return JSON.stringify(data)\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e)\n if (/circular|cyclic/i.test(msg)) {\n throw new Error('Referencia circular detectada: TOON no soporta objetos ciclicos')\n }\n throw new Error(`No se pudo codificar a TOON: ${msg}`)\n }\n}\n\nfunction compactDecode(toon: string): unknown {\n try {\n return JSON.parse(toon)\n } catch (e) {\n throw new Error(`TOON invalido: ${e instanceof Error ? e.message : String(e)}`)\n }\n}\n\nexport function registerToonTools(server: McpServer): void {\n // ── toon_encode ──\n server.tool(\n 'toon_encode',\n 'Codifica un objeto JSON a formato TOON (JSON compacto token-eficiente, round-trip lossless).',\n {\n data: z.unknown().describe('Valor a codificar (objeto, array, primitivo)'),\n },\n async ({ data }) => {\n try {\n const encoded = compactEncode(data)\n return text(encoded)\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── toon_decode ──\n server.tool(\n 'toon_decode',\n 'Decodifica una cadena TOON a JSON. Devuelve el objeto formateado para lectura.',\n {\n toon: z.string().min(1).describe('Cadena TOON a decodificar'),\n },\n async ({ toon }) => {\n try {\n const decoded = compactDecode(toon)\n return text(JSON.stringify(decoded, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n\n// Exported for tests\nexport const _internal = { compactEncode, compactDecode }\n","// token-optimizer://coach/tips resource — Phase 4.H\n// Mirrors coach_tips() tool payload, readable without a tool call.\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\n\ntype DB = Database.Database\n\nexport const COACH_TIPS_URI = 'token-optimizer://coach/tips'\n\nexport function registerCoachTipsResource(server: McpServer, db: DB): void {\n server.resource(\n 'coach-tips',\n COACH_TIPS_URI,\n {\n description:\n 'Tips activos del coach, catalogo completo de trucos, medicion de contexto y tabla de referencia.',\n mimeType: 'application/json',\n },\n async (uri: URL) => {\n try {\n const payload = await computeCoachTipsPayload({ db })\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify(payload, null, 2),\n },\n ],\n }\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e)\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify({ error: message }),\n },\n ],\n }\n }\n },\n )\n}\n"],"mappings":"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","names":["z","z","z","DAY_MS","DAY_MS","z","z","z"]} |
| #!/usr/bin/env node | ||
| import { | ||
| getActiveBudgetSummary, | ||
| getUsageStats | ||
| } from "./chunk-BJYHIS4U.js"; | ||
| import "./chunk-XAYSOAZV.js"; | ||
| import "./chunk-FT4EJUIT.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-L4GG4274.js"; | ||
| import { | ||
| projectHash, | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/status.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| function runStatus(_args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = path.join(home, ".claude", "settings.json"); | ||
| const installed = (() => { | ||
| try { | ||
| if (!fs.existsSync(settingsPath)) return false; | ||
| const json = JSON.parse(fs.readFileSync(settingsPath, "utf8")); | ||
| const mcp = json.mcpServers ?? {}; | ||
| return "token-optimizer" in mcp; | ||
| } catch { | ||
| return false; | ||
| } | ||
| })(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| let eventsToday = 0; | ||
| let tokensBySource = []; | ||
| let budgetLine = "sin presupuesto activo"; | ||
| if (fs.existsSync(dbPath)) { | ||
| try { | ||
| const db = getDb(dbPath); | ||
| const usage = getUsageStats(db, 1); | ||
| eventsToday = usage.total_events; | ||
| tokensBySource = usage.by_source.map((r) => ({ source: r.source, tokens: r.tokens })); | ||
| const budget = getActiveBudgetSummary(db, "default", projectHash(projectDir)); | ||
| if (budget.active) { | ||
| const pct = (budget.percent_used * 100).toFixed(1); | ||
| budgetLine = `gastado=${budget.spent} restante=${budget.remaining} uso=${pct}% modo=${budget.mode}`; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| const lines = []; | ||
| lines.push("token-optimizer-mcp status"); | ||
| lines.push(""); | ||
| lines.push(`Instalado: ${installed ? "\u2713" : "\u2717"} (${settingsPath})`); | ||
| lines.push(`Storage DB: ${dbPath}${fs.existsSync(dbPath) ? "" : " (no existe aun)"}`); | ||
| lines.push(`Eventos hoy: ${eventsToday}`); | ||
| lines.push( | ||
| `Tokens por fuente: ${tokensBySource.length > 0 ? tokensBySource.map((s) => `${s.source}=${s.tokens}`).join(", ") : "(sin datos)"}` | ||
| ); | ||
| lines.push(`Presupuesto: ${budgetLine}`); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| export { | ||
| runStatus | ||
| }; | ||
| //# sourceMappingURL=status-ATJQRVRE.js.map |
| {"version":3,"sources":["../src/cli/status.ts"],"sourcesContent":["// Status CLI — Phase 4.13\n// Prints install detection, storage DB, events today, tokens by source, active budget.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport os from 'node:os'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath, projectHash } from '../lib/paths.js'\nimport { getUsageStats, getActiveBudgetSummary } from '../services/stats.js'\n\nexport interface StatusOptions {\n home?: string\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runStatus(_args: string[] = [], opts: StatusOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const home = opts.home ?? os.homedir()\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = path.join(home, '.claude', 'settings.json')\n\n const installed = (() => {\n try {\n if (!fs.existsSync(settingsPath)) return false\n const json = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as Record<string, unknown>\n const mcp = (json.mcpServers ?? {}) as Record<string, unknown>\n return 'token-optimizer' in mcp\n } catch {\n return false\n }\n })()\n\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n let eventsToday = 0\n let tokensBySource: Array<{ source: string; tokens: number }> = []\n let budgetLine = 'sin presupuesto activo'\n\n if (fs.existsSync(dbPath)) {\n try {\n const db = getDb(dbPath)\n const usage = getUsageStats(db, 1)\n eventsToday = usage.total_events\n tokensBySource = usage.by_source.map((r) => ({ source: r.source, tokens: r.tokens }))\n const budget = getActiveBudgetSummary(db, 'default', projectHash(projectDir))\n if (budget.active) {\n const pct = (budget.percent_used * 100).toFixed(1)\n budgetLine = `gastado=${budget.spent} restante=${budget.remaining} uso=${pct}% modo=${budget.mode}`\n }\n } catch {\n // swallow\n }\n }\n\n const lines: string[] = []\n lines.push('token-optimizer-mcp status')\n lines.push('')\n lines.push(`Instalado: ${installed ? '✓' : '✗'} (${settingsPath})`)\n lines.push(`Storage DB: ${dbPath}${fs.existsSync(dbPath) ? '' : ' (no existe aun)'}`)\n lines.push(`Eventos hoy: ${eventsToday}`)\n lines.push(\n `Tokens por fuente: ${\n tokensBySource.length > 0\n ? tokensBySource.map((s) => `${s.source}=${s.tokens}`).join(', ')\n : '(sin datos)'\n }`,\n )\n lines.push(`Presupuesto: ${budgetLine}`)\n\n print(lines.join('\\n'))\n return 0\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAGA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAWR,SAAS,UAAU,QAAkB,CAAC,GAAG,OAAsB,CAAC,GAAW;AAChF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ;AACrC,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,eAAe,KAAK,KAAK,MAAM,WAAW,eAAe;AAE/D,QAAM,aAAa,MAAM;AACvB,QAAI;AACF,UAAI,CAAC,GAAG,WAAW,YAAY,EAAG,QAAO;AACzC,YAAM,OAAO,KAAK,MAAM,GAAG,aAAa,cAAc,MAAM,CAAC;AAC7D,YAAM,MAAO,KAAK,cAAc,CAAC;AACjC,aAAO,qBAAqB;AAAA,IAC9B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,MAAI,cAAc;AAClB,MAAI,iBAA4D,CAAC;AACjE,MAAI,aAAa;AAEjB,MAAI,GAAG,WAAW,MAAM,GAAG;AACzB,QAAI;AACF,YAAM,KAAK,MAAM,MAAM;AACvB,YAAM,QAAQ,cAAc,IAAI,CAAC;AACjC,oBAAc,MAAM;AACpB,uBAAiB,MAAM,UAAU,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,QAAQ,EAAE,OAAO,EAAE;AACpF,YAAM,SAAS,uBAAuB,IAAI,WAAW,YAAY,UAAU,CAAC;AAC5E,UAAI,OAAO,QAAQ;AACjB,cAAM,OAAO,OAAO,eAAe,KAAK,QAAQ,CAAC;AACjD,qBAAa,WAAW,OAAO,KAAK,aAAa,OAAO,SAAS,QAAQ,GAAG,UAAU,OAAO,IAAI;AAAA,MACnG;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,4BAA4B;AACvC,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,uBAAuB,YAAY,WAAM,QAAG,KAAK,YAAY,GAAG;AAC3E,QAAM,KAAK,uBAAuB,MAAM,GAAG,GAAG,WAAW,MAAM,IAAI,KAAK,kBAAkB,EAAE;AAC5F,QAAM,KAAK,uBAAuB,WAAW,EAAE;AAC/C,QAAM;AAAA,IACJ,uBACE,eAAe,SAAS,IACpB,eAAe,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,IAAI,IAC9D,aACN;AAAA,EACF;AACA,QAAM,KAAK,uBAAuB,UAAU,EAAE;AAE9C,QAAM,MAAM,KAAK,IAAI,CAAC;AACtB,SAAO;AACT;","names":[]} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
759279
0.6%7030
0.23%