@nxuss/lemma 1.22.0 → 1.23.0

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.
Files changed (61) hide show
  1. package/README.md +33 -7
  2. package/bin/init.js +108 -37
  3. package/dist/cjs/cli/lemma-proxy.d.ts +2 -0
  4. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  5. package/dist/cjs/cli/lemma-proxy.js +41 -0
  6. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  7. package/dist/cjs/cloud/store/FileBrainStore.d.ts.map +1 -1
  8. package/dist/cjs/cloud/store/FileBrainStore.js +5 -2
  9. package/dist/cjs/cloud/store/FileBrainStore.js.map +1 -1
  10. package/dist/cjs/mcp/tool-profiles.d.ts.map +1 -1
  11. package/dist/cjs/mcp/tool-profiles.js +3 -0
  12. package/dist/cjs/mcp/tool-profiles.js.map +1 -1
  13. package/dist/cjs/mcp/tools/memory.d.ts.map +1 -1
  14. package/dist/cjs/mcp/tools/memory.js +212 -7
  15. package/dist/cjs/mcp/tools/memory.js.map +1 -1
  16. package/dist/cjs/mcp/tools.d.ts +9 -0
  17. package/dist/cjs/mcp/tools.d.ts.map +1 -1
  18. package/dist/cjs/mcp/tools.js +25 -4
  19. package/dist/cjs/mcp/tools.js.map +1 -1
  20. package/dist/cjs/security/scrubForStorage.d.ts +16 -0
  21. package/dist/cjs/security/scrubForStorage.d.ts.map +1 -0
  22. package/dist/cjs/security/scrubForStorage.js +35 -0
  23. package/dist/cjs/security/scrubForStorage.js.map +1 -0
  24. package/dist/cjs/subconscious/BrainEncryption.d.ts +38 -0
  25. package/dist/cjs/subconscious/BrainEncryption.d.ts.map +1 -0
  26. package/dist/cjs/subconscious/BrainEncryption.js +137 -0
  27. package/dist/cjs/subconscious/BrainEncryption.js.map +1 -0
  28. package/dist/cjs/subconscious/TheBrainV2.d.ts +294 -0
  29. package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
  30. package/dist/cjs/subconscious/TheBrainV2.js +1070 -111
  31. package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
  32. package/dist/esm/cli/lemma-proxy.d.ts +2 -0
  33. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  34. package/dist/esm/cli/lemma-proxy.js +39 -0
  35. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  36. package/dist/esm/cloud/store/FileBrainStore.d.ts.map +1 -1
  37. package/dist/esm/cloud/store/FileBrainStore.js +5 -2
  38. package/dist/esm/cloud/store/FileBrainStore.js.map +1 -1
  39. package/dist/esm/mcp/tool-profiles.d.ts.map +1 -1
  40. package/dist/esm/mcp/tool-profiles.js +3 -0
  41. package/dist/esm/mcp/tool-profiles.js.map +1 -1
  42. package/dist/esm/mcp/tools/memory.d.ts.map +1 -1
  43. package/dist/esm/mcp/tools/memory.js +212 -7
  44. package/dist/esm/mcp/tools/memory.js.map +1 -1
  45. package/dist/esm/mcp/tools.d.ts +9 -0
  46. package/dist/esm/mcp/tools.d.ts.map +1 -1
  47. package/dist/esm/mcp/tools.js +25 -5
  48. package/dist/esm/mcp/tools.js.map +1 -1
  49. package/dist/esm/security/scrubForStorage.d.ts +16 -0
  50. package/dist/esm/security/scrubForStorage.d.ts.map +1 -0
  51. package/dist/esm/security/scrubForStorage.js +32 -0
  52. package/dist/esm/security/scrubForStorage.js.map +1 -0
  53. package/dist/esm/subconscious/BrainEncryption.d.ts +38 -0
  54. package/dist/esm/subconscious/BrainEncryption.d.ts.map +1 -0
  55. package/dist/esm/subconscious/BrainEncryption.js +126 -0
  56. package/dist/esm/subconscious/BrainEncryption.js.map +1 -0
  57. package/dist/esm/subconscious/TheBrainV2.d.ts +294 -0
  58. package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
  59. package/dist/esm/subconscious/TheBrainV2.js +1068 -111
  60. package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
  61. package/package.json +1 -1
package/README.md CHANGED
@@ -52,6 +52,23 @@ lemma start # gateway + dashboard on http://localhost:8081
52
52
  real usage (`hits`), explicit negative feedback (`downvote_memory`), and `cognitive_map` domain
53
53
  tags; a passing `test_oracle` run over an uncommitted diff auto-captures itself; two memories
54
54
  about the same symbol that disagree on outcome are flagged, not silently both suggested.
55
+ Every hit explains itself (`whyShown` ranking signals + cited `evidence` files/symbols), and
56
+ every reuse books its savings onto the memory itself — `brain_stats` names which memories
57
+ actually pay for the Brain (`totalTokensSaved`, top savers).
58
+ - **A memory that maintains itself** — `brain_upkeep` revalidates the most-reused memories,
59
+ surfaces exact duplicates and merge candidates, dead weight, and savings in one read-only
60
+ pass, each finding with its fix. `brain_merge` folds near-duplicate fragments into one
61
+ canonical entry (fragments survive as evidence-carrying claims). `forget_memory` moves to a
62
+ trash (`brain_trash`: list/restore/purge) instead of destroying, automatic snapshots roll
63
+ back every forget/import/clear/merge, and `brain_import dryRun:true` previews a bundle —
64
+ counts plus opposite-outcome conflicts — before a byte is written. Session onboarding
65
+ re-checks freshness of the memories it lists, so a day-one answer never starts stale.
66
+ - **Integrity and encryption at rest** — the corpus carries a sha256 sidecar with `.prev`
67
+ backup rotation: a half-written or rotted file serves its intact lines, falls back to the
68
+ backup on total loss, and quarantines (never deletes) the bad bytes, with the incident
69
+ surfaced in `brain_stats`. `LEMMA_BRAIN_ENCRYPT=1` (or `LEMMA_BRAIN_KEY`) stores the
70
+ corpus AES-256-GCM encrypted under a local 0600 key; secrets never reach disk at all —
71
+ scrub-on-store redacts credentials one-way before indexing or persisting.
55
72
  - **Verifiable caching, not marketing numbers** — `state_hash_cache` only hits when the files an
56
73
  answer depends on are byte-identical; `token_receipt` gives a real per-session breakdown of how
57
74
  each answer was produced instead of an invented "% saved."
@@ -66,7 +83,7 @@ lemma start # gateway + dashboard on http://localhost:8081
66
83
  compiler-verified AST location instead of asking the model to output a full-file rewrite.
67
84
  - **Infrastructure Command Center** — 22 free tools for Jenkins, OKD/OpenShift, and Terraform:
68
85
  discover, connect, deploy, diagnose, and detect drift, all via MCP.
69
- - **Scoped tool surface** — of the 98 registered tools, only the ones your project shows evidence
86
+ - **Scoped tool surface** — of the 108 registered tools, only the ones your project shows evidence
70
87
  of needing are advertised to the model each turn; the rest stay one `lemma_toolbox` call away.
71
88
 
72
89
  See [`CHANGELOG.md`](CHANGELOG.md) for the full, per-version history.
@@ -75,13 +92,14 @@ See [`CHANGELOG.md`](CHANGELOG.md) for the full, per-version history.
75
92
 
76
93
  ## 💎 Free vs Pro
77
94
 
78
- 48 of the 98 registered tools work with no license; 50 require Pro. Enforced by one allowlist in
79
- code (`FREE_TOOLS` in `src/mcp/tools.ts`), not by narrative. A session without a license doesn't
80
- even advertise the Pro tools — they stay registered and discoverable via `lemma_toolbox`.
95
+ 58 of the 108 registered tools work with no license; 50 require Pro. Enforced by one allowlist in
96
+ code (`FREE_TOOLS` in `src/mcp/tools.ts`, including all 22 infrastructure tools), not by narrative.
97
+ A session without a license doesn't even advertise the Pro tools — they stay registered and
98
+ discoverable via `lemma_toolbox`.
81
99
 
82
- | | Free (48) | Pro (50) |
100
+ | | Free (58) | Pro (50) |
83
101
  | :--- | :--- | :--- |
84
- | **Caching & memory** | `smarter_cache`, `state_hash_cache`, `token_receipt`, `search_memory`, `store_memory`, `downvote_memory`, `verify_memory`, `token_budget` | `get_telepathic_hints`, `query_hybrid_consensus`, `semantic_dedup_guard`, `local_semantic_autofix` |
102
+ | **Caching & memory** | `smarter_cache`, `state_hash_cache`, `token_receipt`, `search_memory`, `store_memory`, `downvote_memory`, `verify_memory`, `token_budget`, `brain_upkeep`, `brain_trash`, `brain_merge` | `get_telepathic_hints`, `query_hybrid_consensus`, `semantic_dedup_guard`, `local_semantic_autofix` |
85
103
  | **Reading & context** | `read_workspace_file`, `scrub_privacy`, `import_tree_context`, `bulk_file_digest`, `auto_context_bundle`, `git_blame_heat`, `type_coverage_report` | `get_ast_hologram`, `smart_file_slice`, `get_symbol_surgical_context`, `diff_only` |
86
104
  | **Writing & workspace** | — | `write_workspace_file`, `apply_workspace_patch`, `multi_file_patch`, `search_workspace`, `run_workspace_command`, `validate_patch_sandbox`, `surgical_ast_insert` |
87
105
  | **Static analysis** | — | `entropy_score`, `coupling_radar`, `pattern_fossil`, `precrime_static`, `dead_export_necromancer`, `depgraph`, `refactor`, `ast_flow_visualizer`, `cognitive_map` |
@@ -96,7 +114,7 @@ Full per-tool listing: `lemma_toolbox { "action": "list" }`.
96
114
 
97
115
  ## 🎚️ Tool profiles
98
116
 
99
- Every advertised tool schema is re-sent on **every request** of the session — advertising all 97
117
+ Every advertised tool schema is re-sent on **every request** of the session — advertising all 108
100
118
  costs ~11,600 tokens/turn. Lemma advertises a scoped surface instead: core tools always, situational
101
119
  groups (e.g. infrastructure) only when the repo shows evidence for them. Nothing is removed —
102
120
  everything stays registered and reachable via `lemma_toolbox`.
@@ -194,6 +212,14 @@ JS/TS, Python, Go, Rust, Ruby, JVM, `make`, git/gh, read-only shell utilities, a
194
212
  CLIs. Extend via `lemma.config.json` → `mcp.allowedCommandPrefixes`. See
195
213
  [`.env.security.example`](.env.security.example) for every security-related env var.
196
214
 
215
+ **Brain at rest.** Secrets are redacted one-way before a memory is indexed or persisted
216
+ (scrub-on-store, same patterns as the traffic firewall — the token map is discarded, there is
217
+ no unmask step). `LEMMA_BRAIN_ENCRYPT=1` additionally stores the corpus AES-256-GCM encrypted
218
+ under `<brainDir>/.brain.key` (0600, auto-generated); `LEMMA_BRAIN_KEY` (64 hex chars or base64
219
+ of 32 bytes) overrides it for teams sharing one key. A malformed key fails loud at startup;
220
+ a wrong key flows into the integrity fallback (backup → quarantine → fresh start), never an
221
+ exception, never half a corpus.
222
+
197
223
  ---
198
224
 
199
225
  ## 🦜 SDK integrations & proxy mode
package/bin/init.js CHANGED
@@ -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 (see installProxyDaemon). So ANTHROPIC_AUTH_TOKEN here is just a
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) {
@@ -795,20 +795,19 @@ function configureOpenCode() {
795
795
  return true;
796
796
  }
797
797
 
798
- function installProxyDaemon() {
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()) {
799
804
  const isMac = process.platform === 'darwin';
800
805
  const isLinux = process.platform === 'linux';
801
806
 
802
- // The full proxy server (lemma-proxy.ts) needs its own server-side credentials to sign
803
- // upstream requests — it never trusts whatever token the calling client sends. A launchd/
804
- // systemd service does not inherit the shell's exported env, so these must be baked into
805
- // the unit definition at install time or the daemon silently falls back to local Ollama.
806
- const anthropicApiKey = process.env.ANTHROPIC_API_KEY || '';
807
- const openaiApiKey = process.env.OPENAI_API_KEY || '';
808
807
  const proxyArgs = ['start', '--port', PORT, '--no-configure', '--no-clipboard'];
809
808
 
810
809
  if (isMac) {
811
- const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
810
+ const plistDir = path.join(homeDir, 'Library', 'LaunchAgents');
812
811
  ensureDir(plistDir);
813
812
  const plistPath = path.join(plistDir, 'com.lemma.cache-proxy.plist');
814
813
  const argEntries = proxyArgs.map((a) => ` <string>${a}</string>`).join('\n');
@@ -816,9 +815,7 @@ function installProxyDaemon() {
816
815
  ` <key>LEMMA_PROXY_PORT</key><string>${PORT}</string>`,
817
816
  ` <key>LEMMA_CACHE_DIR</key><string>${LEMMA_CACHE}</string>`,
818
817
  ` <key>LEMMA_DISABLE_CLIPBOARD</key><string>true</string>`,
819
- anthropicApiKey ? ` <key>ANTHROPIC_API_KEY</key><string>${anthropicApiKey}</string>` : '',
820
- openaiApiKey ? ` <key>OPENAI_API_KEY</key><string>${openaiApiKey}</string>` : '',
821
- ].filter(Boolean).join('\n');
818
+ ].join('\n');
822
819
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
823
820
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
824
821
  <plist version="1.0">
@@ -848,15 +845,13 @@ ${envEntries}
848
845
  }
849
846
 
850
847
  if (isLinux) {
851
- const svcDir = path.join(os.homedir(), '.config', 'systemd', 'user');
848
+ const svcDir = path.join(homeDir, '.config', 'systemd', 'user');
852
849
  ensureDir(svcDir);
853
850
  const envLines = [
854
851
  `Environment=LEMMA_PROXY_PORT=${PORT}`,
855
852
  `Environment=LEMMA_CACHE_DIR=${LEMMA_CACHE}`,
856
853
  `Environment=LEMMA_DISABLE_CLIPBOARD=true`,
857
- anthropicApiKey ? `Environment=ANTHROPIC_API_KEY=${anthropicApiKey}` : '',
858
- openaiApiKey ? `Environment=OPENAI_API_KEY=${openaiApiKey}` : '',
859
- ].filter(Boolean).join('\n');
854
+ ].join('\n');
860
855
  const svc = `[Unit]
861
856
  Description=Lemma Cache Proxy
862
857
  After=network.target
@@ -879,6 +874,39 @@ WantedBy=default.target
879
874
  return false;
880
875
  }
881
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
+
882
910
  function applyEnvToCurrentProcess() {
883
911
  const proxyUrl = `http://localhost:${PORT}`;
884
912
  process.env.LEMMA_PROXY_PORT = PORT;
@@ -889,21 +917,29 @@ function applyEnvToCurrentProcess() {
889
917
  process.env.LITELLM_PROXY_BASE_URL = proxyUrl;
890
918
  }
891
919
 
892
- function createEnvFile() {
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) {
893
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');
894
929
  const envContent = `# Lemma Cache Proxy — Auto-generated by \`lemma init\`
895
930
  # Source this file in your shell to route LLM traffic through the cache:
896
- # source ${ENV_FILE}
931
+ # source ${envFile}
897
932
  export LEMMA_PROXY_PORT=${PORT}
898
933
  export LEMMA_CACHE_DIR=${LEMMA_CACHE}
899
934
  export OPENAI_BASE_URL=${proxyUrl}
900
935
  export OPENAI_API_BASE=${proxyUrl}
901
936
  export CODEX_BASE_URL=${proxyUrl}
902
937
  export LITELLM_PROXY_BASE_URL=${proxyUrl}
903
- `;
904
- fs.writeFileSync(ENV_FILE, envContent, 'utf8');
938
+ ${keyLines ? keyLines + '\n' : ''}`;
939
+ fs.writeFileSync(envFile, envContent, 'utf8');
940
+ try { fs.chmodSync(envFile, 0o600); } catch {}
905
941
  applyEnvToCurrentProcess();
906
- return ENV_FILE;
942
+ return envFile;
907
943
  }
908
944
 
909
945
  function setupShellIntegration() {
@@ -942,7 +978,26 @@ function setupShellIntegration() {
942
978
  return configured;
943
979
  }
944
980
 
945
- 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
+
946
1001
  process.stderr.write(`\n ${BOLD}${CYAN}Lemma Init — Magic Setup${RESET}\n\n`);
947
1002
 
948
1003
  ensureDir(LEMMA_CACHE);
@@ -961,17 +1016,23 @@ async function main() {
961
1016
  configureClaudeCode();
962
1017
  ok('Claude Code / CLI configured (CLAUDE.md + .claude/settings.local.json)');
963
1018
 
964
- const anthropicApiKey = process.env.ANTHROPIC_API_KEY || '';
965
- const gatewayResult = configureClaudeCodeGateway(anthropicApiKey);
966
- if (gatewayResult.activated) {
967
- ok(`Claude Code gateway activated (~/.claude/settings.json ANTHROPIC_BASE_URL=http://localhost:${PORT})`);
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.');
968
1023
  } else {
969
- warn('Claude Code gateway NOT activated — no ANTHROPIC_API_KEY in this environment.');
970
- warn(` Model traffic keeps going directly to Anthropic (subscription/OAuth or your own key, unchanged).`);
971
- warn(` Only Lemma's MCP tools are active for Claude Code — no cache savings on the model traffic itself.`);
972
- warn(` To enable it: export ANTHROPIC_API_KEY=sk-ant-... and re-run \`lemma init\`.`);
973
- warn(` This SWITCHES Claude Code off your subscription/OAuth session for LLM calls — they'll be`);
974
- warn(` signed by the proxy's own ANTHROPIC_API_KEY instead, and billed/rate-limited as API usage.`);
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
+ }
975
1036
  }
976
1037
 
977
1038
  updateAgentsMd();
@@ -1024,9 +1085,15 @@ async function main() {
1024
1085
  if (hasWindsurf) ok('Windsurf detected — env auto-configured');
1025
1086
  else skip('Windsurf not detected');
1026
1087
 
1027
- const daemonOk = installProxyDaemon();
1028
- if (daemonOk) ok('Cache proxy daemon installed & started (OpenAI + Anthropic /v1/messages, streaming)');
1029
- else warn('Could not install daemon run `lemma proxy start` manually');
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
+ }
1030
1097
 
1031
1098
  const envFile = createEnvFile();
1032
1099
  ok(`Env file created: ${envFile}`);
@@ -1037,7 +1104,11 @@ async function main() {
1037
1104
 
1038
1105
  process.stderr.write(`\n ${BOLD}${GREEN}✓ Lemma init complete${RESET}\n`);
1039
1106
  process.stderr.write(` ${DIM}──────────────────────────────────────${RESET}\n`);
1040
- process.stderr.write(` ${GREEN}●${RESET} Cache proxy running on http://localhost:${PORT}\n`);
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
+ }
1041
1112
  process.stderr.write(` ${GREEN}●${RESET} MCP server ready ${DIM}(lemma mcp)${RESET}\n`);
1042
1113
  process.stderr.write(` ${GREEN}●${RESET} Dashboard active ${DIM}(lemma stats)${RESET}\n`);
1043
1114
  process.stderr.write(` ${GREEN}●${RESET} New terminals: env vars auto-loaded\n`);
@@ -1057,4 +1128,4 @@ if (require.main === module) {
1057
1128
  });
1058
1129
  }
1059
1130
 
1060
- module.exports = { main, configureMuse, configureMuseUserSkill, updateAgentsMd, MUSE_SKILL_CONTENT, LEMMA_RULES_CONTENT };
1131
+ module.exports = { main, configureMuse, configureMuseUserSkill, updateAgentsMd, MUSE_SKILL_CONTENT, LEMMA_RULES_CONTENT, installProxyDaemon, removeProxyDaemon, parseInitArgs, createEnvFile };
@@ -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;AA4nED,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"}
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"}
@@ -4,7 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.MAX_WORKSPACE_WATCHERS = void 0;
7
8
  exports.startBackgroundClipboardWatcher = startBackgroundClipboardWatcher;
9
+ exports.isUnsafeWatchRoot = isUnsafeWatchRoot;
8
10
  exports.performAutoHeal = performAutoHeal;
9
11
  const commander_1 = require("commander");
10
12
  const express_1 = __importDefault(require("express"));
@@ -960,6 +962,16 @@ class WriteQueue {
960
962
  }
961
963
  }
962
964
  const usageWriteQueue = new WriteQueue();
965
+ // One fs.watch FD is opened per watched directory. Watching $HOME (the launchd
966
+ // daemon's WorkingDirectory) or a filesystem root exhausts FDs (EMFILE), crashes
967
+ // the process, and a KeepAlive supervisor turns that into an infinite
968
+ // crash/relaunch loop — on macOS each relaunch re-triggers the TCC data-access
969
+ // prompt. Refuse those roots outright.
970
+ exports.MAX_WORKSPACE_WATCHERS = 512;
971
+ function isUnsafeWatchRoot(dir) {
972
+ const resolved = path_1.default.resolve(dir);
973
+ return resolved === path_1.default.resolve(os_1.default.homedir()) || resolved === path_1.default.parse(resolved).root;
974
+ }
963
975
  // ── Proxy Server class ─────────────────────────────────────────────────────────
964
976
  class LemmaServer {
965
977
  constructor(port, projectName, openBrowser = true) {
@@ -1648,12 +1660,28 @@ class LemmaServer {
1648
1660
  }
1649
1661
  startWorkspaceTimeWatcher() {
1650
1662
  const cwd = process.cwd();
1663
+ if (isUnsafeWatchRoot(cwd)) {
1664
+ console.log(`⏭️ [TimeTravel] Skipping workspace watcher: refusing to watch ${cwd} recursively (file-descriptor exhaustion risk). Start the proxy from a project directory instead.`);
1665
+ return;
1666
+ }
1651
1667
  const { watch } = require('fs');
1652
1668
  // Debounced capture function
1653
1669
  let debounceTimer = null;
1654
1670
  const pendingChanges = new Set();
1671
+ // One FD per watched directory: cap it so giant trees degrade to a warning
1672
+ // instead of an EMFILE crash (which a KeepAlive supervisor turns into a
1673
+ // crash/relaunch loop).
1674
+ let watchCount = 0;
1675
+ let limitWarned = false;
1655
1676
  const ignoreDirs = ['node_modules', '.git', 'dist', 'chroma_data', '.lemma', 'dashboard', 'bin', 'sdks'];
1656
1677
  const watchDirRecursive = (dir) => {
1678
+ if (watchCount >= exports.MAX_WORKSPACE_WATCHERS) {
1679
+ if (!limitWarned) {
1680
+ limitWarned = true;
1681
+ console.log(`⚠️ [TimeTravel] Watcher limit (${exports.MAX_WORKSPACE_WATCHERS} dirs) reached at ${dir} — deeper directories won't trigger snapshots.`);
1682
+ }
1683
+ return;
1684
+ }
1657
1685
  try {
1658
1686
  const watcher = watch(dir, (eventType, filename) => {
1659
1687
  if (!filename)
@@ -1675,6 +1703,19 @@ class LemmaServer {
1675
1703
  // Prevent watcher from keeping process alive if in background
1676
1704
  if (watcher.unref)
1677
1705
  watcher.unref();
1706
+ watchCount++;
1707
+ // An async EMFILE/ENOSPC on a giant tree must never take down the server:
1708
+ // contain it to a single warning and keep serving.
1709
+ watcher.on('error', (err) => {
1710
+ if (!limitWarned) {
1711
+ limitWarned = true;
1712
+ console.log(`⚠️ [TimeTravel] Watcher error (${(err && err.code) || 'unknown'}) — snapshots degraded, server keeps running.`);
1713
+ }
1714
+ try {
1715
+ watcher.close();
1716
+ }
1717
+ catch { }
1718
+ });
1678
1719
  // Recursively watch subfolders
1679
1720
  const files = fs_1.default.readdirSync(dir, { withFileTypes: true });
1680
1721
  for (const file of files) {