@cocaxcode/token-optimizer-mcp
Advanced tools
| #!/usr/bin/env node | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-AF6RQ5F5.js"; | ||
| import "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| projectHash, | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-U3OXZD52.js"; | ||
| // src/cli/budget.ts | ||
| function runBudgetCli(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const db = getDb(dbPath); | ||
| const mgr = new BudgetManager(db); | ||
| const hash = projectHash(projectDir); | ||
| const sub = args[0]; | ||
| if (sub === "set") { | ||
| const scope = args[1]; | ||
| const limitRaw = args[2]; | ||
| const limit = limitRaw ? parseInt(limitRaw, 10) : NaN; | ||
| if (scope !== "session" && scope !== "project" || !Number.isFinite(limit)) { | ||
| print("Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| try { | ||
| const budget = mgr.setBudget({ | ||
| scope, | ||
| scope_key: scopeKey, | ||
| limit_tokens: limit | ||
| }); | ||
| print( | ||
| `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}` | ||
| ); | ||
| return 0; | ||
| } catch (e) { | ||
| print(`Error: ${e instanceof Error ? e.message : String(e)}`); | ||
| return 1; | ||
| } | ||
| } | ||
| if (sub === "get") { | ||
| const status = mgr.checkBudget("default", hash); | ||
| if (!status.active) { | ||
| print("Sin presupuesto activo"); | ||
| return 0; | ||
| } | ||
| const pct = (status.percent_used * 100).toFixed(1); | ||
| print( | ||
| `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? "n/a"}` | ||
| ); | ||
| return 0; | ||
| } | ||
| if (sub === "clear") { | ||
| const scope = args[1]; | ||
| if (scope !== "session" && scope !== "project") { | ||
| print("Uso: token-optimizer-mcp budget clear <session|project>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| const removed = mgr.clearBudget(scope, scopeKey); | ||
| print(removed ? `Eliminado (${scope})` : "No habia presupuesto para este scope"); | ||
| return 0; | ||
| } | ||
| print("Uso: token-optimizer-mcp budget <set|get|clear> [args]"); | ||
| return 1; | ||
| } | ||
| export { | ||
| runBudgetCli | ||
| }; | ||
| //# sourceMappingURL=budget-F22UXU7N.js.map |
| {"version":3,"sources":["../src/cli/budget.ts"],"sourcesContent":["// Budget CLI — Phase 4.15\r\n// Thin wrapper delegating to BudgetManager. Subcommands: set / get / clear.\r\n\r\nimport { getDb } from '../db/connection.js'\r\nimport {\r\n resolveProjectDir,\r\n resolveAnalyticsDbPath,\r\n projectHash,\r\n} from '../lib/paths.js'\r\nimport { BudgetManager } from '../services/budget-manager.js'\r\nimport type { BudgetScope } from '../lib/types.js'\r\n\r\nexport interface BudgetCliOptions {\r\n cwd?: string\r\n print?: (msg: string) => void\r\n}\r\n\r\nexport function runBudgetCli(args: string[] = [], opts: BudgetCliOptions = {}): number {\r\n const print = opts.print ?? ((m: string) => console.error(m))\r\n const cwd = opts.cwd ?? process.cwd()\r\n const projectDir = resolveProjectDir(cwd)\r\n const dbPath = resolveAnalyticsDbPath(projectDir)\r\n\r\n const db = getDb(dbPath)\r\n const mgr = new BudgetManager(db)\r\n const hash = projectHash(projectDir)\r\n\r\n const sub = args[0]\r\n\r\n if (sub === 'set') {\r\n const scope = args[1] as BudgetScope | undefined\r\n const limitRaw = args[2]\r\n const limit = limitRaw ? parseInt(limitRaw, 10) : NaN\r\n if ((scope !== 'session' && scope !== 'project') || !Number.isFinite(limit)) {\r\n print('Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>')\r\n return 1\r\n }\r\n const scopeKey = scope === 'session' ? 'default' : hash\r\n try {\r\n const budget = mgr.setBudget({\r\n scope,\r\n scope_key: scopeKey,\r\n limit_tokens: limit,\r\n })\r\n print(\r\n `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}`,\r\n )\r\n return 0\r\n } catch (e) {\r\n print(`Error: ${e instanceof Error ? e.message : String(e)}`)\r\n return 1\r\n }\r\n }\r\n\r\n if (sub === 'get') {\r\n const status = mgr.checkBudget('default', hash)\r\n if (!status.active) {\r\n print('Sin presupuesto activo')\r\n return 0\r\n }\r\n const pct = (status.percent_used * 100).toFixed(1)\r\n print(\r\n `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? 'n/a'}`,\r\n )\r\n return 0\r\n }\r\n\r\n if (sub === 'clear') {\r\n const scope = args[1] as BudgetScope | undefined\r\n if (scope !== 'session' && scope !== 'project') {\r\n print('Uso: token-optimizer-mcp budget clear <session|project>')\r\n return 1\r\n }\r\n const scopeKey = scope === 'session' ? 'default' : hash\r\n const removed = mgr.clearBudget(scope, scopeKey)\r\n print(removed ? `Eliminado (${scope})` : 'No habia presupuesto para este scope')\r\n return 0\r\n }\r\n\r\n print('Uso: token-optimizer-mcp budget <set|get|clear> [args]')\r\n return 1\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;AAiBO,SAAS,aAAa,OAAiB,CAAC,GAAG,OAAyB,CAAC,GAAW;AACrF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,QAAM,KAAK,MAAM,MAAM;AACvB,QAAM,MAAM,IAAI,cAAc,EAAE;AAChC,QAAM,OAAO,YAAY,UAAU;AAEnC,QAAM,MAAM,KAAK,CAAC;AAElB,MAAI,QAAQ,OAAO;AACjB,UAAM,QAAQ,KAAK,CAAC;AACpB,UAAM,WAAW,KAAK,CAAC;AACvB,UAAM,QAAQ,WAAW,SAAS,UAAU,EAAE,IAAI;AAClD,QAAK,UAAU,aAAa,UAAU,aAAc,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3E,YAAM,sEAAsE;AAC5E,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,QAAI;AACF,YAAM,SAAS,IAAI,UAAU;AAAA,QAC3B;AAAA,QACA,WAAW;AAAA,QACX,cAAc;AAAA,MAChB,CAAC;AACD;AAAA,QACE,yBAAyB,OAAO,KAAK,IAAI,OAAO,SAAS,UAAU,OAAO,YAAY,SAAS,OAAO,IAAI;AAAA,MAC5G;AACA,aAAO;AAAA,IACT,SAAS,GAAG;AACV,YAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AAC5D,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,QAAQ,OAAO;AACjB,UAAM,SAAS,IAAI,YAAY,WAAW,IAAI;AAC9C,QAAI,CAAC,OAAO,QAAQ;AAClB,YAAM,wBAAwB;AAC9B,aAAO;AAAA,IACT;AACA,UAAM,OAAO,OAAO,eAAe,KAAK,QAAQ,CAAC;AACjD;AAAA,MACE,WAAW,OAAO,KAAK,aAAa,OAAO,SAAS,QAAQ,GAAG,UAAU,OAAO,QAAQ,KAAK;AAAA,IAC/F;AACA,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,SAAS;AACnB,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,UAAU,aAAa,UAAU,WAAW;AAC9C,YAAM,yDAAyD;AAC/D,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,UAAM,UAAU,IAAI,YAAY,OAAO,QAAQ;AAC/C,UAAM,UAAU,cAAc,KAAK,MAAM,sCAAsC;AAC/E,WAAO;AAAA,EACT;AAEA,QAAM,wDAAwD;AAC9D,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| // src/orchestration/detector.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| function readSettings(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return null; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function globalSettings(home) { | ||
| return path.join(home, ".claude", "settings.json"); | ||
| } | ||
| function globalClaudeJson(home) { | ||
| return path.join(home, ".claude.json"); | ||
| } | ||
| function localSettings(cwd) { | ||
| return path.join(cwd, ".claude", "settings.local.json"); | ||
| } | ||
| function teamSettings(cwd) { | ||
| return path.join(cwd, ".claude", "settings.json"); | ||
| } | ||
| function mcpServerKeys(json) { | ||
| if (!json) return []; | ||
| const mcp = json.mcpServers; | ||
| if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) { | ||
| return Object.keys(mcp); | ||
| } | ||
| return []; | ||
| } | ||
| function runProbe(name, checks) { | ||
| const signals = []; | ||
| let hits = 0; | ||
| for (const check of checks) { | ||
| try { | ||
| const [hit, label] = check(); | ||
| if (hit) { | ||
| hits++; | ||
| signals.push(label); | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| const confidence = checks.length > 0 ? hits / checks.length : 0; | ||
| return { | ||
| present: hits > 0, | ||
| confidence, | ||
| signals, | ||
| details: { probe: name, signal_count: hits, total_checks: checks.length } | ||
| }; | ||
| } | ||
| function probeSerena(paths = {}) { | ||
| const home = paths.home ?? os.homedir(); | ||
| const cwd = paths.cwd ?? process.cwd(); | ||
| return runProbe("serena", [ | ||
| () => { | ||
| const keys = mcpServerKeys(readSettings(globalSettings(home))); | ||
| return [keys.some((k) => k.toLowerCase().includes("serena")), "global-settings-registered"]; | ||
| }, | ||
| () => { | ||
| const keys = mcpServerKeys(readSettings(globalClaudeJson(home))); | ||
| return [keys.some((k) => k.toLowerCase().includes("serena")), "claude-json-registered"]; | ||
| }, | ||
| () => { | ||
| const keys = mcpServerKeys(readSettings(teamSettings(cwd))); | ||
| return [keys.some((k) => k.toLowerCase().includes("serena")), "project-mcp-registered"]; | ||
| }, | ||
| () => { | ||
| const keys = mcpServerKeys(readSettings(localSettings(cwd))); | ||
| return [keys.some((k) => k.toLowerCase().includes("serena")), "local-mcp-registered"]; | ||
| }, | ||
| () => { | ||
| const configPath = path.join(home, ".serena", "serena_config.yml"); | ||
| if (!fs.existsSync(configPath)) return [false, "project-registered-for-cwd"]; | ||
| try { | ||
| const content = fs.readFileSync(configPath, "utf8"); | ||
| const normalizedCwd = cwd.replace(/\\/g, "/").toLowerCase(); | ||
| const normalizedContent = content.replace(/\\/g, "/").toLowerCase(); | ||
| return [normalizedContent.includes(normalizedCwd), "project-registered-for-cwd"]; | ||
| } catch { | ||
| return [false, "project-registered-for-cwd"]; | ||
| } | ||
| } | ||
| ]); | ||
| } | ||
| function checkSerenaHealth(paths = {}) { | ||
| const home = paths.home ?? os.homedir(); | ||
| const cwd = paths.cwd ?? process.cwd(); | ||
| const warnings = []; | ||
| try { | ||
| const configPath = path.join(home, ".serena", "serena_config.yml"); | ||
| if (fs.existsSync(configPath)) { | ||
| const content = fs.readFileSync(configPath, "utf8"); | ||
| const match = content.match(/web_dashboard_open_on_launch\s*:\s*(\w+)/); | ||
| const value = match ? match[1].toLowerCase() : "true"; | ||
| if (value === "true") { | ||
| warnings.push({ | ||
| id: "dashboard-auto-open", | ||
| message: "El dashboard de Serena se abre automaticamente al iniciar cada terminal", | ||
| fix: "Pon web_dashboard_open_on_launch: false en ~/.serena/serena_config.yml" | ||
| }); | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| try { | ||
| const hasContextFlag = checkSerenaContextFlag(home, cwd); | ||
| if (!hasContextFlag) { | ||
| warnings.push({ | ||
| id: "missing-context-claude-code", | ||
| message: "Serena no usa el contexto claude-code (modo headless optimizado para CLI)", | ||
| fix: "A\xF1ade --context claude-code a los args del MCP server de serena" | ||
| }); | ||
| } | ||
| } catch { | ||
| } | ||
| return warnings; | ||
| } | ||
| function checkSerenaContextFlag(home, cwd) { | ||
| const settingsFiles = [ | ||
| path.join(home, ".claude", "settings.json"), | ||
| path.join(home, ".claude.json"), | ||
| path.join(cwd, ".claude", "settings.json"), | ||
| path.join(cwd, ".claude", "settings.local.json") | ||
| ]; | ||
| for (const file of settingsFiles) { | ||
| if (hasContextClaudeCodeInFile(file)) return true; | ||
| } | ||
| try { | ||
| const pluginsDir = path.join(home, ".claude", "plugins"); | ||
| if (fs.existsSync(pluginsDir)) { | ||
| const mcpFiles = findMcpJsonFiles(pluginsDir); | ||
| for (const file of mcpFiles) { | ||
| if (hasContextClaudeCodeInFile(file)) return true; | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| return false; | ||
| } | ||
| function hasContextClaudeCodeInFile(filePath) { | ||
| try { | ||
| if (!fs.existsSync(filePath)) return false; | ||
| const json = JSON.parse(fs.readFileSync(filePath, "utf8")); | ||
| const servers = json.mcpServers ?? json; | ||
| if (!servers || typeof servers !== "object") return false; | ||
| for (const key of Object.keys(servers)) { | ||
| if (!key.toLowerCase().includes("serena")) continue; | ||
| const server = servers[key]; | ||
| if (!server || !Array.isArray(server.args)) continue; | ||
| const args = server.args; | ||
| for (let i = 0; i < args.length; i++) { | ||
| const a = args[i]; | ||
| if ((a === "--context" || a === "-c") && args[i + 1] === "claude-code") { | ||
| return true; | ||
| } | ||
| if (a === "--context=claude-code" || a === "-c=claude-code") { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| return false; | ||
| } | ||
| function findMcpJsonFiles(dir) { | ||
| const results = []; | ||
| try { | ||
| const entries = fs.readdirSync(dir, { withFileTypes: true }); | ||
| for (const entry of entries) { | ||
| const full = path.join(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| results.push(...findMcpJsonFiles(full)); | ||
| } else if (entry.name === ".mcp.json") { | ||
| results.push(full); | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| return results; | ||
| } | ||
| function probeRtk(paths = {}) { | ||
| const home = paths.home ?? os.homedir(); | ||
| const isWindows = process.platform === "win32"; | ||
| return runProbe("rtk", [ | ||
| () => { | ||
| const rtkDb = path.join(home, ".rtk", "tracking.db"); | ||
| return [fs.existsSync(rtkDb), "rtk-db-present"]; | ||
| }, | ||
| () => { | ||
| const bin = isWindows ? path.join(home, ".cargo", "bin", "rtk.exe") : path.join(home, ".cargo", "bin", "rtk"); | ||
| return [fs.existsSync(bin), "rtk-binary-in-cargo"]; | ||
| }, | ||
| () => { | ||
| const pathDirs = (process.env.PATH ?? "").split(path.delimiter); | ||
| const binName = process.platform === "win32" ? "rtk.exe" : "rtk"; | ||
| const found = pathDirs.some((dir) => { | ||
| try { | ||
| return fs.existsSync(path.join(dir, binName)); | ||
| } catch { | ||
| return false; | ||
| } | ||
| }); | ||
| return [found, "rtk-binary-in-path"]; | ||
| }, | ||
| () => { | ||
| const json = readSettings(globalSettings(home)); | ||
| const hooks = json?.hooks; | ||
| if (!hooks || typeof hooks !== "object") return [false, "rtk-hook-registered"]; | ||
| const serialized = JSON.stringify(hooks); | ||
| return [serialized.toLowerCase().includes("rtk"), "rtk-hook-registered"]; | ||
| } | ||
| ]); | ||
| } | ||
| function probeMcpPruning(paths = {}) { | ||
| const cwd = paths.cwd ?? process.cwd(); | ||
| return runProbe("mcp_pruning", [ | ||
| () => { | ||
| const json = readSettings(localSettings(cwd)); | ||
| const allowlist = json?.enabledMcpjsonServers; | ||
| return [Array.isArray(allowlist) && allowlist.length > 0, "allowlist-in-settings-local"]; | ||
| }, | ||
| () => { | ||
| const json = readSettings(teamSettings(cwd)); | ||
| const allowlist = json?.enabledMcpjsonServers; | ||
| return [Array.isArray(allowlist) && allowlist.length > 0, "allowlist-in-settings"]; | ||
| } | ||
| ]); | ||
| } | ||
| function probePromptCaching() { | ||
| return { | ||
| present: true, | ||
| confidence: 0.5, | ||
| signals: ["claude-code-default-enabled"], | ||
| details: { | ||
| probe: "prompt_caching", | ||
| note: "Revisa tu factura Anthropic para confirmar el ahorro real" | ||
| } | ||
| }; | ||
| } | ||
| export { | ||
| probeSerena, | ||
| checkSerenaHealth, | ||
| probeRtk, | ||
| probeMcpPruning, | ||
| probePromptCaching | ||
| }; | ||
| //# sourceMappingURL=chunk-4MJNQPFS.js.map |
| {"version":3,"sources":["../src/orchestration/detector.ts"],"sourcesContent":["// Detection probes — Phase 4.1\r\n// Multi-signal checks for serena, RTK, MCP pruning and prompt caching.\r\n// Each probe returns DetectionResult { present, confidence, signals, details }\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport os from 'node:os'\r\nimport type { DetectionResult, SerenaHealthWarning } from '../lib/types.js'\r\n\r\nfunction readSettings(p: string): Record<string, unknown> | null {\r\n try {\r\n if (!fs.existsSync(p)) return null\r\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\nexport interface DetectorPaths {\r\n home?: string\r\n cwd?: string\r\n}\r\n\r\nfunction globalSettings(home: string): string {\r\n return path.join(home, '.claude', 'settings.json')\r\n}\r\n\r\nfunction globalClaudeJson(home: string): string {\r\n return path.join(home, '.claude.json')\r\n}\r\n\r\nfunction localSettings(cwd: string): string {\r\n return path.join(cwd, '.claude', 'settings.local.json')\r\n}\r\n\r\nfunction teamSettings(cwd: string): string {\r\n return path.join(cwd, '.claude', 'settings.json')\r\n}\r\n\r\nfunction mcpServerKeys(json: Record<string, unknown> | null): string[] {\r\n if (!json) return []\r\n const mcp = json.mcpServers\r\n if (mcp && typeof mcp === 'object' && !Array.isArray(mcp)) {\r\n return Object.keys(mcp as Record<string, unknown>)\r\n }\r\n return []\r\n}\r\n\r\nfunction runProbe(\r\n name: string,\r\n checks: Array<() => [boolean, string]>,\r\n): DetectionResult {\r\n const signals: string[] = []\r\n let hits = 0\r\n for (const check of checks) {\r\n try {\r\n const [hit, label] = check()\r\n if (hit) {\r\n hits++\r\n signals.push(label)\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n }\r\n const confidence = checks.length > 0 ? hits / checks.length : 0\r\n return {\r\n present: hits > 0,\r\n confidence,\r\n signals,\r\n details: { probe: name, signal_count: hits, total_checks: checks.length },\r\n }\r\n}\r\n\r\nexport function probeSerena(paths: DetectorPaths = {}): DetectionResult {\r\n const home = paths.home ?? os.homedir()\r\n const cwd = paths.cwd ?? process.cwd()\r\n return runProbe('serena', [\r\n () => {\r\n const keys = mcpServerKeys(readSettings(globalSettings(home)))\r\n return [keys.some((k) => k.toLowerCase().includes('serena')), 'global-settings-registered']\r\n },\r\n () => {\r\n // ~/.claude.json — Claude Code also reads MCP servers from here\r\n const keys = mcpServerKeys(readSettings(globalClaudeJson(home)))\r\n return [keys.some((k) => k.toLowerCase().includes('serena')), 'claude-json-registered']\r\n },\r\n () => {\r\n const keys = mcpServerKeys(readSettings(teamSettings(cwd)))\r\n return [keys.some((k) => k.toLowerCase().includes('serena')), 'project-mcp-registered']\r\n },\r\n () => {\r\n const keys = mcpServerKeys(readSettings(localSettings(cwd)))\r\n return [keys.some((k) => k.toLowerCase().includes('serena')), 'local-mcp-registered']\r\n },\r\n () => {\r\n // Check if current CWD is registered as a serena project\r\n const configPath = path.join(home, '.serena', 'serena_config.yml')\r\n if (!fs.existsSync(configPath)) return [false, 'project-registered-for-cwd']\r\n try {\r\n const content = fs.readFileSync(configPath, 'utf8')\r\n const normalizedCwd = cwd.replace(/\\\\/g, '/').toLowerCase()\r\n // Simple check: does the config mention a path matching our CWD?\r\n const normalizedContent = content.replace(/\\\\/g, '/').toLowerCase()\r\n return [normalizedContent.includes(normalizedCwd), 'project-registered-for-cwd']\r\n } catch {\r\n return [false, 'project-registered-for-cwd']\r\n }\r\n },\r\n ])\r\n}\r\n\r\n/**\r\n * Health checks for Serena configuration.\r\n * Separate from probeSerena() (presence detection) to avoid polluting confidence scores.\r\n * Returns actionable warnings when Serena is misconfigured for Claude Code usage.\r\n */\r\nexport function checkSerenaHealth(paths: DetectorPaths = {}): SerenaHealthWarning[] {\r\n const home = paths.home ?? os.homedir()\r\n const cwd = paths.cwd ?? process.cwd()\r\n const warnings: SerenaHealthWarning[] = []\r\n\r\n // Check 1: web_dashboard_open_on_launch should be false\r\n try {\r\n const configPath = path.join(home, '.serena', 'serena_config.yml')\r\n if (fs.existsSync(configPath)) {\r\n const content = fs.readFileSync(configPath, 'utf8')\r\n const match = content.match(/web_dashboard_open_on_launch\\s*:\\s*(\\w+)/)\r\n const value = match ? match[1].toLowerCase() : 'true' // default is true\r\n if (value === 'true') {\r\n warnings.push({\r\n id: 'dashboard-auto-open',\r\n message: 'El dashboard de Serena se abre automaticamente al iniciar cada terminal',\r\n fix: 'Pon web_dashboard_open_on_launch: false en ~/.serena/serena_config.yml',\r\n })\r\n }\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n\r\n // Check 2: --context claude-code should be in MCP server args\r\n try {\r\n const hasContextFlag = checkSerenaContextFlag(home, cwd)\r\n if (!hasContextFlag) {\r\n warnings.push({\r\n id: 'missing-context-claude-code',\r\n message: 'Serena no usa el contexto claude-code (modo headless optimizado para CLI)',\r\n fix: 'Añade --context claude-code a los args del MCP server de serena',\r\n })\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n\r\n return warnings\r\n}\r\n\r\nfunction checkSerenaContextFlag(home: string, cwd: string): boolean {\r\n // Search across all possible MCP config locations\r\n const settingsFiles = [\r\n path.join(home, '.claude', 'settings.json'),\r\n path.join(home, '.claude.json'),\r\n path.join(cwd, '.claude', 'settings.json'),\r\n path.join(cwd, '.claude', 'settings.local.json'),\r\n ]\r\n\r\n for (const file of settingsFiles) {\r\n if (hasContextClaudeCodeInFile(file)) return true\r\n }\r\n\r\n // Also check plugin .mcp.json files\r\n try {\r\n const pluginsDir = path.join(home, '.claude', 'plugins')\r\n if (fs.existsSync(pluginsDir)) {\r\n const mcpFiles = findMcpJsonFiles(pluginsDir)\r\n for (const file of mcpFiles) {\r\n if (hasContextClaudeCodeInFile(file)) return true\r\n }\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n\r\n return false\r\n}\r\n\r\nfunction hasContextClaudeCodeInFile(filePath: string): boolean {\r\n try {\r\n if (!fs.existsSync(filePath)) return false\r\n const json = JSON.parse(fs.readFileSync(filePath, 'utf8'))\r\n\r\n // Check mcpServers keys for serena entries\r\n const servers = json.mcpServers ?? json\r\n if (!servers || typeof servers !== 'object') return false\r\n\r\n for (const key of Object.keys(servers)) {\r\n if (!key.toLowerCase().includes('serena')) continue\r\n const server = servers[key]\r\n if (!server || !Array.isArray(server.args)) continue\r\n const args = server.args as string[]\r\n // Aceptar las 3 formas válidas en CLI:\r\n // --context claude-code (dos args separados)\r\n // --context=claude-code (un arg fusionado con =)\r\n // -c claude-code / -c=claude-code (forma corta)\r\n for (let i = 0; i < args.length; i++) {\r\n const a = args[i]\r\n if ((a === '--context' || a === '-c') && args[i + 1] === 'claude-code') {\r\n return true\r\n }\r\n if (a === '--context=claude-code' || a === '-c=claude-code') {\r\n return true\r\n }\r\n }\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n return false\r\n}\r\n\r\nfunction findMcpJsonFiles(dir: string): string[] {\r\n const results: string[] = []\r\n try {\r\n const entries = fs.readdirSync(dir, { withFileTypes: true })\r\n for (const entry of entries) {\r\n const full = path.join(dir, entry.name)\r\n if (entry.isDirectory()) {\r\n results.push(...findMcpJsonFiles(full))\r\n } else if (entry.name === '.mcp.json') {\r\n results.push(full)\r\n }\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n return results\r\n}\r\n\r\nexport function probeRtk(paths: DetectorPaths = {}): DetectionResult {\r\n const home = paths.home ?? os.homedir()\r\n const isWindows = process.platform === 'win32'\r\n return runProbe('rtk', [\r\n () => {\r\n const rtkDb = path.join(home, '.rtk', 'tracking.db')\r\n return [fs.existsSync(rtkDb), 'rtk-db-present']\r\n },\r\n () => {\r\n const bin = isWindows\r\n ? path.join(home, '.cargo', 'bin', 'rtk.exe')\r\n : path.join(home, '.cargo', 'bin', 'rtk')\r\n return [fs.existsSync(bin), 'rtk-binary-in-cargo']\r\n },\r\n () => {\r\n // Check common PATH locations directly (avoid dynamic import in sync probe)\r\n const pathDirs = (process.env.PATH ?? '').split(path.delimiter)\r\n const binName = process.platform === 'win32' ? 'rtk.exe' : 'rtk'\r\n const found = pathDirs.some((dir) => {\r\n try {\r\n return fs.existsSync(path.join(dir, binName))\r\n } catch {\r\n return false\r\n }\r\n })\r\n return [found, 'rtk-binary-in-path']\r\n },\r\n () => {\r\n // RTK's own `rtk hook claude` PreToolUse hook — the canonical Bash rewriter.\r\n const json = readSettings(globalSettings(home))\r\n const hooks = json?.hooks\r\n if (!hooks || typeof hooks !== 'object') return [false, 'rtk-hook-registered']\r\n const serialized = JSON.stringify(hooks)\r\n return [serialized.toLowerCase().includes('rtk'), 'rtk-hook-registered']\r\n },\r\n ])\r\n}\r\n\r\nexport function probeMcpPruning(paths: DetectorPaths = {}): DetectionResult {\r\n const cwd = paths.cwd ?? process.cwd()\r\n return runProbe('mcp_pruning', [\r\n () => {\r\n const json = readSettings(localSettings(cwd))\r\n const allowlist = json?.enabledMcpjsonServers\r\n return [Array.isArray(allowlist) && allowlist.length > 0, 'allowlist-in-settings-local']\r\n },\r\n () => {\r\n const json = readSettings(teamSettings(cwd))\r\n const allowlist = json?.enabledMcpjsonServers\r\n return [Array.isArray(allowlist) && allowlist.length > 0, 'allowlist-in-settings']\r\n },\r\n ])\r\n}\r\n\r\nexport function probePromptCaching(): DetectionResult {\r\n // Claude Code has prompt caching enabled by default; no reliable local probe.\r\n return {\r\n present: true,\r\n confidence: 0.5,\r\n signals: ['claude-code-default-enabled'],\r\n details: {\r\n probe: 'prompt_caching',\r\n note: 'Revisa tu factura Anthropic para confirmar el ahorro real',\r\n },\r\n }\r\n}\r\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| // src/services/budget-manager.ts | ||
| var MAX_TOKENS = 1e7; | ||
| var BudgetManager = class { | ||
| queries; | ||
| constructor(db) { | ||
| this.queries = buildQueries(db); | ||
| } | ||
| setBudget(input) { | ||
| if (!Number.isInteger(input.limit_tokens)) { | ||
| throw new Error("limit_tokens debe ser un entero"); | ||
| } | ||
| if (input.limit_tokens <= 0) { | ||
| throw new Error("limit_tokens debe ser mayor que 0"); | ||
| } | ||
| if (input.limit_tokens > MAX_TOKENS) { | ||
| throw new Error(`limit_tokens no puede exceder ${MAX_TOKENS}`); | ||
| } | ||
| if (input.scope !== "session" && input.scope !== "project") { | ||
| throw new Error(`scope invalido: ${String(input.scope)}`); | ||
| } | ||
| const mode = "warn"; | ||
| this.queries.upsertBudget(input.scope, input.scope_key, input.limit_tokens, mode); | ||
| const budget = this.queries.getBudgetByScope(input.scope, input.scope_key); | ||
| if (!budget) { | ||
| throw new Error("no se pudo leer el budget recien creado"); | ||
| } | ||
| return budget; | ||
| } | ||
| /** | ||
| * Resolve the active budget for a given session + project. | ||
| * Returns the session-scoped budget if present, else the project-scoped one, else null. | ||
| */ | ||
| getActiveBudget(sessionId, projectHash) { | ||
| const sessionBudget = this.queries.getBudgetByScope("session", sessionId); | ||
| if (sessionBudget) return sessionBudget; | ||
| if (projectHash) { | ||
| const projectBudget = this.queries.getBudgetByScope("project", projectHash); | ||
| if (projectBudget) return projectBudget; | ||
| } | ||
| return null; | ||
| } | ||
| /** Compute spent tokens against a budget from tool_calls since the budget creation timestamp. */ | ||
| computeSpent(budget) { | ||
| if (budget.scope === "session") { | ||
| return this.queries.sumTokensBySessionSince(budget.scope_key, budget.created_at); | ||
| } | ||
| return this.queries.sumTokensByProjectSince(budget.scope_key, budget.created_at); | ||
| } | ||
| checkBudget(sessionId, projectHash) { | ||
| const active = this.getActiveBudget(sessionId, projectHash); | ||
| if (!active) { | ||
| return { active: false, spent: 0, remaining: 0, percent_used: 0, mode: null }; | ||
| } | ||
| const spent = this.computeSpent(active); | ||
| const remaining = Math.max(0, active.limit_tokens - spent); | ||
| const percent = active.limit_tokens > 0 ? spent / active.limit_tokens : 0; | ||
| return { | ||
| active: true, | ||
| spent, | ||
| remaining, | ||
| percent_used: percent, | ||
| mode: active.mode | ||
| }; | ||
| } | ||
| clearBudget(scope, scopeKey) { | ||
| return this.queries.deleteBudgetByScope(scope, scopeKey) > 0; | ||
| } | ||
| getBudgetReport(since) { | ||
| return { | ||
| by_tool: this.queries.countToolCallsByTool(since), | ||
| by_source: this.queries.countToolCallsBySource(since), | ||
| period_since: since | ||
| }; | ||
| } | ||
| recordBudgetEvent(budgetId, eventType, tokens) { | ||
| this.queries.insertBudgetEvent(budgetId, eventType, tokens); | ||
| } | ||
| }; | ||
| export { | ||
| BudgetManager | ||
| }; | ||
| //# sourceMappingURL=chunk-AF6RQ5F5.js.map |
| {"version":3,"sources":["../src/services/budget-manager.ts"],"sourcesContent":["// Budget manager — Phase 2.1 + 2.3\r\n// Precedence: session > project. Spent tokens computed from tool_calls since budget.created_at.\r\n\r\nimport type Database from 'better-sqlite3'\r\nimport type {\r\n Budget,\r\n BudgetScope,\r\n BudgetMode,\r\n BudgetStatus,\r\n} from '../lib/types.js'\r\nimport { buildQueries, type Queries, type ToolCountRow, type SourceCountRow } from '../db/queries.js'\r\n\r\ntype DB = Database.Database\r\n\r\nexport interface SetBudgetInput {\r\n scope: BudgetScope\r\n scope_key: string\r\n limit_tokens: number\r\n mode?: BudgetMode\r\n}\r\n\r\nexport interface BudgetReport {\r\n by_tool: ToolCountRow[]\r\n by_source: SourceCountRow[]\r\n period_since: string\r\n}\r\n\r\nconst MAX_TOKENS = 1e7\r\n\r\nexport class BudgetManager {\r\n private queries: Queries\r\n\r\n constructor(db: DB) {\r\n this.queries = buildQueries(db)\r\n }\r\n\r\n setBudget(input: SetBudgetInput): Budget {\r\n if (!Number.isInteger(input.limit_tokens)) {\r\n throw new Error('limit_tokens debe ser un entero')\r\n }\r\n if (input.limit_tokens <= 0) {\r\n throw new Error('limit_tokens debe ser mayor que 0')\r\n }\r\n if (input.limit_tokens > MAX_TOKENS) {\r\n throw new Error(`limit_tokens no puede exceder ${MAX_TOKENS}`)\r\n }\r\n if (input.scope !== 'session' && input.scope !== 'project') {\r\n throw new Error(`scope invalido: ${String(input.scope)}`)\r\n }\r\n const mode: BudgetMode = 'warn'\r\n this.queries.upsertBudget(input.scope, input.scope_key, input.limit_tokens, mode)\r\n const budget = this.queries.getBudgetByScope(input.scope, input.scope_key)\r\n if (!budget) {\r\n throw new Error('no se pudo leer el budget recien creado')\r\n }\r\n return budget\r\n }\r\n\r\n /**\r\n * Resolve the active budget for a given session + project.\r\n * Returns the session-scoped budget if present, else the project-scoped one, else null.\r\n */\r\n getActiveBudget(sessionId: string, projectHash: string | null): Budget | null {\r\n const sessionBudget = this.queries.getBudgetByScope('session', sessionId)\r\n if (sessionBudget) return sessionBudget\r\n if (projectHash) {\r\n const projectBudget = this.queries.getBudgetByScope('project', projectHash)\r\n if (projectBudget) return projectBudget\r\n }\r\n return null\r\n }\r\n\r\n /** Compute spent tokens against a budget from tool_calls since the budget creation timestamp. */\r\n computeSpent(budget: Budget): number {\r\n if (budget.scope === 'session') {\r\n return this.queries.sumTokensBySessionSince(budget.scope_key, budget.created_at)\r\n }\r\n return this.queries.sumTokensByProjectSince(budget.scope_key, budget.created_at)\r\n }\r\n\r\n checkBudget(sessionId: string, projectHash: string | null): BudgetStatus {\r\n const active = this.getActiveBudget(sessionId, projectHash)\r\n if (!active) {\r\n return { active: false, spent: 0, remaining: 0, percent_used: 0, mode: null }\r\n }\r\n const spent = this.computeSpent(active)\r\n const remaining = Math.max(0, active.limit_tokens - spent)\r\n const percent = active.limit_tokens > 0 ? spent / active.limit_tokens : 0\r\n return {\r\n active: true,\r\n spent,\r\n remaining,\r\n percent_used: percent,\r\n mode: active.mode,\r\n }\r\n }\r\n\r\n clearBudget(scope: BudgetScope, scopeKey: string): boolean {\r\n return this.queries.deleteBudgetByScope(scope, scopeKey) > 0\r\n }\r\n\r\n getBudgetReport(since: string): BudgetReport {\r\n return {\r\n by_tool: this.queries.countToolCallsByTool(since),\r\n by_source: this.queries.countToolCallsBySource(since),\r\n period_since: since,\r\n }\r\n }\r\n\r\n recordBudgetEvent(\r\n budgetId: number,\r\n eventType: 'spend' | 'warn' | 'block' | 'reset',\r\n tokens: number | null,\r\n ): void {\r\n this.queries.insertBudgetEvent(budgetId, eventType, tokens)\r\n }\r\n}\r\n"],"mappings":";;;;;;AA2BA,IAAM,aAAa;AAEZ,IAAM,gBAAN,MAAoB;AAAA,EACjB;AAAA,EAER,YAAY,IAAQ;AAClB,SAAK,UAAU,aAAa,EAAE;AAAA,EAChC;AAAA,EAEA,UAAU,OAA+B;AACvC,QAAI,CAAC,OAAO,UAAU,MAAM,YAAY,GAAG;AACzC,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AACA,QAAI,MAAM,gBAAgB,GAAG;AAC3B,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AACA,QAAI,MAAM,eAAe,YAAY;AACnC,YAAM,IAAI,MAAM,iCAAiC,UAAU,EAAE;AAAA,IAC/D;AACA,QAAI,MAAM,UAAU,aAAa,MAAM,UAAU,WAAW;AAC1D,YAAM,IAAI,MAAM,mBAAmB,OAAO,MAAM,KAAK,CAAC,EAAE;AAAA,IAC1D;AACA,UAAM,OAAmB;AACzB,SAAK,QAAQ,aAAa,MAAM,OAAO,MAAM,WAAW,MAAM,cAAc,IAAI;AAChF,UAAM,SAAS,KAAK,QAAQ,iBAAiB,MAAM,OAAO,MAAM,SAAS;AACzE,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB,WAAmB,aAA2C;AAC5E,UAAM,gBAAgB,KAAK,QAAQ,iBAAiB,WAAW,SAAS;AACxE,QAAI,cAAe,QAAO;AAC1B,QAAI,aAAa;AACf,YAAM,gBAAgB,KAAK,QAAQ,iBAAiB,WAAW,WAAW;AAC1E,UAAI,cAAe,QAAO;AAAA,IAC5B;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,aAAa,QAAwB;AACnC,QAAI,OAAO,UAAU,WAAW;AAC9B,aAAO,KAAK,QAAQ,wBAAwB,OAAO,WAAW,OAAO,UAAU;AAAA,IACjF;AACA,WAAO,KAAK,QAAQ,wBAAwB,OAAO,WAAW,OAAO,UAAU;AAAA,EACjF;AAAA,EAEA,YAAY,WAAmB,aAA0C;AACvE,UAAM,SAAS,KAAK,gBAAgB,WAAW,WAAW;AAC1D,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,QAAQ,OAAO,OAAO,GAAG,WAAW,GAAG,cAAc,GAAG,MAAM,KAAK;AAAA,IAC9E;AACA,UAAM,QAAQ,KAAK,aAAa,MAAM;AACtC,UAAM,YAAY,KAAK,IAAI,GAAG,OAAO,eAAe,KAAK;AACzD,UAAM,UAAU,OAAO,eAAe,IAAI,QAAQ,OAAO,eAAe;AACxE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,cAAc;AAAA,MACd,MAAM,OAAO;AAAA,IACf;AAAA,EACF;AAAA,EAEA,YAAY,OAAoB,UAA2B;AACzD,WAAO,KAAK,QAAQ,oBAAoB,OAAO,QAAQ,IAAI;AAAA,EAC7D;AAAA,EAEA,gBAAgB,OAA6B;AAC3C,WAAO;AAAA,MACL,SAAS,KAAK,QAAQ,qBAAqB,KAAK;AAAA,MAChD,WAAW,KAAK,QAAQ,uBAAuB,KAAK;AAAA,MACpD,cAAc;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,kBACE,UACA,WACA,QACM;AACN,SAAK,QAAQ,kBAAkB,UAAU,WAAW,MAAM;AAAA,EAC5D;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| // src/orchestration/schema-measurer.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| var TOKENS_PER_TOOL = 400; | ||
| var ESTIMATED_TOOLS_PER_SERVER = 10; | ||
| var BYTES_PER_TOKEN = 4; | ||
| function readJson(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return null; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function collectServerNames(jsonFiles) { | ||
| const set = /* @__PURE__ */ new Set(); | ||
| for (const json of jsonFiles) { | ||
| if (!json) continue; | ||
| const mcp = json.mcpServers; | ||
| if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) { | ||
| for (const k of Object.keys(mcp)) { | ||
| set.add(k); | ||
| } | ||
| } | ||
| } | ||
| return Array.from(set); | ||
| } | ||
| function measureCurrentSchemaBytes(opts = {}) { | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const sources = [ | ||
| readJson(path.join(home, ".claude", "settings.json")), | ||
| readJson(path.join(home, ".claude.json")), | ||
| // Claude Code also reads MCPs from here | ||
| readJson(path.join(cwd, ".claude", "settings.json")), | ||
| readJson(path.join(cwd, ".claude", "settings.local.json")) | ||
| ]; | ||
| const servers = collectServerNames(sources); | ||
| const toolCount = servers.length * ESTIMATED_TOOLS_PER_SERVER; | ||
| const tokens = toolCount * TOKENS_PER_TOOL; | ||
| return { | ||
| tool_schema_bytes: tokens * BYTES_PER_TOKEN, | ||
| tool_schema_tokens: tokens, | ||
| tool_count_estimated: toolCount, | ||
| mcp_servers: servers, | ||
| measurement_method: servers.length > 0 ? "heuristic" : "unknown" | ||
| }; | ||
| } | ||
| export { | ||
| measureCurrentSchemaBytes | ||
| }; | ||
| //# sourceMappingURL=chunk-ESRDZMZJ.js.map |
| {"version":3,"sources":["../src/orchestration/schema-measurer.ts"],"sourcesContent":["// Tool-schema size measurement — Phase 4.2\r\n// Heuristic: count registered MCP servers from settings files and estimate tool-schema cost.\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport os from 'node:os'\r\n\r\nconst TOKENS_PER_TOOL = 400\r\nconst ESTIMATED_TOOLS_PER_SERVER = 10\r\nconst BYTES_PER_TOKEN = 4\r\n\r\nexport interface SchemaMeasurement {\r\n tool_schema_bytes: number\r\n tool_schema_tokens: number\r\n tool_count_estimated: number\r\n mcp_servers: string[]\r\n measurement_method: 'accurate' | 'heuristic' | 'unknown'\r\n}\r\n\r\nexport interface SchemaMeasurerOptions {\r\n home?: string\r\n cwd?: string\r\n}\r\n\r\nfunction readJson(p: string): Record<string, unknown> | null {\r\n try {\r\n if (!fs.existsSync(p)) return null\r\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\nfunction collectServerNames(jsonFiles: Array<Record<string, unknown> | null>): string[] {\r\n const set = new Set<string>()\r\n for (const json of jsonFiles) {\r\n if (!json) continue\r\n const mcp = json.mcpServers\r\n if (mcp && typeof mcp === 'object' && !Array.isArray(mcp)) {\r\n for (const k of Object.keys(mcp as Record<string, unknown>)) {\r\n set.add(k)\r\n }\r\n }\r\n }\r\n return Array.from(set)\r\n}\r\n\r\nexport function measureCurrentSchemaBytes(\r\n opts: SchemaMeasurerOptions = {},\r\n): SchemaMeasurement {\r\n const home = opts.home ?? os.homedir()\r\n const cwd = opts.cwd ?? process.cwd()\r\n const sources = [\r\n readJson(path.join(home, '.claude', 'settings.json')),\r\n readJson(path.join(home, '.claude.json')), // Claude Code also reads MCPs from here\r\n readJson(path.join(cwd, '.claude', 'settings.json')),\r\n readJson(path.join(cwd, '.claude', 'settings.local.json')),\r\n ]\r\n const servers = collectServerNames(sources)\r\n const toolCount = servers.length * ESTIMATED_TOOLS_PER_SERVER\r\n const tokens = toolCount * TOKENS_PER_TOOL\r\n return {\r\n tool_schema_bytes: tokens * BYTES_PER_TOKEN,\r\n tool_schema_tokens: tokens,\r\n tool_count_estimated: toolCount,\r\n mcp_servers: servers,\r\n measurement_method: servers.length > 0 ? 'heuristic' : 'unknown',\r\n }\r\n}\r\n"],"mappings":";;;AAGA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAEf,IAAM,kBAAkB;AACxB,IAAM,6BAA6B;AACnC,IAAM,kBAAkB;AAexB,SAAS,SAAS,GAA2C;AAC3D,MAAI;AACF,QAAI,CAAC,GAAG,WAAW,CAAC,EAAG,QAAO;AAC9B,WAAO,KAAK,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,mBAAmB,WAA4D;AACtF,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,QAAQ,WAAW;AAC5B,QAAI,CAAC,KAAM;AACX,UAAM,MAAM,KAAK;AACjB,QAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,GAAG,GAAG;AACzD,iBAAW,KAAK,OAAO,KAAK,GAA8B,GAAG;AAC3D,YAAI,IAAI,CAAC;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG;AACvB;AAEO,SAAS,0BACd,OAA8B,CAAC,GACZ;AACnB,QAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ;AACrC,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,UAAU;AAAA,IACd,SAAS,KAAK,KAAK,MAAM,WAAW,eAAe,CAAC;AAAA,IACpD,SAAS,KAAK,KAAK,MAAM,cAAc,CAAC;AAAA;AAAA,IACxC,SAAS,KAAK,KAAK,KAAK,WAAW,eAAe,CAAC;AAAA,IACnD,SAAS,KAAK,KAAK,KAAK,WAAW,qBAAqB,CAAC;AAAA,EAC3D;AACA,QAAM,UAAU,mBAAmB,OAAO;AAC1C,QAAM,YAAY,QAAQ,SAAS;AACnC,QAAM,SAAS,YAAY;AAC3B,SAAO;AAAA,IACL,mBAAmB,SAAS;AAAA,IAC5B,oBAAoB;AAAA,IACpB,sBAAsB;AAAA,IACtB,aAAa;AAAA,IACb,oBAAoB,QAAQ,SAAS,IAAI,cAAc;AAAA,EACzD;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| checkSerenaHealth | ||
| } from "./chunk-4MJNQPFS.js"; | ||
| // src/orchestration/advisor.ts | ||
| function buildSuggestions(status, paths) { | ||
| const suggestions = []; | ||
| if (!status.serena.present) { | ||
| suggestions.push( | ||
| [ | ||
| "[serena] Para lecturas simbolicas (menos tokens en archivos grandes), instala serena-mcp:", | ||
| " uvx --from git+https://github.com/oraios/serena serena start-mcp-server", | ||
| " Nota de seguridad: serena incluye execute_shell_command, revisa la configuracion.", | ||
| " Ahorro estimado: 20-30% en lecturas de archivos grandes." | ||
| ].join("\n") | ||
| ); | ||
| } else { | ||
| if (!status.serena.signals.includes("project-registered-for-cwd")) { | ||
| suggestions.push( | ||
| [ | ||
| "[serena] Serena esta instalada pero este proyecto no esta registrado.", | ||
| " Ejecuta: mcp__serena__activate_project con la ruta de este proyecto.", | ||
| " O crea .serena/project.yml en la raiz del proyecto para auto-deteccion.", | ||
| " Sin proyecto activo, serena no puede hacer lecturas simbolicas." | ||
| ].join("\n") | ||
| ); | ||
| } | ||
| const healthWarnings = checkSerenaHealth(paths); | ||
| for (const w of healthWarnings) { | ||
| suggestions.push(`[serena] \u26A0 ${w.message} | ||
| Fix: ${w.fix}`); | ||
| } | ||
| } | ||
| if (!status.rtk.present) { | ||
| suggestions.push( | ||
| [ | ||
| "[rtk] Para filtrar salida ruidosa de Bash (builds, tests), instala RTK:", | ||
| " brew install standard-input/tap/rtk (macOS) o descarga binario firmado en github.com/standard-input/rtk", | ||
| " Nota de seguridad: RTK publica releases firmadas con GPG.", | ||
| " Ahorro estimado: 15-25% en ciclos build/test." | ||
| ].join("\n") | ||
| ); | ||
| } else if (!status.rtk.signals.includes("rtk-hook-registered")) { | ||
| suggestions.push( | ||
| [ | ||
| "[rtk] RTK esta instalado pero su hook de Claude Code no esta configurado.", | ||
| " Ejecuta: rtk init -g \u2014 registra el hook PreToolUse de RTK que reescribe comandos Bash automaticamente.", | ||
| " Sin el hook, RTK solo funciona si se invoca manualmente (rtk ls, rtk git, etc)." | ||
| ].join("\n") | ||
| ); | ||
| } | ||
| if (!status.mcp_pruning.present) { | ||
| suggestions.push( | ||
| [ | ||
| "[mcp-pruning] Reduce el coste de tool-schema activando un allowlist por proyecto:", | ||
| " Ejecuta mcp_prune_suggest para generar uno basado en tu historial y aplicalo con mcp_prune_apply.", | ||
| " Se escribe en .claude/settings.local.json (personal, no afecta al equipo).", | ||
| " Ahorro estimado: 5-12% por turno sobre el Tool Search nativo de Claude Code." | ||
| ].join("\n") | ||
| ); | ||
| } | ||
| return suggestions; | ||
| } | ||
| export { | ||
| buildSuggestions | ||
| }; | ||
| //# sourceMappingURL=chunk-EV4HR7LB.js.map |
| {"version":3,"sources":["../src/orchestration/advisor.ts"],"sourcesContent":["// Advisory suggestions — Phase 4.3\r\n// Takes an OptimizationStatus and returns Spanish actionable recommendations.\r\n\r\nimport type { OptimizationStatus } from '../lib/types.js'\r\nimport { checkSerenaHealth } from './detector.js'\r\nimport type { DetectorPaths } from './detector.js'\r\n\r\nexport function buildSuggestions(\r\n status: OptimizationStatus,\r\n paths?: DetectorPaths,\r\n): string[] {\r\n const suggestions: string[] = []\r\n\r\n if (!status.serena.present) {\r\n suggestions.push(\r\n [\r\n '[serena] Para lecturas simbolicas (menos tokens en archivos grandes), instala serena-mcp:',\r\n ' uvx --from git+https://github.com/oraios/serena serena start-mcp-server',\r\n ' Nota de seguridad: serena incluye execute_shell_command, revisa la configuracion.',\r\n ' Ahorro estimado: 20-30% en lecturas de archivos grandes.',\r\n ].join('\\n'),\r\n )\r\n } else {\r\n if (!status.serena.signals.includes('project-registered-for-cwd')) {\r\n suggestions.push(\r\n [\r\n '[serena] Serena esta instalada pero este proyecto no esta registrado.',\r\n ' Ejecuta: mcp__serena__activate_project con la ruta de este proyecto.',\r\n ' O crea .serena/project.yml en la raiz del proyecto para auto-deteccion.',\r\n ' Sin proyecto activo, serena no puede hacer lecturas simbolicas.',\r\n ].join('\\n'),\r\n )\r\n }\r\n\r\n // Health checks — only when serena is present\r\n const healthWarnings = checkSerenaHealth(paths)\r\n for (const w of healthWarnings) {\r\n suggestions.push(`[serena] ⚠ ${w.message}\\n Fix: ${w.fix}`)\r\n }\r\n }\r\n\r\n if (!status.rtk.present) {\r\n suggestions.push(\r\n [\r\n '[rtk] Para filtrar salida ruidosa de Bash (builds, tests), instala RTK:',\r\n ' brew install standard-input/tap/rtk (macOS) o descarga binario firmado en github.com/standard-input/rtk',\r\n ' Nota de seguridad: RTK publica releases firmadas con GPG.',\r\n ' Ahorro estimado: 15-25% en ciclos build/test.',\r\n ].join('\\n'),\r\n )\r\n } else if (!status.rtk.signals.includes('rtk-hook-registered')) {\r\n suggestions.push(\r\n [\r\n '[rtk] RTK esta instalado pero su hook de Claude Code no esta configurado.',\r\n ' Ejecuta: rtk init -g — registra el hook PreToolUse de RTK que reescribe comandos Bash automaticamente.',\r\n ' Sin el hook, RTK solo funciona si se invoca manualmente (rtk ls, rtk git, etc).',\r\n ].join('\\n'),\r\n )\r\n }\r\n\r\n if (!status.mcp_pruning.present) {\r\n suggestions.push(\r\n [\r\n '[mcp-pruning] Reduce el coste de tool-schema activando un allowlist por proyecto:',\r\n ' Ejecuta mcp_prune_suggest para generar uno basado en tu historial y aplicalo con mcp_prune_apply.',\r\n ' Se escribe en .claude/settings.local.json (personal, no afecta al equipo).',\r\n ' Ahorro estimado: 5-12% por turno sobre el Tool Search nativo de Claude Code.',\r\n ].join('\\n'),\r\n )\r\n }\r\n\r\n return suggestions\r\n}\r\n"],"mappings":";;;;;;AAOO,SAAS,iBACd,QACA,OACU;AACV,QAAM,cAAwB,CAAC;AAE/B,MAAI,CAAC,OAAO,OAAO,SAAS;AAC1B,gBAAY;AAAA,MACV;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,EACF,OAAO;AACL,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAS,4BAA4B,GAAG;AACjE,kBAAY;AAAA,QACV;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,EAAE,KAAK,IAAI;AAAA,MACb;AAAA,IACF;AAGA,UAAM,iBAAiB,kBAAkB,KAAK;AAC9C,eAAW,KAAK,gBAAgB;AAC9B,kBAAY,KAAK,mBAAc,EAAE,OAAO;AAAA,SAAY,EAAE,GAAG,EAAE;AAAA,IAC7D;AAAA,EACF;AAEA,MAAI,CAAC,OAAO,IAAI,SAAS;AACvB,gBAAY;AAAA,MACV;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,EACF,WAAW,CAAC,OAAO,IAAI,QAAQ,SAAS,qBAAqB,GAAG;AAC9D,gBAAY;AAAA,MACV;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,EACF;AAEA,MAAI,CAAC,OAAO,YAAY,SAAS;AAC/B,gBAAY;AAAA,MACV;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,EACF;AAEA,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveXrayUrl | ||
| } from "./chunk-METYJF7E.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| resolveTranscriptPath | ||
| } from "./chunk-U3OXZD52.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.7.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-GSWB574D.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\r\n// See sdd/token-optimizer-v0.1/coach-layer-addendum CO-1\r\n\r\nimport type { CoachTip } from '../lib/types.js'\r\n\r\nexport const KNOWLEDGE_BASE: readonly CoachTip[] = [\r\n {\r\n id: 'use-opusplan',\r\n title: 'Usa /opusplan para planificar con Opus y ejecutar con Sonnet',\r\n description:\r\n 'opusplan usa Opus durante plan mode para razonamiento complejo y vuelve a Sonnet para implementacion. Solo pagas Opus en la fase de planning.',\r\n savings_estimate: '60-80% de reduccion de coste en sesiones con planning intensivo',\r\n savings_source: 'community-measured',\r\n how_to_invoke: '/model opusplan',\r\n when_applicable: 'Sesiones con razonamiento largo antes de codigo',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-long-reasoning-no-code',\r\n },\r\n {\r\n id: 'use-plan-mode',\r\n title: 'Activa plan mode para exploracion sin escribir codigo',\r\n description:\r\n 'EnterPlanMode permite razonar y explorar sin hacer ediciones, reduciendo iteraciones costosas.',\r\n savings_estimate: 'Variable segun tarea',\r\n savings_source: 'internal',\r\n how_to_invoke: 'EnterPlanMode tool',\r\n when_applicable: 'Tareas no triviales antes de escribir codigo',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-long-reasoning-no-code',\r\n },\r\n {\r\n id: 'use-fast-mode',\r\n title: 'Activa /fast para respuestas mas directas',\r\n description: 'Modo rapido mantiene el modelo pero reduce el detalle de las respuestas.',\r\n savings_estimate: 'Reduce tiempo principalmente',\r\n savings_source: 'internal',\r\n how_to_invoke: '/fast',\r\n when_applicable: 'Cuando quieres respuestas mas concisas',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: null,\r\n },\r\n {\r\n id: 'default-to-sonnet',\r\n title: 'Arranca cada sesion con Sonnet y sube a Opus solo cuando haga falta',\r\n description:\r\n 'Sonnet resuelve ~80% de tareas de coding bien. El switching tactico a Opus solo en razonamiento complejo ahorra el grueso del coste.',\r\n savings_estimate: '60-80% reduccion de coste total',\r\n savings_source: 'community-measured',\r\n how_to_invoke: '/model sonnet (inicio) → /model opus (cuando sea necesario)',\r\n when_applicable: 'Siempre como default',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-opus-for-simple-task',\r\n },\r\n {\r\n id: 'use-haiku-for-simple',\r\n title: 'Usa Haiku para formato, Q&A simple y tareas de alto volumen',\r\n description:\r\n 'Haiku es mucho mas barato y rapido. Para formateo, preguntas puntuales o tareas repetitivas es el modelo adecuado.',\r\n savings_estimate: '~90% reduccion vs Opus en tareas simples',\r\n savings_source: 'anthropic-docs',\r\n how_to_invoke: '/model haiku',\r\n when_applicable: 'Formateo, Q&A simple, alto volumen',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-opus-for-simple-task',\r\n },\r\n {\r\n id: 'use-compact-long-session',\r\n title: 'Corre /compact cuando el contexto supere el 75%',\r\n description:\r\n '/compact genera un resumen del contexto actual liberando ~60-80% de la ventana sin perder continuidad.',\r\n savings_estimate: '60-80% de contexto liberado',\r\n savings_source: 'community-measured',\r\n how_to_invoke: '/compact',\r\n when_applicable: 'Contexto > 75% de la ventana',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-context-threshold',\r\n },\r\n {\r\n id: 'use-clear-rename-resume',\r\n title: 'Usa /rename → /clear → /resume para pivotes de tema',\r\n description:\r\n 'Al cambiar a un tema no relacionado, renombra la sesion, haz /clear para empezar limpio, y resume cuando vuelvas.',\r\n savings_estimate: 'Variable segun contexto descartado',\r\n savings_source: 'internal',\r\n how_to_invoke: '/rename <nombre> → /clear → (trabajar) → /resume <nombre>',\r\n when_applicable: 'Pivote total a tema no relacionado',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-clear-opportunity',\r\n },\r\n {\r\n id: 'use-sessionstart-compact-hook',\r\n title: 'Activa el hook SessionStart:compact de token-optimizer',\r\n description:\r\n 'Cuando Claude Code compacta el contexto, token-optimizer inyecta un resumen con archivos, comandos y presupuesto.',\r\n savings_estimate: 'Evita re-lectura tras compactacion',\r\n savings_source: 'internal',\r\n how_to_invoke: 'token-optimizer-mcp install (ya lo configura)',\r\n when_applicable: 'Siempre como parte del install',\r\n source_type: 'mcp',\r\n verified_at: '2026-04-11',\r\n detector_id: null,\r\n },\r\n {\r\n id: 'use-memory-save',\r\n title: 'Guarda decisiones con mem_save antes de compactar',\r\n description:\r\n 'Persistir decisiones arquitectonicas en engram evita tener que re-derivarlas cuando el contexto se compacta.',\r\n savings_estimate: 'Variable',\r\n savings_source: 'internal',\r\n how_to_invoke: 'mem_save (via engram MCP)',\r\n when_applicable: 'Antes de /compact o cambiar de sesion',\r\n source_type: 'mcp',\r\n verified_at: '2026-04-11',\r\n detector_id: null,\r\n },\r\n {\r\n id: 'use-agent-explore',\r\n title: 'Delega busquedas amplias al subagente Explore',\r\n description:\r\n 'El subagente Explore tiene su propio contexto y no consume el de la sesion principal. Ideal para buscar en muchos archivos.',\r\n savings_estimate: 'Aisla contexto al subagente',\r\n savings_source: 'internal',\r\n how_to_invoke: 'Agent tool con subagent_type=\"Explore\"',\r\n when_applicable: '3+ busquedas Grep/Glob similares',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-repeated-searches',\r\n },\r\n {\r\n id: 'use-todowrite-long-task',\r\n title: 'Usa TodoWrite para tareas multi-paso',\r\n description:\r\n 'TodoWrite mantiene el estado de la tarea sin re-leer archivos, reduciendo redundancia.',\r\n savings_estimate: 'Evita re-lectura de estado',\r\n savings_source: 'internal',\r\n how_to_invoke: 'TodoWrite',\r\n when_applicable: '3+ pasos independientes',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: null,\r\n },\r\n {\r\n id: 'use-skill-trigger',\r\n title: 'Invoca skills en lugar de re-derivar instrucciones',\r\n description:\r\n 'Los skills cargan instrucciones especializadas solo cuando se invocan. Mejor que un CLAUDE.md monolitico.',\r\n savings_estimate: '~15k tokens/sesion con progressive disclosure',\r\n savings_source: 'community-measured',\r\n how_to_invoke: 'Skill tool con nombre del skill',\r\n when_applicable: 'Tareas que matchean un skill disponible',\r\n source_type: 'skill',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-skill-trigger-ignored',\r\n },\r\n {\r\n id: 'install-serena',\r\n title: 'Instala serena-mcp para lecturas simbolicas',\r\n description:\r\n 'serena usa LSP para leer solo los simbolos que necesitas en lugar del archivo completo. Nota: incluye execute_shell_command.',\r\n savings_estimate: '20-30% en lecturas de archivos grandes',\r\n savings_source: 'community-measured',\r\n how_to_invoke: 'uvx --from git+https://github.com/oraios/serena serena start-mcp-server',\r\n when_applicable: 'Proyectos con archivos >50k tokens',\r\n source_type: 'mcp',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-huge-file-reads',\r\n },\r\n {\r\n id: 'prefer-serena-reads',\r\n title: 'Usa Serena en vez de Read para archivos de codigo',\r\n description:\r\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.',\r\n savings_estimate: '60-90% en lecturas de codigo',\r\n savings_source: 'internal',\r\n how_to_invoke: 'get_symbols_overview(path) → find_symbol(name, include_body=true)',\r\n when_applicable: 'Archivos .ts/.js/.py/.java >50 lineas donde solo necesitas 1-2 funciones',\r\n source_type: 'mcp',\r\n verified_at: '2026-04-12',\r\n detector_id: 'detect-read-over-serena',\r\n },\r\n {\r\n id: 'install-rtk',\r\n title: 'Instala RTK para filtrar salida ruidosa de Bash',\r\n description:\r\n 'RTK filtra output de builds/tests antes de llegar a Claude Code. Publica releases firmadas con GPG.',\r\n savings_estimate: '15-25% en ciclos build/test',\r\n savings_source: 'community-measured',\r\n how_to_invoke: 'brew install standard-input/tap/rtk (macOS) o binario firmado en github.com/standard-input/rtk',\r\n when_applicable: 'Proyectos con builds/tests ruidosos',\r\n source_type: 'mcp',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-many-bash-commands',\r\n },\r\n {\r\n id: 'use-mcp-prune',\r\n title: 'Aplica un allowlist de MCPs por proyecto',\r\n description:\r\n 'Reduce el coste del tool-schema excluyendo MCPs que no usas en este proyecto. ~5-12% adicional sobre Tool Search.',\r\n savings_estimate: '5-12% por turno sobre Tool Search nativo',\r\n savings_source: 'internal',\r\n how_to_invoke: 'mcp_prune_suggest → mcp_prune_apply',\r\n when_applicable: 'MCPs registrados pero no usados en el proyecto',\r\n source_type: 'mcp',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-unused-mcp-servers',\r\n },\r\n {\r\n id: 'migrate-claudemd-to-skills',\r\n title: 'Migra CLAUDE.md grande a skills con progressive disclosure',\r\n description:\r\n 'Un CLAUDE.md monolitico se carga en cada sesion. Los skills solo cargan cuando se invocan. ~15k tokens recuperados.',\r\n savings_estimate: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\r\n savings_source: 'community-measured',\r\n how_to_invoke: 'Crear skills en .claude/skills/ con triggers especificos',\r\n when_applicable: 'CLAUDE.md > 10k tokens con uso parcial',\r\n source_type: 'skill',\r\n verified_at: '2026-04-11',\r\n detector_id: 'detect-claudemd-bloat',\r\n },\r\n {\r\n id: 'use-settings-local',\r\n title: 'Configuracion personal en settings.local.json',\r\n description:\r\n 'Evita contaminar settings.json del equipo. settings.local.json es personal y gitignored por defecto.',\r\n savings_estimate: 'Higiene, no tokens',\r\n savings_source: 'internal',\r\n how_to_invoke: 'Editar .claude/settings.local.json',\r\n when_applicable: 'Configuracion personal no compartible',\r\n source_type: 'settings',\r\n verified_at: '2026-04-11',\r\n detector_id: null,\r\n },\r\n {\r\n id: 'use-serena-overview-first',\r\n title: 'Usa get_symbols_overview antes de find_symbol',\r\n description:\r\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.',\r\n savings_estimate: '30-50% menos llamadas Serena por sesion',\r\n savings_source: 'internal',\r\n how_to_invoke: 'mcp__serena__get_symbols_overview con relative_path antes de find_symbol',\r\n when_applicable: 'Al explorar un archivo por primera vez en la sesion',\r\n source_type: 'mcp',\r\n verified_at: '2026-04-15',\r\n detector_id: 'detect-serena-read-cascade',\r\n },\r\n {\r\n id: 'use-prompt-caching',\r\n title: 'Estructura prompts para maximizar cache hits',\r\n description:\r\n 'Los tokens leidos del cache cuestan 10x menos. Mantener el prefijo estable (system, CLAUDE.md) aprovecha el cache.',\r\n savings_estimate: '10x mas barato en reads cacheados',\r\n savings_source: 'anthropic-docs',\r\n how_to_invoke: 'Mantener prefijo estable entre turns',\r\n when_applicable: 'Siempre',\r\n source_type: 'built-in',\r\n verified_at: '2026-04-11',\r\n detector_id: null,\r\n },\r\n]\r\n","// Detection rules registry (11 rules) — Phase 4.43\r\n// Each rule is a pure function over EventContext returning DetectionHit | null.\r\n// Rules MUST NOT throw; the orchestrator catches everything.\r\n\r\nimport type { DetectionRule, DetectionSeverity, ToolEvent } from '../lib/types.js'\r\n\r\nfunction countMatching(events: readonly ToolEvent[], predicate: (e: ToolEvent) => boolean): number {\r\n let c = 0\r\n for (const e of events) if (predicate(e)) c++\r\n return c\r\n}\r\n\r\nconst EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])\r\n\r\nexport const DETECTION_RULES: readonly DetectionRule[] = [\r\n // 1. detect-context-threshold\r\n {\r\n id: 'detect-context-threshold',\r\n tip_ids: ['use-compact-long-session'],\r\n run(ctx) {\r\n if (ctx.session_token_total === null || ctx.session_token_limit <= 0) return null\r\n const percent = ctx.session_token_total / ctx.session_token_limit\r\n if (percent < 0.5) return null\r\n let severity: DetectionSeverity = 'info'\r\n if (percent >= 0.9) severity = 'critical'\r\n else if (percent >= 0.75) severity = 'warn'\r\n return {\r\n rule_id: 'detect-context-threshold',\r\n tip_ids: ['use-compact-long-session'],\r\n severity,\r\n evidence: `Contexto: ${(percent * 100).toFixed(1)}% usado (${ctx.session_token_total}/${ctx.session_token_limit} tokens)`,\r\n estimation_method: ctx.session_token_method,\r\n }\r\n },\r\n },\r\n\r\n // 2. detect-long-reasoning-no-code\r\n {\r\n id: 'detect-long-reasoning-no-code',\r\n tip_ids: ['use-plan-mode', 'use-opusplan'],\r\n run(ctx) {\r\n const recent = ctx.events.slice(0, 10)\r\n if (recent.length < 10) return null\r\n const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name))\r\n if (edits > 0) return null\r\n return {\r\n rule_id: 'detect-long-reasoning-no-code',\r\n tip_ids: ['use-plan-mode', 'use-opusplan'],\r\n severity: 'info',\r\n evidence: '10 eventos recientes sin ediciones de codigo',\r\n estimation_method: 'measured_exact',\r\n }\r\n },\r\n },\r\n\r\n // 3. detect-repeated-searches\r\n {\r\n id: 'detect-repeated-searches',\r\n tip_ids: ['use-agent-explore'],\r\n run(ctx) {\r\n const window = ctx.events.slice(0, 20)\r\n const searches = countMatching(window, (e) => e.tool_name === 'Grep' || e.tool_name === 'Glob')\r\n if (searches < 3) return null\r\n return {\r\n rule_id: 'detect-repeated-searches',\r\n tip_ids: ['use-agent-explore'],\r\n severity: 'info',\r\n evidence: `${searches} busquedas Grep/Glob en los ultimos 20 eventos`,\r\n estimation_method: 'measured_exact',\r\n }\r\n },\r\n },\r\n\r\n // 4. detect-huge-file-reads\r\n {\r\n id: 'detect-huge-file-reads',\r\n tip_ids: ['install-serena'],\r\n run(ctx) {\r\n const huge = ctx.events.find((e) => e.tool_name === 'Read' && e.tokens_estimated > 50_000)\r\n if (!huge) return null\r\n return {\r\n rule_id: 'detect-huge-file-reads',\r\n tip_ids: ['install-serena'],\r\n severity: 'warn',\r\n evidence: `Read consumio ${huge.tokens_estimated} tokens (umbral 50k)`,\r\n estimation_method: 'measured_exact',\r\n }\r\n },\r\n },\r\n\r\n // 5. detect-many-bash-commands\r\n {\r\n id: 'detect-many-bash-commands',\r\n tip_ids: ['install-rtk'],\r\n run(ctx) {\r\n const window = ctx.events.slice(0, 100)\r\n const bash = countMatching(window, (e) => e.tool_name === 'Bash')\r\n if (bash <= 10) return null\r\n return {\r\n rule_id: 'detect-many-bash-commands',\r\n tip_ids: ['install-rtk'],\r\n severity: 'info',\r\n evidence: `${bash} comandos Bash en los ultimos ${window.length} eventos`,\r\n estimation_method: 'measured_exact',\r\n }\r\n },\r\n },\r\n\r\n // 6. detect-clear-opportunity (was #7 — detect-unused-mcp-servers stub removed)\r\n {\r\n id: 'detect-clear-opportunity',\r\n tip_ids: ['use-clear-rename-resume'],\r\n run(ctx) {\r\n if (ctx.events.length < 40) return null\r\n const recentTools = new Set(ctx.events.slice(0, 20).map((e) => e.tool_name))\r\n const priorTools = new Set(ctx.events.slice(20, 40).map((e) => e.tool_name))\r\n if (recentTools.size === 0) return null\r\n let overlap = 0\r\n for (const t of recentTools) if (priorTools.has(t)) overlap++\r\n const ratio = overlap / recentTools.size\r\n if (ratio >= 0.3) return null\r\n return {\r\n rule_id: 'detect-clear-opportunity',\r\n tip_ids: ['use-clear-rename-resume'],\r\n severity: 'info',\r\n evidence: `Solapamiento de herramientas ${(ratio * 100).toFixed(0)}% — posible pivote de tema`,\r\n estimation_method: 'measured_exact',\r\n }\r\n },\r\n },\r\n\r\n // 8. detect-opus-for-simple-task\r\n {\r\n id: 'detect-opus-for-simple-task',\r\n tip_ids: ['default-to-sonnet', 'use-haiku-for-simple'],\r\n run(ctx) {\r\n if (!ctx.active_model || !/opus/i.test(ctx.active_model)) return null\r\n const recent = ctx.events.slice(0, 20)\r\n if (recent.length < 6) return null\r\n const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name))\r\n const bash = countMatching(recent, (e) => e.tool_name === 'Bash')\r\n // Opus es correcto para planificar/preguntar — solo avisar cuando está ejecutando código\r\n if (edits + bash < 6) return null\r\n return {\r\n rule_id: 'detect-opus-for-simple-task',\r\n tip_ids: ['default-to-sonnet', 'use-haiku-for-simple'],\r\n severity: 'info',\r\n evidence: `Opus ejecutando trabajo mecanico: ${edits} edits + ${bash} Bash en ultimos 20 eventos. Sonnet haria lo mismo un 80% mas barato.`,\r\n estimation_method: 'measured_exact',\r\n }\r\n },\r\n },\r\n\r\n // 9. detect-claudemd-bloat (stub — requires filesystem stat at runtime)\r\n {\r\n id: 'detect-claudemd-bloat',\r\n tip_ids: ['migrate-claudemd-to-skills'],\r\n run() {\r\n return null\r\n },\r\n },\r\n\r\n // 10. detect-post-milestone-opportunity\r\n {\r\n id: 'detect-post-milestone-opportunity',\r\n tip_ids: ['use-compact-long-session'],\r\n run(ctx) {\r\n const recent = ctx.events.slice(0, 20)\r\n const edits = countMatching(recent, (e) => e.tool_name === 'Edit' || e.tool_name === 'Write')\r\n const hasBash = countMatching(recent, (e) => e.tool_name === 'Bash') > 0\r\n if (edits < 5 || !hasBash) return null\r\n if (ctx.session_token_total === null) return null\r\n const percent = ctx.session_token_total / ctx.session_token_limit\r\n if (percent < 0.4) return null\r\n return {\r\n rule_id: 'detect-post-milestone-opportunity',\r\n tip_ids: ['use-compact-long-session'],\r\n severity: 'info',\r\n evidence: `${edits} ediciones + Bash reciente + contexto ${(percent * 100).toFixed(0)}%`,\r\n estimation_method: ctx.session_token_method,\r\n }\r\n },\r\n },\r\n\r\n // 11. detect-skill-trigger-ignored (stub — requires skill registry)\r\n {\r\n id: 'detect-skill-trigger-ignored',\r\n tip_ids: ['use-skill-trigger'],\r\n run() {\r\n return null\r\n },\r\n },\r\n\r\n // 12. detect-serena-read-cascade\r\n // Fires when the agent makes ≥5 find_symbol calls without a get_symbols_overview\r\n // in the same window — suggests starting with an overview first.\r\n {\r\n id: 'detect-serena-read-cascade',\r\n tip_ids: ['use-serena-overview-first'],\r\n run(ctx) {\r\n const window = ctx.events.slice(0, 15)\r\n const findSymbolCount = countMatching(\r\n window,\r\n (e) => e.tool_name === 'mcp__serena__find_symbol',\r\n )\r\n if (findSymbolCount < 5) return null\r\n const hasOverview = window.some(\r\n (e) => e.tool_name === 'mcp__serena__get_symbols_overview',\r\n )\r\n if (hasOverview) return null\r\n return {\r\n rule_id: 'detect-serena-read-cascade',\r\n tip_ids: ['use-serena-overview-first'],\r\n severity: 'info' as DetectionSeverity,\r\n evidence: `${findSymbolCount} llamadas find_symbol sin get_symbols_overview en los ultimos 15 eventos.`,\r\n estimation_method: ctx.session_token_method,\r\n }\r\n },\r\n },\r\n\r\n // 13. detect-read-over-serena\r\n {\r\n id: 'detect-read-over-serena',\r\n tip_ids: ['prefer-serena-reads'],\r\n run(ctx) {\r\n const window = ctx.events.slice(0, 30)\r\n const largeReads = window.filter(\r\n (e) => e.tool_name === 'Read' && e.tokens_estimated > 2_000,\r\n )\r\n if (largeReads.length < 3) return null\r\n const totalTokens = largeReads.reduce((sum, e) => sum + e.tokens_estimated, 0)\r\n const estimatedSaving = Math.round(totalTokens * 0.7)\r\n const severity: DetectionSeverity = largeReads.length >= 6 ? 'warn' : 'info'\r\n return {\r\n rule_id: 'detect-read-over-serena',\r\n tip_ids: ['prefer-serena-reads'],\r\n severity,\r\n evidence: `${largeReads.length} lecturas Read >2k tokens (total: ${totalTokens}). Serena ahorraria ~${estimatedSaving} tokens (~70%).`,\r\n estimation_method: ctx.session_token_method,\r\n }\r\n },\r\n },\r\n]\r\n","// Rules orchestrator — Phase 4.44\r\n// Runs all detection rules, dedupes by (rule_id, tip_id), sorts by severity desc.\r\n\r\nimport type { DetectionHit, EventContext } from '../lib/types.js'\r\nimport { DETECTION_RULES } from './rules.js'\r\n\r\nconst SEVERITY_ORDER: Record<string, number> = { critical: 0, warn: 1, info: 2 }\r\n\r\nexport function runRules(ctx: EventContext): DetectionHit[] {\r\n const hits: DetectionHit[] = []\r\n for (const rule of DETECTION_RULES) {\r\n try {\r\n const hit = rule.run(ctx)\r\n if (hit) hits.push(hit)\r\n } catch {\r\n // swallow — rules must never crash the caller\r\n }\r\n }\r\n // Dedupe by rule_id\r\n const seen = new Set<string>()\r\n const unique: DetectionHit[] = []\r\n for (const h of hits) {\r\n if (seen.has(h.rule_id)) continue\r\n seen.add(h.rule_id)\r\n unique.push(h)\r\n }\r\n unique.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99))\r\n return unique\r\n}\r\n","// Context size meter with 3-source fallback — Phase 4.42\r\n// (1) transcript JSONL → (2) xray HTTP → (3) cumulative DB estimate\r\n\r\nimport fs from 'node:fs'\r\nimport type Database from 'better-sqlite3'\r\nimport type { ContextMeasurement, EstimationMethod } from '../lib/types.js'\r\nimport { resolveTranscriptPath } from '../lib/paths.js'\r\nimport { buildQueries } from '../db/queries.js'\r\nimport { getSessionTokens } from '../services/xray-client.js'\r\n\r\ntype DB = Database.Database\r\n\r\nconst DEFAULT_LIMIT = 200_000\r\nconst OPUS_1M_LIMIT = 1_000_000\r\nconst BASELINE_TOKENS = 15_000\r\n\r\nexport interface ContextMeterOptions {\r\n projectDir?: string\r\n db?: DB\r\n fetchImpl?: typeof fetch\r\n activeModel?: string\r\n}\r\n\r\nexport async function measureContextSize(\r\n sessionId: string,\r\n opts: ContextMeterOptions = {},\r\n): Promise<ContextMeasurement> {\r\n // Strategy 1: transcript JSONL (measured_exact)\r\n if (opts.projectDir) {\r\n const transcript = readTranscript(opts.projectDir, sessionId)\r\n if (transcript) return transcript\r\n }\r\n\r\n // Strategy 2: xray HTTP (measured_exact)\r\n const xrayResult = await tryXray(sessionId, opts.fetchImpl)\r\n if (xrayResult) return xrayResult\r\n\r\n // Strategy 3: cumulative estimate from our DB (estimated_cumulative)\r\n const limit = resolveLimit(opts.activeModel)\r\n if (opts.db) {\r\n return cumulativeEstimate(opts.db, sessionId, limit)\r\n }\r\n\r\n return { tokens: 0, limit, percent: 0, estimation_method: 'unknown' }\r\n}\r\n\r\nfunction readTranscript(projectDir: string, sessionId: string): ContextMeasurement | null {\r\n try {\r\n const p = resolveTranscriptPath(projectDir, sessionId)\r\n if (!fs.existsSync(p)) return null\r\n const content = fs.readFileSync(p, 'utf8')\r\n const lines = content.split('\\n').filter((l) => l.trim().length > 0)\r\n let totalTokens = 0\r\n let limit = DEFAULT_LIMIT\r\n for (const line of lines) {\r\n try {\r\n const turn = JSON.parse(line) as {\r\n usage?: {\r\n input_tokens?: number\r\n output_tokens?: number\r\n cache_read_input_tokens?: number\r\n }\r\n model?: string\r\n }\r\n if (turn.usage) {\r\n totalTokens +=\r\n (turn.usage.input_tokens ?? 0) +\r\n (turn.usage.output_tokens ?? 0) +\r\n (turn.usage.cache_read_input_tokens ?? 0)\r\n }\r\n if (turn.model && /1m/i.test(turn.model)) limit = OPUS_1M_LIMIT\r\n } catch {\r\n // skip unparseable line\r\n }\r\n }\r\n return {\r\n tokens: totalTokens,\r\n limit,\r\n percent: limit > 0 ? totalTokens / limit : 0,\r\n estimation_method: 'measured_exact' as EstimationMethod,\r\n }\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\nasync function tryXray(\r\n sessionId: string,\r\n fetchImpl?: typeof fetch,\r\n): Promise<ContextMeasurement | null> {\r\n const opts: Parameters<typeof getSessionTokens>[1] = {}\r\n if (fetchImpl !== undefined) opts.fetchImpl = fetchImpl\r\n return getSessionTokens(sessionId, opts)\r\n}\r\n\r\nfunction resolveLimit(activeModel?: string): number {\r\n if (activeModel && /1m|opus/i.test(activeModel)) return OPUS_1M_LIMIT\r\n return DEFAULT_LIMIT\r\n}\r\n\r\nfunction cumulativeEstimate(\r\n db: DB,\r\n sessionId: string,\r\n limit: number = DEFAULT_LIMIT,\r\n): ContextMeasurement {\r\n const queries = buildQueries(db)\r\n const sessionTokens = queries.sumTokensBySession(sessionId)\r\n const total = sessionTokens + BASELINE_TOKENS\r\n return {\r\n tokens: total,\r\n limit,\r\n percent: total / limit,\r\n estimation_method: 'estimated_cumulative',\r\n }\r\n}\r\n\r\n/**\r\n * Synchronous DB-only context measurement for hot paths (PostToolUse).\r\n * Skips transcript + xray strategies to stay under the 5ms budget. The\r\n * estimation_method returned ('estimated_cumulative') is surfaced verbatim\r\n * in tips so the agent knows this is a fast approximation.\r\n */\r\nexport function measureContextSizeFromDbSync(\r\n db: DB,\r\n sessionId: string,\r\n activeModel?: string,\r\n): ContextMeasurement {\r\n const limit = resolveLimit(activeModel)\r\n return cumulativeEstimate(db, sessionId, limit)\r\n}\r\n","// xray client — Phase 5.1\r\n// Fire-and-forget POST for tool events + GET for session tokens (used by coach context meter).\r\n// Silent on all failures: no stderr, no throw. Timeout 500ms for post, 300ms for get.\r\n\r\nimport type { ContextMeasurement } from '../lib/types.js'\r\nimport { resolveXrayUrl } from '../cli/config.js'\r\n\r\nconst POST_TIMEOUT_MS = 500\r\nconst GET_TIMEOUT_MS = 300\r\nconst DEFAULT_LIMIT = 200_000\r\n\r\ndeclare const __PKG_VERSION__: string\r\nconst VERSION = typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : '0.1.0'\r\n\r\nexport interface PostToXrayOptions {\r\n xrayUrl?: string\r\n fetchImpl?: typeof fetch\r\n timeoutMs?: number\r\n}\r\n\r\n/**\r\n * Fire-and-forget POST of a tool event to an xray server.\r\n * Returns true if the request was attempted, false if skipped (no URL).\r\n * Any network/parse error is swallowed silently.\r\n */\r\nexport async function postToXray(\r\n event: Record<string, unknown>,\r\n opts: PostToXrayOptions = {},\r\n): Promise<boolean> {\r\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\r\n if (!xrayUrl) return false\r\n const fetchFn = opts.fetchImpl ?? fetch\r\n const controller = new AbortController()\r\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? POST_TIMEOUT_MS)\r\n try {\r\n await fetchFn(`${xrayUrl}/hooks/token-optimizer`, {\r\n method: 'POST',\r\n headers: { 'content-type': 'application/json' },\r\n body: JSON.stringify({\r\n source: 'token-optimizer-mcp',\r\n version: VERSION,\r\n event,\r\n }),\r\n signal: controller.signal,\r\n })\r\n return true\r\n } catch {\r\n return false\r\n } finally {\r\n clearTimeout(timer)\r\n }\r\n}\r\n\r\nconst SUMMARY_TIMEOUT_MS = 2000\r\n\r\n/**\r\n * Fire-and-forget POST of session summary to xray.\r\n * Only called once per session (not in hot path), so allows longer timeout.\r\n */\r\nexport async function postSummaryToXray(\r\n summary: Record<string, unknown>,\r\n opts: PostToXrayOptions = {},\r\n): Promise<boolean> {\r\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\r\n if (!xrayUrl) return false\r\n const fetchFn = opts.fetchImpl ?? fetch\r\n const controller = new AbortController()\r\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUMMARY_TIMEOUT_MS)\r\n try {\r\n await fetchFn(`${xrayUrl}/hooks/token-optimizer/summary`, {\r\n method: 'POST',\r\n headers: { 'content-type': 'application/json' },\r\n body: JSON.stringify({\r\n source: 'token-optimizer-mcp',\r\n version: VERSION,\r\n summary,\r\n }),\r\n signal: controller.signal,\r\n })\r\n return true\r\n } catch {\r\n return false\r\n } finally {\r\n clearTimeout(timer)\r\n }\r\n}\r\n\r\nexport interface GetSessionTokensOptions {\r\n xrayUrl?: string\r\n fetchImpl?: typeof fetch\r\n timeoutMs?: number\r\n}\r\n\r\n/**\r\n * Read real token counts from xray for a given session.\r\n * Returns null on any failure or when XRAY_URL is unset.\r\n */\r\nexport async function getSessionTokens(\r\n sessionId: string,\r\n opts: GetSessionTokensOptions = {},\r\n): Promise<ContextMeasurement | null> {\r\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\r\n if (!xrayUrl) return null\r\n const fetchFn = opts.fetchImpl ?? fetch\r\n const controller = new AbortController()\r\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? GET_TIMEOUT_MS)\r\n try {\r\n const res = await fetchFn(\r\n `${xrayUrl}/sessions/${encodeURIComponent(sessionId)}/tokens`,\r\n { signal: controller.signal },\r\n )\r\n if (!res.ok) return null\r\n const data = (await res.json()) as { tokens?: number; limit?: number }\r\n const tokens = data.tokens ?? 0\r\n const limit = data.limit ?? DEFAULT_LIMIT\r\n return {\r\n tokens,\r\n limit,\r\n percent: limit > 0 ? tokens / limit : 0,\r\n estimation_method: 'measured_exact',\r\n }\r\n } catch {\r\n return null\r\n } finally {\r\n clearTimeout(timer)\r\n }\r\n}\r\n","// Surfacing dedupe + log writer — Phase 4.45\r\n// Writes to coach_surface_log with session+rule+tip+via+severity.\r\n\r\nimport type Database from 'better-sqlite3'\r\nimport type { DetectionHit } from '../lib/types.js'\r\n\r\ntype DB = Database.Database\r\n\r\nexport type SurfacedVia = 'sessionstart' | 'posttooluse' | 'mcp' | 'cli'\r\n\r\n/**\r\n * Returns true if this (session, rule, tip) was surfaced within the last\r\n * `windowSeconds` seconds. Used by the PostToolUse throttle path.\r\n */\r\nexport function checkDedupe(\r\n db: DB,\r\n sessionId: string,\r\n ruleId: string,\r\n tipId: string,\r\n windowSeconds: number,\r\n): boolean {\r\n const row = db\r\n .prepare(\r\n `SELECT 1 FROM coach_surface_log\r\n WHERE session_id = ? AND rule_id = ? AND tip_id = ?\r\n AND created_at > datetime('now', ?)\r\n LIMIT 1`,\r\n )\r\n .get(sessionId, ruleId, tipId, `-${windowSeconds} seconds`) as unknown\r\n return row !== undefined && row !== null\r\n}\r\n\r\nexport function logSurface(\r\n db: DB,\r\n sessionId: string,\r\n hit: DetectionHit,\r\n via: SurfacedVia,\r\n): void {\r\n // Ensure session exists so FK succeeds\r\n db.prepare(`INSERT OR IGNORE INTO sessions (id) VALUES (?)`).run(sessionId)\r\n const stmt = db.prepare(\r\n `INSERT INTO coach_surface_log (session_id, rule_id, tip_id, surfaced_via, severity)\r\n VALUES (?, ?, ?, ?, ?)`,\r\n )\r\n for (const tipId of hit.tip_ids) {\r\n stmt.run(sessionId, hit.rule_id, tipId, via, hit.severity)\r\n }\r\n}\r\n\r\n/**\r\n * Log the list of hits under dedupe. A hit is considered \"fresh\" (to be\r\n * logged and returned to the caller) if at least one of its tip_ids was NOT\r\n * surfaced within `windowSeconds`. Returns only the surfaced hits.\r\n */\r\nexport function surfaceWithDedupe(\r\n db: DB,\r\n sessionId: string,\r\n hits: DetectionHit[],\r\n via: SurfacedVia,\r\n windowSeconds: number,\r\n): DetectionHit[] {\r\n const surfaced: DetectionHit[] = []\r\n for (const hit of hits) {\r\n const anyFresh = hit.tip_ids.some(\r\n (tipId) => !checkDedupe(db, sessionId, hit.rule_id, tipId, windowSeconds),\r\n )\r\n if (anyFresh) {\r\n logSurface(db, sessionId, hit, via)\r\n surfaced.push(hit)\r\n }\r\n }\r\n return surfaced\r\n}\r\n\r\n/**\r\n * Read all coach tips surfaced during a session, grouped by rule.\r\n * Used by session-summary-builder for xray integration.\r\n */\r\nexport function getCoachSurfaceLog(\r\n db: DB,\r\n sessionId: string,\r\n): Array<{ rule_id: string; tip_ids: string[]; severity: string }> {\r\n const rows = db\r\n .prepare(\r\n `SELECT rule_id, tip_id, severity\r\n FROM coach_surface_log\r\n WHERE session_id = ?\r\n ORDER BY created_at`,\r\n )\r\n .all(sessionId) as Array<{ rule_id: string; tip_id: string; severity: string }>\r\n\r\n // Group tip_ids by rule_id\r\n const map = new Map<string, { tip_ids: string[]; severity: string }>()\r\n for (const row of rows) {\r\n if (!map.has(row.rule_id)) {\r\n map.set(row.rule_id, { tip_ids: [], severity: row.severity })\r\n }\r\n const entry = map.get(row.rule_id)!\r\n if (!entry.tip_ids.includes(row.tip_id)) {\r\n entry.tip_ids.push(row.tip_id)\r\n }\r\n }\r\n\r\n return Array.from(map.entries()).map(([rule_id, v]) => ({\r\n rule_id,\r\n tip_ids: v.tip_ids,\r\n severity: v.severity,\r\n }))\r\n}\r\n\r\nexport function clearSurfaceLog(db: DB, sessionId?: string): number {\r\n if (sessionId) {\r\n const info = db.prepare(`DELETE FROM coach_surface_log WHERE session_id = ?`).run(sessionId)\r\n return info.changes as number\r\n }\r\n const info = db.prepare(`DELETE FROM coach_surface_log`).run()\r\n return info.changes as number\r\n}\r\n"],"mappings":"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","names":["DEFAULT_LIMIT","info"]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveGlobalDir | ||
| } from "./chunk-U3OXZD52.js"; | ||
| // src/cli/config.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var DEFAULT_CONFIG = { | ||
| xray_url: null, | ||
| shadow_measurement: { serena: false }, | ||
| rtk_integration: { rtk_db_path: null }, | ||
| coach: { | ||
| enabled: true, | ||
| auto_surface: true, | ||
| posttooluse_throttle: 20, | ||
| sessionstart_tips_max: 3, | ||
| context_thresholds: { | ||
| info: 0.5, | ||
| warn: 0.75, | ||
| critical: 0.9 | ||
| }, | ||
| dedupe_window_seconds: 60, | ||
| stale_tip_days: 90 | ||
| } | ||
| }; | ||
| function getConfigPath(home) { | ||
| if (home !== void 0) { | ||
| return path.join(home, ".token-optimizer", "config.json"); | ||
| } | ||
| return path.join(resolveGlobalDir(), "config.json"); | ||
| } | ||
| function deepMerge(target, source) { | ||
| if (source === null || typeof source !== "object") return target; | ||
| if (typeof target !== "object" || target === null) return target; | ||
| const result = { ...target }; | ||
| const src = source; | ||
| for (const key of Object.keys(src)) { | ||
| const s = src[key]; | ||
| const t = result[key]; | ||
| if (s !== null && typeof s === "object" && !Array.isArray(s) && t !== null && typeof t === "object" && !Array.isArray(t)) { | ||
| result[key] = deepMerge(t, s); | ||
| } else { | ||
| result[key] = s; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function loadConfig(home) { | ||
| const p = getConfigPath(home); | ||
| try { | ||
| if (!fs.existsSync(p)) return DEFAULT_CONFIG; | ||
| const raw = fs.readFileSync(p, "utf8"); | ||
| const parsed = JSON.parse(raw); | ||
| return deepMerge(DEFAULT_CONFIG, parsed); | ||
| } catch { | ||
| return DEFAULT_CONFIG; | ||
| } | ||
| } | ||
| function saveConfig(config, home) { | ||
| const p = getConfigPath(home); | ||
| const dir = path.dirname(p); | ||
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(config, null, 2)); | ||
| } | ||
| function dotGet(obj, dotted) { | ||
| const parts = dotted.split("."); | ||
| let cur = obj; | ||
| for (const p of parts) { | ||
| if (typeof cur !== "object" || cur === null) return void 0; | ||
| cur = cur[p]; | ||
| } | ||
| return cur; | ||
| } | ||
| function dotSet(obj, dotted, value) { | ||
| const parts = dotted.split("."); | ||
| let cur = obj; | ||
| for (let i = 0; i < parts.length - 1; i++) { | ||
| const key = parts[i]; | ||
| const next = cur[key]; | ||
| if (typeof next !== "object" || next === null || Array.isArray(next)) { | ||
| cur[key] = {}; | ||
| } | ||
| cur = cur[key]; | ||
| } | ||
| cur[parts[parts.length - 1]] = value; | ||
| } | ||
| function coerceValue(raw) { | ||
| if (raw === "true") return true; | ||
| if (raw === "false") return false; | ||
| if (raw === "null") return null; | ||
| if (raw.trim() !== "" && !Number.isNaN(Number(raw))) return Number(raw); | ||
| return raw; | ||
| } | ||
| function runConfigCommand(args, opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const sub = args[0]; | ||
| if (sub === "get") { | ||
| const key = args[1]; | ||
| const cfg = loadConfig(opts.home); | ||
| if (!key) { | ||
| print(JSON.stringify(cfg, null, 2)); | ||
| return 0; | ||
| } | ||
| const value = dotGet(cfg, key); | ||
| print(value === void 0 ? "(undefined)" : JSON.stringify(value)); | ||
| return 0; | ||
| } | ||
| if (sub === "set") { | ||
| const key = args[1]; | ||
| const rawValue = args[2]; | ||
| if (!key || rawValue === void 0) { | ||
| print("Uso: token-optimizer-mcp config set <key> <value>"); | ||
| return 1; | ||
| } | ||
| const cfg = loadConfig(opts.home); | ||
| dotSet(cfg, key, coerceValue(rawValue)); | ||
| saveConfig(cfg, opts.home); | ||
| print(`Guardado: ${key} = ${rawValue}`); | ||
| return 0; | ||
| } | ||
| print("Uso: token-optimizer-mcp config <get|set> [key] [value]"); | ||
| return 1; | ||
| } | ||
| function resolveXrayUrl(home) { | ||
| const cfg = loadConfig(home); | ||
| return cfg.xray_url ?? process.env.XRAY_URL ?? null; | ||
| } | ||
| export { | ||
| DEFAULT_CONFIG, | ||
| getConfigPath, | ||
| loadConfig, | ||
| saveConfig, | ||
| runConfigCommand, | ||
| resolveXrayUrl | ||
| }; | ||
| //# sourceMappingURL=chunk-METYJF7E.js.map |
| {"version":3,"sources":["../src/cli/config.ts"],"sourcesContent":["// Config CLI + loader — Phase 4.8\r\n// Reads/writes ~/.token-optimizer/config.json. Supports dotted key get/set.\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport { resolveGlobalDir } from '../lib/paths.js'\r\n\r\nexport interface CoachConfig {\r\n enabled: boolean\r\n auto_surface: boolean\r\n posttooluse_throttle: number\r\n sessionstart_tips_max: number\r\n context_thresholds: {\r\n info: number\r\n warn: number\r\n critical: number\r\n }\r\n dedupe_window_seconds: number\r\n stale_tip_days: number\r\n}\r\n\r\nexport interface Config {\r\n xray_url: string | null\r\n shadow_measurement: {\r\n serena: boolean\r\n }\r\n rtk_integration: {\r\n rtk_db_path: string | null\r\n }\r\n coach: CoachConfig\r\n}\r\n\r\nexport const DEFAULT_CONFIG: Config = {\r\n xray_url: null,\r\n shadow_measurement: { serena: false },\r\n rtk_integration: { rtk_db_path: null },\r\n coach: {\r\n enabled: true,\r\n auto_surface: true,\r\n posttooluse_throttle: 20,\r\n sessionstart_tips_max: 3,\r\n context_thresholds: {\r\n info: 0.5,\r\n warn: 0.75,\r\n critical: 0.9,\r\n },\r\n dedupe_window_seconds: 60,\r\n stale_tip_days: 90,\r\n },\r\n}\r\n\r\nexport function getConfigPath(home?: string): string {\r\n // Respect explicit `home` override first (tests, CLI --home). Otherwise use\r\n // resolveGlobalDir() which honours TOKEN_OPTIMIZER_HOME env var, keeping the\r\n // config path consistent with the analytics.db path in every caller.\r\n if (home !== undefined) {\r\n return path.join(home, '.token-optimizer', 'config.json')\r\n }\r\n return path.join(resolveGlobalDir(), 'config.json')\r\n}\r\n\r\nfunction deepMerge<T>(target: T, source: unknown): T {\r\n if (source === null || typeof source !== 'object') return target\r\n if (typeof target !== 'object' || target === null) return target\r\n const result: Record<string, unknown> = { ...(target as Record<string, unknown>) }\r\n const src = source as Record<string, unknown>\r\n for (const key of Object.keys(src)) {\r\n const s = src[key]\r\n const t = result[key]\r\n if (\r\n s !== null &&\r\n typeof s === 'object' &&\r\n !Array.isArray(s) &&\r\n t !== null &&\r\n typeof t === 'object' &&\r\n !Array.isArray(t)\r\n ) {\r\n result[key] = deepMerge(t, s)\r\n } else {\r\n result[key] = s\r\n }\r\n }\r\n return result as T\r\n}\r\n\r\nexport function loadConfig(home?: string): Config {\r\n const p = getConfigPath(home)\r\n try {\r\n if (!fs.existsSync(p)) return DEFAULT_CONFIG\r\n const raw = fs.readFileSync(p, 'utf8')\r\n const parsed = JSON.parse(raw) as unknown\r\n return deepMerge(DEFAULT_CONFIG, parsed)\r\n } catch {\r\n return DEFAULT_CONFIG\r\n }\r\n}\r\n\r\nexport function saveConfig(config: Config, home?: string): void {\r\n const p = getConfigPath(home)\r\n const dir = path.dirname(p)\r\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\r\n fs.writeFileSync(p, JSON.stringify(config, null, 2))\r\n}\r\n\r\nfunction dotGet(obj: unknown, dotted: string): unknown {\r\n const parts = dotted.split('.')\r\n let cur: unknown = obj\r\n for (const p of parts) {\r\n if (typeof cur !== 'object' || cur === null) return undefined\r\n cur = (cur as Record<string, unknown>)[p]\r\n }\r\n return cur\r\n}\r\n\r\nfunction dotSet(obj: Record<string, unknown>, dotted: string, value: unknown): void {\r\n const parts = dotted.split('.')\r\n let cur: Record<string, unknown> = obj\r\n for (let i = 0; i < parts.length - 1; i++) {\r\n const key = parts[i]\r\n const next = cur[key]\r\n if (typeof next !== 'object' || next === null || Array.isArray(next)) {\r\n cur[key] = {}\r\n }\r\n cur = cur[key] as Record<string, unknown>\r\n }\r\n cur[parts[parts.length - 1]] = value\r\n}\r\n\r\nfunction coerceValue(raw: string): unknown {\r\n if (raw === 'true') return true\r\n if (raw === 'false') return false\r\n if (raw === 'null') return null\r\n if (raw.trim() !== '' && !Number.isNaN(Number(raw))) return Number(raw)\r\n return raw\r\n}\r\n\r\nexport interface ConfigCliOptions {\r\n home?: string\r\n print?: (msg: string) => void\r\n}\r\n\r\nexport function runConfigCommand(args: string[], opts: ConfigCliOptions = {}): number {\r\n const print = opts.print ?? ((m: string) => console.error(m))\r\n const sub = args[0]\r\n if (sub === 'get') {\r\n const key = args[1]\r\n const cfg = loadConfig(opts.home)\r\n if (!key) {\r\n print(JSON.stringify(cfg, null, 2))\r\n return 0\r\n }\r\n const value = dotGet(cfg, key)\r\n print(value === undefined ? '(undefined)' : JSON.stringify(value))\r\n return 0\r\n }\r\n if (sub === 'set') {\r\n const key = args[1]\r\n const rawValue = args[2]\r\n if (!key || rawValue === undefined) {\r\n print('Uso: token-optimizer-mcp config set <key> <value>')\r\n return 1\r\n }\r\n const cfg = loadConfig(opts.home) as unknown as Record<string, unknown>\r\n dotSet(cfg, key, coerceValue(rawValue))\r\n saveConfig(cfg as unknown as Config, opts.home)\r\n print(`Guardado: ${key} = ${rawValue}`)\r\n return 0\r\n }\r\n print('Uso: token-optimizer-mcp config <get|set> [key] [value]')\r\n return 1\r\n}\r\n\r\n/**\r\n * Resolve xray URL: config.json xray_url > XRAY_URL env var > null\r\n */\r\nexport function resolveXrayUrl(home?: string): string | null {\r\n const cfg = loadConfig(home)\r\n return cfg.xray_url ?? process.env.XRAY_URL ?? null\r\n}\r\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| // src/lib/storage.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var GITIGNORE_ENTRIES = [".serena/"]; | ||
| function ensureGitignore(projectDir) { | ||
| const gitDir = path.join(projectDir, ".git"); | ||
| if (!fs.existsSync(gitDir)) return; | ||
| const gitignorePath = path.join(projectDir, ".gitignore"); | ||
| let current = ""; | ||
| if (fs.existsSync(gitignorePath)) { | ||
| current = fs.readFileSync(gitignorePath, "utf8"); | ||
| } | ||
| const lines = current.split(/\r?\n/).map((l) => l.trim()); | ||
| const missing = GITIGNORE_ENTRIES.filter( | ||
| (entry) => fs.existsSync(path.join(projectDir, entry)) && !lines.includes(entry) | ||
| ); | ||
| if (missing.length === 0) return; | ||
| const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : ""; | ||
| fs.appendFileSync(gitignorePath, `${prefix}${missing.join("\n")} | ||
| `); | ||
| } | ||
| export { | ||
| ensureGitignore | ||
| }; | ||
| //# sourceMappingURL=chunk-N754BAPB.js.map |
| {"version":3,"sources":["../src/lib/storage.ts"],"sourcesContent":["// Gitignore management — appends .serena/ to .gitignore idempotently.\r\n// The MCP no longer creates a per-project .token-optimizer/ dir: all storage\r\n// is global under ~/.token-optimizer/ (analytics.db, config.json).\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\n\r\nconst GITIGNORE_ENTRIES = ['.serena/']\r\n\r\nexport function ensureGitignore(projectDir: string): void {\r\n const gitDir = path.join(projectDir, '.git')\r\n if (!fs.existsSync(gitDir)) return\r\n\r\n const gitignorePath = path.join(projectDir, '.gitignore')\r\n let current = ''\r\n if (fs.existsSync(gitignorePath)) {\r\n current = fs.readFileSync(gitignorePath, 'utf8')\r\n }\r\n const lines = current.split(/\\r?\\n/).map((l) => l.trim())\r\n const missing = GITIGNORE_ENTRIES.filter(\r\n (entry) => fs.existsSync(path.join(projectDir, entry)) && !lines.includes(entry),\r\n )\r\n if (missing.length === 0) return\r\n const prefix = current.length > 0 && !current.endsWith('\\n') ? '\\n' : ''\r\n fs.appendFileSync(gitignorePath, `${prefix}${missing.join('\\n')}\\n`)\r\n}\r\n"],"mappings":";;;AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AAEjB,IAAM,oBAAoB,CAAC,UAAU;AAE9B,SAAS,gBAAgB,YAA0B;AACxD,QAAM,SAAS,KAAK,KAAK,YAAY,MAAM;AAC3C,MAAI,CAAC,GAAG,WAAW,MAAM,EAAG;AAE5B,QAAM,gBAAgB,KAAK,KAAK,YAAY,YAAY;AACxD,MAAI,UAAU;AACd,MAAI,GAAG,WAAW,aAAa,GAAG;AAChC,cAAU,GAAG,aAAa,eAAe,MAAM;AAAA,EACjD;AACA,QAAM,QAAQ,QAAQ,MAAM,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACxD,QAAM,UAAU,kBAAkB;AAAA,IAChC,CAAC,UAAU,GAAG,WAAW,KAAK,KAAK,YAAY,KAAK,CAAC,KAAK,CAAC,MAAM,SAAS,KAAK;AAAA,EACjF;AACA,MAAI,QAAQ,WAAW,EAAG;AAC1B,QAAM,SAAS,QAAQ,SAAS,KAAK,CAAC,QAAQ,SAAS,IAAI,IAAI,OAAO;AACtE,KAAG,eAAe,eAAe,GAAG,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAC;AAAA,CAAI;AACrE;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-ESRDZMZJ.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-U3OXZD52.js"; | ||
| // src/cli/prune-mcp.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/; | ||
| function extractServerFromToolName(toolName) { | ||
| const m = MCP_TOOL_RE.exec(toolName); | ||
| return m ? m[1] : null; | ||
| } | ||
| function settingsLocalPath(cwd) { | ||
| return path.join(cwd, ".claude", "settings.local.json"); | ||
| } | ||
| function readJsonSafe(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return {}; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| function writeJson(p, data) { | ||
| fs.mkdirSync(path.dirname(p), { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(data, null, 2)); | ||
| } | ||
| function generateFromHistory(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const days = opts.days ?? 14; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const serverCounts = {}; | ||
| if (fs.existsSync(dbPath)) { | ||
| const db = getDb(dbPath); | ||
| const rows = db.prepare( | ||
| `SELECT tool_name, COUNT(*) as count | ||
| FROM tool_calls | ||
| WHERE created_at >= ? AND tool_name LIKE 'mcp__%' | ||
| GROUP BY tool_name` | ||
| ).all(since); | ||
| for (const row of rows) { | ||
| const server = extractServerFromToolName(row.tool_name); | ||
| if (server) { | ||
| serverCounts[server] = (serverCounts[server] ?? 0) + row.count; | ||
| } | ||
| } | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd, home: opts.home }); | ||
| const registered = new Set(schema.mcp_servers); | ||
| const used = new Set(Object.keys(serverCounts)); | ||
| const inactive = [...registered].filter((s) => !used.has(s)); | ||
| return { | ||
| proposed_allowlist: [...used], | ||
| inactive_servers: inactive, | ||
| analysis_days: days, | ||
| total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0), | ||
| server_counts: serverCounts | ||
| }; | ||
| } | ||
| function timestampForBackup() { | ||
| return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); | ||
| } | ||
| function insertSnapshot(cwd, method, details) { | ||
| try { | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) return; | ||
| const db = getDb(dbPath); | ||
| db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run( | ||
| method, | ||
| JSON.stringify(details) | ||
| ); | ||
| } catch { | ||
| } | ||
| } | ||
| function applyAllowlist(allowlist, opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const source = opts.source ?? "cli"; | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| if (fs.existsSync(settingsPath)) { | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| } else { | ||
| fs.mkdirSync(path.dirname(backupPath), { recursive: true }); | ||
| fs.writeFileSync(backupPath, "{}"); | ||
| } | ||
| const current = readJsonSafe(settingsPath); | ||
| current.enabledMcpjsonServers = allowlist; | ||
| writeJson(settingsPath, current); | ||
| insertSnapshot(cwd, source === "mcp" ? "allowlist_generated_via_mcp" : "allowlist_generated", { | ||
| allowlist, | ||
| target: settingsPath, | ||
| backup: backupPath | ||
| }); | ||
| return { settings_path: settingsPath, backup_path: backupPath }; | ||
| } | ||
| function rollback(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const dir = path.dirname(settingsPath); | ||
| if (!fs.existsSync(dir)) return { restored: false, from: null }; | ||
| const backups = fs.readdirSync(dir).filter((f) => f.startsWith("settings.local.json.backup-")).sort(); | ||
| if (backups.length === 0) return { restored: false, from: null }; | ||
| const target = opts.to ? backups.find((b) => b.includes(opts.to)) : backups[backups.length - 1]; | ||
| if (!target) return { restored: false, from: null }; | ||
| const backupPath = path.join(dir, target); | ||
| fs.copyFileSync(backupPath, settingsPath); | ||
| insertSnapshot(cwd, "rollback", { from: backupPath, to: settingsPath }); | ||
| return { restored: true, from: backupPath }; | ||
| } | ||
| function clearAllowlist(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }; | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| const json = readJsonSafe(settingsPath); | ||
| delete json.enabledMcpjsonServers; | ||
| writeJson(settingsPath, json); | ||
| insertSnapshot(cwd, "allowlist_cleared", { backup: backupPath }); | ||
| return { cleared: true, backup_path: backupPath }; | ||
| } | ||
| function impact(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const db = getDb(dbPath); | ||
| const snapshot = db.prepare( | ||
| `SELECT created_at FROM optimization_snapshots | ||
| WHERE method LIKE 'allowlist_%' | ||
| ORDER BY created_at DESC LIMIT 1` | ||
| ).get(); | ||
| if (!snapshot) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const before = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const after = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const beforeAvg = before.avg; | ||
| const afterAvg = after.avg; | ||
| const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null; | ||
| const percent = beforeAvg !== null && beforeAvg > 0 && afterAvg !== null ? (afterAvg - beforeAvg) / beforeAvg : null; | ||
| return { | ||
| before_avg: beforeAvg, | ||
| after_avg: afterAvg, | ||
| delta, | ||
| percent, | ||
| snapshot_at: snapshot.created_at | ||
| }; | ||
| } | ||
| function runPruneMcp(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| if (args.includes("--generate-from-history")) { | ||
| const daysFlag = args.find((a) => a.startsWith("--days=")); | ||
| const days = daysFlag ? parseInt(daysFlag.split("=")[1], 10) : 14; | ||
| const result = generateFromHistory({ cwd, days }); | ||
| print(`Propuesta de allowlist (${days} dias de historial):`); | ||
| print(` Usados: ${result.proposed_allowlist.join(", ") || "(ninguno)"}`); | ||
| print(` Inactivos: ${result.inactive_servers.join(", ") || "(ninguno)"}`); | ||
| print(` Eventos MCP totales: ${result.total_mcp_events}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--apply")) { | ||
| const generated = generateFromHistory({ cwd }); | ||
| if (generated.proposed_allowlist.length === 0) { | ||
| print("No hay MCPs activos en el historial. Nada que aplicar."); | ||
| return 1; | ||
| } | ||
| const applied = applyAllowlist(generated.proposed_allowlist, { cwd }); | ||
| print(`Allowlist aplicado a ${applied.settings_path}`); | ||
| print(`Backup: ${applied.backup_path}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--rollback")) { | ||
| const toFlag = args.find((a) => a.startsWith("--to=")); | ||
| const to = toFlag ? toFlag.split("=")[1] : void 0; | ||
| const result = rollback({ cwd, to }); | ||
| if (result.restored) { | ||
| print(`Restaurado desde ${result.from}`); | ||
| return 0; | ||
| } | ||
| print("No hay backups disponibles."); | ||
| return 1; | ||
| } | ||
| if (args.includes("--clear")) { | ||
| const result = clearAllowlist({ cwd }); | ||
| print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : "Nada que eliminar"); | ||
| return 0; | ||
| } | ||
| if (args.includes("--impact")) { | ||
| const result = impact({ cwd }); | ||
| if (result.snapshot_at === null) { | ||
| print("No hay snapshots de allowlist todavia."); | ||
| return 0; | ||
| } | ||
| print(`Snapshot mas reciente: ${result.snapshot_at}`); | ||
| print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? "n/a"}`); | ||
| print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? "n/a"}`); | ||
| if (result.percent !== null) { | ||
| print(`Delta: ${(result.percent * 100).toFixed(1)}%`); | ||
| } | ||
| return 0; | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd }); | ||
| print(`MCPs registrados (${schema.mcp_servers.length}):`); | ||
| for (const s of schema.mcp_servers) { | ||
| print(` ${s}`); | ||
| } | ||
| print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`); | ||
| print(""); | ||
| print("Flags:"); | ||
| print(" --generate-from-history [--days N] Propone allowlist (read-only)"); | ||
| print(" --apply Aplica el allowlist generado"); | ||
| print(" --rollback [--to TIMESTAMP] Restaura el ultimo backup"); | ||
| print(" --clear Elimina allowlist actual"); | ||
| print(" --impact Compara antes/despues del ultimo snapshot"); | ||
| return 0; | ||
| } | ||
| export { | ||
| settingsLocalPath, | ||
| generateFromHistory, | ||
| applyAllowlist, | ||
| rollback, | ||
| clearAllowlist, | ||
| impact, | ||
| runPruneMcp | ||
| }; | ||
| //# sourceMappingURL=chunk-NWTRLY2M.js.map |
| {"version":3,"sources":["../src/cli/prune-mcp.ts"],"sourcesContent":["// prune-mcp CLI + service — Phase 4.16-4.22\r\n// Generate allowlist from history, apply/rollback/clear, compute impact.\r\n// Writes to .claude/settings.local.json (NOT settings.json).\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport { getDb } from '../db/connection.js'\r\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\r\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\r\n\r\nconst MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/\r\n\r\nfunction extractServerFromToolName(toolName: string): string | null {\r\n const m = MCP_TOOL_RE.exec(toolName)\r\n return m ? m[1] : null\r\n}\r\n\r\nexport function settingsLocalPath(cwd: string): string {\r\n return path.join(cwd, '.claude', 'settings.local.json')\r\n}\r\n\r\nfunction readJsonSafe(p: string): Record<string, unknown> {\r\n try {\r\n if (!fs.existsSync(p)) return {}\r\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\nfunction writeJson(p: string, data: Record<string, unknown>): void {\r\n fs.mkdirSync(path.dirname(p), { recursive: true })\r\n fs.writeFileSync(p, JSON.stringify(data, null, 2))\r\n}\r\n\r\nexport interface GeneratedAllowlist {\r\n proposed_allowlist: string[]\r\n inactive_servers: string[]\r\n analysis_days: number\r\n total_mcp_events: number\r\n server_counts: Record<string, number>\r\n}\r\n\r\nexport interface GenerateOptions {\r\n cwd?: string\r\n days?: number\r\n home?: string\r\n}\r\n\r\nexport function generateFromHistory(opts: GenerateOptions = {}): GeneratedAllowlist {\r\n const cwd = opts.cwd ?? process.cwd()\r\n const days = opts.days ?? 14\r\n const projectDir = resolveProjectDir(cwd)\r\n const dbPath = resolveAnalyticsDbPath(projectDir)\r\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\r\n\r\n const serverCounts: Record<string, number> = {}\r\n if (fs.existsSync(dbPath)) {\r\n const db = getDb(dbPath)\r\n const rows = db\r\n .prepare(\r\n `SELECT tool_name, COUNT(*) as count\r\n FROM tool_calls\r\n WHERE created_at >= ? AND tool_name LIKE 'mcp__%'\r\n GROUP BY tool_name`,\r\n )\r\n .all(since) as Array<{ tool_name: string; count: number }>\r\n for (const row of rows) {\r\n const server = extractServerFromToolName(row.tool_name)\r\n if (server) {\r\n serverCounts[server] = (serverCounts[server] ?? 0) + row.count\r\n }\r\n }\r\n }\r\n\r\n const schema = measureCurrentSchemaBytes({ cwd, home: opts.home })\r\n const registered = new Set(schema.mcp_servers)\r\n const used = new Set(Object.keys(serverCounts))\r\n const inactive = [...registered].filter((s) => !used.has(s))\r\n\r\n return {\r\n proposed_allowlist: [...used],\r\n inactive_servers: inactive,\r\n analysis_days: days,\r\n total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0),\r\n server_counts: serverCounts,\r\n }\r\n}\r\n\r\nexport interface ApplyOptions {\r\n cwd?: string\r\n source?: 'cli' | 'mcp'\r\n}\r\n\r\nexport interface ApplyResult {\r\n settings_path: string\r\n backup_path: string\r\n}\r\n\r\nfunction timestampForBackup(): string {\r\n return new Date().toISOString().replace(/[:.]/g, '-')\r\n}\r\n\r\nfunction insertSnapshot(cwd: string, method: string, details: Record<string, unknown>): void {\r\n try {\r\n const projectDir = resolveProjectDir(cwd)\r\n const dbPath = resolveAnalyticsDbPath(projectDir)\r\n if (!fs.existsSync(dbPath)) return\r\n const db = getDb(dbPath)\r\n db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run(\r\n method,\r\n JSON.stringify(details),\r\n )\r\n } catch {\r\n // swallow\r\n }\r\n}\r\n\r\nexport function applyAllowlist(allowlist: string[], opts: ApplyOptions = {}): ApplyResult {\r\n const cwd = opts.cwd ?? process.cwd()\r\n const source = opts.source ?? 'cli'\r\n const settingsPath = settingsLocalPath(cwd)\r\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\r\n\r\n if (fs.existsSync(settingsPath)) {\r\n fs.copyFileSync(settingsPath, backupPath)\r\n } else {\r\n fs.mkdirSync(path.dirname(backupPath), { recursive: true })\r\n fs.writeFileSync(backupPath, '{}')\r\n }\r\n\r\n const current = readJsonSafe(settingsPath)\r\n current.enabledMcpjsonServers = allowlist\r\n writeJson(settingsPath, current)\r\n\r\n insertSnapshot(cwd, source === 'mcp' ? 'allowlist_generated_via_mcp' : 'allowlist_generated', {\r\n allowlist,\r\n target: settingsPath,\r\n backup: backupPath,\r\n })\r\n\r\n return { settings_path: settingsPath, backup_path: backupPath }\r\n}\r\n\r\nexport interface RollbackOptions {\r\n cwd?: string\r\n to?: string\r\n}\r\n\r\nexport interface RollbackResult {\r\n restored: boolean\r\n from: string | null\r\n}\r\n\r\nexport function rollback(opts: RollbackOptions = {}): RollbackResult {\r\n const cwd = opts.cwd ?? process.cwd()\r\n const settingsPath = settingsLocalPath(cwd)\r\n const dir = path.dirname(settingsPath)\r\n if (!fs.existsSync(dir)) return { restored: false, from: null }\r\n\r\n const backups = fs\r\n .readdirSync(dir)\r\n .filter((f) => f.startsWith('settings.local.json.backup-'))\r\n .sort()\r\n if (backups.length === 0) return { restored: false, from: null }\r\n\r\n const target = opts.to ? backups.find((b) => b.includes(opts.to!)) : backups[backups.length - 1]\r\n if (!target) return { restored: false, from: null }\r\n\r\n const backupPath = path.join(dir, target)\r\n fs.copyFileSync(backupPath, settingsPath)\r\n\r\n insertSnapshot(cwd, 'rollback', { from: backupPath, to: settingsPath })\r\n return { restored: true, from: backupPath }\r\n}\r\n\r\nexport function clearAllowlist(opts: { cwd?: string } = {}): { cleared: boolean; backup_path: string | null } {\r\n const cwd = opts.cwd ?? process.cwd()\r\n const settingsPath = settingsLocalPath(cwd)\r\n if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }\r\n\r\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\r\n fs.copyFileSync(settingsPath, backupPath)\r\n\r\n const json = readJsonSafe(settingsPath)\r\n delete json.enabledMcpjsonServers\r\n writeJson(settingsPath, json)\r\n\r\n insertSnapshot(cwd, 'allowlist_cleared', { backup: backupPath })\r\n return { cleared: true, backup_path: backupPath }\r\n}\r\n\r\nexport interface ImpactResult {\r\n before_avg: number | null\r\n after_avg: number | null\r\n delta: number | null\r\n percent: number | null\r\n snapshot_at: string | null\r\n}\r\n\r\nexport function impact(opts: { cwd?: string } = {}): ImpactResult {\r\n const cwd = opts.cwd ?? process.cwd()\r\n const projectDir = resolveProjectDir(cwd)\r\n const dbPath = resolveAnalyticsDbPath(projectDir)\r\n if (!fs.existsSync(dbPath)) {\r\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\r\n }\r\n const db = getDb(dbPath)\r\n const snapshot = db\r\n .prepare(\r\n `SELECT created_at FROM optimization_snapshots\r\n WHERE method LIKE 'allowlist_%'\r\n ORDER BY created_at DESC LIMIT 1`,\r\n )\r\n .get() as { created_at: string } | undefined\r\n if (!snapshot) {\r\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\r\n }\r\n\r\n const before = db\r\n .prepare(\r\n `SELECT AVG(tokens_estimated) as avg FROM (\r\n SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100\r\n )`,\r\n )\r\n .get(snapshot.created_at) as { avg: number | null }\r\n const after = db\r\n .prepare(\r\n `SELECT AVG(tokens_estimated) as avg FROM (\r\n SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100\r\n )`,\r\n )\r\n .get(snapshot.created_at) as { avg: number | null }\r\n\r\n const beforeAvg = before.avg\r\n const afterAvg = after.avg\r\n const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null\r\n const percent =\r\n beforeAvg !== null && beforeAvg > 0 && afterAvg !== null\r\n ? (afterAvg - beforeAvg) / beforeAvg\r\n : null\r\n\r\n return {\r\n before_avg: beforeAvg,\r\n after_avg: afterAvg,\r\n delta,\r\n percent,\r\n snapshot_at: snapshot.created_at,\r\n }\r\n}\r\n\r\nexport interface PruneMcpCliOptions {\r\n cwd?: string\r\n print?: (msg: string) => void\r\n}\r\n\r\nexport function runPruneMcp(args: string[] = [], opts: PruneMcpCliOptions = {}): number {\r\n const print = opts.print ?? ((m: string) => console.error(m))\r\n const cwd = opts.cwd ?? process.cwd()\r\n\r\n if (args.includes('--generate-from-history')) {\r\n const daysFlag = args.find((a) => a.startsWith('--days='))\r\n const days = daysFlag ? parseInt(daysFlag.split('=')[1], 10) : 14\r\n const result = generateFromHistory({ cwd, days })\r\n print(`Propuesta de allowlist (${days} dias de historial):`)\r\n print(` Usados: ${result.proposed_allowlist.join(', ') || '(ninguno)'}`)\r\n print(` Inactivos: ${result.inactive_servers.join(', ') || '(ninguno)'}`)\r\n print(` Eventos MCP totales: ${result.total_mcp_events}`)\r\n return 0\r\n }\r\n\r\n if (args.includes('--apply')) {\r\n const generated = generateFromHistory({ cwd })\r\n if (generated.proposed_allowlist.length === 0) {\r\n print('No hay MCPs activos en el historial. Nada que aplicar.')\r\n return 1\r\n }\r\n const applied = applyAllowlist(generated.proposed_allowlist, { cwd })\r\n print(`Allowlist aplicado a ${applied.settings_path}`)\r\n print(`Backup: ${applied.backup_path}`)\r\n return 0\r\n }\r\n\r\n if (args.includes('--rollback')) {\r\n const toFlag = args.find((a) => a.startsWith('--to='))\r\n const to = toFlag ? toFlag.split('=')[1] : undefined\r\n const result = rollback({ cwd, to })\r\n if (result.restored) {\r\n print(`Restaurado desde ${result.from}`)\r\n return 0\r\n }\r\n print('No hay backups disponibles.')\r\n return 1\r\n }\r\n\r\n if (args.includes('--clear')) {\r\n const result = clearAllowlist({ cwd })\r\n print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar')\r\n return 0\r\n }\r\n\r\n if (args.includes('--impact')) {\r\n const result = impact({ cwd })\r\n if (result.snapshot_at === null) {\r\n print('No hay snapshots de allowlist todavia.')\r\n return 0\r\n }\r\n print(`Snapshot mas reciente: ${result.snapshot_at}`)\r\n print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? 'n/a'}`)\r\n print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? 'n/a'}`)\r\n if (result.percent !== null) {\r\n print(`Delta: ${(result.percent * 100).toFixed(1)}%`)\r\n }\r\n return 0\r\n }\r\n\r\n // Default: list registered MCPs with estimated cost\r\n const schema = measureCurrentSchemaBytes({ cwd })\r\n print(`MCPs registrados (${schema.mcp_servers.length}):`)\r\n for (const s of schema.mcp_servers) {\r\n print(` ${s}`)\r\n }\r\n print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`)\r\n print('')\r\n print('Flags:')\r\n print(' --generate-from-history [--days N] Propone allowlist (read-only)')\r\n print(' --apply Aplica el allowlist generado')\r\n print(' --rollback [--to TIMESTAMP] Restaura el ultimo backup')\r\n print(' --clear Elimina allowlist actual')\r\n print(' --impact Compara antes/despues del ultimo snapshot')\r\n return 0\r\n}\r\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| buildSuggestions | ||
| } from "./chunk-EV4HR7LB.js"; | ||
| import { | ||
| checkSerenaHealth, | ||
| probeMcpPruning, | ||
| probePromptCaching, | ||
| probeRtk, | ||
| probeSerena | ||
| } from "./chunk-4MJNQPFS.js"; | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-ESRDZMZJ.js"; | ||
| // src/cli/doctor.ts | ||
| function symbol(present) { | ||
| return present ? "\u2713" : "\u2717"; | ||
| } | ||
| function runDoctor(_args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const paths = { home: opts.home, cwd: opts.cwd }; | ||
| const serena = probeSerena(paths); | ||
| const rtk = probeRtk(paths); | ||
| const pruning = probeMcpPruning(paths); | ||
| const promptCaching = probePromptCaching(); | ||
| const schema = measureCurrentSchemaBytes(paths); | ||
| 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 lines = []; | ||
| lines.push("token-optimizer-mcp doctor"); | ||
| lines.push(""); | ||
| lines.push( | ||
| `[serena] ${symbol(status.serena.present)} conf=${status.serena.confidence.toFixed(2)} signals: ${status.serena.signals.join(", ") || "(ninguno)"}` | ||
| ); | ||
| if (serena.present) { | ||
| const healthWarnings = checkSerenaHealth(paths); | ||
| for (const w of healthWarnings) { | ||
| lines.push(` \u26A0 ${w.message} \u2014 ${w.fix}`); | ||
| } | ||
| } | ||
| lines.push( | ||
| `[rtk] ${symbol(status.rtk.present)} conf=${status.rtk.confidence.toFixed(2)} signals: ${status.rtk.signals.join(", ") || "(ninguno)"}` | ||
| ); | ||
| lines.push( | ||
| `[mcp-pruning] ${symbol(status.mcp_pruning.present)} conf=${status.mcp_pruning.confidence.toFixed(2)} signals: ${status.mcp_pruning.signals.join(", ") || "(ninguno)"}` | ||
| ); | ||
| lines.push( | ||
| `[prompt-cache] ~ activo por defecto en Claude Code \u2014 ${promptCaching.details.note}` | ||
| ); | ||
| lines.push( | ||
| `[schema-size] ~${schema.tool_schema_tokens} tokens / ${schema.tool_schema_bytes} bytes (${schema.measurement_method}) \u2014 ${schema.mcp_servers.length} MCP server(s): ${schema.mcp_servers.join(", ") || "(ninguno)"}` | ||
| ); | ||
| const suggestions = buildSuggestions(status, paths); | ||
| if (suggestions.length > 0) { | ||
| lines.push(""); | ||
| lines.push("Sugerencias:"); | ||
| for (const s of suggestions) { | ||
| lines.push(""); | ||
| lines.push(s); | ||
| } | ||
| } | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| export { | ||
| runDoctor | ||
| }; | ||
| //# sourceMappingURL=chunk-TDNW6PE3.js.map |
| {"version":3,"sources":["../src/cli/doctor.ts"],"sourcesContent":["// Doctor CLI — Phase 4.12\r\n// Runs all detection probes + schema-measurer + advisor and prints a Spanish report.\r\n// Always exits 0.\r\n\r\nimport {\r\n probeSerena,\r\n probeRtk,\r\n probeMcpPruning,\r\n probePromptCaching,\r\n checkSerenaHealth,\r\n} from '../orchestration/detector.js'\r\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\r\nimport { buildSuggestions } from '../orchestration/advisor.js'\r\nimport type { OptimizationStatus } from '../lib/types.js'\r\n\r\nexport interface DoctorOptions {\r\n home?: string\r\n cwd?: string\r\n print?: (msg: string) => void\r\n}\r\n\r\nfunction symbol(present: boolean): string {\r\n return present ? '✓' : '✗'\r\n}\r\n\r\nexport function runDoctor(_args: string[] = [], opts: DoctorOptions = {}): number {\r\n const print = opts.print ?? ((m: string) => console.error(m))\r\n const paths = { home: opts.home, cwd: opts.cwd }\r\n\r\n const serena = probeSerena(paths)\r\n const rtk = probeRtk(paths)\r\n const pruning = probeMcpPruning(paths)\r\n const promptCaching = probePromptCaching()\r\n const schema = measureCurrentSchemaBytes(paths)\r\n\r\n const status: OptimizationStatus = {\r\n serena,\r\n rtk,\r\n mcp_pruning: pruning,\r\n prompt_caching: {\r\n active_by_default: true,\r\n savings_tokens: null,\r\n estimation_method: 'unknown',\r\n note: 'Revisa tu factura Anthropic para confirmar el ahorro real',\r\n },\r\n schema_bytes: {\r\n tool_schema_bytes: schema.tool_schema_bytes,\r\n measurement_method: schema.measurement_method,\r\n },\r\n }\r\n\r\n const lines: string[] = []\r\n lines.push('token-optimizer-mcp doctor')\r\n lines.push('')\r\n lines.push(\r\n `[serena] ${symbol(status.serena.present)} conf=${status.serena.confidence.toFixed(2)} signals: ${status.serena.signals.join(', ') || '(ninguno)'}`,\r\n )\r\n if (serena.present) {\r\n const healthWarnings = checkSerenaHealth(paths)\r\n for (const w of healthWarnings) {\r\n lines.push(` ⚠ ${w.message} — ${w.fix}`)\r\n }\r\n }\r\n lines.push(\r\n `[rtk] ${symbol(status.rtk.present)} conf=${status.rtk.confidence.toFixed(2)} signals: ${status.rtk.signals.join(', ') || '(ninguno)'}`,\r\n )\r\n lines.push(\r\n `[mcp-pruning] ${symbol(status.mcp_pruning.present)} conf=${status.mcp_pruning.confidence.toFixed(2)} signals: ${status.mcp_pruning.signals.join(', ') || '(ninguno)'}`,\r\n )\r\n lines.push(\r\n `[prompt-cache] ~ activo por defecto en Claude Code — ${promptCaching.details.note as string}`,\r\n )\r\n lines.push(\r\n `[schema-size] ~${schema.tool_schema_tokens} tokens / ${schema.tool_schema_bytes} bytes (${schema.measurement_method}) — ${schema.mcp_servers.length} MCP server(s): ${schema.mcp_servers.join(', ') || '(ninguno)'}`,\r\n )\r\n\r\n const suggestions = buildSuggestions(status, paths)\r\n if (suggestions.length > 0) {\r\n lines.push('')\r\n lines.push('Sugerencias:')\r\n for (const s of suggestions) {\r\n lines.push('')\r\n lines.push(s)\r\n }\r\n }\r\n\r\n print(lines.join('\\n'))\r\n return 0\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;AAqBA,SAAS,OAAO,SAA0B;AACxC,SAAO,UAAU,WAAM;AACzB;AAEO,SAAS,UAAU,QAAkB,CAAC,GAAG,OAAsB,CAAC,GAAW;AAChF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,QAAQ,EAAE,MAAM,KAAK,MAAM,KAAK,KAAK,IAAI;AAE/C,QAAM,SAAS,YAAY,KAAK;AAChC,QAAM,MAAM,SAAS,KAAK;AAC1B,QAAM,UAAU,gBAAgB,KAAK;AACrC,QAAM,gBAAgB,mBAAmB;AACzC,QAAM,SAAS,0BAA0B,KAAK;AAE9C,QAAM,SAA6B;AAAA,IACjC;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb,gBAAgB;AAAA,MACd,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,mBAAmB;AAAA,MACnB,MAAM;AAAA,IACR;AAAA,IACA,cAAc;AAAA,MACZ,mBAAmB,OAAO;AAAA,MAC1B,oBAAoB,OAAO;AAAA,IAC7B;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,4BAA4B;AACvC,QAAM,KAAK,EAAE;AACb,QAAM;AAAA,IACJ,mBAAmB,OAAO,OAAO,OAAO,OAAO,CAAC,SAAS,OAAO,OAAO,WAAW,QAAQ,CAAC,CAAC,cAAc,OAAO,OAAO,QAAQ,KAAK,IAAI,KAAK,WAAW;AAAA,EAC3J;AACA,MAAI,OAAO,SAAS;AAClB,UAAM,iBAAiB,kBAAkB,KAAK;AAC9C,eAAW,KAAK,gBAAgB;AAC9B,YAAM,KAAK,YAAO,EAAE,OAAO,WAAM,EAAE,GAAG,EAAE;AAAA,IAC1C;AAAA,EACF;AACA,QAAM;AAAA,IACJ,mBAAmB,OAAO,OAAO,IAAI,OAAO,CAAC,SAAS,OAAO,IAAI,WAAW,QAAQ,CAAC,CAAC,cAAc,OAAO,IAAI,QAAQ,KAAK,IAAI,KAAK,WAAW;AAAA,EAClJ;AACA,QAAM;AAAA,IACJ,mBAAmB,OAAO,OAAO,YAAY,OAAO,CAAC,SAAS,OAAO,YAAY,WAAW,QAAQ,CAAC,CAAC,cAAc,OAAO,YAAY,QAAQ,KAAK,IAAI,KAAK,WAAW;AAAA,EAC1K;AACA,QAAM;AAAA,IACJ,8DAAyD,cAAc,QAAQ,IAAc;AAAA,EAC/F;AACA,QAAM;AAAA,IACJ,oBAAoB,OAAO,kBAAkB,aAAa,OAAO,iBAAiB,WAAW,OAAO,kBAAkB,YAAO,OAAO,YAAY,MAAM,mBAAmB,OAAO,YAAY,KAAK,IAAI,KAAK,WAAW;AAAA,EACvN;AAEA,QAAM,cAAc,iBAAiB,QAAQ,KAAK;AAClD,MAAI,YAAY,SAAS,GAAG;AAC1B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,cAAc;AACzB,eAAW,KAAK,aAAa;AAC3B,YAAM,KAAK,EAAE;AACb,YAAM,KAAK,CAAC;AAAA,IACd;AAAA,EACF;AAEA,QAAM,MAAM,KAAK,IAAI,CAAC;AACtB,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| // src/lib/paths.ts | ||
| import path from "path"; | ||
| import fs from "fs"; | ||
| import os from "os"; | ||
| import crypto from "crypto"; | ||
| var IS_WINDOWS = process.platform === "win32"; | ||
| function normalizePath(p) { | ||
| const resolved = path.resolve(p); | ||
| return IS_WINDOWS ? resolved.toLowerCase() : resolved; | ||
| } | ||
| function resolveProjectDir(cwd = process.cwd()) { | ||
| let current = path.resolve(cwd); | ||
| const initial = current; | ||
| while (true) { | ||
| if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, "package.json"))) { | ||
| return current; | ||
| } | ||
| const parent = path.dirname(current); | ||
| if (parent === current) return initial; | ||
| current = parent; | ||
| } | ||
| } | ||
| function resolveAnalyticsDbPath(_projectDir) { | ||
| return path.join(resolveGlobalDir(), "analytics.db"); | ||
| } | ||
| function resolveGlobalDir() { | ||
| const override = process.env.TOKEN_OPTIMIZER_HOME; | ||
| if (override && override.trim().length > 0) return override; | ||
| return path.join(os.homedir(), ".token-optimizer"); | ||
| } | ||
| function projectHash(projectDir) { | ||
| return crypto.createHash("sha256").update(normalizePath(projectDir)).digest("hex").slice(0, 16); | ||
| } | ||
| function resolveTranscriptPath(projectDir, sessionId) { | ||
| const claudeDir = path.join(os.homedir(), ".claude", "projects"); | ||
| const projectKey = path.resolve(projectDir).replace(/[:\\/]/g, "-"); | ||
| return path.join(claudeDir, projectKey, `${sessionId}.jsonl`); | ||
| } | ||
| export { | ||
| normalizePath, | ||
| resolveProjectDir, | ||
| resolveAnalyticsDbPath, | ||
| resolveGlobalDir, | ||
| projectHash, | ||
| resolveTranscriptPath | ||
| }; | ||
| //# sourceMappingURL=chunk-U3OXZD52.js.map |
| {"version":3,"sources":["../src/lib/paths.ts"],"sourcesContent":["// Path helpers — Phase 1.4\r\n// Cross-platform project dir resolution, storage dir, transcript path\r\n\r\nimport path from 'node:path'\r\nimport fs from 'node:fs'\r\nimport os from 'node:os'\r\nimport crypto from 'node:crypto'\r\n\r\nconst IS_WINDOWS = process.platform === 'win32'\r\n\r\nexport function normalizePath(p: string): string {\r\n const resolved = path.resolve(p)\r\n return IS_WINDOWS ? resolved.toLowerCase() : resolved\r\n}\r\n\r\nexport function resolveProjectDir(cwd: string = process.cwd()): string {\r\n let current = path.resolve(cwd)\r\n const initial = current\r\n // Walk up looking for .git or package.json; fall back to cwd if not found\r\n while (true) {\r\n if (\r\n fs.existsSync(path.join(current, '.git')) ||\r\n fs.existsSync(path.join(current, 'package.json'))\r\n ) {\r\n return current\r\n }\r\n const parent = path.dirname(current)\r\n if (parent === current) return initial\r\n current = parent\r\n }\r\n}\r\n\r\n/**\r\n * Path to the analytics DB. v0.4.7+: always returns the global DB under ~/.token-optimizer/\r\n * so hooks, CLI and MCP tools share a single source of truth regardless of CWD.\r\n * Per-project filtering is still available via `sessions.project_hash`.\r\n *\r\n * The `projectDir` argument is kept for backward compatibility with existing callers\r\n * and tests (tests pass an explicit `dbPath` bypassing this function entirely).\r\n */\r\nexport function resolveAnalyticsDbPath(_projectDir: string): string {\r\n return path.join(resolveGlobalDir(), 'analytics.db')\r\n}\r\n\r\nexport function resolveGlobalDir(): string {\r\n // TOKEN_OPTIMIZER_HOME overrides the default for tests and multi-user setups.\r\n const override = process.env.TOKEN_OPTIMIZER_HOME\r\n if (override && override.trim().length > 0) return override\r\n return path.join(os.homedir(), '.token-optimizer')\r\n}\r\n\r\nexport function ensureGlobalStorageDir(): string {\r\n const dir = resolveGlobalDir()\r\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\r\n return dir\r\n}\r\n\r\nexport function projectHash(projectDir: string): string {\r\n return crypto.createHash('sha256').update(normalizePath(projectDir)).digest('hex').slice(0, 16)\r\n}\r\n\r\n/**\r\n * Resolve the Claude Code transcript JSONL path for a given project + session.\r\n * Claude Code stores transcripts under `~/.claude/projects/{project-key}/{sessionId}.jsonl`\r\n * where project-key replaces path separators (/, \\, :) with dashes.\r\n */\r\nexport function resolveTranscriptPath(projectDir: string, sessionId: string): string {\r\n const claudeDir = path.join(os.homedir(), '.claude', 'projects')\r\n const projectKey = path.resolve(projectDir).replace(/[:\\\\/]/g, '-')\r\n return path.join(claudeDir, projectKey, `${sessionId}.jsonl`)\r\n}\r\n"],"mappings":";;;AAGA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,OAAO,QAAQ;AACf,OAAO,YAAY;AAEnB,IAAM,aAAa,QAAQ,aAAa;AAEjC,SAAS,cAAc,GAAmB;AAC/C,QAAM,WAAW,KAAK,QAAQ,CAAC;AAC/B,SAAO,aAAa,SAAS,YAAY,IAAI;AAC/C;AAEO,SAAS,kBAAkB,MAAc,QAAQ,IAAI,GAAW;AACrE,MAAI,UAAU,KAAK,QAAQ,GAAG;AAC9B,QAAM,UAAU;AAEhB,SAAO,MAAM;AACX,QACE,GAAG,WAAW,KAAK,KAAK,SAAS,MAAM,CAAC,KACxC,GAAG,WAAW,KAAK,KAAK,SAAS,cAAc,CAAC,GAChD;AACA,aAAO;AAAA,IACT;AACA,UAAM,SAAS,KAAK,QAAQ,OAAO;AACnC,QAAI,WAAW,QAAS,QAAO;AAC/B,cAAU;AAAA,EACZ;AACF;AAUO,SAAS,uBAAuB,aAA6B;AAClE,SAAO,KAAK,KAAK,iBAAiB,GAAG,cAAc;AACrD;AAEO,SAAS,mBAA2B;AAEzC,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO;AACnD,SAAO,KAAK,KAAK,GAAG,QAAQ,GAAG,kBAAkB;AACnD;AAQO,SAAS,YAAY,YAA4B;AACtD,SAAO,OAAO,WAAW,QAAQ,EAAE,OAAO,cAAc,UAAU,CAAC,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AAChG;AAOO,SAAS,sBAAsB,YAAoB,WAA2B;AACnF,QAAM,YAAY,KAAK,KAAK,GAAG,QAAQ,GAAG,WAAW,UAAU;AAC/D,QAAM,aAAa,KAAK,QAAQ,UAAU,EAAE,QAAQ,WAAW,GAAG;AAClE,SAAO,KAAK,KAAK,WAAW,YAAY,GAAG,SAAS,QAAQ;AAC9D;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-AF6RQ5F5.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| // src/services/stats.ts | ||
| var DAY_MS = 864e5; | ||
| var HAIKU_INPUT_PER_MTOK = 1; | ||
| var SONNET_INPUT_PER_MTOK = 3; | ||
| var OPUS_INPUT_PER_MTOK = 5; | ||
| function sinceDays(days) { | ||
| return new Date(Date.now() - days * DAY_MS).toISOString(); | ||
| } | ||
| function getUsageStats(db, days = 7) { | ||
| const queries = buildQueries(db); | ||
| const since = sinceDays(days); | ||
| const byTool = queries.countToolCallsByTool(since); | ||
| const bySource = queries.countToolCallsBySource(since); | ||
| const totalTokens = bySource.reduce((sum, r) => sum + r.tokens, 0); | ||
| const totalEvents = bySource.reduce((sum, r) => sum + r.count, 0); | ||
| return { | ||
| period_days: days, | ||
| period_since: since, | ||
| by_tool: byTool, | ||
| by_source: bySource, | ||
| total_tokens: totalTokens, | ||
| total_events: totalEvents | ||
| }; | ||
| } | ||
| function getCostReport(db, days = 7) { | ||
| const usage = getUsageStats(db, days); | ||
| const mtok = usage.total_tokens / 1e6; | ||
| const haiku = Number((mtok * HAIKU_INPUT_PER_MTOK).toFixed(4)); | ||
| const sonnet = Number((mtok * SONNET_INPUT_PER_MTOK).toFixed(4)); | ||
| const opus = Number((mtok * OPUS_INPUT_PER_MTOK).toFixed(4)); | ||
| return { | ||
| period_days: days, | ||
| total_tokens: usage.total_tokens, | ||
| estimated_cost_usd_haiku: haiku, | ||
| estimated_cost_usd_sonnet: sonnet, | ||
| estimated_cost_usd_opus: opus, | ||
| estimated_cost_usd_min: haiku, | ||
| estimated_cost_usd_max: opus, | ||
| by_source: usage.by_source, | ||
| disclaimer: "Coste estimado de tokens de herramientas (input al modelo). Haiku $1, Sonnet $3, Opus $5 por MTok. Revisa tu factura Anthropic para el coste real." | ||
| }; | ||
| } | ||
| function getActiveBudgetSummary(db, sessionId, projectHash) { | ||
| const mgr = new BudgetManager(db); | ||
| return mgr.checkBudget(sessionId, projectHash); | ||
| } | ||
| export { | ||
| getUsageStats, | ||
| getCostReport, | ||
| getActiveBudgetSummary | ||
| }; | ||
| //# sourceMappingURL=chunk-VD6RKGFO.js.map |
| {"version":3,"sources":["../src/services/stats.ts"],"sourcesContent":["// Shared stats service — Phase 4.4\r\n// Read-only aggregates used by CLI and MCP tools.\r\n\r\nimport type Database from 'better-sqlite3'\r\nimport { buildQueries, type ToolCountRow, type SourceCountRow } from '../db/queries.js'\r\nimport { BudgetManager } from './budget-manager.js'\r\nimport type { BudgetStatus } from '../lib/types.js'\r\n\r\ntype DB = Database.Database\r\n\r\nconst DAY_MS = 86_400_000\r\n// Pricing April 2026 — input tokens (tool output → model input)\r\n// Haiku 4.5: $1/$5, Sonnet 4.6: $3/$15, Opus 4.6: $5/$25 per MTok (input/output)\r\n// token-optimizer tracks tool output (= model input), so we use input pricing.\r\nconst HAIKU_INPUT_PER_MTOK = 1\r\nconst SONNET_INPUT_PER_MTOK = 3\r\nconst OPUS_INPUT_PER_MTOK = 5\r\n\r\nfunction sinceDays(days: number): string {\r\n return new Date(Date.now() - days * DAY_MS).toISOString()\r\n}\r\n\r\nexport interface UsageStats {\r\n period_days: number\r\n period_since: string\r\n by_tool: ToolCountRow[]\r\n by_source: SourceCountRow[]\r\n total_tokens: number\r\n total_events: number\r\n}\r\n\r\nexport interface CostReport {\r\n period_days: number\r\n total_tokens: number\r\n estimated_cost_usd_haiku: number\r\n estimated_cost_usd_sonnet: number\r\n estimated_cost_usd_opus: number\r\n /** @deprecated Use estimated_cost_usd_haiku */\r\n estimated_cost_usd_min: number\r\n /** @deprecated Use estimated_cost_usd_opus */\r\n estimated_cost_usd_max: number\r\n by_source: SourceCountRow[]\r\n disclaimer: string\r\n}\r\n\r\nexport interface SavingsToday {\r\n date: string\r\n by_source: SourceCountRow[]\r\n total_tokens: number\r\n note: string\r\n}\r\n\r\nexport function getUsageStats(db: DB, days = 7): UsageStats {\r\n const queries = buildQueries(db)\r\n const since = sinceDays(days)\r\n const byTool = queries.countToolCallsByTool(since)\r\n const bySource = queries.countToolCallsBySource(since)\r\n const totalTokens = bySource.reduce((sum, r) => sum + r.tokens, 0)\r\n const totalEvents = bySource.reduce((sum, r) => sum + r.count, 0)\r\n return {\r\n period_days: days,\r\n period_since: since,\r\n by_tool: byTool,\r\n by_source: bySource,\r\n total_tokens: totalTokens,\r\n total_events: totalEvents,\r\n }\r\n}\r\n\r\nexport function getCostReport(db: DB, days = 7): CostReport {\r\n const usage = getUsageStats(db, days)\r\n const mtok = usage.total_tokens / 1_000_000\r\n const haiku = Number((mtok * HAIKU_INPUT_PER_MTOK).toFixed(4))\r\n const sonnet = Number((mtok * SONNET_INPUT_PER_MTOK).toFixed(4))\r\n const opus = Number((mtok * OPUS_INPUT_PER_MTOK).toFixed(4))\r\n return {\r\n period_days: days,\r\n total_tokens: usage.total_tokens,\r\n estimated_cost_usd_haiku: haiku,\r\n estimated_cost_usd_sonnet: sonnet,\r\n estimated_cost_usd_opus: opus,\r\n estimated_cost_usd_min: haiku,\r\n estimated_cost_usd_max: opus,\r\n by_source: usage.by_source,\r\n disclaimer:\r\n 'Coste estimado de tokens de herramientas (input al modelo). Haiku $1, Sonnet $3, Opus $5 por MTok. Revisa tu factura Anthropic para el coste real.',\r\n }\r\n}\r\n\r\nexport function getActiveBudgetSummary(\r\n db: DB,\r\n sessionId: string,\r\n projectHash: string | null,\r\n): BudgetStatus {\r\n const mgr = new BudgetManager(db)\r\n return mgr.checkBudget(sessionId, projectHash)\r\n}\r\n\r\nexport function getSavingsToday(db: DB): SavingsToday {\r\n const queries = buildQueries(db)\r\n const since = sinceDays(1)\r\n const bySource = queries.countToolCallsBySource(since)\r\n const total = bySource.reduce((sum, r) => sum + r.tokens, 0)\r\n return {\r\n date: new Date().toISOString().slice(0, 10),\r\n by_source: bySource,\r\n total_tokens: total,\r\n note: 'Ahorros por fuente no medidos directamente; revisa el reporte para el split Medido/Estimado.',\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;AAUA,IAAM,SAAS;AAIf,IAAM,uBAAuB;AAC7B,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB;AAE5B,SAAS,UAAU,MAAsB;AACvC,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE,YAAY;AAC1D;AAgCO,SAAS,cAAc,IAAQ,OAAO,GAAe;AAC1D,QAAM,UAAU,aAAa,EAAE;AAC/B,QAAM,QAAQ,UAAU,IAAI;AAC5B,QAAM,SAAS,QAAQ,qBAAqB,KAAK;AACjD,QAAM,WAAW,QAAQ,uBAAuB,KAAK;AACrD,QAAM,cAAc,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,QAAQ,CAAC;AACjE,QAAM,cAAc,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AAChE,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc;AAAA,IACd,SAAS;AAAA,IACT,WAAW;AAAA,IACX,cAAc;AAAA,IACd,cAAc;AAAA,EAChB;AACF;AAEO,SAAS,cAAc,IAAQ,OAAO,GAAe;AAC1D,QAAM,QAAQ,cAAc,IAAI,IAAI;AACpC,QAAM,OAAO,MAAM,eAAe;AAClC,QAAM,QAAQ,QAAQ,OAAO,sBAAsB,QAAQ,CAAC,CAAC;AAC7D,QAAM,SAAS,QAAQ,OAAO,uBAAuB,QAAQ,CAAC,CAAC;AAC/D,QAAM,OAAO,QAAQ,OAAO,qBAAqB,QAAQ,CAAC,CAAC;AAC3D,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc,MAAM;AAAA,IACpB,0BAA0B;AAAA,IAC1B,2BAA2B;AAAA,IAC3B,yBAAyB;AAAA,IACzB,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,IACxB,WAAW,MAAM;AAAA,IACjB,YACE;AAAA,EACJ;AACF;AAEO,SAAS,uBACd,IACA,WACA,aACc;AACd,QAAM,MAAM,IAAI,cAAc,EAAE;AAChC,SAAO,IAAI,YAAY,WAAW,WAAW;AAC/C;","names":[]} |
| #!/usr/bin/env node | ||
| // src/hooks/serena-activate.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| import { spawnSync } from "child_process"; | ||
| var INSTRUCTION = [ | ||
| "**IMPORTANT \u2014 Serena activation (do this FIRST, before anything else):**", | ||
| '1. Call ToolSearch with query "select:mcp__serena__initial_instructions,mcp__serena__activate_project,mcp__serena__check_onboarding_performed" to load the tool schemas (they are deferred and cannot be called without this step).', | ||
| "2. Call mcp__serena__initial_instructions (read the manual).", | ||
| "3. Call mcp__serena__activate_project with the current working directory as the `project` argument.", | ||
| "4. Call mcp__serena__check_onboarding_performed.", | ||
| "Do NOT skip step 1 \u2014 calling Serena tools without loading their schemas first will fail with InputValidationError." | ||
| ].join(" "); | ||
| function probeSerenaPresence(env = process.env) { | ||
| const isWindows = process.platform === "win32"; | ||
| const whichCmd = isWindows ? "where" : "which"; | ||
| const binName = isWindows ? "serena-hooks.exe" : "serena-hooks"; | ||
| let serena_cli_installed = false; | ||
| try { | ||
| const result = spawnSync(whichCmd, ["serena-hooks"], { | ||
| encoding: "utf8", | ||
| timeout: 500, | ||
| windowsHide: true, | ||
| env | ||
| }); | ||
| if (result.status === 0 && result.stdout && result.stdout.trim().length > 0) { | ||
| serena_cli_installed = true; | ||
| } | ||
| } catch { | ||
| } | ||
| if (!serena_cli_installed) { | ||
| const candidates = [ | ||
| path.join(os.homedir(), ".local", "bin", binName), | ||
| ...isWindows ? [ | ||
| path.join(os.homedir(), "scoop", "shims", binName), | ||
| path.join("C:\\", "tools", "serena", binName) | ||
| ] : ["/usr/local/bin/serena-hooks", "/opt/homebrew/bin/serena-hooks"] | ||
| ]; | ||
| for (const c of candidates) { | ||
| if (fs.existsSync(c)) { | ||
| serena_cli_installed = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| const serena_mcp_registered = fs.existsSync(path.join(os.homedir(), ".serena")); | ||
| return { | ||
| serena_cli_installed, | ||
| serena_mcp_registered, | ||
| present: serena_cli_installed || serena_mcp_registered | ||
| }; | ||
| } | ||
| function runSerenaActivateHook(opts = {}) { | ||
| const probe = opts.probe ?? probeSerenaPresence(); | ||
| if (!probe.present) { | ||
| if (opts.writeStdout !== false) process.stdout.write("{}"); | ||
| return { emitted: false, probe }; | ||
| } | ||
| const payload = { | ||
| hookSpecificOutput: { | ||
| hookEventName: "SessionStart", | ||
| additionalContext: INSTRUCTION | ||
| } | ||
| }; | ||
| if (opts.writeStdout !== false) process.stdout.write(JSON.stringify(payload)); | ||
| return { emitted: true, probe }; | ||
| } | ||
| function runSerenaActivateHookFromCli() { | ||
| try { | ||
| fs.readFileSync(0, "utf8"); | ||
| } catch { | ||
| } | ||
| return runSerenaActivateHook(); | ||
| } | ||
| export { | ||
| probeSerenaPresence, | ||
| runSerenaActivateHookFromCli | ||
| }; | ||
| //# sourceMappingURL=chunk-Z3HKBI5O.js.map |
| {"version":3,"sources":["../src/hooks/serena-activate.ts"],"sourcesContent":["// SessionStart hook that forces Serena activation via explicit ToolSearch.\r\n//\r\n// Context: Serena ships its own `serena-hooks activate` hook, but the output of\r\n// that binary does NOT mention ToolSearch — it just tells the agent to \"activate\r\n// the current working directory as project using Serena's tools\". In a Claude\r\n// Code environment where MCP tools are marked `deferred` at session start, the\r\n// agent cannot call `mcp__serena__*` tools without first loading their schemas\r\n// via the `ToolSearch` tool — trying to do so fails with InputValidationError.\r\n//\r\n// This hook emits an explicit four-step instruction block that starts with the\r\n// ToolSearch call, so the agent does the right thing on PCs where Serena tools\r\n// happen to be deferred.\r\n//\r\n// Behaviour:\r\n// - If Serena is NOT detected on the machine → emit `{}` and exit 0 (noop).\r\n// This keeps the hook safe to install globally, even on PCs without Serena.\r\n// - If Serena IS detected → emit the JSON with hookSpecificOutput.\r\n//\r\n// Intentionally stateless. Detection is a cheap synchronous probe so the hook\r\n// stays well under the p95 latency budget for SessionStart.\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport os from 'node:os'\r\nimport { spawnSync } from 'node:child_process'\r\n\r\nconst INSTRUCTION = [\r\n '**IMPORTANT — Serena activation (do this FIRST, before anything else):**',\r\n '1. Call ToolSearch with query \"select:mcp__serena__initial_instructions,mcp__serena__activate_project,mcp__serena__check_onboarding_performed\" to load the tool schemas (they are deferred and cannot be called without this step).',\r\n '2. Call mcp__serena__initial_instructions (read the manual).',\r\n '3. Call mcp__serena__activate_project with the current working directory as the `project` argument.',\r\n '4. Call mcp__serena__check_onboarding_performed.',\r\n 'Do NOT skip step 1 — calling Serena tools without loading their schemas first will fail with InputValidationError.',\r\n].join(' ')\r\n\r\nexport interface SerenaProbe {\r\n /**\r\n * The `serena-hooks` CLI binary is actually installed and executable on this\r\n * machine. Required to register the 3 official Serena hooks (remind,\r\n * auto-approve, cleanup) in settings.json — without the binary, Claude Code\r\n * would try to run them and fail with \"command not found\" on every hook\r\n * dispatch.\r\n */\r\n serena_cli_installed: boolean\r\n /**\r\n * There is some Serena footprint on this machine (`~/.serena/` directory,\r\n * usually created by the Serena MCP server or CLI the first time it runs).\r\n * This is independent of whether the CLI binary is installed; the user may\r\n * have only the MCP server registered.\r\n */\r\n serena_mcp_registered: boolean\r\n /**\r\n * True when at least one of the two above is true. Used by the\r\n * serena-activate hook to decide whether to emit its SessionStart payload —\r\n * that hook does NOT depend on the CLI, it only writes JSON to stdout.\r\n */\r\n present: boolean\r\n}\r\n\r\n/** Detect Serena installation state. Cheap and sync. */\r\nexport function probeSerenaPresence(\r\n env: NodeJS.ProcessEnv = process.env,\r\n): SerenaProbe {\r\n const isWindows = process.platform === 'win32'\r\n const whichCmd = isWindows ? 'where' : 'which'\r\n const binName = isWindows ? 'serena-hooks.exe' : 'serena-hooks'\r\n\r\n // Strategy 1 — $PATH via where/which. This is what actually matters for\r\n // `serena-hooks ...` commands to execute at runtime.\r\n let serena_cli_installed = false\r\n try {\r\n const result = spawnSync(whichCmd, ['serena-hooks'], {\r\n encoding: 'utf8',\r\n timeout: 500,\r\n windowsHide: true,\r\n env,\r\n })\r\n if (result.status === 0 && result.stdout && result.stdout.trim().length > 0) {\r\n serena_cli_installed = true\r\n }\r\n } catch {\r\n /* swallow — fall through to strategy 2 */\r\n }\r\n\r\n // Strategy 2 — common install locations (fallback when `where`/`which` can't\r\n // find things in a minimal shell PATH, or when the hook is invoked with a\r\n // stripped env).\r\n if (!serena_cli_installed) {\r\n const candidates = [\r\n path.join(os.homedir(), '.local', 'bin', binName),\r\n ...(isWindows\r\n ? [\r\n path.join(os.homedir(), 'scoop', 'shims', binName),\r\n path.join('C:\\\\', 'tools', 'serena', binName),\r\n ]\r\n : ['/usr/local/bin/serena-hooks', '/opt/homebrew/bin/serena-hooks']),\r\n ]\r\n for (const c of candidates) {\r\n if (fs.existsSync(c)) {\r\n serena_cli_installed = true\r\n break\r\n }\r\n }\r\n }\r\n\r\n // Independent signal — is the MCP server / config dir around? This alone is\r\n // NOT enough to register the 3 official hooks (they need the CLI), but it is\r\n // enough to install our own `--hook serena-activate` (which just emits\r\n // JSON and doesn't shell out to any binary).\r\n const serena_mcp_registered = fs.existsSync(path.join(os.homedir(), '.serena'))\r\n\r\n return {\r\n serena_cli_installed,\r\n serena_mcp_registered,\r\n present: serena_cli_installed || serena_mcp_registered,\r\n }\r\n}\r\n\r\nexport interface RunSerenaActivateOptions {\r\n writeStdout?: boolean\r\n /** Inject a custom probe result (used by tests). */\r\n probe?: SerenaProbe\r\n}\r\n\r\nexport interface SerenaActivateResult {\r\n emitted: boolean\r\n probe: SerenaProbe\r\n}\r\n\r\n/**\r\n * Pure function — doesn't read stdin. Callers from the entry point should\r\n * drain stdin themselves before invoking (Claude Code pipes a payload we\r\n * don't use). Tests call this directly without touching stdin.\r\n */\r\nexport function runSerenaActivateHook(\r\n opts: RunSerenaActivateOptions = {},\r\n): SerenaActivateResult {\r\n const probe = opts.probe ?? probeSerenaPresence()\r\n\r\n if (!probe.present) {\r\n if (opts.writeStdout !== false) process.stdout.write('{}')\r\n return { emitted: false, probe }\r\n }\r\n\r\n const payload = {\r\n hookSpecificOutput: {\r\n hookEventName: 'SessionStart',\r\n additionalContext: INSTRUCTION,\r\n },\r\n }\r\n if (opts.writeStdout !== false) process.stdout.write(JSON.stringify(payload))\r\n return { emitted: true, probe }\r\n}\r\n\r\n/**\r\n * Entry-point helper that drains stdin (Claude Code always pipes a payload)\r\n * and delegates to `runSerenaActivateHook`. Safe to call from the CLI\r\n * dispatcher in src/index.ts.\r\n */\r\nexport function runSerenaActivateHookFromCli(): SerenaActivateResult {\r\n try {\r\n fs.readFileSync(0, 'utf8')\r\n } catch {\r\n /* no stdin attached — fine */\r\n }\r\n return runSerenaActivateHook()\r\n}\r\n"],"mappings":";;;AAqBA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,iBAAiB;AAE1B,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AA2BH,SAAS,oBACd,MAAyB,QAAQ,KACpB;AACb,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,WAAW,YAAY,UAAU;AACvC,QAAM,UAAU,YAAY,qBAAqB;AAIjD,MAAI,uBAAuB;AAC3B,MAAI;AACF,UAAM,SAAS,UAAU,UAAU,CAAC,cAAc,GAAG;AAAA,MACnD,UAAU;AAAA,MACV,SAAS;AAAA,MACT,aAAa;AAAA,MACb;AAAA,IACF,CAAC;AACD,QAAI,OAAO,WAAW,KAAK,OAAO,UAAU,OAAO,OAAO,KAAK,EAAE,SAAS,GAAG;AAC3E,6BAAuB;AAAA,IACzB;AAAA,EACF,QAAQ;AAAA,EAER;AAKA,MAAI,CAAC,sBAAsB;AACzB,UAAM,aAAa;AAAA,MACjB,KAAK,KAAK,GAAG,QAAQ,GAAG,UAAU,OAAO,OAAO;AAAA,MAChD,GAAI,YACA;AAAA,QACE,KAAK,KAAK,GAAG,QAAQ,GAAG,SAAS,SAAS,OAAO;AAAA,QACjD,KAAK,KAAK,QAAQ,SAAS,UAAU,OAAO;AAAA,MAC9C,IACA,CAAC,+BAA+B,gCAAgC;AAAA,IACtE;AACA,eAAW,KAAK,YAAY;AAC1B,UAAI,GAAG,WAAW,CAAC,GAAG;AACpB,+BAAuB;AACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAMA,QAAM,wBAAwB,GAAG,WAAW,KAAK,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9E,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,SAAS,wBAAwB;AAAA,EACnC;AACF;AAkBO,SAAS,sBACd,OAAiC,CAAC,GACZ;AACtB,QAAM,QAAQ,KAAK,SAAS,oBAAoB;AAEhD,MAAI,CAAC,MAAM,SAAS;AAClB,QAAI,KAAK,gBAAgB,MAAO,SAAQ,OAAO,MAAM,IAAI;AACzD,WAAO,EAAE,SAAS,OAAO,MAAM;AAAA,EACjC;AAEA,QAAM,UAAU;AAAA,IACd,oBAAoB;AAAA,MAClB,eAAe;AAAA,MACf,mBAAmB;AAAA,IACrB;AAAA,EACF;AACA,MAAI,KAAK,gBAAgB,MAAO,SAAQ,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;AAC5E,SAAO,EAAE,SAAS,MAAM,MAAM;AAChC;AAOO,SAAS,+BAAqD;AACnE,MAAI;AACF,OAAG,aAAa,GAAG,MAAM;AAAA,EAC3B,QAAQ;AAAA,EAER;AACA,SAAO,sBAAsB;AAC/B;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| clearSurfaceLog, | ||
| measureContextSize, | ||
| runRules | ||
| } from "./chunk-GSWB574D.js"; | ||
| import "./chunk-METYJF7E.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-U3OXZD52.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-B72KFC3I.js.map |
| {"version":3,"sources":["../src/cli/coach.ts"],"sourcesContent":["// Coach CLI — Phase 4.50\r\n// Subcommands: status | list | explain <tip_id> | reset\r\n\r\nimport fs from 'node:fs'\r\nimport { KNOWLEDGE_BASE } from '../coach/knowledge-base.js'\r\nimport { runRules } from '../coach/detector.js'\r\nimport { measureContextSize } from '../coach/context-meter.js'\r\nimport { clearSurfaceLog } from '../coach/surface.js'\r\nimport { getDb } from '../db/connection.js'\r\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\r\nimport { buildQueries } from '../db/queries.js'\r\nimport type { EventContext, ToolEvent } from '../lib/types.js'\r\n\r\nexport interface CoachCliOptions {\r\n cwd?: string\r\n print?: (msg: string) => void\r\n}\r\n\r\nexport async function runCoachCli(\r\n args: string[] = [],\r\n opts: CoachCliOptions = {},\r\n): Promise<number> {\r\n const print = opts.print ?? ((m: string) => console.error(m))\r\n const cwd = opts.cwd ?? process.cwd()\r\n const sub = args[0] ?? 'status'\r\n\r\n if (sub === 'list') {\r\n print(`Knowledge base (${KNOWLEDGE_BASE.length} tips):`)\r\n for (const tip of KNOWLEDGE_BASE) {\r\n print(` • ${tip.id.padEnd(32)} ${tip.title}`)\r\n }\r\n return 0\r\n }\r\n\r\n if (sub === 'explain') {\r\n const tipId = args[1]\r\n if (!tipId) {\r\n print('Uso: token-optimizer-mcp coach explain <tip_id>')\r\n return 1\r\n }\r\n const tip = KNOWLEDGE_BASE.find((t) => t.id === tipId)\r\n if (!tip) {\r\n print(`Tip no encontrado: ${tipId}`)\r\n return 1\r\n }\r\n print(tip.title)\r\n print('')\r\n print(tip.description)\r\n print('')\r\n print(`Como usarlo: ${tip.how_to_invoke}`)\r\n print(`Cuando: ${tip.when_applicable}`)\r\n print(`Ahorro: ${tip.savings_estimate}`)\r\n print(`Fuente: ${tip.savings_source} · verificado: ${tip.verified_at}`)\r\n return 0\r\n }\r\n\r\n if (sub === 'reset') {\r\n const projectDir = resolveProjectDir(cwd)\r\n const dbPath = resolveAnalyticsDbPath(projectDir)\r\n if (!fs.existsSync(dbPath)) {\r\n print('Sin DB; nada que resetear.')\r\n return 0\r\n }\r\n const db = getDb(dbPath)\r\n const deleted = clearSurfaceLog(db)\r\n print(`Log de coach reseteado (${deleted} entradas eliminadas)`)\r\n return 0\r\n }\r\n\r\n // Default: status\r\n const projectDir = resolveProjectDir(cwd)\r\n const dbPath = resolveAnalyticsDbPath(projectDir)\r\n if (!fs.existsSync(dbPath)) {\r\n print('Coach status: sin datos. Ejecuta el hook posttooluse al menos una vez.')\r\n return 0\r\n }\r\n const db = getDb(dbPath)\r\n const contextOpts: {\r\n db: typeof db\r\n projectDir?: string\r\n } = { db }\r\n if (projectDir) contextOpts.projectDir = projectDir\r\n const context = await measureContextSize('default', contextOpts)\r\n\r\n const queries = buildQueries(db)\r\n const since = new Date(Date.now() - 86_400_000).toISOString()\r\n const rawRows = queries.getToolCallsSince(since) as ToolEvent[]\r\n const ctx: EventContext = {\r\n session_id: 'default',\r\n events: rawRows.slice(0, 100),\r\n session_token_total: context.tokens,\r\n session_token_method: context.estimation_method,\r\n session_token_limit: context.limit,\r\n active_model: null,\r\n }\r\n const hits = runRules(ctx)\r\n\r\n print('token-optimizer-mcp coach status')\r\n print('')\r\n print(\r\n `Contexto: ${(context.percent * 100).toFixed(1)}% (${context.tokens}/${context.limit} tokens, ${context.estimation_method})`,\r\n )\r\n print(`Tips activos: ${hits.length}`)\r\n if (hits.length === 0) {\r\n print(' (sin tips disparados en este momento)')\r\n } else {\r\n for (const h of hits) {\r\n print(` [${h.severity}] ${h.rule_id}: ${h.evidence}`)\r\n }\r\n }\r\n return 0\r\n}\r\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 | ||
| import { | ||
| DEFAULT_CONFIG, | ||
| getConfigPath, | ||
| loadConfig, | ||
| resolveXrayUrl, | ||
| runConfigCommand, | ||
| saveConfig | ||
| } from "./chunk-METYJF7E.js"; | ||
| import "./chunk-U3OXZD52.js"; | ||
| export { | ||
| DEFAULT_CONFIG, | ||
| getConfigPath, | ||
| loadConfig, | ||
| resolveXrayUrl, | ||
| runConfigCommand, | ||
| saveConfig | ||
| }; | ||
| //# sourceMappingURL=config-T5BH6KWQ.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| #!/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-Z4QLGTA3.js"); | ||
| return mod.runInstall(rest); | ||
| } | ||
| case "uninstall": { | ||
| const mod = await import("./uninstall-ELUPZDG7.js"); | ||
| return mod.runUninstall(rest); | ||
| } | ||
| case "doctor": { | ||
| const mod = await import("./doctor-SZQMXDW4.js"); | ||
| return mod.runDoctor(rest); | ||
| } | ||
| case "status": { | ||
| const mod = await import("./status-435S7GQI.js"); | ||
| return mod.runStatus(rest); | ||
| } | ||
| case "report": { | ||
| const mod = await import("./report-XRFU55JC.js"); | ||
| return mod.runReport(rest); | ||
| } | ||
| case "budget": { | ||
| const mod = await import("./budget-F22UXU7N.js"); | ||
| return mod.runBudgetCli(rest); | ||
| } | ||
| case "config": { | ||
| const mod = await import("./config-T5BH6KWQ.js"); | ||
| return mod.runConfigCommand(rest); | ||
| } | ||
| case "prune-mcp": { | ||
| const mod = await import("./prune-mcp-PXUSFZJU.js"); | ||
| return mod.runPruneMcp(rest); | ||
| } | ||
| case "coach": { | ||
| const mod = await import("./coach-B72KFC3I.js"); | ||
| return mod.runCoachCli(rest); | ||
| } | ||
| case "sync-xray": { | ||
| const mod = await import("./sync-xray-FZOQPPOU.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-WI357YQ4.js.map |
| {"version":3,"sources":["../src/cli/dispatcher.ts"],"sourcesContent":["// CLI subcommand dispatcher — Phase 4.9\r\n// Routes argv[0] to the appropriate subcommand module (lazy-imported).\r\n\r\nexport async function dispatchCli(argv: string[]): Promise<number> {\r\n const [sub, ...rest] = argv\r\n switch (sub) {\r\n case 'install': {\r\n const mod = await import('./install.js')\r\n return mod.runInstall(rest)\r\n }\r\n case 'uninstall': {\r\n const mod = await import('./uninstall.js')\r\n return mod.runUninstall(rest)\r\n }\r\n case 'doctor': {\r\n const mod = await import('./doctor.js')\r\n return mod.runDoctor(rest)\r\n }\r\n case 'status': {\r\n const mod = await import('./status.js')\r\n return mod.runStatus(rest)\r\n }\r\n case 'report': {\r\n const mod = await import('./report.js')\r\n return mod.runReport(rest)\r\n }\r\n case 'budget': {\r\n const mod = await import('./budget.js')\r\n return mod.runBudgetCli(rest)\r\n }\r\n case 'config': {\r\n const mod = await import('./config.js')\r\n return mod.runConfigCommand(rest)\r\n }\r\n case 'prune-mcp': {\r\n const mod = await import('./prune-mcp.js')\r\n return mod.runPruneMcp(rest)\r\n }\r\n case 'coach': {\r\n const mod = await import('./coach.js')\r\n return mod.runCoachCli(rest)\r\n }\r\n case 'sync-xray': {\r\n const mod = await import('./sync-xray.js')\r\n return mod.runSyncXray(rest)\r\n }\r\n default:\r\n console.error(`Subcomando desconocido: ${sub ?? '(ninguno)'}`)\r\n console.error(\r\n 'Disponibles: install, uninstall, doctor, status, report, budget, prune-mcp, coach, config, sync-xray',\r\n )\r\n return 1\r\n }\r\n}\r\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 { | ||
| runDoctor | ||
| } from "./chunk-TDNW6PE3.js"; | ||
| import "./chunk-EV4HR7LB.js"; | ||
| import "./chunk-4MJNQPFS.js"; | ||
| import "./chunk-ESRDZMZJ.js"; | ||
| export { | ||
| runDoctor | ||
| }; | ||
| //# sourceMappingURL=doctor-SZQMXDW4.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| probeSerenaPresence | ||
| } from "./chunk-Z3HKBI5O.js"; | ||
| import { | ||
| ensureGitignore | ||
| } from "./chunk-N754BAPB.js"; | ||
| import { | ||
| getConfigPath, | ||
| loadConfig, | ||
| saveConfig | ||
| } from "./chunk-METYJF7E.js"; | ||
| import { | ||
| runDoctor | ||
| } from "./chunk-TDNW6PE3.js"; | ||
| import "./chunk-EV4HR7LB.js"; | ||
| import "./chunk-4MJNQPFS.js"; | ||
| import "./chunk-ESRDZMZJ.js"; | ||
| import "./chunk-U3OXZD52.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"))) { | ||
| ensureGitignore(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-Z4QLGTA3.js.map |
| {"version":3,"sources":["../src/cli/install.ts"],"sourcesContent":["// Install CLI — Phase 4.10\r\n// Writes token-optimizer mcpServers entry + 3 hooks into ~/.claude/settings.json\r\n// Also appends .gitignore entries in git repos.\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport os from 'node:os'\r\nimport { spawnSync } from 'node:child_process'\r\nimport { ensureGitignore } from '../lib/storage.js'\r\nimport { probeSerenaPresence, type SerenaProbe } from '../hooks/serena-activate.js'\r\nimport { runDoctor } from './doctor.js'\r\nimport { loadConfig, saveConfig, getConfigPath } from './config.js'\r\n\r\nconst SERVER_NAME = 'token-optimizer'\r\n\r\n/**\r\n * Resolve the hook command base.\r\n * Prefer `node <global-path>/dist/index.js` for speed (~0.2s vs ~1.5s with npx).\r\n * Falls back to `npx @cocaxcode/token-optimizer-mcp` if global path not found.\r\n */\r\nfunction resolveHookCommandBase(): string {\r\n try {\r\n const globalRoot = path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules')\r\n const indexPath = path.join(globalRoot, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\r\n if (fs.existsSync(indexPath)) {\r\n return `node \"${indexPath.replace(/\\\\/g, '/')}\"`\r\n }\r\n } catch { /* fallback */ }\r\n\r\n // Unix global paths\r\n const unixPaths = [\r\n '/usr/local/lib/node_modules',\r\n '/usr/lib/node_modules',\r\n path.join(os.homedir(), '.npm-global', 'lib', 'node_modules'),\r\n ]\r\n for (const root of unixPaths) {\r\n try {\r\n const indexPath = path.join(root, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\r\n if (fs.existsSync(indexPath)) {\r\n return `node \"${indexPath}\"`\r\n }\r\n } catch { /* fallback */ }\r\n }\r\n\r\n // npm root -g fallback\r\n try {\r\n const result = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 3000, shell: true })\r\n const npmRoot = (result.stdout ?? '').trim()\r\n const indexPath = path.join(npmRoot, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\r\n if (fs.existsSync(indexPath)) {\r\n return `node \"${indexPath.replace(/\\\\/g, '/')}\"`\r\n }\r\n } catch { /* fallback */ }\r\n\r\n return 'npx @cocaxcode/token-optimizer-mcp'\r\n}\r\n\r\nexport interface InstallOptions {\r\n home?: string\r\n cwd?: string\r\n print?: (msg: string) => void\r\n runDoctorAtEnd?: boolean\r\n /**\r\n * Override the Serena presence probe. Used by tests to get deterministic\r\n * behaviour independent of whether the test host actually has Serena.\r\n * Undefined = probe the real filesystem/PATH.\r\n */\r\n serenaProbe?: SerenaProbe\r\n /**\r\n * Skip installing the 3 official Serena reminder hooks (remind, auto-approve,\r\n * cleanup) even when Serena is detected. The Serena-activate hook we own is\r\n * still installed. Use this if you prefer managing the official hooks yourself\r\n * or you don't want to depend on Serena's alpha feature.\r\n */\r\n skipSerenaHooks?: boolean\r\n}\r\n\r\nfunction settingsPath(home: string): string {\r\n return path.join(home, '.claude', 'settings.json')\r\n}\r\n\r\nfunction readSettings(p: string): Record<string, unknown> {\r\n try {\r\n if (!fs.existsSync(p)) return {}\r\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\r\n } catch {\r\n return {}\r\n }\r\n}\r\n\r\nfunction writeSettings(p: string, data: Record<string, unknown>): void {\r\n const dir = path.dirname(p)\r\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\r\n fs.writeFileSync(p, JSON.stringify(data, null, 2))\r\n}\r\n\r\ninterface HookEntry {\r\n matcher?: string\r\n hooks?: Array<{ type?: string; command?: string }>\r\n [key: string]: unknown\r\n}\r\n\r\n/**\r\n * Extract the `--hook <kind>` flag from a command line so we can use it as\r\n * a unique identity for upsert. `node .../index.js --hook serena-activate`\r\n * becomes `--hook serena-activate`. Anything without a `--hook X` returns null.\r\n */\r\nfunction extractHookFlag(command: string): string | null {\r\n const match = command.match(/--hook\\s+(\\S+)/)\r\n return match ? `--hook ${match[1]}` : null\r\n}\r\n\r\n/**\r\n * Remove any handlers whose command contains `identifier` from the given\r\n * (eventName, matcher) group. Used to un-register hooks that were installed\r\n * in a previous run but no longer apply (e.g. the 3 official Serena hooks\r\n * when the `serena-hooks` CLI is no longer in PATH).\r\n *\r\n * Returns the number of handlers removed. If the matcher group becomes empty,\r\n * the whole group is dropped from the event list. If the event itself becomes\r\n * empty, the event key is deleted from the hooks map.\r\n */\r\nfunction removeHook(\r\n allHooks: Record<string, unknown>,\r\n eventName: string,\r\n matcher: string,\r\n identifier: string,\r\n): number {\r\n const existing = allHooks[eventName]\r\n if (!Array.isArray(existing)) return 0\r\n const list = existing as HookEntry[]\r\n const matchEntry = list.find((e) => e.matcher === matcher)\r\n if (!matchEntry || !Array.isArray(matchEntry.hooks)) return 0\r\n\r\n const before = matchEntry.hooks.length\r\n matchEntry.hooks = matchEntry.hooks.filter(\r\n (h) => !(typeof h.command === 'string' && h.command.includes(identifier)),\r\n )\r\n const removed = before - matchEntry.hooks.length\r\n if (removed === 0) return 0\r\n\r\n // Clean empty matcher groups\r\n if (matchEntry.hooks.length === 0) {\r\n const idx = list.indexOf(matchEntry)\r\n if (idx >= 0) list.splice(idx, 1)\r\n }\r\n // Clean empty event\r\n if (list.length === 0) {\r\n delete allHooks[eventName]\r\n } else {\r\n allHooks[eventName] = list\r\n }\r\n return removed\r\n}\r\n\r\n/**\r\n * Options for upsertHook.\r\n * - `identifier`: a substring that uniquely identifies an existing handler of\r\n * the same kind so we can replace it in place. Defaults to \"token-optimizer\"\r\n * for our own hooks. For external hooks (e.g. serena-hooks), callers should\r\n * pass something like \"serena-hooks remind\".\r\n * - `useFlagDisambiguation`: if true, use the `--hook X` flag as extra\r\n * disambiguation so multiple token-optimizer hooks can coexist in the same\r\n * matcher group without trampling each other. Default true.\r\n */\r\ninterface UpsertHookOptions {\r\n identifier?: string\r\n useFlagDisambiguation?: boolean\r\n}\r\n\r\nfunction upsertHook(\r\n allHooks: Record<string, unknown>,\r\n eventName: string,\r\n matcher: string,\r\n command: string,\r\n upsertOpts: UpsertHookOptions = {},\r\n): void {\r\n const identifier = upsertOpts.identifier ?? 'token-optimizer'\r\n const useFlagDisambiguation = upsertOpts.useFlagDisambiguation ?? true\r\n\r\n const existing = (allHooks[eventName] ?? []) as HookEntry[]\r\n const list: HookEntry[] = Array.isArray(existing) ? [...existing] : []\r\n const matchEntry = list.find((e) => e.matcher === matcher)\r\n const ourHandler = { type: 'command', command }\r\n const ourFlag = useFlagDisambiguation ? extractHookFlag(command) : null\r\n\r\n if (matchEntry) {\r\n const handlers = Array.isArray(matchEntry.hooks) ? [...matchEntry.hooks] : []\r\n // 1) Preferred: find the exact handler we own by identifier + flag.\r\n let idx = -1\r\n if (ourFlag) {\r\n idx = handlers.findIndex(\r\n (h) =>\r\n typeof h.command === 'string' &&\r\n h.command.includes(identifier) &&\r\n extractHookFlag(h.command) === ourFlag,\r\n )\r\n }\r\n // 2) Fallback: any handler that includes the identifier (and doesn't\r\n // have a --hook flag of its own so we don't steal a sibling's slot).\r\n if (idx < 0) {\r\n idx = handlers.findIndex(\r\n (h) =>\r\n typeof h.command === 'string' &&\r\n h.command.includes(identifier) &&\r\n extractHookFlag(h.command) === null,\r\n )\r\n }\r\n\r\n if (idx >= 0) {\r\n handlers[idx] = ourHandler\r\n } else {\r\n handlers.push(ourHandler)\r\n }\r\n matchEntry.hooks = handlers\r\n } else {\r\n list.push({ matcher, hooks: [ourHandler] })\r\n }\r\n allHooks[eventName] = list\r\n}\r\n\r\nexport function runInstall(_args: string[] = [], opts: InstallOptions = {}): number {\r\n const home = opts.home ?? os.homedir()\r\n const cwd = opts.cwd ?? process.cwd()\r\n const print = opts.print ?? ((m: string) => console.error(m))\r\n\r\n const p = settingsPath(home)\r\n const settings = readSettings(p)\r\n\r\n // mcpServers upsert\r\n const mcpServers = (settings.mcpServers ?? {}) as Record<string, unknown>\r\n mcpServers[SERVER_NAME] = {\r\n command: 'npx',\r\n args: ['-y', '@cocaxcode/token-optimizer-mcp', '--mcp'],\r\n }\r\n settings.mcpServers = mcpServers\r\n\r\n // hooks upsert — prefer node direct for speed (~0.2s vs ~1.5s with npx)\r\n const hookBase = resolveHookCommandBase()\r\n const hooks = (settings.hooks ?? {}) as Record<string, unknown>\r\n upsertHook(hooks, 'PreToolUse', 'Bash', `${hookBase} --hook pretooluse`)\r\n upsertHook(hooks, 'PostToolUse', '*', `${hookBase} --hook posttooluse`)\r\n upsertHook(hooks, 'SessionStart', 'compact', `${hookBase} --hook sessionstart`)\r\n\r\n // Serena integration — two independent decisions based on two probe signals:\r\n //\r\n // (a) `--hook serena-activate` (our own SessionStart hook). Fixes the\r\n // ToolSearch gap in the official `serena-hooks activate` output. Does\r\n // NOT shell out to any binary — it's a node entry point that emits\r\n // JSON. Gated by `serena_mcp_registered` (i.e. the user uses Serena\r\n // as an MCP server at all).\r\n //\r\n // (b) The 3 OFFICIAL Serena reminder hooks (remind, auto-approve, cleanup).\r\n // These ARE invoked as `serena-hooks <cmd> ...` at runtime by Claude\r\n // Code, so they require the actual CLI binary to be on PATH. Gated by\r\n // `serena_cli_installed`. If the CLI disappears (user uninstalled\r\n // Serena, or the probe was wrong in a previous release), we actively\r\n // REMOVE the orphan entries so settings.json stops pointing at a\r\n // missing binary.\r\n //\r\n // Both blocks can be individually skipped via `skipSerenaHooks: true`.\r\n const serenaProbe = opts.serenaProbe ?? probeSerenaPresence()\r\n const wantOfficialHooks =\r\n serenaProbe.serena_cli_installed && opts.skipSerenaHooks !== true\r\n\r\n if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) {\r\n upsertHook(hooks, 'SessionStart', '', `${hookBase} --hook serena-activate`)\r\n }\r\n\r\n if (wantOfficialHooks) {\r\n upsertHook(hooks, 'PreToolUse', '', 'serena-hooks remind --client=claude-code', {\r\n identifier: 'serena-hooks remind',\r\n useFlagDisambiguation: false,\r\n })\r\n upsertHook(\r\n hooks,\r\n 'PreToolUse',\r\n 'mcp__serena__.*',\r\n 'serena-hooks auto-approve --client=claude-code',\r\n {\r\n identifier: 'serena-hooks auto-approve',\r\n useFlagDisambiguation: false,\r\n },\r\n )\r\n upsertHook(hooks, 'Stop', '', 'serena-hooks cleanup --client=claude-code', {\r\n identifier: 'serena-hooks cleanup',\r\n useFlagDisambiguation: false,\r\n })\r\n } else {\r\n // Reconcile: if the 3 official hooks were added by a previous install\r\n // (maybe from a buggier probe that accepted ~/.serena/ as sufficient),\r\n // but the CLI isn't actually available now, take them OUT so Claude\r\n // Code stops logging \"command not found\" on every hook dispatch.\r\n removeHook(hooks, 'PreToolUse', '', 'serena-hooks remind')\r\n removeHook(hooks, 'PreToolUse', 'mcp__serena__.*', 'serena-hooks auto-approve')\r\n removeHook(hooks, 'Stop', '', 'serena-hooks cleanup')\r\n }\r\n settings.hooks = hooks\r\n\r\n // Auto-activar shadow_measurement.serena si:\r\n // - Serena está registrada (MCP o CLI)\r\n // - El usuario NO ha puesto explícitamente el flag (ni true ni false)\r\n // Si ya lo tocó (aunque sea a false), respetamos su decisión.\r\n // Con el flag activo, cada call a serena en PostToolUse mide\r\n // shadow_delta_tokens = fullFileTokens - serena_output_tokens, que es lo que\r\n // xray enseña como ahorro real.\r\n let shadowAutoEnabled = false\r\n if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) {\r\n const configPath = getConfigPath(home)\r\n let userHasExplicitFlag = false\r\n try {\r\n if (fs.existsSync(configPath)) {\r\n const raw = fs.readFileSync(configPath, 'utf8')\r\n const parsed = JSON.parse(raw) as Record<string, unknown>\r\n const sm = parsed.shadow_measurement as Record<string, unknown> | undefined\r\n userHasExplicitFlag = sm !== undefined && sm !== null && 'serena' in sm\r\n }\r\n } catch {\r\n // archivo corrupto o no legible → tratamos como si no estuviera\r\n }\r\n if (!userHasExplicitFlag) {\r\n const cfg = loadConfig(home)\r\n if (!cfg.shadow_measurement.serena) {\r\n cfg.shadow_measurement.serena = true\r\n saveConfig(cfg, home)\r\n shadowAutoEnabled = true\r\n }\r\n }\r\n }\r\n\r\n writeSettings(p, settings)\r\n\r\n // Global storage dir\r\n const globalDir = path.join(home, '.token-optimizer')\r\n if (!fs.existsSync(globalDir)) fs.mkdirSync(globalDir, { recursive: true })\r\n\r\n // Per-project storage dir (only in git repos)\r\n if (fs.existsSync(path.join(cwd, '.git'))) {\r\n ensureGitignore(cwd)\r\n }\r\n\r\n print('token-optimizer-mcp instalado correctamente.')\r\n print(` settings: ${p}`)\r\n print(` global: ${globalDir}`)\r\n\r\n // Serena status — 3 states\r\n if (serenaProbe.serena_cli_installed) {\r\n print(` serena: CLI detectado — 4 hooks registrados`)\r\n print(` (activate + remind + auto-approve + cleanup)`)\r\n if (opts.skipSerenaHooks) {\r\n print(` note: --skipSerenaHooks activo → solo se registró serena-activate`)\r\n }\r\n } else if (serenaProbe.serena_mcp_registered) {\r\n print(` serena: MCP detectado pero CLI no instalado`)\r\n print(` → solo se registró serena-activate`)\r\n print(` → para los otros 3: uv tool install git+https://github.com/oraios/serena`)\r\n } else {\r\n print(` serena: no detectado — hooks de serena omitidos`)\r\n }\r\n\r\n if (shadowAutoEnabled) {\r\n print(` shadow_measurement.serena = true (auto-activado)`)\r\n print(` → mide ahorro real vs lectura completa de archivo por cada call`)\r\n }\r\n\r\n if (opts.runDoctorAtEnd !== false) {\r\n print('')\r\n runDoctor([], { cwd, home, print })\r\n }\r\n\r\n return 0\r\n}\r\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| } from "./chunk-NWTRLY2M.js"; | ||
| import "./chunk-ESRDZMZJ.js"; | ||
| import "./chunk-TOEPQYR3.js"; | ||
| import "./chunk-U3OXZD52.js"; | ||
| export { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| }; | ||
| //# sourceMappingURL=prune-mcp-PXUSFZJU.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-U3OXZD52.js"; | ||
| // src/cli/report.ts | ||
| import fs from "fs"; | ||
| var PERIOD_DAYS = { | ||
| session: 3650, | ||
| day: 1, | ||
| week: 7, | ||
| month: 30 | ||
| }; | ||
| function isMeasured(method) { | ||
| return method === "measured_exact" || method === "measured_delta"; | ||
| } | ||
| function queryBySourceAndMethod(db, sinceIso) { | ||
| return db.prepare( | ||
| `SELECT source, estimation_method, | ||
| COUNT(*) as count, | ||
| COALESCE(SUM(tokens_estimated), 0) as tokens | ||
| FROM tool_calls | ||
| WHERE created_at >= ? | ||
| GROUP BY source, estimation_method | ||
| ORDER BY tokens DESC` | ||
| ).all(sinceIso); | ||
| } | ||
| var REFERENCE_DATA = [ | ||
| { | ||
| feature: "Model switching (opusplan / default-to-sonnet)", | ||
| saving: "60-80% reduccion de coste", | ||
| source: "mindstudio.ai, verdent.ai, claudelab.net", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Progressive disclosure skills", | ||
| saving: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| source: "claudefast.com", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Prompt caching read hit", | ||
| saving: "10x mas barato que uncached", | ||
| source: "Anthropic docs", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Claude Code Tool Search", | ||
| saving: "~85% schema reduction (77k \u2192 8.7k)", | ||
| source: "observado en sesion", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "MCP pruning sobre Tool Search", | ||
| saving: "~5-12% adicional por turno", | ||
| source: "estimacion interna", | ||
| verified_at: "2026-04-11" | ||
| } | ||
| ]; | ||
| function resolvePeriod(args, fallback) { | ||
| const flag = args.find((a) => a.startsWith("--period=")); | ||
| if (flag) { | ||
| const value = flag.split("=")[1]; | ||
| if (value && value in PERIOD_DAYS) return value; | ||
| } | ||
| return fallback; | ||
| } | ||
| function runReport(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const period = opts.period ?? resolvePeriod(args, "day"); | ||
| const days = PERIOD_DAYS[period]; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const lines = []; | ||
| lines.push(`token-optimizer-mcp reporte \u2014 periodo: ${period} (${days} dia(s))`); | ||
| lines.push(""); | ||
| if (!fs.existsSync(dbPath)) { | ||
| lines.push("No hay datos registrados todavia."); | ||
| } else { | ||
| const db = getDb(dbPath); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const rows = queryBySourceAndMethod(db, since); | ||
| let medidoTotal = 0; | ||
| let estimadoTotal = 0; | ||
| lines.push("Por fuente y metodo de estimacion:"); | ||
| if (rows.length === 0) { | ||
| lines.push(" (sin eventos en este periodo)"); | ||
| } else { | ||
| for (const row of rows) { | ||
| const method = row.estimation_method ?? "unknown"; | ||
| lines.push( | ||
| ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens` | ||
| ); | ||
| if (isMeasured(method)) medidoTotal += row.tokens; | ||
| else estimadoTotal += row.tokens; | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push(`Resumen: Medido: ${medidoTotal} tokens \xB7 Estimado: ${estimadoTotal} tokens`); | ||
| lines.push(""); | ||
| } | ||
| lines.push("Coach activity:"); | ||
| lines.push(" (sin tips surfaceados todavia \u2014 coach layer se activa en Phase 4.H)"); | ||
| lines.push(""); | ||
| printReference(lines); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| function printReference(lines) { | ||
| lines.push("Referencia (datos publicos verificables):"); | ||
| for (const row of REFERENCE_DATA) { | ||
| lines.push(` \u2022 ${row.feature}`); | ||
| lines.push(` ahorro: ${row.saving}`); | ||
| lines.push(` fuente: ${row.source} \xB7 verificado: ${row.verified_at}`); | ||
| } | ||
| } | ||
| export { | ||
| runReport | ||
| }; | ||
| //# sourceMappingURL=report-XRFU55JC.js.map |
| {"version":3,"sources":["../src/cli/report.ts"],"sourcesContent":["// Report CLI — Phase 4.14\r\n// Per-source breakdown WITH estimation_method label + Medido/Estimado split +\r\n// reference-data table (coach-layer addendum CO-4). Spanish.\r\n\r\nimport fs from 'node:fs'\r\nimport type Database from 'better-sqlite3'\r\nimport { getDb } from '../db/connection.js'\r\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\r\n\r\ntype DB = Database.Database\r\n\r\ntype Period = 'session' | 'day' | 'week' | 'month'\r\n\r\nconst PERIOD_DAYS: Record<Period, number> = {\r\n session: 3650,\r\n day: 1,\r\n week: 7,\r\n month: 30,\r\n}\r\n\r\ninterface SourceMethodRow {\r\n source: string\r\n estimation_method: string | null\r\n count: number\r\n tokens: number\r\n}\r\n\r\nfunction isMeasured(method: string | null): boolean {\r\n return method === 'measured_exact' || method === 'measured_delta'\r\n}\r\n\r\nfunction queryBySourceAndMethod(db: DB, sinceIso: string): SourceMethodRow[] {\r\n return db\r\n .prepare(\r\n `SELECT source, estimation_method,\r\n COUNT(*) as count,\r\n COALESCE(SUM(tokens_estimated), 0) as tokens\r\n FROM tool_calls\r\n WHERE created_at >= ?\r\n GROUP BY source, estimation_method\r\n ORDER BY tokens DESC`,\r\n )\r\n .all(sinceIso) as SourceMethodRow[]\r\n}\r\n\r\nexport interface ReportOptions {\r\n cwd?: string\r\n period?: Period\r\n print?: (msg: string) => void\r\n}\r\n\r\nconst REFERENCE_DATA: Array<{\r\n feature: string\r\n saving: string\r\n source: string\r\n verified_at: string\r\n}> = [\r\n {\r\n feature: 'Model switching (opusplan / default-to-sonnet)',\r\n saving: '60-80% reduccion de coste',\r\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\r\n verified_at: '2026-04-11',\r\n },\r\n {\r\n feature: 'Progressive disclosure skills',\r\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\r\n source: 'claudefast.com',\r\n verified_at: '2026-04-11',\r\n },\r\n {\r\n feature: 'Prompt caching read hit',\r\n saving: '10x mas barato que uncached',\r\n source: 'Anthropic docs',\r\n verified_at: '2026-04-11',\r\n },\r\n {\r\n feature: 'Claude Code Tool Search',\r\n saving: '~85% schema reduction (77k → 8.7k)',\r\n source: 'observado en sesion',\r\n verified_at: '2026-04-11',\r\n },\r\n {\r\n feature: 'MCP pruning sobre Tool Search',\r\n saving: '~5-12% adicional por turno',\r\n source: 'estimacion interna',\r\n verified_at: '2026-04-11',\r\n },\r\n]\r\n\r\nfunction resolvePeriod(args: string[], fallback: Period): Period {\r\n const flag = args.find((a) => a.startsWith('--period='))\r\n if (flag) {\r\n const value = flag.split('=')[1] as Period | undefined\r\n if (value && value in PERIOD_DAYS) return value\r\n }\r\n return fallback\r\n}\r\n\r\nexport function runReport(args: string[] = [], opts: ReportOptions = {}): number {\r\n const print = opts.print ?? ((m: string) => console.error(m))\r\n const cwd = opts.cwd ?? process.cwd()\r\n const period: Period = opts.period ?? resolvePeriod(args, 'day')\r\n const days = PERIOD_DAYS[period]\r\n\r\n const projectDir = resolveProjectDir(cwd)\r\n const dbPath = resolveAnalyticsDbPath(projectDir)\r\n\r\n const lines: string[] = []\r\n lines.push(`token-optimizer-mcp reporte — periodo: ${period} (${days} dia(s))`)\r\n lines.push('')\r\n\r\n if (!fs.existsSync(dbPath)) {\r\n lines.push('No hay datos registrados todavia.')\r\n } else {\r\n const db = getDb(dbPath)\r\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\r\n const rows = queryBySourceAndMethod(db, since)\r\n\r\n let medidoTotal = 0\r\n let estimadoTotal = 0\r\n\r\n lines.push('Por fuente y metodo de estimacion:')\r\n if (rows.length === 0) {\r\n lines.push(' (sin eventos en este periodo)')\r\n } else {\r\n for (const row of rows) {\r\n const method = row.estimation_method ?? 'unknown'\r\n lines.push(\r\n ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens`,\r\n )\r\n if (isMeasured(method)) medidoTotal += row.tokens\r\n else estimadoTotal += row.tokens\r\n }\r\n }\r\n lines.push('')\r\n lines.push(`Resumen: Medido: ${medidoTotal} tokens · Estimado: ${estimadoTotal} tokens`)\r\n lines.push('')\r\n }\r\n\r\n // Coach activity section (always present; filled in Phase 4.H with real data)\r\n lines.push('Coach activity:')\r\n lines.push(' (sin tips surfaceados todavia — coach layer se activa en Phase 4.H)')\r\n lines.push('')\r\n\r\n printReference(lines)\r\n print(lines.join('\\n'))\r\n return 0\r\n}\r\n\r\nfunction printReference(lines: string[]): void {\r\n lines.push('Referencia (datos publicos verificables):')\r\n for (const row of REFERENCE_DATA) {\r\n lines.push(` • ${row.feature}`)\r\n lines.push(` ahorro: ${row.saving}`)\r\n lines.push(` fuente: ${row.source} · verificado: ${row.verified_at}`)\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;AAIA,OAAO,QAAQ;AASf,IAAM,cAAsC;AAAA,EAC1C,SAAS;AAAA,EACT,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AACT;AASA,SAAS,WAAW,QAAgC;AAClD,SAAO,WAAW,oBAAoB,WAAW;AACnD;AAEA,SAAS,uBAAuB,IAAQ,UAAqC;AAC3E,SAAO,GACJ;AAAA,IACC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOF,EACC,IAAI,QAAQ;AACjB;AAQA,IAAM,iBAKD;AAAA,EACH;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AACF;AAEA,SAAS,cAAc,MAAgB,UAA0B;AAC/D,QAAM,OAAO,KAAK,KAAK,CAAC,MAAM,EAAE,WAAW,WAAW,CAAC;AACvD,MAAI,MAAM;AACR,UAAM,QAAQ,KAAK,MAAM,GAAG,EAAE,CAAC;AAC/B,QAAI,SAAS,SAAS,YAAa,QAAO;AAAA,EAC5C;AACA,SAAO;AACT;AAEO,SAAS,UAAU,OAAiB,CAAC,GAAG,OAAsB,CAAC,GAAW;AAC/E,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,SAAiB,KAAK,UAAU,cAAc,MAAM,KAAK;AAC/D,QAAM,OAAO,YAAY,MAAM;AAE/B,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,+CAA0C,MAAM,KAAK,IAAI,UAAU;AAC9E,QAAM,KAAK,EAAE;AAEb,MAAI,CAAC,GAAG,WAAW,MAAM,GAAG;AAC1B,UAAM,KAAK,mCAAmC;AAAA,EAChD,OAAO;AACL,UAAM,KAAK,MAAM,MAAM;AACvB,UAAM,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAU,EAAE,YAAY;AACnE,UAAM,OAAO,uBAAuB,IAAI,KAAK;AAE7C,QAAI,cAAc;AAClB,QAAI,gBAAgB;AAEpB,UAAM,KAAK,oCAAoC;AAC/C,QAAI,KAAK,WAAW,GAAG;AACrB,YAAM,KAAK,iCAAiC;AAAA,IAC9C,OAAO;AACL,iBAAW,OAAO,MAAM;AACtB,cAAM,SAAS,IAAI,qBAAqB;AACxC,cAAM;AAAA,UACJ,KAAK,IAAI,OAAO,OAAO,CAAC,CAAC,KAAK,MAAM,MAAM,IAAI,KAAK,cAAc,IAAI,MAAM;AAAA,QAC7E;AACA,YAAI,WAAW,MAAM,EAAG,gBAAe,IAAI;AAAA,YACtC,kBAAiB,IAAI;AAAA,MAC5B;AAAA,IACF;AACA,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,oBAAoB,WAAW,0BAAuB,aAAa,SAAS;AACvF,UAAM,KAAK,EAAE;AAAA,EACf;AAGA,QAAM,KAAK,iBAAiB;AAC5B,QAAM,KAAK,4EAAuE;AAClF,QAAM,KAAK,EAAE;AAEb,iBAAe,KAAK;AACpB,QAAM,MAAM,KAAK,IAAI,CAAC;AACtB,SAAO;AACT;AAEA,SAAS,eAAe,OAAuB;AAC7C,QAAM,KAAK,2CAA2C;AACtD,aAAW,OAAO,gBAAgB;AAChC,UAAM,KAAK,YAAO,IAAI,OAAO,EAAE;AAC/B,UAAM,KAAK,eAAe,IAAI,MAAM,EAAE;AACtC,UAAM,KAAK,eAAe,IAAI,MAAM,qBAAkB,IAAI,WAAW,EAAE;AAAA,EACzE;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| getCoachSurfaceLog, | ||
| measureContextSize, | ||
| postSummaryToXray, | ||
| runRules | ||
| } from "./chunk-GSWB574D.js"; | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| rollback | ||
| } from "./chunk-NWTRLY2M.js"; | ||
| import { | ||
| ensureGitignore | ||
| } from "./chunk-N754BAPB.js"; | ||
| import "./chunk-METYJF7E.js"; | ||
| import { | ||
| buildSuggestions | ||
| } from "./chunk-EV4HR7LB.js"; | ||
| import { | ||
| checkSerenaHealth, | ||
| probeMcpPruning, | ||
| probePromptCaching, | ||
| probeRtk, | ||
| probeSerena | ||
| } from "./chunk-4MJNQPFS.js"; | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-ESRDZMZJ.js"; | ||
| import { | ||
| getCostReport, | ||
| getUsageStats | ||
| } from "./chunk-VD6RKGFO.js"; | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-AF6RQ5F5.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-U3OXZD52.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.7.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:" : (ensureGitignore(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-RIDC4JTC.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\r\n// Returns a configured McpServer. Tools are registered in later phases.\r\n\r\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport { getDb } from './db/connection.js'\r\nimport { resolveProjectDir, resolveAnalyticsDbPath } from './lib/paths.js'\r\nimport { ensureGitignore } from './lib/storage.js'\r\nimport { registerBudgetTools } from './tools/budget.js'\r\nimport { registerSessionTools } from './tools/session.js'\r\nimport { registerOrchestrationTools } from './tools/orchestration.js'\r\nimport { registerCoachTools } from './tools/coach.js'\r\nimport { registerToonTools } from './tools/toon.js'\r\nimport { registerCoachTipsResource } from './resources/coach-tips.js'\r\n\r\ndeclare const __PKG_VERSION__: string\r\nconst VERSION = typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : '0.1.0'\r\n\r\nconst INSTRUCTIONS = `token-optimizer-mcp: orchestration + observability + coach layer for Claude Code.\r\n\r\nMeasures tool usage, enforces token budgets, advises on complementary tools (serena, RTK),\r\nand proactively surfaces savings tips. Coach layer detects inefficiencies and suggests\r\noptimizations like opusplan, /compact, plan mode, and more.\r\n\r\nDoes NOT replace serena (symbolic file reads) or RTK (Bash output filtering) —\r\ncoordinates with them and adds measurements, budgets, compact recovery, and coaching.`\r\n\r\nexport interface CreateServerOptions {\r\n storageDir?: string\r\n projectDir?: string\r\n dbPath?: string\r\n}\r\n\r\nexport function createServer(options: CreateServerOptions = {}): McpServer {\r\n const resolvedProject = options.projectDir ?? resolveProjectDir()\r\n const dbPath =\r\n options.dbPath ??\r\n (options.storageDir === ':memory:'\r\n ? ':memory:'\r\n : (ensureGitignore(resolvedProject), resolveAnalyticsDbPath(resolvedProject)))\r\n\r\n // Initialize DB (schema created by getDb)\r\n const db = getDb(dbPath)\r\n\r\n const server = new McpServer(\r\n {\r\n name: 'token-optimizer-mcp',\r\n version: VERSION,\r\n },\r\n {\r\n instructions: INSTRUCTIONS,\r\n },\r\n )\r\n\r\n // Phase 2 tools\r\n registerBudgetTools(server, db)\r\n // Phase 3 tools\r\n registerSessionTools(server, db)\r\n // Phase 4 tools\r\n registerOrchestrationTools(server, db)\r\n registerCoachTools(server, db)\r\n registerCoachTipsResource(server, db)\r\n // Phase 5 tools\r\n registerToonTools(server)\r\n\r\n return server\r\n}\r\n","// Budget MCP tools — Phase 2.4\r\n// budget_set, budget_check, budget_report\r\n\r\nimport { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type Database from 'better-sqlite3'\r\nimport { BudgetManager } from '../services/budget-manager.js'\r\nimport { text, error } from '../lib/response.js'\r\n\r\ntype DB = Database.Database\r\n\r\nconst DAY_MS = 86_400_000\r\n\r\nfunction sinceForPeriod(period: 'session' | 'day' | 'week' | 'month'): string {\r\n const now = Date.now()\r\n switch (period) {\r\n case 'day':\r\n return new Date(now - DAY_MS).toISOString()\r\n case 'week':\r\n return new Date(now - 7 * DAY_MS).toISOString()\r\n case 'month':\r\n return new Date(now - 30 * DAY_MS).toISOString()\r\n case 'session':\r\n default:\r\n return '1970-01-01T00:00:00.000Z'\r\n }\r\n}\r\n\r\nexport function registerBudgetTools(server: McpServer, db: DB): void {\r\n const manager = new BudgetManager(db)\r\n\r\n // ── budget_set ──\r\n server.tool(\r\n 'budget_set',\r\n 'Define o actualiza un presupuesto de tokens. Precedencia: session > project. Modo warn avisa al exceder.',\r\n {\r\n scope: z.enum(['session', 'project']).describe('Ambito del presupuesto'),\r\n scope_key: z.string().min(1).describe('Clave del scope (sessionId o projectHash)'),\r\n limit_tokens: z\r\n .number()\r\n .int()\r\n .positive()\r\n .max(10_000_000)\r\n .describe('Limite en tokens (1..10_000_000)'),\r\n },\r\n async ({ scope, scope_key, limit_tokens }) => {\r\n try {\r\n const budget = manager.setBudget({ scope, scope_key, limit_tokens })\r\n return text(\r\n [\r\n 'Presupuesto guardado:',\r\n '',\r\n ` scope: ${budget.scope}`,\r\n ` scope_key: ${budget.scope_key}`,\r\n ` limit_tokens: ${budget.limit_tokens}`,\r\n ` mode: ${budget.mode}`,\r\n ].join('\\n'),\r\n )\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n\r\n // ── budget_check ──\r\n server.tool(\r\n 'budget_check',\r\n 'Consulta el estado del presupuesto activo (gasto actual, restante y porcentaje).',\r\n {\r\n session_id: z.string().optional().describe('ID de sesion (default: \"default\")'),\r\n project_hash: z\r\n .string()\r\n .optional()\r\n .describe('Hash del proyecto para fallback a scope project'),\r\n },\r\n async ({ session_id, project_hash }) => {\r\n try {\r\n const status = manager.checkBudget(session_id ?? 'default', project_hash ?? null)\r\n if (!status.active) {\r\n return text('Sin presupuesto activo para la sesion/proyecto actual.')\r\n }\r\n const percent = (status.percent_used * 100).toFixed(1)\r\n return text(\r\n [\r\n 'Estado del presupuesto:',\r\n '',\r\n ` gastado: ${status.spent} tokens`,\r\n ` restante: ${status.remaining} tokens`,\r\n ` uso: ${percent}%`,\r\n ` modo: ${status.mode ?? 'n/a'}`,\r\n ].join('\\n'),\r\n )\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n\r\n // ── budget_report ──\r\n server.tool(\r\n 'budget_report',\r\n 'Muestra el consumo de tokens agrupado por herramienta y por fuente durante un periodo.',\r\n {\r\n period: z\r\n .enum(['session', 'day', 'week', 'month'])\r\n .optional()\r\n .describe('Periodo del reporte (default: day)'),\r\n },\r\n async ({ period }) => {\r\n try {\r\n const since = sinceForPeriod(period ?? 'day')\r\n const report = manager.getBudgetReport(since)\r\n const lines = [`Reporte de consumo (desde ${report.period_since}):`, '']\r\n lines.push('Por herramienta:')\r\n if (report.by_tool.length === 0) {\r\n lines.push(' (sin datos)')\r\n } else {\r\n for (const row of report.by_tool) {\r\n lines.push(` ${row.tool_name}: ${row.count} llamadas, ${row.tokens} tokens`)\r\n }\r\n }\r\n lines.push('')\r\n lines.push('Por fuente:')\r\n if (report.by_source.length === 0) {\r\n lines.push(' (sin datos)')\r\n } else {\r\n for (const row of report.by_source) {\r\n lines.push(` ${row.source}: ${row.count} llamadas, ${row.tokens} tokens`)\r\n }\r\n }\r\n return text(lines.join('\\n'))\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n}\r\n","// Shared MCP tool response helpers — Phase 2\r\n\r\nexport const text = (t: string) => ({\r\n content: [{ type: 'text' as const, text: t }],\r\n})\r\n\r\nexport const error = (t: string) => ({\r\n content: [{ type: 'text' as const, text: `Error: ${t}` }],\r\n isError: true as const,\r\n})\r\n","// Session tools — Phase 3.3 (simplified: session_search removed, FTS5 no longer available)\r\n\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type Database from 'better-sqlite3'\r\n\r\ntype DB = Database.Database\r\n\r\n// No session tools registered after FTS5 removal.\r\n// Keeping the function signature for backwards compatibility with server.ts imports.\r\nexport function registerSessionTools(_server: McpServer, _db: DB): void {\r\n // noop\r\n}\r\n","// Orchestration MCP tools — Phase 4.23-4.28\r\n// mcp_usage_stats, mcp_cost_report, optimization_status,\r\n// mcp_prune_suggest, mcp_prune_apply, mcp_prune_rollback, mcp_prune_clear\r\n\r\nimport { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type Database from 'better-sqlite3'\r\nimport { text, error } from '../lib/response.js'\r\nimport { getUsageStats, getCostReport } from '../services/stats.js'\r\nimport {\r\n probeSerena,\r\n probeRtk,\r\n probeMcpPruning,\r\n probePromptCaching,\r\n checkSerenaHealth,\r\n} from '../orchestration/detector.js'\r\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\r\nimport { buildSuggestions } from '../orchestration/advisor.js'\r\nimport {\r\n generateFromHistory,\r\n applyAllowlist,\r\n rollback,\r\n clearAllowlist,\r\n} from '../cli/prune-mcp.js'\r\nimport type { OptimizationStatus } from '../lib/types.js'\r\nimport { buildSessionSummary } from '../services/session-summary-builder.js'\r\nimport { postSummaryToXray } from '../services/xray-client.js'\r\n\r\ntype DB = Database.Database\r\n\r\nexport function registerOrchestrationTools(server: McpServer, db: DB): void {\r\n // ── mcp_usage_stats ──\r\n server.tool(\r\n 'mcp_usage_stats',\r\n 'Estadisticas de uso de tokens por herramienta y fuente en un periodo.',\r\n {\r\n days: z.number().int().positive().max(365).optional().describe('Dias a analizar (default: 7)'),\r\n },\r\n async ({ days }) => {\r\n try {\r\n const stats = getUsageStats(db, days ?? 7)\r\n const lines = [\r\n `Uso en los ultimos ${stats.period_days} dia(s):`,\r\n '',\r\n `Total: ${stats.total_tokens} tokens, ${stats.total_events} eventos`,\r\n '',\r\n 'Por fuente:',\r\n ]\r\n if (stats.by_source.length === 0) {\r\n lines.push(' (sin datos)')\r\n } else {\r\n for (const row of stats.by_source) {\r\n lines.push(` ${row.source}: ${row.tokens} tokens, ${row.count} llamadas`)\r\n }\r\n }\r\n lines.push('')\r\n lines.push('Top herramientas:')\r\n if (stats.by_tool.length === 0) {\r\n lines.push(' (sin datos)')\r\n } else {\r\n for (const row of stats.by_tool.slice(0, 10)) {\r\n lines.push(` ${row.tool_name}: ${row.tokens} tokens, ${row.count} llamadas`)\r\n }\r\n }\r\n return text(lines.join('\\n'))\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n\r\n // ── mcp_cost_report ──\r\n server.tool(\r\n 'mcp_cost_report',\r\n 'Reporte de coste estimado con rango Haiku-Sonnet-Opus y disclaimer honesto.',\r\n {\r\n days: z\r\n .number()\r\n .int()\r\n .positive()\r\n .max(365)\r\n .optional()\r\n .describe('Dias a analizar (default: 7)'),\r\n },\r\n async ({ days }) => {\r\n try {\r\n const cost = getCostReport(db, days ?? 7)\r\n const lines = [\r\n `Reporte de coste (${cost.period_days} dia(s)):`,\r\n '',\r\n `Tokens totales: ${cost.total_tokens}`,\r\n `Coste estimado (input pricing):`,\r\n ` Haiku 4.5: $${cost.estimated_cost_usd_haiku.toFixed(4)} ($1/MTok)`,\r\n ` Sonnet 4.6: $${cost.estimated_cost_usd_sonnet.toFixed(4)} ($3/MTok)`,\r\n ` Opus 4.6: $${cost.estimated_cost_usd_opus.toFixed(4)} ($5/MTok)`,\r\n '',\r\n `Nota: ${cost.disclaimer}`,\r\n ]\r\n return text(lines.join('\\n'))\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n\r\n // ── optimization_status ──\r\n server.tool(\r\n 'optimization_status',\r\n 'Estado de las optimizaciones detectadas: serena, RTK, MCP pruning, prompt caching, schema size.',\r\n {},\r\n async () => {\r\n try {\r\n const serena = probeSerena()\r\n const rtk = probeRtk()\r\n const pruning = probeMcpPruning()\r\n const pcProbe = probePromptCaching()\r\n void pcProbe\r\n const schema = measureCurrentSchemaBytes()\r\n // Always include prompt_caching with explicit estimation_method per measurement-honesty spec\r\n const status: OptimizationStatus = {\r\n serena,\r\n rtk,\r\n mcp_pruning: pruning,\r\n prompt_caching: {\r\n active_by_default: true,\r\n savings_tokens: null,\r\n estimation_method: 'unknown',\r\n note: 'Revisa tu factura Anthropic para confirmar el ahorro real',\r\n },\r\n schema_bytes: {\r\n tool_schema_bytes: schema.tool_schema_bytes,\r\n measurement_method: schema.measurement_method,\r\n },\r\n }\r\n const serenaHealth = serena.present ? checkSerenaHealth() : []\r\n const suggestions = buildSuggestions(status)\r\n\r\n // Fire-and-forget summary to xray (if XRAY_URL is set)\r\n try {\r\n const lastSession = db\r\n .prepare('SELECT id FROM sessions ORDER BY started_at DESC LIMIT 1')\r\n .get() as { id: string } | undefined\r\n if (lastSession) {\r\n const summary = buildSessionSummary(db, lastSession.id, '0.2.6')\r\n void postSummaryToXray(summary as unknown as Record<string, unknown>).catch(() => {})\r\n }\r\n } catch {\r\n // Silent — xray is optional\r\n }\r\n\r\n return text(JSON.stringify({ status, serena_health: serenaHealth, suggestions }, null, 2))\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n\r\n // ── mcp_prune_suggest ──\r\n server.tool(\r\n 'mcp_prune_suggest',\r\n 'Genera un allowlist de MCPs basandose en el historial (NO modifica archivos).',\r\n {\r\n days: z\r\n .number()\r\n .int()\r\n .positive()\r\n .max(365)\r\n .optional()\r\n .describe('Dias de historial a analizar (default: 14)'),\r\n },\r\n async ({ days }) => {\r\n try {\r\n const proposal = generateFromHistory({ days: days ?? 14 })\r\n return text(JSON.stringify(proposal, null, 2))\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n\r\n // ── mcp_prune_apply ──\r\n server.tool(\r\n 'mcp_prune_apply',\r\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.',\r\n {\r\n allowlist: z\r\n .array(z.string())\r\n .optional()\r\n .describe('Nombres de MCPs a permitir (lista blanca). Exclusivo con exclude.'),\r\n exclude: z\r\n .array(z.string())\r\n .optional()\r\n .describe(\r\n 'Nombres de MCPs a desactivar (lista negra). Internamente se traduce a allowlist = registrados - exclude. Exclusivo con allowlist.',\r\n ),\r\n confirm: z.boolean().describe('Debe ser true para confirmar la escritura'),\r\n },\r\n async ({ allowlist, exclude, confirm }) => {\r\n try {\r\n if (confirm !== true) {\r\n return error(\r\n 'Operacion destructiva: requiere confirm:true. Revisa el allowlist antes de aplicar.',\r\n )\r\n }\r\n const hasAllow = Array.isArray(allowlist)\r\n const hasExclude = Array.isArray(exclude)\r\n if (hasAllow === hasExclude) {\r\n return error(\r\n 'Debes pasar exactamente uno: allowlist (los que SI quieres) o exclude (los que NO quieres).',\r\n )\r\n }\r\n\r\n const schema = measureCurrentSchemaBytes()\r\n const registered = new Set(schema.mcp_servers)\r\n\r\n let effective: string[]\r\n let translationNote = ''\r\n\r\n if (hasAllow) {\r\n effective = allowlist as string[]\r\n if (registered.size > 0) {\r\n const invalid = effective.filter((s) => !registered.has(s))\r\n if (invalid.length > 0) {\r\n return error(\r\n `Allowlist contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\r\n )\r\n }\r\n }\r\n } else {\r\n const excludeSet = new Set(exclude as string[])\r\n if (registered.size > 0) {\r\n const invalid = (exclude as string[]).filter((s) => !registered.has(s))\r\n if (invalid.length > 0) {\r\n return error(\r\n `Exclude contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\r\n )\r\n }\r\n }\r\n effective = [...registered].filter((s) => !excludeSet.has(s))\r\n translationNote = `\\n exclude: [${(exclude as string[]).join(', ')}]\\n → allowlist efectivo: [${effective.join(', ')}]`\r\n }\r\n\r\n const applied = applyAllowlist(effective, { source: 'mcp' })\r\n return text(\r\n `Allowlist aplicado.${translationNote}\\n settings: ${applied.settings_path}\\n backup: ${applied.backup_path}`,\r\n )\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n\r\n // ── mcp_prune_rollback ──\r\n server.tool(\r\n 'mcp_prune_rollback',\r\n 'Restaura el backup mas reciente de settings.local.json. Requiere confirm:true.',\r\n {\r\n confirm: z.boolean(),\r\n to: z.string().optional().describe('Timestamp opcional del backup a restaurar'),\r\n },\r\n async ({ confirm, to }) => {\r\n try {\r\n if (confirm !== true) {\r\n return error('Operacion destructiva: requiere confirm:true.')\r\n }\r\n const result = rollback(to !== undefined ? { to } : {})\r\n if (!result.restored) return error('No hay backups disponibles.')\r\n return text(`Restaurado desde ${result.from}`)\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n\r\n // ── mcp_prune_clear ──\r\n server.tool(\r\n 'mcp_prune_clear',\r\n 'Elimina el allowlist de settings.local.json (crea backup). Requiere confirm:true.',\r\n {\r\n confirm: z.boolean(),\r\n },\r\n async ({ confirm }) => {\r\n try {\r\n if (confirm !== true) {\r\n return error('Operacion destructiva: requiere confirm:true.')\r\n }\r\n const result = clearAllowlist()\r\n return text(\r\n result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar',\r\n )\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n}\r\n","// Session summary builder for xray integration.\r\n// Aggregates all local data sources into a single payload for xray.\r\n// Only called once per session (not in PostToolUse hot path).\r\n\r\nimport type Database from 'better-sqlite3'\r\nimport { getUsageStats, getCostReport } from './stats.js'\r\nimport {\r\n probeSerena,\r\n probeRtk,\r\n probeMcpPruning,\r\n probePromptCaching,\r\n} from '../orchestration/detector.js'\r\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\r\nimport { getCoachSurfaceLog } from '../coach/surface.js'\r\nimport { resolveProjectDir } from '../lib/paths.js'\r\n\r\ntype DB = Database.Database\r\n\r\nexport interface XraySummaryPayload {\r\n session_id: string\r\n project_path: string\r\n project_name: string\r\n total_tokens: number\r\n total_events: number\r\n by_source: Array<{ source: string; count: number; tokens: number }>\r\n by_tool: Array<{ tool_name: string; count: number; tokens: number }>\r\n cost_haiku: number\r\n cost_sonnet: number\r\n cost_opus: number\r\n probes: {\r\n serena: { present: boolean; confidence: number; signals: string[] }\r\n rtk: { present: boolean; confidence: number; signals: string[] }\r\n mcp_pruning: { present: boolean; confidence: number; signals: string[] }\r\n prompt_caching: { present: boolean; confidence: number }\r\n }\r\n coach_tips_surfaced: Array<{ rule_id: string; tip_ids: string[]; severity: string }>\r\n schema_measurement: { tool_schema_tokens: number; mcp_servers: string[] }\r\n optimizer_version: string\r\n}\r\n\r\nexport function buildSessionSummary(\r\n db: DB,\r\n sessionId: string,\r\n version: string,\r\n): XraySummaryPayload {\r\n // Usage stats for last 24h (covers the session)\r\n const usage = getUsageStats(db, 1)\r\n const cost = getCostReport(db, 1)\r\n\r\n // Detection probes (reads local files, no network)\r\n const serena = probeSerena()\r\n const rtk = probeRtk()\r\n const mcpPruning = probeMcpPruning()\r\n const promptCaching = probePromptCaching()\r\n\r\n // Schema measurement (reads settings files, no network)\r\n const schema = measureCurrentSchemaBytes()\r\n\r\n // Coach tips surfaced during this session\r\n const coachTips = getCoachSurfaceLog(db, sessionId)\r\n\r\n const projDir = resolveProjectDir()\r\n const projName = projDir.split(/[\\\\/]/).filter(Boolean).pop() ?? 'unknown'\r\n\r\n return {\r\n session_id: sessionId,\r\n project_path: projDir,\r\n project_name: projName,\r\n total_tokens: usage.total_tokens,\r\n total_events: usage.total_events,\r\n by_source: usage.by_source,\r\n by_tool: usage.by_tool.map((t) => ({\r\n tool_name: t.tool_name,\r\n count: t.count,\r\n tokens: t.tokens,\r\n })),\r\n cost_haiku: cost.estimated_cost_usd_haiku,\r\n cost_sonnet: cost.estimated_cost_usd_sonnet,\r\n cost_opus: cost.estimated_cost_usd_opus,\r\n probes: {\r\n serena: { present: serena.present, confidence: serena.confidence, signals: serena.signals },\r\n rtk: { present: rtk.present, confidence: rtk.confidence, signals: rtk.signals },\r\n mcp_pruning: {\r\n present: mcpPruning.present,\r\n confidence: mcpPruning.confidence,\r\n signals: mcpPruning.signals,\r\n },\r\n prompt_caching: { present: promptCaching.present, confidence: promptCaching.confidence },\r\n },\r\n coach_tips_surfaced: coachTips,\r\n schema_measurement: {\r\n tool_schema_tokens: schema.tool_schema_tokens,\r\n mcp_servers: schema.mcp_servers,\r\n },\r\n optimizer_version: version,\r\n }\r\n}\r\n","// Coach MCP tool — Phase 4.46\r\n// coach_tips: returns active hits + full knowledge base + context measurement + reference table\r\n\r\nimport { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type Database from 'better-sqlite3'\r\nimport { text, error } from '../lib/response.js'\r\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\r\n\r\ntype DB = Database.Database\r\n\r\nexport function registerCoachTools(server: McpServer, db: DB): void {\r\n server.tool(\r\n 'coach_tips',\r\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).',\r\n {\r\n session_id: z.string().optional().describe('ID de la sesion (default: \"default\")'),\r\n project_dir: z\r\n .string()\r\n .optional()\r\n .describe('Directorio del proyecto para medir contexto desde transcript JSONL'),\r\n active_model: z\r\n .string()\r\n .optional()\r\n .describe('Modelo activo (opcional, habilita regla detect-opus-for-simple-task)'),\r\n verbose: z\r\n .boolean()\r\n .optional()\r\n .describe(\r\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.',\r\n ),\r\n },\r\n async ({ session_id, project_dir, active_model, verbose }) => {\r\n try {\r\n const payloadOpts: Parameters<typeof computeCoachTipsPayload>[0] = { db }\r\n if (session_id !== undefined) payloadOpts.sessionId = session_id\r\n if (project_dir !== undefined) payloadOpts.projectDir = project_dir\r\n if (active_model !== undefined) payloadOpts.activeModel = active_model\r\n const response = await computeCoachTipsPayload(payloadOpts)\r\n\r\n // Default compact mode: strip the heavy knowledge_base + reference_data\r\n // to avoid burning ~3.3k tokens per call. verbose=true restores them.\r\n if (verbose !== true) {\r\n const { known_tricks: _kb, reference_data: _ref, ...compact } = response\r\n return text(JSON.stringify(compact, null, 2))\r\n }\r\n return text(JSON.stringify(response, null, 2))\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n}\r\n","// Reference data table with publicly-verifiable savings numbers — Phase 4.41\r\n// Every row tagged estimation_method: 'reference_measured'\r\n\r\nimport type { EstimationMethod } from '../lib/types.js'\r\n\r\nexport interface ReferenceDataRow {\r\n feature: string\r\n saving: string\r\n source: string\r\n verified_at: string\r\n estimation_method: EstimationMethod\r\n}\r\n\r\nexport const REFERENCE_DATA: readonly ReferenceDataRow[] = [\r\n {\r\n feature: 'Model switching (opusplan / default-to-sonnet)',\r\n saving: '60-80% reduccion de coste',\r\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\r\n verified_at: '2026-04-11',\r\n estimation_method: 'reference_measured',\r\n },\r\n {\r\n feature: 'Progressive disclosure skills',\r\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\r\n source: 'claudefast.com',\r\n verified_at: '2026-04-11',\r\n estimation_method: 'reference_measured',\r\n },\r\n {\r\n feature: 'Prompt caching read hit',\r\n saving: '10x mas barato que uncached',\r\n source: 'Anthropic docs',\r\n verified_at: '2026-04-11',\r\n estimation_method: 'reference_measured',\r\n },\r\n {\r\n feature: 'Claude Code Tool Search',\r\n saving: '~85% schema reduction (77k → 8.7k tokens)',\r\n source: 'observado en sesion',\r\n verified_at: '2026-04-11',\r\n estimation_method: 'reference_measured',\r\n },\r\n {\r\n feature: 'MCP pruning sobre Tool Search',\r\n saving: '~5-12% adicional por turno',\r\n source: 'estimacion interna',\r\n verified_at: '2026-04-11',\r\n estimation_method: 'reference_measured',\r\n },\r\n]\r\n\r\nconst DAY_MS = 86_400_000\r\n\r\nexport function getFreshRows(\r\n daysThreshold = 90,\r\n today: Date = new Date(),\r\n): ReferenceDataRow[] {\r\n const cutoff = today.getTime() - daysThreshold * DAY_MS\r\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() >= cutoff)\r\n}\r\n\r\nexport function getStaleRows(\r\n daysThreshold = 90,\r\n today: Date = new Date(),\r\n): ReferenceDataRow[] {\r\n const cutoff = today.getTime() - daysThreshold * DAY_MS\r\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() < cutoff)\r\n}\r\n","// Shared payload builder for coach_tips MCP tool + token-optimizer://coach/tips\r\n// resource. Keeps tool/resource outputs identical — Phase 4.H.\r\n\r\nimport type Database from 'better-sqlite3'\r\nimport type { ContextMeasurement, EventContext, ToolEvent, DetectionHit, CoachTip } from '../lib/types.js'\r\nimport { KNOWLEDGE_BASE } from './knowledge-base.js'\r\nimport { REFERENCE_DATA, getStaleRows } from './reference-data.js'\r\nimport { runRules } from './detector.js'\r\nimport { measureContextSize } from './context-meter.js'\r\nimport { buildQueries } from '../db/queries.js'\r\n\r\ntype DB = Database.Database\r\n\r\nexport interface CoachTipsPayload {\r\n current: DetectionHit[]\r\n known_tricks: readonly CoachTip[]\r\n context: ContextMeasurement\r\n reference_data: typeof REFERENCE_DATA\r\n stale_reference_count: number\r\n last_computed_at: string\r\n}\r\n\r\nexport interface ComputeCoachTipsPayloadOptions {\r\n db: DB\r\n sessionId?: string\r\n projectDir?: string\r\n activeModel?: string\r\n}\r\n\r\nexport async function computeCoachTipsPayload(\r\n opts: ComputeCoachTipsPayloadOptions,\r\n): Promise<CoachTipsPayload> {\r\n const { db } = opts\r\n const sessionId = opts.sessionId ?? 'default'\r\n\r\n const contextOpts: Parameters<typeof measureContextSize>[1] = { db }\r\n if (opts.projectDir !== undefined) contextOpts.projectDir = opts.projectDir\r\n if (opts.activeModel !== undefined) contextOpts.activeModel = opts.activeModel\r\n const context = await measureContextSize(sessionId, contextOpts)\r\n\r\n const queries = buildQueries(db)\r\n const since = new Date(Date.now() - 86_400_000).toISOString()\r\n const rawRows = queries.getToolCallsSince(since) as ToolEvent[]\r\n const events = rawRows.slice(0, 100)\r\n\r\n const ctx: EventContext = {\r\n session_id: sessionId,\r\n events,\r\n session_token_total: context.tokens,\r\n session_token_method: context.estimation_method,\r\n session_token_limit: context.limit,\r\n active_model: opts.activeModel ?? null,\r\n }\r\n\r\n const hits = runRules(ctx)\r\n const staleTips = getStaleRows()\r\n\r\n return {\r\n current: hits,\r\n known_tricks: KNOWLEDGE_BASE,\r\n context,\r\n reference_data: REFERENCE_DATA,\r\n stale_reference_count: staleTips.length,\r\n last_computed_at: new Date().toISOString(),\r\n }\r\n}\r\n","// TOON encoding tools — Phase 5.3\r\n// toon_encode: data -> compact JSON (no whitespace) = token-efficient\r\n// toon_decode: toon string -> JSON object\r\n//\r\n// Note: the original `toon-format` npm package was deferred during Phase 0\r\n// due to package-name uncertainty. This implementation uses compact JSON under\r\n// the hood, which is round-trip lossless and ~30-40% cheaper in tokens than\r\n// pretty-printed JSON. The tool names are preserved so a real TOON impl can\r\n// drop in later without changing the MCP API.\r\n\r\nimport { z } from 'zod'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport { text, error } from '../lib/response.js'\r\n\r\nfunction compactEncode(data: unknown): string {\r\n try {\r\n return JSON.stringify(data)\r\n } catch (e) {\r\n const msg = e instanceof Error ? e.message : String(e)\r\n if (/circular|cyclic/i.test(msg)) {\r\n throw new Error('Referencia circular detectada: TOON no soporta objetos ciclicos')\r\n }\r\n throw new Error(`No se pudo codificar a TOON: ${msg}`)\r\n }\r\n}\r\n\r\nfunction compactDecode(toon: string): unknown {\r\n try {\r\n return JSON.parse(toon)\r\n } catch (e) {\r\n throw new Error(`TOON invalido: ${e instanceof Error ? e.message : String(e)}`)\r\n }\r\n}\r\n\r\nexport function registerToonTools(server: McpServer): void {\r\n // ── toon_encode ──\r\n server.tool(\r\n 'toon_encode',\r\n 'Codifica un objeto JSON a formato TOON (JSON compacto token-eficiente, round-trip lossless).',\r\n {\r\n data: z.unknown().describe('Valor a codificar (objeto, array, primitivo)'),\r\n },\r\n async ({ data }) => {\r\n try {\r\n const encoded = compactEncode(data)\r\n return text(encoded)\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n\r\n // ── toon_decode ──\r\n server.tool(\r\n 'toon_decode',\r\n 'Decodifica una cadena TOON a JSON. Devuelve el objeto formateado para lectura.',\r\n {\r\n toon: z.string().min(1).describe('Cadena TOON a decodificar'),\r\n },\r\n async ({ toon }) => {\r\n try {\r\n const decoded = compactDecode(toon)\r\n return text(JSON.stringify(decoded, null, 2))\r\n } catch (e) {\r\n return error(e instanceof Error ? e.message : String(e))\r\n }\r\n },\r\n )\r\n}\r\n\r\n// Exported for tests\r\nexport const _internal = { compactEncode, compactDecode }\r\n","// token-optimizer://coach/tips resource — Phase 4.H\r\n// Mirrors coach_tips() tool payload, readable without a tool call.\r\n\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport type Database from 'better-sqlite3'\r\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\r\n\r\ntype DB = Database.Database\r\n\r\nexport const COACH_TIPS_URI = 'token-optimizer://coach/tips'\r\n\r\nexport function registerCoachTipsResource(server: McpServer, db: DB): void {\r\n server.resource(\r\n 'coach-tips',\r\n COACH_TIPS_URI,\r\n {\r\n description:\r\n 'Tips activos del coach, catalogo completo de trucos, medicion de contexto y tabla de referencia.',\r\n mimeType: 'application/json',\r\n },\r\n async (uri: URL) => {\r\n try {\r\n const payload = await computeCoachTipsPayload({ db })\r\n return {\r\n contents: [\r\n {\r\n uri: uri.href,\r\n mimeType: 'application/json',\r\n text: JSON.stringify(payload, null, 2),\r\n },\r\n ],\r\n }\r\n } catch (e) {\r\n const message = e instanceof Error ? e.message : String(e)\r\n return {\r\n contents: [\r\n {\r\n uri: uri.href,\r\n mimeType: 'application/json',\r\n text: JSON.stringify({ error: message }),\r\n },\r\n ],\r\n }\r\n }\r\n },\r\n )\r\n}\r\n"],"mappings":"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","names":["z","z","z","DAY_MS","DAY_MS","z","z","z"]} |
| #!/usr/bin/env node | ||
| import { | ||
| getActiveBudgetSummary, | ||
| getUsageStats | ||
| } from "./chunk-VD6RKGFO.js"; | ||
| import "./chunk-AF6RQ5F5.js"; | ||
| import "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| projectHash, | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-U3OXZD52.js"; | ||
| // src/cli/status.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| function runStatus(_args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = path.join(home, ".claude", "settings.json"); | ||
| const installed = (() => { | ||
| try { | ||
| if (!fs.existsSync(settingsPath)) return false; | ||
| const json = JSON.parse(fs.readFileSync(settingsPath, "utf8")); | ||
| const mcp = json.mcpServers ?? {}; | ||
| return "token-optimizer" in mcp; | ||
| } catch { | ||
| return false; | ||
| } | ||
| })(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| let eventsToday = 0; | ||
| let tokensBySource = []; | ||
| let budgetLine = "sin presupuesto activo"; | ||
| if (fs.existsSync(dbPath)) { | ||
| try { | ||
| const db = getDb(dbPath); | ||
| const usage = getUsageStats(db, 1); | ||
| eventsToday = usage.total_events; | ||
| tokensBySource = usage.by_source.map((r) => ({ source: r.source, tokens: r.tokens })); | ||
| const budget = getActiveBudgetSummary(db, "default", projectHash(projectDir)); | ||
| if (budget.active) { | ||
| const pct = (budget.percent_used * 100).toFixed(1); | ||
| budgetLine = `gastado=${budget.spent} restante=${budget.remaining} uso=${pct}% modo=${budget.mode}`; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| const lines = []; | ||
| lines.push("token-optimizer-mcp status"); | ||
| lines.push(""); | ||
| lines.push(`Instalado: ${installed ? "\u2713" : "\u2717"} (${settingsPath})`); | ||
| lines.push(`Storage DB: ${dbPath}${fs.existsSync(dbPath) ? "" : " (no existe aun)"}`); | ||
| lines.push(`Eventos hoy: ${eventsToday}`); | ||
| lines.push( | ||
| `Tokens por fuente: ${tokensBySource.length > 0 ? tokensBySource.map((s) => `${s.source}=${s.tokens}`).join(", ") : "(sin datos)"}` | ||
| ); | ||
| lines.push(`Presupuesto: ${budgetLine}`); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| export { | ||
| runStatus | ||
| }; | ||
| //# sourceMappingURL=status-435S7GQI.js.map |
| {"version":3,"sources":["../src/cli/status.ts"],"sourcesContent":["// Status CLI — Phase 4.13\r\n// Prints install detection, storage DB, events today, tokens by source, active budget.\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport os from 'node:os'\r\nimport { getDb } from '../db/connection.js'\r\nimport { resolveProjectDir, resolveAnalyticsDbPath, projectHash } from '../lib/paths.js'\r\nimport { getUsageStats, getActiveBudgetSummary } from '../services/stats.js'\r\n\r\nexport interface StatusOptions {\r\n home?: string\r\n cwd?: string\r\n print?: (msg: string) => void\r\n}\r\n\r\nexport function runStatus(_args: string[] = [], opts: StatusOptions = {}): number {\r\n const print = opts.print ?? ((m: string) => console.error(m))\r\n const home = opts.home ?? os.homedir()\r\n const cwd = opts.cwd ?? process.cwd()\r\n const settingsPath = path.join(home, '.claude', 'settings.json')\r\n\r\n const installed = (() => {\r\n try {\r\n if (!fs.existsSync(settingsPath)) return false\r\n const json = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as Record<string, unknown>\r\n const mcp = (json.mcpServers ?? {}) as Record<string, unknown>\r\n return 'token-optimizer' in mcp\r\n } catch {\r\n return false\r\n }\r\n })()\r\n\r\n const projectDir = resolveProjectDir(cwd)\r\n const dbPath = resolveAnalyticsDbPath(projectDir)\r\n\r\n let eventsToday = 0\r\n let tokensBySource: Array<{ source: string; tokens: number }> = []\r\n let budgetLine = 'sin presupuesto activo'\r\n\r\n if (fs.existsSync(dbPath)) {\r\n try {\r\n const db = getDb(dbPath)\r\n const usage = getUsageStats(db, 1)\r\n eventsToday = usage.total_events\r\n tokensBySource = usage.by_source.map((r) => ({ source: r.source, tokens: r.tokens }))\r\n const budget = getActiveBudgetSummary(db, 'default', projectHash(projectDir))\r\n if (budget.active) {\r\n const pct = (budget.percent_used * 100).toFixed(1)\r\n budgetLine = `gastado=${budget.spent} restante=${budget.remaining} uso=${pct}% modo=${budget.mode}`\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n }\r\n\r\n const lines: string[] = []\r\n lines.push('token-optimizer-mcp status')\r\n lines.push('')\r\n lines.push(`Instalado: ${installed ? '✓' : '✗'} (${settingsPath})`)\r\n lines.push(`Storage DB: ${dbPath}${fs.existsSync(dbPath) ? '' : ' (no existe aun)'}`)\r\n lines.push(`Eventos hoy: ${eventsToday}`)\r\n lines.push(\r\n `Tokens por fuente: ${\r\n tokensBySource.length > 0\r\n ? tokensBySource.map((s) => `${s.source}=${s.tokens}`).join(', ')\r\n : '(sin datos)'\r\n }`,\r\n )\r\n lines.push(`Presupuesto: ${budgetLine}`)\r\n\r\n print(lines.join('\\n'))\r\n return 0\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;AAGA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAWR,SAAS,UAAU,QAAkB,CAAC,GAAG,OAAsB,CAAC,GAAW;AAChF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ;AACrC,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,eAAe,KAAK,KAAK,MAAM,WAAW,eAAe;AAE/D,QAAM,aAAa,MAAM;AACvB,QAAI;AACF,UAAI,CAAC,GAAG,WAAW,YAAY,EAAG,QAAO;AACzC,YAAM,OAAO,KAAK,MAAM,GAAG,aAAa,cAAc,MAAM,CAAC;AAC7D,YAAM,MAAO,KAAK,cAAc,CAAC;AACjC,aAAO,qBAAqB;AAAA,IAC9B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,MAAI,cAAc;AAClB,MAAI,iBAA4D,CAAC;AACjE,MAAI,aAAa;AAEjB,MAAI,GAAG,WAAW,MAAM,GAAG;AACzB,QAAI;AACF,YAAM,KAAK,MAAM,MAAM;AACvB,YAAM,QAAQ,cAAc,IAAI,CAAC;AACjC,oBAAc,MAAM;AACpB,uBAAiB,MAAM,UAAU,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,QAAQ,EAAE,OAAO,EAAE;AACpF,YAAM,SAAS,uBAAuB,IAAI,WAAW,YAAY,UAAU,CAAC;AAC5E,UAAI,OAAO,QAAQ;AACjB,cAAM,OAAO,OAAO,eAAe,KAAK,QAAQ,CAAC;AACjD,qBAAa,WAAW,OAAO,KAAK,aAAa,OAAO,SAAS,QAAQ,GAAG,UAAU,OAAO,IAAI;AAAA,MACnG;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,4BAA4B;AACvC,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,uBAAuB,YAAY,WAAM,QAAG,KAAK,YAAY,GAAG;AAC3E,QAAM,KAAK,uBAAuB,MAAM,GAAG,GAAG,WAAW,MAAM,IAAI,KAAK,kBAAkB,EAAE;AAC5F,QAAM,KAAK,uBAAuB,WAAW,EAAE;AAC/C,QAAM;AAAA,IACJ,uBACE,eAAe,SAAS,IACpB,eAAe,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,IAAI,IAC9D,aACN;AAAA,EACF;AACA,QAAM,KAAK,uBAAuB,UAAU,EAAE;AAE9C,QAAM,MAAM,KAAK,IAAI,CAAC;AACtB,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveXrayUrl | ||
| } from "./chunk-METYJF7E.js"; | ||
| import "./chunk-U3OXZD52.js"; | ||
| // src/cli/sync-xray.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| async function findAnalyticsDbs(rootDir) { | ||
| const results = []; | ||
| const rootDb = path.join(rootDir, ".token-optimizer", "analytics.db"); | ||
| if (fs.existsSync(rootDb)) { | ||
| results.push({ dbPath: rootDb, projectDir: rootDir, projectName: path.basename(rootDir) }); | ||
| } | ||
| const projectsDir = path.join(rootDir, "projects"); | ||
| if (fs.existsSync(projectsDir)) { | ||
| for (const entry of fs.readdirSync(projectsDir, { withFileTypes: true })) { | ||
| if (!entry.isDirectory()) continue; | ||
| const dbPath = path.join(projectsDir, entry.name, ".token-optimizer", "analytics.db"); | ||
| if (fs.existsSync(dbPath)) { | ||
| results.push({ | ||
| dbPath, | ||
| projectDir: path.join(projectsDir, entry.name), | ||
| projectName: entry.name | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| async function runSyncXray(args) { | ||
| const print = (m) => console.error(m); | ||
| const xrayUrl = resolveXrayUrl(); | ||
| if (!xrayUrl) { | ||
| print("Error: XRAY_URL no configurado."); | ||
| print("Ejecuta: npx @cocaxcode/token-optimizer-mcp config set xray_url http://localhost:3333"); | ||
| return 1; | ||
| } | ||
| const rootDir = args.find((a) => !a.startsWith("--")) ?? process.cwd(); | ||
| print(`Buscando analytics.db en ${rootDir}...`); | ||
| const dbs = await findAnalyticsDbs(rootDir); | ||
| if (dbs.length === 0) { | ||
| print("No se encontraron bases de datos de token-optimizer."); | ||
| return 1; | ||
| } | ||
| print(`Encontradas ${dbs.length} base(s) de datos:`); | ||
| for (const db of dbs) { | ||
| print(` - ${db.projectName}: ${db.dbPath}`); | ||
| } | ||
| let totalSent = 0; | ||
| let totalSkipped = 0; | ||
| for (const dbInfo of dbs) { | ||
| print(` | ||
| Sincronizando ${dbInfo.projectName}...`); | ||
| const Database = (await import("better-sqlite3")).default; | ||
| const db = new Database(dbInfo.dbPath, { readonly: true }); | ||
| const rows = db.prepare(` | ||
| SELECT session_id, tool_name, source, output_bytes, tokens_estimated, | ||
| tokens_actual, duration_ms, estimation_method, created_at | ||
| FROM tool_calls | ||
| ORDER BY created_at ASC | ||
| `).all(); | ||
| db.close(); | ||
| print(` ${rows.length} eventos en la DB`); | ||
| const BATCH_SIZE = 50; | ||
| for (let i = 0; i < rows.length; i += BATCH_SIZE) { | ||
| const batch = rows.slice(i, i + BATCH_SIZE); | ||
| const promises = batch.map(async (row) => { | ||
| const event = { | ||
| session_id: row.session_id, | ||
| tool_name: row.tool_name, | ||
| source: row.source, | ||
| output_bytes: row.output_bytes, | ||
| tokens_estimated: row.tokens_estimated, | ||
| tokens_actual: row.tokens_actual, | ||
| duration_ms: row.duration_ms, | ||
| estimation_method: row.estimation_method, | ||
| created_at: row.created_at, | ||
| project_path: dbInfo.projectDir, | ||
| project_name: dbInfo.projectName | ||
| }; | ||
| try { | ||
| const res = await fetch(`${xrayUrl}/hooks/token-optimizer`, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ source: "token-optimizer-mcp", version: "sync", event }), | ||
| signal: AbortSignal.timeout(5e3) | ||
| }); | ||
| if (res.ok) return true; | ||
| return false; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }); | ||
| const results = await Promise.all(promises); | ||
| const sent = results.filter(Boolean).length; | ||
| totalSent += sent; | ||
| totalSkipped += results.length - sent; | ||
| } | ||
| print(` Enviados: ${rows.length} eventos`); | ||
| } | ||
| print(` | ||
| Sincronizacion completa:`); | ||
| print(` Enviados: ${totalSent}`); | ||
| if (totalSkipped > 0) print(` Fallidos: ${totalSkipped}`); | ||
| print(` Dashboard: ${xrayUrl}`); | ||
| return 0; | ||
| } | ||
| export { | ||
| runSyncXray | ||
| }; | ||
| //# sourceMappingURL=sync-xray-FZOQPPOU.js.map |
| {"version":3,"sources":["../src/cli/sync-xray.ts"],"sourcesContent":["// sync-xray CLI — Sends historical analytics data to xray\r\n// Reads all .token-optimizer/analytics.db files and POSTs events to xray.\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport { resolveXrayUrl } from './config.js'\r\n\r\ninterface ToolCallRow {\r\n session_id: string\r\n tool_name: string\r\n source: string\r\n output_bytes: number\r\n tokens_estimated: number\r\n tokens_actual: number | null\r\n duration_ms: number | null\r\n estimation_method: string\r\n created_at: string\r\n}\r\n\r\nasync function findAnalyticsDbs(rootDir: string): Promise<Array<{ dbPath: string; projectDir: string; projectName: string }>> {\r\n const results: Array<{ dbPath: string; projectDir: string; projectName: string }> = []\r\n\r\n // Check root dir\r\n const rootDb = path.join(rootDir, '.token-optimizer', 'analytics.db')\r\n if (fs.existsSync(rootDb)) {\r\n results.push({ dbPath: rootDb, projectDir: rootDir, projectName: path.basename(rootDir) })\r\n }\r\n\r\n // Check projects/ subdirectories\r\n const projectsDir = path.join(rootDir, 'projects')\r\n if (fs.existsSync(projectsDir)) {\r\n for (const entry of fs.readdirSync(projectsDir, { withFileTypes: true })) {\r\n if (!entry.isDirectory()) continue\r\n const dbPath = path.join(projectsDir, entry.name, '.token-optimizer', 'analytics.db')\r\n if (fs.existsSync(dbPath)) {\r\n results.push({\r\n dbPath,\r\n projectDir: path.join(projectsDir, entry.name),\r\n projectName: entry.name,\r\n })\r\n }\r\n }\r\n }\r\n\r\n return results\r\n}\r\n\r\nexport async function runSyncXray(args: string[]): Promise<number> {\r\n const print = (m: string) => console.error(m)\r\n\r\n const xrayUrl = resolveXrayUrl()\r\n if (!xrayUrl) {\r\n print('Error: XRAY_URL no configurado.')\r\n print('Ejecuta: npx @cocaxcode/token-optimizer-mcp config set xray_url http://localhost:3333')\r\n return 1\r\n }\r\n\r\n // Determine root dir\r\n const rootDir = args.find(a => !a.startsWith('--')) ?? process.cwd()\r\n\r\n print(`Buscando analytics.db en ${rootDir}...`)\r\n const dbs = await findAnalyticsDbs(rootDir)\r\n\r\n if (dbs.length === 0) {\r\n print('No se encontraron bases de datos de token-optimizer.')\r\n return 1\r\n }\r\n\r\n print(`Encontradas ${dbs.length} base(s) de datos:`)\r\n for (const db of dbs) {\r\n print(` - ${db.projectName}: ${db.dbPath}`)\r\n }\r\n\r\n let totalSent = 0\r\n let totalSkipped = 0\r\n\r\n for (const dbInfo of dbs) {\r\n print(`\\nSincronizando ${dbInfo.projectName}...`)\r\n\r\n // Dynamic import to avoid loading better-sqlite3 if not needed\r\n const Database = (await import('better-sqlite3')).default\r\n const db = new Database(dbInfo.dbPath, { readonly: true })\r\n\r\n const rows = db.prepare(`\r\n SELECT session_id, tool_name, source, output_bytes, tokens_estimated,\r\n tokens_actual, duration_ms, estimation_method, created_at\r\n FROM tool_calls\r\n ORDER BY created_at ASC\r\n `).all() as ToolCallRow[]\r\n\r\n db.close()\r\n\r\n print(` ${rows.length} eventos en la DB`)\r\n\r\n // Send in batches of 50 to avoid overwhelming xray\r\n const BATCH_SIZE = 50\r\n for (let i = 0; i < rows.length; i += BATCH_SIZE) {\r\n const batch = rows.slice(i, i + BATCH_SIZE)\r\n const promises = batch.map(async (row) => {\r\n const event = {\r\n session_id: row.session_id,\r\n tool_name: row.tool_name,\r\n source: row.source,\r\n output_bytes: row.output_bytes,\r\n tokens_estimated: row.tokens_estimated,\r\n tokens_actual: row.tokens_actual,\r\n duration_ms: row.duration_ms,\r\n estimation_method: row.estimation_method,\r\n created_at: row.created_at,\r\n project_path: dbInfo.projectDir,\r\n project_name: dbInfo.projectName,\r\n }\r\n\r\n try {\r\n const res = await fetch(`${xrayUrl}/hooks/token-optimizer`, {\r\n method: 'POST',\r\n headers: { 'content-type': 'application/json' },\r\n body: JSON.stringify({ source: 'token-optimizer-mcp', version: 'sync', event }),\r\n signal: AbortSignal.timeout(5000),\r\n })\r\n if (res.ok) return true\r\n return false\r\n } catch {\r\n return false\r\n }\r\n })\r\n\r\n const results = await Promise.all(promises)\r\n const sent = results.filter(Boolean).length\r\n totalSent += sent\r\n totalSkipped += results.length - sent\r\n }\r\n\r\n print(` Enviados: ${rows.length} eventos`)\r\n }\r\n\r\n print(`\\nSincronizacion completa:`)\r\n print(` Enviados: ${totalSent}`)\r\n if (totalSkipped > 0) print(` Fallidos: ${totalSkipped}`)\r\n print(` Dashboard: ${xrayUrl}`)\r\n\r\n return 0\r\n}\r\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| // src/cli/uninstall.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| function settingsPath(home) { | ||
| return path.join(home, ".claude", "settings.json"); | ||
| } | ||
| function stripTokenOptimizerFromEvent(entries) { | ||
| if (!Array.isArray(entries)) return []; | ||
| return entries.map((entry) => { | ||
| const handlers = Array.isArray(entry.hooks) ? entry.hooks : []; | ||
| const filtered = handlers.filter( | ||
| (h) => !(typeof h.command === "string" && h.command.includes("token-optimizer")) | ||
| ); | ||
| return { ...entry, hooks: filtered }; | ||
| }).filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0); | ||
| } | ||
| function runUninstall(args = [], opts = {}) { | ||
| const home = opts.home ?? os.homedir(); | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const purge = opts.purge ?? args.includes("--purge"); | ||
| const confirm = opts.confirm ?? args.includes("--confirm"); | ||
| const p = settingsPath(home); | ||
| if (fs.existsSync(p)) { | ||
| try { | ||
| const json = JSON.parse(fs.readFileSync(p, "utf8")); | ||
| const mcpServers = json.mcpServers ?? {}; | ||
| delete mcpServers["token-optimizer"]; | ||
| json.mcpServers = mcpServers; | ||
| const hooks = json.hooks ?? {}; | ||
| for (const eventName of ["PreToolUse", "PostToolUse", "SessionStart"]) { | ||
| hooks[eventName] = stripTokenOptimizerFromEvent(hooks[eventName]); | ||
| } | ||
| json.hooks = hooks; | ||
| fs.writeFileSync(p, JSON.stringify(json, null, 2)); | ||
| print("Entradas de token-optimizer eliminadas de settings.json"); | ||
| } catch (e) { | ||
| print(`Error editando settings.json: ${e instanceof Error ? e.message : String(e)}`); | ||
| return 1; | ||
| } | ||
| } else { | ||
| print("settings.json no existe; nada que eliminar."); | ||
| } | ||
| if (purge) { | ||
| if (!confirm) { | ||
| print("--purge requiere tambien --confirm para borrar datos. Nada borrado."); | ||
| return 0; | ||
| } | ||
| const globalDir = path.join(home, ".token-optimizer"); | ||
| if (fs.existsSync(globalDir)) { | ||
| fs.rmSync(globalDir, { recursive: true, force: true }); | ||
| print(`Borrado: ${globalDir}`); | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
| export { | ||
| runUninstall | ||
| }; | ||
| //# sourceMappingURL=uninstall-ELUPZDG7.js.map |
| {"version":3,"sources":["../src/cli/uninstall.ts"],"sourcesContent":["// Uninstall CLI — Phase 4.11\r\n// Removes token-optimizer entries from settings.json. --purge --confirm also\r\n// removes the global storage dir.\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport os from 'node:os'\r\n\r\nexport interface UninstallOptions {\r\n home?: string\r\n cwd?: string\r\n print?: (msg: string) => void\r\n purge?: boolean\r\n confirm?: boolean\r\n}\r\n\r\nfunction settingsPath(home: string): string {\r\n return path.join(home, '.claude', 'settings.json')\r\n}\r\n\r\ninterface HookEntry {\r\n matcher?: string\r\n hooks?: Array<{ type?: string; command?: string }>\r\n [key: string]: unknown\r\n}\r\n\r\nfunction stripTokenOptimizerFromEvent(entries: unknown): HookEntry[] {\r\n if (!Array.isArray(entries)) return []\r\n return (entries as HookEntry[])\r\n .map((entry) => {\r\n const handlers = Array.isArray(entry.hooks) ? entry.hooks : []\r\n const filtered = handlers.filter(\r\n (h) => !(typeof h.command === 'string' && h.command.includes('token-optimizer')),\r\n )\r\n return { ...entry, hooks: filtered }\r\n })\r\n .filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0)\r\n}\r\n\r\nexport function runUninstall(args: string[] = [], opts: UninstallOptions = {}): number {\r\n const home = opts.home ?? os.homedir()\r\n const print = opts.print ?? ((m: string) => console.error(m))\r\n const purge = opts.purge ?? args.includes('--purge')\r\n const confirm = opts.confirm ?? args.includes('--confirm')\r\n\r\n const p = settingsPath(home)\r\n if (fs.existsSync(p)) {\r\n try {\r\n const json = JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\r\n\r\n // mcpServers: delete token-optimizer\r\n const mcpServers = (json.mcpServers ?? {}) as Record<string, unknown>\r\n delete mcpServers['token-optimizer']\r\n json.mcpServers = mcpServers\r\n\r\n // hooks: strip token-optimizer handlers from all 3 events\r\n const hooks = (json.hooks ?? {}) as Record<string, unknown>\r\n for (const eventName of ['PreToolUse', 'PostToolUse', 'SessionStart']) {\r\n hooks[eventName] = stripTokenOptimizerFromEvent(hooks[eventName])\r\n }\r\n json.hooks = hooks\r\n\r\n fs.writeFileSync(p, JSON.stringify(json, null, 2))\r\n print('Entradas de token-optimizer eliminadas de settings.json')\r\n } catch (e) {\r\n print(`Error editando settings.json: ${e instanceof Error ? e.message : String(e)}`)\r\n return 1\r\n }\r\n } else {\r\n print('settings.json no existe; nada que eliminar.')\r\n }\r\n\r\n if (purge) {\r\n if (!confirm) {\r\n print('--purge requiere tambien --confirm para borrar datos. Nada borrado.')\r\n return 0\r\n }\r\n const globalDir = path.join(home, '.token-optimizer')\r\n if (fs.existsSync(globalDir)) {\r\n fs.rmSync(globalDir, { recursive: true, force: true })\r\n print(`Borrado: ${globalDir}`)\r\n }\r\n }\r\n\r\n return 0\r\n}\r\n"],"mappings":";;;AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAUf,SAAS,aAAa,MAAsB;AAC1C,SAAO,KAAK,KAAK,MAAM,WAAW,eAAe;AACnD;AAQA,SAAS,6BAA6B,SAA+B;AACnE,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO,CAAC;AACrC,SAAQ,QACL,IAAI,CAAC,UAAU;AACd,UAAM,WAAW,MAAM,QAAQ,MAAM,KAAK,IAAI,MAAM,QAAQ,CAAC;AAC7D,UAAM,WAAW,SAAS;AAAA,MACxB,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,YAAY,EAAE,QAAQ,SAAS,iBAAiB;AAAA,IAChF;AACA,WAAO,EAAE,GAAG,OAAO,OAAO,SAAS;AAAA,EACrC,CAAC,EACA,OAAO,CAAC,UAAU,MAAM,QAAQ,MAAM,KAAK,KAAK,MAAM,MAAM,SAAS,CAAC;AAC3E;AAEO,SAAS,aAAa,OAAiB,CAAC,GAAG,OAAyB,CAAC,GAAW;AACrF,QAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ;AACrC,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,QAAQ,KAAK,SAAS,KAAK,SAAS,SAAS;AACnD,QAAM,UAAU,KAAK,WAAW,KAAK,SAAS,WAAW;AAEzD,QAAM,IAAI,aAAa,IAAI;AAC3B,MAAI,GAAG,WAAW,CAAC,GAAG;AACpB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;AAGlD,YAAM,aAAc,KAAK,cAAc,CAAC;AACxC,aAAO,WAAW,iBAAiB;AACnC,WAAK,aAAa;AAGlB,YAAM,QAAS,KAAK,SAAS,CAAC;AAC9B,iBAAW,aAAa,CAAC,cAAc,eAAe,cAAc,GAAG;AACrE,cAAM,SAAS,IAAI,6BAA6B,MAAM,SAAS,CAAC;AAAA,MAClE;AACA,WAAK,QAAQ;AAEb,SAAG,cAAc,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AACjD,YAAM,yDAAyD;AAAA,IACjE,SAAS,GAAG;AACV,YAAM,iCAAiC,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AACnF,aAAO;AAAA,IACT;AAAA,EACF,OAAO;AACL,UAAM,6CAA6C;AAAA,EACrD;AAEA,MAAI,OAAO;AACT,QAAI,CAAC,SAAS;AACZ,YAAM,qEAAqE;AAC3E,aAAO;AAAA,IACT;AACA,UAAM,YAAY,KAAK,KAAK,MAAM,kBAAkB;AACpD,QAAI,GAAG,WAAW,SAAS,GAAG;AAC5B,SAAG,OAAO,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACrD,YAAM,YAAY,SAAS,EAAE;AAAA,IAC/B;AAAA,EACF;AAEA,SAAO;AACT;","names":[]} |
+46
-121
@@ -9,19 +9,19 @@ #!/usr/bin/env node | ||
| surfaceWithDedupe | ||
| } from "./chunk-PMVZIR3X.js"; | ||
| } from "./chunk-GSWB574D.js"; | ||
| import { | ||
| runSerenaActivateHookFromCli | ||
| } from "./chunk-2NKYFIPW.js"; | ||
| } from "./chunk-Z3HKBI5O.js"; | ||
| import { | ||
| ensureGitignore | ||
| } from "./chunk-VHU3U64E.js"; | ||
| } from "./chunk-N754BAPB.js"; | ||
| import { | ||
| loadConfig | ||
| } from "./chunk-DBLVAFU5.js"; | ||
| } from "./chunk-METYJF7E.js"; | ||
| import { | ||
| probeRtk, | ||
| probeSerena | ||
| } from "./chunk-DOYJNIB2.js"; | ||
| } from "./chunk-4MJNQPFS.js"; | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-VV5KKIQ4.js"; | ||
| } from "./chunk-AF6RQ5F5.js"; | ||
| import { | ||
@@ -38,3 +38,3 @@ buildQueries | ||
| resolveProjectDir | ||
| } from "./chunk-AWG3ZQRZ.js"; | ||
| } from "./chunk-U3OXZD52.js"; | ||
@@ -72,3 +72,34 @@ // src/hooks/posttooluse.ts | ||
| ]); | ||
| var RTK_BASH_COMMAND = /^\s*(?:(?:[A-Z_][A-Z0-9_]*=\S*\s+)*(?:sudo\s+)?)rtk(?:\s|$)/; | ||
| var RTK_STATEMENT = /^\s*(?:[A-Z_][A-Z0-9_]*=\S*\s+)*(?:sudo\s+)?rtk(?:\s|$)/; | ||
| function splitTopLevelStatements(command) { | ||
| const segments = []; | ||
| let current = ""; | ||
| let quote = null; | ||
| for (let i = 0; i < command.length; i++) { | ||
| const ch = command[i]; | ||
| if (ch === "\\" && quote !== "'" && i + 1 < command.length) { | ||
| current += ch + command[i + 1]; | ||
| i++; | ||
| continue; | ||
| } | ||
| if (quote) { | ||
| current += ch; | ||
| if (ch === quote) quote = null; | ||
| continue; | ||
| } | ||
| if (ch === '"' || ch === "'") { | ||
| quote = ch; | ||
| current += ch; | ||
| continue; | ||
| } | ||
| if (ch === ";" || ch === "&" || ch === "|") { | ||
| segments.push(current); | ||
| current = ""; | ||
| continue; | ||
| } | ||
| current += ch; | ||
| } | ||
| segments.push(current); | ||
| return segments; | ||
| } | ||
| function isRtkWrappedBash(toolInput) { | ||
@@ -78,3 +109,3 @@ if (!toolInput || typeof toolInput !== "object") return false; | ||
| if (typeof command !== "string") return false; | ||
| return RTK_BASH_COMMAND.test(command); | ||
| return splitTopLevelStatements(command).some((s) => RTK_STATEMENT.test(s)); | ||
| } | ||
@@ -591,82 +622,6 @@ function classifySource(toolName, toolInput) { | ||
| // src/hooks/pretooluse.ts | ||
| import fs5 from "fs"; | ||
| // src/lib/rtk-bridge.ts | ||
| import { spawnSync } from "child_process"; | ||
| import fs4 from "fs"; | ||
| import path3 from "path"; | ||
| import os2 from "os"; | ||
| var IS_WINDOWS = process.platform === "win32"; | ||
| var RTK_BIN = IS_WINDOWS ? "rtk.exe" : "rtk"; | ||
| var REWRITE_TIMEOUT_MS = 2e3; | ||
| var cachedRtkPath = void 0; | ||
| function findRtkBinary(opts = {}) { | ||
| if (opts.resetCache) cachedRtkPath = void 0; | ||
| if (cachedRtkPath !== void 0) return cachedRtkPath; | ||
| if (opts.searchPaths) { | ||
| for (const dir of opts.searchPaths) { | ||
| const candidate = path3.join(dir, RTK_BIN); | ||
| if (fs4.existsSync(candidate)) { | ||
| cachedRtkPath = candidate; | ||
| return candidate; | ||
| } | ||
| } | ||
| } | ||
| try { | ||
| const whichCmd = IS_WINDOWS ? "where" : "which"; | ||
| const result = spawnSync(whichCmd, [IS_WINDOWS ? "rtk" : "rtk"], { | ||
| encoding: "utf8", | ||
| timeout: 1e3, | ||
| windowsHide: true | ||
| }); | ||
| if (result.status === 0 && result.stdout?.trim()) { | ||
| const lines = result.stdout.trim().split(/\r?\n/).map((l) => l.trim()).filter(Boolean); | ||
| const found = IS_WINDOWS ? lines.find((l) => l.toLowerCase().endsWith(".exe")) ?? lines[0] : lines[0]; | ||
| if (found) { | ||
| cachedRtkPath = found; | ||
| return found; | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| const fallbackDirs = [ | ||
| 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 = path3.join(dir, RTK_BIN); | ||
| if (fs4.existsSync(candidate)) { | ||
| cachedRtkPath = candidate; | ||
| return candidate; | ||
| } | ||
| } | ||
| cachedRtkPath = null; | ||
| return null; | ||
| } | ||
| function rtkRewrite(command, rtkPath, timeoutMs = REWRITE_TIMEOUT_MS) { | ||
| if (!command.trim()) return null; | ||
| try { | ||
| const result = spawnSync(rtkPath, ["rewrite", command], { | ||
| encoding: "utf8", | ||
| timeout: timeoutMs, | ||
| windowsHide: true, | ||
| stdio: ["ignore", "pipe", "ignore"] | ||
| }); | ||
| if (result.error) return null; | ||
| const exitCode2 = result.status ?? 1; | ||
| const rewritten = result.stdout?.trim() ?? ""; | ||
| return { | ||
| rewritten, | ||
| exitCode: exitCode2, | ||
| success: exitCode2 === 0 && rewritten.length > 0 | ||
| }; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| // src/hooks/pretooluse.ts | ||
| function readStdinSync2() { | ||
| try { | ||
| return fs5.readFileSync(0, "utf8"); | ||
| return fs4.readFileSync(0, "utf8"); | ||
| } catch { | ||
@@ -720,38 +675,8 @@ return ""; | ||
| } | ||
| try { | ||
| const rtkPath = opts.rtkPath !== void 0 ? opts.rtkPath : findRtkBinary(); | ||
| if (rtkPath && command.trim()) { | ||
| const result = rtkRewrite(command, rtkPath); | ||
| if (result) { | ||
| if ((result.exitCode === 0 || result.exitCode === 3) && result.rewritten) { | ||
| decision.updatedInput = { command: result.rewritten }; | ||
| decision.permissionDecision = "allow"; | ||
| try { | ||
| const projectDir = opts.projectDir ?? resolveProjectDir(); | ||
| const dbPath = opts.dbPath !== void 0 ? opts.dbPath : (ensureGitignore(projectDir), resolveAnalyticsDbPath(projectDir)); | ||
| const db = getDb(dbPath); | ||
| const queries = buildQueries(db); | ||
| queries.insertRtkRewrite(sessionId, hashCommand(command), result.rewritten); | ||
| queries.purgeStaleRtkMarks(); | ||
| } catch { | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| if (opts.writeStdout !== false) { | ||
| const output = {}; | ||
| if (decision.updatedInput) { | ||
| output.updatedInput = decision.updatedInput; | ||
| } | ||
| if (decision.permissionDecision) { | ||
| output.permissionDecision = decision.permissionDecision; | ||
| } | ||
| if (decision.additionalContext) { | ||
| output.additionalContext = decision.additionalContext; | ||
| } | ||
| process.stdout.write(JSON.stringify( | ||
| Object.keys(output).length > 0 ? output : {} | ||
| )); | ||
| process.stdout.write(JSON.stringify(output)); | ||
| } | ||
@@ -762,3 +687,3 @@ return decision; | ||
| // src/hooks/sessionstart.ts | ||
| import fs6 from "fs"; | ||
| import fs5 from "fs"; | ||
@@ -904,3 +829,3 @@ // src/services/session-retriever.ts | ||
| try { | ||
| return fs6.readFileSync(0, "utf8"); | ||
| return fs5.readFileSync(0, "utf8"); | ||
| } catch { | ||
@@ -1036,3 +961,3 @@ return ""; | ||
| const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js"); | ||
| const { createServer } = await import("./server-WUE7RS3B.js"); | ||
| const { createServer } = await import("./server-RIDC4JTC.js"); | ||
| const server = createServer(); | ||
@@ -1047,5 +972,5 @@ const transport = new StdioServerTransport(); | ||
| } | ||
| var { dispatchCli } = await import("./dispatcher-OMTXHUCU.js"); | ||
| var { dispatchCli } = await import("./dispatcher-WI357YQ4.js"); | ||
| var exitCode = await dispatchCli(args); | ||
| process.exit(exitCode); | ||
| //# sourceMappingURL=index.js.map |
+1
-1
| { | ||
| "name": "@cocaxcode/token-optimizer-mcp", | ||
| "version": "0.6.1", | ||
| "version": "0.7.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.", |
+13
-21
@@ -130,3 +130,3 @@ <p align="center"> | ||
| > **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. | ||
| > **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 budget enforcement, no analytics, no compact recovery. The MCP server itself works fine with `npx -y`, but hooks need the package in PATH. | ||
@@ -395,24 +395,16 @@ **Step 3 — Set up hooks:** | ||
| **Step 2 — token-optimizer bridge (automatic via global install):** | ||
| **Step 2 — Register RTK's Claude Code hook:** | ||
| The **PreToolUse hook** acts as an RTK bridge — but it requires `npm install -g @cocaxcode/token-optimizer-mcp` (see [Installation](#installation)): | ||
| RTK ships its own PreToolUse hook that transparently rewrites Bash commands. Register it with RTK's own installer: | ||
| 1. Claude wants to run `git status` | ||
| 2. The hook calls `rtk rewrite "git status"` | ||
| 3. RTK returns `rtk git status` (exit 0 = auto-allow) | ||
| 4. The hook sets `updatedInput` so Claude runs the RTK-wrapped version | ||
| 5. Output is filtered before it enters the context window | ||
| ```bash | ||
| rtk init -g | ||
| ``` | ||
| This happens **automatically** for every Bash command — no manual `rtk` invocation needed. | ||
| After this, every Bash command is rewritten automatically before it runs (e.g. `git status` → `rtk git status`), and its output is filtered before it enters the context window — no manual `rtk` invocation needed. | ||
| > **Important**: After installing, **restart Claude Code**. Hooks are loaded at session start — if the package wasn't installed when the session started, hooks won't fire until the next session. | ||
| > **Important**: After installing, **restart Claude Code**. Hooks are loaded at session start. | ||
| RTK exit codes (all handled by the bridge): | ||
| - **0** — rewrite + auto-allow (e.g., `npm run build` → `rtk npm run build`) | ||
| - **1** — no RTK equivalent → passthrough (command runs as-is) | ||
| - **2** — deny rule → passthrough | ||
| - **3** — rewrite + allow (e.g., `git status` → `rtk git status`, `find` → `rtk find`) | ||
| token-optimizer does **not** rewrite Bash itself. Earlier versions shipped a bridge for this; it was removed in **v0.7.0** — RTK's native hook has been cross-platform (incl. Windows) since rtk v0.37.2, so a second hook rewriting the same command was redundant and racy. token-optimizer's role here is to **measure**: its PostToolUse hook detects rtk-wrapped commands and records how many tokens RTK filtered, so reports and xray credit the savings. | ||
| > Both exit 0 and 3 set `permissionDecision: "allow"` so Claude Code applies the rewrite. Without this field, Claude Code ignores `updatedInput`. | ||
| **Step 3 — Verify with token-optimizer:** | ||
@@ -424,9 +416,9 @@ | ||
| Expected output when fully configured: | ||
| Expected output when RTK is installed and its hook is registered: | ||
| ``` | ||
| [rtk] ✓ conf=0.40 signals: rtk-binary-in-path, token-optimizer-bridge-active | ||
| [rtk] ✓ signals: rtk-binary-in-path, rtk-hook-registered | ||
| ``` | ||
| If you see `rtk-binary-in-path` but no `token-optimizer-bridge-active`, RTK is installed but the token-optimizer hooks are not — run `npx @cocaxcode/token-optimizer-mcp install` to set them up. | ||
| If you see `rtk-binary-in-path` but no `rtk-hook-registered`, the RTK binary is installed but its Claude Code hook is not — run `rtk init -g`. | ||
@@ -502,3 +494,3 @@ **What RTK can wrap** (partial list): `ls`, `tree`, `git`, `gh`, `test`, `err`, `json`, `diff`, `grep`, `docker`, `kubectl`, `pnpm`, `dotnet`, `psql`, `aws`, and more. Run `rtk --help` for the full list. | ||
| |---|---|---| | ||
| | `PreToolUse` | `Bash` | Checks budget (passthrough / warn), then RTK rewrite. Sets `updatedInput` + `permissionDecision: "allow"` when RTK rewrites (exit 0/3). Budget warn always wins over RTK. | | ||
| | `PreToolUse` | `Bash` | Checks the token budget (passthrough / warn). Never mutates the command — RTK's own `rtk hook claude` hook owns Bash rewriting. | | ||
| | `PostToolUse` | `*` | Async analytics to SQLite. Fire-and-forget to xray. Target p95: 10ms. | | ||
@@ -505,0 +497,0 @@ | `SessionStart` | `compact` | Injects markdown: recent files, commands, budget, recently touched Serena symbols. Token-capped at 2000. | |
| #!/usr/bin/env node | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-VV5KKIQ4.js"; | ||
| import "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| projectHash, | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-AWG3ZQRZ.js"; | ||
| // src/cli/budget.ts | ||
| function runBudgetCli(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const db = getDb(dbPath); | ||
| const mgr = new BudgetManager(db); | ||
| const hash = projectHash(projectDir); | ||
| const sub = args[0]; | ||
| if (sub === "set") { | ||
| const scope = args[1]; | ||
| const limitRaw = args[2]; | ||
| const limit = limitRaw ? parseInt(limitRaw, 10) : NaN; | ||
| if (scope !== "session" && scope !== "project" || !Number.isFinite(limit)) { | ||
| print("Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| try { | ||
| const budget = mgr.setBudget({ | ||
| scope, | ||
| scope_key: scopeKey, | ||
| limit_tokens: limit | ||
| }); | ||
| print( | ||
| `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}` | ||
| ); | ||
| return 0; | ||
| } catch (e) { | ||
| print(`Error: ${e instanceof Error ? e.message : String(e)}`); | ||
| return 1; | ||
| } | ||
| } | ||
| if (sub === "get") { | ||
| const status = mgr.checkBudget("default", hash); | ||
| if (!status.active) { | ||
| print("Sin presupuesto activo"); | ||
| return 0; | ||
| } | ||
| const pct = (status.percent_used * 100).toFixed(1); | ||
| print( | ||
| `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? "n/a"}` | ||
| ); | ||
| return 0; | ||
| } | ||
| if (sub === "clear") { | ||
| const scope = args[1]; | ||
| if (scope !== "session" && scope !== "project") { | ||
| print("Uso: token-optimizer-mcp budget clear <session|project>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| const removed = mgr.clearBudget(scope, scopeKey); | ||
| print(removed ? `Eliminado (${scope})` : "No habia presupuesto para este scope"); | ||
| return 0; | ||
| } | ||
| print("Uso: token-optimizer-mcp budget <set|get|clear> [args]"); | ||
| return 1; | ||
| } | ||
| export { | ||
| runBudgetCli | ||
| }; | ||
| //# sourceMappingURL=budget-ZBEVUS4Y.js.map |
| {"version":3,"sources":["../src/cli/budget.ts"],"sourcesContent":["// Budget CLI — Phase 4.15\n// Thin wrapper delegating to BudgetManager. Subcommands: set / get / clear.\n\nimport { getDb } from '../db/connection.js'\nimport {\n resolveProjectDir,\n resolveAnalyticsDbPath,\n projectHash,\n} from '../lib/paths.js'\nimport { BudgetManager } from '../services/budget-manager.js'\nimport type { BudgetScope } from '../lib/types.js'\n\nexport interface BudgetCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runBudgetCli(args: string[] = [], opts: BudgetCliOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n const db = getDb(dbPath)\n const mgr = new BudgetManager(db)\n const hash = projectHash(projectDir)\n\n const sub = args[0]\n\n if (sub === 'set') {\n const scope = args[1] as BudgetScope | undefined\n const limitRaw = args[2]\n const limit = limitRaw ? parseInt(limitRaw, 10) : NaN\n if ((scope !== 'session' && scope !== 'project') || !Number.isFinite(limit)) {\n print('Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>')\n return 1\n }\n const scopeKey = scope === 'session' ? 'default' : hash\n try {\n const budget = mgr.setBudget({\n scope,\n scope_key: scopeKey,\n limit_tokens: limit,\n })\n print(\n `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}`,\n )\n return 0\n } catch (e) {\n print(`Error: ${e instanceof Error ? e.message : String(e)}`)\n return 1\n }\n }\n\n if (sub === 'get') {\n const status = mgr.checkBudget('default', hash)\n if (!status.active) {\n print('Sin presupuesto activo')\n return 0\n }\n const pct = (status.percent_used * 100).toFixed(1)\n print(\n `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? 'n/a'}`,\n )\n return 0\n }\n\n if (sub === 'clear') {\n const scope = args[1] as BudgetScope | undefined\n if (scope !== 'session' && scope !== 'project') {\n print('Uso: token-optimizer-mcp budget clear <session|project>')\n return 1\n }\n const scopeKey = scope === 'session' ? 'default' : hash\n const removed = mgr.clearBudget(scope, scopeKey)\n print(removed ? `Eliminado (${scope})` : 'No habia presupuesto para este scope')\n return 0\n }\n\n print('Uso: token-optimizer-mcp budget <set|get|clear> [args]')\n return 1\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBO,SAAS,aAAa,OAAiB,CAAC,GAAG,OAAyB,CAAC,GAAW;AACrF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,QAAM,KAAK,MAAM,MAAM;AACvB,QAAM,MAAM,IAAI,cAAc,EAAE;AAChC,QAAM,OAAO,YAAY,UAAU;AAEnC,QAAM,MAAM,KAAK,CAAC;AAElB,MAAI,QAAQ,OAAO;AACjB,UAAM,QAAQ,KAAK,CAAC;AACpB,UAAM,WAAW,KAAK,CAAC;AACvB,UAAM,QAAQ,WAAW,SAAS,UAAU,EAAE,IAAI;AAClD,QAAK,UAAU,aAAa,UAAU,aAAc,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3E,YAAM,sEAAsE;AAC5E,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,QAAI;AACF,YAAM,SAAS,IAAI,UAAU;AAAA,QAC3B;AAAA,QACA,WAAW;AAAA,QACX,cAAc;AAAA,MAChB,CAAC;AACD;AAAA,QACE,yBAAyB,OAAO,KAAK,IAAI,OAAO,SAAS,UAAU,OAAO,YAAY,SAAS,OAAO,IAAI;AAAA,MAC5G;AACA,aAAO;AAAA,IACT,SAAS,GAAG;AACV,YAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AAC5D,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,QAAQ,OAAO;AACjB,UAAM,SAAS,IAAI,YAAY,WAAW,IAAI;AAC9C,QAAI,CAAC,OAAO,QAAQ;AAClB,YAAM,wBAAwB;AAC9B,aAAO;AAAA,IACT;AACA,UAAM,OAAO,OAAO,eAAe,KAAK,QAAQ,CAAC;AACjD;AAAA,MACE,WAAW,OAAO,KAAK,aAAa,OAAO,SAAS,QAAQ,GAAG,UAAU,OAAO,QAAQ,KAAK;AAAA,IAC/F;AACA,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,SAAS;AACnB,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,UAAU,aAAa,UAAU,WAAW;AAC9C,YAAM,yDAAyD;AAC/D,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,UAAM,UAAU,IAAI,YAAY,OAAO,QAAQ;AAC/C,UAAM,UAAU,cAAc,KAAK,MAAM,sCAAsC;AAC/E,WAAO;AAAA,EACT;AAEA,QAAM,wDAAwD;AAC9D,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| // src/hooks/serena-activate.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| import { spawnSync } from "child_process"; | ||
| var INSTRUCTION = [ | ||
| "**IMPORTANT \u2014 Serena activation (do this FIRST, before anything else):**", | ||
| '1. Call ToolSearch with query "select:mcp__serena__initial_instructions,mcp__serena__activate_project,mcp__serena__check_onboarding_performed" to load the tool schemas (they are deferred and cannot be called without this step).', | ||
| "2. Call mcp__serena__initial_instructions (read the manual).", | ||
| "3. Call mcp__serena__activate_project with the current working directory as the `project` argument.", | ||
| "4. Call mcp__serena__check_onboarding_performed.", | ||
| "Do NOT skip step 1 \u2014 calling Serena tools without loading their schemas first will fail with InputValidationError." | ||
| ].join(" "); | ||
| function probeSerenaPresence(env = process.env) { | ||
| const isWindows = process.platform === "win32"; | ||
| const whichCmd = isWindows ? "where" : "which"; | ||
| const binName = isWindows ? "serena-hooks.exe" : "serena-hooks"; | ||
| let serena_cli_installed = false; | ||
| try { | ||
| const result = spawnSync(whichCmd, ["serena-hooks"], { | ||
| encoding: "utf8", | ||
| timeout: 500, | ||
| windowsHide: true, | ||
| env | ||
| }); | ||
| if (result.status === 0 && result.stdout && result.stdout.trim().length > 0) { | ||
| serena_cli_installed = true; | ||
| } | ||
| } catch { | ||
| } | ||
| if (!serena_cli_installed) { | ||
| const candidates = [ | ||
| path.join(os.homedir(), ".local", "bin", binName), | ||
| ...isWindows ? [ | ||
| path.join(os.homedir(), "scoop", "shims", binName), | ||
| path.join("C:\\", "tools", "serena", binName) | ||
| ] : ["/usr/local/bin/serena-hooks", "/opt/homebrew/bin/serena-hooks"] | ||
| ]; | ||
| for (const c of candidates) { | ||
| if (fs.existsSync(c)) { | ||
| serena_cli_installed = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| const serena_mcp_registered = fs.existsSync(path.join(os.homedir(), ".serena")); | ||
| return { | ||
| serena_cli_installed, | ||
| serena_mcp_registered, | ||
| present: serena_cli_installed || serena_mcp_registered | ||
| }; | ||
| } | ||
| function runSerenaActivateHook(opts = {}) { | ||
| const probe = opts.probe ?? probeSerenaPresence(); | ||
| if (!probe.present) { | ||
| if (opts.writeStdout !== false) process.stdout.write("{}"); | ||
| return { emitted: false, probe }; | ||
| } | ||
| const payload = { | ||
| hookSpecificOutput: { | ||
| hookEventName: "SessionStart", | ||
| additionalContext: INSTRUCTION | ||
| } | ||
| }; | ||
| if (opts.writeStdout !== false) process.stdout.write(JSON.stringify(payload)); | ||
| return { emitted: true, probe }; | ||
| } | ||
| function runSerenaActivateHookFromCli() { | ||
| try { | ||
| fs.readFileSync(0, "utf8"); | ||
| } catch { | ||
| } | ||
| return runSerenaActivateHook(); | ||
| } | ||
| export { | ||
| probeSerenaPresence, | ||
| runSerenaActivateHookFromCli | ||
| }; | ||
| //# sourceMappingURL=chunk-2NKYFIPW.js.map |
| {"version":3,"sources":["../src/hooks/serena-activate.ts"],"sourcesContent":["// SessionStart hook that forces Serena activation via explicit ToolSearch.\n//\n// Context: Serena ships its own `serena-hooks activate` hook, but the output of\n// that binary does NOT mention ToolSearch — it just tells the agent to \"activate\n// the current working directory as project using Serena's tools\". In a Claude\n// Code environment where MCP tools are marked `deferred` at session start, the\n// agent cannot call `mcp__serena__*` tools without first loading their schemas\n// via the `ToolSearch` tool — trying to do so fails with InputValidationError.\n//\n// This hook emits an explicit four-step instruction block that starts with the\n// ToolSearch call, so the agent does the right thing on PCs where Serena tools\n// happen to be deferred.\n//\n// Behaviour:\n// - If Serena is NOT detected on the machine → emit `{}` and exit 0 (noop).\n// This keeps the hook safe to install globally, even on PCs without Serena.\n// - If Serena IS detected → emit the JSON with hookSpecificOutput.\n//\n// Intentionally stateless. Detection is a cheap synchronous probe so the hook\n// stays well under the p95 latency budget for SessionStart.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport os from 'node:os'\nimport { spawnSync } from 'node:child_process'\n\nconst INSTRUCTION = [\n '**IMPORTANT — Serena activation (do this FIRST, before anything else):**',\n '1. Call ToolSearch with query \"select:mcp__serena__initial_instructions,mcp__serena__activate_project,mcp__serena__check_onboarding_performed\" to load the tool schemas (they are deferred and cannot be called without this step).',\n '2. Call mcp__serena__initial_instructions (read the manual).',\n '3. Call mcp__serena__activate_project with the current working directory as the `project` argument.',\n '4. Call mcp__serena__check_onboarding_performed.',\n 'Do NOT skip step 1 — calling Serena tools without loading their schemas first will fail with InputValidationError.',\n].join(' ')\n\nexport interface SerenaProbe {\n /**\n * The `serena-hooks` CLI binary is actually installed and executable on this\n * machine. Required to register the 3 official Serena hooks (remind,\n * auto-approve, cleanup) in settings.json — without the binary, Claude Code\n * would try to run them and fail with \"command not found\" on every hook\n * dispatch.\n */\n serena_cli_installed: boolean\n /**\n * There is some Serena footprint on this machine (`~/.serena/` directory,\n * usually created by the Serena MCP server or CLI the first time it runs).\n * This is independent of whether the CLI binary is installed; the user may\n * have only the MCP server registered.\n */\n serena_mcp_registered: boolean\n /**\n * True when at least one of the two above is true. Used by the\n * serena-activate hook to decide whether to emit its SessionStart payload —\n * that hook does NOT depend on the CLI, it only writes JSON to stdout.\n */\n present: boolean\n}\n\n/** Detect Serena installation state. Cheap and sync. */\nexport function probeSerenaPresence(\n env: NodeJS.ProcessEnv = process.env,\n): SerenaProbe {\n const isWindows = process.platform === 'win32'\n const whichCmd = isWindows ? 'where' : 'which'\n const binName = isWindows ? 'serena-hooks.exe' : 'serena-hooks'\n\n // Strategy 1 — $PATH via where/which. This is what actually matters for\n // `serena-hooks ...` commands to execute at runtime.\n let serena_cli_installed = false\n try {\n const result = spawnSync(whichCmd, ['serena-hooks'], {\n encoding: 'utf8',\n timeout: 500,\n windowsHide: true,\n env,\n })\n if (result.status === 0 && result.stdout && result.stdout.trim().length > 0) {\n serena_cli_installed = true\n }\n } catch {\n /* swallow — fall through to strategy 2 */\n }\n\n // Strategy 2 — common install locations (fallback when `where`/`which` can't\n // find things in a minimal shell PATH, or when the hook is invoked with a\n // stripped env).\n if (!serena_cli_installed) {\n const candidates = [\n path.join(os.homedir(), '.local', 'bin', binName),\n ...(isWindows\n ? [\n path.join(os.homedir(), 'scoop', 'shims', binName),\n path.join('C:\\\\', 'tools', 'serena', binName),\n ]\n : ['/usr/local/bin/serena-hooks', '/opt/homebrew/bin/serena-hooks']),\n ]\n for (const c of candidates) {\n if (fs.existsSync(c)) {\n serena_cli_installed = true\n break\n }\n }\n }\n\n // Independent signal — is the MCP server / config dir around? This alone is\n // NOT enough to register the 3 official hooks (they need the CLI), but it is\n // enough to install our own `--hook serena-activate` (which just emits\n // JSON and doesn't shell out to any binary).\n const serena_mcp_registered = fs.existsSync(path.join(os.homedir(), '.serena'))\n\n return {\n serena_cli_installed,\n serena_mcp_registered,\n present: serena_cli_installed || serena_mcp_registered,\n }\n}\n\nexport interface RunSerenaActivateOptions {\n writeStdout?: boolean\n /** Inject a custom probe result (used by tests). */\n probe?: SerenaProbe\n}\n\nexport interface SerenaActivateResult {\n emitted: boolean\n probe: SerenaProbe\n}\n\n/**\n * Pure function — doesn't read stdin. Callers from the entry point should\n * drain stdin themselves before invoking (Claude Code pipes a payload we\n * don't use). Tests call this directly without touching stdin.\n */\nexport function runSerenaActivateHook(\n opts: RunSerenaActivateOptions = {},\n): SerenaActivateResult {\n const probe = opts.probe ?? probeSerenaPresence()\n\n if (!probe.present) {\n if (opts.writeStdout !== false) process.stdout.write('{}')\n return { emitted: false, probe }\n }\n\n const payload = {\n hookSpecificOutput: {\n hookEventName: 'SessionStart',\n additionalContext: INSTRUCTION,\n },\n }\n if (opts.writeStdout !== false) process.stdout.write(JSON.stringify(payload))\n return { emitted: true, probe }\n}\n\n/**\n * Entry-point helper that drains stdin (Claude Code always pipes a payload)\n * and delegates to `runSerenaActivateHook`. Safe to call from the CLI\n * dispatcher in src/index.ts.\n */\nexport function runSerenaActivateHookFromCli(): SerenaActivateResult {\n try {\n fs.readFileSync(0, 'utf8')\n } catch {\n /* no stdin attached — fine */\n }\n return runSerenaActivateHook()\n}\n"],"mappings":";;;AAqBA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,iBAAiB;AAE1B,IAAM,cAAc;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,EAAE,KAAK,GAAG;AA2BH,SAAS,oBACd,MAAyB,QAAQ,KACpB;AACb,QAAM,YAAY,QAAQ,aAAa;AACvC,QAAM,WAAW,YAAY,UAAU;AACvC,QAAM,UAAU,YAAY,qBAAqB;AAIjD,MAAI,uBAAuB;AAC3B,MAAI;AACF,UAAM,SAAS,UAAU,UAAU,CAAC,cAAc,GAAG;AAAA,MACnD,UAAU;AAAA,MACV,SAAS;AAAA,MACT,aAAa;AAAA,MACb;AAAA,IACF,CAAC;AACD,QAAI,OAAO,WAAW,KAAK,OAAO,UAAU,OAAO,OAAO,KAAK,EAAE,SAAS,GAAG;AAC3E,6BAAuB;AAAA,IACzB;AAAA,EACF,QAAQ;AAAA,EAER;AAKA,MAAI,CAAC,sBAAsB;AACzB,UAAM,aAAa;AAAA,MACjB,KAAK,KAAK,GAAG,QAAQ,GAAG,UAAU,OAAO,OAAO;AAAA,MAChD,GAAI,YACA;AAAA,QACE,KAAK,KAAK,GAAG,QAAQ,GAAG,SAAS,SAAS,OAAO;AAAA,QACjD,KAAK,KAAK,QAAQ,SAAS,UAAU,OAAO;AAAA,MAC9C,IACA,CAAC,+BAA+B,gCAAgC;AAAA,IACtE;AACA,eAAW,KAAK,YAAY;AAC1B,UAAI,GAAG,WAAW,CAAC,GAAG;AACpB,+BAAuB;AACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAMA,QAAM,wBAAwB,GAAG,WAAW,KAAK,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9E,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,SAAS,wBAAwB;AAAA,EACnC;AACF;AAkBO,SAAS,sBACd,OAAiC,CAAC,GACZ;AACtB,QAAM,QAAQ,KAAK,SAAS,oBAAoB;AAEhD,MAAI,CAAC,MAAM,SAAS;AAClB,QAAI,KAAK,gBAAgB,MAAO,SAAQ,OAAO,MAAM,IAAI;AACzD,WAAO,EAAE,SAAS,OAAO,MAAM;AAAA,EACjC;AAEA,QAAM,UAAU;AAAA,IACd,oBAAoB;AAAA,MAClB,eAAe;AAAA,MACf,mBAAmB;AAAA,IACrB;AAAA,EACF;AACA,MAAI,KAAK,gBAAgB,MAAO,SAAQ,OAAO,MAAM,KAAK,UAAU,OAAO,CAAC;AAC5E,SAAO,EAAE,SAAS,MAAM,MAAM;AAChC;AAOO,SAAS,+BAAqD;AACnE,MAAI;AACF,OAAG,aAAa,GAAG,MAAM;AAAA,EAC3B,QAAQ;AAAA,EAER;AACA,SAAO,sBAAsB;AAC/B;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-VV5KKIQ4.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| // src/services/stats.ts | ||
| var DAY_MS = 864e5; | ||
| var HAIKU_INPUT_PER_MTOK = 1; | ||
| var SONNET_INPUT_PER_MTOK = 3; | ||
| var OPUS_INPUT_PER_MTOK = 5; | ||
| function sinceDays(days) { | ||
| return new Date(Date.now() - days * DAY_MS).toISOString(); | ||
| } | ||
| function getUsageStats(db, days = 7) { | ||
| const queries = buildQueries(db); | ||
| const since = sinceDays(days); | ||
| const byTool = queries.countToolCallsByTool(since); | ||
| const bySource = queries.countToolCallsBySource(since); | ||
| const totalTokens = bySource.reduce((sum, r) => sum + r.tokens, 0); | ||
| const totalEvents = bySource.reduce((sum, r) => sum + r.count, 0); | ||
| return { | ||
| period_days: days, | ||
| period_since: since, | ||
| by_tool: byTool, | ||
| by_source: bySource, | ||
| total_tokens: totalTokens, | ||
| total_events: totalEvents | ||
| }; | ||
| } | ||
| function getCostReport(db, days = 7) { | ||
| const usage = getUsageStats(db, days); | ||
| const mtok = usage.total_tokens / 1e6; | ||
| const haiku = Number((mtok * HAIKU_INPUT_PER_MTOK).toFixed(4)); | ||
| const sonnet = Number((mtok * SONNET_INPUT_PER_MTOK).toFixed(4)); | ||
| const opus = Number((mtok * OPUS_INPUT_PER_MTOK).toFixed(4)); | ||
| return { | ||
| period_days: days, | ||
| total_tokens: usage.total_tokens, | ||
| estimated_cost_usd_haiku: haiku, | ||
| estimated_cost_usd_sonnet: sonnet, | ||
| estimated_cost_usd_opus: opus, | ||
| estimated_cost_usd_min: haiku, | ||
| estimated_cost_usd_max: opus, | ||
| by_source: usage.by_source, | ||
| disclaimer: "Coste estimado de tokens de herramientas (input al modelo). Haiku $1, Sonnet $3, Opus $5 por MTok. Revisa tu factura Anthropic para el coste real." | ||
| }; | ||
| } | ||
| function getActiveBudgetSummary(db, sessionId, projectHash) { | ||
| const mgr = new BudgetManager(db); | ||
| return mgr.checkBudget(sessionId, projectHash); | ||
| } | ||
| export { | ||
| getUsageStats, | ||
| getCostReport, | ||
| getActiveBudgetSummary | ||
| }; | ||
| //# sourceMappingURL=chunk-633PY32C.js.map |
| {"version":3,"sources":["../src/services/stats.ts"],"sourcesContent":["// Shared stats service — Phase 4.4\n// Read-only aggregates used by CLI and MCP tools.\n\nimport type Database from 'better-sqlite3'\nimport { buildQueries, type ToolCountRow, type SourceCountRow } from '../db/queries.js'\nimport { BudgetManager } from './budget-manager.js'\nimport type { BudgetStatus } from '../lib/types.js'\n\ntype DB = Database.Database\n\nconst DAY_MS = 86_400_000\n// Pricing April 2026 — input tokens (tool output → model input)\n// Haiku 4.5: $1/$5, Sonnet 4.6: $3/$15, Opus 4.6: $5/$25 per MTok (input/output)\n// token-optimizer tracks tool output (= model input), so we use input pricing.\nconst HAIKU_INPUT_PER_MTOK = 1\nconst SONNET_INPUT_PER_MTOK = 3\nconst OPUS_INPUT_PER_MTOK = 5\n\nfunction sinceDays(days: number): string {\n return new Date(Date.now() - days * DAY_MS).toISOString()\n}\n\nexport interface UsageStats {\n period_days: number\n period_since: string\n by_tool: ToolCountRow[]\n by_source: SourceCountRow[]\n total_tokens: number\n total_events: number\n}\n\nexport interface CostReport {\n period_days: number\n total_tokens: number\n estimated_cost_usd_haiku: number\n estimated_cost_usd_sonnet: number\n estimated_cost_usd_opus: number\n /** @deprecated Use estimated_cost_usd_haiku */\n estimated_cost_usd_min: number\n /** @deprecated Use estimated_cost_usd_opus */\n estimated_cost_usd_max: number\n by_source: SourceCountRow[]\n disclaimer: string\n}\n\nexport interface SavingsToday {\n date: string\n by_source: SourceCountRow[]\n total_tokens: number\n note: string\n}\n\nexport function getUsageStats(db: DB, days = 7): UsageStats {\n const queries = buildQueries(db)\n const since = sinceDays(days)\n const byTool = queries.countToolCallsByTool(since)\n const bySource = queries.countToolCallsBySource(since)\n const totalTokens = bySource.reduce((sum, r) => sum + r.tokens, 0)\n const totalEvents = bySource.reduce((sum, r) => sum + r.count, 0)\n return {\n period_days: days,\n period_since: since,\n by_tool: byTool,\n by_source: bySource,\n total_tokens: totalTokens,\n total_events: totalEvents,\n }\n}\n\nexport function getCostReport(db: DB, days = 7): CostReport {\n const usage = getUsageStats(db, days)\n const mtok = usage.total_tokens / 1_000_000\n const haiku = Number((mtok * HAIKU_INPUT_PER_MTOK).toFixed(4))\n const sonnet = Number((mtok * SONNET_INPUT_PER_MTOK).toFixed(4))\n const opus = Number((mtok * OPUS_INPUT_PER_MTOK).toFixed(4))\n return {\n period_days: days,\n total_tokens: usage.total_tokens,\n estimated_cost_usd_haiku: haiku,\n estimated_cost_usd_sonnet: sonnet,\n estimated_cost_usd_opus: opus,\n estimated_cost_usd_min: haiku,\n estimated_cost_usd_max: opus,\n by_source: usage.by_source,\n disclaimer:\n 'Coste estimado de tokens de herramientas (input al modelo). Haiku $1, Sonnet $3, Opus $5 por MTok. Revisa tu factura Anthropic para el coste real.',\n }\n}\n\nexport function getActiveBudgetSummary(\n db: DB,\n sessionId: string,\n projectHash: string | null,\n): BudgetStatus {\n const mgr = new BudgetManager(db)\n return mgr.checkBudget(sessionId, projectHash)\n}\n\nexport function getSavingsToday(db: DB): SavingsToday {\n const queries = buildQueries(db)\n const since = sinceDays(1)\n const bySource = queries.countToolCallsBySource(since)\n const total = bySource.reduce((sum, r) => sum + r.tokens, 0)\n return {\n date: new Date().toISOString().slice(0, 10),\n by_source: bySource,\n total_tokens: total,\n note: 'Ahorros por fuente no medidos directamente; revisa el reporte para el split Medido/Estimado.',\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAM,SAAS;AAIf,IAAM,uBAAuB;AAC7B,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB;AAE5B,SAAS,UAAU,MAAsB;AACvC,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,MAAM,EAAE,YAAY;AAC1D;AAgCO,SAAS,cAAc,IAAQ,OAAO,GAAe;AAC1D,QAAM,UAAU,aAAa,EAAE;AAC/B,QAAM,QAAQ,UAAU,IAAI;AAC5B,QAAM,SAAS,QAAQ,qBAAqB,KAAK;AACjD,QAAM,WAAW,QAAQ,uBAAuB,KAAK;AACrD,QAAM,cAAc,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,QAAQ,CAAC;AACjE,QAAM,cAAc,SAAS,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AAChE,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc;AAAA,IACd,SAAS;AAAA,IACT,WAAW;AAAA,IACX,cAAc;AAAA,IACd,cAAc;AAAA,EAChB;AACF;AAEO,SAAS,cAAc,IAAQ,OAAO,GAAe;AAC1D,QAAM,QAAQ,cAAc,IAAI,IAAI;AACpC,QAAM,OAAO,MAAM,eAAe;AAClC,QAAM,QAAQ,QAAQ,OAAO,sBAAsB,QAAQ,CAAC,CAAC;AAC7D,QAAM,SAAS,QAAQ,OAAO,uBAAuB,QAAQ,CAAC,CAAC;AAC/D,QAAM,OAAO,QAAQ,OAAO,qBAAqB,QAAQ,CAAC,CAAC;AAC3D,SAAO;AAAA,IACL,aAAa;AAAA,IACb,cAAc,MAAM;AAAA,IACpB,0BAA0B;AAAA,IAC1B,2BAA2B;AAAA,IAC3B,yBAAyB;AAAA,IACzB,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,IACxB,WAAW,MAAM;AAAA,IACjB,YACE;AAAA,EACJ;AACF;AAEO,SAAS,uBACd,IACA,WACA,aACc;AACd,QAAM,MAAM,IAAI,cAAc,EAAE;AAChC,SAAO,IAAI,YAAY,WAAW,WAAW;AAC/C;","names":[]} |
| #!/usr/bin/env node | ||
| // src/lib/paths.ts | ||
| import path from "path"; | ||
| import fs from "fs"; | ||
| import os from "os"; | ||
| import crypto from "crypto"; | ||
| var IS_WINDOWS = process.platform === "win32"; | ||
| function normalizePath(p) { | ||
| const resolved = path.resolve(p); | ||
| return IS_WINDOWS ? resolved.toLowerCase() : resolved; | ||
| } | ||
| function resolveProjectDir(cwd = process.cwd()) { | ||
| let current = path.resolve(cwd); | ||
| const initial = current; | ||
| while (true) { | ||
| if (fs.existsSync(path.join(current, ".git")) || fs.existsSync(path.join(current, "package.json"))) { | ||
| return current; | ||
| } | ||
| const parent = path.dirname(current); | ||
| if (parent === current) return initial; | ||
| current = parent; | ||
| } | ||
| } | ||
| function resolveAnalyticsDbPath(_projectDir) { | ||
| return path.join(resolveGlobalDir(), "analytics.db"); | ||
| } | ||
| function resolveGlobalDir() { | ||
| const override = process.env.TOKEN_OPTIMIZER_HOME; | ||
| if (override && override.trim().length > 0) return override; | ||
| return path.join(os.homedir(), ".token-optimizer"); | ||
| } | ||
| function projectHash(projectDir) { | ||
| return crypto.createHash("sha256").update(normalizePath(projectDir)).digest("hex").slice(0, 16); | ||
| } | ||
| function resolveTranscriptPath(projectDir, sessionId) { | ||
| const claudeDir = path.join(os.homedir(), ".claude", "projects"); | ||
| const projectKey = path.resolve(projectDir).replace(/[:\\/]/g, "-"); | ||
| return path.join(claudeDir, projectKey, `${sessionId}.jsonl`); | ||
| } | ||
| export { | ||
| normalizePath, | ||
| resolveProjectDir, | ||
| resolveAnalyticsDbPath, | ||
| resolveGlobalDir, | ||
| projectHash, | ||
| resolveTranscriptPath | ||
| }; | ||
| //# sourceMappingURL=chunk-AWG3ZQRZ.js.map |
| {"version":3,"sources":["../src/lib/paths.ts"],"sourcesContent":["// Path helpers — Phase 1.4\n// Cross-platform project dir resolution, storage dir, transcript path\n\nimport path from 'node:path'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport crypto from 'node:crypto'\n\nconst IS_WINDOWS = process.platform === 'win32'\n\nexport function normalizePath(p: string): string {\n const resolved = path.resolve(p)\n return IS_WINDOWS ? resolved.toLowerCase() : resolved\n}\n\nexport function resolveProjectDir(cwd: string = process.cwd()): string {\n let current = path.resolve(cwd)\n const initial = current\n // Walk up looking for .git or package.json; fall back to cwd if not found\n while (true) {\n if (\n fs.existsSync(path.join(current, '.git')) ||\n fs.existsSync(path.join(current, 'package.json'))\n ) {\n return current\n }\n const parent = path.dirname(current)\n if (parent === current) return initial\n current = parent\n }\n}\n\n/**\n * Path to the analytics DB. v0.4.7+: always returns the global DB under ~/.token-optimizer/\n * so hooks, CLI and MCP tools share a single source of truth regardless of CWD.\n * Per-project filtering is still available via `sessions.project_hash`.\n *\n * The `projectDir` argument is kept for backward compatibility with existing callers\n * and tests (tests pass an explicit `dbPath` bypassing this function entirely).\n */\nexport function resolveAnalyticsDbPath(_projectDir: string): string {\n return path.join(resolveGlobalDir(), 'analytics.db')\n}\n\nexport function resolveGlobalDir(): string {\n // TOKEN_OPTIMIZER_HOME overrides the default for tests and multi-user setups.\n const override = process.env.TOKEN_OPTIMIZER_HOME\n if (override && override.trim().length > 0) return override\n return path.join(os.homedir(), '.token-optimizer')\n}\n\nexport function ensureGlobalStorageDir(): string {\n const dir = resolveGlobalDir()\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n return dir\n}\n\nexport function projectHash(projectDir: string): string {\n return crypto.createHash('sha256').update(normalizePath(projectDir)).digest('hex').slice(0, 16)\n}\n\n/**\n * Resolve the Claude Code transcript JSONL path for a given project + session.\n * Claude Code stores transcripts under `~/.claude/projects/{project-key}/{sessionId}.jsonl`\n * where project-key replaces path separators (/, \\, :) with dashes.\n */\nexport function resolveTranscriptPath(projectDir: string, sessionId: string): string {\n const claudeDir = path.join(os.homedir(), '.claude', 'projects')\n const projectKey = path.resolve(projectDir).replace(/[:\\\\/]/g, '-')\n return path.join(claudeDir, projectKey, `${sessionId}.jsonl`)\n}\n"],"mappings":";;;AAGA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,OAAO,QAAQ;AACf,OAAO,YAAY;AAEnB,IAAM,aAAa,QAAQ,aAAa;AAEjC,SAAS,cAAc,GAAmB;AAC/C,QAAM,WAAW,KAAK,QAAQ,CAAC;AAC/B,SAAO,aAAa,SAAS,YAAY,IAAI;AAC/C;AAEO,SAAS,kBAAkB,MAAc,QAAQ,IAAI,GAAW;AACrE,MAAI,UAAU,KAAK,QAAQ,GAAG;AAC9B,QAAM,UAAU;AAEhB,SAAO,MAAM;AACX,QACE,GAAG,WAAW,KAAK,KAAK,SAAS,MAAM,CAAC,KACxC,GAAG,WAAW,KAAK,KAAK,SAAS,cAAc,CAAC,GAChD;AACA,aAAO;AAAA,IACT;AACA,UAAM,SAAS,KAAK,QAAQ,OAAO;AACnC,QAAI,WAAW,QAAS,QAAO;AAC/B,cAAU;AAAA,EACZ;AACF;AAUO,SAAS,uBAAuB,aAA6B;AAClE,SAAO,KAAK,KAAK,iBAAiB,GAAG,cAAc;AACrD;AAEO,SAAS,mBAA2B;AAEzC,QAAM,WAAW,QAAQ,IAAI;AAC7B,MAAI,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO;AACnD,SAAO,KAAK,KAAK,GAAG,QAAQ,GAAG,kBAAkB;AACnD;AAQO,SAAS,YAAY,YAA4B;AACtD,SAAO,OAAO,WAAW,QAAQ,EAAE,OAAO,cAAc,UAAU,CAAC,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AAChG;AAOO,SAAS,sBAAsB,YAAoB,WAA2B;AACnF,QAAM,YAAY,KAAK,KAAK,GAAG,QAAQ,GAAG,WAAW,UAAU;AAC/D,QAAM,aAAa,KAAK,QAAQ,UAAU,EAAE,QAAQ,WAAW,GAAG;AAClE,SAAO,KAAK,KAAK,WAAW,YAAY,GAAG,SAAS,QAAQ;AAC9D;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveGlobalDir | ||
| } from "./chunk-AWG3ZQRZ.js"; | ||
| // src/cli/config.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var DEFAULT_CONFIG = { | ||
| xray_url: null, | ||
| shadow_measurement: { serena: false }, | ||
| rtk_integration: { rtk_db_path: null }, | ||
| coach: { | ||
| enabled: true, | ||
| auto_surface: true, | ||
| posttooluse_throttle: 20, | ||
| sessionstart_tips_max: 3, | ||
| context_thresholds: { | ||
| info: 0.5, | ||
| warn: 0.75, | ||
| critical: 0.9 | ||
| }, | ||
| dedupe_window_seconds: 60, | ||
| stale_tip_days: 90 | ||
| } | ||
| }; | ||
| function getConfigPath(home) { | ||
| if (home !== void 0) { | ||
| return path.join(home, ".token-optimizer", "config.json"); | ||
| } | ||
| return path.join(resolveGlobalDir(), "config.json"); | ||
| } | ||
| function deepMerge(target, source) { | ||
| if (source === null || typeof source !== "object") return target; | ||
| if (typeof target !== "object" || target === null) return target; | ||
| const result = { ...target }; | ||
| const src = source; | ||
| for (const key of Object.keys(src)) { | ||
| const s = src[key]; | ||
| const t = result[key]; | ||
| if (s !== null && typeof s === "object" && !Array.isArray(s) && t !== null && typeof t === "object" && !Array.isArray(t)) { | ||
| result[key] = deepMerge(t, s); | ||
| } else { | ||
| result[key] = s; | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| function loadConfig(home) { | ||
| const p = getConfigPath(home); | ||
| try { | ||
| if (!fs.existsSync(p)) return DEFAULT_CONFIG; | ||
| const raw = fs.readFileSync(p, "utf8"); | ||
| const parsed = JSON.parse(raw); | ||
| return deepMerge(DEFAULT_CONFIG, parsed); | ||
| } catch { | ||
| return DEFAULT_CONFIG; | ||
| } | ||
| } | ||
| function saveConfig(config, home) { | ||
| const p = getConfigPath(home); | ||
| const dir = path.dirname(p); | ||
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(config, null, 2)); | ||
| } | ||
| function dotGet(obj, dotted) { | ||
| const parts = dotted.split("."); | ||
| let cur = obj; | ||
| for (const p of parts) { | ||
| if (typeof cur !== "object" || cur === null) return void 0; | ||
| cur = cur[p]; | ||
| } | ||
| return cur; | ||
| } | ||
| function dotSet(obj, dotted, value) { | ||
| const parts = dotted.split("."); | ||
| let cur = obj; | ||
| for (let i = 0; i < parts.length - 1; i++) { | ||
| const key = parts[i]; | ||
| const next = cur[key]; | ||
| if (typeof next !== "object" || next === null || Array.isArray(next)) { | ||
| cur[key] = {}; | ||
| } | ||
| cur = cur[key]; | ||
| } | ||
| cur[parts[parts.length - 1]] = value; | ||
| } | ||
| function coerceValue(raw) { | ||
| if (raw === "true") return true; | ||
| if (raw === "false") return false; | ||
| if (raw === "null") return null; | ||
| if (raw.trim() !== "" && !Number.isNaN(Number(raw))) return Number(raw); | ||
| return raw; | ||
| } | ||
| function runConfigCommand(args, opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const sub = args[0]; | ||
| if (sub === "get") { | ||
| const key = args[1]; | ||
| const cfg = loadConfig(opts.home); | ||
| if (!key) { | ||
| print(JSON.stringify(cfg, null, 2)); | ||
| return 0; | ||
| } | ||
| const value = dotGet(cfg, key); | ||
| print(value === void 0 ? "(undefined)" : JSON.stringify(value)); | ||
| return 0; | ||
| } | ||
| if (sub === "set") { | ||
| const key = args[1]; | ||
| const rawValue = args[2]; | ||
| if (!key || rawValue === void 0) { | ||
| print("Uso: token-optimizer-mcp config set <key> <value>"); | ||
| return 1; | ||
| } | ||
| const cfg = loadConfig(opts.home); | ||
| dotSet(cfg, key, coerceValue(rawValue)); | ||
| saveConfig(cfg, opts.home); | ||
| print(`Guardado: ${key} = ${rawValue}`); | ||
| return 0; | ||
| } | ||
| print("Uso: token-optimizer-mcp config <get|set> [key] [value]"); | ||
| return 1; | ||
| } | ||
| function resolveXrayUrl(home) { | ||
| const cfg = loadConfig(home); | ||
| return cfg.xray_url ?? process.env.XRAY_URL ?? null; | ||
| } | ||
| export { | ||
| DEFAULT_CONFIG, | ||
| getConfigPath, | ||
| loadConfig, | ||
| saveConfig, | ||
| runConfigCommand, | ||
| resolveXrayUrl | ||
| }; | ||
| //# sourceMappingURL=chunk-DBLVAFU5.js.map |
| {"version":3,"sources":["../src/cli/config.ts"],"sourcesContent":["// Config CLI + loader — Phase 4.8\n// Reads/writes ~/.token-optimizer/config.json. Supports dotted key get/set.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { resolveGlobalDir } from '../lib/paths.js'\n\nexport interface CoachConfig {\n enabled: boolean\n auto_surface: boolean\n posttooluse_throttle: number\n sessionstart_tips_max: number\n context_thresholds: {\n info: number\n warn: number\n critical: number\n }\n dedupe_window_seconds: number\n stale_tip_days: number\n}\n\nexport interface Config {\n xray_url: string | null\n shadow_measurement: {\n serena: boolean\n }\n rtk_integration: {\n rtk_db_path: string | null\n }\n coach: CoachConfig\n}\n\nexport const DEFAULT_CONFIG: Config = {\n xray_url: null,\n shadow_measurement: { serena: false },\n rtk_integration: { rtk_db_path: null },\n coach: {\n enabled: true,\n auto_surface: true,\n posttooluse_throttle: 20,\n sessionstart_tips_max: 3,\n context_thresholds: {\n info: 0.5,\n warn: 0.75,\n critical: 0.9,\n },\n dedupe_window_seconds: 60,\n stale_tip_days: 90,\n },\n}\n\nexport function getConfigPath(home?: string): string {\n // Respect explicit `home` override first (tests, CLI --home). Otherwise use\n // resolveGlobalDir() which honours TOKEN_OPTIMIZER_HOME env var, keeping the\n // config path consistent with the analytics.db path in every caller.\n if (home !== undefined) {\n return path.join(home, '.token-optimizer', 'config.json')\n }\n return path.join(resolveGlobalDir(), 'config.json')\n}\n\nfunction deepMerge<T>(target: T, source: unknown): T {\n if (source === null || typeof source !== 'object') return target\n if (typeof target !== 'object' || target === null) return target\n const result: Record<string, unknown> = { ...(target as Record<string, unknown>) }\n const src = source as Record<string, unknown>\n for (const key of Object.keys(src)) {\n const s = src[key]\n const t = result[key]\n if (\n s !== null &&\n typeof s === 'object' &&\n !Array.isArray(s) &&\n t !== null &&\n typeof t === 'object' &&\n !Array.isArray(t)\n ) {\n result[key] = deepMerge(t, s)\n } else {\n result[key] = s\n }\n }\n return result as T\n}\n\nexport function loadConfig(home?: string): Config {\n const p = getConfigPath(home)\n try {\n if (!fs.existsSync(p)) return DEFAULT_CONFIG\n const raw = fs.readFileSync(p, 'utf8')\n const parsed = JSON.parse(raw) as unknown\n return deepMerge(DEFAULT_CONFIG, parsed)\n } catch {\n return DEFAULT_CONFIG\n }\n}\n\nexport function saveConfig(config: Config, home?: string): void {\n const p = getConfigPath(home)\n const dir = path.dirname(p)\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(p, JSON.stringify(config, null, 2))\n}\n\nfunction dotGet(obj: unknown, dotted: string): unknown {\n const parts = dotted.split('.')\n let cur: unknown = obj\n for (const p of parts) {\n if (typeof cur !== 'object' || cur === null) return undefined\n cur = (cur as Record<string, unknown>)[p]\n }\n return cur\n}\n\nfunction dotSet(obj: Record<string, unknown>, dotted: string, value: unknown): void {\n const parts = dotted.split('.')\n let cur: Record<string, unknown> = obj\n for (let i = 0; i < parts.length - 1; i++) {\n const key = parts[i]\n const next = cur[key]\n if (typeof next !== 'object' || next === null || Array.isArray(next)) {\n cur[key] = {}\n }\n cur = cur[key] as Record<string, unknown>\n }\n cur[parts[parts.length - 1]] = value\n}\n\nfunction coerceValue(raw: string): unknown {\n if (raw === 'true') return true\n if (raw === 'false') return false\n if (raw === 'null') return null\n if (raw.trim() !== '' && !Number.isNaN(Number(raw))) return Number(raw)\n return raw\n}\n\nexport interface ConfigCliOptions {\n home?: string\n print?: (msg: string) => void\n}\n\nexport function runConfigCommand(args: string[], opts: ConfigCliOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const sub = args[0]\n if (sub === 'get') {\n const key = args[1]\n const cfg = loadConfig(opts.home)\n if (!key) {\n print(JSON.stringify(cfg, null, 2))\n return 0\n }\n const value = dotGet(cfg, key)\n print(value === undefined ? '(undefined)' : JSON.stringify(value))\n return 0\n }\n if (sub === 'set') {\n const key = args[1]\n const rawValue = args[2]\n if (!key || rawValue === undefined) {\n print('Uso: token-optimizer-mcp config set <key> <value>')\n return 1\n }\n const cfg = loadConfig(opts.home) as unknown as Record<string, unknown>\n dotSet(cfg, key, coerceValue(rawValue))\n saveConfig(cfg as unknown as Config, opts.home)\n print(`Guardado: ${key} = ${rawValue}`)\n return 0\n }\n print('Uso: token-optimizer-mcp config <get|set> [key] [value]')\n return 1\n}\n\n/**\n * Resolve xray URL: config.json xray_url > XRAY_URL env var > null\n */\nexport function resolveXrayUrl(home?: string): string | null {\n const cfg = loadConfig(home)\n return cfg.xray_url ?? process.env.XRAY_URL ?? null\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| // src/orchestration/detector.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| function readSettings(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return null; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function globalSettings(home) { | ||
| return path.join(home, ".claude", "settings.json"); | ||
| } | ||
| function globalClaudeJson(home) { | ||
| return path.join(home, ".claude.json"); | ||
| } | ||
| function localSettings(cwd) { | ||
| return path.join(cwd, ".claude", "settings.local.json"); | ||
| } | ||
| function teamSettings(cwd) { | ||
| return path.join(cwd, ".claude", "settings.json"); | ||
| } | ||
| function mcpServerKeys(json) { | ||
| if (!json) return []; | ||
| const mcp = json.mcpServers; | ||
| if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) { | ||
| return Object.keys(mcp); | ||
| } | ||
| return []; | ||
| } | ||
| function runProbe(name, checks) { | ||
| const signals = []; | ||
| let hits = 0; | ||
| for (const check of checks) { | ||
| try { | ||
| const [hit, label] = check(); | ||
| if (hit) { | ||
| hits++; | ||
| signals.push(label); | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| const confidence = checks.length > 0 ? hits / checks.length : 0; | ||
| return { | ||
| present: hits > 0, | ||
| confidence, | ||
| signals, | ||
| details: { probe: name, signal_count: hits, total_checks: checks.length } | ||
| }; | ||
| } | ||
| function probeSerena(paths = {}) { | ||
| const home = paths.home ?? os.homedir(); | ||
| const cwd = paths.cwd ?? process.cwd(); | ||
| return runProbe("serena", [ | ||
| () => { | ||
| const keys = mcpServerKeys(readSettings(globalSettings(home))); | ||
| return [keys.some((k) => k.toLowerCase().includes("serena")), "global-settings-registered"]; | ||
| }, | ||
| () => { | ||
| const keys = mcpServerKeys(readSettings(globalClaudeJson(home))); | ||
| return [keys.some((k) => k.toLowerCase().includes("serena")), "claude-json-registered"]; | ||
| }, | ||
| () => { | ||
| const keys = mcpServerKeys(readSettings(teamSettings(cwd))); | ||
| return [keys.some((k) => k.toLowerCase().includes("serena")), "project-mcp-registered"]; | ||
| }, | ||
| () => { | ||
| const keys = mcpServerKeys(readSettings(localSettings(cwd))); | ||
| return [keys.some((k) => k.toLowerCase().includes("serena")), "local-mcp-registered"]; | ||
| }, | ||
| () => { | ||
| const configPath = path.join(home, ".serena", "serena_config.yml"); | ||
| if (!fs.existsSync(configPath)) return [false, "project-registered-for-cwd"]; | ||
| try { | ||
| const content = fs.readFileSync(configPath, "utf8"); | ||
| const normalizedCwd = cwd.replace(/\\/g, "/").toLowerCase(); | ||
| const normalizedContent = content.replace(/\\/g, "/").toLowerCase(); | ||
| return [normalizedContent.includes(normalizedCwd), "project-registered-for-cwd"]; | ||
| } catch { | ||
| return [false, "project-registered-for-cwd"]; | ||
| } | ||
| } | ||
| ]); | ||
| } | ||
| function checkSerenaHealth(paths = {}) { | ||
| const home = paths.home ?? os.homedir(); | ||
| const cwd = paths.cwd ?? process.cwd(); | ||
| const warnings = []; | ||
| try { | ||
| const configPath = path.join(home, ".serena", "serena_config.yml"); | ||
| if (fs.existsSync(configPath)) { | ||
| const content = fs.readFileSync(configPath, "utf8"); | ||
| const match = content.match(/web_dashboard_open_on_launch\s*:\s*(\w+)/); | ||
| const value = match ? match[1].toLowerCase() : "true"; | ||
| if (value === "true") { | ||
| warnings.push({ | ||
| id: "dashboard-auto-open", | ||
| message: "El dashboard de Serena se abre automaticamente al iniciar cada terminal", | ||
| fix: "Pon web_dashboard_open_on_launch: false en ~/.serena/serena_config.yml" | ||
| }); | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| try { | ||
| const hasContextFlag = checkSerenaContextFlag(home, cwd); | ||
| if (!hasContextFlag) { | ||
| warnings.push({ | ||
| id: "missing-context-claude-code", | ||
| message: "Serena no usa el contexto claude-code (modo headless optimizado para CLI)", | ||
| fix: "A\xF1ade --context claude-code a los args del MCP server de serena" | ||
| }); | ||
| } | ||
| } catch { | ||
| } | ||
| return warnings; | ||
| } | ||
| function checkSerenaContextFlag(home, cwd) { | ||
| const settingsFiles = [ | ||
| path.join(home, ".claude", "settings.json"), | ||
| path.join(home, ".claude.json"), | ||
| path.join(cwd, ".claude", "settings.json"), | ||
| path.join(cwd, ".claude", "settings.local.json") | ||
| ]; | ||
| for (const file of settingsFiles) { | ||
| if (hasContextClaudeCodeInFile(file)) return true; | ||
| } | ||
| try { | ||
| const pluginsDir = path.join(home, ".claude", "plugins"); | ||
| if (fs.existsSync(pluginsDir)) { | ||
| const mcpFiles = findMcpJsonFiles(pluginsDir); | ||
| for (const file of mcpFiles) { | ||
| if (hasContextClaudeCodeInFile(file)) return true; | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| return false; | ||
| } | ||
| function hasContextClaudeCodeInFile(filePath) { | ||
| try { | ||
| if (!fs.existsSync(filePath)) return false; | ||
| const json = JSON.parse(fs.readFileSync(filePath, "utf8")); | ||
| const servers = json.mcpServers ?? json; | ||
| if (!servers || typeof servers !== "object") return false; | ||
| for (const key of Object.keys(servers)) { | ||
| if (!key.toLowerCase().includes("serena")) continue; | ||
| const server = servers[key]; | ||
| if (!server || !Array.isArray(server.args)) continue; | ||
| const args = server.args; | ||
| for (let i = 0; i < args.length; i++) { | ||
| const a = args[i]; | ||
| if ((a === "--context" || a === "-c") && args[i + 1] === "claude-code") { | ||
| return true; | ||
| } | ||
| if (a === "--context=claude-code" || a === "-c=claude-code") { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| return false; | ||
| } | ||
| function findMcpJsonFiles(dir) { | ||
| const results = []; | ||
| try { | ||
| const entries = fs.readdirSync(dir, { withFileTypes: true }); | ||
| for (const entry of entries) { | ||
| const full = path.join(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| results.push(...findMcpJsonFiles(full)); | ||
| } else if (entry.name === ".mcp.json") { | ||
| results.push(full); | ||
| } | ||
| } | ||
| } catch { | ||
| } | ||
| return results; | ||
| } | ||
| function probeRtk(paths = {}) { | ||
| const home = paths.home ?? os.homedir(); | ||
| const isWindows = process.platform === "win32"; | ||
| return runProbe("rtk", [ | ||
| () => { | ||
| const rtkDb = path.join(home, ".rtk", "tracking.db"); | ||
| return [fs.existsSync(rtkDb), "rtk-db-present"]; | ||
| }, | ||
| () => { | ||
| const bin = isWindows ? path.join(home, ".cargo", "bin", "rtk.exe") : path.join(home, ".cargo", "bin", "rtk"); | ||
| return [fs.existsSync(bin), "rtk-binary-in-cargo"]; | ||
| }, | ||
| () => { | ||
| const pathDirs = (process.env.PATH ?? "").split(path.delimiter); | ||
| const binName = process.platform === "win32" ? "rtk.exe" : "rtk"; | ||
| const found = pathDirs.some((dir) => { | ||
| try { | ||
| return fs.existsSync(path.join(dir, binName)); | ||
| } catch { | ||
| return false; | ||
| } | ||
| }); | ||
| return [found, "rtk-binary-in-path"]; | ||
| }, | ||
| () => { | ||
| const json = readSettings(globalSettings(home)); | ||
| const hooks = json?.hooks; | ||
| if (!hooks || typeof hooks !== "object") return [false, "rtk-hook-registered"]; | ||
| const serialized = JSON.stringify(hooks); | ||
| return [serialized.toLowerCase().includes("rtk"), "rtk-hook-registered"]; | ||
| }, | ||
| () => { | ||
| const json = readSettings(globalSettings(home)); | ||
| const hooks = json?.hooks; | ||
| if (!hooks || typeof hooks !== "object") return [false, "token-optimizer-bridge-active"]; | ||
| const serialized = JSON.stringify(hooks); | ||
| const hasPreToolUse = serialized.includes("token-optimizer") && serialized.includes("pretooluse"); | ||
| return [hasPreToolUse, "token-optimizer-bridge-active"]; | ||
| } | ||
| ]); | ||
| } | ||
| function probeMcpPruning(paths = {}) { | ||
| const cwd = paths.cwd ?? process.cwd(); | ||
| return runProbe("mcp_pruning", [ | ||
| () => { | ||
| const json = readSettings(localSettings(cwd)); | ||
| const allowlist = json?.enabledMcpjsonServers; | ||
| return [Array.isArray(allowlist) && allowlist.length > 0, "allowlist-in-settings-local"]; | ||
| }, | ||
| () => { | ||
| const json = readSettings(teamSettings(cwd)); | ||
| const allowlist = json?.enabledMcpjsonServers; | ||
| return [Array.isArray(allowlist) && allowlist.length > 0, "allowlist-in-settings"]; | ||
| } | ||
| ]); | ||
| } | ||
| function probePromptCaching() { | ||
| return { | ||
| present: true, | ||
| confidence: 0.5, | ||
| signals: ["claude-code-default-enabled"], | ||
| details: { | ||
| probe: "prompt_caching", | ||
| note: "Revisa tu factura Anthropic para confirmar el ahorro real" | ||
| } | ||
| }; | ||
| } | ||
| export { | ||
| probeSerena, | ||
| checkSerenaHealth, | ||
| probeRtk, | ||
| probeMcpPruning, | ||
| probePromptCaching | ||
| }; | ||
| //# sourceMappingURL=chunk-DOYJNIB2.js.map |
| {"version":3,"sources":["../src/orchestration/detector.ts"],"sourcesContent":["// Detection probes — Phase 4.1\r\n// Multi-signal checks for serena, RTK, MCP pruning and prompt caching.\r\n// Each probe returns DetectionResult { present, confidence, signals, details }\r\n\r\nimport fs from 'node:fs'\r\nimport path from 'node:path'\r\nimport os from 'node:os'\r\nimport type { DetectionResult, SerenaHealthWarning } from '../lib/types.js'\r\n\r\nfunction readSettings(p: string): Record<string, unknown> | null {\r\n try {\r\n if (!fs.existsSync(p)) return null\r\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\r\n } catch {\r\n return null\r\n }\r\n}\r\n\r\nexport interface DetectorPaths {\r\n home?: string\r\n cwd?: string\r\n}\r\n\r\nfunction globalSettings(home: string): string {\r\n return path.join(home, '.claude', 'settings.json')\r\n}\r\n\r\nfunction globalClaudeJson(home: string): string {\r\n return path.join(home, '.claude.json')\r\n}\r\n\r\nfunction localSettings(cwd: string): string {\r\n return path.join(cwd, '.claude', 'settings.local.json')\r\n}\r\n\r\nfunction teamSettings(cwd: string): string {\r\n return path.join(cwd, '.claude', 'settings.json')\r\n}\r\n\r\nfunction mcpServerKeys(json: Record<string, unknown> | null): string[] {\r\n if (!json) return []\r\n const mcp = json.mcpServers\r\n if (mcp && typeof mcp === 'object' && !Array.isArray(mcp)) {\r\n return Object.keys(mcp as Record<string, unknown>)\r\n }\r\n return []\r\n}\r\n\r\nfunction runProbe(\r\n name: string,\r\n checks: Array<() => [boolean, string]>,\r\n): DetectionResult {\r\n const signals: string[] = []\r\n let hits = 0\r\n for (const check of checks) {\r\n try {\r\n const [hit, label] = check()\r\n if (hit) {\r\n hits++\r\n signals.push(label)\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n }\r\n const confidence = checks.length > 0 ? hits / checks.length : 0\r\n return {\r\n present: hits > 0,\r\n confidence,\r\n signals,\r\n details: { probe: name, signal_count: hits, total_checks: checks.length },\r\n }\r\n}\r\n\r\nexport function probeSerena(paths: DetectorPaths = {}): DetectionResult {\r\n const home = paths.home ?? os.homedir()\r\n const cwd = paths.cwd ?? process.cwd()\r\n return runProbe('serena', [\r\n () => {\r\n const keys = mcpServerKeys(readSettings(globalSettings(home)))\r\n return [keys.some((k) => k.toLowerCase().includes('serena')), 'global-settings-registered']\r\n },\r\n () => {\r\n // ~/.claude.json — Claude Code also reads MCP servers from here\r\n const keys = mcpServerKeys(readSettings(globalClaudeJson(home)))\r\n return [keys.some((k) => k.toLowerCase().includes('serena')), 'claude-json-registered']\r\n },\r\n () => {\r\n const keys = mcpServerKeys(readSettings(teamSettings(cwd)))\r\n return [keys.some((k) => k.toLowerCase().includes('serena')), 'project-mcp-registered']\r\n },\r\n () => {\r\n const keys = mcpServerKeys(readSettings(localSettings(cwd)))\r\n return [keys.some((k) => k.toLowerCase().includes('serena')), 'local-mcp-registered']\r\n },\r\n () => {\r\n // Check if current CWD is registered as a serena project\r\n const configPath = path.join(home, '.serena', 'serena_config.yml')\r\n if (!fs.existsSync(configPath)) return [false, 'project-registered-for-cwd']\r\n try {\r\n const content = fs.readFileSync(configPath, 'utf8')\r\n const normalizedCwd = cwd.replace(/\\\\/g, '/').toLowerCase()\r\n // Simple check: does the config mention a path matching our CWD?\r\n const normalizedContent = content.replace(/\\\\/g, '/').toLowerCase()\r\n return [normalizedContent.includes(normalizedCwd), 'project-registered-for-cwd']\r\n } catch {\r\n return [false, 'project-registered-for-cwd']\r\n }\r\n },\r\n ])\r\n}\r\n\r\n/**\r\n * Health checks for Serena configuration.\r\n * Separate from probeSerena() (presence detection) to avoid polluting confidence scores.\r\n * Returns actionable warnings when Serena is misconfigured for Claude Code usage.\r\n */\r\nexport function checkSerenaHealth(paths: DetectorPaths = {}): SerenaHealthWarning[] {\r\n const home = paths.home ?? os.homedir()\r\n const cwd = paths.cwd ?? process.cwd()\r\n const warnings: SerenaHealthWarning[] = []\r\n\r\n // Check 1: web_dashboard_open_on_launch should be false\r\n try {\r\n const configPath = path.join(home, '.serena', 'serena_config.yml')\r\n if (fs.existsSync(configPath)) {\r\n const content = fs.readFileSync(configPath, 'utf8')\r\n const match = content.match(/web_dashboard_open_on_launch\\s*:\\s*(\\w+)/)\r\n const value = match ? match[1].toLowerCase() : 'true' // default is true\r\n if (value === 'true') {\r\n warnings.push({\r\n id: 'dashboard-auto-open',\r\n message: 'El dashboard de Serena se abre automaticamente al iniciar cada terminal',\r\n fix: 'Pon web_dashboard_open_on_launch: false en ~/.serena/serena_config.yml',\r\n })\r\n }\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n\r\n // Check 2: --context claude-code should be in MCP server args\r\n try {\r\n const hasContextFlag = checkSerenaContextFlag(home, cwd)\r\n if (!hasContextFlag) {\r\n warnings.push({\r\n id: 'missing-context-claude-code',\r\n message: 'Serena no usa el contexto claude-code (modo headless optimizado para CLI)',\r\n fix: 'Añade --context claude-code a los args del MCP server de serena',\r\n })\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n\r\n return warnings\r\n}\r\n\r\nfunction checkSerenaContextFlag(home: string, cwd: string): boolean {\r\n // Search across all possible MCP config locations\r\n const settingsFiles = [\r\n path.join(home, '.claude', 'settings.json'),\r\n path.join(home, '.claude.json'),\r\n path.join(cwd, '.claude', 'settings.json'),\r\n path.join(cwd, '.claude', 'settings.local.json'),\r\n ]\r\n\r\n for (const file of settingsFiles) {\r\n if (hasContextClaudeCodeInFile(file)) return true\r\n }\r\n\r\n // Also check plugin .mcp.json files\r\n try {\r\n const pluginsDir = path.join(home, '.claude', 'plugins')\r\n if (fs.existsSync(pluginsDir)) {\r\n const mcpFiles = findMcpJsonFiles(pluginsDir)\r\n for (const file of mcpFiles) {\r\n if (hasContextClaudeCodeInFile(file)) return true\r\n }\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n\r\n return false\r\n}\r\n\r\nfunction hasContextClaudeCodeInFile(filePath: string): boolean {\r\n try {\r\n if (!fs.existsSync(filePath)) return false\r\n const json = JSON.parse(fs.readFileSync(filePath, 'utf8'))\r\n\r\n // Check mcpServers keys for serena entries\r\n const servers = json.mcpServers ?? json\r\n if (!servers || typeof servers !== 'object') return false\r\n\r\n for (const key of Object.keys(servers)) {\r\n if (!key.toLowerCase().includes('serena')) continue\r\n const server = servers[key]\r\n if (!server || !Array.isArray(server.args)) continue\r\n const args = server.args as string[]\r\n // Aceptar las 3 formas válidas en CLI:\r\n // --context claude-code (dos args separados)\r\n // --context=claude-code (un arg fusionado con =)\r\n // -c claude-code / -c=claude-code (forma corta)\r\n for (let i = 0; i < args.length; i++) {\r\n const a = args[i]\r\n if ((a === '--context' || a === '-c') && args[i + 1] === 'claude-code') {\r\n return true\r\n }\r\n if (a === '--context=claude-code' || a === '-c=claude-code') {\r\n return true\r\n }\r\n }\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n return false\r\n}\r\n\r\nfunction findMcpJsonFiles(dir: string): string[] {\r\n const results: string[] = []\r\n try {\r\n const entries = fs.readdirSync(dir, { withFileTypes: true })\r\n for (const entry of entries) {\r\n const full = path.join(dir, entry.name)\r\n if (entry.isDirectory()) {\r\n results.push(...findMcpJsonFiles(full))\r\n } else if (entry.name === '.mcp.json') {\r\n results.push(full)\r\n }\r\n }\r\n } catch {\r\n // swallow\r\n }\r\n return results\r\n}\r\n\r\nexport function probeRtk(paths: DetectorPaths = {}): DetectionResult {\r\n const home = paths.home ?? os.homedir()\r\n const isWindows = process.platform === 'win32'\r\n return runProbe('rtk', [\r\n () => {\r\n const rtkDb = path.join(home, '.rtk', 'tracking.db')\r\n return [fs.existsSync(rtkDb), 'rtk-db-present']\r\n },\r\n () => {\r\n const bin = isWindows\r\n ? path.join(home, '.cargo', 'bin', 'rtk.exe')\r\n : path.join(home, '.cargo', 'bin', 'rtk')\r\n return [fs.existsSync(bin), 'rtk-binary-in-cargo']\r\n },\r\n () => {\r\n // Check common PATH locations directly (avoid dynamic import in sync probe)\r\n const pathDirs = (process.env.PATH ?? '').split(path.delimiter)\r\n const binName = process.platform === 'win32' ? 'rtk.exe' : 'rtk'\r\n const found = pathDirs.some((dir) => {\r\n try {\r\n return fs.existsSync(path.join(dir, binName))\r\n } catch {\r\n return false\r\n }\r\n })\r\n return [found, 'rtk-binary-in-path']\r\n },\r\n () => {\r\n const json = readSettings(globalSettings(home))\r\n const hooks = json?.hooks\r\n if (!hooks || typeof hooks !== 'object') return [false, 'rtk-hook-registered']\r\n const serialized = JSON.stringify(hooks)\r\n return [serialized.toLowerCase().includes('rtk'), 'rtk-hook-registered']\r\n },\r\n () => {\r\n // Check if token-optimizer PreToolUse hook is installed (acts as RTK bridge)\r\n const json = readSettings(globalSettings(home))\r\n const hooks = json?.hooks\r\n if (!hooks || typeof hooks !== 'object') return [false, 'token-optimizer-bridge-active']\r\n const serialized = JSON.stringify(hooks)\r\n const hasPreToolUse = serialized.includes('token-optimizer') && serialized.includes('pretooluse')\r\n return [hasPreToolUse, 'token-optimizer-bridge-active']\r\n },\r\n ])\r\n}\r\n\r\nexport function probeMcpPruning(paths: DetectorPaths = {}): DetectionResult {\r\n const cwd = paths.cwd ?? process.cwd()\r\n return runProbe('mcp_pruning', [\r\n () => {\r\n const json = readSettings(localSettings(cwd))\r\n const allowlist = json?.enabledMcpjsonServers\r\n return [Array.isArray(allowlist) && allowlist.length > 0, 'allowlist-in-settings-local']\r\n },\r\n () => {\r\n const json = readSettings(teamSettings(cwd))\r\n const allowlist = json?.enabledMcpjsonServers\r\n return [Array.isArray(allowlist) && allowlist.length > 0, 'allowlist-in-settings']\r\n },\r\n ])\r\n}\r\n\r\nexport function probePromptCaching(): DetectionResult {\r\n // Claude Code has prompt caching enabled by default; no reliable local probe.\r\n return {\r\n present: true,\r\n confidence: 0.5,\r\n signals: ['claude-code-default-enabled'],\r\n details: {\r\n probe: 'prompt_caching',\r\n note: 'Revisa tu factura Anthropic para confirmar el ahorro real',\r\n },\r\n }\r\n}\r\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| buildSuggestions | ||
| } from "./chunk-XTFQTQMU.js"; | ||
| import { | ||
| checkSerenaHealth, | ||
| probeMcpPruning, | ||
| probePromptCaching, | ||
| probeRtk, | ||
| probeSerena | ||
| } from "./chunk-DOYJNIB2.js"; | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-L5Z32XXL.js"; | ||
| // src/cli/doctor.ts | ||
| function symbol(present) { | ||
| return present ? "\u2713" : "\u2717"; | ||
| } | ||
| function runDoctor(_args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const paths = { home: opts.home, cwd: opts.cwd }; | ||
| const serena = probeSerena(paths); | ||
| const rtk = probeRtk(paths); | ||
| const pruning = probeMcpPruning(paths); | ||
| const promptCaching = probePromptCaching(); | ||
| const schema = measureCurrentSchemaBytes(paths); | ||
| 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 lines = []; | ||
| lines.push("token-optimizer-mcp doctor"); | ||
| lines.push(""); | ||
| lines.push( | ||
| `[serena] ${symbol(status.serena.present)} conf=${status.serena.confidence.toFixed(2)} signals: ${status.serena.signals.join(", ") || "(ninguno)"}` | ||
| ); | ||
| if (serena.present) { | ||
| const healthWarnings = checkSerenaHealth(paths); | ||
| for (const w of healthWarnings) { | ||
| lines.push(` \u26A0 ${w.message} \u2014 ${w.fix}`); | ||
| } | ||
| } | ||
| lines.push( | ||
| `[rtk] ${symbol(status.rtk.present)} conf=${status.rtk.confidence.toFixed(2)} signals: ${status.rtk.signals.join(", ") || "(ninguno)"}` | ||
| ); | ||
| lines.push( | ||
| `[mcp-pruning] ${symbol(status.mcp_pruning.present)} conf=${status.mcp_pruning.confidence.toFixed(2)} signals: ${status.mcp_pruning.signals.join(", ") || "(ninguno)"}` | ||
| ); | ||
| lines.push( | ||
| `[prompt-cache] ~ activo por defecto en Claude Code \u2014 ${promptCaching.details.note}` | ||
| ); | ||
| lines.push( | ||
| `[schema-size] ~${schema.tool_schema_tokens} tokens / ${schema.tool_schema_bytes} bytes (${schema.measurement_method}) \u2014 ${schema.mcp_servers.length} MCP server(s): ${schema.mcp_servers.join(", ") || "(ninguno)"}` | ||
| ); | ||
| const suggestions = buildSuggestions(status, paths); | ||
| if (suggestions.length > 0) { | ||
| lines.push(""); | ||
| lines.push("Sugerencias:"); | ||
| for (const s of suggestions) { | ||
| lines.push(""); | ||
| lines.push(s); | ||
| } | ||
| } | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| export { | ||
| runDoctor | ||
| }; | ||
| //# sourceMappingURL=chunk-EEZSSD5Q.js.map |
| {"version":3,"sources":["../src/cli/doctor.ts"],"sourcesContent":["// Doctor CLI — Phase 4.12\n// Runs all detection probes + schema-measurer + advisor and prints a Spanish report.\n// Always exits 0.\n\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 type { OptimizationStatus } from '../lib/types.js'\n\nexport interface DoctorOptions {\n home?: string\n cwd?: string\n print?: (msg: string) => void\n}\n\nfunction symbol(present: boolean): string {\n return present ? '✓' : '✗'\n}\n\nexport function runDoctor(_args: string[] = [], opts: DoctorOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const paths = { home: opts.home, cwd: opts.cwd }\n\n const serena = probeSerena(paths)\n const rtk = probeRtk(paths)\n const pruning = probeMcpPruning(paths)\n const promptCaching = probePromptCaching()\n const schema = measureCurrentSchemaBytes(paths)\n\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\n const lines: string[] = []\n lines.push('token-optimizer-mcp doctor')\n lines.push('')\n lines.push(\n `[serena] ${symbol(status.serena.present)} conf=${status.serena.confidence.toFixed(2)} signals: ${status.serena.signals.join(', ') || '(ninguno)'}`,\n )\n if (serena.present) {\n const healthWarnings = checkSerenaHealth(paths)\n for (const w of healthWarnings) {\n lines.push(` ⚠ ${w.message} — ${w.fix}`)\n }\n }\n lines.push(\n `[rtk] ${symbol(status.rtk.present)} conf=${status.rtk.confidence.toFixed(2)} signals: ${status.rtk.signals.join(', ') || '(ninguno)'}`,\n )\n lines.push(\n `[mcp-pruning] ${symbol(status.mcp_pruning.present)} conf=${status.mcp_pruning.confidence.toFixed(2)} signals: ${status.mcp_pruning.signals.join(', ') || '(ninguno)'}`,\n )\n lines.push(\n `[prompt-cache] ~ activo por defecto en Claude Code — ${promptCaching.details.note as string}`,\n )\n lines.push(\n `[schema-size] ~${schema.tool_schema_tokens} tokens / ${schema.tool_schema_bytes} bytes (${schema.measurement_method}) — ${schema.mcp_servers.length} MCP server(s): ${schema.mcp_servers.join(', ') || '(ninguno)'}`,\n )\n\n const suggestions = buildSuggestions(status, paths)\n if (suggestions.length > 0) {\n lines.push('')\n lines.push('Sugerencias:')\n for (const s of suggestions) {\n lines.push('')\n lines.push(s)\n }\n }\n\n print(lines.join('\\n'))\n return 0\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAqBA,SAAS,OAAO,SAA0B;AACxC,SAAO,UAAU,WAAM;AACzB;AAEO,SAAS,UAAU,QAAkB,CAAC,GAAG,OAAsB,CAAC,GAAW;AAChF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,QAAQ,EAAE,MAAM,KAAK,MAAM,KAAK,KAAK,IAAI;AAE/C,QAAM,SAAS,YAAY,KAAK;AAChC,QAAM,MAAM,SAAS,KAAK;AAC1B,QAAM,UAAU,gBAAgB,KAAK;AACrC,QAAM,gBAAgB,mBAAmB;AACzC,QAAM,SAAS,0BAA0B,KAAK;AAE9C,QAAM,SAA6B;AAAA,IACjC;AAAA,IACA;AAAA,IACA,aAAa;AAAA,IACb,gBAAgB;AAAA,MACd,mBAAmB;AAAA,MACnB,gBAAgB;AAAA,MAChB,mBAAmB;AAAA,MACnB,MAAM;AAAA,IACR;AAAA,IACA,cAAc;AAAA,MACZ,mBAAmB,OAAO;AAAA,MAC1B,oBAAoB,OAAO;AAAA,IAC7B;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,4BAA4B;AACvC,QAAM,KAAK,EAAE;AACb,QAAM;AAAA,IACJ,mBAAmB,OAAO,OAAO,OAAO,OAAO,CAAC,SAAS,OAAO,OAAO,WAAW,QAAQ,CAAC,CAAC,cAAc,OAAO,OAAO,QAAQ,KAAK,IAAI,KAAK,WAAW;AAAA,EAC3J;AACA,MAAI,OAAO,SAAS;AAClB,UAAM,iBAAiB,kBAAkB,KAAK;AAC9C,eAAW,KAAK,gBAAgB;AAC9B,YAAM,KAAK,YAAO,EAAE,OAAO,WAAM,EAAE,GAAG,EAAE;AAAA,IAC1C;AAAA,EACF;AACA,QAAM;AAAA,IACJ,mBAAmB,OAAO,OAAO,IAAI,OAAO,CAAC,SAAS,OAAO,IAAI,WAAW,QAAQ,CAAC,CAAC,cAAc,OAAO,IAAI,QAAQ,KAAK,IAAI,KAAK,WAAW;AAAA,EAClJ;AACA,QAAM;AAAA,IACJ,mBAAmB,OAAO,OAAO,YAAY,OAAO,CAAC,SAAS,OAAO,YAAY,WAAW,QAAQ,CAAC,CAAC,cAAc,OAAO,YAAY,QAAQ,KAAK,IAAI,KAAK,WAAW;AAAA,EAC1K;AACA,QAAM;AAAA,IACJ,8DAAyD,cAAc,QAAQ,IAAc;AAAA,EAC/F;AACA,QAAM;AAAA,IACJ,oBAAoB,OAAO,kBAAkB,aAAa,OAAO,iBAAiB,WAAW,OAAO,kBAAkB,YAAO,OAAO,YAAY,MAAM,mBAAmB,OAAO,YAAY,KAAK,IAAI,KAAK,WAAW;AAAA,EACvN;AAEA,QAAM,cAAc,iBAAiB,QAAQ,KAAK;AAClD,MAAI,YAAY,SAAS,GAAG;AAC1B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,cAAc;AACzB,eAAW,KAAK,aAAa;AAC3B,YAAM,KAAK,EAAE;AACb,YAAM,KAAK,CAAC;AAAA,IACd;AAAA,EACF;AAEA,QAAM,MAAM,KAAK,IAAI,CAAC;AACtB,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| // src/orchestration/schema-measurer.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| var TOKENS_PER_TOOL = 400; | ||
| var ESTIMATED_TOOLS_PER_SERVER = 10; | ||
| var BYTES_PER_TOKEN = 4; | ||
| function readJson(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return null; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function collectServerNames(jsonFiles) { | ||
| const set = /* @__PURE__ */ new Set(); | ||
| for (const json of jsonFiles) { | ||
| if (!json) continue; | ||
| const mcp = json.mcpServers; | ||
| if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) { | ||
| for (const k of Object.keys(mcp)) { | ||
| set.add(k); | ||
| } | ||
| } | ||
| } | ||
| return Array.from(set); | ||
| } | ||
| function measureCurrentSchemaBytes(opts = {}) { | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const sources = [ | ||
| readJson(path.join(home, ".claude", "settings.json")), | ||
| readJson(path.join(home, ".claude.json")), | ||
| // Claude Code also reads MCPs from here | ||
| readJson(path.join(cwd, ".claude", "settings.json")), | ||
| readJson(path.join(cwd, ".claude", "settings.local.json")) | ||
| ]; | ||
| const servers = collectServerNames(sources); | ||
| const toolCount = servers.length * ESTIMATED_TOOLS_PER_SERVER; | ||
| const tokens = toolCount * TOKENS_PER_TOOL; | ||
| return { | ||
| tool_schema_bytes: tokens * BYTES_PER_TOKEN, | ||
| tool_schema_tokens: tokens, | ||
| tool_count_estimated: toolCount, | ||
| mcp_servers: servers, | ||
| measurement_method: servers.length > 0 ? "heuristic" : "unknown" | ||
| }; | ||
| } | ||
| export { | ||
| measureCurrentSchemaBytes | ||
| }; | ||
| //# sourceMappingURL=chunk-L5Z32XXL.js.map |
| {"version":3,"sources":["../src/orchestration/schema-measurer.ts"],"sourcesContent":["// Tool-schema size measurement — Phase 4.2\n// Heuristic: count registered MCP servers from settings files and estimate tool-schema cost.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport os from 'node:os'\n\nconst TOKENS_PER_TOOL = 400\nconst ESTIMATED_TOOLS_PER_SERVER = 10\nconst BYTES_PER_TOKEN = 4\n\nexport interface SchemaMeasurement {\n tool_schema_bytes: number\n tool_schema_tokens: number\n tool_count_estimated: number\n mcp_servers: string[]\n measurement_method: 'accurate' | 'heuristic' | 'unknown'\n}\n\nexport interface SchemaMeasurerOptions {\n home?: string\n cwd?: string\n}\n\nfunction readJson(p: string): Record<string, unknown> | null {\n try {\n if (!fs.existsSync(p)) return null\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\n } catch {\n return null\n }\n}\n\nfunction collectServerNames(jsonFiles: Array<Record<string, unknown> | null>): string[] {\n const set = new Set<string>()\n for (const json of jsonFiles) {\n if (!json) continue\n const mcp = json.mcpServers\n if (mcp && typeof mcp === 'object' && !Array.isArray(mcp)) {\n for (const k of Object.keys(mcp as Record<string, unknown>)) {\n set.add(k)\n }\n }\n }\n return Array.from(set)\n}\n\nexport function measureCurrentSchemaBytes(\n opts: SchemaMeasurerOptions = {},\n): SchemaMeasurement {\n const home = opts.home ?? os.homedir()\n const cwd = opts.cwd ?? process.cwd()\n const sources = [\n readJson(path.join(home, '.claude', 'settings.json')),\n readJson(path.join(home, '.claude.json')), // Claude Code also reads MCPs from here\n readJson(path.join(cwd, '.claude', 'settings.json')),\n readJson(path.join(cwd, '.claude', 'settings.local.json')),\n ]\n const servers = collectServerNames(sources)\n const toolCount = servers.length * ESTIMATED_TOOLS_PER_SERVER\n const tokens = toolCount * TOKENS_PER_TOOL\n return {\n tool_schema_bytes: tokens * BYTES_PER_TOKEN,\n tool_schema_tokens: tokens,\n tool_count_estimated: toolCount,\n mcp_servers: servers,\n measurement_method: servers.length > 0 ? 'heuristic' : 'unknown',\n }\n}\n"],"mappings":";;;AAGA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAEf,IAAM,kBAAkB;AACxB,IAAM,6BAA6B;AACnC,IAAM,kBAAkB;AAexB,SAAS,SAAS,GAA2C;AAC3D,MAAI;AACF,QAAI,CAAC,GAAG,WAAW,CAAC,EAAG,QAAO;AAC9B,WAAO,KAAK,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,mBAAmB,WAA4D;AACtF,QAAM,MAAM,oBAAI,IAAY;AAC5B,aAAW,QAAQ,WAAW;AAC5B,QAAI,CAAC,KAAM;AACX,UAAM,MAAM,KAAK;AACjB,QAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,GAAG,GAAG;AACzD,iBAAW,KAAK,OAAO,KAAK,GAA8B,GAAG;AAC3D,YAAI,IAAI,CAAC;AAAA,MACX;AAAA,IACF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,GAAG;AACvB;AAEO,SAAS,0BACd,OAA8B,CAAC,GACZ;AACnB,QAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ;AACrC,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,UAAU;AAAA,IACd,SAAS,KAAK,KAAK,MAAM,WAAW,eAAe,CAAC;AAAA,IACpD,SAAS,KAAK,KAAK,MAAM,cAAc,CAAC;AAAA;AAAA,IACxC,SAAS,KAAK,KAAK,KAAK,WAAW,eAAe,CAAC;AAAA,IACnD,SAAS,KAAK,KAAK,KAAK,WAAW,qBAAqB,CAAC;AAAA,EAC3D;AACA,QAAM,UAAU,mBAAmB,OAAO;AAC1C,QAAM,YAAY,QAAQ,SAAS;AACnC,QAAM,SAAS,YAAY;AAC3B,SAAO;AAAA,IACL,mBAAmB,SAAS;AAAA,IAC5B,oBAAoB;AAAA,IACpB,sBAAsB;AAAA,IACtB,aAAa;AAAA,IACb,oBAAoB,QAAQ,SAAS,IAAI,cAAc;AAAA,EACzD;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveXrayUrl | ||
| } from "./chunk-DBLVAFU5.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| resolveTranscriptPath | ||
| } from "./chunk-AWG3ZQRZ.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.6.1" : "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-PMVZIR3X.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 | ||
| // src/lib/storage.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var GITIGNORE_ENTRIES = [".serena/"]; | ||
| function ensureGitignore(projectDir) { | ||
| const gitDir = path.join(projectDir, ".git"); | ||
| if (!fs.existsSync(gitDir)) return; | ||
| const gitignorePath = path.join(projectDir, ".gitignore"); | ||
| let current = ""; | ||
| if (fs.existsSync(gitignorePath)) { | ||
| current = fs.readFileSync(gitignorePath, "utf8"); | ||
| } | ||
| const lines = current.split(/\r?\n/).map((l) => l.trim()); | ||
| const missing = GITIGNORE_ENTRIES.filter( | ||
| (entry) => fs.existsSync(path.join(projectDir, entry)) && !lines.includes(entry) | ||
| ); | ||
| if (missing.length === 0) return; | ||
| const prefix = current.length > 0 && !current.endsWith("\n") ? "\n" : ""; | ||
| fs.appendFileSync(gitignorePath, `${prefix}${missing.join("\n")} | ||
| `); | ||
| } | ||
| export { | ||
| ensureGitignore | ||
| }; | ||
| //# sourceMappingURL=chunk-VHU3U64E.js.map |
| {"version":3,"sources":["../src/lib/storage.ts"],"sourcesContent":["// Gitignore management — appends .serena/ to .gitignore idempotently.\n// The MCP no longer creates a per-project .token-optimizer/ dir: all storage\n// is global under ~/.token-optimizer/ (analytics.db, config.json).\n\nimport fs from 'node:fs'\nimport path from 'node:path'\n\nconst GITIGNORE_ENTRIES = ['.serena/']\n\nexport function ensureGitignore(projectDir: string): void {\n const gitDir = path.join(projectDir, '.git')\n if (!fs.existsSync(gitDir)) return\n\n const gitignorePath = path.join(projectDir, '.gitignore')\n let current = ''\n if (fs.existsSync(gitignorePath)) {\n current = fs.readFileSync(gitignorePath, 'utf8')\n }\n const lines = current.split(/\\r?\\n/).map((l) => l.trim())\n const missing = GITIGNORE_ENTRIES.filter(\n (entry) => fs.existsSync(path.join(projectDir, entry)) && !lines.includes(entry),\n )\n if (missing.length === 0) return\n const prefix = current.length > 0 && !current.endsWith('\\n') ? '\\n' : ''\n fs.appendFileSync(gitignorePath, `${prefix}${missing.join('\\n')}\\n`)\n}\n"],"mappings":";;;AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AAEjB,IAAM,oBAAoB,CAAC,UAAU;AAE9B,SAAS,gBAAgB,YAA0B;AACxD,QAAM,SAAS,KAAK,KAAK,YAAY,MAAM;AAC3C,MAAI,CAAC,GAAG,WAAW,MAAM,EAAG;AAE5B,QAAM,gBAAgB,KAAK,KAAK,YAAY,YAAY;AACxD,MAAI,UAAU;AACd,MAAI,GAAG,WAAW,aAAa,GAAG;AAChC,cAAU,GAAG,aAAa,eAAe,MAAM;AAAA,EACjD;AACA,QAAM,QAAQ,QAAQ,MAAM,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACxD,QAAM,UAAU,kBAAkB;AAAA,IAChC,CAAC,UAAU,GAAG,WAAW,KAAK,KAAK,YAAY,KAAK,CAAC,KAAK,CAAC,MAAM,SAAS,KAAK;AAAA,EACjF;AACA,MAAI,QAAQ,WAAW,EAAG;AAC1B,QAAM,SAAS,QAAQ,SAAS,KAAK,CAAC,QAAQ,SAAS,IAAI,IAAI,OAAO;AACtE,KAAG,eAAe,eAAe,GAAG,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAC;AAAA,CAAI;AACrE;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| // src/services/budget-manager.ts | ||
| var MAX_TOKENS = 1e7; | ||
| var BudgetManager = class { | ||
| queries; | ||
| constructor(db) { | ||
| this.queries = buildQueries(db); | ||
| } | ||
| setBudget(input) { | ||
| if (!Number.isInteger(input.limit_tokens)) { | ||
| throw new Error("limit_tokens debe ser un entero"); | ||
| } | ||
| if (input.limit_tokens <= 0) { | ||
| throw new Error("limit_tokens debe ser mayor que 0"); | ||
| } | ||
| if (input.limit_tokens > MAX_TOKENS) { | ||
| throw new Error(`limit_tokens no puede exceder ${MAX_TOKENS}`); | ||
| } | ||
| if (input.scope !== "session" && input.scope !== "project") { | ||
| throw new Error(`scope invalido: ${String(input.scope)}`); | ||
| } | ||
| const mode = "warn"; | ||
| this.queries.upsertBudget(input.scope, input.scope_key, input.limit_tokens, mode); | ||
| const budget = this.queries.getBudgetByScope(input.scope, input.scope_key); | ||
| if (!budget) { | ||
| throw new Error("no se pudo leer el budget recien creado"); | ||
| } | ||
| return budget; | ||
| } | ||
| /** | ||
| * Resolve the active budget for a given session + project. | ||
| * Returns the session-scoped budget if present, else the project-scoped one, else null. | ||
| */ | ||
| getActiveBudget(sessionId, projectHash) { | ||
| const sessionBudget = this.queries.getBudgetByScope("session", sessionId); | ||
| if (sessionBudget) return sessionBudget; | ||
| if (projectHash) { | ||
| const projectBudget = this.queries.getBudgetByScope("project", projectHash); | ||
| if (projectBudget) return projectBudget; | ||
| } | ||
| return null; | ||
| } | ||
| /** Compute spent tokens against a budget from tool_calls since the budget creation timestamp. */ | ||
| computeSpent(budget) { | ||
| if (budget.scope === "session") { | ||
| return this.queries.sumTokensBySessionSince(budget.scope_key, budget.created_at); | ||
| } | ||
| return this.queries.sumTokensByProjectSince(budget.scope_key, budget.created_at); | ||
| } | ||
| checkBudget(sessionId, projectHash) { | ||
| const active = this.getActiveBudget(sessionId, projectHash); | ||
| if (!active) { | ||
| return { active: false, spent: 0, remaining: 0, percent_used: 0, mode: null }; | ||
| } | ||
| const spent = this.computeSpent(active); | ||
| const remaining = Math.max(0, active.limit_tokens - spent); | ||
| const percent = active.limit_tokens > 0 ? spent / active.limit_tokens : 0; | ||
| return { | ||
| active: true, | ||
| spent, | ||
| remaining, | ||
| percent_used: percent, | ||
| mode: active.mode | ||
| }; | ||
| } | ||
| clearBudget(scope, scopeKey) { | ||
| return this.queries.deleteBudgetByScope(scope, scopeKey) > 0; | ||
| } | ||
| getBudgetReport(since) { | ||
| return { | ||
| by_tool: this.queries.countToolCallsByTool(since), | ||
| by_source: this.queries.countToolCallsBySource(since), | ||
| period_since: since | ||
| }; | ||
| } | ||
| recordBudgetEvent(budgetId, eventType, tokens) { | ||
| this.queries.insertBudgetEvent(budgetId, eventType, tokens); | ||
| } | ||
| }; | ||
| export { | ||
| BudgetManager | ||
| }; | ||
| //# sourceMappingURL=chunk-VV5KKIQ4.js.map |
| {"version":3,"sources":["../src/services/budget-manager.ts"],"sourcesContent":["// Budget manager — Phase 2.1 + 2.3\n// Precedence: session > project. Spent tokens computed from tool_calls since budget.created_at.\n\nimport type Database from 'better-sqlite3'\nimport type {\n Budget,\n BudgetScope,\n BudgetMode,\n BudgetStatus,\n} from '../lib/types.js'\nimport { buildQueries, type Queries, type ToolCountRow, type SourceCountRow } from '../db/queries.js'\n\ntype DB = Database.Database\n\nexport interface SetBudgetInput {\n scope: BudgetScope\n scope_key: string\n limit_tokens: number\n mode?: BudgetMode\n}\n\nexport interface BudgetReport {\n by_tool: ToolCountRow[]\n by_source: SourceCountRow[]\n period_since: string\n}\n\nconst MAX_TOKENS = 1e7\n\nexport class BudgetManager {\n private queries: Queries\n\n constructor(db: DB) {\n this.queries = buildQueries(db)\n }\n\n setBudget(input: SetBudgetInput): Budget {\n if (!Number.isInteger(input.limit_tokens)) {\n throw new Error('limit_tokens debe ser un entero')\n }\n if (input.limit_tokens <= 0) {\n throw new Error('limit_tokens debe ser mayor que 0')\n }\n if (input.limit_tokens > MAX_TOKENS) {\n throw new Error(`limit_tokens no puede exceder ${MAX_TOKENS}`)\n }\n if (input.scope !== 'session' && input.scope !== 'project') {\n throw new Error(`scope invalido: ${String(input.scope)}`)\n }\n const mode: BudgetMode = 'warn'\n this.queries.upsertBudget(input.scope, input.scope_key, input.limit_tokens, mode)\n const budget = this.queries.getBudgetByScope(input.scope, input.scope_key)\n if (!budget) {\n throw new Error('no se pudo leer el budget recien creado')\n }\n return budget\n }\n\n /**\n * Resolve the active budget for a given session + project.\n * Returns the session-scoped budget if present, else the project-scoped one, else null.\n */\n getActiveBudget(sessionId: string, projectHash: string | null): Budget | null {\n const sessionBudget = this.queries.getBudgetByScope('session', sessionId)\n if (sessionBudget) return sessionBudget\n if (projectHash) {\n const projectBudget = this.queries.getBudgetByScope('project', projectHash)\n if (projectBudget) return projectBudget\n }\n return null\n }\n\n /** Compute spent tokens against a budget from tool_calls since the budget creation timestamp. */\n computeSpent(budget: Budget): number {\n if (budget.scope === 'session') {\n return this.queries.sumTokensBySessionSince(budget.scope_key, budget.created_at)\n }\n return this.queries.sumTokensByProjectSince(budget.scope_key, budget.created_at)\n }\n\n checkBudget(sessionId: string, projectHash: string | null): BudgetStatus {\n const active = this.getActiveBudget(sessionId, projectHash)\n if (!active) {\n return { active: false, spent: 0, remaining: 0, percent_used: 0, mode: null }\n }\n const spent = this.computeSpent(active)\n const remaining = Math.max(0, active.limit_tokens - spent)\n const percent = active.limit_tokens > 0 ? spent / active.limit_tokens : 0\n return {\n active: true,\n spent,\n remaining,\n percent_used: percent,\n mode: active.mode,\n }\n }\n\n clearBudget(scope: BudgetScope, scopeKey: string): boolean {\n return this.queries.deleteBudgetByScope(scope, scopeKey) > 0\n }\n\n getBudgetReport(since: string): BudgetReport {\n return {\n by_tool: this.queries.countToolCallsByTool(since),\n by_source: this.queries.countToolCallsBySource(since),\n period_since: since,\n }\n }\n\n recordBudgetEvent(\n budgetId: number,\n eventType: 'spend' | 'warn' | 'block' | 'reset',\n tokens: number | null,\n ): void {\n this.queries.insertBudgetEvent(budgetId, eventType, tokens)\n }\n}\n"],"mappings":";;;;;;AA2BA,IAAM,aAAa;AAEZ,IAAM,gBAAN,MAAoB;AAAA,EACjB;AAAA,EAER,YAAY,IAAQ;AAClB,SAAK,UAAU,aAAa,EAAE;AAAA,EAChC;AAAA,EAEA,UAAU,OAA+B;AACvC,QAAI,CAAC,OAAO,UAAU,MAAM,YAAY,GAAG;AACzC,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AACA,QAAI,MAAM,gBAAgB,GAAG;AAC3B,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AACA,QAAI,MAAM,eAAe,YAAY;AACnC,YAAM,IAAI,MAAM,iCAAiC,UAAU,EAAE;AAAA,IAC/D;AACA,QAAI,MAAM,UAAU,aAAa,MAAM,UAAU,WAAW;AAC1D,YAAM,IAAI,MAAM,mBAAmB,OAAO,MAAM,KAAK,CAAC,EAAE;AAAA,IAC1D;AACA,UAAM,OAAmB;AACzB,SAAK,QAAQ,aAAa,MAAM,OAAO,MAAM,WAAW,MAAM,cAAc,IAAI;AAChF,UAAM,SAAS,KAAK,QAAQ,iBAAiB,MAAM,OAAO,MAAM,SAAS;AACzE,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB,WAAmB,aAA2C;AAC5E,UAAM,gBAAgB,KAAK,QAAQ,iBAAiB,WAAW,SAAS;AACxE,QAAI,cAAe,QAAO;AAC1B,QAAI,aAAa;AACf,YAAM,gBAAgB,KAAK,QAAQ,iBAAiB,WAAW,WAAW;AAC1E,UAAI,cAAe,QAAO;AAAA,IAC5B;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,aAAa,QAAwB;AACnC,QAAI,OAAO,UAAU,WAAW;AAC9B,aAAO,KAAK,QAAQ,wBAAwB,OAAO,WAAW,OAAO,UAAU;AAAA,IACjF;AACA,WAAO,KAAK,QAAQ,wBAAwB,OAAO,WAAW,OAAO,UAAU;AAAA,EACjF;AAAA,EAEA,YAAY,WAAmB,aAA0C;AACvE,UAAM,SAAS,KAAK,gBAAgB,WAAW,WAAW;AAC1D,QAAI,CAAC,QAAQ;AACX,aAAO,EAAE,QAAQ,OAAO,OAAO,GAAG,WAAW,GAAG,cAAc,GAAG,MAAM,KAAK;AAAA,IAC9E;AACA,UAAM,QAAQ,KAAK,aAAa,MAAM;AACtC,UAAM,YAAY,KAAK,IAAI,GAAG,OAAO,eAAe,KAAK;AACzD,UAAM,UAAU,OAAO,eAAe,IAAI,QAAQ,OAAO,eAAe;AACxE,WAAO;AAAA,MACL,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA,cAAc;AAAA,MACd,MAAM,OAAO;AAAA,IACf;AAAA,EACF;AAAA,EAEA,YAAY,OAAoB,UAA2B;AACzD,WAAO,KAAK,QAAQ,oBAAoB,OAAO,QAAQ,IAAI;AAAA,EAC7D;AAAA,EAEA,gBAAgB,OAA6B;AAC3C,WAAO;AAAA,MACL,SAAS,KAAK,QAAQ,qBAAqB,KAAK;AAAA,MAChD,WAAW,KAAK,QAAQ,uBAAuB,KAAK;AAAA,MACpD,cAAc;AAAA,IAChB;AAAA,EACF;AAAA,EAEA,kBACE,UACA,WACA,QACM;AACN,SAAK,QAAQ,kBAAkB,UAAU,WAAW,MAAM;AAAA,EAC5D;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| checkSerenaHealth | ||
| } from "./chunk-DOYJNIB2.js"; | ||
| // src/orchestration/advisor.ts | ||
| function buildSuggestions(status, paths) { | ||
| const suggestions = []; | ||
| if (!status.serena.present) { | ||
| suggestions.push( | ||
| [ | ||
| "[serena] Para lecturas simbolicas (menos tokens en archivos grandes), instala serena-mcp:", | ||
| " uvx --from git+https://github.com/oraios/serena serena start-mcp-server", | ||
| " Nota de seguridad: serena incluye execute_shell_command, revisa la configuracion.", | ||
| " Ahorro estimado: 20-30% en lecturas de archivos grandes." | ||
| ].join("\n") | ||
| ); | ||
| } else { | ||
| if (!status.serena.signals.includes("project-registered-for-cwd")) { | ||
| suggestions.push( | ||
| [ | ||
| "[serena] Serena esta instalada pero este proyecto no esta registrado.", | ||
| " Ejecuta: mcp__serena__activate_project con la ruta de este proyecto.", | ||
| " O crea .serena/project.yml en la raiz del proyecto para auto-deteccion.", | ||
| " Sin proyecto activo, serena no puede hacer lecturas simbolicas." | ||
| ].join("\n") | ||
| ); | ||
| } | ||
| const healthWarnings = checkSerenaHealth(paths); | ||
| for (const w of healthWarnings) { | ||
| suggestions.push(`[serena] \u26A0 ${w.message} | ||
| Fix: ${w.fix}`); | ||
| } | ||
| } | ||
| if (!status.rtk.present) { | ||
| suggestions.push( | ||
| [ | ||
| "[rtk] Para filtrar salida ruidosa de Bash (builds, tests), instala RTK:", | ||
| " brew install standard-input/tap/rtk (macOS) o descarga binario firmado en github.com/standard-input/rtk", | ||
| " Nota de seguridad: RTK publica releases firmadas con GPG.", | ||
| " Ahorro estimado: 15-25% en ciclos build/test." | ||
| ].join("\n") | ||
| ); | ||
| } else if (!status.rtk.signals.includes("rtk-hook-registered") && !status.rtk.signals.includes("token-optimizer-bridge-active")) { | ||
| suggestions.push( | ||
| [ | ||
| "[rtk] RTK esta instalado pero no tiene hooks configurados.", | ||
| " Ejecuta: npx @cocaxcode/token-optimizer-mcp install \u2014 el bridge PreToolUse reescribe comandos Bash via RTK automaticamente.", | ||
| " Sin hooks, RTK solo funciona si se invoca manualmente (rtk ls, rtk git, etc)." | ||
| ].join("\n") | ||
| ); | ||
| } | ||
| if (!status.mcp_pruning.present) { | ||
| suggestions.push( | ||
| [ | ||
| "[mcp-pruning] Reduce el coste de tool-schema activando un allowlist por proyecto:", | ||
| " Ejecuta mcp_prune_suggest para generar uno basado en tu historial y aplicalo con mcp_prune_apply.", | ||
| " Se escribe en .claude/settings.local.json (personal, no afecta al equipo).", | ||
| " Ahorro estimado: 5-12% por turno sobre el Tool Search nativo de Claude Code." | ||
| ].join("\n") | ||
| ); | ||
| } | ||
| return suggestions; | ||
| } | ||
| export { | ||
| buildSuggestions | ||
| }; | ||
| //# sourceMappingURL=chunk-XTFQTQMU.js.map |
| {"version":3,"sources":["../src/orchestration/advisor.ts"],"sourcesContent":["// Advisory suggestions — Phase 4.3\n// Takes an OptimizationStatus and returns Spanish actionable recommendations.\n\nimport type { OptimizationStatus } from '../lib/types.js'\nimport { checkSerenaHealth } from './detector.js'\nimport type { DetectorPaths } from './detector.js'\n\nexport function buildSuggestions(\n status: OptimizationStatus,\n paths?: DetectorPaths,\n): string[] {\n const suggestions: string[] = []\n\n if (!status.serena.present) {\n suggestions.push(\n [\n '[serena] Para lecturas simbolicas (menos tokens en archivos grandes), instala serena-mcp:',\n ' uvx --from git+https://github.com/oraios/serena serena start-mcp-server',\n ' Nota de seguridad: serena incluye execute_shell_command, revisa la configuracion.',\n ' Ahorro estimado: 20-30% en lecturas de archivos grandes.',\n ].join('\\n'),\n )\n } else {\n if (!status.serena.signals.includes('project-registered-for-cwd')) {\n suggestions.push(\n [\n '[serena] Serena esta instalada pero este proyecto no esta registrado.',\n ' Ejecuta: mcp__serena__activate_project con la ruta de este proyecto.',\n ' O crea .serena/project.yml en la raiz del proyecto para auto-deteccion.',\n ' Sin proyecto activo, serena no puede hacer lecturas simbolicas.',\n ].join('\\n'),\n )\n }\n\n // Health checks — only when serena is present\n const healthWarnings = checkSerenaHealth(paths)\n for (const w of healthWarnings) {\n suggestions.push(`[serena] ⚠ ${w.message}\\n Fix: ${w.fix}`)\n }\n }\n\n if (!status.rtk.present) {\n suggestions.push(\n [\n '[rtk] Para filtrar salida ruidosa de Bash (builds, tests), instala RTK:',\n ' brew install standard-input/tap/rtk (macOS) o descarga binario firmado en github.com/standard-input/rtk',\n ' Nota de seguridad: RTK publica releases firmadas con GPG.',\n ' Ahorro estimado: 15-25% en ciclos build/test.',\n ].join('\\n'),\n )\n } else if (!status.rtk.signals.includes('rtk-hook-registered') &&\n !status.rtk.signals.includes('token-optimizer-bridge-active')) {\n suggestions.push(\n [\n '[rtk] RTK esta instalado pero no tiene hooks configurados.',\n ' Ejecuta: npx @cocaxcode/token-optimizer-mcp install — el bridge PreToolUse reescribe comandos Bash via RTK automaticamente.',\n ' Sin hooks, RTK solo funciona si se invoca manualmente (rtk ls, rtk git, etc).',\n ].join('\\n'),\n )\n }\n\n if (!status.mcp_pruning.present) {\n suggestions.push(\n [\n '[mcp-pruning] Reduce el coste de tool-schema activando un allowlist por proyecto:',\n ' Ejecuta mcp_prune_suggest para generar uno basado en tu historial y aplicalo con mcp_prune_apply.',\n ' Se escribe en .claude/settings.local.json (personal, no afecta al equipo).',\n ' Ahorro estimado: 5-12% por turno sobre el Tool Search nativo de Claude Code.',\n ].join('\\n'),\n )\n }\n\n return suggestions\n}\n"],"mappings":";;;;;;AAOO,SAAS,iBACd,QACA,OACU;AACV,QAAM,cAAwB,CAAC;AAE/B,MAAI,CAAC,OAAO,OAAO,SAAS;AAC1B,gBAAY;AAAA,MACV;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,EACF,OAAO;AACL,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAS,4BAA4B,GAAG;AACjE,kBAAY;AAAA,QACV;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,EAAE,KAAK,IAAI;AAAA,MACb;AAAA,IACF;AAGA,UAAM,iBAAiB,kBAAkB,KAAK;AAC9C,eAAW,KAAK,gBAAgB;AAC9B,kBAAY,KAAK,mBAAc,EAAE,OAAO;AAAA,SAAY,EAAE,GAAG,EAAE;AAAA,IAC7D;AAAA,EACF;AAEA,MAAI,CAAC,OAAO,IAAI,SAAS;AACvB,gBAAY;AAAA,MACV;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,EACF,WAAW,CAAC,OAAO,IAAI,QAAQ,SAAS,qBAAqB,KAClD,CAAC,OAAO,IAAI,QAAQ,SAAS,+BAA+B,GAAG;AACxE,gBAAY;AAAA,MACV;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,EACF;AAEA,MAAI,CAAC,OAAO,YAAY,SAAS;AAC/B,gBAAY;AAAA,MACV;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,EACF;AAEA,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-L5Z32XXL.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-AWG3ZQRZ.js"; | ||
| // src/cli/prune-mcp.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/; | ||
| function extractServerFromToolName(toolName) { | ||
| const m = MCP_TOOL_RE.exec(toolName); | ||
| return m ? m[1] : null; | ||
| } | ||
| function settingsLocalPath(cwd) { | ||
| return path.join(cwd, ".claude", "settings.local.json"); | ||
| } | ||
| function readJsonSafe(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return {}; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| function writeJson(p, data) { | ||
| fs.mkdirSync(path.dirname(p), { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(data, null, 2)); | ||
| } | ||
| function generateFromHistory(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const days = opts.days ?? 14; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const serverCounts = {}; | ||
| if (fs.existsSync(dbPath)) { | ||
| const db = getDb(dbPath); | ||
| const rows = db.prepare( | ||
| `SELECT tool_name, COUNT(*) as count | ||
| FROM tool_calls | ||
| WHERE created_at >= ? AND tool_name LIKE 'mcp__%' | ||
| GROUP BY tool_name` | ||
| ).all(since); | ||
| for (const row of rows) { | ||
| const server = extractServerFromToolName(row.tool_name); | ||
| if (server) { | ||
| serverCounts[server] = (serverCounts[server] ?? 0) + row.count; | ||
| } | ||
| } | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd, home: opts.home }); | ||
| const registered = new Set(schema.mcp_servers); | ||
| const used = new Set(Object.keys(serverCounts)); | ||
| const inactive = [...registered].filter((s) => !used.has(s)); | ||
| return { | ||
| proposed_allowlist: [...used], | ||
| inactive_servers: inactive, | ||
| analysis_days: days, | ||
| total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0), | ||
| server_counts: serverCounts | ||
| }; | ||
| } | ||
| function timestampForBackup() { | ||
| return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); | ||
| } | ||
| function insertSnapshot(cwd, method, details) { | ||
| try { | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) return; | ||
| const db = getDb(dbPath); | ||
| db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run( | ||
| method, | ||
| JSON.stringify(details) | ||
| ); | ||
| } catch { | ||
| } | ||
| } | ||
| function applyAllowlist(allowlist, opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const source = opts.source ?? "cli"; | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| if (fs.existsSync(settingsPath)) { | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| } else { | ||
| fs.mkdirSync(path.dirname(backupPath), { recursive: true }); | ||
| fs.writeFileSync(backupPath, "{}"); | ||
| } | ||
| const current = readJsonSafe(settingsPath); | ||
| current.enabledMcpjsonServers = allowlist; | ||
| writeJson(settingsPath, current); | ||
| insertSnapshot(cwd, source === "mcp" ? "allowlist_generated_via_mcp" : "allowlist_generated", { | ||
| allowlist, | ||
| target: settingsPath, | ||
| backup: backupPath | ||
| }); | ||
| return { settings_path: settingsPath, backup_path: backupPath }; | ||
| } | ||
| function rollback(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const dir = path.dirname(settingsPath); | ||
| if (!fs.existsSync(dir)) return { restored: false, from: null }; | ||
| const backups = fs.readdirSync(dir).filter((f) => f.startsWith("settings.local.json.backup-")).sort(); | ||
| if (backups.length === 0) return { restored: false, from: null }; | ||
| const target = opts.to ? backups.find((b) => b.includes(opts.to)) : backups[backups.length - 1]; | ||
| if (!target) return { restored: false, from: null }; | ||
| const backupPath = path.join(dir, target); | ||
| fs.copyFileSync(backupPath, settingsPath); | ||
| insertSnapshot(cwd, "rollback", { from: backupPath, to: settingsPath }); | ||
| return { restored: true, from: backupPath }; | ||
| } | ||
| function clearAllowlist(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }; | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| const json = readJsonSafe(settingsPath); | ||
| delete json.enabledMcpjsonServers; | ||
| writeJson(settingsPath, json); | ||
| insertSnapshot(cwd, "allowlist_cleared", { backup: backupPath }); | ||
| return { cleared: true, backup_path: backupPath }; | ||
| } | ||
| function impact(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const db = getDb(dbPath); | ||
| const snapshot = db.prepare( | ||
| `SELECT created_at FROM optimization_snapshots | ||
| WHERE method LIKE 'allowlist_%' | ||
| ORDER BY created_at DESC LIMIT 1` | ||
| ).get(); | ||
| if (!snapshot) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const before = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const after = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const beforeAvg = before.avg; | ||
| const afterAvg = after.avg; | ||
| const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null; | ||
| const percent = beforeAvg !== null && beforeAvg > 0 && afterAvg !== null ? (afterAvg - beforeAvg) / beforeAvg : null; | ||
| return { | ||
| before_avg: beforeAvg, | ||
| after_avg: afterAvg, | ||
| delta, | ||
| percent, | ||
| snapshot_at: snapshot.created_at | ||
| }; | ||
| } | ||
| function runPruneMcp(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| if (args.includes("--generate-from-history")) { | ||
| const daysFlag = args.find((a) => a.startsWith("--days=")); | ||
| const days = daysFlag ? parseInt(daysFlag.split("=")[1], 10) : 14; | ||
| const result = generateFromHistory({ cwd, days }); | ||
| print(`Propuesta de allowlist (${days} dias de historial):`); | ||
| print(` Usados: ${result.proposed_allowlist.join(", ") || "(ninguno)"}`); | ||
| print(` Inactivos: ${result.inactive_servers.join(", ") || "(ninguno)"}`); | ||
| print(` Eventos MCP totales: ${result.total_mcp_events}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--apply")) { | ||
| const generated = generateFromHistory({ cwd }); | ||
| if (generated.proposed_allowlist.length === 0) { | ||
| print("No hay MCPs activos en el historial. Nada que aplicar."); | ||
| return 1; | ||
| } | ||
| const applied = applyAllowlist(generated.proposed_allowlist, { cwd }); | ||
| print(`Allowlist aplicado a ${applied.settings_path}`); | ||
| print(`Backup: ${applied.backup_path}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--rollback")) { | ||
| const toFlag = args.find((a) => a.startsWith("--to=")); | ||
| const to = toFlag ? toFlag.split("=")[1] : void 0; | ||
| const result = rollback({ cwd, to }); | ||
| if (result.restored) { | ||
| print(`Restaurado desde ${result.from}`); | ||
| return 0; | ||
| } | ||
| print("No hay backups disponibles."); | ||
| return 1; | ||
| } | ||
| if (args.includes("--clear")) { | ||
| const result = clearAllowlist({ cwd }); | ||
| print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : "Nada que eliminar"); | ||
| return 0; | ||
| } | ||
| if (args.includes("--impact")) { | ||
| const result = impact({ cwd }); | ||
| if (result.snapshot_at === null) { | ||
| print("No hay snapshots de allowlist todavia."); | ||
| return 0; | ||
| } | ||
| print(`Snapshot mas reciente: ${result.snapshot_at}`); | ||
| print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? "n/a"}`); | ||
| print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? "n/a"}`); | ||
| if (result.percent !== null) { | ||
| print(`Delta: ${(result.percent * 100).toFixed(1)}%`); | ||
| } | ||
| return 0; | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd }); | ||
| print(`MCPs registrados (${schema.mcp_servers.length}):`); | ||
| for (const s of schema.mcp_servers) { | ||
| print(` ${s}`); | ||
| } | ||
| print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`); | ||
| print(""); | ||
| print("Flags:"); | ||
| print(" --generate-from-history [--days N] Propone allowlist (read-only)"); | ||
| print(" --apply Aplica el allowlist generado"); | ||
| print(" --rollback [--to TIMESTAMP] Restaura el ultimo backup"); | ||
| print(" --clear Elimina allowlist actual"); | ||
| print(" --impact Compara antes/despues del ultimo snapshot"); | ||
| return 0; | ||
| } | ||
| export { | ||
| settingsLocalPath, | ||
| generateFromHistory, | ||
| applyAllowlist, | ||
| rollback, | ||
| clearAllowlist, | ||
| impact, | ||
| runPruneMcp | ||
| }; | ||
| //# sourceMappingURL=chunk-YKQGA3IS.js.map |
| {"version":3,"sources":["../src/cli/prune-mcp.ts"],"sourcesContent":["// prune-mcp CLI + service — Phase 4.16-4.22\n// Generate allowlist from history, apply/rollback/clear, compute impact.\n// Writes to .claude/settings.local.json (NOT settings.json).\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\n\nconst MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/\n\nfunction extractServerFromToolName(toolName: string): string | null {\n const m = MCP_TOOL_RE.exec(toolName)\n return m ? m[1] : null\n}\n\nexport function settingsLocalPath(cwd: string): string {\n return path.join(cwd, '.claude', 'settings.local.json')\n}\n\nfunction readJsonSafe(p: string): Record<string, unknown> {\n try {\n if (!fs.existsSync(p)) return {}\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\n } catch {\n return {}\n }\n}\n\nfunction writeJson(p: string, data: Record<string, unknown>): void {\n fs.mkdirSync(path.dirname(p), { recursive: true })\n fs.writeFileSync(p, JSON.stringify(data, null, 2))\n}\n\nexport interface GeneratedAllowlist {\n proposed_allowlist: string[]\n inactive_servers: string[]\n analysis_days: number\n total_mcp_events: number\n server_counts: Record<string, number>\n}\n\nexport interface GenerateOptions {\n cwd?: string\n days?: number\n home?: string\n}\n\nexport function generateFromHistory(opts: GenerateOptions = {}): GeneratedAllowlist {\n const cwd = opts.cwd ?? process.cwd()\n const days = opts.days ?? 14\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\n\n const serverCounts: Record<string, number> = {}\n if (fs.existsSync(dbPath)) {\n const db = getDb(dbPath)\n const rows = db\n .prepare(\n `SELECT tool_name, COUNT(*) as count\n FROM tool_calls\n WHERE created_at >= ? AND tool_name LIKE 'mcp__%'\n GROUP BY tool_name`,\n )\n .all(since) as Array<{ tool_name: string; count: number }>\n for (const row of rows) {\n const server = extractServerFromToolName(row.tool_name)\n if (server) {\n serverCounts[server] = (serverCounts[server] ?? 0) + row.count\n }\n }\n }\n\n const schema = measureCurrentSchemaBytes({ cwd, home: opts.home })\n const registered = new Set(schema.mcp_servers)\n const used = new Set(Object.keys(serverCounts))\n const inactive = [...registered].filter((s) => !used.has(s))\n\n return {\n proposed_allowlist: [...used],\n inactive_servers: inactive,\n analysis_days: days,\n total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0),\n server_counts: serverCounts,\n }\n}\n\nexport interface ApplyOptions {\n cwd?: string\n source?: 'cli' | 'mcp'\n}\n\nexport interface ApplyResult {\n settings_path: string\n backup_path: string\n}\n\nfunction timestampForBackup(): string {\n return new Date().toISOString().replace(/[:.]/g, '-')\n}\n\nfunction insertSnapshot(cwd: string, method: string, details: Record<string, unknown>): void {\n try {\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) return\n const db = getDb(dbPath)\n db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run(\n method,\n JSON.stringify(details),\n )\n } catch {\n // swallow\n }\n}\n\nexport function applyAllowlist(allowlist: string[], opts: ApplyOptions = {}): ApplyResult {\n const cwd = opts.cwd ?? process.cwd()\n const source = opts.source ?? 'cli'\n const settingsPath = settingsLocalPath(cwd)\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\n\n if (fs.existsSync(settingsPath)) {\n fs.copyFileSync(settingsPath, backupPath)\n } else {\n fs.mkdirSync(path.dirname(backupPath), { recursive: true })\n fs.writeFileSync(backupPath, '{}')\n }\n\n const current = readJsonSafe(settingsPath)\n current.enabledMcpjsonServers = allowlist\n writeJson(settingsPath, current)\n\n insertSnapshot(cwd, source === 'mcp' ? 'allowlist_generated_via_mcp' : 'allowlist_generated', {\n allowlist,\n target: settingsPath,\n backup: backupPath,\n })\n\n return { settings_path: settingsPath, backup_path: backupPath }\n}\n\nexport interface RollbackOptions {\n cwd?: string\n to?: string\n}\n\nexport interface RollbackResult {\n restored: boolean\n from: string | null\n}\n\nexport function rollback(opts: RollbackOptions = {}): RollbackResult {\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = settingsLocalPath(cwd)\n const dir = path.dirname(settingsPath)\n if (!fs.existsSync(dir)) return { restored: false, from: null }\n\n const backups = fs\n .readdirSync(dir)\n .filter((f) => f.startsWith('settings.local.json.backup-'))\n .sort()\n if (backups.length === 0) return { restored: false, from: null }\n\n const target = opts.to ? backups.find((b) => b.includes(opts.to!)) : backups[backups.length - 1]\n if (!target) return { restored: false, from: null }\n\n const backupPath = path.join(dir, target)\n fs.copyFileSync(backupPath, settingsPath)\n\n insertSnapshot(cwd, 'rollback', { from: backupPath, to: settingsPath })\n return { restored: true, from: backupPath }\n}\n\nexport function clearAllowlist(opts: { cwd?: string } = {}): { cleared: boolean; backup_path: string | null } {\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = settingsLocalPath(cwd)\n if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }\n\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\n fs.copyFileSync(settingsPath, backupPath)\n\n const json = readJsonSafe(settingsPath)\n delete json.enabledMcpjsonServers\n writeJson(settingsPath, json)\n\n insertSnapshot(cwd, 'allowlist_cleared', { backup: backupPath })\n return { cleared: true, backup_path: backupPath }\n}\n\nexport interface ImpactResult {\n before_avg: number | null\n after_avg: number | null\n delta: number | null\n percent: number | null\n snapshot_at: string | null\n}\n\nexport function impact(opts: { cwd?: string } = {}): ImpactResult {\n const cwd = opts.cwd ?? process.cwd()\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) {\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\n }\n const db = getDb(dbPath)\n const snapshot = db\n .prepare(\n `SELECT created_at FROM optimization_snapshots\n WHERE method LIKE 'allowlist_%'\n ORDER BY created_at DESC LIMIT 1`,\n )\n .get() as { created_at: string } | undefined\n if (!snapshot) {\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\n }\n\n const before = db\n .prepare(\n `SELECT AVG(tokens_estimated) as avg FROM (\n SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100\n )`,\n )\n .get(snapshot.created_at) as { avg: number | null }\n const after = db\n .prepare(\n `SELECT AVG(tokens_estimated) as avg FROM (\n SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100\n )`,\n )\n .get(snapshot.created_at) as { avg: number | null }\n\n const beforeAvg = before.avg\n const afterAvg = after.avg\n const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null\n const percent =\n beforeAvg !== null && beforeAvg > 0 && afterAvg !== null\n ? (afterAvg - beforeAvg) / beforeAvg\n : null\n\n return {\n before_avg: beforeAvg,\n after_avg: afterAvg,\n delta,\n percent,\n snapshot_at: snapshot.created_at,\n }\n}\n\nexport interface PruneMcpCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runPruneMcp(args: string[] = [], opts: PruneMcpCliOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n\n if (args.includes('--generate-from-history')) {\n const daysFlag = args.find((a) => a.startsWith('--days='))\n const days = daysFlag ? parseInt(daysFlag.split('=')[1], 10) : 14\n const result = generateFromHistory({ cwd, days })\n print(`Propuesta de allowlist (${days} dias de historial):`)\n print(` Usados: ${result.proposed_allowlist.join(', ') || '(ninguno)'}`)\n print(` Inactivos: ${result.inactive_servers.join(', ') || '(ninguno)'}`)\n print(` Eventos MCP totales: ${result.total_mcp_events}`)\n return 0\n }\n\n if (args.includes('--apply')) {\n const generated = generateFromHistory({ cwd })\n if (generated.proposed_allowlist.length === 0) {\n print('No hay MCPs activos en el historial. Nada que aplicar.')\n return 1\n }\n const applied = applyAllowlist(generated.proposed_allowlist, { cwd })\n print(`Allowlist aplicado a ${applied.settings_path}`)\n print(`Backup: ${applied.backup_path}`)\n return 0\n }\n\n if (args.includes('--rollback')) {\n const toFlag = args.find((a) => a.startsWith('--to='))\n const to = toFlag ? toFlag.split('=')[1] : undefined\n const result = rollback({ cwd, to })\n if (result.restored) {\n print(`Restaurado desde ${result.from}`)\n return 0\n }\n print('No hay backups disponibles.')\n return 1\n }\n\n if (args.includes('--clear')) {\n const result = clearAllowlist({ cwd })\n print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar')\n return 0\n }\n\n if (args.includes('--impact')) {\n const result = impact({ cwd })\n if (result.snapshot_at === null) {\n print('No hay snapshots de allowlist todavia.')\n return 0\n }\n print(`Snapshot mas reciente: ${result.snapshot_at}`)\n print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? 'n/a'}`)\n print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? 'n/a'}`)\n if (result.percent !== null) {\n print(`Delta: ${(result.percent * 100).toFixed(1)}%`)\n }\n return 0\n }\n\n // Default: list registered MCPs with estimated cost\n const schema = measureCurrentSchemaBytes({ cwd })\n print(`MCPs registrados (${schema.mcp_servers.length}):`)\n for (const s of schema.mcp_servers) {\n print(` ${s}`)\n }\n print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`)\n print('')\n print('Flags:')\n print(' --generate-from-history [--days N] Propone allowlist (read-only)')\n print(' --apply Aplica el allowlist generado')\n print(' --rollback [--to TIMESTAMP] Restaura el ultimo backup')\n print(' --clear Elimina allowlist actual')\n print(' --impact Compara antes/despues del ultimo snapshot')\n return 0\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| clearSurfaceLog, | ||
| measureContextSize, | ||
| runRules | ||
| } from "./chunk-PMVZIR3X.js"; | ||
| import "./chunk-DBLVAFU5.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-AWG3ZQRZ.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-HZTGRVVW.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 | ||
| import { | ||
| DEFAULT_CONFIG, | ||
| getConfigPath, | ||
| loadConfig, | ||
| resolveXrayUrl, | ||
| runConfigCommand, | ||
| saveConfig | ||
| } from "./chunk-DBLVAFU5.js"; | ||
| import "./chunk-AWG3ZQRZ.js"; | ||
| export { | ||
| DEFAULT_CONFIG, | ||
| getConfigPath, | ||
| loadConfig, | ||
| resolveXrayUrl, | ||
| runConfigCommand, | ||
| saveConfig | ||
| }; | ||
| //# sourceMappingURL=config-R4XV4JWR.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| #!/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-U6QRKVX3.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-ACPK7XZC.js"); | ||
| return mod.runDoctor(rest); | ||
| } | ||
| case "status": { | ||
| const mod = await import("./status-Q5QIU23E.js"); | ||
| return mod.runStatus(rest); | ||
| } | ||
| case "report": { | ||
| const mod = await import("./report-J4R7FQCT.js"); | ||
| return mod.runReport(rest); | ||
| } | ||
| case "budget": { | ||
| const mod = await import("./budget-ZBEVUS4Y.js"); | ||
| return mod.runBudgetCli(rest); | ||
| } | ||
| case "config": { | ||
| const mod = await import("./config-R4XV4JWR.js"); | ||
| return mod.runConfigCommand(rest); | ||
| } | ||
| case "prune-mcp": { | ||
| const mod = await import("./prune-mcp-HBAGDTUE.js"); | ||
| return mod.runPruneMcp(rest); | ||
| } | ||
| case "coach": { | ||
| const mod = await import("./coach-HZTGRVVW.js"); | ||
| return mod.runCoachCli(rest); | ||
| } | ||
| case "sync-xray": { | ||
| const mod = await import("./sync-xray-MUOJBDOV.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-OMTXHUCU.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 { | ||
| runDoctor | ||
| } from "./chunk-EEZSSD5Q.js"; | ||
| import "./chunk-XTFQTQMU.js"; | ||
| import "./chunk-DOYJNIB2.js"; | ||
| import "./chunk-L5Z32XXL.js"; | ||
| export { | ||
| runDoctor | ||
| }; | ||
| //# sourceMappingURL=doctor-ACPK7XZC.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| probeSerenaPresence | ||
| } from "./chunk-2NKYFIPW.js"; | ||
| import { | ||
| ensureGitignore | ||
| } from "./chunk-VHU3U64E.js"; | ||
| import { | ||
| getConfigPath, | ||
| loadConfig, | ||
| saveConfig | ||
| } from "./chunk-DBLVAFU5.js"; | ||
| import { | ||
| runDoctor | ||
| } from "./chunk-EEZSSD5Q.js"; | ||
| import "./chunk-XTFQTQMU.js"; | ||
| import "./chunk-DOYJNIB2.js"; | ||
| import "./chunk-L5Z32XXL.js"; | ||
| import "./chunk-AWG3ZQRZ.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"))) { | ||
| ensureGitignore(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-U6QRKVX3.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 appends .gitignore entries 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 { ensureGitignore } 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 ensureGitignore(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 { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| } from "./chunk-YKQGA3IS.js"; | ||
| import "./chunk-L5Z32XXL.js"; | ||
| import "./chunk-TOEPQYR3.js"; | ||
| import "./chunk-AWG3ZQRZ.js"; | ||
| export { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| }; | ||
| //# sourceMappingURL=prune-mcp-HBAGDTUE.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-AWG3ZQRZ.js"; | ||
| // src/cli/report.ts | ||
| import fs from "fs"; | ||
| var PERIOD_DAYS = { | ||
| session: 3650, | ||
| day: 1, | ||
| week: 7, | ||
| month: 30 | ||
| }; | ||
| function isMeasured(method) { | ||
| return method === "measured_exact" || method === "measured_delta"; | ||
| } | ||
| function queryBySourceAndMethod(db, sinceIso) { | ||
| return db.prepare( | ||
| `SELECT source, estimation_method, | ||
| COUNT(*) as count, | ||
| COALESCE(SUM(tokens_estimated), 0) as tokens | ||
| FROM tool_calls | ||
| WHERE created_at >= ? | ||
| GROUP BY source, estimation_method | ||
| ORDER BY tokens DESC` | ||
| ).all(sinceIso); | ||
| } | ||
| var REFERENCE_DATA = [ | ||
| { | ||
| feature: "Model switching (opusplan / default-to-sonnet)", | ||
| saving: "60-80% reduccion de coste", | ||
| source: "mindstudio.ai, verdent.ai, claudelab.net", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Progressive disclosure skills", | ||
| saving: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| source: "claudefast.com", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Prompt caching read hit", | ||
| saving: "10x mas barato que uncached", | ||
| source: "Anthropic docs", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Claude Code Tool Search", | ||
| saving: "~85% schema reduction (77k \u2192 8.7k)", | ||
| source: "observado en sesion", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "MCP pruning sobre Tool Search", | ||
| saving: "~5-12% adicional por turno", | ||
| source: "estimacion interna", | ||
| verified_at: "2026-04-11" | ||
| } | ||
| ]; | ||
| function resolvePeriod(args, fallback) { | ||
| const flag = args.find((a) => a.startsWith("--period=")); | ||
| if (flag) { | ||
| const value = flag.split("=")[1]; | ||
| if (value && value in PERIOD_DAYS) return value; | ||
| } | ||
| return fallback; | ||
| } | ||
| function runReport(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const period = opts.period ?? resolvePeriod(args, "day"); | ||
| const days = PERIOD_DAYS[period]; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const lines = []; | ||
| lines.push(`token-optimizer-mcp reporte \u2014 periodo: ${period} (${days} dia(s))`); | ||
| lines.push(""); | ||
| if (!fs.existsSync(dbPath)) { | ||
| lines.push("No hay datos registrados todavia."); | ||
| } else { | ||
| const db = getDb(dbPath); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const rows = queryBySourceAndMethod(db, since); | ||
| let medidoTotal = 0; | ||
| let estimadoTotal = 0; | ||
| lines.push("Por fuente y metodo de estimacion:"); | ||
| if (rows.length === 0) { | ||
| lines.push(" (sin eventos en este periodo)"); | ||
| } else { | ||
| for (const row of rows) { | ||
| const method = row.estimation_method ?? "unknown"; | ||
| lines.push( | ||
| ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens` | ||
| ); | ||
| if (isMeasured(method)) medidoTotal += row.tokens; | ||
| else estimadoTotal += row.tokens; | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push(`Resumen: Medido: ${medidoTotal} tokens \xB7 Estimado: ${estimadoTotal} tokens`); | ||
| lines.push(""); | ||
| } | ||
| lines.push("Coach activity:"); | ||
| lines.push(" (sin tips surfaceados todavia \u2014 coach layer se activa en Phase 4.H)"); | ||
| lines.push(""); | ||
| printReference(lines); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| function printReference(lines) { | ||
| lines.push("Referencia (datos publicos verificables):"); | ||
| for (const row of REFERENCE_DATA) { | ||
| lines.push(` \u2022 ${row.feature}`); | ||
| lines.push(` ahorro: ${row.saving}`); | ||
| lines.push(` fuente: ${row.source} \xB7 verificado: ${row.verified_at}`); | ||
| } | ||
| } | ||
| export { | ||
| runReport | ||
| }; | ||
| //# sourceMappingURL=report-J4R7FQCT.js.map |
| {"version":3,"sources":["../src/cli/report.ts"],"sourcesContent":["// Report CLI — Phase 4.14\n// Per-source breakdown WITH estimation_method label + Medido/Estimado split +\n// reference-data table (coach-layer addendum CO-4). Spanish.\n\nimport fs from 'node:fs'\nimport type Database from 'better-sqlite3'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\n\ntype DB = Database.Database\n\ntype Period = 'session' | 'day' | 'week' | 'month'\n\nconst PERIOD_DAYS: Record<Period, number> = {\n session: 3650,\n day: 1,\n week: 7,\n month: 30,\n}\n\ninterface SourceMethodRow {\n source: string\n estimation_method: string | null\n count: number\n tokens: number\n}\n\nfunction isMeasured(method: string | null): boolean {\n return method === 'measured_exact' || method === 'measured_delta'\n}\n\nfunction queryBySourceAndMethod(db: DB, sinceIso: string): SourceMethodRow[] {\n return db\n .prepare(\n `SELECT source, estimation_method,\n COUNT(*) as count,\n COALESCE(SUM(tokens_estimated), 0) as tokens\n FROM tool_calls\n WHERE created_at >= ?\n GROUP BY source, estimation_method\n ORDER BY tokens DESC`,\n )\n .all(sinceIso) as SourceMethodRow[]\n}\n\nexport interface ReportOptions {\n cwd?: string\n period?: Period\n print?: (msg: string) => void\n}\n\nconst REFERENCE_DATA: Array<{\n feature: string\n saving: string\n source: string\n verified_at: string\n}> = [\n {\n feature: 'Model switching (opusplan / default-to-sonnet)',\n saving: '60-80% reduccion de coste',\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Progressive disclosure skills',\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n source: 'claudefast.com',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Prompt caching read hit',\n saving: '10x mas barato que uncached',\n source: 'Anthropic docs',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Claude Code Tool Search',\n saving: '~85% schema reduction (77k → 8.7k)',\n source: 'observado en sesion',\n verified_at: '2026-04-11',\n },\n {\n feature: 'MCP pruning sobre Tool Search',\n saving: '~5-12% adicional por turno',\n source: 'estimacion interna',\n verified_at: '2026-04-11',\n },\n]\n\nfunction resolvePeriod(args: string[], fallback: Period): Period {\n const flag = args.find((a) => a.startsWith('--period='))\n if (flag) {\n const value = flag.split('=')[1] as Period | undefined\n if (value && value in PERIOD_DAYS) return value\n }\n return fallback\n}\n\nexport function runReport(args: string[] = [], opts: ReportOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const period: Period = opts.period ?? resolvePeriod(args, 'day')\n const days = PERIOD_DAYS[period]\n\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n const lines: string[] = []\n lines.push(`token-optimizer-mcp reporte — periodo: ${period} (${days} dia(s))`)\n lines.push('')\n\n if (!fs.existsSync(dbPath)) {\n lines.push('No hay datos registrados todavia.')\n } else {\n const db = getDb(dbPath)\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\n const rows = queryBySourceAndMethod(db, since)\n\n let medidoTotal = 0\n let estimadoTotal = 0\n\n lines.push('Por fuente y metodo de estimacion:')\n if (rows.length === 0) {\n lines.push(' (sin eventos en este periodo)')\n } else {\n for (const row of rows) {\n const method = row.estimation_method ?? 'unknown'\n lines.push(\n ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens`,\n )\n if (isMeasured(method)) medidoTotal += row.tokens\n else estimadoTotal += row.tokens\n }\n }\n lines.push('')\n lines.push(`Resumen: Medido: ${medidoTotal} tokens · Estimado: ${estimadoTotal} tokens`)\n lines.push('')\n }\n\n // Coach activity section (always present; filled in Phase 4.H with real data)\n lines.push('Coach activity:')\n lines.push(' (sin tips surfaceados todavia — coach layer se activa en Phase 4.H)')\n lines.push('')\n\n printReference(lines)\n print(lines.join('\\n'))\n return 0\n}\n\nfunction printReference(lines: string[]): void {\n lines.push('Referencia (datos publicos verificables):')\n for (const row of REFERENCE_DATA) {\n lines.push(` • ${row.feature}`)\n lines.push(` ahorro: ${row.saving}`)\n lines.push(` fuente: ${row.source} · verificado: ${row.verified_at}`)\n }\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| getCoachSurfaceLog, | ||
| measureContextSize, | ||
| postSummaryToXray, | ||
| runRules | ||
| } from "./chunk-PMVZIR3X.js"; | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| rollback | ||
| } from "./chunk-YKQGA3IS.js"; | ||
| import { | ||
| ensureGitignore | ||
| } from "./chunk-VHU3U64E.js"; | ||
| import "./chunk-DBLVAFU5.js"; | ||
| import { | ||
| buildSuggestions | ||
| } from "./chunk-XTFQTQMU.js"; | ||
| import { | ||
| checkSerenaHealth, | ||
| probeMcpPruning, | ||
| probePromptCaching, | ||
| probeRtk, | ||
| probeSerena | ||
| } from "./chunk-DOYJNIB2.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-AWG3ZQRZ.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.6.1" : "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:" : (ensureGitignore(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-WUE7RS3B.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 { ensureGitignore } 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 : (ensureGitignore(resolvedProject), resolveAnalyticsDbPath(resolvedProject)))\n\n // Initialize DB (schema created by getDb)\n const db = getDb(dbPath)\n\n const server = new McpServer(\n {\n name: 'token-optimizer-mcp',\n version: VERSION,\n },\n {\n instructions: INSTRUCTIONS,\n },\n )\n\n // Phase 2 tools\n registerBudgetTools(server, db)\n // Phase 3 tools\n registerSessionTools(server, db)\n // Phase 4 tools\n registerOrchestrationTools(server, db)\n registerCoachTools(server, db)\n registerCoachTipsResource(server, db)\n // Phase 5 tools\n registerToonTools(server)\n\n return server\n}\n","// Budget MCP tools — Phase 2.4\n// budget_set, budget_check, budget_report\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { BudgetManager } from '../services/budget-manager.js'\nimport { text, error } from '../lib/response.js'\n\ntype DB = Database.Database\n\nconst DAY_MS = 86_400_000\n\nfunction sinceForPeriod(period: 'session' | 'day' | 'week' | 'month'): string {\n const now = Date.now()\n switch (period) {\n case 'day':\n return new Date(now - DAY_MS).toISOString()\n case 'week':\n return new Date(now - 7 * DAY_MS).toISOString()\n case 'month':\n return new Date(now - 30 * DAY_MS).toISOString()\n case 'session':\n default:\n return '1970-01-01T00:00:00.000Z'\n }\n}\n\nexport function registerBudgetTools(server: McpServer, db: DB): void {\n const manager = new BudgetManager(db)\n\n // ── budget_set ──\n server.tool(\n 'budget_set',\n 'Define o actualiza un presupuesto de tokens. Precedencia: session > project. Modo warn avisa al exceder.',\n {\n scope: z.enum(['session', 'project']).describe('Ambito del presupuesto'),\n scope_key: z.string().min(1).describe('Clave del scope (sessionId o projectHash)'),\n limit_tokens: z\n .number()\n .int()\n .positive()\n .max(10_000_000)\n .describe('Limite en tokens (1..10_000_000)'),\n },\n async ({ scope, scope_key, limit_tokens }) => {\n try {\n const budget = manager.setBudget({ scope, scope_key, limit_tokens })\n return text(\n [\n 'Presupuesto guardado:',\n '',\n ` scope: ${budget.scope}`,\n ` scope_key: ${budget.scope_key}`,\n ` limit_tokens: ${budget.limit_tokens}`,\n ` mode: ${budget.mode}`,\n ].join('\\n'),\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── budget_check ──\n server.tool(\n 'budget_check',\n 'Consulta el estado del presupuesto activo (gasto actual, restante y porcentaje).',\n {\n session_id: z.string().optional().describe('ID de sesion (default: \"default\")'),\n project_hash: z\n .string()\n .optional()\n .describe('Hash del proyecto para fallback a scope project'),\n },\n async ({ session_id, project_hash }) => {\n try {\n const status = manager.checkBudget(session_id ?? 'default', project_hash ?? null)\n if (!status.active) {\n return text('Sin presupuesto activo para la sesion/proyecto actual.')\n }\n const percent = (status.percent_used * 100).toFixed(1)\n return text(\n [\n 'Estado del presupuesto:',\n '',\n ` gastado: ${status.spent} tokens`,\n ` restante: ${status.remaining} tokens`,\n ` uso: ${percent}%`,\n ` modo: ${status.mode ?? 'n/a'}`,\n ].join('\\n'),\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── budget_report ──\n server.tool(\n 'budget_report',\n 'Muestra el consumo de tokens agrupado por herramienta y por fuente durante un periodo.',\n {\n period: z\n .enum(['session', 'day', 'week', 'month'])\n .optional()\n .describe('Periodo del reporte (default: day)'),\n },\n async ({ period }) => {\n try {\n const since = sinceForPeriod(period ?? 'day')\n const report = manager.getBudgetReport(since)\n const lines = [`Reporte de consumo (desde ${report.period_since}):`, '']\n lines.push('Por herramienta:')\n if (report.by_tool.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of report.by_tool) {\n lines.push(` ${row.tool_name}: ${row.count} llamadas, ${row.tokens} tokens`)\n }\n }\n lines.push('')\n lines.push('Por fuente:')\n if (report.by_source.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of report.by_source) {\n lines.push(` ${row.source}: ${row.count} llamadas, ${row.tokens} tokens`)\n }\n }\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Shared MCP tool response helpers — Phase 2\n\nexport const text = (t: string) => ({\n content: [{ type: 'text' as const, text: t }],\n})\n\nexport const error = (t: string) => ({\n content: [{ type: 'text' as const, text: `Error: ${t}` }],\n isError: true as const,\n})\n","// Session tools — Phase 3.3 (simplified: session_search removed, FTS5 no longer available)\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\n\ntype DB = Database.Database\n\n// No session tools registered after FTS5 removal.\n// Keeping the function signature for backwards compatibility with server.ts imports.\nexport function registerSessionTools(_server: McpServer, _db: DB): void {\n // noop\n}\n","// Orchestration MCP tools — Phase 4.23-4.28\n// mcp_usage_stats, mcp_cost_report, optimization_status,\n// mcp_prune_suggest, mcp_prune_apply, mcp_prune_rollback, mcp_prune_clear\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { text, error } from '../lib/response.js'\nimport { getUsageStats, getCostReport } from '../services/stats.js'\nimport {\n probeSerena,\n probeRtk,\n probeMcpPruning,\n probePromptCaching,\n checkSerenaHealth,\n} from '../orchestration/detector.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\nimport { buildSuggestions } from '../orchestration/advisor.js'\nimport {\n generateFromHistory,\n applyAllowlist,\n rollback,\n clearAllowlist,\n} from '../cli/prune-mcp.js'\nimport type { OptimizationStatus } from '../lib/types.js'\nimport { buildSessionSummary } from '../services/session-summary-builder.js'\nimport { postSummaryToXray } from '../services/xray-client.js'\n\ntype DB = Database.Database\n\nexport function registerOrchestrationTools(server: McpServer, db: DB): void {\n // ── mcp_usage_stats ──\n server.tool(\n 'mcp_usage_stats',\n 'Estadisticas de uso de tokens por herramienta y fuente en un periodo.',\n {\n days: z.number().int().positive().max(365).optional().describe('Dias a analizar (default: 7)'),\n },\n async ({ days }) => {\n try {\n const stats = getUsageStats(db, days ?? 7)\n const lines = [\n `Uso en los ultimos ${stats.period_days} dia(s):`,\n '',\n `Total: ${stats.total_tokens} tokens, ${stats.total_events} eventos`,\n '',\n 'Por fuente:',\n ]\n if (stats.by_source.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of stats.by_source) {\n lines.push(` ${row.source}: ${row.tokens} tokens, ${row.count} llamadas`)\n }\n }\n lines.push('')\n lines.push('Top herramientas:')\n if (stats.by_tool.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of stats.by_tool.slice(0, 10)) {\n lines.push(` ${row.tool_name}: ${row.tokens} tokens, ${row.count} llamadas`)\n }\n }\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_cost_report ──\n server.tool(\n 'mcp_cost_report',\n 'Reporte de coste estimado con rango Haiku-Sonnet-Opus y disclaimer honesto.',\n {\n days: z\n .number()\n .int()\n .positive()\n .max(365)\n .optional()\n .describe('Dias a analizar (default: 7)'),\n },\n async ({ days }) => {\n try {\n const cost = getCostReport(db, days ?? 7)\n const lines = [\n `Reporte de coste (${cost.period_days} dia(s)):`,\n '',\n `Tokens totales: ${cost.total_tokens}`,\n `Coste estimado (input pricing):`,\n ` Haiku 4.5: $${cost.estimated_cost_usd_haiku.toFixed(4)} ($1/MTok)`,\n ` Sonnet 4.6: $${cost.estimated_cost_usd_sonnet.toFixed(4)} ($3/MTok)`,\n ` Opus 4.6: $${cost.estimated_cost_usd_opus.toFixed(4)} ($5/MTok)`,\n '',\n `Nota: ${cost.disclaimer}`,\n ]\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── optimization_status ──\n server.tool(\n 'optimization_status',\n 'Estado de las optimizaciones detectadas: serena, RTK, MCP pruning, prompt caching, schema size.',\n {},\n async () => {\n try {\n const serena = probeSerena()\n const rtk = probeRtk()\n const pruning = probeMcpPruning()\n const pcProbe = probePromptCaching()\n void pcProbe\n const schema = measureCurrentSchemaBytes()\n // Always include prompt_caching with explicit estimation_method per measurement-honesty spec\n const status: OptimizationStatus = {\n serena,\n rtk,\n mcp_pruning: pruning,\n prompt_caching: {\n active_by_default: true,\n savings_tokens: null,\n estimation_method: 'unknown',\n note: 'Revisa tu factura Anthropic para confirmar el ahorro real',\n },\n schema_bytes: {\n tool_schema_bytes: schema.tool_schema_bytes,\n measurement_method: schema.measurement_method,\n },\n }\n const serenaHealth = serena.present ? checkSerenaHealth() : []\n const suggestions = buildSuggestions(status)\n\n // Fire-and-forget summary to xray (if XRAY_URL is set)\n try {\n const lastSession = db\n .prepare('SELECT id FROM sessions ORDER BY started_at DESC LIMIT 1')\n .get() as { id: string } | undefined\n if (lastSession) {\n const summary = buildSessionSummary(db, lastSession.id, '0.2.6')\n void postSummaryToXray(summary as unknown as Record<string, unknown>).catch(() => {})\n }\n } catch {\n // Silent — xray is optional\n }\n\n return text(JSON.stringify({ status, serena_health: serenaHealth, suggestions }, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_suggest ──\n server.tool(\n 'mcp_prune_suggest',\n 'Genera un allowlist de MCPs basandose en el historial (NO modifica archivos).',\n {\n days: z\n .number()\n .int()\n .positive()\n .max(365)\n .optional()\n .describe('Dias de historial a analizar (default: 14)'),\n },\n async ({ days }) => {\n try {\n const proposal = generateFromHistory({ days: days ?? 14 })\n return text(JSON.stringify(proposal, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_apply ──\n server.tool(\n 'mcp_prune_apply',\n 'Restringe los MCPs activos escribiendo enabledMcpjsonServers en .claude/settings.local.json. Requiere confirm:true. Acepta dos formas equivalentes: allowlist (lista blanca, los que SI quieres) o exclude (lista negra, los que NO quieres). Se debe pasar exactamente una de las dos.',\n {\n allowlist: z\n .array(z.string())\n .optional()\n .describe('Nombres de MCPs a permitir (lista blanca). Exclusivo con exclude.'),\n exclude: z\n .array(z.string())\n .optional()\n .describe(\n 'Nombres de MCPs a desactivar (lista negra). Internamente se traduce a allowlist = registrados - exclude. Exclusivo con allowlist.',\n ),\n confirm: z.boolean().describe('Debe ser true para confirmar la escritura'),\n },\n async ({ allowlist, exclude, confirm }) => {\n try {\n if (confirm !== true) {\n return error(\n 'Operacion destructiva: requiere confirm:true. Revisa el allowlist antes de aplicar.',\n )\n }\n const hasAllow = Array.isArray(allowlist)\n const hasExclude = Array.isArray(exclude)\n if (hasAllow === hasExclude) {\n return error(\n 'Debes pasar exactamente uno: allowlist (los que SI quieres) o exclude (los que NO quieres).',\n )\n }\n\n const schema = measureCurrentSchemaBytes()\n const registered = new Set(schema.mcp_servers)\n\n let effective: string[]\n let translationNote = ''\n\n if (hasAllow) {\n effective = allowlist as string[]\n if (registered.size > 0) {\n const invalid = effective.filter((s) => !registered.has(s))\n if (invalid.length > 0) {\n return error(\n `Allowlist contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\n )\n }\n }\n } else {\n const excludeSet = new Set(exclude as string[])\n if (registered.size > 0) {\n const invalid = (exclude as string[]).filter((s) => !registered.has(s))\n if (invalid.length > 0) {\n return error(\n `Exclude contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\n )\n }\n }\n effective = [...registered].filter((s) => !excludeSet.has(s))\n translationNote = `\\n exclude: [${(exclude as string[]).join(', ')}]\\n → allowlist efectivo: [${effective.join(', ')}]`\n }\n\n const applied = applyAllowlist(effective, { source: 'mcp' })\n return text(\n `Allowlist aplicado.${translationNote}\\n settings: ${applied.settings_path}\\n backup: ${applied.backup_path}`,\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_rollback ──\n server.tool(\n 'mcp_prune_rollback',\n 'Restaura el backup mas reciente de settings.local.json. Requiere confirm:true.',\n {\n confirm: z.boolean(),\n to: z.string().optional().describe('Timestamp opcional del backup a restaurar'),\n },\n async ({ confirm, to }) => {\n try {\n if (confirm !== true) {\n return error('Operacion destructiva: requiere confirm:true.')\n }\n const result = rollback(to !== undefined ? { to } : {})\n if (!result.restored) return error('No hay backups disponibles.')\n return text(`Restaurado desde ${result.from}`)\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_clear ──\n server.tool(\n 'mcp_prune_clear',\n 'Elimina el allowlist de settings.local.json (crea backup). Requiere confirm:true.',\n {\n confirm: z.boolean(),\n },\n async ({ confirm }) => {\n try {\n if (confirm !== true) {\n return error('Operacion destructiva: requiere confirm:true.')\n }\n const result = clearAllowlist()\n return text(\n result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar',\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Session summary builder for xray integration.\n// Aggregates all local data sources into a single payload for xray.\n// Only called once per session (not in PostToolUse hot path).\n\nimport type Database from 'better-sqlite3'\nimport { getUsageStats, getCostReport } from './stats.js'\nimport {\n probeSerena,\n probeRtk,\n probeMcpPruning,\n probePromptCaching,\n} from '../orchestration/detector.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\nimport { getCoachSurfaceLog } from '../coach/surface.js'\nimport { resolveProjectDir } from '../lib/paths.js'\n\ntype DB = Database.Database\n\nexport interface XraySummaryPayload {\n session_id: string\n project_path: string\n project_name: string\n total_tokens: number\n total_events: number\n by_source: Array<{ source: string; count: number; tokens: number }>\n by_tool: Array<{ tool_name: string; count: number; tokens: number }>\n cost_haiku: number\n cost_sonnet: number\n cost_opus: number\n probes: {\n serena: { present: boolean; confidence: number; signals: string[] }\n rtk: { present: boolean; confidence: number; signals: string[] }\n mcp_pruning: { present: boolean; confidence: number; signals: string[] }\n prompt_caching: { present: boolean; confidence: number }\n }\n coach_tips_surfaced: Array<{ rule_id: string; tip_ids: string[]; severity: string }>\n schema_measurement: { tool_schema_tokens: number; mcp_servers: string[] }\n optimizer_version: string\n}\n\nexport function buildSessionSummary(\n db: DB,\n sessionId: string,\n version: string,\n): XraySummaryPayload {\n // Usage stats for last 24h (covers the session)\n const usage = getUsageStats(db, 1)\n const cost = getCostReport(db, 1)\n\n // Detection probes (reads local files, no network)\n const serena = probeSerena()\n const rtk = probeRtk()\n const mcpPruning = probeMcpPruning()\n const promptCaching = probePromptCaching()\n\n // Schema measurement (reads settings files, no network)\n const schema = measureCurrentSchemaBytes()\n\n // Coach tips surfaced during this session\n const coachTips = getCoachSurfaceLog(db, sessionId)\n\n const projDir = resolveProjectDir()\n const projName = projDir.split(/[\\\\/]/).filter(Boolean).pop() ?? 'unknown'\n\n return {\n session_id: sessionId,\n project_path: projDir,\n project_name: projName,\n total_tokens: usage.total_tokens,\n total_events: usage.total_events,\n by_source: usage.by_source,\n by_tool: usage.by_tool.map((t) => ({\n tool_name: t.tool_name,\n count: t.count,\n tokens: t.tokens,\n })),\n cost_haiku: cost.estimated_cost_usd_haiku,\n cost_sonnet: cost.estimated_cost_usd_sonnet,\n cost_opus: cost.estimated_cost_usd_opus,\n probes: {\n serena: { present: serena.present, confidence: serena.confidence, signals: serena.signals },\n rtk: { present: rtk.present, confidence: rtk.confidence, signals: rtk.signals },\n mcp_pruning: {\n present: mcpPruning.present,\n confidence: mcpPruning.confidence,\n signals: mcpPruning.signals,\n },\n prompt_caching: { present: promptCaching.present, confidence: promptCaching.confidence },\n },\n coach_tips_surfaced: coachTips,\n schema_measurement: {\n tool_schema_tokens: schema.tool_schema_tokens,\n mcp_servers: schema.mcp_servers,\n },\n optimizer_version: version,\n }\n}\n","// Coach MCP tool — Phase 4.46\n// coach_tips: returns active hits + full knowledge base + context measurement + reference table\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { text, error } from '../lib/response.js'\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\n\ntype DB = Database.Database\n\nexport function registerCoachTools(server: McpServer, db: DB): void {\n server.tool(\n 'coach_tips',\n 'Devuelve tips activos (rules disparadas) y medicion de contexto. Por defecto modo compacto (~500 tokens). Usa verbose=true para incluir el catalogo completo de 18 tips y la tabla de referencia (~3.5k tokens).',\n {\n session_id: z.string().optional().describe('ID de la sesion (default: \"default\")'),\n project_dir: z\n .string()\n .optional()\n .describe('Directorio del proyecto para medir contexto desde transcript JSONL'),\n active_model: z\n .string()\n .optional()\n .describe('Modelo activo (opcional, habilita regla detect-opus-for-simple-task)'),\n verbose: z\n .boolean()\n .optional()\n .describe(\n 'Si true, incluye el knowledge base completo (18 tips) y la reference data. Default false = solo hits activos + contexto. Ahorra ~3.3k tokens por llamada en modo compacto.',\n ),\n },\n async ({ session_id, project_dir, active_model, verbose }) => {\n try {\n const payloadOpts: Parameters<typeof computeCoachTipsPayload>[0] = { db }\n if (session_id !== undefined) payloadOpts.sessionId = session_id\n if (project_dir !== undefined) payloadOpts.projectDir = project_dir\n if (active_model !== undefined) payloadOpts.activeModel = active_model\n const response = await computeCoachTipsPayload(payloadOpts)\n\n // Default compact mode: strip the heavy knowledge_base + reference_data\n // to avoid burning ~3.3k tokens per call. verbose=true restores them.\n if (verbose !== true) {\n const { known_tricks: _kb, reference_data: _ref, ...compact } = response\n return text(JSON.stringify(compact, null, 2))\n }\n return text(JSON.stringify(response, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Reference data table with publicly-verifiable savings numbers — Phase 4.41\n// Every row tagged estimation_method: 'reference_measured'\n\nimport type { EstimationMethod } from '../lib/types.js'\n\nexport interface ReferenceDataRow {\n feature: string\n saving: string\n source: string\n verified_at: string\n estimation_method: EstimationMethod\n}\n\nexport const REFERENCE_DATA: readonly ReferenceDataRow[] = [\n {\n feature: 'Model switching (opusplan / default-to-sonnet)',\n saving: '60-80% reduccion de coste',\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Progressive disclosure skills',\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n source: 'claudefast.com',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Prompt caching read hit',\n saving: '10x mas barato que uncached',\n source: 'Anthropic docs',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Claude Code Tool Search',\n saving: '~85% schema reduction (77k → 8.7k tokens)',\n source: 'observado en sesion',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'MCP pruning sobre Tool Search',\n saving: '~5-12% adicional por turno',\n source: 'estimacion interna',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n]\n\nconst DAY_MS = 86_400_000\n\nexport function getFreshRows(\n daysThreshold = 90,\n today: Date = new Date(),\n): ReferenceDataRow[] {\n const cutoff = today.getTime() - daysThreshold * DAY_MS\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() >= cutoff)\n}\n\nexport function getStaleRows(\n daysThreshold = 90,\n today: Date = new Date(),\n): ReferenceDataRow[] {\n const cutoff = today.getTime() - daysThreshold * DAY_MS\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() < cutoff)\n}\n","// Shared payload builder for coach_tips MCP tool + token-optimizer://coach/tips\n// resource. Keeps tool/resource outputs identical — Phase 4.H.\n\nimport type Database from 'better-sqlite3'\nimport type { ContextMeasurement, EventContext, ToolEvent, DetectionHit, CoachTip } from '../lib/types.js'\nimport { KNOWLEDGE_BASE } from './knowledge-base.js'\nimport { REFERENCE_DATA, getStaleRows } from './reference-data.js'\nimport { runRules } from './detector.js'\nimport { measureContextSize } from './context-meter.js'\nimport { buildQueries } from '../db/queries.js'\n\ntype DB = Database.Database\n\nexport interface CoachTipsPayload {\n current: DetectionHit[]\n known_tricks: readonly CoachTip[]\n context: ContextMeasurement\n reference_data: typeof REFERENCE_DATA\n stale_reference_count: number\n last_computed_at: string\n}\n\nexport interface ComputeCoachTipsPayloadOptions {\n db: DB\n sessionId?: string\n projectDir?: string\n activeModel?: string\n}\n\nexport async function computeCoachTipsPayload(\n opts: ComputeCoachTipsPayloadOptions,\n): Promise<CoachTipsPayload> {\n const { db } = opts\n const sessionId = opts.sessionId ?? 'default'\n\n const contextOpts: Parameters<typeof measureContextSize>[1] = { db }\n if (opts.projectDir !== undefined) contextOpts.projectDir = opts.projectDir\n if (opts.activeModel !== undefined) contextOpts.activeModel = opts.activeModel\n const context = await measureContextSize(sessionId, contextOpts)\n\n const queries = buildQueries(db)\n const since = new Date(Date.now() - 86_400_000).toISOString()\n const rawRows = queries.getToolCallsSince(since) as ToolEvent[]\n const events = rawRows.slice(0, 100)\n\n const ctx: EventContext = {\n session_id: sessionId,\n events,\n session_token_total: context.tokens,\n session_token_method: context.estimation_method,\n session_token_limit: context.limit,\n active_model: opts.activeModel ?? null,\n }\n\n const hits = runRules(ctx)\n const staleTips = getStaleRows()\n\n return {\n current: hits,\n known_tricks: KNOWLEDGE_BASE,\n context,\n reference_data: REFERENCE_DATA,\n stale_reference_count: staleTips.length,\n last_computed_at: new Date().toISOString(),\n }\n}\n","// TOON encoding tools — Phase 5.3\n// toon_encode: data -> compact JSON (no whitespace) = token-efficient\n// toon_decode: toon string -> JSON object\n//\n// Note: the original `toon-format` npm package was deferred during Phase 0\n// due to package-name uncertainty. This implementation uses compact JSON under\n// the hood, which is round-trip lossless and ~30-40% cheaper in tokens than\n// pretty-printed JSON. The tool names are preserved so a real TOON impl can\n// drop in later without changing the MCP API.\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { text, error } from '../lib/response.js'\n\nfunction compactEncode(data: unknown): string {\n try {\n return JSON.stringify(data)\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e)\n if (/circular|cyclic/i.test(msg)) {\n throw new Error('Referencia circular detectada: TOON no soporta objetos ciclicos')\n }\n throw new Error(`No se pudo codificar a TOON: ${msg}`)\n }\n}\n\nfunction compactDecode(toon: string): unknown {\n try {\n return JSON.parse(toon)\n } catch (e) {\n throw new Error(`TOON invalido: ${e instanceof Error ? e.message : String(e)}`)\n }\n}\n\nexport function registerToonTools(server: McpServer): void {\n // ── toon_encode ──\n server.tool(\n 'toon_encode',\n 'Codifica un objeto JSON a formato TOON (JSON compacto token-eficiente, round-trip lossless).',\n {\n data: z.unknown().describe('Valor a codificar (objeto, array, primitivo)'),\n },\n async ({ data }) => {\n try {\n const encoded = compactEncode(data)\n return text(encoded)\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── toon_decode ──\n server.tool(\n 'toon_decode',\n 'Decodifica una cadena TOON a JSON. Devuelve el objeto formateado para lectura.',\n {\n toon: z.string().min(1).describe('Cadena TOON a decodificar'),\n },\n async ({ toon }) => {\n try {\n const decoded = compactDecode(toon)\n return text(JSON.stringify(decoded, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n\n// Exported for tests\nexport const _internal = { compactEncode, compactDecode }\n","// token-optimizer://coach/tips resource — Phase 4.H\n// Mirrors coach_tips() tool payload, readable without a tool call.\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\n\ntype DB = Database.Database\n\nexport const COACH_TIPS_URI = 'token-optimizer://coach/tips'\n\nexport function registerCoachTipsResource(server: McpServer, db: DB): void {\n server.resource(\n 'coach-tips',\n COACH_TIPS_URI,\n {\n description:\n 'Tips activos del coach, catalogo completo de trucos, medicion de contexto y tabla de referencia.',\n mimeType: 'application/json',\n },\n async (uri: URL) => {\n try {\n const payload = await computeCoachTipsPayload({ db })\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify(payload, null, 2),\n },\n ],\n }\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e)\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify({ error: message }),\n },\n ],\n }\n }\n },\n )\n}\n"],"mappings":"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","names":["z","z","z","DAY_MS","DAY_MS","z","z","z"]} |
| #!/usr/bin/env node | ||
| import { | ||
| getActiveBudgetSummary, | ||
| getUsageStats | ||
| } from "./chunk-633PY32C.js"; | ||
| import "./chunk-VV5KKIQ4.js"; | ||
| import "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| projectHash, | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-AWG3ZQRZ.js"; | ||
| // src/cli/status.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| function runStatus(_args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = path.join(home, ".claude", "settings.json"); | ||
| const installed = (() => { | ||
| try { | ||
| if (!fs.existsSync(settingsPath)) return false; | ||
| const json = JSON.parse(fs.readFileSync(settingsPath, "utf8")); | ||
| const mcp = json.mcpServers ?? {}; | ||
| return "token-optimizer" in mcp; | ||
| } catch { | ||
| return false; | ||
| } | ||
| })(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| let eventsToday = 0; | ||
| let tokensBySource = []; | ||
| let budgetLine = "sin presupuesto activo"; | ||
| if (fs.existsSync(dbPath)) { | ||
| try { | ||
| const db = getDb(dbPath); | ||
| const usage = getUsageStats(db, 1); | ||
| eventsToday = usage.total_events; | ||
| tokensBySource = usage.by_source.map((r) => ({ source: r.source, tokens: r.tokens })); | ||
| const budget = getActiveBudgetSummary(db, "default", projectHash(projectDir)); | ||
| if (budget.active) { | ||
| const pct = (budget.percent_used * 100).toFixed(1); | ||
| budgetLine = `gastado=${budget.spent} restante=${budget.remaining} uso=${pct}% modo=${budget.mode}`; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| const lines = []; | ||
| lines.push("token-optimizer-mcp status"); | ||
| lines.push(""); | ||
| lines.push(`Instalado: ${installed ? "\u2713" : "\u2717"} (${settingsPath})`); | ||
| lines.push(`Storage DB: ${dbPath}${fs.existsSync(dbPath) ? "" : " (no existe aun)"}`); | ||
| lines.push(`Eventos hoy: ${eventsToday}`); | ||
| lines.push( | ||
| `Tokens por fuente: ${tokensBySource.length > 0 ? tokensBySource.map((s) => `${s.source}=${s.tokens}`).join(", ") : "(sin datos)"}` | ||
| ); | ||
| lines.push(`Presupuesto: ${budgetLine}`); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| export { | ||
| runStatus | ||
| }; | ||
| //# sourceMappingURL=status-Q5QIU23E.js.map |
| {"version":3,"sources":["../src/cli/status.ts"],"sourcesContent":["// Status CLI — Phase 4.13\n// Prints install detection, storage DB, events today, tokens by source, active budget.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport os from 'node:os'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath, projectHash } from '../lib/paths.js'\nimport { getUsageStats, getActiveBudgetSummary } from '../services/stats.js'\n\nexport interface StatusOptions {\n home?: string\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runStatus(_args: string[] = [], opts: StatusOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const home = opts.home ?? os.homedir()\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = path.join(home, '.claude', 'settings.json')\n\n const installed = (() => {\n try {\n if (!fs.existsSync(settingsPath)) return false\n const json = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as Record<string, unknown>\n const mcp = (json.mcpServers ?? {}) as Record<string, unknown>\n return 'token-optimizer' in mcp\n } catch {\n return false\n }\n })()\n\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n let eventsToday = 0\n let tokensBySource: Array<{ source: string; tokens: number }> = []\n let budgetLine = 'sin presupuesto activo'\n\n if (fs.existsSync(dbPath)) {\n try {\n const db = getDb(dbPath)\n const usage = getUsageStats(db, 1)\n eventsToday = usage.total_events\n tokensBySource = usage.by_source.map((r) => ({ source: r.source, tokens: r.tokens }))\n const budget = getActiveBudgetSummary(db, 'default', projectHash(projectDir))\n if (budget.active) {\n const pct = (budget.percent_used * 100).toFixed(1)\n budgetLine = `gastado=${budget.spent} restante=${budget.remaining} uso=${pct}% modo=${budget.mode}`\n }\n } catch {\n // swallow\n }\n }\n\n const lines: string[] = []\n lines.push('token-optimizer-mcp status')\n lines.push('')\n lines.push(`Instalado: ${installed ? '✓' : '✗'} (${settingsPath})`)\n lines.push(`Storage DB: ${dbPath}${fs.existsSync(dbPath) ? '' : ' (no existe aun)'}`)\n lines.push(`Eventos hoy: ${eventsToday}`)\n lines.push(\n `Tokens por fuente: ${\n tokensBySource.length > 0\n ? tokensBySource.map((s) => `${s.source}=${s.tokens}`).join(', ')\n : '(sin datos)'\n }`,\n )\n lines.push(`Presupuesto: ${budgetLine}`)\n\n print(lines.join('\\n'))\n return 0\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAGA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAWR,SAAS,UAAU,QAAkB,CAAC,GAAG,OAAsB,CAAC,GAAW;AAChF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ;AACrC,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,eAAe,KAAK,KAAK,MAAM,WAAW,eAAe;AAE/D,QAAM,aAAa,MAAM;AACvB,QAAI;AACF,UAAI,CAAC,GAAG,WAAW,YAAY,EAAG,QAAO;AACzC,YAAM,OAAO,KAAK,MAAM,GAAG,aAAa,cAAc,MAAM,CAAC;AAC7D,YAAM,MAAO,KAAK,cAAc,CAAC;AACjC,aAAO,qBAAqB;AAAA,IAC9B,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF,GAAG;AAEH,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,MAAI,cAAc;AAClB,MAAI,iBAA4D,CAAC;AACjE,MAAI,aAAa;AAEjB,MAAI,GAAG,WAAW,MAAM,GAAG;AACzB,QAAI;AACF,YAAM,KAAK,MAAM,MAAM;AACvB,YAAM,QAAQ,cAAc,IAAI,CAAC;AACjC,oBAAc,MAAM;AACpB,uBAAiB,MAAM,UAAU,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,QAAQ,EAAE,OAAO,EAAE;AACpF,YAAM,SAAS,uBAAuB,IAAI,WAAW,YAAY,UAAU,CAAC;AAC5E,UAAI,OAAO,QAAQ;AACjB,cAAM,OAAO,OAAO,eAAe,KAAK,QAAQ,CAAC;AACjD,qBAAa,WAAW,OAAO,KAAK,aAAa,OAAO,SAAS,QAAQ,GAAG,UAAU,OAAO,IAAI;AAAA,MACnG;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,4BAA4B;AACvC,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,uBAAuB,YAAY,WAAM,QAAG,KAAK,YAAY,GAAG;AAC3E,QAAM,KAAK,uBAAuB,MAAM,GAAG,GAAG,WAAW,MAAM,IAAI,KAAK,kBAAkB,EAAE;AAC5F,QAAM,KAAK,uBAAuB,WAAW,EAAE;AAC/C,QAAM;AAAA,IACJ,uBACE,eAAe,SAAS,IACpB,eAAe,IAAI,CAAC,MAAM,GAAG,EAAE,MAAM,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,IAAI,IAC9D,aACN;AAAA,EACF;AACA,QAAM,KAAK,uBAAuB,UAAU,EAAE;AAE9C,QAAM,MAAM,KAAK,IAAI,CAAC;AACtB,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveXrayUrl | ||
| } from "./chunk-DBLVAFU5.js"; | ||
| import "./chunk-AWG3ZQRZ.js"; | ||
| // src/cli/sync-xray.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| async function findAnalyticsDbs(rootDir) { | ||
| const results = []; | ||
| const rootDb = path.join(rootDir, ".token-optimizer", "analytics.db"); | ||
| if (fs.existsSync(rootDb)) { | ||
| results.push({ dbPath: rootDb, projectDir: rootDir, projectName: path.basename(rootDir) }); | ||
| } | ||
| const projectsDir = path.join(rootDir, "projects"); | ||
| if (fs.existsSync(projectsDir)) { | ||
| for (const entry of fs.readdirSync(projectsDir, { withFileTypes: true })) { | ||
| if (!entry.isDirectory()) continue; | ||
| const dbPath = path.join(projectsDir, entry.name, ".token-optimizer", "analytics.db"); | ||
| if (fs.existsSync(dbPath)) { | ||
| results.push({ | ||
| dbPath, | ||
| projectDir: path.join(projectsDir, entry.name), | ||
| projectName: entry.name | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| async function runSyncXray(args) { | ||
| const print = (m) => console.error(m); | ||
| const xrayUrl = resolveXrayUrl(); | ||
| if (!xrayUrl) { | ||
| print("Error: XRAY_URL no configurado."); | ||
| print("Ejecuta: npx @cocaxcode/token-optimizer-mcp config set xray_url http://localhost:3333"); | ||
| return 1; | ||
| } | ||
| const rootDir = args.find((a) => !a.startsWith("--")) ?? process.cwd(); | ||
| print(`Buscando analytics.db en ${rootDir}...`); | ||
| const dbs = await findAnalyticsDbs(rootDir); | ||
| if (dbs.length === 0) { | ||
| print("No se encontraron bases de datos de token-optimizer."); | ||
| return 1; | ||
| } | ||
| print(`Encontradas ${dbs.length} base(s) de datos:`); | ||
| for (const db of dbs) { | ||
| print(` - ${db.projectName}: ${db.dbPath}`); | ||
| } | ||
| let totalSent = 0; | ||
| let totalSkipped = 0; | ||
| for (const dbInfo of dbs) { | ||
| print(` | ||
| Sincronizando ${dbInfo.projectName}...`); | ||
| const Database = (await import("better-sqlite3")).default; | ||
| const db = new Database(dbInfo.dbPath, { readonly: true }); | ||
| const rows = db.prepare(` | ||
| SELECT session_id, tool_name, source, output_bytes, tokens_estimated, | ||
| tokens_actual, duration_ms, estimation_method, created_at | ||
| FROM tool_calls | ||
| ORDER BY created_at ASC | ||
| `).all(); | ||
| db.close(); | ||
| print(` ${rows.length} eventos en la DB`); | ||
| const BATCH_SIZE = 50; | ||
| for (let i = 0; i < rows.length; i += BATCH_SIZE) { | ||
| const batch = rows.slice(i, i + BATCH_SIZE); | ||
| const promises = batch.map(async (row) => { | ||
| const event = { | ||
| session_id: row.session_id, | ||
| tool_name: row.tool_name, | ||
| source: row.source, | ||
| output_bytes: row.output_bytes, | ||
| tokens_estimated: row.tokens_estimated, | ||
| tokens_actual: row.tokens_actual, | ||
| duration_ms: row.duration_ms, | ||
| estimation_method: row.estimation_method, | ||
| created_at: row.created_at, | ||
| project_path: dbInfo.projectDir, | ||
| project_name: dbInfo.projectName | ||
| }; | ||
| try { | ||
| const res = await fetch(`${xrayUrl}/hooks/token-optimizer`, { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ source: "token-optimizer-mcp", version: "sync", event }), | ||
| signal: AbortSignal.timeout(5e3) | ||
| }); | ||
| if (res.ok) return true; | ||
| return false; | ||
| } catch { | ||
| return false; | ||
| } | ||
| }); | ||
| const results = await Promise.all(promises); | ||
| const sent = results.filter(Boolean).length; | ||
| totalSent += sent; | ||
| totalSkipped += results.length - sent; | ||
| } | ||
| print(` Enviados: ${rows.length} eventos`); | ||
| } | ||
| print(` | ||
| Sincronizacion completa:`); | ||
| print(` Enviados: ${totalSent}`); | ||
| if (totalSkipped > 0) print(` Fallidos: ${totalSkipped}`); | ||
| print(` Dashboard: ${xrayUrl}`); | ||
| return 0; | ||
| } | ||
| export { | ||
| runSyncXray | ||
| }; | ||
| //# sourceMappingURL=sync-xray-MUOJBDOV.js.map |
| {"version":3,"sources":["../src/cli/sync-xray.ts"],"sourcesContent":["// sync-xray CLI — Sends historical analytics data to xray\n// Reads all .token-optimizer/analytics.db files and POSTs events to xray.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { resolveXrayUrl } from './config.js'\n\ninterface ToolCallRow {\n session_id: string\n tool_name: string\n source: string\n output_bytes: number\n tokens_estimated: number\n tokens_actual: number | null\n duration_ms: number | null\n estimation_method: string\n created_at: string\n}\n\nasync function findAnalyticsDbs(rootDir: string): Promise<Array<{ dbPath: string; projectDir: string; projectName: string }>> {\n const results: Array<{ dbPath: string; projectDir: string; projectName: string }> = []\n\n // Check root dir\n const rootDb = path.join(rootDir, '.token-optimizer', 'analytics.db')\n if (fs.existsSync(rootDb)) {\n results.push({ dbPath: rootDb, projectDir: rootDir, projectName: path.basename(rootDir) })\n }\n\n // Check projects/ subdirectories\n const projectsDir = path.join(rootDir, 'projects')\n if (fs.existsSync(projectsDir)) {\n for (const entry of fs.readdirSync(projectsDir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue\n const dbPath = path.join(projectsDir, entry.name, '.token-optimizer', 'analytics.db')\n if (fs.existsSync(dbPath)) {\n results.push({\n dbPath,\n projectDir: path.join(projectsDir, entry.name),\n projectName: entry.name,\n })\n }\n }\n }\n\n return results\n}\n\nexport async function runSyncXray(args: string[]): Promise<number> {\n const print = (m: string) => console.error(m)\n\n const xrayUrl = resolveXrayUrl()\n if (!xrayUrl) {\n print('Error: XRAY_URL no configurado.')\n print('Ejecuta: npx @cocaxcode/token-optimizer-mcp config set xray_url http://localhost:3333')\n return 1\n }\n\n // Determine root dir\n const rootDir = args.find(a => !a.startsWith('--')) ?? process.cwd()\n\n print(`Buscando analytics.db en ${rootDir}...`)\n const dbs = await findAnalyticsDbs(rootDir)\n\n if (dbs.length === 0) {\n print('No se encontraron bases de datos de token-optimizer.')\n return 1\n }\n\n print(`Encontradas ${dbs.length} base(s) de datos:`)\n for (const db of dbs) {\n print(` - ${db.projectName}: ${db.dbPath}`)\n }\n\n let totalSent = 0\n let totalSkipped = 0\n\n for (const dbInfo of dbs) {\n print(`\\nSincronizando ${dbInfo.projectName}...`)\n\n // Dynamic import to avoid loading better-sqlite3 if not needed\n const Database = (await import('better-sqlite3')).default\n const db = new Database(dbInfo.dbPath, { readonly: true })\n\n const rows = db.prepare(`\n SELECT session_id, tool_name, source, output_bytes, tokens_estimated,\n tokens_actual, duration_ms, estimation_method, created_at\n FROM tool_calls\n ORDER BY created_at ASC\n `).all() as ToolCallRow[]\n\n db.close()\n\n print(` ${rows.length} eventos en la DB`)\n\n // Send in batches of 50 to avoid overwhelming xray\n const BATCH_SIZE = 50\n for (let i = 0; i < rows.length; i += BATCH_SIZE) {\n const batch = rows.slice(i, i + BATCH_SIZE)\n const promises = batch.map(async (row) => {\n const event = {\n session_id: row.session_id,\n tool_name: row.tool_name,\n source: row.source,\n output_bytes: row.output_bytes,\n tokens_estimated: row.tokens_estimated,\n tokens_actual: row.tokens_actual,\n duration_ms: row.duration_ms,\n estimation_method: row.estimation_method,\n created_at: row.created_at,\n project_path: dbInfo.projectDir,\n project_name: dbInfo.projectName,\n }\n\n try {\n const res = await fetch(`${xrayUrl}/hooks/token-optimizer`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ source: 'token-optimizer-mcp', version: 'sync', event }),\n signal: AbortSignal.timeout(5000),\n })\n if (res.ok) return true\n return false\n } catch {\n return false\n }\n })\n\n const results = await Promise.all(promises)\n const sent = results.filter(Boolean).length\n totalSent += sent\n totalSkipped += results.length - sent\n }\n\n print(` Enviados: ${rows.length} eventos`)\n }\n\n print(`\\nSincronizacion completa:`)\n print(` Enviados: ${totalSent}`)\n if (totalSkipped > 0) print(` Fallidos: ${totalSkipped}`)\n print(` Dashboard: ${xrayUrl}`)\n\n return 0\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| // src/cli/uninstall.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| function settingsPath(home) { | ||
| return path.join(home, ".claude", "settings.json"); | ||
| } | ||
| function stripTokenOptimizerFromEvent(entries) { | ||
| if (!Array.isArray(entries)) return []; | ||
| return entries.map((entry) => { | ||
| const handlers = Array.isArray(entry.hooks) ? entry.hooks : []; | ||
| const filtered = handlers.filter( | ||
| (h) => !(typeof h.command === "string" && h.command.includes("token-optimizer")) | ||
| ); | ||
| return { ...entry, hooks: filtered }; | ||
| }).filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0); | ||
| } | ||
| function runUninstall(args = [], opts = {}) { | ||
| const home = opts.home ?? os.homedir(); | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const purge = opts.purge ?? args.includes("--purge"); | ||
| const confirm = opts.confirm ?? args.includes("--confirm"); | ||
| const p = settingsPath(home); | ||
| if (fs.existsSync(p)) { | ||
| try { | ||
| const json = JSON.parse(fs.readFileSync(p, "utf8")); | ||
| const mcpServers = json.mcpServers ?? {}; | ||
| delete mcpServers["token-optimizer"]; | ||
| json.mcpServers = mcpServers; | ||
| const hooks = json.hooks ?? {}; | ||
| for (const eventName of ["PreToolUse", "PostToolUse", "SessionStart"]) { | ||
| hooks[eventName] = stripTokenOptimizerFromEvent(hooks[eventName]); | ||
| } | ||
| json.hooks = hooks; | ||
| fs.writeFileSync(p, JSON.stringify(json, null, 2)); | ||
| print("Entradas de token-optimizer eliminadas de settings.json"); | ||
| } catch (e) { | ||
| print(`Error editando settings.json: ${e instanceof Error ? e.message : String(e)}`); | ||
| return 1; | ||
| } | ||
| } else { | ||
| print("settings.json no existe; nada que eliminar."); | ||
| } | ||
| if (purge) { | ||
| if (!confirm) { | ||
| print("--purge requiere tambien --confirm para borrar datos. Nada borrado."); | ||
| return 0; | ||
| } | ||
| const globalDir = path.join(home, ".token-optimizer"); | ||
| if (fs.existsSync(globalDir)) { | ||
| fs.rmSync(globalDir, { recursive: true, force: true }); | ||
| print(`Borrado: ${globalDir}`); | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
| export { | ||
| runUninstall | ||
| }; | ||
| //# sourceMappingURL=uninstall-UK255POR.js.map |
| {"version":3,"sources":["../src/cli/uninstall.ts"],"sourcesContent":["// Uninstall CLI — Phase 4.11\n// Removes token-optimizer entries from settings.json. --purge --confirm also\n// removes the global storage dir.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport os from 'node:os'\n\nexport interface UninstallOptions {\n home?: string\n cwd?: string\n print?: (msg: string) => void\n purge?: boolean\n confirm?: boolean\n}\n\nfunction settingsPath(home: string): string {\n return path.join(home, '.claude', 'settings.json')\n}\n\ninterface HookEntry {\n matcher?: string\n hooks?: Array<{ type?: string; command?: string }>\n [key: string]: unknown\n}\n\nfunction stripTokenOptimizerFromEvent(entries: unknown): HookEntry[] {\n if (!Array.isArray(entries)) return []\n return (entries as HookEntry[])\n .map((entry) => {\n const handlers = Array.isArray(entry.hooks) ? entry.hooks : []\n const filtered = handlers.filter(\n (h) => !(typeof h.command === 'string' && h.command.includes('token-optimizer')),\n )\n return { ...entry, hooks: filtered }\n })\n .filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0)\n}\n\nexport function runUninstall(args: string[] = [], opts: UninstallOptions = {}): number {\n const home = opts.home ?? os.homedir()\n const print = opts.print ?? ((m: string) => console.error(m))\n const purge = opts.purge ?? args.includes('--purge')\n const confirm = opts.confirm ?? args.includes('--confirm')\n\n const p = settingsPath(home)\n if (fs.existsSync(p)) {\n try {\n const json = JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\n\n // mcpServers: delete token-optimizer\n const mcpServers = (json.mcpServers ?? {}) as Record<string, unknown>\n delete mcpServers['token-optimizer']\n json.mcpServers = mcpServers\n\n // hooks: strip token-optimizer handlers from all 3 events\n const hooks = (json.hooks ?? {}) as Record<string, unknown>\n for (const eventName of ['PreToolUse', 'PostToolUse', 'SessionStart']) {\n hooks[eventName] = stripTokenOptimizerFromEvent(hooks[eventName])\n }\n json.hooks = hooks\n\n fs.writeFileSync(p, JSON.stringify(json, null, 2))\n print('Entradas de token-optimizer eliminadas de settings.json')\n } catch (e) {\n print(`Error editando settings.json: ${e instanceof Error ? e.message : String(e)}`)\n return 1\n }\n } else {\n print('settings.json no existe; nada que eliminar.')\n }\n\n if (purge) {\n if (!confirm) {\n print('--purge requiere tambien --confirm para borrar datos. Nada borrado.')\n return 0\n }\n const globalDir = path.join(home, '.token-optimizer')\n if (fs.existsSync(globalDir)) {\n fs.rmSync(globalDir, { recursive: true, force: true })\n print(`Borrado: ${globalDir}`)\n }\n }\n\n return 0\n}\n"],"mappings":";;;AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,QAAQ;AAUf,SAAS,aAAa,MAAsB;AAC1C,SAAO,KAAK,KAAK,MAAM,WAAW,eAAe;AACnD;AAQA,SAAS,6BAA6B,SAA+B;AACnE,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO,CAAC;AACrC,SAAQ,QACL,IAAI,CAAC,UAAU;AACd,UAAM,WAAW,MAAM,QAAQ,MAAM,KAAK,IAAI,MAAM,QAAQ,CAAC;AAC7D,UAAM,WAAW,SAAS;AAAA,MACxB,CAAC,MAAM,EAAE,OAAO,EAAE,YAAY,YAAY,EAAE,QAAQ,SAAS,iBAAiB;AAAA,IAChF;AACA,WAAO,EAAE,GAAG,OAAO,OAAO,SAAS;AAAA,EACrC,CAAC,EACA,OAAO,CAAC,UAAU,MAAM,QAAQ,MAAM,KAAK,KAAK,MAAM,MAAM,SAAS,CAAC;AAC3E;AAEO,SAAS,aAAa,OAAiB,CAAC,GAAG,OAAyB,CAAC,GAAW;AACrF,QAAM,OAAO,KAAK,QAAQ,GAAG,QAAQ;AACrC,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,QAAQ,KAAK,SAAS,KAAK,SAAS,SAAS;AACnD,QAAM,UAAU,KAAK,WAAW,KAAK,SAAS,WAAW;AAEzD,QAAM,IAAI,aAAa,IAAI;AAC3B,MAAI,GAAG,WAAW,CAAC,GAAG;AACpB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,GAAG,aAAa,GAAG,MAAM,CAAC;AAGlD,YAAM,aAAc,KAAK,cAAc,CAAC;AACxC,aAAO,WAAW,iBAAiB;AACnC,WAAK,aAAa;AAGlB,YAAM,QAAS,KAAK,SAAS,CAAC;AAC9B,iBAAW,aAAa,CAAC,cAAc,eAAe,cAAc,GAAG;AACrE,cAAM,SAAS,IAAI,6BAA6B,MAAM,SAAS,CAAC;AAAA,MAClE;AACA,WAAK,QAAQ;AAEb,SAAG,cAAc,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AACjD,YAAM,yDAAyD;AAAA,IACjE,SAAS,GAAG;AACV,YAAM,iCAAiC,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AACnF,aAAO;AAAA,IACT;AAAA,EACF,OAAO;AACL,UAAM,6CAA6C;AAAA,EACrD;AAEA,MAAI,OAAO;AACT,QAAI,CAAC,SAAS;AACZ,YAAM,qEAAqE;AAC3E,aAAO;AAAA,IACT;AACA,UAAM,YAAY,KAAK,KAAK,MAAM,kBAAkB;AACpD,QAAI,GAAG,WAAW,SAAS,GAAG;AAC5B,SAAG,OAAO,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACrD,YAAM,YAAY,SAAS,EAAE;AAAA,IAC/B;AAAA,EACF;AAEA,SAAO;AACT;","names":[]} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
788507
0.48%43
-2.27%8
-11.11%7113
-1.24%579
-1.36%