@cocaxcode/token-optimizer-mcp
Advanced tools
| #!/usr/bin/env node | ||
| import { | ||
| resolveXrayUrl | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.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.5.0" : "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-K2MBDETU.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 { | ||
| KNOWLEDGE_BASE, | ||
| clearSurfaceLog, | ||
| measureContextSize, | ||
| runRules | ||
| } from "./chunk-K2MBDETU.js"; | ||
| import "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.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-VZEQ2SZP.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-SITLVH3I.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-PGH4NXTW.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-BC6HFBNA.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-VZEQ2SZP.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-DSDEBDF6.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 { | ||
| probeSerenaPresence | ||
| } from "./chunk-2NKYFIPW.js"; | ||
| import { | ||
| ensureStorageDir | ||
| } from "./chunk-VHP52B3J.js"; | ||
| import { | ||
| getConfigPath, | ||
| loadConfig, | ||
| saveConfig | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| runDoctor | ||
| } from "./chunk-QWZDLYG4.js"; | ||
| import "./chunk-DSMHUGNS.js"; | ||
| import "./chunk-L5Z32XXL.js"; | ||
| import "./chunk-V4PINTCV.js"; | ||
| // src/cli/install.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| import { spawnSync } from "child_process"; | ||
| var SERVER_NAME = "token-optimizer"; | ||
| function resolveHookCommandBase() { | ||
| try { | ||
| const globalRoot = path.join(os.homedir(), "AppData", "Roaming", "npm", "node_modules"); | ||
| const indexPath = path.join(globalRoot, "@cocaxcode", "token-optimizer-mcp", "dist", "index.js"); | ||
| if (fs.existsSync(indexPath)) { | ||
| return `node "${indexPath.replace(/\\/g, "/")}"`; | ||
| } | ||
| } catch { | ||
| } | ||
| const unixPaths = [ | ||
| "/usr/local/lib/node_modules", | ||
| "/usr/lib/node_modules", | ||
| path.join(os.homedir(), ".npm-global", "lib", "node_modules") | ||
| ]; | ||
| for (const root of unixPaths) { | ||
| try { | ||
| const indexPath = path.join(root, "@cocaxcode", "token-optimizer-mcp", "dist", "index.js"); | ||
| if (fs.existsSync(indexPath)) { | ||
| return `node "${indexPath}"`; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| try { | ||
| const result = spawnSync("npm", ["root", "-g"], { encoding: "utf8", timeout: 3e3, shell: true }); | ||
| const npmRoot = (result.stdout ?? "").trim(); | ||
| const indexPath = path.join(npmRoot, "@cocaxcode", "token-optimizer-mcp", "dist", "index.js"); | ||
| if (fs.existsSync(indexPath)) { | ||
| return `node "${indexPath.replace(/\\/g, "/")}"`; | ||
| } | ||
| } catch { | ||
| } | ||
| return "npx @cocaxcode/token-optimizer-mcp"; | ||
| } | ||
| function settingsPath(home) { | ||
| return path.join(home, ".claude", "settings.json"); | ||
| } | ||
| function readSettings(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return {}; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| function writeSettings(p, data) { | ||
| const dir = path.dirname(p); | ||
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(data, null, 2)); | ||
| } | ||
| function extractHookFlag(command) { | ||
| const match = command.match(/--hook\s+(\S+)/); | ||
| return match ? `--hook ${match[1]}` : null; | ||
| } | ||
| function removeHook(allHooks, eventName, matcher, identifier) { | ||
| const existing = allHooks[eventName]; | ||
| if (!Array.isArray(existing)) return 0; | ||
| const list = existing; | ||
| const matchEntry = list.find((e) => e.matcher === matcher); | ||
| if (!matchEntry || !Array.isArray(matchEntry.hooks)) return 0; | ||
| const before = matchEntry.hooks.length; | ||
| matchEntry.hooks = matchEntry.hooks.filter( | ||
| (h) => !(typeof h.command === "string" && h.command.includes(identifier)) | ||
| ); | ||
| const removed = before - matchEntry.hooks.length; | ||
| if (removed === 0) return 0; | ||
| if (matchEntry.hooks.length === 0) { | ||
| const idx = list.indexOf(matchEntry); | ||
| if (idx >= 0) list.splice(idx, 1); | ||
| } | ||
| if (list.length === 0) { | ||
| delete allHooks[eventName]; | ||
| } else { | ||
| allHooks[eventName] = list; | ||
| } | ||
| return removed; | ||
| } | ||
| function upsertHook(allHooks, eventName, matcher, command, upsertOpts = {}) { | ||
| const identifier = upsertOpts.identifier ?? "token-optimizer"; | ||
| const useFlagDisambiguation = upsertOpts.useFlagDisambiguation ?? true; | ||
| const existing = allHooks[eventName] ?? []; | ||
| const list = Array.isArray(existing) ? [...existing] : []; | ||
| const matchEntry = list.find((e) => e.matcher === matcher); | ||
| const ourHandler = { type: "command", command }; | ||
| const ourFlag = useFlagDisambiguation ? extractHookFlag(command) : null; | ||
| if (matchEntry) { | ||
| const handlers = Array.isArray(matchEntry.hooks) ? [...matchEntry.hooks] : []; | ||
| let idx = -1; | ||
| if (ourFlag) { | ||
| idx = handlers.findIndex( | ||
| (h) => typeof h.command === "string" && h.command.includes(identifier) && extractHookFlag(h.command) === ourFlag | ||
| ); | ||
| } | ||
| if (idx < 0) { | ||
| idx = handlers.findIndex( | ||
| (h) => typeof h.command === "string" && h.command.includes(identifier) && extractHookFlag(h.command) === null | ||
| ); | ||
| } | ||
| if (idx >= 0) { | ||
| handlers[idx] = ourHandler; | ||
| } else { | ||
| handlers.push(ourHandler); | ||
| } | ||
| matchEntry.hooks = handlers; | ||
| } else { | ||
| list.push({ matcher, hooks: [ourHandler] }); | ||
| } | ||
| allHooks[eventName] = list; | ||
| } | ||
| function runInstall(_args = [], opts = {}) { | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const p = settingsPath(home); | ||
| const settings = readSettings(p); | ||
| const mcpServers = settings.mcpServers ?? {}; | ||
| mcpServers[SERVER_NAME] = { | ||
| command: "npx", | ||
| args: ["-y", "@cocaxcode/token-optimizer-mcp", "--mcp"] | ||
| }; | ||
| settings.mcpServers = mcpServers; | ||
| const hookBase = resolveHookCommandBase(); | ||
| const hooks = settings.hooks ?? {}; | ||
| upsertHook(hooks, "PreToolUse", "Bash", `${hookBase} --hook pretooluse`); | ||
| upsertHook(hooks, "PostToolUse", "*", `${hookBase} --hook posttooluse`); | ||
| upsertHook(hooks, "SessionStart", "compact", `${hookBase} --hook sessionstart`); | ||
| const serenaProbe = opts.serenaProbe ?? probeSerenaPresence(); | ||
| const wantOfficialHooks = serenaProbe.serena_cli_installed && opts.skipSerenaHooks !== true; | ||
| if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) { | ||
| upsertHook(hooks, "SessionStart", "", `${hookBase} --hook serena-activate`); | ||
| } | ||
| if (wantOfficialHooks) { | ||
| upsertHook(hooks, "PreToolUse", "", "serena-hooks remind --client=claude-code", { | ||
| identifier: "serena-hooks remind", | ||
| useFlagDisambiguation: false | ||
| }); | ||
| upsertHook( | ||
| hooks, | ||
| "PreToolUse", | ||
| "mcp__serena__.*", | ||
| "serena-hooks auto-approve --client=claude-code", | ||
| { | ||
| identifier: "serena-hooks auto-approve", | ||
| useFlagDisambiguation: false | ||
| } | ||
| ); | ||
| upsertHook(hooks, "Stop", "", "serena-hooks cleanup --client=claude-code", { | ||
| identifier: "serena-hooks cleanup", | ||
| useFlagDisambiguation: false | ||
| }); | ||
| } else { | ||
| removeHook(hooks, "PreToolUse", "", "serena-hooks remind"); | ||
| removeHook(hooks, "PreToolUse", "mcp__serena__.*", "serena-hooks auto-approve"); | ||
| removeHook(hooks, "Stop", "", "serena-hooks cleanup"); | ||
| } | ||
| settings.hooks = hooks; | ||
| let shadowAutoEnabled = false; | ||
| if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) { | ||
| const configPath = getConfigPath(home); | ||
| let userHasExplicitFlag = false; | ||
| try { | ||
| if (fs.existsSync(configPath)) { | ||
| const raw = fs.readFileSync(configPath, "utf8"); | ||
| const parsed = JSON.parse(raw); | ||
| const sm = parsed.shadow_measurement; | ||
| userHasExplicitFlag = sm !== void 0 && sm !== null && "serena" in sm; | ||
| } | ||
| } catch { | ||
| } | ||
| if (!userHasExplicitFlag) { | ||
| const cfg = loadConfig(home); | ||
| if (!cfg.shadow_measurement.serena) { | ||
| cfg.shadow_measurement.serena = true; | ||
| saveConfig(cfg, home); | ||
| shadowAutoEnabled = true; | ||
| } | ||
| } | ||
| } | ||
| writeSettings(p, settings); | ||
| const globalDir = path.join(home, ".token-optimizer"); | ||
| if (!fs.existsSync(globalDir)) fs.mkdirSync(globalDir, { recursive: true }); | ||
| if (fs.existsSync(path.join(cwd, ".git"))) { | ||
| ensureStorageDir(cwd); | ||
| } | ||
| print("token-optimizer-mcp instalado correctamente."); | ||
| print(` settings: ${p}`); | ||
| print(` global: ${globalDir}`); | ||
| if (serenaProbe.serena_cli_installed) { | ||
| print(` serena: CLI detectado \u2014 4 hooks registrados`); | ||
| print(` (activate + remind + auto-approve + cleanup)`); | ||
| if (opts.skipSerenaHooks) { | ||
| print(` note: --skipSerenaHooks activo \u2192 solo se registr\xF3 serena-activate`); | ||
| } | ||
| } else if (serenaProbe.serena_mcp_registered) { | ||
| print(` serena: MCP detectado pero CLI no instalado`); | ||
| print(` \u2192 solo se registr\xF3 serena-activate`); | ||
| print(` \u2192 para los otros 3: uv tool install git+https://github.com/oraios/serena`); | ||
| } else { | ||
| print(` serena: no detectado \u2014 hooks de serena omitidos`); | ||
| } | ||
| if (shadowAutoEnabled) { | ||
| print(` shadow_measurement.serena = true (auto-activado)`); | ||
| print(` \u2192 mide ahorro real vs lectura completa de archivo por cada call`); | ||
| } | ||
| if (opts.runDoctorAtEnd !== false) { | ||
| print(""); | ||
| runDoctor([], { cwd, home, print }); | ||
| } | ||
| return 0; | ||
| } | ||
| export { | ||
| runInstall | ||
| }; | ||
| //# sourceMappingURL=install-SITLVH3I.js.map |
| {"version":3,"sources":["../src/cli/install.ts"],"sourcesContent":["// Install CLI — Phase 4.10\n// Writes token-optimizer mcpServers entry + 3 hooks into ~/.claude/settings.json\n// Also creates per-project storage dir and appends .gitignore in git repos.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport os from 'node:os'\nimport { spawnSync } from 'node:child_process'\nimport { ensureStorageDir } from '../lib/storage.js'\nimport { probeSerenaPresence, type SerenaProbe } from '../hooks/serena-activate.js'\nimport { runDoctor } from './doctor.js'\nimport { loadConfig, saveConfig, getConfigPath } from './config.js'\n\nconst SERVER_NAME = 'token-optimizer'\n\n/**\n * Resolve the hook command base.\n * Prefer `node <global-path>/dist/index.js` for speed (~0.2s vs ~1.5s with npx).\n * Falls back to `npx @cocaxcode/token-optimizer-mcp` if global path not found.\n */\nfunction resolveHookCommandBase(): string {\n try {\n const globalRoot = path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules')\n const indexPath = path.join(globalRoot, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\n if (fs.existsSync(indexPath)) {\n return `node \"${indexPath.replace(/\\\\/g, '/')}\"`\n }\n } catch { /* fallback */ }\n\n // Unix global paths\n const unixPaths = [\n '/usr/local/lib/node_modules',\n '/usr/lib/node_modules',\n path.join(os.homedir(), '.npm-global', 'lib', 'node_modules'),\n ]\n for (const root of unixPaths) {\n try {\n const indexPath = path.join(root, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\n if (fs.existsSync(indexPath)) {\n return `node \"${indexPath}\"`\n }\n } catch { /* fallback */ }\n }\n\n // npm root -g fallback\n try {\n const result = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 3000, shell: true })\n const npmRoot = (result.stdout ?? '').trim()\n const indexPath = path.join(npmRoot, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\n if (fs.existsSync(indexPath)) {\n return `node \"${indexPath.replace(/\\\\/g, '/')}\"`\n }\n } catch { /* fallback */ }\n\n return 'npx @cocaxcode/token-optimizer-mcp'\n}\n\nexport interface InstallOptions {\n home?: string\n cwd?: string\n print?: (msg: string) => void\n runDoctorAtEnd?: boolean\n /**\n * Override the Serena presence probe. Used by tests to get deterministic\n * behaviour independent of whether the test host actually has Serena.\n * Undefined = probe the real filesystem/PATH.\n */\n serenaProbe?: SerenaProbe\n /**\n * Skip installing the 3 official Serena reminder hooks (remind, auto-approve,\n * cleanup) even when Serena is detected. The Serena-activate hook we own is\n * still installed. Use this if you prefer managing the official hooks yourself\n * or you don't want to depend on Serena's alpha feature.\n */\n skipSerenaHooks?: boolean\n}\n\nfunction settingsPath(home: string): string {\n return path.join(home, '.claude', 'settings.json')\n}\n\nfunction readSettings(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 writeSettings(p: string, data: Record<string, unknown>): void {\n const dir = path.dirname(p)\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(p, JSON.stringify(data, null, 2))\n}\n\ninterface HookEntry {\n matcher?: string\n hooks?: Array<{ type?: string; command?: string }>\n [key: string]: unknown\n}\n\n/**\n * Extract the `--hook <kind>` flag from a command line so we can use it as\n * a unique identity for upsert. `node .../index.js --hook serena-activate`\n * becomes `--hook serena-activate`. Anything without a `--hook X` returns null.\n */\nfunction extractHookFlag(command: string): string | null {\n const match = command.match(/--hook\\s+(\\S+)/)\n return match ? `--hook ${match[1]}` : null\n}\n\n/**\n * Remove any handlers whose command contains `identifier` from the given\n * (eventName, matcher) group. Used to un-register hooks that were installed\n * in a previous run but no longer apply (e.g. the 3 official Serena hooks\n * when the `serena-hooks` CLI is no longer in PATH).\n *\n * Returns the number of handlers removed. If the matcher group becomes empty,\n * the whole group is dropped from the event list. If the event itself becomes\n * empty, the event key is deleted from the hooks map.\n */\nfunction removeHook(\n allHooks: Record<string, unknown>,\n eventName: string,\n matcher: string,\n identifier: string,\n): number {\n const existing = allHooks[eventName]\n if (!Array.isArray(existing)) return 0\n const list = existing as HookEntry[]\n const matchEntry = list.find((e) => e.matcher === matcher)\n if (!matchEntry || !Array.isArray(matchEntry.hooks)) return 0\n\n const before = matchEntry.hooks.length\n matchEntry.hooks = matchEntry.hooks.filter(\n (h) => !(typeof h.command === 'string' && h.command.includes(identifier)),\n )\n const removed = before - matchEntry.hooks.length\n if (removed === 0) return 0\n\n // Clean empty matcher groups\n if (matchEntry.hooks.length === 0) {\n const idx = list.indexOf(matchEntry)\n if (idx >= 0) list.splice(idx, 1)\n }\n // Clean empty event\n if (list.length === 0) {\n delete allHooks[eventName]\n } else {\n allHooks[eventName] = list\n }\n return removed\n}\n\n/**\n * Options for upsertHook.\n * - `identifier`: a substring that uniquely identifies an existing handler of\n * the same kind so we can replace it in place. Defaults to \"token-optimizer\"\n * for our own hooks. For external hooks (e.g. serena-hooks), callers should\n * pass something like \"serena-hooks remind\".\n * - `useFlagDisambiguation`: if true, use the `--hook X` flag as extra\n * disambiguation so multiple token-optimizer hooks can coexist in the same\n * matcher group without trampling each other. Default true.\n */\ninterface UpsertHookOptions {\n identifier?: string\n useFlagDisambiguation?: boolean\n}\n\nfunction upsertHook(\n allHooks: Record<string, unknown>,\n eventName: string,\n matcher: string,\n command: string,\n upsertOpts: UpsertHookOptions = {},\n): void {\n const identifier = upsertOpts.identifier ?? 'token-optimizer'\n const useFlagDisambiguation = upsertOpts.useFlagDisambiguation ?? true\n\n const existing = (allHooks[eventName] ?? []) as HookEntry[]\n const list: HookEntry[] = Array.isArray(existing) ? [...existing] : []\n const matchEntry = list.find((e) => e.matcher === matcher)\n const ourHandler = { type: 'command', command }\n const ourFlag = useFlagDisambiguation ? extractHookFlag(command) : null\n\n if (matchEntry) {\n const handlers = Array.isArray(matchEntry.hooks) ? [...matchEntry.hooks] : []\n // 1) Preferred: find the exact handler we own by identifier + flag.\n let idx = -1\n if (ourFlag) {\n idx = handlers.findIndex(\n (h) =>\n typeof h.command === 'string' &&\n h.command.includes(identifier) &&\n extractHookFlag(h.command) === ourFlag,\n )\n }\n // 2) Fallback: any handler that includes the identifier (and doesn't\n // have a --hook flag of its own so we don't steal a sibling's slot).\n if (idx < 0) {\n idx = handlers.findIndex(\n (h) =>\n typeof h.command === 'string' &&\n h.command.includes(identifier) &&\n extractHookFlag(h.command) === null,\n )\n }\n\n if (idx >= 0) {\n handlers[idx] = ourHandler\n } else {\n handlers.push(ourHandler)\n }\n matchEntry.hooks = handlers\n } else {\n list.push({ matcher, hooks: [ourHandler] })\n }\n allHooks[eventName] = list\n}\n\nexport function runInstall(_args: string[] = [], opts: InstallOptions = {}): number {\n const home = opts.home ?? os.homedir()\n const cwd = opts.cwd ?? process.cwd()\n const print = opts.print ?? ((m: string) => console.error(m))\n\n const p = settingsPath(home)\n const settings = readSettings(p)\n\n // mcpServers upsert\n const mcpServers = (settings.mcpServers ?? {}) as Record<string, unknown>\n mcpServers[SERVER_NAME] = {\n command: 'npx',\n args: ['-y', '@cocaxcode/token-optimizer-mcp', '--mcp'],\n }\n settings.mcpServers = mcpServers\n\n // hooks upsert — prefer node direct for speed (~0.2s vs ~1.5s with npx)\n const hookBase = resolveHookCommandBase()\n const hooks = (settings.hooks ?? {}) as Record<string, unknown>\n upsertHook(hooks, 'PreToolUse', 'Bash', `${hookBase} --hook pretooluse`)\n upsertHook(hooks, 'PostToolUse', '*', `${hookBase} --hook posttooluse`)\n upsertHook(hooks, 'SessionStart', 'compact', `${hookBase} --hook sessionstart`)\n\n // Serena integration — two independent decisions based on two probe signals:\n //\n // (a) `--hook serena-activate` (our own SessionStart hook). Fixes the\n // ToolSearch gap in the official `serena-hooks activate` output. Does\n // NOT shell out to any binary — it's a node entry point that emits\n // JSON. Gated by `serena_mcp_registered` (i.e. the user uses Serena\n // as an MCP server at all).\n //\n // (b) The 3 OFFICIAL Serena reminder hooks (remind, auto-approve, cleanup).\n // These ARE invoked as `serena-hooks <cmd> ...` at runtime by Claude\n // Code, so they require the actual CLI binary to be on PATH. Gated by\n // `serena_cli_installed`. If the CLI disappears (user uninstalled\n // Serena, or the probe was wrong in a previous release), we actively\n // REMOVE the orphan entries so settings.json stops pointing at a\n // missing binary.\n //\n // Both blocks can be individually skipped via `skipSerenaHooks: true`.\n const serenaProbe = opts.serenaProbe ?? probeSerenaPresence()\n const wantOfficialHooks =\n serenaProbe.serena_cli_installed && opts.skipSerenaHooks !== true\n\n if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) {\n upsertHook(hooks, 'SessionStart', '', `${hookBase} --hook serena-activate`)\n }\n\n if (wantOfficialHooks) {\n upsertHook(hooks, 'PreToolUse', '', 'serena-hooks remind --client=claude-code', {\n identifier: 'serena-hooks remind',\n useFlagDisambiguation: false,\n })\n upsertHook(\n hooks,\n 'PreToolUse',\n 'mcp__serena__.*',\n 'serena-hooks auto-approve --client=claude-code',\n {\n identifier: 'serena-hooks auto-approve',\n useFlagDisambiguation: false,\n },\n )\n upsertHook(hooks, 'Stop', '', 'serena-hooks cleanup --client=claude-code', {\n identifier: 'serena-hooks cleanup',\n useFlagDisambiguation: false,\n })\n } else {\n // Reconcile: if the 3 official hooks were added by a previous install\n // (maybe from a buggier probe that accepted ~/.serena/ as sufficient),\n // but the CLI isn't actually available now, take them OUT so Claude\n // Code stops logging \"command not found\" on every hook dispatch.\n removeHook(hooks, 'PreToolUse', '', 'serena-hooks remind')\n removeHook(hooks, 'PreToolUse', 'mcp__serena__.*', 'serena-hooks auto-approve')\n removeHook(hooks, 'Stop', '', 'serena-hooks cleanup')\n }\n settings.hooks = hooks\n\n // Auto-activar shadow_measurement.serena si:\n // - Serena está registrada (MCP o CLI)\n // - El usuario NO ha puesto explícitamente el flag (ni true ni false)\n // Si ya lo tocó (aunque sea a false), respetamos su decisión.\n // Con el flag activo, cada call a serena en PostToolUse mide\n // shadow_delta_tokens = fullFileTokens - serena_output_tokens, que es lo que\n // xray enseña como ahorro real.\n let shadowAutoEnabled = false\n if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) {\n const configPath = getConfigPath(home)\n let userHasExplicitFlag = false\n try {\n if (fs.existsSync(configPath)) {\n const raw = fs.readFileSync(configPath, 'utf8')\n const parsed = JSON.parse(raw) as Record<string, unknown>\n const sm = parsed.shadow_measurement as Record<string, unknown> | undefined\n userHasExplicitFlag = sm !== undefined && sm !== null && 'serena' in sm\n }\n } catch {\n // archivo corrupto o no legible → tratamos como si no estuviera\n }\n if (!userHasExplicitFlag) {\n const cfg = loadConfig(home)\n if (!cfg.shadow_measurement.serena) {\n cfg.shadow_measurement.serena = true\n saveConfig(cfg, home)\n shadowAutoEnabled = true\n }\n }\n }\n\n writeSettings(p, settings)\n\n // Global storage dir\n const globalDir = path.join(home, '.token-optimizer')\n if (!fs.existsSync(globalDir)) fs.mkdirSync(globalDir, { recursive: true })\n\n // Per-project storage dir (only in git repos)\n if (fs.existsSync(path.join(cwd, '.git'))) {\n ensureStorageDir(cwd)\n }\n\n print('token-optimizer-mcp instalado correctamente.')\n print(` settings: ${p}`)\n print(` global: ${globalDir}`)\n\n // Serena status — 3 states\n if (serenaProbe.serena_cli_installed) {\n print(` serena: CLI detectado — 4 hooks registrados`)\n print(` (activate + remind + auto-approve + cleanup)`)\n if (opts.skipSerenaHooks) {\n print(` note: --skipSerenaHooks activo → solo se registró serena-activate`)\n }\n } else if (serenaProbe.serena_mcp_registered) {\n print(` serena: MCP detectado pero CLI no instalado`)\n print(` → solo se registró serena-activate`)\n print(` → para los otros 3: uv tool install git+https://github.com/oraios/serena`)\n } else {\n print(` serena: no detectado — hooks de serena omitidos`)\n }\n\n if (shadowAutoEnabled) {\n print(` shadow_measurement.serena = true (auto-activado)`)\n print(` → mide ahorro real vs lectura completa de archivo por cada call`)\n }\n\n if (opts.runDoctorAtEnd !== false) {\n print('')\n runDoctor([], { cwd, home, print })\n }\n\n return 0\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| getCoachSurfaceLog, | ||
| measureContextSize, | ||
| postSummaryToXray, | ||
| runRules | ||
| } from "./chunk-K2MBDETU.js"; | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| rollback | ||
| } from "./chunk-3CAOEPYE.js"; | ||
| import { | ||
| ensureStorageDir | ||
| } from "./chunk-VHP52B3J.js"; | ||
| import "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildSuggestions, | ||
| checkSerenaHealth, | ||
| probeMcpPruning, | ||
| probePromptCaching, | ||
| probeRtk, | ||
| probeSerena | ||
| } from "./chunk-DSMHUGNS.js"; | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-L5Z32XXL.js"; | ||
| import { | ||
| getCostReport, | ||
| getUsageStats | ||
| } from "./chunk-633PY32C.js"; | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-VV5KKIQ4.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.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.5.0" : "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-CD2LT3JH.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"]} |
+137
-23
@@ -9,7 +9,4 @@ #!/usr/bin/env node | ||
| surfaceWithDedupe | ||
| } from "./chunk-FVA4JKVS.js"; | ||
| } from "./chunk-K2MBDETU.js"; | ||
| import { | ||
| loadConfig | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| runSerenaActivateHookFromCli | ||
@@ -21,2 +18,5 @@ } from "./chunk-2NKYFIPW.js"; | ||
| import { | ||
| loadConfig | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| BudgetManager | ||
@@ -38,3 +38,3 @@ } from "./chunk-VV5KKIQ4.js"; | ||
| // src/hooks/posttooluse.ts | ||
| import fs2 from "fs"; | ||
| import fs3 from "fs"; | ||
@@ -303,4 +303,99 @@ // src/services/analytics-logger.ts | ||
| // src/services/rtk-reader.ts | ||
| import Database from "better-sqlite3"; | ||
| import fs2 from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| var RTK_FALLBACK_RATIO = 0.7; | ||
| var RTK_MARKER_RE = /\[rtk: filtered (\d+) tokens\]/i; | ||
| function defaultRtkDbPath(home = os.homedir()) { | ||
| return path.join(home, ".rtk", "tracking.db"); | ||
| } | ||
| function importFromRtkDb(dbPath) { | ||
| const p = dbPath ?? defaultRtkDbPath(); | ||
| if (!fs2.existsSync(p)) return null; | ||
| let db = null; | ||
| try { | ||
| db = new Database(p, { fileMustExist: true, readonly: true }); | ||
| const rows = db.prepare( | ||
| `SELECT tool_name, command, filtered_tokens, created_at | ||
| FROM tracking | ||
| ORDER BY created_at DESC | ||
| LIMIT 1000` | ||
| ).all(); | ||
| return rows.map((r) => ({ | ||
| tool_name: "Bash", | ||
| command: r.command ?? "", | ||
| filtered_tokens: r.filtered_tokens ?? 0, | ||
| created_at: r.created_at, | ||
| strategy: "rtk_db", | ||
| estimation_method: "estimated_rtk_db" | ||
| })); | ||
| } catch { | ||
| return null; | ||
| } finally { | ||
| if (db) { | ||
| try { | ||
| db.close(); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| } | ||
| function extractMarkerFromContent(content) { | ||
| if (!content) return null; | ||
| const match = RTK_MARKER_RE.exec(content); | ||
| if (!match) return null; | ||
| const parsed = parseInt(match[1], 10); | ||
| return Number.isFinite(parsed) ? parsed : null; | ||
| } | ||
| function applyFallback(filteredCount) { | ||
| if (!Number.isFinite(filteredCount) || filteredCount < 0) return 0; | ||
| return Math.round(filteredCount * RTK_FALLBACK_RATIO); | ||
| } | ||
| var RTK_DB_CACHE_TTL_MS = 3e4; | ||
| var rtkDbCache = null; | ||
| function getCachedRtkEvents(dbPath) { | ||
| const now = Date.now(); | ||
| if (rtkDbCache && now - rtkDbCache.loadedAt < RTK_DB_CACHE_TTL_MS) { | ||
| return rtkDbCache.events; | ||
| } | ||
| const events = importFromRtkDb(dbPath); | ||
| rtkDbCache = { events, loadedAt: now }; | ||
| return events; | ||
| } | ||
| function safeStringifyResponse(v) { | ||
| if (v == null) return null; | ||
| if (typeof v === "string") return v; | ||
| try { | ||
| return JSON.stringify(v); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function measureRtkDelta(opts) { | ||
| const content = safeStringifyResponse(opts.toolResponse); | ||
| const markerDelta = extractMarkerFromContent(content); | ||
| if (markerDelta !== null && markerDelta > 0) { | ||
| return { delta: markerDelta, method: "estimated_rtk_marker" }; | ||
| } | ||
| if (opts.command && opts.command.trim().length > 0) { | ||
| const events = getCachedRtkEvents(opts.rtkDbPath ?? void 0); | ||
| if (events && events.length > 0) { | ||
| const cmdNorm = opts.command.trim(); | ||
| const match = events.find((e) => (e.command ?? "").trim() === cmdNorm); | ||
| if (match && match.filtered_tokens > 0) { | ||
| return { delta: match.filtered_tokens, method: "estimated_rtk_db" }; | ||
| } | ||
| } | ||
| } | ||
| const fallbackDelta = applyFallback(opts.outputTokens); | ||
| if (fallbackDelta > 0) { | ||
| return { delta: fallbackDelta, method: "estimated_rtk_fallback" }; | ||
| } | ||
| return null; | ||
| } | ||
| // src/hooks/posttooluse.ts | ||
| import path from "path"; | ||
| import path2 from "path"; | ||
| var SERENA_SHADOW_TOOLS = /* @__PURE__ */ new Set([ | ||
@@ -312,3 +407,3 @@ "mcp__serena__find_symbol", | ||
| try { | ||
| return fs2.readFileSync(0, "utf8"); | ||
| return fs3.readFileSync(0, "utf8"); | ||
| } catch { | ||
@@ -403,2 +498,21 @@ return ""; | ||
| const cfg = loadConfig(opts.home); | ||
| if (event.source === "rtk" && event.shadow_delta_tokens == null) { | ||
| try { | ||
| const rawInput = parsed.tool_input; | ||
| const cmd = rawInput?.command; | ||
| const rtkMeasurement = measureRtkDelta({ | ||
| toolResponse: parsed.tool_response, | ||
| command: typeof cmd === "string" ? cmd : void 0, | ||
| outputTokens: event.tokens_estimated, | ||
| rtkDbPath: cfg.rtk_integration.rtk_db_path | ||
| }); | ||
| if (rtkMeasurement) { | ||
| event.shadow_delta_tokens = rtkMeasurement.delta; | ||
| if (rtkMeasurement.method !== "estimated_rtk_fallback") { | ||
| event.estimation_method = rtkMeasurement.method; | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| if (SERENA_SHADOW_TOOLS.has(toolName) && cfg.shadow_measurement.serena) { | ||
@@ -410,3 +524,3 @@ try { | ||
| const projectDir2 = opts.projectDir ?? resolveProjectDir(); | ||
| const absPath = path.resolve(projectDir2, relPath); | ||
| const absPath = path2.resolve(projectDir2, relPath); | ||
| const shadow = shadowMeasureSerena( | ||
@@ -476,9 +590,9 @@ { file_path: absPath, tokens_estimated: event.tokens_estimated, tool_name: toolName }, | ||
| // src/hooks/pretooluse.ts | ||
| import fs4 from "fs"; | ||
| import fs5 from "fs"; | ||
| // src/lib/rtk-bridge.ts | ||
| import { spawnSync } from "child_process"; | ||
| import fs3 from "fs"; | ||
| import path2 from "path"; | ||
| import os from "os"; | ||
| import fs4 from "fs"; | ||
| import path3 from "path"; | ||
| import os2 from "os"; | ||
| var IS_WINDOWS = process.platform === "win32"; | ||
@@ -493,4 +607,4 @@ var RTK_BIN = IS_WINDOWS ? "rtk.exe" : "rtk"; | ||
| for (const dir of opts.searchPaths) { | ||
| const candidate = path2.join(dir, RTK_BIN); | ||
| if (fs3.existsSync(candidate)) { | ||
| const candidate = path3.join(dir, RTK_BIN); | ||
| if (fs4.existsSync(candidate)) { | ||
| cachedRtkPath = candidate; | ||
@@ -519,8 +633,8 @@ return candidate; | ||
| const fallbackDirs = [ | ||
| path2.join(os.homedir(), ".cargo", "bin"), | ||
| ...IS_WINDOWS ? ["C:\\tools\\rtk", path2.join(os.homedir(), "scoop", "shims")] : ["/usr/local/bin", "/opt/homebrew/bin"] | ||
| path3.join(os2.homedir(), ".cargo", "bin"), | ||
| ...IS_WINDOWS ? ["C:\\tools\\rtk", path3.join(os2.homedir(), "scoop", "shims")] : ["/usr/local/bin", "/opt/homebrew/bin"] | ||
| ]; | ||
| for (const dir of fallbackDirs) { | ||
| const candidate = path2.join(dir, RTK_BIN); | ||
| if (fs3.existsSync(candidate)) { | ||
| const candidate = path3.join(dir, RTK_BIN); | ||
| if (fs4.existsSync(candidate)) { | ||
| cachedRtkPath = candidate; | ||
@@ -558,3 +672,3 @@ return candidate; | ||
| try { | ||
| return fs4.readFileSync(0, "utf8"); | ||
| return fs5.readFileSync(0, "utf8"); | ||
| } catch { | ||
@@ -649,3 +763,3 @@ return ""; | ||
| // src/hooks/sessionstart.ts | ||
| import fs5 from "fs"; | ||
| import fs6 from "fs"; | ||
@@ -772,3 +886,3 @@ // src/services/session-retriever.ts | ||
| try { | ||
| return fs5.readFileSync(0, "utf8"); | ||
| return fs6.readFileSync(0, "utf8"); | ||
| } catch { | ||
@@ -901,3 +1015,3 @@ return ""; | ||
| const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js"); | ||
| const { createServer } = await import("./server-QE46OPSQ.js"); | ||
| const { createServer } = await import("./server-CD2LT3JH.js"); | ||
| const server = createServer(); | ||
@@ -912,5 +1026,5 @@ const transport = new StdioServerTransport(); | ||
| } | ||
| var { dispatchCli } = await import("./dispatcher-C7JKRCLC.js"); | ||
| var { dispatchCli } = await import("./dispatcher-DSDEBDF6.js"); | ||
| var exitCode = await dispatchCli(args); | ||
| process.exit(exitCode); | ||
| //# sourceMappingURL=index.js.map |
+1
-1
| { | ||
| "name": "@cocaxcode/token-optimizer-mcp", | ||
| "version": "0.4.23", | ||
| "version": "0.5.0", | ||
| "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.", |
+37
-22
@@ -5,3 +5,3 @@ <p align="center"> | ||
| <strong>Know what your tokens cost. Control where they go.</strong><br/> | ||
| 13 tools · 9 CLI commands · 3 hooks · Coach with 18 tips · Measurement honesty · Budget enforcement | ||
| 14 tools · 9 CLI commands · 4 hooks · Coach with 20 tips · Measurement honesty · Budget enforcement | ||
| </p> | ||
@@ -15,4 +15,4 @@ </p> | ||
| <img src="https://img.shields.io/badge/node-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node" /> | ||
| <img src="https://img.shields.io/badge/tools-13-blueviolet?style=flat-square" alt="13 tools" /> | ||
| <img src="https://img.shields.io/badge/tests-253-brightgreen?style=flat-square" alt="238 tests" /> | ||
| <img src="https://img.shields.io/badge/tools-14-blueviolet?style=flat-square" alt="14 tools" /> | ||
| <img src="https://img.shields.io/badge/tests-322-brightgreen?style=flat-square" alt="322 tests" /> | ||
| </p> | ||
@@ -39,3 +39,3 @@ | ||
| Three hooks record everything silently in a per-project SQLite database. Thirteen MCP tools let the AI (and you) query stats, set budgets, search sessions, prune unused MCPs, and get proactive coaching tips. Nine CLI subcommands let you manage everything from the terminal. All data stays on your machine — nothing is synced, nothing is tracked, nothing leaves your disk. | ||
| Four hooks record everything silently in a per-project SQLite database. Fourteen MCP tools let the AI (and you) query stats, set budgets, search sessions, prune unused MCPs, and get proactive coaching tips. Nine CLI subcommands let you manage everything from the terminal. All data stays on your machine — nothing is synced, nothing is tracked, nothing leaves your disk. | ||
@@ -80,3 +80,3 @@ Works with **Claude Code**, **Claude Desktop**, **Cursor**, **Windsurf**, **VS Code**, **Codex CLI**, **Gemini CLI**, and any MCP-compatible client. | ||
| "Any tips for me?" | ||
| -> 18 tips: opusplan, /compact, plan mode, serena, RTK, skills migration... | ||
| -> 20 tips: opusplan, /compact, plan mode, serena, RTK, skills migration... | ||
| -> Active rules detect: too many searches, huge file reads, Opus on simple tasks | ||
@@ -111,3 +111,4 @@ | ||
| (Claude Code auto-compacts the context) | ||
| -> SessionStart:compact hook injects: recent files, recent commands, budget status | ||
| -> SessionStart:compact hook injects: recent files, recent commands, budget status, | ||
| recently touched Serena symbols | ||
| -> You pick up where you left off | ||
@@ -134,3 +135,3 @@ ``` | ||
| > **Why?** The 3 hooks (`PreToolUse`, `PostToolUse`, `SessionStart`) run via `npx @cocaxcode/token-optimizer-mcp --hook <name>`. Without a global install, `npx` can't find the binary and the hooks **fail silently** — no RTK bridge, no analytics, no compact recovery. The MCP server itself works fine with `npx -y`, but hooks need the package in PATH. | ||
| > **Why?** The 4 hooks (`PreToolUse`, `PostToolUse`, `SessionStart`) run via `npx @cocaxcode/token-optimizer-mcp --hook <name>`. Without a global install, `npx` can't find the binary and the hooks **fail silently** — no RTK bridge, no analytics, no compact recovery. The MCP server itself works fine with `npx -y`, but hooks need the package in PATH. | ||
@@ -143,2 +144,4 @@ **Step 3 — Set up hooks:** | ||
| This registers the 4 core hooks and, if Serena is detected, also auto-registers the Serena-specific hooks (see [Serena integration](#serena-mcp--symbolic-file-reads) below). | ||
| **Step 4 — Restart Claude Code** (hooks are loaded at session start). | ||
@@ -240,5 +243,5 @@ | ||
| The coach combines a **static knowledge base** of 18 tips with a **dynamic detector** of 11 rules that fire based on your recent activity. | ||
| The coach combines a **static knowledge base** of 20 tips with a **dynamic detector** of 12 rules that fire based on your recent activity. | ||
| ### 18 tips | ||
| ### 20 tips | ||
@@ -249,3 +252,3 @@ | Category | Tips | | ||
| | **Context** | `use-compact-long-session`, `use-clear-rename-resume`, `use-sessionstart-compact-hook`, `use-memory-save` | | ||
| | **Tools** | `use-agent-explore`, `use-todowrite-long-task`, `use-skill-trigger`, `install-serena`, `install-rtk` | | ||
| | **Tools** | `use-agent-explore`, `use-todowrite-long-task`, `use-skill-trigger`, `install-serena`, `install-rtk`, `prefer-serena-reads`, `use-serena-overview-first` | | ||
| | **Config** | `use-mcp-prune`, `migrate-claudemd-to-skills`, `use-settings-local`, `use-prompt-caching` | | ||
@@ -255,3 +258,3 @@ | ||
| ### 11 detection rules | ||
| ### 12 detection rules | ||
@@ -265,5 +268,9 @@ | Rule | Fires when | Suggests | | ||
| | `detect-many-bash-commands` | 11+ Bash in 100 events | `install RTK` | | ||
| | `detect-opus-for-simple-task` | Opus active, no edits | `switch to Sonnet/Haiku` | | ||
| | `detect-clear-opportunity` | Topic pivot detected | `/rename + /clear + /resume` | | ||
| | `detect-post-milestone-opportunity` | 5+ edits + tests + context > 40% | `/compact` at natural breakpoint | | ||
| | `detect-opus-for-simple-task` | Opus active, 6+ edits/Bash in 20 events | `switch to Sonnet/Haiku` | | ||
| | `detect-clear-opportunity` | Topic pivot detected (< 30% tool overlap) | `/rename + /clear + /resume` | | ||
| | `detect-post-milestone-opportunity` | 5+ edits + Bash + context > 40% | `/compact` at natural breakpoint | | ||
| | `detect-serena-read-cascade` | ≥5 `find_symbol` without `get_symbols_overview` | `use-serena-overview-first` | | ||
| | `detect-read-over-serena` | 3+ Read > 2k tokens in 30 events | `prefer-serena-reads` | | ||
| | `detect-claudemd-bloat` | *(stub — filesystem stat at runtime)* | `migrate-claudemd-to-skills` | | ||
| | `detect-skill-trigger-ignored` | *(stub — requires skill registry)* | `use-skill-trigger` | | ||
@@ -316,3 +323,3 @@ ### Context meter (3-source fallback) | ||
| Instead of reading entire files (500+ lines), serena uses **LSP** to read only the symbols (classes, functions, methods) you actually need. Saves **20-30%** on large file reads. | ||
| Instead of reading entire files (500+ lines), serena uses **LSP** to read only the symbols (classes, functions, methods) you actually need. Saves **20-30%** on large file reads — or **60-90%** when using `get_symbols_overview` + `find_symbol` instead of `Read`. | ||
@@ -370,4 +377,9 @@ **Step 1 — Install serena as MCP server:** | ||
| **How token-optimizer integrates**: the `optimization_status` tool and `doctor` CLI detect serena presence across 5 signals (global settings, ~/.claude.json, project settings, local settings, project registration). The coach `detect-huge-file-reads` rule fires when a Read exceeds 50k tokens and suggests using serena instead. | ||
| **Serena hook auto-registration**: when you run `token-optimizer-mcp install`, the installer probes for Serena automatically: | ||
| - If the **MCP server** is registered (detected via `~/.serena/`): installs the `serena-activate` hook — a lightweight SessionStart hook that emits the correct `ToolSearch` instruction so the agent can call Serena tools even when MCP tools are deferred at session start. | ||
| - If the **CLI** (`serena-hooks`) is also on PATH: additionally installs the 3 official Serena hooks (`remind`, `auto-approve`, `cleanup`). These are kept in sync — if the CLI disappears, the next `install` run removes the orphan entries. | ||
| **How token-optimizer integrates**: the `optimization_status` tool and `doctor` CLI detect serena presence across 5 signals (global settings, ~/.claude.json, project settings, local settings, project registration). The coach `detect-huge-file-reads` and `detect-read-over-serena` rules fire when Read usage is heavy and suggest using serena instead. | ||
| **Security note**: serena includes `execute_shell_command` among its tools. Review the configuration before enabling. | ||
@@ -484,3 +496,3 @@ | ||
| |---|---| | ||
| | `install` | Register MCP server + 3 hooks in `~/.claude/settings.json` | | ||
| | `install` | Register MCP server + 4 hooks in `~/.claude/settings.json` (auto-registers Serena hooks if detected) | | ||
| | `uninstall` | Remove entries; `--purge --confirm` deletes stored data | | ||
@@ -503,4 +515,7 @@ | `doctor` | Run all probes + schema measurer + advisor (always exits 0) | | ||
| | `PostToolUse` | `*` | Async analytics to SQLite. Fire-and-forget to xray. Target p95: 10ms. | | ||
| | `SessionStart` | `compact` | Injects markdown: recent files, commands, budget. Token-capped at 2000. | | ||
| | `SessionStart` | `compact` | Injects markdown: recent files, commands, budget, recently touched Serena symbols. Token-capped at 2000. | | ||
| | `SessionStart` | *(any)* | **serena-activate** — when Serena is detected, emits the `ToolSearch` activation sequence so the agent can call Serena tools even when MCP tools are deferred. Noop when Serena is not installed. | | ||
| Additionally, if the `serena-hooks` CLI binary is on PATH, `install` registers the 3 official Serena hooks: `PreToolUse → remind`, `PreToolUse(mcp__serena__.*) → auto-approve`, and `Stop → cleanup`. | ||
| --- | ||
@@ -559,5 +574,5 @@ | ||
| ├── index.ts # Entry — --mcp | --hook X | subcommand | ||
| ├── server.ts # createServer() — registers 13 tools | ||
| ├── server.ts # createServer() — registers 14 tools + 1 resource | ||
| ├── cli/ # 9 subcommands + dispatcher | ||
| ├── hooks/ # pretooluse, posttooluse, sessionstart | ||
| ├── hooks/ # pretooluse, posttooluse, sessionstart, serena-activate | ||
| ├── tools/ # budget, session, orchestration, coach, toon | ||
@@ -567,3 +582,3 @@ ├── services/ # analytics-logger, budget-manager, session-retriever, | ||
| ├── orchestration/ # detector (probes), schema-measurer, advisor | ||
| ├── coach/ # knowledge-base (18), rules (11), detector, | ||
| ├── coach/ # knowledge-base (20 tips), rules (12), detector, | ||
| │ # context-meter, reference-data, surface (dedupe) | ||
@@ -576,3 +591,3 @@ ├── lib/ # types, paths, storage, token-estimator, response | ||
| **253 tests** across 29 suites. All tools tested via `InMemoryTransport`. | ||
| **322 tests** across 35 suites. All tools tested via `InMemoryTransport`. | ||
@@ -579,0 +594,0 @@ --- |
| #!/usr/bin/env node | ||
| import { | ||
| resolveXrayUrl | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.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.23" : "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-FVA4JKVS.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 { | ||
| KNOWLEDGE_BASE, | ||
| clearSurfaceLog, | ||
| measureContextSize, | ||
| runRules | ||
| } from "./chunk-FVA4JKVS.js"; | ||
| import "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.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-N3USX2E3.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-PGH4NXTW.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-BC6HFBNA.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-N3USX2E3.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-C7JKRCLC.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 { | ||
| probeSerenaPresence | ||
| } from "./chunk-2NKYFIPW.js"; | ||
| import { | ||
| ensureStorageDir | ||
| } from "./chunk-VHP52B3J.js"; | ||
| import { | ||
| runDoctor | ||
| } from "./chunk-QWZDLYG4.js"; | ||
| import "./chunk-DSMHUGNS.js"; | ||
| import "./chunk-L5Z32XXL.js"; | ||
| import "./chunk-V4PINTCV.js"; | ||
| // src/cli/install.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| import { spawnSync } from "child_process"; | ||
| var SERVER_NAME = "token-optimizer"; | ||
| function resolveHookCommandBase() { | ||
| try { | ||
| const globalRoot = path.join(os.homedir(), "AppData", "Roaming", "npm", "node_modules"); | ||
| const indexPath = path.join(globalRoot, "@cocaxcode", "token-optimizer-mcp", "dist", "index.js"); | ||
| if (fs.existsSync(indexPath)) { | ||
| return `node "${indexPath.replace(/\\/g, "/")}"`; | ||
| } | ||
| } catch { | ||
| } | ||
| const unixPaths = [ | ||
| "/usr/local/lib/node_modules", | ||
| "/usr/lib/node_modules", | ||
| path.join(os.homedir(), ".npm-global", "lib", "node_modules") | ||
| ]; | ||
| for (const root of unixPaths) { | ||
| try { | ||
| const indexPath = path.join(root, "@cocaxcode", "token-optimizer-mcp", "dist", "index.js"); | ||
| if (fs.existsSync(indexPath)) { | ||
| return `node "${indexPath}"`; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| try { | ||
| const result = spawnSync("npm", ["root", "-g"], { encoding: "utf8", timeout: 3e3, shell: true }); | ||
| const npmRoot = (result.stdout ?? "").trim(); | ||
| const indexPath = path.join(npmRoot, "@cocaxcode", "token-optimizer-mcp", "dist", "index.js"); | ||
| if (fs.existsSync(indexPath)) { | ||
| return `node "${indexPath.replace(/\\/g, "/")}"`; | ||
| } | ||
| } catch { | ||
| } | ||
| return "npx @cocaxcode/token-optimizer-mcp"; | ||
| } | ||
| function settingsPath(home) { | ||
| return path.join(home, ".claude", "settings.json"); | ||
| } | ||
| function readSettings(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return {}; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| function writeSettings(p, data) { | ||
| const dir = path.dirname(p); | ||
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(data, null, 2)); | ||
| } | ||
| function extractHookFlag(command) { | ||
| const match = command.match(/--hook\s+(\S+)/); | ||
| return match ? `--hook ${match[1]}` : null; | ||
| } | ||
| function removeHook(allHooks, eventName, matcher, identifier) { | ||
| const existing = allHooks[eventName]; | ||
| if (!Array.isArray(existing)) return 0; | ||
| const list = existing; | ||
| const matchEntry = list.find((e) => e.matcher === matcher); | ||
| if (!matchEntry || !Array.isArray(matchEntry.hooks)) return 0; | ||
| const before = matchEntry.hooks.length; | ||
| matchEntry.hooks = matchEntry.hooks.filter( | ||
| (h) => !(typeof h.command === "string" && h.command.includes(identifier)) | ||
| ); | ||
| const removed = before - matchEntry.hooks.length; | ||
| if (removed === 0) return 0; | ||
| if (matchEntry.hooks.length === 0) { | ||
| const idx = list.indexOf(matchEntry); | ||
| if (idx >= 0) list.splice(idx, 1); | ||
| } | ||
| if (list.length === 0) { | ||
| delete allHooks[eventName]; | ||
| } else { | ||
| allHooks[eventName] = list; | ||
| } | ||
| return removed; | ||
| } | ||
| function upsertHook(allHooks, eventName, matcher, command, upsertOpts = {}) { | ||
| const identifier = upsertOpts.identifier ?? "token-optimizer"; | ||
| const useFlagDisambiguation = upsertOpts.useFlagDisambiguation ?? true; | ||
| const existing = allHooks[eventName] ?? []; | ||
| const list = Array.isArray(existing) ? [...existing] : []; | ||
| const matchEntry = list.find((e) => e.matcher === matcher); | ||
| const ourHandler = { type: "command", command }; | ||
| const ourFlag = useFlagDisambiguation ? extractHookFlag(command) : null; | ||
| if (matchEntry) { | ||
| const handlers = Array.isArray(matchEntry.hooks) ? [...matchEntry.hooks] : []; | ||
| let idx = -1; | ||
| if (ourFlag) { | ||
| idx = handlers.findIndex( | ||
| (h) => typeof h.command === "string" && h.command.includes(identifier) && extractHookFlag(h.command) === ourFlag | ||
| ); | ||
| } | ||
| if (idx < 0) { | ||
| idx = handlers.findIndex( | ||
| (h) => typeof h.command === "string" && h.command.includes(identifier) && extractHookFlag(h.command) === null | ||
| ); | ||
| } | ||
| if (idx >= 0) { | ||
| handlers[idx] = ourHandler; | ||
| } else { | ||
| handlers.push(ourHandler); | ||
| } | ||
| matchEntry.hooks = handlers; | ||
| } else { | ||
| list.push({ matcher, hooks: [ourHandler] }); | ||
| } | ||
| allHooks[eventName] = list; | ||
| } | ||
| function runInstall(_args = [], opts = {}) { | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const p = settingsPath(home); | ||
| const settings = readSettings(p); | ||
| const mcpServers = settings.mcpServers ?? {}; | ||
| mcpServers[SERVER_NAME] = { | ||
| command: "npx", | ||
| args: ["-y", "@cocaxcode/token-optimizer-mcp", "--mcp"] | ||
| }; | ||
| settings.mcpServers = mcpServers; | ||
| const hookBase = resolveHookCommandBase(); | ||
| const hooks = settings.hooks ?? {}; | ||
| upsertHook(hooks, "PreToolUse", "Bash", `${hookBase} --hook pretooluse`); | ||
| upsertHook(hooks, "PostToolUse", "*", `${hookBase} --hook posttooluse`); | ||
| upsertHook(hooks, "SessionStart", "compact", `${hookBase} --hook sessionstart`); | ||
| const serenaProbe = opts.serenaProbe ?? probeSerenaPresence(); | ||
| const wantOfficialHooks = serenaProbe.serena_cli_installed && opts.skipSerenaHooks !== true; | ||
| if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) { | ||
| upsertHook(hooks, "SessionStart", "", `${hookBase} --hook serena-activate`); | ||
| } | ||
| if (wantOfficialHooks) { | ||
| upsertHook(hooks, "PreToolUse", "", "serena-hooks remind --client=claude-code", { | ||
| identifier: "serena-hooks remind", | ||
| useFlagDisambiguation: false | ||
| }); | ||
| upsertHook( | ||
| hooks, | ||
| "PreToolUse", | ||
| "mcp__serena__.*", | ||
| "serena-hooks auto-approve --client=claude-code", | ||
| { | ||
| identifier: "serena-hooks auto-approve", | ||
| useFlagDisambiguation: false | ||
| } | ||
| ); | ||
| upsertHook(hooks, "Stop", "", "serena-hooks cleanup --client=claude-code", { | ||
| identifier: "serena-hooks cleanup", | ||
| useFlagDisambiguation: false | ||
| }); | ||
| } else { | ||
| removeHook(hooks, "PreToolUse", "", "serena-hooks remind"); | ||
| removeHook(hooks, "PreToolUse", "mcp__serena__.*", "serena-hooks auto-approve"); | ||
| removeHook(hooks, "Stop", "", "serena-hooks cleanup"); | ||
| } | ||
| settings.hooks = hooks; | ||
| writeSettings(p, settings); | ||
| const globalDir = path.join(home, ".token-optimizer"); | ||
| if (!fs.existsSync(globalDir)) fs.mkdirSync(globalDir, { recursive: true }); | ||
| if (fs.existsSync(path.join(cwd, ".git"))) { | ||
| ensureStorageDir(cwd); | ||
| } | ||
| print("token-optimizer-mcp instalado correctamente."); | ||
| print(` settings: ${p}`); | ||
| print(` global: ${globalDir}`); | ||
| if (serenaProbe.serena_cli_installed) { | ||
| print(` serena: CLI detectado \u2014 4 hooks registrados`); | ||
| print(` (activate + remind + auto-approve + cleanup)`); | ||
| if (opts.skipSerenaHooks) { | ||
| print(` note: --skipSerenaHooks activo \u2192 solo se registr\xF3 serena-activate`); | ||
| } | ||
| } else if (serenaProbe.serena_mcp_registered) { | ||
| print(` serena: MCP detectado pero CLI no instalado`); | ||
| print(` \u2192 solo se registr\xF3 serena-activate`); | ||
| print(` \u2192 para los otros 3: uv tool install git+https://github.com/oraios/serena`); | ||
| } else { | ||
| print(` serena: no detectado \u2014 hooks de serena omitidos`); | ||
| } | ||
| if (opts.runDoctorAtEnd !== false) { | ||
| print(""); | ||
| runDoctor([], { cwd, home, print }); | ||
| } | ||
| return 0; | ||
| } | ||
| export { | ||
| runInstall | ||
| }; | ||
| //# sourceMappingURL=install-TNLZRW5R.js.map |
| {"version":3,"sources":["../src/cli/install.ts"],"sourcesContent":["// Install CLI — Phase 4.10\n// Writes token-optimizer mcpServers entry + 3 hooks into ~/.claude/settings.json\n// Also creates per-project storage dir and appends .gitignore in git repos.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport os from 'node:os'\nimport { spawnSync } from 'node:child_process'\nimport { ensureStorageDir } from '../lib/storage.js'\nimport { probeSerenaPresence, type SerenaProbe } from '../hooks/serena-activate.js'\nimport { runDoctor } from './doctor.js'\n\nconst SERVER_NAME = 'token-optimizer'\n\n/**\n * Resolve the hook command base.\n * Prefer `node <global-path>/dist/index.js` for speed (~0.2s vs ~1.5s with npx).\n * Falls back to `npx @cocaxcode/token-optimizer-mcp` if global path not found.\n */\nfunction resolveHookCommandBase(): string {\n try {\n const globalRoot = path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules')\n const indexPath = path.join(globalRoot, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\n if (fs.existsSync(indexPath)) {\n return `node \"${indexPath.replace(/\\\\/g, '/')}\"`\n }\n } catch { /* fallback */ }\n\n // Unix global paths\n const unixPaths = [\n '/usr/local/lib/node_modules',\n '/usr/lib/node_modules',\n path.join(os.homedir(), '.npm-global', 'lib', 'node_modules'),\n ]\n for (const root of unixPaths) {\n try {\n const indexPath = path.join(root, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\n if (fs.existsSync(indexPath)) {\n return `node \"${indexPath}\"`\n }\n } catch { /* fallback */ }\n }\n\n // npm root -g fallback\n try {\n const result = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 3000, shell: true })\n const npmRoot = (result.stdout ?? '').trim()\n const indexPath = path.join(npmRoot, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\n if (fs.existsSync(indexPath)) {\n return `node \"${indexPath.replace(/\\\\/g, '/')}\"`\n }\n } catch { /* fallback */ }\n\n return 'npx @cocaxcode/token-optimizer-mcp'\n}\n\nexport interface InstallOptions {\n home?: string\n cwd?: string\n print?: (msg: string) => void\n runDoctorAtEnd?: boolean\n /**\n * Override the Serena presence probe. Used by tests to get deterministic\n * behaviour independent of whether the test host actually has Serena.\n * Undefined = probe the real filesystem/PATH.\n */\n serenaProbe?: SerenaProbe\n /**\n * Skip installing the 3 official Serena reminder hooks (remind, auto-approve,\n * cleanup) even when Serena is detected. The Serena-activate hook we own is\n * still installed. Use this if you prefer managing the official hooks yourself\n * or you don't want to depend on Serena's alpha feature.\n */\n skipSerenaHooks?: boolean\n}\n\nfunction settingsPath(home: string): string {\n return path.join(home, '.claude', 'settings.json')\n}\n\nfunction readSettings(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 writeSettings(p: string, data: Record<string, unknown>): void {\n const dir = path.dirname(p)\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(p, JSON.stringify(data, null, 2))\n}\n\ninterface HookEntry {\n matcher?: string\n hooks?: Array<{ type?: string; command?: string }>\n [key: string]: unknown\n}\n\n/**\n * Extract the `--hook <kind>` flag from a command line so we can use it as\n * a unique identity for upsert. `node .../index.js --hook serena-activate`\n * becomes `--hook serena-activate`. Anything without a `--hook X` returns null.\n */\nfunction extractHookFlag(command: string): string | null {\n const match = command.match(/--hook\\s+(\\S+)/)\n return match ? `--hook ${match[1]}` : null\n}\n\n/**\n * Remove any handlers whose command contains `identifier` from the given\n * (eventName, matcher) group. Used to un-register hooks that were installed\n * in a previous run but no longer apply (e.g. the 3 official Serena hooks\n * when the `serena-hooks` CLI is no longer in PATH).\n *\n * Returns the number of handlers removed. If the matcher group becomes empty,\n * the whole group is dropped from the event list. If the event itself becomes\n * empty, the event key is deleted from the hooks map.\n */\nfunction removeHook(\n allHooks: Record<string, unknown>,\n eventName: string,\n matcher: string,\n identifier: string,\n): number {\n const existing = allHooks[eventName]\n if (!Array.isArray(existing)) return 0\n const list = existing as HookEntry[]\n const matchEntry = list.find((e) => e.matcher === matcher)\n if (!matchEntry || !Array.isArray(matchEntry.hooks)) return 0\n\n const before = matchEntry.hooks.length\n matchEntry.hooks = matchEntry.hooks.filter(\n (h) => !(typeof h.command === 'string' && h.command.includes(identifier)),\n )\n const removed = before - matchEntry.hooks.length\n if (removed === 0) return 0\n\n // Clean empty matcher groups\n if (matchEntry.hooks.length === 0) {\n const idx = list.indexOf(matchEntry)\n if (idx >= 0) list.splice(idx, 1)\n }\n // Clean empty event\n if (list.length === 0) {\n delete allHooks[eventName]\n } else {\n allHooks[eventName] = list\n }\n return removed\n}\n\n/**\n * Options for upsertHook.\n * - `identifier`: a substring that uniquely identifies an existing handler of\n * the same kind so we can replace it in place. Defaults to \"token-optimizer\"\n * for our own hooks. For external hooks (e.g. serena-hooks), callers should\n * pass something like \"serena-hooks remind\".\n * - `useFlagDisambiguation`: if true, use the `--hook X` flag as extra\n * disambiguation so multiple token-optimizer hooks can coexist in the same\n * matcher group without trampling each other. Default true.\n */\ninterface UpsertHookOptions {\n identifier?: string\n useFlagDisambiguation?: boolean\n}\n\nfunction upsertHook(\n allHooks: Record<string, unknown>,\n eventName: string,\n matcher: string,\n command: string,\n upsertOpts: UpsertHookOptions = {},\n): void {\n const identifier = upsertOpts.identifier ?? 'token-optimizer'\n const useFlagDisambiguation = upsertOpts.useFlagDisambiguation ?? true\n\n const existing = (allHooks[eventName] ?? []) as HookEntry[]\n const list: HookEntry[] = Array.isArray(existing) ? [...existing] : []\n const matchEntry = list.find((e) => e.matcher === matcher)\n const ourHandler = { type: 'command', command }\n const ourFlag = useFlagDisambiguation ? extractHookFlag(command) : null\n\n if (matchEntry) {\n const handlers = Array.isArray(matchEntry.hooks) ? [...matchEntry.hooks] : []\n // 1) Preferred: find the exact handler we own by identifier + flag.\n let idx = -1\n if (ourFlag) {\n idx = handlers.findIndex(\n (h) =>\n typeof h.command === 'string' &&\n h.command.includes(identifier) &&\n extractHookFlag(h.command) === ourFlag,\n )\n }\n // 2) Fallback: any handler that includes the identifier (and doesn't\n // have a --hook flag of its own so we don't steal a sibling's slot).\n if (idx < 0) {\n idx = handlers.findIndex(\n (h) =>\n typeof h.command === 'string' &&\n h.command.includes(identifier) &&\n extractHookFlag(h.command) === null,\n )\n }\n\n if (idx >= 0) {\n handlers[idx] = ourHandler\n } else {\n handlers.push(ourHandler)\n }\n matchEntry.hooks = handlers\n } else {\n list.push({ matcher, hooks: [ourHandler] })\n }\n allHooks[eventName] = list\n}\n\nexport function runInstall(_args: string[] = [], opts: InstallOptions = {}): number {\n const home = opts.home ?? os.homedir()\n const cwd = opts.cwd ?? process.cwd()\n const print = opts.print ?? ((m: string) => console.error(m))\n\n const p = settingsPath(home)\n const settings = readSettings(p)\n\n // mcpServers upsert\n const mcpServers = (settings.mcpServers ?? {}) as Record<string, unknown>\n mcpServers[SERVER_NAME] = {\n command: 'npx',\n args: ['-y', '@cocaxcode/token-optimizer-mcp', '--mcp'],\n }\n settings.mcpServers = mcpServers\n\n // hooks upsert — prefer node direct for speed (~0.2s vs ~1.5s with npx)\n const hookBase = resolveHookCommandBase()\n const hooks = (settings.hooks ?? {}) as Record<string, unknown>\n upsertHook(hooks, 'PreToolUse', 'Bash', `${hookBase} --hook pretooluse`)\n upsertHook(hooks, 'PostToolUse', '*', `${hookBase} --hook posttooluse`)\n upsertHook(hooks, 'SessionStart', 'compact', `${hookBase} --hook sessionstart`)\n\n // Serena integration — two independent decisions based on two probe signals:\n //\n // (a) `--hook serena-activate` (our own SessionStart hook). Fixes the\n // ToolSearch gap in the official `serena-hooks activate` output. Does\n // NOT shell out to any binary — it's a node entry point that emits\n // JSON. Gated by `serena_mcp_registered` (i.e. the user uses Serena\n // as an MCP server at all).\n //\n // (b) The 3 OFFICIAL Serena reminder hooks (remind, auto-approve, cleanup).\n // These ARE invoked as `serena-hooks <cmd> ...` at runtime by Claude\n // Code, so they require the actual CLI binary to be on PATH. Gated by\n // `serena_cli_installed`. If the CLI disappears (user uninstalled\n // Serena, or the probe was wrong in a previous release), we actively\n // REMOVE the orphan entries so settings.json stops pointing at a\n // missing binary.\n //\n // Both blocks can be individually skipped via `skipSerenaHooks: true`.\n const serenaProbe = opts.serenaProbe ?? probeSerenaPresence()\n const wantOfficialHooks =\n serenaProbe.serena_cli_installed && opts.skipSerenaHooks !== true\n\n if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) {\n upsertHook(hooks, 'SessionStart', '', `${hookBase} --hook serena-activate`)\n }\n\n if (wantOfficialHooks) {\n upsertHook(hooks, 'PreToolUse', '', 'serena-hooks remind --client=claude-code', {\n identifier: 'serena-hooks remind',\n useFlagDisambiguation: false,\n })\n upsertHook(\n hooks,\n 'PreToolUse',\n 'mcp__serena__.*',\n 'serena-hooks auto-approve --client=claude-code',\n {\n identifier: 'serena-hooks auto-approve',\n useFlagDisambiguation: false,\n },\n )\n upsertHook(hooks, 'Stop', '', 'serena-hooks cleanup --client=claude-code', {\n identifier: 'serena-hooks cleanup',\n useFlagDisambiguation: false,\n })\n } else {\n // Reconcile: if the 3 official hooks were added by a previous install\n // (maybe from a buggier probe that accepted ~/.serena/ as sufficient),\n // but the CLI isn't actually available now, take them OUT so Claude\n // Code stops logging \"command not found\" on every hook dispatch.\n removeHook(hooks, 'PreToolUse', '', 'serena-hooks remind')\n removeHook(hooks, 'PreToolUse', 'mcp__serena__.*', 'serena-hooks auto-approve')\n removeHook(hooks, 'Stop', '', 'serena-hooks cleanup')\n }\n settings.hooks = hooks\n\n writeSettings(p, settings)\n\n // Global storage dir\n const globalDir = path.join(home, '.token-optimizer')\n if (!fs.existsSync(globalDir)) fs.mkdirSync(globalDir, { recursive: true })\n\n // Per-project storage dir (only in git repos)\n if (fs.existsSync(path.join(cwd, '.git'))) {\n ensureStorageDir(cwd)\n }\n\n print('token-optimizer-mcp instalado correctamente.')\n print(` settings: ${p}`)\n print(` global: ${globalDir}`)\n\n // Serena status — 3 states\n if (serenaProbe.serena_cli_installed) {\n print(` serena: CLI detectado — 4 hooks registrados`)\n print(` (activate + remind + auto-approve + cleanup)`)\n if (opts.skipSerenaHooks) {\n print(` note: --skipSerenaHooks activo → solo se registró serena-activate`)\n }\n } else if (serenaProbe.serena_mcp_registered) {\n print(` serena: MCP detectado pero CLI no instalado`)\n print(` → solo se registró serena-activate`)\n print(` → para los otros 3: uv tool install git+https://github.com/oraios/serena`)\n } else {\n print(` serena: no detectado — hooks de serena omitidos`)\n }\n\n if (opts.runDoctorAtEnd !== false) {\n print('')\n runDoctor([], { cwd, home, print })\n }\n\n return 0\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| getCoachSurfaceLog, | ||
| measureContextSize, | ||
| postSummaryToXray, | ||
| runRules | ||
| } from "./chunk-FVA4JKVS.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-633PY32C.js"; | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-VV5KKIQ4.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.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.23" : "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-QE46OPSQ.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"]} |
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.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
780107
2.71%7176
2.05%587
2.62%