@nxuss/lemma 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dashboard/dist/assets/{index-B4TA31pA.js → index-CIlkFwDs.js} +98 -98
- package/dashboard/dist/assets/index-CIlkFwDs.js.map +1 -0
- package/dashboard/dist/index.html +1 -1
- package/dist/cjs/cli/lemma-proxy.d.ts +2 -0
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +416 -121
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/cjs/config/index.d.ts.map +1 -1
- package/dist/cjs/config/index.js +2 -1
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/errors/LemmaError.d.ts +52 -0
- package/dist/cjs/errors/LemmaError.d.ts.map +1 -0
- package/dist/cjs/errors/LemmaError.js +84 -0
- package/dist/cjs/errors/LemmaError.js.map +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +24 -22
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/mcp/index.js +81 -0
- package/dist/cjs/mcp/index.js.map +1 -1
- package/dist/cjs/subconscious/SemanticCache.d.ts +4 -4
- package/dist/cjs/subconscious/SemanticCache.d.ts.map +1 -1
- package/dist/cjs/subconscious/SemanticCache.js +14 -10
- package/dist/cjs/subconscious/SemanticCache.js.map +1 -1
- package/dist/cjs/types/index.d.ts +8 -8
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/utils/ClipboardWatcher.d.ts.map +1 -1
- package/dist/cjs/utils/ClipboardWatcher.js +3 -0
- package/dist/cjs/utils/ClipboardWatcher.js.map +1 -1
- package/dist/cjs/utils/SavingsLedger.d.ts +52 -0
- package/dist/cjs/utils/SavingsLedger.d.ts.map +1 -0
- package/dist/cjs/utils/SavingsLedger.js +101 -0
- package/dist/cjs/utils/SavingsLedger.js.map +1 -0
- package/dist/cjs/utils/logger.d.ts +27 -6
- package/dist/cjs/utils/logger.d.ts.map +1 -1
- package/dist/cjs/utils/logger.js +96 -28
- package/dist/cjs/utils/logger.js.map +1 -1
- package/dist/cjs/utils/reportSavings.d.ts +24 -0
- package/dist/cjs/utils/reportSavings.d.ts.map +1 -0
- package/dist/cjs/utils/reportSavings.js +53 -0
- package/dist/cjs/utils/reportSavings.js.map +1 -0
- package/dist/esm/cli/lemma-proxy.d.ts +2 -0
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +414 -121
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/config/index.d.ts.map +1 -1
- package/dist/esm/config/index.js +2 -1
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/errors/LemmaError.d.ts +52 -0
- package/dist/esm/errors/LemmaError.d.ts.map +1 -0
- package/dist/esm/errors/LemmaError.js +75 -0
- package/dist/esm/errors/LemmaError.js.map +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +24 -22
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/mcp/index.js +81 -0
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/subconscious/SemanticCache.d.ts +4 -4
- package/dist/esm/subconscious/SemanticCache.d.ts.map +1 -1
- package/dist/esm/subconscious/SemanticCache.js +14 -10
- package/dist/esm/subconscious/SemanticCache.js.map +1 -1
- package/dist/esm/types/index.d.ts +8 -8
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/esm/utils/ClipboardWatcher.d.ts.map +1 -1
- package/dist/esm/utils/ClipboardWatcher.js +3 -0
- package/dist/esm/utils/ClipboardWatcher.js.map +1 -1
- package/dist/esm/utils/SavingsLedger.d.ts +52 -0
- package/dist/esm/utils/SavingsLedger.d.ts.map +1 -0
- package/dist/esm/utils/SavingsLedger.js +98 -0
- package/dist/esm/utils/SavingsLedger.js.map +1 -0
- package/dist/esm/utils/logger.d.ts +27 -6
- package/dist/esm/utils/logger.d.ts.map +1 -1
- package/dist/esm/utils/logger.js +93 -28
- package/dist/esm/utils/logger.js.map +1 -1
- package/dist/esm/utils/reportSavings.d.ts +24 -0
- package/dist/esm/utils/reportSavings.d.ts.map +1 -0
- package/dist/esm/utils/reportSavings.js +47 -0
- package/dist/esm/utils/reportSavings.js.map +1 -0
- package/package.json +27 -24
- package/dashboard/dist/assets/index-B4TA31pA.js.map +0 -1
|
@@ -24,7 +24,7 @@ function getVersion() {
|
|
|
24
24
|
catch { }
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
return '0.
|
|
27
|
+
return '0.5.3';
|
|
28
28
|
}
|
|
29
29
|
const VERSION = getVersion();
|
|
30
30
|
import ComplexityRouter from '../proxy/ComplexityRouter';
|
|
@@ -32,6 +32,7 @@ import SemanticScrubber from '../security/SemanticScrubber';
|
|
|
32
32
|
import CloudSyncClient from '../cloud/CloudSyncClient';
|
|
33
33
|
import { pruneHistoryMessages, squeezePrompt } from '../utils/ContextSqueezer';
|
|
34
34
|
import { AgentMultiplexer } from '../proxy/AgentMultiplexer';
|
|
35
|
+
import { savingsLedger } from '../utils/SavingsLedger';
|
|
35
36
|
const chroma = new ChromaClient({ host: 'localhost', port: 8000 });
|
|
36
37
|
const dummyEmbeddingFunction = { generate: (texts) => Promise.resolve([]) };
|
|
37
38
|
const complexityRouter = new ComplexityRouter();
|
|
@@ -53,6 +54,13 @@ function logEvent(event) {
|
|
|
53
54
|
EVENT_LOG.push(ev);
|
|
54
55
|
if (EVENT_LOG.length > MAX_EVENTS)
|
|
55
56
|
EVENT_LOG.shift();
|
|
57
|
+
// Print a terminal savings summary every 10 events
|
|
58
|
+
if (EVENT_LOG.length > 0 && EVENT_LOG.length % 10 === 0) {
|
|
59
|
+
const snap = savingsLedger.getSnapshot();
|
|
60
|
+
if (snap.total.tokensSaved > 0) {
|
|
61
|
+
console.log('\n' + savingsLedger.getTerminalSummary() + '\n');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
56
64
|
}
|
|
57
65
|
const FREE_LIMIT = 500;
|
|
58
66
|
const WARN_PCT = 0.8;
|
|
@@ -70,13 +78,20 @@ async function readJson(file, fallback) {
|
|
|
70
78
|
catch { }
|
|
71
79
|
return fallback;
|
|
72
80
|
}
|
|
81
|
+
const writeQueues = new Map();
|
|
73
82
|
async function writeJson(file, data) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
const chain = writeQueues.get(file) || Promise.resolve();
|
|
84
|
+
const writeOperation = async () => {
|
|
85
|
+
try {
|
|
86
|
+
const tempFile = `${file}.${crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2, 15)}.tmp`;
|
|
87
|
+
await fs.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
|
|
88
|
+
await fs.promises.rename(tempFile, file);
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
};
|
|
92
|
+
const nextChain = chain.then(writeOperation).catch(() => { });
|
|
93
|
+
writeQueues.set(file, nextChain);
|
|
94
|
+
return nextChain;
|
|
80
95
|
}
|
|
81
96
|
function projectHash(name) {
|
|
82
97
|
return crypto.createHash('sha1').update(name).digest('hex').slice(0, 12);
|
|
@@ -193,13 +208,198 @@ function ensureGitIgnore() {
|
|
|
193
208
|
catch { }
|
|
194
209
|
return added;
|
|
195
210
|
}
|
|
211
|
+
const CLIPBOARD_PID_FILE = path.join(CACHE_DIR, 'clipboard.pid');
|
|
212
|
+
export function autoConfigureAll(projectName) {
|
|
213
|
+
const project = projectName || detectProject();
|
|
214
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
215
|
+
// 1. Claude Desktop Config
|
|
216
|
+
try {
|
|
217
|
+
const isWin = process.platform === 'win32';
|
|
218
|
+
const claudePath = isWin
|
|
219
|
+
? path.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json')
|
|
220
|
+
: path.join(HOME, 'Library/Application Support/Claude/claude_desktop_config.json');
|
|
221
|
+
if (fs.existsSync(path.dirname(claudePath))) {
|
|
222
|
+
let config = { mcpServers: {} };
|
|
223
|
+
if (fs.existsSync(claudePath)) {
|
|
224
|
+
try {
|
|
225
|
+
config = JSON.parse(fs.readFileSync(claudePath, 'utf8'));
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
}
|
|
229
|
+
if (!config.mcpServers)
|
|
230
|
+
config.mcpServers = {};
|
|
231
|
+
config.mcpServers.lemma = {
|
|
232
|
+
command: 'npx',
|
|
233
|
+
args: ['-y', '@nxuss/lemma', 'mcp'],
|
|
234
|
+
env: { LEMMA_PROJECT: project }
|
|
235
|
+
};
|
|
236
|
+
fs.writeFileSync(claudePath, JSON.stringify(config, null, 2));
|
|
237
|
+
console.log(`✅ [Claude Desktop] Configured automatically! (Restart Claude to activate)`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
console.log(`⚠️ Could not auto-configure Claude Desktop: ${e.message}`);
|
|
242
|
+
}
|
|
243
|
+
// 2. Shell Profiles Environment Override
|
|
244
|
+
try {
|
|
245
|
+
const profiles = [
|
|
246
|
+
path.join(HOME, '.zshrc'),
|
|
247
|
+
path.join(HOME, '.bashrc'),
|
|
248
|
+
path.join(HOME, '.bash_profile'),
|
|
249
|
+
path.join(HOME, '.profile')
|
|
250
|
+
];
|
|
251
|
+
const lines = [
|
|
252
|
+
'',
|
|
253
|
+
'# Lemma AI Gateway Overrides',
|
|
254
|
+
'export OPENAI_BASE_URL="http://localhost:8081/v1"',
|
|
255
|
+
'export ANTHROPIC_BASE_URL="http://localhost:8081"',
|
|
256
|
+
'export LEMMA_PROJECT="' + project + '"',
|
|
257
|
+
''
|
|
258
|
+
].join('\n');
|
|
259
|
+
for (const profile of profiles) {
|
|
260
|
+
if (fs.existsSync(profile)) {
|
|
261
|
+
try {
|
|
262
|
+
const content = fs.readFileSync(profile, 'utf8');
|
|
263
|
+
if (!content.includes('OPENAI_BASE_URL') && !content.includes('LEMMA_PROJECT')) {
|
|
264
|
+
fs.appendFileSync(profile, lines);
|
|
265
|
+
console.log(`✅ [Shell Profile] Configured ${path.basename(profile)} with local redirect variables!`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch { }
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
console.log(`⚠️ Could not auto-configure shell profiles: ${e.message}`);
|
|
274
|
+
}
|
|
275
|
+
// 3. Cursor & Windsurf settings.json updates
|
|
276
|
+
try {
|
|
277
|
+
const isWin = process.platform === 'win32';
|
|
278
|
+
const isMac = process.platform === 'darwin';
|
|
279
|
+
const candidates = [];
|
|
280
|
+
if (isMac) {
|
|
281
|
+
candidates.push({
|
|
282
|
+
name: 'Cursor',
|
|
283
|
+
path: path.join(HOME, 'Library/Application Support/Cursor/User/settings.json')
|
|
284
|
+
});
|
|
285
|
+
candidates.push({
|
|
286
|
+
name: 'Windsurf',
|
|
287
|
+
path: path.join(HOME, 'Library/Application Support/Windsurf/User/settings.json')
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else if (isWin) {
|
|
291
|
+
const appData = process.env.APPDATA || '';
|
|
292
|
+
candidates.push({
|
|
293
|
+
name: 'Cursor',
|
|
294
|
+
path: path.join(appData, 'Cursor/User/settings.json')
|
|
295
|
+
});
|
|
296
|
+
candidates.push({
|
|
297
|
+
name: 'Windsurf',
|
|
298
|
+
path: path.join(appData, 'Windsurf/User/settings.json')
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
candidates.push({
|
|
303
|
+
name: 'Cursor',
|
|
304
|
+
path: path.join(HOME, '.config/Cursor/User/settings.json')
|
|
305
|
+
});
|
|
306
|
+
candidates.push({
|
|
307
|
+
name: 'Windsurf',
|
|
308
|
+
path: path.join(HOME, '.config/Windsurf/User/settings.json')
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
for (const cand of candidates) {
|
|
312
|
+
const dir = path.dirname(cand.path);
|
|
313
|
+
if (fs.existsSync(dir)) {
|
|
314
|
+
let settings = {};
|
|
315
|
+
if (fs.existsSync(cand.path)) {
|
|
316
|
+
try {
|
|
317
|
+
settings = JSON.parse(fs.readFileSync(cand.path, 'utf8'));
|
|
318
|
+
}
|
|
319
|
+
catch { }
|
|
320
|
+
}
|
|
321
|
+
let updated = false;
|
|
322
|
+
// Settings configuration to route custom openai/anthropic models through the proxy
|
|
323
|
+
const updates = {
|
|
324
|
+
"openai.baseURL": "http://localhost:8081/v1",
|
|
325
|
+
"openai.apiKey": "dummy-key-for-lemma",
|
|
326
|
+
"anthropic.baseURL": "http://localhost:8081",
|
|
327
|
+
"anthropic.apiKey": "dummy-key-for-lemma"
|
|
328
|
+
};
|
|
329
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
330
|
+
if (settings[key] !== value) {
|
|
331
|
+
settings[key] = value;
|
|
332
|
+
updated = true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (updated) {
|
|
336
|
+
fs.writeFileSync(cand.path, JSON.stringify(settings, null, 2));
|
|
337
|
+
console.log(`✅ [${cand.name}] Configured editor settings to route via local gateway!`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
console.log(`⚠️ Could not auto-configure editor settings: ${e.message}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
export function startBackgroundClipboardWatcher() {
|
|
347
|
+
try {
|
|
348
|
+
if (fs.existsSync(CLIPBOARD_PID_FILE)) {
|
|
349
|
+
const oldPid = fs.readFileSync(CLIPBOARD_PID_FILE, 'utf8');
|
|
350
|
+
try {
|
|
351
|
+
process.kill(parseInt(oldPid, 10), 0); // Check if process is still alive
|
|
352
|
+
console.log(`✅ [Clipboard] Watcher is already running in background (PID: ${oldPid})`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// Process is dead, clean up pid file
|
|
357
|
+
try {
|
|
358
|
+
fs.unlinkSync(CLIPBOARD_PID_FILE);
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
console.log(`🔄 Starting Clipboard Watcher in background...`);
|
|
364
|
+
const { spawn } = require('child_process');
|
|
365
|
+
// Resolve path of lemma command or script
|
|
366
|
+
const scriptPath = fs.existsSync(path.join(__dirname, 'dist', 'cjs', 'cli', 'lemma-proxy.js'))
|
|
367
|
+
? path.join(__dirname, 'dist', 'cjs', 'cli', 'lemma-proxy.js')
|
|
368
|
+
: (fs.existsSync(path.join(__dirname, 'lemma-proxy.ts')) ? path.join(__dirname, 'lemma-proxy.ts') : __filename);
|
|
369
|
+
const isTs = scriptPath.endsWith('.ts');
|
|
370
|
+
const cmd = isTs ? 'npx' : 'node';
|
|
371
|
+
const args = isTs ? ['tsx', scriptPath, 'clipboard'] : [scriptPath, 'clipboard'];
|
|
372
|
+
const cp = spawn(cmd, args, {
|
|
373
|
+
stdio: 'ignore',
|
|
374
|
+
detached: true,
|
|
375
|
+
env: { ...process.env }
|
|
376
|
+
});
|
|
377
|
+
cp.unref();
|
|
378
|
+
if (cp.pid) {
|
|
379
|
+
fs.writeFileSync(CLIPBOARD_PID_FILE, String(cp.pid));
|
|
380
|
+
console.log(`✅ [Clipboard] Watcher has been started (PID: ${cp.pid})`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
console.error(`❌ Failed to automatically start Clipboard Watcher in background: ${err.message}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
196
387
|
// ── License helpers ────────────────────────────────────────────────────────────
|
|
388
|
+
let isProCached = null;
|
|
389
|
+
let lastLicenseCheck = 0;
|
|
390
|
+
const LICENSE_CACHE_TTL = 10000; // 10 seconds cache
|
|
197
391
|
async function loadLicense() {
|
|
198
392
|
return readJson(LICENSE_FILE, { isPro: false });
|
|
199
393
|
}
|
|
200
394
|
async function isPro() {
|
|
395
|
+
const now = Date.now();
|
|
396
|
+
if (isProCached !== null && (now - lastLicenseCheck) < LICENSE_CACHE_TTL) {
|
|
397
|
+
return isProCached;
|
|
398
|
+
}
|
|
201
399
|
const l = await loadLicense();
|
|
202
|
-
|
|
400
|
+
isProCached = !!(l && l.isPro);
|
|
401
|
+
lastLicenseCheck = now;
|
|
402
|
+
return isProCached;
|
|
203
403
|
}
|
|
204
404
|
async function validateKeyRemote(key) {
|
|
205
405
|
return new Promise((resolve, reject) => {
|
|
@@ -292,7 +492,15 @@ function cacheGet(provider, prompt) {
|
|
|
292
492
|
return e;
|
|
293
493
|
}
|
|
294
494
|
function cacheSet(provider, prompt, data) {
|
|
295
|
-
|
|
495
|
+
const MAX_CACHE_SIZE = 5000;
|
|
496
|
+
const k = cacheKey(provider, prompt);
|
|
497
|
+
if (!CACHE.has(k) && CACHE.size >= MAX_CACHE_SIZE) {
|
|
498
|
+
// Evict oldest cache entry (FIFO eviction)
|
|
499
|
+
const oldestKey = CACHE.keys().next().value;
|
|
500
|
+
if (oldestKey)
|
|
501
|
+
CACHE.delete(oldestKey);
|
|
502
|
+
}
|
|
503
|
+
CACHE.set(k, { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS, provider });
|
|
296
504
|
}
|
|
297
505
|
// ── Semantic Search (Pro) ──────────────────────────────────────────────────────
|
|
298
506
|
async function getEmbedding(text) {
|
|
@@ -337,7 +545,7 @@ async function semanticGet(provider, prompt) {
|
|
|
337
545
|
distance = distances[0][i];
|
|
338
546
|
}
|
|
339
547
|
const similarity = Math.max(0, 1 - distance);
|
|
340
|
-
const THRESHOLD = 0.7
|
|
548
|
+
const THRESHOLD = 0.82; // raised from 0.7 to 0.82 to prevent false positives
|
|
341
549
|
if (similarity >= THRESHOLD) {
|
|
342
550
|
return {
|
|
343
551
|
data: JSON.parse(metadata.response),
|
|
@@ -564,6 +772,16 @@ async function relayMissStream(res, body, provider, tier) {
|
|
|
564
772
|
req.end();
|
|
565
773
|
});
|
|
566
774
|
}
|
|
775
|
+
class WriteQueue {
|
|
776
|
+
constructor() {
|
|
777
|
+
this.queue = Promise.resolve();
|
|
778
|
+
}
|
|
779
|
+
async enqueue(fn) {
|
|
780
|
+
this.queue = this.queue.then(fn).catch(() => { });
|
|
781
|
+
return this.queue;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const usageWriteQueue = new WriteQueue();
|
|
567
785
|
// ── Proxy Server class ─────────────────────────────────────────────────────────
|
|
568
786
|
class LemmaServer {
|
|
569
787
|
constructor(port, projectName, openBrowser = true) {
|
|
@@ -693,23 +911,76 @@ class LemmaServer {
|
|
|
693
911
|
res.status(500).json({ error: e.message });
|
|
694
912
|
}
|
|
695
913
|
});
|
|
696
|
-
this.app.post('/api/cache/clear', (req, res) => {
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
914
|
+
this.app.post('/api/cache/clear', async (req, res) => {
|
|
915
|
+
const { provider } = req.body || {};
|
|
916
|
+
let clearedCount = 0;
|
|
917
|
+
if (!provider) {
|
|
918
|
+
// Global clear
|
|
919
|
+
clearedCount = CACHE.size;
|
|
920
|
+
CACHE.clear();
|
|
921
|
+
EVENT_LOG.length = 0; // Clear live event log
|
|
922
|
+
savingsLedger.reset(); // Reset savings counters
|
|
923
|
+
// Reset and persist statistics for the current project
|
|
924
|
+
if (this.stats[this.projectName]) {
|
|
925
|
+
this.stats[this.projectName] = {
|
|
926
|
+
total: 0,
|
|
927
|
+
hits: 0,
|
|
928
|
+
misses: 0,
|
|
929
|
+
totalLatency: 0,
|
|
930
|
+
totalTokensSaved: 0,
|
|
931
|
+
providers: {}
|
|
932
|
+
};
|
|
933
|
+
writeJson(STATS_FILE, this.stats).catch(() => { });
|
|
934
|
+
}
|
|
935
|
+
// Clear Chroma DB collection
|
|
936
|
+
try {
|
|
937
|
+
await chroma.deleteCollection({ name: 'lemma-cache' }).catch(() => { });
|
|
938
|
+
}
|
|
939
|
+
catch (e) {
|
|
940
|
+
console.error(`[CacheClear] Failed to clear Chroma collection: ${e.message}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
// Granular clear
|
|
945
|
+
for (const [k, e] of CACHE.entries()) {
|
|
946
|
+
if (e.provider === provider) {
|
|
947
|
+
CACHE.delete(k);
|
|
948
|
+
clearedCount++;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// Selective delete from Chroma DB collection using provider metadata filter
|
|
952
|
+
try {
|
|
953
|
+
const collection = await chroma.getOrCreateCollection({
|
|
954
|
+
name: 'lemma-cache',
|
|
955
|
+
embeddingFunction: dummyEmbeddingFunction,
|
|
956
|
+
metadata: { "hnsw:space": "cosine" }
|
|
957
|
+
});
|
|
958
|
+
await collection.delete({ where: { provider } });
|
|
959
|
+
}
|
|
960
|
+
catch (e) {
|
|
961
|
+
console.error(`[CacheClear] Failed to selectively delete provider ${provider} from Chroma: ${e.message}`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
res.json({ success: true, clearedEntries: clearedCount });
|
|
965
|
+
});
|
|
966
|
+
// Savings breakdown by source for the dashboard
|
|
967
|
+
this.app.get('/api/savings-breakdown', (req, res) => {
|
|
968
|
+
res.json(savingsLedger.getSnapshot());
|
|
969
|
+
});
|
|
970
|
+
// Cross-process savings reporter — MCP and other processes POST here
|
|
971
|
+
this.app.post('/api/savings-record', (req, res) => {
|
|
972
|
+
const { source, charsBefore, charsAfter, tokens } = req.body || {};
|
|
973
|
+
const validSources = ['clipboard', 'contextSqueeze', 'historyPrune', 'complexityRouting', 'cache'];
|
|
974
|
+
if (!source || !validSources.includes(source)) {
|
|
975
|
+
return res.status(400).json({ error: 'Invalid source' });
|
|
976
|
+
}
|
|
977
|
+
if (typeof tokens === 'number' && tokens > 0) {
|
|
978
|
+
savingsLedger.recordTokens(source, tokens);
|
|
711
979
|
}
|
|
712
|
-
|
|
980
|
+
else if (typeof charsBefore === 'number' && typeof charsAfter === 'number') {
|
|
981
|
+
savingsLedger.record(source, charsBefore, charsAfter);
|
|
982
|
+
}
|
|
983
|
+
res.json({ ok: true, snapshot: savingsLedger.getSnapshot().total });
|
|
713
984
|
});
|
|
714
985
|
this.app.get('/dashboard/*', (req, res) => res.sendFile(path.join(this.dashboardPath, 'index.html')));
|
|
715
986
|
}
|
|
@@ -734,76 +1005,36 @@ class LemmaServer {
|
|
|
734
1005
|
logEvent({ type: 'privacy:mask', count: Object.keys(tokenMap).length, project: this.projectName });
|
|
735
1006
|
}
|
|
736
1007
|
const originalModel = req.body.model || 'gpt-4o';
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
if (req.body.model)
|
|
740
|
-
req.body.model = model;
|
|
741
|
-
// Apply the Agent Multiplexer to local-verify the prompt in parallel
|
|
742
|
-
const enrichedPrompt = await agentMultiplexer.multiplexPrompt(prompt, model);
|
|
743
|
-
// Resolve conversation session key and retrieve/create the fileHistory map
|
|
744
|
-
let fileHistory;
|
|
745
|
-
if (req.body.messages && req.body.messages.length > 0) {
|
|
746
|
-
try {
|
|
747
|
-
const firstMsg = req.body.messages[0];
|
|
748
|
-
const content = typeof firstMsg.content === 'string' ? firstMsg.content : JSON.stringify(firstMsg.content);
|
|
749
|
-
const sessionKey = crypto.createHash('sha1').update(content).digest('hex');
|
|
750
|
-
if (!this.sessions.has(sessionKey)) {
|
|
751
|
-
this.sessions.set(sessionKey, { fileHistory: new Map() });
|
|
752
|
-
}
|
|
753
|
-
fileHistory = this.sessions.get(sessionKey).fileHistory;
|
|
754
|
-
}
|
|
755
|
-
catch { }
|
|
756
|
-
}
|
|
757
|
-
// Compress long TS/JS codebase file blocks in the enriched prompt using the Context Squeezer AST Tree-Shaker!
|
|
758
|
-
// The query context is the user's primary prompt to ensure we keep relevant methods!
|
|
759
|
-
const squeezeResult = squeezePrompt(enrichedPrompt, prompt, fileHistory);
|
|
760
|
-
const squeezedPrompt = squeezeResult.squeezed;
|
|
761
|
-
const compressionSavings = squeezeResult.originalSize - squeezeResult.squeezedSize;
|
|
762
|
-
if (compressionSavings > 0) {
|
|
763
|
-
console.log(`⚡ [Squeezer] Compressed prompt from ${squeezeResult.originalSize} to ${squeezeResult.squeezedSize} chars (Saved ${(squeezeResult.compressionRatio * 100).toFixed(1)}% context tokens!)`);
|
|
764
|
-
}
|
|
765
|
-
if (provider === 'openai' || provider === 'anthropic') {
|
|
766
|
-
if (req.body.messages && req.body.messages.length > 0) {
|
|
767
|
-
injectSqueezedContent(req.body.messages[req.body.messages.length - 1], squeezedPrompt);
|
|
768
|
-
// Apply conversational history pruning to save tokens on historical messages!
|
|
769
|
-
const pruneResult = pruneHistoryMessages(req.body.messages);
|
|
770
|
-
req.body.messages = pruneResult.messages;
|
|
771
|
-
historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
else if (provider === 'gemini') {
|
|
775
|
-
if (req.body.contents && req.body.contents.length > 0) {
|
|
776
|
-
const lastContent = req.body.contents[req.body.contents.length - 1];
|
|
777
|
-
if (lastContent.parts && lastContent.parts.length > 0) {
|
|
778
|
-
lastContent.parts[lastContent.parts.length - 1].text = squeezedPrompt;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
}
|
|
1008
|
+
// ── 1. Cache-First Hot Path ──
|
|
1009
|
+
// Perform exact cache check immediately (resolves in <1ms)
|
|
782
1010
|
const hit = cacheGet(provider, prompt);
|
|
783
1011
|
if (hit) {
|
|
784
|
-
|
|
1012
|
+
const cacheTokensSaved = 2000;
|
|
1013
|
+
savingsLedger.recordTokens('cache', cacheTokensSaved);
|
|
1014
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, cacheTokensSaved);
|
|
785
1015
|
const unmaskedData = semanticScrubber.unmask(hit.data, tokenMap);
|
|
786
1016
|
if (isStream)
|
|
787
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1017
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, 1.0);
|
|
788
1018
|
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
789
1019
|
res.setHeader('X-Lemma-Similarity', '1.000');
|
|
790
1020
|
res.setHeader('X-Lemma-Tier', tier);
|
|
791
1021
|
return res.json(unmaskedData);
|
|
792
1022
|
}
|
|
1023
|
+
// Perform semantic cache check (resolves in ~15ms)
|
|
793
1024
|
if (pro) {
|
|
794
1025
|
const semHit = await semanticGet(provider, prompt);
|
|
795
1026
|
if (semHit) {
|
|
796
1027
|
if (semHit.similarity >= 0.90) {
|
|
797
|
-
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000
|
|
1028
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
|
|
798
1029
|
const unmaskedData = semanticScrubber.unmask(semHit.data, tokenMap);
|
|
799
1030
|
if (isStream)
|
|
800
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1031
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, semHit.similarity);
|
|
801
1032
|
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
802
1033
|
res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
|
|
803
1034
|
res.setHeader('X-Lemma-Tier', tier);
|
|
804
1035
|
return res.json(unmaskedData);
|
|
805
1036
|
}
|
|
806
|
-
else if (semHit.similarity >= 0.
|
|
1037
|
+
else if (semHit.similarity >= 0.82) { // raised from 0.70 to 0.82 to prevent false positives
|
|
807
1038
|
console.log(`💡 [CARS] Near Cache Hit (Similarity: ${(semHit.similarity * 100).toFixed(1)}%). Synthesizing answer locally via Ollama...`);
|
|
808
1039
|
let cachedContent = '';
|
|
809
1040
|
if (provider === 'openai') {
|
|
@@ -841,10 +1072,10 @@ Adjusted Answer:`;
|
|
|
841
1072
|
else if (provider === 'anthropic') {
|
|
842
1073
|
synthesizedData.content[0].text = adjustedText;
|
|
843
1074
|
}
|
|
844
|
-
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000
|
|
1075
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
|
|
845
1076
|
const unmaskedData = semanticScrubber.unmask(synthesizedData, tokenMap);
|
|
846
1077
|
if (isStream) {
|
|
847
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1078
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, semHit.similarity);
|
|
848
1079
|
}
|
|
849
1080
|
res.setHeader('X-Lemma-Cache', 'HIT-CARS');
|
|
850
1081
|
res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
|
|
@@ -861,22 +1092,86 @@ Adjusted Answer:`;
|
|
|
861
1092
|
const cloudHit = await cloudSync.get(prompt);
|
|
862
1093
|
if (cloudHit) {
|
|
863
1094
|
cacheSet(provider, prompt, cloudHit.data);
|
|
864
|
-
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000
|
|
1095
|
+
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
|
|
865
1096
|
const unmaskedData = semanticScrubber.unmask(cloudHit.data, tokenMap);
|
|
866
1097
|
if (isStream)
|
|
867
|
-
return simulateHitStream(res, unmaskedData, provider,
|
|
1098
|
+
return simulateHitStream(res, unmaskedData, provider, originalModel, tier, cloudHit.similarity || 0.95);
|
|
868
1099
|
res.setHeader('X-Lemma-Cache', 'HIT');
|
|
869
1100
|
res.setHeader('X-Lemma-Similarity', (cloudHit.similarity || 0.95).toFixed(3));
|
|
870
1101
|
res.setHeader('X-Lemma-Tier', tier);
|
|
871
1102
|
return res.json(unmaskedData);
|
|
872
1103
|
}
|
|
873
1104
|
}
|
|
1105
|
+
// ── 2. Cache Miss Path ──
|
|
1106
|
+
const routingDecision = complexityRouter.evaluate(prompt, originalModel);
|
|
1107
|
+
const model = routingDecision.model;
|
|
1108
|
+
if (req.body.model)
|
|
1109
|
+
req.body.model = model;
|
|
1110
|
+
// Record complexity routing savings when model is downgraded
|
|
1111
|
+
if (routingDecision.complexity === 'low' && model !== originalModel) {
|
|
1112
|
+
const estimatedPromptTokens = Math.floor(prompt.length / 4);
|
|
1113
|
+
const routingSavings = Math.floor(estimatedPromptTokens * 0.8);
|
|
1114
|
+
savingsLedger.recordTokens('complexityRouting', routingSavings);
|
|
1115
|
+
}
|
|
1116
|
+
// Apply the Agent Multiplexer to local-verify the prompt in parallel
|
|
1117
|
+
const enrichedPrompt = await agentMultiplexer.multiplexPrompt(prompt, model);
|
|
1118
|
+
// Resolve conversation session key and retrieve/create the fileHistory map
|
|
1119
|
+
let fileHistory;
|
|
1120
|
+
if (req.body.messages && req.body.messages.length > 0) {
|
|
1121
|
+
try {
|
|
1122
|
+
const firstMsg = req.body.messages[0];
|
|
1123
|
+
const content = typeof firstMsg.content === 'string' ? firstMsg.content : JSON.stringify(firstMsg.content);
|
|
1124
|
+
const sessionKey = crypto.createHash('sha1').update(content).digest('hex');
|
|
1125
|
+
if (!this.sessions.has(sessionKey)) {
|
|
1126
|
+
// Enforce 500 active sessions cap to prevent memory leak (FIFO eviction)
|
|
1127
|
+
const MAX_SESSIONS = 500;
|
|
1128
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
1129
|
+
const oldestKey = this.sessions.keys().next().value;
|
|
1130
|
+
if (oldestKey)
|
|
1131
|
+
this.sessions.delete(oldestKey);
|
|
1132
|
+
}
|
|
1133
|
+
this.sessions.set(sessionKey, { fileHistory: new Map() });
|
|
1134
|
+
}
|
|
1135
|
+
fileHistory = this.sessions.get(sessionKey).fileHistory;
|
|
1136
|
+
}
|
|
1137
|
+
catch { }
|
|
1138
|
+
}
|
|
1139
|
+
// Compress long TS/JS codebase file blocks in the enriched prompt
|
|
1140
|
+
const squeezeResult = squeezePrompt(enrichedPrompt, prompt, fileHistory);
|
|
1141
|
+
const squeezedPrompt = squeezeResult.squeezed;
|
|
1142
|
+
const compressionSavings = squeezeResult.originalSize - squeezeResult.squeezedSize;
|
|
1143
|
+
if (compressionSavings > 0) {
|
|
1144
|
+
savingsLedger.record('contextSqueeze', squeezeResult.originalSize, squeezeResult.squeezedSize);
|
|
1145
|
+
console.log(`⚡ [Squeezer] Compressed prompt from ${squeezeResult.originalSize} to ${squeezeResult.squeezedSize} chars (Saved ${(squeezeResult.compressionRatio * 100).toFixed(1)}% context tokens!)`);
|
|
1146
|
+
}
|
|
1147
|
+
if (provider === 'openai' || provider === 'anthropic') {
|
|
1148
|
+
if (req.body.messages && req.body.messages.length > 0) {
|
|
1149
|
+
injectSqueezedContent(req.body.messages[req.body.messages.length - 1], squeezedPrompt);
|
|
1150
|
+
// Apply conversational history pruning
|
|
1151
|
+
const pruneResult = pruneHistoryMessages(req.body.messages);
|
|
1152
|
+
req.body.messages = pruneResult.messages;
|
|
1153
|
+
historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
|
|
1154
|
+
if (pruneResult.tokensSavedEstimate > 0) {
|
|
1155
|
+
savingsLedger.recordTokens('historyPrune', pruneResult.tokensSavedEstimate);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
else if (provider === 'gemini') {
|
|
1160
|
+
if (req.body.contents && req.body.contents.length > 0) {
|
|
1161
|
+
const lastContent = req.body.contents[req.body.contents.length - 1];
|
|
1162
|
+
if (lastContent.parts && lastContent.parts.length > 0) {
|
|
1163
|
+
lastContent.parts[lastContent.parts.length - 1].text = squeezedPrompt;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
874
1167
|
if (isStream) {
|
|
875
1168
|
await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
|
|
876
1169
|
if (!pro) {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1170
|
+
await usageWriteQueue.enqueue(async () => {
|
|
1171
|
+
const u = await loadUsage();
|
|
1172
|
+
u.count++;
|
|
1173
|
+
await writeJson(USAGE_FILE, u);
|
|
1174
|
+
});
|
|
880
1175
|
}
|
|
881
1176
|
return relayMissStream(res, req.body, provider, tier);
|
|
882
1177
|
}
|
|
@@ -888,9 +1183,11 @@ Adjusted Answer:`;
|
|
|
888
1183
|
await cloudSync.set(prompt, data);
|
|
889
1184
|
}
|
|
890
1185
|
if (!pro) {
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1186
|
+
await usageWriteQueue.enqueue(async () => {
|
|
1187
|
+
const u = await loadUsage();
|
|
1188
|
+
u.count++;
|
|
1189
|
+
await writeJson(USAGE_FILE, u);
|
|
1190
|
+
});
|
|
894
1191
|
}
|
|
895
1192
|
await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
|
|
896
1193
|
return res.json(semanticScrubber.unmask(data, tokenMap));
|
|
@@ -927,6 +1224,15 @@ Adjusted Answer:`;
|
|
|
927
1224
|
}
|
|
928
1225
|
async start() {
|
|
929
1226
|
this.stats = await loadStats();
|
|
1227
|
+
// Start active cache eviction sweep every hour
|
|
1228
|
+
setInterval(() => {
|
|
1229
|
+
const now = Date.now();
|
|
1230
|
+
for (const [k, e] of CACHE.entries()) {
|
|
1231
|
+
if (e.ttl > 0 && now - e.createdAt > e.ttl) {
|
|
1232
|
+
CACHE.delete(k);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}, 3600000).unref();
|
|
930
1236
|
const server = http.createServer(this.app);
|
|
931
1237
|
server.on('error', (e) => {
|
|
932
1238
|
if (e.code === 'EADDRINUSE') {
|
|
@@ -1046,10 +1352,17 @@ program.command('start')
|
|
|
1046
1352
|
if (resp.data && resp.data.status === 'ok') {
|
|
1047
1353
|
console.log(`\n⚠️ Lemma Proxy is already running on port ${port} (Project: ${resp.data.project})`);
|
|
1048
1354
|
console.log(`💡 Use 'lemma status' for details or 'lemma stop' to restart.\n`);
|
|
1355
|
+
// Keep configurations up to date even if server was already started
|
|
1356
|
+
const runningProject = resp.data.project || detectProject();
|
|
1357
|
+
autoConfigureAll(runningProject);
|
|
1358
|
+
startBackgroundClipboardWatcher();
|
|
1049
1359
|
process.exit(0);
|
|
1050
1360
|
}
|
|
1051
1361
|
}
|
|
1052
1362
|
catch { }
|
|
1363
|
+
const startProject = opts.project || detectProject();
|
|
1364
|
+
autoConfigureAll(startProject);
|
|
1365
|
+
startBackgroundClipboardWatcher();
|
|
1053
1366
|
// Automatically enable autopilot if stack is enabled and user is Pro
|
|
1054
1367
|
const runAutopilot = opts.autopilot || opts.stack;
|
|
1055
1368
|
if (runAutopilot) {
|
|
@@ -1148,6 +1461,19 @@ program.command('stop')
|
|
|
1148
1461
|
}
|
|
1149
1462
|
if (fs.existsSync(PID_FILE))
|
|
1150
1463
|
fs.unlinkSync(PID_FILE);
|
|
1464
|
+
// Stop background Clipboard Watcher if active
|
|
1465
|
+
try {
|
|
1466
|
+
if (fs.existsSync(CLIPBOARD_PID_FILE)) {
|
|
1467
|
+
const cbPid = fs.readFileSync(CLIPBOARD_PID_FILE, 'utf8');
|
|
1468
|
+
try {
|
|
1469
|
+
process.kill(parseInt(cbPid, 10), 'SIGTERM');
|
|
1470
|
+
console.log(`✅ Stopped background Clipboard Watcher (PID: ${cbPid})`);
|
|
1471
|
+
}
|
|
1472
|
+
catch { }
|
|
1473
|
+
fs.unlinkSync(CLIPBOARD_PID_FILE);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
catch { }
|
|
1151
1477
|
});
|
|
1152
1478
|
program.command('status')
|
|
1153
1479
|
.description('Check proxy status')
|
|
@@ -1241,41 +1567,8 @@ program.command('init')
|
|
|
1241
1567
|
fs.writeFileSync(envFile, lemmaConfig);
|
|
1242
1568
|
console.log('✅ Created .env with Lemma configuration');
|
|
1243
1569
|
}
|
|
1244
|
-
console.log('\n🧠 \x1b[35mAuto-Setup MCP for AI IDEs
|
|
1245
|
-
|
|
1246
|
-
const { exec } = require('child_process');
|
|
1247
|
-
const isWin = process.platform === 'win32';
|
|
1248
|
-
const claudePath = isWin
|
|
1249
|
-
? path.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json')
|
|
1250
|
-
: path.join(process.env.HOME || '~', 'Library/Application Support/Claude/claude_desktop_config.json');
|
|
1251
|
-
if (fs.existsSync(path.dirname(claudePath))) {
|
|
1252
|
-
try {
|
|
1253
|
-
let config = { mcpServers: {} };
|
|
1254
|
-
if (fs.existsSync(claudePath)) {
|
|
1255
|
-
config = JSON.parse(fs.readFileSync(claudePath, 'utf8'));
|
|
1256
|
-
}
|
|
1257
|
-
if (!config.mcpServers)
|
|
1258
|
-
config.mcpServers = {};
|
|
1259
|
-
config.mcpServers.lemma = {
|
|
1260
|
-
command: 'npx',
|
|
1261
|
-
args: ['-y', '@nxuss/lemma', 'mcp'],
|
|
1262
|
-
env: { LEMMA_PROJECT: project }
|
|
1263
|
-
};
|
|
1264
|
-
fs.writeFileSync(claudePath, JSON.stringify(config, null, 2));
|
|
1265
|
-
console.log(`✅ \x1b[32mClaude Desktop configured automatically!\x1b[0m (Restart Claude to activate)`);
|
|
1266
|
-
}
|
|
1267
|
-
catch (e) {
|
|
1268
|
-
console.log('⚠️ Could not auto-configure Claude Desktop. No worries, you can do it manually.');
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
console.log('🚀 Opening IDE configuration pages...');
|
|
1272
|
-
const urls = [
|
|
1273
|
-
'https://docs.anthropic.com/en/docs/agents-and-tools/mcp#using-mcp-in-claude-desktop',
|
|
1274
|
-
'https://cursor.com',
|
|
1275
|
-
'https://codeium.com/windsurf'
|
|
1276
|
-
];
|
|
1277
|
-
const opener = process.platform === 'darwin' ? 'open' : (process.platform === 'win32' ? 'start' : 'xdg-open');
|
|
1278
|
-
urls.forEach(url => exec(`${opener} ${url}`));
|
|
1570
|
+
console.log('\n🧠 \x1b[35mAuto-Setup MCP and redirection overrides for AI IDEs...\x1b[0m');
|
|
1571
|
+
autoConfigureAll(project);
|
|
1279
1572
|
console.log('\n✨ Project initialized for the Agentic Era!');
|
|
1280
1573
|
console.log('🚀 Run "lemma start" to begin.\n');
|
|
1281
1574
|
});
|