@nxuss/lemma 0.7.1 → 0.7.3

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