@nxuss/lemma 1.27.1 → 1.28.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 (77) hide show
  1. package/README.md +53 -14
  2. package/bin/brain-ingest.js +64 -2
  3. package/dist/cjs/cli/gitHooks.d.ts +28 -0
  4. package/dist/cjs/cli/gitHooks.d.ts.map +1 -0
  5. package/dist/cjs/cli/gitHooks.js +112 -0
  6. package/dist/cjs/cli/gitHooks.js.map +1 -0
  7. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  8. package/dist/cjs/cli/lemma-proxy.js +12 -0
  9. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  10. package/dist/cjs/mcp/tool-profiles.d.ts.map +1 -1
  11. package/dist/cjs/mcp/tool-profiles.js +8 -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 +56 -0
  15. package/dist/cjs/mcp/tools/memory.js.map +1 -1
  16. package/dist/cjs/mcp/tools/misc.d.ts.map +1 -1
  17. package/dist/cjs/mcp/tools/misc.js +16 -1
  18. package/dist/cjs/mcp/tools/misc.js.map +1 -1
  19. package/dist/cjs/mcp/tools/workspace.d.ts.map +1 -1
  20. package/dist/cjs/mcp/tools/workspace.js +17 -3
  21. package/dist/cjs/mcp/tools/workspace.js.map +1 -1
  22. package/dist/cjs/mcp/tools.d.ts.map +1 -1
  23. package/dist/cjs/mcp/tools.js +2 -0
  24. package/dist/cjs/mcp/tools.js.map +1 -1
  25. package/dist/cjs/subconscious/GitIngest.d.ts.map +1 -1
  26. package/dist/cjs/subconscious/GitIngest.js +49 -1
  27. package/dist/cjs/subconscious/GitIngest.js.map +1 -1
  28. package/dist/cjs/subconscious/TeamBrain.d.ts +82 -0
  29. package/dist/cjs/subconscious/TeamBrain.d.ts.map +1 -0
  30. package/dist/cjs/subconscious/TeamBrain.js +543 -0
  31. package/dist/cjs/subconscious/TeamBrain.js.map +1 -0
  32. package/dist/cjs/subconscious/TheBrainV2.d.ts +4 -2
  33. package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
  34. package/dist/cjs/subconscious/TheBrainV2.js +1 -0
  35. package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
  36. package/dist/cjs/utils/FixCapture.d.ts +66 -0
  37. package/dist/cjs/utils/FixCapture.d.ts.map +1 -0
  38. package/dist/cjs/utils/FixCapture.js +338 -0
  39. package/dist/cjs/utils/FixCapture.js.map +1 -0
  40. package/dist/esm/cli/gitHooks.d.ts +28 -0
  41. package/dist/esm/cli/gitHooks.d.ts.map +1 -0
  42. package/dist/esm/cli/gitHooks.js +106 -0
  43. package/dist/esm/cli/gitHooks.js.map +1 -0
  44. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  45. package/dist/esm/cli/lemma-proxy.js +12 -0
  46. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  47. package/dist/esm/mcp/tool-profiles.d.ts.map +1 -1
  48. package/dist/esm/mcp/tool-profiles.js +8 -0
  49. package/dist/esm/mcp/tool-profiles.js.map +1 -1
  50. package/dist/esm/mcp/tools/memory.d.ts.map +1 -1
  51. package/dist/esm/mcp/tools/memory.js +56 -0
  52. package/dist/esm/mcp/tools/memory.js.map +1 -1
  53. package/dist/esm/mcp/tools/misc.d.ts.map +1 -1
  54. package/dist/esm/mcp/tools/misc.js +16 -1
  55. package/dist/esm/mcp/tools/misc.js.map +1 -1
  56. package/dist/esm/mcp/tools/workspace.d.ts.map +1 -1
  57. package/dist/esm/mcp/tools/workspace.js +17 -3
  58. package/dist/esm/mcp/tools/workspace.js.map +1 -1
  59. package/dist/esm/mcp/tools.d.ts.map +1 -1
  60. package/dist/esm/mcp/tools.js +2 -0
  61. package/dist/esm/mcp/tools.js.map +1 -1
  62. package/dist/esm/subconscious/GitIngest.d.ts.map +1 -1
  63. package/dist/esm/subconscious/GitIngest.js +49 -1
  64. package/dist/esm/subconscious/GitIngest.js.map +1 -1
  65. package/dist/esm/subconscious/TeamBrain.d.ts +82 -0
  66. package/dist/esm/subconscious/TeamBrain.d.ts.map +1 -0
  67. package/dist/esm/subconscious/TeamBrain.js +533 -0
  68. package/dist/esm/subconscious/TeamBrain.js.map +1 -0
  69. package/dist/esm/subconscious/TheBrainV2.d.ts +4 -2
  70. package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
  71. package/dist/esm/subconscious/TheBrainV2.js +1 -0
  72. package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
  73. package/dist/esm/utils/FixCapture.d.ts +66 -0
  74. package/dist/esm/utils/FixCapture.d.ts.map +1 -0
  75. package/dist/esm/utils/FixCapture.js +328 -0
  76. package/dist/esm/utils/FixCapture.js.map +1 -0
  77. package/package.json +1 -1
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@nxuss/lemma.svg?color=orange&style=flat-square)](https://www.npmjs.com/package/@nxuss/lemma)
4
4
  [![License](https://img.shields.io/npm/l/@nxuss/lemma.svg?color=blue&style=flat-square)](https://github.com/Nxusbets/lemma/blob/main/LICENSE)
5
- [![MCP Tools](https://img.shields.io/badge/MCP%20tools-109-ff69b4?style=flat-square)](https://github.com/Nxusbets/lemma)
5
+ [![MCP Tools](https://img.shields.io/badge/MCP%20tools-110-ff69b4?style=flat-square)](https://github.com/Nxusbets/lemma)
6
6
  [![Security](https://img.shields.io/badge/privacy-firewall%20active-blueviolet?style=flat-square)](https://github.com/Nxusbets/lemma)
7
7
  [![Caching](https://img.shields.io/badge/semantic%20memory-enabled-success?style=flat-square)](https://github.com/Nxusbets/lemma)
8
- [![Tests](https://img.shields.io/badge/tests-919%20passing-00c853?style=flat-square)](https://github.com/Nxusbets/lemma)
8
+ [![Tests](https://img.shields.io/badge/tests-964%20passing-00c853?style=flat-square)](https://github.com/Nxusbets/lemma)
9
9
 
10
10
  ```text
11
11
  __
@@ -35,6 +35,18 @@ lemma start # gateway + dashboard on http://localhost:8081
35
35
  >
36
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`.
37
37
  >
38
+ > **🆕 v1.28.0 — the Brain goes team-wide through git, and fills itself.** `brain_team push`
39
+ > writes your verified memories into the repo (`.lemma-brain/memories/`, one reviewable JSON file
40
+ > each, paths relative to the repo root); `brain_team pull` — automatic on merge/checkout once
41
+ > `lemma init` installs the hooks — imports a teammate's memories rebased onto *your* checkout. A
42
+ > memory about a file that is byte-identical on your machine arrives **fresh**; one whose file has
43
+ > moved on arrives **stale**, never silently trusted. And memories no longer depend on the model
44
+ > remembering `store_memory`: a test/build/lint command that goes **red → green** is captured as
45
+ > `fix: <the error>` + the files and diff that fixed it — searchable by the exact error message next
46
+ > time — whether it ran through `run_workspace_command` or the agent's own shell (Claude Code
47
+ > `PostToolUse` hook). Commits become memories anchored to their own blobs. Also fixes
48
+ > `lemma-brain-ingest` silently dropping everything it ingested.
49
+ >
38
50
  > **🆕 v1.26.0 — a memory without evidence is no longer sold as a verified hit, and the Brain
39
51
  > repairs it on the spot.** `search_memory` now splits its results into three buckets — `fresh`
40
52
  > (tracked + verified, the only ones credited `tokensSaved`/`hits`), `stale`, and `UNTRACKED`
@@ -64,7 +76,7 @@ lemma start # gateway + dashboard on http://localhost:8081
64
76
  the code they're about to touch has dependent memories — including a documented failed
65
77
  attempt — instead of only finding out on the next search. Ranking factors in
66
78
  real usage (`hits`), explicit negative feedback (`downvote_memory`), and `cognitive_map` domain
67
- tags; a passing `test_oracle` run over an uncommitted diff auto-captures itself; two memories
79
+ tags; a command that goes red → green auto-captures the fix (see below); two memories
68
80
  about the same symbol that disagree on outcome are flagged, not silently both suggested.
69
81
  Every hit explains itself (`whyShown` ranking signals + cited `evidence` files/symbols), and
70
82
  every reuse books its savings onto the memory itself — `brain_stats` and `brain_upkeep` name
@@ -72,6 +84,27 @@ lemma start # gateway + dashboard on http://localhost:8081
72
84
  it, never one without the other). `store_memory` derives `filePaths`/`symbols` by default from
73
85
  whatever the session already read, wrote, or patched when the caller doesn't pass them —
74
86
  flagged as auto-derived in the response, correctable, but no longer opt-in.
87
+ - **A team Brain, shared through git** — `brain_team push` writes this project's fresh, tracked
88
+ memories to `.lemma-brain/memories/<id>.json`: one sorted-key JSON file per memory, so two people
89
+ sharing at once never conflict, every shared memory goes through code review, and deleting one is
90
+ a `git rm`. Evidence is stored relative to the repo root and rebased onto each clone on `pull`,
91
+ hashes untouched — a teammate's memory is verified against *your* files, fresh only if they are
92
+ byte-identical. Never shared: conversation checkpoints, commit memories (each clone derives its
93
+ own), untracked or stale memories, evidence outside the repo, or anything derived from a memory
94
+ that isn't shared. A pulled path that is absolute or escapes the repo is rejected; a memory a
95
+ teammate deletes leaves your clone into `brain_trash` (restorable), and one you wrote yourself is
96
+ never removed. Pushing is always explicit; pulling is a no-op when nothing changed, so it runs on
97
+ every merge/checkout. `brain_team status` shows both sides; `dryRun:true` previews either.
98
+ - **Memories that write themselves** — a verdict command (jest, vitest, pytest, tsc, eslint, `go
99
+ test`, `cargo test`, `npm test`/`build`/`lint`, …) that fails and later passes stores
100
+ `fix: <headline error>` with the failure signature, the files whose content changed between the
101
+ two runs, and their diff — tracked against those files, tagged `confirmed`, source `auto-fix`.
102
+ A pass with nothing changed since the failure is treated as flaky and stores nothing; a "fix" over
103
+ 15 files is a refactor and stores nothing. It sees `run_workspace_command`, `test_oracle`, and — via
104
+ `lemma-brain-ingest --hook-result` — the agent's own Bash tool. Commits are ingested with each
105
+ touched file anchored to its blob *in that commit* (`git show sha:path`), not to the working copy.
106
+ `lemma init` installs the `post-commit`, `post-merge` and `post-checkout` hooks that drive this
107
+ (appended to existing hooks, idempotent, never touches a versioned `core.hooksPath` like husky).
75
108
  - **A memory that maintains itself** — `brain_upkeep` revalidates the most-reused memories,
76
109
  surfaces exact duplicates and merge candidates, dead weight, and savings, each finding with its
77
110
  fix. Since v1.26.0 it also **auto-anchors** evidence-free memories whose body names real repo
@@ -106,7 +139,7 @@ lemma start # gateway + dashboard on http://localhost:8081
106
139
  compiler-verified AST location instead of asking the model to output a full-file rewrite.
107
140
  - **Infrastructure Command Center** — 22 free tools for Jenkins, OKD/OpenShift, and Terraform:
108
141
  discover, connect, deploy, diagnose, and detect drift, all via MCP.
109
- - **Scoped tool surface** — of the 109 registered tools, only the ones your project shows evidence
142
+ - **Scoped tool surface** — of the 110 registered tools, only the ones your project shows evidence
110
143
  of needing are advertised to the model each turn; the rest stay one `lemma_toolbox` call away.
111
144
 
112
145
  See [`CHANGELOG.md`](CHANGELOG.md) for the full, per-version history.
@@ -115,14 +148,14 @@ See [`CHANGELOG.md`](CHANGELOG.md) for the full, per-version history.
115
148
 
116
149
  ## 💎 Free vs Pro
117
150
 
118
- 59 of the 109 registered tools work with no license; 50 require Pro. Enforced by one allowlist in
151
+ 60 of the 110 registered tools work with no license; 50 require Pro. Enforced by one allowlist in
119
152
  code (`FREE_TOOLS` in `src/mcp/tools.ts`, including all 22 infrastructure tools), not by narrative.
120
153
  A session without a license doesn't even advertise the Pro tools — they stay registered and
121
154
  discoverable via `lemma_toolbox`.
122
155
 
123
- | | Free (59) | Pro (50) |
156
+ | | Free (60) | Pro (50) |
124
157
  | :--- | :--- | :--- |
125
- | **Caching & memory** | `smarter_cache`, `state_hash_cache`, `token_receipt`, `search_memory`, `list_memories`, `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` |
158
+ | **Caching & memory** | `smarter_cache`, `state_hash_cache`, `token_receipt`, `search_memory`, `list_memories`, `store_memory`, `downvote_memory`, `verify_memory`, `token_budget`, `brain_upkeep`, `brain_trash`, `brain_merge`, `brain_team` | `get_telepathic_hints`, `query_hybrid_consensus`, `semantic_dedup_guard`, `local_semantic_autofix` |
126
159
  | **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` |
127
160
  | **Writing & workspace** | — | `write_workspace_file`, `apply_workspace_patch`, `multi_file_patch`, `search_workspace`, `run_workspace_command`, `validate_patch_sandbox`, `surgical_ast_insert` |
128
161
  | **Static analysis** | — | `entropy_score`, `coupling_radar`, `pattern_fossil`, `precrime_static`, `dead_export_necromancer`, `depgraph`, `refactor`, `ast_flow_visualizer`, `cognitive_map` |
@@ -137,8 +170,8 @@ Full per-tool listing: `lemma_toolbox { "action": "list" }`.
137
170
 
138
171
  ## 🎚️ Tool profiles
139
172
 
140
- Every advertised tool schema is re-sent on **every request** of the session — advertising all 109
141
- costs ~11,600 tokens/turn. Lemma advertises a scoped surface instead: core tools always, situational
173
+ Every advertised tool schema is re-sent on **every request** of the session — advertising all 110
174
+ costs ~11,900 tokens/turn. Lemma advertises a scoped surface instead: core tools always, situational
142
175
  groups (e.g. infrastructure) only when the repo shows evidence for them. Nothing is removed —
143
176
  everything stays registered and reachable via `lemma_toolbox`.
144
177
 
@@ -161,7 +194,7 @@ lemma_toolbox { "action": "call", "tool": "terraform_drift", "args": {} }
161
194
 
162
195
  | Command | Action |
163
196
  | :--- | :--- |
164
- | `lemma init` | Bootstraps config and auto-configures MCP for OpenCode, Claude Desktop, Cursor, Windsurf, Kiro, VS Code. |
197
+ | `lemma init` | Bootstraps config, auto-configures MCP for OpenCode, Claude Desktop, Cursor, Windsurf, Kiro, VS Code, and installs the git hooks that feed the Brain (`--no-hooks` to skip). |
165
198
  | `lemma start` | Launches proxy, cache engine, and dashboard at `http://localhost:8081`. |
166
199
  | `lemma stop` / `lemma status` / `lemma stats` | Stop, health check, and net token/cost report (reports `NET COST` when a session is underwater). |
167
200
  | `lemma mcp` | Starts the stdin/stdout MCP server for IDE integration. |
@@ -169,7 +202,8 @@ lemma_toolbox { "action": "call", "tool": "terraform_drift", "args": {} }
169
202
  | `lemma patches` / `lemma apply <id>` / `lemma discard <id>` | List, apply, or discard a pending Silent Patch Watcher fix. |
170
203
  | `lemma activate <key>` | Activate a Pro license. |
171
204
  | `lemma-checkpoint [projectRoot]` | Derive a session checkpoint from repo state (branch, changed files, commits) with no assistant call. |
172
- | `lemma-brain-ingest [projectRoot] [--changelog]` | Feed The Brain from git commits and `CHANGELOG.md`, incrementally. |
205
+ | `lemma-brain-ingest [projectRoot] [--changelog] [--team-pull]` | Feed The Brain from git commits and `CHANGELOG.md`, incrementally; `--team-pull` imports the repo's `.lemma-brain/`. |
206
+ | `lemma-brain-ingest --hook-result` | Read a host `PostToolUse` payload on stdin and capture red → green fixes from the agent's own shell. |
173
207
 
174
208
  Wire the standalone bins into hooks so memory doesn't depend on the assistant remembering to call a tool:
175
209
 
@@ -179,11 +213,16 @@ Wire the standalone bins into hooks so memory doesn't depend on the assistant re
179
213
  "command": "lemma-checkpoint \"${CLAUDE_PROJECT_DIR:-$PWD}\"" }] }] } }
180
214
  ```
181
215
 
182
- ```bash
183
- echo 'lemma-brain-ingest "$(git rev-parse --show-toplevel)" &' >> .git/hooks/post-commit
184
- chmod +x .git/hooks/post-commit
216
+ ```jsonc
217
+ // .claude/settings.json — red → green fixes from Claude Code's own Bash tool become memories
218
+ { "hooks": { "PostToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command",
219
+ "command": "lemma-brain-ingest --hook-result" }] }] } }
185
220
  ```
186
221
 
222
+ The git side needs nothing by hand: `lemma init` installs `post-commit` (commit → memory) and
223
+ `post-merge`/`post-checkout` (pull the team's `.lemma-brain/`). With a versioned hooks dir
224
+ (`core.hooksPath`, e.g. husky) it prints the lines to add instead of editing team files.
225
+
187
226
  Customize via `lemma.config.json`:
188
227
 
189
228
  ```json
@@ -13,18 +13,54 @@
13
13
  * last commit ingested, so running this after every commit or by hand both take the same path
14
14
  * and never double-store. `--max N` only matters the first time there is no marker yet.
15
15
  *
16
- * Usage: lemma-brain-ingest [projectRoot] [--max N] [--changelog] [--no-commits]
16
+ * Usage: lemma-brain-ingest [projectRoot] [--max N] [--changelog] [--no-commits] [--team-pull]
17
+ * lemma-brain-ingest --hook-result < payload.json
18
+ *
19
+ * --team-pull Import the repo's shared memories from .lemma-brain/ (post-merge/post-checkout).
20
+ * --hook-result Read a host PostToolUse hook payload on stdin (Claude Code's Bash tool shape) and
21
+ * feed it to FixCapture, so a test that goes red → green through the agent's own
22
+ * shell becomes a memory too. Wire it in .claude/settings.json:
23
+ * "hooks": { "PostToolUse": [{ "matcher": "Bash",
24
+ * "hooks": [{ "type": "command", "command": "lemma-brain-ingest --hook-result" }] }] }
17
25
  */
18
26
 
19
27
  const path = require('path');
20
28
 
29
+ function readStdin() {
30
+ try {
31
+ return require('fs').readFileSync(0, 'utf8');
32
+ } catch {
33
+ return '';
34
+ }
35
+ }
36
+
37
+ function hookResult() {
38
+ const distPath = path.join(__dirname, '..', 'dist', 'cjs', 'utils', 'FixCapture.js');
39
+ let recordHookPayload;
40
+ try {
41
+ ({ recordHookPayload } = require(distPath));
42
+ } catch {
43
+ return; // no build — a hook must stay silent and harmless
44
+ }
45
+ let payload;
46
+ try {
47
+ payload = JSON.parse(readStdin());
48
+ } catch {
49
+ return;
50
+ }
51
+ const res = recordHookPayload(payload, payload && payload.cwd);
52
+ if (res && res.captured) console.error(res.note.trim());
53
+ }
54
+
21
55
  function main() {
22
56
  const args = process.argv.slice(2);
57
+ if (args.includes('--hook-result')) return hookResult();
23
58
  const maxIndex = args.indexOf('--max');
24
59
  const maxCommits = maxIndex >= 0 ? parseInt(args[maxIndex + 1], 10) : undefined;
25
60
  const includeChangelog = args.includes('--changelog');
26
61
  const includeCommits = !args.includes('--no-commits');
27
- const positional = args.filter((a, i) => !a.startsWith('--') && i !== maxIndex + 1);
62
+ const teamPull = args.includes('--team-pull');
63
+ const positional = args.filter((a, i) => !a.startsWith("--") && (maxIndex < 0 || i !== maxIndex + 1));
28
64
  const projectRoot = path.resolve(positional[0] || process.cwd());
29
65
 
30
66
  const distPath = path.join(__dirname, '..', 'dist', 'cjs', 'subconscious', 'GitIngest.js');
@@ -41,16 +77,42 @@ function main() {
41
77
  console.error(`[lemma-brain-ingest] commits: ${res.ingested} stored, ${res.skipped} skipped (up to ${res.lastSha ? res.lastSha.substring(0, 12) : 'none'})`);
42
78
  }
43
79
 
80
+ if (teamPull) {
81
+ try {
82
+ const { pullTeamBrain } = require(path.join(__dirname, '..', 'dist', 'cjs', 'subconscious', 'TeamBrain.js'));
83
+ const res = pullTeamBrain(projectRoot);
84
+ console.error(`[lemma-brain-ingest] team: ${res.message}`);
85
+ } catch (err) {
86
+ console.error('[lemma-brain-ingest] team pull skipped:', err && err.message);
87
+ }
88
+ }
89
+
44
90
  if (includeChangelog) {
45
91
  const res = ingestChangelog(projectRoot);
46
92
  console.error(`[lemma-brain-ingest] changelog: ${res.ingested} stored, ${res.skipped} skipped (${res.versionsSeen} version(s) seen)`);
47
93
  }
48
94
  }
49
95
 
96
+ /**
97
+ * The Brain debounces its writes by 500ms (right for a long-lived server), and process.exit()
98
+ * below drops pending timers — so without an explicit flush every memory this script stored was
99
+ * lost the moment it exited. Only flush when the Brain was actually loaded.
100
+ */
101
+ function flushBrain() {
102
+ try {
103
+ const modPath = require.resolve(path.join(__dirname, '..', 'dist', 'cjs', 'subconscious', 'TheBrainV2.js'));
104
+ if (!require.cache[modPath]) return;
105
+ require(modPath).getBrain().flushSync();
106
+ } catch (err) {
107
+ console.error('[lemma-brain-ingest] flush failed:', err && err.message);
108
+ }
109
+ }
110
+
50
111
  try {
51
112
  main();
52
113
  } catch (err) {
53
114
  // A hook that fails must never be the reason a commit or session goes bad.
54
115
  console.error('[lemma-brain-ingest] Skipped:', err && err.message);
55
116
  }
117
+ flushBrain();
56
118
  process.exit(0);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Installs the git hooks that feed the Brain without anyone remembering to: post-commit ingests
3
+ * the new commit, post-merge/post-checkout pull the team's shared memories (`.lemma-brain/`).
4
+ *
5
+ * `lemma-brain-ingest` shipped with a post-commit recipe in its header comment and nothing ever
6
+ * installed it — on the author's own machine the corpus held 0 commit memories. This is the
7
+ * missing install step, with the constraints a hook installer owes the repo it touches:
8
+ *
9
+ * - Append, never overwrite. An existing hook keeps running exactly as before; Lemma's part is
10
+ * one delimited block, and re-running init replaces that block in place (idempotent).
11
+ * - Never break a commit. Every line is guarded by `command -v`, backgrounded, and silenced.
12
+ * - Respect `core.hooksPath`. `git rev-parse --git-path hooks` resolves it; when it points inside
13
+ * the working tree (husky and friends — versioned hooks), the files are the team's, so they are
14
+ * left alone and the caller gets the lines to add instead.
15
+ */
16
+ export interface HookInstallResult {
17
+ installed: string[];
18
+ updated: string[];
19
+ unchanged: string[];
20
+ /** Set when hooks live in a versioned directory Lemma won't write to; holds what to add by hand. */
21
+ manual?: {
22
+ hooksDir: string;
23
+ snippet: string;
24
+ };
25
+ skippedReason?: string;
26
+ }
27
+ export declare function installBrainHooks(cwd?: string): HookInstallResult;
28
+ //# sourceMappingURL=gitHooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitHooks.d.ts","sourceRoot":"","sources":["../../../src/cli/gitHooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAmBH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,oGAAoG;IACpG,MAAM,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAcD,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAsB,GAAG,iBAAiB,CA0DhF"}
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /**
3
+ * Installs the git hooks that feed the Brain without anyone remembering to: post-commit ingests
4
+ * the new commit, post-merge/post-checkout pull the team's shared memories (`.lemma-brain/`).
5
+ *
6
+ * `lemma-brain-ingest` shipped with a post-commit recipe in its header comment and nothing ever
7
+ * installed it — on the author's own machine the corpus held 0 commit memories. This is the
8
+ * missing install step, with the constraints a hook installer owes the repo it touches:
9
+ *
10
+ * - Append, never overwrite. An existing hook keeps running exactly as before; Lemma's part is
11
+ * one delimited block, and re-running init replaces that block in place (idempotent).
12
+ * - Never break a commit. Every line is guarded by `command -v`, backgrounded, and silenced.
13
+ * - Respect `core.hooksPath`. `git rev-parse --git-path hooks` resolves it; when it points inside
14
+ * the working tree (husky and friends — versioned hooks), the files are the team's, so they are
15
+ * left alone and the caller gets the lines to add instead.
16
+ */
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.installBrainHooks = installBrainHooks;
22
+ const fs_1 = __importDefault(require("fs"));
23
+ const path_1 = __importDefault(require("path"));
24
+ const child_process_1 = require("child_process");
25
+ const BEGIN = '# >>> lemma brain hooks >>>';
26
+ const END = '# <<< lemma brain hooks <<<';
27
+ const HOOK_BODIES = {
28
+ 'post-commit': 'command -v lemma-brain-ingest >/dev/null 2>&1 && (lemma-brain-ingest "$(git rev-parse --show-toplevel)" >/dev/null 2>&1 &)',
29
+ 'post-merge': 'command -v lemma-brain-ingest >/dev/null 2>&1 && (lemma-brain-ingest "$(git rev-parse --show-toplevel)" --team-pull --no-commits >/dev/null 2>&1 &)',
30
+ // $3 is 1 for a branch checkout, 0 for a file checkout — only a branch switch can change the shared set.
31
+ 'post-checkout': '[ "$3" = "1" ] && command -v lemma-brain-ingest >/dev/null 2>&1 && (lemma-brain-ingest "$(git rev-parse --show-toplevel)" --team-pull --no-commits >/dev/null 2>&1 &)',
32
+ };
33
+ function git(cwd, args) {
34
+ try {
35
+ return (0, child_process_1.execFileSync)('git', ['-C', cwd, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function block(hook) {
42
+ return `${BEGIN}\n${HOOK_BODIES[hook]}\n${END}\n`;
43
+ }
44
+ function installBrainHooks(cwd = process.cwd()) {
45
+ const result = { installed: [], updated: [], unchanged: [] };
46
+ const top = git(cwd, ['rev-parse', '--show-toplevel']);
47
+ const hooksRel = git(cwd, ['rev-parse', '--git-path', 'hooks']);
48
+ if (!top || !hooksRel) {
49
+ result.skippedReason = 'not a git repository';
50
+ return result;
51
+ }
52
+ const hooksDir = path_1.default.resolve(cwd, hooksRel);
53
+ // Inside the working tree and not under .git → a versioned hooks dir (husky etc.). Compared
54
+ // on realpaths: git reports the toplevel resolved (/private/var/… on macOS) while hooksDir is
55
+ // built from the cwd as given (/var/…), and the two would otherwise look unrelated.
56
+ const real = (p) => { try {
57
+ return fs_1.default.realpathSync(p);
58
+ }
59
+ catch {
60
+ return p;
61
+ } };
62
+ const rel = path_1.default.relative(real(top), real(hooksDir));
63
+ const insideTree = !rel.startsWith('..') && !path_1.default.isAbsolute(rel);
64
+ const underGitDir = rel === '.git' || rel.startsWith(`.git${path_1.default.sep}`);
65
+ if (insideTree && !underGitDir) {
66
+ result.manual = {
67
+ hooksDir: rel,
68
+ snippet: Object.keys(HOOK_BODIES).map((h) => `# ${rel}/${h}\n${HOOK_BODIES[h]}`).join('\n\n'),
69
+ };
70
+ return result;
71
+ }
72
+ fs_1.default.mkdirSync(hooksDir, { recursive: true });
73
+ for (const hook of Object.keys(HOOK_BODIES)) {
74
+ const file = path_1.default.join(hooksDir, hook);
75
+ let current = null;
76
+ try {
77
+ current = fs_1.default.readFileSync(file, 'utf8');
78
+ }
79
+ catch { /* no hook yet */ }
80
+ let next;
81
+ if (current === null) {
82
+ next = `#!/bin/sh\n${block(hook)}`;
83
+ }
84
+ else {
85
+ const start = current.indexOf(BEGIN);
86
+ const end = current.indexOf(END);
87
+ if (start !== -1 && end > start) {
88
+ next = current.slice(0, start) + block(hook) + current.slice(end + END.length).replace(/^\n/, '');
89
+ }
90
+ else {
91
+ // Appended after the existing script. A hook that ends in `exit 0` would never reach
92
+ // it — insert before a trailing bare exit instead of after it.
93
+ const trailingExit = current.match(/\n(exit\s+\d+\s*)$/);
94
+ next = trailingExit
95
+ ? current.slice(0, trailingExit.index + 1) + block(hook) + trailingExit[1]
96
+ : `${current}${current.endsWith('\n') ? '' : '\n'}${block(hook)}`;
97
+ }
98
+ }
99
+ if (next === current) {
100
+ result.unchanged.push(hook);
101
+ continue;
102
+ }
103
+ fs_1.default.writeFileSync(file, next, 'utf8');
104
+ try {
105
+ fs_1.default.chmodSync(file, 0o755);
106
+ }
107
+ catch { /* Windows: git for Windows runs hooks via sh regardless */ }
108
+ (current === null ? result.installed : result.updated).push(hook);
109
+ }
110
+ return result;
111
+ }
112
+ //# sourceMappingURL=gitHooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitHooks.js","sourceRoot":"","sources":["../../../src/cli/gitHooks.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"lemma-proxy.d.ts","sourceRoot":"","sources":["../../../src/cli/lemma-proxy.ts"],"names":[],"mappings":";AA8cA,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA8DvF;AAqfD,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGtD;AAwrDD,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":";AA+cA,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CA8DvF;AAqfD,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGtD;AAmsDD,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"}
@@ -57,6 +57,7 @@ const ProjectRegistry_1 = require("../proxy/ProjectRegistry");
57
57
  const ContextInjector_1 = require("../proxy/ContextInjector");
58
58
  const SavingsLedger_1 = require("../utils/SavingsLedger");
59
59
  const ProjectStore_1 = require("../proxy/ProjectStore");
60
+ const gitHooks_1 = require("./gitHooks");
60
61
  const chroma = new chromadb_1.ChromaClient({ host: 'localhost', port: 8000 });
61
62
  const dummyEmbeddingFunction = { generate: (texts) => Promise.resolve([]) };
62
63
  const complexityRouter = new ComplexityRouter_1.default();
@@ -2503,6 +2504,7 @@ commander_1.program.command('init')
2503
2504
  .description('Initialize Lemma in the current project (auto-discovery)')
2504
2505
  .option('--no-configure', 'Skip automatic configuration of editors, shell overrides, and Claude Desktop')
2505
2506
  .option('--yes', 'Skip prompts and accept defaults')
2507
+ .option('--no-hooks', 'Skip installing the git hooks that feed the Brain (post-commit, post-merge, post-checkout)')
2506
2508
  .action(async (opts) => {
2507
2509
  const project = detectProject();
2508
2510
  console.log(`\n🛠️ Initializing Lemma for [${project}]...`);
@@ -2516,6 +2518,16 @@ commander_1.program.command('init')
2516
2518
  if (ensureGitIgnore()) {
2517
2519
  console.log('✅ Updated .gitignore with Lemma ignore rules');
2518
2520
  }
2521
+ if (opts.hooks !== false) {
2522
+ const hooks = (0, gitHooks_1.installBrainHooks)(process.cwd());
2523
+ const changed = [...hooks.installed, ...hooks.updated];
2524
+ if (hooks.manual) {
2525
+ console.log(`⚠️ Git hooks live in versioned ${hooks.manual.hooksDir}/ — not touching them. To feed the Brain, add:\n${hooks.manual.snippet}`);
2526
+ }
2527
+ else if (changed.length > 0) {
2528
+ console.log(`✅ Git hooks feed the Brain (${changed.join(', ')}): commits become memories, merges pull .lemma-brain/`);
2529
+ }
2530
+ }
2519
2531
  const envFile = path_1.default.join(process.cwd(), '.env');
2520
2532
  const lemmaConfig = `\n# Lemma AI Gateway Configuration\nOPENAI_BASE_URL=http://localhost:8081/v1\nLEMMA_PROJECT=${project}\n`;
2521
2533
  if (fs_1.default.existsSync(envFile)) {