@nxuss/lemma 1.21.0 → 1.22.1
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/.agents/skills/lemma/SKILL.md +8 -6
- package/README.md +4 -2
- package/bin/doctor.js +131 -0
- package/bin/init.js +173 -57
- package/bin/postinstall.js +43 -0
- package/dist/cjs/cli/lemma-proxy.d.ts +2 -0
- package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/cjs/cli/lemma-proxy.js +41 -0
- package/dist/cjs/cli/lemma-proxy.js.map +1 -1
- package/dist/esm/cli/lemma-proxy.d.ts +2 -0
- package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
- package/dist/esm/cli/lemma-proxy.js +39 -0
- package/dist/esm/cli/lemma-proxy.js.map +1 -1
- package/lemma-proxy.cjs +19 -1
- package/package.json +4 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lemma
|
|
3
|
-
description: Lemma — cache semántico, firewall de privacidad y memoria cross-sesión para Muse. Usa `lemma call <tool>` vía bash para acceder a 70+ tools sin MCP nativo.
|
|
3
|
+
description: Lemma — cache semántico, firewall de privacidad y memoria cross-sesión para Muse. Usa `lemma call <tool>` vía bash para acceder a 70+ tools sin MCP nativo. Cárgala al iniciar cada sesión: search_memory antes de razonar, store_memory al verificar una solución.
|
|
4
4
|
user-invocable: true
|
|
5
5
|
allowed-tools: bash
|
|
6
6
|
---
|
|
@@ -33,19 +33,20 @@ Si Lemma no encaja, usa tu tool nativo. No fuerces Lemma donde no aporta.
|
|
|
33
33
|
|
|
34
34
|
## Cómo llamar a Lemma desde Muse (preferido)
|
|
35
35
|
|
|
36
|
-
Usa el wrapper `lemma call` — evita craft de JSON-RPC manual y escapa args por ti. Está disponible como `node ./lemma-proxy.cjs call`
|
|
36
|
+
Usa el wrapper `lemma call` — evita craft de JSON-RPC manual y escapa args por ti. Está disponible como `lemma call` (instalación global por npm), `npx @nxuss/lemma call`, o local `node ./lemma-proxy.cjs call` / `node ./bin/lemma-call.js`.
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
# 1. Lookup en memoria antes de razonar (preferido — wrapper)
|
|
40
|
-
|
|
40
|
+
lemma call search_memory --query "tu pregunta"
|
|
41
41
|
# o JSON explícito:
|
|
42
|
-
|
|
42
|
+
lemma call search_memory '{"query":"tu pregunta"}'
|
|
43
|
+
# (en este repo también vale: node ./lemma-proxy.cjs call search_memory --query "tu pregunta")
|
|
43
44
|
|
|
44
45
|
# 2. Busca en workspace (con scrubbing)
|
|
45
|
-
|
|
46
|
+
lemma call search_workspace --query "texto" --maxResults 10
|
|
46
47
|
|
|
47
48
|
# 3. Guarda solución verificada
|
|
48
|
-
|
|
49
|
+
lemma call store_memory '{"content":"solución verificada","tags":["bugfix"]}'
|
|
49
50
|
|
|
50
51
|
# 4. Lista tools disponibles
|
|
51
52
|
node ./lemma-proxy.cjs call --list
|
|
@@ -122,3 +123,4 @@ npm i -D @nxuss/lemma && npx lemma init
|
|
|
122
123
|
```
|
|
123
124
|
|
|
124
125
|
> Generado por `lemma init` — @nxuss/lemma para Muse.
|
|
126
|
+
<!-- managed by lemma init (@nxuss/lemma): re-run lemma init after upgrading to refresh -->
|
package/README.md
CHANGED
|
@@ -27,11 +27,13 @@ to an auditable ledger, never an estimate.
|
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
npm install -g @nxuss/lemma
|
|
30
|
-
lemma init # auto-configures MCP for your IDE (Claude Code, Cursor, Codex, Windsurf, Kiro, OpenCode)
|
|
30
|
+
lemma init # auto-configures MCP for your IDE (Muse, Claude Code, Cursor, Codex, Windsurf, Kiro, OpenCode)
|
|
31
31
|
lemma start # gateway + dashboard on http://localhost:8081
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
> **Muse** — no setup needed. This repo is already Muse-compatible zero-config: clone and open with `muse` — the skill at `.agents/skills/lemma/SKILL.md` auto-loads and `lemma call` works via `bash`. See [MCP integration](#-mcp-integration).
|
|
35
|
+
>
|
|
36
|
+
> **🆕 v1.22.0 — Lemma auto-activates in every LLM client.** `npm i -g @nxuss/lemma` registers the skill in your **user** catalog (auto-loads in every `muse` session, any project) and `lemma init` wires each project for Muse, Claude Code, Cursor, Codex, Windsurf, Kiro and OpenCode. Health check: `lemma doctor`.
|
|
35
37
|
|
|
36
38
|
---
|
|
37
39
|
|
|
@@ -156,7 +158,7 @@ Customize via `lemma.config.json`:
|
|
|
156
158
|
|
|
157
159
|
| Client | Setup |
|
|
158
160
|
| :--- | :--- |
|
|
159
|
-
| **Muse** (`muse` CLI · Muse Spark) |
|
|
161
|
+
| **Muse** (`muse` CLI · Muse Spark) | `lemma init` installs the skill into your **user** catalog (`muse skills install --scope user`, auto-loads in every session) + project skill; `npm i -g @nxuss/lemma` also registers it via postinstall. `lemma call` wrapper via `bash` (no MCP native needed). Health check: `lemma doctor`. |
|
|
160
162
|
| OpenCode | `lemma init` — registers itself in `~/.opencode.json`, zero config. |
|
|
161
163
|
| Cursor / Windsurf / Kiro / Antigravity | Settings → MCP → add server, command `lemma mcp`. |
|
|
162
164
|
| Claude Desktop | Add to `mcpServers`: `{ "lemma": { "command": "npx", "args": ["-y", "@nxuss/lemma", "mcp"] } }` |
|
package/bin/doctor.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lemma doctor — verifies the Lemma wiring on this machine / project:
|
|
6
|
+
* wrapper, project skill, user skill (muse), and AGENTS.md block.
|
|
7
|
+
*
|
|
8
|
+
* Usage: `lemma doctor` (exit 0 = usable, exit 1 = wrapper broken).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { execFileSync } = require('child_process');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const CYAN = '\x1b[36m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', DIM = '\x1b[2m', RESET = '\x1b[0m', BOLD = '\x1b[1m';
|
|
17
|
+
|
|
18
|
+
function ok(msg) { process.stdout.write(` ${GREEN}✓${RESET} ${msg}\n`); }
|
|
19
|
+
function warn(msg) { process.stdout.write(` ${YELLOW}!${RESET} ${msg}\n`); }
|
|
20
|
+
function fail(msg) { process.stdout.write(` ${YELLOW}✗${RESET} ${msg}\n`); }
|
|
21
|
+
|
|
22
|
+
function shortErr(e) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = (e && (e.stderr || e.stdout || e.message)) || '';
|
|
25
|
+
const line = raw.toString().split('\n').map((s) => s.trim()).filter(Boolean)[0] || '';
|
|
26
|
+
return line.slice(0, 160);
|
|
27
|
+
} catch {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function museOnPath() {
|
|
33
|
+
try {
|
|
34
|
+
if (process.platform === 'win32') return true;
|
|
35
|
+
require('child_process').execSync('command -v muse', { stdio: 'ignore', timeout: 10000 });
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveMcpServer(pkgRoot) {
|
|
43
|
+
const candidates = [
|
|
44
|
+
path.join(pkgRoot, 'dist', 'cjs', 'mcp', 'index.js'),
|
|
45
|
+
path.join(pkgRoot, 'mcp-server.js'),
|
|
46
|
+
];
|
|
47
|
+
return candidates.find((p) => fs.existsSync(p)) || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
const pkgRoot = path.join(__dirname, '..');
|
|
52
|
+
const cwd = process.cwd();
|
|
53
|
+
let broken = false;
|
|
54
|
+
process.stdout.write(`\n ${BOLD}${CYAN}Lemma Doctor${RESET}\n\n`);
|
|
55
|
+
|
|
56
|
+
// 1. Wrapper + tool catalog (the core: everything else is advice).
|
|
57
|
+
const server = resolveMcpServer(pkgRoot);
|
|
58
|
+
if (!server) {
|
|
59
|
+
fail('no MCP server found (dist/cjs/mcp/index.js nor mcp-server.js) — run npm run build');
|
|
60
|
+
broken = true;
|
|
61
|
+
} else {
|
|
62
|
+
try {
|
|
63
|
+
const out = execFileSync(process.execPath,
|
|
64
|
+
[path.join(pkgRoot, 'bin', 'lemma-call.js'), '--list'],
|
|
65
|
+
{ encoding: 'utf8', timeout: 60000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
66
|
+
const tools = JSON.parse(out);
|
|
67
|
+
ok(`wrapper works via ${path.relative(pkgRoot, server)} — ${tools.length} tools`);
|
|
68
|
+
} catch {
|
|
69
|
+
fail('wrapper failed: `lemma call --list` did not return a tool catalog');
|
|
70
|
+
broken = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Project skill (this repo / project).
|
|
75
|
+
const projSkill = path.join(cwd, '.agents', 'skills', 'lemma', 'SKILL.md');
|
|
76
|
+
if (fs.existsSync(projSkill)) {
|
|
77
|
+
const body = fs.readFileSync(projSkill, 'utf8');
|
|
78
|
+
if (body.includes('Cárgala al iniciar')) ok('project skill current (.agents/skills/lemma/SKILL.md)');
|
|
79
|
+
else warn('project skill is stale (pre auto-load trigger) — re-run `lemma init`');
|
|
80
|
+
} else {
|
|
81
|
+
warn('no project skill — run `lemma init` in this project');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. User skill (every `muse` session, any project).
|
|
85
|
+
// Note: inside a project that defines its own `lemma` skill, the CLI shadows
|
|
86
|
+
// the user skill (project wins — by design) and `inspect --source user` then
|
|
87
|
+
// reports "not found". Fall back to the on-disk check before crying missing.
|
|
88
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
89
|
+
const userSkillFile = path.join(configHome, 'muse', 'skills', 'lemma', 'SKILL.md');
|
|
90
|
+
const projSkillHere = fs.existsSync(projSkill);
|
|
91
|
+
if (!museOnPath()) {
|
|
92
|
+
warn('muse CLI not on PATH — user skill not applicable here');
|
|
93
|
+
} else {
|
|
94
|
+
try {
|
|
95
|
+
execFileSync('muse', ['skills', 'inspect', 'lemma', '--source', 'user'],
|
|
96
|
+
{ stdio: 'pipe', timeout: 30000 });
|
|
97
|
+
ok('user skill installed (auto-loads in every muse session)');
|
|
98
|
+
} catch (e) {
|
|
99
|
+
const detail = shortErr(e);
|
|
100
|
+
if (fs.existsSync(userSkillFile) && projSkillHere) {
|
|
101
|
+
ok('user skill installed (shadowed by this project\u2019s skill here — project wins, as designed)');
|
|
102
|
+
} else if (fs.existsSync(userSkillFile)) {
|
|
103
|
+
warn('user skill files exist but the CLI does not see them' + (detail ? ` (${detail})` : ''));
|
|
104
|
+
} else {
|
|
105
|
+
warn('user skill missing' + (detail ? ` (${detail})` : '') + ' — run `lemma init` or `muse skills install <pkg>/.agents/skills/lemma --scope user --force`');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 4. AGENTS.md block (session-start nudge for AGENTS.md-aware agents).
|
|
111
|
+
const agentsPath = path.join(cwd, 'AGENTS.md');
|
|
112
|
+
if (!fs.existsSync(agentsPath) || !fs.readFileSync(agentsPath, 'utf8').includes('@nxuss/lemma')) {
|
|
113
|
+
warn('no Lemma block in ./AGENTS.md — run `lemma init`');
|
|
114
|
+
} else if (fs.readFileSync(agentsPath, 'utf8').includes('At session')) {
|
|
115
|
+
ok('AGENTS.md block current (session-start guard)');
|
|
116
|
+
} else {
|
|
117
|
+
warn('AGENTS.md block is stale (pre Muse guard) — re-run `lemma init`');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
process.stdout.write(`\n ${DIM}Brain: ~/.lemma-cache/lemma_brain.json · proxy: lemma-proxy.cjs status${RESET}\n\n`);
|
|
121
|
+
return broken ? 1 : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (require.main === module) {
|
|
125
|
+
main().then((code) => process.exit(code)).catch((e) => {
|
|
126
|
+
console.error('doctor failed:', e.message);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { main };
|
package/bin/init.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
-
const { execSync, spawn } = require('child_process');
|
|
7
|
+
const { execSync, execFileSync, spawn } = require('child_process');
|
|
8
8
|
|
|
9
9
|
const LEMMA_CACHE = path.join(os.homedir(), '.lemma-cache');
|
|
10
10
|
const BRAIN_FILE = path.join(LEMMA_CACHE, 'lemma_brain.json');
|
|
@@ -399,7 +399,7 @@ function configureClaudeCode() {
|
|
|
399
399
|
// license tier (semantic cache vs. exact-match cache), which has nothing to do with
|
|
400
400
|
// gateway credentials. The proxy (lemma-proxy.ts) never validates whatever token the
|
|
401
401
|
// client sends anyway: it always signs upstream requests with its OWN server-side
|
|
402
|
-
// ANTHROPIC_API_KEY (
|
|
402
|
+
// ANTHROPIC_API_KEY (loaded from ~/.lemma-cache/env). So ANTHROPIC_AUTH_TOKEN here is just a
|
|
403
403
|
// fixed local placeholder to satisfy Claude Code's "a token is present" check.
|
|
404
404
|
function configureClaudeCodeGateway(anthropicApiKey) {
|
|
405
405
|
if (!anthropicApiKey) {
|
|
@@ -436,10 +436,13 @@ The MCP server \`lemma\` is connected. For clients with native MCP support
|
|
|
436
436
|
(Claude Code, Cursor, Codex, Windsurf, Kiro, OpenCode), NEVER use native tools
|
|
437
437
|
when a Lemma equivalent exists.
|
|
438
438
|
|
|
439
|
-
> **Muse Code:** you DO NOT have native \`mcp__lemma__*\` tools.
|
|
440
|
-
> \`
|
|
441
|
-
>
|
|
442
|
-
>
|
|
439
|
+
> **Muse Code:** you DO NOT have native \`mcp__lemma__*\` tools. At session
|
|
440
|
+
> start, load the \`lemma\` skill (user scope if present, else
|
|
441
|
+
> \`.agents/skills/lemma/SKILL.md\`) and follow it instead of this file — it
|
|
442
|
+
> has priority and uses \`lemma call\` via bash. Then call \`search_memory\`
|
|
443
|
+
> with the task topic before reasoning. Do not enforce the table below as a
|
|
444
|
+
> hard block; use \`lemma call\` when it actually helps. If no \`lemma\` skill
|
|
445
|
+
> is installed, tell the user to run \`npx @nxuss/lemma init\`.
|
|
443
446
|
|
|
444
447
|
## Mandatory Substitutions (MCP-native clients only)
|
|
445
448
|
|
|
@@ -464,7 +467,7 @@ when a Lemma equivalent exists.
|
|
|
464
467
|
2. Call \`search_memory\` with the task topic — recall past solutions
|
|
465
468
|
3. When done: call \`store_memory\` with the solution
|
|
466
469
|
|
|
467
|
-
> Muse: use \`node ./lemma-proxy.cjs call <tool>\`
|
|
470
|
+
> Muse: use \`lemma call <tool>\` (or \`node ./lemma-proxy.cjs call <tool>\` in this repo — see \`.agents/skills/lemma/SKILL.md\`).
|
|
468
471
|
|
|
469
472
|
## Bug Reporting
|
|
470
473
|
|
|
@@ -574,7 +577,7 @@ function configureKiro() {
|
|
|
574
577
|
// OpenCode: register MCP server in opencode.json + Muse skill
|
|
575
578
|
const MUSE_SKILL_CONTENT = `---
|
|
576
579
|
name: lemma
|
|
577
|
-
description: Lemma — cache semántico, firewall de privacidad y memoria cross-sesión para Muse. Usa lemma call <tool
|
|
580
|
+
description: Lemma — cache semántico, firewall de privacidad y memoria cross-sesión para Muse. Usa \`lemma call <tool>\` vía bash para acceder a 70+ tools sin MCP nativo. Cárgala al iniciar cada sesión: search_memory antes de razonar, store_memory al verificar una solución.
|
|
578
581
|
user-invocable: true
|
|
579
582
|
allowed-tools: bash
|
|
580
583
|
---
|
|
@@ -607,19 +610,20 @@ Si Lemma no encaja, usa tu tool nativo. No fuerces Lemma donde no aporta.
|
|
|
607
610
|
|
|
608
611
|
## Cómo llamar a Lemma desde Muse (preferido)
|
|
609
612
|
|
|
610
|
-
Usa el wrapper \`lemma call\` — evita craft de JSON-RPC manual y escapa args por ti. Está disponible como \`node ./lemma-proxy.cjs call\`
|
|
613
|
+
Usa el wrapper \`lemma call\` — evita craft de JSON-RPC manual y escapa args por ti. Está disponible como \`lemma call\` (instalación global por npm), \`npx @nxuss/lemma call\`, o local \`node ./lemma-proxy.cjs call\` / \`node ./bin/lemma-call.js\`.
|
|
611
614
|
|
|
612
615
|
\`\`\`bash
|
|
613
616
|
# 1. Lookup en memoria antes de razonar (preferido — wrapper)
|
|
614
|
-
|
|
617
|
+
lemma call search_memory --query "tu pregunta"
|
|
615
618
|
# o JSON explícito:
|
|
616
|
-
|
|
619
|
+
lemma call search_memory '{"query":"tu pregunta"}'
|
|
620
|
+
# (en este repo también vale: node ./lemma-proxy.cjs call search_memory --query "tu pregunta")
|
|
617
621
|
|
|
618
622
|
# 2. Busca en workspace (con scrubbing)
|
|
619
|
-
|
|
623
|
+
lemma call search_workspace --query "texto" --maxResults 10
|
|
620
624
|
|
|
621
625
|
# 3. Guarda solución verificada
|
|
622
|
-
|
|
626
|
+
lemma call store_memory '{"content":"solución verificada","tags":["bugfix"]}'
|
|
623
627
|
|
|
624
628
|
# 4. Lista tools disponibles
|
|
625
629
|
node ./lemma-proxy.cjs call --list
|
|
@@ -696,17 +700,51 @@ npm i -D @nxuss/lemma && npx lemma init
|
|
|
696
700
|
\`\`\`
|
|
697
701
|
|
|
698
702
|
> Generado por \`lemma init\` — @nxuss/lemma para Muse.
|
|
703
|
+
<!-- managed by lemma init (@nxuss/lemma): re-run lemma init after upgrading to refresh -->
|
|
699
704
|
`;
|
|
700
705
|
|
|
706
|
+
// Installs the Lemma skill into the user's GLOBAL muse skill catalog so it
|
|
707
|
+
// auto-loads in every `muse` session — not just in projects where `lemma init`
|
|
708
|
+
// was run. Best-effort: never fails `lemma init` — but LOUD about the reason,
|
|
709
|
+
// so a missing user skill is diagnosable instead of silent.
|
|
710
|
+
function shortErr(e) {
|
|
711
|
+
try {
|
|
712
|
+
const raw = (e && (e.stderr || e.stdout || e.message)) || '';
|
|
713
|
+
const line = raw.toString().split('\n').map((s) => s.trim()).filter(Boolean)[0] || '';
|
|
714
|
+
return line.slice(0, 160);
|
|
715
|
+
} catch {
|
|
716
|
+
return '';
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function configureMuseUserSkill(skillDir) {
|
|
721
|
+
try {
|
|
722
|
+
execSync('command -v muse', { stdio: 'ignore' });
|
|
723
|
+
} catch {
|
|
724
|
+
return { ok: false, reason: 'muse CLI not found on PATH' };
|
|
725
|
+
}
|
|
726
|
+
try {
|
|
727
|
+
execFileSync('muse', ['skills', 'install', skillDir, '--scope', 'user', '--force'],
|
|
728
|
+
{ stdio: 'pipe', timeout: 30000 });
|
|
729
|
+
return { ok: true, reason: '' };
|
|
730
|
+
} catch (e) {
|
|
731
|
+
const detail = shortErr(e);
|
|
732
|
+
return { ok: false, reason: 'muse skills install failed' + (detail ? `: ${detail}` : '') };
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
701
736
|
function configureMuse() {
|
|
702
737
|
const cwd = process.cwd();
|
|
703
738
|
const skillDir = path.join(cwd, '.agents', 'skills', 'lemma');
|
|
739
|
+
let project = false;
|
|
704
740
|
try {
|
|
705
741
|
ensureDir(skillDir);
|
|
706
742
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
707
743
|
fs.writeFileSync(skillPath, MUSE_SKILL_CONTENT, 'utf8');
|
|
708
|
-
|
|
709
|
-
} catch {
|
|
744
|
+
project = true;
|
|
745
|
+
} catch { project = false; }
|
|
746
|
+
const userRes = project ? configureMuseUserSkill(skillDir) : { ok: false, reason: 'project skill write failed' };
|
|
747
|
+
return { project, user: userRes.ok, userReason: userRes.reason };
|
|
710
748
|
}
|
|
711
749
|
|
|
712
750
|
function ensureKiroMigratesAgentsMd() {
|
|
@@ -757,20 +795,19 @@ function configureOpenCode() {
|
|
|
757
795
|
return true;
|
|
758
796
|
}
|
|
759
797
|
|
|
760
|
-
|
|
798
|
+
// Installs the cache-proxy daemon as a user-level launchd/systemd unit.
|
|
799
|
+
// OPT-IN ONLY: `lemma init` never calls this without an explicit --daemon flag.
|
|
800
|
+
// Secrets are never baked into the unit file: the daemon loads ~/.lemma-cache/env
|
|
801
|
+
// at startup (see lemma-proxy.cjs loadEnvFile), where `lemma init` persists keys
|
|
802
|
+
// with mode 0600 instead.
|
|
803
|
+
function installProxyDaemon(homeDir = os.homedir()) {
|
|
761
804
|
const isMac = process.platform === 'darwin';
|
|
762
805
|
const isLinux = process.platform === 'linux';
|
|
763
806
|
|
|
764
|
-
// The full proxy server (lemma-proxy.ts) needs its own server-side credentials to sign
|
|
765
|
-
// upstream requests — it never trusts whatever token the calling client sends. A launchd/
|
|
766
|
-
// systemd service does not inherit the shell's exported env, so these must be baked into
|
|
767
|
-
// the unit definition at install time or the daemon silently falls back to local Ollama.
|
|
768
|
-
const anthropicApiKey = process.env.ANTHROPIC_API_KEY || '';
|
|
769
|
-
const openaiApiKey = process.env.OPENAI_API_KEY || '';
|
|
770
807
|
const proxyArgs = ['start', '--port', PORT, '--no-configure', '--no-clipboard'];
|
|
771
808
|
|
|
772
809
|
if (isMac) {
|
|
773
|
-
const plistDir = path.join(
|
|
810
|
+
const plistDir = path.join(homeDir, 'Library', 'LaunchAgents');
|
|
774
811
|
ensureDir(plistDir);
|
|
775
812
|
const plistPath = path.join(plistDir, 'com.lemma.cache-proxy.plist');
|
|
776
813
|
const argEntries = proxyArgs.map((a) => ` <string>${a}</string>`).join('\n');
|
|
@@ -778,9 +815,7 @@ function installProxyDaemon() {
|
|
|
778
815
|
` <key>LEMMA_PROXY_PORT</key><string>${PORT}</string>`,
|
|
779
816
|
` <key>LEMMA_CACHE_DIR</key><string>${LEMMA_CACHE}</string>`,
|
|
780
817
|
` <key>LEMMA_DISABLE_CLIPBOARD</key><string>true</string>`,
|
|
781
|
-
|
|
782
|
-
openaiApiKey ? ` <key>OPENAI_API_KEY</key><string>${openaiApiKey}</string>` : '',
|
|
783
|
-
].filter(Boolean).join('\n');
|
|
818
|
+
].join('\n');
|
|
784
819
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
785
820
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
786
821
|
<plist version="1.0">
|
|
@@ -810,15 +845,13 @@ ${envEntries}
|
|
|
810
845
|
}
|
|
811
846
|
|
|
812
847
|
if (isLinux) {
|
|
813
|
-
const svcDir = path.join(
|
|
848
|
+
const svcDir = path.join(homeDir, '.config', 'systemd', 'user');
|
|
814
849
|
ensureDir(svcDir);
|
|
815
850
|
const envLines = [
|
|
816
851
|
`Environment=LEMMA_PROXY_PORT=${PORT}`,
|
|
817
852
|
`Environment=LEMMA_CACHE_DIR=${LEMMA_CACHE}`,
|
|
818
853
|
`Environment=LEMMA_DISABLE_CLIPBOARD=true`,
|
|
819
|
-
|
|
820
|
-
openaiApiKey ? `Environment=OPENAI_API_KEY=${openaiApiKey}` : '',
|
|
821
|
-
].filter(Boolean).join('\n');
|
|
854
|
+
].join('\n');
|
|
822
855
|
const svc = `[Unit]
|
|
823
856
|
Description=Lemma Cache Proxy
|
|
824
857
|
After=network.target
|
|
@@ -841,6 +874,39 @@ WantedBy=default.target
|
|
|
841
874
|
return false;
|
|
842
875
|
}
|
|
843
876
|
|
|
877
|
+
// Best-effort removal of a previously installed daemon unit. Plain `lemma init`
|
|
878
|
+
// (no --daemon) calls this so a silent or legacy install can never linger behind:
|
|
879
|
+
// the daemon only exists while the user explicitly asked for it.
|
|
880
|
+
function removeProxyDaemon(homeDir = os.homedir()) {
|
|
881
|
+
if (process.platform === 'darwin') {
|
|
882
|
+
const plistPath = path.join(homeDir, 'Library', 'LaunchAgents', 'com.lemma.cache-proxy.plist');
|
|
883
|
+
if (!fs.existsSync(plistPath)) return false;
|
|
884
|
+
try { execSync('launchctl unload ' + plistPath, { stdio: 'ignore' }); } catch {}
|
|
885
|
+
try { fs.unlinkSync(plistPath); } catch {}
|
|
886
|
+
return !fs.existsSync(plistPath);
|
|
887
|
+
}
|
|
888
|
+
if (process.platform === 'linux') {
|
|
889
|
+
const svcPath = path.join(homeDir, '.config', 'systemd', 'user', 'lemma-cache-proxy.service');
|
|
890
|
+
if (!fs.existsSync(svcPath)) return false;
|
|
891
|
+
try { execSync('systemctl --user disable lemma-cache-proxy --now', { stdio: 'ignore' }); } catch {}
|
|
892
|
+
try { fs.unlinkSync(svcPath); } catch {}
|
|
893
|
+
return !fs.existsSync(svcPath);
|
|
894
|
+
}
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Global side effects stay opt-in: a plain `lemma init` only writes project-local
|
|
899
|
+
// files + MCP registrations. --daemon installs the background proxy daemon,
|
|
900
|
+
// --gateway reroutes the user's Claude Code model traffic through that proxy.
|
|
901
|
+
function parseInitArgs(argv = process.argv) {
|
|
902
|
+
const args = argv.slice(2);
|
|
903
|
+
return {
|
|
904
|
+
showHelp: args.includes('--help') || args.includes('-h'),
|
|
905
|
+
wantDaemon: args.includes('--daemon'),
|
|
906
|
+
wantGateway: args.includes('--gateway'),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
844
910
|
function applyEnvToCurrentProcess() {
|
|
845
911
|
const proxyUrl = `http://localhost:${PORT}`;
|
|
846
912
|
process.env.LEMMA_PROXY_PORT = PORT;
|
|
@@ -851,21 +917,29 @@ function applyEnvToCurrentProcess() {
|
|
|
851
917
|
process.env.LITELLM_PROXY_BASE_URL = proxyUrl;
|
|
852
918
|
}
|
|
853
919
|
|
|
854
|
-
|
|
920
|
+
// Upstream API keys live here (mode 0600) instead of inside the launchd/systemd
|
|
921
|
+
// unit: the proxy daemon loads this file at startup (lemma-proxy.cjs loadEnvFile),
|
|
922
|
+
// so a background service gets credentials without baking secrets into a unit file.
|
|
923
|
+
function createEnvFile(envFile = ENV_FILE) {
|
|
855
924
|
const proxyUrl = `http://localhost:${PORT}`;
|
|
925
|
+
const keyLines = [
|
|
926
|
+
process.env.ANTHROPIC_API_KEY ? `export ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY}` : '',
|
|
927
|
+
process.env.OPENAI_API_KEY ? `export OPENAI_API_KEY=${process.env.OPENAI_API_KEY}` : '',
|
|
928
|
+
].filter(Boolean).join('\n');
|
|
856
929
|
const envContent = `# Lemma Cache Proxy — Auto-generated by \`lemma init\`
|
|
857
930
|
# Source this file in your shell to route LLM traffic through the cache:
|
|
858
|
-
# source ${
|
|
931
|
+
# source ${envFile}
|
|
859
932
|
export LEMMA_PROXY_PORT=${PORT}
|
|
860
933
|
export LEMMA_CACHE_DIR=${LEMMA_CACHE}
|
|
861
934
|
export OPENAI_BASE_URL=${proxyUrl}
|
|
862
935
|
export OPENAI_API_BASE=${proxyUrl}
|
|
863
936
|
export CODEX_BASE_URL=${proxyUrl}
|
|
864
937
|
export LITELLM_PROXY_BASE_URL=${proxyUrl}
|
|
865
|
-
`;
|
|
866
|
-
fs.writeFileSync(
|
|
938
|
+
${keyLines ? keyLines + '\n' : ''}`;
|
|
939
|
+
fs.writeFileSync(envFile, envContent, 'utf8');
|
|
940
|
+
try { fs.chmodSync(envFile, 0o600); } catch {}
|
|
867
941
|
applyEnvToCurrentProcess();
|
|
868
|
-
return
|
|
942
|
+
return envFile;
|
|
869
943
|
}
|
|
870
944
|
|
|
871
945
|
function setupShellIntegration() {
|
|
@@ -904,7 +978,26 @@ function setupShellIntegration() {
|
|
|
904
978
|
return configured;
|
|
905
979
|
}
|
|
906
980
|
|
|
907
|
-
async function main() {
|
|
981
|
+
async function main(argv = process.argv) {
|
|
982
|
+
const opts = parseInitArgs(argv);
|
|
983
|
+
if (opts.showHelp) {
|
|
984
|
+
process.stderr.write(`
|
|
985
|
+
Lemma Init — project setup (project-local files + MCP registrations only)
|
|
986
|
+
|
|
987
|
+
Usage: lemma init [--daemon] [--gateway]
|
|
988
|
+
|
|
989
|
+
--daemon Install + start the background cache-proxy daemon
|
|
990
|
+
(launchd KeepAlive on macOS, systemd user unit on Linux).
|
|
991
|
+
Without it, no daemon is installed — and a previously installed
|
|
992
|
+
one is removed.
|
|
993
|
+
--gateway Reroute this user's Claude Code model traffic through the proxy
|
|
994
|
+
(~/.claude/settings.json). Requires ANTHROPIC_API_KEY in env.
|
|
995
|
+
Without it, Claude Code keeps its own auth untouched.
|
|
996
|
+
|
|
997
|
+
`);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
908
1001
|
process.stderr.write(`\n ${BOLD}${CYAN}Lemma Init — Magic Setup${RESET}\n\n`);
|
|
909
1002
|
|
|
910
1003
|
ensureDir(LEMMA_CACHE);
|
|
@@ -923,17 +1016,23 @@ async function main() {
|
|
|
923
1016
|
configureClaudeCode();
|
|
924
1017
|
ok('Claude Code / CLI configured (CLAUDE.md + .claude/settings.local.json)');
|
|
925
1018
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
if (
|
|
929
|
-
|
|
1019
|
+
// --gateway is opt-in: flipping the user's global Claude Code auth without being
|
|
1020
|
+
// asked is exactly how Pro subscribers end up billed as API usage.
|
|
1021
|
+
if (!opts.wantGateway) {
|
|
1022
|
+
skip('Claude Code gateway not requested — model traffic + auth untouched (MCP tools only). Re-run with --gateway to route it via the proxy.');
|
|
930
1023
|
} else {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1024
|
+
const anthropicApiKey = process.env.ANTHROPIC_API_KEY || '';
|
|
1025
|
+
const gatewayResult = configureClaudeCodeGateway(anthropicApiKey);
|
|
1026
|
+
if (gatewayResult.activated) {
|
|
1027
|
+
ok(`Claude Code gateway activated (~/.claude/settings.json → ANTHROPIC_BASE_URL=http://localhost:${PORT})`);
|
|
1028
|
+
} else {
|
|
1029
|
+
warn('Claude Code gateway NOT activated — no ANTHROPIC_API_KEY in this environment.');
|
|
1030
|
+
warn(` Model traffic keeps going directly to Anthropic (subscription/OAuth or your own key, unchanged).`);
|
|
1031
|
+
warn(` Only Lemma's MCP tools are active for Claude Code — no cache savings on the model traffic itself.`);
|
|
1032
|
+
warn(` To enable it: export ANTHROPIC_API_KEY=sk-ant-... and re-run \`lemma init --gateway\`.`);
|
|
1033
|
+
warn(` This SWITCHES Claude Code off your subscription/OAuth session for LLM calls — they'll be`);
|
|
1034
|
+
warn(` signed by the proxy's own ANTHROPIC_API_KEY instead, and billed/rate-limited as API usage.`);
|
|
1035
|
+
}
|
|
937
1036
|
}
|
|
938
1037
|
|
|
939
1038
|
updateAgentsMd();
|
|
@@ -951,9 +1050,11 @@ async function main() {
|
|
|
951
1050
|
configureOpenCode();
|
|
952
1051
|
ok('OpenCode configured (opencode.json MCP + instructions)');
|
|
953
1052
|
|
|
954
|
-
const
|
|
955
|
-
if (
|
|
956
|
-
|
|
1053
|
+
const museResult = configureMuse();
|
|
1054
|
+
if (museResult.project) {
|
|
1055
|
+
ok('Muse configured (.agents/skills/lemma/SKILL.md)' + (museResult.user ? ' + user skill (auto-load in every muse session)' : ''));
|
|
1056
|
+
if (!museResult.user) warn(`Muse user skill NOT installed — ${museResult.userReason || 'unknown reason'}`);
|
|
1057
|
+
} else warn('Muse skill could not be configured');
|
|
957
1058
|
|
|
958
1059
|
const hasContinue = detectTool('Continue.dev', () => configureContinueDotDev());
|
|
959
1060
|
if (hasContinue) ok('Continue.dev configured');
|
|
@@ -984,9 +1085,15 @@ async function main() {
|
|
|
984
1085
|
if (hasWindsurf) ok('Windsurf detected — env auto-configured');
|
|
985
1086
|
else skip('Windsurf not detected');
|
|
986
1087
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1088
|
+
if (opts.wantDaemon) {
|
|
1089
|
+
const daemonOk = installProxyDaemon();
|
|
1090
|
+
if (daemonOk) ok('Cache proxy daemon installed & started (OpenAI + Anthropic /v1/messages, streaming)');
|
|
1091
|
+
else warn('Could not install daemon — run `lemma proxy start` manually');
|
|
1092
|
+
} else if (removeProxyDaemon()) {
|
|
1093
|
+
ok('Removed previously installed proxy daemon (re-run with --daemon to restore it)');
|
|
1094
|
+
} else {
|
|
1095
|
+
skip('Proxy daemon not installed (opt-in via `lemma init --daemon`)');
|
|
1096
|
+
}
|
|
990
1097
|
|
|
991
1098
|
const envFile = createEnvFile();
|
|
992
1099
|
ok(`Env file created: ${envFile}`);
|
|
@@ -997,10 +1104,15 @@ async function main() {
|
|
|
997
1104
|
|
|
998
1105
|
process.stderr.write(`\n ${BOLD}${GREEN}✓ Lemma init complete${RESET}\n`);
|
|
999
1106
|
process.stderr.write(` ${DIM}──────────────────────────────────────${RESET}\n`);
|
|
1000
|
-
|
|
1107
|
+
if (opts.wantDaemon) {
|
|
1108
|
+
process.stderr.write(` ${GREEN}●${RESET} Cache proxy running on http://localhost:${PORT}\n`);
|
|
1109
|
+
} else {
|
|
1110
|
+
process.stderr.write(` ${DIM}○ Cache proxy daemon not installed (opt-in: \`lemma init --daemon\`)${RESET}\n`);
|
|
1111
|
+
}
|
|
1001
1112
|
process.stderr.write(` ${GREEN}●${RESET} MCP server ready ${DIM}(lemma mcp)${RESET}\n`);
|
|
1002
1113
|
process.stderr.write(` ${GREEN}●${RESET} Dashboard active ${DIM}(lemma stats)${RESET}\n`);
|
|
1003
1114
|
process.stderr.write(` ${GREEN}●${RESET} New terminals: env vars auto-loaded\n`);
|
|
1115
|
+
process.stderr.write(` ${DIM}Upkeep: re-run 'lemma init' after upgrading @nxuss/lemma (refreshes skills + rules)${RESET}\n`);
|
|
1004
1116
|
process.stderr.write(` ${GREEN}●${RESET} Todos los IDE/CLI nuevos ya apuntan al proxy\n`);
|
|
1005
1117
|
process.stderr.write(` ${gatewayResult.activated ? GREEN + '●' : YELLOW + '○'}${RESET} Claude Code gateway: ${gatewayResult.activated ? 'ACTIVE (model traffic cached)' : 'inactive (MCP tools only)'}\n`);
|
|
1006
1118
|
process.stderr.write(` ${DIM}──────────────────────────────────────${RESET}\n`);
|
|
@@ -1009,7 +1121,11 @@ async function main() {
|
|
|
1009
1121
|
process.stderr.write(` ${DIM}Logs: ${LEMMA_CACHE}/proxy-daemon.log${RESET}\n\n`);
|
|
1010
1122
|
}
|
|
1011
1123
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1124
|
+
if (require.main === module) {
|
|
1125
|
+
main().catch(e => {
|
|
1126
|
+
console.error('init failed:', e.message);
|
|
1127
|
+
process.exit(1);
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
module.exports = { main, configureMuse, configureMuseUserSkill, updateAgentsMd, MUSE_SKILL_CONTENT, LEMMA_RULES_CONTENT, installProxyDaemon, removeProxyDaemon, parseInitArgs, createEnvFile };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lemma postinstall — registers the Lemma skill in the user's GLOBAL muse
|
|
6
|
+
* catalog so it auto-loads in every `muse` session, with zero extra steps
|
|
7
|
+
* after `npm i [-g] @nxuss/lemma`.
|
|
8
|
+
*
|
|
9
|
+
* Total function: never fails the install, never prints on success, finishes
|
|
10
|
+
* in well under a second when there is nothing to do. No build output needed —
|
|
11
|
+
* skill install only registers a markdown file, it never invokes any tool.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { execSync, execFileSync } = require('child_process');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
function museOnPath() {
|
|
19
|
+
try {
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
execFileSync('where', ['muse'], { stdio: 'ignore', timeout: 10000 });
|
|
22
|
+
} else {
|
|
23
|
+
execSync('command -v muse', { stdio: 'ignore', timeout: 10000 });
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function main() {
|
|
32
|
+
try {
|
|
33
|
+
const skillDir = path.join(__dirname, '..', '.agents', 'skills', 'lemma');
|
|
34
|
+
if (!fs.existsSync(path.join(skillDir, 'SKILL.md'))) return;
|
|
35
|
+
if (!museOnPath()) return;
|
|
36
|
+
execFileSync('muse', ['skills', 'install', skillDir, '--scope', 'user', '--force'],
|
|
37
|
+
{ stdio: 'ignore', timeout: 25000 });
|
|
38
|
+
} catch {
|
|
39
|
+
// Best-effort by contract: npm install must never fail because of this.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main();
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export declare function startBackgroundClipboardWatcher(cliOpts?: {
|
|
3
3
|
clipboard?: boolean;
|
|
4
4
|
}): void;
|
|
5
|
+
export declare const MAX_WORKSPACE_WATCHERS = 512;
|
|
6
|
+
export declare function isUnsafeWatchRoot(dir: string): boolean;
|
|
5
7
|
export declare function performAutoHeal(apply: boolean): Promise<{
|
|
6
8
|
success: boolean;
|
|
7
9
|
message: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lemma-proxy.d.ts","sourceRoot":"","sources":["../../../src/cli/lemma-proxy.ts"],"names":[],"mappings":";AA0cA,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA4DvF;
|
|
1
|
+
{"version":3,"file":"lemma-proxy.d.ts","sourceRoot":"","sources":["../../../src/cli/lemma-proxy.ts"],"names":[],"mappings":";AA0cA,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA4DvF;AAqfD,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGtD;AAyqDD,wBAAsB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA8KhL"}
|