@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.
@@ -24,7 +24,7 @@ function getVersion() {
24
24
  catch { }
25
25
  }
26
26
  }
27
- return '0.5.3';
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
- // 1. Claude Desktop Config
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 isWin = process.platform === 'win32';
218
- const claudePath = isWin
219
- ? path.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json')
220
- : path.join(HOME, 'Library/Application Support/Claude/claude_desktop_config.json');
221
- if (fs.existsSync(path.dirname(claudePath))) {
222
- let config = { mcpServers: {} };
223
- if (fs.existsSync(claudePath)) {
224
- try {
225
- config = JSON.parse(fs.readFileSync(claudePath, 'utf8'));
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
- catch { }
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
- if (!config.mcpServers)
230
- config.mcpServers = {};
231
- config.mcpServers.lemma = {
232
- command: 'npx',
233
- args: ['-y', '@nxuss/lemma', 'mcp'],
234
- env: { LEMMA_PROJECT: project }
235
- };
236
- fs.writeFileSync(claudePath, JSON.stringify(config, null, 2));
237
- console.log(`✅ [Claude Desktop] Configured automatically! (Restart Claude to activate)`);
272
+ }
273
+ catch (e) {
274
+ console.log(`⚠️ Could not auto-configure Claude Desktop: ${e.message}`);
238
275
  }
239
276
  }
240
- catch (e) {
241
- console.log(`⚠️ Could not auto-configure Claude Desktop: ${e.message}`);
277
+ else {
278
+ console.log(`⏭️ [Claude Desktop] Skip auto-configuration.`);
242
279
  }
243
280
  // 2. Shell Profiles Environment Override
244
- try {
245
- const profiles = [
246
- path.join(HOME, '.zshrc'),
247
- path.join(HOME, '.bashrc'),
248
- path.join(HOME, '.bash_profile'),
249
- path.join(HOME, '.profile')
250
- ];
251
- const lines = [
252
- '',
253
- '# Lemma AI Gateway Overrides',
254
- 'export OPENAI_BASE_URL="http://localhost:8081/v1"',
255
- 'export ANTHROPIC_BASE_URL="http://localhost:8081"',
256
- 'export LEMMA_PROJECT="' + project + '"',
257
- ''
258
- ].join('\n');
259
- for (const profile of profiles) {
260
- if (fs.existsSync(profile)) {
261
- try {
262
- const content = fs.readFileSync(profile, 'utf8');
263
- if (!content.includes('OPENAI_BASE_URL') && !content.includes('LEMMA_PROJECT')) {
264
- fs.appendFileSync(profile, lines);
265
- console.log(`✅ [Shell Profile] Configured ${path.basename(profile)} with local redirect variables!`);
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
- catch (e) {
273
- console.log(`⚠️ Could not auto-configure shell profiles: ${e.message}`);
314
+ else {
315
+ console.log(`⏭️ [Shell Profile] Skip auto-configuration.`);
274
316
  }
275
317
  // 3. Cursor & Windsurf settings.json updates
276
- try {
277
- const isWin = process.platform === 'win32';
278
- const isMac = process.platform === 'darwin';
279
- const candidates = [];
280
- if (isMac) {
281
- candidates.push({
282
- name: 'Cursor',
283
- path: path.join(HOME, 'Library/Application Support/Cursor/User/settings.json')
284
- });
285
- candidates.push({
286
- name: 'Windsurf',
287
- path: path.join(HOME, 'Library/Application Support/Windsurf/User/settings.json')
288
- });
289
- }
290
- else if (isWin) {
291
- const appData = process.env.APPDATA || '';
292
- candidates.push({
293
- name: 'Cursor',
294
- path: path.join(appData, 'Cursor/User/settings.json')
295
- });
296
- candidates.push({
297
- name: 'Windsurf',
298
- path: path.join(appData, 'Windsurf/User/settings.json')
299
- });
300
- }
301
- else {
302
- candidates.push({
303
- name: 'Cursor',
304
- path: path.join(HOME, '.config/Cursor/User/settings.json')
305
- });
306
- candidates.push({
307
- name: 'Windsurf',
308
- path: path.join(HOME, '.config/Windsurf/User/settings.json')
309
- });
310
- }
311
- for (const cand of candidates) {
312
- const dir = path.dirname(cand.path);
313
- if (fs.existsSync(dir)) {
314
- let settings = {};
315
- if (fs.existsSync(cand.path)) {
316
- try {
317
- settings = JSON.parse(fs.readFileSync(cand.path, 'utf8'));
318
+ 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
- catch { }
320
- }
321
- let updated = false;
322
- // Settings configuration to route custom openai/anthropic models through the proxy
323
- const updates = {
324
- "openai.baseURL": "http://localhost:8081/v1",
325
- "openai.apiKey": "dummy-key-for-lemma",
326
- "anthropic.baseURL": "http://localhost:8081",
327
- "anthropic.apiKey": "dummy-key-for-lemma"
328
- };
329
- for (const [key, value] of Object.entries(updates)) {
330
- if (settings[key] !== value) {
331
- settings[key] = value;
332
- updated = true;
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
- catch (e) {
343
- console.log(`⚠️ Could not auto-configure editor settings: ${e.message}`);
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
- s.totalTokensSaved += (fromCache ? (tokensSaved || 2000) : (tokensSaved || 0));
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: fromCache ? (tokensSaved || 2000) : (tokensSaved || 0)
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: stats.totalTokensSaved,
855
- costSaved: (stats.totalTokensSaved / 1000) * 0.02,
937
+ tokensSaved: ledgerSnap.total.tokensSaved,
938
+ costSaved: ledgerSnap.total.costSaved,
856
939
  secretsMasked: secretsMasked,
857
- activeAgents: 1,
858
- totalAgents: 1,
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
- res.json({ agents: [
866
- { id: 'lemma-gateway', name: 'Lemma Gateway', status: 'active', capabilities: ['Privacy Firewall', 'Semantic Cache', 'Complexity Router'], tasksCompleted: stats[this.projectName]?.total || 0, lastSeen: new Date().toISOString(), errorCount: 0, cacheHitRate: 0.85 }
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
- const events = EVENT_LOG.slice().reverse().slice(start, start + limit);
874
- res.json({ events, pagination: { total: EVENT_LOG.length, page, limit } });
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
- res.json({ topQueries: [] });
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
- .action(async () => {
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
  });