@nxuss/lemma 0.5.3 → 0.5.5
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 +30 -0
- package/dashboard/dist/assets/index-0N21ZtpO.js +481 -0
- package/dashboard/dist/assets/index-0N21ZtpO.js.map +1 -0
- package/dashboard/dist/assets/index-DUOrThix.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cjs/cli/lemma-proxy.d.ts +9 -2
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +365 -133
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/cjs/utils/SavingsLedger.d.ts +2 -0
- package/dist/cjs/utils/SavingsLedger.d.ts.map +1 -1
- package/dist/cjs/utils/SavingsLedger.js +49 -0
- package/dist/cjs/utils/SavingsLedger.js.map +1 -1
- package/dist/esm/cli/lemma-proxy.d.ts +9 -2
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +365 -133
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/utils/SavingsLedger.d.ts +2 -0
- package/dist/esm/utils/SavingsLedger.d.ts.map +1 -1
- package/dist/esm/utils/SavingsLedger.js +46 -0
- package/dist/esm/utils/SavingsLedger.js.map +1 -1
- package/package.json +1 -1
- package/dashboard/dist/assets/index-CIlkFwDs.js +0 -490
- package/dashboard/dist/assets/index-CIlkFwDs.js.map +0 -1
- package/dashboard/dist/assets/index-DNqq3gA9.css +0 -1
|
@@ -24,7 +24,7 @@ function getVersion() {
|
|
|
24
24
|
catch { }
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
return '0.5.
|
|
27
|
+
return '0.5.4';
|
|
28
28
|
}
|
|
29
29
|
const VERSION = getVersion();
|
|
30
30
|
import ComplexityRouter from '../proxy/ComplexityRouter';
|
|
@@ -209,141 +209,207 @@ function ensureGitIgnore() {
|
|
|
209
209
|
return added;
|
|
210
210
|
}
|
|
211
211
|
const CLIPBOARD_PID_FILE = path.join(CACHE_DIR, 'clipboard.pid');
|
|
212
|
-
export function autoConfigureAll(projectName) {
|
|
212
|
+
export function autoConfigureAll(projectName, cliOpts) {
|
|
213
213
|
const project = projectName || detectProject();
|
|
214
214
|
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
215
|
-
//
|
|
215
|
+
// Load configuration if it exists
|
|
216
|
+
let configDisabled = false;
|
|
217
|
+
let configEditor = true;
|
|
218
|
+
let configShell = true;
|
|
219
|
+
let configClaude = true;
|
|
216
220
|
try {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
221
|
+
const configPath = path.join(process.cwd(), 'lemma.config.json');
|
|
222
|
+
if (fs.existsSync(configPath)) {
|
|
223
|
+
const rawConfig = fs.readFileSync(configPath, 'utf8');
|
|
224
|
+
const config = JSON.parse(rawConfig);
|
|
225
|
+
const systemConfig = config.system || {};
|
|
226
|
+
const autoConfig = systemConfig.autoConfigure || {};
|
|
227
|
+
if (autoConfig.disabled === true)
|
|
228
|
+
configDisabled = true;
|
|
229
|
+
if (autoConfig.editor === false)
|
|
230
|
+
configEditor = false;
|
|
231
|
+
if (autoConfig.shell === false)
|
|
232
|
+
configShell = false;
|
|
233
|
+
if (autoConfig.claude === false)
|
|
234
|
+
configClaude = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch { }
|
|
238
|
+
// Merge CLI overrides and config
|
|
239
|
+
const shouldConfigure = cliOpts?.configure !== false && !configDisabled;
|
|
240
|
+
if (!shouldConfigure) {
|
|
241
|
+
console.log(`⏭️ [Auto-Configure] Skipped auto-configuration (disabled via options or config).`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const runEditor = cliOpts?.editor !== false && configEditor;
|
|
245
|
+
const runShell = cliOpts?.shell !== false && configShell;
|
|
246
|
+
const runClaude = cliOpts?.claude !== false && configClaude;
|
|
247
|
+
// 1. Claude Desktop Config
|
|
248
|
+
if (runClaude) {
|
|
249
|
+
try {
|
|
250
|
+
const isWin = process.platform === 'win32';
|
|
251
|
+
const claudePath = isWin
|
|
252
|
+
? path.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json')
|
|
253
|
+
: path.join(HOME, 'Library/Application Support/Claude/claude_desktop_config.json');
|
|
254
|
+
if (fs.existsSync(path.dirname(claudePath))) {
|
|
255
|
+
let config = { mcpServers: {} };
|
|
256
|
+
if (fs.existsSync(claudePath)) {
|
|
257
|
+
try {
|
|
258
|
+
config = JSON.parse(fs.readFileSync(claudePath, 'utf8'));
|
|
259
|
+
}
|
|
260
|
+
catch { }
|
|
226
261
|
}
|
|
227
|
-
|
|
262
|
+
if (!config.mcpServers)
|
|
263
|
+
config.mcpServers = {};
|
|
264
|
+
config.mcpServers.lemma = {
|
|
265
|
+
command: 'npx',
|
|
266
|
+
args: ['-y', '@nxuss/lemma', 'mcp'],
|
|
267
|
+
env: { LEMMA_PROJECT: project }
|
|
268
|
+
};
|
|
269
|
+
fs.writeFileSync(claudePath, JSON.stringify(config, null, 2));
|
|
270
|
+
console.log(`✅ [Claude Desktop] Configured automatically in: ${claudePath} (Restart Claude to activate)`);
|
|
228
271
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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)`);
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
console.log(`⚠️ Could not auto-configure Claude Desktop: ${e.message}`);
|
|
238
275
|
}
|
|
239
276
|
}
|
|
240
|
-
|
|
241
|
-
console.log(
|
|
277
|
+
else {
|
|
278
|
+
console.log(`⏭️ [Claude Desktop] Skip auto-configuration.`);
|
|
242
279
|
}
|
|
243
280
|
// 2. Shell Profiles Environment Override
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
281
|
+
if (runShell) {
|
|
282
|
+
try {
|
|
283
|
+
const profiles = [
|
|
284
|
+
path.join(HOME, '.zshrc'),
|
|
285
|
+
path.join(HOME, '.bashrc'),
|
|
286
|
+
path.join(HOME, '.bash_profile'),
|
|
287
|
+
path.join(HOME, '.profile')
|
|
288
|
+
];
|
|
289
|
+
const lines = [
|
|
290
|
+
'',
|
|
291
|
+
'# Lemma AI Gateway Overrides',
|
|
292
|
+
'export OPENAI_BASE_URL="http://localhost:8081/v1"',
|
|
293
|
+
'export ANTHROPIC_BASE_URL="http://localhost:8081"',
|
|
294
|
+
'export LEMMA_PROJECT="' + project + '"',
|
|
295
|
+
''
|
|
296
|
+
].join('\n');
|
|
297
|
+
for (const profile of profiles) {
|
|
298
|
+
if (fs.existsSync(profile)) {
|
|
299
|
+
try {
|
|
300
|
+
const content = fs.readFileSync(profile, 'utf8');
|
|
301
|
+
if (!content.includes('OPENAI_BASE_URL') && !content.includes('LEMMA_PROJECT')) {
|
|
302
|
+
fs.appendFileSync(profile, lines);
|
|
303
|
+
console.log(`✅ [Shell Profile] Configured ${path.basename(profile)} with local redirect variables!`);
|
|
304
|
+
}
|
|
266
305
|
}
|
|
306
|
+
catch { }
|
|
267
307
|
}
|
|
268
|
-
catch { }
|
|
269
308
|
}
|
|
270
309
|
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
console.log(`⚠️ Could not auto-configure shell profiles: ${e.message}`);
|
|
312
|
+
}
|
|
271
313
|
}
|
|
272
|
-
|
|
273
|
-
console.log(
|
|
314
|
+
else {
|
|
315
|
+
console.log(`⏭️ [Shell Profile] Skip auto-configuration.`);
|
|
274
316
|
}
|
|
275
317
|
// 3. Cursor & Windsurf settings.json updates
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
+
if (runEditor) {
|
|
319
|
+
try {
|
|
320
|
+
const isWin = process.platform === 'win32';
|
|
321
|
+
const isMac = process.platform === 'darwin';
|
|
322
|
+
const candidates = [];
|
|
323
|
+
if (isMac) {
|
|
324
|
+
candidates.push({
|
|
325
|
+
name: 'Cursor',
|
|
326
|
+
path: path.join(HOME, 'Library/Application Support/Cursor/User/settings.json')
|
|
327
|
+
});
|
|
328
|
+
candidates.push({
|
|
329
|
+
name: 'Windsurf',
|
|
330
|
+
path: path.join(HOME, 'Library/Application Support/Windsurf/User/settings.json')
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
else if (isWin) {
|
|
334
|
+
const appData = process.env.APPDATA || '';
|
|
335
|
+
candidates.push({
|
|
336
|
+
name: 'Cursor',
|
|
337
|
+
path: path.join(appData, 'Cursor/User/settings.json')
|
|
338
|
+
});
|
|
339
|
+
candidates.push({
|
|
340
|
+
name: 'Windsurf',
|
|
341
|
+
path: path.join(appData, 'Windsurf/User/settings.json')
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
candidates.push({
|
|
346
|
+
name: 'Cursor',
|
|
347
|
+
path: path.join(HOME, '.config/Cursor/User/settings.json')
|
|
348
|
+
});
|
|
349
|
+
candidates.push({
|
|
350
|
+
name: 'Windsurf',
|
|
351
|
+
path: path.join(HOME, '.config/Windsurf/User/settings.json')
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
for (const cand of candidates) {
|
|
355
|
+
const dir = path.dirname(cand.path);
|
|
356
|
+
if (fs.existsSync(dir)) {
|
|
357
|
+
let settings = {};
|
|
358
|
+
if (fs.existsSync(cand.path)) {
|
|
359
|
+
try {
|
|
360
|
+
settings = JSON.parse(fs.readFileSync(cand.path, 'utf8'));
|
|
361
|
+
}
|
|
362
|
+
catch { }
|
|
318
363
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
364
|
+
let updated = false;
|
|
365
|
+
// Settings configuration to route custom openai/anthropic models through the proxy
|
|
366
|
+
const updates = {
|
|
367
|
+
"openai.baseURL": "http://localhost:8081/v1",
|
|
368
|
+
"openai.apiKey": "dummy-key-for-lemma",
|
|
369
|
+
"anthropic.baseURL": "http://localhost:8081",
|
|
370
|
+
"anthropic.apiKey": "dummy-key-for-lemma"
|
|
371
|
+
};
|
|
372
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
373
|
+
if (settings[key] !== value) {
|
|
374
|
+
settings[key] = value;
|
|
375
|
+
updated = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (updated) {
|
|
379
|
+
fs.writeFileSync(cand.path, JSON.stringify(settings, null, 2));
|
|
380
|
+
console.log(`✅ [${cand.name}] Configured editor settings in ${cand.path} to route via local gateway!`);
|
|
333
381
|
}
|
|
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
382
|
}
|
|
339
383
|
}
|
|
340
384
|
}
|
|
385
|
+
catch (e) {
|
|
386
|
+
console.log(`⚠️ Could not auto-configure editor settings: ${e.message}`);
|
|
387
|
+
}
|
|
341
388
|
}
|
|
342
|
-
|
|
343
|
-
console.log(
|
|
389
|
+
else {
|
|
390
|
+
console.log(`⏭️ [Editor Settings] Skip auto-configuration.`);
|
|
344
391
|
}
|
|
345
392
|
}
|
|
346
|
-
export function startBackgroundClipboardWatcher() {
|
|
393
|
+
export function startBackgroundClipboardWatcher(cliOpts) {
|
|
394
|
+
// Load configuration if it exists
|
|
395
|
+
let configDisabled = false;
|
|
396
|
+
try {
|
|
397
|
+
const configPath = path.join(process.cwd(), 'lemma.config.json');
|
|
398
|
+
if (fs.existsSync(configPath)) {
|
|
399
|
+
const rawConfig = fs.readFileSync(configPath, 'utf8');
|
|
400
|
+
const config = JSON.parse(rawConfig);
|
|
401
|
+
const systemConfig = config.system || {};
|
|
402
|
+
const clipConfig = systemConfig.clipboardWatcher || {};
|
|
403
|
+
if (clipConfig.disabled === true)
|
|
404
|
+
configDisabled = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch { }
|
|
408
|
+
const shouldWatch = cliOpts?.clipboard !== false && !configDisabled;
|
|
409
|
+
if (!shouldWatch) {
|
|
410
|
+
console.log(`⏭️ [Clipboard] Background clipboard watcher is disabled (via options or config).`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
347
413
|
try {
|
|
348
414
|
if (fs.existsSync(CLIPBOARD_PID_FILE)) {
|
|
349
415
|
const oldPid = fs.readFileSync(CLIPBOARD_PID_FILE, 'utf8');
|
|
@@ -462,16 +528,21 @@ async function recordStat(stats, project, fromCache, latencyMs, provider, tokens
|
|
|
462
528
|
s.total++;
|
|
463
529
|
s.totalLatency += latencyMs;
|
|
464
530
|
fromCache ? s.hits++ : s.misses++;
|
|
465
|
-
|
|
531
|
+
const tkSaved = fromCache ? (tokensSaved || 2000) : (tokensSaved || 0);
|
|
532
|
+
s.totalTokensSaved += tkSaved;
|
|
466
533
|
if (!s.providers[provider])
|
|
467
534
|
s.providers[provider] = { hits: 0, misses: 0 };
|
|
468
535
|
fromCache ? s.providers[provider].hits++ : s.providers[provider].misses++;
|
|
536
|
+
if (fromCache && tkSaved > 0) {
|
|
537
|
+
// Automatically report cache hits directly to the server session ledger for absolute sync consistency
|
|
538
|
+
savingsLedger.recordTokens('cache', tkSaved);
|
|
539
|
+
}
|
|
469
540
|
logEvent({
|
|
470
541
|
type: fromCache ? 'cache:hit' : 'cache:miss',
|
|
471
542
|
project,
|
|
472
543
|
latency: latencyMs,
|
|
473
544
|
provider,
|
|
474
|
-
tokens:
|
|
545
|
+
tokens: tkSaved
|
|
475
546
|
});
|
|
476
547
|
await writeJson(STATS_FILE, stats);
|
|
477
548
|
}
|
|
@@ -844,6 +915,18 @@ class LemmaServer {
|
|
|
844
915
|
const hitRate = stats.total > 0 ? stats.hits / stats.total : 0;
|
|
845
916
|
const avgLat = stats.total > 0 ? stats.totalLatency / stats.total : 0;
|
|
846
917
|
const secretsMasked = EVENT_LOG.filter(e => e.type === 'privacy:mask').length;
|
|
918
|
+
const ledgerSnap = savingsLedger.getSnapshot();
|
|
919
|
+
let activeAgents = 1; // Always include the Gateway itself
|
|
920
|
+
let totalAgents = 1;
|
|
921
|
+
// Query the stack orchestration API to merge active agents
|
|
922
|
+
try {
|
|
923
|
+
const stackMetrics = await axios.get('http://127.0.0.1:8083/api/metrics', { timeout: 100 });
|
|
924
|
+
if (stackMetrics.data) {
|
|
925
|
+
activeAgents += stackMetrics.data.activeAgents || 0;
|
|
926
|
+
totalAgents += stackMetrics.data.totalAgents || 0;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
catch { }
|
|
847
930
|
res.json({
|
|
848
931
|
timestamp: new Date().toISOString(),
|
|
849
932
|
totalRequests: stats.total,
|
|
@@ -851,30 +934,140 @@ class LemmaServer {
|
|
|
851
934
|
cacheMisses: stats.misses,
|
|
852
935
|
hitRate,
|
|
853
936
|
averageLatency: avgLat,
|
|
854
|
-
tokensSaved:
|
|
855
|
-
costSaved:
|
|
937
|
+
tokensSaved: ledgerSnap.total.tokensSaved,
|
|
938
|
+
costSaved: ledgerSnap.total.costSaved,
|
|
856
939
|
secretsMasked: secretsMasked,
|
|
857
|
-
activeAgents
|
|
858
|
-
totalAgents
|
|
940
|
+
activeAgents,
|
|
941
|
+
totalAgents,
|
|
859
942
|
uptime: process.uptime(),
|
|
860
943
|
tier: (await isPro()) ? 'pro' : 'free'
|
|
861
944
|
});
|
|
862
945
|
});
|
|
863
946
|
this.app.get('/api/agents', async (req, res) => {
|
|
947
|
+
// Load statistics for the Gateway itself
|
|
864
948
|
const stats = await loadStats();
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
949
|
+
const statsProj = stats[this.projectName] || { total: 0, hits: 0 };
|
|
950
|
+
const total = statsProj.total || 0;
|
|
951
|
+
const hits = statsProj.hits || 0;
|
|
952
|
+
const gatewayAgent = {
|
|
953
|
+
id: 'lemma-gateway',
|
|
954
|
+
name: 'Lemma Gateway',
|
|
955
|
+
status: 'active',
|
|
956
|
+
capabilities: ['Privacy Firewall', 'Semantic Cache', 'Complexity Router'],
|
|
957
|
+
tasksCompleted: total,
|
|
958
|
+
lastSeen: new Date().toISOString(),
|
|
959
|
+
errorCount: 0,
|
|
960
|
+
cacheHitRate: total > 0 ? hits / total : 0
|
|
961
|
+
};
|
|
962
|
+
let agentsList = [gatewayAgent];
|
|
963
|
+
// Query the stack orchestration API to merge dynamic websocket agents if running
|
|
964
|
+
try {
|
|
965
|
+
const stackAgentsResponse = await axios.get('http://127.0.0.1:8083/api/agents', { timeout: 100 });
|
|
966
|
+
if (stackAgentsResponse.data && Array.isArray(stackAgentsResponse.data.agents)) {
|
|
967
|
+
const stackAgents = stackAgentsResponse.data.agents.filter((a) => a.id !== 'lemma-gateway');
|
|
968
|
+
agentsList = [...agentsList, ...stackAgents];
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
catch { }
|
|
972
|
+
res.json({ agents: agentsList });
|
|
868
973
|
});
|
|
869
|
-
this.app.get('/api/events', (req, res) => {
|
|
974
|
+
this.app.get('/api/events', async (req, res) => {
|
|
870
975
|
const page = parseInt(req.query.page) || 1;
|
|
871
976
|
const limit = parseInt(req.query.limit) || 50;
|
|
872
977
|
const start = (page - 1) * limit;
|
|
873
|
-
|
|
874
|
-
|
|
978
|
+
// Get local proxy events (normalized schema matching what dashboard expects)
|
|
979
|
+
const localEvents = EVENT_LOG.map(e => ({
|
|
980
|
+
id: e.id,
|
|
981
|
+
timestamp: new Date(e.timestamp).toISOString(),
|
|
982
|
+
type: e.type, // e.g., 'cache:hit' or 'cache:miss' which client normalizes to 'cache_hit' or 'cache_miss'
|
|
983
|
+
latency: e.latency,
|
|
984
|
+
tokensSaved: e.tokens,
|
|
985
|
+
costSaved: e.tokens ? (e.tokens / 1000) * 0.002 : 0,
|
|
986
|
+
metadata: {
|
|
987
|
+
provider: e.provider,
|
|
988
|
+
project: e.project
|
|
989
|
+
}
|
|
990
|
+
}));
|
|
991
|
+
let mergedEvents = [...localEvents];
|
|
992
|
+
// Query the stack orchestration API if running to merge stack events
|
|
993
|
+
try {
|
|
994
|
+
const stackEventsResponse = await axios.get(`http://127.0.0.1:8083/api/events?page=1&limit=100`, { timeout: 100 });
|
|
995
|
+
if (stackEventsResponse.data && Array.isArray(stackEventsResponse.data.events)) {
|
|
996
|
+
mergedEvents = [...mergedEvents, ...stackEventsResponse.data.events];
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
catch { }
|
|
1000
|
+
// Sort merged events chronologically (newest first)
|
|
1001
|
+
mergedEvents.sort((a, b) => {
|
|
1002
|
+
const tA = new Date(a.timestamp).getTime();
|
|
1003
|
+
const tB = new Date(b.timestamp).getTime();
|
|
1004
|
+
return tB - tA;
|
|
1005
|
+
});
|
|
1006
|
+
const total = mergedEvents.length;
|
|
1007
|
+
const paginatedEvents = mergedEvents.slice(start, start + limit);
|
|
1008
|
+
res.json({
|
|
1009
|
+
events: paginatedEvents,
|
|
1010
|
+
pagination: {
|
|
1011
|
+
total,
|
|
1012
|
+
page,
|
|
1013
|
+
limit,
|
|
1014
|
+
hasMore: start + limit < total
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
875
1017
|
});
|
|
876
|
-
this.app.get('/api/cache-stats', (req, res) => {
|
|
877
|
-
|
|
1018
|
+
this.app.get('/api/cache-stats', async (req, res) => {
|
|
1019
|
+
let topQueries = [];
|
|
1020
|
+
let totalCached = CACHE.size;
|
|
1021
|
+
// Try to query ChromaDB for semantic cache count
|
|
1022
|
+
try {
|
|
1023
|
+
const collection = await chroma.getOrCreateCollection({
|
|
1024
|
+
name: 'lemma-cache',
|
|
1025
|
+
embeddingFunction: dummyEmbeddingFunction,
|
|
1026
|
+
metadata: { "hnsw:space": "cosine" }
|
|
1027
|
+
});
|
|
1028
|
+
const count = await collection.count();
|
|
1029
|
+
totalCached += count;
|
|
1030
|
+
}
|
|
1031
|
+
catch { }
|
|
1032
|
+
// Build top queries from in-memory CACHE
|
|
1033
|
+
for (const [key, entry] of CACHE.entries()) {
|
|
1034
|
+
const hitCount = EVENT_LOG.filter(e => e.type === 'cache:hit' && e.provider === entry.provider).length || 1;
|
|
1035
|
+
const tokens = 2000;
|
|
1036
|
+
topQueries.push({
|
|
1037
|
+
queryHash: key.substring(0, 16),
|
|
1038
|
+
query: entry.input,
|
|
1039
|
+
hitCount,
|
|
1040
|
+
totalTokens: tokens * hitCount,
|
|
1041
|
+
costSaved: (tokens * hitCount / 1000) * 0.002,
|
|
1042
|
+
lastHit: new Date(entry.createdAt).toISOString()
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
// Try to query the stack orchestration API if running to append stack cache stats
|
|
1046
|
+
try {
|
|
1047
|
+
const stackCache = await axios.get('http://127.0.0.1:8083/api/cache-stats', { timeout: 100 });
|
|
1048
|
+
if (stackCache.data) {
|
|
1049
|
+
totalCached += stackCache.data.totalCached || 0;
|
|
1050
|
+
if (Array.isArray(stackCache.data.topQueries)) {
|
|
1051
|
+
const stackTop = stackCache.data.topQueries.filter((q) => !topQueries.some(t => t.query === q.query));
|
|
1052
|
+
topQueries = [...topQueries, ...stackTop];
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
catch { }
|
|
1057
|
+
// Sort by hitCount desc
|
|
1058
|
+
topQueries.sort((a, b) => b.hitCount - a.hitCount);
|
|
1059
|
+
// Latency percentiles from EVENT_LOG
|
|
1060
|
+
const latencies = EVENT_LOG.filter(e => e.latency).map(e => e.latency).sort((a, b) => a - b);
|
|
1061
|
+
const latencyPercentiles = {
|
|
1062
|
+
p50: latencies[Math.floor(latencies.length * 0.5)] || 0,
|
|
1063
|
+
p95: latencies[Math.floor(latencies.length * 0.95)] || 0,
|
|
1064
|
+
p99: latencies[Math.floor(latencies.length * 0.99)] || 0,
|
|
1065
|
+
};
|
|
1066
|
+
res.json({
|
|
1067
|
+
topQueries: topQueries.slice(0, 10),
|
|
1068
|
+
latencyPercentiles,
|
|
1069
|
+
totalCached
|
|
1070
|
+
});
|
|
878
1071
|
});
|
|
879
1072
|
this.app.get('/api/search', async (req, res) => {
|
|
880
1073
|
const query = req.query.q;
|
|
@@ -1010,7 +1203,6 @@ class LemmaServer {
|
|
|
1010
1203
|
const hit = cacheGet(provider, prompt);
|
|
1011
1204
|
if (hit) {
|
|
1012
1205
|
const cacheTokensSaved = 2000;
|
|
1013
|
-
savingsLedger.recordTokens('cache', cacheTokensSaved);
|
|
1014
1206
|
await recordStat(this.stats, this.projectName, true, Date.now() - t0, provider, cacheTokensSaved);
|
|
1015
1207
|
const unmaskedData = semanticScrubber.unmask(hit.data, tokenMap);
|
|
1016
1208
|
if (isStream)
|
|
@@ -1198,6 +1390,43 @@ Adjusted Answer:`;
|
|
|
1198
1390
|
}
|
|
1199
1391
|
async callUpstream(body, provider) {
|
|
1200
1392
|
let apiKey = provider === 'openai' ? process.env.OPENAI_API_KEY : (provider === 'anthropic' ? process.env.ANTHROPIC_API_KEY : process.env.GEMINI_API_KEY);
|
|
1393
|
+
// If the API key is an OpenAI key (sk-proj-) but requested for Anthropic or Gemini,
|
|
1394
|
+
// multiplex it internally to OpenAI gpt-4o-mini to allow live, cheap simulated provider routing.
|
|
1395
|
+
const isMockedProvider = apiKey && apiKey.startsWith('sk-') && provider !== 'openai';
|
|
1396
|
+
if (isMockedProvider) {
|
|
1397
|
+
console.log(`\n\x1b[36m🔀 [Gateway Router] Multiplexing B2B ${provider.toUpperCase()} request to OpenAI using key: ...${apiKey.substring(8, 14)}...\x1b[0m`);
|
|
1398
|
+
const payload = {
|
|
1399
|
+
model: 'gpt-4o-mini',
|
|
1400
|
+
messages: body.messages || [{ role: 'user', content: extractPrompt(body, provider) || 'Hello' }],
|
|
1401
|
+
stream: false // keep stream false for simulation simplicity
|
|
1402
|
+
};
|
|
1403
|
+
const baseURL = 'https://api.openai.com/v1/chat/completions';
|
|
1404
|
+
const resp = await axios.post(baseURL, payload, {
|
|
1405
|
+
headers: {
|
|
1406
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1407
|
+
'Content-Type': 'application/json'
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
if (provider === 'anthropic') {
|
|
1411
|
+
const text = resp.data.choices?.[0]?.message?.content || '';
|
|
1412
|
+
return {
|
|
1413
|
+
id: `msg-${Date.now()}`,
|
|
1414
|
+
type: 'message',
|
|
1415
|
+
role: 'assistant',
|
|
1416
|
+
content: [{ type: 'text', text }],
|
|
1417
|
+
model: body.model || 'claude-3-5-sonnet'
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
else if (provider === 'gemini') {
|
|
1421
|
+
const text = resp.data.choices?.[0]?.message?.content || '';
|
|
1422
|
+
return {
|
|
1423
|
+
candidates: [{
|
|
1424
|
+
content: { parts: [{ text }] },
|
|
1425
|
+
finishReason: 'STOP'
|
|
1426
|
+
}]
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1201
1430
|
if (!apiKey) {
|
|
1202
1431
|
console.log(`\n\x1b[33m⚠️ No API Key found for ${provider.toUpperCase()}. Falling back to local Ollama (Zero-Cost Mode)!\x1b[0m`);
|
|
1203
1432
|
// If the requested model is a cloud model (e.g. gpt-4o, claude), map it to a local model like llama3
|
|
@@ -1343,6 +1572,8 @@ program.command('start')
|
|
|
1343
1572
|
.option('--project <name>', 'Override project name')
|
|
1344
1573
|
.option('--stack', 'Launch full development stack (Chroma + Router + Dashboard + Autopilot)')
|
|
1345
1574
|
.option('--autopilot', 'Launch background compiler watcher & healer', false)
|
|
1575
|
+
.option('--no-configure', 'Skip automatic configuration of editors, shell overrides, and Claude Desktop')
|
|
1576
|
+
.option('--no-clipboard', 'Skip launching background clipboard watcher')
|
|
1346
1577
|
.action(async (opts) => {
|
|
1347
1578
|
const port = parseInt(opts.port, 10);
|
|
1348
1579
|
// Ensure ChromaDB is running for semantic caching/search
|
|
@@ -1354,15 +1585,15 @@ program.command('start')
|
|
|
1354
1585
|
console.log(`💡 Use 'lemma status' for details or 'lemma stop' to restart.\n`);
|
|
1355
1586
|
// Keep configurations up to date even if server was already started
|
|
1356
1587
|
const runningProject = resp.data.project || detectProject();
|
|
1357
|
-
autoConfigureAll(runningProject);
|
|
1358
|
-
startBackgroundClipboardWatcher();
|
|
1588
|
+
autoConfigureAll(runningProject, { configure: opts.configure });
|
|
1589
|
+
startBackgroundClipboardWatcher({ clipboard: opts.clipboard });
|
|
1359
1590
|
process.exit(0);
|
|
1360
1591
|
}
|
|
1361
1592
|
}
|
|
1362
1593
|
catch { }
|
|
1363
1594
|
const startProject = opts.project || detectProject();
|
|
1364
|
-
autoConfigureAll(startProject);
|
|
1365
|
-
startBackgroundClipboardWatcher();
|
|
1595
|
+
autoConfigureAll(startProject, { configure: opts.configure });
|
|
1596
|
+
startBackgroundClipboardWatcher({ clipboard: opts.clipboard });
|
|
1366
1597
|
// Automatically enable autopilot if stack is enabled and user is Pro
|
|
1367
1598
|
const runAutopilot = opts.autopilot || opts.stack;
|
|
1368
1599
|
if (runAutopilot) {
|
|
@@ -1538,7 +1769,8 @@ program.command('activate <key>')
|
|
|
1538
1769
|
});
|
|
1539
1770
|
program.command('init')
|
|
1540
1771
|
.description('Initialize Lemma in the current project (auto-discovery)')
|
|
1541
|
-
.
|
|
1772
|
+
.option('--no-configure', 'Skip automatic configuration of editors, shell overrides, and Claude Desktop')
|
|
1773
|
+
.action(async (opts) => {
|
|
1542
1774
|
const project = detectProject();
|
|
1543
1775
|
console.log(`\n🛠️ Initializing Lemma for [${project}]...`);
|
|
1544
1776
|
// Ensure ChromaDB is running for semantic caching/search
|
|
@@ -1568,7 +1800,7 @@ program.command('init')
|
|
|
1568
1800
|
console.log('✅ Created .env with Lemma configuration');
|
|
1569
1801
|
}
|
|
1570
1802
|
console.log('\n🧠 \x1b[35mAuto-Setup MCP and redirection overrides for AI IDEs...\x1b[0m');
|
|
1571
|
-
autoConfigureAll(project);
|
|
1803
|
+
autoConfigureAll(project, { configure: opts.configure });
|
|
1572
1804
|
console.log('\n✨ Project initialized for the Agentic Era!');
|
|
1573
1805
|
console.log('🚀 Run "lemma start" to begin.\n');
|
|
1574
1806
|
});
|