@nxuss/lemma 0.7.0 → 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.
Files changed (61) hide show
  1. package/dashboard/dist/assets/index-BoZujIjB.css +1 -0
  2. package/dashboard/dist/assets/{index-BJzMIGrT.js → index-C3X0fqmd.js} +113 -113
  3. package/dashboard/dist/assets/index-C3X0fqmd.js.map +1 -0
  4. package/dashboard/dist/index.html +2 -2
  5. package/dist/cjs/api/dashboardRoutes.d.ts.map +1 -1
  6. package/dist/cjs/api/dashboardRoutes.js +41 -1
  7. package/dist/cjs/api/dashboardRoutes.js.map +1 -1
  8. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  9. package/dist/cjs/cli/lemma-proxy.js +80 -11
  10. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  11. package/dist/cjs/mcp/index.js +64 -1107
  12. package/dist/cjs/mcp/index.js.map +1 -1
  13. package/dist/cjs/mcp/prompts.d.ts +4 -0
  14. package/dist/cjs/mcp/prompts.d.ts.map +1 -0
  15. package/dist/cjs/mcp/prompts.js +84 -0
  16. package/dist/cjs/mcp/prompts.js.map +1 -0
  17. package/dist/cjs/mcp/resources.d.ts +3 -0
  18. package/dist/cjs/mcp/resources.d.ts.map +1 -0
  19. package/dist/cjs/mcp/resources.js +179 -0
  20. package/dist/cjs/mcp/resources.js.map +1 -0
  21. package/dist/cjs/mcp/tools.d.ts +11 -0
  22. package/dist/cjs/mcp/tools.d.ts.map +1 -0
  23. package/dist/cjs/mcp/tools.js +1018 -0
  24. package/dist/cjs/mcp/tools.js.map +1 -0
  25. package/dist/cjs/mcp/utils.d.ts +11 -0
  26. package/dist/cjs/mcp/utils.d.ts.map +1 -0
  27. package/dist/cjs/mcp/utils.js +85 -0
  28. package/dist/cjs/mcp/utils.js.map +1 -0
  29. package/dist/cjs/utils/reportSavings.d.ts +6 -0
  30. package/dist/cjs/utils/reportSavings.d.ts.map +1 -1
  31. package/dist/cjs/utils/reportSavings.js.map +1 -1
  32. package/dist/esm/api/dashboardRoutes.d.ts.map +1 -1
  33. package/dist/esm/api/dashboardRoutes.js +41 -1
  34. package/dist/esm/api/dashboardRoutes.js.map +1 -1
  35. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  36. package/dist/esm/cli/lemma-proxy.js +80 -11
  37. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  38. package/dist/esm/mcp/index.js +63 -1106
  39. package/dist/esm/mcp/index.js.map +1 -1
  40. package/dist/esm/mcp/prompts.d.ts +4 -0
  41. package/dist/esm/mcp/prompts.d.ts.map +1 -0
  42. package/dist/esm/mcp/prompts.js +80 -0
  43. package/dist/esm/mcp/prompts.js.map +1 -0
  44. package/dist/esm/mcp/resources.d.ts +3 -0
  45. package/dist/esm/mcp/resources.d.ts.map +1 -0
  46. package/dist/esm/mcp/resources.js +173 -0
  47. package/dist/esm/mcp/resources.js.map +1 -0
  48. package/dist/esm/mcp/tools.d.ts +11 -0
  49. package/dist/esm/mcp/tools.d.ts.map +1 -0
  50. package/dist/esm/mcp/tools.js +979 -0
  51. package/dist/esm/mcp/tools.js.map +1 -0
  52. package/dist/esm/mcp/utils.d.ts +11 -0
  53. package/dist/esm/mcp/utils.d.ts.map +1 -0
  54. package/dist/esm/mcp/utils.js +73 -0
  55. package/dist/esm/mcp/utils.js.map +1 -0
  56. package/dist/esm/utils/reportSavings.d.ts +6 -0
  57. package/dist/esm/utils/reportSavings.d.ts.map +1 -1
  58. package/dist/esm/utils/reportSavings.js.map +1 -1
  59. package/package.json +1 -1
  60. package/dashboard/dist/assets/index-BJzMIGrT.js.map +0 -1
  61. package/dashboard/dist/assets/index-DUOrThix.css +0 -1
@@ -1,30 +1,42 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
3
  import fs from "fs";
5
4
  import path from "path";
6
- import { reportSavings } from "../utils/reportSavings";
7
- /**
8
- * Lemma MCP Server
9
- * Exposes Lemma's intelligence layer as Tools and Resources for LLMs.
10
- */
5
+ import axios from "axios";
6
+ import { spawn } from "child_process";
7
+ import { setupPromptsHandlers } from "./prompts";
8
+ import { setupResourcesHandlers } from "./resources";
9
+ import { setupToolsHandlers } from "./tools";
10
+ import { getProxyPort, sanitizeArgsForLogging, logWarn } from "./utils";
11
+ const SERVER_VERSION = "0.7.2";
11
12
  class LemmaMcpServer {
12
13
  constructor() {
13
14
  this.server = new Server({
14
15
  name: "lemma-mcp-server",
15
- version: "0.1.0",
16
+ version: SERVER_VERSION,
16
17
  }, {
17
18
  capabilities: {
18
- resources: {
19
- subscribe: true
20
- },
19
+ resources: { subscribe: true },
21
20
  tools: {},
21
+ prompts: {},
22
22
  },
23
23
  });
24
24
  this.setupHandlers();
25
25
  this.setupErrorHandling();
26
26
  this.setupLiveContextWatcher();
27
27
  }
28
+ setupHandlers() {
29
+ setupPromptsHandlers(this.server);
30
+ setupResourcesHandlers(this.server);
31
+ setupToolsHandlers(this.server, (event) => this.reportMcpEvent(event));
32
+ }
33
+ async run() {
34
+ await this.ensureProxyRunning();
35
+ const transport = new StdioServerTransport();
36
+ await this.server.connect(transport);
37
+ console.error("Lemma MCP Server running on stdio");
38
+ }
39
+ // ── Live Context Watcher ──────────────────────────────────────────
28
40
  setupLiveContextWatcher() {
29
41
  try {
30
42
  const lemmaDir = path.join(process.cwd(), ".lemma");
@@ -41,1106 +53,25 @@ class LemmaMcpServer {
41
53
  // @ts-ignore
42
54
  this.server.sendResourceUpdated({ uri: "lemma://runtime/context" });
43
55
  }
44
- catch { }
45
- }
46
- });
47
- }
48
- catch { }
49
- }
50
- listDirRecursive(currentDir, relativePath, depth, maxDepth) {
51
- if (depth > maxDepth)
52
- return [];
53
- try {
54
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
55
- let result = [];
56
- for (const entry of entries) {
57
- if (["node_modules", ".git", "dist", "chroma_data"].includes(entry.name))
58
- continue;
59
- const rel = relativePath ? path.join(relativePath, entry.name) : entry.name;
60
- const full = path.join(currentDir, entry.name);
61
- if (entry.isDirectory()) {
62
- result.push(`${rel}/`);
63
- result.push(...this.listDirRecursive(full, rel, depth + 1, maxDepth));
64
- }
65
- else {
66
- result.push(rel);
67
- }
68
- }
69
- return result;
70
- }
71
- catch {
72
- return [];
73
- }
74
- }
75
- searchDirRecursive(currentDir, relativePath, query, extFilter) {
76
- try {
77
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
78
- let result = [];
79
- for (const entry of entries) {
80
- if (["node_modules", ".git", "dist", "chroma_data"].includes(entry.name))
81
- continue;
82
- const rel = relativePath ? path.join(relativePath, entry.name) : entry.name;
83
- const full = path.join(currentDir, entry.name);
84
- if (entry.isDirectory()) {
85
- result.push(...this.searchDirRecursive(full, rel, query, extFilter));
86
- }
87
- else {
88
- if (extFilter && !entry.name.endsWith(`.${extFilter}`))
89
- continue;
90
- try {
91
- const content = fs.readFileSync(full, "utf8");
92
- if (content.toLowerCase().includes(query.toLowerCase())) {
93
- const lines = content.split("\n");
94
- lines.forEach((lineText, idx) => {
95
- if (lineText.toLowerCase().includes(query.toLowerCase())) {
96
- result.push({
97
- filePath: rel,
98
- line: idx + 1,
99
- text: lineText.trim()
100
- });
101
- }
102
- });
103
- }
56
+ catch (err) {
57
+ logWarn("live-context", "Failed to send resource update");
104
58
  }
105
- catch { }
106
59
  }
107
- }
108
- return result;
60
+ });
109
61
  }
110
- catch {
111
- return [];
62
+ catch (err) {
63
+ logWarn("live-context", "Could not set up live context watcher");
112
64
  }
113
65
  }
114
- setupHandlers() {
115
- // 1. List Resources
116
- this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
117
- return {
118
- resources: [
119
- {
120
- uri: "lemma://runtime/context",
121
- name: "Lemma Live Context",
122
- description: "Real-time application errors, stack traces, and runtime state captured by Lemma.",
123
- mimeType: "text/markdown",
124
- },
125
- {
126
- uri: "lemma://stats/usage",
127
- name: "Lemma Intelligence Stats",
128
- description: "Current project stats, token savings, and cost optimization report.",
129
- mimeType: "application/json",
130
- },
131
- {
132
- uri: "lemma://project/onboarding",
133
- name: "Lemma Project Onboarding Mental Model",
134
- description: "Dynamic codebase architecture, technical stack, folder structure, and core design principles compiled in one shot.",
135
- mimeType: "text/markdown",
136
- },
137
- {
138
- uri: "lemma://multiverse/timeline",
139
- name: "Lemma Codebase Multiverse AST Timeline",
140
- description: "Live, chronological time-travel database of the last 10 micro-snapshots of AST changes, code diffs, and execution results.",
141
- mimeType: "text/markdown",
142
- },
143
- ],
144
- };
145
- });
146
- // 2. Read Resource
147
- this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
148
- const { uri } = request.params;
149
- if (uri === "lemma://runtime/context") {
150
- const filePath = path.join(process.cwd(), ".lemma/live-context.md");
151
- try {
152
- const content = fs.readFileSync(filePath, "utf8");
153
- return {
154
- contents: [
155
- {
156
- uri,
157
- mimeType: "text/markdown",
158
- text: content,
159
- },
160
- ],
161
- };
162
- }
163
- catch (error) {
164
- return {
165
- contents: [
166
- {
167
- uri,
168
- mimeType: "text/markdown",
169
- text: "# No Live Context\nNo recent crashes or state changes recorded by Lemma.",
170
- },
171
- ],
172
- };
173
- }
174
- }
175
- if (uri === "lemma://stats/usage") {
176
- const statsFile = path.join(process.env.HOME || "~", ".lemma-cache/stats.json");
177
- try {
178
- const stats = JSON.parse(fs.readFileSync(statsFile, "utf8"));
179
- return {
180
- contents: [
181
- {
182
- uri,
183
- mimeType: "application/json",
184
- text: JSON.stringify(stats, null, 2),
185
- },
186
- ],
187
- };
188
- }
189
- catch (error) {
190
- return {
191
- contents: [
192
- {
193
- uri,
194
- mimeType: "application/json",
195
- text: JSON.stringify({ error: "Stats not found. Is Lemma running?" }),
196
- },
197
- ],
198
- };
199
- }
200
- }
201
- if (uri === "lemma://project/onboarding") {
202
- try {
203
- const HOME = process.env.HOME || process.env.USERPROFILE || "~";
204
- const portFile = path.join(HOME, ".lemma-cache/proxy.port");
205
- let port = "8081";
206
- if (fs.existsSync(portFile)) {
207
- port = fs.readFileSync(portFile, "utf8").trim();
208
- }
209
- const axios = require("axios");
210
- const response = await axios.get(`http://localhost:${port}/api/project/onboarding`);
211
- return {
212
- contents: [
213
- {
214
- uri,
215
- mimeType: "text/markdown",
216
- text: response.data.markdown || "# Onboarding mental model could not be generated."
217
- }
218
- ]
219
- };
220
- }
221
- catch (error) {
222
- return {
223
- contents: [
224
- {
225
- uri,
226
- mimeType: "text/markdown",
227
- text: `# Onboarding Error\nCould not fetch dynamic project onboarding: ${error.message}. Is Lemma proxy running?`
228
- }
229
- ]
230
- };
231
- }
232
- }
233
- if (uri === "lemma://multiverse/timeline") {
234
- try {
235
- const HOME = process.env.HOME || process.env.USERPROFILE || "~";
236
- const portFile = path.join(HOME, ".lemma-cache/proxy.port");
237
- let port = "8081";
238
- if (fs.existsSync(portFile)) {
239
- port = fs.readFileSync(portFile, "utf8").trim();
240
- }
241
- const axios = require("axios");
242
- const response = await axios.get(`http://localhost:${port}/api/project/timeline`);
243
- const timeline = response.data.timeline || [];
244
- let markdown = `# 🌌 Codebase Multiverse Timeline: Recent AST Snapshots\n\n`;
245
- markdown += `This timeline tracks recent modifications and compiler states to reconstruct your workspace's historical state.\n\n`;
246
- if (timeline.length === 0) {
247
- markdown += `*(No AST snapshots captured yet. Make a few edits to code files in your workspace to trigger micro-snapshots!)*\n`;
248
- }
249
- else {
250
- // Display snapshots in reverse chronological order (newest first)
251
- const reversed = [...timeline].reverse();
252
- reversed.forEach((snap, idx) => {
253
- const timeStr = new Date(snap.timestamp).toLocaleTimeString();
254
- const agoStr = Math.max(0, Math.floor((Date.now() - snap.timestamp) / 1000 / 60));
255
- const ageText = agoStr === 0 ? 'just now' : `${agoStr} minute(s) ago`;
256
- const statusSymbol = snap.status === 'failure' ? '🔴' : (snap.status === 'success' ? '🟢' : '🔵');
257
- const statusText = snap.status.toUpperCase();
258
- markdown += `### ${idx + 1}. 🕒 Snapshot \`${snap.id}\` - ${ageText} (${timeStr})\n`;
259
- markdown += `- **Compiler/Execution Status:** ${statusSymbol} ${statusText}\n`;
260
- if (snap.errorLog) {
261
- markdown += `- **Active Exception / Crash Log:**\n\`\`\`text\n${snap.errorLog.substring(0, 400)}...\n\`\`\`\n`;
262
- }
263
- markdown += `- **Modified Files:**\n`;
264
- snap.changedFiles.forEach((file) => {
265
- markdown += ` - \`${file.filePath}\`\n`;
266
- if (file.diff) {
267
- markdown += ` - **Incremental AST Diff:**\n \`\`\`diff\n${file.diff}\n \`\`\`\n`;
268
- }
269
- });
270
- markdown += `\n---\n\n`;
271
- });
272
- }
273
- return {
274
- contents: [
275
- {
276
- uri,
277
- mimeType: "text/markdown",
278
- text: markdown
279
- }
280
- ]
281
- };
282
- }
283
- catch (error) {
284
- return {
285
- contents: [
286
- {
287
- uri,
288
- mimeType: "text/markdown",
289
- text: `# Multiverse Error\nCould not fetch codebase timeline: ${error.message}. Is Lemma proxy running?`
290
- }
291
- ]
292
- };
293
- }
294
- }
295
- throw new Error(`Resource not found: ${uri}`);
296
- });
297
- // 3. List Tools
298
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
299
- return {
300
- tools: [
301
- {
302
- name: "scrub_privacy",
303
- description: "Mask sensitive data (PII, API Keys, Credentials) from a text block using Lemma's Privacy Firewall.",
304
- inputSchema: {
305
- type: "object",
306
- properties: {
307
- text: { type: "string", description: "The raw text to scrub" },
308
- },
309
- required: ["text"],
310
- },
311
- },
312
- {
313
- name: "search_memory",
314
- description: "Search Lemma's semantic memory (The Brain) to find code snippets, solutions, and context from your past AI conversations.",
315
- inputSchema: {
316
- type: "object",
317
- properties: {
318
- query: { type: "string", description: "The natural language query (e.g., 'How did I handle Stripe webhooks?')" },
319
- limit: { type: "number", description: "Maximum results to return", default: 5 },
320
- },
321
- required: ["query"],
322
- },
323
- },
324
- {
325
- name: "store_memory",
326
- description: "Explicitly record/store a key technical decision, architecture map, code snippet, or fact into Lemma's semantic memory database (The Brain). This makes the knowledge permanently available for future AI sessions.",
327
- inputSchema: {
328
- type: "object",
329
- properties: {
330
- query: { type: "string", description: "The natural language query key (e.g., 'Sazonia Architecture Map' or 'How Supabase auth works')" },
331
- response: { type: "string", description: "The complete technical content, analysis, or code to memorize" },
332
- provider: { type: "string", description: "Optional model provider name (defaults to 'generic')", default: "generic" },
333
- },
334
- required: ["query", "response"],
335
- },
336
- },
337
- {
338
- name: "get_routing_advice",
339
- description: "Analyzes a prompt and suggests the best model based on Lemma's Complexity Router.",
340
- inputSchema: {
341
- type: "object",
342
- properties: {
343
- prompt: { type: "string", description: "The prompt to analyze" },
344
- intended_model: { type: "string", description: "The model you were planning to use (e.g., gpt-4o)" },
345
- },
346
- required: ["prompt"],
347
- },
348
- },
349
- {
350
- name: "auto_heal",
351
- description: "Diagnose and auto-heal the latest local server crash registered in Lemma's context logs. Can automatically apply the fix.",
352
- inputSchema: {
353
- type: "object",
354
- properties: {
355
- apply: { type: "boolean", description: "Whether to apply the generated code fix directly to the file", default: false },
356
- },
357
- },
358
- },
359
- {
360
- name: "read_workspace_file",
361
- description: "Read the contents of a file inside the local workspace project. Automatically compresses comments and excessive whitespaces to save up to 80% tokens on your local model's context window.",
362
- inputSchema: {
363
- type: "object",
364
- properties: {
365
- filePath: { type: "string", description: "The path to the file relative to the project root" },
366
- compact: { type: "boolean", description: "Whether to compress comments and whitespace to save local/cloud LLM tokens", default: true }
367
- },
368
- required: ["filePath"],
369
- },
370
- },
371
- {
372
- name: "write_workspace_file",
373
- description: "Write the full contents to a file inside the local workspace project. Creates any necessary parent directories automatically.",
374
- inputSchema: {
375
- type: "object",
376
- properties: {
377
- filePath: { type: "string", description: "The path to the file relative to the project root" },
378
- content: { type: "string", description: "The complete content to write to the file" }
379
- },
380
- required: ["filePath", "content"],
381
- },
382
- },
383
- {
384
- name: "apply_workspace_patch",
385
- description: "Apply a smart search-and-replace patch to an existing file inside the workspace. The search block must match exactly (including indentation and newlines). Safe against duplicate matches.",
386
- inputSchema: {
387
- type: "object",
388
- properties: {
389
- filePath: { type: "string", description: "The path to the file relative to the project root" },
390
- searchContent: { type: "string", description: "The exact search block to find in the file" },
391
- replaceContent: { type: "string", description: "The replacement block to replace the search block with" }
392
- },
393
- required: ["filePath", "searchContent", "replaceContent"],
394
- },
395
- },
396
- {
397
- name: "run_workspace_command",
398
- description: "Execute a bash command (e.g., tests, linters, compilers, git diffs) in the workspace root. Runs synchronously with a maximum execution timeout of 15 seconds. Returns exit code, stdout, and stderr.",
399
- inputSchema: {
400
- type: "object",
401
- properties: {
402
- command: { type: "string", description: "The bash command to run in the workspace root" }
403
- },
404
- required: ["command"],
405
- },
406
- },
407
- {
408
- name: "list_workspace_dir",
409
- description: "List all files and subdirectories recursively in the project to help navigate the repository structure.",
410
- inputSchema: {
411
- type: "object",
412
- properties: {
413
- dirPath: { type: "string", description: "The directory path relative to the project root (empty string for root)", default: "" },
414
- maxDepth: { type: "number", description: "Maximum recursion depth", default: 3 }
415
- },
416
- required: [],
417
- },
418
- },
419
- {
420
- name: "search_workspace",
421
- description: "Perform a fast local text search (grep) across all files in the project workspace.",
422
- inputSchema: {
423
- type: "object",
424
- properties: {
425
- query: { type: "string", description: "The keyword or text pattern to search for" },
426
- extension: { type: "string", description: "Optional file extension filter (e.g., 'ts', 'json')" }
427
- },
428
- required: ["query"],
429
- },
430
- },
431
- {
432
- name: "squeeze_prompt",
433
- description: "Compress code blocks, comments, and boilerplate in any AI prompt or message using Lemma's context optimizer. Saves up to 80% tokens on your context window.",
434
- inputSchema: {
435
- type: "object",
436
- properties: {
437
- prompt: { type: "string", description: "The raw prompt containing code blocks to squeeze" },
438
- query: { type: "string", description: "Optional user's primary query to preserve relevant code parts", default: "" }
439
- },
440
- required: ["prompt"],
441
- },
442
- },
443
- {
444
- name: "get_project_onboarding",
445
- description: "Downloads a dynamic, highly condensed architectural and stack overview (Mental Model) of the current project codebase in markdown, helping you understand how it is structured in a single shot without reading hundreds of files.",
446
- inputSchema: {
447
- type: "object",
448
- properties: {},
449
- },
450
- },
451
- {
452
- name: "get_ast_hologram",
453
- description: "Generate a dense, token-efficient Holographic AST Map of the workspace: a structured JSON index of all exported symbols, classes, functions, interfaces, and their file locations. Use this INSTEAD of reading individual files when you need to understand codebase structure. Saves up to 90% tokens compared to reading files one by one.",
454
- inputSchema: {
455
- type: "object",
456
- properties: {
457
- dirPath: { type: "string", description: "Directory to scan relative to workspace root (empty for root)", default: "" },
458
- extensions: { type: "array", items: { type: "string" }, description: "File extensions to scan (default: ['ts', 'tsx', 'js', 'jsx'])", default: ["ts", "tsx", "js", "jsx"] }
459
- },
460
- required: [],
461
- },
462
- },
463
- {
464
- name: "validate_patch_sandbox",
465
- description: "AST Multiverse Auto-Debugger: Validate a proposed code patch in an isolated sandbox BEFORE applying it to the workspace. Runs TypeScript compiler checks and a quick syntax validation pass. Returns success/failure with detailed diagnostics so you never break the codebase.",
466
- inputSchema: {
467
- type: "object",
468
- properties: {
469
- filePath: { type: "string", description: "Target file path relative to workspace root" },
470
- patchedContent: { type: "string", description: "The complete proposed file content after the patch is applied" }
471
- },
472
- required: ["filePath", "patchedContent"],
473
- },
474
- },
475
- {
476
- name: "query_hybrid_consensus",
477
- description: "Hybrid Consensus Engine: First searches Lemma's semantic Brain (The Brain) for a cached high-similarity answer. If a strong match (>85%) is found locally, returns it instantly WITHOUT any cloud LLM call — saving tokens and latency. If no strong match exists, escalates to cloud and auto-stores the result for future hits. Always call this BEFORE asking an external LLM.",
478
- inputSchema: {
479
- type: "object",
480
- properties: {
481
- query: { type: "string", description: "The technical question or task description" },
482
- context: { type: "string", description: "Optional extra context (active file path, error message, etc.)" },
483
- threshold: { type: "number", description: "Similarity threshold to consider a Brain hit sufficient (0.0-1.0, default: 0.80)", default: 0.80 }
484
- },
485
- required: ["query"],
486
- },
487
- },
488
- {
489
- name: "get_telepathic_hints",
490
- description: "Proactive Telepathy: Given the path of the file the user is currently editing, automatically surfaces the top N most relevant memories, past solutions, and architectural patterns from Lemma's Brain — without requiring an explicit query. Call this when starting work on a file to front-load all relevant context before writing any code.",
491
- inputSchema: {
492
- type: "object",
493
- properties: {
494
- activeFile: { type: "string", description: "Path of the file currently being edited (relative or absolute)" },
495
- limit: { type: "number", description: "Maximum number of hints to return (default: 5)", default: 5 }
496
- },
497
- required: ["activeFile"],
498
- },
499
- },
500
- ],
501
- };
502
- });
503
- // 4. Call Tool
504
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
505
- const { name, arguments: args } = request.params;
506
- switch (name) {
507
- case "scrub_privacy": {
508
- const text = args?.text;
509
- if (!text)
510
- throw new Error("Text is required");
511
- // @ts-ignore
512
- const { SemanticScrubber } = require("../security/SemanticScrubber");
513
- const scrubber = new SemanticScrubber();
514
- const { maskedPrompt } = scrubber.mask(text);
515
- return {
516
- content: [{ type: "text", text: maskedPrompt }],
517
- };
518
- }
519
- case "search_memory": {
520
- const query = args?.query;
521
- const limit = args?.limit || 5;
522
- if (!query)
523
- throw new Error("Query is required");
524
- try {
525
- const HOME = process.env.HOME || process.env.USERPROFILE || "~";
526
- const portFile = path.join(HOME, ".lemma-cache/proxy.port");
527
- let port = "8081";
528
- if (fs.existsSync(portFile)) {
529
- port = fs.readFileSync(portFile, "utf8").trim();
530
- }
531
- const axios = require("axios");
532
- const response = await axios.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(query)}&limit=${limit}`);
533
- const results = response.data.results || [];
534
- if (results.length === 0) {
535
- return { content: [{ type: "text", text: "No relevant memories found in Lemma's Brain." }] };
536
- }
537
- const formatted = results.map((r, i) => {
538
- 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)}`;
539
- }).join("\n\n---\n\n");
540
- return {
541
- content: [{ type: "text", text: `Lemma found ${results.length} memories:\n\n${formatted}` }],
542
- };
543
- }
544
- catch (e) {
545
- return {
546
- content: [{ type: "text", text: `Neural Search failed: ${e.message}. Is Lemma Proxy running?` }],
547
- };
548
- }
549
- }
550
- case "store_memory": {
551
- const query = args?.query;
552
- const responseText = args?.response;
553
- const provider = args?.provider || 'generic';
554
- if (!query || !responseText)
555
- throw new Error("Query and response are required");
556
- try {
557
- const HOME = process.env.HOME || process.env.USERPROFILE || "~";
558
- const portFile = path.join(HOME, ".lemma-cache/proxy.port");
559
- let port = "8081";
560
- if (fs.existsSync(portFile)) {
561
- port = fs.readFileSync(portFile, "utf8").trim();
562
- }
563
- const axios = require("axios");
564
- const response = await axios.post(`http://localhost:${port}/api/memory/store`, {
565
- query,
566
- response: responseText,
567
- provider
568
- });
569
- // Report the savings to the proxy ledger so it updates the dashboard instantly!
570
- const tokensSaved = Math.max(100, Math.floor(responseText.length / 4));
571
- reportSavings({ source: 'cache', tokens: tokensSaved });
572
- return {
573
- content: [{ type: "text", text: `Success: ${response.data.message}` }],
574
- };
575
- }
576
- catch (e) {
577
- return {
578
- content: [{ type: "text", text: `Failed to store memory in Lemma: ${e.message}. Is Lemma Proxy running?` }],
579
- };
580
- }
581
- }
582
- case "get_routing_advice": {
583
- const prompt = args?.prompt;
584
- const intendedModel = args?.intended_model;
585
- // @ts-ignore
586
- const { ComplexityRouter } = require("../proxy/ComplexityRouter");
587
- const router = new ComplexityRouter();
588
- const decision = router.evaluate(prompt, intendedModel);
589
- // Report routing savings if model was downgraded
590
- if (decision.complexity === 'low' && intendedModel && decision.model !== intendedModel) {
591
- const estimatedPromptTokens = Math.floor((prompt?.length || 0) / 4);
592
- reportSavings({ source: 'complexityRouting', tokens: Math.floor(estimatedPromptTokens * 0.8) });
593
- }
594
- return {
595
- content: [{
596
- type: "text",
597
- text: `Lemma Routing Advice: Use ${decision.model}. Reason: Complexity is ${decision.complexity}.`
598
- }],
599
- };
600
- }
601
- case "auto_heal": {
602
- const apply = !!args?.apply;
603
- try {
604
- // @ts-ignore
605
- const { performAutoHeal } = require("../cli/lemma-proxy");
606
- const res = await performAutoHeal(apply);
607
- return {
608
- content: [{
609
- type: "text",
610
- text: JSON.stringify(res, null, 2)
611
- }],
612
- };
613
- }
614
- catch (e) {
615
- return {
616
- content: [{ type: "text", text: `Auto-heal failed: ${e.message}` }],
617
- };
618
- }
619
- }
620
- case "read_workspace_file": {
621
- const filePath = args?.filePath;
622
- const compact = args?.compact !== false;
623
- if (!filePath)
624
- throw new Error("filePath is required");
625
- const workspaceRoot = process.cwd();
626
- const resolvedPath = path.resolve(workspaceRoot, filePath);
627
- if (!resolvedPath.startsWith(workspaceRoot)) {
628
- throw new Error("Access denied: path is outside of workspace root");
629
- }
630
- try {
631
- let content = fs.readFileSync(resolvedPath, "utf8");
632
- const originalSize = content.length;
633
- if (compact) {
634
- const { squeezeCode } = require("../utils/ContextSqueezer");
635
- content = squeezeCode(content);
636
- // Report context squeeze savings to the proxy ledger
637
- if (content.length < originalSize) {
638
- reportSavings({ source: 'contextSqueeze', charsBefore: originalSize, charsAfter: content.length });
639
- }
640
- }
641
- // Run SemanticScrubber to automatically mask any credentials/PII/API keys
642
- try {
643
- const { SemanticScrubber } = require("../security/SemanticScrubber");
644
- const scrubber = new SemanticScrubber();
645
- const { maskedPrompt } = scrubber.mask(content);
646
- content = maskedPrompt;
647
- }
648
- catch { }
649
- return {
650
- content: [{ type: "text", text: content }],
651
- };
652
- }
653
- catch (err) {
654
- return {
655
- content: [{ type: "text", text: `Error reading file: ${err.message}` }],
656
- };
657
- }
658
- }
659
- case "write_workspace_file": {
660
- const filePath = args?.filePath;
661
- const content = args?.content;
662
- if (!filePath || content === undefined) {
663
- throw new Error("filePath and content are required");
664
- }
665
- const workspaceRoot = process.cwd();
666
- const resolvedPath = path.resolve(workspaceRoot, filePath);
667
- if (!resolvedPath.startsWith(workspaceRoot)) {
668
- throw new Error("Access denied: path is outside of workspace root");
669
- }
670
- try {
671
- const dir = path.dirname(resolvedPath);
672
- if (!fs.existsSync(dir)) {
673
- fs.mkdirSync(dir, { recursive: true });
674
- }
675
- fs.writeFileSync(resolvedPath, content, "utf8");
676
- return {
677
- content: [{ type: "text", text: `Success: Written file to ${filePath}` }],
678
- };
679
- }
680
- catch (err) {
681
- return {
682
- content: [{ type: "text", text: `Error writing file: ${err.message}` }],
683
- };
684
- }
685
- }
686
- case "apply_workspace_patch": {
687
- const filePath = args?.filePath;
688
- const searchContent = args?.searchContent;
689
- const replaceContent = args?.replaceContent;
690
- if (!filePath || !searchContent || replaceContent === undefined) {
691
- throw new Error("filePath, searchContent, and replaceContent are required");
692
- }
693
- const workspaceRoot = process.cwd();
694
- const resolvedPath = path.resolve(workspaceRoot, filePath);
695
- if (!resolvedPath.startsWith(workspaceRoot)) {
696
- throw new Error("Access denied: path is outside of workspace root");
697
- }
698
- try {
699
- if (!fs.existsSync(resolvedPath)) {
700
- return {
701
- content: [{ type: "text", text: `Error: File ${filePath} does not exist.` }],
702
- };
703
- }
704
- const originalContent = fs.readFileSync(resolvedPath, "utf8");
705
- const index = originalContent.indexOf(searchContent);
706
- if (index === -1) {
707
- return {
708
- content: [{ type: "text", text: `Error: Target search content not found in file. Make sure the search content matches exactly, including indentation, tabs, spaces, and newlines.` }],
709
- };
710
- }
711
- if (originalContent.indexOf(searchContent, index + 1) !== -1) {
712
- return {
713
- content: [{ type: "text", text: `Error: Multiple matches found for the search content. Please provide a larger search block with unique surrounding context lines to make it unambiguous.` }],
714
- };
715
- }
716
- const updatedContent = originalContent.replace(searchContent, replaceContent);
717
- fs.writeFileSync(resolvedPath, updatedContent, "utf8");
718
- return {
719
- content: [{ type: "text", text: `Success: Patch successfully applied to ${filePath}` }],
720
- };
721
- }
722
- catch (err) {
723
- return {
724
- content: [{ type: "text", text: `Error applying patch: ${err.message}` }],
725
- };
726
- }
727
- }
728
- case "run_workspace_command": {
729
- const command = args?.command;
730
- if (!command)
731
- throw new Error("command is required");
732
- const { execSync } = require("child_process");
733
- const workspaceRoot = process.cwd();
734
- try {
735
- const output = execSync(command, {
736
- cwd: workspaceRoot,
737
- encoding: "utf8",
738
- timeout: 15000,
739
- env: { ...process.env }
740
- });
741
- return {
742
- content: [{ type: "text", text: `Command completed successfully.\n\nOutput:\n${output}` }],
743
- };
744
- }
745
- catch (err) {
746
- return {
747
- content: [{
748
- type: "text",
749
- text: `Command failed with exit code ${err.status || 'unknown'}.\n\nStdout:\n${err.stdout || ''}\n\nStderr:\n${err.stderr || err.message || ''}`
750
- }],
751
- };
752
- }
753
- }
754
- case "list_workspace_dir": {
755
- const dirPath = args?.dirPath || "";
756
- const maxDepth = args?.maxDepth || 3;
757
- const workspaceRoot = process.cwd();
758
- const resolvedPath = path.resolve(workspaceRoot, dirPath);
759
- if (!resolvedPath.startsWith(workspaceRoot)) {
760
- throw new Error("Access denied: path is outside of workspace root");
761
- }
762
- try {
763
- const list = this.listDirRecursive(resolvedPath, dirPath, 1, maxDepth);
764
- return {
765
- content: [{
766
- type: "text",
767
- text: list.length > 0 ? list.join("\n") : "(Directory is empty or inaccessible)"
768
- }],
769
- };
770
- }
771
- catch (err) {
772
- return {
773
- content: [{ type: "text", text: `Error listing directory: ${err.message}` }],
774
- };
775
- }
776
- }
777
- case "search_workspace": {
778
- const query = args?.query;
779
- const extension = args?.extension;
780
- if (!query)
781
- throw new Error("query is required");
782
- const workspaceRoot = process.cwd();
783
- try {
784
- const results = this.searchDirRecursive(workspaceRoot, "", query, extension);
785
- if (results.length === 0) {
786
- return {
787
- content: [{ type: "text", text: `No matches found in the workspace for "${query}".` }],
788
- };
789
- }
790
- let formatted = results.map(r => `[${r.filePath}:${r.line}] ${r.text}`).join("\n");
791
- // Run SemanticScrubber to automatically mask any credentials/PII/API keys in search results
792
- try {
793
- const { SemanticScrubber } = require("../security/SemanticScrubber");
794
- const scrubber = new SemanticScrubber();
795
- const { maskedPrompt } = scrubber.mask(formatted);
796
- formatted = maskedPrompt;
797
- }
798
- catch { }
799
- return {
800
- content: [{ type: "text", text: `Found ${results.length} matches:\n\n${formatted}` }],
801
- };
802
- }
803
- catch (err) {
804
- return {
805
- content: [{ type: "text", text: `Error searching workspace: ${err.message}` }],
806
- };
807
- }
808
- }
809
- case "squeeze_prompt": {
810
- const promptArg = args?.prompt;
811
- const queryArg = args?.query || "";
812
- if (!promptArg)
813
- throw new Error("prompt is required");
814
- try {
815
- const { squeezePrompt } = require("../utils/ContextSqueezer");
816
- const result = squeezePrompt(promptArg, queryArg);
817
- if (result.originalSize > result.squeezedSize) {
818
- reportSavings({ source: 'contextSqueeze', charsBefore: result.originalSize, charsAfter: result.squeezedSize });
819
- }
820
- return {
821
- content: [
822
- {
823
- type: "text",
824
- text: result.squeezed,
825
- },
826
- ],
827
- };
828
- }
829
- catch (err) {
830
- return {
831
- content: [{ type: "text", text: `Error squeezing prompt: ${err.message}` }],
832
- };
833
- }
834
- }
835
- case "get_project_onboarding": {
836
- try {
837
- const HOME = process.env.HOME || process.env.USERPROFILE || "~";
838
- const portFile = path.join(HOME, ".lemma-cache/proxy.port");
839
- let port = "8081";
840
- if (fs.existsSync(portFile)) {
841
- port = fs.readFileSync(portFile, "utf8").trim();
842
- }
843
- const axios = require("axios");
844
- const response = await axios.get(`http://localhost:${port}/api/project/onboarding`);
845
- return {
846
- content: [
847
- {
848
- type: "text",
849
- text: response.data.markdown || "# Onboarding mental model could not be generated."
850
- }
851
- ]
852
- };
853
- }
854
- catch (error) {
855
- return {
856
- content: [
857
- {
858
- type: "text",
859
- text: `Failed to fetch codebase onboarding mental model: ${error.message}. Is Lemma proxy running?`
860
- }
861
- ]
862
- };
863
- }
864
- }
865
- case "get_ast_hologram": {
866
- const dirPath = args?.dirPath || "";
867
- const extensions = args?.extensions || ["ts", "tsx", "js", "jsx"];
868
- const workspaceRoot = process.cwd();
869
- const resolvedDir = path.resolve(workspaceRoot, dirPath);
870
- if (!resolvedDir.startsWith(workspaceRoot)) {
871
- throw new Error("Access denied: path is outside of workspace root");
872
- }
873
- // Recursive AST symbol extractor (regex-based, no heavy deps)
874
- const extractSymbols = (filePath, relPath) => {
875
- try {
876
- const src = fs.readFileSync(filePath, "utf8");
877
- const symbols = [];
878
- const lines = src.split("\n");
879
- lines.forEach((line, idx) => {
880
- // Export declarations: functions, classes, interfaces, types, consts
881
- const m = line.match(/^export\s+(?:default\s+)?(?:async\s+)?(class|function|interface|type|const|enum|abstract\s+class)\s+(\w+)/);
882
- if (m) {
883
- symbols.push({ kind: m[1].replace('abstract ', ''), name: m[2], file: relPath, line: idx + 1 });
884
- }
885
- // Named exports
886
- const exportMatch = line.match(/^export\s*\{([^}]+)\}/);
887
- if (exportMatch) {
888
- exportMatch[1].split(',').forEach(s => {
889
- const name = s.trim().split(/\s+as\s+/).pop()?.trim();
890
- if (name)
891
- symbols.push({ kind: 'export', name, file: relPath, line: idx + 1 });
892
- });
893
- }
894
- });
895
- return symbols;
896
- }
897
- catch {
898
- return [];
899
- }
900
- };
901
- const walkDir = (dir, rel) => {
902
- let allSymbols = [];
903
- try {
904
- const entries = fs.readdirSync(dir, { withFileTypes: true });
905
- for (const entry of entries) {
906
- if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(entry.name))
907
- continue;
908
- const fullPath = path.join(dir, entry.name);
909
- const relPath = rel ? path.join(rel, entry.name) : entry.name;
910
- if (entry.isDirectory()) {
911
- allSymbols = allSymbols.concat(walkDir(fullPath, relPath));
912
- }
913
- else {
914
- const ext = entry.name.split('.').pop() || '';
915
- if (extensions.includes(ext)) {
916
- allSymbols = allSymbols.concat(extractSymbols(fullPath, relPath));
917
- }
918
- }
919
- }
920
- }
921
- catch { }
922
- return allSymbols;
923
- };
924
- const symbols = walkDir(resolvedDir, dirPath);
925
- const byFile = {};
926
- symbols.forEach(s => {
927
- if (!byFile[s.file])
928
- byFile[s.file] = [];
929
- byFile[s.file].push({ kind: s.kind, name: s.name, line: s.line });
930
- });
931
- const hologram = {
932
- workspace: path.basename(workspaceRoot),
933
- scannedDir: dirPath || '.',
934
- totalSymbols: symbols.length,
935
- totalFiles: Object.keys(byFile).length,
936
- map: byFile,
937
- };
938
- return {
939
- content: [{ type: "text", text: JSON.stringify(hologram, null, 2) }],
940
- };
941
- }
942
- case "validate_patch_sandbox": {
943
- const filePath = args?.filePath;
944
- const patchedContent = args?.patchedContent;
945
- if (!filePath || patchedContent === undefined) {
946
- throw new Error("filePath and patchedContent are required");
947
- }
948
- const workspaceRoot = process.cwd();
949
- const resolvedPath = path.resolve(workspaceRoot, filePath);
950
- if (!resolvedPath.startsWith(workspaceRoot)) {
951
- throw new Error("Access denied: path is outside of workspace root");
952
- }
953
- const { execSync } = require('child_process');
954
- const os = require('os');
955
- const sandboxDir = path.join(os.tmpdir(), `lemma-sandbox-${Date.now()}`);
956
- const sandboxFile = path.join(sandboxDir, path.basename(filePath));
957
- try {
958
- fs.mkdirSync(sandboxDir, { recursive: true });
959
- fs.writeFileSync(sandboxFile, patchedContent, 'utf8');
960
- // Step 1: Basic syntax check via Node.js --check (works for JS/TS after strip)
961
- // Step 2: Try tsc --noEmit on the patched file using the workspace tsconfig
962
- const tsconfigPath = path.join(workspaceRoot, 'tsconfig.json');
963
- let tscResult = { success: true, output: 'No TypeScript config found — skipping tsc validation.' };
964
- if (fs.existsSync(tsconfigPath) && filePath.match(/\.tsx?$/)) {
965
- try {
966
- // Copy the patched file to a temp location respecting the workspace structure
967
- const sandboxWorkspace = path.join(sandboxDir, 'workspace');
968
- // Sync relevant source structure for valid imports check
969
- execSync(`cp -r ${workspaceRoot}/src ${sandboxWorkspace}/src 2>/dev/null || true`, { timeout: 5000 });
970
- // Overwrite the target file with patched content
971
- const sandboxTargetFile = path.join(sandboxWorkspace, filePath);
972
- fs.mkdirSync(path.dirname(sandboxTargetFile), { recursive: true });
973
- fs.writeFileSync(sandboxTargetFile, patchedContent, 'utf8');
974
- const tscOut = execSync(`cd ${workspaceRoot} && npx tsc --noEmit --skipLibCheck --allowJs --esModuleInterop --strict false 2>&1 | head -40 || true`, { timeout: 20000, encoding: 'utf8' });
975
- // Filter errors that reference our target file
976
- const relevantErrors = tscOut.split('\n').filter((l) => l.includes(path.basename(filePath)) || l.includes('error TS'));
977
- tscResult = {
978
- success: relevantErrors.length === 0,
979
- output: relevantErrors.length > 0 ? relevantErrors.join('\n') : '✅ TypeScript check passed with no errors in target file.'
980
- };
981
- }
982
- catch (e) {
983
- tscResult = { success: false, output: e.stdout || e.message };
984
- }
985
- }
986
- // Step 3: Quick regex-based syntax sanity (balanced braces/brackets)
987
- const opens = (patchedContent.match(/[{[(]/g) || []).length;
988
- const closes = (patchedContent.match(/[}\])]/g) || []).length;
989
- const balanced = Math.abs(opens - closes) <= 2; // allow small tolerance
990
- fs.rmSync(sandboxDir, { recursive: true, force: true });
991
- const verdict = tscResult.success && balanced;
992
- return {
993
- content: [{
994
- type: "text",
995
- text: JSON.stringify({
996
- verdict: verdict ? '✅ SAFE TO APPLY' : '❌ DO NOT APPLY — Issues Found',
997
- syntaxBalanced: balanced ? '✅' : '⚠️ Unbalanced brackets/braces detected',
998
- tscCheck: tscResult.output,
999
- recommendation: verdict
1000
- ? 'Patch looks valid. You can safely call apply_workspace_patch to apply it.'
1001
- : 'Fix the reported issues before applying the patch to avoid breaking the codebase.',
1002
- }, null, 2)
1003
- }]
1004
- };
1005
- }
1006
- catch (err) {
1007
- try {
1008
- fs.rmSync(sandboxDir, { recursive: true, force: true });
1009
- }
1010
- catch { }
1011
- return {
1012
- content: [{ type: "text", text: `Sandbox validation error: ${err.message}` }]
1013
- };
1014
- }
1015
- }
1016
- case "query_hybrid_consensus": {
1017
- const query = args?.query;
1018
- const context = args?.context || '';
1019
- const threshold = typeof args?.threshold === 'number' ? args.threshold : 0.80;
1020
- if (!query)
1021
- throw new Error('query is required');
1022
- const HOME = process.env.HOME || process.env.USERPROFILE || '~';
1023
- const portFile = path.join(HOME, '.lemma-cache/proxy.port');
1024
- let port = '8081';
1025
- if (fs.existsSync(portFile)) {
1026
- port = fs.readFileSync(portFile, 'utf8').trim();
1027
- }
1028
- const axios = require('axios');
1029
- const fullQuery = context ? `${query}\n\nContext: ${context}` : query;
1030
- try {
1031
- // Step 1: Hit The Brain first
1032
- const searchRes = await axios.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(fullQuery)}&limit=3`);
1033
- const results = searchRes.data.results || [];
1034
- const topHit = results[0];
1035
- if (topHit && topHit.similarity >= threshold) {
1036
- // 🎯 Brain HIT — return cached answer, zero cloud tokens spent
1037
- const savedTokens = Math.floor((topHit.response?.choices?.[0]?.message?.content?.length || 500) / 4);
1038
- reportSavings({ source: 'cache', tokens: savedTokens });
1039
- const responseText = typeof topHit.response === 'string'
1040
- ? topHit.response
1041
- : topHit.response?.choices?.[0]?.message?.content
1042
- || JSON.stringify(topHit.response, null, 2);
1043
- return {
1044
- content: [{
1045
- type: 'text',
1046
- 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}`
1047
- }]
1048
- };
1049
- }
1050
- // Step 2: Brain MISS — inform the IDE to proceed with cloud and store the result
1051
- const missMsg = results.length > 0
1052
- ? `🔍 **Brain Miss** — Best match was only ${(topHit.similarity * 100).toFixed(1)}% (below ${(threshold * 100).toFixed(0)}% threshold).`
1053
- : `🔍 **Brain Miss** — No relevant memories found for this query.`;
1054
- return {
1055
- content: [{
1056
- type: 'text',
1057
- 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)}"
1058
- - response: [your full answer]\n\nThis will cache it for future sessions and save tokens next time.`
1059
- }]
1060
- };
1061
- }
1062
- catch (e) {
1063
- return {
1064
- content: [{ type: 'text', text: `Hybrid Consensus failed: ${e.message}. Is Lemma Proxy running?` }]
1065
- };
1066
- }
1067
- }
1068
- case "get_telepathic_hints": {
1069
- const activeFile = args?.activeFile;
1070
- const limit = typeof args?.limit === 'number' ? args.limit : 5;
1071
- if (!activeFile)
1072
- throw new Error('activeFile is required');
1073
- const HOME = process.env.HOME || process.env.USERPROFILE || '~';
1074
- const portFile = path.join(HOME, '.lemma-cache/proxy.port');
1075
- let port = '8081';
1076
- if (fs.existsSync(portFile)) {
1077
- port = fs.readFileSync(portFile, 'utf8').trim();
1078
- }
1079
- const axios = require('axios');
1080
- // Build a rich query from the file path: extract module name, dir context, and file extension
1081
- const basename = path.basename(activeFile, path.extname(activeFile));
1082
- const dirContext = path.dirname(activeFile).split(path.sep).filter(Boolean).slice(-2).join(' ');
1083
- const ext = path.extname(activeFile).replace('.', '');
1084
- const telepathicQuery = `${basename} ${dirContext} ${ext} patterns solutions architecture`;
1085
- try {
1086
- const searchRes = await axios.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(telepathicQuery)}&limit=${limit}`);
1087
- const results = searchRes.data.results || [];
1088
- if (results.length === 0) {
1089
- return {
1090
- content: [{
1091
- type: 'text',
1092
- text: `📡 **Telepathic Hints** for \`${activeFile}\`\n\nNo relevant memories found yet. As you use Lemma and store solutions, they will appear here automatically.`
1093
- }]
1094
- };
1095
- }
1096
- let hintsText = `📡 **Telepathic Hints** for \`${activeFile}\`\n*(${results.length} relevant memories surfaced from The Brain)*\n\n`;
1097
- results.forEach((r, i) => {
1098
- const similarity = (r.similarity * 100).toFixed(1);
1099
- const prompt = typeof r.prompt === 'string' ? r.prompt.substring(0, 120) : 'Unknown';
1100
- const responseContent = typeof r.response === 'string'
1101
- ? r.response
1102
- : r.response?.choices?.[0]?.message?.content
1103
- || JSON.stringify(r.response).substring(0, 300);
1104
- hintsText += `### 💡 Hint ${i + 1} (${similarity}% match)\n`;
1105
- hintsText += `**Memory:** ${prompt}\n\n`;
1106
- hintsText += `${responseContent.substring(0, 400)}${responseContent.length > 400 ? '...' : ''}\n\n---\n\n`;
1107
- });
1108
- // Report token savings: surface context avoids re-asking the LLM
1109
- const estimatedTokensSaved = results.length * 150;
1110
- reportSavings({ source: 'cache', tokens: estimatedTokensSaved });
1111
- return {
1112
- content: [{ type: 'text', text: hintsText }]
1113
- };
1114
- }
1115
- catch (e) {
1116
- return {
1117
- content: [{ type: 'text', text: `Telepathic Hints failed: ${e.message}. Is Lemma Proxy running?` }]
1118
- };
1119
- }
1120
- }
1121
- default:
1122
- throw new Error(`Unknown tool: ${name}`);
1123
- }
1124
- });
1125
- }
1126
- setupErrorHandling() {
1127
- this.server.onerror = (error) => {
1128
- console.error("[MCP Error]", error);
1129
- };
1130
- process.on("SIGINT", async () => {
1131
- await this.server.close();
1132
- process.exit(0);
1133
- });
1134
- }
1135
- async ensureProxyAndClipboardRunning() {
66
+ // ── Proxy Auto-Start ──────────────────────────────────────────────
67
+ async ensureProxyRunning() {
1136
68
  try {
1137
- const { spawn } = require('child_process');
1138
69
  const cliCandidates = [
1139
70
  path.join(__dirname, "..", "cli", "lemma-proxy.js"),
1140
71
  path.join(__dirname, "..", "cli", "lemma-proxy.ts"),
1141
72
  path.join(__dirname, "lemma-proxy.ts"),
1142
73
  path.join(process.cwd(), "src", "cli", "lemma-proxy.ts"),
1143
- path.join(process.cwd(), "dist", "cjs", "cli", "lemma-proxy.js")
74
+ path.join(process.cwd(), "dist", "cjs", "cli", "lemma-proxy.js"),
1144
75
  ];
1145
76
  let cliPath = "";
1146
77
  for (const cand of cliCandidates) {
@@ -1156,18 +87,44 @@ class LemmaMcpServer {
1156
87
  const proc = spawn(cmd, args, {
1157
88
  stdio: "ignore",
1158
89
  detached: true,
1159
- env: { ...process.env }
90
+ env: { ...process.env },
1160
91
  });
1161
92
  proc.unref();
1162
93
  }
1163
94
  }
1164
- catch { }
95
+ catch (err) {
96
+ logWarn("proxy-auto-start", "Could not auto-start Lemma proxy");
97
+ }
1165
98
  }
1166
- async run() {
1167
- await this.ensureProxyAndClipboardRunning();
1168
- const transport = new StdioServerTransport();
1169
- await this.server.connect(transport);
1170
- console.error("Lemma MCP Server running on stdio");
99
+ // ── MCP Event Reporting ───────────────────────────────────────────
100
+ async reportMcpEvent(event) {
101
+ try {
102
+ const port = getProxyPort();
103
+ axios
104
+ .post(`http://localhost:${port}/api/mcp-event`, {
105
+ type: "mcp_tool_call",
106
+ tool: event.tool,
107
+ args: sanitizeArgsForLogging(event.args),
108
+ result: event.result,
109
+ latency: event.latency,
110
+ tokensImpact: event.tokensImpact || 0,
111
+ timestamp: new Date().toISOString(),
112
+ })
113
+ .catch(() => { });
114
+ }
115
+ catch {
116
+ // Non-critical, silently ignore
117
+ }
118
+ }
119
+ // ── Error Handling ────────────────────────────────────────────────
120
+ setupErrorHandling() {
121
+ this.server.onerror = (error) => {
122
+ console.error("[Lemma MCP Error]", error);
123
+ };
124
+ process.on("SIGINT", async () => {
125
+ await this.server.close();
126
+ process.exit(0);
127
+ });
1171
128
  }
1172
129
  }
1173
130
  const server = new LemmaMcpServer();