@nxuss/lemma 0.7.2 → 0.7.4
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 +6 -5
- package/dashboard/dist/assets/{index-C3X0fqmd.js → index-BNYoIN8-.js} +34 -34
- package/dashboard/dist/assets/index-BNYoIN8-.js.map +1 -0
- package/dashboard/dist/assets/index-DtEKr0hI.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cjs/cli/lemma-proxy.d.ts +0 -6
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +251 -338
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/cjs/mcp/index.js +103 -1
- package/dist/cjs/mcp/index.js.map +1 -1
- package/dist/cjs/mcp/resources.d.ts.map +1 -1
- package/dist/cjs/mcp/resources.js +75 -0
- package/dist/cjs/mcp/resources.js.map +1 -1
- package/dist/cjs/mcp/tools.d.ts.map +1 -1
- package/dist/cjs/mcp/tools.js +174 -0
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/proxy/ContextInjector.d.ts +14 -0
- package/dist/cjs/proxy/ContextInjector.d.ts.map +1 -0
- package/dist/cjs/proxy/ContextInjector.js +148 -0
- package/dist/cjs/proxy/ContextInjector.js.map +1 -0
- package/dist/cjs/proxy/ProjectRegistry.d.ts +17 -0
- package/dist/cjs/proxy/ProjectRegistry.d.ts.map +1 -0
- package/dist/cjs/proxy/ProjectRegistry.js +60 -0
- package/dist/cjs/proxy/ProjectRegistry.js.map +1 -0
- package/dist/esm/cli/lemma-proxy.d.ts +0 -6
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +251 -337
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/mcp/index.js +103 -1
- package/dist/esm/mcp/index.js.map +1 -1
- package/dist/esm/mcp/resources.d.ts.map +1 -1
- package/dist/esm/mcp/resources.js +75 -0
- package/dist/esm/mcp/resources.js.map +1 -1
- package/dist/esm/mcp/tools.d.ts.map +1 -1
- package/dist/esm/mcp/tools.js +175 -1
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/proxy/ContextInjector.d.ts +14 -0
- package/dist/esm/proxy/ContextInjector.d.ts.map +1 -0
- package/dist/esm/proxy/ContextInjector.js +108 -0
- package/dist/esm/proxy/ContextInjector.js.map +1 -0
- package/dist/esm/proxy/ProjectRegistry.d.ts +17 -0
- package/dist/esm/proxy/ProjectRegistry.d.ts.map +1 -0
- package/dist/esm/proxy/ProjectRegistry.js +56 -0
- package/dist/esm/proxy/ProjectRegistry.js.map +1 -0
- package/package.json +2 -2
- package/dashboard/dist/assets/index-BoZujIjB.css +0 -1
- package/dashboard/dist/assets/index-C3X0fqmd.js.map +0 -1
|
@@ -32,6 +32,8 @@ import SemanticScrubber from '../security/SemanticScrubber';
|
|
|
32
32
|
import CloudSyncClient from '../cloud/CloudSyncClient';
|
|
33
33
|
import { pruneHistoryMessages, squeezePrompt } from '../utils/ContextSqueezer';
|
|
34
34
|
import { AgentMultiplexer } from '../proxy/AgentMultiplexer';
|
|
35
|
+
import { ProjectRegistry } from '../proxy/ProjectRegistry';
|
|
36
|
+
import { shouldInject, buildMinimalContext, buildDeepContext, sniffProjectFromMessages } from '../proxy/ContextInjector';
|
|
35
37
|
import { savingsLedger } from '../utils/SavingsLedger';
|
|
36
38
|
const chroma = new ChromaClient({ host: 'localhost', port: 8000 });
|
|
37
39
|
const dummyEmbeddingFunction = { generate: (texts) => Promise.resolve([]) };
|
|
@@ -47,381 +49,184 @@ const PORT_FILE = path.join(CACHE_DIR, 'proxy.port');
|
|
|
47
49
|
const STATS_FILE = path.join(CACHE_DIR, 'stats.json');
|
|
48
50
|
const USAGE_FILE = path.join(CACHE_DIR, 'usage.json');
|
|
49
51
|
const LICENSE_FILE = path.join(CACHE_DIR, 'license.json');
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
};
|
|
58
|
-
function logEvent(event) {
|
|
59
|
-
const ev = { id: Math.random().toString(36).slice(2, 11), timestamp: Date.now(), ...event };
|
|
60
|
-
EVENT_LOG.push(ev);
|
|
61
|
-
if (EVENT_LOG.length > MAX_EVENTS)
|
|
62
|
-
EVENT_LOG.shift();
|
|
63
|
-
// Track MCP activity
|
|
64
|
-
if (event.type === 'mcp_tool_call') {
|
|
65
|
-
MCP_STATUS.lastActivity = Date.now();
|
|
66
|
-
MCP_STATUS.toolCallCount++;
|
|
67
|
-
MCP_STATUS.status = 'active';
|
|
52
|
+
const CLIPBOARD_PID_FILE = path.join(CACHE_DIR, 'clipboard.pid');
|
|
53
|
+
const FREE_LIMIT = 300;
|
|
54
|
+
const VALIDATE_URL = 'https://lemma.nxus.studio/api/validate-license';
|
|
55
|
+
async function checkDependency(url) {
|
|
56
|
+
try {
|
|
57
|
+
const resp = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(2000) });
|
|
58
|
+
return resp.ok;
|
|
68
59
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const snap = savingsLedger.getSnapshot();
|
|
72
|
-
if (snap.total.tokensSaved > 0) {
|
|
73
|
-
console.log('\n' + savingsLedger.getTerminalSummary() + '\n');
|
|
74
|
-
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
75
62
|
}
|
|
76
63
|
}
|
|
77
|
-
|
|
78
|
-
const WARN_PCT = 0.8;
|
|
79
|
-
const VALIDATE_URL = 'https://lemma.nxus.studio/api/v1/validate';
|
|
80
|
-
if (!fs.existsSync(CACHE_DIR))
|
|
81
|
-
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
82
|
-
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
83
|
-
async function readJson(file, fallback) {
|
|
64
|
+
function getPidByPort(port) {
|
|
84
65
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
66
|
+
const result = require('child_process').execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
67
|
+
return result || null;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
89
71
|
}
|
|
90
|
-
catch { }
|
|
91
|
-
return fallback;
|
|
92
72
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
const tempFile = `${file}.${crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2, 15)}.tmp`;
|
|
99
|
-
await fs.promises.writeFile(tempFile, JSON.stringify(data, null, 2));
|
|
100
|
-
await fs.promises.rename(tempFile, file);
|
|
73
|
+
function readJson(filePath, defaultVal) {
|
|
74
|
+
try {
|
|
75
|
+
if (fs.existsSync(filePath)) {
|
|
76
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
101
77
|
}
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
writeQueues.set(file, nextChain);
|
|
106
|
-
return nextChain;
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
return defaultVal;
|
|
107
81
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
82
|
+
function writeJson(filePath, data) {
|
|
83
|
+
try {
|
|
84
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
110
87
|
}
|
|
111
88
|
function getCollectionName(projectName) {
|
|
112
|
-
|
|
113
|
-
cleanName = cleanName.replace(/^[^a-zA-Z0-9]+/, '');
|
|
114
|
-
cleanName = cleanName.replace(/[^a-zA-Z0-9]+$/, '');
|
|
115
|
-
if (cleanName.length < 3) {
|
|
116
|
-
cleanName = 'lemma_proj_' + cleanName;
|
|
117
|
-
}
|
|
118
|
-
return `lemma-cache-${cleanName}`;
|
|
89
|
+
return `lemma-cache-${projectName}`;
|
|
119
90
|
}
|
|
120
91
|
function detectProject() {
|
|
121
|
-
const
|
|
92
|
+
const cwd = process.cwd();
|
|
93
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
122
94
|
if (fs.existsSync(pkgPath)) {
|
|
123
95
|
try {
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
return
|
|
96
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
97
|
+
if (pkg.name)
|
|
98
|
+
return pkg.name;
|
|
127
99
|
}
|
|
128
100
|
catch { }
|
|
129
101
|
}
|
|
130
|
-
return path.basename(
|
|
102
|
+
return path.basename(cwd);
|
|
131
103
|
}
|
|
132
|
-
function ensureProjectDir(
|
|
133
|
-
const
|
|
134
|
-
|
|
104
|
+
function ensureProjectDir(projectName) {
|
|
105
|
+
const hash = crypto.createHash('sha1').update(projectName).digest('hex').slice(0, 12);
|
|
106
|
+
const dir = path.join(CACHE_DIR, hash);
|
|
107
|
+
if (!fs.existsSync(dir)) {
|
|
135
108
|
fs.mkdirSync(dir, { recursive: true });
|
|
136
|
-
|
|
137
|
-
if (!fs.existsSync(marker))
|
|
138
|
-
fs.writeFileSync(marker, name);
|
|
109
|
+
}
|
|
139
110
|
return dir;
|
|
140
111
|
}
|
|
141
|
-
function
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
else {
|
|
150
|
-
const out = execSync(`lsof -t -i :${port}`).toString().trim();
|
|
151
|
-
return out.split('\n')[0]; // Take first if multiple
|
|
152
|
-
}
|
|
112
|
+
function ensureGitIgnore() {
|
|
113
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
114
|
+
const lemmaRules = '\n# Lemma AI Gateway\n.lemma/\n';
|
|
115
|
+
if (fs.existsSync(gitignorePath)) {
|
|
116
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
117
|
+
if (content.includes('.lemma/'))
|
|
118
|
+
return false;
|
|
119
|
+
fs.appendFileSync(gitignorePath, lemmaRules);
|
|
153
120
|
}
|
|
154
|
-
|
|
155
|
-
|
|
121
|
+
else {
|
|
122
|
+
fs.writeFileSync(gitignorePath, lemmaRules);
|
|
156
123
|
}
|
|
124
|
+
return true;
|
|
157
125
|
}
|
|
158
|
-
async function
|
|
126
|
+
async function ensureChromaRunning() {
|
|
159
127
|
try {
|
|
160
|
-
|
|
161
|
-
return res.status >= 200 && res.status < 500;
|
|
128
|
+
await chroma.heartbeat();
|
|
162
129
|
}
|
|
163
130
|
catch {
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
async function ensureChromaRunning() {
|
|
168
|
-
const chromaPort = process.env.CHROMA_PORT || '8000';
|
|
169
|
-
const chromaHost = process.env.CHROMA_HOST || 'http://localhost';
|
|
170
|
-
const chromaUrl = `${chromaHost}:${chromaPort}`;
|
|
171
|
-
const isChromaUp = await checkDependency(`${chromaUrl}/api/v1/heartbeat`);
|
|
172
|
-
if (isChromaUp) {
|
|
173
|
-
console.log(`✅ \x1b[32m[ChromaDB]\x1b[0m ChromaDB is already running on port ${chromaPort}`);
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
console.log(`\n📦 \x1b[35m[ChromaDB]\x1b[0m ChromaDB is not running on port ${chromaPort}.`);
|
|
177
|
-
console.log(`🔄 Starting ChromaDB in background...`);
|
|
178
|
-
const chromaDataPath = path.join(CACHE_DIR, 'chroma_data');
|
|
179
|
-
try {
|
|
131
|
+
console.log('⚠️ ChromaDB is not running. Starting it...');
|
|
180
132
|
const { spawn } = require('child_process');
|
|
181
|
-
|
|
182
|
-
fs.mkdirSync(chromaDataPath, { recursive: true });
|
|
183
|
-
}
|
|
184
|
-
const chromaProcess = spawn('chroma', ['run', '--path', chromaDataPath, '--port', chromaPort], {
|
|
133
|
+
const cp = spawn('chroma', ['run', '--path', path.join(CACHE_DIR, 'chroma_data')], {
|
|
185
134
|
stdio: 'ignore',
|
|
186
|
-
detached: true
|
|
135
|
+
detached: true,
|
|
187
136
|
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
for (let i = 0; i < 5; i++) {
|
|
191
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
192
|
-
const up = await checkDependency(`${chromaUrl}/api/v1/heartbeat`);
|
|
193
|
-
if (up) {
|
|
194
|
-
console.log(`✅ \x1b[32m[ChromaDB]\x1b[0m ChromaDB has been started and is ready!`);
|
|
195
|
-
return true;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
console.log(`⚠️ \x1b[33m[ChromaDB]\x1b[0m ChromaDB was started but is taking longer to respond. It will continue starting in the background.`);
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
catch (err) {
|
|
202
|
-
console.error(`❌ Failed to automatically start ChromaDB: ${err.message}`);
|
|
203
|
-
console.error(`👉 Please start it manually: chroma run --path ${chromaDataPath} --port ${chromaPort}`);
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
function ensureGitIgnore() {
|
|
208
|
-
const gi = path.join(process.cwd(), '.gitignore');
|
|
209
|
-
let added = false;
|
|
210
|
-
try {
|
|
211
|
-
let content = '';
|
|
212
|
-
if (fs.existsSync(gi)) {
|
|
213
|
-
content = fs.readFileSync(gi, 'utf8');
|
|
214
|
-
}
|
|
215
|
-
const appendLines = [];
|
|
216
|
-
if (!content.includes('.lemma/')) {
|
|
217
|
-
appendLines.push('# Lemma Context Logs', '.lemma/');
|
|
218
|
-
added = true;
|
|
219
|
-
}
|
|
220
|
-
if (!content.includes('chroma_data/')) {
|
|
221
|
-
appendLines.push('# Lemma Local Vector Database', 'chroma_data/');
|
|
222
|
-
added = true;
|
|
223
|
-
}
|
|
224
|
-
if (appendLines.length > 0) {
|
|
225
|
-
const prefix = content.endsWith('\n') || content === '' ? '' : '\n';
|
|
226
|
-
fs.appendFileSync(gi, prefix + appendLines.join('\n') + '\n');
|
|
227
|
-
}
|
|
137
|
+
cp.unref();
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
228
139
|
}
|
|
229
|
-
catch { }
|
|
230
|
-
return added;
|
|
231
140
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
236
|
-
// Load configuration if it exists
|
|
237
|
-
let configDisabled = false;
|
|
238
|
-
let configEditor = true;
|
|
239
|
-
let configShell = true;
|
|
240
|
-
let configClaude = true;
|
|
241
|
-
try {
|
|
242
|
-
const configPath = path.join(process.cwd(), 'lemma.config.json');
|
|
243
|
-
if (fs.existsSync(configPath)) {
|
|
244
|
-
const rawConfig = fs.readFileSync(configPath, 'utf8');
|
|
245
|
-
const config = JSON.parse(rawConfig);
|
|
246
|
-
const systemConfig = config.system || {};
|
|
247
|
-
const autoConfig = systemConfig.autoConfigure || {};
|
|
248
|
-
if (autoConfig.disabled === true)
|
|
249
|
-
configDisabled = true;
|
|
250
|
-
if (autoConfig.editor === false)
|
|
251
|
-
configEditor = false;
|
|
252
|
-
if (autoConfig.shell === false)
|
|
253
|
-
configShell = false;
|
|
254
|
-
if (autoConfig.claude === false)
|
|
255
|
-
configClaude = false;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch { }
|
|
259
|
-
// Merge CLI overrides and config
|
|
260
|
-
const shouldConfigure = cliOpts?.configure !== false && !configDisabled;
|
|
261
|
-
if (!shouldConfigure) {
|
|
262
|
-
console.log(`⏭️ [Auto-Configure] Skipped auto-configuration (disabled via options or config).`);
|
|
141
|
+
function autoConfigureAll(project, opts) {
|
|
142
|
+
if (!opts.configure) {
|
|
143
|
+
console.log('⏭️ Skipping auto-configuration (--no-configure)');
|
|
263
144
|
return;
|
|
264
145
|
}
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const isWin = process.platform === 'win32';
|
|
272
|
-
const claudePath = isWin
|
|
273
|
-
? path.join(process.env.APPDATA || '', 'Claude/claude_desktop_config.json')
|
|
274
|
-
: path.join(HOME, 'Library/Application Support/Claude/claude_desktop_config.json');
|
|
275
|
-
if (fs.existsSync(path.dirname(claudePath))) {
|
|
276
|
-
let config = { mcpServers: {} };
|
|
277
|
-
if (fs.existsSync(claudePath)) {
|
|
278
|
-
try {
|
|
279
|
-
config = JSON.parse(fs.readFileSync(claudePath, 'utf8'));
|
|
280
|
-
}
|
|
281
|
-
catch { }
|
|
282
|
-
}
|
|
283
|
-
if (!config.mcpServers)
|
|
284
|
-
config.mcpServers = {};
|
|
285
|
-
config.mcpServers.lemma = {
|
|
286
|
-
command: 'npx',
|
|
287
|
-
args: ['-y', '@nxuss/lemma', 'mcp'],
|
|
288
|
-
env: { LEMMA_PROJECT: project }
|
|
289
|
-
};
|
|
290
|
-
fs.writeFileSync(claudePath, JSON.stringify(config, null, 2));
|
|
291
|
-
console.log(`✅ [Claude Desktop] Configured automatically in: ${claudePath} (Restart Claude to activate)`);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
catch (e) {
|
|
295
|
-
console.log(`⚠️ Could not auto-configure Claude Desktop: ${e.message}`);
|
|
146
|
+
const envFile = path.join(process.cwd(), '.env');
|
|
147
|
+
const lemmaConfig = `\n# Lemma AI Gateway Configuration\nOPENAI_BASE_URL=http://localhost:8081/v1\nLEMMA_PROJECT=${project}\n`;
|
|
148
|
+
if (fs.existsSync(envFile)) {
|
|
149
|
+
const content = fs.readFileSync(envFile, 'utf8');
|
|
150
|
+
if (!content.includes('OPENAI_BASE_URL')) {
|
|
151
|
+
fs.appendFileSync(envFile, lemmaConfig);
|
|
296
152
|
}
|
|
297
153
|
}
|
|
298
|
-
|
|
299
|
-
|
|
154
|
+
const mcpDir = path.join(process.cwd(), '.lemma', 'mcp');
|
|
155
|
+
if (!fs.existsSync(mcpDir)) {
|
|
156
|
+
fs.mkdirSync(mcpDir, { recursive: true });
|
|
300
157
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const content = fs.readFileSync(profile, 'utf8');
|
|
322
|
-
if (!content.includes('OPENAI_BASE_URL') && !content.includes('LEMMA_PROJECT')) {
|
|
323
|
-
fs.appendFileSync(profile, lines);
|
|
324
|
-
console.log(`✅ [Shell Profile] Configured ${path.basename(profile)} with local redirect variables!`);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch { }
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
catch (e) {
|
|
332
|
-
console.log(`⚠️ Could not auto-configure shell profiles: ${e.message}`);
|
|
333
|
-
}
|
|
158
|
+
console.log(`✅ Auto-configured Lemma for [${project}]`);
|
|
159
|
+
}
|
|
160
|
+
const EVENT_LOG = [];
|
|
161
|
+
const MAX_EVENTS = 100;
|
|
162
|
+
// MCP Status tracking
|
|
163
|
+
const MCP_STATUS = {
|
|
164
|
+
lastActivity: Date.now(),
|
|
165
|
+
toolCallCount: 0,
|
|
166
|
+
status: 'idle'
|
|
167
|
+
};
|
|
168
|
+
function logEvent(event) {
|
|
169
|
+
const ev = { id: Math.random().toString(36).slice(2, 11), timestamp: Date.now(), ...event };
|
|
170
|
+
EVENT_LOG.push(ev);
|
|
171
|
+
if (EVENT_LOG.length > MAX_EVENTS)
|
|
172
|
+
EVENT_LOG.shift();
|
|
173
|
+
// Track MCP activity
|
|
174
|
+
if (event.type === 'mcp_tool_call') {
|
|
175
|
+
MCP_STATUS.lastActivity = Date.now();
|
|
176
|
+
MCP_STATUS.toolCallCount++;
|
|
177
|
+
MCP_STATUS.status = 'active';
|
|
334
178
|
}
|
|
335
|
-
|
|
336
|
-
|
|
179
|
+
// Print a terminal savings summary every 10 events
|
|
180
|
+
if (EVENT_LOG.length > 0 && EVENT_LOG.length % 10 === 0) {
|
|
181
|
+
const snap = savingsLedger.getSnapshot();
|
|
182
|
+
if (snap.total.tokensSaved > 0) {
|
|
183
|
+
console.log('\n' + savingsLedger.getTerminalSummary() + '\n');
|
|
184
|
+
}
|
|
337
185
|
}
|
|
338
|
-
|
|
339
|
-
|
|
186
|
+
}
|
|
187
|
+
const LEMMA_RULES_BLOCK = `
|
|
188
|
+
🧠 Lemma MCP — Autonomous Behaviors
|
|
189
|
+
|
|
190
|
+
I have Lemma MCP tools connected. I MUST use them proactively:
|
|
191
|
+
|
|
192
|
+
1. When starting work on a project, call \`get_project_onboarding\` or read \`lemma://project/context\` to understand the architecture.
|
|
193
|
+
2. Before answering technical questions, call \`search_memory\` for relevant past solutions.
|
|
194
|
+
3. When examining a file, call \`get_telepathic_hints\` for relevant context.
|
|
195
|
+
4. After solving a problem, call \`store_memory\` to persist the solution for future sessions.
|
|
196
|
+
5. Use \`read_workspace_file\`, \`list_workspace_dir\`, \`search_workspace\` instead of asking the user to read or navigate files.
|
|
197
|
+
6. Always call \`validate_patch_sandbox\` before writing patches.
|
|
198
|
+
7. Read \`lemma://runtime/context\` first when the user reports a bug.
|
|
199
|
+
`;
|
|
200
|
+
function injectIdeRules(projectName) {
|
|
201
|
+
const content = `\n# ${projectName} — ${LEMMA_RULES_BLOCK.trim()}\n`;
|
|
202
|
+
const rulesTargets = [
|
|
203
|
+
{ name: 'Cursor', path: path.join(process.cwd(), '.cursorrules') },
|
|
204
|
+
{ name: 'Cursor (v2)', path: path.join(process.cwd(), '.cursor/rules/lemma.mdc') },
|
|
205
|
+
{ name: 'Windsurf', path: path.join(process.cwd(), '.windsurfrules') },
|
|
206
|
+
{ name: 'Cline', path: path.join(process.cwd(), '.clinerules') },
|
|
207
|
+
];
|
|
208
|
+
for (const target of rulesTargets) {
|
|
340
209
|
try {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
candidates.push({
|
|
346
|
-
name: 'Cursor',
|
|
347
|
-
path: path.join(HOME, 'Library/Application Support/Cursor/User/settings.json')
|
|
348
|
-
});
|
|
349
|
-
candidates.push({
|
|
350
|
-
name: 'Windsurf',
|
|
351
|
-
path: path.join(HOME, 'Library/Application Support/Windsurf/User/settings.json')
|
|
352
|
-
});
|
|
353
|
-
candidates.push({
|
|
354
|
-
name: 'VS Code',
|
|
355
|
-
path: path.join(HOME, 'Library/Application Support/Code/User/settings.json')
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
else if (isWin) {
|
|
359
|
-
const appData = process.env.APPDATA || '';
|
|
360
|
-
candidates.push({
|
|
361
|
-
name: 'Cursor',
|
|
362
|
-
path: path.join(appData, 'Cursor/User/settings.json')
|
|
363
|
-
});
|
|
364
|
-
candidates.push({
|
|
365
|
-
name: 'Windsurf',
|
|
366
|
-
path: path.join(appData, 'Windsurf/User/settings.json')
|
|
367
|
-
});
|
|
368
|
-
candidates.push({
|
|
369
|
-
name: 'VS Code',
|
|
370
|
-
path: path.join(appData, 'Code/User/settings.json')
|
|
371
|
-
});
|
|
210
|
+
const dir = path.dirname(target.path);
|
|
211
|
+
if (target.name === 'Cursor (v2)') {
|
|
212
|
+
if (!fs.existsSync(dir))
|
|
213
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
372
214
|
}
|
|
373
|
-
else {
|
|
374
|
-
|
|
375
|
-
name: 'Cursor',
|
|
376
|
-
path: path.join(HOME, '.config/Cursor/User/settings.json')
|
|
377
|
-
});
|
|
378
|
-
candidates.push({
|
|
379
|
-
name: 'Windsurf',
|
|
380
|
-
path: path.join(HOME, '.config/Windsurf/User/settings.json')
|
|
381
|
-
});
|
|
382
|
-
candidates.push({
|
|
383
|
-
name: 'VS Code',
|
|
384
|
-
path: path.join(HOME, '.config/Code/User/settings.json')
|
|
385
|
-
});
|
|
215
|
+
else if (!fs.existsSync(dir)) {
|
|
216
|
+
continue;
|
|
386
217
|
}
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
if (fs.existsSync(cand.path)) {
|
|
392
|
-
try {
|
|
393
|
-
settings = JSON.parse(fs.readFileSync(cand.path, 'utf8'));
|
|
394
|
-
}
|
|
395
|
-
catch { }
|
|
396
|
-
}
|
|
397
|
-
let updated = false;
|
|
398
|
-
// Settings configuration to route custom openai/anthropic models through the proxy
|
|
399
|
-
const updates = {
|
|
400
|
-
"openai.baseURL": "http://localhost:8081/v1",
|
|
401
|
-
"openai.apiKey": "dummy-key-for-lemma",
|
|
402
|
-
"anthropic.baseURL": "http://localhost:8081",
|
|
403
|
-
"anthropic.apiKey": "dummy-key-for-lemma"
|
|
404
|
-
};
|
|
405
|
-
for (const [key, value] of Object.entries(updates)) {
|
|
406
|
-
if (settings[key] !== value) {
|
|
407
|
-
settings[key] = value;
|
|
408
|
-
updated = true;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
if (updated) {
|
|
412
|
-
fs.writeFileSync(cand.path, JSON.stringify(settings, null, 2));
|
|
413
|
-
console.log(`✅ [${cand.name}] Configured editor settings in ${cand.path} to route via local gateway!`);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
218
|
+
if (fs.existsSync(target.path)) {
|
|
219
|
+
const existing = fs.readFileSync(target.path, 'utf8');
|
|
220
|
+
if (existing.includes('Lemma MCP'))
|
|
221
|
+
continue;
|
|
416
222
|
}
|
|
223
|
+
fs.writeFileSync(target.path, content, { flag: 'a' });
|
|
224
|
+
console.log(`✅ [${target.name}] Injected Lemma rules: ${target.path}`);
|
|
417
225
|
}
|
|
418
|
-
catch
|
|
419
|
-
console.log(`⚠️ Could not
|
|
226
|
+
catch {
|
|
227
|
+
console.log(`⚠️ Could not inject rules for ${target.name}`);
|
|
420
228
|
}
|
|
421
229
|
}
|
|
422
|
-
else {
|
|
423
|
-
console.log(`⏭️ [Editor Settings] Skip auto-configuration.`);
|
|
424
|
-
}
|
|
425
230
|
}
|
|
426
231
|
export function startBackgroundClipboardWatcher(cliOpts) {
|
|
427
232
|
// Load configuration if it exists
|
|
@@ -950,6 +755,8 @@ class LemmaServer {
|
|
|
950
755
|
this.sessions = new Map();
|
|
951
756
|
this.timeline = [];
|
|
952
757
|
this.lastFileContents = new Map();
|
|
758
|
+
this.registry = new ProjectRegistry();
|
|
759
|
+
this.projectSessions = new Map();
|
|
953
760
|
this.port = port;
|
|
954
761
|
this.projectName = projectName || detectProject();
|
|
955
762
|
this.projectDir = ensureProjectDir(this.projectName);
|
|
@@ -995,6 +802,18 @@ class LemmaServer {
|
|
|
995
802
|
uptime: Math.floor(process.uptime())
|
|
996
803
|
});
|
|
997
804
|
});
|
|
805
|
+
// Project registration from MCP handshake
|
|
806
|
+
this.app.post('/api/handshake', (req, res) => {
|
|
807
|
+
const { projectPath, projectName, stack } = req.body;
|
|
808
|
+
if (projectPath && projectName) {
|
|
809
|
+
this.registry.register(projectPath, projectName, stack || '');
|
|
810
|
+
console.log(`📋 [Handshake] Project registered: "${projectName}" (${projectPath})`);
|
|
811
|
+
res.json({ ok: true });
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
res.status(400).json({ error: 'projectPath and projectName required' });
|
|
815
|
+
}
|
|
816
|
+
});
|
|
998
817
|
this.app.get('/v1/models', (req, res) => {
|
|
999
818
|
res.json({ object: 'list', data: [
|
|
1000
819
|
{ id: 'gpt-4o', object: 'model', owned_by: 'lemma-proxy' },
|
|
@@ -1263,7 +1082,7 @@ class LemmaServer {
|
|
|
1263
1082
|
totalTokensSaved: 0,
|
|
1264
1083
|
providers: {}
|
|
1265
1084
|
};
|
|
1266
|
-
writeJson(STATS_FILE, this.stats)
|
|
1085
|
+
writeJson(STATS_FILE, this.stats);
|
|
1267
1086
|
}
|
|
1268
1087
|
// Clear Chroma DB collection
|
|
1269
1088
|
try {
|
|
@@ -1487,7 +1306,7 @@ class LemmaServer {
|
|
|
1487
1306
|
s.providers['mcp'] = { hits: 0, misses: 0 };
|
|
1488
1307
|
source === 'cache' ? s.providers['mcp'].hits++ : s.providers['mcp'].misses++;
|
|
1489
1308
|
// Write updated stats back to disk immediately
|
|
1490
|
-
writeJson(STATS_FILE, this.stats)
|
|
1309
|
+
writeJson(STATS_FILE, this.stats);
|
|
1491
1310
|
// Push to live telemetry logEvent array so the Dashboard event timeline updates instantly!
|
|
1492
1311
|
logEvent({
|
|
1493
1312
|
type: source === 'cache' ? 'cache:hit' : source,
|
|
@@ -1628,6 +1447,15 @@ class LemmaServer {
|
|
|
1628
1447
|
}
|
|
1629
1448
|
console.log(`🌌 \x1b[35m[Multiverse]\x1b[0m Captured micro-snapshot ${snapshot.id} (${changedFiles.length} file edits mapped).`);
|
|
1630
1449
|
}
|
|
1450
|
+
detectActiveProject(req) {
|
|
1451
|
+
const mcpActive = this.registry.getActive();
|
|
1452
|
+
if (mcpActive)
|
|
1453
|
+
return mcpActive;
|
|
1454
|
+
const sniffed = sniffProjectFromMessages(req.body.messages || [], this.registry);
|
|
1455
|
+
if (sniffed)
|
|
1456
|
+
return sniffed;
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1631
1459
|
async handleCompletion(req, res, provider) {
|
|
1632
1460
|
const t0 = Date.now();
|
|
1633
1461
|
const pro = await isPro();
|
|
@@ -1748,6 +1576,24 @@ Adjusted Answer:`;
|
|
|
1748
1576
|
return res.json(unmaskedData);
|
|
1749
1577
|
}
|
|
1750
1578
|
}
|
|
1579
|
+
// ── 3. Context Injection (for cache misses) ──
|
|
1580
|
+
const activeProject = this.detectActiveProject(req);
|
|
1581
|
+
if (activeProject) {
|
|
1582
|
+
const psKey = activeProject.path;
|
|
1583
|
+
if (!this.projectSessions.has(psKey)) {
|
|
1584
|
+
this.projectSessions.set(psKey, { alreadyInjected: false });
|
|
1585
|
+
}
|
|
1586
|
+
const session = this.projectSessions.get(psKey);
|
|
1587
|
+
const injectionLevel = shouldInject(activeProject, req.body.messages, session);
|
|
1588
|
+
if (injectionLevel !== 'none') {
|
|
1589
|
+
const context = injectionLevel === 'deep'
|
|
1590
|
+
? await buildDeepContext(activeProject, this.port)
|
|
1591
|
+
: buildMinimalContext(activeProject);
|
|
1592
|
+
req.body.messages.unshift({ role: 'system', content: context });
|
|
1593
|
+
session.alreadyInjected = true;
|
|
1594
|
+
console.log(`🧠 [Context] Injected ${injectionLevel} context for "${activeProject.name}"`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1751
1597
|
// ── 2. Cache Miss Path ──
|
|
1752
1598
|
const routingDecision = complexityRouter.evaluate(prompt, originalModel);
|
|
1753
1599
|
const model = routingDecision.model;
|
|
@@ -1988,9 +1834,46 @@ Adjusted Answer:`;
|
|
|
1988
1834
|
}
|
|
1989
1835
|
}
|
|
1990
1836
|
// ── CLI ────────────────────────────────────────────────────────────────────────
|
|
1991
|
-
|
|
1837
|
+
function showWelcomeScreen() {
|
|
1838
|
+
console.log(`
|
|
1839
|
+
\x1b[1m\x1b[35m _ \x1b[0m
|
|
1840
|
+
\x1b[1m\x1b[35m | | ___ _ __ ___ __ _ \x1b[0m \x1b[1mLEMMA\x1b[0m \x1b[35mv${VERSION}\x1b[0m
|
|
1841
|
+
\x1b[1m\x1b[35m | | / _ \\ '_ \` _ \\ / _\` |\x1b[0m
|
|
1842
|
+
\x1b[1m\x1b[35m | |__| __/ | | | | | (_| |\x1b[0m Intelligent AI Gateway
|
|
1843
|
+
\x1b[1m\x1b[35m |_____\\___|_| |_| |_|\\__,_|\x1b[0m
|
|
1844
|
+
|
|
1845
|
+
\x1b[1mQuick Start\x1b[0m
|
|
1846
|
+
\x1b[33mlemma init\x1b[0m Initialize Lemma in this project
|
|
1847
|
+
\x1b[33mlemma start\x1b[0m Start the proxy server
|
|
1848
|
+
\x1b[33mlemma mcp\x1b[0m Start MCP Server for Cursor/Claude
|
|
1849
|
+
|
|
1850
|
+
\x1b[1mFree Features\x1b[0m (all included)
|
|
1851
|
+
🔒 Privacy Firewall 💾 Exact-match Cache
|
|
1852
|
+
🔀 Complexity Router 📡 Telepathic Clipboard
|
|
1853
|
+
📊 Basic Usage Stats 🧠 MCP Tools & Resources
|
|
1854
|
+
|
|
1855
|
+
\x1b[1mPro Features\x1b[0m (\x1b[35mlemma activate <key>\x1b[0m or \x1b[36mhttps://lemma.nxus.studio/upgrade\x1b[0m)
|
|
1856
|
+
🎯 Semantic Vector Cache 🌐 Hive Mind Cloud Sync
|
|
1857
|
+
🛸 Autopilot Healer 🩺 Auto-Diagnose & Heal
|
|
1858
|
+
🌌 Codebase Multiverse 🏗️ Architecture Onboarding
|
|
1859
|
+
|
|
1860
|
+
\x1b[90mRun \`lemma --help\` for all commands.\x1b[0m
|
|
1861
|
+
`);
|
|
1862
|
+
}
|
|
1863
|
+
program
|
|
1864
|
+
.name('lemma')
|
|
1865
|
+
.description('Intelligent AI Gateway — cache, route, and optimize AI requests')
|
|
1866
|
+
.version(VERSION)
|
|
1867
|
+
.addHelpText('after', `
|
|
1868
|
+
\x1b[1mPlan Comparison\x1b[0m
|
|
1869
|
+
\x1b[32mFree\x1b[0m 500 req/mo · Exact cache · Privacy firewall · MCP tools
|
|
1870
|
+
\x1b[35mPro\x1b[0m Unlimited · Semantic cache · Cloud sync · Autopilot · Heal
|
|
1871
|
+
\x1b[36mhttps://lemma.nxus.studio/upgrade\x1b[0m
|
|
1872
|
+
|
|
1873
|
+
\x1b[90mActivate Pro: lemma activate <key>\x1b[0m
|
|
1874
|
+
`);
|
|
1992
1875
|
program.command('mcp')
|
|
1993
|
-
.description('Start the
|
|
1876
|
+
.description('Start the MCP Server for Cursor / Claude Desktop integration')
|
|
1994
1877
|
.action(() => {
|
|
1995
1878
|
const { spawn } = require('child_process');
|
|
1996
1879
|
// Robust search candidates for both development (TS source) and compiled (JS production) environments.
|
|
@@ -2045,6 +1928,7 @@ program.command('start')
|
|
|
2045
1928
|
// Keep configurations up to date even if server was already started
|
|
2046
1929
|
const runningProject = resp.data.project || detectProject();
|
|
2047
1930
|
autoConfigureAll(runningProject, { configure: opts.configure });
|
|
1931
|
+
injectIdeRules(runningProject);
|
|
2048
1932
|
startBackgroundClipboardWatcher({ clipboard: opts.clipboard });
|
|
2049
1933
|
process.exit(0);
|
|
2050
1934
|
}
|
|
@@ -2052,6 +1936,7 @@ program.command('start')
|
|
|
2052
1936
|
catch { }
|
|
2053
1937
|
const startProject = opts.project || detectProject();
|
|
2054
1938
|
autoConfigureAll(startProject, { configure: opts.configure });
|
|
1939
|
+
injectIdeRules(startProject);
|
|
2055
1940
|
startBackgroundClipboardWatcher({ clipboard: opts.clipboard });
|
|
2056
1941
|
// Automatically enable autopilot if stack is enabled and user is Pro
|
|
2057
1942
|
const runAutopilot = opts.autopilot || opts.stack;
|
|
@@ -2264,8 +2149,12 @@ program.command('init')
|
|
|
2264
2149
|
}
|
|
2265
2150
|
console.log('\n🧠 \x1b[35mAuto-Setup MCP and redirection overrides for AI IDEs...\x1b[0m');
|
|
2266
2151
|
autoConfigureAll(project, { configure: opts.configure });
|
|
2267
|
-
|
|
2268
|
-
console.log('
|
|
2152
|
+
injectIdeRules(project);
|
|
2153
|
+
console.log('\n✨ Project initialized for the Agentic Era!\n');
|
|
2154
|
+
console.log('\x1b[1mNext steps:\x1b[0m');
|
|
2155
|
+
console.log(' \x1b[33mlemma start\x1b[0m Start the proxy & unlock AI savings');
|
|
2156
|
+
console.log(' \x1b[33mlemma upgrade\x1b[0m See Pro features & get a key');
|
|
2157
|
+
console.log(' \x1b[33mlemma --help\x1b[0m All available commands\n');
|
|
2269
2158
|
});
|
|
2270
2159
|
// ── Auto-Healing Diagnostics (Zero-Cost Suite) ───────────────────────────────
|
|
2271
2160
|
export async function performAutoHeal(apply) {
|
|
@@ -2435,7 +2324,7 @@ Ensure you return strictly JSON. Do not return any other text outside the JSON b
|
|
|
2435
2324
|
}
|
|
2436
2325
|
}
|
|
2437
2326
|
program.command('heal')
|
|
2438
|
-
.description('Diagnose and auto-heal the latest local server crash from .lemma/live-context.md')
|
|
2327
|
+
.description('[Pro] Diagnose and auto-heal the latest local server crash from .lemma/live-context.md')
|
|
2439
2328
|
.option('--apply', 'Apply the auto-generated code fix automatically', false)
|
|
2440
2329
|
.action(async (opts) => {
|
|
2441
2330
|
try {
|
|
@@ -2464,7 +2353,7 @@ program.command('heal')
|
|
|
2464
2353
|
}
|
|
2465
2354
|
});
|
|
2466
2355
|
program.command('autopilot')
|
|
2467
|
-
.description('Start the autonomous background compiler watcher & active healer')
|
|
2356
|
+
.description('[Pro] Start the autonomous background compiler watcher & active healer')
|
|
2468
2357
|
.option('--dir <directory>', 'Directory to monitor for changes', 'src')
|
|
2469
2358
|
.action(async (opts) => {
|
|
2470
2359
|
try {
|
|
@@ -2489,6 +2378,26 @@ program.command('autopilot')
|
|
|
2489
2378
|
process.exit(1);
|
|
2490
2379
|
}
|
|
2491
2380
|
});
|
|
2381
|
+
program.command('upgrade')
|
|
2382
|
+
.description('Learn about Pro features and get an activation key')
|
|
2383
|
+
.action(() => {
|
|
2384
|
+
console.log(`
|
|
2385
|
+
\x1b[1m\x1b[35mLemma Pro\x1b[0m — Unlimited AI Gateway power
|
|
2386
|
+
|
|
2387
|
+
\x1b[1mPro features:\x1b[0m
|
|
2388
|
+
🎯 \x1b[1mSemantic Vector Cache\x1b[0m — Smarter cache hits via vector similarity
|
|
2389
|
+
🌐 \x1b[1mHive Mind Cloud Sync\x1b[0m — Cross-project cache & bug telepathy
|
|
2390
|
+
🛸 \x1b[1mAutopilot Healer\x1b[0m — Autonomous background compiler watcher
|
|
2391
|
+
🩺 \x1b[1mAuto-Diagnose & Heal\x1b[0m — AI-driven crash analysis & patch
|
|
2392
|
+
🌌 \x1b[1mCodebase Multiverse\x1b[0m — AST time-travel & snapshots
|
|
2393
|
+
🏗️ \x1b[1mArchitecture Onboarding\x1b[0m — Zero-shot project understanding
|
|
2394
|
+
🔓 \x1b[1mUnlimited requests\x1b[0m — No monthly caps
|
|
2395
|
+
|
|
2396
|
+
\x1b[36m https://lemma.nxus.studio/upgrade\x1b[0m
|
|
2397
|
+
|
|
2398
|
+
Already have a key? Run: \x1b[33mlemma activate <key>\x1b[0m
|
|
2399
|
+
`);
|
|
2400
|
+
});
|
|
2492
2401
|
program.command('clipboard')
|
|
2493
2402
|
.description('Start the Lemma Local Clipboard Telepathic Optimizer (auto-squeezes copied code blocks)')
|
|
2494
2403
|
.option('--interval <ms>', 'Polling interval in milliseconds', '1000')
|
|
@@ -2506,6 +2415,11 @@ program.command('clipboard')
|
|
|
2506
2415
|
}
|
|
2507
2416
|
});
|
|
2508
2417
|
if (process.argv[1] && (process.argv[1].includes('lemma') || process.argv[1].endsWith('.cjs') || process.argv[1].endsWith('.js'))) {
|
|
2418
|
+
// Bare `lemma` with no args → show welcome screen
|
|
2419
|
+
if (process.argv.length <= 2) {
|
|
2420
|
+
showWelcomeScreen();
|
|
2421
|
+
process.exit(0);
|
|
2422
|
+
}
|
|
2509
2423
|
program.parse(process.argv);
|
|
2510
2424
|
}
|
|
2511
2425
|
//# sourceMappingURL=lemma-proxy.js.map
|