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