@nxuss/lemma 0.6.1 → 0.7.1
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 +71 -13
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +57 -30
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/cjs/mcp/index.js +411 -4
- package/dist/cjs/mcp/index.js.map +1 -1
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +57 -30
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/mcp/index.js +412 -5
- package/dist/esm/mcp/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/mcp/index.js
CHANGED
|
@@ -9,6 +9,58 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const reportSavings_1 = require("../utils/reportSavings");
|
|
12
|
+
/**
|
|
13
|
+
* Lemma Autopilot System Prompt — injected into every connected Chat IDE.
|
|
14
|
+
* This is what makes Lemma tools AUTOMATIC: no user friction, no manual invocations.
|
|
15
|
+
*/
|
|
16
|
+
const LEMMA_SYSTEM_PROMPT = `You are connected to Lemma — an intelligent local-first AI gateway.
|
|
17
|
+
Lemma gives you a set of tools you MUST use proactively and automatically. NEVER ask the user
|
|
18
|
+
to call a tool manually. YOU invoke the tools. The user should experience zero friction.
|
|
19
|
+
|
|
20
|
+
## MANDATORY AUTOMATIC BEHAVIORS
|
|
21
|
+
|
|
22
|
+
### 1. Before reading ANY file → call get_ast_hologram first
|
|
23
|
+
When you need to understand the structure of a codebase or navigate files, ALWAYS call
|
|
24
|
+
\`get_ast_hologram\` first (with the relevant dirPath, e.g. "src"). Use the returned symbol
|
|
25
|
+
map to pinpoint exactly which files to read. NEVER blindly read every file one by one.
|
|
26
|
+
|
|
27
|
+
### 2. Before answering ANY technical question → call query_hybrid_consensus first
|
|
28
|
+
BEFORE making any reasoning or cloud LLM call, ALWAYS call \`query_hybrid_consensus\` with
|
|
29
|
+
the user's question. If similarity ≥ 80%, return the cached answer immediately — no cloud
|
|
30
|
+
call needed. Only proceed with your own reasoning if the Brain returns a MISS.
|
|
31
|
+
|
|
32
|
+
### 3. When you start working on ANY file → call get_telepathic_hints automatically
|
|
33
|
+
Whenever the user mentions a file or you are about to read/edit a file, IMMEDIATELY call
|
|
34
|
+
\`get_telepathic_hints\` with that file's path. Surface the results to the user as relevant
|
|
35
|
+
context BEFORE doing any work. Do not ask permission — just do it.
|
|
36
|
+
|
|
37
|
+
### 4. Before applying ANY code patch → call validate_patch_sandbox first
|
|
38
|
+
NEVER call \`write_workspace_file\` or \`apply_workspace_patch\` without FIRST calling
|
|
39
|
+
\`validate_patch_sandbox\` with the proposed content. Only proceed with the write if the
|
|
40
|
+
verdict is "✅ SAFE TO APPLY". If it fails, fix the issues and re-validate.
|
|
41
|
+
|
|
42
|
+
### 5. After solving any technical problem → call store_memory automatically
|
|
43
|
+
After resolving a bug, implementing a feature, or answering a significant technical question,
|
|
44
|
+
ALWAYS call \`store_memory\` to persist the solution. Use a descriptive query key so it can
|
|
45
|
+
be retrieved in future sessions across ALL projects.
|
|
46
|
+
|
|
47
|
+
### 6. Workspace navigation → always use Lemma tools, NEVER guess file contents
|
|
48
|
+
- Use \`list_workspace_dir\` to navigate structure
|
|
49
|
+
- Use \`read_workspace_file\` (auto-compresses + scrubs secrets) instead of asking the user
|
|
50
|
+
- Use \`search_workspace\` for text search across files
|
|
51
|
+
- Use \`run_workspace_command\` for builds, tests, linters
|
|
52
|
+
|
|
53
|
+
### 7. Privacy is automatic
|
|
54
|
+
\`read_workspace_file\` and \`search_workspace\` automatically scrub API keys and PII.
|
|
55
|
+
You never need to manually sanitize file content.
|
|
56
|
+
|
|
57
|
+
## LEMMA RESOURCES (auto-subscribe when available)
|
|
58
|
+
- \`lemma://runtime/context\` — live crash logs. Read this FIRST when the user reports a bug.
|
|
59
|
+
- \`lemma://multiverse/timeline\` — AST history. Use when the user says "it was working before".
|
|
60
|
+
- \`lemma://project/onboarding\` — architecture map. Read at session start in new repos.
|
|
61
|
+
- \`lemma://stats/usage\` — token savings report.
|
|
62
|
+
|
|
63
|
+
Remember: the user hired YOU to be autonomous. Act like it.`;
|
|
12
64
|
/**
|
|
13
65
|
* Lemma MCP Server
|
|
14
66
|
* Exposes Lemma's intelligence layer as Tools and Resources for LLMs.
|
|
@@ -24,9 +76,11 @@ class LemmaMcpServer {
|
|
|
24
76
|
subscribe: true
|
|
25
77
|
},
|
|
26
78
|
tools: {},
|
|
79
|
+
prompts: {},
|
|
27
80
|
},
|
|
28
81
|
});
|
|
29
82
|
this.setupHandlers();
|
|
83
|
+
this.setupPromptsHandlers();
|
|
30
84
|
this.setupErrorHandling();
|
|
31
85
|
this.setupLiveContextWatcher();
|
|
32
86
|
}
|
|
@@ -116,6 +170,38 @@ class LemmaMcpServer {
|
|
|
116
170
|
return [];
|
|
117
171
|
}
|
|
118
172
|
}
|
|
173
|
+
setupPromptsHandlers() {
|
|
174
|
+
// Expose LEMMA_SYSTEM_PROMPT as a named MCP prompt so connected Chat IDEs
|
|
175
|
+
// load it as a system instruction, making all tool invocations automatic.
|
|
176
|
+
this.server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => {
|
|
177
|
+
return {
|
|
178
|
+
prompts: [
|
|
179
|
+
{
|
|
180
|
+
name: "lemma-autopilot",
|
|
181
|
+
description: "Lemma Autopilot: mandatory behavioral instructions for the AI assistant. Load this prompt to make all Lemma tools fire automatically with zero user friction.",
|
|
182
|
+
arguments: [],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
this.server.setRequestHandler(types_js_1.GetPromptRequestSchema, async (request) => {
|
|
188
|
+
if (request.params.name === "lemma-autopilot") {
|
|
189
|
+
return {
|
|
190
|
+
description: "Lemma Autopilot System Instructions",
|
|
191
|
+
messages: [
|
|
192
|
+
{
|
|
193
|
+
role: "user",
|
|
194
|
+
content: {
|
|
195
|
+
type: "text",
|
|
196
|
+
text: LEMMA_SYSTEM_PROMPT,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`Unknown prompt: ${request.params.name}`);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
119
205
|
setupHandlers() {
|
|
120
206
|
// 1. List Resources
|
|
121
207
|
this.server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async () => {
|
|
@@ -316,7 +402,7 @@ class LemmaMcpServer {
|
|
|
316
402
|
},
|
|
317
403
|
{
|
|
318
404
|
name: "search_memory",
|
|
319
|
-
description: "Search Lemma's semantic memory (The Brain)
|
|
405
|
+
description: "[CALL FIRST] Search Lemma's semantic memory (The Brain) before answering technical questions. Retrieves past solutions, fixes, and context from ALL your projects globally. If similarity > 50%, use this answer directly. Saves cloud tokens on every hit.",
|
|
320
406
|
inputSchema: {
|
|
321
407
|
type: "object",
|
|
322
408
|
properties: {
|
|
@@ -328,7 +414,7 @@ class LemmaMcpServer {
|
|
|
328
414
|
},
|
|
329
415
|
{
|
|
330
416
|
name: "store_memory",
|
|
331
|
-
description: "
|
|
417
|
+
description: "[CALL AUTOMATICALLY AFTER SOLVING ANYTHING] Persist a technical solution, bug fix, architecture decision, or key fact into Lemma's Brain. ALWAYS call this after resolving a bug, implementing a feature, or completing a meaningful task. Do NOT ask the user — just call it. Future sessions across ALL projects will benefit.",
|
|
332
418
|
inputSchema: {
|
|
333
419
|
type: "object",
|
|
334
420
|
properties: {
|
|
@@ -363,7 +449,7 @@ class LemmaMcpServer {
|
|
|
363
449
|
},
|
|
364
450
|
{
|
|
365
451
|
name: "read_workspace_file",
|
|
366
|
-
description: "
|
|
452
|
+
description: "[ALWAYS USE THIS] Read a file inside the workspace. AUTOMATICALLY compresses comments/whitespace (up to 80% token savings) and AUTOMATICALLY scrubs API keys, credentials, and PII before they reach the AI context. ALWAYS use this instead of asking the user to paste file content.",
|
|
367
453
|
inputSchema: {
|
|
368
454
|
type: "object",
|
|
369
455
|
properties: {
|
|
@@ -453,6 +539,55 @@ class LemmaMcpServer {
|
|
|
453
539
|
properties: {},
|
|
454
540
|
},
|
|
455
541
|
},
|
|
542
|
+
{
|
|
543
|
+
name: "get_ast_hologram",
|
|
544
|
+
description: "[CALL FIRST — BEFORE READING FILES] Generate a dense, token-efficient Holographic AST Map of the workspace. Returns a structured JSON index of ALL exported symbols (classes, functions, interfaces, types, consts) with file paths and line numbers. ALWAYS call this FIRST when navigating or understanding a codebase. NEVER read files blindly one-by-one — use this map to target exactly the right files. Saves up to 90% tokens.",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: {
|
|
548
|
+
dirPath: { type: "string", description: "Directory to scan relative to workspace root (empty for root)", default: "" },
|
|
549
|
+
extensions: { type: "array", items: { type: "string" }, description: "File extensions to scan (default: ['ts', 'tsx', 'js', 'jsx'])", default: ["ts", "tsx", "js", "jsx"] }
|
|
550
|
+
},
|
|
551
|
+
required: [],
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: "validate_patch_sandbox",
|
|
556
|
+
description: "[MANDATORY PRE-FLIGHT CHECK] ALWAYS call this BEFORE apply_workspace_patch or write_workspace_file. Validates the proposed code in an isolated /tmp sandbox via tsc --noEmit + bracket-balance check. NEVER skip this step — only proceed with writing if verdict is '✅ SAFE TO APPLY'. If it fails, fix and re-validate.",
|
|
557
|
+
inputSchema: {
|
|
558
|
+
type: "object",
|
|
559
|
+
properties: {
|
|
560
|
+
filePath: { type: "string", description: "Target file path relative to workspace root" },
|
|
561
|
+
patchedContent: { type: "string", description: "The complete proposed file content after the patch is applied" }
|
|
562
|
+
},
|
|
563
|
+
required: ["filePath", "patchedContent"],
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "query_hybrid_consensus",
|
|
568
|
+
description: "[CALL BEFORE EVERY TECHNICAL QUESTION] Hybrid Consensus Engine: ALWAYS call this FIRST before reasoning about any technical question or problem. Searches The Brain at configurable threshold (default 80%). Brain HIT → return cached answer instantly, ZERO cloud tokens spent. Brain MISS → proceed with your reasoning, then call store_memory with the answer. This is your primary token-saving mechanism.",
|
|
569
|
+
inputSchema: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: {
|
|
572
|
+
query: { type: "string", description: "The technical question or task description" },
|
|
573
|
+
context: { type: "string", description: "Optional extra context (active file path, error message, etc.)" },
|
|
574
|
+
threshold: { type: "number", description: "Similarity threshold to consider a Brain hit sufficient (0.0-1.0, default: 0.80)", default: 0.80 }
|
|
575
|
+
},
|
|
576
|
+
required: ["query"],
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: "get_telepathic_hints",
|
|
581
|
+
description: "[CALL AUTOMATICALLY ON FILE OPEN] Proactive Telepathy: IMMEDIATELY call this whenever a file is mentioned or about to be edited. Automatically surfaces the most relevant past solutions, patterns, and memories from The Brain based on the file path — NO explicit query needed. Call this BEFORE writing any code. Surface the hints to the user without being asked.",
|
|
582
|
+
inputSchema: {
|
|
583
|
+
type: "object",
|
|
584
|
+
properties: {
|
|
585
|
+
activeFile: { type: "string", description: "Path of the file currently being edited (relative or absolute)" },
|
|
586
|
+
limit: { type: "number", description: "Maximum number of hints to return (default: 5)", default: 5 }
|
|
587
|
+
},
|
|
588
|
+
required: ["activeFile"],
|
|
589
|
+
},
|
|
590
|
+
},
|
|
456
591
|
],
|
|
457
592
|
};
|
|
458
593
|
});
|
|
@@ -594,6 +729,14 @@ class LemmaMcpServer {
|
|
|
594
729
|
(0, reportSavings_1.reportSavings)({ source: 'contextSqueeze', charsBefore: originalSize, charsAfter: content.length });
|
|
595
730
|
}
|
|
596
731
|
}
|
|
732
|
+
// Run SemanticScrubber to automatically mask any credentials/PII/API keys
|
|
733
|
+
try {
|
|
734
|
+
const { SemanticScrubber } = require("../security/SemanticScrubber");
|
|
735
|
+
const scrubber = new SemanticScrubber();
|
|
736
|
+
const { maskedPrompt } = scrubber.mask(content);
|
|
737
|
+
content = maskedPrompt;
|
|
738
|
+
}
|
|
739
|
+
catch { }
|
|
597
740
|
return {
|
|
598
741
|
content: [{ type: "text", text: content }],
|
|
599
742
|
};
|
|
@@ -735,7 +878,15 @@ class LemmaMcpServer {
|
|
|
735
878
|
content: [{ type: "text", text: `No matches found in the workspace for "${query}".` }],
|
|
736
879
|
};
|
|
737
880
|
}
|
|
738
|
-
|
|
881
|
+
let formatted = results.map(r => `[${r.filePath}:${r.line}] ${r.text}`).join("\n");
|
|
882
|
+
// Run SemanticScrubber to automatically mask any credentials/PII/API keys in search results
|
|
883
|
+
try {
|
|
884
|
+
const { SemanticScrubber } = require("../security/SemanticScrubber");
|
|
885
|
+
const scrubber = new SemanticScrubber();
|
|
886
|
+
const { maskedPrompt } = scrubber.mask(formatted);
|
|
887
|
+
formatted = maskedPrompt;
|
|
888
|
+
}
|
|
889
|
+
catch { }
|
|
739
890
|
return {
|
|
740
891
|
content: [{ type: "text", text: `Found ${results.length} matches:\n\n${formatted}` }],
|
|
741
892
|
};
|
|
@@ -802,6 +953,262 @@ class LemmaMcpServer {
|
|
|
802
953
|
};
|
|
803
954
|
}
|
|
804
955
|
}
|
|
956
|
+
case "get_ast_hologram": {
|
|
957
|
+
const dirPath = args?.dirPath || "";
|
|
958
|
+
const extensions = args?.extensions || ["ts", "tsx", "js", "jsx"];
|
|
959
|
+
const workspaceRoot = process.cwd();
|
|
960
|
+
const resolvedDir = path_1.default.resolve(workspaceRoot, dirPath);
|
|
961
|
+
if (!resolvedDir.startsWith(workspaceRoot)) {
|
|
962
|
+
throw new Error("Access denied: path is outside of workspace root");
|
|
963
|
+
}
|
|
964
|
+
// Recursive AST symbol extractor (regex-based, no heavy deps)
|
|
965
|
+
const extractSymbols = (filePath, relPath) => {
|
|
966
|
+
try {
|
|
967
|
+
const src = fs_1.default.readFileSync(filePath, "utf8");
|
|
968
|
+
const symbols = [];
|
|
969
|
+
const lines = src.split("\n");
|
|
970
|
+
lines.forEach((line, idx) => {
|
|
971
|
+
// Export declarations: functions, classes, interfaces, types, consts
|
|
972
|
+
const m = line.match(/^export\s+(?:default\s+)?(?:async\s+)?(class|function|interface|type|const|enum|abstract\s+class)\s+(\w+)/);
|
|
973
|
+
if (m) {
|
|
974
|
+
symbols.push({ kind: m[1].replace('abstract ', ''), name: m[2], file: relPath, line: idx + 1 });
|
|
975
|
+
}
|
|
976
|
+
// Named exports
|
|
977
|
+
const exportMatch = line.match(/^export\s*\{([^}]+)\}/);
|
|
978
|
+
if (exportMatch) {
|
|
979
|
+
exportMatch[1].split(',').forEach(s => {
|
|
980
|
+
const name = s.trim().split(/\s+as\s+/).pop()?.trim();
|
|
981
|
+
if (name)
|
|
982
|
+
symbols.push({ kind: 'export', name, file: relPath, line: idx + 1 });
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
return symbols;
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
return [];
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const walkDir = (dir, rel) => {
|
|
993
|
+
let allSymbols = [];
|
|
994
|
+
try {
|
|
995
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
996
|
+
for (const entry of entries) {
|
|
997
|
+
if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(entry.name))
|
|
998
|
+
continue;
|
|
999
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
1000
|
+
const relPath = rel ? path_1.default.join(rel, entry.name) : entry.name;
|
|
1001
|
+
if (entry.isDirectory()) {
|
|
1002
|
+
allSymbols = allSymbols.concat(walkDir(fullPath, relPath));
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
const ext = entry.name.split('.').pop() || '';
|
|
1006
|
+
if (extensions.includes(ext)) {
|
|
1007
|
+
allSymbols = allSymbols.concat(extractSymbols(fullPath, relPath));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
catch { }
|
|
1013
|
+
return allSymbols;
|
|
1014
|
+
};
|
|
1015
|
+
const symbols = walkDir(resolvedDir, dirPath);
|
|
1016
|
+
const byFile = {};
|
|
1017
|
+
symbols.forEach(s => {
|
|
1018
|
+
if (!byFile[s.file])
|
|
1019
|
+
byFile[s.file] = [];
|
|
1020
|
+
byFile[s.file].push({ kind: s.kind, name: s.name, line: s.line });
|
|
1021
|
+
});
|
|
1022
|
+
const hologram = {
|
|
1023
|
+
workspace: path_1.default.basename(workspaceRoot),
|
|
1024
|
+
scannedDir: dirPath || '.',
|
|
1025
|
+
totalSymbols: symbols.length,
|
|
1026
|
+
totalFiles: Object.keys(byFile).length,
|
|
1027
|
+
map: byFile,
|
|
1028
|
+
};
|
|
1029
|
+
return {
|
|
1030
|
+
content: [{ type: "text", text: JSON.stringify(hologram, null, 2) }],
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
case "validate_patch_sandbox": {
|
|
1034
|
+
const filePath = args?.filePath;
|
|
1035
|
+
const patchedContent = args?.patchedContent;
|
|
1036
|
+
if (!filePath || patchedContent === undefined) {
|
|
1037
|
+
throw new Error("filePath and patchedContent are required");
|
|
1038
|
+
}
|
|
1039
|
+
const workspaceRoot = process.cwd();
|
|
1040
|
+
const resolvedPath = path_1.default.resolve(workspaceRoot, filePath);
|
|
1041
|
+
if (!resolvedPath.startsWith(workspaceRoot)) {
|
|
1042
|
+
throw new Error("Access denied: path is outside of workspace root");
|
|
1043
|
+
}
|
|
1044
|
+
const { execSync } = require('child_process');
|
|
1045
|
+
const os = require('os');
|
|
1046
|
+
const sandboxDir = path_1.default.join(os.tmpdir(), `lemma-sandbox-${Date.now()}`);
|
|
1047
|
+
const sandboxFile = path_1.default.join(sandboxDir, path_1.default.basename(filePath));
|
|
1048
|
+
try {
|
|
1049
|
+
fs_1.default.mkdirSync(sandboxDir, { recursive: true });
|
|
1050
|
+
fs_1.default.writeFileSync(sandboxFile, patchedContent, 'utf8');
|
|
1051
|
+
// Step 1: Basic syntax check via Node.js --check (works for JS/TS after strip)
|
|
1052
|
+
// Step 2: Try tsc --noEmit on the patched file using the workspace tsconfig
|
|
1053
|
+
const tsconfigPath = path_1.default.join(workspaceRoot, 'tsconfig.json');
|
|
1054
|
+
let tscResult = { success: true, output: 'No TypeScript config found — skipping tsc validation.' };
|
|
1055
|
+
if (fs_1.default.existsSync(tsconfigPath) && filePath.match(/\.tsx?$/)) {
|
|
1056
|
+
try {
|
|
1057
|
+
// Copy the patched file to a temp location respecting the workspace structure
|
|
1058
|
+
const sandboxWorkspace = path_1.default.join(sandboxDir, 'workspace');
|
|
1059
|
+
// Sync relevant source structure for valid imports check
|
|
1060
|
+
execSync(`cp -r ${workspaceRoot}/src ${sandboxWorkspace}/src 2>/dev/null || true`, { timeout: 5000 });
|
|
1061
|
+
// Overwrite the target file with patched content
|
|
1062
|
+
const sandboxTargetFile = path_1.default.join(sandboxWorkspace, filePath);
|
|
1063
|
+
fs_1.default.mkdirSync(path_1.default.dirname(sandboxTargetFile), { recursive: true });
|
|
1064
|
+
fs_1.default.writeFileSync(sandboxTargetFile, patchedContent, 'utf8');
|
|
1065
|
+
const tscOut = execSync(`cd ${workspaceRoot} && npx tsc --noEmit --skipLibCheck --allowJs --esModuleInterop --strict false 2>&1 | head -40 || true`, { timeout: 20000, encoding: 'utf8' });
|
|
1066
|
+
// Filter errors that reference our target file
|
|
1067
|
+
const relevantErrors = tscOut.split('\n').filter((l) => l.includes(path_1.default.basename(filePath)) || l.includes('error TS'));
|
|
1068
|
+
tscResult = {
|
|
1069
|
+
success: relevantErrors.length === 0,
|
|
1070
|
+
output: relevantErrors.length > 0 ? relevantErrors.join('\n') : '✅ TypeScript check passed with no errors in target file.'
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
catch (e) {
|
|
1074
|
+
tscResult = { success: false, output: e.stdout || e.message };
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
// Step 3: Quick regex-based syntax sanity (balanced braces/brackets)
|
|
1078
|
+
const opens = (patchedContent.match(/[{[(]/g) || []).length;
|
|
1079
|
+
const closes = (patchedContent.match(/[}\])]/g) || []).length;
|
|
1080
|
+
const balanced = Math.abs(opens - closes) <= 2; // allow small tolerance
|
|
1081
|
+
fs_1.default.rmSync(sandboxDir, { recursive: true, force: true });
|
|
1082
|
+
const verdict = tscResult.success && balanced;
|
|
1083
|
+
return {
|
|
1084
|
+
content: [{
|
|
1085
|
+
type: "text",
|
|
1086
|
+
text: JSON.stringify({
|
|
1087
|
+
verdict: verdict ? '✅ SAFE TO APPLY' : '❌ DO NOT APPLY — Issues Found',
|
|
1088
|
+
syntaxBalanced: balanced ? '✅' : '⚠️ Unbalanced brackets/braces detected',
|
|
1089
|
+
tscCheck: tscResult.output,
|
|
1090
|
+
recommendation: verdict
|
|
1091
|
+
? 'Patch looks valid. You can safely call apply_workspace_patch to apply it.'
|
|
1092
|
+
: 'Fix the reported issues before applying the patch to avoid breaking the codebase.',
|
|
1093
|
+
}, null, 2)
|
|
1094
|
+
}]
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
catch (err) {
|
|
1098
|
+
try {
|
|
1099
|
+
fs_1.default.rmSync(sandboxDir, { recursive: true, force: true });
|
|
1100
|
+
}
|
|
1101
|
+
catch { }
|
|
1102
|
+
return {
|
|
1103
|
+
content: [{ type: "text", text: `Sandbox validation error: ${err.message}` }]
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
case "query_hybrid_consensus": {
|
|
1108
|
+
const query = args?.query;
|
|
1109
|
+
const context = args?.context || '';
|
|
1110
|
+
const threshold = typeof args?.threshold === 'number' ? args.threshold : 0.80;
|
|
1111
|
+
if (!query)
|
|
1112
|
+
throw new Error('query is required');
|
|
1113
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
1114
|
+
const portFile = path_1.default.join(HOME, '.lemma-cache/proxy.port');
|
|
1115
|
+
let port = '8081';
|
|
1116
|
+
if (fs_1.default.existsSync(portFile)) {
|
|
1117
|
+
port = fs_1.default.readFileSync(portFile, 'utf8').trim();
|
|
1118
|
+
}
|
|
1119
|
+
const axios = require('axios');
|
|
1120
|
+
const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
|
|
1121
|
+
try {
|
|
1122
|
+
// Step 1: Hit The Brain first
|
|
1123
|
+
const searchRes = await axios.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(fullQuery)}&limit=3`);
|
|
1124
|
+
const results = searchRes.data.results || [];
|
|
1125
|
+
const topHit = results[0];
|
|
1126
|
+
if (topHit && topHit.similarity >= threshold) {
|
|
1127
|
+
// 🎯 Brain HIT — return cached answer, zero cloud tokens spent
|
|
1128
|
+
const savedTokens = Math.floor((topHit.response?.choices?.[0]?.message?.content?.length || 500) / 4);
|
|
1129
|
+
(0, reportSavings_1.reportSavings)({ source: 'cache', tokens: savedTokens });
|
|
1130
|
+
const responseText = typeof topHit.response === 'string'
|
|
1131
|
+
? topHit.response
|
|
1132
|
+
: topHit.response?.choices?.[0]?.message?.content
|
|
1133
|
+
|| JSON.stringify(topHit.response, null, 2);
|
|
1134
|
+
return {
|
|
1135
|
+
content: [{
|
|
1136
|
+
type: 'text',
|
|
1137
|
+
text: `🧠 **Brain Cache HIT** (similarity: ${(topHit.similarity * 100).toFixed(1)}% — above ${(threshold * 100).toFixed(0)}% threshold)\n\n**No cloud LLM call needed. ~${savedTokens} tokens saved.**\n\n---\n\n${responseText}`
|
|
1138
|
+
}]
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
// Step 2: Brain MISS — inform the IDE to proceed with cloud and store the result
|
|
1142
|
+
const missMsg = results.length > 0
|
|
1143
|
+
? `🔍 **Brain Miss** — Best match was only ${(topHit.similarity * 100).toFixed(1)}% (below ${(threshold * 100).toFixed(0)}% threshold).`
|
|
1144
|
+
: `🔍 **Brain Miss** — No relevant memories found for this query.`;
|
|
1145
|
+
return {
|
|
1146
|
+
content: [{
|
|
1147
|
+
type: 'text',
|
|
1148
|
+
text: `${missMsg}\n\n**Proceed with your cloud LLM call.** Once you have the answer, call \`store_memory\` with:\n- query: "${query.substring(0, 100)}"
|
|
1149
|
+
- response: [your full answer]\n\nThis will cache it for future sessions and save tokens next time.`
|
|
1150
|
+
}]
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
catch (e) {
|
|
1154
|
+
return {
|
|
1155
|
+
content: [{ type: 'text', text: `Hybrid Consensus failed: ${e.message}. Is Lemma Proxy running?` }]
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
case "get_telepathic_hints": {
|
|
1160
|
+
const activeFile = args?.activeFile;
|
|
1161
|
+
const limit = typeof args?.limit === 'number' ? args.limit : 5;
|
|
1162
|
+
if (!activeFile)
|
|
1163
|
+
throw new Error('activeFile is required');
|
|
1164
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
1165
|
+
const portFile = path_1.default.join(HOME, '.lemma-cache/proxy.port');
|
|
1166
|
+
let port = '8081';
|
|
1167
|
+
if (fs_1.default.existsSync(portFile)) {
|
|
1168
|
+
port = fs_1.default.readFileSync(portFile, 'utf8').trim();
|
|
1169
|
+
}
|
|
1170
|
+
const axios = require('axios');
|
|
1171
|
+
// Build a rich query from the file path: extract module name, dir context, and file extension
|
|
1172
|
+
const basename = path_1.default.basename(activeFile, path_1.default.extname(activeFile));
|
|
1173
|
+
const dirContext = path_1.default.dirname(activeFile).split(path_1.default.sep).filter(Boolean).slice(-2).join(' ');
|
|
1174
|
+
const ext = path_1.default.extname(activeFile).replace('.', '');
|
|
1175
|
+
const telepathicQuery = `${basename} ${dirContext} ${ext} patterns solutions architecture`;
|
|
1176
|
+
try {
|
|
1177
|
+
const searchRes = await axios.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(telepathicQuery)}&limit=${limit}`);
|
|
1178
|
+
const results = searchRes.data.results || [];
|
|
1179
|
+
if (results.length === 0) {
|
|
1180
|
+
return {
|
|
1181
|
+
content: [{
|
|
1182
|
+
type: 'text',
|
|
1183
|
+
text: `📡 **Telepathic Hints** for \`${activeFile}\`\n\nNo relevant memories found yet. As you use Lemma and store solutions, they will appear here automatically.`
|
|
1184
|
+
}]
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
let hintsText = `📡 **Telepathic Hints** for \`${activeFile}\`\n*(${results.length} relevant memories surfaced from The Brain)*\n\n`;
|
|
1188
|
+
results.forEach((r, i) => {
|
|
1189
|
+
const similarity = (r.similarity * 100).toFixed(1);
|
|
1190
|
+
const prompt = typeof r.prompt === 'string' ? r.prompt.substring(0, 120) : 'Unknown';
|
|
1191
|
+
const responseContent = typeof r.response === 'string'
|
|
1192
|
+
? r.response
|
|
1193
|
+
: r.response?.choices?.[0]?.message?.content
|
|
1194
|
+
|| JSON.stringify(r.response).substring(0, 300);
|
|
1195
|
+
hintsText += `### 💡 Hint ${i + 1} (${similarity}% match)\n`;
|
|
1196
|
+
hintsText += `**Memory:** ${prompt}\n\n`;
|
|
1197
|
+
hintsText += `${responseContent.substring(0, 400)}${responseContent.length > 400 ? '...' : ''}\n\n---\n\n`;
|
|
1198
|
+
});
|
|
1199
|
+
// Report token savings: surface context avoids re-asking the LLM
|
|
1200
|
+
const estimatedTokensSaved = results.length * 150;
|
|
1201
|
+
(0, reportSavings_1.reportSavings)({ source: 'cache', tokens: estimatedTokensSaved });
|
|
1202
|
+
return {
|
|
1203
|
+
content: [{ type: 'text', text: hintsText }]
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
catch (e) {
|
|
1207
|
+
return {
|
|
1208
|
+
content: [{ type: 'text', text: `Telepathic Hints failed: ${e.message}. Is Lemma Proxy running?` }]
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
805
1212
|
default:
|
|
806
1213
|
throw new Error(`Unknown tool: ${name}`);
|
|
807
1214
|
}
|