@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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/dashboard/dist/assets/{index-B4TA31pA.js → index-CIlkFwDs.js} +98 -98
  3. package/dashboard/dist/assets/index-CIlkFwDs.js.map +1 -0
  4. package/dashboard/dist/index.html +1 -1
  5. package/dist/cjs/cli/lemma-proxy.d.ts +2 -0
  6. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  7. package/dist/cjs/cli/lemma-proxy.js +416 -121
  8. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  9. package/dist/cjs/config/index.d.ts.map +1 -1
  10. package/dist/cjs/config/index.js +2 -1
  11. package/dist/cjs/config/index.js.map +1 -1
  12. package/dist/cjs/errors/LemmaError.d.ts +52 -0
  13. package/dist/cjs/errors/LemmaError.d.ts.map +1 -0
  14. package/dist/cjs/errors/LemmaError.js +84 -0
  15. package/dist/cjs/errors/LemmaError.js.map +1 -0
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +24 -22
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/mcp/index.js +81 -0
  20. package/dist/cjs/mcp/index.js.map +1 -1
  21. package/dist/cjs/subconscious/SemanticCache.d.ts +4 -4
  22. package/dist/cjs/subconscious/SemanticCache.d.ts.map +1 -1
  23. package/dist/cjs/subconscious/SemanticCache.js +14 -10
  24. package/dist/cjs/subconscious/SemanticCache.js.map +1 -1
  25. package/dist/cjs/types/index.d.ts +8 -8
  26. package/dist/cjs/types/index.d.ts.map +1 -1
  27. package/dist/cjs/utils/ClipboardWatcher.d.ts.map +1 -1
  28. package/dist/cjs/utils/ClipboardWatcher.js +3 -0
  29. package/dist/cjs/utils/ClipboardWatcher.js.map +1 -1
  30. package/dist/cjs/utils/SavingsLedger.d.ts +52 -0
  31. package/dist/cjs/utils/SavingsLedger.d.ts.map +1 -0
  32. package/dist/cjs/utils/SavingsLedger.js +101 -0
  33. package/dist/cjs/utils/SavingsLedger.js.map +1 -0
  34. package/dist/cjs/utils/logger.d.ts +27 -6
  35. package/dist/cjs/utils/logger.d.ts.map +1 -1
  36. package/dist/cjs/utils/logger.js +96 -28
  37. package/dist/cjs/utils/logger.js.map +1 -1
  38. package/dist/cjs/utils/reportSavings.d.ts +24 -0
  39. package/dist/cjs/utils/reportSavings.d.ts.map +1 -0
  40. package/dist/cjs/utils/reportSavings.js +53 -0
  41. package/dist/cjs/utils/reportSavings.js.map +1 -0
  42. package/dist/esm/cli/lemma-proxy.d.ts +2 -0
  43. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  44. package/dist/esm/cli/lemma-proxy.js +414 -121
  45. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  46. package/dist/esm/config/index.d.ts.map +1 -1
  47. package/dist/esm/config/index.js +2 -1
  48. package/dist/esm/config/index.js.map +1 -1
  49. package/dist/esm/errors/LemmaError.d.ts +52 -0
  50. package/dist/esm/errors/LemmaError.d.ts.map +1 -0
  51. package/dist/esm/errors/LemmaError.js +75 -0
  52. package/dist/esm/errors/LemmaError.js.map +1 -0
  53. package/dist/esm/index.d.ts.map +1 -1
  54. package/dist/esm/index.js +24 -22
  55. package/dist/esm/index.js.map +1 -1
  56. package/dist/esm/mcp/index.js +81 -0
  57. package/dist/esm/mcp/index.js.map +1 -1
  58. package/dist/esm/subconscious/SemanticCache.d.ts +4 -4
  59. package/dist/esm/subconscious/SemanticCache.d.ts.map +1 -1
  60. package/dist/esm/subconscious/SemanticCache.js +14 -10
  61. package/dist/esm/subconscious/SemanticCache.js.map +1 -1
  62. package/dist/esm/types/index.d.ts +8 -8
  63. package/dist/esm/types/index.d.ts.map +1 -1
  64. package/dist/esm/utils/ClipboardWatcher.d.ts.map +1 -1
  65. package/dist/esm/utils/ClipboardWatcher.js +3 -0
  66. package/dist/esm/utils/ClipboardWatcher.js.map +1 -1
  67. package/dist/esm/utils/SavingsLedger.d.ts +52 -0
  68. package/dist/esm/utils/SavingsLedger.d.ts.map +1 -0
  69. package/dist/esm/utils/SavingsLedger.js +98 -0
  70. package/dist/esm/utils/SavingsLedger.js.map +1 -0
  71. package/dist/esm/utils/logger.d.ts +27 -6
  72. package/dist/esm/utils/logger.d.ts.map +1 -1
  73. package/dist/esm/utils/logger.js +93 -28
  74. package/dist/esm/utils/logger.js.map +1 -1
  75. package/dist/esm/utils/reportSavings.d.ts +24 -0
  76. package/dist/esm/utils/reportSavings.d.ts.map +1 -0
  77. package/dist/esm/utils/reportSavings.js +47 -0
  78. package/dist/esm/utils/reportSavings.js.map +1 -0
  79. package/package.json +27 -24
  80. 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.4.6';
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
- try {
75
- const tempFile = file + '.tmp';
76
- await fs.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
77
- await fs.promises.rename(tempFile, file);
78
- }
79
- catch { }
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
- return !!(l && l.isPro);
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
- CACHE.set(cacheKey(provider, prompt), { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS });
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 count = CACHE.size;
698
- CACHE.clear();
699
- EVENT_LOG.length = 0; // Clear live event log
700
- // Reset and persist statistics for the current project
701
- if (this.stats[this.projectName]) {
702
- this.stats[this.projectName] = {
703
- total: 0,
704
- hits: 0,
705
- misses: 0,
706
- totalLatency: 0,
707
- totalTokensSaved: 0,
708
- providers: {}
709
- };
710
- writeJson(STATS_FILE, this.stats).catch(() => { });
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
- res.json({ success: true, clearedEntries: count });
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
- const routingDecision = complexityRouter.evaluate(prompt, originalModel);
738
- const model = routingDecision.model;
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
- await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000 + historyTokensSaved);
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, model, tier, 1.0);
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 + historyTokensSaved);
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, model, tier, semHit.similarity);
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.70) {
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 + historyTokensSaved);
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, model, tier, semHit.similarity);
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 + historyTokensSaved);
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, model, tier, cloudHit.similarity || 0.95);
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
- const u = await loadUsage();
878
- u.count++;
879
- await writeJson(USAGE_FILE, u);
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
- const u = await loadUsage();
892
- u.count++;
893
- await writeJson(USAGE_FILE, u);
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?\x1b[0m');
1245
- console.log(' Lemma can automatically configure Claude Desktop and open settings for Cursor/Windsurf.');
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
  });