@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.
- package/README.md +79 -12
- package/bin/cli.js +97 -0
- package/bin/init.js +307 -0
- package/cache-proxy.js +191 -0
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +2 -4
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/cjs/mcp/index.js +1 -1
- package/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +911 -71
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts +102 -0
- package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -0
- package/dist/cjs/subconscious/TheBrainV2.js +444 -0
- package/dist/cjs/subconscious/TheBrainV2.js.map +1 -0
- package/dist/cjs/utils/ContextCollapser.d.ts +21 -0
- package/dist/cjs/utils/ContextCollapser.d.ts.map +1 -0
- package/dist/cjs/utils/ContextCollapser.js +132 -0
- package/dist/cjs/utils/ContextCollapser.js.map +1 -0
- package/dist/cjs/utils/ExecutiveReport.d.ts +29 -0
- package/dist/cjs/utils/ExecutiveReport.d.ts.map +1 -0
- package/dist/cjs/utils/ExecutiveReport.js +111 -0
- package/dist/cjs/utils/ExecutiveReport.js.map +1 -0
- package/dist/cjs/utils/SurgicalASTInsert.d.ts +21 -0
- package/dist/cjs/utils/SurgicalASTInsert.d.ts.map +1 -0
- package/dist/cjs/utils/SurgicalASTInsert.js +173 -0
- package/dist/cjs/utils/SurgicalASTInsert.js.map +1 -0
- package/dist/cjs/utils/SymbolSurgicalContext.d.ts +25 -0
- package/dist/cjs/utils/SymbolSurgicalContext.d.ts.map +1 -0
- package/dist/cjs/utils/SymbolSurgicalContext.js +265 -0
- package/dist/cjs/utils/SymbolSurgicalContext.js.map +1 -0
- package/dist/cjs/utils/WormholeCompressor.d.ts +22 -0
- package/dist/cjs/utils/WormholeCompressor.d.ts.map +1 -0
- package/dist/cjs/utils/WormholeCompressor.js +152 -0
- package/dist/cjs/utils/WormholeCompressor.js.map +1 -0
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +2 -4
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/mcp/index.js +1 -1
- package/dist/esm/mcp/tools.d.ts.map +1 -1
- package/dist/esm/mcp/tools.js +911 -71
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts +102 -0
- package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -0
- package/dist/esm/subconscious/TheBrainV2.js +431 -0
- package/dist/esm/subconscious/TheBrainV2.js.map +1 -0
- package/dist/esm/utils/ContextCollapser.d.ts +21 -0
- package/dist/esm/utils/ContextCollapser.d.ts.map +1 -0
- package/dist/esm/utils/ContextCollapser.js +128 -0
- package/dist/esm/utils/ContextCollapser.js.map +1 -0
- package/dist/esm/utils/ExecutiveReport.d.ts +29 -0
- package/dist/esm/utils/ExecutiveReport.d.ts.map +1 -0
- package/dist/esm/utils/ExecutiveReport.js +105 -0
- package/dist/esm/utils/ExecutiveReport.js.map +1 -0
- package/dist/esm/utils/SurgicalASTInsert.d.ts +21 -0
- package/dist/esm/utils/SurgicalASTInsert.d.ts.map +1 -0
- package/dist/esm/utils/SurgicalASTInsert.js +134 -0
- package/dist/esm/utils/SurgicalASTInsert.js.map +1 -0
- package/dist/esm/utils/SymbolSurgicalContext.d.ts +25 -0
- package/dist/esm/utils/SymbolSurgicalContext.d.ts.map +1 -0
- package/dist/esm/utils/SymbolSurgicalContext.js +226 -0
- package/dist/esm/utils/SymbolSurgicalContext.js.map +1 -0
- package/dist/esm/utils/WormholeCompressor.d.ts +22 -0
- package/dist/esm/utils/WormholeCompressor.d.ts.map +1 -0
- package/dist/esm/utils/WormholeCompressor.js +148 -0
- package/dist/esm/utils/WormholeCompressor.js.map +1 -0
- package/lemma-proxy.cjs +31 -14
- package/mcp-server.js +220 -0
- package/package.json +5 -1
package/dist/esm/mcp/tools.js
CHANGED
|
@@ -13,6 +13,12 @@ import { getProxyPort, safeResolvePath, logError, logWarn, estimateTokensFromRes
|
|
|
13
13
|
import { runAnalysis, parseDiff } from "../pr-review/PRReviewEngine";
|
|
14
14
|
import { ReviewStore } from "../pr-review/ReviewStore";
|
|
15
15
|
import { runPRReview } from "../pr-review/reviewRunner";
|
|
16
|
+
import { getBrain } from "../subconscious/TheBrainV2";
|
|
17
|
+
import { collapseContext } from "../utils/ContextCollapser";
|
|
18
|
+
import { getSymbolSurgicalContext } from "../utils/SymbolSurgicalContext";
|
|
19
|
+
import { compressWormhole } from "../utils/WormholeCompressor";
|
|
20
|
+
import { generateExecutiveReport } from "../utils/ExecutiveReport";
|
|
21
|
+
import { surgicalASTInsert } from "../utils/SurgicalASTInsert";
|
|
16
22
|
// ── Pro License Gate ──────────────────────────────────────────────────────────
|
|
17
23
|
const LICENSE_FILE = path.join(os.homedir(), '.lemma-cache', 'license.json');
|
|
18
24
|
let _proStatusCache = null;
|
|
@@ -320,6 +326,67 @@ const toolDefinitions = [
|
|
|
320
326
|
required: ["text"],
|
|
321
327
|
},
|
|
322
328
|
},
|
|
329
|
+
{
|
|
330
|
+
name: "get_symbol_surgical_context",
|
|
331
|
+
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.",
|
|
332
|
+
inputSchema: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {
|
|
335
|
+
filePath: { type: "string", description: "Ruta relativa del archivo que contiene el símbolo" },
|
|
336
|
+
symbolName: { type: "string", description: "Nombre de la función, clase, tipo o interfaz a extraer" },
|
|
337
|
+
},
|
|
338
|
+
required: ["filePath", "symbolName"],
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "wormhole_squeeze",
|
|
343
|
+
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.",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: "object",
|
|
346
|
+
properties: {
|
|
347
|
+
text: { type: "string", description: "El código fuente a comprimir" },
|
|
348
|
+
},
|
|
349
|
+
required: ["text"],
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "generate_executive_roi_report",
|
|
354
|
+
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.",
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: "object",
|
|
357
|
+
properties: {
|
|
358
|
+
teamSize: { type: "number", description: "Número de ingenieros en el equipo (default: 10)", default: 10 },
|
|
359
|
+
costPerDevHour: { type: "number", description: "Costo estimado en USD por hora de desarrollador (default: 50)", default: 50 },
|
|
360
|
+
targetPath: { type: "string", description: "Nombre de archivo del reporte generado (default: LEMMA_ROI_REPORT.md)", default: "LEMMA_ROI_REPORT.md" },
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: "surgical_ast_insert",
|
|
366
|
+
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.",
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: "object",
|
|
369
|
+
properties: {
|
|
370
|
+
filePath: { type: "string", description: "Ruta del archivo TypeScript (relativo al root)" },
|
|
371
|
+
symbolName: { type: "string", description: "Nombre de la clase o interfaz donde insertar (opcional)" },
|
|
372
|
+
anchorSymbol: { type: "string", description: "Nombre del método o propiedad como anclaje (ej. 'login')" },
|
|
373
|
+
insertPosition: { type: "string", enum: ["before", "after", "start", "end"], description: "Posición relativa al anclaje (default: 'after')", default: "after" },
|
|
374
|
+
codeToInsert: { type: "string", description: "El código fuente exacto a insertar" },
|
|
375
|
+
},
|
|
376
|
+
required: ["filePath", "codeToInsert"],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: "local_semantic_autofix",
|
|
381
|
+
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.",
|
|
382
|
+
inputSchema: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {
|
|
385
|
+
errorMessage: { type: "string", description: "El traceback del error o mensaje de error a resolver" },
|
|
386
|
+
},
|
|
387
|
+
required: ["errorMessage"],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
323
390
|
{
|
|
324
391
|
name: "compress_context",
|
|
325
392
|
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.",
|
|
@@ -491,6 +558,39 @@ const toolDefinitions = [
|
|
|
491
558
|
required: ["platform"],
|
|
492
559
|
},
|
|
493
560
|
},
|
|
561
|
+
{
|
|
562
|
+
name: "depgraph",
|
|
563
|
+
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.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
type: "object",
|
|
566
|
+
properties: {
|
|
567
|
+
filePath: { type: "string", description: "Target file path relative to workspace root" },
|
|
568
|
+
reverse: { type: "boolean", description: "Include reverse dependencies (who imports this file)", default: true },
|
|
569
|
+
depth: { type: "number", description: "Graph traversal depth 1-5 (default: 1)", default: 1 },
|
|
570
|
+
format: { type: "string", enum: ["text", "mermaid", "json"], description: "Output format: text tree, Mermaid flowchart, or JSON", default: "text" },
|
|
571
|
+
external: { type: "boolean", description: "Include node_modules dependencies (default: false)", default: false },
|
|
572
|
+
circular: { type: "boolean", description: "Detect and highlight circular dependencies", default: false },
|
|
573
|
+
},
|
|
574
|
+
required: ["filePath"],
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: "refactor",
|
|
579
|
+
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.",
|
|
580
|
+
inputSchema: {
|
|
581
|
+
type: "object",
|
|
582
|
+
properties: {
|
|
583
|
+
operation: { type: "string", enum: ["rename", "move"], description: "Refactor operation: 'rename' a symbol everywhere, or 'move' a file updating all importers" },
|
|
584
|
+
from: { type: "string", description: "Symbol name to rename (rename op) OR source file path relative to workspace root (move op)" },
|
|
585
|
+
to: { type: "string", description: "New symbol name (rename op) OR destination file path relative to workspace root (move op)" },
|
|
586
|
+
file: { type: "string", description: "File where the symbol is declared — required for 'rename' operation" },
|
|
587
|
+
dryRun: { type: "boolean", description: "Preview changes as a unified diff without applying them", default: false },
|
|
588
|
+
tscCheck: { type: "boolean", description: "Run tsc --noEmit after applying to guarantee zero TypeScript errors", default: false },
|
|
589
|
+
backup: { type: "boolean", description: "Create .bak backup of each modified file before changing", default: false },
|
|
590
|
+
},
|
|
591
|
+
required: ["operation", "from", "to"],
|
|
592
|
+
},
|
|
593
|
+
},
|
|
494
594
|
];
|
|
495
595
|
const toolHandlers = {
|
|
496
596
|
scrub_privacy: handleScrubPrivacy,
|
|
@@ -515,6 +615,11 @@ const toolHandlers = {
|
|
|
515
615
|
diff_only: handleDiffOnly,
|
|
516
616
|
batch_tool_calls: handleBatchToolCalls,
|
|
517
617
|
turbosqueeze: handleTurboSqueeze,
|
|
618
|
+
get_symbol_surgical_context: handleGetSymbolSurgicalContext,
|
|
619
|
+
wormhole_squeeze: handleWormholeSqueeze,
|
|
620
|
+
generate_executive_roi_report: handleGenerateExecutiveRoiReport,
|
|
621
|
+
surgical_ast_insert: handleSurgicalAstInsert,
|
|
622
|
+
local_semantic_autofix: handleLocalSemanticAutofix,
|
|
518
623
|
compress_context: handleCompressContext,
|
|
519
624
|
smarter_cache: handleSmarterCache,
|
|
520
625
|
token_budget: handleTokenBudget,
|
|
@@ -529,6 +634,8 @@ const toolHandlers = {
|
|
|
529
634
|
review_pr: handleReviewPR,
|
|
530
635
|
pr_status: handlePRStatus,
|
|
531
636
|
generate_pr_workflow: handleGeneratePRWorkflow,
|
|
637
|
+
depgraph: handleDepgraph,
|
|
638
|
+
refactor: handleRefactor,
|
|
532
639
|
};
|
|
533
640
|
export function setupToolsHandlers(server, onToolCall) {
|
|
534
641
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -601,22 +708,21 @@ async function handleSearchMemory(args) {
|
|
|
601
708
|
if (!query)
|
|
602
709
|
throw new Error("Query is required");
|
|
603
710
|
try {
|
|
604
|
-
const
|
|
605
|
-
const
|
|
606
|
-
const results = response.data.results || [];
|
|
711
|
+
const brain = getBrain();
|
|
712
|
+
const results = brain.search(query, limit);
|
|
607
713
|
if (results.length === 0) {
|
|
608
714
|
return { content: [{ type: "text", text: "No relevant memories found in Lemma's Brain." }] };
|
|
609
715
|
}
|
|
610
716
|
const formatted = results
|
|
611
717
|
.map((r, i) => {
|
|
612
|
-
return `Result ${i + 1} (Similarity: ${(r.similarity * 100).toFixed(1)}%)\nPrompt: ${r.
|
|
718
|
+
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)}`;
|
|
613
719
|
})
|
|
614
720
|
.join("\n\n---\n\n");
|
|
615
721
|
return { content: [{ type: "text", text: `Lemma found ${results.length} memories:\n\n${formatted}` }] };
|
|
616
722
|
}
|
|
617
723
|
catch (e) {
|
|
618
724
|
logError("search_memory", e);
|
|
619
|
-
return { content: [{ type: "text", text: `
|
|
725
|
+
return { content: [{ type: "text", text: `Local Search failed: ${e.message}` }] };
|
|
620
726
|
}
|
|
621
727
|
}
|
|
622
728
|
async function handleStoreMemory(args) {
|
|
@@ -626,12 +732,8 @@ async function handleStoreMemory(args) {
|
|
|
626
732
|
if (!query || !responseText)
|
|
627
733
|
throw new Error("Query and response are required");
|
|
628
734
|
try {
|
|
629
|
-
const
|
|
630
|
-
const
|
|
631
|
-
query,
|
|
632
|
-
response: responseText,
|
|
633
|
-
provider,
|
|
634
|
-
});
|
|
735
|
+
const brain = getBrain();
|
|
736
|
+
const storeRes = brain.store(query, responseText, provider);
|
|
635
737
|
const tokensSaved = Math.max(100, Math.floor(responseText.length / 4));
|
|
636
738
|
reportSavings({
|
|
637
739
|
source: "cache",
|
|
@@ -639,11 +741,11 @@ async function handleStoreMemory(args) {
|
|
|
639
741
|
toolName: "store_memory",
|
|
640
742
|
query: query.substring(0, 100),
|
|
641
743
|
});
|
|
642
|
-
return { content: [{ type: "text", text: `Success: ${
|
|
744
|
+
return { content: [{ type: "text", text: `Success: Memory stored. ${storeRes.reason}` }] };
|
|
643
745
|
}
|
|
644
746
|
catch (e) {
|
|
645
747
|
logError("store_memory", e);
|
|
646
|
-
return { content: [{ type: "text", text: `Failed to store memory in
|
|
748
|
+
return { content: [{ type: "text", text: `Failed to store memory in local Brain: ${e.message}` }] };
|
|
647
749
|
}
|
|
648
750
|
}
|
|
649
751
|
async function handleGetRoutingAdvice(args) {
|
|
@@ -1211,24 +1313,20 @@ async function handleQueryHybridConsensus(args) {
|
|
|
1211
1313
|
const threshold = typeof args?.threshold === "number" ? args.threshold : 0.8;
|
|
1212
1314
|
if (!query)
|
|
1213
1315
|
throw new Error("query is required");
|
|
1214
|
-
const port = getProxyPort();
|
|
1215
1316
|
const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
|
|
1216
1317
|
try {
|
|
1217
|
-
const
|
|
1218
|
-
const results =
|
|
1318
|
+
const brain = getBrain();
|
|
1319
|
+
const results = brain.search(fullQuery, 3);
|
|
1219
1320
|
const topHit = results[0];
|
|
1220
1321
|
if (topHit && topHit.similarity >= threshold) {
|
|
1221
|
-
const savedTokens = Math.floor((topHit.response?.
|
|
1322
|
+
const savedTokens = Math.floor((topHit.response?.length || 500) / 4);
|
|
1222
1323
|
reportSavings({
|
|
1223
1324
|
source: "cache",
|
|
1224
1325
|
tokens: savedTokens,
|
|
1225
1326
|
toolName: "query_hybrid_consensus",
|
|
1226
1327
|
query: query.substring(0, 100),
|
|
1227
1328
|
});
|
|
1228
|
-
const responseText =
|
|
1229
|
-
? topHit.response
|
|
1230
|
-
: topHit.response?.choices?.[0]?.message?.content ||
|
|
1231
|
-
JSON.stringify(topHit.response, null, 2);
|
|
1329
|
+
const responseText = topHit.response;
|
|
1232
1330
|
return {
|
|
1233
1331
|
content: [
|
|
1234
1332
|
{
|
|
@@ -1254,7 +1352,7 @@ async function handleQueryHybridConsensus(args) {
|
|
|
1254
1352
|
logError("query_hybrid_consensus", e);
|
|
1255
1353
|
return {
|
|
1256
1354
|
content: [
|
|
1257
|
-
{ type: "text", text: `Hybrid Consensus failed: ${e.message}
|
|
1355
|
+
{ type: "text", text: `Hybrid Consensus failed: ${e.message}` },
|
|
1258
1356
|
],
|
|
1259
1357
|
};
|
|
1260
1358
|
}
|
|
@@ -1265,7 +1363,6 @@ async function handleGetTelepathicHints(args) {
|
|
|
1265
1363
|
const limit = typeof args?.limit === "number" ? args.limit : 5;
|
|
1266
1364
|
if (!activeFile)
|
|
1267
1365
|
throw new Error("activeFile is required");
|
|
1268
|
-
const port = getProxyPort();
|
|
1269
1366
|
const basename = path.basename(activeFile, path.extname(activeFile));
|
|
1270
1367
|
const dirContext = path
|
|
1271
1368
|
.dirname(activeFile)
|
|
@@ -1276,8 +1373,8 @@ async function handleGetTelepathicHints(args) {
|
|
|
1276
1373
|
const ext = path.extname(activeFile).replace(".", "");
|
|
1277
1374
|
const telepathicQuery = `${basename} ${dirContext} ${ext} patterns solutions architecture`;
|
|
1278
1375
|
try {
|
|
1279
|
-
const
|
|
1280
|
-
const results =
|
|
1376
|
+
const brain = getBrain();
|
|
1377
|
+
const results = brain.search(telepathicQuery, limit);
|
|
1281
1378
|
if (results.length === 0) {
|
|
1282
1379
|
return {
|
|
1283
1380
|
content: [
|
|
@@ -1291,11 +1388,8 @@ async function handleGetTelepathicHints(args) {
|
|
|
1291
1388
|
let hintsText = `Telepathic Hints for \`${activeFile}\`\n*(${results.length} relevant memories surfaced from The Brain)*\n\n`;
|
|
1292
1389
|
results.forEach((r, i) => {
|
|
1293
1390
|
const similarity = (r.similarity * 100).toFixed(1);
|
|
1294
|
-
const prompt =
|
|
1295
|
-
const responseContent =
|
|
1296
|
-
? r.response
|
|
1297
|
-
: r.response?.choices?.[0]?.message?.content ||
|
|
1298
|
-
JSON.stringify(r.response).substring(0, 300);
|
|
1391
|
+
const prompt = r.query || "Unknown";
|
|
1392
|
+
const responseContent = r.response || "";
|
|
1299
1393
|
hintsText += `### Hint ${i + 1} (${similarity}% match)\n`;
|
|
1300
1394
|
hintsText += `**Memory:** ${prompt}\n\n`;
|
|
1301
1395
|
hintsText += `${responseContent.substring(0, 400)}${responseContent.length > 400 ? "..." : ""}\n\n---\n\n`;
|
|
@@ -1315,7 +1409,7 @@ async function handleGetTelepathicHints(args) {
|
|
|
1315
1409
|
content: [
|
|
1316
1410
|
{
|
|
1317
1411
|
type: "text",
|
|
1318
|
-
text: `Telepathic Hints failed: ${e.message}
|
|
1412
|
+
text: `Telepathic Hints failed: ${e.message}`,
|
|
1319
1413
|
},
|
|
1320
1414
|
],
|
|
1321
1415
|
};
|
|
@@ -1327,17 +1421,8 @@ async function handleSummarizeLongText(args) {
|
|
|
1327
1421
|
const maxLength = args?.maxLength || 500;
|
|
1328
1422
|
if (!text)
|
|
1329
1423
|
throw new Error("text is required");
|
|
1330
|
-
const host = process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
1331
|
-
const model = process.env.OLLAMA_GEN_MODEL || 'llama3';
|
|
1332
1424
|
try {
|
|
1333
|
-
const
|
|
1334
|
-
const resp = await axios.post(`${host}/api/generate`, {
|
|
1335
|
-
model,
|
|
1336
|
-
prompt,
|
|
1337
|
-
stream: false,
|
|
1338
|
-
options: { temperature: 0.1, num_predict: maxLength * 2 },
|
|
1339
|
-
});
|
|
1340
|
-
const summary = resp.data.response.trim();
|
|
1425
|
+
const summary = collapseContext(text, { maxLength });
|
|
1341
1426
|
reportSavings({
|
|
1342
1427
|
source: "contextSqueeze",
|
|
1343
1428
|
charsBefore: text.length,
|
|
@@ -1348,7 +1433,7 @@ async function handleSummarizeLongText(args) {
|
|
|
1348
1433
|
}
|
|
1349
1434
|
catch (e) {
|
|
1350
1435
|
logError("summarize_long_text", e);
|
|
1351
|
-
return { content: [{ type: "text", text: `Summarization failed: ${e.message}
|
|
1436
|
+
return { content: [{ type: "text", text: `Summarization failed: ${e.message}` }] };
|
|
1352
1437
|
}
|
|
1353
1438
|
}
|
|
1354
1439
|
// ── Prune Conversation History ──────────────────────────────────────
|
|
@@ -1497,21 +1582,14 @@ async function handleCompressContext(args) {
|
|
|
1497
1582
|
const recent = messages.slice(-keepRecent);
|
|
1498
1583
|
const old = messages.slice(0, -keepRecent);
|
|
1499
1584
|
let summary = "";
|
|
1500
|
-
let summaryMethod = "
|
|
1585
|
+
let summaryMethod = "local-collapser";
|
|
1501
1586
|
try {
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
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) : '...' })))}`;
|
|
1505
|
-
const resp = await axios.post(`${host}/api/generate`, {
|
|
1506
|
-
model, prompt: summaryPrompt, stream: false,
|
|
1507
|
-
options: { temperature: 0.1, num_predict: 300 },
|
|
1508
|
-
});
|
|
1509
|
-
summary = resp.data.response.trim();
|
|
1510
|
-
summaryMethod = "ollama";
|
|
1587
|
+
const oldText = old.map((m, i) => `[Turn ${i + 1} / ${m.role}]: ${typeof m.content === 'string' ? m.content : '...'}`).join('\n');
|
|
1588
|
+
summary = collapseContext(oldText, { maxLength: 800 });
|
|
1511
1589
|
}
|
|
1512
|
-
catch {
|
|
1590
|
+
catch (err) {
|
|
1513
1591
|
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);
|
|
1514
|
-
summaryMethod = "truncate";
|
|
1592
|
+
summaryMethod = "truncate-fallback";
|
|
1515
1593
|
}
|
|
1516
1594
|
const oldLength = JSON.stringify(old).length;
|
|
1517
1595
|
const summaryLength = summary.length;
|
|
@@ -1539,16 +1617,13 @@ async function handleSmarterCache(args) {
|
|
|
1539
1617
|
const threshold = typeof args?.threshold === "number" ? args.threshold : 0.75;
|
|
1540
1618
|
if (!query)
|
|
1541
1619
|
throw new Error("query is required");
|
|
1542
|
-
const port = getProxyPort();
|
|
1543
1620
|
const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
|
|
1544
1621
|
try {
|
|
1545
|
-
const
|
|
1546
|
-
const results =
|
|
1622
|
+
const brain = getBrain();
|
|
1623
|
+
const results = brain.search(fullQuery, 5);
|
|
1547
1624
|
const topHit = results[0];
|
|
1548
1625
|
if (topHit && topHit.similarity >= threshold) {
|
|
1549
|
-
const responseText =
|
|
1550
|
-
? topHit.response
|
|
1551
|
-
: topHit.response?.choices?.[0]?.message?.content || JSON.stringify(topHit.response, null, 2);
|
|
1626
|
+
const responseText = topHit.response;
|
|
1552
1627
|
const tokensSaved = Math.floor(responseText.length / 4);
|
|
1553
1628
|
try {
|
|
1554
1629
|
const { reportSavings } = await import("../utils/reportSavings");
|
|
@@ -1593,7 +1668,7 @@ async function handleSmarterCache(args) {
|
|
|
1593
1668
|
hit: false,
|
|
1594
1669
|
error: e.message,
|
|
1595
1670
|
source: "cache-unavailable",
|
|
1596
|
-
hint: "
|
|
1671
|
+
hint: "El cache local del brain falló. El LLM funcionará normalmente sin cache.",
|
|
1597
1672
|
}, null, 2),
|
|
1598
1673
|
}],
|
|
1599
1674
|
};
|
|
@@ -2246,10 +2321,9 @@ async function handleSemanticDedupGuard(args) {
|
|
|
2246
2321
|
const threshold = typeof args?.threshold === "number" ? args.threshold : 0.92;
|
|
2247
2322
|
if (!query || !response)
|
|
2248
2323
|
throw new Error("query and response are required");
|
|
2249
|
-
const port = getProxyPort();
|
|
2250
2324
|
try {
|
|
2251
|
-
const
|
|
2252
|
-
const results =
|
|
2325
|
+
const brain = getBrain();
|
|
2326
|
+
const results = brain.search(query, 3);
|
|
2253
2327
|
const topHit = results[0];
|
|
2254
2328
|
if (!topHit) {
|
|
2255
2329
|
return {
|
|
@@ -2260,17 +2334,18 @@ async function handleSemanticDedupGuard(args) {
|
|
|
2260
2334
|
}, null, 2) }],
|
|
2261
2335
|
};
|
|
2262
2336
|
}
|
|
2263
|
-
|
|
2337
|
+
let sim = topHit.similarity || 0;
|
|
2338
|
+
if (topHit.query.trim().toLowerCase() === query.trim().toLowerCase()) {
|
|
2339
|
+
sim = 1.0;
|
|
2340
|
+
}
|
|
2264
2341
|
if (sim >= threshold) {
|
|
2265
2342
|
return {
|
|
2266
2343
|
content: [{ type: "text", text: JSON.stringify({
|
|
2267
2344
|
verdict: "DUPLICATE_REJECTED",
|
|
2268
2345
|
reason: `Duplicado exacto detectado (${(sim * 100).toFixed(1)}% ≥ ${(threshold * 100).toFixed(0)}%). NO hagas store_memory — ya existe.`,
|
|
2269
2346
|
similarity: sim,
|
|
2270
|
-
existingQuery: typeof topHit.
|
|
2271
|
-
existingResponse: (
|
|
2272
|
-
? topHit.response
|
|
2273
|
-
: topHit.response?.choices?.[0]?.message?.content || "").substring(0, 300),
|
|
2347
|
+
existingQuery: typeof topHit.query === "string" ? topHit.query.substring(0, 200) : "N/A",
|
|
2348
|
+
existingResponse: (topHit.response || "").substring(0, 300),
|
|
2274
2349
|
tokensSaved: Math.floor(response.length / 4),
|
|
2275
2350
|
}, null, 2) }],
|
|
2276
2351
|
};
|
|
@@ -2281,7 +2356,7 @@ async function handleSemanticDedupGuard(args) {
|
|
|
2281
2356
|
verdict: "SIMILAR_WARNING",
|
|
2282
2357
|
reason: `Contenido similar detectado (${(sim * 100).toFixed(1)}%). Decide si es distinto suficiente para guardar.`,
|
|
2283
2358
|
similarity: sim,
|
|
2284
|
-
existingQuery: typeof topHit.
|
|
2359
|
+
existingQuery: typeof topHit.query === "string" ? topHit.query.substring(0, 200) : "N/A",
|
|
2285
2360
|
recommendation: "Si aporta info nueva → procede con store_memory. Si es redundante → descarta.",
|
|
2286
2361
|
}, null, 2) }],
|
|
2287
2362
|
};
|
|
@@ -2299,7 +2374,7 @@ async function handleSemanticDedupGuard(args) {
|
|
|
2299
2374
|
return {
|
|
2300
2375
|
content: [{ type: "text", text: JSON.stringify({
|
|
2301
2376
|
verdict: "STORE_ALLOWED",
|
|
2302
|
-
reason: "
|
|
2377
|
+
reason: "El cache local del brain falló. Procediendo con store.",
|
|
2303
2378
|
error: e.message,
|
|
2304
2379
|
}, null, 2) }],
|
|
2305
2380
|
};
|
|
@@ -2621,4 +2696,769 @@ function generateAzurePipelineYaml(threshold) {
|
|
|
2621
2696
|
` AZURE_DEVOPS_TOKEN: \$(AZURE_DEVOPS_TOKEN)`,
|
|
2622
2697
|
].join("\n");
|
|
2623
2698
|
}
|
|
2699
|
+
function buildDepGraphMap(workspaceRoot) {
|
|
2700
|
+
const map = new Map();
|
|
2701
|
+
const IGNORE_DG = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
|
|
2702
|
+
function walkDG(dir) {
|
|
2703
|
+
let entries = [];
|
|
2704
|
+
try {
|
|
2705
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2706
|
+
}
|
|
2707
|
+
catch {
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
for (const e of entries) {
|
|
2711
|
+
if (IGNORE_DG.has(e.name))
|
|
2712
|
+
continue;
|
|
2713
|
+
const abs = path.join(dir, e.name);
|
|
2714
|
+
if (e.isDirectory()) {
|
|
2715
|
+
walkDG(abs);
|
|
2716
|
+
}
|
|
2717
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
2718
|
+
const rel = path.relative(workspaceRoot, abs).replace(/\\/g, "/");
|
|
2719
|
+
map.set(rel, { rel, abs, imports: [], importedBy: [], externalImports: [], exports: [] });
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
walkDG(workspaceRoot);
|
|
2724
|
+
for (const [, node] of map) {
|
|
2725
|
+
try {
|
|
2726
|
+
const src = fs.readFileSync(node.abs, "utf8");
|
|
2727
|
+
const sf = ts.createSourceFile(node.abs, src, ts.ScriptTarget.Latest, true);
|
|
2728
|
+
ts.forEachChild(sf, (n) => {
|
|
2729
|
+
if (ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) {
|
|
2730
|
+
const spec = n.moduleSpecifier.text;
|
|
2731
|
+
if (spec.startsWith(".")) {
|
|
2732
|
+
const dir = path.dirname(node.abs);
|
|
2733
|
+
let resolved = path.resolve(dir, spec);
|
|
2734
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"]) {
|
|
2735
|
+
if (fs.existsSync(resolved + ext)) {
|
|
2736
|
+
resolved += ext;
|
|
2737
|
+
break;
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
const rel = path.relative(workspaceRoot, resolved).replace(/\\/g, "/");
|
|
2741
|
+
if (!node.imports.includes(rel))
|
|
2742
|
+
node.imports.push(rel);
|
|
2743
|
+
}
|
|
2744
|
+
else {
|
|
2745
|
+
const pkg = spec.split("/")[0];
|
|
2746
|
+
if (!node.externalImports.includes(pkg))
|
|
2747
|
+
node.externalImports.push(pkg);
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
const hasExportMod = (nd) => ts.canHaveModifiers(nd) &&
|
|
2751
|
+
(ts.getModifiers(nd) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
2752
|
+
if (hasExportMod(n)) {
|
|
2753
|
+
if (ts.isFunctionDeclaration(n) && n.name)
|
|
2754
|
+
node.exports.push(n.name.text);
|
|
2755
|
+
else if (ts.isClassDeclaration(n) && n.name)
|
|
2756
|
+
node.exports.push(n.name.text);
|
|
2757
|
+
else if (ts.isVariableStatement(n)) {
|
|
2758
|
+
for (const decl of n.declarationList.declarations) {
|
|
2759
|
+
if (ts.isIdentifier(decl.name))
|
|
2760
|
+
node.exports.push(decl.name.text);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
else if (ts.isInterfaceDeclaration(n))
|
|
2764
|
+
node.exports.push(n.name.text);
|
|
2765
|
+
else if (ts.isTypeAliasDeclaration(n))
|
|
2766
|
+
node.exports.push(n.name.text);
|
|
2767
|
+
else if (ts.isEnumDeclaration(n))
|
|
2768
|
+
node.exports.push(n.name.text);
|
|
2769
|
+
}
|
|
2770
|
+
if (ts.isExportDeclaration(n) && n.exportClause && ts.isNamedExports(n.exportClause)) {
|
|
2771
|
+
for (const el of n.exportClause.elements)
|
|
2772
|
+
node.exports.push(el.name.text);
|
|
2773
|
+
}
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
catch { /* skip unparseable */ }
|
|
2777
|
+
}
|
|
2778
|
+
for (const [, node] of map) {
|
|
2779
|
+
for (const imp of node.imports) {
|
|
2780
|
+
const target = map.get(imp);
|
|
2781
|
+
if (target && !target.importedBy.includes(node.rel))
|
|
2782
|
+
target.importedBy.push(node.rel);
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
return map;
|
|
2786
|
+
}
|
|
2787
|
+
function bfsDepGraph(map, startRel, direction, maxDepth) {
|
|
2788
|
+
const visited = new Map();
|
|
2789
|
+
const queue = [{ rel: startRel, depth: 0 }];
|
|
2790
|
+
while (queue.length > 0) {
|
|
2791
|
+
const item = queue.shift();
|
|
2792
|
+
if (visited.has(item.rel) || item.depth > maxDepth)
|
|
2793
|
+
continue;
|
|
2794
|
+
const node = map.get(item.rel);
|
|
2795
|
+
if (!node)
|
|
2796
|
+
continue;
|
|
2797
|
+
visited.set(item.rel, { node, depth: item.depth });
|
|
2798
|
+
if (item.depth < maxDepth) {
|
|
2799
|
+
const neighbors = direction === "forward" ? node.imports : node.importedBy;
|
|
2800
|
+
for (const n of neighbors)
|
|
2801
|
+
queue.push({ rel: n, depth: item.depth + 1 });
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return visited;
|
|
2805
|
+
}
|
|
2806
|
+
function detectDepCycles(map, subset) {
|
|
2807
|
+
const cycles = [];
|
|
2808
|
+
const visited = new Set();
|
|
2809
|
+
const stack = new Set();
|
|
2810
|
+
function dfs(rel, currentPath) {
|
|
2811
|
+
if (stack.has(rel)) {
|
|
2812
|
+
const idx = currentPath.indexOf(rel);
|
|
2813
|
+
if (idx >= 0)
|
|
2814
|
+
cycles.push([...currentPath.slice(idx), rel]);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
if (visited.has(rel))
|
|
2818
|
+
return;
|
|
2819
|
+
visited.add(rel);
|
|
2820
|
+
stack.add(rel);
|
|
2821
|
+
const node = map.get(rel);
|
|
2822
|
+
if (node) {
|
|
2823
|
+
for (const imp of node.imports) {
|
|
2824
|
+
if (!subset || subset.has(imp))
|
|
2825
|
+
dfs(imp, [...currentPath, rel]);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
stack.delete(rel);
|
|
2829
|
+
}
|
|
2830
|
+
const keys = subset ? [...subset] : [...map.keys()];
|
|
2831
|
+
for (const k of keys)
|
|
2832
|
+
dfs(k, []);
|
|
2833
|
+
return cycles;
|
|
2834
|
+
}
|
|
2835
|
+
function formatDGText(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse) {
|
|
2836
|
+
const lines = [`📦 ${targetNode.rel}`];
|
|
2837
|
+
const directImports = targetNode.imports;
|
|
2838
|
+
const externalImps = includeExternal ? targetNode.externalImports : [];
|
|
2839
|
+
const allImports = [...directImports, ...externalImps];
|
|
2840
|
+
if (allImports.length > 0) {
|
|
2841
|
+
lines.push("├── importa:");
|
|
2842
|
+
allImports.forEach((imp, i) => {
|
|
2843
|
+
const isLast = i === allImports.length - 1;
|
|
2844
|
+
const prefix = isLast ? "│ └──" : "│ ├──";
|
|
2845
|
+
const isExt = externalImps.includes(imp);
|
|
2846
|
+
if (isExt) {
|
|
2847
|
+
lines.push(`${prefix} ${imp} (external)`);
|
|
2848
|
+
}
|
|
2849
|
+
else {
|
|
2850
|
+
const impNode = forwardVisited.get(imp)?.node;
|
|
2851
|
+
const expSuffix = impNode && impNode.exports.length > 0
|
|
2852
|
+
? ` → ${impNode.exports.slice(0, 3).join(", ")}${impNode.exports.length > 3 ? "…" : ""}`
|
|
2853
|
+
: "";
|
|
2854
|
+
lines.push(`${prefix} ${imp}${expSuffix}`);
|
|
2855
|
+
if (impNode && impNode.imports.length > 0) {
|
|
2856
|
+
impNode.imports.forEach((subImp, si) => {
|
|
2857
|
+
const subPrefix = si === impNode.imports.length - 1 ? "│ └──" : "│ ├──";
|
|
2858
|
+
if (subImp !== targetNode.rel)
|
|
2859
|
+
lines.push(`${subPrefix} ${subImp}`);
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
else {
|
|
2866
|
+
lines.push("├── importa: (ninguno)");
|
|
2867
|
+
}
|
|
2868
|
+
if (showReverse) {
|
|
2869
|
+
const revDeps = targetNode.importedBy;
|
|
2870
|
+
if (revDeps.length > 0) {
|
|
2871
|
+
lines.push("├── es importado por:");
|
|
2872
|
+
revDeps.forEach((dep, i) => {
|
|
2873
|
+
const prefix = i === revDeps.length - 1 ? "│ └──" : "│ ├──";
|
|
2874
|
+
lines.push(`${prefix} ${dep}`);
|
|
2875
|
+
const depNode = backwardVisited.get(dep)?.node;
|
|
2876
|
+
if (depNode && depNode.importedBy.length > 0) {
|
|
2877
|
+
depNode.importedBy.forEach((subDep, si) => {
|
|
2878
|
+
const subPrefix = si === depNode.importedBy.length - 1 ? "│ └──" : "│ ├──";
|
|
2879
|
+
if (subDep !== targetNode.rel)
|
|
2880
|
+
lines.push(`${subPrefix} ${subDep}`);
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
else {
|
|
2886
|
+
lines.push("├── es importado por: (nadie — posible dead export)");
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
if (targetNode.exports.length > 0) {
|
|
2890
|
+
lines.push("└── exporta:");
|
|
2891
|
+
targetNode.exports.forEach((exp, i) => {
|
|
2892
|
+
const prefix = i === targetNode.exports.length - 1 ? " └──" : " ├──";
|
|
2893
|
+
lines.push(`${prefix} ${exp}`);
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
else {
|
|
2897
|
+
lines.push("└── exporta: (ningún export detectado)");
|
|
2898
|
+
}
|
|
2899
|
+
if (cycles.length > 0) {
|
|
2900
|
+
lines.push("");
|
|
2901
|
+
lines.push("⚠️ DEPENDENCIAS CIRCULARES DETECTADAS:");
|
|
2902
|
+
cycles.slice(0, 5).forEach((c) => lines.push(` 🔄 ${c.join(" → ")}`));
|
|
2903
|
+
}
|
|
2904
|
+
return lines.join("\n");
|
|
2905
|
+
}
|
|
2906
|
+
function formatDGMermaid(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse) {
|
|
2907
|
+
const lines = ["flowchart LR"];
|
|
2908
|
+
const addedEdges = new Set();
|
|
2909
|
+
const safeId = (s) => s.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
2910
|
+
const tid = safeId(targetNode.rel);
|
|
2911
|
+
lines.push(` ${tid}["📦 ${path.basename(targetNode.rel)}"]`);
|
|
2912
|
+
lines.push(` style ${tid} fill:#6366f1,color:#fff,stroke:#4f46e5`);
|
|
2913
|
+
for (const imp of targetNode.imports) {
|
|
2914
|
+
const eid = `${tid}-->${safeId(imp)}`;
|
|
2915
|
+
if (!addedEdges.has(eid)) {
|
|
2916
|
+
addedEdges.add(eid);
|
|
2917
|
+
lines.push(` ${tid} --> ${safeId(imp)}["${path.basename(imp)}"]`);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
if (includeExternal) {
|
|
2921
|
+
for (const ext of targetNode.externalImports) {
|
|
2922
|
+
const extId = safeId(`ext_${ext}`);
|
|
2923
|
+
lines.push(` ${tid} --> ${extId}["📦 ${ext}"]`);
|
|
2924
|
+
lines.push(` style ${extId} fill:#374151,color:#9ca3af,stroke:#374151`);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
for (const [rel, { node }] of forwardVisited) {
|
|
2928
|
+
if (rel === targetNode.rel)
|
|
2929
|
+
continue;
|
|
2930
|
+
for (const imp of node.imports) {
|
|
2931
|
+
const eid = `${safeId(rel)}-->${safeId(imp)}`;
|
|
2932
|
+
if (!addedEdges.has(eid)) {
|
|
2933
|
+
addedEdges.add(eid);
|
|
2934
|
+
lines.push(` ${safeId(rel)} --> ${safeId(imp)}["${path.basename(imp)}"]`);
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
if (showReverse) {
|
|
2939
|
+
for (const dep of targetNode.importedBy) {
|
|
2940
|
+
const eid = `${safeId(dep)}-->${tid}`;
|
|
2941
|
+
if (!addedEdges.has(eid)) {
|
|
2942
|
+
addedEdges.add(eid);
|
|
2943
|
+
lines.push(` ${safeId(dep)}["${path.basename(dep)}"] --> ${tid}`);
|
|
2944
|
+
lines.push(` style ${safeId(dep)} fill:#065f46,color:#fff,stroke:#047857`);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
for (const [rel, { node }] of backwardVisited) {
|
|
2948
|
+
if (rel === targetNode.rel)
|
|
2949
|
+
continue;
|
|
2950
|
+
for (const dep of node.importedBy) {
|
|
2951
|
+
const eid = `${safeId(dep)}-->${safeId(rel)}`;
|
|
2952
|
+
if (!addedEdges.has(eid)) {
|
|
2953
|
+
addedEdges.add(eid);
|
|
2954
|
+
lines.push(` ${safeId(dep)}["${path.basename(dep)}"] --> ${safeId(rel)}["${path.basename(rel)}"]`);
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
if (cycles.length > 0) {
|
|
2960
|
+
lines.push(` %% ⚠️ Cycles: ${cycles.length}`);
|
|
2961
|
+
cycles.slice(0, 3).forEach(c => lines.push(` %% ${c.join(" → ")}`));
|
|
2962
|
+
}
|
|
2963
|
+
return lines.join("\n");
|
|
2964
|
+
}
|
|
2965
|
+
async function handleDepgraph(args) {
|
|
2966
|
+
const filePath = args?.filePath;
|
|
2967
|
+
const showReverse = args?.reverse !== false;
|
|
2968
|
+
const depth = Math.min(Math.max(typeof args?.depth === "number" ? args.depth : 1, 1), 5);
|
|
2969
|
+
const format = args?.format || "text";
|
|
2970
|
+
const includeExternal = !!(args?.external);
|
|
2971
|
+
const detectCircular = !!(args?.circular);
|
|
2972
|
+
const workspaceRoot = process.cwd();
|
|
2973
|
+
if (!filePath)
|
|
2974
|
+
throw new Error("filePath is required");
|
|
2975
|
+
try {
|
|
2976
|
+
const { resolved } = safeResolvePath(workspaceRoot, filePath);
|
|
2977
|
+
const relTarget = path.relative(workspaceRoot, resolved).replace(/\\/g, "/");
|
|
2978
|
+
const map = buildDepGraphMap(workspaceRoot);
|
|
2979
|
+
const targetNode = map.get(relTarget);
|
|
2980
|
+
if (!targetNode) {
|
|
2981
|
+
return { content: [{ type: "text", text: `File not found in workspace graph: ${relTarget}\nCheck path is correct and has .ts/.tsx/.js/.jsx extension.` }] };
|
|
2982
|
+
}
|
|
2983
|
+
const forwardVisited = bfsDepGraph(map, relTarget, "forward", depth);
|
|
2984
|
+
const backwardVisited = showReverse ? bfsDepGraph(map, relTarget, "backward", depth) : new Map();
|
|
2985
|
+
const allRelInScope = new Set([...forwardVisited.keys(), ...(showReverse ? backwardVisited.keys() : [])]);
|
|
2986
|
+
const cycles = detectCircular ? detectDepCycles(map, allRelInScope) : [];
|
|
2987
|
+
let output;
|
|
2988
|
+
if (format === "json") {
|
|
2989
|
+
const jsonResult = {
|
|
2990
|
+
target: relTarget,
|
|
2991
|
+
imports: targetNode.imports,
|
|
2992
|
+
importedBy: showReverse ? targetNode.importedBy : undefined,
|
|
2993
|
+
externalImports: includeExternal ? targetNode.externalImports : undefined,
|
|
2994
|
+
exports: targetNode.exports,
|
|
2995
|
+
cycles: cycles.length > 0 ? cycles : undefined,
|
|
2996
|
+
subGraph: {
|
|
2997
|
+
forward: Object.fromEntries([...forwardVisited.entries()].map(([k, v]) => [k, { depth: v.depth, imports: v.node.imports }])),
|
|
2998
|
+
backward: showReverse ? Object.fromEntries([...backwardVisited.entries()].map(([k, v]) => [k, { depth: v.depth, importedBy: v.node.importedBy }])) : undefined,
|
|
2999
|
+
},
|
|
3000
|
+
};
|
|
3001
|
+
output = JSON.stringify(jsonResult, null, 2);
|
|
3002
|
+
}
|
|
3003
|
+
else if (format === "mermaid") {
|
|
3004
|
+
output = formatDGMermaid(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse);
|
|
3005
|
+
}
|
|
3006
|
+
else {
|
|
3007
|
+
output = formatDGText(targetNode, forwardVisited, backwardVisited, cycles, includeExternal, showReverse);
|
|
3008
|
+
}
|
|
3009
|
+
const stats = `\n\n📊 Stats: ${forwardVisited.size} nodos forward, ${backwardVisited.size} nodos reverse, depth=${depth}`;
|
|
3010
|
+
return { content: [{ type: "text", text: output + (format === "json" ? "" : stats) }] };
|
|
3011
|
+
}
|
|
3012
|
+
catch (err) {
|
|
3013
|
+
logError("depgraph", err);
|
|
3014
|
+
return { content: [{ type: "text", text: `depgraph failed: ${err.message}` }] };
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
3018
|
+
// ─── refactor ─────────────────────────────────────────────────────────────────
|
|
3019
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
3020
|
+
function escapeRegexStr(s) {
|
|
3021
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3022
|
+
}
|
|
3023
|
+
function findSymbolRefs(workspaceRoot, symbolName) {
|
|
3024
|
+
const results = [];
|
|
3025
|
+
const IGNORE_RF = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
|
|
3026
|
+
function walkRF(dir) {
|
|
3027
|
+
let entries = [];
|
|
3028
|
+
try {
|
|
3029
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3030
|
+
}
|
|
3031
|
+
catch {
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
for (const e of entries) {
|
|
3035
|
+
if (IGNORE_RF.has(e.name))
|
|
3036
|
+
continue;
|
|
3037
|
+
const abs = path.join(dir, e.name);
|
|
3038
|
+
if (e.isDirectory()) {
|
|
3039
|
+
walkRF(abs);
|
|
3040
|
+
}
|
|
3041
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
3042
|
+
const rel = path.relative(workspaceRoot, abs).replace(/\\/g, "/");
|
|
3043
|
+
try {
|
|
3044
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
3045
|
+
if (!content.includes(symbolName))
|
|
3046
|
+
continue;
|
|
3047
|
+
const sf = ts.createSourceFile(abs, content, ts.ScriptTarget.Latest, true);
|
|
3048
|
+
const contentLines = content.split("\n");
|
|
3049
|
+
const identifierLines = new Set();
|
|
3050
|
+
function collectIdents(node) {
|
|
3051
|
+
if (ts.isIdentifier(node) && node.text === symbolName) {
|
|
3052
|
+
const { line } = sf.getLineAndCharacterOfPosition(node.getStart());
|
|
3053
|
+
identifierLines.add(line);
|
|
3054
|
+
}
|
|
3055
|
+
ts.forEachChild(node, collectIdents);
|
|
3056
|
+
}
|
|
3057
|
+
collectIdents(sf);
|
|
3058
|
+
const matchingLines = [...identifierLines].map(li => ({ lineIndex: li, lineContent: contentLines[li] ?? "" }));
|
|
3059
|
+
if (matchingLines.length > 0)
|
|
3060
|
+
results.push({ abs, rel, lines: matchingLines });
|
|
3061
|
+
}
|
|
3062
|
+
catch { /* skip */ }
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
walkRF(workspaceRoot);
|
|
3067
|
+
return results;
|
|
3068
|
+
}
|
|
3069
|
+
function applySymbolRenameInContent(content, fromName, toName) {
|
|
3070
|
+
const regex = new RegExp(`(?<![a-zA-Z0-9_$])${escapeRegexStr(fromName)}(?![a-zA-Z0-9_$])`, "g");
|
|
3071
|
+
return content.replace(regex, toName);
|
|
3072
|
+
}
|
|
3073
|
+
function buildUnifiedDiff(filePath, before, after) {
|
|
3074
|
+
if (before === after)
|
|
3075
|
+
return "";
|
|
3076
|
+
const bl = before.split("\n");
|
|
3077
|
+
const al = after.split("\n");
|
|
3078
|
+
const diffLines = [`--- a/${filePath}`, `+++ b/${filePath}`];
|
|
3079
|
+
const changed = new Set();
|
|
3080
|
+
const maxLen = Math.max(bl.length, al.length);
|
|
3081
|
+
for (let i = 0; i < maxLen; i++) {
|
|
3082
|
+
if (bl[i] !== al[i])
|
|
3083
|
+
changed.add(i);
|
|
3084
|
+
}
|
|
3085
|
+
let i = 0;
|
|
3086
|
+
while (i < maxLen) {
|
|
3087
|
+
if (!changed.has(i)) {
|
|
3088
|
+
i++;
|
|
3089
|
+
continue;
|
|
3090
|
+
}
|
|
3091
|
+
const hunkStart = Math.max(0, i - 2);
|
|
3092
|
+
let hunkEnd = i + 1;
|
|
3093
|
+
while (hunkEnd < maxLen && changed.has(hunkEnd))
|
|
3094
|
+
hunkEnd++;
|
|
3095
|
+
hunkEnd = Math.min(maxLen, hunkEnd + 2);
|
|
3096
|
+
const bc = Math.min(hunkEnd, bl.length) - hunkStart;
|
|
3097
|
+
const ac = Math.min(hunkEnd, al.length) - hunkStart;
|
|
3098
|
+
diffLines.push(`@@ -${hunkStart + 1},${bc} +${hunkStart + 1},${ac} @@`);
|
|
3099
|
+
for (let j = hunkStart; j < hunkEnd; j++) {
|
|
3100
|
+
if (j >= maxLen)
|
|
3101
|
+
break;
|
|
3102
|
+
if (changed.has(j)) {
|
|
3103
|
+
if (j < bl.length)
|
|
3104
|
+
diffLines.push(`-${bl[j]}`);
|
|
3105
|
+
if (j < al.length)
|
|
3106
|
+
diffLines.push(`+${al[j]}`);
|
|
3107
|
+
}
|
|
3108
|
+
else {
|
|
3109
|
+
if (j < bl.length)
|
|
3110
|
+
diffLines.push(` ${bl[j]}`);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
i = hunkEnd;
|
|
3114
|
+
}
|
|
3115
|
+
return diffLines.join("\n");
|
|
3116
|
+
}
|
|
3117
|
+
async function handleRefactorRename(args, workspaceRoot) {
|
|
3118
|
+
const fromName = args.from;
|
|
3119
|
+
const toName = args.to;
|
|
3120
|
+
const dryRun = !!(args.dryRun);
|
|
3121
|
+
const tscCheck = !!(args.tscCheck);
|
|
3122
|
+
const backup = !!(args.backup);
|
|
3123
|
+
if (!fromName || !toName)
|
|
3124
|
+
throw new Error("'from' and 'to' are required for rename");
|
|
3125
|
+
const refs = findSymbolRefs(workspaceRoot, fromName);
|
|
3126
|
+
if (refs.length === 0) {
|
|
3127
|
+
return { content: [{ type: "text", text: `No references found to symbol "${fromName}" in the workspace.` }] };
|
|
3128
|
+
}
|
|
3129
|
+
let filesModified = 0;
|
|
3130
|
+
let totalRefs = 0;
|
|
3131
|
+
const report = [];
|
|
3132
|
+
const diffs = [];
|
|
3133
|
+
for (const ref of refs) {
|
|
3134
|
+
const before = fs.readFileSync(ref.abs, "utf8");
|
|
3135
|
+
const after = applySymbolRenameInContent(before, fromName, toName);
|
|
3136
|
+
if (before === after)
|
|
3137
|
+
continue;
|
|
3138
|
+
const count = (before.match(new RegExp(`(?<![a-zA-Z0-9_$])${escapeRegexStr(fromName)}(?![a-zA-Z0-9_$])`, "g")) || []).length;
|
|
3139
|
+
totalRefs += count;
|
|
3140
|
+
filesModified++;
|
|
3141
|
+
if (dryRun) {
|
|
3142
|
+
const d = buildUnifiedDiff(ref.rel, before, after);
|
|
3143
|
+
if (d)
|
|
3144
|
+
diffs.push(d);
|
|
3145
|
+
}
|
|
3146
|
+
else {
|
|
3147
|
+
if (backup)
|
|
3148
|
+
fs.writeFileSync(ref.abs + ".bak", before, "utf8");
|
|
3149
|
+
fs.writeFileSync(ref.abs, after, "utf8");
|
|
3150
|
+
report.push(` ✅ ${ref.rel} (${count} refs)`);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
if (dryRun) {
|
|
3154
|
+
return { content: [{ type: "text", text: [`🔍 DRY RUN — Rename "${fromName}" → "${toName}"`, ``, `Archivos que cambiarían: ${filesModified} | Referencias: ${totalRefs}`, ``, `=== DIFF PREVIEW ===`, ...diffs].join("\n") }] };
|
|
3155
|
+
}
|
|
3156
|
+
let tscOutput = "";
|
|
3157
|
+
if (tscCheck && filesModified > 0) {
|
|
3158
|
+
try {
|
|
3159
|
+
const out = execSync("npx tsc --noEmit 2>&1 | head -30 || true", { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
|
|
3160
|
+
const errors = out.split("\n").filter((l) => l.includes("error TS"));
|
|
3161
|
+
tscOutput = errors.length === 0 ? "\n\n✅ tsc --noEmit: 0 errores" : `\n\n⚠️ tsc: ${errors.length} error(es):\n${errors.join("\n")}`;
|
|
3162
|
+
}
|
|
3163
|
+
catch (e) {
|
|
3164
|
+
tscOutput = `\n\n⚠️ tsc check falló: ${e.message}`;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
return { content: [{ type: "text", text: [`✅ Rename: "${fromName}" → "${toName}"`, ``, `📁 ${filesModified} archivos | 🔗 ${totalRefs} refs`, ``, ...report].join("\n") + tscOutput }] };
|
|
3168
|
+
}
|
|
3169
|
+
async function handleRefactorMove(args, workspaceRoot) {
|
|
3170
|
+
const fromRel = args.from;
|
|
3171
|
+
const toRel = args.to;
|
|
3172
|
+
const dryRun = !!(args.dryRun);
|
|
3173
|
+
const tscCheck = !!(args.tscCheck);
|
|
3174
|
+
const backup = !!(args.backup);
|
|
3175
|
+
const { resolved: fromAbs } = safeResolvePath(workspaceRoot, fromRel);
|
|
3176
|
+
const toAbs = path.resolve(workspaceRoot, toRel);
|
|
3177
|
+
const fromRelNorm = path.relative(workspaceRoot, fromAbs).replace(/\\/g, "/");
|
|
3178
|
+
const toRelNorm = path.relative(workspaceRoot, toAbs).replace(/\\/g, "/");
|
|
3179
|
+
if (!fs.existsSync(fromAbs)) {
|
|
3180
|
+
return { content: [{ type: "text", text: `Source file not found: ${fromRelNorm}` }] };
|
|
3181
|
+
}
|
|
3182
|
+
const IGNORE_MV = new Set(["node_modules", ".git", "dist", "chroma_data", ".lemma", "build", "coverage", ".next"]);
|
|
3183
|
+
const importers = [];
|
|
3184
|
+
function walkMV(dir) {
|
|
3185
|
+
let entries = [];
|
|
3186
|
+
try {
|
|
3187
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3188
|
+
}
|
|
3189
|
+
catch {
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
for (const e of entries) {
|
|
3193
|
+
if (IGNORE_MV.has(e.name))
|
|
3194
|
+
continue;
|
|
3195
|
+
const abs = path.join(dir, e.name);
|
|
3196
|
+
if (e.isDirectory()) {
|
|
3197
|
+
walkMV(abs);
|
|
3198
|
+
}
|
|
3199
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
3200
|
+
const rel = path.relative(workspaceRoot, abs).replace(/\\/g, "/");
|
|
3201
|
+
if (rel === fromRelNorm)
|
|
3202
|
+
return;
|
|
3203
|
+
try {
|
|
3204
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
3205
|
+
const sf = ts.createSourceFile(abs, content, ts.ScriptTarget.Latest, true);
|
|
3206
|
+
let found = false;
|
|
3207
|
+
ts.forEachChild(sf, (n) => {
|
|
3208
|
+
if (!found && ts.isImportDeclaration(n) && ts.isStringLiteral(n.moduleSpecifier)) {
|
|
3209
|
+
const spec = n.moduleSpecifier.text;
|
|
3210
|
+
if (spec.startsWith(".")) {
|
|
3211
|
+
const res = path.resolve(path.dirname(abs), spec).replace(/\\/g, "/");
|
|
3212
|
+
const fromN = fromAbs.replace(/\\/g, "/");
|
|
3213
|
+
const fromNoExt = fromN.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
3214
|
+
if (res === fromN || res === fromNoExt)
|
|
3215
|
+
found = true;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
});
|
|
3219
|
+
if (found)
|
|
3220
|
+
importers.push({ abs, rel, content });
|
|
3221
|
+
}
|
|
3222
|
+
catch { /* skip */ }
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
walkMV(workspaceRoot);
|
|
3227
|
+
const addDot = (p) => p.startsWith(".") ? p : `./${p}`;
|
|
3228
|
+
const patches = [];
|
|
3229
|
+
for (const imp of importers) {
|
|
3230
|
+
const oldRelImport = path.relative(path.dirname(imp.abs), fromAbs).replace(/\\/g, "/");
|
|
3231
|
+
const newRelImport = path.relative(path.dirname(imp.abs), toAbs).replace(/\\/g, "/");
|
|
3232
|
+
const oldSpec = addDot(oldRelImport.replace(/\.(ts|tsx|js|jsx)$/, ""));
|
|
3233
|
+
const newSpec = addDot(newRelImport.replace(/\.(ts|tsx|js|jsx)$/, ""));
|
|
3234
|
+
const after = imp.content.replace(new RegExp(`(['"])${escapeRegexStr(oldSpec)}(['"])`, "g"), `$1${newSpec}$2`);
|
|
3235
|
+
if (after !== imp.content)
|
|
3236
|
+
patches.push({ rel: imp.rel, abs: imp.abs, before: imp.content, after });
|
|
3237
|
+
}
|
|
3238
|
+
if (dryRun) {
|
|
3239
|
+
const diffs = patches.map(p => buildUnifiedDiff(p.rel, p.before, p.after)).filter(Boolean);
|
|
3240
|
+
return { content: [{ type: "text", text: [`🔍 DRY RUN — Move "${fromRelNorm}" → "${toRelNorm}"`, ``, `Importadores que se actualizarían: ${patches.length}`, ``, `=== DIFF PREVIEW ===`, ...diffs].join("\n") }] };
|
|
3241
|
+
}
|
|
3242
|
+
fs.mkdirSync(path.dirname(toAbs), { recursive: true });
|
|
3243
|
+
if (backup)
|
|
3244
|
+
fs.copyFileSync(fromAbs, fromAbs + ".bak");
|
|
3245
|
+
fs.copyFileSync(fromAbs, toAbs);
|
|
3246
|
+
fs.unlinkSync(fromAbs);
|
|
3247
|
+
for (const p of patches) {
|
|
3248
|
+
if (backup)
|
|
3249
|
+
fs.writeFileSync(p.abs + ".bak", p.before, "utf8");
|
|
3250
|
+
fs.writeFileSync(p.abs, p.after, "utf8");
|
|
3251
|
+
}
|
|
3252
|
+
let tscOutput = "";
|
|
3253
|
+
if (tscCheck) {
|
|
3254
|
+
try {
|
|
3255
|
+
const out = execSync("npx tsc --noEmit 2>&1 | head -30 || true", { cwd: workspaceRoot, encoding: "utf8", timeout: 30000 });
|
|
3256
|
+
const errors = out.split("\n").filter((l) => l.includes("error TS"));
|
|
3257
|
+
tscOutput = errors.length === 0 ? "\n\n✅ tsc --noEmit: 0 errores" : `\n\n⚠️ tsc: ${errors.length} error(es):\n${errors.join("\n")}`;
|
|
3258
|
+
}
|
|
3259
|
+
catch (e) {
|
|
3260
|
+
tscOutput = `\n\n⚠️ tsc check falló: ${e.message}`;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
return { content: [{ type: "text", text: [`✅ Move: "${fromRelNorm}" → "${toRelNorm}"`, ``, `📁 ${patches.length} importadores actualizados:`, ...patches.map(p => ` ✅ ${p.rel}`)].join("\n") + tscOutput }] };
|
|
3264
|
+
}
|
|
3265
|
+
async function handleRefactor(args) {
|
|
3266
|
+
const operation = args?.operation;
|
|
3267
|
+
const workspaceRoot = process.cwd();
|
|
3268
|
+
if (!operation)
|
|
3269
|
+
throw new Error("'operation' is required (rename | move)");
|
|
3270
|
+
try {
|
|
3271
|
+
switch (operation) {
|
|
3272
|
+
case "rename": return await handleRefactorRename(args, workspaceRoot);
|
|
3273
|
+
case "move": return await handleRefactorMove(args, workspaceRoot);
|
|
3274
|
+
default:
|
|
3275
|
+
return { content: [{ type: "text", text: `Unknown operation: "${operation}". Supported: rename, move` }] };
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
catch (err) {
|
|
3279
|
+
logError("refactor", err);
|
|
3280
|
+
return { content: [{ type: "text", text: `refactor failed: ${err.message}` }] };
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
// ── Surgical Symbol Context ──────────────────────────────────────────
|
|
3284
|
+
async function handleGetSymbolSurgicalContext(args) {
|
|
3285
|
+
const filePath = args?.filePath;
|
|
3286
|
+
const symbolName = args?.symbolName;
|
|
3287
|
+
if (!filePath || !symbolName)
|
|
3288
|
+
throw new Error("filePath and symbolName are required");
|
|
3289
|
+
const workspaceRoot = process.cwd();
|
|
3290
|
+
try {
|
|
3291
|
+
const res = getSymbolSurgicalContext(workspaceRoot, filePath, symbolName);
|
|
3292
|
+
const lines = [];
|
|
3293
|
+
lines.push(`# 🧬 Surgical Context: Symbol \`${symbolName}\` in \`${filePath}\``);
|
|
3294
|
+
lines.push(`## Implementation`);
|
|
3295
|
+
lines.push(`\`\`\`typescript\n${res.implementation}\n\`\`\``);
|
|
3296
|
+
if (res.dependencies.length > 0) {
|
|
3297
|
+
lines.push(`## Workspace Dependencies (Signatures ONLY)`);
|
|
3298
|
+
for (const dep of res.dependencies) {
|
|
3299
|
+
lines.push(`### \`${dep.name}\` (${dep.type}) declared in \`${dep.filePath}\``);
|
|
3300
|
+
lines.push(`\`\`\`typescript\n${dep.signature}\n\`\`\``);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
else {
|
|
3304
|
+
lines.push(`*No local workspace dependencies detected for this symbol.*`);
|
|
3305
|
+
}
|
|
3306
|
+
const output = lines.join("\n\n");
|
|
3307
|
+
const originalSize = fs.readFileSync(path.resolve(workspaceRoot, filePath), "utf8").length;
|
|
3308
|
+
const totalRawSize = originalSize + res.dependencies.reduce((sum, dep) => {
|
|
3309
|
+
try {
|
|
3310
|
+
return sum + fs.readFileSync(path.resolve(workspaceRoot, dep.filePath), "utf8").length;
|
|
3311
|
+
}
|
|
3312
|
+
catch {
|
|
3313
|
+
return sum;
|
|
3314
|
+
}
|
|
3315
|
+
}, 0);
|
|
3316
|
+
reportSavings({
|
|
3317
|
+
source: "contextSqueeze",
|
|
3318
|
+
charsBefore: totalRawSize,
|
|
3319
|
+
charsAfter: output.length,
|
|
3320
|
+
toolName: "get_symbol_surgical_context",
|
|
3321
|
+
filePath
|
|
3322
|
+
});
|
|
3323
|
+
return {
|
|
3324
|
+
content: [{ type: "text", text: output }]
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
3327
|
+
catch (err) {
|
|
3328
|
+
logError("get_symbol_surgical_context", err);
|
|
3329
|
+
return {
|
|
3330
|
+
content: [{ type: "text", text: `Surgical context extraction failed: ${err.message}` }]
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
// ── Wormhole Squeeze ────────────────────────────────────────────────
|
|
3335
|
+
async function handleWormholeSqueeze(args) {
|
|
3336
|
+
const text = args?.text;
|
|
3337
|
+
if (!text)
|
|
3338
|
+
throw new Error("text is required");
|
|
3339
|
+
try {
|
|
3340
|
+
const compressed = compressWormhole(text);
|
|
3341
|
+
reportSavings({
|
|
3342
|
+
source: "contextSqueeze",
|
|
3343
|
+
charsBefore: text.length,
|
|
3344
|
+
charsAfter: compressed.length,
|
|
3345
|
+
toolName: "wormhole_squeeze"
|
|
3346
|
+
});
|
|
3347
|
+
return {
|
|
3348
|
+
content: [{ type: "text", text: compressed }]
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
catch (err) {
|
|
3352
|
+
logError("wormhole_squeeze", err);
|
|
3353
|
+
return {
|
|
3354
|
+
content: [{ type: "text", text: `Wormhole compression failed: ${err.message}` }]
|
|
3355
|
+
};
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
// ── Executive ROI Report ──────────────────────────────────────────────
|
|
3359
|
+
async function handleGenerateExecutiveRoiReport(args) {
|
|
3360
|
+
const teamSize = typeof args?.teamSize === "number" ? args.teamSize : 10;
|
|
3361
|
+
const costPerDevHour = typeof args?.costPerDevHour === "number" ? args.costPerDevHour : 50;
|
|
3362
|
+
const targetPath = args?.targetPath || "LEMMA_ROI_REPORT.md";
|
|
3363
|
+
const workspaceRoot = process.cwd();
|
|
3364
|
+
try {
|
|
3365
|
+
const res = generateExecutiveReport(workspaceRoot, {
|
|
3366
|
+
teamSize,
|
|
3367
|
+
costPerDevHour,
|
|
3368
|
+
targetPath
|
|
3369
|
+
});
|
|
3370
|
+
return {
|
|
3371
|
+
content: [{
|
|
3372
|
+
type: "text",
|
|
3373
|
+
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`
|
|
3374
|
+
}]
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
catch (err) {
|
|
3378
|
+
logError("generate_executive_roi_report", err);
|
|
3379
|
+
return {
|
|
3380
|
+
content: [{ type: "text", text: `ROI Report generation failed: ${err.message}` }]
|
|
3381
|
+
};
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
// ── Surgical AST Insert ──────────────────────────────────────────────
|
|
3385
|
+
async function handleSurgicalAstInsert(args) {
|
|
3386
|
+
const filePath = args?.filePath;
|
|
3387
|
+
const symbolName = args?.symbolName;
|
|
3388
|
+
const anchorSymbol = args?.anchorSymbol;
|
|
3389
|
+
const insertPosition = args?.insertPosition;
|
|
3390
|
+
const codeToInsert = args?.codeToInsert;
|
|
3391
|
+
if (!filePath || !codeToInsert)
|
|
3392
|
+
throw new Error("filePath and codeToInsert are required");
|
|
3393
|
+
const workspaceRoot = process.cwd();
|
|
3394
|
+
try {
|
|
3395
|
+
const resolvedPath = surgicalASTInsert(workspaceRoot, {
|
|
3396
|
+
filePath,
|
|
3397
|
+
symbolName,
|
|
3398
|
+
anchorSymbol,
|
|
3399
|
+
insertPosition,
|
|
3400
|
+
codeToInsert
|
|
3401
|
+
});
|
|
3402
|
+
const newTextSize = fs.readFileSync(resolvedPath, "utf8").length;
|
|
3403
|
+
const outputCharsSaved = Math.max(0, newTextSize - codeToInsert.length);
|
|
3404
|
+
const tokensSaved = Math.floor(outputCharsSaved / 4);
|
|
3405
|
+
reportSavings({
|
|
3406
|
+
source: "contextSqueeze",
|
|
3407
|
+
tokens: tokensSaved,
|
|
3408
|
+
toolName: "surgical_ast_insert",
|
|
3409
|
+
filePath
|
|
3410
|
+
});
|
|
3411
|
+
return {
|
|
3412
|
+
content: [{
|
|
3413
|
+
type: "text",
|
|
3414
|
+
text: `Success: Surgically inserted code into ${filePath} using AST compiler.`
|
|
3415
|
+
}]
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
catch (err) {
|
|
3419
|
+
logError("surgical_ast_insert", err);
|
|
3420
|
+
return {
|
|
3421
|
+
content: [{ type: "text", text: `Surgical AST insertion failed: ${err.message}` }]
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
// ── Local Semantic Autofix ────────────────────────────────────────────
|
|
3426
|
+
async function handleLocalSemanticAutofix(args) {
|
|
3427
|
+
const errorMessage = args?.errorMessage;
|
|
3428
|
+
if (!errorMessage)
|
|
3429
|
+
throw new Error("errorMessage is required");
|
|
3430
|
+
try {
|
|
3431
|
+
const brain = getBrain();
|
|
3432
|
+
const results = brain.search(errorMessage, 3, 0.70);
|
|
3433
|
+
const topHit = results[0];
|
|
3434
|
+
if (topHit) {
|
|
3435
|
+
const responseText = topHit.response;
|
|
3436
|
+
const tokensSaved = Math.max(100, Math.floor(responseText.length / 4));
|
|
3437
|
+
reportSavings({
|
|
3438
|
+
source: "cache",
|
|
3439
|
+
tokens: tokensSaved,
|
|
3440
|
+
toolName: "local_semantic_autofix",
|
|
3441
|
+
query: errorMessage.substring(0, 100)
|
|
3442
|
+
});
|
|
3443
|
+
return {
|
|
3444
|
+
content: [{
|
|
3445
|
+
type: "text",
|
|
3446
|
+
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}`
|
|
3447
|
+
}]
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
return {
|
|
3451
|
+
content: [{
|
|
3452
|
+
type: "text",
|
|
3453
|
+
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!`
|
|
3454
|
+
}]
|
|
3455
|
+
};
|
|
3456
|
+
}
|
|
3457
|
+
catch (err) {
|
|
3458
|
+
logError("local_semantic_autofix", err);
|
|
3459
|
+
return {
|
|
3460
|
+
content: [{ type: "text", text: `Local auto-fix failed: ${err.message}` }]
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
2624
3464
|
//# sourceMappingURL=tools.js.map
|