@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
package/lemma-proxy.cjs CHANGED
@@ -1,748 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- /**
4
- * Lemma Proxy — Universal AI Cache CLI
5
- * Commands: start, stop, stats, status, activate <key>
6
- */
7
- const pkg = require('./package.json');
8
- const VERSION = pkg.version;
9
3
 
10
- const { program } = require('commander');
11
- const express = require('express');
12
4
  const fs = require('fs');
13
5
  const path = require('path');
14
- const crypto = require('crypto');
15
- const { ChromaClient } = require('chromadb');
16
- const chroma = new ChromaClient({ host: 'localhost', port: 8000 });
17
- const dummyEmbeddingFunction = { generate: (texts) => Promise.resolve([]) };
18
- const https = require('https');
19
- const http = require('http');
20
- const axios = require('axios');
21
- const WebSocket = require('ws');
22
6
 
23
- // --- Killer Features ---
24
- const ComplexityRouter = require('./src/proxy/ComplexityRouter.js');
25
- const SemanticScrubber = require('./src/security/SemanticScrubber.js');
26
- const CloudSyncClient = require('./src/cloud/CloudSyncClient.js');
27
-
28
- const complexityRouter = new ComplexityRouter();
29
- const semanticScrubber = new SemanticScrubber();
30
- const cloudSync = new CloudSyncClient();
31
- // ── Paths ──────────────────────────────────────────────────────────────────────
32
- const HOME = process.env.HOME || process.env.USERPROFILE || '~';
33
- const CACHE_DIR = path.join(HOME, '.lemma-cache');
34
- const PID_FILE = path.join(CACHE_DIR, 'proxy.pid');
35
- const PORT_FILE = path.join(CACHE_DIR, 'proxy.port');
36
- const STATS_FILE = path.join(CACHE_DIR, 'stats.json');
37
- const USAGE_FILE = path.join(CACHE_DIR, 'usage.json');
38
- const LICENSE_FILE = path.join(CACHE_DIR, 'license.json');
39
-
40
- const EVENT_LOG = [];
41
- const MAX_EVENTS = 100;
42
-
43
- function logEvent(event) {
44
- const ev = { id: Math.random().toString(36).slice(2, 11), timestamp: Date.now(), ...event };
45
- EVENT_LOG.push(ev);
46
- if (EVENT_LOG.length > MAX_EVENTS) EVENT_LOG.shift();
47
- }
48
-
49
- const FREE_LIMIT = 300;
50
- const WARN_PCT = 0.8;
51
- const VALIDATE_URL = 'https://lemma.nxus.studio/api/v1/validate';
52
-
53
- if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
54
-
55
- // ── Helpers ────────────────────────────────────────────────────────────────────
56
- function readJson(file, fallback) {
57
- try { if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
58
- return fallback;
59
- }
60
- function writeJson(file, data) {
61
- try { fs.writeFileSync(file, JSON.stringify(data, null, 2)); } catch {}
62
- }
63
- function projectHash(name) {
64
- return crypto.createHash('sha1').update(name).digest('hex').slice(0, 12);
65
- }
66
- function detectProject() {
67
- const pkg = path.join(process.cwd(), 'package.json');
68
- if (fs.existsSync(pkg)) {
69
- try { const p = JSON.parse(fs.readFileSync(pkg, 'utf8')); if (p.name) return p.name; } catch {}
70
- }
71
- return path.basename(process.cwd());
72
- }
73
- function ensureProjectDir(name) {
74
- const dir = path.join(CACHE_DIR, projectHash(name));
75
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
76
- const marker = path.join(dir, 'project-name.txt');
77
- if (!fs.existsSync(marker)) fs.writeFileSync(marker, name);
78
- return dir;
79
- }
80
-
81
- function getPidByPort(port) {
82
- const { execSync } = require('child_process');
83
- try {
84
- if (process.platform === 'win32') {
85
- const out = execSync(`netstat -ano | findstr :${port}`).toString();
86
- const lines = out.split('\n').filter(l => l.includes('LISTENING'));
87
- if (lines.length > 0) return lines[0].trim().split(/\s+/).pop();
88
- } else {
89
- const out = execSync(`lsof -t -i :${port}`).toString().trim();
90
- return out.split('\n')[0]; // Take first if multiple
91
- }
92
- } catch { return null; }
93
- return null;
94
- }
95
-
96
- async function checkDependency(url) {
97
- try {
98
- const res = await axios.get(url, { timeout: 1000, validateStatus: () => true });
99
- return res.status >= 200 && res.status < 500;
100
- } catch { return false; }
101
- }
102
-
103
- function ensureGitIgnore() {
104
- const gi = path.join(process.cwd(), '.gitignore');
105
- const line = '.lemma/';
106
- try {
107
- if (fs.existsSync(gi)) {
108
- const content = fs.readFileSync(gi, 'utf8');
109
- if (!content.includes(line)) {
110
- fs.appendFileSync(gi, `\n# Lemma Context Logs\n${line}\n`);
111
- return true;
7
+ // Automatically load environment variables from the user's workspace using a zero-dependency parser
8
+ function loadEnvFile(filePath) {
9
+ if (fs.existsSync(filePath)) {
10
+ try {
11
+ const content = fs.readFileSync(filePath, 'utf8');
12
+ const lines = content.split('\n');
13
+ for (const line of lines) {
14
+ const cleanLine = line.trim();
15
+ if (!cleanLine || cleanLine.startsWith('#')) continue;
16
+ const idx = cleanLine.indexOf('=');
17
+ if (idx === -1) continue;
18
+ const key = cleanLine.substring(0, idx).trim();
19
+ let val = cleanLine.substring(idx + 1).trim();
20
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
21
+ val = val.substring(1, val.length - 1);
22
+ }
23
+ process.env[key] = val;
112
24
  }
113
- } else {
114
- fs.writeFileSync(gi, `# Lemma Context Logs\n${line}\n`);
115
- return true;
116
- }
117
- } catch {}
118
- return false;
119
- }
120
-
121
- // ── License helpers ────────────────────────────────────────────────────────────
122
- function loadLicense() {
123
- return readJson(LICENSE_FILE, { isPro: false });
124
- }
125
- function isPro() { const l = loadLicense(); return !!(l && l.isPro); }
126
-
127
- async function validateKeyRemote(key) {
128
- return new Promise((resolve, reject) => {
129
- const body = JSON.stringify({ key });
130
- const u = new URL(VALIDATE_URL);
131
- const req = https.request({ hostname: u.hostname, port: 443, path: u.pathname, method: 'POST',
132
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'User-Agent': `lemma-proxy/${VERSION}` }
133
- }, res => {
134
- let d = '';
135
- res.on('data', c => d += c);
136
- res.on('end', () => { try { resolve(JSON.parse(d)); } catch { reject(new Error('bad json')); } });
137
- });
138
- req.on('error', reject);
139
- const t = setTimeout(() => { req.destroy(); reject(new Error('timeout')); }, 5000);
140
- req.on('close', () => clearTimeout(t));
141
- req.write(body); req.end();
142
- });
143
- }
144
-
145
- // ── Usage helpers ──────────────────────────────────────────────────────────────
146
- function loadUsage() {
147
- const u = readJson(USAGE_FILE, { count: 0, lastReset: new Date().toISOString(), history: [] });
148
- const now = new Date(), last = new Date(u.lastReset || 0);
149
- if (now.getMonth() !== last.getMonth() || now.getFullYear() !== last.getFullYear()) {
150
- const fresh = { count: 0, lastReset: now.toISOString(), history: [...(u.history||[]), { month: last.toISOString().slice(0,7), count: u.count }] };
151
- writeJson(USAGE_FILE, fresh); return fresh;
25
+ } catch (e) {}
152
26
  }
153
- return u;
154
27
  }
155
28
 
156
- // ── Stats helpers ──────────────────────────────────────────────────────────────
157
- function loadStats() { return readJson(STATS_FILE, {}); }
158
- function recordStat(stats, project, fromCache, latencyMs, provider, tokensSaved) {
159
- if (!stats[project]) stats[project] = { total:0, hits:0, misses:0, totalLatency:0, totalTokensSaved:0, providers:{} };
160
- const s = stats[project];
161
- s.total++; s.totalLatency += latencyMs;
162
- fromCache ? s.hits++ : s.misses++;
163
- s.totalTokensSaved += (fromCache ? (tokensSaved||2000) : 0);
164
- if (!s.providers[provider]) s.providers[provider] = { hits:0, misses:0 };
165
- fromCache ? s.providers[provider].hits++ : s.providers[provider].misses++;
166
-
167
- logEvent({
168
- type: fromCache ? 'cache:hit' : 'cache:miss',
169
- project,
170
- latency: latencyMs,
171
- provider,
172
- tokens: fromCache ? (tokensSaved||2000) : 0
173
- });
174
-
175
- writeJson(STATS_FILE, stats);
176
- }
29
+ loadEnvFile(path.join(process.cwd(), '.env'));
30
+ loadEnvFile(path.join(process.cwd(), '.env.local'));
177
31
 
178
- // ── Cache (in-process) ─────────────────────────────────────────────────────────
179
- const CACHE = new Map();
180
- const TTL_MS = 86400000 * 7;
181
32
 
182
- function cacheKey(prompt) {
183
- return crypto.createHash('sha256').update(prompt.toLowerCase().trim()).digest('hex');
184
- }
33
+ // Target path in the compiled CommonJS distribution
34
+ const distPath = path.join(__dirname, 'dist', 'cjs', 'cli', 'lemma-proxy.js');
185
35
 
186
- // ── Semantic Search (Pro) ──────────────────────────────────────────────────────
187
- async function getEmbedding(text) {
36
+ if (fs.existsSync(distPath)) {
37
+ // Production mode: run the compiled CJS JavaScript binary
38
+ require(distPath);
39
+ } else {
40
+ // Development mode: run via ts-node directly from src/
188
41
  try {
189
- const resp = await axios.post('http://localhost:11434/api/embeddings', {
190
- model: 'nomic-embed-text',
191
- prompt: text
192
- }, { timeout: 5000 });
193
- return resp.data.embedding;
194
- } catch (e) {
195
- return null;
42
+ require('ts-node/register');
43
+ require('./src/cli/lemma-proxy.ts');
44
+ } catch (err) {
45
+ console.error('❌ Error: Could not launch Lemma. Please run "npm run build" to compile the package.');
46
+ process.exit(1);
196
47
  }
197
48
  }
198
-
199
- async function semanticGet(prompt) {
200
- if (!isPro()) return null;
201
- const emb = await getEmbedding(prompt);
202
- if (!emb) return null;
203
-
204
- try {
205
- const collection = await chroma.getOrCreateCollection({
206
- name: 'lemma-cache',
207
- embeddingFunction: dummyEmbeddingFunction,
208
- metadata: { "hnsw:space": "cosine" }
209
- });
210
- const res = await collection.query({
211
- queryEmbeddings: [emb],
212
- nResults: 1
213
- });
214
-
215
- if (!res.ids[0] || res.ids[0].length === 0) return null;
216
-
217
- const distance = res.distances[0][0];
218
- const similarity = Math.max(0, 1 - distance);
219
- const THRESHOLD = 0.7;
220
-
221
- if (similarity >= THRESHOLD) {
222
- const metadata = res.metadatas[0][0];
223
- return {
224
- data: JSON.parse(metadata.response),
225
- similarity,
226
- id: res.ids[0][0]
227
- };
228
- }
229
- } catch (e) {}
230
- return null;
231
- }
232
-
233
- async function semanticSet(prompt, data) {
234
- if (!isPro()) return;
235
- const emb = await getEmbedding(prompt);
236
- if (!emb) return;
237
-
238
- try {
239
- const collection = await chroma.getOrCreateCollection({
240
- name: 'lemma-cache',
241
- embeddingFunction: dummyEmbeddingFunction,
242
- metadata: { "hnsw:space": "cosine" }
243
- });
244
-
245
- await collection.add({
246
- ids: [crypto.randomUUID?.() || Math.random().toString(36).substring(7)],
247
- embeddings: [emb],
248
- metadatas: [{
249
- prompt,
250
- response: JSON.stringify(data),
251
- timestamp: Date.now()
252
- }],
253
- documents: [prompt]
254
- });
255
- } catch (e) {}
256
- }
257
-
258
- function cacheGet(prompt) {
259
- const k = cacheKey(prompt);
260
- const e = CACHE.get(k);
261
- if (!e) return null;
262
- if (e.ttl > 0 && Date.now() - e.createdAt > e.ttl) { CACHE.delete(k); return null; }
263
- return e;
264
- }
265
- function cacheSet(prompt, data) {
266
- CACHE.set(cacheKey(prompt), { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS });
267
- }
268
-
269
- // ── Prompt extraction ──────────────────────────────────────────────────────────
270
- function extractPrompt(body, provider) {
271
- const msgs = body.messages || [];
272
- if (provider === 'openai' || provider === 'anthropic') return msgs.map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content))).join('\n');
273
- if (provider === 'gemini') {
274
- const contents = body.contents || [];
275
- return contents.map(c => c.parts.map(p => p.text).join(' ')).join('\n');
276
- }
277
- return null;
278
- }
279
-
280
- // ── SSE helpers ────────────────────────────────────────────────────────────────
281
- function setSseHeaders(res, fromCache, similarity, tier) {
282
- res.setHeader('Content-Type', 'text/event-stream');
283
- res.setHeader('Cache-Control', 'no-cache');
284
- res.setHeader('Connection', 'keep-alive');
285
- res.setHeader('X-Accel-Buffering', 'no');
286
- res.setHeader('X-Lemma-Cache', fromCache ? 'HIT' : 'MISS');
287
- res.setHeader('X-Lemma-Similarity', (similarity || 1.0).toFixed(3));
288
- res.setHeader('X-Lemma-Tier', tier);
289
- }
290
-
291
- function buildOaiChunk(id, model, text, stop) {
292
- return { id, object:'chat.completion.chunk', created: Math.floor(Date.now()/1000), model,
293
- choices:[{ index:0, delta: stop ? {} : { content: text }, finish_reason: stop ? 'stop' : null }] };
294
- }
295
- function buildAntChunk(text, stop) {
296
- if (stop) return { type:'message_stop' };
297
- return { type:'content_block_delta', index:0, delta:{ type:'text_delta', text } };
298
- }
299
- function buildGemChunk(text, stop) {
300
- return { candidates: [{ content: { parts: [{ text }] }, finishReason: stop ? 'STOP' : undefined }] };
301
- }
302
-
303
- async function simulateHitStream(res, cachedData, provider, model, tier, similarity) {
304
- setSseHeaders(res, true, similarity, tier);
305
- const id = `lemma-${Date.now()}`;
306
- let text = '';
307
- try {
308
- if (provider === 'openai') text = cachedData?.choices?.[0]?.message?.content || '';
309
- else if (provider === 'anthropic') { const c = cachedData?.content; text = Array.isArray(c) ? c.map(x=>x.text||'').join('') : String(c||''); }
310
- else if (provider === 'gemini') { text = cachedData?.candidates?.[0]?.content?.parts?.[0]?.text || ''; }
311
- } catch {}
312
- const words = text.split(' ');
313
- for (let i = 0; i < words.length; i += 5) {
314
- const chunk = words.slice(i, i+5).join(' ');
315
- let evt;
316
- if (provider === 'openai') evt = buildOaiChunk(id, model, chunk, false);
317
- else if (provider === 'anthropic') evt = buildAntChunk(chunk, false);
318
- else if (provider === 'gemini') evt = buildGemChunk(chunk, false);
319
-
320
- res.write(provider === 'gemini' ? `${JSON.stringify(evt)}\n` : `data: ${JSON.stringify(evt)}\n\n`);
321
- await new Promise(r => setTimeout(r, 15));
322
- }
323
- const stopEvt = provider === 'openai' ? buildOaiChunk(id, model, '', true) : (provider === 'anthropic' ? buildAntChunk('', true) : buildGemChunk('', true));
324
- res.write(provider === 'gemini' ? `${JSON.stringify(stopEvt)}\n` : `data: ${JSON.stringify(stopEvt)}\n\n`);
325
- if (provider !== 'gemini') res.write('data: [DONE]\n\n');
326
- res.end();
327
- }
328
-
329
- async function relayMissStream(res, body, provider, tier) {
330
- setSseHeaders(res, false, 1.0, tier);
331
- const apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : process.env.ANTHROPIC_API_KEY;
332
- if (!apiKey) { res.write(`data: ${JSON.stringify({error:`${provider.toUpperCase()}_API_KEY not set`})}\n\ndata: [DONE]\n\n`); res.end(); return; }
333
- const hostname = provider === 'openai' ? 'api.openai.com' : 'api.anthropic.com';
334
- const upPath = provider === 'openai' ? '/v1/chat/completions' : '/v1/messages';
335
- const headers = provider === 'openai'
336
- ? { Authorization: `Bearer ${apiKey}`, 'Content-Type':'application/json' }
337
- : { 'x-api-key': apiKey, 'anthropic-version':'2023-06-01', 'Content-Type':'application/json' };
338
- const payload = JSON.stringify({ ...body, stream: true });
339
- await new Promise(resolve => {
340
- const req = https.request({ hostname, port:443, path:upPath, method:'POST',
341
- headers: { ...headers, 'Content-Length': Buffer.byteLength(payload) }
342
- }, upstream => {
343
- upstream.on('data', chunk => res.write(chunk));
344
- upstream.on('end', () => { res.write('data: [DONE]\n\n'); res.end(); resolve(); });
345
- upstream.on('error', err => { res.write(`data: ${JSON.stringify({error:err.message})}\n\ndata: [DONE]\n\n`); res.end(); resolve(); });
346
- });
347
- req.on('error', err => { res.write(`data: ${JSON.stringify({error:err.message})}\n\ndata: [DONE]\n\n`); res.end(); resolve(); });
348
- req.write(payload); req.end();
349
- });
350
- }
351
-
352
- // ── Proxy Server class ─────────────────────────────────────────────────────────
353
- class LemmaServer {
354
- constructor(port, projectName) {
355
- this.port = port;
356
- this.projectName = projectName || detectProject();
357
- this.projectDir = ensureProjectDir(this.projectName);
358
- this.stats = loadStats();
359
- this.app = express();
360
- this.setupMiddleware();
361
- this.setupRoutes();
362
- }
363
-
364
- setupMiddleware() {
365
- this.app.use(express.json({ limit: '10mb' }));
366
- this.app.use((req, res, next) => {
367
- res.header('Access-Control-Allow-Origin', '*');
368
- res.header('Access-Control-Allow-Headers', '*');
369
- res.header('Access-Control-Allow-Methods', '*');
370
- if (req.method === 'OPTIONS') return res.sendStatus(200);
371
- next();
372
- });
373
- }
374
-
375
- setupRoutes() {
376
- this.app.get('/health', (req, res) => {
377
- const pro = isPro();
378
- res.json({ status:'ok', project: this.projectName, mode: pro?'pro':'standard', tier: pro?'pro':'free', port: this.port });
379
- });
380
-
381
- this.app.get('/v1/models', (req, res) => {
382
- res.json({ object:'list', data:[
383
- { id:'gpt-4o', object:'model', owned_by:'lemma-proxy' },
384
- { id:'claude-3-5-sonnet-20241022', object:'model', owned_by:'lemma-proxy' }
385
- ]});
386
- });
387
-
388
- this.app.post('/v1/chat/completions', (req, res) => this.handleCompletion(req, res, 'openai'));
389
- this.app.post('/v1/messages', (req, res) => this.handleCompletion(req, res, 'anthropic'));
390
- this.app.post('/v1beta/models/:modelId:generateContent', (req, res) => this.handleCompletion(req, res, 'gemini'));
391
-
392
- // Dashboard APIs
393
- const dashboardPath = path.join(__dirname, 'dashboard', 'dist');
394
- if (fs.existsSync(dashboardPath)) {
395
- this.app.use('/dashboard/', express.static(dashboardPath));
396
- this.app.get('/api/metrics', (req, res) => {
397
- const stats = this.stats[this.projectName] || { total:0, hits:0, misses:0, totalLatency:0, totalTokensSaved:0 };
398
- res.json({ timestamp: new Date().toISOString(), totalRequests: stats.total, cacheHits: stats.hits, tokensSaved: stats.totalTokensSaved, tier: isPro()?'pro':'free' });
399
- });
400
- this.app.get('/dashboard/*', (req, res) => res.sendFile(path.join(dashboardPath, 'index.html')));
401
- }
402
- }
403
-
404
- async handleCompletion(req, res, provider) {
405
- const t0 = Date.now();
406
- const pro = isPro();
407
- const tier = pro ? 'pro' : 'standard';
408
- const isStream = !!req.body.stream;
409
-
410
- if (!pro) {
411
- const usage = loadUsage();
412
- if (usage.count >= FREE_LIMIT) return res.status(429).json({ error:{ message:`Free tier limit (${FREE_LIMIT}/mo). Upgrade: https://lemma.nxus.studio/upgrade` }});
413
- }
414
-
415
- const rawPrompt = extractPrompt(req.body, provider);
416
- if (!rawPrompt) return res.status(400).json({ error:'Invalid request format' });
417
-
418
- const { maskedPrompt: prompt, tokenMap } = semanticScrubber.mask(rawPrompt);
419
-
420
- const originalModel = req.body.model || 'gpt-4o';
421
- const routingDecision = complexityRouter.evaluate(prompt, originalModel);
422
- const model = routingDecision.model;
423
-
424
- if (req.body.model) req.body.model = model;
425
-
426
- if (provider === 'openai' || provider === 'anthropic') {
427
- if (req.body.messages && req.body.messages.length > 0) {
428
- req.body.messages[req.body.messages.length - 1].content = prompt;
429
- }
430
- } else if (provider === 'gemini') {
431
- if (req.body.contents && req.body.contents.length > 0) {
432
- const lastContent = req.body.contents[req.body.contents.length - 1];
433
- if (lastContent.parts && lastContent.parts.length > 0) {
434
- lastContent.parts[lastContent.parts.length - 1].text = prompt;
435
- }
436
- }
437
- }
438
-
439
- const hit = cacheGet(prompt);
440
- if (hit) {
441
- recordStat(this.stats, this.projectName, true, Date.now()-t0, provider, 2000);
442
- const unmaskedData = semanticScrubber.unmask(hit.data, tokenMap);
443
- if (isStream) return simulateHitStream(res, unmaskedData, provider, model, tier, 1.0);
444
-
445
- res.setHeader('X-Lemma-Cache', 'HIT');
446
- res.setHeader('X-Lemma-Similarity', '1.000');
447
- res.setHeader('X-Lemma-Tier', tier);
448
- return res.json(unmaskedData);
449
- }
450
-
451
- if (pro) {
452
- const semHit = await semanticGet(prompt);
453
- if (semHit) {
454
- recordStat(this.stats, this.projectName, true, Date.now()-t0, provider, 2000);
455
- const unmaskedData = semanticScrubber.unmask(semHit.data, tokenMap);
456
- if (isStream) return simulateHitStream(res, unmaskedData, provider, model, tier, semHit.similarity);
457
-
458
- res.setHeader('X-Lemma-Cache', 'HIT');
459
- res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
460
- res.setHeader('X-Lemma-Tier', tier);
461
- return res.json(unmaskedData);
462
- }
463
-
464
- const cloudHit = await cloudSync.get(prompt);
465
- if (cloudHit) {
466
- cacheSet(prompt, cloudHit.data);
467
- recordStat(this.stats, this.projectName, true, Date.now()-t0, provider, 2000);
468
- const unmaskedData = semanticScrubber.unmask(cloudHit.data, tokenMap);
469
- if (isStream) return simulateHitStream(res, unmaskedData, provider, model, tier, cloudHit.similarity || 0.95);
470
-
471
- res.setHeader('X-Lemma-Cache', 'HIT');
472
- res.setHeader('X-Lemma-Similarity', (cloudHit.similarity || 0.95).toFixed(3));
473
- res.setHeader('X-Lemma-Tier', tier);
474
- return res.json(unmaskedData);
475
- }
476
- }
477
-
478
- if (isStream) {
479
- recordStat(this.stats, this.projectName, false, Date.now()-t0, provider, 0);
480
- if (!pro) { const u = loadUsage(); u.count++; writeJson(USAGE_FILE, u); }
481
- return relayMissStream(res, req.body, provider, tier);
482
- }
483
-
484
- try {
485
- const data = await this.callUpstream(req.body, provider);
486
- cacheSet(prompt, data);
487
- if (pro) {
488
- semanticSet(prompt, data);
489
- cloudSync.set(prompt, data);
490
- }
491
- if (!pro) { const u = loadUsage(); u.count++; writeJson(USAGE_FILE, u); }
492
- recordStat(this.stats, this.projectName, false, Date.now()-t0, provider, 0);
493
- return res.json(semanticScrubber.unmask(data, tokenMap));
494
- } catch (err) {
495
- return res.status(500).json({ error: err.message });
496
- }
497
- }
498
-
499
- async callUpstream(body, provider) {
500
- const apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : (provider === 'anthropic' ? process.env.ANTHROPIC_API_KEY : process.env.GEMINI_API_KEY);
501
- 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}`);
502
- const headers = provider === 'openai' ? { Authorization:`Bearer ${apiKey}` } : (provider === 'anthropic' ? { 'x-api-key':apiKey, 'anthropic-version':'2023-06-01' } : {});
503
- const resp = await axios.post(baseURL, body, { headers: { ...headers, 'Content-Type':'application/json' } });
504
- return resp.data;
505
- }
506
-
507
- start() {
508
- const server = http.createServer(this.app);
509
- server.on('error', (e) => {
510
- if (e.code === 'EADDRINUSE') {
511
- console.error(`\n❌ Error: Port ${this.port} is already in use.`);
512
- console.error(`💡 Try stopping the existing process with 'lemma stop' or use a different port with '--port <number>'.\n`);
513
- process.exit(1);
514
- }
515
- throw e;
516
- });
517
- server.listen(this.port, async () => {
518
- fs.writeFileSync(PID_FILE, String(process.pid));
519
- fs.writeFileSync(PORT_FILE, String(this.port));
520
-
521
- const pro = isPro();
522
- console.log(`\n🚀 Lemma Proxy v${VERSION} — ${pro ? 'PRO' : 'STANDARD'}`);
523
- console.log(`📁 Project : ${this.projectName}\n🔌 Port : ${this.port}`);
524
-
525
- console.log('\n🧠 Intelligence Report');
526
- console.log('────────────────────────────────────────────────');
527
- console.log(`🔒 Privacy Firewall : \x1b[32m[ACTIVE]\x1b[0m`);
528
- console.log(`🔀 Complexity Router : \x1b[32m[ACTIVE]\x1b[0m`);
529
- console.log(`💾 Exact Cache : \x1b[32m[ACTIVE]\x1b[0m`);
530
-
531
- const ollamaOk = await checkDependency('http://localhost:11434/api/tags');
532
- const chromaOk = await checkDependency('http://localhost:8000/'); // Basic check for Chroma
533
-
534
- if (pro) {
535
- console.log(`🎯 Semantic Cache : ${ollamaOk && chromaOk ? '\x1b[32m[ACTIVE]\x1b[0m' : '\x1b[33m[OFFLINE - Check Ollama/Chroma]\x1b[0m'}`);
536
- console.log(`🌐 Hive Mind (Cloud) : \x1b[32m[ACTIVE]\x1b[0m`);
537
- } else {
538
- console.log(`🎯 Semantic Cache : \x1b[90m[PRO ONLY]\x1b[0m -> https://lemma.nxus.studio/upgrade`);
539
- console.log(`🌐 Hive Mind (Cloud) : \x1b[90m[PRO ONLY]\x1b[0m -> https://lemma.nxus.studio/upgrade`);
540
- }
541
-
542
- const hasStackDir = fs.existsSync(path.join(process.cwd(), '.lemma'));
543
- console.log(`📡 Telepathic Sync : ${hasStackDir ? '\x1b[32m[READY]\x1b[0m' : '\x1b[90m[DISABLED - Run "lemma init"]\x1b[0m'}`);
544
- console.log('────────────────────────────────────────────────\n');
545
-
546
- if (!pro) {
547
- console.log('\x1b[36m💡 Unlock Semantic Search & Team Caching at https://lemma.nxus.studio/upgrade\x1b[0m\n');
548
- }
549
- });
550
- process.on('SIGTERM', () => server.close());
551
- }
552
- }
553
-
554
- // ── CLI ────────────────────────────────────────────────────────────────────────
555
- program.name('lemma').description('Lemma Proxy CLI — Intelligent AI Gateway').version(VERSION);
556
-
557
- program.command('start')
558
- .description('Start the proxy server')
559
- .option('-p, --port <number>', 'Port to listen on', '8081')
560
- .option('--project <name>', 'Override project name')
561
- .option('--stack', 'Launch full development stack (Chroma + Router + Dashboard)')
562
- .action(async (opts) => {
563
- const port = parseInt(opts.port, 10);
564
-
565
- // Check if already running
566
- try {
567
- const resp = await axios.get(`http://localhost:${port}/health`, { timeout: 500 });
568
- if (resp.data && resp.data.status === 'ok') {
569
- console.log(`\n⚠️ Lemma Proxy is already running on port ${port} (Project: ${resp.data.project})`);
570
- console.log(`💡 Use 'lemma status' for details or 'lemma stop' to restart.\n`);
571
- process.exit(0);
572
- }
573
- } catch {}
574
-
575
- if (opts.stack) {
576
- const { spawn } = require('child_process');
577
- console.log('🚀 Launching Lemma Full Stack...');
578
-
579
- // 1. Start Chroma
580
- const chroma = spawn('chroma', ['run', '--path', './chroma_data'], { stdio: 'ignore', detached: true });
581
- chroma.unref();
582
- console.log(' ✅ ChromaDB requested');
583
-
584
- // 2. Start Dashboard
585
- // Use port 8082 to avoid conflicts
586
- const dash = spawn('npm', ['run', 'dev', '--prefix', 'dashboard', '--', '--port', '8082'], { stdio: 'ignore', detached: true });
587
- dash.unref();
588
- console.log(' ✅ Dashboard requested (http://localhost:8082)');
589
-
590
- // 3. Start Router (using internal script)
591
- console.log(' 🚀 Starting Router & Proxy...\n');
592
-
593
- // Fallback for dev/package mode
594
- const stackScript = fs.existsSync(path.join(__dirname, 'dist', 'cjs', 'cli', 'stack.js'))
595
- ? path.join(__dirname, 'dist', 'cjs', 'cli', 'stack.js')
596
- : (fs.existsSync(path.join(__dirname, 'src', 'cli', 'stack.ts')) ? 'src/cli/stack.ts' : null);
597
-
598
- if (!stackScript) {
599
- console.error('❌ Error: Could not find Lemma Stack script. Reinstall the package.');
600
- process.exit(1);
601
- }
602
-
603
- const routerCmd = stackScript.endsWith('.ts') ? ['npx', 'tsx', stackScript] : ['node', stackScript];
604
- const router = spawn(routerCmd[0], routerCmd.slice(1), {
605
- stdio: 'inherit',
606
- env: { ...process.env, WS_PORT: '8080', DASHBOARD_API_PORT: '8083' }
607
- });
608
-
609
- // Start Proxy Server alongside
610
- const server = new LemmaServer(port, opts.project || null);
611
- server.start();
612
-
613
- router.on('exit', (code) => {
614
- console.log(`\n🛑 Router exited with code ${code}`);
615
- process.exit(code || 0);
616
- });
617
- } else {
618
- const server = new LemmaServer(port, opts.project || null);
619
- server.start();
620
- }
621
- });
622
-
623
- program.command('stop')
624
- .description('Stop the proxy server')
625
- .option('-p, --port <number>', 'Port to check', '8081')
626
- .action((opts) => {
627
- const port = parseInt(opts.port, 10);
628
- let stopped = false;
629
- try {
630
- if (fs.existsSync(PID_FILE)) {
631
- const pid = fs.readFileSync(PID_FILE, 'utf8');
632
- try {
633
- process.kill(parseInt(pid), 'SIGTERM');
634
- stopped = true;
635
- } catch {}
636
- }
637
-
638
- if (!stopped) {
639
- const pidByPort = getPidByPort(port);
640
- if (pidByPort) {
641
- process.kill(parseInt(pidByPort), 'SIGTERM');
642
- console.log(`✅ Stopped process ${pidByPort} on port ${port}`);
643
- stopped = true;
644
- }
645
- }
646
-
647
- if (stopped) {
648
- console.log('✅ Stopped');
649
- } else {
650
- console.log('⚠️ No running process found.');
651
- }
652
- } catch { console.log('⚠️ Could not stop process.'); }
653
- if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
654
- });
655
-
656
- program.command('status')
657
- .description('Check proxy status')
658
- .action(async () => {
659
- const port = fs.existsSync(PORT_FILE) ? fs.readFileSync(PORT_FILE, 'utf8') : '8081';
660
- try {
661
- const resp = await axios.get(`http://localhost:${port}/health`, { timeout: 1000 });
662
- if (resp.data && resp.data.status === 'ok') {
663
- console.log(`✅ Running (Project: ${resp.data.project}, Port: ${port})`);
664
- } else {
665
- console.log('❌ Stopped (Health check failed)');
666
- }
667
- } catch {
668
- console.log('❌ Stopped');
669
- if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
670
- }
671
- });
672
-
673
- program.command('stats')
674
- .description('Show usage statistics')
675
- .action(() => {
676
- const stats = loadStats();
677
- const usage = loadUsage();
678
- const project = detectProject();
679
- const s = stats[project] || { total:0, hits:0, misses:0, totalLatency:0, totalTokensSaved:0 };
680
-
681
- console.log(`\n📊 Lemma Stats for [${project}]`);
682
- console.log(`──────────────────────────────────`);
683
- console.log(`Total Requests : ${s.total}`);
684
- console.log(`Cache Hits : ${s.hits} (${((s.hits/s.total||0)*100).toFixed(1)}%)`);
685
- console.log(`Tokens Saved : ${s.totalTokensSaved.toLocaleString()}`);
686
- console.log(`Avg Latency : ${(s.totalLatency/(s.total||1)).toFixed(2)}ms`);
687
- console.log(`Tier : ${isPro() ? 'PRO' : 'FREE'}`);
688
- if (!isPro()) console.log(`Free Usage : ${usage.count}/${FREE_LIMIT} requests`);
689
- console.log(`──────────────────────────────────\n`);
690
- });
691
-
692
- program.command('activate <key>')
693
- .description('Activate Pro features with a license key')
694
- .action(async (key) => {
695
- console.log('🔑 Validating license key...');
696
- try {
697
- const res = await validateKeyRemote(key);
698
- if (res.valid) {
699
- writeJson(LICENSE_FILE, { key, isPro: true, activatedAt: new Date().toISOString() });
700
- console.log('✅ License activated! Enjoy Pro features.');
701
- } else {
702
- console.log('❌ Invalid license key.');
703
- }
704
- } catch (e) {
705
- console.error('❌ Error validating key:', e.message);
706
- }
707
- });
708
-
709
- program.command('init')
710
- .description('Initialize Lemma in the current project (auto-discovery)')
711
- .action(() => {
712
- const project = detectProject();
713
- console.log(`\n🛠️ Initializing Lemma for [${project}]...`);
714
-
715
- // 1. Create .lemma directory
716
- const lemmaDir = path.join(process.cwd(), '.lemma');
717
- if (!fs.existsSync(lemmaDir)) {
718
- fs.mkdirSync(lemmaDir, { recursive: true });
719
- console.log('✅ Created .lemma directory (Telepathic Sync enabled)');
720
- }
721
-
722
- // 2. Update .gitignore
723
- if (ensureGitIgnore()) {
724
- console.log('✅ Added .lemma/ to .gitignore');
725
- }
726
-
727
- // 3. Update .env
728
- const envFile = path.join(process.cwd(), '.env');
729
- const lemmaConfig = `\n# Lemma AI Gateway Configuration\nOPENAI_BASE_URL=http://localhost:8081/v1\nLEMMA_PROJECT=${project}\n`;
730
-
731
- if (fs.existsSync(envFile)) {
732
- const content = fs.readFileSync(envFile, 'utf8');
733
- if (content.includes('OPENAI_BASE_URL')) {
734
- console.log('⚠️ OPENAI_BASE_URL already exists in .env. Update it to: http://localhost:8081/v1');
735
- } else {
736
- fs.appendFileSync(envFile, lemmaConfig);
737
- console.log('✅ Added Lemma configuration to .env');
738
- }
739
- } else {
740
- fs.writeFileSync(envFile, lemmaConfig);
741
- console.log('✅ Created .env with Lemma configuration');
742
- }
743
-
744
- console.log('\n✨ Project initialized for the Agentic Era!');
745
- console.log('🚀 Run "lemma start" to begin.\n');
746
- });
747
-
748
- program.parse(process.argv);