@cocaxcode/token-optimizer-mcp
Advanced tools
| #!/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/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 | ||
| 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 { | ||
| 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 { | ||
| 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":[]} |
+11
-11
@@ -9,3 +9,3 @@ #!/usr/bin/env node | ||
| surfaceWithDedupe | ||
| } from "./chunk-HZUEH22U.js"; | ||
| } from "./chunk-PMVZIR3X.js"; | ||
| import { | ||
@@ -15,7 +15,7 @@ runSerenaActivateHookFromCli | ||
| import { | ||
| ensureStorageDir | ||
| } from "./chunk-VHP52B3J.js"; | ||
| ensureGitignore | ||
| } from "./chunk-VHU3U64E.js"; | ||
| import { | ||
| loadConfig | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| } from "./chunk-DBLVAFU5.js"; | ||
| import { | ||
@@ -39,3 +39,3 @@ probeRtk, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| } from "./chunk-AWG3ZQRZ.js"; | ||
@@ -480,3 +480,3 @@ // src/hooks/posttooluse.ts | ||
| } else { | ||
| ensureStorageDir(projectDir); | ||
| ensureGitignore(projectDir); | ||
| dbPath = resolveAnalyticsDbPath(projectDir); | ||
@@ -703,3 +703,3 @@ } | ||
| } else { | ||
| ensureStorageDir(projectDir); | ||
| ensureGitignore(projectDir); | ||
| dbPath = resolveAnalyticsDbPath(projectDir); | ||
@@ -730,3 +730,3 @@ } | ||
| const projectDir = opts.projectDir ?? resolveProjectDir(); | ||
| const dbPath = opts.dbPath !== void 0 ? opts.dbPath : (ensureStorageDir(projectDir), resolveAnalyticsDbPath(projectDir)); | ||
| const dbPath = opts.dbPath !== void 0 ? opts.dbPath : (ensureGitignore(projectDir), resolveAnalyticsDbPath(projectDir)); | ||
| const db = getDb(dbPath); | ||
@@ -929,3 +929,3 @@ const queries = buildQueries(db); | ||
| } else { | ||
| ensureStorageDir(projectDir); | ||
| ensureGitignore(projectDir); | ||
| dbPath = resolveAnalyticsDbPath(projectDir); | ||
@@ -1035,3 +1035,3 @@ } | ||
| const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js"); | ||
| const { createServer } = await import("./server-LNVIXOHG.js"); | ||
| const { createServer } = await import("./server-WUE7RS3B.js"); | ||
| const server = createServer(); | ||
@@ -1046,5 +1046,5 @@ const transport = new StdioServerTransport(); | ||
| } | ||
| var { dispatchCli } = await import("./dispatcher-PKJLDZ2H.js"); | ||
| var { dispatchCli } = await import("./dispatcher-OMTXHUCU.js"); | ||
| var exitCode = await dispatchCli(args); | ||
| process.exit(exitCode); | ||
| //# sourceMappingURL=index.js.map |
+1
-1
| { | ||
| "name": "@cocaxcode/token-optimizer-mcp", | ||
| "version": "0.6.0", | ||
| "version": "0.6.1", | ||
| "mcpName": "io.github.cocaxcode/token-optimizer-mcp", | ||
@@ -5,0 +5,0 @@ "description": "Orchestration + observability + coach layer for Claude Code token optimization. Measures tool usage, enforces budgets, advises on complementary tools (serena, RTK), and proactively surfaces savings tips.", |
| #!/usr/bin/env node | ||
| import { | ||
| BudgetManager | ||
| } from "./chunk-VV5KKIQ4.js"; | ||
| import "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| projectHash, | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/budget.ts | ||
| function runBudgetCli(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const db = getDb(dbPath); | ||
| const mgr = new BudgetManager(db); | ||
| const hash = projectHash(projectDir); | ||
| const sub = args[0]; | ||
| if (sub === "set") { | ||
| const scope = args[1]; | ||
| const limitRaw = args[2]; | ||
| const limit = limitRaw ? parseInt(limitRaw, 10) : NaN; | ||
| if (scope !== "session" && scope !== "project" || !Number.isFinite(limit)) { | ||
| print("Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| try { | ||
| const budget = mgr.setBudget({ | ||
| scope, | ||
| scope_key: scopeKey, | ||
| limit_tokens: limit | ||
| }); | ||
| print( | ||
| `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}` | ||
| ); | ||
| return 0; | ||
| } catch (e) { | ||
| print(`Error: ${e instanceof Error ? e.message : String(e)}`); | ||
| return 1; | ||
| } | ||
| } | ||
| if (sub === "get") { | ||
| const status = mgr.checkBudget("default", hash); | ||
| if (!status.active) { | ||
| print("Sin presupuesto activo"); | ||
| return 0; | ||
| } | ||
| const pct = (status.percent_used * 100).toFixed(1); | ||
| print( | ||
| `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? "n/a"}` | ||
| ); | ||
| return 0; | ||
| } | ||
| if (sub === "clear") { | ||
| const scope = args[1]; | ||
| if (scope !== "session" && scope !== "project") { | ||
| print("Uso: token-optimizer-mcp budget clear <session|project>"); | ||
| return 1; | ||
| } | ||
| const scopeKey = scope === "session" ? "default" : hash; | ||
| const removed = mgr.clearBudget(scope, scopeKey); | ||
| print(removed ? `Eliminado (${scope})` : "No habia presupuesto para este scope"); | ||
| return 0; | ||
| } | ||
| print("Uso: token-optimizer-mcp budget <set|get|clear> [args]"); | ||
| return 1; | ||
| } | ||
| export { | ||
| runBudgetCli | ||
| }; | ||
| //# sourceMappingURL=budget-BC6HFBNA.js.map |
| {"version":3,"sources":["../src/cli/budget.ts"],"sourcesContent":["// Budget CLI — Phase 4.15\n// Thin wrapper delegating to BudgetManager. Subcommands: set / get / clear.\n\nimport { getDb } from '../db/connection.js'\nimport {\n resolveProjectDir,\n resolveAnalyticsDbPath,\n projectHash,\n} from '../lib/paths.js'\nimport { BudgetManager } from '../services/budget-manager.js'\nimport type { BudgetScope } from '../lib/types.js'\n\nexport interface BudgetCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runBudgetCli(args: string[] = [], opts: BudgetCliOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n const db = getDb(dbPath)\n const mgr = new BudgetManager(db)\n const hash = projectHash(projectDir)\n\n const sub = args[0]\n\n if (sub === 'set') {\n const scope = args[1] as BudgetScope | undefined\n const limitRaw = args[2]\n const limit = limitRaw ? parseInt(limitRaw, 10) : NaN\n if ((scope !== 'session' && scope !== 'project') || !Number.isFinite(limit)) {\n print('Uso: token-optimizer-mcp budget set <session|project> <limit_tokens>')\n return 1\n }\n const scopeKey = scope === 'session' ? 'default' : hash\n try {\n const budget = mgr.setBudget({\n scope,\n scope_key: scopeKey,\n limit_tokens: limit,\n })\n print(\n `Presupuesto guardado: ${budget.scope}=${budget.scope_key} limit=${budget.limit_tokens} mode=${budget.mode}`,\n )\n return 0\n } catch (e) {\n print(`Error: ${e instanceof Error ? e.message : String(e)}`)\n return 1\n }\n }\n\n if (sub === 'get') {\n const status = mgr.checkBudget('default', hash)\n if (!status.active) {\n print('Sin presupuesto activo')\n return 0\n }\n const pct = (status.percent_used * 100).toFixed(1)\n print(\n `gastado=${status.spent} restante=${status.remaining} uso=${pct}% modo=${status.mode ?? 'n/a'}`,\n )\n return 0\n }\n\n if (sub === 'clear') {\n const scope = args[1] as BudgetScope | undefined\n if (scope !== 'session' && scope !== 'project') {\n print('Uso: token-optimizer-mcp budget clear <session|project>')\n return 1\n }\n const scopeKey = scope === 'session' ? 'default' : hash\n const removed = mgr.clearBudget(scope, scopeKey)\n print(removed ? `Eliminado (${scope})` : 'No habia presupuesto para este scope')\n return 0\n }\n\n print('Uso: token-optimizer-mcp budget <set|get|clear> [args]')\n return 1\n}\n"],"mappings":";;;;;;;;;;;;;;;AAiBO,SAAS,aAAa,OAAiB,CAAC,GAAG,OAAyB,CAAC,GAAW;AACrF,QAAM,QAAQ,KAAK,UAAU,CAAC,MAAc,QAAQ,MAAM,CAAC;AAC3D,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,aAAa,kBAAkB,GAAG;AACxC,QAAM,SAAS,uBAAuB,UAAU;AAEhD,QAAM,KAAK,MAAM,MAAM;AACvB,QAAM,MAAM,IAAI,cAAc,EAAE;AAChC,QAAM,OAAO,YAAY,UAAU;AAEnC,QAAM,MAAM,KAAK,CAAC;AAElB,MAAI,QAAQ,OAAO;AACjB,UAAM,QAAQ,KAAK,CAAC;AACpB,UAAM,WAAW,KAAK,CAAC;AACvB,UAAM,QAAQ,WAAW,SAAS,UAAU,EAAE,IAAI;AAClD,QAAK,UAAU,aAAa,UAAU,aAAc,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3E,YAAM,sEAAsE;AAC5E,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,QAAI;AACF,YAAM,SAAS,IAAI,UAAU;AAAA,QAC3B;AAAA,QACA,WAAW;AAAA,QACX,cAAc;AAAA,MAChB,CAAC;AACD;AAAA,QACE,yBAAyB,OAAO,KAAK,IAAI,OAAO,SAAS,UAAU,OAAO,YAAY,SAAS,OAAO,IAAI;AAAA,MAC5G;AACA,aAAO;AAAA,IACT,SAAS,GAAG;AACV,YAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AAC5D,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,QAAQ,OAAO;AACjB,UAAM,SAAS,IAAI,YAAY,WAAW,IAAI;AAC9C,QAAI,CAAC,OAAO,QAAQ;AAClB,YAAM,wBAAwB;AAC9B,aAAO;AAAA,IACT;AACA,UAAM,OAAO,OAAO,eAAe,KAAK,QAAQ,CAAC;AACjD;AAAA,MACE,WAAW,OAAO,KAAK,aAAa,OAAO,SAAS,QAAQ,GAAG,UAAU,OAAO,QAAQ,KAAK;AAAA,IAC/F;AACA,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,SAAS;AACnB,UAAM,QAAQ,KAAK,CAAC;AACpB,QAAI,UAAU,aAAa,UAAU,WAAW;AAC9C,YAAM,yDAAyD;AAC/D,aAAO;AAAA,IACT;AACA,UAAM,WAAW,UAAU,YAAY,YAAY;AACnD,UAAM,UAAU,IAAI,YAAY,OAAO,QAAQ;AAC/C,UAAM,UAAU,cAAc,KAAK,MAAM,sCAAsC;AAC/E,WAAO;AAAA,EACT;AAEA,QAAM,wDAAwD;AAC9D,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| measureCurrentSchemaBytes | ||
| } from "./chunk-L5Z32XXL.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/prune-mcp.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/; | ||
| function extractServerFromToolName(toolName) { | ||
| const m = MCP_TOOL_RE.exec(toolName); | ||
| return m ? m[1] : null; | ||
| } | ||
| function settingsLocalPath(cwd) { | ||
| return path.join(cwd, ".claude", "settings.local.json"); | ||
| } | ||
| function readJsonSafe(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return {}; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| function writeJson(p, data) { | ||
| fs.mkdirSync(path.dirname(p), { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(data, null, 2)); | ||
| } | ||
| function generateFromHistory(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const days = opts.days ?? 14; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const serverCounts = {}; | ||
| if (fs.existsSync(dbPath)) { | ||
| const db = getDb(dbPath); | ||
| const rows = db.prepare( | ||
| `SELECT tool_name, COUNT(*) as count | ||
| FROM tool_calls | ||
| WHERE created_at >= ? AND tool_name LIKE 'mcp__%' | ||
| GROUP BY tool_name` | ||
| ).all(since); | ||
| for (const row of rows) { | ||
| const server = extractServerFromToolName(row.tool_name); | ||
| if (server) { | ||
| serverCounts[server] = (serverCounts[server] ?? 0) + row.count; | ||
| } | ||
| } | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd, home: opts.home }); | ||
| const registered = new Set(schema.mcp_servers); | ||
| const used = new Set(Object.keys(serverCounts)); | ||
| const inactive = [...registered].filter((s) => !used.has(s)); | ||
| return { | ||
| proposed_allowlist: [...used], | ||
| inactive_servers: inactive, | ||
| analysis_days: days, | ||
| total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0), | ||
| server_counts: serverCounts | ||
| }; | ||
| } | ||
| function timestampForBackup() { | ||
| return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"); | ||
| } | ||
| function insertSnapshot(cwd, method, details) { | ||
| try { | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) return; | ||
| const db = getDb(dbPath); | ||
| db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run( | ||
| method, | ||
| JSON.stringify(details) | ||
| ); | ||
| } catch { | ||
| } | ||
| } | ||
| function applyAllowlist(allowlist, opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const source = opts.source ?? "cli"; | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| if (fs.existsSync(settingsPath)) { | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| } else { | ||
| fs.mkdirSync(path.dirname(backupPath), { recursive: true }); | ||
| fs.writeFileSync(backupPath, "{}"); | ||
| } | ||
| const current = readJsonSafe(settingsPath); | ||
| current.enabledMcpjsonServers = allowlist; | ||
| writeJson(settingsPath, current); | ||
| insertSnapshot(cwd, source === "mcp" ? "allowlist_generated_via_mcp" : "allowlist_generated", { | ||
| allowlist, | ||
| target: settingsPath, | ||
| backup: backupPath | ||
| }); | ||
| return { settings_path: settingsPath, backup_path: backupPath }; | ||
| } | ||
| function rollback(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| const dir = path.dirname(settingsPath); | ||
| if (!fs.existsSync(dir)) return { restored: false, from: null }; | ||
| const backups = fs.readdirSync(dir).filter((f) => f.startsWith("settings.local.json.backup-")).sort(); | ||
| if (backups.length === 0) return { restored: false, from: null }; | ||
| const target = opts.to ? backups.find((b) => b.includes(opts.to)) : backups[backups.length - 1]; | ||
| if (!target) return { restored: false, from: null }; | ||
| const backupPath = path.join(dir, target); | ||
| fs.copyFileSync(backupPath, settingsPath); | ||
| insertSnapshot(cwd, "rollback", { from: backupPath, to: settingsPath }); | ||
| return { restored: true, from: backupPath }; | ||
| } | ||
| function clearAllowlist(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = settingsLocalPath(cwd); | ||
| if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }; | ||
| const backupPath = `${settingsPath}.backup-${timestampForBackup()}`; | ||
| fs.copyFileSync(settingsPath, backupPath); | ||
| const json = readJsonSafe(settingsPath); | ||
| delete json.enabledMcpjsonServers; | ||
| writeJson(settingsPath, json); | ||
| insertSnapshot(cwd, "allowlist_cleared", { backup: backupPath }); | ||
| return { cleared: true, backup_path: backupPath }; | ||
| } | ||
| function impact(opts = {}) { | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const db = getDb(dbPath); | ||
| const snapshot = db.prepare( | ||
| `SELECT created_at FROM optimization_snapshots | ||
| WHERE method LIKE 'allowlist_%' | ||
| ORDER BY created_at DESC LIMIT 1` | ||
| ).get(); | ||
| if (!snapshot) { | ||
| return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }; | ||
| } | ||
| const before = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const after = db.prepare( | ||
| `SELECT AVG(tokens_estimated) as avg FROM ( | ||
| SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100 | ||
| )` | ||
| ).get(snapshot.created_at); | ||
| const beforeAvg = before.avg; | ||
| const afterAvg = after.avg; | ||
| const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null; | ||
| const percent = beforeAvg !== null && beforeAvg > 0 && afterAvg !== null ? (afterAvg - beforeAvg) / beforeAvg : null; | ||
| return { | ||
| before_avg: beforeAvg, | ||
| after_avg: afterAvg, | ||
| delta, | ||
| percent, | ||
| snapshot_at: snapshot.created_at | ||
| }; | ||
| } | ||
| function runPruneMcp(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| if (args.includes("--generate-from-history")) { | ||
| const daysFlag = args.find((a) => a.startsWith("--days=")); | ||
| const days = daysFlag ? parseInt(daysFlag.split("=")[1], 10) : 14; | ||
| const result = generateFromHistory({ cwd, days }); | ||
| print(`Propuesta de allowlist (${days} dias de historial):`); | ||
| print(` Usados: ${result.proposed_allowlist.join(", ") || "(ninguno)"}`); | ||
| print(` Inactivos: ${result.inactive_servers.join(", ") || "(ninguno)"}`); | ||
| print(` Eventos MCP totales: ${result.total_mcp_events}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--apply")) { | ||
| const generated = generateFromHistory({ cwd }); | ||
| if (generated.proposed_allowlist.length === 0) { | ||
| print("No hay MCPs activos en el historial. Nada que aplicar."); | ||
| return 1; | ||
| } | ||
| const applied = applyAllowlist(generated.proposed_allowlist, { cwd }); | ||
| print(`Allowlist aplicado a ${applied.settings_path}`); | ||
| print(`Backup: ${applied.backup_path}`); | ||
| return 0; | ||
| } | ||
| if (args.includes("--rollback")) { | ||
| const toFlag = args.find((a) => a.startsWith("--to=")); | ||
| const to = toFlag ? toFlag.split("=")[1] : void 0; | ||
| const result = rollback({ cwd, to }); | ||
| if (result.restored) { | ||
| print(`Restaurado desde ${result.from}`); | ||
| return 0; | ||
| } | ||
| print("No hay backups disponibles."); | ||
| return 1; | ||
| } | ||
| if (args.includes("--clear")) { | ||
| const result = clearAllowlist({ cwd }); | ||
| print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : "Nada que eliminar"); | ||
| return 0; | ||
| } | ||
| if (args.includes("--impact")) { | ||
| const result = impact({ cwd }); | ||
| if (result.snapshot_at === null) { | ||
| print("No hay snapshots de allowlist todavia."); | ||
| return 0; | ||
| } | ||
| print(`Snapshot mas reciente: ${result.snapshot_at}`); | ||
| print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? "n/a"}`); | ||
| print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? "n/a"}`); | ||
| if (result.percent !== null) { | ||
| print(`Delta: ${(result.percent * 100).toFixed(1)}%`); | ||
| } | ||
| return 0; | ||
| } | ||
| const schema = measureCurrentSchemaBytes({ cwd }); | ||
| print(`MCPs registrados (${schema.mcp_servers.length}):`); | ||
| for (const s of schema.mcp_servers) { | ||
| print(` ${s}`); | ||
| } | ||
| print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`); | ||
| print(""); | ||
| print("Flags:"); | ||
| print(" --generate-from-history [--days N] Propone allowlist (read-only)"); | ||
| print(" --apply Aplica el allowlist generado"); | ||
| print(" --rollback [--to TIMESTAMP] Restaura el ultimo backup"); | ||
| print(" --clear Elimina allowlist actual"); | ||
| print(" --impact Compara antes/despues del ultimo snapshot"); | ||
| return 0; | ||
| } | ||
| export { | ||
| settingsLocalPath, | ||
| generateFromHistory, | ||
| applyAllowlist, | ||
| rollback, | ||
| clearAllowlist, | ||
| impact, | ||
| runPruneMcp | ||
| }; | ||
| //# sourceMappingURL=chunk-3CAOEPYE.js.map |
| {"version":3,"sources":["../src/cli/prune-mcp.ts"],"sourcesContent":["// prune-mcp CLI + service — Phase 4.16-4.22\n// Generate allowlist from history, apply/rollback/clear, compute impact.\n// Writes to .claude/settings.local.json (NOT settings.json).\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\n\nconst MCP_TOOL_RE = /^mcp__([^_]+(?:_[^_]+)*?)__/\n\nfunction extractServerFromToolName(toolName: string): string | null {\n const m = MCP_TOOL_RE.exec(toolName)\n return m ? m[1] : null\n}\n\nexport function settingsLocalPath(cwd: string): string {\n return path.join(cwd, '.claude', 'settings.local.json')\n}\n\nfunction readJsonSafe(p: string): Record<string, unknown> {\n try {\n if (!fs.existsSync(p)) return {}\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\n } catch {\n return {}\n }\n}\n\nfunction writeJson(p: string, data: Record<string, unknown>): void {\n fs.mkdirSync(path.dirname(p), { recursive: true })\n fs.writeFileSync(p, JSON.stringify(data, null, 2))\n}\n\nexport interface GeneratedAllowlist {\n proposed_allowlist: string[]\n inactive_servers: string[]\n analysis_days: number\n total_mcp_events: number\n server_counts: Record<string, number>\n}\n\nexport interface GenerateOptions {\n cwd?: string\n days?: number\n home?: string\n}\n\nexport function generateFromHistory(opts: GenerateOptions = {}): GeneratedAllowlist {\n const cwd = opts.cwd ?? process.cwd()\n const days = opts.days ?? 14\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\n\n const serverCounts: Record<string, number> = {}\n if (fs.existsSync(dbPath)) {\n const db = getDb(dbPath)\n const rows = db\n .prepare(\n `SELECT tool_name, COUNT(*) as count\n FROM tool_calls\n WHERE created_at >= ? AND tool_name LIKE 'mcp__%'\n GROUP BY tool_name`,\n )\n .all(since) as Array<{ tool_name: string; count: number }>\n for (const row of rows) {\n const server = extractServerFromToolName(row.tool_name)\n if (server) {\n serverCounts[server] = (serverCounts[server] ?? 0) + row.count\n }\n }\n }\n\n const schema = measureCurrentSchemaBytes({ cwd, home: opts.home })\n const registered = new Set(schema.mcp_servers)\n const used = new Set(Object.keys(serverCounts))\n const inactive = [...registered].filter((s) => !used.has(s))\n\n return {\n proposed_allowlist: [...used],\n inactive_servers: inactive,\n analysis_days: days,\n total_mcp_events: Object.values(serverCounts).reduce((a, b) => a + b, 0),\n server_counts: serverCounts,\n }\n}\n\nexport interface ApplyOptions {\n cwd?: string\n source?: 'cli' | 'mcp'\n}\n\nexport interface ApplyResult {\n settings_path: string\n backup_path: string\n}\n\nfunction timestampForBackup(): string {\n return new Date().toISOString().replace(/[:.]/g, '-')\n}\n\nfunction insertSnapshot(cwd: string, method: string, details: Record<string, unknown>): void {\n try {\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) return\n const db = getDb(dbPath)\n db.prepare(`INSERT INTO optimization_snapshots (method, details) VALUES (?, ?)`).run(\n method,\n JSON.stringify(details),\n )\n } catch {\n // swallow\n }\n}\n\nexport function applyAllowlist(allowlist: string[], opts: ApplyOptions = {}): ApplyResult {\n const cwd = opts.cwd ?? process.cwd()\n const source = opts.source ?? 'cli'\n const settingsPath = settingsLocalPath(cwd)\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\n\n if (fs.existsSync(settingsPath)) {\n fs.copyFileSync(settingsPath, backupPath)\n } else {\n fs.mkdirSync(path.dirname(backupPath), { recursive: true })\n fs.writeFileSync(backupPath, '{}')\n }\n\n const current = readJsonSafe(settingsPath)\n current.enabledMcpjsonServers = allowlist\n writeJson(settingsPath, current)\n\n insertSnapshot(cwd, source === 'mcp' ? 'allowlist_generated_via_mcp' : 'allowlist_generated', {\n allowlist,\n target: settingsPath,\n backup: backupPath,\n })\n\n return { settings_path: settingsPath, backup_path: backupPath }\n}\n\nexport interface RollbackOptions {\n cwd?: string\n to?: string\n}\n\nexport interface RollbackResult {\n restored: boolean\n from: string | null\n}\n\nexport function rollback(opts: RollbackOptions = {}): RollbackResult {\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = settingsLocalPath(cwd)\n const dir = path.dirname(settingsPath)\n if (!fs.existsSync(dir)) return { restored: false, from: null }\n\n const backups = fs\n .readdirSync(dir)\n .filter((f) => f.startsWith('settings.local.json.backup-'))\n .sort()\n if (backups.length === 0) return { restored: false, from: null }\n\n const target = opts.to ? backups.find((b) => b.includes(opts.to!)) : backups[backups.length - 1]\n if (!target) return { restored: false, from: null }\n\n const backupPath = path.join(dir, target)\n fs.copyFileSync(backupPath, settingsPath)\n\n insertSnapshot(cwd, 'rollback', { from: backupPath, to: settingsPath })\n return { restored: true, from: backupPath }\n}\n\nexport function clearAllowlist(opts: { cwd?: string } = {}): { cleared: boolean; backup_path: string | null } {\n const cwd = opts.cwd ?? process.cwd()\n const settingsPath = settingsLocalPath(cwd)\n if (!fs.existsSync(settingsPath)) return { cleared: false, backup_path: null }\n\n const backupPath = `${settingsPath}.backup-${timestampForBackup()}`\n fs.copyFileSync(settingsPath, backupPath)\n\n const json = readJsonSafe(settingsPath)\n delete json.enabledMcpjsonServers\n writeJson(settingsPath, json)\n\n insertSnapshot(cwd, 'allowlist_cleared', { backup: backupPath })\n return { cleared: true, backup_path: backupPath }\n}\n\nexport interface ImpactResult {\n before_avg: number | null\n after_avg: number | null\n delta: number | null\n percent: number | null\n snapshot_at: string | null\n}\n\nexport function impact(opts: { cwd?: string } = {}): ImpactResult {\n const cwd = opts.cwd ?? process.cwd()\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n if (!fs.existsSync(dbPath)) {\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\n }\n const db = getDb(dbPath)\n const snapshot = db\n .prepare(\n `SELECT created_at FROM optimization_snapshots\n WHERE method LIKE 'allowlist_%'\n ORDER BY created_at DESC LIMIT 1`,\n )\n .get() as { created_at: string } | undefined\n if (!snapshot) {\n return { before_avg: null, after_avg: null, delta: null, percent: null, snapshot_at: null }\n }\n\n const before = db\n .prepare(\n `SELECT AVG(tokens_estimated) as avg FROM (\n SELECT tokens_estimated FROM tool_calls WHERE created_at < ? ORDER BY created_at DESC LIMIT 100\n )`,\n )\n .get(snapshot.created_at) as { avg: number | null }\n const after = db\n .prepare(\n `SELECT AVG(tokens_estimated) as avg FROM (\n SELECT tokens_estimated FROM tool_calls WHERE created_at >= ? ORDER BY created_at ASC LIMIT 100\n )`,\n )\n .get(snapshot.created_at) as { avg: number | null }\n\n const beforeAvg = before.avg\n const afterAvg = after.avg\n const delta = beforeAvg !== null && afterAvg !== null ? afterAvg - beforeAvg : null\n const percent =\n beforeAvg !== null && beforeAvg > 0 && afterAvg !== null\n ? (afterAvg - beforeAvg) / beforeAvg\n : null\n\n return {\n before_avg: beforeAvg,\n after_avg: afterAvg,\n delta,\n percent,\n snapshot_at: snapshot.created_at,\n }\n}\n\nexport interface PruneMcpCliOptions {\n cwd?: string\n print?: (msg: string) => void\n}\n\nexport function runPruneMcp(args: string[] = [], opts: PruneMcpCliOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n\n if (args.includes('--generate-from-history')) {\n const daysFlag = args.find((a) => a.startsWith('--days='))\n const days = daysFlag ? parseInt(daysFlag.split('=')[1], 10) : 14\n const result = generateFromHistory({ cwd, days })\n print(`Propuesta de allowlist (${days} dias de historial):`)\n print(` Usados: ${result.proposed_allowlist.join(', ') || '(ninguno)'}`)\n print(` Inactivos: ${result.inactive_servers.join(', ') || '(ninguno)'}`)\n print(` Eventos MCP totales: ${result.total_mcp_events}`)\n return 0\n }\n\n if (args.includes('--apply')) {\n const generated = generateFromHistory({ cwd })\n if (generated.proposed_allowlist.length === 0) {\n print('No hay MCPs activos en el historial. Nada que aplicar.')\n return 1\n }\n const applied = applyAllowlist(generated.proposed_allowlist, { cwd })\n print(`Allowlist aplicado a ${applied.settings_path}`)\n print(`Backup: ${applied.backup_path}`)\n return 0\n }\n\n if (args.includes('--rollback')) {\n const toFlag = args.find((a) => a.startsWith('--to='))\n const to = toFlag ? toFlag.split('=')[1] : undefined\n const result = rollback({ cwd, to })\n if (result.restored) {\n print(`Restaurado desde ${result.from}`)\n return 0\n }\n print('No hay backups disponibles.')\n return 1\n }\n\n if (args.includes('--clear')) {\n const result = clearAllowlist({ cwd })\n print(result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar')\n return 0\n }\n\n if (args.includes('--impact')) {\n const result = impact({ cwd })\n if (result.snapshot_at === null) {\n print('No hay snapshots de allowlist todavia.')\n return 0\n }\n print(`Snapshot mas reciente: ${result.snapshot_at}`)\n print(`Promedio tokens/evento antes: ${result.before_avg?.toFixed(1) ?? 'n/a'}`)\n print(`Promedio tokens/evento despues: ${result.after_avg?.toFixed(1) ?? 'n/a'}`)\n if (result.percent !== null) {\n print(`Delta: ${(result.percent * 100).toFixed(1)}%`)\n }\n return 0\n }\n\n // Default: list registered MCPs with estimated cost\n const schema = measureCurrentSchemaBytes({ cwd })\n print(`MCPs registrados (${schema.mcp_servers.length}):`)\n for (const s of schema.mcp_servers) {\n print(` ${s}`)\n }\n print(`Coste estimado (heuristica): ~${schema.tool_schema_tokens} tokens`)\n print('')\n print('Flags:')\n print(' --generate-from-history [--days N] Propone allowlist (read-only)')\n print(' --apply Aplica el allowlist generado')\n print(' --rollback [--to TIMESTAMP] Restaura el ultimo backup')\n print(' --clear Elimina allowlist actual')\n print(' --impact Compara antes/despues del ultimo snapshot')\n return 0\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveXrayUrl | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| resolveTranscriptPath | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/coach/knowledge-base.ts | ||
| var KNOWLEDGE_BASE = [ | ||
| { | ||
| id: "use-opusplan", | ||
| title: "Usa /opusplan para planificar con Opus y ejecutar con Sonnet", | ||
| description: "opusplan usa Opus durante plan mode para razonamiento complejo y vuelve a Sonnet para implementacion. Solo pagas Opus en la fase de planning.", | ||
| savings_estimate: "60-80% de reduccion de coste en sesiones con planning intensivo", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "/model opusplan", | ||
| when_applicable: "Sesiones con razonamiento largo antes de codigo", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-long-reasoning-no-code" | ||
| }, | ||
| { | ||
| id: "use-plan-mode", | ||
| title: "Activa plan mode para exploracion sin escribir codigo", | ||
| description: "EnterPlanMode permite razonar y explorar sin hacer ediciones, reduciendo iteraciones costosas.", | ||
| savings_estimate: "Variable segun tarea", | ||
| savings_source: "internal", | ||
| how_to_invoke: "EnterPlanMode tool", | ||
| when_applicable: "Tareas no triviales antes de escribir codigo", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-long-reasoning-no-code" | ||
| }, | ||
| { | ||
| id: "use-fast-mode", | ||
| title: "Activa /fast para respuestas mas directas", | ||
| description: "Modo rapido mantiene el modelo pero reduce el detalle de las respuestas.", | ||
| savings_estimate: "Reduce tiempo principalmente", | ||
| savings_source: "internal", | ||
| how_to_invoke: "/fast", | ||
| when_applicable: "Cuando quieres respuestas mas concisas", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "default-to-sonnet", | ||
| title: "Arranca cada sesion con Sonnet y sube a Opus solo cuando haga falta", | ||
| description: "Sonnet resuelve ~80% de tareas de coding bien. El switching tactico a Opus solo en razonamiento complejo ahorra el grueso del coste.", | ||
| savings_estimate: "60-80% reduccion de coste total", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "/model sonnet (inicio) \u2192 /model opus (cuando sea necesario)", | ||
| when_applicable: "Siempre como default", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-opus-for-simple-task" | ||
| }, | ||
| { | ||
| id: "use-haiku-for-simple", | ||
| title: "Usa Haiku para formato, Q&A simple y tareas de alto volumen", | ||
| description: "Haiku es mucho mas barato y rapido. Para formateo, preguntas puntuales o tareas repetitivas es el modelo adecuado.", | ||
| savings_estimate: "~90% reduccion vs Opus en tareas simples", | ||
| savings_source: "anthropic-docs", | ||
| how_to_invoke: "/model haiku", | ||
| when_applicable: "Formateo, Q&A simple, alto volumen", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-opus-for-simple-task" | ||
| }, | ||
| { | ||
| id: "use-compact-long-session", | ||
| title: "Corre /compact cuando el contexto supere el 75%", | ||
| description: "/compact genera un resumen del contexto actual liberando ~60-80% de la ventana sin perder continuidad.", | ||
| savings_estimate: "60-80% de contexto liberado", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "/compact", | ||
| when_applicable: "Contexto > 75% de la ventana", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-context-threshold" | ||
| }, | ||
| { | ||
| id: "use-clear-rename-resume", | ||
| title: "Usa /rename \u2192 /clear \u2192 /resume para pivotes de tema", | ||
| description: "Al cambiar a un tema no relacionado, renombra la sesion, haz /clear para empezar limpio, y resume cuando vuelvas.", | ||
| savings_estimate: "Variable segun contexto descartado", | ||
| savings_source: "internal", | ||
| how_to_invoke: "/rename <nombre> \u2192 /clear \u2192 (trabajar) \u2192 /resume <nombre>", | ||
| when_applicable: "Pivote total a tema no relacionado", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-clear-opportunity" | ||
| }, | ||
| { | ||
| id: "use-sessionstart-compact-hook", | ||
| title: "Activa el hook SessionStart:compact de token-optimizer", | ||
| description: "Cuando Claude Code compacta el contexto, token-optimizer inyecta un resumen con archivos, comandos y presupuesto.", | ||
| savings_estimate: "Evita re-lectura tras compactacion", | ||
| savings_source: "internal", | ||
| how_to_invoke: "token-optimizer-mcp install (ya lo configura)", | ||
| when_applicable: "Siempre como parte del install", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-memory-save", | ||
| title: "Guarda decisiones con mem_save antes de compactar", | ||
| description: "Persistir decisiones arquitectonicas en engram evita tener que re-derivarlas cuando el contexto se compacta.", | ||
| savings_estimate: "Variable", | ||
| savings_source: "internal", | ||
| how_to_invoke: "mem_save (via engram MCP)", | ||
| when_applicable: "Antes de /compact o cambiar de sesion", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-agent-explore", | ||
| title: "Delega busquedas amplias al subagente Explore", | ||
| description: "El subagente Explore tiene su propio contexto y no consume el de la sesion principal. Ideal para buscar en muchos archivos.", | ||
| savings_estimate: "Aisla contexto al subagente", | ||
| savings_source: "internal", | ||
| how_to_invoke: 'Agent tool con subagent_type="Explore"', | ||
| when_applicable: "3+ busquedas Grep/Glob similares", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-repeated-searches" | ||
| }, | ||
| { | ||
| id: "use-todowrite-long-task", | ||
| title: "Usa TodoWrite para tareas multi-paso", | ||
| description: "TodoWrite mantiene el estado de la tarea sin re-leer archivos, reduciendo redundancia.", | ||
| savings_estimate: "Evita re-lectura de estado", | ||
| savings_source: "internal", | ||
| how_to_invoke: "TodoWrite", | ||
| when_applicable: "3+ pasos independientes", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-skill-trigger", | ||
| title: "Invoca skills en lugar de re-derivar instrucciones", | ||
| description: "Los skills cargan instrucciones especializadas solo cuando se invocan. Mejor que un CLAUDE.md monolitico.", | ||
| savings_estimate: "~15k tokens/sesion con progressive disclosure", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "Skill tool con nombre del skill", | ||
| when_applicable: "Tareas que matchean un skill disponible", | ||
| source_type: "skill", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-skill-trigger-ignored" | ||
| }, | ||
| { | ||
| id: "install-serena", | ||
| title: "Instala serena-mcp para lecturas simbolicas", | ||
| description: "serena usa LSP para leer solo los simbolos que necesitas en lugar del archivo completo. Nota: incluye execute_shell_command.", | ||
| savings_estimate: "20-30% en lecturas de archivos grandes", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "uvx --from git+https://github.com/oraios/serena serena start-mcp-server", | ||
| when_applicable: "Proyectos con archivos >50k tokens", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-huge-file-reads" | ||
| }, | ||
| { | ||
| id: "prefer-serena-reads", | ||
| title: "Usa Serena en vez de Read para archivos de codigo", | ||
| description: "Serena lee simbolos (funciones, clases) sin cargar el archivo completo. Usa get_symbols_overview para explorar y find_symbol con include_body para leer solo lo que necesitas. Ahorro tipico: 60-90% vs Read.", | ||
| savings_estimate: "60-90% en lecturas de codigo", | ||
| savings_source: "internal", | ||
| how_to_invoke: "get_symbols_overview(path) \u2192 find_symbol(name, include_body=true)", | ||
| when_applicable: "Archivos .ts/.js/.py/.java >50 lineas donde solo necesitas 1-2 funciones", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-12", | ||
| detector_id: "detect-read-over-serena" | ||
| }, | ||
| { | ||
| id: "install-rtk", | ||
| title: "Instala RTK para filtrar salida ruidosa de Bash", | ||
| description: "RTK filtra output de builds/tests antes de llegar a Claude Code. Publica releases firmadas con GPG.", | ||
| savings_estimate: "15-25% en ciclos build/test", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "brew install standard-input/tap/rtk (macOS) o binario firmado en github.com/standard-input/rtk", | ||
| when_applicable: "Proyectos con builds/tests ruidosos", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-many-bash-commands" | ||
| }, | ||
| { | ||
| id: "use-mcp-prune", | ||
| title: "Aplica un allowlist de MCPs por proyecto", | ||
| description: "Reduce el coste del tool-schema excluyendo MCPs que no usas en este proyecto. ~5-12% adicional sobre Tool Search.", | ||
| savings_estimate: "5-12% por turno sobre Tool Search nativo", | ||
| savings_source: "internal", | ||
| how_to_invoke: "mcp_prune_suggest \u2192 mcp_prune_apply", | ||
| when_applicable: "MCPs registrados pero no usados en el proyecto", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-unused-mcp-servers" | ||
| }, | ||
| { | ||
| id: "migrate-claudemd-to-skills", | ||
| title: "Migra CLAUDE.md grande a skills con progressive disclosure", | ||
| description: "Un CLAUDE.md monolitico se carga en cada sesion. Los skills solo cargan cuando se invocan. ~15k tokens recuperados.", | ||
| savings_estimate: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| savings_source: "community-measured", | ||
| how_to_invoke: "Crear skills en .claude/skills/ con triggers especificos", | ||
| when_applicable: "CLAUDE.md > 10k tokens con uso parcial", | ||
| source_type: "skill", | ||
| verified_at: "2026-04-11", | ||
| detector_id: "detect-claudemd-bloat" | ||
| }, | ||
| { | ||
| id: "use-settings-local", | ||
| title: "Configuracion personal en settings.local.json", | ||
| description: "Evita contaminar settings.json del equipo. settings.local.json es personal y gitignored por defecto.", | ||
| savings_estimate: "Higiene, no tokens", | ||
| savings_source: "internal", | ||
| how_to_invoke: "Editar .claude/settings.local.json", | ||
| when_applicable: "Configuracion personal no compartible", | ||
| source_type: "settings", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| }, | ||
| { | ||
| id: "use-serena-overview-first", | ||
| title: "Usa get_symbols_overview antes de find_symbol", | ||
| description: "Llamar get_symbols_overview una vez da el mapa del archivo. Las llamadas sucesivas find_symbol sin overview previo leen el mismo archivo repetidamente.", | ||
| savings_estimate: "30-50% menos llamadas Serena por sesion", | ||
| savings_source: "internal", | ||
| how_to_invoke: "mcp__serena__get_symbols_overview con relative_path antes de find_symbol", | ||
| when_applicable: "Al explorar un archivo por primera vez en la sesion", | ||
| source_type: "mcp", | ||
| verified_at: "2026-04-15", | ||
| detector_id: "detect-serena-read-cascade" | ||
| }, | ||
| { | ||
| id: "use-prompt-caching", | ||
| title: "Estructura prompts para maximizar cache hits", | ||
| description: "Los tokens leidos del cache cuestan 10x menos. Mantener el prefijo estable (system, CLAUDE.md) aprovecha el cache.", | ||
| savings_estimate: "10x mas barato en reads cacheados", | ||
| savings_source: "anthropic-docs", | ||
| how_to_invoke: "Mantener prefijo estable entre turns", | ||
| when_applicable: "Siempre", | ||
| source_type: "built-in", | ||
| verified_at: "2026-04-11", | ||
| detector_id: null | ||
| } | ||
| ]; | ||
| // src/coach/rules.ts | ||
| function countMatching(events, predicate) { | ||
| let c = 0; | ||
| for (const e of events) if (predicate(e)) c++; | ||
| return c; | ||
| } | ||
| var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]); | ||
| var DETECTION_RULES = [ | ||
| // 1. detect-context-threshold | ||
| { | ||
| id: "detect-context-threshold", | ||
| tip_ids: ["use-compact-long-session"], | ||
| run(ctx) { | ||
| if (ctx.session_token_total === null || ctx.session_token_limit <= 0) return null; | ||
| const percent = ctx.session_token_total / ctx.session_token_limit; | ||
| if (percent < 0.5) return null; | ||
| let severity = "info"; | ||
| if (percent >= 0.9) severity = "critical"; | ||
| else if (percent >= 0.75) severity = "warn"; | ||
| return { | ||
| rule_id: "detect-context-threshold", | ||
| tip_ids: ["use-compact-long-session"], | ||
| severity, | ||
| evidence: `Contexto: ${(percent * 100).toFixed(1)}% usado (${ctx.session_token_total}/${ctx.session_token_limit} tokens)`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| }, | ||
| // 2. detect-long-reasoning-no-code | ||
| { | ||
| id: "detect-long-reasoning-no-code", | ||
| tip_ids: ["use-plan-mode", "use-opusplan"], | ||
| run(ctx) { | ||
| const recent = ctx.events.slice(0, 10); | ||
| if (recent.length < 10) return null; | ||
| const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name)); | ||
| if (edits > 0) return null; | ||
| return { | ||
| rule_id: "detect-long-reasoning-no-code", | ||
| tip_ids: ["use-plan-mode", "use-opusplan"], | ||
| severity: "info", | ||
| evidence: "10 eventos recientes sin ediciones de codigo", | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 3. detect-repeated-searches | ||
| { | ||
| id: "detect-repeated-searches", | ||
| tip_ids: ["use-agent-explore"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 20); | ||
| const searches = countMatching(window, (e) => e.tool_name === "Grep" || e.tool_name === "Glob"); | ||
| if (searches < 3) return null; | ||
| return { | ||
| rule_id: "detect-repeated-searches", | ||
| tip_ids: ["use-agent-explore"], | ||
| severity: "info", | ||
| evidence: `${searches} busquedas Grep/Glob en los ultimos 20 eventos`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 4. detect-huge-file-reads | ||
| { | ||
| id: "detect-huge-file-reads", | ||
| tip_ids: ["install-serena"], | ||
| run(ctx) { | ||
| const huge = ctx.events.find((e) => e.tool_name === "Read" && e.tokens_estimated > 5e4); | ||
| if (!huge) return null; | ||
| return { | ||
| rule_id: "detect-huge-file-reads", | ||
| tip_ids: ["install-serena"], | ||
| severity: "warn", | ||
| evidence: `Read consumio ${huge.tokens_estimated} tokens (umbral 50k)`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 5. detect-many-bash-commands | ||
| { | ||
| id: "detect-many-bash-commands", | ||
| tip_ids: ["install-rtk"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 100); | ||
| const bash = countMatching(window, (e) => e.tool_name === "Bash"); | ||
| if (bash <= 10) return null; | ||
| return { | ||
| rule_id: "detect-many-bash-commands", | ||
| tip_ids: ["install-rtk"], | ||
| severity: "info", | ||
| evidence: `${bash} comandos Bash en los ultimos ${window.length} eventos`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 6. detect-clear-opportunity (was #7 — detect-unused-mcp-servers stub removed) | ||
| { | ||
| id: "detect-clear-opportunity", | ||
| tip_ids: ["use-clear-rename-resume"], | ||
| run(ctx) { | ||
| if (ctx.events.length < 40) return null; | ||
| const recentTools = new Set(ctx.events.slice(0, 20).map((e) => e.tool_name)); | ||
| const priorTools = new Set(ctx.events.slice(20, 40).map((e) => e.tool_name)); | ||
| if (recentTools.size === 0) return null; | ||
| let overlap = 0; | ||
| for (const t of recentTools) if (priorTools.has(t)) overlap++; | ||
| const ratio = overlap / recentTools.size; | ||
| if (ratio >= 0.3) return null; | ||
| return { | ||
| rule_id: "detect-clear-opportunity", | ||
| tip_ids: ["use-clear-rename-resume"], | ||
| severity: "info", | ||
| evidence: `Solapamiento de herramientas ${(ratio * 100).toFixed(0)}% \u2014 posible pivote de tema`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 8. detect-opus-for-simple-task | ||
| { | ||
| id: "detect-opus-for-simple-task", | ||
| tip_ids: ["default-to-sonnet", "use-haiku-for-simple"], | ||
| run(ctx) { | ||
| if (!ctx.active_model || !/opus/i.test(ctx.active_model)) return null; | ||
| const recent = ctx.events.slice(0, 20); | ||
| if (recent.length < 6) return null; | ||
| const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name)); | ||
| const bash = countMatching(recent, (e) => e.tool_name === "Bash"); | ||
| if (edits + bash < 6) return null; | ||
| return { | ||
| rule_id: "detect-opus-for-simple-task", | ||
| tip_ids: ["default-to-sonnet", "use-haiku-for-simple"], | ||
| severity: "info", | ||
| evidence: `Opus ejecutando trabajo mecanico: ${edits} edits + ${bash} Bash en ultimos 20 eventos. Sonnet haria lo mismo un 80% mas barato.`, | ||
| estimation_method: "measured_exact" | ||
| }; | ||
| } | ||
| }, | ||
| // 9. detect-claudemd-bloat (stub — requires filesystem stat at runtime) | ||
| { | ||
| id: "detect-claudemd-bloat", | ||
| tip_ids: ["migrate-claudemd-to-skills"], | ||
| run() { | ||
| return null; | ||
| } | ||
| }, | ||
| // 10. detect-post-milestone-opportunity | ||
| { | ||
| id: "detect-post-milestone-opportunity", | ||
| tip_ids: ["use-compact-long-session"], | ||
| run(ctx) { | ||
| const recent = ctx.events.slice(0, 20); | ||
| const edits = countMatching(recent, (e) => e.tool_name === "Edit" || e.tool_name === "Write"); | ||
| const hasBash = countMatching(recent, (e) => e.tool_name === "Bash") > 0; | ||
| if (edits < 5 || !hasBash) return null; | ||
| if (ctx.session_token_total === null) return null; | ||
| const percent = ctx.session_token_total / ctx.session_token_limit; | ||
| if (percent < 0.4) return null; | ||
| return { | ||
| rule_id: "detect-post-milestone-opportunity", | ||
| tip_ids: ["use-compact-long-session"], | ||
| severity: "info", | ||
| evidence: `${edits} ediciones + Bash reciente + contexto ${(percent * 100).toFixed(0)}%`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| }, | ||
| // 11. detect-skill-trigger-ignored (stub — requires skill registry) | ||
| { | ||
| id: "detect-skill-trigger-ignored", | ||
| tip_ids: ["use-skill-trigger"], | ||
| run() { | ||
| return null; | ||
| } | ||
| }, | ||
| // 12. detect-serena-read-cascade | ||
| // Fires when the agent makes ≥5 find_symbol calls without a get_symbols_overview | ||
| // in the same window — suggests starting with an overview first. | ||
| { | ||
| id: "detect-serena-read-cascade", | ||
| tip_ids: ["use-serena-overview-first"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 15); | ||
| const findSymbolCount = countMatching( | ||
| window, | ||
| (e) => e.tool_name === "mcp__serena__find_symbol" | ||
| ); | ||
| if (findSymbolCount < 5) return null; | ||
| const hasOverview = window.some( | ||
| (e) => e.tool_name === "mcp__serena__get_symbols_overview" | ||
| ); | ||
| if (hasOverview) return null; | ||
| return { | ||
| rule_id: "detect-serena-read-cascade", | ||
| tip_ids: ["use-serena-overview-first"], | ||
| severity: "info", | ||
| evidence: `${findSymbolCount} llamadas find_symbol sin get_symbols_overview en los ultimos 15 eventos.`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| }, | ||
| // 13. detect-read-over-serena | ||
| { | ||
| id: "detect-read-over-serena", | ||
| tip_ids: ["prefer-serena-reads"], | ||
| run(ctx) { | ||
| const window = ctx.events.slice(0, 30); | ||
| const largeReads = window.filter( | ||
| (e) => e.tool_name === "Read" && e.tokens_estimated > 2e3 | ||
| ); | ||
| if (largeReads.length < 3) return null; | ||
| const totalTokens = largeReads.reduce((sum, e) => sum + e.tokens_estimated, 0); | ||
| const estimatedSaving = Math.round(totalTokens * 0.7); | ||
| const severity = largeReads.length >= 6 ? "warn" : "info"; | ||
| return { | ||
| rule_id: "detect-read-over-serena", | ||
| tip_ids: ["prefer-serena-reads"], | ||
| severity, | ||
| evidence: `${largeReads.length} lecturas Read >2k tokens (total: ${totalTokens}). Serena ahorraria ~${estimatedSaving} tokens (~70%).`, | ||
| estimation_method: ctx.session_token_method | ||
| }; | ||
| } | ||
| } | ||
| ]; | ||
| // src/coach/detector.ts | ||
| var SEVERITY_ORDER = { critical: 0, warn: 1, info: 2 }; | ||
| function runRules(ctx) { | ||
| const hits = []; | ||
| for (const rule of DETECTION_RULES) { | ||
| try { | ||
| const hit = rule.run(ctx); | ||
| if (hit) hits.push(hit); | ||
| } catch { | ||
| } | ||
| } | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| const unique = []; | ||
| for (const h of hits) { | ||
| if (seen.has(h.rule_id)) continue; | ||
| seen.add(h.rule_id); | ||
| unique.push(h); | ||
| } | ||
| unique.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99)); | ||
| return unique; | ||
| } | ||
| // src/coach/context-meter.ts | ||
| import fs from "fs"; | ||
| // src/services/xray-client.ts | ||
| var POST_TIMEOUT_MS = 500; | ||
| var GET_TIMEOUT_MS = 300; | ||
| var DEFAULT_LIMIT = 2e5; | ||
| var VERSION = true ? "0.6.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-HZUEH22U.js.map |
| {"version":3,"sources":["../src/coach/knowledge-base.ts","../src/coach/rules.ts","../src/coach/detector.ts","../src/coach/context-meter.ts","../src/services/xray-client.ts","../src/coach/surface.ts"],"sourcesContent":["// Static catalog of 18 CoachTip entries — Phase 4.40\n// See sdd/token-optimizer-v0.1/coach-layer-addendum CO-1\n\nimport type { CoachTip } from '../lib/types.js'\n\nexport const KNOWLEDGE_BASE: readonly CoachTip[] = [\n {\n id: 'use-opusplan',\n title: 'Usa /opusplan para planificar con Opus y ejecutar con Sonnet',\n description:\n 'opusplan usa Opus durante plan mode para razonamiento complejo y vuelve a Sonnet para implementacion. Solo pagas Opus en la fase de planning.',\n savings_estimate: '60-80% de reduccion de coste en sesiones con planning intensivo',\n savings_source: 'community-measured',\n how_to_invoke: '/model opusplan',\n when_applicable: 'Sesiones con razonamiento largo antes de codigo',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-long-reasoning-no-code',\n },\n {\n id: 'use-plan-mode',\n title: 'Activa plan mode para exploracion sin escribir codigo',\n description:\n 'EnterPlanMode permite razonar y explorar sin hacer ediciones, reduciendo iteraciones costosas.',\n savings_estimate: 'Variable segun tarea',\n savings_source: 'internal',\n how_to_invoke: 'EnterPlanMode tool',\n when_applicable: 'Tareas no triviales antes de escribir codigo',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-long-reasoning-no-code',\n },\n {\n id: 'use-fast-mode',\n title: 'Activa /fast para respuestas mas directas',\n description: 'Modo rapido mantiene el modelo pero reduce el detalle de las respuestas.',\n savings_estimate: 'Reduce tiempo principalmente',\n savings_source: 'internal',\n how_to_invoke: '/fast',\n when_applicable: 'Cuando quieres respuestas mas concisas',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'default-to-sonnet',\n title: 'Arranca cada sesion con Sonnet y sube a Opus solo cuando haga falta',\n description:\n 'Sonnet resuelve ~80% de tareas de coding bien. El switching tactico a Opus solo en razonamiento complejo ahorra el grueso del coste.',\n savings_estimate: '60-80% reduccion de coste total',\n savings_source: 'community-measured',\n how_to_invoke: '/model sonnet (inicio) → /model opus (cuando sea necesario)',\n when_applicable: 'Siempre como default',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-opus-for-simple-task',\n },\n {\n id: 'use-haiku-for-simple',\n title: 'Usa Haiku para formato, Q&A simple y tareas de alto volumen',\n description:\n 'Haiku es mucho mas barato y rapido. Para formateo, preguntas puntuales o tareas repetitivas es el modelo adecuado.',\n savings_estimate: '~90% reduccion vs Opus en tareas simples',\n savings_source: 'anthropic-docs',\n how_to_invoke: '/model haiku',\n when_applicable: 'Formateo, Q&A simple, alto volumen',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-opus-for-simple-task',\n },\n {\n id: 'use-compact-long-session',\n title: 'Corre /compact cuando el contexto supere el 75%',\n description:\n '/compact genera un resumen del contexto actual liberando ~60-80% de la ventana sin perder continuidad.',\n savings_estimate: '60-80% de contexto liberado',\n savings_source: 'community-measured',\n how_to_invoke: '/compact',\n when_applicable: 'Contexto > 75% de la ventana',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-context-threshold',\n },\n {\n id: 'use-clear-rename-resume',\n title: 'Usa /rename → /clear → /resume para pivotes de tema',\n description:\n 'Al cambiar a un tema no relacionado, renombra la sesion, haz /clear para empezar limpio, y resume cuando vuelvas.',\n savings_estimate: 'Variable segun contexto descartado',\n savings_source: 'internal',\n how_to_invoke: '/rename <nombre> → /clear → (trabajar) → /resume <nombre>',\n when_applicable: 'Pivote total a tema no relacionado',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-clear-opportunity',\n },\n {\n id: 'use-sessionstart-compact-hook',\n title: 'Activa el hook SessionStart:compact de token-optimizer',\n description:\n 'Cuando Claude Code compacta el contexto, token-optimizer inyecta un resumen con archivos, comandos y presupuesto.',\n savings_estimate: 'Evita re-lectura tras compactacion',\n savings_source: 'internal',\n how_to_invoke: 'token-optimizer-mcp install (ya lo configura)',\n when_applicable: 'Siempre como parte del install',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-memory-save',\n title: 'Guarda decisiones con mem_save antes de compactar',\n description:\n 'Persistir decisiones arquitectonicas en engram evita tener que re-derivarlas cuando el contexto se compacta.',\n savings_estimate: 'Variable',\n savings_source: 'internal',\n how_to_invoke: 'mem_save (via engram MCP)',\n when_applicable: 'Antes de /compact o cambiar de sesion',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-agent-explore',\n title: 'Delega busquedas amplias al subagente Explore',\n description:\n 'El subagente Explore tiene su propio contexto y no consume el de la sesion principal. Ideal para buscar en muchos archivos.',\n savings_estimate: 'Aisla contexto al subagente',\n savings_source: 'internal',\n how_to_invoke: 'Agent tool con subagent_type=\"Explore\"',\n when_applicable: '3+ busquedas Grep/Glob similares',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: 'detect-repeated-searches',\n },\n {\n id: 'use-todowrite-long-task',\n title: 'Usa TodoWrite para tareas multi-paso',\n description:\n 'TodoWrite mantiene el estado de la tarea sin re-leer archivos, reduciendo redundancia.',\n savings_estimate: 'Evita re-lectura de estado',\n savings_source: 'internal',\n how_to_invoke: 'TodoWrite',\n when_applicable: '3+ pasos independientes',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-skill-trigger',\n title: 'Invoca skills en lugar de re-derivar instrucciones',\n description:\n 'Los skills cargan instrucciones especializadas solo cuando se invocan. Mejor que un CLAUDE.md monolitico.',\n savings_estimate: '~15k tokens/sesion con progressive disclosure',\n savings_source: 'community-measured',\n how_to_invoke: 'Skill tool con nombre del skill',\n when_applicable: 'Tareas que matchean un skill disponible',\n source_type: 'skill',\n verified_at: '2026-04-11',\n detector_id: 'detect-skill-trigger-ignored',\n },\n {\n id: 'install-serena',\n title: 'Instala serena-mcp para lecturas simbolicas',\n description:\n 'serena usa LSP para leer solo los simbolos que necesitas en lugar del archivo completo. Nota: incluye execute_shell_command.',\n savings_estimate: '20-30% en lecturas de archivos grandes',\n savings_source: 'community-measured',\n how_to_invoke: 'uvx --from git+https://github.com/oraios/serena serena start-mcp-server',\n when_applicable: 'Proyectos con archivos >50k tokens',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: 'detect-huge-file-reads',\n },\n {\n id: 'prefer-serena-reads',\n title: 'Usa Serena en vez de Read para archivos de codigo',\n description:\n 'Serena lee simbolos (funciones, clases) sin cargar el archivo completo. Usa get_symbols_overview para explorar y find_symbol con include_body para leer solo lo que necesitas. Ahorro tipico: 60-90% vs Read.',\n savings_estimate: '60-90% en lecturas de codigo',\n savings_source: 'internal',\n how_to_invoke: 'get_symbols_overview(path) → find_symbol(name, include_body=true)',\n when_applicable: 'Archivos .ts/.js/.py/.java >50 lineas donde solo necesitas 1-2 funciones',\n source_type: 'mcp',\n verified_at: '2026-04-12',\n detector_id: 'detect-read-over-serena',\n },\n {\n id: 'install-rtk',\n title: 'Instala RTK para filtrar salida ruidosa de Bash',\n description:\n 'RTK filtra output de builds/tests antes de llegar a Claude Code. Publica releases firmadas con GPG.',\n savings_estimate: '15-25% en ciclos build/test',\n savings_source: 'community-measured',\n how_to_invoke: 'brew install standard-input/tap/rtk (macOS) o binario firmado en github.com/standard-input/rtk',\n when_applicable: 'Proyectos con builds/tests ruidosos',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: 'detect-many-bash-commands',\n },\n {\n id: 'use-mcp-prune',\n title: 'Aplica un allowlist de MCPs por proyecto',\n description:\n 'Reduce el coste del tool-schema excluyendo MCPs que no usas en este proyecto. ~5-12% adicional sobre Tool Search.',\n savings_estimate: '5-12% por turno sobre Tool Search nativo',\n savings_source: 'internal',\n how_to_invoke: 'mcp_prune_suggest → mcp_prune_apply',\n when_applicable: 'MCPs registrados pero no usados en el proyecto',\n source_type: 'mcp',\n verified_at: '2026-04-11',\n detector_id: 'detect-unused-mcp-servers',\n },\n {\n id: 'migrate-claudemd-to-skills',\n title: 'Migra CLAUDE.md grande a skills con progressive disclosure',\n description:\n 'Un CLAUDE.md monolitico se carga en cada sesion. Los skills solo cargan cuando se invocan. ~15k tokens recuperados.',\n savings_estimate: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n savings_source: 'community-measured',\n how_to_invoke: 'Crear skills en .claude/skills/ con triggers especificos',\n when_applicable: 'CLAUDE.md > 10k tokens con uso parcial',\n source_type: 'skill',\n verified_at: '2026-04-11',\n detector_id: 'detect-claudemd-bloat',\n },\n {\n id: 'use-settings-local',\n title: 'Configuracion personal en settings.local.json',\n description:\n 'Evita contaminar settings.json del equipo. settings.local.json es personal y gitignored por defecto.',\n savings_estimate: 'Higiene, no tokens',\n savings_source: 'internal',\n how_to_invoke: 'Editar .claude/settings.local.json',\n when_applicable: 'Configuracion personal no compartible',\n source_type: 'settings',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n {\n id: 'use-serena-overview-first',\n title: 'Usa get_symbols_overview antes de find_symbol',\n description:\n 'Llamar get_symbols_overview una vez da el mapa del archivo. Las llamadas sucesivas find_symbol sin overview previo leen el mismo archivo repetidamente.',\n savings_estimate: '30-50% menos llamadas Serena por sesion',\n savings_source: 'internal',\n how_to_invoke: 'mcp__serena__get_symbols_overview con relative_path antes de find_symbol',\n when_applicable: 'Al explorar un archivo por primera vez en la sesion',\n source_type: 'mcp',\n verified_at: '2026-04-15',\n detector_id: 'detect-serena-read-cascade',\n },\n {\n id: 'use-prompt-caching',\n title: 'Estructura prompts para maximizar cache hits',\n description:\n 'Los tokens leidos del cache cuestan 10x menos. Mantener el prefijo estable (system, CLAUDE.md) aprovecha el cache.',\n savings_estimate: '10x mas barato en reads cacheados',\n savings_source: 'anthropic-docs',\n how_to_invoke: 'Mantener prefijo estable entre turns',\n when_applicable: 'Siempre',\n source_type: 'built-in',\n verified_at: '2026-04-11',\n detector_id: null,\n },\n]\n","// Detection rules registry (11 rules) — Phase 4.43\n// Each rule is a pure function over EventContext returning DetectionHit | null.\n// Rules MUST NOT throw; the orchestrator catches everything.\n\nimport type { DetectionRule, DetectionSeverity, ToolEvent } from '../lib/types.js'\n\nfunction countMatching(events: readonly ToolEvent[], predicate: (e: ToolEvent) => boolean): number {\n let c = 0\n for (const e of events) if (predicate(e)) c++\n return c\n}\n\nconst EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])\n\nexport const DETECTION_RULES: readonly DetectionRule[] = [\n // 1. detect-context-threshold\n {\n id: 'detect-context-threshold',\n tip_ids: ['use-compact-long-session'],\n run(ctx) {\n if (ctx.session_token_total === null || ctx.session_token_limit <= 0) return null\n const percent = ctx.session_token_total / ctx.session_token_limit\n if (percent < 0.5) return null\n let severity: DetectionSeverity = 'info'\n if (percent >= 0.9) severity = 'critical'\n else if (percent >= 0.75) severity = 'warn'\n return {\n rule_id: 'detect-context-threshold',\n tip_ids: ['use-compact-long-session'],\n severity,\n evidence: `Contexto: ${(percent * 100).toFixed(1)}% usado (${ctx.session_token_total}/${ctx.session_token_limit} tokens)`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n\n // 2. detect-long-reasoning-no-code\n {\n id: 'detect-long-reasoning-no-code',\n tip_ids: ['use-plan-mode', 'use-opusplan'],\n run(ctx) {\n const recent = ctx.events.slice(0, 10)\n if (recent.length < 10) return null\n const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name))\n if (edits > 0) return null\n return {\n rule_id: 'detect-long-reasoning-no-code',\n tip_ids: ['use-plan-mode', 'use-opusplan'],\n severity: 'info',\n evidence: '10 eventos recientes sin ediciones de codigo',\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 3. detect-repeated-searches\n {\n id: 'detect-repeated-searches',\n tip_ids: ['use-agent-explore'],\n run(ctx) {\n const window = ctx.events.slice(0, 20)\n const searches = countMatching(window, (e) => e.tool_name === 'Grep' || e.tool_name === 'Glob')\n if (searches < 3) return null\n return {\n rule_id: 'detect-repeated-searches',\n tip_ids: ['use-agent-explore'],\n severity: 'info',\n evidence: `${searches} busquedas Grep/Glob en los ultimos 20 eventos`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 4. detect-huge-file-reads\n {\n id: 'detect-huge-file-reads',\n tip_ids: ['install-serena'],\n run(ctx) {\n const huge = ctx.events.find((e) => e.tool_name === 'Read' && e.tokens_estimated > 50_000)\n if (!huge) return null\n return {\n rule_id: 'detect-huge-file-reads',\n tip_ids: ['install-serena'],\n severity: 'warn',\n evidence: `Read consumio ${huge.tokens_estimated} tokens (umbral 50k)`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 5. detect-many-bash-commands\n {\n id: 'detect-many-bash-commands',\n tip_ids: ['install-rtk'],\n run(ctx) {\n const window = ctx.events.slice(0, 100)\n const bash = countMatching(window, (e) => e.tool_name === 'Bash')\n if (bash <= 10) return null\n return {\n rule_id: 'detect-many-bash-commands',\n tip_ids: ['install-rtk'],\n severity: 'info',\n evidence: `${bash} comandos Bash en los ultimos ${window.length} eventos`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 6. detect-clear-opportunity (was #7 — detect-unused-mcp-servers stub removed)\n {\n id: 'detect-clear-opportunity',\n tip_ids: ['use-clear-rename-resume'],\n run(ctx) {\n if (ctx.events.length < 40) return null\n const recentTools = new Set(ctx.events.slice(0, 20).map((e) => e.tool_name))\n const priorTools = new Set(ctx.events.slice(20, 40).map((e) => e.tool_name))\n if (recentTools.size === 0) return null\n let overlap = 0\n for (const t of recentTools) if (priorTools.has(t)) overlap++\n const ratio = overlap / recentTools.size\n if (ratio >= 0.3) return null\n return {\n rule_id: 'detect-clear-opportunity',\n tip_ids: ['use-clear-rename-resume'],\n severity: 'info',\n evidence: `Solapamiento de herramientas ${(ratio * 100).toFixed(0)}% — posible pivote de tema`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 8. detect-opus-for-simple-task\n {\n id: 'detect-opus-for-simple-task',\n tip_ids: ['default-to-sonnet', 'use-haiku-for-simple'],\n run(ctx) {\n if (!ctx.active_model || !/opus/i.test(ctx.active_model)) return null\n const recent = ctx.events.slice(0, 20)\n if (recent.length < 6) return null\n const edits = countMatching(recent, (e) => EDIT_TOOLS.has(e.tool_name))\n const bash = countMatching(recent, (e) => e.tool_name === 'Bash')\n // Opus es correcto para planificar/preguntar — solo avisar cuando está ejecutando código\n if (edits + bash < 6) return null\n return {\n rule_id: 'detect-opus-for-simple-task',\n tip_ids: ['default-to-sonnet', 'use-haiku-for-simple'],\n severity: 'info',\n evidence: `Opus ejecutando trabajo mecanico: ${edits} edits + ${bash} Bash en ultimos 20 eventos. Sonnet haria lo mismo un 80% mas barato.`,\n estimation_method: 'measured_exact',\n }\n },\n },\n\n // 9. detect-claudemd-bloat (stub — requires filesystem stat at runtime)\n {\n id: 'detect-claudemd-bloat',\n tip_ids: ['migrate-claudemd-to-skills'],\n run() {\n return null\n },\n },\n\n // 10. detect-post-milestone-opportunity\n {\n id: 'detect-post-milestone-opportunity',\n tip_ids: ['use-compact-long-session'],\n run(ctx) {\n const recent = ctx.events.slice(0, 20)\n const edits = countMatching(recent, (e) => e.tool_name === 'Edit' || e.tool_name === 'Write')\n const hasBash = countMatching(recent, (e) => e.tool_name === 'Bash') > 0\n if (edits < 5 || !hasBash) return null\n if (ctx.session_token_total === null) return null\n const percent = ctx.session_token_total / ctx.session_token_limit\n if (percent < 0.4) return null\n return {\n rule_id: 'detect-post-milestone-opportunity',\n tip_ids: ['use-compact-long-session'],\n severity: 'info',\n evidence: `${edits} ediciones + Bash reciente + contexto ${(percent * 100).toFixed(0)}%`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n\n // 11. detect-skill-trigger-ignored (stub — requires skill registry)\n {\n id: 'detect-skill-trigger-ignored',\n tip_ids: ['use-skill-trigger'],\n run() {\n return null\n },\n },\n\n // 12. detect-serena-read-cascade\n // Fires when the agent makes ≥5 find_symbol calls without a get_symbols_overview\n // in the same window — suggests starting with an overview first.\n {\n id: 'detect-serena-read-cascade',\n tip_ids: ['use-serena-overview-first'],\n run(ctx) {\n const window = ctx.events.slice(0, 15)\n const findSymbolCount = countMatching(\n window,\n (e) => e.tool_name === 'mcp__serena__find_symbol',\n )\n if (findSymbolCount < 5) return null\n const hasOverview = window.some(\n (e) => e.tool_name === 'mcp__serena__get_symbols_overview',\n )\n if (hasOverview) return null\n return {\n rule_id: 'detect-serena-read-cascade',\n tip_ids: ['use-serena-overview-first'],\n severity: 'info' as DetectionSeverity,\n evidence: `${findSymbolCount} llamadas find_symbol sin get_symbols_overview en los ultimos 15 eventos.`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n\n // 13. detect-read-over-serena\n {\n id: 'detect-read-over-serena',\n tip_ids: ['prefer-serena-reads'],\n run(ctx) {\n const window = ctx.events.slice(0, 30)\n const largeReads = window.filter(\n (e) => e.tool_name === 'Read' && e.tokens_estimated > 2_000,\n )\n if (largeReads.length < 3) return null\n const totalTokens = largeReads.reduce((sum, e) => sum + e.tokens_estimated, 0)\n const estimatedSaving = Math.round(totalTokens * 0.7)\n const severity: DetectionSeverity = largeReads.length >= 6 ? 'warn' : 'info'\n return {\n rule_id: 'detect-read-over-serena',\n tip_ids: ['prefer-serena-reads'],\n severity,\n evidence: `${largeReads.length} lecturas Read >2k tokens (total: ${totalTokens}). Serena ahorraria ~${estimatedSaving} tokens (~70%).`,\n estimation_method: ctx.session_token_method,\n }\n },\n },\n]\n","// Rules orchestrator — Phase 4.44\n// Runs all detection rules, dedupes by (rule_id, tip_id), sorts by severity desc.\n\nimport type { DetectionHit, EventContext } from '../lib/types.js'\nimport { DETECTION_RULES } from './rules.js'\n\nconst SEVERITY_ORDER: Record<string, number> = { critical: 0, warn: 1, info: 2 }\n\nexport function runRules(ctx: EventContext): DetectionHit[] {\n const hits: DetectionHit[] = []\n for (const rule of DETECTION_RULES) {\n try {\n const hit = rule.run(ctx)\n if (hit) hits.push(hit)\n } catch {\n // swallow — rules must never crash the caller\n }\n }\n // Dedupe by rule_id\n const seen = new Set<string>()\n const unique: DetectionHit[] = []\n for (const h of hits) {\n if (seen.has(h.rule_id)) continue\n seen.add(h.rule_id)\n unique.push(h)\n }\n unique.sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99))\n return unique\n}\n","// Context size meter with 3-source fallback — Phase 4.42\n// (1) transcript JSONL → (2) xray HTTP → (3) cumulative DB estimate\n\nimport fs from 'node:fs'\nimport type Database from 'better-sqlite3'\nimport type { ContextMeasurement, EstimationMethod } from '../lib/types.js'\nimport { resolveTranscriptPath } from '../lib/paths.js'\nimport { buildQueries } from '../db/queries.js'\nimport { getSessionTokens } from '../services/xray-client.js'\n\ntype DB = Database.Database\n\nconst DEFAULT_LIMIT = 200_000\nconst OPUS_1M_LIMIT = 1_000_000\nconst BASELINE_TOKENS = 15_000\n\nexport interface ContextMeterOptions {\n projectDir?: string\n db?: DB\n fetchImpl?: typeof fetch\n activeModel?: string\n}\n\nexport async function measureContextSize(\n sessionId: string,\n opts: ContextMeterOptions = {},\n): Promise<ContextMeasurement> {\n // Strategy 1: transcript JSONL (measured_exact)\n if (opts.projectDir) {\n const transcript = readTranscript(opts.projectDir, sessionId)\n if (transcript) return transcript\n }\n\n // Strategy 2: xray HTTP (measured_exact)\n const xrayResult = await tryXray(sessionId, opts.fetchImpl)\n if (xrayResult) return xrayResult\n\n // Strategy 3: cumulative estimate from our DB (estimated_cumulative)\n const limit = resolveLimit(opts.activeModel)\n if (opts.db) {\n return cumulativeEstimate(opts.db, sessionId, limit)\n }\n\n return { tokens: 0, limit, percent: 0, estimation_method: 'unknown' }\n}\n\nfunction readTranscript(projectDir: string, sessionId: string): ContextMeasurement | null {\n try {\n const p = resolveTranscriptPath(projectDir, sessionId)\n if (!fs.existsSync(p)) return null\n const content = fs.readFileSync(p, 'utf8')\n const lines = content.split('\\n').filter((l) => l.trim().length > 0)\n let totalTokens = 0\n let limit = DEFAULT_LIMIT\n for (const line of lines) {\n try {\n const turn = JSON.parse(line) as {\n usage?: {\n input_tokens?: number\n output_tokens?: number\n cache_read_input_tokens?: number\n }\n model?: string\n }\n if (turn.usage) {\n totalTokens +=\n (turn.usage.input_tokens ?? 0) +\n (turn.usage.output_tokens ?? 0) +\n (turn.usage.cache_read_input_tokens ?? 0)\n }\n if (turn.model && /1m/i.test(turn.model)) limit = OPUS_1M_LIMIT\n } catch {\n // skip unparseable line\n }\n }\n return {\n tokens: totalTokens,\n limit,\n percent: limit > 0 ? totalTokens / limit : 0,\n estimation_method: 'measured_exact' as EstimationMethod,\n }\n } catch {\n return null\n }\n}\n\nasync function tryXray(\n sessionId: string,\n fetchImpl?: typeof fetch,\n): Promise<ContextMeasurement | null> {\n const opts: Parameters<typeof getSessionTokens>[1] = {}\n if (fetchImpl !== undefined) opts.fetchImpl = fetchImpl\n return getSessionTokens(sessionId, opts)\n}\n\nfunction resolveLimit(activeModel?: string): number {\n if (activeModel && /1m|opus/i.test(activeModel)) return OPUS_1M_LIMIT\n return DEFAULT_LIMIT\n}\n\nfunction cumulativeEstimate(\n db: DB,\n sessionId: string,\n limit: number = DEFAULT_LIMIT,\n): ContextMeasurement {\n const queries = buildQueries(db)\n const sessionTokens = queries.sumTokensBySession(sessionId)\n const total = sessionTokens + BASELINE_TOKENS\n return {\n tokens: total,\n limit,\n percent: total / limit,\n estimation_method: 'estimated_cumulative',\n }\n}\n\n/**\n * Synchronous DB-only context measurement for hot paths (PostToolUse).\n * Skips transcript + xray strategies to stay under the 5ms budget. The\n * estimation_method returned ('estimated_cumulative') is surfaced verbatim\n * in tips so the agent knows this is a fast approximation.\n */\nexport function measureContextSizeFromDbSync(\n db: DB,\n sessionId: string,\n activeModel?: string,\n): ContextMeasurement {\n const limit = resolveLimit(activeModel)\n return cumulativeEstimate(db, sessionId, limit)\n}\n","// xray client — Phase 5.1\n// Fire-and-forget POST for tool events + GET for session tokens (used by coach context meter).\n// Silent on all failures: no stderr, no throw. Timeout 500ms for post, 300ms for get.\n\nimport type { ContextMeasurement } from '../lib/types.js'\nimport { resolveXrayUrl } from '../cli/config.js'\n\nconst POST_TIMEOUT_MS = 500\nconst GET_TIMEOUT_MS = 300\nconst DEFAULT_LIMIT = 200_000\n\ndeclare const __PKG_VERSION__: string\nconst VERSION = typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : '0.1.0'\n\nexport interface PostToXrayOptions {\n xrayUrl?: string\n fetchImpl?: typeof fetch\n timeoutMs?: number\n}\n\n/**\n * Fire-and-forget POST of a tool event to an xray server.\n * Returns true if the request was attempted, false if skipped (no URL).\n * Any network/parse error is swallowed silently.\n */\nexport async function postToXray(\n event: Record<string, unknown>,\n opts: PostToXrayOptions = {},\n): Promise<boolean> {\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\n if (!xrayUrl) return false\n const fetchFn = opts.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? POST_TIMEOUT_MS)\n try {\n await fetchFn(`${xrayUrl}/hooks/token-optimizer`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n source: 'token-optimizer-mcp',\n version: VERSION,\n event,\n }),\n signal: controller.signal,\n })\n return true\n } catch {\n return false\n } finally {\n clearTimeout(timer)\n }\n}\n\nconst SUMMARY_TIMEOUT_MS = 2000\n\n/**\n * Fire-and-forget POST of session summary to xray.\n * Only called once per session (not in hot path), so allows longer timeout.\n */\nexport async function postSummaryToXray(\n summary: Record<string, unknown>,\n opts: PostToXrayOptions = {},\n): Promise<boolean> {\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\n if (!xrayUrl) return false\n const fetchFn = opts.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? SUMMARY_TIMEOUT_MS)\n try {\n await fetchFn(`${xrayUrl}/hooks/token-optimizer/summary`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n source: 'token-optimizer-mcp',\n version: VERSION,\n summary,\n }),\n signal: controller.signal,\n })\n return true\n } catch {\n return false\n } finally {\n clearTimeout(timer)\n }\n}\n\nexport interface GetSessionTokensOptions {\n xrayUrl?: string\n fetchImpl?: typeof fetch\n timeoutMs?: number\n}\n\n/**\n * Read real token counts from xray for a given session.\n * Returns null on any failure or when XRAY_URL is unset.\n */\nexport async function getSessionTokens(\n sessionId: string,\n opts: GetSessionTokensOptions = {},\n): Promise<ContextMeasurement | null> {\n const xrayUrl = opts.xrayUrl ?? resolveXrayUrl()\n if (!xrayUrl) return null\n const fetchFn = opts.fetchImpl ?? fetch\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? GET_TIMEOUT_MS)\n try {\n const res = await fetchFn(\n `${xrayUrl}/sessions/${encodeURIComponent(sessionId)}/tokens`,\n { signal: controller.signal },\n )\n if (!res.ok) return null\n const data = (await res.json()) as { tokens?: number; limit?: number }\n const tokens = data.tokens ?? 0\n const limit = data.limit ?? DEFAULT_LIMIT\n return {\n tokens,\n limit,\n percent: limit > 0 ? tokens / limit : 0,\n estimation_method: 'measured_exact',\n }\n } catch {\n return null\n } finally {\n clearTimeout(timer)\n }\n}\n","// Surfacing dedupe + log writer — Phase 4.45\n// Writes to coach_surface_log with session+rule+tip+via+severity.\n\nimport type Database from 'better-sqlite3'\nimport type { DetectionHit } from '../lib/types.js'\n\ntype DB = Database.Database\n\nexport type SurfacedVia = 'sessionstart' | 'posttooluse' | 'mcp' | 'cli'\n\n/**\n * Returns true if this (session, rule, tip) was surfaced within the last\n * `windowSeconds` seconds. Used by the PostToolUse throttle path.\n */\nexport function checkDedupe(\n db: DB,\n sessionId: string,\n ruleId: string,\n tipId: string,\n windowSeconds: number,\n): boolean {\n const row = db\n .prepare(\n `SELECT 1 FROM coach_surface_log\n WHERE session_id = ? AND rule_id = ? AND tip_id = ?\n AND created_at > datetime('now', ?)\n LIMIT 1`,\n )\n .get(sessionId, ruleId, tipId, `-${windowSeconds} seconds`) as unknown\n return row !== undefined && row !== null\n}\n\nexport function logSurface(\n db: DB,\n sessionId: string,\n hit: DetectionHit,\n via: SurfacedVia,\n): void {\n // Ensure session exists so FK succeeds\n db.prepare(`INSERT OR IGNORE INTO sessions (id) VALUES (?)`).run(sessionId)\n const stmt = db.prepare(\n `INSERT INTO coach_surface_log (session_id, rule_id, tip_id, surfaced_via, severity)\n VALUES (?, ?, ?, ?, ?)`,\n )\n for (const tipId of hit.tip_ids) {\n stmt.run(sessionId, hit.rule_id, tipId, via, hit.severity)\n }\n}\n\n/**\n * Log the list of hits under dedupe. A hit is considered \"fresh\" (to be\n * logged and returned to the caller) if at least one of its tip_ids was NOT\n * surfaced within `windowSeconds`. Returns only the surfaced hits.\n */\nexport function surfaceWithDedupe(\n db: DB,\n sessionId: string,\n hits: DetectionHit[],\n via: SurfacedVia,\n windowSeconds: number,\n): DetectionHit[] {\n const surfaced: DetectionHit[] = []\n for (const hit of hits) {\n const anyFresh = hit.tip_ids.some(\n (tipId) => !checkDedupe(db, sessionId, hit.rule_id, tipId, windowSeconds),\n )\n if (anyFresh) {\n logSurface(db, sessionId, hit, via)\n surfaced.push(hit)\n }\n }\n return surfaced\n}\n\n/**\n * Read all coach tips surfaced during a session, grouped by rule.\n * Used by session-summary-builder for xray integration.\n */\nexport function getCoachSurfaceLog(\n db: DB,\n sessionId: string,\n): Array<{ rule_id: string; tip_ids: string[]; severity: string }> {\n const rows = db\n .prepare(\n `SELECT rule_id, tip_id, severity\n FROM coach_surface_log\n WHERE session_id = ?\n ORDER BY created_at`,\n )\n .all(sessionId) as Array<{ rule_id: string; tip_id: string; severity: string }>\n\n // Group tip_ids by rule_id\n const map = new Map<string, { tip_ids: string[]; severity: string }>()\n for (const row of rows) {\n if (!map.has(row.rule_id)) {\n map.set(row.rule_id, { tip_ids: [], severity: row.severity })\n }\n const entry = map.get(row.rule_id)!\n if (!entry.tip_ids.includes(row.tip_id)) {\n entry.tip_ids.push(row.tip_id)\n }\n }\n\n return Array.from(map.entries()).map(([rule_id, v]) => ({\n rule_id,\n tip_ids: v.tip_ids,\n severity: v.severity,\n }))\n}\n\nexport function clearSurfaceLog(db: DB, sessionId?: string): number {\n if (sessionId) {\n const info = db.prepare(`DELETE FROM coach_surface_log WHERE session_id = ?`).run(sessionId)\n return info.changes as number\n }\n const info = db.prepare(`DELETE FROM coach_surface_log`).run()\n return info.changes as number\n}\n"],"mappings":"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","names":["DEFAULT_LIMIT","info"]} |
| #!/usr/bin/env node | ||
| import { | ||
| resolveGlobalDir | ||
| } from "./chunk-V4PINTCV.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-KNYWGCEX.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/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 resolveStorageDir(projectDir) { | ||
| return path.join(projectDir, ".token-optimizer"); | ||
| } | ||
| 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, | ||
| resolveStorageDir, | ||
| resolveAnalyticsDbPath, | ||
| resolveGlobalDir, | ||
| projectHash, | ||
| resolveTranscriptPath | ||
| }; | ||
| //# sourceMappingURL=chunk-V4PINTCV.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\nexport function resolveStorageDir(projectDir: string): string {\n return path.join(projectDir, '.token-optimizer')\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;AAEO,SAAS,kBAAkB,YAA4B;AAC5D,SAAO,KAAK,KAAK,YAAY,kBAAkB;AACjD;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 { | ||
| resolveStorageDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/lib/storage.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| var GITIGNORE_ENTRIES = [".token-optimizer/", ".serena/"]; | ||
| function ensureGitignoreEntries(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")} | ||
| `); | ||
| } | ||
| function ensureStorageDir(projectDir) { | ||
| const storageDir = resolveStorageDir(projectDir); | ||
| if (!fs.existsSync(storageDir)) { | ||
| fs.mkdirSync(storageDir, { recursive: true }); | ||
| } | ||
| ensureGitignoreEntries(projectDir); | ||
| return storageDir; | ||
| } | ||
| export { | ||
| ensureStorageDir | ||
| }; | ||
| //# sourceMappingURL=chunk-VHP52B3J.js.map |
| {"version":3,"sources":["../src/lib/storage.ts"],"sourcesContent":["// Storage dir initialization — Phase 1.5\n// Creates .token-optimizer/ and appends to .gitignore idempotently\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { resolveStorageDir } from './paths.js'\n\nconst GITIGNORE_ENTRIES = ['.token-optimizer/', '.serena/']\n\nfunction ensureGitignoreEntries(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\nexport function ensureStorageDir(projectDir: string): string {\n const storageDir = resolveStorageDir(projectDir)\n if (!fs.existsSync(storageDir)) {\n fs.mkdirSync(storageDir, { recursive: true })\n }\n ensureGitignoreEntries(projectDir)\n return storageDir\n}\n"],"mappings":";;;;;;AAGA,OAAO,QAAQ;AACf,OAAO,UAAU;AAGjB,IAAM,oBAAoB,CAAC,qBAAqB,UAAU;AAE1D,SAAS,uBAAuB,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;AAEO,SAAS,iBAAiB,YAA4B;AAC3D,QAAM,aAAa,kBAAkB,UAAU;AAC/C,MAAI,CAAC,GAAG,WAAW,UAAU,GAAG;AAC9B,OAAG,UAAU,YAAY,EAAE,WAAW,KAAK,CAAC;AAAA,EAC9C;AACA,yBAAuB,UAAU;AACjC,SAAO;AACT;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| clearSurfaceLog, | ||
| measureContextSize, | ||
| runRules | ||
| } from "./chunk-HZUEH22U.js"; | ||
| import "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| buildQueries | ||
| } from "./chunk-FNCW6SLR.js"; | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/coach.ts | ||
| import fs from "fs"; | ||
| async function runCoachCli(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const sub = args[0] ?? "status"; | ||
| if (sub === "list") { | ||
| print(`Knowledge base (${KNOWLEDGE_BASE.length} tips):`); | ||
| for (const tip of KNOWLEDGE_BASE) { | ||
| print(` \u2022 ${tip.id.padEnd(32)} ${tip.title}`); | ||
| } | ||
| return 0; | ||
| } | ||
| if (sub === "explain") { | ||
| const tipId = args[1]; | ||
| if (!tipId) { | ||
| print("Uso: token-optimizer-mcp coach explain <tip_id>"); | ||
| return 1; | ||
| } | ||
| const tip = KNOWLEDGE_BASE.find((t) => t.id === tipId); | ||
| if (!tip) { | ||
| print(`Tip no encontrado: ${tipId}`); | ||
| return 1; | ||
| } | ||
| print(tip.title); | ||
| print(""); | ||
| print(tip.description); | ||
| print(""); | ||
| print(`Como usarlo: ${tip.how_to_invoke}`); | ||
| print(`Cuando: ${tip.when_applicable}`); | ||
| print(`Ahorro: ${tip.savings_estimate}`); | ||
| print(`Fuente: ${tip.savings_source} \xB7 verificado: ${tip.verified_at}`); | ||
| return 0; | ||
| } | ||
| if (sub === "reset") { | ||
| const projectDir2 = resolveProjectDir(cwd); | ||
| const dbPath2 = resolveAnalyticsDbPath(projectDir2); | ||
| if (!fs.existsSync(dbPath2)) { | ||
| print("Sin DB; nada que resetear."); | ||
| return 0; | ||
| } | ||
| const db2 = getDb(dbPath2); | ||
| const deleted = clearSurfaceLog(db2); | ||
| print(`Log de coach reseteado (${deleted} entradas eliminadas)`); | ||
| return 0; | ||
| } | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| if (!fs.existsSync(dbPath)) { | ||
| print("Coach status: sin datos. Ejecuta el hook posttooluse al menos una vez."); | ||
| return 0; | ||
| } | ||
| const db = getDb(dbPath); | ||
| const contextOpts = { db }; | ||
| if (projectDir) contextOpts.projectDir = projectDir; | ||
| const context = await measureContextSize("default", contextOpts); | ||
| const queries = buildQueries(db); | ||
| const since = new Date(Date.now() - 864e5).toISOString(); | ||
| const rawRows = queries.getToolCallsSince(since); | ||
| const ctx = { | ||
| session_id: "default", | ||
| events: rawRows.slice(0, 100), | ||
| session_token_total: context.tokens, | ||
| session_token_method: context.estimation_method, | ||
| session_token_limit: context.limit, | ||
| active_model: null | ||
| }; | ||
| const hits = runRules(ctx); | ||
| print("token-optimizer-mcp coach status"); | ||
| print(""); | ||
| print( | ||
| `Contexto: ${(context.percent * 100).toFixed(1)}% (${context.tokens}/${context.limit} tokens, ${context.estimation_method})` | ||
| ); | ||
| print(`Tips activos: ${hits.length}`); | ||
| if (hits.length === 0) { | ||
| print(" (sin tips disparados en este momento)"); | ||
| } else { | ||
| for (const h of hits) { | ||
| print(` [${h.severity}] ${h.rule_id}: ${h.evidence}`); | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
| export { | ||
| runCoachCli | ||
| }; | ||
| //# sourceMappingURL=coach-27KAY3MK.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-KNYWGCEX.js"; | ||
| import "./chunk-V4PINTCV.js"; | ||
| export { | ||
| DEFAULT_CONFIG, | ||
| getConfigPath, | ||
| loadConfig, | ||
| resolveXrayUrl, | ||
| runConfigCommand, | ||
| saveConfig | ||
| }; | ||
| //# sourceMappingURL=config-ETWSZHVJ.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-22M5OKPL.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-PGH4NXTW.js"); | ||
| return mod.runStatus(rest); | ||
| } | ||
| case "report": { | ||
| const mod = await import("./report-JX5CQ6FO.js"); | ||
| return mod.runReport(rest); | ||
| } | ||
| case "budget": { | ||
| const mod = await import("./budget-BC6HFBNA.js"); | ||
| return mod.runBudgetCli(rest); | ||
| } | ||
| case "config": { | ||
| const mod = await import("./config-ETWSZHVJ.js"); | ||
| return mod.runConfigCommand(rest); | ||
| } | ||
| case "prune-mcp": { | ||
| const mod = await import("./prune-mcp-TGHEFZ3X.js"); | ||
| return mod.runPruneMcp(rest); | ||
| } | ||
| case "coach": { | ||
| const mod = await import("./coach-27KAY3MK.js"); | ||
| return mod.runCoachCli(rest); | ||
| } | ||
| case "sync-xray": { | ||
| const mod = await import("./sync-xray-U6ORS5MP.js"); | ||
| return mod.runSyncXray(rest); | ||
| } | ||
| default: | ||
| console.error(`Subcomando desconocido: ${sub ?? "(ninguno)"}`); | ||
| console.error( | ||
| "Disponibles: install, uninstall, doctor, status, report, budget, prune-mcp, coach, config, sync-xray" | ||
| ); | ||
| return 1; | ||
| } | ||
| } | ||
| export { | ||
| dispatchCli | ||
| }; | ||
| //# sourceMappingURL=dispatcher-PKJLDZ2H.js.map |
| {"version":3,"sources":["../src/cli/dispatcher.ts"],"sourcesContent":["// CLI subcommand dispatcher — Phase 4.9\n// Routes argv[0] to the appropriate subcommand module (lazy-imported).\n\nexport async function dispatchCli(argv: string[]): Promise<number> {\n const [sub, ...rest] = argv\n switch (sub) {\n case 'install': {\n const mod = await import('./install.js')\n return mod.runInstall(rest)\n }\n case 'uninstall': {\n const mod = await import('./uninstall.js')\n return mod.runUninstall(rest)\n }\n case 'doctor': {\n const mod = await import('./doctor.js')\n return mod.runDoctor(rest)\n }\n case 'status': {\n const mod = await import('./status.js')\n return mod.runStatus(rest)\n }\n case 'report': {\n const mod = await import('./report.js')\n return mod.runReport(rest)\n }\n case 'budget': {\n const mod = await import('./budget.js')\n return mod.runBudgetCli(rest)\n }\n case 'config': {\n const mod = await import('./config.js')\n return mod.runConfigCommand(rest)\n }\n case 'prune-mcp': {\n const mod = await import('./prune-mcp.js')\n return mod.runPruneMcp(rest)\n }\n case 'coach': {\n const mod = await import('./coach.js')\n return mod.runCoachCli(rest)\n }\n case 'sync-xray': {\n const mod = await import('./sync-xray.js')\n return mod.runSyncXray(rest)\n }\n default:\n console.error(`Subcomando desconocido: ${sub ?? '(ninguno)'}`)\n console.error(\n 'Disponibles: install, uninstall, doctor, status, report, budget, prune-mcp, coach, config, sync-xray',\n )\n return 1\n }\n}\n"],"mappings":";;;AAGA,eAAsB,YAAY,MAAiC;AACjE,QAAM,CAAC,KAAK,GAAG,IAAI,IAAI;AACvB,UAAQ,KAAK;AAAA,IACX,KAAK,WAAW;AACd,YAAM,MAAM,MAAM,OAAO,uBAAc;AACvC,aAAO,IAAI,WAAW,IAAI;AAAA,IAC5B;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,MAAM,MAAM,OAAO,yBAAgB;AACzC,aAAO,IAAI,aAAa,IAAI;AAAA,IAC9B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,UAAU,IAAI;AAAA,IAC3B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,aAAa,IAAI;AAAA,IAC9B;AAAA,IACA,KAAK,UAAU;AACb,YAAM,MAAM,MAAM,OAAO,sBAAa;AACtC,aAAO,IAAI,iBAAiB,IAAI;AAAA,IAClC;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,MAAM,MAAM,OAAO,yBAAgB;AACzC,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA,KAAK,SAAS;AACZ,YAAM,MAAM,MAAM,OAAO,qBAAY;AACrC,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA,KAAK,aAAa;AAChB,YAAM,MAAM,MAAM,OAAO,yBAAgB;AACzC,aAAO,IAAI,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA;AACE,cAAQ,MAAM,2BAA2B,OAAO,WAAW,EAAE;AAC7D,cAAQ;AAAA,QACN;AAAA,MACF;AACA,aAAO;AAAA,EACX;AACF;","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| probeSerenaPresence | ||
| } from "./chunk-2NKYFIPW.js"; | ||
| import { | ||
| ensureStorageDir | ||
| } from "./chunk-VHP52B3J.js"; | ||
| import { | ||
| getConfigPath, | ||
| loadConfig, | ||
| saveConfig | ||
| } from "./chunk-KNYWGCEX.js"; | ||
| import { | ||
| runDoctor | ||
| } from "./chunk-EEZSSD5Q.js"; | ||
| import "./chunk-XTFQTQMU.js"; | ||
| import "./chunk-DOYJNIB2.js"; | ||
| import "./chunk-L5Z32XXL.js"; | ||
| import "./chunk-V4PINTCV.js"; | ||
| // src/cli/install.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| import { spawnSync } from "child_process"; | ||
| var SERVER_NAME = "token-optimizer"; | ||
| function resolveHookCommandBase() { | ||
| try { | ||
| const globalRoot = path.join(os.homedir(), "AppData", "Roaming", "npm", "node_modules"); | ||
| const indexPath = path.join(globalRoot, "@cocaxcode", "token-optimizer-mcp", "dist", "index.js"); | ||
| if (fs.existsSync(indexPath)) { | ||
| return `node "${indexPath.replace(/\\/g, "/")}"`; | ||
| } | ||
| } catch { | ||
| } | ||
| const unixPaths = [ | ||
| "/usr/local/lib/node_modules", | ||
| "/usr/lib/node_modules", | ||
| path.join(os.homedir(), ".npm-global", "lib", "node_modules") | ||
| ]; | ||
| for (const root of unixPaths) { | ||
| try { | ||
| const indexPath = path.join(root, "@cocaxcode", "token-optimizer-mcp", "dist", "index.js"); | ||
| if (fs.existsSync(indexPath)) { | ||
| return `node "${indexPath}"`; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| try { | ||
| const result = spawnSync("npm", ["root", "-g"], { encoding: "utf8", timeout: 3e3, shell: true }); | ||
| const npmRoot = (result.stdout ?? "").trim(); | ||
| const indexPath = path.join(npmRoot, "@cocaxcode", "token-optimizer-mcp", "dist", "index.js"); | ||
| if (fs.existsSync(indexPath)) { | ||
| return `node "${indexPath.replace(/\\/g, "/")}"`; | ||
| } | ||
| } catch { | ||
| } | ||
| return "npx @cocaxcode/token-optimizer-mcp"; | ||
| } | ||
| function settingsPath(home) { | ||
| return path.join(home, ".claude", "settings.json"); | ||
| } | ||
| function readSettings(p) { | ||
| try { | ||
| if (!fs.existsSync(p)) return {}; | ||
| return JSON.parse(fs.readFileSync(p, "utf8")); | ||
| } catch { | ||
| return {}; | ||
| } | ||
| } | ||
| function writeSettings(p, data) { | ||
| const dir = path.dirname(p); | ||
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | ||
| fs.writeFileSync(p, JSON.stringify(data, null, 2)); | ||
| } | ||
| function extractHookFlag(command) { | ||
| const match = command.match(/--hook\s+(\S+)/); | ||
| return match ? `--hook ${match[1]}` : null; | ||
| } | ||
| function removeHook(allHooks, eventName, matcher, identifier) { | ||
| const existing = allHooks[eventName]; | ||
| if (!Array.isArray(existing)) return 0; | ||
| const list = existing; | ||
| const matchEntry = list.find((e) => e.matcher === matcher); | ||
| if (!matchEntry || !Array.isArray(matchEntry.hooks)) return 0; | ||
| const before = matchEntry.hooks.length; | ||
| matchEntry.hooks = matchEntry.hooks.filter( | ||
| (h) => !(typeof h.command === "string" && h.command.includes(identifier)) | ||
| ); | ||
| const removed = before - matchEntry.hooks.length; | ||
| if (removed === 0) return 0; | ||
| if (matchEntry.hooks.length === 0) { | ||
| const idx = list.indexOf(matchEntry); | ||
| if (idx >= 0) list.splice(idx, 1); | ||
| } | ||
| if (list.length === 0) { | ||
| delete allHooks[eventName]; | ||
| } else { | ||
| allHooks[eventName] = list; | ||
| } | ||
| return removed; | ||
| } | ||
| function upsertHook(allHooks, eventName, matcher, command, upsertOpts = {}) { | ||
| const identifier = upsertOpts.identifier ?? "token-optimizer"; | ||
| const useFlagDisambiguation = upsertOpts.useFlagDisambiguation ?? true; | ||
| const existing = allHooks[eventName] ?? []; | ||
| const list = Array.isArray(existing) ? [...existing] : []; | ||
| const matchEntry = list.find((e) => e.matcher === matcher); | ||
| const ourHandler = { type: "command", command }; | ||
| const ourFlag = useFlagDisambiguation ? extractHookFlag(command) : null; | ||
| if (matchEntry) { | ||
| const handlers = Array.isArray(matchEntry.hooks) ? [...matchEntry.hooks] : []; | ||
| let idx = -1; | ||
| if (ourFlag) { | ||
| idx = handlers.findIndex( | ||
| (h) => typeof h.command === "string" && h.command.includes(identifier) && extractHookFlag(h.command) === ourFlag | ||
| ); | ||
| } | ||
| if (idx < 0) { | ||
| idx = handlers.findIndex( | ||
| (h) => typeof h.command === "string" && h.command.includes(identifier) && extractHookFlag(h.command) === null | ||
| ); | ||
| } | ||
| if (idx >= 0) { | ||
| handlers[idx] = ourHandler; | ||
| } else { | ||
| handlers.push(ourHandler); | ||
| } | ||
| matchEntry.hooks = handlers; | ||
| } else { | ||
| list.push({ matcher, hooks: [ourHandler] }); | ||
| } | ||
| allHooks[eventName] = list; | ||
| } | ||
| function runInstall(_args = [], opts = {}) { | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const p = settingsPath(home); | ||
| const settings = readSettings(p); | ||
| const mcpServers = settings.mcpServers ?? {}; | ||
| mcpServers[SERVER_NAME] = { | ||
| command: "npx", | ||
| args: ["-y", "@cocaxcode/token-optimizer-mcp", "--mcp"] | ||
| }; | ||
| settings.mcpServers = mcpServers; | ||
| const hookBase = resolveHookCommandBase(); | ||
| const hooks = settings.hooks ?? {}; | ||
| upsertHook(hooks, "PreToolUse", "Bash", `${hookBase} --hook pretooluse`); | ||
| upsertHook(hooks, "PostToolUse", "*", `${hookBase} --hook posttooluse`); | ||
| upsertHook(hooks, "SessionStart", "compact", `${hookBase} --hook sessionstart`); | ||
| const serenaProbe = opts.serenaProbe ?? probeSerenaPresence(); | ||
| const wantOfficialHooks = serenaProbe.serena_cli_installed && opts.skipSerenaHooks !== true; | ||
| if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) { | ||
| upsertHook(hooks, "SessionStart", "", `${hookBase} --hook serena-activate`); | ||
| } | ||
| if (wantOfficialHooks) { | ||
| upsertHook(hooks, "PreToolUse", "", "serena-hooks remind --client=claude-code", { | ||
| identifier: "serena-hooks remind", | ||
| useFlagDisambiguation: false | ||
| }); | ||
| upsertHook( | ||
| hooks, | ||
| "PreToolUse", | ||
| "mcp__serena__.*", | ||
| "serena-hooks auto-approve --client=claude-code", | ||
| { | ||
| identifier: "serena-hooks auto-approve", | ||
| useFlagDisambiguation: false | ||
| } | ||
| ); | ||
| upsertHook(hooks, "Stop", "", "serena-hooks cleanup --client=claude-code", { | ||
| identifier: "serena-hooks cleanup", | ||
| useFlagDisambiguation: false | ||
| }); | ||
| } else { | ||
| removeHook(hooks, "PreToolUse", "", "serena-hooks remind"); | ||
| removeHook(hooks, "PreToolUse", "mcp__serena__.*", "serena-hooks auto-approve"); | ||
| removeHook(hooks, "Stop", "", "serena-hooks cleanup"); | ||
| } | ||
| settings.hooks = hooks; | ||
| let shadowAutoEnabled = false; | ||
| if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) { | ||
| const configPath = getConfigPath(home); | ||
| let userHasExplicitFlag = false; | ||
| try { | ||
| if (fs.existsSync(configPath)) { | ||
| const raw = fs.readFileSync(configPath, "utf8"); | ||
| const parsed = JSON.parse(raw); | ||
| const sm = parsed.shadow_measurement; | ||
| userHasExplicitFlag = sm !== void 0 && sm !== null && "serena" in sm; | ||
| } | ||
| } catch { | ||
| } | ||
| if (!userHasExplicitFlag) { | ||
| const cfg = loadConfig(home); | ||
| if (!cfg.shadow_measurement.serena) { | ||
| cfg.shadow_measurement.serena = true; | ||
| saveConfig(cfg, home); | ||
| shadowAutoEnabled = true; | ||
| } | ||
| } | ||
| } | ||
| writeSettings(p, settings); | ||
| const globalDir = path.join(home, ".token-optimizer"); | ||
| if (!fs.existsSync(globalDir)) fs.mkdirSync(globalDir, { recursive: true }); | ||
| if (fs.existsSync(path.join(cwd, ".git"))) { | ||
| ensureStorageDir(cwd); | ||
| } | ||
| print("token-optimizer-mcp instalado correctamente."); | ||
| print(` settings: ${p}`); | ||
| print(` global: ${globalDir}`); | ||
| if (serenaProbe.serena_cli_installed) { | ||
| print(` serena: CLI detectado \u2014 4 hooks registrados`); | ||
| print(` (activate + remind + auto-approve + cleanup)`); | ||
| if (opts.skipSerenaHooks) { | ||
| print(` note: --skipSerenaHooks activo \u2192 solo se registr\xF3 serena-activate`); | ||
| } | ||
| } else if (serenaProbe.serena_mcp_registered) { | ||
| print(` serena: MCP detectado pero CLI no instalado`); | ||
| print(` \u2192 solo se registr\xF3 serena-activate`); | ||
| print(` \u2192 para los otros 3: uv tool install git+https://github.com/oraios/serena`); | ||
| } else { | ||
| print(` serena: no detectado \u2014 hooks de serena omitidos`); | ||
| } | ||
| if (shadowAutoEnabled) { | ||
| print(` shadow_measurement.serena = true (auto-activado)`); | ||
| print(` \u2192 mide ahorro real vs lectura completa de archivo por cada call`); | ||
| } | ||
| if (opts.runDoctorAtEnd !== false) { | ||
| print(""); | ||
| runDoctor([], { cwd, home, print }); | ||
| } | ||
| return 0; | ||
| } | ||
| export { | ||
| runInstall | ||
| }; | ||
| //# sourceMappingURL=install-22M5OKPL.js.map |
| {"version":3,"sources":["../src/cli/install.ts"],"sourcesContent":["// Install CLI — Phase 4.10\n// Writes token-optimizer mcpServers entry + 3 hooks into ~/.claude/settings.json\n// Also creates per-project storage dir and appends .gitignore in git repos.\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport os from 'node:os'\nimport { spawnSync } from 'node:child_process'\nimport { ensureStorageDir } from '../lib/storage.js'\nimport { probeSerenaPresence, type SerenaProbe } from '../hooks/serena-activate.js'\nimport { runDoctor } from './doctor.js'\nimport { loadConfig, saveConfig, getConfigPath } from './config.js'\n\nconst SERVER_NAME = 'token-optimizer'\n\n/**\n * Resolve the hook command base.\n * Prefer `node <global-path>/dist/index.js` for speed (~0.2s vs ~1.5s with npx).\n * Falls back to `npx @cocaxcode/token-optimizer-mcp` if global path not found.\n */\nfunction resolveHookCommandBase(): string {\n try {\n const globalRoot = path.join(os.homedir(), 'AppData', 'Roaming', 'npm', 'node_modules')\n const indexPath = path.join(globalRoot, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\n if (fs.existsSync(indexPath)) {\n return `node \"${indexPath.replace(/\\\\/g, '/')}\"`\n }\n } catch { /* fallback */ }\n\n // Unix global paths\n const unixPaths = [\n '/usr/local/lib/node_modules',\n '/usr/lib/node_modules',\n path.join(os.homedir(), '.npm-global', 'lib', 'node_modules'),\n ]\n for (const root of unixPaths) {\n try {\n const indexPath = path.join(root, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\n if (fs.existsSync(indexPath)) {\n return `node \"${indexPath}\"`\n }\n } catch { /* fallback */ }\n }\n\n // npm root -g fallback\n try {\n const result = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 3000, shell: true })\n const npmRoot = (result.stdout ?? '').trim()\n const indexPath = path.join(npmRoot, '@cocaxcode', 'token-optimizer-mcp', 'dist', 'index.js')\n if (fs.existsSync(indexPath)) {\n return `node \"${indexPath.replace(/\\\\/g, '/')}\"`\n }\n } catch { /* fallback */ }\n\n return 'npx @cocaxcode/token-optimizer-mcp'\n}\n\nexport interface InstallOptions {\n home?: string\n cwd?: string\n print?: (msg: string) => void\n runDoctorAtEnd?: boolean\n /**\n * Override the Serena presence probe. Used by tests to get deterministic\n * behaviour independent of whether the test host actually has Serena.\n * Undefined = probe the real filesystem/PATH.\n */\n serenaProbe?: SerenaProbe\n /**\n * Skip installing the 3 official Serena reminder hooks (remind, auto-approve,\n * cleanup) even when Serena is detected. The Serena-activate hook we own is\n * still installed. Use this if you prefer managing the official hooks yourself\n * or you don't want to depend on Serena's alpha feature.\n */\n skipSerenaHooks?: boolean\n}\n\nfunction settingsPath(home: string): string {\n return path.join(home, '.claude', 'settings.json')\n}\n\nfunction readSettings(p: string): Record<string, unknown> {\n try {\n if (!fs.existsSync(p)) return {}\n return JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>\n } catch {\n return {}\n }\n}\n\nfunction writeSettings(p: string, data: Record<string, unknown>): void {\n const dir = path.dirname(p)\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })\n fs.writeFileSync(p, JSON.stringify(data, null, 2))\n}\n\ninterface HookEntry {\n matcher?: string\n hooks?: Array<{ type?: string; command?: string }>\n [key: string]: unknown\n}\n\n/**\n * Extract the `--hook <kind>` flag from a command line so we can use it as\n * a unique identity for upsert. `node .../index.js --hook serena-activate`\n * becomes `--hook serena-activate`. Anything without a `--hook X` returns null.\n */\nfunction extractHookFlag(command: string): string | null {\n const match = command.match(/--hook\\s+(\\S+)/)\n return match ? `--hook ${match[1]}` : null\n}\n\n/**\n * Remove any handlers whose command contains `identifier` from the given\n * (eventName, matcher) group. Used to un-register hooks that were installed\n * in a previous run but no longer apply (e.g. the 3 official Serena hooks\n * when the `serena-hooks` CLI is no longer in PATH).\n *\n * Returns the number of handlers removed. If the matcher group becomes empty,\n * the whole group is dropped from the event list. If the event itself becomes\n * empty, the event key is deleted from the hooks map.\n */\nfunction removeHook(\n allHooks: Record<string, unknown>,\n eventName: string,\n matcher: string,\n identifier: string,\n): number {\n const existing = allHooks[eventName]\n if (!Array.isArray(existing)) return 0\n const list = existing as HookEntry[]\n const matchEntry = list.find((e) => e.matcher === matcher)\n if (!matchEntry || !Array.isArray(matchEntry.hooks)) return 0\n\n const before = matchEntry.hooks.length\n matchEntry.hooks = matchEntry.hooks.filter(\n (h) => !(typeof h.command === 'string' && h.command.includes(identifier)),\n )\n const removed = before - matchEntry.hooks.length\n if (removed === 0) return 0\n\n // Clean empty matcher groups\n if (matchEntry.hooks.length === 0) {\n const idx = list.indexOf(matchEntry)\n if (idx >= 0) list.splice(idx, 1)\n }\n // Clean empty event\n if (list.length === 0) {\n delete allHooks[eventName]\n } else {\n allHooks[eventName] = list\n }\n return removed\n}\n\n/**\n * Options for upsertHook.\n * - `identifier`: a substring that uniquely identifies an existing handler of\n * the same kind so we can replace it in place. Defaults to \"token-optimizer\"\n * for our own hooks. For external hooks (e.g. serena-hooks), callers should\n * pass something like \"serena-hooks remind\".\n * - `useFlagDisambiguation`: if true, use the `--hook X` flag as extra\n * disambiguation so multiple token-optimizer hooks can coexist in the same\n * matcher group without trampling each other. Default true.\n */\ninterface UpsertHookOptions {\n identifier?: string\n useFlagDisambiguation?: boolean\n}\n\nfunction upsertHook(\n allHooks: Record<string, unknown>,\n eventName: string,\n matcher: string,\n command: string,\n upsertOpts: UpsertHookOptions = {},\n): void {\n const identifier = upsertOpts.identifier ?? 'token-optimizer'\n const useFlagDisambiguation = upsertOpts.useFlagDisambiguation ?? true\n\n const existing = (allHooks[eventName] ?? []) as HookEntry[]\n const list: HookEntry[] = Array.isArray(existing) ? [...existing] : []\n const matchEntry = list.find((e) => e.matcher === matcher)\n const ourHandler = { type: 'command', command }\n const ourFlag = useFlagDisambiguation ? extractHookFlag(command) : null\n\n if (matchEntry) {\n const handlers = Array.isArray(matchEntry.hooks) ? [...matchEntry.hooks] : []\n // 1) Preferred: find the exact handler we own by identifier + flag.\n let idx = -1\n if (ourFlag) {\n idx = handlers.findIndex(\n (h) =>\n typeof h.command === 'string' &&\n h.command.includes(identifier) &&\n extractHookFlag(h.command) === ourFlag,\n )\n }\n // 2) Fallback: any handler that includes the identifier (and doesn't\n // have a --hook flag of its own so we don't steal a sibling's slot).\n if (idx < 0) {\n idx = handlers.findIndex(\n (h) =>\n typeof h.command === 'string' &&\n h.command.includes(identifier) &&\n extractHookFlag(h.command) === null,\n )\n }\n\n if (idx >= 0) {\n handlers[idx] = ourHandler\n } else {\n handlers.push(ourHandler)\n }\n matchEntry.hooks = handlers\n } else {\n list.push({ matcher, hooks: [ourHandler] })\n }\n allHooks[eventName] = list\n}\n\nexport function runInstall(_args: string[] = [], opts: InstallOptions = {}): number {\n const home = opts.home ?? os.homedir()\n const cwd = opts.cwd ?? process.cwd()\n const print = opts.print ?? ((m: string) => console.error(m))\n\n const p = settingsPath(home)\n const settings = readSettings(p)\n\n // mcpServers upsert\n const mcpServers = (settings.mcpServers ?? {}) as Record<string, unknown>\n mcpServers[SERVER_NAME] = {\n command: 'npx',\n args: ['-y', '@cocaxcode/token-optimizer-mcp', '--mcp'],\n }\n settings.mcpServers = mcpServers\n\n // hooks upsert — prefer node direct for speed (~0.2s vs ~1.5s with npx)\n const hookBase = resolveHookCommandBase()\n const hooks = (settings.hooks ?? {}) as Record<string, unknown>\n upsertHook(hooks, 'PreToolUse', 'Bash', `${hookBase} --hook pretooluse`)\n upsertHook(hooks, 'PostToolUse', '*', `${hookBase} --hook posttooluse`)\n upsertHook(hooks, 'SessionStart', 'compact', `${hookBase} --hook sessionstart`)\n\n // Serena integration — two independent decisions based on two probe signals:\n //\n // (a) `--hook serena-activate` (our own SessionStart hook). Fixes the\n // ToolSearch gap in the official `serena-hooks activate` output. Does\n // NOT shell out to any binary — it's a node entry point that emits\n // JSON. Gated by `serena_mcp_registered` (i.e. the user uses Serena\n // as an MCP server at all).\n //\n // (b) The 3 OFFICIAL Serena reminder hooks (remind, auto-approve, cleanup).\n // These ARE invoked as `serena-hooks <cmd> ...` at runtime by Claude\n // Code, so they require the actual CLI binary to be on PATH. Gated by\n // `serena_cli_installed`. If the CLI disappears (user uninstalled\n // Serena, or the probe was wrong in a previous release), we actively\n // REMOVE the orphan entries so settings.json stops pointing at a\n // missing binary.\n //\n // Both blocks can be individually skipped via `skipSerenaHooks: true`.\n const serenaProbe = opts.serenaProbe ?? probeSerenaPresence()\n const wantOfficialHooks =\n serenaProbe.serena_cli_installed && opts.skipSerenaHooks !== true\n\n if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) {\n upsertHook(hooks, 'SessionStart', '', `${hookBase} --hook serena-activate`)\n }\n\n if (wantOfficialHooks) {\n upsertHook(hooks, 'PreToolUse', '', 'serena-hooks remind --client=claude-code', {\n identifier: 'serena-hooks remind',\n useFlagDisambiguation: false,\n })\n upsertHook(\n hooks,\n 'PreToolUse',\n 'mcp__serena__.*',\n 'serena-hooks auto-approve --client=claude-code',\n {\n identifier: 'serena-hooks auto-approve',\n useFlagDisambiguation: false,\n },\n )\n upsertHook(hooks, 'Stop', '', 'serena-hooks cleanup --client=claude-code', {\n identifier: 'serena-hooks cleanup',\n useFlagDisambiguation: false,\n })\n } else {\n // Reconcile: if the 3 official hooks were added by a previous install\n // (maybe from a buggier probe that accepted ~/.serena/ as sufficient),\n // but the CLI isn't actually available now, take them OUT so Claude\n // Code stops logging \"command not found\" on every hook dispatch.\n removeHook(hooks, 'PreToolUse', '', 'serena-hooks remind')\n removeHook(hooks, 'PreToolUse', 'mcp__serena__.*', 'serena-hooks auto-approve')\n removeHook(hooks, 'Stop', '', 'serena-hooks cleanup')\n }\n settings.hooks = hooks\n\n // Auto-activar shadow_measurement.serena si:\n // - Serena está registrada (MCP o CLI)\n // - El usuario NO ha puesto explícitamente el flag (ni true ni false)\n // Si ya lo tocó (aunque sea a false), respetamos su decisión.\n // Con el flag activo, cada call a serena en PostToolUse mide\n // shadow_delta_tokens = fullFileTokens - serena_output_tokens, que es lo que\n // xray enseña como ahorro real.\n let shadowAutoEnabled = false\n if (serenaProbe.serena_mcp_registered || serenaProbe.serena_cli_installed) {\n const configPath = getConfigPath(home)\n let userHasExplicitFlag = false\n try {\n if (fs.existsSync(configPath)) {\n const raw = fs.readFileSync(configPath, 'utf8')\n const parsed = JSON.parse(raw) as Record<string, unknown>\n const sm = parsed.shadow_measurement as Record<string, unknown> | undefined\n userHasExplicitFlag = sm !== undefined && sm !== null && 'serena' in sm\n }\n } catch {\n // archivo corrupto o no legible → tratamos como si no estuviera\n }\n if (!userHasExplicitFlag) {\n const cfg = loadConfig(home)\n if (!cfg.shadow_measurement.serena) {\n cfg.shadow_measurement.serena = true\n saveConfig(cfg, home)\n shadowAutoEnabled = true\n }\n }\n }\n\n writeSettings(p, settings)\n\n // Global storage dir\n const globalDir = path.join(home, '.token-optimizer')\n if (!fs.existsSync(globalDir)) fs.mkdirSync(globalDir, { recursive: true })\n\n // Per-project storage dir (only in git repos)\n if (fs.existsSync(path.join(cwd, '.git'))) {\n ensureStorageDir(cwd)\n }\n\n print('token-optimizer-mcp instalado correctamente.')\n print(` settings: ${p}`)\n print(` global: ${globalDir}`)\n\n // Serena status — 3 states\n if (serenaProbe.serena_cli_installed) {\n print(` serena: CLI detectado — 4 hooks registrados`)\n print(` (activate + remind + auto-approve + cleanup)`)\n if (opts.skipSerenaHooks) {\n print(` note: --skipSerenaHooks activo → solo se registró serena-activate`)\n }\n } else if (serenaProbe.serena_mcp_registered) {\n print(` serena: MCP detectado pero CLI no instalado`)\n print(` → solo se registró serena-activate`)\n print(` → para los otros 3: uv tool install git+https://github.com/oraios/serena`)\n } else {\n print(` serena: no detectado — hooks de serena omitidos`)\n }\n\n if (shadowAutoEnabled) {\n print(` shadow_measurement.serena = true (auto-activado)`)\n print(` → mide ahorro real vs lectura completa de archivo por cada call`)\n }\n\n if (opts.runDoctorAtEnd !== false) {\n print('')\n runDoctor([], { cwd, home, print })\n }\n\n return 0\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| } from "./chunk-3CAOEPYE.js"; | ||
| import "./chunk-L5Z32XXL.js"; | ||
| import "./chunk-TOEPQYR3.js"; | ||
| import "./chunk-V4PINTCV.js"; | ||
| export { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| impact, | ||
| rollback, | ||
| runPruneMcp, | ||
| settingsLocalPath | ||
| }; | ||
| //# sourceMappingURL=prune-mcp-TGHEFZ3X.js.map |
| {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| getDb | ||
| } from "./chunk-TOEPQYR3.js"; | ||
| import { | ||
| resolveAnalyticsDbPath, | ||
| resolveProjectDir | ||
| } from "./chunk-V4PINTCV.js"; | ||
| // src/cli/report.ts | ||
| import fs from "fs"; | ||
| var PERIOD_DAYS = { | ||
| session: 3650, | ||
| day: 1, | ||
| week: 7, | ||
| month: 30 | ||
| }; | ||
| function isMeasured(method) { | ||
| return method === "measured_exact" || method === "measured_delta"; | ||
| } | ||
| function queryBySourceAndMethod(db, sinceIso) { | ||
| return db.prepare( | ||
| `SELECT source, estimation_method, | ||
| COUNT(*) as count, | ||
| COALESCE(SUM(tokens_estimated), 0) as tokens | ||
| FROM tool_calls | ||
| WHERE created_at >= ? | ||
| GROUP BY source, estimation_method | ||
| ORDER BY tokens DESC` | ||
| ).all(sinceIso); | ||
| } | ||
| var REFERENCE_DATA = [ | ||
| { | ||
| feature: "Model switching (opusplan / default-to-sonnet)", | ||
| saving: "60-80% reduccion de coste", | ||
| source: "mindstudio.ai, verdent.ai, claudelab.net", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Progressive disclosure skills", | ||
| saving: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| source: "claudefast.com", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Prompt caching read hit", | ||
| saving: "10x mas barato que uncached", | ||
| source: "Anthropic docs", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "Claude Code Tool Search", | ||
| saving: "~85% schema reduction (77k \u2192 8.7k)", | ||
| source: "observado en sesion", | ||
| verified_at: "2026-04-11" | ||
| }, | ||
| { | ||
| feature: "MCP pruning sobre Tool Search", | ||
| saving: "~5-12% adicional por turno", | ||
| source: "estimacion interna", | ||
| verified_at: "2026-04-11" | ||
| } | ||
| ]; | ||
| function resolvePeriod(args, fallback) { | ||
| const flag = args.find((a) => a.startsWith("--period=")); | ||
| if (flag) { | ||
| const value = flag.split("=")[1]; | ||
| if (value && value in PERIOD_DAYS) return value; | ||
| } | ||
| return fallback; | ||
| } | ||
| function runReport(args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const period = opts.period ?? resolvePeriod(args, "day"); | ||
| const days = PERIOD_DAYS[period]; | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| const lines = []; | ||
| lines.push(`token-optimizer-mcp reporte \u2014 periodo: ${period} (${days} dia(s))`); | ||
| lines.push(""); | ||
| if (!fs.existsSync(dbPath)) { | ||
| lines.push("No hay datos registrados todavia."); | ||
| } else { | ||
| const db = getDb(dbPath); | ||
| const since = new Date(Date.now() - days * 864e5).toISOString(); | ||
| const rows = queryBySourceAndMethod(db, since); | ||
| let medidoTotal = 0; | ||
| let estimadoTotal = 0; | ||
| lines.push("Por fuente y metodo de estimacion:"); | ||
| if (rows.length === 0) { | ||
| lines.push(" (sin eventos en este periodo)"); | ||
| } else { | ||
| for (const row of rows) { | ||
| const method = row.estimation_method ?? "unknown"; | ||
| lines.push( | ||
| ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens` | ||
| ); | ||
| if (isMeasured(method)) medidoTotal += row.tokens; | ||
| else estimadoTotal += row.tokens; | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push(`Resumen: Medido: ${medidoTotal} tokens \xB7 Estimado: ${estimadoTotal} tokens`); | ||
| lines.push(""); | ||
| } | ||
| lines.push("Coach activity:"); | ||
| lines.push(" (sin tips surfaceados todavia \u2014 coach layer se activa en Phase 4.H)"); | ||
| lines.push(""); | ||
| printReference(lines); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| function printReference(lines) { | ||
| lines.push("Referencia (datos publicos verificables):"); | ||
| for (const row of REFERENCE_DATA) { | ||
| lines.push(` \u2022 ${row.feature}`); | ||
| lines.push(` ahorro: ${row.saving}`); | ||
| lines.push(` fuente: ${row.source} \xB7 verificado: ${row.verified_at}`); | ||
| } | ||
| } | ||
| export { | ||
| runReport | ||
| }; | ||
| //# sourceMappingURL=report-JX5CQ6FO.js.map |
| {"version":3,"sources":["../src/cli/report.ts"],"sourcesContent":["// Report CLI — Phase 4.14\n// Per-source breakdown WITH estimation_method label + Medido/Estimado split +\n// reference-data table (coach-layer addendum CO-4). Spanish.\n\nimport fs from 'node:fs'\nimport type Database from 'better-sqlite3'\nimport { getDb } from '../db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from '../lib/paths.js'\n\ntype DB = Database.Database\n\ntype Period = 'session' | 'day' | 'week' | 'month'\n\nconst PERIOD_DAYS: Record<Period, number> = {\n session: 3650,\n day: 1,\n week: 7,\n month: 30,\n}\n\ninterface SourceMethodRow {\n source: string\n estimation_method: string | null\n count: number\n tokens: number\n}\n\nfunction isMeasured(method: string | null): boolean {\n return method === 'measured_exact' || method === 'measured_delta'\n}\n\nfunction queryBySourceAndMethod(db: DB, sinceIso: string): SourceMethodRow[] {\n return db\n .prepare(\n `SELECT source, estimation_method,\n COUNT(*) as count,\n COALESCE(SUM(tokens_estimated), 0) as tokens\n FROM tool_calls\n WHERE created_at >= ?\n GROUP BY source, estimation_method\n ORDER BY tokens DESC`,\n )\n .all(sinceIso) as SourceMethodRow[]\n}\n\nexport interface ReportOptions {\n cwd?: string\n period?: Period\n print?: (msg: string) => void\n}\n\nconst REFERENCE_DATA: Array<{\n feature: string\n saving: string\n source: string\n verified_at: string\n}> = [\n {\n feature: 'Model switching (opusplan / default-to-sonnet)',\n saving: '60-80% reduccion de coste',\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Progressive disclosure skills',\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n source: 'claudefast.com',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Prompt caching read hit',\n saving: '10x mas barato que uncached',\n source: 'Anthropic docs',\n verified_at: '2026-04-11',\n },\n {\n feature: 'Claude Code Tool Search',\n saving: '~85% schema reduction (77k → 8.7k)',\n source: 'observado en sesion',\n verified_at: '2026-04-11',\n },\n {\n feature: 'MCP pruning sobre Tool Search',\n saving: '~5-12% adicional por turno',\n source: 'estimacion interna',\n verified_at: '2026-04-11',\n },\n]\n\nfunction resolvePeriod(args: string[], fallback: Period): Period {\n const flag = args.find((a) => a.startsWith('--period='))\n if (flag) {\n const value = flag.split('=')[1] as Period | undefined\n if (value && value in PERIOD_DAYS) return value\n }\n return fallback\n}\n\nexport function runReport(args: string[] = [], opts: ReportOptions = {}): number {\n const print = opts.print ?? ((m: string) => console.error(m))\n const cwd = opts.cwd ?? process.cwd()\n const period: Period = opts.period ?? resolvePeriod(args, 'day')\n const days = PERIOD_DAYS[period]\n\n const projectDir = resolveProjectDir(cwd)\n const dbPath = resolveAnalyticsDbPath(projectDir)\n\n const lines: string[] = []\n lines.push(`token-optimizer-mcp reporte — periodo: ${period} (${days} dia(s))`)\n lines.push('')\n\n if (!fs.existsSync(dbPath)) {\n lines.push('No hay datos registrados todavia.')\n } else {\n const db = getDb(dbPath)\n const since = new Date(Date.now() - days * 86_400_000).toISOString()\n const rows = queryBySourceAndMethod(db, since)\n\n let medidoTotal = 0\n let estimadoTotal = 0\n\n lines.push('Por fuente y metodo de estimacion:')\n if (rows.length === 0) {\n lines.push(' (sin eventos en este periodo)')\n } else {\n for (const row of rows) {\n const method = row.estimation_method ?? 'unknown'\n lines.push(\n ` ${row.source.padEnd(8)} [${method}] ${row.count} llamadas ${row.tokens} tokens`,\n )\n if (isMeasured(method)) medidoTotal += row.tokens\n else estimadoTotal += row.tokens\n }\n }\n lines.push('')\n lines.push(`Resumen: Medido: ${medidoTotal} tokens · Estimado: ${estimadoTotal} tokens`)\n lines.push('')\n }\n\n // Coach activity section (always present; filled in Phase 4.H with real data)\n lines.push('Coach activity:')\n lines.push(' (sin tips surfaceados todavia — coach layer se activa en Phase 4.H)')\n lines.push('')\n\n printReference(lines)\n print(lines.join('\\n'))\n return 0\n}\n\nfunction printReference(lines: string[]): void {\n lines.push('Referencia (datos publicos verificables):')\n for (const row of REFERENCE_DATA) {\n lines.push(` • ${row.feature}`)\n lines.push(` ahorro: ${row.saving}`)\n lines.push(` fuente: ${row.source} · verificado: ${row.verified_at}`)\n }\n}\n"],"mappings":"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","names":[]} |
| #!/usr/bin/env node | ||
| import { | ||
| KNOWLEDGE_BASE, | ||
| getCoachSurfaceLog, | ||
| measureContextSize, | ||
| postSummaryToXray, | ||
| runRules | ||
| } from "./chunk-HZUEH22U.js"; | ||
| import { | ||
| applyAllowlist, | ||
| clearAllowlist, | ||
| generateFromHistory, | ||
| rollback | ||
| } from "./chunk-3CAOEPYE.js"; | ||
| import { | ||
| ensureStorageDir | ||
| } from "./chunk-VHP52B3J.js"; | ||
| import "./chunk-KNYWGCEX.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-V4PINTCV.js"; | ||
| // src/server.ts | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| // src/tools/budget.ts | ||
| import { z } from "zod"; | ||
| // src/lib/response.ts | ||
| var text = (t) => ({ | ||
| content: [{ type: "text", text: t }] | ||
| }); | ||
| var error = (t) => ({ | ||
| content: [{ type: "text", text: `Error: ${t}` }], | ||
| isError: true | ||
| }); | ||
| // src/tools/budget.ts | ||
| var DAY_MS = 864e5; | ||
| function sinceForPeriod(period) { | ||
| const now = Date.now(); | ||
| switch (period) { | ||
| case "day": | ||
| return new Date(now - DAY_MS).toISOString(); | ||
| case "week": | ||
| return new Date(now - 7 * DAY_MS).toISOString(); | ||
| case "month": | ||
| return new Date(now - 30 * DAY_MS).toISOString(); | ||
| case "session": | ||
| default: | ||
| return "1970-01-01T00:00:00.000Z"; | ||
| } | ||
| } | ||
| function registerBudgetTools(server, db) { | ||
| const manager = new BudgetManager(db); | ||
| server.tool( | ||
| "budget_set", | ||
| "Define o actualiza un presupuesto de tokens. Precedencia: session > project. Modo warn avisa al exceder.", | ||
| { | ||
| scope: z.enum(["session", "project"]).describe("Ambito del presupuesto"), | ||
| scope_key: z.string().min(1).describe("Clave del scope (sessionId o projectHash)"), | ||
| limit_tokens: z.number().int().positive().max(1e7).describe("Limite en tokens (1..10_000_000)") | ||
| }, | ||
| async ({ scope, scope_key, limit_tokens }) => { | ||
| try { | ||
| const budget = manager.setBudget({ scope, scope_key, limit_tokens }); | ||
| return text( | ||
| [ | ||
| "Presupuesto guardado:", | ||
| "", | ||
| ` scope: ${budget.scope}`, | ||
| ` scope_key: ${budget.scope_key}`, | ||
| ` limit_tokens: ${budget.limit_tokens}`, | ||
| ` mode: ${budget.mode}` | ||
| ].join("\n") | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "budget_check", | ||
| "Consulta el estado del presupuesto activo (gasto actual, restante y porcentaje).", | ||
| { | ||
| session_id: z.string().optional().describe('ID de sesion (default: "default")'), | ||
| project_hash: z.string().optional().describe("Hash del proyecto para fallback a scope project") | ||
| }, | ||
| async ({ session_id, project_hash }) => { | ||
| try { | ||
| const status = manager.checkBudget(session_id ?? "default", project_hash ?? null); | ||
| if (!status.active) { | ||
| return text("Sin presupuesto activo para la sesion/proyecto actual."); | ||
| } | ||
| const percent = (status.percent_used * 100).toFixed(1); | ||
| return text( | ||
| [ | ||
| "Estado del presupuesto:", | ||
| "", | ||
| ` gastado: ${status.spent} tokens`, | ||
| ` restante: ${status.remaining} tokens`, | ||
| ` uso: ${percent}%`, | ||
| ` modo: ${status.mode ?? "n/a"}` | ||
| ].join("\n") | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "budget_report", | ||
| "Muestra el consumo de tokens agrupado por herramienta y por fuente durante un periodo.", | ||
| { | ||
| period: z.enum(["session", "day", "week", "month"]).optional().describe("Periodo del reporte (default: day)") | ||
| }, | ||
| async ({ period }) => { | ||
| try { | ||
| const since = sinceForPeriod(period ?? "day"); | ||
| const report = manager.getBudgetReport(since); | ||
| const lines = [`Reporte de consumo (desde ${report.period_since}):`, ""]; | ||
| lines.push("Por herramienta:"); | ||
| if (report.by_tool.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of report.by_tool) { | ||
| lines.push(` ${row.tool_name}: ${row.count} llamadas, ${row.tokens} tokens`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push("Por fuente:"); | ||
| if (report.by_source.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of report.by_source) { | ||
| lines.push(` ${row.source}: ${row.count} llamadas, ${row.tokens} tokens`); | ||
| } | ||
| } | ||
| return text(lines.join("\n")); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/tools/session.ts | ||
| function registerSessionTools(_server, _db) { | ||
| } | ||
| // src/tools/orchestration.ts | ||
| import { z as z2 } from "zod"; | ||
| // src/services/session-summary-builder.ts | ||
| function buildSessionSummary(db, sessionId, version) { | ||
| const usage = getUsageStats(db, 1); | ||
| const cost = getCostReport(db, 1); | ||
| const serena = probeSerena(); | ||
| const rtk = probeRtk(); | ||
| const mcpPruning = probeMcpPruning(); | ||
| const promptCaching = probePromptCaching(); | ||
| const schema = measureCurrentSchemaBytes(); | ||
| const coachTips = getCoachSurfaceLog(db, sessionId); | ||
| const projDir = resolveProjectDir(); | ||
| const projName = projDir.split(/[\\/]/).filter(Boolean).pop() ?? "unknown"; | ||
| return { | ||
| session_id: sessionId, | ||
| project_path: projDir, | ||
| project_name: projName, | ||
| total_tokens: usage.total_tokens, | ||
| total_events: usage.total_events, | ||
| by_source: usage.by_source, | ||
| by_tool: usage.by_tool.map((t) => ({ | ||
| tool_name: t.tool_name, | ||
| count: t.count, | ||
| tokens: t.tokens | ||
| })), | ||
| cost_haiku: cost.estimated_cost_usd_haiku, | ||
| cost_sonnet: cost.estimated_cost_usd_sonnet, | ||
| cost_opus: cost.estimated_cost_usd_opus, | ||
| probes: { | ||
| serena: { present: serena.present, confidence: serena.confidence, signals: serena.signals }, | ||
| rtk: { present: rtk.present, confidence: rtk.confidence, signals: rtk.signals }, | ||
| mcp_pruning: { | ||
| present: mcpPruning.present, | ||
| confidence: mcpPruning.confidence, | ||
| signals: mcpPruning.signals | ||
| }, | ||
| prompt_caching: { present: promptCaching.present, confidence: promptCaching.confidence } | ||
| }, | ||
| coach_tips_surfaced: coachTips, | ||
| schema_measurement: { | ||
| tool_schema_tokens: schema.tool_schema_tokens, | ||
| mcp_servers: schema.mcp_servers | ||
| }, | ||
| optimizer_version: version | ||
| }; | ||
| } | ||
| // src/tools/orchestration.ts | ||
| function registerOrchestrationTools(server, db) { | ||
| server.tool( | ||
| "mcp_usage_stats", | ||
| "Estadisticas de uso de tokens por herramienta y fuente en un periodo.", | ||
| { | ||
| days: z2.number().int().positive().max(365).optional().describe("Dias a analizar (default: 7)") | ||
| }, | ||
| async ({ days }) => { | ||
| try { | ||
| const stats = getUsageStats(db, days ?? 7); | ||
| const lines = [ | ||
| `Uso en los ultimos ${stats.period_days} dia(s):`, | ||
| "", | ||
| `Total: ${stats.total_tokens} tokens, ${stats.total_events} eventos`, | ||
| "", | ||
| "Por fuente:" | ||
| ]; | ||
| if (stats.by_source.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of stats.by_source) { | ||
| lines.push(` ${row.source}: ${row.tokens} tokens, ${row.count} llamadas`); | ||
| } | ||
| } | ||
| lines.push(""); | ||
| lines.push("Top herramientas:"); | ||
| if (stats.by_tool.length === 0) { | ||
| lines.push(" (sin datos)"); | ||
| } else { | ||
| for (const row of stats.by_tool.slice(0, 10)) { | ||
| lines.push(` ${row.tool_name}: ${row.tokens} tokens, ${row.count} llamadas`); | ||
| } | ||
| } | ||
| return text(lines.join("\n")); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_cost_report", | ||
| "Reporte de coste estimado con rango Haiku-Sonnet-Opus y disclaimer honesto.", | ||
| { | ||
| days: z2.number().int().positive().max(365).optional().describe("Dias a analizar (default: 7)") | ||
| }, | ||
| async ({ days }) => { | ||
| try { | ||
| const cost = getCostReport(db, days ?? 7); | ||
| const lines = [ | ||
| `Reporte de coste (${cost.period_days} dia(s)):`, | ||
| "", | ||
| `Tokens totales: ${cost.total_tokens}`, | ||
| `Coste estimado (input pricing):`, | ||
| ` Haiku 4.5: $${cost.estimated_cost_usd_haiku.toFixed(4)} ($1/MTok)`, | ||
| ` Sonnet 4.6: $${cost.estimated_cost_usd_sonnet.toFixed(4)} ($3/MTok)`, | ||
| ` Opus 4.6: $${cost.estimated_cost_usd_opus.toFixed(4)} ($5/MTok)`, | ||
| "", | ||
| `Nota: ${cost.disclaimer}` | ||
| ]; | ||
| return text(lines.join("\n")); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "optimization_status", | ||
| "Estado de las optimizaciones detectadas: serena, RTK, MCP pruning, prompt caching, schema size.", | ||
| {}, | ||
| async () => { | ||
| try { | ||
| const serena = probeSerena(); | ||
| const rtk = probeRtk(); | ||
| const pruning = probeMcpPruning(); | ||
| const pcProbe = probePromptCaching(); | ||
| void pcProbe; | ||
| const schema = measureCurrentSchemaBytes(); | ||
| const status = { | ||
| serena, | ||
| rtk, | ||
| mcp_pruning: pruning, | ||
| prompt_caching: { | ||
| active_by_default: true, | ||
| savings_tokens: null, | ||
| estimation_method: "unknown", | ||
| note: "Revisa tu factura Anthropic para confirmar el ahorro real" | ||
| }, | ||
| schema_bytes: { | ||
| tool_schema_bytes: schema.tool_schema_bytes, | ||
| measurement_method: schema.measurement_method | ||
| } | ||
| }; | ||
| const serenaHealth = serena.present ? checkSerenaHealth() : []; | ||
| const suggestions = buildSuggestions(status); | ||
| try { | ||
| const lastSession = db.prepare("SELECT id FROM sessions ORDER BY started_at DESC LIMIT 1").get(); | ||
| if (lastSession) { | ||
| const summary = buildSessionSummary(db, lastSession.id, "0.2.6"); | ||
| void postSummaryToXray(summary).catch(() => { | ||
| }); | ||
| } | ||
| } catch { | ||
| } | ||
| return text(JSON.stringify({ status, serena_health: serenaHealth, suggestions }, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_suggest", | ||
| "Genera un allowlist de MCPs basandose en el historial (NO modifica archivos).", | ||
| { | ||
| days: z2.number().int().positive().max(365).optional().describe("Dias de historial a analizar (default: 14)") | ||
| }, | ||
| async ({ days }) => { | ||
| try { | ||
| const proposal = generateFromHistory({ days: days ?? 14 }); | ||
| return text(JSON.stringify(proposal, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_apply", | ||
| "Restringe los MCPs activos escribiendo enabledMcpjsonServers en .claude/settings.local.json. Requiere confirm:true. Acepta dos formas equivalentes: allowlist (lista blanca, los que SI quieres) o exclude (lista negra, los que NO quieres). Se debe pasar exactamente una de las dos.", | ||
| { | ||
| allowlist: z2.array(z2.string()).optional().describe("Nombres de MCPs a permitir (lista blanca). Exclusivo con exclude."), | ||
| exclude: z2.array(z2.string()).optional().describe( | ||
| "Nombres de MCPs a desactivar (lista negra). Internamente se traduce a allowlist = registrados - exclude. Exclusivo con allowlist." | ||
| ), | ||
| confirm: z2.boolean().describe("Debe ser true para confirmar la escritura") | ||
| }, | ||
| async ({ allowlist, exclude, confirm }) => { | ||
| try { | ||
| if (confirm !== true) { | ||
| return error( | ||
| "Operacion destructiva: requiere confirm:true. Revisa el allowlist antes de aplicar." | ||
| ); | ||
| } | ||
| const hasAllow = Array.isArray(allowlist); | ||
| const hasExclude = Array.isArray(exclude); | ||
| if (hasAllow === hasExclude) { | ||
| return error( | ||
| "Debes pasar exactamente uno: allowlist (los que SI quieres) o exclude (los que NO quieres)." | ||
| ); | ||
| } | ||
| const schema = measureCurrentSchemaBytes(); | ||
| const registered = new Set(schema.mcp_servers); | ||
| let effective; | ||
| let translationNote = ""; | ||
| if (hasAllow) { | ||
| effective = allowlist; | ||
| if (registered.size > 0) { | ||
| const invalid = effective.filter((s) => !registered.has(s)); | ||
| if (invalid.length > 0) { | ||
| return error( | ||
| `Allowlist contiene MCPs no registrados en settings: ${invalid.join(", ")}` | ||
| ); | ||
| } | ||
| } | ||
| } else { | ||
| const excludeSet = new Set(exclude); | ||
| if (registered.size > 0) { | ||
| const invalid = exclude.filter((s) => !registered.has(s)); | ||
| if (invalid.length > 0) { | ||
| return error( | ||
| `Exclude contiene MCPs no registrados en settings: ${invalid.join(", ")}` | ||
| ); | ||
| } | ||
| } | ||
| effective = [...registered].filter((s) => !excludeSet.has(s)); | ||
| translationNote = ` | ||
| exclude: [${exclude.join(", ")}] | ||
| \u2192 allowlist efectivo: [${effective.join(", ")}]`; | ||
| } | ||
| const applied = applyAllowlist(effective, { source: "mcp" }); | ||
| return text( | ||
| `Allowlist aplicado.${translationNote} | ||
| settings: ${applied.settings_path} | ||
| backup: ${applied.backup_path}` | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_rollback", | ||
| "Restaura el backup mas reciente de settings.local.json. Requiere confirm:true.", | ||
| { | ||
| confirm: z2.boolean(), | ||
| to: z2.string().optional().describe("Timestamp opcional del backup a restaurar") | ||
| }, | ||
| async ({ confirm, to }) => { | ||
| try { | ||
| if (confirm !== true) { | ||
| return error("Operacion destructiva: requiere confirm:true."); | ||
| } | ||
| const result = rollback(to !== void 0 ? { to } : {}); | ||
| if (!result.restored) return error("No hay backups disponibles."); | ||
| return text(`Restaurado desde ${result.from}`); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "mcp_prune_clear", | ||
| "Elimina el allowlist de settings.local.json (crea backup). Requiere confirm:true.", | ||
| { | ||
| confirm: z2.boolean() | ||
| }, | ||
| async ({ confirm }) => { | ||
| try { | ||
| if (confirm !== true) { | ||
| return error("Operacion destructiva: requiere confirm:true."); | ||
| } | ||
| const result = clearAllowlist(); | ||
| return text( | ||
| result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : "Nada que eliminar" | ||
| ); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/tools/coach.ts | ||
| import { z as z3 } from "zod"; | ||
| // src/coach/reference-data.ts | ||
| var REFERENCE_DATA = [ | ||
| { | ||
| feature: "Model switching (opusplan / default-to-sonnet)", | ||
| saving: "60-80% reduccion de coste", | ||
| source: "mindstudio.ai, verdent.ai, claudelab.net", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "Progressive disclosure skills", | ||
| saving: "~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)", | ||
| source: "claudefast.com", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "Prompt caching read hit", | ||
| saving: "10x mas barato que uncached", | ||
| source: "Anthropic docs", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "Claude Code Tool Search", | ||
| saving: "~85% schema reduction (77k \u2192 8.7k tokens)", | ||
| source: "observado en sesion", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| }, | ||
| { | ||
| feature: "MCP pruning sobre Tool Search", | ||
| saving: "~5-12% adicional por turno", | ||
| source: "estimacion interna", | ||
| verified_at: "2026-04-11", | ||
| estimation_method: "reference_measured" | ||
| } | ||
| ]; | ||
| var DAY_MS2 = 864e5; | ||
| function getStaleRows(daysThreshold = 90, today = /* @__PURE__ */ new Date()) { | ||
| const cutoff = today.getTime() - daysThreshold * DAY_MS2; | ||
| return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() < cutoff); | ||
| } | ||
| // src/coach/tips-payload.ts | ||
| async function computeCoachTipsPayload(opts) { | ||
| const { db } = opts; | ||
| const sessionId = opts.sessionId ?? "default"; | ||
| const contextOpts = { db }; | ||
| if (opts.projectDir !== void 0) contextOpts.projectDir = opts.projectDir; | ||
| if (opts.activeModel !== void 0) contextOpts.activeModel = opts.activeModel; | ||
| const context = await measureContextSize(sessionId, contextOpts); | ||
| const queries = buildQueries(db); | ||
| const since = new Date(Date.now() - 864e5).toISOString(); | ||
| const rawRows = queries.getToolCallsSince(since); | ||
| const events = rawRows.slice(0, 100); | ||
| const ctx = { | ||
| session_id: sessionId, | ||
| events, | ||
| session_token_total: context.tokens, | ||
| session_token_method: context.estimation_method, | ||
| session_token_limit: context.limit, | ||
| active_model: opts.activeModel ?? null | ||
| }; | ||
| const hits = runRules(ctx); | ||
| const staleTips = getStaleRows(); | ||
| return { | ||
| current: hits, | ||
| known_tricks: KNOWLEDGE_BASE, | ||
| context, | ||
| reference_data: REFERENCE_DATA, | ||
| stale_reference_count: staleTips.length, | ||
| last_computed_at: (/* @__PURE__ */ new Date()).toISOString() | ||
| }; | ||
| } | ||
| // src/tools/coach.ts | ||
| function registerCoachTools(server, db) { | ||
| server.tool( | ||
| "coach_tips", | ||
| "Devuelve tips activos (rules disparadas) y medicion de contexto. Por defecto modo compacto (~500 tokens). Usa verbose=true para incluir el catalogo completo de 18 tips y la tabla de referencia (~3.5k tokens).", | ||
| { | ||
| session_id: z3.string().optional().describe('ID de la sesion (default: "default")'), | ||
| project_dir: z3.string().optional().describe("Directorio del proyecto para medir contexto desde transcript JSONL"), | ||
| active_model: z3.string().optional().describe("Modelo activo (opcional, habilita regla detect-opus-for-simple-task)"), | ||
| verbose: z3.boolean().optional().describe( | ||
| "Si true, incluye el knowledge base completo (18 tips) y la reference data. Default false = solo hits activos + contexto. Ahorra ~3.3k tokens por llamada en modo compacto." | ||
| ) | ||
| }, | ||
| async ({ session_id, project_dir, active_model, verbose }) => { | ||
| try { | ||
| const payloadOpts = { db }; | ||
| if (session_id !== void 0) payloadOpts.sessionId = session_id; | ||
| if (project_dir !== void 0) payloadOpts.projectDir = project_dir; | ||
| if (active_model !== void 0) payloadOpts.activeModel = active_model; | ||
| const response = await computeCoachTipsPayload(payloadOpts); | ||
| if (verbose !== true) { | ||
| const { known_tricks: _kb, reference_data: _ref, ...compact } = response; | ||
| return text(JSON.stringify(compact, null, 2)); | ||
| } | ||
| return text(JSON.stringify(response, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/tools/toon.ts | ||
| import { z as z4 } from "zod"; | ||
| function compactEncode(data) { | ||
| try { | ||
| return JSON.stringify(data); | ||
| } catch (e) { | ||
| const msg = e instanceof Error ? e.message : String(e); | ||
| if (/circular|cyclic/i.test(msg)) { | ||
| throw new Error("Referencia circular detectada: TOON no soporta objetos ciclicos"); | ||
| } | ||
| throw new Error(`No se pudo codificar a TOON: ${msg}`); | ||
| } | ||
| } | ||
| function compactDecode(toon) { | ||
| try { | ||
| return JSON.parse(toon); | ||
| } catch (e) { | ||
| throw new Error(`TOON invalido: ${e instanceof Error ? e.message : String(e)}`); | ||
| } | ||
| } | ||
| function registerToonTools(server) { | ||
| server.tool( | ||
| "toon_encode", | ||
| "Codifica un objeto JSON a formato TOON (JSON compacto token-eficiente, round-trip lossless).", | ||
| { | ||
| data: z4.unknown().describe("Valor a codificar (objeto, array, primitivo)") | ||
| }, | ||
| async ({ data }) => { | ||
| try { | ||
| const encoded = compactEncode(data); | ||
| return text(encoded); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| server.tool( | ||
| "toon_decode", | ||
| "Decodifica una cadena TOON a JSON. Devuelve el objeto formateado para lectura.", | ||
| { | ||
| toon: z4.string().min(1).describe("Cadena TOON a decodificar") | ||
| }, | ||
| async ({ toon }) => { | ||
| try { | ||
| const decoded = compactDecode(toon); | ||
| return text(JSON.stringify(decoded, null, 2)); | ||
| } catch (e) { | ||
| return error(e instanceof Error ? e.message : String(e)); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/resources/coach-tips.ts | ||
| var COACH_TIPS_URI = "token-optimizer://coach/tips"; | ||
| function registerCoachTipsResource(server, db) { | ||
| server.resource( | ||
| "coach-tips", | ||
| COACH_TIPS_URI, | ||
| { | ||
| description: "Tips activos del coach, catalogo completo de trucos, medicion de contexto y tabla de referencia.", | ||
| mimeType: "application/json" | ||
| }, | ||
| async (uri) => { | ||
| try { | ||
| const payload = await computeCoachTipsPayload({ db }); | ||
| return { | ||
| contents: [ | ||
| { | ||
| uri: uri.href, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify(payload, null, 2) | ||
| } | ||
| ] | ||
| }; | ||
| } catch (e) { | ||
| const message = e instanceof Error ? e.message : String(e); | ||
| return { | ||
| contents: [ | ||
| { | ||
| uri: uri.href, | ||
| mimeType: "application/json", | ||
| text: JSON.stringify({ error: message }) | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| // src/server.ts | ||
| var VERSION = true ? "0.6.0" : "0.1.0"; | ||
| var INSTRUCTIONS = `token-optimizer-mcp: orchestration + observability + coach layer for Claude Code. | ||
| Measures tool usage, enforces token budgets, advises on complementary tools (serena, RTK), | ||
| and proactively surfaces savings tips. Coach layer detects inefficiencies and suggests | ||
| optimizations like opusplan, /compact, plan mode, and more. | ||
| Does NOT replace serena (symbolic file reads) or RTK (Bash output filtering) \u2014 | ||
| coordinates with them and adds measurements, budgets, compact recovery, and coaching.`; | ||
| function createServer(options = {}) { | ||
| const resolvedProject = options.projectDir ?? resolveProjectDir(); | ||
| const dbPath = options.dbPath ?? (options.storageDir === ":memory:" ? ":memory:" : (ensureStorageDir(resolvedProject), resolveAnalyticsDbPath(resolvedProject))); | ||
| const db = getDb(dbPath); | ||
| const server = new McpServer( | ||
| { | ||
| name: "token-optimizer-mcp", | ||
| version: VERSION | ||
| }, | ||
| { | ||
| instructions: INSTRUCTIONS | ||
| } | ||
| ); | ||
| registerBudgetTools(server, db); | ||
| registerSessionTools(server, db); | ||
| registerOrchestrationTools(server, db); | ||
| registerCoachTools(server, db); | ||
| registerCoachTipsResource(server, db); | ||
| registerToonTools(server); | ||
| return server; | ||
| } | ||
| export { | ||
| createServer | ||
| }; | ||
| //# sourceMappingURL=server-LNVIXOHG.js.map |
| {"version":3,"sources":["../src/server.ts","../src/tools/budget.ts","../src/lib/response.ts","../src/tools/session.ts","../src/tools/orchestration.ts","../src/services/session-summary-builder.ts","../src/tools/coach.ts","../src/coach/reference-data.ts","../src/coach/tips-payload.ts","../src/tools/toon.ts","../src/resources/coach-tips.ts"],"sourcesContent":["// createServer factory — Phase 1.13\n// Returns a configured McpServer. Tools are registered in later phases.\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { getDb } from './db/connection.js'\nimport { resolveProjectDir, resolveAnalyticsDbPath } from './lib/paths.js'\nimport { ensureStorageDir } from './lib/storage.js'\nimport { registerBudgetTools } from './tools/budget.js'\nimport { registerSessionTools } from './tools/session.js'\nimport { registerOrchestrationTools } from './tools/orchestration.js'\nimport { registerCoachTools } from './tools/coach.js'\nimport { registerToonTools } from './tools/toon.js'\nimport { registerCoachTipsResource } from './resources/coach-tips.js'\n\ndeclare const __PKG_VERSION__: string\nconst VERSION = typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : '0.1.0'\n\nconst INSTRUCTIONS = `token-optimizer-mcp: orchestration + observability + coach layer for Claude Code.\n\nMeasures tool usage, enforces token budgets, advises on complementary tools (serena, RTK),\nand proactively surfaces savings tips. Coach layer detects inefficiencies and suggests\noptimizations like opusplan, /compact, plan mode, and more.\n\nDoes NOT replace serena (symbolic file reads) or RTK (Bash output filtering) —\ncoordinates with them and adds measurements, budgets, compact recovery, and coaching.`\n\nexport interface CreateServerOptions {\n storageDir?: string\n projectDir?: string\n dbPath?: string\n}\n\nexport function createServer(options: CreateServerOptions = {}): McpServer {\n const resolvedProject = options.projectDir ?? resolveProjectDir()\n const dbPath =\n options.dbPath ??\n (options.storageDir === ':memory:'\n ? ':memory:'\n : (ensureStorageDir(resolvedProject), resolveAnalyticsDbPath(resolvedProject)))\n\n // Initialize DB (schema created by getDb)\n const db = getDb(dbPath)\n\n const server = new McpServer(\n {\n name: 'token-optimizer-mcp',\n version: VERSION,\n },\n {\n instructions: INSTRUCTIONS,\n },\n )\n\n // Phase 2 tools\n registerBudgetTools(server, db)\n // Phase 3 tools\n registerSessionTools(server, db)\n // Phase 4 tools\n registerOrchestrationTools(server, db)\n registerCoachTools(server, db)\n registerCoachTipsResource(server, db)\n // Phase 5 tools\n registerToonTools(server)\n\n return server\n}\n","// Budget MCP tools — Phase 2.4\n// budget_set, budget_check, budget_report\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { BudgetManager } from '../services/budget-manager.js'\nimport { text, error } from '../lib/response.js'\n\ntype DB = Database.Database\n\nconst DAY_MS = 86_400_000\n\nfunction sinceForPeriod(period: 'session' | 'day' | 'week' | 'month'): string {\n const now = Date.now()\n switch (period) {\n case 'day':\n return new Date(now - DAY_MS).toISOString()\n case 'week':\n return new Date(now - 7 * DAY_MS).toISOString()\n case 'month':\n return new Date(now - 30 * DAY_MS).toISOString()\n case 'session':\n default:\n return '1970-01-01T00:00:00.000Z'\n }\n}\n\nexport function registerBudgetTools(server: McpServer, db: DB): void {\n const manager = new BudgetManager(db)\n\n // ── budget_set ──\n server.tool(\n 'budget_set',\n 'Define o actualiza un presupuesto de tokens. Precedencia: session > project. Modo warn avisa al exceder.',\n {\n scope: z.enum(['session', 'project']).describe('Ambito del presupuesto'),\n scope_key: z.string().min(1).describe('Clave del scope (sessionId o projectHash)'),\n limit_tokens: z\n .number()\n .int()\n .positive()\n .max(10_000_000)\n .describe('Limite en tokens (1..10_000_000)'),\n },\n async ({ scope, scope_key, limit_tokens }) => {\n try {\n const budget = manager.setBudget({ scope, scope_key, limit_tokens })\n return text(\n [\n 'Presupuesto guardado:',\n '',\n ` scope: ${budget.scope}`,\n ` scope_key: ${budget.scope_key}`,\n ` limit_tokens: ${budget.limit_tokens}`,\n ` mode: ${budget.mode}`,\n ].join('\\n'),\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── budget_check ──\n server.tool(\n 'budget_check',\n 'Consulta el estado del presupuesto activo (gasto actual, restante y porcentaje).',\n {\n session_id: z.string().optional().describe('ID de sesion (default: \"default\")'),\n project_hash: z\n .string()\n .optional()\n .describe('Hash del proyecto para fallback a scope project'),\n },\n async ({ session_id, project_hash }) => {\n try {\n const status = manager.checkBudget(session_id ?? 'default', project_hash ?? null)\n if (!status.active) {\n return text('Sin presupuesto activo para la sesion/proyecto actual.')\n }\n const percent = (status.percent_used * 100).toFixed(1)\n return text(\n [\n 'Estado del presupuesto:',\n '',\n ` gastado: ${status.spent} tokens`,\n ` restante: ${status.remaining} tokens`,\n ` uso: ${percent}%`,\n ` modo: ${status.mode ?? 'n/a'}`,\n ].join('\\n'),\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── budget_report ──\n server.tool(\n 'budget_report',\n 'Muestra el consumo de tokens agrupado por herramienta y por fuente durante un periodo.',\n {\n period: z\n .enum(['session', 'day', 'week', 'month'])\n .optional()\n .describe('Periodo del reporte (default: day)'),\n },\n async ({ period }) => {\n try {\n const since = sinceForPeriod(period ?? 'day')\n const report = manager.getBudgetReport(since)\n const lines = [`Reporte de consumo (desde ${report.period_since}):`, '']\n lines.push('Por herramienta:')\n if (report.by_tool.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of report.by_tool) {\n lines.push(` ${row.tool_name}: ${row.count} llamadas, ${row.tokens} tokens`)\n }\n }\n lines.push('')\n lines.push('Por fuente:')\n if (report.by_source.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of report.by_source) {\n lines.push(` ${row.source}: ${row.count} llamadas, ${row.tokens} tokens`)\n }\n }\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Shared MCP tool response helpers — Phase 2\n\nexport const text = (t: string) => ({\n content: [{ type: 'text' as const, text: t }],\n})\n\nexport const error = (t: string) => ({\n content: [{ type: 'text' as const, text: `Error: ${t}` }],\n isError: true as const,\n})\n","// Session tools — Phase 3.3 (simplified: session_search removed, FTS5 no longer available)\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\n\ntype DB = Database.Database\n\n// No session tools registered after FTS5 removal.\n// Keeping the function signature for backwards compatibility with server.ts imports.\nexport function registerSessionTools(_server: McpServer, _db: DB): void {\n // noop\n}\n","// Orchestration MCP tools — Phase 4.23-4.28\n// mcp_usage_stats, mcp_cost_report, optimization_status,\n// mcp_prune_suggest, mcp_prune_apply, mcp_prune_rollback, mcp_prune_clear\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { text, error } from '../lib/response.js'\nimport { getUsageStats, getCostReport } from '../services/stats.js'\nimport {\n probeSerena,\n probeRtk,\n probeMcpPruning,\n probePromptCaching,\n checkSerenaHealth,\n} from '../orchestration/detector.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\nimport { buildSuggestions } from '../orchestration/advisor.js'\nimport {\n generateFromHistory,\n applyAllowlist,\n rollback,\n clearAllowlist,\n} from '../cli/prune-mcp.js'\nimport type { OptimizationStatus } from '../lib/types.js'\nimport { buildSessionSummary } from '../services/session-summary-builder.js'\nimport { postSummaryToXray } from '../services/xray-client.js'\n\ntype DB = Database.Database\n\nexport function registerOrchestrationTools(server: McpServer, db: DB): void {\n // ── mcp_usage_stats ──\n server.tool(\n 'mcp_usage_stats',\n 'Estadisticas de uso de tokens por herramienta y fuente en un periodo.',\n {\n days: z.number().int().positive().max(365).optional().describe('Dias a analizar (default: 7)'),\n },\n async ({ days }) => {\n try {\n const stats = getUsageStats(db, days ?? 7)\n const lines = [\n `Uso en los ultimos ${stats.period_days} dia(s):`,\n '',\n `Total: ${stats.total_tokens} tokens, ${stats.total_events} eventos`,\n '',\n 'Por fuente:',\n ]\n if (stats.by_source.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of stats.by_source) {\n lines.push(` ${row.source}: ${row.tokens} tokens, ${row.count} llamadas`)\n }\n }\n lines.push('')\n lines.push('Top herramientas:')\n if (stats.by_tool.length === 0) {\n lines.push(' (sin datos)')\n } else {\n for (const row of stats.by_tool.slice(0, 10)) {\n lines.push(` ${row.tool_name}: ${row.tokens} tokens, ${row.count} llamadas`)\n }\n }\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_cost_report ──\n server.tool(\n 'mcp_cost_report',\n 'Reporte de coste estimado con rango Haiku-Sonnet-Opus y disclaimer honesto.',\n {\n days: z\n .number()\n .int()\n .positive()\n .max(365)\n .optional()\n .describe('Dias a analizar (default: 7)'),\n },\n async ({ days }) => {\n try {\n const cost = getCostReport(db, days ?? 7)\n const lines = [\n `Reporte de coste (${cost.period_days} dia(s)):`,\n '',\n `Tokens totales: ${cost.total_tokens}`,\n `Coste estimado (input pricing):`,\n ` Haiku 4.5: $${cost.estimated_cost_usd_haiku.toFixed(4)} ($1/MTok)`,\n ` Sonnet 4.6: $${cost.estimated_cost_usd_sonnet.toFixed(4)} ($3/MTok)`,\n ` Opus 4.6: $${cost.estimated_cost_usd_opus.toFixed(4)} ($5/MTok)`,\n '',\n `Nota: ${cost.disclaimer}`,\n ]\n return text(lines.join('\\n'))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── optimization_status ──\n server.tool(\n 'optimization_status',\n 'Estado de las optimizaciones detectadas: serena, RTK, MCP pruning, prompt caching, schema size.',\n {},\n async () => {\n try {\n const serena = probeSerena()\n const rtk = probeRtk()\n const pruning = probeMcpPruning()\n const pcProbe = probePromptCaching()\n void pcProbe\n const schema = measureCurrentSchemaBytes()\n // Always include prompt_caching with explicit estimation_method per measurement-honesty spec\n const status: OptimizationStatus = {\n serena,\n rtk,\n mcp_pruning: pruning,\n prompt_caching: {\n active_by_default: true,\n savings_tokens: null,\n estimation_method: 'unknown',\n note: 'Revisa tu factura Anthropic para confirmar el ahorro real',\n },\n schema_bytes: {\n tool_schema_bytes: schema.tool_schema_bytes,\n measurement_method: schema.measurement_method,\n },\n }\n const serenaHealth = serena.present ? checkSerenaHealth() : []\n const suggestions = buildSuggestions(status)\n\n // Fire-and-forget summary to xray (if XRAY_URL is set)\n try {\n const lastSession = db\n .prepare('SELECT id FROM sessions ORDER BY started_at DESC LIMIT 1')\n .get() as { id: string } | undefined\n if (lastSession) {\n const summary = buildSessionSummary(db, lastSession.id, '0.2.6')\n void postSummaryToXray(summary as unknown as Record<string, unknown>).catch(() => {})\n }\n } catch {\n // Silent — xray is optional\n }\n\n return text(JSON.stringify({ status, serena_health: serenaHealth, suggestions }, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_suggest ──\n server.tool(\n 'mcp_prune_suggest',\n 'Genera un allowlist de MCPs basandose en el historial (NO modifica archivos).',\n {\n days: z\n .number()\n .int()\n .positive()\n .max(365)\n .optional()\n .describe('Dias de historial a analizar (default: 14)'),\n },\n async ({ days }) => {\n try {\n const proposal = generateFromHistory({ days: days ?? 14 })\n return text(JSON.stringify(proposal, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_apply ──\n server.tool(\n 'mcp_prune_apply',\n 'Restringe los MCPs activos escribiendo enabledMcpjsonServers en .claude/settings.local.json. Requiere confirm:true. Acepta dos formas equivalentes: allowlist (lista blanca, los que SI quieres) o exclude (lista negra, los que NO quieres). Se debe pasar exactamente una de las dos.',\n {\n allowlist: z\n .array(z.string())\n .optional()\n .describe('Nombres de MCPs a permitir (lista blanca). Exclusivo con exclude.'),\n exclude: z\n .array(z.string())\n .optional()\n .describe(\n 'Nombres de MCPs a desactivar (lista negra). Internamente se traduce a allowlist = registrados - exclude. Exclusivo con allowlist.',\n ),\n confirm: z.boolean().describe('Debe ser true para confirmar la escritura'),\n },\n async ({ allowlist, exclude, confirm }) => {\n try {\n if (confirm !== true) {\n return error(\n 'Operacion destructiva: requiere confirm:true. Revisa el allowlist antes de aplicar.',\n )\n }\n const hasAllow = Array.isArray(allowlist)\n const hasExclude = Array.isArray(exclude)\n if (hasAllow === hasExclude) {\n return error(\n 'Debes pasar exactamente uno: allowlist (los que SI quieres) o exclude (los que NO quieres).',\n )\n }\n\n const schema = measureCurrentSchemaBytes()\n const registered = new Set(schema.mcp_servers)\n\n let effective: string[]\n let translationNote = ''\n\n if (hasAllow) {\n effective = allowlist as string[]\n if (registered.size > 0) {\n const invalid = effective.filter((s) => !registered.has(s))\n if (invalid.length > 0) {\n return error(\n `Allowlist contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\n )\n }\n }\n } else {\n const excludeSet = new Set(exclude as string[])\n if (registered.size > 0) {\n const invalid = (exclude as string[]).filter((s) => !registered.has(s))\n if (invalid.length > 0) {\n return error(\n `Exclude contiene MCPs no registrados en settings: ${invalid.join(', ')}`,\n )\n }\n }\n effective = [...registered].filter((s) => !excludeSet.has(s))\n translationNote = `\\n exclude: [${(exclude as string[]).join(', ')}]\\n → allowlist efectivo: [${effective.join(', ')}]`\n }\n\n const applied = applyAllowlist(effective, { source: 'mcp' })\n return text(\n `Allowlist aplicado.${translationNote}\\n settings: ${applied.settings_path}\\n backup: ${applied.backup_path}`,\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_rollback ──\n server.tool(\n 'mcp_prune_rollback',\n 'Restaura el backup mas reciente de settings.local.json. Requiere confirm:true.',\n {\n confirm: z.boolean(),\n to: z.string().optional().describe('Timestamp opcional del backup a restaurar'),\n },\n async ({ confirm, to }) => {\n try {\n if (confirm !== true) {\n return error('Operacion destructiva: requiere confirm:true.')\n }\n const result = rollback(to !== undefined ? { to } : {})\n if (!result.restored) return error('No hay backups disponibles.')\n return text(`Restaurado desde ${result.from}`)\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── mcp_prune_clear ──\n server.tool(\n 'mcp_prune_clear',\n 'Elimina el allowlist de settings.local.json (crea backup). Requiere confirm:true.',\n {\n confirm: z.boolean(),\n },\n async ({ confirm }) => {\n try {\n if (confirm !== true) {\n return error('Operacion destructiva: requiere confirm:true.')\n }\n const result = clearAllowlist()\n return text(\n result.cleared ? `Allowlist eliminado (backup: ${result.backup_path})` : 'Nada que eliminar',\n )\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Session summary builder for xray integration.\n// Aggregates all local data sources into a single payload for xray.\n// Only called once per session (not in PostToolUse hot path).\n\nimport type Database from 'better-sqlite3'\nimport { getUsageStats, getCostReport } from './stats.js'\nimport {\n probeSerena,\n probeRtk,\n probeMcpPruning,\n probePromptCaching,\n} from '../orchestration/detector.js'\nimport { measureCurrentSchemaBytes } from '../orchestration/schema-measurer.js'\nimport { getCoachSurfaceLog } from '../coach/surface.js'\nimport { resolveProjectDir } from '../lib/paths.js'\n\ntype DB = Database.Database\n\nexport interface XraySummaryPayload {\n session_id: string\n project_path: string\n project_name: string\n total_tokens: number\n total_events: number\n by_source: Array<{ source: string; count: number; tokens: number }>\n by_tool: Array<{ tool_name: string; count: number; tokens: number }>\n cost_haiku: number\n cost_sonnet: number\n cost_opus: number\n probes: {\n serena: { present: boolean; confidence: number; signals: string[] }\n rtk: { present: boolean; confidence: number; signals: string[] }\n mcp_pruning: { present: boolean; confidence: number; signals: string[] }\n prompt_caching: { present: boolean; confidence: number }\n }\n coach_tips_surfaced: Array<{ rule_id: string; tip_ids: string[]; severity: string }>\n schema_measurement: { tool_schema_tokens: number; mcp_servers: string[] }\n optimizer_version: string\n}\n\nexport function buildSessionSummary(\n db: DB,\n sessionId: string,\n version: string,\n): XraySummaryPayload {\n // Usage stats for last 24h (covers the session)\n const usage = getUsageStats(db, 1)\n const cost = getCostReport(db, 1)\n\n // Detection probes (reads local files, no network)\n const serena = probeSerena()\n const rtk = probeRtk()\n const mcpPruning = probeMcpPruning()\n const promptCaching = probePromptCaching()\n\n // Schema measurement (reads settings files, no network)\n const schema = measureCurrentSchemaBytes()\n\n // Coach tips surfaced during this session\n const coachTips = getCoachSurfaceLog(db, sessionId)\n\n const projDir = resolveProjectDir()\n const projName = projDir.split(/[\\\\/]/).filter(Boolean).pop() ?? 'unknown'\n\n return {\n session_id: sessionId,\n project_path: projDir,\n project_name: projName,\n total_tokens: usage.total_tokens,\n total_events: usage.total_events,\n by_source: usage.by_source,\n by_tool: usage.by_tool.map((t) => ({\n tool_name: t.tool_name,\n count: t.count,\n tokens: t.tokens,\n })),\n cost_haiku: cost.estimated_cost_usd_haiku,\n cost_sonnet: cost.estimated_cost_usd_sonnet,\n cost_opus: cost.estimated_cost_usd_opus,\n probes: {\n serena: { present: serena.present, confidence: serena.confidence, signals: serena.signals },\n rtk: { present: rtk.present, confidence: rtk.confidence, signals: rtk.signals },\n mcp_pruning: {\n present: mcpPruning.present,\n confidence: mcpPruning.confidence,\n signals: mcpPruning.signals,\n },\n prompt_caching: { present: promptCaching.present, confidence: promptCaching.confidence },\n },\n coach_tips_surfaced: coachTips,\n schema_measurement: {\n tool_schema_tokens: schema.tool_schema_tokens,\n mcp_servers: schema.mcp_servers,\n },\n optimizer_version: version,\n }\n}\n","// Coach MCP tool — Phase 4.46\n// coach_tips: returns active hits + full knowledge base + context measurement + reference table\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { text, error } from '../lib/response.js'\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\n\ntype DB = Database.Database\n\nexport function registerCoachTools(server: McpServer, db: DB): void {\n server.tool(\n 'coach_tips',\n 'Devuelve tips activos (rules disparadas) y medicion de contexto. Por defecto modo compacto (~500 tokens). Usa verbose=true para incluir el catalogo completo de 18 tips y la tabla de referencia (~3.5k tokens).',\n {\n session_id: z.string().optional().describe('ID de la sesion (default: \"default\")'),\n project_dir: z\n .string()\n .optional()\n .describe('Directorio del proyecto para medir contexto desde transcript JSONL'),\n active_model: z\n .string()\n .optional()\n .describe('Modelo activo (opcional, habilita regla detect-opus-for-simple-task)'),\n verbose: z\n .boolean()\n .optional()\n .describe(\n 'Si true, incluye el knowledge base completo (18 tips) y la reference data. Default false = solo hits activos + contexto. Ahorra ~3.3k tokens por llamada en modo compacto.',\n ),\n },\n async ({ session_id, project_dir, active_model, verbose }) => {\n try {\n const payloadOpts: Parameters<typeof computeCoachTipsPayload>[0] = { db }\n if (session_id !== undefined) payloadOpts.sessionId = session_id\n if (project_dir !== undefined) payloadOpts.projectDir = project_dir\n if (active_model !== undefined) payloadOpts.activeModel = active_model\n const response = await computeCoachTipsPayload(payloadOpts)\n\n // Default compact mode: strip the heavy knowledge_base + reference_data\n // to avoid burning ~3.3k tokens per call. verbose=true restores them.\n if (verbose !== true) {\n const { known_tricks: _kb, reference_data: _ref, ...compact } = response\n return text(JSON.stringify(compact, null, 2))\n }\n return text(JSON.stringify(response, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n","// Reference data table with publicly-verifiable savings numbers — Phase 4.41\n// Every row tagged estimation_method: 'reference_measured'\n\nimport type { EstimationMethod } from '../lib/types.js'\n\nexport interface ReferenceDataRow {\n feature: string\n saving: string\n source: string\n verified_at: string\n estimation_method: EstimationMethod\n}\n\nexport const REFERENCE_DATA: readonly ReferenceDataRow[] = [\n {\n feature: 'Model switching (opusplan / default-to-sonnet)',\n saving: '60-80% reduccion de coste',\n source: 'mindstudio.ai, verdent.ai, claudelab.net',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Progressive disclosure skills',\n saving: '~15k tokens/sesion (82% mejor que CLAUDE.md monolitico)',\n source: 'claudefast.com',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Prompt caching read hit',\n saving: '10x mas barato que uncached',\n source: 'Anthropic docs',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'Claude Code Tool Search',\n saving: '~85% schema reduction (77k → 8.7k tokens)',\n source: 'observado en sesion',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n {\n feature: 'MCP pruning sobre Tool Search',\n saving: '~5-12% adicional por turno',\n source: 'estimacion interna',\n verified_at: '2026-04-11',\n estimation_method: 'reference_measured',\n },\n]\n\nconst DAY_MS = 86_400_000\n\nexport function getFreshRows(\n daysThreshold = 90,\n today: Date = new Date(),\n): ReferenceDataRow[] {\n const cutoff = today.getTime() - daysThreshold * DAY_MS\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() >= cutoff)\n}\n\nexport function getStaleRows(\n daysThreshold = 90,\n today: Date = new Date(),\n): ReferenceDataRow[] {\n const cutoff = today.getTime() - daysThreshold * DAY_MS\n return REFERENCE_DATA.filter((r) => new Date(r.verified_at).getTime() < cutoff)\n}\n","// Shared payload builder for coach_tips MCP tool + token-optimizer://coach/tips\n// resource. Keeps tool/resource outputs identical — Phase 4.H.\n\nimport type Database from 'better-sqlite3'\nimport type { ContextMeasurement, EventContext, ToolEvent, DetectionHit, CoachTip } from '../lib/types.js'\nimport { KNOWLEDGE_BASE } from './knowledge-base.js'\nimport { REFERENCE_DATA, getStaleRows } from './reference-data.js'\nimport { runRules } from './detector.js'\nimport { measureContextSize } from './context-meter.js'\nimport { buildQueries } from '../db/queries.js'\n\ntype DB = Database.Database\n\nexport interface CoachTipsPayload {\n current: DetectionHit[]\n known_tricks: readonly CoachTip[]\n context: ContextMeasurement\n reference_data: typeof REFERENCE_DATA\n stale_reference_count: number\n last_computed_at: string\n}\n\nexport interface ComputeCoachTipsPayloadOptions {\n db: DB\n sessionId?: string\n projectDir?: string\n activeModel?: string\n}\n\nexport async function computeCoachTipsPayload(\n opts: ComputeCoachTipsPayloadOptions,\n): Promise<CoachTipsPayload> {\n const { db } = opts\n const sessionId = opts.sessionId ?? 'default'\n\n const contextOpts: Parameters<typeof measureContextSize>[1] = { db }\n if (opts.projectDir !== undefined) contextOpts.projectDir = opts.projectDir\n if (opts.activeModel !== undefined) contextOpts.activeModel = opts.activeModel\n const context = await measureContextSize(sessionId, contextOpts)\n\n const queries = buildQueries(db)\n const since = new Date(Date.now() - 86_400_000).toISOString()\n const rawRows = queries.getToolCallsSince(since) as ToolEvent[]\n const events = rawRows.slice(0, 100)\n\n const ctx: EventContext = {\n session_id: sessionId,\n events,\n session_token_total: context.tokens,\n session_token_method: context.estimation_method,\n session_token_limit: context.limit,\n active_model: opts.activeModel ?? null,\n }\n\n const hits = runRules(ctx)\n const staleTips = getStaleRows()\n\n return {\n current: hits,\n known_tricks: KNOWLEDGE_BASE,\n context,\n reference_data: REFERENCE_DATA,\n stale_reference_count: staleTips.length,\n last_computed_at: new Date().toISOString(),\n }\n}\n","// TOON encoding tools — Phase 5.3\n// toon_encode: data -> compact JSON (no whitespace) = token-efficient\n// toon_decode: toon string -> JSON object\n//\n// Note: the original `toon-format` npm package was deferred during Phase 0\n// due to package-name uncertainty. This implementation uses compact JSON under\n// the hood, which is round-trip lossless and ~30-40% cheaper in tokens than\n// pretty-printed JSON. The tool names are preserved so a real TOON impl can\n// drop in later without changing the MCP API.\n\nimport { z } from 'zod'\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { text, error } from '../lib/response.js'\n\nfunction compactEncode(data: unknown): string {\n try {\n return JSON.stringify(data)\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e)\n if (/circular|cyclic/i.test(msg)) {\n throw new Error('Referencia circular detectada: TOON no soporta objetos ciclicos')\n }\n throw new Error(`No se pudo codificar a TOON: ${msg}`)\n }\n}\n\nfunction compactDecode(toon: string): unknown {\n try {\n return JSON.parse(toon)\n } catch (e) {\n throw new Error(`TOON invalido: ${e instanceof Error ? e.message : String(e)}`)\n }\n}\n\nexport function registerToonTools(server: McpServer): void {\n // ── toon_encode ──\n server.tool(\n 'toon_encode',\n 'Codifica un objeto JSON a formato TOON (JSON compacto token-eficiente, round-trip lossless).',\n {\n data: z.unknown().describe('Valor a codificar (objeto, array, primitivo)'),\n },\n async ({ data }) => {\n try {\n const encoded = compactEncode(data)\n return text(encoded)\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n\n // ── toon_decode ──\n server.tool(\n 'toon_decode',\n 'Decodifica una cadena TOON a JSON. Devuelve el objeto formateado para lectura.',\n {\n toon: z.string().min(1).describe('Cadena TOON a decodificar'),\n },\n async ({ toon }) => {\n try {\n const decoded = compactDecode(toon)\n return text(JSON.stringify(decoded, null, 2))\n } catch (e) {\n return error(e instanceof Error ? e.message : String(e))\n }\n },\n )\n}\n\n// Exported for tests\nexport const _internal = { compactEncode, compactDecode }\n","// token-optimizer://coach/tips resource — Phase 4.H\n// Mirrors coach_tips() tool payload, readable without a tool call.\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type Database from 'better-sqlite3'\nimport { computeCoachTipsPayload } from '../coach/tips-payload.js'\n\ntype DB = Database.Database\n\nexport const COACH_TIPS_URI = 'token-optimizer://coach/tips'\n\nexport function registerCoachTipsResource(server: McpServer, db: DB): void {\n server.resource(\n 'coach-tips',\n COACH_TIPS_URI,\n {\n description:\n 'Tips activos del coach, catalogo completo de trucos, medicion de contexto y tabla de referencia.',\n mimeType: 'application/json',\n },\n async (uri: URL) => {\n try {\n const payload = await computeCoachTipsPayload({ db })\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify(payload, null, 2),\n },\n ],\n }\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e)\n return {\n contents: [\n {\n uri: uri.href,\n mimeType: 'application/json',\n text: JSON.stringify({ error: message }),\n },\n ],\n }\n }\n },\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,SAAS,iBAAiB;;;ACA1B,SAAS,SAAS;;;ACDX,IAAM,OAAO,CAAC,OAAe;AAAA,EAClC,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,EAAE,CAAC;AAC9C;AAEO,IAAM,QAAQ,CAAC,OAAe;AAAA,EACnC,SAAS,CAAC,EAAE,MAAM,QAAiB,MAAM,UAAU,CAAC,GAAG,CAAC;AAAA,EACxD,SAAS;AACX;;;ADEA,IAAM,SAAS;AAEf,SAAS,eAAe,QAAsD;AAC5E,QAAM,MAAM,KAAK,IAAI;AACrB,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,aAAO,IAAI,KAAK,MAAM,MAAM,EAAE,YAAY;AAAA,IAC5C,KAAK;AACH,aAAO,IAAI,KAAK,MAAM,IAAI,MAAM,EAAE,YAAY;AAAA,IAChD,KAAK;AACH,aAAO,IAAI,KAAK,MAAM,KAAK,MAAM,EAAE,YAAY;AAAA,IACjD,KAAK;AAAA,IACL;AACE,aAAO;AAAA,EACX;AACF;AAEO,SAAS,oBAAoB,QAAmB,IAAc;AACnE,QAAM,UAAU,IAAI,cAAc,EAAE;AAGpC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,OAAO,EAAE,KAAK,CAAC,WAAW,SAAS,CAAC,EAAE,SAAS,wBAAwB;AAAA,MACvE,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,2CAA2C;AAAA,MACjF,cAAc,EACX,OAAO,EACP,IAAI,EACJ,SAAS,EACT,IAAI,GAAU,EACd,SAAS,kCAAkC;AAAA,IAChD;AAAA,IACA,OAAO,EAAE,OAAO,WAAW,aAAa,MAAM;AAC5C,UAAI;AACF,cAAM,SAAS,QAAQ,UAAU,EAAE,OAAO,WAAW,aAAa,CAAC;AACnE,eAAO;AAAA,UACL;AAAA,YACE;AAAA,YACA;AAAA,YACA,mBAAmB,OAAO,KAAK;AAAA,YAC/B,mBAAmB,OAAO,SAAS;AAAA,YACnC,mBAAmB,OAAO,YAAY;AAAA,YACtC,mBAAmB,OAAO,IAAI;AAAA,UAChC,EAAE,KAAK,IAAI;AAAA,QACb;AAAA,MACF,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,mCAAmC;AAAA,MAC9E,cAAc,EACX,OAAO,EACP,SAAS,EACT,SAAS,iDAAiD;AAAA,IAC/D;AAAA,IACA,OAAO,EAAE,YAAY,aAAa,MAAM;AACtC,UAAI;AACF,cAAM,SAAS,QAAQ,YAAY,cAAc,WAAW,gBAAgB,IAAI;AAChF,YAAI,CAAC,OAAO,QAAQ;AAClB,iBAAO,KAAK,wDAAwD;AAAA,QACtE;AACA,cAAM,WAAW,OAAO,eAAe,KAAK,QAAQ,CAAC;AACrD,eAAO;AAAA,UACL;AAAA,YACE;AAAA,YACA;AAAA,YACA,eAAe,OAAO,KAAK;AAAA,YAC3B,eAAe,OAAO,SAAS;AAAA,YAC/B,eAAe,OAAO;AAAA,YACtB,eAAe,OAAO,QAAQ,KAAK;AAAA,UACrC,EAAE,KAAK,IAAI;AAAA,QACb;AAAA,MACF,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,QAAQ,EACL,KAAK,CAAC,WAAW,OAAO,QAAQ,OAAO,CAAC,EACxC,SAAS,EACT,SAAS,oCAAoC;AAAA,IAClD;AAAA,IACA,OAAO,EAAE,OAAO,MAAM;AACpB,UAAI;AACF,cAAM,QAAQ,eAAe,UAAU,KAAK;AAC5C,cAAM,SAAS,QAAQ,gBAAgB,KAAK;AAC5C,cAAM,QAAQ,CAAC,6BAA6B,OAAO,YAAY,MAAM,EAAE;AACvE,cAAM,KAAK,kBAAkB;AAC7B,YAAI,OAAO,QAAQ,WAAW,GAAG;AAC/B,gBAAM,KAAK,eAAe;AAAA,QAC5B,OAAO;AACL,qBAAW,OAAO,OAAO,SAAS;AAChC,kBAAM,KAAK,KAAK,IAAI,SAAS,KAAK,IAAI,KAAK,cAAc,IAAI,MAAM,SAAS;AAAA,UAC9E;AAAA,QACF;AACA,cAAM,KAAK,EAAE;AACb,cAAM,KAAK,aAAa;AACxB,YAAI,OAAO,UAAU,WAAW,GAAG;AACjC,gBAAM,KAAK,eAAe;AAAA,QAC5B,OAAO;AACL,qBAAW,OAAO,OAAO,WAAW;AAClC,kBAAM,KAAK,KAAK,IAAI,MAAM,KAAK,IAAI,KAAK,cAAc,IAAI,MAAM,SAAS;AAAA,UAC3E;AAAA,QACF;AACA,eAAO,KAAK,MAAM,KAAK,IAAI,CAAC;AAAA,MAC9B,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;;;AE/HO,SAAS,qBAAqB,SAAoB,KAAe;AAExE;;;ACPA,SAAS,KAAAA,UAAS;;;ACoCX,SAAS,oBACd,IACA,WACA,SACoB;AAEpB,QAAM,QAAQ,cAAc,IAAI,CAAC;AACjC,QAAM,OAAO,cAAc,IAAI,CAAC;AAGhC,QAAM,SAAS,YAAY;AAC3B,QAAM,MAAM,SAAS;AACrB,QAAM,aAAa,gBAAgB;AACnC,QAAM,gBAAgB,mBAAmB;AAGzC,QAAM,SAAS,0BAA0B;AAGzC,QAAM,YAAY,mBAAmB,IAAI,SAAS;AAElD,QAAM,UAAU,kBAAkB;AAClC,QAAM,WAAW,QAAQ,MAAM,OAAO,EAAE,OAAO,OAAO,EAAE,IAAI,KAAK;AAEjE,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc,MAAM;AAAA,IACpB,cAAc,MAAM;AAAA,IACpB,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM,QAAQ,IAAI,CAAC,OAAO;AAAA,MACjC,WAAW,EAAE;AAAA,MACb,OAAO,EAAE;AAAA,MACT,QAAQ,EAAE;AAAA,IACZ,EAAE;AAAA,IACF,YAAY,KAAK;AAAA,IACjB,aAAa,KAAK;AAAA,IAClB,WAAW,KAAK;AAAA,IAChB,QAAQ;AAAA,MACN,QAAQ,EAAE,SAAS,OAAO,SAAS,YAAY,OAAO,YAAY,SAAS,OAAO,QAAQ;AAAA,MAC1F,KAAK,EAAE,SAAS,IAAI,SAAS,YAAY,IAAI,YAAY,SAAS,IAAI,QAAQ;AAAA,MAC9E,aAAa;AAAA,QACX,SAAS,WAAW;AAAA,QACpB,YAAY,WAAW;AAAA,QACvB,SAAS,WAAW;AAAA,MACtB;AAAA,MACA,gBAAgB,EAAE,SAAS,cAAc,SAAS,YAAY,cAAc,WAAW;AAAA,IACzF;AAAA,IACA,qBAAqB;AAAA,IACrB,oBAAoB;AAAA,MAClB,oBAAoB,OAAO;AAAA,MAC3B,aAAa,OAAO;AAAA,IACtB;AAAA,IACA,mBAAmB;AAAA,EACrB;AACF;;;ADlEO,SAAS,2BAA2B,QAAmB,IAAc;AAE1E,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,MAAMC,GAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,SAAS,8BAA8B;AAAA,IAC/F;AAAA,IACA,OAAO,EAAE,KAAK,MAAM;AAClB,UAAI;AACF,cAAM,QAAQ,cAAc,IAAI,QAAQ,CAAC;AACzC,cAAM,QAAQ;AAAA,UACZ,sBAAsB,MAAM,WAAW;AAAA,UACvC;AAAA,UACA,UAAU,MAAM,YAAY,YAAY,MAAM,YAAY;AAAA,UAC1D;AAAA,UACA;AAAA,QACF;AACA,YAAI,MAAM,UAAU,WAAW,GAAG;AAChC,gBAAM,KAAK,eAAe;AAAA,QAC5B,OAAO;AACL,qBAAW,OAAO,MAAM,WAAW;AACjC,kBAAM,KAAK,KAAK,IAAI,MAAM,KAAK,IAAI,MAAM,YAAY,IAAI,KAAK,WAAW;AAAA,UAC3E;AAAA,QACF;AACA,cAAM,KAAK,EAAE;AACb,cAAM,KAAK,mBAAmB;AAC9B,YAAI,MAAM,QAAQ,WAAW,GAAG;AAC9B,gBAAM,KAAK,eAAe;AAAA,QAC5B,OAAO;AACL,qBAAW,OAAO,MAAM,QAAQ,MAAM,GAAG,EAAE,GAAG;AAC5C,kBAAM,KAAK,KAAK,IAAI,SAAS,KAAK,IAAI,MAAM,YAAY,IAAI,KAAK,WAAW;AAAA,UAC9E;AAAA,QACF;AACA,eAAO,KAAK,MAAM,KAAK,IAAI,CAAC;AAAA,MAC9B,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,MAAMA,GACH,OAAO,EACP,IAAI,EACJ,SAAS,EACT,IAAI,GAAG,EACP,SAAS,EACT,SAAS,8BAA8B;AAAA,IAC5C;AAAA,IACA,OAAO,EAAE,KAAK,MAAM;AAClB,UAAI;AACF,cAAM,OAAO,cAAc,IAAI,QAAQ,CAAC;AACxC,cAAM,QAAQ;AAAA,UACZ,qBAAqB,KAAK,WAAW;AAAA,UACrC;AAAA,UACA,mBAAmB,KAAK,YAAY;AAAA,UACpC;AAAA,UACA,kBAAkB,KAAK,yBAAyB,QAAQ,CAAC,CAAC;AAAA,UAC1D,kBAAkB,KAAK,0BAA0B,QAAQ,CAAC,CAAC;AAAA,UAC3D,kBAAkB,KAAK,wBAAwB,QAAQ,CAAC,CAAC;AAAA,UACzD;AAAA,UACA,SAAS,KAAK,UAAU;AAAA,QAC1B;AACA,eAAO,KAAK,MAAM,KAAK,IAAI,CAAC;AAAA,MAC9B,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC;AAAA,IACD,YAAY;AACV,UAAI;AACF,cAAM,SAAS,YAAY;AAC3B,cAAM,MAAM,SAAS;AACrB,cAAM,UAAU,gBAAgB;AAChC,cAAM,UAAU,mBAAmB;AACnC,aAAK;AACL,cAAM,SAAS,0BAA0B;AAEzC,cAAM,SAA6B;AAAA,UACjC;AAAA,UACA;AAAA,UACA,aAAa;AAAA,UACb,gBAAgB;AAAA,YACd,mBAAmB;AAAA,YACnB,gBAAgB;AAAA,YAChB,mBAAmB;AAAA,YACnB,MAAM;AAAA,UACR;AAAA,UACA,cAAc;AAAA,YACZ,mBAAmB,OAAO;AAAA,YAC1B,oBAAoB,OAAO;AAAA,UAC7B;AAAA,QACF;AACA,cAAM,eAAe,OAAO,UAAU,kBAAkB,IAAI,CAAC;AAC7D,cAAM,cAAc,iBAAiB,MAAM;AAG3C,YAAI;AACF,gBAAM,cAAc,GACjB,QAAQ,0DAA0D,EAClE,IAAI;AACP,cAAI,aAAa;AACf,kBAAM,UAAU,oBAAoB,IAAI,YAAY,IAAI,OAAO;AAC/D,iBAAK,kBAAkB,OAA6C,EAAE,MAAM,MAAM;AAAA,YAAC,CAAC;AAAA,UACtF;AAAA,QACF,QAAQ;AAAA,QAER;AAEA,eAAO,KAAK,KAAK,UAAU,EAAE,QAAQ,eAAe,cAAc,YAAY,GAAG,MAAM,CAAC,CAAC;AAAA,MAC3F,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,MAAMA,GACH,OAAO,EACP,IAAI,EACJ,SAAS,EACT,IAAI,GAAG,EACP,SAAS,EACT,SAAS,4CAA4C;AAAA,IAC1D;AAAA,IACA,OAAO,EAAE,KAAK,MAAM;AAClB,UAAI;AACF,cAAM,WAAW,oBAAoB,EAAE,MAAM,QAAQ,GAAG,CAAC;AACzD,eAAO,KAAK,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAAA,MAC/C,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,WAAWA,GACR,MAAMA,GAAE,OAAO,CAAC,EAChB,SAAS,EACT,SAAS,mEAAmE;AAAA,MAC/E,SAASA,GACN,MAAMA,GAAE,OAAO,CAAC,EAChB,SAAS,EACT;AAAA,QACC;AAAA,MACF;AAAA,MACF,SAASA,GAAE,QAAQ,EAAE,SAAS,2CAA2C;AAAA,IAC3E;AAAA,IACA,OAAO,EAAE,WAAW,SAAS,QAAQ,MAAM;AACzC,UAAI;AACF,YAAI,YAAY,MAAM;AACpB,iBAAO;AAAA,YACL;AAAA,UACF;AAAA,QACF;AACA,cAAM,WAAW,MAAM,QAAQ,SAAS;AACxC,cAAM,aAAa,MAAM,QAAQ,OAAO;AACxC,YAAI,aAAa,YAAY;AAC3B,iBAAO;AAAA,YACL;AAAA,UACF;AAAA,QACF;AAEA,cAAM,SAAS,0BAA0B;AACzC,cAAM,aAAa,IAAI,IAAI,OAAO,WAAW;AAE7C,YAAI;AACJ,YAAI,kBAAkB;AAEtB,YAAI,UAAU;AACZ,sBAAY;AACZ,cAAI,WAAW,OAAO,GAAG;AACvB,kBAAM,UAAU,UAAU,OAAO,CAAC,MAAM,CAAC,WAAW,IAAI,CAAC,CAAC;AAC1D,gBAAI,QAAQ,SAAS,GAAG;AACtB,qBAAO;AAAA,gBACL,uDAAuD,QAAQ,KAAK,IAAI,CAAC;AAAA,cAC3E;AAAA,YACF;AAAA,UACF;AAAA,QACF,OAAO;AACL,gBAAM,aAAa,IAAI,IAAI,OAAmB;AAC9C,cAAI,WAAW,OAAO,GAAG;AACvB,kBAAM,UAAW,QAAqB,OAAO,CAAC,MAAM,CAAC,WAAW,IAAI,CAAC,CAAC;AACtE,gBAAI,QAAQ,SAAS,GAAG;AACtB,qBAAO;AAAA,gBACL,qDAAqD,QAAQ,KAAK,IAAI,CAAC;AAAA,cACzE;AAAA,YACF;AAAA,UACF;AACA,sBAAY,CAAC,GAAG,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,IAAI,CAAC,CAAC;AAC5D,4BAAkB;AAAA,cAAkB,QAAqB,KAAK,IAAI,CAAC;AAAA,gCAA+B,UAAU,KAAK,IAAI,CAAC;AAAA,QACxH;AAEA,cAAM,UAAU,eAAe,WAAW,EAAE,QAAQ,MAAM,CAAC;AAC3D,eAAO;AAAA,UACL,sBAAsB,eAAe;AAAA,cAAiB,QAAQ,aAAa;AAAA,cAAiB,QAAQ,WAAW;AAAA,QACjH;AAAA,MACF,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,SAASA,GAAE,QAAQ;AAAA,MACnB,IAAIA,GAAE,OAAO,EAAE,SAAS,EAAE,SAAS,2CAA2C;AAAA,IAChF;AAAA,IACA,OAAO,EAAE,SAAS,GAAG,MAAM;AACzB,UAAI;AACF,YAAI,YAAY,MAAM;AACpB,iBAAO,MAAM,+CAA+C;AAAA,QAC9D;AACA,cAAM,SAAS,SAAS,OAAO,SAAY,EAAE,GAAG,IAAI,CAAC,CAAC;AACtD,YAAI,CAAC,OAAO,SAAU,QAAO,MAAM,6BAA6B;AAChE,eAAO,KAAK,oBAAoB,OAAO,IAAI,EAAE;AAAA,MAC/C,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,SAASA,GAAE,QAAQ;AAAA,IACrB;AAAA,IACA,OAAO,EAAE,QAAQ,MAAM;AACrB,UAAI;AACF,YAAI,YAAY,MAAM;AACpB,iBAAO,MAAM,+CAA+C;AAAA,QAC9D;AACA,cAAM,SAAS,eAAe;AAC9B,eAAO;AAAA,UACL,OAAO,UAAU,gCAAgC,OAAO,WAAW,MAAM;AAAA,QAC3E;AAAA,MACF,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;;;AEpSA,SAAS,KAAAC,UAAS;;;ACUX,IAAM,iBAA8C;AAAA,EACzD;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,mBAAmB;AAAA,EACrB;AACF;AAEA,IAAMC,UAAS;AAUR,SAAS,aACd,gBAAgB,IAChB,QAAc,oBAAI,KAAK,GACH;AACpB,QAAM,SAAS,MAAM,QAAQ,IAAI,gBAAgBC;AACjD,SAAO,eAAe,OAAO,CAAC,MAAM,IAAI,KAAK,EAAE,WAAW,EAAE,QAAQ,IAAI,MAAM;AAChF;;;ACtCA,eAAsB,wBACpB,MAC2B;AAC3B,QAAM,EAAE,GAAG,IAAI;AACf,QAAM,YAAY,KAAK,aAAa;AAEpC,QAAM,cAAwD,EAAE,GAAG;AACnE,MAAI,KAAK,eAAe,OAAW,aAAY,aAAa,KAAK;AACjE,MAAI,KAAK,gBAAgB,OAAW,aAAY,cAAc,KAAK;AACnE,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,SAAS,QAAQ,MAAM,GAAG,GAAG;AAEnC,QAAM,MAAoB;AAAA,IACxB,YAAY;AAAA,IACZ;AAAA,IACA,qBAAqB,QAAQ;AAAA,IAC7B,sBAAsB,QAAQ;AAAA,IAC9B,qBAAqB,QAAQ;AAAA,IAC7B,cAAc,KAAK,eAAe;AAAA,EACpC;AAEA,QAAM,OAAO,SAAS,GAAG;AACzB,QAAM,YAAY,aAAa;AAE/B,SAAO;AAAA,IACL,SAAS;AAAA,IACT,cAAc;AAAA,IACd;AAAA,IACA,gBAAgB;AAAA,IAChB,uBAAuB,UAAU;AAAA,IACjC,mBAAkB,oBAAI,KAAK,GAAE,YAAY;AAAA,EAC3C;AACF;;;AFtDO,SAAS,mBAAmB,QAAmB,IAAc;AAClE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,YAAYC,GAAE,OAAO,EAAE,SAAS,EAAE,SAAS,sCAAsC;AAAA,MACjF,aAAaA,GACV,OAAO,EACP,SAAS,EACT,SAAS,oEAAoE;AAAA,MAChF,cAAcA,GACX,OAAO,EACP,SAAS,EACT,SAAS,sEAAsE;AAAA,MAClF,SAASA,GACN,QAAQ,EACR,SAAS,EACT;AAAA,QACC;AAAA,MACF;AAAA,IACJ;AAAA,IACA,OAAO,EAAE,YAAY,aAAa,cAAc,QAAQ,MAAM;AAC5D,UAAI;AACF,cAAM,cAA6D,EAAE,GAAG;AACxE,YAAI,eAAe,OAAW,aAAY,YAAY;AACtD,YAAI,gBAAgB,OAAW,aAAY,aAAa;AACxD,YAAI,iBAAiB,OAAW,aAAY,cAAc;AAC1D,cAAM,WAAW,MAAM,wBAAwB,WAAW;AAI1D,YAAI,YAAY,MAAM;AACpB,gBAAM,EAAE,cAAc,KAAK,gBAAgB,MAAM,GAAG,QAAQ,IAAI;AAChE,iBAAO,KAAK,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,QAC9C;AACA,eAAO,KAAK,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAAA,MAC/C,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;;;AG1CA,SAAS,KAAAC,UAAS;AAIlB,SAAS,cAAc,MAAuB;AAC5C,MAAI;AACF,WAAO,KAAK,UAAU,IAAI;AAAA,EAC5B,SAAS,GAAG;AACV,UAAM,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AACrD,QAAI,mBAAmB,KAAK,GAAG,GAAG;AAChC,YAAM,IAAI,MAAM,iEAAiE;AAAA,IACnF;AACA,UAAM,IAAI,MAAM,gCAAgC,GAAG,EAAE;AAAA,EACvD;AACF;AAEA,SAAS,cAAc,MAAuB;AAC5C,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,SAAS,GAAG;AACV,UAAM,IAAI,MAAM,kBAAkB,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC,EAAE;AAAA,EAChF;AACF;AAEO,SAAS,kBAAkB,QAAyB;AAEzD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,MAAMC,GAAE,QAAQ,EAAE,SAAS,8CAA8C;AAAA,IAC3E;AAAA,IACA,OAAO,EAAE,KAAK,MAAM;AAClB,UAAI;AACF,cAAM,UAAU,cAAc,IAAI;AAClC,eAAO,KAAK,OAAO;AAAA,MACrB,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS,2BAA2B;AAAA,IAC9D;AAAA,IACA,OAAO,EAAE,KAAK,MAAM;AAClB,UAAI;AACF,cAAM,UAAU,cAAc,IAAI;AAClC,eAAO,KAAK,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAAA,MAC9C,SAAS,GAAG;AACV,eAAO,MAAM,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACF;;;AC3DO,IAAM,iBAAiB;AAEvB,SAAS,0BAA0B,QAAmB,IAAc;AACzE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,MACE,aACE;AAAA,MACF,UAAU;AAAA,IACZ;AAAA,IACA,OAAO,QAAa;AAClB,UAAI;AACF,cAAM,UAAU,MAAM,wBAAwB,EAAE,GAAG,CAAC;AACpD,eAAO;AAAA,UACL,UAAU;AAAA,YACR;AAAA,cACE,KAAK,IAAI;AAAA,cACT,UAAU;AAAA,cACV,MAAM,KAAK,UAAU,SAAS,MAAM,CAAC;AAAA,YACvC;AAAA,UACF;AAAA,QACF;AAAA,MACF,SAAS,GAAG;AACV,cAAM,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;AACzD,eAAO;AAAA,UACL,UAAU;AAAA,YACR;AAAA,cACE,KAAK,IAAI;AAAA,cACT,UAAU;AAAA,cACV,MAAM,KAAK,UAAU,EAAE,OAAO,QAAQ,CAAC;AAAA,YACzC;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;AV/BA,IAAM,UAAU,OAAyC,UAAkB;AAE3E,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAed,SAAS,aAAa,UAA+B,CAAC,GAAc;AACzE,QAAM,kBAAkB,QAAQ,cAAc,kBAAkB;AAChE,QAAM,SACJ,QAAQ,WACP,QAAQ,eAAe,aACpB,cACC,iBAAiB,eAAe,GAAG,uBAAuB,eAAe;AAGhF,QAAM,KAAK,MAAM,MAAM;AAEvB,QAAM,SAAS,IAAI;AAAA,IACjB;AAAA,MACE,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,cAAc;AAAA,IAChB;AAAA,EACF;AAGA,sBAAoB,QAAQ,EAAE;AAE9B,uBAAqB,QAAQ,EAAE;AAE/B,6BAA2B,QAAQ,EAAE;AACrC,qBAAmB,QAAQ,EAAE;AAC7B,4BAA0B,QAAQ,EAAE;AAEpC,oBAAkB,MAAM;AAExB,SAAO;AACT;","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-V4PINTCV.js"; | ||
| // src/cli/status.ts | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import os from "os"; | ||
| function runStatus(_args = [], opts = {}) { | ||
| const print = opts.print ?? ((m) => console.error(m)); | ||
| const home = opts.home ?? os.homedir(); | ||
| const cwd = opts.cwd ?? process.cwd(); | ||
| const settingsPath = path.join(home, ".claude", "settings.json"); | ||
| const installed = (() => { | ||
| try { | ||
| if (!fs.existsSync(settingsPath)) return false; | ||
| const json = JSON.parse(fs.readFileSync(settingsPath, "utf8")); | ||
| const mcp = json.mcpServers ?? {}; | ||
| return "token-optimizer" in mcp; | ||
| } catch { | ||
| return false; | ||
| } | ||
| })(); | ||
| const projectDir = resolveProjectDir(cwd); | ||
| const dbPath = resolveAnalyticsDbPath(projectDir); | ||
| let eventsToday = 0; | ||
| let tokensBySource = []; | ||
| let budgetLine = "sin presupuesto activo"; | ||
| if (fs.existsSync(dbPath)) { | ||
| try { | ||
| const db = getDb(dbPath); | ||
| const usage = getUsageStats(db, 1); | ||
| eventsToday = usage.total_events; | ||
| tokensBySource = usage.by_source.map((r) => ({ source: r.source, tokens: r.tokens })); | ||
| const budget = getActiveBudgetSummary(db, "default", projectHash(projectDir)); | ||
| if (budget.active) { | ||
| const pct = (budget.percent_used * 100).toFixed(1); | ||
| budgetLine = `gastado=${budget.spent} restante=${budget.remaining} uso=${pct}% modo=${budget.mode}`; | ||
| } | ||
| } catch { | ||
| } | ||
| } | ||
| const lines = []; | ||
| lines.push("token-optimizer-mcp status"); | ||
| lines.push(""); | ||
| lines.push(`Instalado: ${installed ? "\u2713" : "\u2717"} (${settingsPath})`); | ||
| lines.push(`Storage DB: ${dbPath}${fs.existsSync(dbPath) ? "" : " (no existe aun)"}`); | ||
| lines.push(`Eventos hoy: ${eventsToday}`); | ||
| lines.push( | ||
| `Tokens por fuente: ${tokensBySource.length > 0 ? tokensBySource.map((s) => `${s.source}=${s.tokens}`).join(", ") : "(sin datos)"}` | ||
| ); | ||
| lines.push(`Presupuesto: ${budgetLine}`); | ||
| print(lines.join("\n")); | ||
| return 0; | ||
| } | ||
| export { | ||
| runStatus | ||
| }; | ||
| //# sourceMappingURL=status-PGH4NXTW.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-KNYWGCEX.js"; | ||
| import "./chunk-V4PINTCV.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-U6ORS5MP.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":[]} |
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.
784771
-0.27%7202
-0.36%44
2.33%