@nxuss/lemma 1.4.0 → 1.6.0

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 (59) hide show
  1. package/README.md +75 -7
  2. package/bin/init.js +1 -1
  3. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  4. package/dist/cjs/cli/lemma-proxy.js +50 -6
  5. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  6. package/dist/cjs/mcp/index.js +23 -0
  7. package/dist/cjs/mcp/index.js.map +1 -1
  8. package/dist/cjs/mcp/prompts.d.ts +2 -2
  9. package/dist/cjs/mcp/prompts.d.ts.map +1 -1
  10. package/dist/cjs/mcp/prompts.js +10 -5
  11. package/dist/cjs/mcp/prompts.js.map +1 -1
  12. package/dist/cjs/mcp/tools.d.ts +2 -0
  13. package/dist/cjs/mcp/tools.d.ts.map +1 -1
  14. package/dist/cjs/mcp/tools.js +111 -5
  15. package/dist/cjs/mcp/tools.js.map +1 -1
  16. package/dist/cjs/utils/CommandOutputDistiller.d.ts +75 -0
  17. package/dist/cjs/utils/CommandOutputDistiller.d.ts.map +1 -0
  18. package/dist/cjs/utils/CommandOutputDistiller.js +598 -0
  19. package/dist/cjs/utils/CommandOutputDistiller.js.map +1 -0
  20. package/dist/cjs/utils/ContextSqueezer.d.ts.map +1 -1
  21. package/dist/cjs/utils/ContextSqueezer.js +5 -3
  22. package/dist/cjs/utils/ContextSqueezer.js.map +1 -1
  23. package/dist/cjs/utils/SavingsLedger.d.ts +38 -1
  24. package/dist/cjs/utils/SavingsLedger.d.ts.map +1 -1
  25. package/dist/cjs/utils/SavingsLedger.js +89 -13
  26. package/dist/cjs/utils/SavingsLedger.js.map +1 -1
  27. package/dist/cjs/utils/reportSavings.d.ts +15 -0
  28. package/dist/cjs/utils/reportSavings.d.ts.map +1 -1
  29. package/dist/cjs/utils/reportSavings.js +13 -1
  30. package/dist/cjs/utils/reportSavings.js.map +1 -1
  31. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  32. package/dist/esm/cli/lemma-proxy.js +50 -6
  33. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  34. package/dist/esm/mcp/index.js +24 -1
  35. package/dist/esm/mcp/index.js.map +1 -1
  36. package/dist/esm/mcp/prompts.d.ts +2 -2
  37. package/dist/esm/mcp/prompts.d.ts.map +1 -1
  38. package/dist/esm/mcp/prompts.js +10 -5
  39. package/dist/esm/mcp/prompts.js.map +1 -1
  40. package/dist/esm/mcp/tools.d.ts +2 -0
  41. package/dist/esm/mcp/tools.d.ts.map +1 -1
  42. package/dist/esm/mcp/tools.js +112 -7
  43. package/dist/esm/mcp/tools.js.map +1 -1
  44. package/dist/esm/utils/CommandOutputDistiller.d.ts +75 -0
  45. package/dist/esm/utils/CommandOutputDistiller.d.ts.map +1 -0
  46. package/dist/esm/utils/CommandOutputDistiller.js +588 -0
  47. package/dist/esm/utils/CommandOutputDistiller.js.map +1 -0
  48. package/dist/esm/utils/ContextSqueezer.d.ts.map +1 -1
  49. package/dist/esm/utils/ContextSqueezer.js +5 -3
  50. package/dist/esm/utils/ContextSqueezer.js.map +1 -1
  51. package/dist/esm/utils/SavingsLedger.d.ts +38 -1
  52. package/dist/esm/utils/SavingsLedger.d.ts.map +1 -1
  53. package/dist/esm/utils/SavingsLedger.js +89 -13
  54. package/dist/esm/utils/SavingsLedger.js.map +1 -1
  55. package/dist/esm/utils/reportSavings.d.ts +15 -0
  56. package/dist/esm/utils/reportSavings.d.ts.map +1 -1
  57. package/dist/esm/utils/reportSavings.js +12 -1
  58. package/dist/esm/utils/reportSavings.js.map +1 -1
  59. package/package.json +1 -1
@@ -580,6 +580,31 @@ async function loadUsage() {
580
580
  async function loadStats() {
581
581
  return readJson(STATS_FILE, {});
582
582
  }
583
+ /**
584
+ * Tokens a cache hit actually avoided regenerating, measured from the cached payload
585
+ * that was served instead of an LLM call. Replaces the old hardcoded 2000 guess.
586
+ */
587
+ function measureCachedTokens(data) {
588
+ try {
589
+ const chars = JSON.stringify(data)?.length;
590
+ if (!chars)
591
+ return undefined;
592
+ return Math.floor(chars / 4);
593
+ }
594
+ catch {
595
+ return undefined;
596
+ }
597
+ }
598
+ /**
599
+ * @param tokensSaved Measured tokens saved. When omitted for a cache hit, stats.json
600
+ * still falls back to a 2000-token placeholder for dashboard continuity, but that
601
+ * guess is deliberately NOT written to the savings ledger — the ledger only accepts
602
+ * measured values, otherwise its net position is built on invented numbers.
603
+ *
604
+ * Note: a cache *miss* is not charged to the cost ledger. The lookup is local (no LLM
605
+ * round-trip), so a miss costs latency, not tokens. Charging it a fabricated token
606
+ * figure would repeat exactly the error the cost ledger exists to correct.
607
+ */
583
608
  async function recordStat(stats, project, fromCache, latencyMs, provider, tokensSaved) {
584
609
  if (!stats[project]) {
585
610
  stats[project] = { total: 0, hits: 0, misses: 0, totalLatency: 0, totalTokensSaved: 0, providers: {} };
@@ -593,9 +618,11 @@ async function recordStat(stats, project, fromCache, latencyMs, provider, tokens
593
618
  if (!s.providers[provider])
594
619
  s.providers[provider] = { hits: 0, misses: 0 };
595
620
  fromCache ? s.providers[provider].hits++ : s.providers[provider].misses++;
596
- if (fromCache && tkSaved > 0) {
597
- // Automatically report cache hits directly to the server session ledger for absolute sync consistency
598
- savingsLedger.recordTokens('cache', tkSaved);
621
+ // Only measured savings reach the ledger. `tokensSaved === undefined` means the
622
+ // caller never counted, so tkSaved is the 2000 placeholder usable for the
623
+ // dashboard, not for a net-position claim.
624
+ if (fromCache && typeof tokensSaved === 'number' && tokensSaved > 0) {
625
+ savingsLedger.recordTokens('cache', tokensSaved);
599
626
  }
600
627
  logEvent({
601
628
  type: fromCache ? 'cache:hit' : 'cache:miss',
@@ -1501,6 +1528,23 @@ class LemmaServer {
1501
1528
  }
1502
1529
  res.json({ ok: true, snapshot: savingsLedger.getSnapshot().total });
1503
1530
  });
1531
+ // Cost Record Endpoint — the counterpart to /api/savings-record. Without it the
1532
+ // ledger only ever counts wins, which makes its totals unfalsifiable.
1533
+ this.app.post('/api/cost-record', (req, res) => {
1534
+ const { source, tokens, chars } = req.body || {};
1535
+ const validSources = ['mcpInstructions', 'toolResult', 'cacheMiss'];
1536
+ if (!source || !validSources.includes(source)) {
1537
+ return res.status(400).json({ error: 'Invalid cost source' });
1538
+ }
1539
+ if (typeof tokens === 'number' && tokens > 0) {
1540
+ savingsLedger.recordCost(source, tokens);
1541
+ }
1542
+ else if (typeof chars === 'number' && chars > 0) {
1543
+ savingsLedger.recordCostChars(source, chars);
1544
+ }
1545
+ const snap = savingsLedger.getSnapshot();
1546
+ res.json({ ok: true, totalCost: snap.totalCost, net: snap.net });
1547
+ });
1504
1548
  // MCP Tool Event Endpoint — called by MCP server when tools execute
1505
1549
  this.app.post('/api/mcp-event', (req, res) => {
1506
1550
  try {
@@ -1726,7 +1770,7 @@ class LemmaServer {
1726
1770
  const semHit = await semanticGet(provider, prompt, this.projectName);
1727
1771
  if (semHit) {
1728
1772
  if (semHit.similarity >= 0.90) {
1729
- await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
1773
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, measureCachedTokens(semHit.data));
1730
1774
  const unmaskedData = semanticScrubber.unmask(semHit.data, tokenMap);
1731
1775
  const cacheHeader = semHit.hiveMind ? 'HIT-HIVE-MIND' : 'HIT';
1732
1776
  if (isStream)
@@ -1777,7 +1821,7 @@ Adjusted Answer:`;
1777
1821
  else if (provider === 'anthropic') {
1778
1822
  synthesizedData.content[0].text = adjustedText;
1779
1823
  }
1780
- await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
1824
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, measureCachedTokens(synthesizedData));
1781
1825
  const unmaskedData = semanticScrubber.unmask(synthesizedData, tokenMap);
1782
1826
  if (isStream) {
1783
1827
  return SseRelay.simulateHit(res, unmaskedData, { provider: provider, tier, similarity: semHit.similarity });
@@ -1797,7 +1841,7 @@ Adjusted Answer:`;
1797
1841
  const cloudHit = await cloudSync.get(prompt);
1798
1842
  if (cloudHit) {
1799
1843
  cacheSet(provider, prompt, cloudHit.data);
1800
- await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, 2000);
1844
+ await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, measureCachedTokens(cloudHit.data));
1801
1845
  const unmaskedData = semanticScrubber.unmask(cloudHit.data, tokenMap);
1802
1846
  if (isStream)
1803
1847
  return SseRelay.simulateHit(res, unmaskedData, { provider: provider, tier, similarity: cloudHit.similarity || 0.95 });