@nxuss/lemma 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/core/GhostAlchemist.d.ts +18 -0
- package/dist/cjs/core/GhostAlchemist.d.ts.map +1 -0
- package/dist/cjs/core/GhostAlchemist.js +55 -0
- package/dist/cjs/core/GhostAlchemist.js.map +1 -0
- package/dist/cjs/core/GhostListener.d.ts +20 -0
- package/dist/cjs/core/GhostListener.d.ts.map +1 -0
- package/dist/cjs/core/GhostListener.js +97 -0
- package/dist/cjs/core/GhostListener.js.map +1 -0
- package/dist/esm/core/GhostAlchemist.d.ts +18 -0
- package/dist/esm/core/GhostAlchemist.d.ts.map +1 -0
- package/dist/esm/core/GhostAlchemist.js +48 -0
- package/dist/esm/core/GhostAlchemist.js.map +1 -0
- package/dist/esm/core/GhostListener.d.ts +20 -0
- package/dist/esm/core/GhostListener.d.ts.map +1 -0
- package/dist/esm/core/GhostListener.js +93 -0
- package/dist/esm/core/GhostListener.js.map +1 -0
- package/lemma-proxy.cjs +477 -0
- package/package.json +9 -7
- package/lemma-proxy.js +0 -810
package/lemma-proxy.js
DELETED
|
@@ -1,810 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
/**
|
|
4
|
-
* Lemma Proxy v0.3.0 — Universal AI Cache CLI
|
|
5
|
-
* Commands: start, stop, stats, status, activate <key>
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { program } = require('commander');
|
|
9
|
-
const express = require('express');
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const crypto = require('crypto');
|
|
13
|
-
const { ChromaClient } = require('chromadb');
|
|
14
|
-
const chroma = new ChromaClient({ host: 'localhost', port: 8000 });
|
|
15
|
-
const dummyEmbeddingFunction = { generate: (texts) => Promise.resolve([]) };
|
|
16
|
-
const https = require('https');
|
|
17
|
-
const http = require('http');
|
|
18
|
-
const axios = require('axios');
|
|
19
|
-
const WebSocket = require('ws');
|
|
20
|
-
|
|
21
|
-
// ── Paths ──────────────────────────────────────────────────────────────────────
|
|
22
|
-
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
23
|
-
const CACHE_DIR = path.join(HOME, '.lemma-cache');
|
|
24
|
-
const PID_FILE = path.join(CACHE_DIR, 'proxy.pid');
|
|
25
|
-
const PORT_FILE = path.join(CACHE_DIR, 'proxy.port');
|
|
26
|
-
const STATS_FILE = path.join(CACHE_DIR, 'stats.json');
|
|
27
|
-
const USAGE_FILE = path.join(CACHE_DIR, 'usage.json');
|
|
28
|
-
const LICENSE_FILE = path.join(CACHE_DIR, 'license.json');
|
|
29
|
-
const LEGACY_LICENSE_FILE = path.join(CACHE_DIR, 'license.key');
|
|
30
|
-
|
|
31
|
-
const EVENT_LOG = [];
|
|
32
|
-
const MAX_EVENTS = 100;
|
|
33
|
-
|
|
34
|
-
function logEvent(event) {
|
|
35
|
-
const ev = { id: Math.random().toString(36).slice(2, 11), timestamp: Date.now(), ...event };
|
|
36
|
-
EVENT_LOG.push(ev);
|
|
37
|
-
if (EVENT_LOG.length > MAX_EVENTS) EVENT_LOG.shift();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const FREE_LIMIT = 300;
|
|
41
|
-
const WARN_PCT = 0.8;
|
|
42
|
-
const VALIDATE_URL = 'https://lemma.nxus.studio/api/v1/validate';
|
|
43
|
-
|
|
44
|
-
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
45
|
-
|
|
46
|
-
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
47
|
-
function readJson(file, fallback) {
|
|
48
|
-
try { if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
|
|
49
|
-
return fallback;
|
|
50
|
-
}
|
|
51
|
-
function writeJson(file, data) {
|
|
52
|
-
try { fs.writeFileSync(file, JSON.stringify(data, null, 2)); } catch {}
|
|
53
|
-
}
|
|
54
|
-
function projectHash(name) {
|
|
55
|
-
return crypto.createHash('sha1').update(name).digest('hex').slice(0, 12);
|
|
56
|
-
}
|
|
57
|
-
function detectProject() {
|
|
58
|
-
const pkg = path.join(process.cwd(), 'package.json');
|
|
59
|
-
if (fs.existsSync(pkg)) {
|
|
60
|
-
try { const p = JSON.parse(fs.readFileSync(pkg, 'utf8')); if (p.name) return p.name; } catch {}
|
|
61
|
-
}
|
|
62
|
-
return path.basename(process.cwd());
|
|
63
|
-
}
|
|
64
|
-
function ensureProjectDir(name) {
|
|
65
|
-
const dir = path.join(CACHE_DIR, projectHash(name));
|
|
66
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
67
|
-
const marker = path.join(dir, 'project-name.txt');
|
|
68
|
-
if (!fs.existsSync(marker)) fs.writeFileSync(marker, name);
|
|
69
|
-
return dir;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── License helpers ────────────────────────────────────────────────────────────
|
|
73
|
-
function loadLicense() {
|
|
74
|
-
let lic = readJson(LICENSE_FILE, null);
|
|
75
|
-
if (lic && lic.isPro) return lic;
|
|
76
|
-
// migrate legacy
|
|
77
|
-
const leg = readJson(LEGACY_LICENSE_FILE, null);
|
|
78
|
-
if (leg && (leg.isPro || leg.tier === 'pro')) {
|
|
79
|
-
const m = { key: leg.key, tier: 'pro', isPro: true, activatedAt: leg.activatedAt || new Date().toISOString(), validatedRemotely: false };
|
|
80
|
-
writeJson(LICENSE_FILE, m);
|
|
81
|
-
try { fs.unlinkSync(LEGACY_LICENSE_FILE); } catch {}
|
|
82
|
-
return m;
|
|
83
|
-
}
|
|
84
|
-
return { isPro: false };
|
|
85
|
-
}
|
|
86
|
-
function isPro() { const l = loadLicense(); return !!(l && l.isPro); }
|
|
87
|
-
|
|
88
|
-
async function validateKeyRemote(key) {
|
|
89
|
-
return new Promise((resolve, reject) => {
|
|
90
|
-
const body = JSON.stringify({ key });
|
|
91
|
-
const u = new URL(VALIDATE_URL);
|
|
92
|
-
const req = https.request({ hostname: u.hostname, port: 443, path: u.pathname, method: 'POST',
|
|
93
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'User-Agent': 'lemma-proxy/0.3.0' }
|
|
94
|
-
}, res => {
|
|
95
|
-
let d = '';
|
|
96
|
-
res.on('data', c => d += c);
|
|
97
|
-
res.on('end', () => { try { resolve(JSON.parse(d)); } catch { reject(new Error('bad json')); } });
|
|
98
|
-
});
|
|
99
|
-
req.on('error', reject);
|
|
100
|
-
const t = setTimeout(() => { req.destroy(); reject(new Error('timeout')); }, 5000);
|
|
101
|
-
req.on('close', () => clearTimeout(t));
|
|
102
|
-
req.write(body); req.end();
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ── Usage helpers ──────────────────────────────────────────────────────────────
|
|
107
|
-
function loadUsage() {
|
|
108
|
-
const u = readJson(USAGE_FILE, { count: 0, lastReset: new Date().toISOString(), history: [] });
|
|
109
|
-
const now = new Date(), last = new Date(u.lastReset || 0);
|
|
110
|
-
if (now.getMonth() !== last.getMonth() || now.getFullYear() !== last.getFullYear()) {
|
|
111
|
-
const fresh = { count: 0, lastReset: now.toISOString(), history: [...(u.history||[]), { month: last.toISOString().slice(0,7), count: u.count }] };
|
|
112
|
-
writeJson(USAGE_FILE, fresh); return fresh;
|
|
113
|
-
}
|
|
114
|
-
return u;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ── Stats helpers ──────────────────────────────────────────────────────────────
|
|
118
|
-
function loadStats() { return readJson(STATS_FILE, {}); }
|
|
119
|
-
function recordStat(stats, project, fromCache, latencyMs, provider, tokensSaved) {
|
|
120
|
-
if (!stats[project]) stats[project] = { total:0, hits:0, misses:0, totalLatency:0, totalTokensSaved:0, providers:{} };
|
|
121
|
-
const s = stats[project];
|
|
122
|
-
s.total++; s.totalLatency += latencyMs;
|
|
123
|
-
fromCache ? s.hits++ : s.misses++;
|
|
124
|
-
s.totalTokensSaved += (fromCache ? (tokensSaved||2000) : 0);
|
|
125
|
-
if (!s.providers[provider]) s.providers[provider] = { hits:0, misses:0 };
|
|
126
|
-
fromCache ? s.providers[provider].hits++ : s.providers[provider].misses++;
|
|
127
|
-
|
|
128
|
-
logEvent({
|
|
129
|
-
type: fromCache ? 'cache:hit' : 'cache:miss',
|
|
130
|
-
project,
|
|
131
|
-
latency: latencyMs,
|
|
132
|
-
provider,
|
|
133
|
-
tokens: fromCache ? (tokensSaved||2000) : 0
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
writeJson(STATS_FILE, stats);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── Cache (in-process) ─────────────────────────────────────────────────────────
|
|
140
|
-
// Simple in-memory map used by the proxy process itself
|
|
141
|
-
const CACHE = new Map(); // key → {input, data, createdAt, ttl}
|
|
142
|
-
const TTL_MS = 86400000 * 7;
|
|
143
|
-
|
|
144
|
-
function cacheKey(prompt) {
|
|
145
|
-
return crypto.createHash('sha256').update(prompt.toLowerCase().trim()).digest('hex');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ── Semantic Search (Pro) ──────────────────────────────────────────────────────
|
|
149
|
-
async function getEmbedding(text) {
|
|
150
|
-
try {
|
|
151
|
-
const resp = await axios.post('http://localhost:11434/api/embeddings', {
|
|
152
|
-
model: 'nomic-embed-text',
|
|
153
|
-
prompt: text
|
|
154
|
-
}, { timeout: 5000 });
|
|
155
|
-
return resp.data.embedding;
|
|
156
|
-
} catch (e) {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function semanticGet(prompt) {
|
|
162
|
-
if (!isPro()) return null;
|
|
163
|
-
const emb = await getEmbedding(prompt);
|
|
164
|
-
if (!emb) return null;
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const collection = await chroma.getOrCreateCollection({
|
|
168
|
-
name: 'lemma-cache',
|
|
169
|
-
embeddingFunction: dummyEmbeddingFunction,
|
|
170
|
-
metadata: { "hnsw:space": "cosine" }
|
|
171
|
-
});
|
|
172
|
-
const res = await collection.query({
|
|
173
|
-
queryEmbeddings: [emb],
|
|
174
|
-
nResults: 1
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
if (!res.ids[0] || res.ids[0].length === 0) return null;
|
|
178
|
-
|
|
179
|
-
const distance = res.distances[0][0];
|
|
180
|
-
const similarity = Math.max(0, 1 - distance); // Chroma Cosine distance is 0 to 2
|
|
181
|
-
const THRESHOLD = 0.7;
|
|
182
|
-
|
|
183
|
-
console.log(`🔍 Semantic Search: Dist=${distance.toFixed(3)}, Sim=${similarity.toFixed(3)} (Threshold=${THRESHOLD})`);
|
|
184
|
-
|
|
185
|
-
if (similarity >= THRESHOLD) {
|
|
186
|
-
console.log(`🎯 Semantic HIT!`);
|
|
187
|
-
const metadata = res.metadatas[0][0];
|
|
188
|
-
return {
|
|
189
|
-
data: JSON.parse(metadata.response),
|
|
190
|
-
similarity,
|
|
191
|
-
id: res.ids[0][0]
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
} catch (e) {
|
|
195
|
-
console.error(`❌ Semantic search error: ${e.message}`);
|
|
196
|
-
}
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function semanticSet(prompt, data) {
|
|
201
|
-
if (!isPro()) return;
|
|
202
|
-
const emb = await getEmbedding(prompt);
|
|
203
|
-
if (!emb) return;
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
const collection = await chroma.getOrCreateCollection({
|
|
207
|
-
name: 'lemma-cache',
|
|
208
|
-
embeddingFunction: dummyEmbeddingFunction,
|
|
209
|
-
metadata: { "hnsw:space": "cosine" }
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
await collection.add({
|
|
213
|
-
ids: [crypto.randomUUID?.() || Math.random().toString(36).substring(7)],
|
|
214
|
-
embeddings: [emb],
|
|
215
|
-
metadatas: [{
|
|
216
|
-
prompt,
|
|
217
|
-
response: JSON.stringify(data),
|
|
218
|
-
timestamp: Date.now()
|
|
219
|
-
}],
|
|
220
|
-
documents: [prompt]
|
|
221
|
-
});
|
|
222
|
-
console.log(`💾 Stored semantic entry: "${prompt.substring(0, 30)}..."`);
|
|
223
|
-
} catch (e) {
|
|
224
|
-
console.error(`❌ Semantic storage failed: ${e.message}`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function cacheGet(prompt) {
|
|
229
|
-
const k = cacheKey(prompt);
|
|
230
|
-
const e = CACHE.get(k);
|
|
231
|
-
if (!e) return null;
|
|
232
|
-
if (e.ttl > 0 && Date.now() - e.createdAt > e.ttl) { CACHE.delete(k); return null; }
|
|
233
|
-
return e;
|
|
234
|
-
}
|
|
235
|
-
function cacheSet(prompt, data) {
|
|
236
|
-
CACHE.set(cacheKey(prompt), { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS });
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ── Prompt extraction ──────────────────────────────────────────────────────────
|
|
240
|
-
function extractPrompt(body, provider) {
|
|
241
|
-
const msgs = body.messages || [];
|
|
242
|
-
if (provider === 'openai') return msgs.map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content))).join('\n');
|
|
243
|
-
if (provider === 'anthropic') return msgs.map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content))).join('\n');
|
|
244
|
-
if (provider === 'gemini') {
|
|
245
|
-
const contents = body.contents || [];
|
|
246
|
-
return contents.map(c => c.parts.map(p => p.text).join(' ')).join('\n');
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ── SSE helpers ────────────────────────────────────────────────────────────────
|
|
252
|
-
function setSseHeaders(res, fromCache, similarity, tier) {
|
|
253
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
254
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
255
|
-
res.setHeader('Connection', 'keep-alive');
|
|
256
|
-
res.setHeader('X-Accel-Buffering', 'no');
|
|
257
|
-
res.setHeader('X-Lemma-Cache', fromCache ? 'HIT' : 'MISS');
|
|
258
|
-
res.setHeader('X-Lemma-Similarity', (similarity || 1.0).toFixed(3));
|
|
259
|
-
res.setHeader('X-Lemma-Tier', tier);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function buildOaiChunk(id, model, text, stop) {
|
|
263
|
-
return { id, object:'chat.completion.chunk', created: Math.floor(Date.now()/1000), model,
|
|
264
|
-
choices:[{ index:0, delta: stop ? {} : { content: text }, finish_reason: stop ? 'stop' : null }] };
|
|
265
|
-
}
|
|
266
|
-
function buildAntChunk(text, stop) {
|
|
267
|
-
if (stop) return { type:'message_stop' };
|
|
268
|
-
return { type:'content_block_delta', index:0, delta:{ type:'text_delta', text } };
|
|
269
|
-
}
|
|
270
|
-
function buildGemChunk(text, stop) {
|
|
271
|
-
return { candidates: [{ content: { parts: [{ text }] }, finishReason: stop ? 'STOP' : undefined }] };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function simulateHitStream(res, cachedData, provider, model, tier, similarity) {
|
|
275
|
-
setSseHeaders(res, true, similarity, tier);
|
|
276
|
-
const id = `lemma-${Date.now()}`;
|
|
277
|
-
let text = '';
|
|
278
|
-
try {
|
|
279
|
-
if (provider === 'openai') text = cachedData?.choices?.[0]?.message?.content || '';
|
|
280
|
-
else if (provider === 'anthropic') { const c = cachedData?.content; text = Array.isArray(c) ? c.map(x=>x.text||'').join('') : String(c||''); }
|
|
281
|
-
else if (provider === 'gemini') { text = cachedData?.candidates?.[0]?.content?.parts?.[0]?.text || ''; }
|
|
282
|
-
} catch {}
|
|
283
|
-
const words = text.split(' ');
|
|
284
|
-
for (let i = 0; i < words.length; i += 5) {
|
|
285
|
-
const chunk = words.slice(i, i+5).join(' ');
|
|
286
|
-
let evt;
|
|
287
|
-
if (provider === 'openai') evt = buildOaiChunk(id, model, chunk, false);
|
|
288
|
-
else if (provider === 'anthropic') evt = buildAntChunk(chunk, false);
|
|
289
|
-
else if (provider === 'gemini') evt = buildGemChunk(chunk, false);
|
|
290
|
-
|
|
291
|
-
res.write(provider === 'gemini' ? `${JSON.stringify(evt)}\n` : `data: ${JSON.stringify(evt)}\n\n`);
|
|
292
|
-
await new Promise(r => setTimeout(r, 15));
|
|
293
|
-
}
|
|
294
|
-
const stopEvt = provider === 'openai' ? buildOaiChunk(id, model, '', true) : (provider === 'anthropic' ? buildAntChunk('', true) : buildGemChunk('', true));
|
|
295
|
-
res.write(provider === 'gemini' ? `${JSON.stringify(stopEvt)}\n` : `data: ${JSON.stringify(stopEvt)}\n\n`);
|
|
296
|
-
if (provider !== 'gemini') res.write('data: [DONE]\n\n');
|
|
297
|
-
res.end();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async function relayMissStream(res, body, provider, tier) {
|
|
301
|
-
setSseHeaders(res, false, 1.0, tier);
|
|
302
|
-
const apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
303
|
-
if (!apiKey) { res.write(`data: ${JSON.stringify({error:`${provider.toUpperCase()}_API_KEY not set`})}\n\ndata: [DONE]\n\n`); res.end(); return; }
|
|
304
|
-
const hostname = provider === 'openai' ? 'api.openai.com' : 'api.anthropic.com';
|
|
305
|
-
const upPath = provider === 'openai' ? '/v1/chat/completions' : '/v1/messages';
|
|
306
|
-
const headers = provider === 'openai'
|
|
307
|
-
? { Authorization: `Bearer ${apiKey}`, 'Content-Type':'application/json' }
|
|
308
|
-
: { 'x-api-key': apiKey, 'anthropic-version':'2023-06-01', 'Content-Type':'application/json' };
|
|
309
|
-
const payload = JSON.stringify({ ...body, stream: true });
|
|
310
|
-
await new Promise(resolve => {
|
|
311
|
-
const req = https.request({ hostname, port:443, path:upPath, method:'POST',
|
|
312
|
-
headers: { ...headers, 'Content-Length': Buffer.byteLength(payload) }
|
|
313
|
-
}, upstream => {
|
|
314
|
-
upstream.on('data', chunk => res.write(chunk));
|
|
315
|
-
upstream.on('end', () => { res.write('data: [DONE]\n\n'); res.end(); resolve(); });
|
|
316
|
-
upstream.on('error', err => { res.write(`data: ${JSON.stringify({error:err.message})}\n\ndata: [DONE]\n\n`); res.end(); resolve(); });
|
|
317
|
-
});
|
|
318
|
-
req.on('error', err => { res.write(`data: ${JSON.stringify({error:err.message})}\n\ndata: [DONE]\n\n`); res.end(); resolve(); });
|
|
319
|
-
req.write(payload); req.end();
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// ── Proxy Server class ─────────────────────────────────────────────────────────
|
|
324
|
-
class LemmaServer {
|
|
325
|
-
constructor(port, projectName) {
|
|
326
|
-
this.port = port;
|
|
327
|
-
this.projectName = projectName || detectProject();
|
|
328
|
-
this.projectDir = ensureProjectDir(this.projectName);
|
|
329
|
-
this.stats = loadStats();
|
|
330
|
-
this.app = express();
|
|
331
|
-
this.setupMiddleware();
|
|
332
|
-
this.setupRoutes();
|
|
333
|
-
this.setupWebSockets();
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
setupMiddleware() {
|
|
337
|
-
this.app.use(express.json({ limit: '10mb' }));
|
|
338
|
-
this.app.use((req, res, next) => {
|
|
339
|
-
res.header('Access-Control-Allow-Origin', '*');
|
|
340
|
-
res.header('Access-Control-Allow-Headers', '*');
|
|
341
|
-
res.header('Access-Control-Allow-Methods', '*');
|
|
342
|
-
if (req.method === 'OPTIONS') return res.sendStatus(200);
|
|
343
|
-
next();
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
setupRoutes() {
|
|
348
|
-
// Health
|
|
349
|
-
this.app.get('/health', (req, res) => {
|
|
350
|
-
const pro = isPro();
|
|
351
|
-
res.json({ status:'ok', project: this.projectName, mode: pro?'pro':'standard',
|
|
352
|
-
tier: pro?'pro':'free', cacheSize: CACHE.size, port: this.port });
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
// Stats endpoint
|
|
356
|
-
this.app.get('/stats', (req, res) => {
|
|
357
|
-
res.json({ project: this.projectName, stats: this.stats[this.projectName] || {} });
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Mock /v1/models (required by Cursor to accept Lemma as a valid base URL)
|
|
361
|
-
this.app.get('/v1/models', (req, res) => {
|
|
362
|
-
res.json({ object:'list', data:[
|
|
363
|
-
{ id:'gpt-4o', object:'model', created:1706745938, owned_by:'lemma-proxy' },
|
|
364
|
-
{ id:'gpt-4', object:'model', created:1687882411, owned_by:'lemma-proxy' },
|
|
365
|
-
{ id:'gpt-4-turbo', object:'model', created:1712361441, owned_by:'lemma-proxy' },
|
|
366
|
-
{ id:'gpt-3.5-turbo', object:'model', created:1677610602, owned_by:'lemma-proxy' },
|
|
367
|
-
{ id:'claude-3-5-sonnet-20241022', object:'model', created:1706745938, owned_by:'lemma-proxy' },
|
|
368
|
-
{ id:'claude-3-opus-20240229', object:'model', created:1706745938, owned_by:'lemma-proxy' },
|
|
369
|
-
]});
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
// OpenAI completions
|
|
373
|
-
this.app.post('/v1/chat/completions', (req, res) => this.handleCompletion(req, res, 'openai'));
|
|
374
|
-
|
|
375
|
-
// Anthropic messages
|
|
376
|
-
this.app.post('/v1/messages', (req, res) => this.handleCompletion(req, res, 'anthropic'));
|
|
377
|
-
|
|
378
|
-
// Gemini completions
|
|
379
|
-
this.app.post('/v1beta/models/:modelId:generateContent', (req, res) => this.handleCompletion(req, res, 'gemini'));
|
|
380
|
-
this.app.post('/v1beta/models/:modelId:streamGenerateContent', (req, res) => this.handleCompletion(req, res, 'gemini'));
|
|
381
|
-
|
|
382
|
-
// Pass-through all other /v1/* calls
|
|
383
|
-
this.app.all('/v1/*', async (req, res) => this.passThrough(req, res));
|
|
384
|
-
|
|
385
|
-
// ── Dashboard APIs ──────────────────────────────────────────────────────────
|
|
386
|
-
|
|
387
|
-
// Serve Dashboard Static UI
|
|
388
|
-
const dashboardPath = path.join(__dirname, 'dashboard', 'dist');
|
|
389
|
-
if (fs.existsSync(dashboardPath)) {
|
|
390
|
-
// Redirect /dashboard to /dashboard/ to ensure relative assets load correctly
|
|
391
|
-
this.app.get('/dashboard', (req, res, next) => {
|
|
392
|
-
if (!req.path.endsWith('/')) return res.redirect(301, '/dashboard/');
|
|
393
|
-
next();
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
this.app.use('/dashboard/', express.static(dashboardPath));
|
|
397
|
-
// API endpoints expected by the dashboard
|
|
398
|
-
this.app.get('/api/metrics', (req, res) => {
|
|
399
|
-
const stats = this.stats[this.projectName] || { total:0, hits:0, misses:0, totalLatency:0, totalTokensSaved:0 };
|
|
400
|
-
const pro = isPro();
|
|
401
|
-
res.json({
|
|
402
|
-
timestamp: new Date().toISOString(),
|
|
403
|
-
totalRequests: stats.total,
|
|
404
|
-
cacheHits: stats.hits,
|
|
405
|
-
cacheMisses: stats.misses,
|
|
406
|
-
hitRate: stats.total > 0 ? (stats.hits / stats.total) : 0,
|
|
407
|
-
averageLatency: stats.total > 0 ? (stats.totalLatency / stats.total) : 0,
|
|
408
|
-
tokensSaved: stats.totalTokensSaved,
|
|
409
|
-
costSaved: stats.hits * 0.003,
|
|
410
|
-
activeAgents: 1,
|
|
411
|
-
totalAgents: 1,
|
|
412
|
-
uptime: process.uptime(),
|
|
413
|
-
tier: pro ? 'pro' : 'free',
|
|
414
|
-
mode: pro ? 'Semantic' : 'Standard'
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
this.app.get('/api/agents', (req, res) => {
|
|
419
|
-
res.json({ agents: [{
|
|
420
|
-
id: this.projectName,
|
|
421
|
-
name: this.projectName,
|
|
422
|
-
status: 'active',
|
|
423
|
-
capabilities: ['Semantic Cache', 'IDE Proxy'],
|
|
424
|
-
tasksCompleted: this.stats[this.projectName]?.total || 0,
|
|
425
|
-
cacheHits: this.stats[this.projectName]?.hits || 0,
|
|
426
|
-
lastSeen: new Date().toISOString()
|
|
427
|
-
}]});
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
this.app.get('/api/events', (req, res) => {
|
|
431
|
-
const page = parseInt(req.query.page) || 1;
|
|
432
|
-
const limit = parseInt(req.query.limit) || 50;
|
|
433
|
-
const reversed = [...EVENT_LOG].reverse();
|
|
434
|
-
res.json({
|
|
435
|
-
events: reversed.slice((page-1)*limit, page*limit),
|
|
436
|
-
pagination: { total: EVENT_LOG.length, page, limit }
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
this.app.get('/api/cache-stats', (req, res) => {
|
|
441
|
-
res.json({ topQueries: [], latencyPercentiles: { p50: 20, p95: 50, p99: 100 }, totalCached: CACHE.size });
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Handle dashboard client-side routing (must be last)
|
|
445
|
-
this.app.get('/dashboard/*', (req, res) => {
|
|
446
|
-
res.sendFile(path.join(dashboardPath, 'index.html'));
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async handleCompletion(req, res, provider) {
|
|
452
|
-
const t0 = Date.now();
|
|
453
|
-
const pro = isPro();
|
|
454
|
-
const tier = pro ? 'pro' : 'standard';
|
|
455
|
-
const isStream = !!req.body.stream;
|
|
456
|
-
|
|
457
|
-
// Free tier gate
|
|
458
|
-
if (!pro) {
|
|
459
|
-
const usage = loadUsage();
|
|
460
|
-
const remaining = FREE_LIMIT - usage.count;
|
|
461
|
-
if (remaining <= 0) {
|
|
462
|
-
const errBody = { error:{ message:`Free tier limit (${FREE_LIMIT}/mo). Upgrade: https://lemma.nxus.studio/upgrade`, type:'rate_limit_exceeded', code:'free_tier_limit' }};
|
|
463
|
-
if (isStream) { res.setHeader('Content-Type','text/event-stream'); res.write(`data: ${JSON.stringify(errBody)}\n\ndata: [DONE]\n\n`); return res.end(); }
|
|
464
|
-
return res.status(429).json(errBody);
|
|
465
|
-
}
|
|
466
|
-
if (remaining <= FREE_LIMIT * (1 - WARN_PCT)) res.setHeader('X-Lemma-Warning', `${remaining} queries remaining this month`);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const prompt = extractPrompt(req.body, provider);
|
|
470
|
-
if (!prompt) return res.status(400).json({ error:'Invalid request format' });
|
|
471
|
-
|
|
472
|
-
const model = req.body.model || (provider==='openai'?'gpt-4o':'claude-3-5-sonnet-20241022');
|
|
473
|
-
|
|
474
|
-
// 1. Exact Match Cache
|
|
475
|
-
const hit = cacheGet(prompt);
|
|
476
|
-
if (hit) {
|
|
477
|
-
const latencyMs = Date.now() - t0;
|
|
478
|
-
recordStat(this.stats, this.projectName, true, latencyMs, provider, 2000);
|
|
479
|
-
if (isStream) return simulateHitStream(res, hit.data, provider, model, tier, 1.0);
|
|
480
|
-
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
481
|
-
res.setHeader('X-Lemma-Similarity', '1.000');
|
|
482
|
-
res.setHeader('X-Lemma-Tier', tier);
|
|
483
|
-
res.setHeader('X-Lemma-Latency', `${latencyMs}ms`);
|
|
484
|
-
return res.json(hit.data);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// 2. Semantic Match Cache (Pro Only)
|
|
488
|
-
if (pro) {
|
|
489
|
-
const semHit = await semanticGet(prompt);
|
|
490
|
-
if (semHit) {
|
|
491
|
-
const latencyMs = Date.now() - t0;
|
|
492
|
-
recordStat(this.stats, this.projectName, true, latencyMs, provider, 2000);
|
|
493
|
-
if (isStream) return simulateHitStream(res, semHit.data, provider, model, tier, semHit.similarity);
|
|
494
|
-
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
495
|
-
res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
|
|
496
|
-
res.setHeader('X-Lemma-Tier', tier);
|
|
497
|
-
res.setHeader('X-Lemma-Latency', `${latencyMs}ms`);
|
|
498
|
-
return res.json(semHit.data);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Cache miss — stream relay or regular call
|
|
503
|
-
if (isStream) {
|
|
504
|
-
// Relay then cache the assembled response (best-effort)
|
|
505
|
-
recordStat(this.stats, this.projectName, false, Date.now()-t0, provider, 0);
|
|
506
|
-
if (!pro) { const u = loadUsage(); u.count++; writeJson(USAGE_FILE, u); }
|
|
507
|
-
return relayMissStream(res, req.body, provider, tier);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Non-stream upstream call
|
|
511
|
-
try {
|
|
512
|
-
const data = await this.callUpstream(req.body, provider);
|
|
513
|
-
cacheSet(prompt, data);
|
|
514
|
-
if (pro) semanticSet(prompt, data); // Store semantically
|
|
515
|
-
if (!pro) { const u = loadUsage(); u.count++; writeJson(USAGE_FILE, u); }
|
|
516
|
-
const latencyMs = Date.now() - t0;
|
|
517
|
-
recordStat(this.stats, this.projectName, false, latencyMs, provider, 0);
|
|
518
|
-
res.setHeader('X-Lemma-Cache', 'MISS');
|
|
519
|
-
res.setHeader('X-Lemma-Similarity', '0.000');
|
|
520
|
-
res.setHeader('X-Lemma-Tier', tier);
|
|
521
|
-
res.setHeader('X-Lemma-Latency', `${latencyMs}ms`);
|
|
522
|
-
return res.json(data);
|
|
523
|
-
} catch (err) {
|
|
524
|
-
return res.status(500).json({ error: err.message });
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async callUpstream(body, provider) {
|
|
529
|
-
if (provider === 'gemini') {
|
|
530
|
-
const apiKey = process.env.GEMINI_API_KEY;
|
|
531
|
-
if (!apiKey) throw new Error('GEMINI_API_KEY not set');
|
|
532
|
-
const model = body.model || 'gemini-1.5-pro';
|
|
533
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
534
|
-
const resp = await axios.post(url, body, { headers: { 'Content-Type': 'application/json' } });
|
|
535
|
-
return resp.data;
|
|
536
|
-
}
|
|
537
|
-
const apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
538
|
-
if (!apiKey) throw new Error(`${provider.toUpperCase()}_API_KEY not set`);
|
|
539
|
-
const baseURL = provider === 'openai' ? 'https://api.openai.com' : 'https://api.anthropic.com';
|
|
540
|
-
const endpoint = provider === 'openai' ? '/v1/chat/completions' : '/v1/messages';
|
|
541
|
-
const headers = provider === 'openai'
|
|
542
|
-
? { Authorization:`Bearer ${apiKey}`, 'Content-Type':'application/json' }
|
|
543
|
-
: { 'x-api-key':apiKey, 'anthropic-version':'2023-06-01', 'Content-Type':'application/json' };
|
|
544
|
-
const resp = await axios.post(`${baseURL}${endpoint}`, body, { headers });
|
|
545
|
-
return resp.data;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async passThrough(req, res) {
|
|
549
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
550
|
-
if (!apiKey) return res.status(401).json({ error:'OPENAI_API_KEY not set' });
|
|
551
|
-
try {
|
|
552
|
-
const resp = await axios({ method:req.method, url:`https://api.openai.com${req.path}`, data:req.body,
|
|
553
|
-
headers:{ Authorization:`Bearer ${apiKey}` } });
|
|
554
|
-
res.json(resp.data);
|
|
555
|
-
} catch (err) { res.status(err.response?.status||500).json(err.response?.data||{error:err.message}); }
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
setupWebSockets() {
|
|
559
|
-
this.wss = new WebSocket.Server({ noServer: true });
|
|
560
|
-
|
|
561
|
-
this.wss.on('connection', (ws) => {
|
|
562
|
-
let agentId = 'unknown';
|
|
563
|
-
console.log(`🔌 New agent connection attempt...`);
|
|
564
|
-
|
|
565
|
-
ws.on('message', async (message) => {
|
|
566
|
-
try {
|
|
567
|
-
const data = JSON.parse(message);
|
|
568
|
-
|
|
569
|
-
// 1. Handle Handshake
|
|
570
|
-
if (data.type === 'handshake') {
|
|
571
|
-
agentId = data.payload?.agentId || `agent-${Date.now()}`;
|
|
572
|
-
console.log(`✅ Agent authenticated: ${agentId}`);
|
|
573
|
-
ws.send(JSON.stringify({
|
|
574
|
-
type: 'handshake_ack',
|
|
575
|
-
timestamp: Date.now(),
|
|
576
|
-
payload: { status: 'connected', agentId }
|
|
577
|
-
}));
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// 2. Handle Task Request (The core of Lemma intercepting agents)
|
|
581
|
-
else if (data.type === 'task_request') {
|
|
582
|
-
const taskId = data.payload?.taskId;
|
|
583
|
-
const prompt = data.payload?.description;
|
|
584
|
-
console.log(`🤖 [${agentId}] Task received: "${prompt.substring(0, 40)}..."`);
|
|
585
|
-
|
|
586
|
-
const t0 = Date.now();
|
|
587
|
-
const pro = isPro();
|
|
588
|
-
|
|
589
|
-
// Free tier gate
|
|
590
|
-
if (!pro) {
|
|
591
|
-
const usage = loadUsage();
|
|
592
|
-
if (usage.count >= FREE_LIMIT) {
|
|
593
|
-
console.log(`⚠️ [${agentId}] Blocked: Free tier limit reached`);
|
|
594
|
-
ws.send(JSON.stringify({
|
|
595
|
-
type: 'task_response',
|
|
596
|
-
payload: {
|
|
597
|
-
taskId,
|
|
598
|
-
success: false,
|
|
599
|
-
error: `Free tier limit (${FREE_LIMIT}/mo). Upgrade: https://lemma.nxus.studio/upgrade`,
|
|
600
|
-
code: 'free_tier_limit'
|
|
601
|
-
}
|
|
602
|
-
}));
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Check Cache (Exact + Semantic if Pro)
|
|
608
|
-
const hit = cacheGet(prompt);
|
|
609
|
-
let responseData = null;
|
|
610
|
-
let fromCache = false;
|
|
611
|
-
let similarity = 1.0;
|
|
612
|
-
|
|
613
|
-
if (hit) {
|
|
614
|
-
responseData = hit.data;
|
|
615
|
-
fromCache = true;
|
|
616
|
-
} else if (isPro()) {
|
|
617
|
-
const semHit = await semanticGet(prompt);
|
|
618
|
-
if (semHit) {
|
|
619
|
-
responseData = semHit.data;
|
|
620
|
-
fromCache = true;
|
|
621
|
-
similarity = semHit.similarity;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
if (fromCache) {
|
|
626
|
-
const latencyMs = Date.now() - t0;
|
|
627
|
-
recordStat(this.stats, this.projectName, true, latencyMs, 'agent-swarm', 500);
|
|
628
|
-
console.log(`⚡ [${agentId}] Cache HIT! (${latencyMs}ms)`);
|
|
629
|
-
|
|
630
|
-
ws.send(JSON.stringify({
|
|
631
|
-
type: 'task_response',
|
|
632
|
-
payload: {
|
|
633
|
-
taskId,
|
|
634
|
-
success: true,
|
|
635
|
-
result: responseData,
|
|
636
|
-
fromCache: true,
|
|
637
|
-
latencyMs
|
|
638
|
-
}
|
|
639
|
-
}));
|
|
640
|
-
} else {
|
|
641
|
-
// Simular una respuesta del modelo para el ejemplo de los agentes
|
|
642
|
-
// En un entorno real, aquí se llamaría a callUpstream
|
|
643
|
-
const mockResult = `Processed by Lemma Engine: ${prompt.toUpperCase()}`;
|
|
644
|
-
const latencyMs = 800 + Math.random() * 500; // Simular latencia de red
|
|
645
|
-
|
|
646
|
-
setTimeout(() => {
|
|
647
|
-
cacheSet(prompt, mockResult);
|
|
648
|
-
if (isPro()) semanticSet(prompt, mockResult);
|
|
649
|
-
recordStat(this.stats, this.projectName, false, latencyMs, 'agent-swarm', 0);
|
|
650
|
-
|
|
651
|
-
ws.send(JSON.stringify({
|
|
652
|
-
type: 'task_response',
|
|
653
|
-
payload: {
|
|
654
|
-
taskId,
|
|
655
|
-
success: true,
|
|
656
|
-
result: mockResult,
|
|
657
|
-
fromCache: false,
|
|
658
|
-
latencyMs
|
|
659
|
-
}
|
|
660
|
-
}));
|
|
661
|
-
}, 500);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
} catch (e) {
|
|
665
|
-
console.error('WebSocket message error:', e);
|
|
666
|
-
}
|
|
667
|
-
});
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
start() {
|
|
672
|
-
const server = this.app.listen(this.port, () => {
|
|
673
|
-
writeJson(PID_FILE.replace('.pid', '.pid'), { pid: process.pid });
|
|
674
|
-
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
675
|
-
fs.writeFileSync(PORT_FILE, String(this.port));
|
|
676
|
-
const pro = isPro();
|
|
677
|
-
console.log(`\n🚀 Lemma Proxy v0.3.0 — ${pro?'✨ Pro (Semantic)':'📦 Standard (Free)'}`);
|
|
678
|
-
console.log(`📁 Project : ${this.projectName}`);
|
|
679
|
-
console.log(`🔌 Port : ${this.port}`);
|
|
680
|
-
console.log(`💾 Cache : ${this.projectDir}`);
|
|
681
|
-
console.log(`\n📝 Configure your IDE:`);
|
|
682
|
-
console.log(` OpenAI Base URL → http://localhost:${this.port}/v1`);
|
|
683
|
-
console.log(` Anthropic Base → http://localhost:${this.port}/v1`);
|
|
684
|
-
console.log(`\n📊 Commands: lemma-proxy stats | lemma-proxy stop`);
|
|
685
|
-
console.log(`🌐 Dashboard: http://localhost:${this.port}/dashboard\n`);
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
server.on('upgrade', (request, socket, head) => {
|
|
689
|
-
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
|
690
|
-
this.wss.emit('connection', ws, request);
|
|
691
|
-
});
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
process.on('SIGTERM', () => this.stop(server));
|
|
695
|
-
process.on('SIGINT', () => this.stop(server));
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
stop(server) {
|
|
699
|
-
console.log('\n🛑 Stopping Lemma Proxy...');
|
|
700
|
-
if (server) server.close();
|
|
701
|
-
try { if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE); } catch {}
|
|
702
|
-
try { if (fs.existsSync(PORT_FILE)) fs.unlinkSync(PORT_FILE); } catch {}
|
|
703
|
-
console.log('✅ Stopped');
|
|
704
|
-
process.exit(0);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// ── CLI ────────────────────────────────────────────────────────────────────────
|
|
709
|
-
program.name('lemma-proxy').description('Lemma Proxy — Universal AI Cache for IDEs').version('0.3.0');
|
|
710
|
-
|
|
711
|
-
program.command('start')
|
|
712
|
-
.description('Start the proxy server')
|
|
713
|
-
.option('-p, --port <number>', 'Port to listen on', '8080')
|
|
714
|
-
.option('--project <name>', 'Override project name')
|
|
715
|
-
.action(async (opts) => {
|
|
716
|
-
const port = parseInt(opts.port, 10);
|
|
717
|
-
const server = new LemmaServer(port, opts.project || null);
|
|
718
|
-
server.start();
|
|
719
|
-
});
|
|
720
|
-
|
|
721
|
-
program.command('stop')
|
|
722
|
-
.description('Stop the running proxy')
|
|
723
|
-
.action(() => {
|
|
724
|
-
if (!fs.existsSync(PID_FILE)) return console.log('⚠️ Proxy not running');
|
|
725
|
-
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
|
|
726
|
-
try { process.kill(pid, 'SIGTERM'); console.log('✅ Proxy stopped'); }
|
|
727
|
-
catch { console.log('⚠️ Process not found — clearing PID'); try { fs.unlinkSync(PID_FILE); } catch {} }
|
|
728
|
-
});
|
|
729
|
-
|
|
730
|
-
program.command('status')
|
|
731
|
-
.description('Show proxy status')
|
|
732
|
-
.action(() => {
|
|
733
|
-
const running = fs.existsSync(PID_FILE);
|
|
734
|
-
const port = fs.existsSync(PORT_FILE) ? fs.readFileSync(PORT_FILE,'utf8').trim() : '?';
|
|
735
|
-
const lic = loadLicense();
|
|
736
|
-
console.log(`\n📊 Lemma Proxy Status`);
|
|
737
|
-
console.log(` Running : ${running ? '✅ Yes (PID '+fs.readFileSync(PID_FILE,'utf8').trim()+')' : '❌ No'}`);
|
|
738
|
-
if (running) console.log(` Port : ${port}`);
|
|
739
|
-
console.log(` Tier : ${lic.isPro ? '✨ Pro' : '📦 Standard (Free)'}`);
|
|
740
|
-
console.log(` URL : http://localhost:${port}/v1\n`);
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
program.command('stats')
|
|
744
|
-
.description('Print the Savings Audit')
|
|
745
|
-
.option('--avg-tokens <n>', 'Avg tokens per request', '2000')
|
|
746
|
-
.action((opts) => {
|
|
747
|
-
const avgTokens = parseInt(opts.avgTokens || '2000', 10);
|
|
748
|
-
const stats = loadStats();
|
|
749
|
-
const usage = loadUsage();
|
|
750
|
-
const lic = loadLicense();
|
|
751
|
-
const border = '═'.repeat(54);
|
|
752
|
-
console.log(`\n╔${border}╗`);
|
|
753
|
-
console.log(`║ 🧠 Lemma Proxy — Savings Audit${' '.repeat(20)}║`);
|
|
754
|
-
console.log(`╠${border}╣`);
|
|
755
|
-
if (Object.keys(stats).length === 0) {
|
|
756
|
-
console.log(`║ No data yet. Start making AI calls through the proxy.${' '.repeat(0)}║`);
|
|
757
|
-
}
|
|
758
|
-
for (const [proj, s] of Object.entries(stats)) {
|
|
759
|
-
const hitRate = s.total > 0 ? (s.hits / s.total * 100).toFixed(1) : '0.0';
|
|
760
|
-
const tokSaved = (s.hits * avgTokens).toLocaleString();
|
|
761
|
-
const usd = (s.hits * 0.003).toFixed(4);
|
|
762
|
-
console.log(`║ Project │ ${proj.slice(0,36).padEnd(36)} ║`);
|
|
763
|
-
console.log(`║ Cache Hits │ ${String(s.hits).padEnd(36)} ║`);
|
|
764
|
-
console.log(`║ Misses │ ${String(s.misses).padEnd(36)} ║`);
|
|
765
|
-
console.log(`║ Hit Rate │ ${(hitRate+'%').padEnd(36)} ║`);
|
|
766
|
-
console.log(`║ Tokens Saved│ ~${tokSaved.padEnd(35)} ║`);
|
|
767
|
-
console.log(`║ USD Saved │ ~$${String(usd).padEnd(34)} ║`);
|
|
768
|
-
console.log(`╠${border}╣`);
|
|
769
|
-
}
|
|
770
|
-
console.log(`║ Mode │ ${(lic.isPro?'✨ Pro (Semantic)':'📦 Standard (Free)').padEnd(36)} ║`);
|
|
771
|
-
console.log(`║ Queries │ ${(usage.count+'/'+FREE_LIMIT+' this month').padEnd(36)} ║`);
|
|
772
|
-
console.log(`╚${border}╝\n`);
|
|
773
|
-
if (!lic.isPro && usage.count >= FREE_LIMIT * WARN_PCT) {
|
|
774
|
-
console.log(`⚠️ Approaching free tier limit. Upgrade: https://lemma.nxus.studio/upgrade\n`);
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
program.command('activate <key>')
|
|
779
|
-
.description('Activate a Lemma Pro license key')
|
|
780
|
-
.action(async (key) => {
|
|
781
|
-
if (!/^LEMMA-PRO-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/.test(key.trim())) {
|
|
782
|
-
console.log('❌ Invalid format. Keys look like: LEMMA-PRO-XXXX-XXXX-XXXX');
|
|
783
|
-
process.exit(1);
|
|
784
|
-
}
|
|
785
|
-
process.stdout.write('🔍 Validating key...');
|
|
786
|
-
let validatedRemotely = false;
|
|
787
|
-
let remoteData = null;
|
|
788
|
-
try {
|
|
789
|
-
remoteData = await validateKeyRemote(key.trim());
|
|
790
|
-
validatedRemotely = true;
|
|
791
|
-
if (!remoteData.valid) {
|
|
792
|
-
console.log(`\n❌ ${remoteData.reason || 'Invalid key'}`);
|
|
793
|
-
process.exit(1);
|
|
794
|
-
}
|
|
795
|
-
} catch (err) {
|
|
796
|
-
console.log(` (server offline — using local validation)`);
|
|
797
|
-
}
|
|
798
|
-
const lic = { key: key.trim(), tier:'pro', isPro:true, activatedAt: new Date().toISOString(),
|
|
799
|
-
validatedRemotely, expiresAt: remoteData?.expiresAt, email: remoteData?.email };
|
|
800
|
-
writeJson(LICENSE_FILE, lic);
|
|
801
|
-
console.log(`\n✅ Lemma Pro activated!${validatedRemotely?' (server-verified)':' (format-validated)'}`);
|
|
802
|
-
console.log('\n✨ Unlocked:');
|
|
803
|
-
console.log(' • Unlimited queries');
|
|
804
|
-
console.log(' • Semantic Mode (embeddings)');
|
|
805
|
-
console.log(' • All projects\n');
|
|
806
|
-
console.log('🚀 Restart the proxy to apply: lemma-proxy stop && lemma-proxy start\n');
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
program.parse(process.argv);
|
|
810
|
-
module.exports = { LemmaServer };
|