@nxuss/lemma 0.9.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +79 -12
  2. package/bin/cli.js +97 -0
  3. package/bin/init.js +307 -0
  4. package/cache-proxy.js +191 -0
  5. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  6. package/dist/cjs/cli/lemma-proxy.js +2 -4
  7. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  8. package/dist/cjs/mcp/index.js +1 -1
  9. package/dist/cjs/mcp/tools.d.ts.map +1 -1
  10. package/dist/cjs/mcp/tools.js +911 -71
  11. package/dist/cjs/mcp/tools.js.map +1 -1
  12. package/dist/cjs/subconscious/TheBrainV2.d.ts +102 -0
  13. package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -0
  14. package/dist/cjs/subconscious/TheBrainV2.js +444 -0
  15. package/dist/cjs/subconscious/TheBrainV2.js.map +1 -0
  16. package/dist/cjs/utils/ContextCollapser.d.ts +21 -0
  17. package/dist/cjs/utils/ContextCollapser.d.ts.map +1 -0
  18. package/dist/cjs/utils/ContextCollapser.js +132 -0
  19. package/dist/cjs/utils/ContextCollapser.js.map +1 -0
  20. package/dist/cjs/utils/ExecutiveReport.d.ts +29 -0
  21. package/dist/cjs/utils/ExecutiveReport.d.ts.map +1 -0
  22. package/dist/cjs/utils/ExecutiveReport.js +111 -0
  23. package/dist/cjs/utils/ExecutiveReport.js.map +1 -0
  24. package/dist/cjs/utils/SurgicalASTInsert.d.ts +21 -0
  25. package/dist/cjs/utils/SurgicalASTInsert.d.ts.map +1 -0
  26. package/dist/cjs/utils/SurgicalASTInsert.js +173 -0
  27. package/dist/cjs/utils/SurgicalASTInsert.js.map +1 -0
  28. package/dist/cjs/utils/SymbolSurgicalContext.d.ts +25 -0
  29. package/dist/cjs/utils/SymbolSurgicalContext.d.ts.map +1 -0
  30. package/dist/cjs/utils/SymbolSurgicalContext.js +265 -0
  31. package/dist/cjs/utils/SymbolSurgicalContext.js.map +1 -0
  32. package/dist/cjs/utils/WormholeCompressor.d.ts +22 -0
  33. package/dist/cjs/utils/WormholeCompressor.d.ts.map +1 -0
  34. package/dist/cjs/utils/WormholeCompressor.js +152 -0
  35. package/dist/cjs/utils/WormholeCompressor.js.map +1 -0
  36. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  37. package/dist/esm/cli/lemma-proxy.js +2 -4
  38. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  39. package/dist/esm/mcp/index.js +1 -1
  40. package/dist/esm/mcp/tools.d.ts.map +1 -1
  41. package/dist/esm/mcp/tools.js +911 -71
  42. package/dist/esm/mcp/tools.js.map +1 -1
  43. package/dist/esm/subconscious/TheBrainV2.d.ts +102 -0
  44. package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -0
  45. package/dist/esm/subconscious/TheBrainV2.js +431 -0
  46. package/dist/esm/subconscious/TheBrainV2.js.map +1 -0
  47. package/dist/esm/utils/ContextCollapser.d.ts +21 -0
  48. package/dist/esm/utils/ContextCollapser.d.ts.map +1 -0
  49. package/dist/esm/utils/ContextCollapser.js +128 -0
  50. package/dist/esm/utils/ContextCollapser.js.map +1 -0
  51. package/dist/esm/utils/ExecutiveReport.d.ts +29 -0
  52. package/dist/esm/utils/ExecutiveReport.d.ts.map +1 -0
  53. package/dist/esm/utils/ExecutiveReport.js +105 -0
  54. package/dist/esm/utils/ExecutiveReport.js.map +1 -0
  55. package/dist/esm/utils/SurgicalASTInsert.d.ts +21 -0
  56. package/dist/esm/utils/SurgicalASTInsert.d.ts.map +1 -0
  57. package/dist/esm/utils/SurgicalASTInsert.js +134 -0
  58. package/dist/esm/utils/SurgicalASTInsert.js.map +1 -0
  59. package/dist/esm/utils/SymbolSurgicalContext.d.ts +25 -0
  60. package/dist/esm/utils/SymbolSurgicalContext.d.ts.map +1 -0
  61. package/dist/esm/utils/SymbolSurgicalContext.js +226 -0
  62. package/dist/esm/utils/SymbolSurgicalContext.js.map +1 -0
  63. package/dist/esm/utils/WormholeCompressor.d.ts +22 -0
  64. package/dist/esm/utils/WormholeCompressor.d.ts.map +1 -0
  65. package/dist/esm/utils/WormholeCompressor.js +148 -0
  66. package/dist/esm/utils/WormholeCompressor.js.map +1 -0
  67. package/lemma-proxy.cjs +31 -14
  68. package/mcp-server.js +220 -0
  69. package/package.json +5 -1
@@ -52,6 +52,12 @@ const utils_1 = require("./utils");
52
52
  const PRReviewEngine_1 = require("../pr-review/PRReviewEngine");
53
53
  const ReviewStore_1 = require("../pr-review/ReviewStore");
54
54
  const reviewRunner_1 = require("../pr-review/reviewRunner");
55
+ const TheBrainV2_1 = require("../subconscious/TheBrainV2");
56
+ const ContextCollapser_1 = require("../utils/ContextCollapser");
57
+ const SymbolSurgicalContext_1 = require("../utils/SymbolSurgicalContext");
58
+ const WormholeCompressor_1 = require("../utils/WormholeCompressor");
59
+ const ExecutiveReport_1 = require("../utils/ExecutiveReport");
60
+ const SurgicalASTInsert_1 = require("../utils/SurgicalASTInsert");
55
61
  // ── Pro License Gate ──────────────────────────────────────────────────────────
56
62
  const LICENSE_FILE = path_1.default.join(os_1.default.homedir(), '.lemma-cache', 'license.json');
57
63
  let _proStatusCache = null;
@@ -359,6 +365,67 @@ const toolDefinitions = [
359
365
  required: ["text"],
360
366
  },
361
367
  },
368
+ {
369
+ name: "get_symbol_surgical_context",
370
+ description: "Extrae quirúrgicamente la implementación de un símbolo específico (clase, función, interfaz) y las firmas (pero no la implementación completa) de todas sus dependencias locales del workspace. Ahorra hasta 90% de tokens.",
371
+ inputSchema: {
372
+ type: "object",
373
+ properties: {
374
+ filePath: { type: "string", description: "Ruta relativa del archivo que contiene el símbolo" },
375
+ symbolName: { type: "string", description: "Nombre de la función, clase, tipo o interfaz a extraer" },
376
+ },
377
+ required: ["filePath", "symbolName"],
378
+ },
379
+ },
380
+ {
381
+ name: "wormhole_squeeze",
382
+ description: "Comprime código a formato WORMHOLE súper denso usando tokens de palabra clave y abreviación de variables reversible. Ideal para alimentar contextos de lectura al LLM.",
383
+ inputSchema: {
384
+ type: "object",
385
+ properties: {
386
+ text: { type: "string", description: "El código fuente a comprimir" },
387
+ },
388
+ required: ["text"],
389
+ },
390
+ },
391
+ {
392
+ name: "generate_executive_roi_report",
393
+ description: "Genera un reporte ejecutivo en formato Markdown de Retorno de Inversión (ROI), dinero ahorrado en API, horas de desarrollo recuperadas y proyecciones de escala para la gerencia.",
394
+ inputSchema: {
395
+ type: "object",
396
+ properties: {
397
+ teamSize: { type: "number", description: "Número de ingenieros en el equipo (default: 10)", default: 10 },
398
+ costPerDevHour: { type: "number", description: "Costo estimado en USD por hora de desarrollador (default: 50)", default: 50 },
399
+ targetPath: { type: "string", description: "Nombre de archivo del reporte generado (default: LEMMA_ROI_REPORT.md)", default: "LEMMA_ROI_REPORT.md" },
400
+ },
401
+ },
402
+ },
403
+ {
404
+ name: "surgical_ast_insert",
405
+ description: "Inserta quirúrgicamente código (métodos, propiedades, funciones) en una clase, interfaz o ámbito de archivo de TypeScript utilizando el AST Compiler. Evita escribir diffs de búsqueda y reemplazo grandes o reescribir todo el archivo. Ahorra 95% de tokens de salida.",
406
+ inputSchema: {
407
+ type: "object",
408
+ properties: {
409
+ filePath: { type: "string", description: "Ruta del archivo TypeScript (relativo al root)" },
410
+ symbolName: { type: "string", description: "Nombre de la clase o interfaz donde insertar (opcional)" },
411
+ anchorSymbol: { type: "string", description: "Nombre del método o propiedad como anclaje (ej. 'login')" },
412
+ insertPosition: { type: "string", enum: ["before", "after", "start", "end"], description: "Posición relativa al anclaje (default: 'after')", default: "after" },
413
+ codeToInsert: { type: "string", description: "El código fuente exacto a insertar" },
414
+ },
415
+ required: ["filePath", "codeToInsert"],
416
+ },
417
+ },
418
+ {
419
+ name: "local_semantic_autofix",
420
+ description: "Intenta resolver errores de compilación o ejecución localmente consultando The Brain por tracebacks/mensajes similares. Si hay coincidencia, devuelve la solución/parche instantáneamente sin gastar tokens de LLM.",
421
+ inputSchema: {
422
+ type: "object",
423
+ properties: {
424
+ errorMessage: { type: "string", description: "El traceback del error o mensaje de error a resolver" },
425
+ },
426
+ required: ["errorMessage"],
427
+ },
428
+ },
362
429
  {
363
430
  name: "compress_context",
364
431
  description: "Comprime el historial de conversación: resume turns antiguos vía Ollama, mantiene solo los últimos N intactos. Reduce contexto 70-80%. USAR antes de cada turno extendido.",
@@ -530,6 +597,39 @@ const toolDefinitions = [
530
597
  required: ["platform"],
531
598
  },
532
599
  },
600
+ {
601
+ name: "depgraph",
602
+ description: "Builds a real-time dependency graph for any file in the workspace using the TypeScript Compiler API. Shows what a file imports, what imports it (reverse deps), and what it exports. Zero LLM calls. Essential before any refactor to understand blast radius.",
603
+ inputSchema: {
604
+ type: "object",
605
+ properties: {
606
+ filePath: { type: "string", description: "Target file path relative to workspace root" },
607
+ reverse: { type: "boolean", description: "Include reverse dependencies (who imports this file)", default: true },
608
+ depth: { type: "number", description: "Graph traversal depth 1-5 (default: 1)", default: 1 },
609
+ format: { type: "string", enum: ["text", "mermaid", "json"], description: "Output format: text tree, Mermaid flowchart, or JSON", default: "text" },
610
+ external: { type: "boolean", description: "Include node_modules dependencies (default: false)", default: false },
611
+ circular: { type: "boolean", description: "Detect and highlight circular dependencies", default: false },
612
+ },
613
+ required: ["filePath"],
614
+ },
615
+ },
616
+ {
617
+ name: "refactor",
618
+ description: "Declarative multi-file codemod engine. Rename symbols across the entire workspace or move files updating all imports. Uses TypeScript Compiler API — zero LLM calls. Supports dry-run diff preview and post-refactor tsc verification.",
619
+ inputSchema: {
620
+ type: "object",
621
+ properties: {
622
+ operation: { type: "string", enum: ["rename", "move"], description: "Refactor operation: 'rename' a symbol everywhere, or 'move' a file updating all importers" },
623
+ from: { type: "string", description: "Symbol name to rename (rename op) OR source file path relative to workspace root (move op)" },
624
+ to: { type: "string", description: "New symbol name (rename op) OR destination file path relative to workspace root (move op)" },
625
+ file: { type: "string", description: "File where the symbol is declared — required for 'rename' operation" },
626
+ dryRun: { type: "boolean", description: "Preview changes as a unified diff without applying them", default: false },
627
+ tscCheck: { type: "boolean", description: "Run tsc --noEmit after applying to guarantee zero TypeScript errors", default: false },
628
+ backup: { type: "boolean", description: "Create .bak backup of each modified file before changing", default: false },
629
+ },
630
+ required: ["operation", "from", "to"],
631
+ },
632
+ },
533
633
  ];
534
634
  const toolHandlers = {
535
635
  scrub_privacy: handleScrubPrivacy,
@@ -554,6 +654,11 @@ const toolHandlers = {
554
654
  diff_only: handleDiffOnly,
555
655
  batch_tool_calls: handleBatchToolCalls,
556
656
  turbosqueeze: handleTurboSqueeze,
657
+ get_symbol_surgical_context: handleGetSymbolSurgicalContext,
658
+ wormhole_squeeze: handleWormholeSqueeze,
659
+ generate_executive_roi_report: handleGenerateExecutiveRoiReport,
660
+ surgical_ast_insert: handleSurgicalAstInsert,
661
+ local_semantic_autofix: handleLocalSemanticAutofix,
557
662
  compress_context: handleCompressContext,
558
663
  smarter_cache: handleSmarterCache,
559
664
  token_budget: handleTokenBudget,
@@ -568,6 +673,8 @@ const toolHandlers = {
568
673
  review_pr: handleReviewPR,
569
674
  pr_status: handlePRStatus,
570
675
  generate_pr_workflow: handleGeneratePRWorkflow,
676
+ depgraph: handleDepgraph,
677
+ refactor: handleRefactor,
571
678
  };
572
679
  function setupToolsHandlers(server, onToolCall) {
573
680
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
@@ -640,22 +747,21 @@ async function handleSearchMemory(args) {
640
747
  if (!query)
641
748
  throw new Error("Query is required");
642
749
  try {
643
- const port = (0, utils_1.getProxyPort)();
644
- const response = await axios_1.default.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`);
645
- const results = response.data.results || [];
750
+ const brain = (0, TheBrainV2_1.getBrain)();
751
+ const results = brain.search(query, limit);
646
752
  if (results.length === 0) {
647
753
  return { content: [{ type: "text", text: "No relevant memories found in Lemma's Brain." }] };
648
754
  }
649
755
  const formatted = results
650
756
  .map((r, i) => {
651
- return `Result ${i + 1} (Similarity: ${(r.similarity * 100).toFixed(1)}%)\nPrompt: ${r.prompt.substring(0, 300)}...\nResponse: ${typeof r.response === "string" ? r.response : JSON.stringify(r.response, null, 2)}`;
757
+ return `Result ${i + 1} (Similarity: ${(r.similarity * 100).toFixed(1)}%)\nPrompt: ${r.query.substring(0, 300)}...\nResponse: ${typeof r.response === "string" ? r.response : JSON.stringify(r.response, null, 2)}`;
652
758
  })
653
759
  .join("\n\n---\n\n");
654
760
  return { content: [{ type: "text", text: `Lemma found ${results.length} memories:\n\n${formatted}` }] };
655
761
  }
656
762
  catch (e) {
657
763
  (0, utils_1.logError)("search_memory", e);
658
- return { content: [{ type: "text", text: `Neural Search failed: ${e.message}. Is Lemma Proxy running?` }] };
764
+ return { content: [{ type: "text", text: `Local Search failed: ${e.message}` }] };
659
765
  }
660
766
  }
661
767
  async function handleStoreMemory(args) {
@@ -665,12 +771,8 @@ async function handleStoreMemory(args) {
665
771
  if (!query || !responseText)
666
772
  throw new Error("Query and response are required");
667
773
  try {
668
- const port = (0, utils_1.getProxyPort)();
669
- const response = await axios_1.default.post(`http://localhost:${port}/api/memory/store`, {
670
- query,
671
- response: responseText,
672
- provider,
673
- });
774
+ const brain = (0, TheBrainV2_1.getBrain)();
775
+ const storeRes = brain.store(query, responseText, provider);
674
776
  const tokensSaved = Math.max(100, Math.floor(responseText.length / 4));
675
777
  (0, reportSavings_1.reportSavings)({
676
778
  source: "cache",
@@ -678,11 +780,11 @@ async function handleStoreMemory(args) {
678
780
  toolName: "store_memory",
679
781
  query: query.substring(0, 100),
680
782
  });
681
- return { content: [{ type: "text", text: `Success: ${response.data.message}` }] };
783
+ return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}` }] };
682
784
  }
683
785
  catch (e) {
684
786
  (0, utils_1.logError)("store_memory", e);
685
- return { content: [{ type: "text", text: `Failed to store memory in Lemma: ${e.message}. Is Lemma Proxy running?` }] };
787
+ return { content: [{ type: "text", text: `Failed to store memory in local Brain: ${e.message}` }] };
686
788
  }
687
789
  }
688
790
  async function handleGetRoutingAdvice(args) {
@@ -1250,24 +1352,20 @@ async function handleQueryHybridConsensus(args) {
1250
1352
  const threshold = typeof args?.threshold === "number" ? args.threshold : 0.8;
1251
1353
  if (!query)
1252
1354
  throw new Error("query is required");
1253
- const port = (0, utils_1.getProxyPort)();
1254
1355
  const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
1255
1356
  try {
1256
- const searchRes = await axios_1.default.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(fullQuery)}&limit=3`);
1257
- const results = searchRes.data.results || [];
1357
+ const brain = (0, TheBrainV2_1.getBrain)();
1358
+ const results = brain.search(fullQuery, 3);
1258
1359
  const topHit = results[0];
1259
1360
  if (topHit && topHit.similarity >= threshold) {
1260
- const savedTokens = Math.floor((topHit.response?.choices?.[0]?.message?.content?.length || 500) / 4);
1361
+ const savedTokens = Math.floor((topHit.response?.length || 500) / 4);
1261
1362
  (0, reportSavings_1.reportSavings)({
1262
1363
  source: "cache",
1263
1364
  tokens: savedTokens,
1264
1365
  toolName: "query_hybrid_consensus",
1265
1366
  query: query.substring(0, 100),
1266
1367
  });
1267
- const responseText = typeof topHit.response === "string"
1268
- ? topHit.response
1269
- : topHit.response?.choices?.[0]?.message?.content ||
1270
- JSON.stringify(topHit.response, null, 2);
1368
+ const responseText = topHit.response;
1271
1369
  return {
1272
1370
  content: [
1273
1371
  {
@@ -1293,7 +1391,7 @@ async function handleQueryHybridConsensus(args) {
1293
1391
  (0, utils_1.logError)("query_hybrid_consensus", e);
1294
1392
  return {
1295
1393
  content: [
1296
- { type: "text", text: `Hybrid Consensus failed: ${e.message}. Is Lemma Proxy running?` },
1394
+ { type: "text", text: `Hybrid Consensus failed: ${e.message}` },
1297
1395
  ],
1298
1396
  };
1299
1397
  }
@@ -1304,7 +1402,6 @@ async function handleGetTelepathicHints(args) {
1304
1402
  const limit = typeof args?.limit === "number" ? args.limit : 5;
1305
1403
  if (!activeFile)
1306
1404
  throw new Error("activeFile is required");
1307
- const port = (0, utils_1.getProxyPort)();
1308
1405
  const basename = path_1.default.basename(activeFile, path_1.default.extname(activeFile));
1309
1406
  const dirContext = path_1.default
1310
1407
  .dirname(activeFile)
@@ -1315,8 +1412,8 @@ async function handleGetTelepathicHints(args) {
1315
1412
  const ext = path_1.default.extname(activeFile).replace(".", "");
1316
1413
  const telepathicQuery = `${basename} ${dirContext} ${ext} patterns solutions architecture`;
1317
1414
  try {
1318
- const searchRes = await axios_1.default.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(telepathicQuery)}&limit=${limit}`);
1319
- const results = searchRes.data.results || [];
1415
+ const brain = (0, TheBrainV2_1.getBrain)();
1416
+ const results = brain.search(telepathicQuery, limit);
1320
1417
  if (results.length === 0) {
1321
1418
  return {
1322
1419
  content: [
@@ -1330,11 +1427,8 @@ async function handleGetTelepathicHints(args) {
1330
1427
  let hintsText = `Telepathic Hints for \`${activeFile}\`\n*(${results.length} relevant memories surfaced from The Brain)*\n\n`;
1331
1428
  results.forEach((r, i) => {
1332
1429
  const similarity = (r.similarity * 100).toFixed(1);
1333
- const prompt = typeof r.prompt === "string" ? r.prompt.substring(0, 120) : "Unknown";
1334
- const responseContent = typeof r.response === "string"
1335
- ? r.response
1336
- : r.response?.choices?.[0]?.message?.content ||
1337
- JSON.stringify(r.response).substring(0, 300);
1430
+ const prompt = r.query || "Unknown";
1431
+ const responseContent = r.response || "";
1338
1432
  hintsText += `### Hint ${i + 1} (${similarity}% match)\n`;
1339
1433
  hintsText += `**Memory:** ${prompt}\n\n`;
1340
1434
  hintsText += `${responseContent.substring(0, 400)}${responseContent.length > 400 ? "..." : ""}\n\n---\n\n`;
@@ -1354,7 +1448,7 @@ async function handleGetTelepathicHints(args) {
1354
1448
  content: [
1355
1449
  {
1356
1450
  type: "text",
1357
- text: `Telepathic Hints failed: ${e.message}. Is Lemma Proxy running?`,
1451
+ text: `Telepathic Hints failed: ${e.message}`,
1358
1452
  },
1359
1453
  ],
1360
1454
  };
@@ -1366,17 +1460,8 @@ async function handleSummarizeLongText(args) {
1366
1460
  const maxLength = args?.maxLength || 500;
1367
1461
  if (!text)
1368
1462
  throw new Error("text is required");
1369
- const host = process.env.OLLAMA_HOST || 'http://localhost:11434';
1370
- const model = process.env.OLLAMA_GEN_MODEL || 'llama3';
1371
1463
  try {
1372
- const prompt = `TASK: Summarize the following text concisely. Max ${maxLength} characters. Focus on key facts, decisions, and action items.\n\nTEXT:\n${text}`;
1373
- const resp = await axios_1.default.post(`${host}/api/generate`, {
1374
- model,
1375
- prompt,
1376
- stream: false,
1377
- options: { temperature: 0.1, num_predict: maxLength * 2 },
1378
- });
1379
- const summary = resp.data.response.trim();
1464
+ const summary = (0, ContextCollapser_1.collapseContext)(text, { maxLength });
1380
1465
  (0, reportSavings_1.reportSavings)({
1381
1466
  source: "contextSqueeze",
1382
1467
  charsBefore: text.length,
@@ -1387,7 +1472,7 @@ async function handleSummarizeLongText(args) {
1387
1472
  }
1388
1473
  catch (e) {
1389
1474
  (0, utils_1.logError)("summarize_long_text", e);
1390
- return { content: [{ type: "text", text: `Summarization failed: ${e.message}. Is Ollama running?` }] };
1475
+ return { content: [{ type: "text", text: `Summarization failed: ${e.message}` }] };
1391
1476
  }
1392
1477
  }
1393
1478
  // ── Prune Conversation History ──────────────────────────────────────
@@ -1536,21 +1621,14 @@ async function handleCompressContext(args) {
1536
1621
  const recent = messages.slice(-keepRecent);
1537
1622
  const old = messages.slice(0, -keepRecent);
1538
1623
  let summary = "";
1539
- let summaryMethod = "trim";
1624
+ let summaryMethod = "local-collapser";
1540
1625
  try {
1541
- const host = process.env.OLLAMA_HOST || 'http://localhost:11434';
1542
- const model = process.env.OLLAMA_GEN_MODEL || 'llama3';
1543
- const summaryPrompt = `Summarize the following conversation history into 2-3 concise sentences. Capture ONLY: key decisions made, code patterns established, unresolved issues. Be extremely brief.\n\n${JSON.stringify(old.map((m) => ({ role: m.role, content: typeof m.content === 'string' ? m.content.substring(0, 400) : '...' })))}`;
1544
- const resp = await axios_1.default.post(`${host}/api/generate`, {
1545
- model, prompt: summaryPrompt, stream: false,
1546
- options: { temperature: 0.1, num_predict: 300 },
1547
- });
1548
- summary = resp.data.response.trim();
1549
- summaryMethod = "ollama";
1626
+ const oldText = old.map((m, i) => `[Turn ${i + 1} / ${m.role}]: ${typeof m.content === 'string' ? m.content : '...'}`).join('\n');
1627
+ summary = (0, ContextCollapser_1.collapseContext)(oldText, { maxLength: 800 });
1550
1628
  }
1551
- catch {
1629
+ catch (err) {
1552
1630
  summary = old.map((m, i) => `[Turn ${i + 1} / ${m.role}]: ${typeof m.content === 'string' ? m.content.substring(0, 150).replace(/\n/g, ' ') : '...'}`).join('\n').substring(0, 2000);
1553
- summaryMethod = "truncate";
1631
+ summaryMethod = "truncate-fallback";
1554
1632
  }
1555
1633
  const oldLength = JSON.stringify(old).length;
1556
1634
  const summaryLength = summary.length;
@@ -1578,16 +1656,13 @@ async function handleSmarterCache(args) {
1578
1656
  const threshold = typeof args?.threshold === "number" ? args.threshold : 0.75;
1579
1657
  if (!query)
1580
1658
  throw new Error("query is required");
1581
- const port = (0, utils_1.getProxyPort)();
1582
1659
  const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
1583
1660
  try {
1584
- const searchRes = await axios_1.default.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(fullQuery)}&limit=5`);
1585
- const results = searchRes.data.results || [];
1661
+ const brain = (0, TheBrainV2_1.getBrain)();
1662
+ const results = brain.search(fullQuery, 5);
1586
1663
  const topHit = results[0];
1587
1664
  if (topHit && topHit.similarity >= threshold) {
1588
- const responseText = typeof topHit.response === "string"
1589
- ? topHit.response
1590
- : topHit.response?.choices?.[0]?.message?.content || JSON.stringify(topHit.response, null, 2);
1665
+ const responseText = topHit.response;
1591
1666
  const tokensSaved = Math.floor(responseText.length / 4);
1592
1667
  try {
1593
1668
  const { reportSavings } = await Promise.resolve().then(() => __importStar(require("../utils/reportSavings")));
@@ -1632,7 +1707,7 @@ async function handleSmarterCache(args) {
1632
1707
  hit: false,
1633
1708
  error: e.message,
1634
1709
  source: "cache-unavailable",
1635
- hint: "Lemma Proxy no está corriendo. El LLM funcionará normalmente sin cache.",
1710
+ hint: "El cache local del brain falló. El LLM funcionará normalmente sin cache.",
1636
1711
  }, null, 2),
1637
1712
  }],
1638
1713
  };
@@ -2285,10 +2360,9 @@ async function handleSemanticDedupGuard(args) {
2285
2360
  const threshold = typeof args?.threshold === "number" ? args.threshold : 0.92;
2286
2361
  if (!query || !response)
2287
2362
  throw new Error("query and response are required");
2288
- const port = (0, utils_1.getProxyPort)();
2289
2363
  try {
2290
- const searchRes = await axios_1.default.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(query)}&limit=3`);
2291
- const results = searchRes.data.results || [];
2364
+ const brain = (0, TheBrainV2_1.getBrain)();
2365
+ const results = brain.search(query, 3);
2292
2366
  const topHit = results[0];
2293
2367
  if (!topHit) {
2294
2368
  return {
@@ -2299,17 +2373,18 @@ async function handleSemanticDedupGuard(args) {
2299
2373
  }, null, 2) }],
2300
2374
  };
2301
2375
  }
2302
- const sim = topHit.similarity || 0;
2376
+ let sim = topHit.similarity || 0;
2377
+ if (topHit.query.trim().toLowerCase() === query.trim().toLowerCase()) {
2378
+ sim = 1.0;
2379
+ }
2303
2380
  if (sim >= threshold) {
2304
2381
  return {
2305
2382
  content: [{ type: "text", text: JSON.stringify({
2306
2383
  verdict: "DUPLICATE_REJECTED",
2307
2384
  reason: `Duplicado exacto detectado (${(sim * 100).toFixed(1)}% ≥ ${(threshold * 100).toFixed(0)}%). NO hagas store_memory — ya existe.`,
2308
2385
  similarity: sim,
2309
- existingQuery: typeof topHit.prompt === "string" ? topHit.prompt.substring(0, 200) : "N/A",
2310
- existingResponse: (typeof topHit.response === "string"
2311
- ? topHit.response
2312
- : topHit.response?.choices?.[0]?.message?.content || "").substring(0, 300),
2386
+ existingQuery: typeof topHit.query === "string" ? topHit.query.substring(0, 200) : "N/A",
2387
+ existingResponse: (topHit.response || "").substring(0, 300),
2313
2388
  tokensSaved: Math.floor(response.length / 4),
2314
2389
  }, null, 2) }],
2315
2390
  };
@@ -2320,7 +2395,7 @@ async function handleSemanticDedupGuard(args) {
2320
2395
  verdict: "SIMILAR_WARNING",
2321
2396
  reason: `Contenido similar detectado (${(sim * 100).toFixed(1)}%). Decide si es distinto suficiente para guardar.`,
2322
2397
  similarity: sim,
2323
- existingQuery: typeof topHit.prompt === "string" ? topHit.prompt.substring(0, 200) : "N/A",
2398
+ existingQuery: typeof topHit.query === "string" ? topHit.query.substring(0, 200) : "N/A",
2324
2399
  recommendation: "Si aporta info nueva → procede con store_memory. Si es redundante → descarta.",
2325
2400
  }, null, 2) }],
2326
2401
  };
@@ -2338,7 +2413,7 @@ async function handleSemanticDedupGuard(args) {
2338
2413
  return {
2339
2414
  content: [{ type: "text", text: JSON.stringify({
2340
2415
  verdict: "STORE_ALLOWED",
2341
- reason: "No se pudo consultar The Brain (proxy no disponible). Procediendo con store.",
2416
+ reason: "El cache local del brain falló. Procediendo con store.",
2342
2417
  error: e.message,
2343
2418
  }, null, 2) }],
2344
2419
  };
@@ -2660,4 +2735,769 @@ function generateAzurePipelineYaml(threshold) {
2660
2735
  ` AZURE_DEVOPS_TOKEN: \$(AZURE_DEVOPS_TOKEN)`,
2661
2736
  ].join("\n");
2662
2737
  }
2738
+ function buildDepGraphMap(workspaceRoot) {
2739
+ const map = new Map();
2740
+ const IGNORE_DG = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
2741
+ function walkDG(dir) {
2742
+ let entries = [];
2743
+ try {
2744
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
2745
+ }
2746
+ catch {
2747
+ return;
2748
+ }
2749
+ for (const e of entries) {
2750
+ if (IGNORE_DG.has(e.name))
2751
+ continue;
2752
+ const abs = path_1.default.join(dir, e.name);
2753
+ if (e.isDirectory()) {
2754
+ walkDG(abs);
2755
+ }
2756
+ else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
2757
+ const rel = path_1.default.relative(workspaceRoot, abs).replace(/\\/g, "/");
2758
+ map.set(rel, { rel, abs, imports: [], importedBy: [], externalImports: [], exports: [] });
2759
+ }
2760
+ }
2761
+ }
2762
+ walkDG(workspaceRoot);
2763
+ for (const [, node] of map) {
2764
+ try {
2765
+ const src = fs_1.default.readFileSync(node.abs, "utf8");
2766
+ const sf = ts.createSourceFile(node.abs, src, ts.ScriptTarget.Latest, true);
2767
+ ts.forEachChild(sf, (n) => {
2768
+ if (ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) {
2769
+ const spec = n.moduleSpecifier.text;
2770
+ if (spec.startsWith(".")) {
2771
+ const dir = path_1.default.dirname(node.abs);
2772
+ let resolved = path_1.default.resolve(dir, spec);
2773
+ for (const ext of [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"]) {
2774
+ if (fs_1.default.existsSync(resolved + ext)) {
2775
+ resolved += ext;
2776
+ break;
2777
+ }
2778
+ }
2779
+ const rel = path_1.default.relative(workspaceRoot, resolved).replace(/\\/g, "/");
2780
+ if (!node.imports.includes(rel))
2781
+ node.imports.push(rel);
2782
+ }
2783
+ else {
2784
+ const pkg = spec.split("/")[0];
2785
+ if (!node.externalImports.includes(pkg))
2786
+ node.externalImports.push(pkg);
2787
+ }
2788
+ }
2789
+ const hasExportMod = (nd) => ts.canHaveModifiers(nd) &&
2790
+ (ts.getModifiers(nd) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
2791
+ if (hasExportMod(n)) {
2792
+ if (ts.isFunctionDeclaration(n) && n.name)
2793
+ node.exports.push(n.name.text);
2794
+ else if (ts.isClassDeclaration(n) && n.name)
2795
+ node.exports.push(n.name.text);
2796
+ else if (ts.isVariableStatement(n)) {
2797
+ for (const decl of n.declarationList.declarations) {
2798
+ if (ts.isIdentifier(decl.name))
2799
+ node.exports.push(decl.name.text);
2800
+ }
2801
+ }
2802
+ else if (ts.isInterfaceDeclaration(n))
2803
+ node.exports.push(n.name.text);
2804
+ else if (ts.isTypeAliasDeclaration(n))
2805
+ node.exports.push(n.name.text);
2806
+ else if (ts.isEnumDeclaration(n))
2807
+ node.exports.push(n.name.text);
2808
+ }
2809
+ if (ts.isExportDeclaration(n) && n.exportClause && ts.isNamedExports(n.exportClause)) {
2810
+ for (const el of n.exportClause.elements)
2811
+ node.exports.push(el.name.text);
2812
+ }
2813
+ });
2814
+ }
2815
+ catch { /* skip unparseable */ }
2816
+ }
2817
+ for (const [, node] of map) {
2818
+ for (const imp of node.imports) {
2819
+ const target = map.get(imp);
2820
+ if (target && !target.importedBy.includes(node.rel))
2821
+ target.importedBy.push(node.rel);
2822
+ }
2823
+ }
2824
+ return map;
2825
+ }
2826
+ function bfsDepGraph(map, startRel, direction, maxDepth) {
2827
+ const visited = new Map();
2828
+ const queue = [{ rel: startRel, depth: 0 }];
2829
+ while (queue.length > 0) {
2830
+ const item = queue.shift();
2831
+ if (visited.has(item.rel) || item.depth > maxDepth)
2832
+ continue;
2833
+ const node = map.get(item.rel);
2834
+ if (!node)
2835
+ continue;
2836
+ visited.set(item.rel, { node, depth: item.depth });
2837
+ if (item.depth < maxDepth) {
2838
+ const neighbors = direction === "forward" ? node.imports : node.importedBy;
2839
+ for (const n of neighbors)
2840
+ queue.push({ rel: n, depth: item.depth + 1 });
2841
+ }
2842
+ }
2843
+ return visited;
2844
+ }
2845
+ function detectDepCycles(map, subset) {
2846
+ const cycles = [];
2847
+ const visited = new Set();
2848
+ const stack = new Set();
2849
+ function dfs(rel, currentPath) {
2850
+ if (stack.has(rel)) {
2851
+ const idx = currentPath.indexOf(rel);
2852
+ if (idx >= 0)
2853
+ cycles.push([...currentPath.slice(idx), rel]);
2854
+ return;
2855
+ }
2856
+ if (visited.has(rel))
2857
+ return;
2858
+ visited.add(rel);
2859
+ stack.add(rel);
2860
+ const node = map.get(rel);
2861
+ if (node) {
2862
+ for (const imp of node.imports) {
2863
+ if (!subset || subset.has(imp))
2864
+ dfs(imp, [...currentPath, rel]);
2865
+ }
2866
+ }
2867
+ stack.delete(rel);
2868
+ }
2869
+ const keys = subset ? [...subset] : [...map.keys()];
2870
+ for (const k of keys)
2871
+ dfs(k, []);
2872
+ return cycles;
2873
+ }
2874
+ function formatDGText(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse) {
2875
+ const lines = [`📦 ${targetNode.rel}`];
2876
+ const directImports = targetNode.imports;
2877
+ const externalImps = includeExternal ? targetNode.externalImports : [];
2878
+ const allImports = [...directImports, ...externalImps];
2879
+ if (allImports.length > 0) {
2880
+ lines.push("├── importa:");
2881
+ allImports.forEach((imp, i) => {
2882
+ const isLast = i === allImports.length - 1;
2883
+ const prefix = isLast ? "│ └──" : "│ ├──";
2884
+ const isExt = externalImps.includes(imp);
2885
+ if (isExt) {
2886
+ lines.push(`${prefix} ${imp} (external)`);
2887
+ }
2888
+ else {
2889
+ const impNode = forwardVisited.get(imp)?.node;
2890
+ const expSuffix = impNode && impNode.exports.length > 0
2891
+ ? ` → ${impNode.exports.slice(0, 3).join(", ")}${impNode.exports.length > 3 ? "…" : ""}`
2892
+ : "";
2893
+ lines.push(`${prefix} ${imp}${expSuffix}`);
2894
+ if (impNode && impNode.imports.length > 0) {
2895
+ impNode.imports.forEach((subImp, si) => {
2896
+ const subPrefix = si === impNode.imports.length - 1 ? "│ └──" : "│ ├──";
2897
+ if (subImp !== targetNode.rel)
2898
+ lines.push(`${subPrefix} ${subImp}`);
2899
+ });
2900
+ }
2901
+ }
2902
+ });
2903
+ }
2904
+ else {
2905
+ lines.push("├── importa: (ninguno)");
2906
+ }
2907
+ if (showReverse) {
2908
+ const revDeps = targetNode.importedBy;
2909
+ if (revDeps.length > 0) {
2910
+ lines.push("├── es importado por:");
2911
+ revDeps.forEach((dep, i) => {
2912
+ const prefix = i === revDeps.length - 1 ? "│ └──" : "│ ├──";
2913
+ lines.push(`${prefix} ${dep}`);
2914
+ const depNode = backwardVisited.get(dep)?.node;
2915
+ if (depNode && depNode.importedBy.length > 0) {
2916
+ depNode.importedBy.forEach((subDep, si) => {
2917
+ const subPrefix = si === depNode.importedBy.length - 1 ? "│ └──" : "│ ├──";
2918
+ if (subDep !== targetNode.rel)
2919
+ lines.push(`${subPrefix} ${subDep}`);
2920
+ });
2921
+ }
2922
+ });
2923
+ }
2924
+ else {
2925
+ lines.push("├── es importado por: (nadie — posible dead export)");
2926
+ }
2927
+ }
2928
+ if (targetNode.exports.length > 0) {
2929
+ lines.push("└── exporta:");
2930
+ targetNode.exports.forEach((exp, i) => {
2931
+ const prefix = i === targetNode.exports.length - 1 ? " └──" : " ├──";
2932
+ lines.push(`${prefix} ${exp}`);
2933
+ });
2934
+ }
2935
+ else {
2936
+ lines.push("└── exporta: (ningún export detectado)");
2937
+ }
2938
+ if (cycles.length > 0) {
2939
+ lines.push("");
2940
+ lines.push("⚠️ DEPENDENCIAS CIRCULARES DETECTADAS:");
2941
+ cycles.slice(0, 5).forEach((c) => lines.push(` 🔄 ${c.join(" → ")}`));
2942
+ }
2943
+ return lines.join("\n");
2944
+ }
2945
+ function formatDGMermaid(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse) {
2946
+ const lines = ["flowchart LR"];
2947
+ const addedEdges = new Set();
2948
+ const safeId = (s) => s.replace(/[^a-zA-Z0-9_]/g, "_");
2949
+ const tid = safeId(targetNode.rel);
2950
+ lines.push(` ${tid}["📦 ${path_1.default.basename(targetNode.rel)}"]`);
2951
+ lines.push(` style ${tid} fill:#6366f1,color:#fff,stroke:#4f46e5`);
2952
+ for (const imp of targetNode.imports) {
2953
+ const eid = `${tid}-->${safeId(imp)}`;
2954
+ if (!addedEdges.has(eid)) {
2955
+ addedEdges.add(eid);
2956
+ lines.push(` ${tid} --> ${safeId(imp)}["${path_1.default.basename(imp)}"]`);
2957
+ }
2958
+ }
2959
+ if (includeExternal) {
2960
+ for (const ext of targetNode.externalImports) {
2961
+ const extId = safeId(`ext_${ext}`);
2962
+ lines.push(` ${tid} --> ${extId}["📦 ${ext}"]`);
2963
+ lines.push(` style ${extId} fill:#374151,color:#9ca3af,stroke:#374151`);
2964
+ }
2965
+ }
2966
+ for (const [rel, { node }] of forwardVisited) {
2967
+ if (rel === targetNode.rel)
2968
+ continue;
2969
+ for (const imp of node.imports) {
2970
+ const eid = `${safeId(rel)}-->${safeId(imp)}`;
2971
+ if (!addedEdges.has(eid)) {
2972
+ addedEdges.add(eid);
2973
+ lines.push(` ${safeId(rel)} --> ${safeId(imp)}["${path_1.default.basename(imp)}"]`);
2974
+ }
2975
+ }
2976
+ }
2977
+ if (showReverse) {
2978
+ for (const dep of targetNode.importedBy) {
2979
+ const eid = `${safeId(dep)}-->${tid}`;
2980
+ if (!addedEdges.has(eid)) {
2981
+ addedEdges.add(eid);
2982
+ lines.push(` ${safeId(dep)}["${path_1.default.basename(dep)}"] --> ${tid}`);
2983
+ lines.push(` style ${safeId(dep)} fill:#065f46,color:#fff,stroke:#047857`);
2984
+ }
2985
+ }
2986
+ for (const [rel, { node }] of backwardVisited) {
2987
+ if (rel === targetNode.rel)
2988
+ continue;
2989
+ for (const dep of node.importedBy) {
2990
+ const eid = `${safeId(dep)}-->${safeId(rel)}`;
2991
+ if (!addedEdges.has(eid)) {
2992
+ addedEdges.add(eid);
2993
+ lines.push(` ${safeId(dep)}["${path_1.default.basename(dep)}"] --> ${safeId(rel)}["${path_1.default.basename(rel)}"]`);
2994
+ }
2995
+ }
2996
+ }
2997
+ }
2998
+ if (cycles.length > 0) {
2999
+ lines.push(` %% ⚠️ Cycles: ${cycles.length}`);
3000
+ cycles.slice(0, 3).forEach(c => lines.push(` %% ${c.join(" → ")}`));
3001
+ }
3002
+ return lines.join("\n");
3003
+ }
3004
+ async function handleDepgraph(args) {
3005
+ const filePath = args?.filePath;
3006
+ const showReverse = args?.reverse !== false;
3007
+ const depth = Math.min(Math.max(typeof args?.depth === "number" ? args.depth : 1, 1), 5);
3008
+ const format = args?.format || "text";
3009
+ const includeExternal = !!(args?.external);
3010
+ const detectCircular = !!(args?.circular);
3011
+ const workspaceRoot = process.cwd();
3012
+ if (!filePath)
3013
+ throw new Error("filePath is required");
3014
+ try {
3015
+ const { resolved } = (0, utils_1.safeResolvePath)(workspaceRoot, filePath);
3016
+ const relTarget = path_1.default.relative(workspaceRoot, resolved).replace(/\\/g, "/");
3017
+ const map = buildDepGraphMap(workspaceRoot);
3018
+ const targetNode = map.get(relTarget);
3019
+ if (!targetNode) {
3020
+ return { content: [{ type: "text", text: `File not found in workspace graph: ${relTarget}\nCheck path is correct and has .ts/.tsx/.js/.jsx extension.` }] };
3021
+ }
3022
+ const forwardVisited = bfsDepGraph(map, relTarget, "forward", depth);
3023
+ const backwardVisited = showReverse ? bfsDepGraph(map, relTarget, "backward", depth) : new Map();
3024
+ const allRelInScope = new Set([...forwardVisited.keys(), ...(showReverse ? backwardVisited.keys() : [])]);
3025
+ const cycles = detectCircular ? detectDepCycles(map, allRelInScope) : [];
3026
+ let output;
3027
+ if (format === "json") {
3028
+ const jsonResult = {
3029
+ target: relTarget,
3030
+ imports: targetNode.imports,
3031
+ importedBy: showReverse ? targetNode.importedBy : undefined,
3032
+ externalImports: includeExternal ? targetNode.externalImports : undefined,
3033
+ exports: targetNode.exports,
3034
+ cycles: cycles.length > 0 ? cycles : undefined,
3035
+ subGraph: {
3036
+ forward: Object.fromEntries([...forwardVisited.entries()].map(([k, v]) => [k, { depth: v.depth, imports: v.node.imports }])),
3037
+ backward: showReverse ? Object.fromEntries([...backwardVisited.entries()].map(([k, v]) => [k, { depth: v.depth, importedBy: v.node.importedBy }])) : undefined,
3038
+ },
3039
+ };
3040
+ output = JSON.stringify(jsonResult, null, 2);
3041
+ }
3042
+ else if (format === "mermaid") {
3043
+ output = formatDGMermaid(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse);
3044
+ }
3045
+ else {
3046
+ output = formatDGText(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse);
3047
+ }
3048
+ const stats = `\n\n📊 Stats: ${forwardVisited.size} nodos forward, ${backwardVisited.size} nodos reverse, depth=${depth}`;
3049
+ return { content: [{ type: "text", text: output + (format === "json" ? "" : stats) }] };
3050
+ }
3051
+ catch (err) {
3052
+ (0, utils_1.logError)("depgraph", err);
3053
+ return { content: [{ type: "text", text: `depgraph failed: ${err.message}` }] };
3054
+ }
3055
+ }
3056
+ // ═══════════════════════════════════════════════════════════════════════════════
3057
+ // ─── refactor ─────────────────────────────────────────────────────────────────
3058
+ // ═══════════════════════════════════════════════════════════════════════════════
3059
+ function escapeRegexStr(s) {
3060
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3061
+ }
3062
+ function findSymbolRefs(workspaceRoot, symbolName) {
3063
+ const results = [];
3064
+ const IGNORE_RF = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
3065
+ function walkRF(dir) {
3066
+ let entries = [];
3067
+ try {
3068
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
3069
+ }
3070
+ catch {
3071
+ return;
3072
+ }
3073
+ for (const e of entries) {
3074
+ if (IGNORE_RF.has(e.name))
3075
+ continue;
3076
+ const abs = path_1.default.join(dir, e.name);
3077
+ if (e.isDirectory()) {
3078
+ walkRF(abs);
3079
+ }
3080
+ else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
3081
+ const rel = path_1.default.relative(workspaceRoot, abs).replace(/\\/g, "/");
3082
+ try {
3083
+ const content = fs_1.default.readFileSync(abs, "utf8");
3084
+ if (!content.includes(symbolName))
3085
+ continue;
3086
+ const sf = ts.createSourceFile(abs, content, ts.ScriptTarget.Latest, true);
3087
+ const contentLines = content.split("\n");
3088
+ const identifierLines = new Set();
3089
+ function collectIdents(node) {
3090
+ if (ts.isIdentifier(node) && node.text === symbolName) {
3091
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
3092
+ identifierLines.add(line);
3093
+ }
3094
+ ts.forEachChild(node, collectIdents);
3095
+ }
3096
+ collectIdents(sf);
3097
+ const matchingLines = [...identifierLines].map(li => ({ lineIndex: li, lineContent: contentLines[li] ?? "" }));
3098
+ if (matchingLines.length > 0)
3099
+ results.push({ abs, rel, lines: matchingLines });
3100
+ }
3101
+ catch { /* skip */ }
3102
+ }
3103
+ }
3104
+ }
3105
+ walkRF(workspaceRoot);
3106
+ return results;
3107
+ }
3108
+ function applySymbolRenameInContent(content, fromName, toName) {
3109
+ const regex = new RegExp(`(?<![a-zA-Z0-9_$])${escapeRegexStr(fromName)}(?![a-zA-Z0-9_$])`, "g");
3110
+ return content.replace(regex, toName);
3111
+ }
3112
+ function buildUnifiedDiff(filePath, before, after) {
3113
+ if (before === after)
3114
+ return "";
3115
+ const bl = before.split("\n");
3116
+ const al = after.split("\n");
3117
+ const diffLines = [`--- a/${filePath}`, `+++ b/${filePath}`];
3118
+ const changed = new Set();
3119
+ const maxLen = Math.max(bl.length, al.length);
3120
+ for (let i = 0; i < maxLen; i++) {
3121
+ if (bl[i] !== al[i])
3122
+ changed.add(i);
3123
+ }
3124
+ let i = 0;
3125
+ while (i < maxLen) {
3126
+ if (!changed.has(i)) {
3127
+ i++;
3128
+ continue;
3129
+ }
3130
+ const hunkStart = Math.max(0, i - 2);
3131
+ let hunkEnd = i + 1;
3132
+ while (hunkEnd < maxLen && changed.has(hunkEnd))
3133
+ hunkEnd++;
3134
+ hunkEnd = Math.min(maxLen, hunkEnd + 2);
3135
+ const bc = Math.min(hunkEnd, bl.length) - hunkStart;
3136
+ const ac = Math.min(hunkEnd, al.length) - hunkStart;
3137
+ diffLines.push(`@@ -${hunkStart + 1},${bc} +${hunkStart + 1},${ac} @@`);
3138
+ for (let j = hunkStart; j < hunkEnd; j++) {
3139
+ if (j >= maxLen)
3140
+ break;
3141
+ if (changed.has(j)) {
3142
+ if (j < bl.length)
3143
+ diffLines.push(`-${bl[j]}`);
3144
+ if (j < al.length)
3145
+ diffLines.push(`+${al[j]}`);
3146
+ }
3147
+ else {
3148
+ if (j < bl.length)
3149
+ diffLines.push(` ${bl[j]}`);
3150
+ }
3151
+ }
3152
+ i = hunkEnd;
3153
+ }
3154
+ return diffLines.join("\n");
3155
+ }
3156
+ async function handleRefactorRename(args, workspaceRoot) {
3157
+ const fromName = args.from;
3158
+ const toName = args.to;
3159
+ const dryRun = !!(args.dryRun);
3160
+ const tscCheck = !!(args.tscCheck);
3161
+ const backup = !!(args.backup);
3162
+ if (!fromName || !toName)
3163
+ throw new Error("'from' and 'to' are required for rename");
3164
+ const refs = findSymbolRefs(workspaceRoot, fromName);
3165
+ if (refs.length === 0) {
3166
+ return { content: [{ type: "text", text: `No references found to symbol "${fromName}" in the workspace.` }] };
3167
+ }
3168
+ let filesModified = 0;
3169
+ let totalRefs = 0;
3170
+ const report = [];
3171
+ const diffs = [];
3172
+ for (const ref of refs) {
3173
+ const before = fs_1.default.readFileSync(ref.abs, "utf8");
3174
+ const after = applySymbolRenameInContent(before, fromName, toName);
3175
+ if (before === after)
3176
+ continue;
3177
+ const count = (before.match(new RegExp(`(?<![a-zA-Z0-9_$])${escapeRegexStr(fromName)}(?![a-zA-Z0-9_$])`, "g")) || []).length;
3178
+ totalRefs += count;
3179
+ filesModified++;
3180
+ if (dryRun) {
3181
+ const d = buildUnifiedDiff(ref.rel, before, after);
3182
+ if (d)
3183
+ diffs.push(d);
3184
+ }
3185
+ else {
3186
+ if (backup)
3187
+ fs_1.default.writeFileSync(ref.abs + ".bak", before, "utf8");
3188
+ fs_1.default.writeFileSync(ref.abs, after, "utf8");
3189
+ report.push(` ✅ ${ref.rel} (${count} refs)`);
3190
+ }
3191
+ }
3192
+ if (dryRun) {
3193
+ return { content: [{ type: "text", text: [`🔍 DRY RUN — Rename "${fromName}" → "${toName}"`, ``, `Archivos que cambiarían: ${filesModified} | Referencias: ${totalRefs}`, ``, `=== DIFF PREVIEW ===`, ...diffs].join("\n") }] };
3194
+ }
3195
+ let tscOutput = "";
3196
+ if (tscCheck && filesModified > 0) {
3197
+ try {
3198
+ const out = (0, child_process_1.execSync)("npx tsc --noEmit 2>&1 | head -30 || true", { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
3199
+ const errors = out.split("\n").filter((l) => l.includes("error TS"));
3200
+ tscOutput = errors.length === 0 ? "\n\n✅ tsc --noEmit: 0 errores" : `\n\n⚠️ tsc: ${errors.length} error(es):\n${errors.join("\n")}`;
3201
+ }
3202
+ catch (e) {
3203
+ tscOutput = `\n\n⚠️ tsc check falló: ${e.message}`;
3204
+ }
3205
+ }
3206
+ return { content: [{ type: "text", text: [`✅ Rename: "${fromName}" → "${toName}"`, ``, `📁 ${filesModified} archivos | 🔗 ${totalRefs} refs`, ``, ...report].join("\n") + tscOutput }] };
3207
+ }
3208
+ async function handleRefactorMove(args, workspaceRoot) {
3209
+ const fromRel = args.from;
3210
+ const toRel = args.to;
3211
+ const dryRun = !!(args.dryRun);
3212
+ const tscCheck = !!(args.tscCheck);
3213
+ const backup = !!(args.backup);
3214
+ const { resolved: fromAbs } = (0, utils_1.safeResolvePath)(workspaceRoot, fromRel);
3215
+ const toAbs = path_1.default.resolve(workspaceRoot, toRel);
3216
+ const fromRelNorm = path_1.default.relative(workspaceRoot, fromAbs).replace(/\\/g, "/");
3217
+ const toRelNorm = path_1.default.relative(workspaceRoot, toAbs).replace(/\\/g, "/");
3218
+ if (!fs_1.default.existsSync(fromAbs)) {
3219
+ return { content: [{ type: "text", text: `Source file not found: ${fromRelNorm}` }] };
3220
+ }
3221
+ const IGNORE_MV = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
3222
+ const importers = [];
3223
+ function walkMV(dir) {
3224
+ let entries = [];
3225
+ try {
3226
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
3227
+ }
3228
+ catch {
3229
+ return;
3230
+ }
3231
+ for (const e of entries) {
3232
+ if (IGNORE_MV.has(e.name))
3233
+ continue;
3234
+ const abs = path_1.default.join(dir, e.name);
3235
+ if (e.isDirectory()) {
3236
+ walkMV(abs);
3237
+ }
3238
+ else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
3239
+ const rel = path_1.default.relative(workspaceRoot, abs).replace(/\\/g, "/");
3240
+ if (rel === fromRelNorm)
3241
+ return;
3242
+ try {
3243
+ const content = fs_1.default.readFileSync(abs, "utf8");
3244
+ const sf = ts.createSourceFile(abs, content, ts.ScriptTarget.Latest, true);
3245
+ let found = false;
3246
+ ts.forEachChild(sf, (n) => {
3247
+ if (!found && ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) {
3248
+ const spec = n.moduleSpecifier.text;
3249
+ if (spec.startsWith(".")) {
3250
+ const res = path_1.default.resolve(path_1.default.dirname(abs), spec).replace(/\\/g, "/");
3251
+ const fromN = fromAbs.replace(/\\/g, "/");
3252
+ const fromNoExt = fromN.replace(/\.(ts|tsx|js|jsx)$/, "");
3253
+ if (res === fromN || res === fromNoExt)
3254
+ found = true;
3255
+ }
3256
+ }
3257
+ });
3258
+ if (found)
3259
+ importers.push({ abs, rel, content });
3260
+ }
3261
+ catch { /* skip */ }
3262
+ }
3263
+ }
3264
+ }
3265
+ walkMV(workspaceRoot);
3266
+ const addDot = (p) => p.startsWith(".") ? p : `./${p}`;
3267
+ const patches = [];
3268
+ for (const imp of importers) {
3269
+ const oldRelImport = path_1.default.relative(path_1.default.dirname(imp.abs), fromAbs).replace(/\\/g, "/");
3270
+ const newRelImport = path_1.default.relative(path_1.default.dirname(imp.abs), toAbs).replace(/\\/g, "/");
3271
+ const oldSpec = addDot(oldRelImport.replace(/\.(ts|tsx|js|jsx)$/, ""));
3272
+ const newSpec = addDot(newRelImport.replace(/\.(ts|tsx|js|jsx)$/, ""));
3273
+ const after = imp.content.replace(new RegExp(`(['"])${escapeRegexStr(oldSpec)}(['"])`, "g"), `$1${newSpec}$2`);
3274
+ if (after !== imp.content)
3275
+ patches.push({ rel: imp.rel, abs: imp.abs, before: imp.content, after });
3276
+ }
3277
+ if (dryRun) {
3278
+ const diffs = patches.map(p => buildUnifiedDiff(p.rel, p.before, p.after)).filter(Boolean);
3279
+ return { content: [{ type: "text", text: [`🔍 DRY RUN — Move "${fromRelNorm}" → "${toRelNorm}"`, ``, `Importadores que se actualizarían: ${patches.length}`, ``, `=== DIFF PREVIEW ===`, ...diffs].join("\n") }] };
3280
+ }
3281
+ fs_1.default.mkdirSync(path_1.default.dirname(toAbs), { recursive: true });
3282
+ if (backup)
3283
+ fs_1.default.copyFileSync(fromAbs, fromAbs + ".bak");
3284
+ fs_1.default.copyFileSync(fromAbs, toAbs);
3285
+ fs_1.default.unlinkSync(fromAbs);
3286
+ for (const p of patches) {
3287
+ if (backup)
3288
+ fs_1.default.writeFileSync(p.abs + ".bak", p.before, "utf8");
3289
+ fs_1.default.writeFileSync(p.abs, p.after, "utf8");
3290
+ }
3291
+ let tscOutput = "";
3292
+ if (tscCheck) {
3293
+ try {
3294
+ const out = (0, child_process_1.execSync)("npx tsc --noEmit 2>&1 | head -30 || true", { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
3295
+ const errors = out.split("\n").filter((l) => l.includes("error TS"));
3296
+ tscOutput = errors.length === 0 ? "\n\n✅ tsc --noEmit: 0 errores" : `\n\n⚠️ tsc: ${errors.length} error(es):\n${errors.join("\n")}`;
3297
+ }
3298
+ catch (e) {
3299
+ tscOutput = `\n\n⚠️ tsc check falló: ${e.message}`;
3300
+ }
3301
+ }
3302
+ return { content: [{ type: "text", text: [`✅ Move: "${fromRelNorm}" → "${toRelNorm}"`, ``, `📁 ${patches.length} importadores actualizados:`, ...patches.map(p => ` ✅ ${p.rel}`)].join("\n") + tscOutput }] };
3303
+ }
3304
+ async function handleRefactor(args) {
3305
+ const operation = args?.operation;
3306
+ const workspaceRoot = process.cwd();
3307
+ if (!operation)
3308
+ throw new Error("'operation' is required (rename | move)");
3309
+ try {
3310
+ switch (operation) {
3311
+ case "rename": return await handleRefactorRename(args, workspaceRoot);
3312
+ case "move": return await handleRefactorMove(args, workspaceRoot);
3313
+ default:
3314
+ return { content: [{ type: "text", text: `Unknown operation: "${operation}". Supported: rename, move` }] };
3315
+ }
3316
+ }
3317
+ catch (err) {
3318
+ (0, utils_1.logError)("refactor", err);
3319
+ return { content: [{ type: "text", text: `refactor failed: ${err.message}` }] };
3320
+ }
3321
+ }
3322
+ // ── Surgical Symbol Context ──────────────────────────────────────────
3323
+ async function handleGetSymbolSurgicalContext(args) {
3324
+ const filePath = args?.filePath;
3325
+ const symbolName = args?.symbolName;
3326
+ if (!filePath || !symbolName)
3327
+ throw new Error("filePath and symbolName are required");
3328
+ const workspaceRoot = process.cwd();
3329
+ try {
3330
+ const res = (0, SymbolSurgicalContext_1.getSymbolSurgicalContext)(workspaceRoot, filePath, symbolName);
3331
+ const lines = [];
3332
+ lines.push(`# 🧬 Surgical Context: Symbol \`${symbolName}\` in \`${filePath}\``);
3333
+ lines.push(`## Implementation`);
3334
+ lines.push(`\`\`\`typescript\n${res.implementation}\n\`\`\``);
3335
+ if (res.dependencies.length > 0) {
3336
+ lines.push(`## Workspace Dependencies (Signatures ONLY)`);
3337
+ for (const dep of res.dependencies) {
3338
+ lines.push(`### \`${dep.name}\` (${dep.type}) declared in \`${dep.filePath}\``);
3339
+ lines.push(`\`\`\`typescript\n${dep.signature}\n\`\`\``);
3340
+ }
3341
+ }
3342
+ else {
3343
+ lines.push(`*No local workspace dependencies detected for this symbol.*`);
3344
+ }
3345
+ const output = lines.join("\n\n");
3346
+ const originalSize = fs_1.default.readFileSync(path_1.default.resolve(workspaceRoot, filePath), "utf8").length;
3347
+ const totalRawSize = originalSize + res.dependencies.reduce((sum, dep) => {
3348
+ try {
3349
+ return sum + fs_1.default.readFileSync(path_1.default.resolve(workspaceRoot, dep.filePath), "utf8").length;
3350
+ }
3351
+ catch {
3352
+ return sum;
3353
+ }
3354
+ }, 0);
3355
+ (0, reportSavings_1.reportSavings)({
3356
+ source: "contextSqueeze",
3357
+ charsBefore: totalRawSize,
3358
+ charsAfter: output.length,
3359
+ toolName: "get_symbol_surgical_context",
3360
+ filePath
3361
+ });
3362
+ return {
3363
+ content: [{ type: "text", text: output }]
3364
+ };
3365
+ }
3366
+ catch (err) {
3367
+ (0, utils_1.logError)("get_symbol_surgical_context", err);
3368
+ return {
3369
+ content: [{ type: "text", text: `Surgical context extraction failed: ${err.message}` }]
3370
+ };
3371
+ }
3372
+ }
3373
+ // ── Wormhole Squeeze ────────────────────────────────────────────────
3374
+ async function handleWormholeSqueeze(args) {
3375
+ const text = args?.text;
3376
+ if (!text)
3377
+ throw new Error("text is required");
3378
+ try {
3379
+ const compressed = (0, WormholeCompressor_1.compressWormhole)(text);
3380
+ (0, reportSavings_1.reportSavings)({
3381
+ source: "contextSqueeze",
3382
+ charsBefore: text.length,
3383
+ charsAfter: compressed.length,
3384
+ toolName: "wormhole_squeeze"
3385
+ });
3386
+ return {
3387
+ content: [{ type: "text", text: compressed }]
3388
+ };
3389
+ }
3390
+ catch (err) {
3391
+ (0, utils_1.logError)("wormhole_squeeze", err);
3392
+ return {
3393
+ content: [{ type: "text", text: `Wormhole compression failed: ${err.message}` }]
3394
+ };
3395
+ }
3396
+ }
3397
+ // ── Executive ROI Report ──────────────────────────────────────────────
3398
+ async function handleGenerateExecutiveRoiReport(args) {
3399
+ const teamSize = typeof args?.teamSize === "number" ? args.teamSize : 10;
3400
+ const costPerDevHour = typeof args?.costPerDevHour === "number" ? args.costPerDevHour : 50;
3401
+ const targetPath = args?.targetPath || "LEMMA_ROI_REPORT.md";
3402
+ const workspaceRoot = process.cwd();
3403
+ try {
3404
+ const res = (0, ExecutiveReport_1.generateExecutiveReport)(workspaceRoot, {
3405
+ teamSize,
3406
+ costPerDevHour,
3407
+ targetPath
3408
+ });
3409
+ return {
3410
+ content: [{
3411
+ type: "text",
3412
+ text: `Success: ROI Report generated at ${targetPath}.\n\nTotal Business Value Saved: $${res.metrics.totalBusinessValue.toFixed(2)} USD\nEstimated Dev Time Saved: ${res.metrics.devHoursSaved} Hours\nDirect API Cost Saved: $${res.metrics.totalCostSaved.toFixed(4)} USD`
3413
+ }]
3414
+ };
3415
+ }
3416
+ catch (err) {
3417
+ (0, utils_1.logError)("generate_executive_roi_report", err);
3418
+ return {
3419
+ content: [{ type: "text", text: `ROI Report generation failed: ${err.message}` }]
3420
+ };
3421
+ }
3422
+ }
3423
+ // ── Surgical AST Insert ──────────────────────────────────────────────
3424
+ async function handleSurgicalAstInsert(args) {
3425
+ const filePath = args?.filePath;
3426
+ const symbolName = args?.symbolName;
3427
+ const anchorSymbol = args?.anchorSymbol;
3428
+ const insertPosition = args?.insertPosition;
3429
+ const codeToInsert = args?.codeToInsert;
3430
+ if (!filePath || !codeToInsert)
3431
+ throw new Error("filePath and codeToInsert are required");
3432
+ const workspaceRoot = process.cwd();
3433
+ try {
3434
+ const resolvedPath = (0, SurgicalASTInsert_1.surgicalASTInsert)(workspaceRoot, {
3435
+ filePath,
3436
+ symbolName,
3437
+ anchorSymbol,
3438
+ insertPosition,
3439
+ codeToInsert
3440
+ });
3441
+ const newTextSize = fs_1.default.readFileSync(resolvedPath, "utf8").length;
3442
+ const outputCharsSaved = Math.max(0, newTextSize - codeToInsert.length);
3443
+ const tokensSaved = Math.floor(outputCharsSaved / 4);
3444
+ (0, reportSavings_1.reportSavings)({
3445
+ source: "contextSqueeze",
3446
+ tokens: tokensSaved,
3447
+ toolName: "surgical_ast_insert",
3448
+ filePath
3449
+ });
3450
+ return {
3451
+ content: [{
3452
+ type: "text",
3453
+ text: `Success: Surgically inserted code into ${filePath} using AST compiler.`
3454
+ }]
3455
+ };
3456
+ }
3457
+ catch (err) {
3458
+ (0, utils_1.logError)("surgical_ast_insert", err);
3459
+ return {
3460
+ content: [{ type: "text", text: `Surgical AST insertion failed: ${err.message}` }]
3461
+ };
3462
+ }
3463
+ }
3464
+ // ── Local Semantic Autofix ────────────────────────────────────────────
3465
+ async function handleLocalSemanticAutofix(args) {
3466
+ const errorMessage = args?.errorMessage;
3467
+ if (!errorMessage)
3468
+ throw new Error("errorMessage is required");
3469
+ try {
3470
+ const brain = (0, TheBrainV2_1.getBrain)();
3471
+ const results = brain.search(errorMessage, 3, 0.70);
3472
+ const topHit = results[0];
3473
+ if (topHit) {
3474
+ const responseText = topHit.response;
3475
+ const tokensSaved = Math.max(100, Math.floor(responseText.length / 4));
3476
+ (0, reportSavings_1.reportSavings)({
3477
+ source: "cache",
3478
+ tokens: tokensSaved,
3479
+ toolName: "local_semantic_autofix",
3480
+ query: errorMessage.substring(0, 100)
3481
+ });
3482
+ return {
3483
+ content: [{
3484
+ type: "text",
3485
+ text: `🎯 Local Auto-Fix HIT (similarity: ${(topHit.similarity * 100).toFixed(1)}%)\n\nFound matching resolution in global memory. Cost: 0 tokens.\n\n---\n\n${responseText}`
3486
+ }]
3487
+ };
3488
+ }
3489
+ return {
3490
+ content: [{
3491
+ type: "text",
3492
+ text: `Brain MISS (No matching resolutions for this error in local memory).\n\nPlease proceed to ask the LLM for guidance. Once resolved, make sure to call store_memory with:\n- query: "[Error description/traceback]"\n- response: "[Exact code patch or steps used to fix]"\n\nThis will allow resolving this error instantly with 0 tokens next time!`
3493
+ }]
3494
+ };
3495
+ }
3496
+ catch (err) {
3497
+ (0, utils_1.logError)("local_semantic_autofix", err);
3498
+ return {
3499
+ content: [{ type: "text", text: `Local auto-fix failed: ${err.message}` }]
3500
+ };
3501
+ }
3502
+ }
2663
3503
  //# sourceMappingURL=tools.js.map