@nxuss/lemma 0.4.5 → 0.4.7
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/README.md +42 -39
- package/dist/cjs/autopilot/AutopilotWatcher.d.ts +11 -0
- package/dist/cjs/autopilot/AutopilotWatcher.d.ts.map +1 -0
- package/dist/cjs/autopilot/AutopilotWatcher.js +119 -0
- package/dist/cjs/autopilot/AutopilotWatcher.js.map +1 -0
- package/dist/cjs/cli/lemma-proxy.d.ts +10 -0
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -0
- package/dist/cjs/cli/lemma-proxy.js +1268 -0
- package/dist/cjs/cli/lemma-proxy.js.map +1 -0
- package/dist/cjs/cloud/CloudSyncClient.d.ts +10 -0
- package/dist/cjs/cloud/CloudSyncClient.d.ts.map +1 -0
- package/dist/cjs/cloud/CloudSyncClient.js +42 -0
- package/dist/cjs/cloud/CloudSyncClient.js.map +1 -0
- package/dist/cjs/mcp/index.d.ts +2 -0
- package/dist/cjs/mcp/index.d.ts.map +1 -0
- package/dist/cjs/mcp/index.js +260 -0
- package/dist/cjs/mcp/index.js.map +1 -0
- package/dist/cjs/observability/IdeContextSync.d.ts.map +1 -1
- package/dist/cjs/observability/IdeContextSync.js +7 -2
- package/dist/cjs/observability/IdeContextSync.js.map +1 -1
- package/dist/cjs/proxy/AgentMultiplexer.d.ts +9 -0
- package/dist/cjs/proxy/AgentMultiplexer.d.ts.map +1 -0
- package/dist/cjs/proxy/AgentMultiplexer.js +69 -0
- package/dist/cjs/proxy/AgentMultiplexer.js.map +1 -0
- package/dist/cjs/proxy/ComplexityRouter.d.ts +19 -0
- package/dist/cjs/proxy/ComplexityRouter.d.ts.map +1 -0
- package/dist/cjs/proxy/ComplexityRouter.js +79 -0
- package/dist/cjs/proxy/ComplexityRouter.js.map +1 -0
- package/dist/cjs/security/SemanticScrubber.d.ts +25 -0
- package/dist/cjs/security/SemanticScrubber.d.ts.map +1 -0
- package/dist/cjs/security/SemanticScrubber.js +99 -0
- package/dist/cjs/security/SemanticScrubber.js.map +1 -0
- package/dist/cjs/utils/ContextSqueezer.d.ts +44 -0
- package/dist/cjs/utils/ContextSqueezer.d.ts.map +1 -0
- package/dist/cjs/utils/ContextSqueezer.js +201 -0
- package/dist/cjs/utils/ContextSqueezer.js.map +1 -0
- package/dist/esm/autopilot/AutopilotWatcher.d.ts +11 -0
- package/dist/esm/autopilot/AutopilotWatcher.d.ts.map +1 -0
- package/dist/esm/autopilot/AutopilotWatcher.js +112 -0
- package/dist/esm/autopilot/AutopilotWatcher.js.map +1 -0
- package/dist/esm/cli/lemma-proxy.d.ts +10 -0
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -0
- package/dist/esm/cli/lemma-proxy.js +1262 -0
- package/dist/esm/cli/lemma-proxy.js.map +1 -0
- package/dist/esm/cloud/CloudSyncClient.d.ts +10 -0
- package/dist/esm/cloud/CloudSyncClient.d.ts.map +1 -0
- package/dist/esm/cloud/CloudSyncClient.js +35 -0
- package/dist/esm/cloud/CloudSyncClient.js.map +1 -0
- package/dist/esm/mcp/index.d.ts +2 -0
- package/dist/esm/mcp/index.d.ts.map +1 -0
- package/dist/esm/mcp/index.js +255 -0
- package/dist/esm/mcp/index.js.map +1 -0
- package/dist/esm/observability/IdeContextSync.d.ts.map +1 -1
- package/dist/esm/observability/IdeContextSync.js +7 -2
- package/dist/esm/observability/IdeContextSync.js.map +1 -1
- package/dist/esm/proxy/AgentMultiplexer.d.ts +9 -0
- package/dist/esm/proxy/AgentMultiplexer.d.ts.map +1 -0
- package/dist/esm/proxy/AgentMultiplexer.js +62 -0
- package/dist/esm/proxy/AgentMultiplexer.js.map +1 -0
- package/dist/esm/proxy/ComplexityRouter.d.ts +19 -0
- package/dist/esm/proxy/ComplexityRouter.d.ts.map +1 -0
- package/dist/esm/proxy/ComplexityRouter.js +75 -0
- package/dist/esm/proxy/ComplexityRouter.js.map +1 -0
- package/dist/esm/security/SemanticScrubber.d.ts +25 -0
- package/dist/esm/security/SemanticScrubber.d.ts.map +1 -0
- package/dist/esm/security/SemanticScrubber.js +92 -0
- package/dist/esm/security/SemanticScrubber.js.map +1 -0
- package/dist/esm/utils/ContextSqueezer.d.ts +44 -0
- package/dist/esm/utils/ContextSqueezer.d.ts.map +1 -0
- package/dist/esm/utils/ContextSqueezer.js +193 -0
- package/dist/esm/utils/ContextSqueezer.js.map +1 -0
- package/lemma-proxy.cjs +12 -738
- package/package.json +4 -3
- package/src/cloud/CloudSyncClient.js +0 -35
- package/src/proxy/ComplexityRouter.js +0 -37
- package/src/security/SemanticScrubber.js +0 -54
package/lemma-proxy.cjs
CHANGED
|
@@ -1,748 +1,22 @@
|
|
|
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
|
-
//
|
|
24
|
-
const
|
|
25
|
-
const SemanticScrubber = require('./src/security/SemanticScrubber.js');
|
|
26
|
-
const CloudSyncClient = require('./src/cloud/CloudSyncClient.js');
|
|
7
|
+
// Target path in the compiled CommonJS distribution
|
|
8
|
+
const distPath = path.join(__dirname, 'dist', 'cjs', 'cli', 'lemma-proxy.js');
|
|
27
9
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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');
|
|
10
|
+
if (fs.existsSync(distPath)) {
|
|
11
|
+
// Production mode: run the compiled CJS JavaScript binary
|
|
12
|
+
require(distPath);
|
|
13
|
+
} else {
|
|
14
|
+
// Development mode: run via ts-node directly from src/
|
|
83
15
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
112
|
-
}
|
|
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;
|
|
16
|
+
require('ts-node/register');
|
|
17
|
+
require('./src/cli/lemma-proxy.ts');
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.error('❌ Error: Could not launch Lemma. Please run "npm run build" to compile the package.');
|
|
20
|
+
process.exit(1);
|
|
152
21
|
}
|
|
153
|
-
return u;
|
|
154
|
-
}
|
|
155
|
-
|
|
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
|
-
}
|
|
177
|
-
|
|
178
|
-
// ── Cache (in-process) ─────────────────────────────────────────────────────────
|
|
179
|
-
const CACHE = new Map();
|
|
180
|
-
const TTL_MS = 86400000 * 7;
|
|
181
|
-
|
|
182
|
-
function cacheKey(prompt) {
|
|
183
|
-
return crypto.createHash('sha256').update(prompt.toLowerCase().trim()).digest('hex');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ── Semantic Search (Pro) ──────────────────────────────────────────────────────
|
|
187
|
-
async function getEmbedding(text) {
|
|
188
|
-
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;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
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
22
|
}
|
|
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);
|