@nxuss/lemma 0.4.6 → 0.4.8

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 (76) hide show
  1. package/README.md +21 -12
  2. package/dist/cjs/autopilot/AutopilotWatcher.d.ts +11 -0
  3. package/dist/cjs/autopilot/AutopilotWatcher.d.ts.map +1 -0
  4. package/dist/cjs/autopilot/AutopilotWatcher.js +119 -0
  5. package/dist/cjs/autopilot/AutopilotWatcher.js.map +1 -0
  6. package/dist/cjs/cli/lemma-proxy.d.ts +10 -0
  7. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -0
  8. package/dist/cjs/cli/lemma-proxy.js +1268 -0
  9. package/dist/cjs/cli/lemma-proxy.js.map +1 -0
  10. package/dist/cjs/cloud/CloudSyncClient.d.ts +10 -0
  11. package/dist/cjs/cloud/CloudSyncClient.d.ts.map +1 -0
  12. package/dist/cjs/cloud/CloudSyncClient.js +42 -0
  13. package/dist/cjs/cloud/CloudSyncClient.js.map +1 -0
  14. package/dist/cjs/mcp/index.d.ts +2 -0
  15. package/dist/cjs/mcp/index.d.ts.map +1 -0
  16. package/dist/cjs/mcp/index.js +260 -0
  17. package/dist/cjs/mcp/index.js.map +1 -0
  18. package/dist/cjs/observability/IdeContextSync.d.ts.map +1 -1
  19. package/dist/cjs/observability/IdeContextSync.js +7 -2
  20. package/dist/cjs/observability/IdeContextSync.js.map +1 -1
  21. package/dist/cjs/proxy/AgentMultiplexer.d.ts +9 -0
  22. package/dist/cjs/proxy/AgentMultiplexer.d.ts.map +1 -0
  23. package/dist/cjs/proxy/AgentMultiplexer.js +69 -0
  24. package/dist/cjs/proxy/AgentMultiplexer.js.map +1 -0
  25. package/dist/cjs/proxy/ComplexityRouter.d.ts +19 -0
  26. package/dist/cjs/proxy/ComplexityRouter.d.ts.map +1 -0
  27. package/dist/cjs/proxy/ComplexityRouter.js +79 -0
  28. package/dist/cjs/proxy/ComplexityRouter.js.map +1 -0
  29. package/dist/cjs/security/SemanticScrubber.d.ts +25 -0
  30. package/dist/cjs/security/SemanticScrubber.d.ts.map +1 -0
  31. package/dist/cjs/security/SemanticScrubber.js +99 -0
  32. package/dist/cjs/security/SemanticScrubber.js.map +1 -0
  33. package/dist/cjs/utils/ContextSqueezer.d.ts +44 -0
  34. package/dist/cjs/utils/ContextSqueezer.d.ts.map +1 -0
  35. package/dist/cjs/utils/ContextSqueezer.js +201 -0
  36. package/dist/cjs/utils/ContextSqueezer.js.map +1 -0
  37. package/dist/esm/autopilot/AutopilotWatcher.d.ts +11 -0
  38. package/dist/esm/autopilot/AutopilotWatcher.d.ts.map +1 -0
  39. package/dist/esm/autopilot/AutopilotWatcher.js +112 -0
  40. package/dist/esm/autopilot/AutopilotWatcher.js.map +1 -0
  41. package/dist/esm/cli/lemma-proxy.d.ts +10 -0
  42. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -0
  43. package/dist/esm/cli/lemma-proxy.js +1262 -0
  44. package/dist/esm/cli/lemma-proxy.js.map +1 -0
  45. package/dist/esm/cloud/CloudSyncClient.d.ts +10 -0
  46. package/dist/esm/cloud/CloudSyncClient.d.ts.map +1 -0
  47. package/dist/esm/cloud/CloudSyncClient.js +35 -0
  48. package/dist/esm/cloud/CloudSyncClient.js.map +1 -0
  49. package/dist/esm/mcp/index.d.ts +2 -0
  50. package/dist/esm/mcp/index.d.ts.map +1 -0
  51. package/dist/esm/mcp/index.js +255 -0
  52. package/dist/esm/mcp/index.js.map +1 -0
  53. package/dist/esm/observability/IdeContextSync.d.ts.map +1 -1
  54. package/dist/esm/observability/IdeContextSync.js +7 -2
  55. package/dist/esm/observability/IdeContextSync.js.map +1 -1
  56. package/dist/esm/proxy/AgentMultiplexer.d.ts +9 -0
  57. package/dist/esm/proxy/AgentMultiplexer.d.ts.map +1 -0
  58. package/dist/esm/proxy/AgentMultiplexer.js +62 -0
  59. package/dist/esm/proxy/AgentMultiplexer.js.map +1 -0
  60. package/dist/esm/proxy/ComplexityRouter.d.ts +19 -0
  61. package/dist/esm/proxy/ComplexityRouter.d.ts.map +1 -0
  62. package/dist/esm/proxy/ComplexityRouter.js +75 -0
  63. package/dist/esm/proxy/ComplexityRouter.js.map +1 -0
  64. package/dist/esm/security/SemanticScrubber.d.ts +25 -0
  65. package/dist/esm/security/SemanticScrubber.d.ts.map +1 -0
  66. package/dist/esm/security/SemanticScrubber.js +92 -0
  67. package/dist/esm/security/SemanticScrubber.js.map +1 -0
  68. package/dist/esm/utils/ContextSqueezer.d.ts +44 -0
  69. package/dist/esm/utils/ContextSqueezer.d.ts.map +1 -0
  70. package/dist/esm/utils/ContextSqueezer.js +193 -0
  71. package/dist/esm/utils/ContextSqueezer.js.map +1 -0
  72. package/lemma-proxy.cjs +32 -732
  73. package/package.json +3 -2
  74. package/src/cloud/CloudSyncClient.js +0 -35
  75. package/src/proxy/ComplexityRouter.js +0 -37
  76. package/src/security/SemanticScrubber.js +0 -54
@@ -0,0 +1,1268 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.performAutoHeal = performAutoHeal;
8
+ const commander_1 = require("commander");
9
+ const express_1 = __importDefault(require("express"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const crypto_1 = __importDefault(require("crypto"));
13
+ const chromadb_1 = require("chromadb");
14
+ const https_1 = __importDefault(require("https"));
15
+ const http_1 = __importDefault(require("http"));
16
+ const axios_1 = __importDefault(require("axios"));
17
+ function getVersion() {
18
+ const paths = [
19
+ path_1.default.join(__dirname, '../../package.json'),
20
+ path_1.default.join(__dirname, '../../../package.json'),
21
+ path_1.default.join(__dirname, '../../../../package.json')
22
+ ];
23
+ for (const p of paths) {
24
+ if (fs_1.default.existsSync(p)) {
25
+ try {
26
+ const pkg = JSON.parse(fs_1.default.readFileSync(p, 'utf8'));
27
+ if (pkg.version)
28
+ return pkg.version;
29
+ }
30
+ catch { }
31
+ }
32
+ }
33
+ return '0.4.6';
34
+ }
35
+ const VERSION = getVersion();
36
+ const ComplexityRouter_1 = __importDefault(require("../proxy/ComplexityRouter"));
37
+ const SemanticScrubber_1 = __importDefault(require("../security/SemanticScrubber"));
38
+ const CloudSyncClient_1 = __importDefault(require("../cloud/CloudSyncClient"));
39
+ const ContextSqueezer_1 = require("../utils/ContextSqueezer");
40
+ const AgentMultiplexer_1 = require("../proxy/AgentMultiplexer");
41
+ const chroma = new chromadb_1.ChromaClient({ host: 'localhost', port: 8000 });
42
+ const dummyEmbeddingFunction = { generate: (texts) => Promise.resolve([]) };
43
+ const complexityRouter = new ComplexityRouter_1.default();
44
+ const semanticScrubber = new SemanticScrubber_1.default();
45
+ const cloudSync = new CloudSyncClient_1.default();
46
+ const agentMultiplexer = new AgentMultiplexer_1.AgentMultiplexer();
47
+ // ── Paths ──────────────────────────────────────────────────────────────────────
48
+ const HOME = process.env.HOME || process.env.USERPROFILE || '~';
49
+ const CACHE_DIR = path_1.default.join(HOME, '.lemma-cache');
50
+ const PID_FILE = path_1.default.join(CACHE_DIR, 'proxy.pid');
51
+ const PORT_FILE = path_1.default.join(CACHE_DIR, 'proxy.port');
52
+ const STATS_FILE = path_1.default.join(CACHE_DIR, 'stats.json');
53
+ const USAGE_FILE = path_1.default.join(CACHE_DIR, 'usage.json');
54
+ const LICENSE_FILE = path_1.default.join(CACHE_DIR, 'license.json');
55
+ const EVENT_LOG = [];
56
+ const MAX_EVENTS = 100;
57
+ function logEvent(event) {
58
+ const ev = { id: Math.random().toString(36).slice(2, 11), timestamp: Date.now(), ...event };
59
+ EVENT_LOG.push(ev);
60
+ if (EVENT_LOG.length > MAX_EVENTS)
61
+ EVENT_LOG.shift();
62
+ }
63
+ const FREE_LIMIT = 500;
64
+ const WARN_PCT = 0.8;
65
+ const VALIDATE_URL = 'https://lemma.nxus.studio/api/v1/validate';
66
+ if (!fs_1.default.existsSync(CACHE_DIR))
67
+ fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
68
+ // ── Helpers ────────────────────────────────────────────────────────────────────
69
+ async function readJson(file, fallback) {
70
+ try {
71
+ if (await fs_1.default.promises.access(file).then(() => true).catch(() => false)) {
72
+ const content = await fs_1.default.promises.readFile(file, 'utf8');
73
+ return JSON.parse(content);
74
+ }
75
+ }
76
+ catch { }
77
+ return fallback;
78
+ }
79
+ async function writeJson(file, data) {
80
+ try {
81
+ const tempFile = file + '.tmp';
82
+ await fs_1.default.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
83
+ await fs_1.default.promises.rename(tempFile, file);
84
+ }
85
+ catch { }
86
+ }
87
+ function projectHash(name) {
88
+ return crypto_1.default.createHash('sha1').update(name).digest('hex').slice(0, 12);
89
+ }
90
+ function detectProject() {
91
+ const pkgPath = path_1.default.join(process.cwd(), 'package.json');
92
+ if (fs_1.default.existsSync(pkgPath)) {
93
+ try {
94
+ const p = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8'));
95
+ if (p.name)
96
+ return p.name;
97
+ }
98
+ catch { }
99
+ }
100
+ return path_1.default.basename(process.cwd());
101
+ }
102
+ function ensureProjectDir(name) {
103
+ const dir = path_1.default.join(CACHE_DIR, projectHash(name));
104
+ if (!fs_1.default.existsSync(dir))
105
+ fs_1.default.mkdirSync(dir, { recursive: true });
106
+ const marker = path_1.default.join(dir, 'project-name.txt');
107
+ if (!fs_1.default.existsSync(marker))
108
+ fs_1.default.writeFileSync(marker, name);
109
+ return dir;
110
+ }
111
+ function getPidByPort(port) {
112
+ const { execSync } = require('child_process');
113
+ try {
114
+ if (process.platform === 'win32') {
115
+ const out = execSync(`netstat -ano | findstr :${port}`).toString();
116
+ const match = out.match(/LISTENING\s+(\d+)/);
117
+ return match ? match[1] : null;
118
+ }
119
+ else {
120
+ const out = execSync(`lsof -t -i :${port}`).toString().trim();
121
+ return out.split('\n')[0]; // Take first if multiple
122
+ }
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }
128
+ async function checkDependency(url) {
129
+ try {
130
+ const res = await axios_1.default.get(url, { timeout: 1000, validateStatus: () => true });
131
+ return res.status >= 200 && res.status < 500;
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ function ensureGitIgnore() {
138
+ const gi = path_1.default.join(process.cwd(), '.gitignore');
139
+ const line = '.lemma/';
140
+ try {
141
+ if (fs_1.default.existsSync(gi)) {
142
+ const content = fs_1.default.readFileSync(gi, 'utf8');
143
+ if (!content.includes(line)) {
144
+ fs_1.default.appendFileSync(gi, `\n# Lemma Context Logs\n${line}\n`);
145
+ return true;
146
+ }
147
+ }
148
+ else {
149
+ fs_1.default.writeFileSync(gi, `# Lemma Context Logs\n${line}\n`);
150
+ return true;
151
+ }
152
+ }
153
+ catch { }
154
+ return false;
155
+ }
156
+ // ── License helpers ────────────────────────────────────────────────────────────
157
+ async function loadLicense() {
158
+ return readJson(LICENSE_FILE, { isPro: false });
159
+ }
160
+ async function isPro() {
161
+ const l = await loadLicense();
162
+ return !!(l && l.isPro);
163
+ }
164
+ async function validateKeyRemote(key) {
165
+ return new Promise((resolve, reject) => {
166
+ const body = JSON.stringify({ key });
167
+ const u = new URL(VALIDATE_URL);
168
+ const req = https_1.default.request({
169
+ hostname: u.hostname,
170
+ port: 443,
171
+ path: u.pathname,
172
+ method: 'POST',
173
+ headers: {
174
+ 'Content-Type': 'application/json',
175
+ 'Content-Length': Buffer.byteLength(body),
176
+ 'User-Agent': `lemma-proxy/${VERSION}`
177
+ }
178
+ }, res => {
179
+ let d = '';
180
+ res.on('data', c => d += c);
181
+ res.on('end', () => {
182
+ try {
183
+ resolve(JSON.parse(d));
184
+ }
185
+ catch {
186
+ reject(new Error('bad json'));
187
+ }
188
+ });
189
+ });
190
+ req.on('error', reject);
191
+ const t = setTimeout(() => { req.destroy(); reject(new Error('timeout')); }, 5000);
192
+ req.on('close', () => clearTimeout(t));
193
+ req.write(body);
194
+ req.end();
195
+ });
196
+ }
197
+ // ── Usage helpers ──────────────────────────────────────────────────────────────
198
+ async function loadUsage() {
199
+ const u = await readJson(USAGE_FILE, { count: 0, lastReset: new Date().toISOString(), history: [] });
200
+ const now = new Date();
201
+ const last = new Date(u.lastReset || 0);
202
+ if (now.getMonth() !== last.getMonth() || now.getFullYear() !== last.getFullYear()) {
203
+ const fresh = {
204
+ count: 0,
205
+ lastReset: now.toISOString(),
206
+ history: [...(u.history || []), { month: last.toISOString().slice(0, 7), count: u.count }]
207
+ };
208
+ await writeJson(USAGE_FILE, fresh);
209
+ return fresh;
210
+ }
211
+ return u;
212
+ }
213
+ // ── Stats helpers ──────────────────────────────────────────────────────────────
214
+ async function loadStats() {
215
+ return readJson(STATS_FILE, {});
216
+ }
217
+ async function recordStat(stats, project, fromCache, latencyMs, provider, tokensSaved) {
218
+ if (!stats[project]) {
219
+ stats[project] = { total: 0, hits: 0, misses: 0, totalLatency: 0, totalTokensSaved: 0, providers: {} };
220
+ }
221
+ const s = stats[project];
222
+ s.total++;
223
+ s.totalLatency += latencyMs;
224
+ fromCache ? s.hits++ : s.misses++;
225
+ s.totalTokensSaved += (fromCache ? (tokensSaved || 2000) : (tokensSaved || 0));
226
+ if (!s.providers[provider])
227
+ s.providers[provider] = { hits: 0, misses: 0 };
228
+ fromCache ? s.providers[provider].hits++ : s.providers[provider].misses++;
229
+ logEvent({
230
+ type: fromCache ? 'cache:hit' : 'cache:miss',
231
+ project,
232
+ latency: latencyMs,
233
+ provider,
234
+ tokens: fromCache ? (tokensSaved || 2000) : (tokensSaved || 0)
235
+ });
236
+ await writeJson(STATS_FILE, stats);
237
+ }
238
+ const CACHE = new Map();
239
+ const TTL_MS = 86400000 * 7;
240
+ function cacheKey(prompt) {
241
+ return crypto_1.default.createHash('sha256').update(prompt).digest('hex');
242
+ }
243
+ function cacheGet(prompt) {
244
+ const k = cacheKey(prompt);
245
+ const e = CACHE.get(k);
246
+ if (!e)
247
+ return null;
248
+ if (e.ttl > 0 && Date.now() - e.createdAt > e.ttl) {
249
+ CACHE.delete(k);
250
+ return null;
251
+ }
252
+ return e;
253
+ }
254
+ function cacheSet(prompt, data) {
255
+ CACHE.set(cacheKey(prompt), { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS });
256
+ }
257
+ // ── Semantic Search (Pro) ──────────────────────────────────────────────────────
258
+ async function getEmbedding(text) {
259
+ try {
260
+ const resp = await axios_1.default.post('http://127.0.0.1:11434/api/embeddings', {
261
+ model: 'nomic-embed-text',
262
+ prompt: text
263
+ }, { timeout: 15000 });
264
+ return resp.data.embedding;
265
+ }
266
+ catch (e) {
267
+ console.warn(`\n\x1b[33m⚠️ [Semantic Cache] Failed to generate embedding from Ollama: ${e.message}\x1b[0m`);
268
+ console.warn(`\x1b[33m👉 Verify that Ollama is running ('ollama run nomic-embed-text'). Bypassing semantic cache for this request.\x1b[0m\n`);
269
+ return null;
270
+ }
271
+ }
272
+ async function semanticGet(prompt) {
273
+ if (!await isPro())
274
+ return null;
275
+ const emb = await getEmbedding(prompt);
276
+ if (!emb)
277
+ return null;
278
+ try {
279
+ const collection = await chroma.getOrCreateCollection({
280
+ name: 'lemma-cache',
281
+ embeddingFunction: dummyEmbeddingFunction,
282
+ metadata: { "hnsw:space": "cosine" }
283
+ });
284
+ const res = await collection.query({
285
+ queryEmbeddings: [emb],
286
+ nResults: 1
287
+ });
288
+ if (!res.ids[0] || res.ids[0].length === 0)
289
+ return null;
290
+ const distances = res.distances;
291
+ const distance = (distances && distances[0] && distances[0][0] !== null && distances[0][0] !== undefined) ? distances[0][0] : 1.0;
292
+ const similarity = Math.max(0, 1 - distance);
293
+ const THRESHOLD = 0.7;
294
+ if (similarity >= THRESHOLD) {
295
+ const metadata = res.metadatas[0][0];
296
+ return {
297
+ data: JSON.parse(metadata.response),
298
+ similarity,
299
+ id: res.ids[0][0],
300
+ prompt: metadata.prompt
301
+ };
302
+ }
303
+ }
304
+ catch (e) { }
305
+ return null;
306
+ }
307
+ async function semanticSet(prompt, data) {
308
+ if (!await isPro())
309
+ return;
310
+ const emb = await getEmbedding(prompt);
311
+ if (!emb)
312
+ return;
313
+ try {
314
+ const collection = await chroma.getOrCreateCollection({
315
+ name: 'lemma-cache',
316
+ embeddingFunction: dummyEmbeddingFunction,
317
+ metadata: { "hnsw:space": "cosine" }
318
+ });
319
+ await collection.add({
320
+ ids: [crypto_1.default.randomUUID?.() || Math.random().toString(36).substring(7)],
321
+ embeddings: [emb],
322
+ metadatas: [{
323
+ prompt,
324
+ response: JSON.stringify(data),
325
+ timestamp: Date.now()
326
+ }],
327
+ documents: [prompt]
328
+ });
329
+ }
330
+ catch (e) { }
331
+ }
332
+ // ── Prompt extraction ──────────────────────────────────────────────────────────
333
+ function extractPrompt(body, provider) {
334
+ const msgs = body.messages || [];
335
+ if (provider === 'openai' || provider === 'anthropic') {
336
+ return msgs.map((m) => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content))).join('\n');
337
+ }
338
+ if (provider === 'gemini') {
339
+ const contents = body.contents || [];
340
+ return contents.map((c) => c.parts.map((p) => p.text).join(' ')).join('\n');
341
+ }
342
+ return null;
343
+ }
344
+ // ── SSE helpers ────────────────────────────────────────────────────────────────
345
+ function setSseHeaders(res, fromCache, similarity, tier) {
346
+ res.setHeader('Content-Type', 'text/event-stream');
347
+ res.setHeader('Cache-Control', 'no-cache');
348
+ res.setHeader('Connection', 'keep-alive');
349
+ res.setHeader('X-Accel-Buffering', 'no');
350
+ res.setHeader('X-Lemma-Cache', fromCache ? 'HIT' : 'MISS');
351
+ res.setHeader('X-Lemma-Similarity', (similarity || 1.0).toFixed(3));
352
+ res.setHeader('X-Lemma-Tier', tier);
353
+ }
354
+ function buildOaiChunk(id, model, text, stop) {
355
+ return { id, object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model,
356
+ choices: [{ index: 0, delta: stop ? {} : { content: text }, finish_reason: stop ? 'stop' : null }] };
357
+ }
358
+ function buildAntChunk(text, stop) {
359
+ if (stop)
360
+ return { type: 'message_stop' };
361
+ return { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text } };
362
+ }
363
+ function buildGemChunk(text, stop) {
364
+ return { candidates: [{ content: { parts: [{ text }] }, finishReason: stop ? 'STOP' : undefined }] };
365
+ }
366
+ async function simulateHitStream(res, cachedData, provider, model, tier, similarity) {
367
+ setSseHeaders(res, true, similarity, tier);
368
+ const id = `lemma-${Date.now()}`;
369
+ let text = '';
370
+ try {
371
+ if (provider === 'openai')
372
+ text = cachedData?.choices?.[0]?.message?.content || '';
373
+ else if (provider === 'anthropic') {
374
+ const c = cachedData?.content;
375
+ text = Array.isArray(c) ? c.map((x) => x.text || '').join('') : String(c || '');
376
+ }
377
+ else if (provider === 'gemini') {
378
+ text = cachedData?.candidates?.[0]?.content?.parts?.[0]?.text || '';
379
+ }
380
+ }
381
+ catch { }
382
+ const words = text.split(' ');
383
+ for (let i = 0; i < words.length; i += 5) {
384
+ const chunk = words.slice(i, i + 5).join(' ');
385
+ let evt;
386
+ if (provider === 'openai')
387
+ evt = buildOaiChunk(id, model, chunk, false);
388
+ else if (provider === 'anthropic')
389
+ evt = buildAntChunk(chunk, false);
390
+ else if (provider === 'gemini')
391
+ evt = buildGemChunk(chunk, false);
392
+ res.write(provider === 'gemini' ? `${JSON.stringify(evt)}\n` : `data: ${JSON.stringify(evt)}\n\n`);
393
+ await new Promise(r => setTimeout(r, 15));
394
+ }
395
+ const stopEvt = provider === 'openai' ? buildOaiChunk(id, model, '', true) : (provider === 'anthropic' ? buildAntChunk('', true) : buildGemChunk('', true));
396
+ res.write(provider === 'gemini' ? `${JSON.stringify(stopEvt)}\n` : `data: ${JSON.stringify(stopEvt)}\n\n`);
397
+ if (provider !== 'gemini')
398
+ res.write('data: [DONE]\n\n');
399
+ res.end();
400
+ }
401
+ async function relayMissStream(res, body, provider, tier) {
402
+ setSseHeaders(res, false, 1.0, tier);
403
+ const apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : process.env.ANTHROPIC_API_KEY;
404
+ if (!apiKey) {
405
+ res.write(`data: ${JSON.stringify({ error: `${provider.toUpperCase()}_API_KEY not set` })}\n\ndata: [DONE]\n\n`);
406
+ res.end();
407
+ return;
408
+ }
409
+ const hostname = provider === 'openai' ? 'api.openai.com' : 'api.anthropic.com';
410
+ const upPath = provider === 'openai' ? '/v1/chat/completions' : '/v1/messages';
411
+ const headers = provider === 'openai'
412
+ ? { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }
413
+ : { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' };
414
+ const payload = JSON.stringify({ ...body, stream: true });
415
+ await new Promise(resolve => {
416
+ const req = https_1.default.request({ hostname, port: 443, path: upPath, method: 'POST',
417
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(payload) }
418
+ }, upstream => {
419
+ upstream.on('data', chunk => res.write(chunk));
420
+ upstream.on('end', () => { res.write('data: [DONE]\n\n'); res.end(); resolve(); });
421
+ upstream.on('error', err => { res.write(`data: ${JSON.stringify({ error: err.message })}\n\ndata: [DONE]\n\n`); res.end(); resolve(); });
422
+ });
423
+ req.on('error', err => { res.write(`data: ${JSON.stringify({ error: err.message })}\n\ndata: [DONE]\n\n`); res.end(); resolve(); });
424
+ req.write(payload);
425
+ req.end();
426
+ });
427
+ }
428
+ // ── Proxy Server class ─────────────────────────────────────────────────────────
429
+ class LemmaServer {
430
+ constructor(port, projectName) {
431
+ this.stats = {};
432
+ this.port = port;
433
+ this.projectName = projectName || detectProject();
434
+ this.projectDir = ensureProjectDir(this.projectName);
435
+ this.app = (0, express_1.default)();
436
+ this.setupMiddleware();
437
+ this.setupRoutes();
438
+ }
439
+ setupMiddleware() {
440
+ this.app.use(express_1.default.json({ limit: '10mb' }));
441
+ this.app.use((req, res, next) => {
442
+ res.header('Access-Control-Allow-Origin', '*');
443
+ res.header('Access-Control-Allow-Headers', '*');
444
+ res.header('Access-Control-Allow-Methods', '*');
445
+ if (req.method === 'OPTIONS')
446
+ return res.sendStatus(200);
447
+ next();
448
+ });
449
+ }
450
+ setupRoutes() {
451
+ this.app.get('/health', async (req, res) => {
452
+ const pro = await isPro();
453
+ res.json({ status: 'ok', project: this.projectName, mode: pro ? 'pro' : 'standard', tier: pro ? 'pro' : 'free', port: this.port });
454
+ });
455
+ this.app.get('/v1/models', (req, res) => {
456
+ res.json({ object: 'list', data: [
457
+ { id: 'gpt-4o', object: 'model', owned_by: 'lemma-proxy' },
458
+ { id: 'claude-3-5-sonnet-20241022', object: 'model', owned_by: 'lemma-proxy' }
459
+ ] });
460
+ });
461
+ this.app.post('/v1/chat/completions', (req, res) => this.handleCompletion(req, res, 'openai'));
462
+ this.app.post('/v1/messages', (req, res) => this.handleCompletion(req, res, 'anthropic'));
463
+ this.app.post('/v1beta/models/:modelId:generateContent', (req, res) => this.handleCompletion(req, res, 'gemini'));
464
+ // Dashboard APIs
465
+ const dashboardPath = path_1.default.join(__dirname, 'dashboard', 'dist');
466
+ if (fs_1.default.existsSync(dashboardPath)) {
467
+ this.app.use('/dashboard/', express_1.default.static(dashboardPath));
468
+ this.app.get('/api/metrics', async (req, res) => {
469
+ const stats = this.stats[this.projectName] || { total: 0, hits: 0, misses: 0, totalLatency: 0, totalTokensSaved: 0 };
470
+ const hitRate = stats.total > 0 ? stats.hits / stats.total : 0;
471
+ const avgLat = stats.total > 0 ? stats.totalLatency / stats.total : 0;
472
+ const secretsMasked = EVENT_LOG.filter(e => e.type === 'privacy:mask').length;
473
+ res.json({
474
+ timestamp: new Date().toISOString(),
475
+ totalRequests: stats.total,
476
+ cacheHits: stats.hits,
477
+ cacheMisses: stats.misses,
478
+ hitRate,
479
+ averageLatency: avgLat,
480
+ tokensSaved: stats.totalTokensSaved,
481
+ costSaved: (stats.totalTokensSaved / 1000) * 0.02,
482
+ secretsMasked: secretsMasked,
483
+ activeAgents: 1,
484
+ totalAgents: 1,
485
+ uptime: process.uptime(),
486
+ tier: (await isPro()) ? 'pro' : 'free'
487
+ });
488
+ });
489
+ this.app.get('/api/agents', async (req, res) => {
490
+ const stats = await loadStats();
491
+ res.json({ agents: [
492
+ { id: 'lemma-gateway', name: 'Lemma Gateway', status: 'active', capabilities: ['Privacy Firewall', 'Semantic Cache', 'Complexity Router'], tasksCompleted: stats[this.projectName]?.total || 0, lastSeen: new Date().toISOString(), errorCount: 0, cacheHitRate: 0.85 }
493
+ ] });
494
+ });
495
+ this.app.get('/api/events', (req, res) => {
496
+ const page = parseInt(req.query.page) || 1;
497
+ const limit = parseInt(req.query.limit) || 50;
498
+ const start = (page - 1) * limit;
499
+ const events = EVENT_LOG.slice().reverse().slice(start, start + limit);
500
+ res.json({ events, pagination: { total: EVENT_LOG.length, page, limit } });
501
+ });
502
+ this.app.get('/api/cache-stats', (req, res) => {
503
+ res.json({ topQueries: [] });
504
+ });
505
+ this.app.get('/api/search', async (req, res) => {
506
+ const query = req.query.q;
507
+ if (!query)
508
+ return res.status(400).json({ error: 'Query required' });
509
+ try {
510
+ const emb = await getEmbedding(query);
511
+ if (!emb)
512
+ throw new Error('Failed to generate embedding');
513
+ const collection = await chroma.getOrCreateCollection({
514
+ name: 'lemma-cache',
515
+ embeddingFunction: dummyEmbeddingFunction,
516
+ metadata: { "hnsw:space": "cosine" }
517
+ });
518
+ const results = await collection.query({
519
+ queryEmbeddings: [emb],
520
+ nResults: 10
521
+ });
522
+ const formatted = (results.ids[0] || []).map((id, i) => {
523
+ const meta = results.metadatas[0][i];
524
+ const dists = results.distances;
525
+ const dist = (dists && dists[0] && dists[0][i] !== null && dists[0][i] !== undefined) ? dists[0][i] : 1.0;
526
+ return {
527
+ id,
528
+ prompt: meta.prompt,
529
+ response: JSON.parse(meta.response),
530
+ timestamp: meta.timestamp,
531
+ similarity: 1 - dist
532
+ };
533
+ });
534
+ res.json({ results: formatted });
535
+ }
536
+ catch (e) {
537
+ res.status(500).json({ error: e.message });
538
+ }
539
+ });
540
+ this.app.post('/api/cache/clear', (req, res) => {
541
+ const count = CACHE.size;
542
+ CACHE.clear();
543
+ res.json({ success: true, clearedEntries: count });
544
+ });
545
+ this.app.get('/dashboard/*', (req, res) => res.sendFile(path_1.default.join(dashboardPath, 'index.html')));
546
+ }
547
+ }
548
+ async handleCompletion(req, res, provider) {
549
+ const t0 = Date.now();
550
+ const pro = await isPro();
551
+ const tier = pro ? 'pro' : 'standard';
552
+ const isStream = !!req.body.stream;
553
+ let historyTokensSaved = 0;
554
+ if (!pro) {
555
+ const usage = await loadUsage();
556
+ if (usage.count >= FREE_LIMIT) {
557
+ return res.status(429).json({ error: { message: `Free tier limit (${FREE_LIMIT}/mo). Upgrade: https://lemma.nxus.studio/upgrade` } });
558
+ }
559
+ }
560
+ const rawPrompt = extractPrompt(req.body, provider);
561
+ if (!rawPrompt)
562
+ return res.status(400).json({ error: 'Invalid request format' });
563
+ const { maskedPrompt: prompt, tokenMap } = semanticScrubber.mask(rawPrompt);
564
+ if (Object.keys(tokenMap).length > 0) {
565
+ logEvent({ type: 'privacy:mask', count: Object.keys(tokenMap).length, project: this.projectName });
566
+ }
567
+ const originalModel = req.body.model || 'gpt-4o';
568
+ const routingDecision = complexityRouter.evaluate(prompt, originalModel);
569
+ const model = routingDecision.model;
570
+ if (req.body.model)
571
+ req.body.model = model;
572
+ // Apply the Agent Multiplexer to local-verify the prompt in parallel
573
+ const enrichedPrompt = await agentMultiplexer.multiplexPrompt(prompt, model);
574
+ // Compress long TS/JS codebase file blocks in the enriched prompt using the Context Squeezer AST Tree-Shaker!
575
+ // The query context is the user's primary prompt to ensure we keep relevant methods!
576
+ const squeezeResult = (0, ContextSqueezer_1.squeezePrompt)(enrichedPrompt, prompt);
577
+ const squeezedPrompt = squeezeResult.squeezed;
578
+ const compressionSavings = squeezeResult.originalSize - squeezeResult.squeezedSize;
579
+ if (compressionSavings > 0) {
580
+ console.log(`⚡ [Squeezer] Compressed prompt from ${squeezeResult.originalSize} to ${squeezeResult.squeezedSize} chars (Saved ${(squeezeResult.compressionRatio * 100).toFixed(1)}% context tokens!)`);
581
+ }
582
+ if (provider === 'openai' || provider === 'anthropic') {
583
+ if (req.body.messages && req.body.messages.length > 0) {
584
+ req.body.messages[req.body.messages.length - 1].content = squeezedPrompt;
585
+ // Apply conversational history pruning to save tokens on historical messages!
586
+ const pruneResult = (0, ContextSqueezer_1.pruneHistoryMessages)(req.body.messages);
587
+ req.body.messages = pruneResult.messages;
588
+ historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
589
+ }
590
+ }
591
+ else if (provider === 'gemini') {
592
+ if (req.body.contents && req.body.contents.length > 0) {
593
+ const lastContent = req.body.contents[req.body.contents.length - 1];
594
+ if (lastContent.parts && lastContent.parts.length > 0) {
595
+ lastContent.parts[lastContent.parts.length - 1].text = squeezedPrompt;
596
+ }
597
+ }
598
+ }
599
+ const hit = cacheGet(prompt);
600
+ if (hit) {
601
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000 + historyTokensSaved);
602
+ const unmaskedData = semanticScrubber.unmask(hit.data, tokenMap);
603
+ if (isStream)
604
+ return simulateHitStream(res, unmaskedData, provider, model, tier, 1.0);
605
+ res.setHeader('X-Lemma-Cache', 'HIT');
606
+ res.setHeader('X-Lemma-Similarity', '1.000');
607
+ res.setHeader('X-Lemma-Tier', tier);
608
+ return res.json(unmaskedData);
609
+ }
610
+ if (pro) {
611
+ const semHit = await semanticGet(prompt);
612
+ if (semHit) {
613
+ if (semHit.similarity >= 0.90) {
614
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000 + historyTokensSaved);
615
+ const unmaskedData = semanticScrubber.unmask(semHit.data, tokenMap);
616
+ if (isStream)
617
+ return simulateHitStream(res, unmaskedData, provider, model, tier, semHit.similarity);
618
+ res.setHeader('X-Lemma-Cache', 'HIT');
619
+ res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
620
+ res.setHeader('X-Lemma-Tier', tier);
621
+ return res.json(unmaskedData);
622
+ }
623
+ else if (semHit.similarity >= 0.70) {
624
+ console.log(`💡 [CARS] Near Cache Hit (Similarity: ${(semHit.similarity * 100).toFixed(1)}%). Synthesizing answer locally via Ollama...`);
625
+ let cachedContent = '';
626
+ if (provider === 'openai') {
627
+ cachedContent = semHit.data.choices?.[0]?.message?.content || '';
628
+ }
629
+ else if (provider === 'anthropic') {
630
+ cachedContent = semHit.data.content?.[0]?.text || '';
631
+ }
632
+ if (cachedContent) {
633
+ const synthesisPrompt = `You are a local helper AI. You have an exact previous query and its professional answer from a cloud LLM.
634
+ Your task is to adjust/synthesize the previous answer slightly so that it perfectly matches the new query. Do not perform any unnecessary changes. Keep the exact premium style of the answer.
635
+
636
+ Previous Query:
637
+ ${semHit.prompt}
638
+
639
+ Previous Answer:
640
+ ${cachedContent}
641
+
642
+ New Query:
643
+ ${prompt}
644
+
645
+ Adjusted Answer:`;
646
+ try {
647
+ const carsResponse = await axios_1.default.post('http://127.0.0.1:11434/api/generate', {
648
+ model: 'llama3',
649
+ prompt: synthesisPrompt,
650
+ stream: false
651
+ }, { timeout: 15000 });
652
+ const adjustedText = carsResponse.data.response || '';
653
+ if (adjustedText && adjustedText.length > 20) {
654
+ const synthesizedData = JSON.parse(JSON.stringify(semHit.data));
655
+ if (provider === 'openai') {
656
+ synthesizedData.choices[0].message.content = adjustedText;
657
+ }
658
+ else if (provider === 'anthropic') {
659
+ synthesizedData.content[0].text = adjustedText;
660
+ }
661
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000 + historyTokensSaved);
662
+ const unmaskedData = semanticScrubber.unmask(synthesizedData, tokenMap);
663
+ if (isStream) {
664
+ return simulateHitStream(res, unmaskedData, provider, model, tier, semHit.similarity);
665
+ }
666
+ res.setHeader('X-Lemma-Cache', 'HIT-CARS');
667
+ res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
668
+ res.setHeader('X-Lemma-Tier', tier);
669
+ return res.json(unmaskedData);
670
+ }
671
+ }
672
+ catch (carsErr) {
673
+ console.log(`⚠️ [CARS] Local synthesis failed or Ollama offline (${carsErr.message}). Falling back to cloud.`);
674
+ }
675
+ }
676
+ }
677
+ }
678
+ const cloudHit = await cloudSync.get(prompt);
679
+ if (cloudHit) {
680
+ cacheSet(prompt, cloudHit.data);
681
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000 + historyTokensSaved);
682
+ const unmaskedData = semanticScrubber.unmask(cloudHit.data, tokenMap);
683
+ if (isStream)
684
+ return simulateHitStream(res, unmaskedData, provider, model, tier, cloudHit.similarity || 0.95);
685
+ res.setHeader('X-Lemma-Cache', 'HIT');
686
+ res.setHeader('X-Lemma-Similarity', (cloudHit.similarity || 0.95).toFixed(3));
687
+ res.setHeader('X-Lemma-Tier', tier);
688
+ return res.json(unmaskedData);
689
+ }
690
+ }
691
+ if (isStream) {
692
+ await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
693
+ if (!pro) {
694
+ const u = await loadUsage();
695
+ u.count++;
696
+ await writeJson(USAGE_FILE, u);
697
+ }
698
+ return relayMissStream(res, req.body, provider, tier);
699
+ }
700
+ try {
701
+ const data = await this.callUpstream(req.body, provider);
702
+ cacheSet(prompt, data);
703
+ if (pro) {
704
+ await semanticSet(prompt, data);
705
+ await cloudSync.set(prompt, data);
706
+ }
707
+ if (!pro) {
708
+ const u = await loadUsage();
709
+ u.count++;
710
+ await writeJson(USAGE_FILE, u);
711
+ }
712
+ await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
713
+ return res.json(semanticScrubber.unmask(data, tokenMap));
714
+ }
715
+ catch (err) {
716
+ return res.status(500).json({ error: err.message });
717
+ }
718
+ }
719
+ async callUpstream(body, provider) {
720
+ let apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : (provider === 'anthropic' ? process.env.ANTHROPIC_API_KEY : process.env.GEMINI_API_KEY);
721
+ if (!apiKey) {
722
+ console.log(`\n\x1b[33m⚠️ No API Key found for ${provider.toUpperCase()}. Falling back to local Ollama (Zero-Cost Mode)!\x1b[0m`);
723
+ const model = body.model || 'llama3';
724
+ const baseURL = 'http://127.0.0.1:11434/v1/chat/completions';
725
+ try {
726
+ const resp = await axios_1.default.post(baseURL, {
727
+ ...body,
728
+ model: model
729
+ }, { timeout: 30000 });
730
+ return resp.data;
731
+ }
732
+ catch (e) {
733
+ throw new Error(`No API Key found and Ollama is offline. Please set ${provider.toUpperCase()}_API_KEY or start Ollama.`);
734
+ }
735
+ }
736
+ const baseURL = provider === 'openai' ? 'https://api.openai.com/v1/chat/completions' : (provider === 'anthropic' ? 'https://api.anthropic.com/v1/messages' : `https://generativelanguage.googleapis.com/v1beta/models/${body.model || 'gemini-1.5-pro'}:generateContent?key=${apiKey}`);
737
+ const headers = provider === 'openai' ? { Authorization: `Bearer ${apiKey}` } : (provider === 'anthropic' ? { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' } : {});
738
+ const resp = await axios_1.default.post(baseURL, body, { headers: { ...headers, 'Content-Type': 'application/json' } });
739
+ return resp.data;
740
+ }
741
+ async start() {
742
+ this.stats = await loadStats();
743
+ const server = http_1.default.createServer(this.app);
744
+ server.on('error', (e) => {
745
+ if (e.code === 'EADDRINUSE') {
746
+ console.error(`\n❌ Error: Port ${this.port} is already in use.`);
747
+ console.error(`💡 Try stopping the existing process with 'lemma stop' or use a different port with '--port <number>'.\n`);
748
+ process.exit(1);
749
+ }
750
+ throw e;
751
+ });
752
+ server.listen(this.port, async () => {
753
+ fs_1.default.writeFileSync(PID_FILE, String(process.pid));
754
+ fs_1.default.writeFileSync(PORT_FILE, String(this.port));
755
+ const pro = await isPro();
756
+ console.log(`\x1b[1m\x1b[35m┌──────────────────────────────────────────────────────────┐\x1b[0m`);
757
+ console.log(`\x1b[1m\x1b[35m│ 🚀 LEMMA PROXY v${VERSION} — THE UNIVERSAL AI GATEWAY │\x1b[0m`);
758
+ console.log(`\x1b[1m\x1b[35m└──────────────────────────────────────────────────────────┘\x1b[0m`);
759
+ console.log(`\n📁 Project : ${this.projectName}\n🔌 Port : ${this.port}`);
760
+ console.log('\n🧠 Intelligence Report');
761
+ console.log('────────────────────────────────────────────────');
762
+ console.log(`🔒 Privacy Firewall : \x1b[32m[ACTIVE]\x1b[0m`);
763
+ console.log(`🔀 Complexity Router : \x1b[32m[ACTIVE]\x1b[0m`);
764
+ console.log(`💾 Exact Cache : \x1b[32m[ACTIVE]\x1b[0m`);
765
+ const ollamaOk = await checkDependency('http://localhost:11434/api/tags');
766
+ const chromaOk = await checkDependency('http://localhost:8000/');
767
+ if (pro) {
768
+ console.log(`🎯 Semantic Cache : ${ollamaOk && chromaOk ? '\x1b[32m[ACTIVE]\x1b[0m' : '\x1b[33m[OFFLINE - Check Ollama/Chroma]\x1b[0m'}`);
769
+ console.log(`🌐 Hive Mind (Cloud) : \x1b[32m[ACTIVE]\x1b[0m`);
770
+ }
771
+ else {
772
+ console.log(`🎯 Semantic Cache : \x1b[90m[PRO ONLY]\x1b[0m -> https://lemma.nxus.studio/upgrade`);
773
+ console.log(`🌐 Hive Mind (Cloud) : \x1b[90m[PRO ONLY]\x1b[0m -> https://lemma.nxus.studio/upgrade`);
774
+ }
775
+ const hasStackDir = fs_1.default.existsSync(path_1.default.join(process.cwd(), '.lemma'));
776
+ console.log(`📡 Telepathic Sync : ${hasStackDir ? '\x1b[32m[READY]\x1b[0m' : '\x1b[90m[DISABLED - Run "lemma init"]\x1b[0m'}`);
777
+ console.log('────────────────────────────────────────────────\n');
778
+ if (!pro) {
779
+ console.log('\x1b[36m💡 Unlock Semantic Search & Team Caching at https://lemma.nxus.studio/upgrade\x1b[0m\n');
780
+ }
781
+ console.log('\x1b[35m✨ Zero-Config IDE Integration (MCP)\x1b[0m');
782
+ console.log('────────────────────────────────────────────────');
783
+ console.log('To use Lemma as a Tool in Cursor or Claude Desktop,');
784
+ console.log('simply copy-paste this command into your MCP settings:');
785
+ console.log(`\n \x1b[1m\x1b[33mlemma mcp\x1b[0m\n`);
786
+ console.log('────────────────────────────────────────────────\n');
787
+ });
788
+ process.on('SIGTERM', () => server.close());
789
+ }
790
+ }
791
+ // ── CLI ────────────────────────────────────────────────────────────────────────
792
+ commander_1.program.name('lemma').description('Lemma Proxy CLI — Intelligent AI Gateway').version(VERSION);
793
+ commander_1.program.command('mcp')
794
+ .description('Start the Lemma MCP Server (Model Context Protocol)')
795
+ .action(() => {
796
+ const { spawn } = require('child_process');
797
+ const mcpPath = path_1.default.join(__dirname, 'src', 'mcp', 'index.ts');
798
+ const mcpDistPath = path_1.default.join(__dirname, 'dist', 'cjs', 'mcp', 'index.js');
799
+ let target = mcpPath;
800
+ let cmd = 'npx';
801
+ let args = ['tsx', mcpPath];
802
+ if (!fs_1.default.existsSync(mcpPath) && fs_1.default.existsSync(mcpDistPath)) {
803
+ target = mcpDistPath;
804
+ cmd = 'node';
805
+ args = [mcpDistPath];
806
+ }
807
+ const mcp = spawn(cmd, args, { stdio: 'inherit' });
808
+ mcp.on('exit', (code) => process.exit(code || 0));
809
+ });
810
+ commander_1.program.command('start')
811
+ .description('Start the proxy server')
812
+ .option('-p, --port <number>', 'Port to listen on', '8081')
813
+ .option('--project <name>', 'Override project name')
814
+ .option('--stack', 'Launch full development stack (Chroma + Router + Dashboard + Autopilot)')
815
+ .option('--autopilot', 'Launch background compiler watcher & healer', false)
816
+ .action(async (opts) => {
817
+ const port = parseInt(opts.port, 10);
818
+ try {
819
+ const resp = await axios_1.default.get(`http://localhost:${port}/health`, { timeout: 500 });
820
+ if (resp.data && resp.data.status === 'ok') {
821
+ console.log(`\n⚠️ Lemma Proxy is already running on port ${port} (Project: ${resp.data.project})`);
822
+ console.log(`💡 Use 'lemma status' for details or 'lemma stop' to restart.\n`);
823
+ process.exit(0);
824
+ }
825
+ }
826
+ catch { }
827
+ // Automatically enable autopilot if stack is enabled and user is Pro
828
+ const runAutopilot = opts.autopilot || opts.stack;
829
+ if (runAutopilot) {
830
+ try {
831
+ const pro = await isPro();
832
+ if (!pro) {
833
+ if (opts.autopilot) {
834
+ console.log(`\n🛸 \x1b[35m[Autopilot]\x1b[0m Active background auto-healing watcher is a Lemma Pro feature.`);
835
+ console.log(`💡 Upgrade to Pro in your Dashboard or visit: \x1b[4mhttps://lemma.nxus.studio/upgrade\x1b[0m\n`);
836
+ }
837
+ else {
838
+ console.log(`\n🛸 \x1b[35m[Autopilot]\x1b[0m Skipping Autopilot background watcher launch (exclusive to Lemma Pro).`);
839
+ }
840
+ }
841
+ else {
842
+ const { AutopilotWatcher } = require('../autopilot/AutopilotWatcher');
843
+ const watcher = new AutopilotWatcher('src');
844
+ watcher.start();
845
+ }
846
+ }
847
+ catch (err) {
848
+ console.log(`⚠️ Could not initialize Autopilot: ${err.message}`);
849
+ }
850
+ }
851
+ if (opts.stack) {
852
+ const { spawn } = require('child_process');
853
+ console.log('🚀 Launching Lemma Full Stack...');
854
+ const chromaProcess = spawn('chroma', ['run', '--path', './chroma_data'], { stdio: 'ignore', detached: true });
855
+ chromaProcess.unref();
856
+ console.log(' ✅ ChromaDB requested');
857
+ const dash = spawn('npm', ['run', 'dev', '--prefix', 'dashboard', '--', '--port', '8082'], { stdio: 'ignore', detached: true });
858
+ dash.unref();
859
+ console.log(' ✅ Dashboard requested (http://localhost:8082)');
860
+ console.log(' 🚀 Starting Router & Proxy...\n');
861
+ const stackScript = fs_1.default.existsSync(path_1.default.join(__dirname, 'dist', 'cjs', 'cli', 'stack.js'))
862
+ ? path_1.default.join(__dirname, 'dist', 'cjs', 'cli', 'stack.js')
863
+ : (fs_1.default.existsSync(path_1.default.join(__dirname, 'src', 'cli', 'stack.ts')) ? 'src/cli/stack.ts' : null);
864
+ if (!stackScript) {
865
+ console.error('❌ Error: Could not find Lemma Stack script. Reinstall the package.');
866
+ process.exit(1);
867
+ }
868
+ const routerCmd = stackScript.endsWith('.ts') ? ['npx', 'tsx', stackScript] : ['node', stackScript];
869
+ const router = spawn(routerCmd[0], routerCmd.slice(1), {
870
+ stdio: 'inherit',
871
+ env: { ...process.env, WS_PORT: '8080', DASHBOARD_API_PORT: '8083' }
872
+ });
873
+ const server = new LemmaServer(port, opts.project || null);
874
+ await server.start();
875
+ router.on('exit', (code) => {
876
+ console.log(`\n🛑 Router exited with code ${code}`);
877
+ process.exit(code || 0);
878
+ });
879
+ }
880
+ else {
881
+ const server = new LemmaServer(port, opts.project || null);
882
+ await server.start();
883
+ }
884
+ });
885
+ commander_1.program.command('stop')
886
+ .description('Stop the proxy server')
887
+ .option('-p, --port <number>', 'Port to check', '8081')
888
+ .action(async (opts) => {
889
+ const port = parseInt(opts.port, 10);
890
+ let stopped = false;
891
+ try {
892
+ if (fs_1.default.existsSync(PID_FILE)) {
893
+ const pid = fs_1.default.readFileSync(PID_FILE, 'utf8');
894
+ try {
895
+ process.kill(parseInt(pid), 'SIGTERM');
896
+ stopped = true;
897
+ }
898
+ catch { }
899
+ }
900
+ if (!stopped) {
901
+ const pidByPort = getPidByPort(port);
902
+ if (pidByPort) {
903
+ process.kill(parseInt(pidByPort), 'SIGTERM');
904
+ console.log(`✅ Stopped process ${pidByPort} on port ${port}`);
905
+ stopped = true;
906
+ }
907
+ }
908
+ if (stopped) {
909
+ console.log('✅ Stopped');
910
+ }
911
+ else {
912
+ console.log('⚠️ No running process found.');
913
+ }
914
+ }
915
+ catch {
916
+ console.log('⚠️ Could not stop process.');
917
+ }
918
+ if (fs_1.default.existsSync(PID_FILE))
919
+ fs_1.default.unlinkSync(PID_FILE);
920
+ });
921
+ commander_1.program.command('status')
922
+ .description('Check proxy status')
923
+ .action(async () => {
924
+ const port = fs_1.default.existsSync(PORT_FILE) ? fs_1.default.readFileSync(PORT_FILE, 'utf8') : '8081';
925
+ try {
926
+ const resp = await axios_1.default.get(`http://localhost:${port}/health`, { timeout: 1000 });
927
+ if (resp.data && resp.data.status === 'ok') {
928
+ console.log(`✅ Running (Project: ${resp.data.project}, Port: ${port})`);
929
+ }
930
+ else {
931
+ console.log('❌ Stopped (Health check failed)');
932
+ }
933
+ }
934
+ catch {
935
+ console.log('❌ Stopped');
936
+ if (fs_1.default.existsSync(PID_FILE))
937
+ fs_1.default.unlinkSync(PID_FILE);
938
+ }
939
+ });
940
+ commander_1.program.command('stats')
941
+ .description('Show usage statistics')
942
+ .action(async () => {
943
+ const stats = await loadStats();
944
+ const usage = await loadUsage();
945
+ const project = detectProject();
946
+ const s = stats[project] || { total: 0, hits: 0, misses: 0, totalLatency: 0, totalTokensSaved: 0 };
947
+ console.log(`\n📊 Lemma Stats for [${project}]`);
948
+ console.log(`──────────────────────────────────`);
949
+ console.log(`Total Requests : ${s.total}`);
950
+ console.log(`Cache Hits : ${s.hits} (${((s.hits / s.total || 0) * 100).toFixed(1)}%)`);
951
+ console.log(`Tokens Saved : ${s.totalTokensSaved.toLocaleString()}`);
952
+ console.log(`Avg Latency : ${(s.totalLatency / (s.total || 1)).toFixed(2)}ms`);
953
+ console.log(`Tier : ${(await isPro()) ? 'PRO' : 'FREE'}`);
954
+ if (!await isPro())
955
+ console.log(`Free Usage : ${usage.count}/${FREE_LIMIT} requests`);
956
+ console.log(`──────────────────────────────────\n`);
957
+ });
958
+ commander_1.program.command('activate <key>')
959
+ .description('Activate Pro features with a license key')
960
+ .action(async (key) => {
961
+ console.log('🔑 Validating license key...');
962
+ try {
963
+ const res = await validateKeyRemote(key);
964
+ if (res.valid) {
965
+ await writeJson(LICENSE_FILE, { key, isPro: true, activatedAt: new Date().toISOString() });
966
+ console.log('✅ License activated! Enjoy Pro features.');
967
+ }
968
+ else {
969
+ console.log('❌ Invalid license key.');
970
+ }
971
+ }
972
+ catch (e) {
973
+ console.error('❌ Error validating key:', e.message);
974
+ }
975
+ });
976
+ commander_1.program.command('init')
977
+ .description('Initialize Lemma in the current project (auto-discovery)')
978
+ .action(() => {
979
+ const project = detectProject();
980
+ console.log(`\n🛠️ Initializing Lemma for [${project}]...`);
981
+ const lemmaDir = path_1.default.join(process.cwd(), '.lemma');
982
+ if (!fs_1.default.existsSync(lemmaDir)) {
983
+ fs_1.default.mkdirSync(lemmaDir, { recursive: true });
984
+ console.log('✅ Created .lemma directory (Telepathic Sync enabled)');
985
+ }
986
+ if (ensureGitIgnore()) {
987
+ console.log('✅ Added .lemma/ to .gitignore');
988
+ }
989
+ const envFile = path_1.default.join(process.cwd(), '.env');
990
+ const lemmaConfig = `\n# Lemma AI Gateway Configuration\nOPENAI_BASE_URL=http://localhost:8081/v1\nLEMMA_PROJECT=${project}\n`;
991
+ if (fs_1.default.existsSync(envFile)) {
992
+ const content = fs_1.default.readFileSync(envFile, 'utf8');
993
+ if (content.includes('OPENAI_BASE_URL')) {
994
+ console.log('⚠️ OPENAI_BASE_URL already exists in .env. Update it to: http://localhost:8081/v1');
995
+ }
996
+ else {
997
+ fs_1.default.appendFileSync(envFile, lemmaConfig);
998
+ console.log('✅ Added Lemma configuration to .env');
999
+ }
1000
+ }
1001
+ else {
1002
+ fs_1.default.writeFileSync(envFile, lemmaConfig);
1003
+ console.log('✅ Created .env with Lemma configuration');
1004
+ }
1005
+ console.log('\n🧠 \x1b[35mAuto-Setup MCP for AI IDEs?\x1b[0m');
1006
+ console.log(' Lemma can automatically configure Claude Desktop and open settings for Cursor/Windsurf.');
1007
+ const { exec } = require('child_process');
1008
+ const isWin = process.platform === 'win32';
1009
+ const claudePath = isWin
1010
+ ? path_1.default.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json')
1011
+ : path_1.default.join(process.env.HOME || '~', 'Library/Application Support/Claude/claude_desktop_config.json');
1012
+ if (fs_1.default.existsSync(path_1.default.dirname(claudePath))) {
1013
+ try {
1014
+ let config = { mcpServers: {} };
1015
+ if (fs_1.default.existsSync(claudePath)) {
1016
+ config = JSON.parse(fs_1.default.readFileSync(claudePath, 'utf8'));
1017
+ }
1018
+ if (!config.mcpServers)
1019
+ config.mcpServers = {};
1020
+ config.mcpServers.lemma = {
1021
+ command: 'npx',
1022
+ args: ['-y', '@nxuss/lemma', 'mcp'],
1023
+ env: { LEMMA_PROJECT: project }
1024
+ };
1025
+ fs_1.default.writeFileSync(claudePath, JSON.stringify(config, null, 2));
1026
+ console.log(`✅ \x1b[32mClaude Desktop configured automatically!\x1b[0m (Restart Claude to activate)`);
1027
+ }
1028
+ catch (e) {
1029
+ console.log('⚠️ Could not auto-configure Claude Desktop. No worries, you can do it manually.');
1030
+ }
1031
+ }
1032
+ console.log('🚀 Opening IDE configuration pages...');
1033
+ const urls = [
1034
+ 'https://docs.anthropic.com/en/docs/agents-and-tools/mcp#using-mcp-in-claude-desktop',
1035
+ 'https://cursor.com',
1036
+ 'https://codeium.com/windsurf'
1037
+ ];
1038
+ const opener = process.platform === 'darwin' ? 'open' : (process.platform === 'win32' ? 'start' : 'xdg-open');
1039
+ urls.forEach(url => exec(`${opener} ${url}`));
1040
+ console.log('\n✨ Project initialized for the Agentic Era!');
1041
+ console.log('🚀 Run "lemma start" to begin.\n');
1042
+ });
1043
+ // ── Auto-Healing Diagnostics (Zero-Cost Suite) ───────────────────────────────
1044
+ async function performAutoHeal(apply) {
1045
+ const contextPath = path_1.default.join(process.cwd(), '.lemma', 'live-context.md');
1046
+ if (!fs_1.default.existsSync(contextPath)) {
1047
+ return { success: false, message: 'No crash report found in .lemma/live-context.md. Run "lemma init" or make sure your server has crashed first.' };
1048
+ }
1049
+ const md = fs_1.default.readFileSync(contextPath, 'utf8');
1050
+ // Extract file path and error from markdown
1051
+ const fileRegex = /((?:\/[a-zA-Z0-9._-]+)+):(\d+):(\d+)/;
1052
+ const match = md.match(fileRegex) || md.match(/in\s+([^\s:]+\.[a-zA-Z0-9]+):(\d+)/);
1053
+ let targetFile = '';
1054
+ let lineNum = 0;
1055
+ if (match) {
1056
+ targetFile = match[1];
1057
+ lineNum = parseInt(match[2], 10);
1058
+ }
1059
+ // Clean target file path to make it absolute or relative to project
1060
+ if (targetFile && !path_1.default.isAbsolute(targetFile)) {
1061
+ targetFile = path_1.default.resolve(process.cwd(), targetFile);
1062
+ }
1063
+ else if (!targetFile) {
1064
+ // If not found in stack trace format, parse by search
1065
+ const fileSearch = md.match(/Message:\s+.*\s+in\s+([^\n]+)/);
1066
+ if (fileSearch) {
1067
+ targetFile = path_1.default.resolve(process.cwd(), fileSearch[1].trim());
1068
+ }
1069
+ }
1070
+ if (!targetFile || !fs_1.default.existsSync(targetFile)) {
1071
+ return { success: false, message: `Could not reliably locate the crashed file in the stack trace. Extracted file path: ${targetFile || 'None'}` };
1072
+ }
1073
+ const fileContent = fs_1.default.readFileSync(targetFile, 'utf8');
1074
+ const errorMsg = md.match(/Message:\s+([^\n]+)/)?.[1] || 'Unknown Runtime Error';
1075
+ console.log(`\n🩺 \x1b[36mDiagnosing crash in:\x1b[0m ${path_1.default.basename(targetFile)}:line ${lineNum}`);
1076
+ console.log(`❌ \x1b[31mError message:\x1b[0m ${errorMsg}`);
1077
+ // Generate prompt for the LLM
1078
+ const prompt = `You are Lemma's Auto-Healing Diagnostics Agent.
1079
+ We detected a runtime crash in the local codebase.
1080
+ File: ${targetFile}
1081
+ Line of failure: ${lineNum}
1082
+ Error: ${errorMsg}
1083
+
1084
+ Here is the context of the crash log:
1085
+ \`\`\`markdown
1086
+ ${md}
1087
+ \`\`\`
1088
+
1089
+ Here is the source code of the file causing the crash:
1090
+ \`\`\`typescript
1091
+ ${fileContent}
1092
+ \`\`\`
1093
+
1094
+ Please analyze the crash and provide:
1095
+ 1. A brief 1-2 sentence explanation of why it crashed.
1096
+ 2. A drop-in replacement or patch for the file.
1097
+ Return your response in a clear JSON format with two keys:
1098
+ {
1099
+ "explanation": "Why it crashed",
1100
+ "fixedContent": "Complete fixed code of the file"
1101
+ }
1102
+ Ensure you return strictly JSON. Do not return any other text outside the JSON block.`;
1103
+ // Call local Ollama or the premium model
1104
+ console.log('🧠 Running AI Diagnostics...');
1105
+ let apiKey = process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GEMINI_API_KEY;
1106
+ let fixedJson = null;
1107
+ try {
1108
+ if (!apiKey) {
1109
+ // Attempt local Ollama first
1110
+ try {
1111
+ const resp = await axios_1.default.post('http://127.0.0.1:11434/api/generate', {
1112
+ model: 'llama3',
1113
+ prompt,
1114
+ stream: false,
1115
+ format: 'json'
1116
+ }, { timeout: 3000 });
1117
+ fixedJson = JSON.parse(resp.data.response);
1118
+ }
1119
+ catch (ollamaErr) {
1120
+ // Local Ollama is offline! Fall back to expert static heuristic fixer!
1121
+ console.log('💡 Ollama offline. Activating Offline Heuristic Auto-Healer Fallback...');
1122
+ // Let's deduce the fix based on the file content and crash details
1123
+ let healedCode = fileContent;
1124
+ let healedExplanation = "Offline Heuristic Auto-Heal";
1125
+ if (errorMsg.includes('assignable to type')) {
1126
+ // e.g. "Type 'string' is not assignable to type 'number'."
1127
+ const lines = fileContent.split('\n');
1128
+ const buggyLineIdx = lineNum - 1;
1129
+ if (buggyLineIdx >= 0 && buggyLineIdx < lines.length) {
1130
+ const buggyLine = lines[buggyLineIdx];
1131
+ if (buggyLine.includes(': number') && buggyLine.includes('"')) {
1132
+ lines[buggyLineIdx] = buggyLine.replace(': number', ': string');
1133
+ healedCode = lines.join('\n');
1134
+ healedExplanation = "Fixed TypeScript type mismatch: changed number to string annotation.";
1135
+ }
1136
+ else if (buggyLine.includes(': string')) {
1137
+ lines[buggyLineIdx] = buggyLine.replace(': string', ': any');
1138
+ healedCode = lines.join('\n');
1139
+ healedExplanation = "Fixed TypeScript type mismatch: changed string to any annotation.";
1140
+ }
1141
+ else {
1142
+ lines[buggyLineIdx] = buggyLine.replace(': number', ': any');
1143
+ healedCode = lines.join('\n');
1144
+ healedExplanation = "Fixed TypeScript type mismatch: changed number to any annotation.";
1145
+ }
1146
+ }
1147
+ }
1148
+ else if (errorMsg.includes('unbalanced') || errorMsg.includes('curly braces')) {
1149
+ // Add missing brace at end of file
1150
+ healedCode = fileContent.trim() + '\n}\n';
1151
+ healedExplanation = "Balanced unclosed curly brace syntax at end of file.";
1152
+ }
1153
+ else {
1154
+ healedCode = fileContent;
1155
+ healedExplanation = "Offline check completed. Please activate Ollama or add an API key for deep analysis.";
1156
+ }
1157
+ fixedJson = {
1158
+ explanation: healedExplanation,
1159
+ fixedContent: healedCode
1160
+ };
1161
+ }
1162
+ }
1163
+ else {
1164
+ // Use OpenAI/Anthropic
1165
+ const resp = await axios_1.default.post('https://api.openai.com/v1/chat/completions', {
1166
+ model: 'gpt-4o',
1167
+ messages: [{ role: 'user', content: prompt }],
1168
+ response_format: { type: 'json_object' }
1169
+ }, {
1170
+ headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
1171
+ timeout: 45000
1172
+ });
1173
+ fixedJson = JSON.parse(resp.data.choices[0].message.content);
1174
+ }
1175
+ }
1176
+ catch (e) {
1177
+ return { success: false, message: `AI Diagnostics failed: ${e.message}. Ensure Ollama is running or API key is set.` };
1178
+ }
1179
+ if (!fixedJson || !fixedJson.fixedContent) {
1180
+ return { success: false, message: 'AI returned an invalid JSON response structure.' };
1181
+ }
1182
+ const fixedContent = fixedJson.fixedContent;
1183
+ const explanation = fixedJson.explanation;
1184
+ console.log(`\n💡 \x1b[32mDiagnostics:\x1b[0m ${explanation}\n`);
1185
+ if (apply) {
1186
+ fs_1.default.writeFileSync(targetFile, fixedContent, 'utf8');
1187
+ console.log(`\x1b[32m✔ Successfully applied fix to:\x1b[0m ${targetFile}`);
1188
+ return { success: true, message: `Successfully healed file: ${explanation}`, filePath: targetFile };
1189
+ }
1190
+ else {
1191
+ // Generate a simple diff
1192
+ const tempFile = targetFile + '.fixed';
1193
+ fs_1.default.writeFileSync(tempFile, fixedContent, 'utf8');
1194
+ try {
1195
+ const { execSync } = require('child_process');
1196
+ const diff = execSync(`diff -u "${targetFile}" "${tempFile}"`).toString();
1197
+ fs_1.default.unlinkSync(tempFile);
1198
+ return { success: true, message: explanation, patch: diff, filePath: targetFile, fileBefore: fileContent, fileAfter: fixedContent };
1199
+ }
1200
+ catch (e) {
1201
+ if (e.stdout) {
1202
+ fs_1.default.unlinkSync(tempFile);
1203
+ return { success: true, message: explanation, patch: e.stdout.toString(), filePath: targetFile, fileBefore: fileContent, fileAfter: fixedContent };
1204
+ }
1205
+ fs_1.default.unlinkSync(tempFile);
1206
+ return { success: true, message: explanation, filePath: targetFile, fileBefore: fileContent, fileAfter: fixedContent };
1207
+ }
1208
+ }
1209
+ }
1210
+ commander_1.program.command('heal')
1211
+ .description('Diagnose and auto-heal the latest local server crash from .lemma/live-context.md')
1212
+ .option('--apply', 'Apply the auto-generated code fix automatically', false)
1213
+ .action(async (opts) => {
1214
+ try {
1215
+ const pro = await isPro();
1216
+ if (!pro) {
1217
+ console.log(`\n🩺 \x1b[35m[Heal]\x1b[0m Guided code diagnostics and auto-healing is a Lemma Pro feature.`);
1218
+ console.log(`💡 Upgrade to Pro in your Dashboard or visit: \x1b[4mhttps://lemma.nxus.studio/upgrade\x1b[0m\n`);
1219
+ process.exit(0);
1220
+ }
1221
+ const res = await performAutoHeal(opts.apply);
1222
+ if (!res.success) {
1223
+ console.error(`\n❌ Error: ${res.message}\n`);
1224
+ process.exit(1);
1225
+ }
1226
+ if (!opts.apply && res.patch) {
1227
+ console.log('📝 \x1b[35mProposed Code Fix Patch:\x1b[0m');
1228
+ console.log('────────────────────────────────────────────────');
1229
+ console.log(res.patch);
1230
+ console.log('────────────────────────────────────────────────');
1231
+ console.log('💡 Run \x1b[1m\x1b[33mlemma heal --apply\x1b[0m to apply this fix automatically.\n');
1232
+ }
1233
+ }
1234
+ catch (err) {
1235
+ console.error('❌ Exception during healing:', err.message);
1236
+ process.exit(1);
1237
+ }
1238
+ });
1239
+ commander_1.program.command('autopilot')
1240
+ .description('Start the autonomous background compiler watcher & active healer')
1241
+ .option('--dir <directory>', 'Directory to monitor for changes', 'src')
1242
+ .action(async (opts) => {
1243
+ try {
1244
+ const pro = await isPro();
1245
+ if (!pro) {
1246
+ console.log(`\n🛸 \x1b[35m[Autopilot]\x1b[0m Active background auto-healing watcher is a Lemma Pro feature.`);
1247
+ console.log(`💡 Upgrade to Pro in your Dashboard or visit: \x1b[4mhttps://lemma.nxus.studio/upgrade\x1b[0m\n`);
1248
+ process.exit(0);
1249
+ }
1250
+ const { AutopilotWatcher } = require('../autopilot/AutopilotWatcher');
1251
+ const watcher = new AutopilotWatcher(opts.dir);
1252
+ watcher.start();
1253
+ // Keep process alive
1254
+ process.stdin.resume();
1255
+ process.on('SIGINT', () => {
1256
+ watcher.stop();
1257
+ process.exit(0);
1258
+ });
1259
+ }
1260
+ catch (err) {
1261
+ console.error('❌ Exception starting Autopilot:', err.message);
1262
+ process.exit(1);
1263
+ }
1264
+ });
1265
+ if (process.argv[1] && (process.argv[1].includes('lemma') || process.argv[1].endsWith('.cjs') || process.argv[1].endsWith('.js'))) {
1266
+ commander_1.program.parse(process.argv);
1267
+ }
1268
+ //# sourceMappingURL=lemma-proxy.js.map