@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
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.autoConfigureAll = autoConfigureAll;
8
+ exports.startBackgroundClipboardWatcher = startBackgroundClipboardWatcher;
7
9
  exports.performAutoHeal = performAutoHeal;
8
10
  const commander_1 = require("commander");
9
11
  const express_1 = __importDefault(require("express"));
@@ -30,7 +32,7 @@ function getVersion() {
30
32
  catch { }
31
33
  }
32
34
  }
33
- return '0.4.6';
35
+ return '0.5.3';
34
36
  }
35
37
  const VERSION = getVersion();
36
38
  const ComplexityRouter_1 = __importDefault(require("../proxy/ComplexityRouter"));
@@ -38,6 +40,7 @@ const SemanticScrubber_1 = __importDefault(require("../security/SemanticScrubber
38
40
  const CloudSyncClient_1 = __importDefault(require("../cloud/CloudSyncClient"));
39
41
  const ContextSqueezer_1 = require("../utils/ContextSqueezer");
40
42
  const AgentMultiplexer_1 = require("../proxy/AgentMultiplexer");
43
+ const SavingsLedger_1 = require("../utils/SavingsLedger");
41
44
  const chroma = new chromadb_1.ChromaClient({ host: 'localhost', port: 8000 });
42
45
  const dummyEmbeddingFunction = { generate: (texts) => Promise.resolve([]) };
43
46
  const complexityRouter = new ComplexityRouter_1.default();
@@ -59,6 +62,13 @@ function logEvent(event) {
59
62
  EVENT_LOG.push(ev);
60
63
  if (EVENT_LOG.length > MAX_EVENTS)
61
64
  EVENT_LOG.shift();
65
+ // Print a terminal savings summary every 10 events
66
+ if (EVENT_LOG.length > 0 && EVENT_LOG.length % 10 === 0) {
67
+ const snap = SavingsLedger_1.savingsLedger.getSnapshot();
68
+ if (snap.total.tokensSaved > 0) {
69
+ console.log('\n' + SavingsLedger_1.savingsLedger.getTerminalSummary() + '\n');
70
+ }
71
+ }
62
72
  }
63
73
  const FREE_LIMIT = 500;
64
74
  const WARN_PCT = 0.8;
@@ -76,13 +86,20 @@ async function readJson(file, fallback) {
76
86
  catch { }
77
87
  return fallback;
78
88
  }
89
+ const writeQueues = new Map();
79
90
  async function writeJson(file, data) {
80
- try {
81
- const tempFile = file + '.tmp';
82
- await fs_1.default.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
83
- await fs_1.default.promises.rename(tempFile, file);
84
- }
85
- catch { }
91
+ const chain = writeQueues.get(file) || Promise.resolve();
92
+ const writeOperation = async () => {
93
+ try {
94
+ const tempFile = `${file}.${crypto_1.default.randomUUID ? crypto_1.default.randomUUID() : Math.random().toString(36).substring(2, 15)}.tmp`;
95
+ await fs_1.default.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
96
+ await fs_1.default.promises.rename(tempFile, file);
97
+ }
98
+ catch { }
99
+ };
100
+ const nextChain = chain.then(writeOperation).catch(() => { });
101
+ writeQueues.set(file, nextChain);
102
+ return nextChain;
86
103
  }
87
104
  function projectHash(name) {
88
105
  return crypto_1.default.createHash('sha1').update(name).digest('hex').slice(0, 12);
@@ -199,13 +216,198 @@ function ensureGitIgnore() {
199
216
  catch { }
200
217
  return added;
201
218
  }
219
+ const CLIPBOARD_PID_FILE = path_1.default.join(CACHE_DIR, 'clipboard.pid');
220
+ function autoConfigureAll(projectName) {
221
+ const project = projectName || detectProject();
222
+ const HOME = process.env.HOME || process.env.USERPROFILE || '~';
223
+ // 1. Claude Desktop Config
224
+ try {
225
+ const isWin = process.platform === 'win32';
226
+ const claudePath = isWin
227
+ ? path_1.default.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json')
228
+ : path_1.default.join(HOME, 'Library/Application Support/Claude/claude_desktop_config.json');
229
+ if (fs_1.default.existsSync(path_1.default.dirname(claudePath))) {
230
+ let config = { mcpServers: {} };
231
+ if (fs_1.default.existsSync(claudePath)) {
232
+ try {
233
+ config = JSON.parse(fs_1.default.readFileSync(claudePath, 'utf8'));
234
+ }
235
+ catch { }
236
+ }
237
+ if (!config.mcpServers)
238
+ config.mcpServers = {};
239
+ config.mcpServers.lemma = {
240
+ command: 'npx',
241
+ args: ['-y', '@nxuss/lemma', 'mcp'],
242
+ env: { LEMMA_PROJECT: project }
243
+ };
244
+ fs_1.default.writeFileSync(claudePath, JSON.stringify(config, null, 2));
245
+ console.log(`✅ [Claude Desktop] Configured automatically! (Restart Claude to activate)`);
246
+ }
247
+ }
248
+ catch (e) {
249
+ console.log(`⚠️ Could not auto-configure Claude Desktop: ${e.message}`);
250
+ }
251
+ // 2. Shell Profiles Environment Override
252
+ try {
253
+ const profiles = [
254
+ path_1.default.join(HOME, '.zshrc'),
255
+ path_1.default.join(HOME, '.bashrc'),
256
+ path_1.default.join(HOME, '.bash_profile'),
257
+ path_1.default.join(HOME, '.profile')
258
+ ];
259
+ const lines = [
260
+ '',
261
+ '# Lemma AI Gateway Overrides',
262
+ 'export OPENAI_BASE_URL="http://localhost:8081/v1"',
263
+ 'export ANTHROPIC_BASE_URL="http://localhost:8081"',
264
+ 'export LEMMA_PROJECT="' + project + '"',
265
+ ''
266
+ ].join('\n');
267
+ for (const profile of profiles) {
268
+ if (fs_1.default.existsSync(profile)) {
269
+ try {
270
+ const content = fs_1.default.readFileSync(profile, 'utf8');
271
+ if (!content.includes('OPENAI_BASE_URL') && !content.includes('LEMMA_PROJECT')) {
272
+ fs_1.default.appendFileSync(profile, lines);
273
+ console.log(`✅ [Shell Profile] Configured ${path_1.default.basename(profile)} with local redirect variables!`);
274
+ }
275
+ }
276
+ catch { }
277
+ }
278
+ }
279
+ }
280
+ catch (e) {
281
+ console.log(`⚠️ Could not auto-configure shell profiles: ${e.message}`);
282
+ }
283
+ // 3. Cursor & Windsurf settings.json updates
284
+ try {
285
+ const isWin = process.platform === 'win32';
286
+ const isMac = process.platform === 'darwin';
287
+ const candidates = [];
288
+ if (isMac) {
289
+ candidates.push({
290
+ name: 'Cursor',
291
+ path: path_1.default.join(HOME, 'Library/Application Support/Cursor/User/settings.json')
292
+ });
293
+ candidates.push({
294
+ name: 'Windsurf',
295
+ path: path_1.default.join(HOME, 'Library/Application Support/Windsurf/User/settings.json')
296
+ });
297
+ }
298
+ else if (isWin) {
299
+ const appData = process.env.APPDATA || '';
300
+ candidates.push({
301
+ name: 'Cursor',
302
+ path: path_1.default.join(appData, 'Cursor/User/settings.json')
303
+ });
304
+ candidates.push({
305
+ name: 'Windsurf',
306
+ path: path_1.default.join(appData, 'Windsurf/User/settings.json')
307
+ });
308
+ }
309
+ else {
310
+ candidates.push({
311
+ name: 'Cursor',
312
+ path: path_1.default.join(HOME, '.config/Cursor/User/settings.json')
313
+ });
314
+ candidates.push({
315
+ name: 'Windsurf',
316
+ path: path_1.default.join(HOME, '.config/Windsurf/User/settings.json')
317
+ });
318
+ }
319
+ for (const cand of candidates) {
320
+ const dir = path_1.default.dirname(cand.path);
321
+ if (fs_1.default.existsSync(dir)) {
322
+ let settings = {};
323
+ if (fs_1.default.existsSync(cand.path)) {
324
+ try {
325
+ settings = JSON.parse(fs_1.default.readFileSync(cand.path, 'utf8'));
326
+ }
327
+ catch { }
328
+ }
329
+ let updated = false;
330
+ // Settings configuration to route custom openai/anthropic models through the proxy
331
+ const updates = {
332
+ "openai.baseURL": "http://localhost:8081/v1",
333
+ "openai.apiKey": "dummy-key-for-lemma",
334
+ "anthropic.baseURL": "http://localhost:8081",
335
+ "anthropic.apiKey": "dummy-key-for-lemma"
336
+ };
337
+ for (const [key, value] of Object.entries(updates)) {
338
+ if (settings[key] !== value) {
339
+ settings[key] = value;
340
+ updated = true;
341
+ }
342
+ }
343
+ if (updated) {
344
+ fs_1.default.writeFileSync(cand.path, JSON.stringify(settings, null, 2));
345
+ console.log(`✅ [${cand.name}] Configured editor settings to route via local gateway!`);
346
+ }
347
+ }
348
+ }
349
+ }
350
+ catch (e) {
351
+ console.log(`⚠️ Could not auto-configure editor settings: ${e.message}`);
352
+ }
353
+ }
354
+ function startBackgroundClipboardWatcher() {
355
+ try {
356
+ if (fs_1.default.existsSync(CLIPBOARD_PID_FILE)) {
357
+ const oldPid = fs_1.default.readFileSync(CLIPBOARD_PID_FILE, 'utf8');
358
+ try {
359
+ process.kill(parseInt(oldPid, 10), 0); // Check if process is still alive
360
+ console.log(`✅ [Clipboard] Watcher is already running in background (PID: ${oldPid})`);
361
+ return;
362
+ }
363
+ catch {
364
+ // Process is dead, clean up pid file
365
+ try {
366
+ fs_1.default.unlinkSync(CLIPBOARD_PID_FILE);
367
+ }
368
+ catch { }
369
+ }
370
+ }
371
+ console.log(`🔄 Starting Clipboard Watcher in background...`);
372
+ const { spawn } = require('child_process');
373
+ // Resolve path of lemma command or script
374
+ const scriptPath = fs_1.default.existsSync(path_1.default.join(__dirname, 'dist', 'cjs', 'cli', 'lemma-proxy.js'))
375
+ ? path_1.default.join(__dirname, 'dist', 'cjs', 'cli', 'lemma-proxy.js')
376
+ : (fs_1.default.existsSync(path_1.default.join(__dirname, 'lemma-proxy.ts')) ? path_1.default.join(__dirname, 'lemma-proxy.ts') : __filename);
377
+ const isTs = scriptPath.endsWith('.ts');
378
+ const cmd = isTs ? 'npx' : 'node';
379
+ const args = isTs ? ['tsx', scriptPath, 'clipboard'] : [scriptPath, 'clipboard'];
380
+ const cp = spawn(cmd, args, {
381
+ stdio: 'ignore',
382
+ detached: true,
383
+ env: { ...process.env }
384
+ });
385
+ cp.unref();
386
+ if (cp.pid) {
387
+ fs_1.default.writeFileSync(CLIPBOARD_PID_FILE, String(cp.pid));
388
+ console.log(`✅ [Clipboard] Watcher has been started (PID: ${cp.pid})`);
389
+ }
390
+ }
391
+ catch (err) {
392
+ console.error(`❌ Failed to automatically start Clipboard Watcher in background: ${err.message}`);
393
+ }
394
+ }
202
395
  // ── License helpers ────────────────────────────────────────────────────────────
396
+ let isProCached = null;
397
+ let lastLicenseCheck = 0;
398
+ const LICENSE_CACHE_TTL = 10000; // 10 seconds cache
203
399
  async function loadLicense() {
204
400
  return readJson(LICENSE_FILE, { isPro: false });
205
401
  }
206
402
  async function isPro() {
403
+ const now = Date.now();
404
+ if (isProCached !== null && (now - lastLicenseCheck) < LICENSE_CACHE_TTL) {
405
+ return isProCached;
406
+ }
207
407
  const l = await loadLicense();
208
- return !!(l && l.isPro);
408
+ isProCached = !!(l && l.isPro);
409
+ lastLicenseCheck = now;
410
+ return isProCached;
209
411
  }
210
412
  async function validateKeyRemote(key) {
211
413
  return new Promise((resolve, reject) => {
@@ -298,7 +500,15 @@ function cacheGet(provider, prompt) {
298
500
  return e;
299
501
  }
300
502
  function cacheSet(provider, prompt, data) {
301
- CACHE.set(cacheKey(provider, prompt), { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS });
503
+ const MAX_CACHE_SIZE = 5000;
504
+ const k = cacheKey(provider, prompt);
505
+ if (!CACHE.has(k) && CACHE.size >= MAX_CACHE_SIZE) {
506
+ // Evict oldest cache entry (FIFO eviction)
507
+ const oldestKey = CACHE.keys().next().value;
508
+ if (oldestKey)
509
+ CACHE.delete(oldestKey);
510
+ }
511
+ CACHE.set(k, { input: prompt, data, createdAt: Date.now(), ttl: TTL_MS, provider });
302
512
  }
303
513
  // ── Semantic Search (Pro) ──────────────────────────────────────────────────────
304
514
  async function getEmbedding(text) {
@@ -343,7 +553,7 @@ async function semanticGet(provider, prompt) {
343
553
  distance = distances[0][i];
344
554
  }
345
555
  const similarity = Math.max(0, 1 - distance);
346
- const THRESHOLD = 0.7;
556
+ const THRESHOLD = 0.82; // raised from 0.7 to 0.82 to prevent false positives
347
557
  if (similarity >= THRESHOLD) {
348
558
  return {
349
559
  data: JSON.parse(metadata.response),
@@ -570,6 +780,16 @@ async function relayMissStream(res, body, provider, tier) {
570
780
  req.end();
571
781
  });
572
782
  }
783
+ class WriteQueue {
784
+ constructor() {
785
+ this.queue = Promise.resolve();
786
+ }
787
+ async enqueue(fn) {
788
+ this.queue = this.queue.then(fn).catch(() => { });
789
+ return this.queue;
790
+ }
791
+ }
792
+ const usageWriteQueue = new WriteQueue();
573
793
  // ── Proxy Server class ─────────────────────────────────────────────────────────
574
794
  class LemmaServer {
575
795
  constructor(port, projectName, openBrowser = true) {
@@ -699,23 +919,76 @@ class LemmaServer {
699
919
  res.status(500).json({ error: e.message });
700
920
  }
701
921
  });
702
- this.app.post('/api/cache/clear', (req, res) => {
703
- const count = CACHE.size;
704
- CACHE.clear();
705
- EVENT_LOG.length = 0; // Clear live event log
706
- // Reset and persist statistics for the current project
707
- if (this.stats[this.projectName]) {
708
- this.stats[this.projectName] = {
709
- total: 0,
710
- hits: 0,
711
- misses: 0,
712
- totalLatency: 0,
713
- totalTokensSaved: 0,
714
- providers: {}
715
- };
716
- writeJson(STATS_FILE, this.stats).catch(() => { });
922
+ this.app.post('/api/cache/clear', async (req, res) => {
923
+ const { provider } = req.body || {};
924
+ let clearedCount = 0;
925
+ if (!provider) {
926
+ // Global clear
927
+ clearedCount = CACHE.size;
928
+ CACHE.clear();
929
+ EVENT_LOG.length = 0; // Clear live event log
930
+ SavingsLedger_1.savingsLedger.reset(); // Reset savings counters
931
+ // Reset and persist statistics for the current project
932
+ if (this.stats[this.projectName]) {
933
+ this.stats[this.projectName] = {
934
+ total: 0,
935
+ hits: 0,
936
+ misses: 0,
937
+ totalLatency: 0,
938
+ totalTokensSaved: 0,
939
+ providers: {}
940
+ };
941
+ writeJson(STATS_FILE, this.stats).catch(() => { });
942
+ }
943
+ // Clear Chroma DB collection
944
+ try {
945
+ await chroma.deleteCollection({ name: 'lemma-cache' }).catch(() => { });
946
+ }
947
+ catch (e) {
948
+ console.error(`[CacheClear] Failed to clear Chroma collection: ${e.message}`);
949
+ }
950
+ }
951
+ else {
952
+ // Granular clear
953
+ for (const [k, e] of CACHE.entries()) {
954
+ if (e.provider === provider) {
955
+ CACHE.delete(k);
956
+ clearedCount++;
957
+ }
958
+ }
959
+ // Selective delete from Chroma DB collection using provider metadata filter
960
+ try {
961
+ const collection = await chroma.getOrCreateCollection({
962
+ name: 'lemma-cache',
963
+ embeddingFunction: dummyEmbeddingFunction,
964
+ metadata: { "hnsw:space": "cosine" }
965
+ });
966
+ await collection.delete({ where: { provider } });
967
+ }
968
+ catch (e) {
969
+ console.error(`[CacheClear] Failed to selectively delete provider ${provider} from Chroma: ${e.message}`);
970
+ }
971
+ }
972
+ res.json({ success: true, clearedEntries: clearedCount });
973
+ });
974
+ // Savings breakdown by source for the dashboard
975
+ this.app.get('/api/savings-breakdown', (req, res) => {
976
+ res.json(SavingsLedger_1.savingsLedger.getSnapshot());
977
+ });
978
+ // Cross-process savings reporter — MCP and other processes POST here
979
+ this.app.post('/api/savings-record', (req, res) => {
980
+ const { source, charsBefore, charsAfter, tokens } = req.body || {};
981
+ const validSources = ['clipboard', 'contextSqueeze', 'historyPrune', 'complexityRouting', 'cache'];
982
+ if (!source || !validSources.includes(source)) {
983
+ return res.status(400).json({ error: 'Invalid source' });
984
+ }
985
+ if (typeof tokens === 'number' && tokens > 0) {
986
+ SavingsLedger_1.savingsLedger.recordTokens(source, tokens);
717
987
  }
718
- res.json({ success: true, clearedEntries: count });
988
+ else if (typeof charsBefore === 'number' && typeof charsAfter === 'number') {
989
+ SavingsLedger_1.savingsLedger.record(source, charsBefore, charsAfter);
990
+ }
991
+ res.json({ ok: true, snapshot: SavingsLedger_1.savingsLedger.getSnapshot().total });
719
992
  });
720
993
  this.app.get('/dashboard/*', (req, res) => res.sendFile(path_1.default.join(this.dashboardPath, 'index.html')));
721
994
  }
@@ -740,76 +1013,36 @@ class LemmaServer {
740
1013
  logEvent({ type: 'privacy:mask', count: Object.keys(tokenMap).length, project: this.projectName });
741
1014
  }
742
1015
  const originalModel = req.body.model || 'gpt-4o';
743
- const routingDecision = complexityRouter.evaluate(prompt, originalModel);
744
- const model = routingDecision.model;
745
- if (req.body.model)
746
- req.body.model = model;
747
- // Apply the Agent Multiplexer to local-verify the prompt in parallel
748
- const enrichedPrompt = await agentMultiplexer.multiplexPrompt(prompt, model);
749
- // Resolve conversation session key and retrieve/create the fileHistory map
750
- let fileHistory;
751
- if (req.body.messages && req.body.messages.length > 0) {
752
- try {
753
- const firstMsg = req.body.messages[0];
754
- const content = typeof firstMsg.content === 'string' ? firstMsg.content : JSON.stringify(firstMsg.content);
755
- const sessionKey = crypto_1.default.createHash('sha1').update(content).digest('hex');
756
- if (!this.sessions.has(sessionKey)) {
757
- this.sessions.set(sessionKey, { fileHistory: new Map() });
758
- }
759
- fileHistory = this.sessions.get(sessionKey).fileHistory;
760
- }
761
- catch { }
762
- }
763
- // Compress long TS/JS codebase file blocks in the enriched prompt using the Context Squeezer AST Tree-Shaker!
764
- // The query context is the user's primary prompt to ensure we keep relevant methods!
765
- const squeezeResult = (0, ContextSqueezer_1.squeezePrompt)(enrichedPrompt, prompt, fileHistory);
766
- const squeezedPrompt = squeezeResult.squeezed;
767
- const compressionSavings = squeezeResult.originalSize - squeezeResult.squeezedSize;
768
- if (compressionSavings > 0) {
769
- console.log(`⚡ [Squeezer] Compressed prompt from ${squeezeResult.originalSize} to ${squeezeResult.squeezedSize} chars (Saved ${(squeezeResult.compressionRatio * 100).toFixed(1)}% context tokens!)`);
770
- }
771
- if (provider === 'openai' || provider === 'anthropic') {
772
- if (req.body.messages && req.body.messages.length > 0) {
773
- injectSqueezedContent(req.body.messages[req.body.messages.length - 1], squeezedPrompt);
774
- // Apply conversational history pruning to save tokens on historical messages!
775
- const pruneResult = (0, ContextSqueezer_1.pruneHistoryMessages)(req.body.messages);
776
- req.body.messages = pruneResult.messages;
777
- historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
778
- }
779
- }
780
- else if (provider === 'gemini') {
781
- if (req.body.contents && req.body.contents.length > 0) {
782
- const lastContent = req.body.contents[req.body.contents.length - 1];
783
- if (lastContent.parts && lastContent.parts.length > 0) {
784
- lastContent.parts[lastContent.parts.length - 1].text = squeezedPrompt;
785
- }
786
- }
787
- }
1016
+ // ── 1. Cache-First Hot Path ──
1017
+ // Perform exact cache check immediately (resolves in <1ms)
788
1018
  const hit = cacheGet(provider, prompt);
789
1019
  if (hit) {
790
- await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000 + historyTokensSaved);
1020
+ const cacheTokensSaved = 2000;
1021
+ SavingsLedger_1.savingsLedger.recordTokens('cache', cacheTokensSaved);
1022
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, cacheTokensSaved);
791
1023
  const unmaskedData = semanticScrubber.unmask(hit.data, tokenMap);
792
1024
  if (isStream)
793
- return simulateHitStream(res, unmaskedData, provider, model, tier, 1.0);
1025
+ return simulateHitStream(res, unmaskedData, provider, originalModel, tier, 1.0);
794
1026
  res.setHeader('X-Lemma-Cache', 'HIT');
795
1027
  res.setHeader('X-Lemma-Similarity', '1.000');
796
1028
  res.setHeader('X-Lemma-Tier', tier);
797
1029
  return res.json(unmaskedData);
798
1030
  }
1031
+ // Perform semantic cache check (resolves in ~15ms)
799
1032
  if (pro) {
800
1033
  const semHit = await semanticGet(provider, prompt);
801
1034
  if (semHit) {
802
1035
  if (semHit.similarity >= 0.90) {
803
- await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000 + historyTokensSaved);
1036
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
804
1037
  const unmaskedData = semanticScrubber.unmask(semHit.data, tokenMap);
805
1038
  if (isStream)
806
- return simulateHitStream(res, unmaskedData, provider, model, tier, semHit.similarity);
1039
+ return simulateHitStream(res, unmaskedData, provider, originalModel, tier, semHit.similarity);
807
1040
  res.setHeader('X-Lemma-Cache', 'HIT');
808
1041
  res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
809
1042
  res.setHeader('X-Lemma-Tier', tier);
810
1043
  return res.json(unmaskedData);
811
1044
  }
812
- else if (semHit.similarity >= 0.70) {
1045
+ else if (semHit.similarity >= 0.82) { // raised from 0.70 to 0.82 to prevent false positives
813
1046
  console.log(`💡 [CARS] Near Cache Hit (Similarity: ${(semHit.similarity * 100).toFixed(1)}%). Synthesizing answer locally via Ollama...`);
814
1047
  let cachedContent = '';
815
1048
  if (provider === 'openai') {
@@ -847,10 +1080,10 @@ Adjusted Answer:`;
847
1080
  else if (provider === 'anthropic') {
848
1081
  synthesizedData.content[0].text = adjustedText;
849
1082
  }
850
- await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000 + historyTokensSaved);
1083
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
851
1084
  const unmaskedData = semanticScrubber.unmask(synthesizedData, tokenMap);
852
1085
  if (isStream) {
853
- return simulateHitStream(res, unmaskedData, provider, model, tier, semHit.similarity);
1086
+ return simulateHitStream(res, unmaskedData, provider, originalModel, tier, semHit.similarity);
854
1087
  }
855
1088
  res.setHeader('X-Lemma-Cache', 'HIT-CARS');
856
1089
  res.setHeader('X-Lemma-Similarity', semHit.similarity.toFixed(3));
@@ -867,22 +1100,86 @@ Adjusted Answer:`;
867
1100
  const cloudHit = await cloudSync.get(prompt);
868
1101
  if (cloudHit) {
869
1102
  cacheSet(provider, prompt, cloudHit.data);
870
- await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000 + historyTokensSaved);
1103
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
871
1104
  const unmaskedData = semanticScrubber.unmask(cloudHit.data, tokenMap);
872
1105
  if (isStream)
873
- return simulateHitStream(res, unmaskedData, provider, model, tier, cloudHit.similarity || 0.95);
1106
+ return simulateHitStream(res, unmaskedData, provider, originalModel, tier, cloudHit.similarity || 0.95);
874
1107
  res.setHeader('X-Lemma-Cache', 'HIT');
875
1108
  res.setHeader('X-Lemma-Similarity', (cloudHit.similarity || 0.95).toFixed(3));
876
1109
  res.setHeader('X-Lemma-Tier', tier);
877
1110
  return res.json(unmaskedData);
878
1111
  }
879
1112
  }
1113
+ // ── 2. Cache Miss Path ──
1114
+ const routingDecision = complexityRouter.evaluate(prompt, originalModel);
1115
+ const model = routingDecision.model;
1116
+ if (req.body.model)
1117
+ req.body.model = model;
1118
+ // Record complexity routing savings when model is downgraded
1119
+ if (routingDecision.complexity === 'low' && model !== originalModel) {
1120
+ const estimatedPromptTokens = Math.floor(prompt.length / 4);
1121
+ const routingSavings = Math.floor(estimatedPromptTokens * 0.8);
1122
+ SavingsLedger_1.savingsLedger.recordTokens('complexityRouting', routingSavings);
1123
+ }
1124
+ // Apply the Agent Multiplexer to local-verify the prompt in parallel
1125
+ const enrichedPrompt = await agentMultiplexer.multiplexPrompt(prompt, model);
1126
+ // Resolve conversation session key and retrieve/create the fileHistory map
1127
+ let fileHistory;
1128
+ if (req.body.messages && req.body.messages.length > 0) {
1129
+ try {
1130
+ const firstMsg = req.body.messages[0];
1131
+ const content = typeof firstMsg.content === 'string' ? firstMsg.content : JSON.stringify(firstMsg.content);
1132
+ const sessionKey = crypto_1.default.createHash('sha1').update(content).digest('hex');
1133
+ if (!this.sessions.has(sessionKey)) {
1134
+ // Enforce 500 active sessions cap to prevent memory leak (FIFO eviction)
1135
+ const MAX_SESSIONS = 500;
1136
+ if (this.sessions.size >= MAX_SESSIONS) {
1137
+ const oldestKey = this.sessions.keys().next().value;
1138
+ if (oldestKey)
1139
+ this.sessions.delete(oldestKey);
1140
+ }
1141
+ this.sessions.set(sessionKey, { fileHistory: new Map() });
1142
+ }
1143
+ fileHistory = this.sessions.get(sessionKey).fileHistory;
1144
+ }
1145
+ catch { }
1146
+ }
1147
+ // Compress long TS/JS codebase file blocks in the enriched prompt
1148
+ const squeezeResult = (0, ContextSqueezer_1.squeezePrompt)(enrichedPrompt, prompt, fileHistory);
1149
+ const squeezedPrompt = squeezeResult.squeezed;
1150
+ const compressionSavings = squeezeResult.originalSize - squeezeResult.squeezedSize;
1151
+ if (compressionSavings > 0) {
1152
+ SavingsLedger_1.savingsLedger.record('contextSqueeze', squeezeResult.originalSize, squeezeResult.squeezedSize);
1153
+ console.log(`⚡ [Squeezer] Compressed prompt from ${squeezeResult.originalSize} to ${squeezeResult.squeezedSize} chars (Saved ${(squeezeResult.compressionRatio * 100).toFixed(1)}% context tokens!)`);
1154
+ }
1155
+ if (provider === 'openai' || provider === 'anthropic') {
1156
+ if (req.body.messages && req.body.messages.length > 0) {
1157
+ injectSqueezedContent(req.body.messages[req.body.messages.length - 1], squeezedPrompt);
1158
+ // Apply conversational history pruning
1159
+ const pruneResult = (0, ContextSqueezer_1.pruneHistoryMessages)(req.body.messages);
1160
+ req.body.messages = pruneResult.messages;
1161
+ historyTokensSaved = pruneResult.tokensSavedEstimate + Math.floor(compressionSavings / 4);
1162
+ if (pruneResult.tokensSavedEstimate > 0) {
1163
+ SavingsLedger_1.savingsLedger.recordTokens('historyPrune', pruneResult.tokensSavedEstimate);
1164
+ }
1165
+ }
1166
+ }
1167
+ else if (provider === 'gemini') {
1168
+ if (req.body.contents && req.body.contents.length > 0) {
1169
+ const lastContent = req.body.contents[req.body.contents.length - 1];
1170
+ if (lastContent.parts && lastContent.parts.length > 0) {
1171
+ lastContent.parts[lastContent.parts.length - 1].text = squeezedPrompt;
1172
+ }
1173
+ }
1174
+ }
880
1175
  if (isStream) {
881
1176
  await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
882
1177
  if (!pro) {
883
- const u = await loadUsage();
884
- u.count++;
885
- await writeJson(USAGE_FILE, u);
1178
+ await usageWriteQueue.enqueue(async () => {
1179
+ const u = await loadUsage();
1180
+ u.count++;
1181
+ await writeJson(USAGE_FILE, u);
1182
+ });
886
1183
  }
887
1184
  return relayMissStream(res, req.body, provider, tier);
888
1185
  }
@@ -894,9 +1191,11 @@ Adjusted Answer:`;
894
1191
  await cloudSync.set(prompt, data);
895
1192
  }
896
1193
  if (!pro) {
897
- const u = await loadUsage();
898
- u.count++;
899
- await writeJson(USAGE_FILE, u);
1194
+ await usageWriteQueue.enqueue(async () => {
1195
+ const u = await loadUsage();
1196
+ u.count++;
1197
+ await writeJson(USAGE_FILE, u);
1198
+ });
900
1199
  }
901
1200
  await recordStat(this.stats, this.projectName, false, Date.now() - t0, provider, historyTokensSaved);
902
1201
  return res.json(semanticScrubber.unmask(data, tokenMap));
@@ -933,6 +1232,15 @@ Adjusted Answer:`;
933
1232
  }
934
1233
  async start() {
935
1234
  this.stats = await loadStats();
1235
+ // Start active cache eviction sweep every hour
1236
+ setInterval(() => {
1237
+ const now = Date.now();
1238
+ for (const [k, e] of CACHE.entries()) {
1239
+ if (e.ttl > 0 && now - e.createdAt > e.ttl) {
1240
+ CACHE.delete(k);
1241
+ }
1242
+ }
1243
+ }, 3600000).unref();
936
1244
  const server = http_1.default.createServer(this.app);
937
1245
  server.on('error', (e) => {
938
1246
  if (e.code === 'EADDRINUSE') {
@@ -1052,10 +1360,17 @@ commander_1.program.command('start')
1052
1360
  if (resp.data && resp.data.status === 'ok') {
1053
1361
  console.log(`\n⚠️ Lemma Proxy is already running on port ${port} (Project: ${resp.data.project})`);
1054
1362
  console.log(`💡 Use 'lemma status' for details or 'lemma stop' to restart.\n`);
1363
+ // Keep configurations up to date even if server was already started
1364
+ const runningProject = resp.data.project || detectProject();
1365
+ autoConfigureAll(runningProject);
1366
+ startBackgroundClipboardWatcher();
1055
1367
  process.exit(0);
1056
1368
  }
1057
1369
  }
1058
1370
  catch { }
1371
+ const startProject = opts.project || detectProject();
1372
+ autoConfigureAll(startProject);
1373
+ startBackgroundClipboardWatcher();
1059
1374
  // Automatically enable autopilot if stack is enabled and user is Pro
1060
1375
  const runAutopilot = opts.autopilot || opts.stack;
1061
1376
  if (runAutopilot) {
@@ -1154,6 +1469,19 @@ commander_1.program.command('stop')
1154
1469
  }
1155
1470
  if (fs_1.default.existsSync(PID_FILE))
1156
1471
  fs_1.default.unlinkSync(PID_FILE);
1472
+ // Stop background Clipboard Watcher if active
1473
+ try {
1474
+ if (fs_1.default.existsSync(CLIPBOARD_PID_FILE)) {
1475
+ const cbPid = fs_1.default.readFileSync(CLIPBOARD_PID_FILE, 'utf8');
1476
+ try {
1477
+ process.kill(parseInt(cbPid, 10), 'SIGTERM');
1478
+ console.log(`✅ Stopped background Clipboard Watcher (PID: ${cbPid})`);
1479
+ }
1480
+ catch { }
1481
+ fs_1.default.unlinkSync(CLIPBOARD_PID_FILE);
1482
+ }
1483
+ }
1484
+ catch { }
1157
1485
  });
1158
1486
  commander_1.program.command('status')
1159
1487
  .description('Check proxy status')
@@ -1247,41 +1575,8 @@ commander_1.program.command('init')
1247
1575
  fs_1.default.writeFileSync(envFile, lemmaConfig);
1248
1576
  console.log('✅ Created .env with Lemma configuration');
1249
1577
  }
1250
- console.log('\n🧠 \x1b[35mAuto-Setup MCP for AI IDEs?\x1b[0m');
1251
- console.log(' Lemma can automatically configure Claude Desktop and open settings for Cursor/Windsurf.');
1252
- const { exec } = require('child_process');
1253
- const isWin = process.platform === 'win32';
1254
- const claudePath = isWin
1255
- ? path_1.default.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json')
1256
- : path_1.default.join(process.env.HOME || '~', 'Library/Application Support/Claude/claude_desktop_config.json');
1257
- if (fs_1.default.existsSync(path_1.default.dirname(claudePath))) {
1258
- try {
1259
- let config = { mcpServers: {} };
1260
- if (fs_1.default.existsSync(claudePath)) {
1261
- config = JSON.parse(fs_1.default.readFileSync(claudePath, 'utf8'));
1262
- }
1263
- if (!config.mcpServers)
1264
- config.mcpServers = {};
1265
- config.mcpServers.lemma = {
1266
- command: 'npx',
1267
- args: ['-y', '@nxuss/lemma', 'mcp'],
1268
- env: { LEMMA_PROJECT: project }
1269
- };
1270
- fs_1.default.writeFileSync(claudePath, JSON.stringify(config, null, 2));
1271
- console.log(`✅ \x1b[32mClaude Desktop configured automatically!\x1b[0m (Restart Claude to activate)`);
1272
- }
1273
- catch (e) {
1274
- console.log('⚠️ Could not auto-configure Claude Desktop. No worries, you can do it manually.');
1275
- }
1276
- }
1277
- console.log('🚀 Opening IDE configuration pages...');
1278
- const urls = [
1279
- 'https://docs.anthropic.com/en/docs/agents-and-tools/mcp#using-mcp-in-claude-desktop',
1280
- 'https://cursor.com',
1281
- 'https://codeium.com/windsurf'
1282
- ];
1283
- const opener = process.platform === 'darwin' ? 'open' : (process.platform === 'win32' ? 'start' : 'xdg-open');
1284
- urls.forEach(url => exec(`${opener} ${url}`));
1578
+ console.log('\n🧠 \x1b[35mAuto-Setup MCP and redirection overrides for AI IDEs...\x1b[0m');
1579
+ autoConfigureAll(project);
1285
1580
  console.log('\n✨ Project initialized for the Agentic Era!');
1286
1581
  console.log('🚀 Run "lemma start" to begin.\n');
1287
1582
  });