@nxuss/lemma 0.7.1 → 0.7.2
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/dashboard/dist/assets/index-BoZujIjB.css +1 -0
- package/dashboard/dist/assets/{index-BJzMIGrT.js → index-C3X0fqmd.js} +113 -113
- package/dashboard/dist/assets/index-C3X0fqmd.js.map +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cjs/api/dashboardRoutes.d.ts.map +1 -1
- package/dist/cjs/api/dashboardRoutes.js +41 -1
- package/dist/cjs/api/dashboardRoutes.js.map +1 -1
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +80 -11
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/cjs/mcp/index.js +63 -1192
- package/dist/cjs/mcp/index.js.map +1 -1
- package/dist/cjs/mcp/prompts.d.ts +4 -0
- package/dist/cjs/mcp/prompts.d.ts.map +1 -0
- package/dist/cjs/mcp/prompts.js +84 -0
- package/dist/cjs/mcp/prompts.js.map +1 -0
- package/dist/cjs/mcp/resources.d.ts +3 -0
- package/dist/cjs/mcp/resources.d.ts.map +1 -0
- package/dist/cjs/mcp/resources.js +179 -0
- package/dist/cjs/mcp/resources.js.map +1 -0
- package/dist/cjs/mcp/tools.d.ts +11 -0
- package/dist/cjs/mcp/tools.d.ts.map +1 -0
- package/dist/cjs/mcp/tools.js +1018 -0
- package/dist/cjs/mcp/tools.js.map +1 -0
- package/dist/cjs/mcp/utils.d.ts +11 -0
- package/dist/cjs/mcp/utils.d.ts.map +1 -0
- package/dist/cjs/mcp/utils.js +85 -0
- package/dist/cjs/mcp/utils.js.map +1 -0
- package/dist/cjs/utils/reportSavings.d.ts +6 -0
- package/dist/cjs/utils/reportSavings.d.ts.map +1 -1
- package/dist/cjs/utils/reportSavings.js.map +1 -1
- package/dist/esm/api/dashboardRoutes.d.ts.map +1 -1
- package/dist/esm/api/dashboardRoutes.js +41 -1
- package/dist/esm/api/dashboardRoutes.js.map +1 -1
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +80 -11
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/mcp/index.js +62 -1191
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/mcp/prompts.d.ts +4 -0
- package/dist/esm/mcp/prompts.d.ts.map +1 -0
- package/dist/esm/mcp/prompts.js +80 -0
- package/dist/esm/mcp/prompts.js.map +1 -0
- package/dist/esm/mcp/resources.d.ts +3 -0
- package/dist/esm/mcp/resources.d.ts.map +1 -0
- package/dist/esm/mcp/resources.js +173 -0
- package/dist/esm/mcp/resources.js.map +1 -0
- package/dist/esm/mcp/tools.d.ts +11 -0
- package/dist/esm/mcp/tools.d.ts.map +1 -0
- package/dist/esm/mcp/tools.js +979 -0
- package/dist/esm/mcp/tools.js.map +1 -0
- package/dist/esm/mcp/utils.d.ts +11 -0
- package/dist/esm/mcp/utils.d.ts.map +1 -0
- package/dist/esm/mcp/utils.js +73 -0
- package/dist/esm/mcp/utils.js.map +1 -0
- package/dist/esm/utils/reportSavings.d.ts +6 -0
- package/dist/esm/utils/reportSavings.d.ts.map +1 -1
- package/dist/esm/utils/reportSavings.js.map +1 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/index-BJzMIGrT.js.map +0 -1
- package/dashboard/dist/assets/index-DUOrThix.css +0 -1
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
import * as ts from "typescript";
|
|
8
|
+
import { SemanticScrubber } from "../security/SemanticScrubber";
|
|
9
|
+
import { ComplexityRouter } from "../proxy/ComplexityRouter";
|
|
10
|
+
import { squeezeCode, squeezePrompt } from "../utils/ContextSqueezer";
|
|
11
|
+
import { reportSavings } from "../utils/reportSavings";
|
|
12
|
+
import { getProxyPort, safeResolvePath, logError, logWarn, estimateTokensFromResult } from "./utils";
|
|
13
|
+
const toolDefinitions = [
|
|
14
|
+
{
|
|
15
|
+
name: "scrub_privacy",
|
|
16
|
+
description: "Mask sensitive data (PII, API Keys, Credentials) from a text block using Lemma's Privacy Firewall.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
text: { type: "string", description: "The raw text to scrub" },
|
|
21
|
+
},
|
|
22
|
+
required: ["text"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "search_memory",
|
|
27
|
+
description: "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.",
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
query: { type: "string", description: "The natural language query" },
|
|
32
|
+
limit: { type: "number", description: "Maximum results to return", default: 5 },
|
|
33
|
+
},
|
|
34
|
+
required: ["query"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "store_memory",
|
|
39
|
+
description: "Persist a technical solution, bug fix, architecture decision, or key fact into Lemma's Brain.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
query: { type: "string", description: "The natural language query key" },
|
|
44
|
+
response: { type: "string", description: "The complete technical content to memorize" },
|
|
45
|
+
provider: { type: "string", description: "Optional model provider name", default: "generic" },
|
|
46
|
+
},
|
|
47
|
+
required: ["query", "response"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "get_routing_advice",
|
|
52
|
+
description: "Analyzes a prompt and suggests the best model based on Lemma's Complexity Router.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
prompt: { type: "string", description: "The prompt to analyze" },
|
|
57
|
+
intended_model: { type: "string", description: "The model you were planning to use (e.g., gpt-4o)" },
|
|
58
|
+
},
|
|
59
|
+
required: ["prompt"],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "auto_heal",
|
|
64
|
+
description: "Diagnose and auto-heal the latest local server crash registered in Lemma's context logs.",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
apply: { type: "boolean", description: "Whether to apply the generated code fix directly", default: false },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "read_workspace_file",
|
|
74
|
+
description: "Read a file inside the workspace. Automatically compresses comments/whitespace and scrubs API keys.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
filePath: { type: "string", description: "Path relative to the project root" },
|
|
79
|
+
compact: { type: "boolean", description: "Whether to compress comments and whitespace", default: true },
|
|
80
|
+
},
|
|
81
|
+
required: ["filePath"],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "write_workspace_file",
|
|
86
|
+
description: "Write full contents to a file inside the workspace. Creates parent directories automatically.",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
filePath: { type: "string", description: "Path relative to the project root" },
|
|
91
|
+
content: { type: "string", description: "The complete content to write" },
|
|
92
|
+
},
|
|
93
|
+
required: ["filePath", "content"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "apply_workspace_patch",
|
|
98
|
+
description: "Apply a smart search-and-replace patch to an existing file. Safe against duplicate matches.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
filePath: { type: "string", description: "Path relative to the project root" },
|
|
103
|
+
searchContent: { type: "string", description: "The exact search block to find" },
|
|
104
|
+
replaceContent: { type: "string", description: "The replacement block" },
|
|
105
|
+
},
|
|
106
|
+
required: ["filePath", "searchContent", "replaceContent"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "run_workspace_command",
|
|
111
|
+
description: "Execute a bash command in the workspace root. Maximum execution timeout of 15 seconds.",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
command: { type: "string", description: "The bash command to run" },
|
|
116
|
+
},
|
|
117
|
+
required: ["command"],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "list_workspace_dir",
|
|
122
|
+
description: "List files and subdirectories recursively to navigate the repository structure.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
dirPath: { type: "string", description: "Directory relative to project root", default: "" },
|
|
127
|
+
maxDepth: { type: "number", description: "Maximum recursion depth", default: 3 },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: "search_workspace",
|
|
133
|
+
description: "Perform a fast local text search (grep) across all files in the project workspace.",
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
query: { type: "string", description: "The keyword or text pattern to search for" },
|
|
138
|
+
extension: { type: "string", description: "Optional file extension filter (e.g., 'ts', 'json')" },
|
|
139
|
+
},
|
|
140
|
+
required: ["query"],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "squeeze_prompt",
|
|
145
|
+
description: "Compress code blocks, comments, and boilerplate in any prompt. Saves up to 80% tokens.",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
prompt: { type: "string", description: "The raw prompt containing code blocks to squeeze" },
|
|
150
|
+
query: { type: "string", description: "Optional user's primary query to preserve relevant parts", default: "" },
|
|
151
|
+
},
|
|
152
|
+
required: ["prompt"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "get_project_onboarding",
|
|
157
|
+
description: "Fetch dynamic architectural and stack overview of the current project in markdown.",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "get_ast_hologram",
|
|
165
|
+
description: "Generate a dense, token-efficient Holographic AST Map of the workspace using the TypeScript compiler. Returns structured JSON of all exported symbols with file paths and line numbers.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
dirPath: { type: "string", description: "Directory to scan relative to workspace root", default: "" },
|
|
170
|
+
extensions: {
|
|
171
|
+
type: "array",
|
|
172
|
+
items: { type: "string" },
|
|
173
|
+
description: "File extensions to scan",
|
|
174
|
+
default: ["ts", "tsx", "js", "jsx"],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: "validate_patch_sandbox",
|
|
181
|
+
description: "Validate proposed code in isolated sandbox via tsc + syntax check before applying.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
filePath: { type: "string", description: "Target file path relative to workspace root" },
|
|
186
|
+
patchedContent: { type: "string", description: "The complete proposed file content after patch" },
|
|
187
|
+
},
|
|
188
|
+
required: ["filePath", "patchedContent"],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "query_hybrid_consensus",
|
|
193
|
+
description: "Search The Brain before reasoning. Brain HIT returns cached answer instantly. Brain MISS proceeds to cloud.",
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
query: { type: "string", description: "The technical question or task description" },
|
|
198
|
+
context: { type: "string", description: "Optional extra context" },
|
|
199
|
+
threshold: { type: "number", description: "Similarity threshold (0.0-1.0, default: 0.80)", default: 0.80 },
|
|
200
|
+
},
|
|
201
|
+
required: ["query"],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "get_telepathic_hints",
|
|
206
|
+
description: "Surface relevant past solutions from The Brain based on the active file path.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
activeFile: { type: "string", description: "Path of the file being edited" },
|
|
211
|
+
limit: { type: "number", description: "Maximum hints (default: 5)", default: 5 },
|
|
212
|
+
},
|
|
213
|
+
required: ["activeFile"],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
const toolHandlers = {
|
|
218
|
+
scrub_privacy: handleScrubPrivacy,
|
|
219
|
+
search_memory: handleSearchMemory,
|
|
220
|
+
store_memory: handleStoreMemory,
|
|
221
|
+
get_routing_advice: handleGetRoutingAdvice,
|
|
222
|
+
auto_heal: handleAutoHeal,
|
|
223
|
+
read_workspace_file: handleReadWorkspaceFile,
|
|
224
|
+
write_workspace_file: handleWriteWorkspaceFile,
|
|
225
|
+
apply_workspace_patch: handleApplyWorkspacePatch,
|
|
226
|
+
run_workspace_command: handleRunWorkspaceCommand,
|
|
227
|
+
list_workspace_dir: handleListWorkspaceDir,
|
|
228
|
+
search_workspace: handleSearchWorkspace,
|
|
229
|
+
squeeze_prompt: handleSqueezePrompt,
|
|
230
|
+
get_project_onboarding: handleGetProjectOnboarding,
|
|
231
|
+
get_ast_hologram: handleGetAstHologram,
|
|
232
|
+
validate_patch_sandbox: handleValidatePatchSandbox,
|
|
233
|
+
query_hybrid_consensus: handleQueryHybridConsensus,
|
|
234
|
+
get_telepathic_hints: handleGetTelepathicHints,
|
|
235
|
+
};
|
|
236
|
+
export function setupToolsHandlers(server, onToolCall) {
|
|
237
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
238
|
+
tools: toolDefinitions,
|
|
239
|
+
}));
|
|
240
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
241
|
+
const { name, arguments: args } = request.params;
|
|
242
|
+
const startTime = Date.now();
|
|
243
|
+
const handler = toolHandlers[name];
|
|
244
|
+
if (!handler) {
|
|
245
|
+
const latency = Date.now() - startTime;
|
|
246
|
+
onToolCall?.({
|
|
247
|
+
tool: name,
|
|
248
|
+
args: args || {},
|
|
249
|
+
result: "ERROR",
|
|
250
|
+
latency,
|
|
251
|
+
error: "Unknown tool",
|
|
252
|
+
});
|
|
253
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const result = await handler((args || {}));
|
|
257
|
+
onToolCall?.({
|
|
258
|
+
tool: name,
|
|
259
|
+
args: args || {},
|
|
260
|
+
result: "OK",
|
|
261
|
+
latency: Date.now() - startTime,
|
|
262
|
+
tokensImpact: estimateTokensFromResult(result),
|
|
263
|
+
});
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
onToolCall?.({
|
|
268
|
+
tool: name,
|
|
269
|
+
args: args || {},
|
|
270
|
+
result: "ERROR",
|
|
271
|
+
latency: Date.now() - startTime,
|
|
272
|
+
error: error.message?.substring(0, 100) || "Unknown error",
|
|
273
|
+
});
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// ── Tool Implementations ────────────────────────────────────────────
|
|
279
|
+
async function handleScrubPrivacy(args) {
|
|
280
|
+
const text = args?.text;
|
|
281
|
+
if (!text)
|
|
282
|
+
throw new Error("Text is required");
|
|
283
|
+
const scrubber = new SemanticScrubber();
|
|
284
|
+
const { maskedPrompt } = scrubber.mask(text);
|
|
285
|
+
return { content: [{ type: "text", text: maskedPrompt }] };
|
|
286
|
+
}
|
|
287
|
+
async function handleSearchMemory(args) {
|
|
288
|
+
const query = args?.query;
|
|
289
|
+
const limit = args?.limit || 5;
|
|
290
|
+
if (!query)
|
|
291
|
+
throw new Error("Query is required");
|
|
292
|
+
try {
|
|
293
|
+
const port = getProxyPort();
|
|
294
|
+
const response = await axios.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`);
|
|
295
|
+
const results = response.data.results || [];
|
|
296
|
+
if (results.length === 0) {
|
|
297
|
+
return { content: [{ type: "text", text: "No relevant memories found in Lemma's Brain." }] };
|
|
298
|
+
}
|
|
299
|
+
const formatted = results
|
|
300
|
+
.map((r, i) => {
|
|
301
|
+
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)}`;
|
|
302
|
+
})
|
|
303
|
+
.join("\n\n---\n\n");
|
|
304
|
+
return { content: [{ type: "text", text: `Lemma found ${results.length} memories:\n\n${formatted}` }] };
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
logError("search_memory", e);
|
|
308
|
+
return { content: [{ type: "text", text: `Neural Search failed: ${e.message}. Is Lemma Proxy running?` }] };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function handleStoreMemory(args) {
|
|
312
|
+
const query = args?.query;
|
|
313
|
+
const responseText = args?.response;
|
|
314
|
+
const provider = args?.provider || "generic";
|
|
315
|
+
if (!query || !responseText)
|
|
316
|
+
throw new Error("Query and response are required");
|
|
317
|
+
try {
|
|
318
|
+
const port = getProxyPort();
|
|
319
|
+
const response = await axios.post(`http://localhost:${port}/api/memory/store`, {
|
|
320
|
+
query,
|
|
321
|
+
response: responseText,
|
|
322
|
+
provider,
|
|
323
|
+
});
|
|
324
|
+
const tokensSaved = Math.max(100, Math.floor(responseText.length / 4));
|
|
325
|
+
reportSavings({
|
|
326
|
+
source: "cache",
|
|
327
|
+
tokens: tokensSaved,
|
|
328
|
+
toolName: "store_memory",
|
|
329
|
+
query: query.substring(0, 100),
|
|
330
|
+
});
|
|
331
|
+
return { content: [{ type: "text", text: `Success: ${response.data.message}` }] };
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
logError("store_memory", e);
|
|
335
|
+
return { content: [{ type: "text", text: `Failed to store memory in Lemma: ${e.message}. Is Lemma Proxy running?` }] };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function handleGetRoutingAdvice(args) {
|
|
339
|
+
const prompt = args?.prompt;
|
|
340
|
+
const intendedModel = args?.intended_model;
|
|
341
|
+
const router = new ComplexityRouter();
|
|
342
|
+
const decision = router.evaluate(prompt, intendedModel);
|
|
343
|
+
if (decision.complexity === "low" && intendedModel && decision.model !== intendedModel) {
|
|
344
|
+
const estimatedPromptTokens = Math.floor((prompt?.length || 0) / 4);
|
|
345
|
+
reportSavings({
|
|
346
|
+
source: "complexityRouting",
|
|
347
|
+
tokens: Math.floor(estimatedPromptTokens * 0.8),
|
|
348
|
+
toolName: "get_routing_advice",
|
|
349
|
+
query: prompt?.substring(0, 100),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
content: [
|
|
354
|
+
{
|
|
355
|
+
type: "text",
|
|
356
|
+
text: `Lemma Routing Advice: Use ${decision.model}. Reason: Complexity is ${decision.complexity}.`,
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function handleAutoHeal(args) {
|
|
362
|
+
const apply = !!args?.apply;
|
|
363
|
+
try {
|
|
364
|
+
const { performAutoHeal: heal } = await import("../cli/lemma-proxy");
|
|
365
|
+
const res = await heal(apply);
|
|
366
|
+
return { content: [{ type: "text", text: JSON.stringify(res, null, 2) }] };
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
logError("auto_heal", e);
|
|
370
|
+
return { content: [{ type: "text", text: `Auto-heal failed: ${e.message}` }] };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function handleReadWorkspaceFile(args) {
|
|
374
|
+
const filePath = args?.filePath;
|
|
375
|
+
const compact = args?.compact !== false;
|
|
376
|
+
if (!filePath)
|
|
377
|
+
throw new Error("filePath is required");
|
|
378
|
+
const workspaceRoot = process.cwd();
|
|
379
|
+
try {
|
|
380
|
+
const { resolved } = safeResolvePath(workspaceRoot, filePath);
|
|
381
|
+
let content = fs.readFileSync(resolved, "utf8");
|
|
382
|
+
const originalSize = content.length;
|
|
383
|
+
if (compact) {
|
|
384
|
+
content = squeezeCode(content);
|
|
385
|
+
if (content.length < originalSize) {
|
|
386
|
+
reportSavings({
|
|
387
|
+
source: "contextSqueeze",
|
|
388
|
+
charsBefore: originalSize,
|
|
389
|
+
charsAfter: content.length,
|
|
390
|
+
toolName: "read_workspace_file",
|
|
391
|
+
filePath,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
const scrubber = new SemanticScrubber();
|
|
397
|
+
const { maskedPrompt } = scrubber.mask(content);
|
|
398
|
+
content = maskedPrompt;
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
logWarn("read_workspace_file", "SemanticScrubber failed, returning raw content");
|
|
402
|
+
}
|
|
403
|
+
return { content: [{ type: "text", text: content }] };
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
logError("read_workspace_file", err);
|
|
407
|
+
return { content: [{ type: "text", text: `Error reading file: ${err.message}` }] };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function handleWriteWorkspaceFile(args) {
|
|
411
|
+
const filePath = args?.filePath;
|
|
412
|
+
const content = args?.content;
|
|
413
|
+
if (!filePath || content === undefined)
|
|
414
|
+
throw new Error("filePath and content are required");
|
|
415
|
+
const workspaceRoot = process.cwd();
|
|
416
|
+
try {
|
|
417
|
+
const { resolved } = safeResolvePath(workspaceRoot, filePath);
|
|
418
|
+
const dir = path.dirname(resolved);
|
|
419
|
+
if (!fs.existsSync(dir)) {
|
|
420
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
421
|
+
}
|
|
422
|
+
fs.writeFileSync(resolved, content, "utf8");
|
|
423
|
+
return { content: [{ type: "text", text: `Success: Written file to ${filePath}` }] };
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
logError("write_workspace_file", err);
|
|
427
|
+
return { content: [{ type: "text", text: `Error writing file: ${err.message}` }] };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function handleApplyWorkspacePatch(args) {
|
|
431
|
+
const filePath = args?.filePath;
|
|
432
|
+
const searchContent = args?.searchContent;
|
|
433
|
+
const replaceContent = args?.replaceContent;
|
|
434
|
+
if (!filePath || !searchContent || replaceContent === undefined) {
|
|
435
|
+
throw new Error("filePath, searchContent, and replaceContent are required");
|
|
436
|
+
}
|
|
437
|
+
const workspaceRoot = process.cwd();
|
|
438
|
+
try {
|
|
439
|
+
const { resolved } = safeResolvePath(workspaceRoot, filePath);
|
|
440
|
+
if (!fs.existsSync(resolved)) {
|
|
441
|
+
return { content: [{ type: "text", text: `Error: File ${filePath} does not exist.` }] };
|
|
442
|
+
}
|
|
443
|
+
const originalContent = fs.readFileSync(resolved, "utf8");
|
|
444
|
+
const index = originalContent.indexOf(searchContent);
|
|
445
|
+
if (index === -1) {
|
|
446
|
+
return {
|
|
447
|
+
content: [
|
|
448
|
+
{
|
|
449
|
+
type: "text",
|
|
450
|
+
text: `Error: Target search content not found in file. Make sure the search content matches exactly, including indentation, tabs, spaces, and newlines.`,
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
if (originalContent.indexOf(searchContent, index + 1) !== -1) {
|
|
456
|
+
return {
|
|
457
|
+
content: [
|
|
458
|
+
{
|
|
459
|
+
type: "text",
|
|
460
|
+
text: `Error: Multiple matches found for the search content. Please provide a larger search block with unique surrounding context lines to make it unambiguous.`,
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const updatedContent = originalContent.replace(searchContent, replaceContent);
|
|
466
|
+
fs.writeFileSync(resolved, updatedContent, "utf8");
|
|
467
|
+
return { content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}` }] };
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
logError("apply_workspace_patch", err);
|
|
471
|
+
return { content: [{ type: "text", text: `Error applying patch: ${err.message}` }] };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function handleRunWorkspaceCommand(args) {
|
|
475
|
+
const command = args?.command;
|
|
476
|
+
if (!command)
|
|
477
|
+
throw new Error("command is required");
|
|
478
|
+
const workspaceRoot = process.cwd();
|
|
479
|
+
try {
|
|
480
|
+
const output = execSync(command, {
|
|
481
|
+
cwd: workspaceRoot,
|
|
482
|
+
encoding: "utf8",
|
|
483
|
+
timeout: 15000,
|
|
484
|
+
env: { ...process.env },
|
|
485
|
+
});
|
|
486
|
+
return { content: [{ type: "text", text: `Command completed successfully.\n\nOutput:\n${output}` }] };
|
|
487
|
+
}
|
|
488
|
+
catch (err) {
|
|
489
|
+
return {
|
|
490
|
+
content: [
|
|
491
|
+
{
|
|
492
|
+
type: "text",
|
|
493
|
+
text: `Command failed with exit code ${err.status || "unknown"}.\n\nStdout:\n${err.stdout || ""}\n\nStderr:\n${err.stderr || err.message || ""}`,
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function listDirRecursive(currentDir, relativePath, depth, maxDepth) {
|
|
500
|
+
if (depth > maxDepth)
|
|
501
|
+
return [];
|
|
502
|
+
try {
|
|
503
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
504
|
+
let result = [];
|
|
505
|
+
for (const entry of entries) {
|
|
506
|
+
if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(entry.name))
|
|
507
|
+
continue;
|
|
508
|
+
const rel = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
509
|
+
const full = path.join(currentDir, entry.name);
|
|
510
|
+
if (entry.isDirectory()) {
|
|
511
|
+
result.push(`${rel}/`);
|
|
512
|
+
result.push(...listDirRecursive(full, rel, depth + 1, maxDepth));
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
result.push(rel);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async function handleListWorkspaceDir(args) {
|
|
525
|
+
const dirPath = args?.dirPath || "";
|
|
526
|
+
const maxDepth = args?.maxDepth || 3;
|
|
527
|
+
const workspaceRoot = process.cwd();
|
|
528
|
+
try {
|
|
529
|
+
const { resolved } = safeResolvePath(workspaceRoot, dirPath);
|
|
530
|
+
const list = listDirRecursive(resolved, dirPath, 1, maxDepth);
|
|
531
|
+
return {
|
|
532
|
+
content: [
|
|
533
|
+
{
|
|
534
|
+
type: "text",
|
|
535
|
+
text: list.length > 0 ? list.join("\n") : "(Directory is empty or inaccessible)",
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
logError("list_workspace_dir", err);
|
|
542
|
+
return { content: [{ type: "text", text: `Error listing directory: ${err.message}` }] };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function searchDirRecursive(currentDir, relativePath, query, extFilter) {
|
|
546
|
+
try {
|
|
547
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
548
|
+
let result = [];
|
|
549
|
+
for (const entry of entries) {
|
|
550
|
+
if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(entry.name))
|
|
551
|
+
continue;
|
|
552
|
+
const rel = relativePath ? path.join(relativePath, entry.name) : entry.name;
|
|
553
|
+
const full = path.join(currentDir, entry.name);
|
|
554
|
+
if (entry.isDirectory()) {
|
|
555
|
+
result.push(...searchDirRecursive(full, rel, query, extFilter));
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
if (extFilter && !entry.name.endsWith(`.${extFilter}`))
|
|
559
|
+
continue;
|
|
560
|
+
try {
|
|
561
|
+
const content = fs.readFileSync(full, "utf8");
|
|
562
|
+
if (content.toLowerCase().includes(query.toLowerCase())) {
|
|
563
|
+
const lines = content.split("\n");
|
|
564
|
+
lines.forEach((lineText, idx) => {
|
|
565
|
+
if (lineText.toLowerCase().includes(query.toLowerCase())) {
|
|
566
|
+
result.push({ filePath: rel, line: idx + 1, text: lineText.trim() });
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch { }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function handleSearchWorkspace(args) {
|
|
581
|
+
const query = args?.query;
|
|
582
|
+
const extension = args?.extension;
|
|
583
|
+
if (!query)
|
|
584
|
+
throw new Error("query is required");
|
|
585
|
+
const workspaceRoot = process.cwd();
|
|
586
|
+
try {
|
|
587
|
+
const results = searchDirRecursive(workspaceRoot, "", query, extension);
|
|
588
|
+
if (results.length === 0) {
|
|
589
|
+
return { content: [{ type: "text", text: `No matches found in the workspace for "${query}".` }] };
|
|
590
|
+
}
|
|
591
|
+
let formatted = results.map((r) => `[${r.filePath}:${r.line}] ${r.text}`).join("\n");
|
|
592
|
+
try {
|
|
593
|
+
const scrubber = new SemanticScrubber();
|
|
594
|
+
const { maskedPrompt } = scrubber.mask(formatted);
|
|
595
|
+
formatted = maskedPrompt;
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
logWarn("search_workspace", "SemanticScrubber failed, returning raw results");
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
content: [{ type: "text", text: `Found ${results.length} matches:\n\n${formatted}` }],
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
logError("search_workspace", err);
|
|
606
|
+
return { content: [{ type: "text", text: `Error searching workspace: ${err.message}` }] };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async function handleSqueezePrompt(args) {
|
|
610
|
+
const promptArg = args?.prompt;
|
|
611
|
+
const queryArg = args?.query || "";
|
|
612
|
+
if (!promptArg)
|
|
613
|
+
throw new Error("prompt is required");
|
|
614
|
+
try {
|
|
615
|
+
const result = squeezePrompt(promptArg, queryArg);
|
|
616
|
+
if (result.originalSize > result.squeezedSize) {
|
|
617
|
+
reportSavings({
|
|
618
|
+
source: "contextSqueeze",
|
|
619
|
+
charsBefore: result.originalSize,
|
|
620
|
+
charsAfter: result.squeezedSize,
|
|
621
|
+
toolName: "squeeze_prompt",
|
|
622
|
+
query: queryArg?.substring(0, 100),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return { content: [{ type: "text", text: result.squeezed }] };
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
logError("squeeze_prompt", err);
|
|
629
|
+
return { content: [{ type: "text", text: `Error squeezing prompt: ${err.message}` }] };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async function handleGetProjectOnboarding(_args) {
|
|
633
|
+
try {
|
|
634
|
+
const port = getProxyPort();
|
|
635
|
+
const response = await axios.get(`http://localhost:${port}/api/project/onboarding`);
|
|
636
|
+
return {
|
|
637
|
+
content: [
|
|
638
|
+
{
|
|
639
|
+
type: "text",
|
|
640
|
+
text: response.data.markdown || "# Onboarding mental model could not be generated.",
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
logError("get_project_onboarding", error);
|
|
647
|
+
return {
|
|
648
|
+
content: [
|
|
649
|
+
{
|
|
650
|
+
type: "text",
|
|
651
|
+
text: `Failed to fetch codebase onboarding mental model: ${error.message}. Is Lemma proxy running?`,
|
|
652
|
+
},
|
|
653
|
+
],
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function extractSymbolsWithTsCompiler(filePath, relPath) {
|
|
658
|
+
try {
|
|
659
|
+
const src = fs.readFileSync(filePath, "utf8");
|
|
660
|
+
const sourceFile = ts.createSourceFile(filePath, src, ts.ScriptTarget.Latest, true);
|
|
661
|
+
const symbols = [];
|
|
662
|
+
function visit(node) {
|
|
663
|
+
const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
664
|
+
const isExport = mods?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
665
|
+
if (isExport || ts.isSourceFile(node.parent)) {
|
|
666
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
667
|
+
symbols.push({
|
|
668
|
+
kind: "function",
|
|
669
|
+
name: node.name.text,
|
|
670
|
+
file: relPath,
|
|
671
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
else if ((ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) &&
|
|
675
|
+
node.name) {
|
|
676
|
+
symbols.push({
|
|
677
|
+
kind: ts.isClassDeclaration(node) ? "class" : "interface",
|
|
678
|
+
name: node.name.text,
|
|
679
|
+
file: relPath,
|
|
680
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
else if (ts.isTypeAliasDeclaration(node) && node.name) {
|
|
684
|
+
symbols.push({
|
|
685
|
+
kind: "type",
|
|
686
|
+
name: node.name.text,
|
|
687
|
+
file: relPath,
|
|
688
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
else if (ts.isEnumDeclaration(node) && node.name) {
|
|
692
|
+
symbols.push({
|
|
693
|
+
kind: "enum",
|
|
694
|
+
name: node.name.text,
|
|
695
|
+
file: relPath,
|
|
696
|
+
line: sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
else if (ts.isVariableStatement(node) &&
|
|
700
|
+
isExport &&
|
|
701
|
+
node.declarationList.declarations.length > 0) {
|
|
702
|
+
for (const decl of node.declarationList.declarations) {
|
|
703
|
+
if (ts.isIdentifier(decl.name)) {
|
|
704
|
+
symbols.push({
|
|
705
|
+
kind: "const",
|
|
706
|
+
name: decl.name.text,
|
|
707
|
+
file: relPath,
|
|
708
|
+
line: sourceFile.getLineAndCharacterOfPosition(decl.getStart()).line + 1,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// Handle `export { X, Y }` and `export { X as Y }`
|
|
715
|
+
if (ts.isExportDeclaration(node)) {
|
|
716
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
717
|
+
for (const spec of node.exportClause.elements) {
|
|
718
|
+
symbols.push({
|
|
719
|
+
kind: "export",
|
|
720
|
+
name: spec.name.text,
|
|
721
|
+
file: relPath,
|
|
722
|
+
line: sourceFile.getLineAndCharacterOfPosition(spec.getStart()).line + 1,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
ts.forEachChild(node, visit);
|
|
728
|
+
}
|
|
729
|
+
visit(sourceFile);
|
|
730
|
+
return symbols;
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
logWarn("get_ast_hologram", `Could not parse ${relPath}: ${err}`);
|
|
734
|
+
return [];
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function walkDirForHologram(dir, rel, extensions) {
|
|
738
|
+
let allSymbols = [];
|
|
739
|
+
try {
|
|
740
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
741
|
+
for (const entry of entries) {
|
|
742
|
+
if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(entry.name))
|
|
743
|
+
continue;
|
|
744
|
+
const fullPath = path.join(dir, entry.name);
|
|
745
|
+
const relPath = rel ? path.join(rel, entry.name) : entry.name;
|
|
746
|
+
if (entry.isDirectory()) {
|
|
747
|
+
allSymbols = allSymbols.concat(walkDirForHologram(fullPath, relPath, extensions));
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
const ext = entry.name.split(".").pop() || "";
|
|
751
|
+
if (extensions.includes(ext)) {
|
|
752
|
+
allSymbols = allSymbols.concat(extractSymbolsWithTsCompiler(fullPath, relPath));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch (err) {
|
|
758
|
+
logWarn("get_ast_hologram", `Error walking dir ${dir}: ${err}`);
|
|
759
|
+
}
|
|
760
|
+
return allSymbols;
|
|
761
|
+
}
|
|
762
|
+
async function handleGetAstHologram(args) {
|
|
763
|
+
const dirPath = args?.dirPath || "";
|
|
764
|
+
const extensions = args?.extensions || ["ts", "tsx", "js", "jsx"];
|
|
765
|
+
const workspaceRoot = process.cwd();
|
|
766
|
+
const { resolved } = safeResolvePath(workspaceRoot, dirPath);
|
|
767
|
+
const symbols = walkDirForHologram(resolved, dirPath, extensions);
|
|
768
|
+
const byFile = {};
|
|
769
|
+
symbols.forEach((s) => {
|
|
770
|
+
if (!byFile[s.file])
|
|
771
|
+
byFile[s.file] = [];
|
|
772
|
+
byFile[s.file].push({ kind: s.kind, name: s.name, line: s.line });
|
|
773
|
+
});
|
|
774
|
+
const hologram = {
|
|
775
|
+
workspace: path.basename(workspaceRoot),
|
|
776
|
+
scannedDir: dirPath || ".",
|
|
777
|
+
totalSymbols: symbols.length,
|
|
778
|
+
totalFiles: Object.keys(byFile).length,
|
|
779
|
+
map: byFile,
|
|
780
|
+
};
|
|
781
|
+
return { content: [{ type: "text", text: JSON.stringify(hologram, null, 2) }] };
|
|
782
|
+
}
|
|
783
|
+
// ── Validate Patch Sandbox ──────────────────────────────────────────
|
|
784
|
+
async function handleValidatePatchSandbox(args) {
|
|
785
|
+
const filePath = args?.filePath;
|
|
786
|
+
const patchedContent = args?.patchedContent;
|
|
787
|
+
if (!filePath || patchedContent === undefined) {
|
|
788
|
+
throw new Error("filePath and patchedContent are required");
|
|
789
|
+
}
|
|
790
|
+
const workspaceRoot = process.cwd();
|
|
791
|
+
try {
|
|
792
|
+
safeResolvePath(workspaceRoot, filePath);
|
|
793
|
+
}
|
|
794
|
+
catch (err) {
|
|
795
|
+
return { content: [{ type: "text", text: `Access denied: ${err.message}` }] };
|
|
796
|
+
}
|
|
797
|
+
const sandboxDir = path.join(os.tmpdir(), `lemma-sandbox-${Date.now()}`);
|
|
798
|
+
const sandboxFile = path.join(sandboxDir, path.basename(filePath));
|
|
799
|
+
try {
|
|
800
|
+
fs.mkdirSync(sandboxDir, { recursive: true });
|
|
801
|
+
fs.writeFileSync(sandboxFile, patchedContent, "utf8");
|
|
802
|
+
// Step 1: Try tsc --noEmit validation
|
|
803
|
+
const tsconfigPath = path.join(workspaceRoot, "tsconfig.json");
|
|
804
|
+
let tscResult = {
|
|
805
|
+
success: true,
|
|
806
|
+
output: "No TypeScript config found -- skipping tsc validation.",
|
|
807
|
+
};
|
|
808
|
+
if (fs.existsSync(tsconfigPath) && filePath.match(/\.tsx?$/)) {
|
|
809
|
+
try {
|
|
810
|
+
const sandboxWorkspace = path.join(sandboxDir, "workspace");
|
|
811
|
+
execSync(`cp -r ${workspaceRoot}/src ${sandboxWorkspace}/src 2>/dev/null || true`, {
|
|
812
|
+
timeout: 5000,
|
|
813
|
+
});
|
|
814
|
+
const sandboxTargetFile = path.join(sandboxWorkspace, filePath);
|
|
815
|
+
fs.mkdirSync(path.dirname(sandboxTargetFile), { recursive: true });
|
|
816
|
+
fs.writeFileSync(sandboxTargetFile, patchedContent, "utf8");
|
|
817
|
+
const tscOut = execSync(`cd ${workspaceRoot} && npx tsc --noEmit --skipLibCheck --allowJs --esModuleInterop --strict false 2>&1 | head -40 || true`, { timeout: 20000, encoding: "utf8" });
|
|
818
|
+
const relevantErrors = tscOut
|
|
819
|
+
.split("\n")
|
|
820
|
+
.filter((l) => l.includes(path.basename(filePath)) || l.includes("error TS"));
|
|
821
|
+
tscResult = {
|
|
822
|
+
success: relevantErrors.length === 0,
|
|
823
|
+
output: relevantErrors.length > 0
|
|
824
|
+
? relevantErrors.join("\n")
|
|
825
|
+
: "TypeScript check passed with no errors in target file.",
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
catch (e) {
|
|
829
|
+
tscResult = { success: false, output: e.stdout || e.message };
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
// Step 2: Quick bracket/brace balance check
|
|
833
|
+
const opens = (patchedContent.match(/[{[(]/g) || []).length;
|
|
834
|
+
const closes = (patchedContent.match(/[}\])]/g) || []).length;
|
|
835
|
+
const balanced = Math.abs(opens - closes) <= 2;
|
|
836
|
+
fs.rmSync(sandboxDir, { recursive: true, force: true });
|
|
837
|
+
const verdict = tscResult.success && balanced;
|
|
838
|
+
return {
|
|
839
|
+
content: [
|
|
840
|
+
{
|
|
841
|
+
type: "text",
|
|
842
|
+
text: JSON.stringify({
|
|
843
|
+
verdict: verdict ? "SAFE TO APPLY" : "DO NOT APPLY -- Issues Found",
|
|
844
|
+
syntaxBalanced: balanced ? "OK" : "Unbalanced brackets/braces detected",
|
|
845
|
+
tscCheck: tscResult.output,
|
|
846
|
+
recommendation: verdict
|
|
847
|
+
? "Patch looks valid. You can safely call apply_workspace_patch to apply it."
|
|
848
|
+
: "Fix the reported issues before applying the patch to avoid breaking the codebase.",
|
|
849
|
+
}, null, 2),
|
|
850
|
+
},
|
|
851
|
+
],
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
catch (err) {
|
|
855
|
+
try {
|
|
856
|
+
fs.rmSync(sandboxDir, { recursive: true, force: true });
|
|
857
|
+
}
|
|
858
|
+
catch { }
|
|
859
|
+
return { content: [{ type: "text", text: `Sandbox validation error: ${err.message}` }] };
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// ── Hybrid Consensus ────────────────────────────────────────────────
|
|
863
|
+
async function handleQueryHybridConsensus(args) {
|
|
864
|
+
const query = args?.query;
|
|
865
|
+
const context = args?.context || "";
|
|
866
|
+
const threshold = typeof args?.threshold === "number" ? args.threshold : 0.8;
|
|
867
|
+
if (!query)
|
|
868
|
+
throw new Error("query is required");
|
|
869
|
+
const port = getProxyPort();
|
|
870
|
+
const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
|
|
871
|
+
try {
|
|
872
|
+
const searchRes = await axios.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(fullQuery)}&limit=3`);
|
|
873
|
+
const results = searchRes.data.results || [];
|
|
874
|
+
const topHit = results[0];
|
|
875
|
+
if (topHit && topHit.similarity >= threshold) {
|
|
876
|
+
const savedTokens = Math.floor((topHit.response?.choices?.[0]?.message?.content?.length || 500) / 4);
|
|
877
|
+
reportSavings({
|
|
878
|
+
source: "cache",
|
|
879
|
+
tokens: savedTokens,
|
|
880
|
+
toolName: "query_hybrid_consensus",
|
|
881
|
+
query: query.substring(0, 100),
|
|
882
|
+
});
|
|
883
|
+
const responseText = typeof topHit.response === "string"
|
|
884
|
+
? topHit.response
|
|
885
|
+
: topHit.response?.choices?.[0]?.message?.content ||
|
|
886
|
+
JSON.stringify(topHit.response, null, 2);
|
|
887
|
+
return {
|
|
888
|
+
content: [
|
|
889
|
+
{
|
|
890
|
+
type: "text",
|
|
891
|
+
text: `Brain Cache HIT (similarity: ${(topHit.similarity * 100).toFixed(1)}% -- above ${(threshold * 100).toFixed(0)}% threshold)\n\nNo cloud LLM call needed. ~${savedTokens} tokens saved.\n\n---\n\n${responseText}`,
|
|
892
|
+
},
|
|
893
|
+
],
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const missMsg = results.length > 0
|
|
897
|
+
? `Brain Miss -- Best match was only ${(topHit.similarity * 100).toFixed(1)}% (below ${(threshold * 100).toFixed(0)}% threshold).`
|
|
898
|
+
: `Brain Miss -- No relevant memories found for this query.`;
|
|
899
|
+
return {
|
|
900
|
+
content: [
|
|
901
|
+
{
|
|
902
|
+
type: "text",
|
|
903
|
+
text: `${missMsg}\n\nProceed with your cloud LLM call. Once you have the answer, call store_memory with:\n- query: "${query.substring(0, 100)}"\n- response: [your full answer]\n\nThis will cache it for future sessions and save tokens next time.`,
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
catch (e) {
|
|
909
|
+
logError("query_hybrid_consensus", e);
|
|
910
|
+
return {
|
|
911
|
+
content: [
|
|
912
|
+
{ type: "text", text: `Hybrid Consensus failed: ${e.message}. Is Lemma Proxy running?` },
|
|
913
|
+
],
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
// ── Telepathic Hints ────────────────────────────────────────────────
|
|
918
|
+
async function handleGetTelepathicHints(args) {
|
|
919
|
+
const activeFile = args?.activeFile;
|
|
920
|
+
const limit = typeof args?.limit === "number" ? args.limit : 5;
|
|
921
|
+
if (!activeFile)
|
|
922
|
+
throw new Error("activeFile is required");
|
|
923
|
+
const port = getProxyPort();
|
|
924
|
+
const basename = path.basename(activeFile, path.extname(activeFile));
|
|
925
|
+
const dirContext = path
|
|
926
|
+
.dirname(activeFile)
|
|
927
|
+
.split(path.sep)
|
|
928
|
+
.filter(Boolean)
|
|
929
|
+
.slice(-2)
|
|
930
|
+
.join(" ");
|
|
931
|
+
const ext = path.extname(activeFile).replace(".", "");
|
|
932
|
+
const telepathicQuery = `${basename} ${dirContext} ${ext} patterns solutions architecture`;
|
|
933
|
+
try {
|
|
934
|
+
const searchRes = await axios.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(telepathicQuery)}&limit=${limit}`);
|
|
935
|
+
const results = searchRes.data.results || [];
|
|
936
|
+
if (results.length === 0) {
|
|
937
|
+
return {
|
|
938
|
+
content: [
|
|
939
|
+
{
|
|
940
|
+
type: "text",
|
|
941
|
+
text: `Telepathic Hints for \`${activeFile}\`\n\nNo relevant memories found yet. As you use Lemma and store solutions, they will appear here automatically.`,
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
let hintsText = `Telepathic Hints for \`${activeFile}\`\n*(${results.length} relevant memories surfaced from The Brain)*\n\n`;
|
|
947
|
+
results.forEach((r, i) => {
|
|
948
|
+
const similarity = (r.similarity * 100).toFixed(1);
|
|
949
|
+
const prompt = typeof r.prompt === "string" ? r.prompt.substring(0, 120) : "Unknown";
|
|
950
|
+
const responseContent = typeof r.response === "string"
|
|
951
|
+
? r.response
|
|
952
|
+
: r.response?.choices?.[0]?.message?.content ||
|
|
953
|
+
JSON.stringify(r.response).substring(0, 300);
|
|
954
|
+
hintsText += `### Hint ${i + 1} (${similarity}% match)\n`;
|
|
955
|
+
hintsText += `**Memory:** ${prompt}\n\n`;
|
|
956
|
+
hintsText += `${responseContent.substring(0, 400)}${responseContent.length > 400 ? "..." : ""}\n\n---\n\n`;
|
|
957
|
+
});
|
|
958
|
+
const estimatedTokensSaved = results.length * 150;
|
|
959
|
+
reportSavings({
|
|
960
|
+
source: "cache",
|
|
961
|
+
tokens: estimatedTokensSaved,
|
|
962
|
+
toolName: "get_telepathic_hints",
|
|
963
|
+
filePath: activeFile,
|
|
964
|
+
});
|
|
965
|
+
return { content: [{ type: "text", text: hintsText }] };
|
|
966
|
+
}
|
|
967
|
+
catch (e) {
|
|
968
|
+
logError("get_telepathic_hints", e);
|
|
969
|
+
return {
|
|
970
|
+
content: [
|
|
971
|
+
{
|
|
972
|
+
type: "text",
|
|
973
|
+
text: `Telepathic Hints failed: ${e.message}. Is Lemma Proxy running?`,
|
|
974
|
+
},
|
|
975
|
+
],
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
//# sourceMappingURL=tools.js.map
|