lore-memory 0.3.0 → 0.4.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.
- package/README.md +54 -564
- package/bin/lore.js +22 -2
- package/package.json +1 -1
- package/src/commands/init.js +21 -4
- package/src/commands/onboard.js +3 -2
- package/src/commands/prompt.js +63 -0
- package/src/commands/search.js +8 -1
- package/src/commands/stale.js +1 -1
- package/src/lib/format.js +45 -2
- package/src/lib/relevance.js +3 -1
- package/src/mcp/tools/search.js +1 -2
- package/src/watcher/comments.js +13 -2
package/README.md
CHANGED
|
@@ -6,17 +6,17 @@ Persistent project memory for developers. Every architectural decision, invarian
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## The Problem
|
|
9
|
+
## 🚀 The Problem
|
|
10
10
|
|
|
11
11
|
Every AI coding session starts from zero. You re-explain why you chose JWT over sessions, why that Redis approach was abandoned, why the 200ms budget is a hard limit. That context lives in your head, not your codebase.
|
|
12
12
|
|
|
13
13
|
Without it, your AI assistant suggests things you've already rejected, removes workarounds that exist for real reasons, and retreads ground you've already covered.
|
|
14
14
|
|
|
15
|
-
Lore fixes that.
|
|
15
|
+
Lore fixes that by acting as the ultimate "glue" between developers and their AI tools.
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
## Install
|
|
19
|
+
## 📦 Install
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
npm install -g lore-memory
|
|
@@ -33,634 +33,124 @@ npm link # makes `lore` available globally
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
-
##
|
|
37
|
-
|
|
38
|
-
- [Quick Start](#quick-start)
|
|
39
|
-
- [Usage Guide](#usage-guide)
|
|
40
|
-
- [Day 1: Initialize and populate](#day-1-initialize-and-populate)
|
|
41
|
-
- [Day 2+: Passive capture with the watcher](#day-2-passive-capture-with-the-watcher)
|
|
42
|
-
- [Reviewing drafts](#reviewing-drafts)
|
|
43
|
-
- [Querying your knowledge base](#querying-your-knowledge-base)
|
|
44
|
-
- [Connecting to Claude Code](#connecting-to-claude-code)
|
|
45
|
-
- [Checking health](#checking-health)
|
|
46
|
-
- [Team workflows](#team-workflows)
|
|
47
|
-
- [Entry Types](#entry-types)
|
|
48
|
-
- [All Commands](#all-commands)
|
|
49
|
-
- [How It Works](#how-it-works)
|
|
50
|
-
- [MCP Tools Reference](#mcp-tools-reference)
|
|
51
|
-
- [Configuration](#configuration)
|
|
52
|
-
- [Semantic Search & Embeddings](#semantic-search--embeddings)
|
|
53
|
-
- [.lore/ Structure](#lore-structure)
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## Quick Start
|
|
36
|
+
## ⚡ Quick Start
|
|
58
37
|
|
|
59
38
|
```bash
|
|
60
39
|
cd your-project
|
|
61
40
|
lore init # sets up .lore/ and scans for comments
|
|
62
41
|
lore watch --daemon # starts passive capture in background
|
|
63
42
|
lore mine . # immediately scan all source files for comments
|
|
64
|
-
lore
|
|
65
|
-
lore
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
## Usage Guide
|
|
71
|
-
|
|
72
|
-
### Day 1: Initialize and populate
|
|
73
|
-
|
|
74
|
-
**1. Initialize**
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
cd your-project
|
|
78
|
-
lore init
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
This creates `.lore/` in your project root, installs a git post-commit hook, and immediately scans your codebase for comments worth capturing.
|
|
82
|
-
|
|
83
|
-
**2. Scan for existing knowledge**
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
lore mine .
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Lore walks every `.js`, `.ts`, `.py`, `.go`, `.rs` file looking for comments that contain signal phrases (`WARNING:`, `because`, `never`, `hack`, `we chose`, etc.). Each match becomes a draft for you to review.
|
|
90
|
-
|
|
91
|
-
```
|
|
92
|
-
📖 Mining comments in . ...
|
|
93
|
-
✓ Found 7 drafts — run: lore drafts
|
|
43
|
+
lore ui # view your knowledge base in a beautiful local browser dashboard
|
|
44
|
+
lore prompt "auth" # generate an AI system prompt based on your memory bank
|
|
94
45
|
```
|
|
95
46
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
lore drafts
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
For each draft Lore shows what it found and asks what you want to do:
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
Draft 1 of 7
|
|
106
|
-
────────────────────────────────
|
|
107
|
-
Type: invariant
|
|
108
|
-
Title: Never add synchronous calls to payment path
|
|
109
|
-
Evidence: // WARNING: never add synchronous calls here — 200ms SLA
|
|
110
|
-
Files: src/payments/processor.js
|
|
111
|
-
Confidence: 90%
|
|
112
|
-
|
|
113
|
-
[a] accept [e] edit [s] skip [d] delete [q] quit
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
- **Accept** — saves it as a real entry, updates the index, generates embedding
|
|
117
|
-
- **Edit** — opens a prompt to tweak the title/type/context before saving
|
|
118
|
-
- **Skip** — leaves it in the queue for later
|
|
119
|
-
- **Delete** — removes permanently
|
|
120
|
-
|
|
121
|
-
**4. Log entries manually**
|
|
122
|
-
|
|
123
|
-
For things you know right now, use `lore log`:
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
lore log
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Interactive prompts walk you through type → title → context → files → tags. Or go inline:
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
lore log \
|
|
133
|
-
--type decision \
|
|
134
|
-
--title "Use JWT over sessions" \
|
|
135
|
-
--context "API consumed by both mobile and web. Stateless auth was the only option that worked cleanly for both. Sessions require sticky routing which our k8s setup doesn't support." \
|
|
136
|
-
--files "src/auth/"
|
|
137
|
-
```
|
|
47
|
+
If you ever forget a command, just type `lore` to open the **Interactive Menu**!
|
|
138
48
|
|
|
139
49
|
---
|
|
140
50
|
|
|
141
|
-
|
|
51
|
+
## 🌟 Key Features
|
|
142
52
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
```
|
|
53
|
+
**1. The Local UI Dashboard (`lore ui`)**
|
|
54
|
+
Forget reading JSON files. Spin up a stunning, real-time local web server on port 3000 to search your project memory, view your Lore Score health, and read beautifully formatted Markdown entries.
|
|
146
55
|
|
|
147
|
-
|
|
56
|
+
**2. Zero-Shot "Context Compactor" (`lore prompt`)**
|
|
57
|
+
Run `lore prompt "Refactoring Auth"` and Lore instantly uses semantic vector search to compile a perfectly formatted, zero-shot system prompt containing the precise rules the LLM needs to know before it touches your code. Just pipe it to your clipboard (`lore prompt "..." | pbcopy`).
|
|
148
58
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
| Delete a file > 100 lines | Why did this exist? → graveyard draft |
|
|
152
|
-
| Edit the same file 5+ times in a week | This might be a footgun → gotcha draft |
|
|
153
|
-
| Commit with "replace", "migrate", "switch" | Decision draft |
|
|
154
|
-
| Commit with "never", "always", "must" | Invariant draft |
|
|
155
|
-
| Add or remove a dep in `package.json` | Decision or graveyard draft |
|
|
156
|
-
| Write `// WARNING:` or `// must never` | Invariant draft |
|
|
157
|
-
| Write `// because` or `// we chose` | Decision draft |
|
|
158
|
-
| Write `// HACK:` or `// workaround` | Gotcha draft |
|
|
59
|
+
**3. Automated Passive Mining (`lore watch` & `lore mine`)**
|
|
60
|
+
You don't have to stop coding to document. Lore passively scans your source code for specific comment patterns (like `// WARNING:`, `// HACK:`, or `// IMPORTANT:`) and automatically drafts Lore entries for you to review later via `lore drafts`.
|
|
159
61
|
|
|
160
|
-
|
|
62
|
+
**4. Built-in MCP Server (`lore serve`)**
|
|
63
|
+
Full integration with AI native editors (like Cursor) and CLI agents (like Claude Code) via the Model Context Protocol. The AI assistant can seamlessly query your project's memory bank *before* it starts writing code.
|
|
161
64
|
|
|
162
|
-
|
|
163
|
-
lore
|
|
164
|
-
```
|
|
65
|
+
**5. The "Architect-in-the-Loop" Git Hook**
|
|
66
|
+
During `lore init`, Lore installs a Git post-commit hook. If it detects a massive code change (e.g., >50 lines), it proactively intercepts the terminal and prompts the developer: *"Significant change detected. Do you want to log a Lore decision?"*
|
|
165
67
|
|
|
166
|
-
|
|
68
|
+
**6. Code-Linked Staleness Tracking (`lore stale`)**
|
|
69
|
+
Traditional wikis die because they go out of date. With Lore, you link memory entries to specific files (`src/api/auth.js`). If that file is modified in a future commit, Lore flags the entry as `[Stale]`, warning your team that the rule might need updating.
|
|
167
70
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**Check what the watcher captured:**
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
lore status # shows draft count alongside entry counts
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
```
|
|
179
|
-
📖 Lore Status
|
|
180
|
-
decisions: 4
|
|
181
|
-
invariants: 2
|
|
182
|
-
graveyard: 1
|
|
183
|
-
gotchas: 3
|
|
184
|
-
drafts: 5 pending — run: lore drafts
|
|
185
|
-
```
|
|
71
|
+
**7. Semantic Vector Search (`lore search`)**
|
|
72
|
+
Lore integrates locally with Ollama (`nomic-embed-text`) to generate offline vector embeddings for every entry. You can search by natural language concept, not just exact keyword matches.
|
|
186
73
|
|
|
187
74
|
---
|
|
188
75
|
|
|
189
|
-
|
|
76
|
+
## 🧠 Entry Types
|
|
190
77
|
|
|
191
|
-
|
|
192
|
-
lore drafts
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Drafts are sorted by confidence — highest first. Each shows the signal that triggered it and which file it came from. You approve or discard each one.
|
|
196
|
-
|
|
197
|
-
**Batch accept everything high-confidence:**
|
|
78
|
+
Lore categorizes engineering knowledge into four distinct, semantic types:
|
|
198
79
|
|
|
80
|
+
**1. Decision** — An architectural or technical choice with its rationale.
|
|
199
81
|
```bash
|
|
200
|
-
lore
|
|
82
|
+
lore log --type decision --title "Use Postgres over MongoDB" --context "We started with Mongo but our data is highly relational..."
|
|
201
83
|
```
|
|
202
84
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
**Edit an existing entry:**
|
|
206
|
-
|
|
85
|
+
**2. Invariant (🔴)** — A rule or constraint that must not be broken without deliberate review.
|
|
207
86
|
```bash
|
|
208
|
-
lore
|
|
87
|
+
lore log --type invariant --title "All auth tokens must be validated on every request" --context "Never cache auth results in memory..."
|
|
209
88
|
```
|
|
210
89
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
---
|
|
214
|
-
|
|
215
|
-
### Querying your knowledge base
|
|
216
|
-
|
|
217
|
-
**Why is this file the way it is?**
|
|
218
|
-
|
|
219
|
-
```bash
|
|
220
|
-
lore why src/payments/stripe.js
|
|
221
|
-
lore why src/auth/ # works on directories too
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
Returns entries weighted by how closely they relate to the file:
|
|
225
|
-
|
|
226
|
-
```
|
|
227
|
-
📖 src/payments/stripe.js
|
|
228
|
-
|
|
229
|
-
[INVARIANT] All payment calls must complete within 200ms
|
|
230
|
-
Never add synchronous external calls to this path.
|
|
231
|
-
Files: src/payments/
|
|
232
|
-
Score: 1.00
|
|
233
|
-
|
|
234
|
-
[GRAVEYARD] Tried Stripe webhooks with idempotency keys (2023)
|
|
235
|
-
Removed — DB transactions weren't atomic with the webhook handler.
|
|
236
|
-
Files: src/payments/stripe.js
|
|
237
|
-
Score: 1.00
|
|
238
|
-
|
|
239
|
-
[DECISION] Use polling over webhook push for payment status
|
|
240
|
-
Files: src/payments/ (imported by this file)
|
|
241
|
-
Score: 0.30
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
**Search by keyword:**
|
|
245
|
-
|
|
90
|
+
**3. Gotcha (⚠️)** — A non-obvious behavior, footgun, or thing that's bitten you.
|
|
246
91
|
```bash
|
|
247
|
-
lore
|
|
248
|
-
lore search "rate limit"
|
|
249
|
-
lore search "why not redis"
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
**See the dependency graph:**
|
|
253
|
-
|
|
254
|
-
```bash
|
|
255
|
-
lore graph --build # index all imports (run once, updates automatically)
|
|
256
|
-
lore graph src/api/server.js # what does this file import, and what imports it?
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
```
|
|
260
|
-
📖 src/api/server.js
|
|
261
|
-
|
|
262
|
-
Imports:
|
|
263
|
-
src/auth/middleware.js → 2 Lore entries
|
|
264
|
-
src/payments/processor.js → 1 Lore entry
|
|
265
|
-
src/lib/db.js → 0 Lore entries
|
|
266
|
-
|
|
267
|
-
Imported by:
|
|
268
|
-
src/index.js → 0 Lore entries
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
---
|
|
272
|
-
|
|
273
|
-
### Connecting to Claude Code
|
|
274
|
-
|
|
275
|
-
This is where Lore pays off the most. Add the MCP server to your Claude Code config:
|
|
276
|
-
|
|
277
|
-
**Global** (`~/.claude/settings.json`) — works in every project:
|
|
278
|
-
|
|
279
|
-
```json
|
|
280
|
-
{
|
|
281
|
-
"mcpServers": {
|
|
282
|
-
"lore": {
|
|
283
|
-
"command": "lore",
|
|
284
|
-
"args": ["serve"]
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
**Per-project** (`.claude/settings.json` in your repo) — only for this project:
|
|
291
|
-
|
|
292
|
-
```json
|
|
293
|
-
{
|
|
294
|
-
"mcpServers": {
|
|
295
|
-
"lore": {
|
|
296
|
-
"command": "lore",
|
|
297
|
-
"args": ["serve"]
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
92
|
+
lore log --type gotcha --title "Date.now() in test fixtures produces flaky tests" --context "Jest doesn't freeze time by default..."
|
|
301
93
|
```
|
|
302
94
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
**What Claude sees when you say "refactor src/payments/stripe.js":**
|
|
306
|
-
|
|
307
|
-
```
|
|
308
|
-
[INVARIANT] All payment calls must complete within 200ms
|
|
309
|
-
Never add synchronous external calls to this path.
|
|
310
|
-
|
|
311
|
-
[GRAVEYARD] Tried Stripe webhooks with idempotency keys
|
|
312
|
-
Removed — DB transactions weren't atomic. Current polling approach is intentional.
|
|
313
|
-
|
|
314
|
-
[GOTCHA] Stripe test clock doesn't advance automatically in CI
|
|
315
|
-
Call stripe.testHelpers.testClocks.advance() explicitly in test setup.
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
Claude now knows the constraints. It won't suggest adding an inline fraud-check API call. It won't suggest switching to webhooks. It won't write tests that silently break.
|
|
319
|
-
|
|
320
|
-
**Claude also surfaces your draft queue:**
|
|
321
|
-
|
|
322
|
-
If you have unreviewed drafts, Claude will mention it:
|
|
323
|
-
> *"You have 3 unreviewed Lore drafts — run `lore drafts` to review."*
|
|
324
|
-
|
|
325
|
-
**Export to CLAUDE.md (without MCP):**
|
|
326
|
-
|
|
327
|
-
If you prefer a static file over the MCP server:
|
|
328
|
-
|
|
95
|
+
**4. Graveyard (🪦)** — An approach that was tried and abandoned, with a record of why.
|
|
329
96
|
```bash
|
|
330
|
-
lore
|
|
97
|
+
lore log --type graveyard --title "Tried GraphQL for the public API" --context "Removed in v2 due to N+1 queries..."
|
|
331
98
|
```
|
|
332
99
|
|
|
333
|
-
Generates a `CLAUDE.md` at project root that Claude Code reads automatically at session start. Less dynamic than MCP (no per-file context injection) but requires no server.
|
|
334
|
-
|
|
335
100
|
---
|
|
336
101
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
**Lore Score:**
|
|
340
|
-
|
|
341
|
-
```bash
|
|
342
|
-
lore score
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
```
|
|
346
|
-
📖 Lore Score: 73/100 (Good)
|
|
347
|
-
────────────────────────────────
|
|
348
|
-
|
|
349
|
-
Coverage ████████░░ 80%
|
|
350
|
-
8 / 10 active modules documented
|
|
351
|
-
Highest risk unlogged: src/billing (12 commits this quarter)
|
|
352
|
-
|
|
353
|
-
Freshness ███████░░░ 70%
|
|
354
|
-
3 entries may be stale — run: lore stale
|
|
355
|
-
|
|
356
|
-
Depth ██████░░░░ 65%
|
|
357
|
-
14 entries across 8 modules
|
|
358
|
-
|
|
359
|
-
────────────────────────────────
|
|
360
|
-
Trend: 45 → 58 → 73 (improving)
|
|
361
|
-
Tip: Add invariants or gotchas to improve your depth score.
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
- **Coverage** — what fraction of your actively-changed modules have at least one entry. "Active" means > 5 commits in the last 90 days.
|
|
365
|
-
- **Freshness** — how recently entries were updated relative to their linked files. Stale entries drag this down.
|
|
366
|
-
- **Depth** — how many entries you have per active module. Invariants and gotchas count 1.5× because they're the most valuable.
|
|
367
|
-
|
|
368
|
-
**Stale entries:**
|
|
369
|
-
|
|
370
|
-
```bash
|
|
371
|
-
lore stale
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
```
|
|
375
|
-
⚠️ Stale entries (linked files changed since entry was written):
|
|
376
|
-
|
|
377
|
-
invariant-payment-timeout → src/payments/processor.js changed 4 days ago
|
|
378
|
-
⚠ External HTTP call added to a performance-critical path
|
|
379
|
-
Review with: lore edit invariant-payment-timeout
|
|
380
|
-
|
|
381
|
-
decision-use-polling → src/payments/stripe.js changed 12 days ago
|
|
382
|
-
Review with: lore edit decision-use-polling
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
Stale detection combines mtime (file changed after entry was written) with pattern analysis — e.g. if you added a `fetch()` call to a file covered by a 200ms invariant, Lore flags it specifically.
|
|
386
|
-
|
|
387
|
-
---
|
|
388
|
-
|
|
389
|
-
### Team workflows
|
|
390
|
-
|
|
391
|
-
Lore entries live in `.lore/decisions/`, `.lore/invariants/`, etc. as plain JSON files. Commit them like any other source file.
|
|
392
|
-
|
|
393
|
-
**Recommended `.gitignore` additions** (already set up by `lore init`):
|
|
394
|
-
|
|
395
|
-
```
|
|
396
|
-
.lore/drafts/ # personal draft queue, not shared
|
|
397
|
-
.lore/graph.json # rebuilt automatically
|
|
398
|
-
.lore/score.json # personal history
|
|
399
|
-
.lore/watch-state.json
|
|
400
|
-
.lore/watcher.pid
|
|
401
|
-
.lore/watcher.log
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
**Onboarding a new team member:**
|
|
405
|
-
|
|
406
|
-
```bash
|
|
407
|
-
lore onboard
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
Prints a brief covering recent decisions, active invariants, and anything flagged stale. Useful after a long weekend too:
|
|
411
|
-
|
|
412
|
-
```bash
|
|
413
|
-
lore onboard --days 7 # pretend you've been away 7 days
|
|
414
|
-
```
|
|
415
|
-
|
|
416
|
-
**When you join a repo that already has Lore:**
|
|
417
|
-
|
|
418
|
-
```bash
|
|
419
|
-
git pull
|
|
420
|
-
lore status # see what's there
|
|
421
|
-
lore onboard # get the brief
|
|
422
|
-
lore why src/ # see what the whole src/ directory remembers
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
---
|
|
426
|
-
|
|
427
|
-
## Entry Types
|
|
428
|
-
|
|
429
|
-
**decision** — An architectural or technical choice with its rationale.
|
|
430
|
-
|
|
431
|
-
```bash
|
|
432
|
-
lore log --type decision \
|
|
433
|
-
--title "Use Postgres over MongoDB" \
|
|
434
|
-
--context "We started with Mongo but our data is highly relational. Join queries were getting complex. Migrated to Postgres in Q3 2023." \
|
|
435
|
-
--files "src/db/"
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
**invariant** — A rule or constraint that must not be broken without deliberate review.
|
|
439
|
-
|
|
440
|
-
```bash
|
|
441
|
-
lore log --type invariant \
|
|
442
|
-
--title "All auth tokens must be validated on every request" \
|
|
443
|
-
--context "Never cache auth results in memory. Token revocation must take effect immediately. This burned us in a security audit." \
|
|
444
|
-
--files "src/auth/middleware.js"
|
|
445
|
-
```
|
|
446
|
-
|
|
447
|
-
**gotcha** — A non-obvious behavior, footgun, or thing that's bitten you.
|
|
448
|
-
|
|
449
|
-
```bash
|
|
450
|
-
lore log --type gotcha \
|
|
451
|
-
--title "Date.now() in test fixtures produces flaky tests" \
|
|
452
|
-
--context "Jest doesn't freeze time by default. Use jest.useFakeTimers() or pass timestamps explicitly. Spent 2 days on this." \
|
|
453
|
-
--files "src/__tests__/"
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
**graveyard** — An approach that was tried and abandoned, with a record of why.
|
|
457
|
-
|
|
458
|
-
```bash
|
|
459
|
-
lore log --type graveyard \
|
|
460
|
-
--title "Tried GraphQL for the public API" \
|
|
461
|
-
--context "Removed in v2. N+1 queries in the default resolver were hammering the DB. REST with explicit eager-loading is intentional." \
|
|
462
|
-
--files "src/api/"
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
---
|
|
466
|
-
|
|
467
|
-
## All Commands
|
|
468
|
-
|
|
469
|
-
### Setup & capture
|
|
102
|
+
## 🛠️ All Commands
|
|
470
103
|
|
|
104
|
+
### Setup & Capture
|
|
471
105
|
| Command | Description |
|
|
472
106
|
|---|---|
|
|
473
107
|
| `lore init` | Initialize `.lore/` in the current repo, install git hook |
|
|
474
108
|
| `lore log` | Log an entry interactively |
|
|
475
|
-
| `lore log --type <t> --title <t> --context <c> --files <f>` | Log inline, no prompts |
|
|
476
109
|
| `lore mine [path]` | Scan a file or directory for lore-worthy comments |
|
|
477
|
-
| `lore mine .` | Scan the entire project |
|
|
478
110
|
| `lore watch` | Start the file watcher in the foreground |
|
|
479
|
-
| `lore watch --daemon` | Start the file watcher in the background |
|
|
480
|
-
| `lore watch --stop` | Stop the background watcher |
|
|
481
|
-
|
|
482
|
-
### Review & edit
|
|
483
111
|
|
|
112
|
+
### Review & Edit
|
|
484
113
|
| Command | Description |
|
|
485
114
|
|---|---|
|
|
115
|
+
| `lore ui` | Launch a local web dashboard to view your memory bank |
|
|
486
116
|
| `lore drafts` | Review pending auto-captured drafts interactively |
|
|
487
|
-
| `lore drafts --auto` | Accept all drafts with ≥ 80% confidence silently |
|
|
488
117
|
| `lore edit <id>` | Open an entry in your editor |
|
|
489
118
|
|
|
490
|
-
### Query
|
|
491
|
-
|
|
119
|
+
### Query & Export
|
|
492
120
|
| Command | Description |
|
|
493
121
|
|---|---|
|
|
122
|
+
| `lore prompt <query>` | Generate a perfectly formatted LLM context prompt from project memory |
|
|
494
123
|
| `lore why <file>` | Show all entries relevant to a file or directory |
|
|
495
124
|
| `lore search <query>` | Search entries by keyword (semantic if Ollama running) |
|
|
496
125
|
| `lore graph` | Show dependency graph stats |
|
|
497
|
-
| `lore
|
|
498
|
-
| `lore graph --build` | Build or rebuild the full dependency graph |
|
|
499
|
-
|
|
500
|
-
### Health & export
|
|
126
|
+
| `lore export` | Generate `CLAUDE.md` at project root |
|
|
501
127
|
|
|
128
|
+
### Health & Onboarding
|
|
502
129
|
| Command | Description |
|
|
503
130
|
|---|---|
|
|
504
131
|
| `lore status` | Entry counts, draft count, stale summary |
|
|
505
132
|
| `lore stale` | Full stale report with semantic pattern analysis |
|
|
506
133
|
| `lore score` | Lore Score (0–100): coverage, freshness, depth |
|
|
507
|
-
| `lore export` | Generate `CLAUDE.md` at project root |
|
|
508
134
|
| `lore onboard` | Print re-onboarding brief |
|
|
509
|
-
| `lore onboard --days <n>` | Simulate returning after N days |
|
|
510
|
-
|
|
511
|
-
### AI integration
|
|
512
|
-
|
|
513
|
-
| Command | Description |
|
|
514
|
-
|---|---|
|
|
515
|
-
| `lore serve` | Start the MCP server (used by Claude Code, called automatically) |
|
|
516
|
-
| `lore embed` | Generate semantic embeddings for all entries via Ollama |
|
|
517
|
-
|
|
518
|
-
---
|
|
519
|
-
|
|
520
|
-
## How It Works
|
|
521
|
-
|
|
522
|
-
### Comment mining
|
|
523
|
-
|
|
524
|
-
Lore uses the Babel AST parser for JS/TS files and regex for Python/Go/Rust. It scores each comment on:
|
|
525
|
-
|
|
526
|
-
- Does it contain a signal phrase? (`must`, `never`, `warning`, `hack`, `because`, `we chose`, `tried`, etc.)
|
|
527
|
-
- Is it longer than 20 characters?
|
|
528
|
-
- Is it more than a generic `TODO` or `FIXME`?
|
|
529
|
-
|
|
530
|
-
Comments scoring above the threshold become drafts with a suggested type and a title extracted from the first meaningful words.
|
|
531
|
-
|
|
532
|
-
### Graph-weighted context
|
|
533
|
-
|
|
534
|
-
When you run `lore why src/api/payments.js`, entries are ranked by how closely they relate:
|
|
535
|
-
|
|
536
|
-
| Relationship | Weight |
|
|
537
|
-
|---|---|
|
|
538
|
-
| Entry directly linked to this file | 1.0 |
|
|
539
|
-
| Entry linked to a parent directory | 0.7 |
|
|
540
|
-
| Entry for a file this file imports | 0.3 |
|
|
541
|
-
| Entry for a file that imports this file | 0.2 |
|
|
542
|
-
|
|
543
|
-
This means the context you get isn't just "entries tagged to this file" — it's the full blast radius of relevant knowledge, ranked by relevance.
|
|
544
|
-
|
|
545
|
-
### Staleness detection
|
|
546
|
-
|
|
547
|
-
Every entry records which files it references. When those files change:
|
|
548
135
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
When connected via MCP, Claude Code has access to these tools:
|
|
559
|
-
|
|
560
|
-
| Tool | Input | Returns |
|
|
561
|
-
|---|---|---|
|
|
562
|
-
| `lore_why` | `filepath` | All relevant entries for that file, graph-weighted and token-budgeted |
|
|
563
|
-
| `lore_overview` | — | Entry counts, Lore Score, draft count, highest-risk unlogged module |
|
|
564
|
-
| `lore_stale` | — | Stale entries with mtime and semantic pattern reasons |
|
|
565
|
-
| `lore_drafts` | — | Pending draft count and summary |
|
|
566
|
-
| `lore_search` | `query` | Matching entries |
|
|
567
|
-
| `lore_log` | `type`, `title`, `context`, `files` | Confirmation after saving |
|
|
568
|
-
|
|
569
|
-
The token budget (default 4000, configurable) ensures Lore never floods Claude's context window. If entries exceed the budget, the lowest-scored ones are summarized to a single line.
|
|
570
|
-
|
|
571
|
-
---
|
|
572
|
-
|
|
573
|
-
## Configuration
|
|
574
|
-
|
|
575
|
-
`.lore/config.yaml` is created by `lore init` with sensible defaults. Edit it to tune behaviour:
|
|
576
|
-
|
|
577
|
-
```yaml
|
|
578
|
-
# Entry types to enable
|
|
579
|
-
types:
|
|
580
|
-
- decision
|
|
581
|
-
- invariant
|
|
582
|
-
- graveyard
|
|
583
|
-
- gotcha
|
|
584
|
-
|
|
585
|
-
# Directories the watcher ignores
|
|
586
|
-
watchIgnore:
|
|
587
|
-
- node_modules
|
|
588
|
-
- dist
|
|
589
|
-
- .git
|
|
590
|
-
- coverage
|
|
591
|
-
|
|
592
|
-
# Comment phrases that trigger draft creation
|
|
593
|
-
commentPatterns:
|
|
594
|
-
- must
|
|
595
|
-
- never
|
|
596
|
-
- warning
|
|
597
|
-
- hack
|
|
598
|
-
- because
|
|
599
|
-
- we chose
|
|
600
|
-
- tried
|
|
601
|
-
|
|
602
|
-
# Minimum confidence for --auto mode in lore drafts
|
|
603
|
-
signalThreshold: 0.8
|
|
604
|
-
|
|
605
|
-
# Pattern-based semantic staleness (no Ollama needed)
|
|
606
|
-
semanticStaleness: true
|
|
607
|
-
|
|
608
|
-
# Lore Score component weights (must sum to 1.0)
|
|
609
|
-
scoringWeights:
|
|
610
|
-
coverage: 0.4
|
|
611
|
-
freshness: 0.35
|
|
612
|
-
depth: 0.25
|
|
613
|
-
|
|
614
|
-
# Max tokens Lore injects into Claude's context per request
|
|
615
|
-
mcp:
|
|
616
|
-
tokenBudget: 4000
|
|
617
|
-
```
|
|
618
|
-
|
|
619
|
-
---
|
|
620
|
-
|
|
621
|
-
## Semantic Search & Embeddings
|
|
622
|
-
|
|
623
|
-
Optional. Requires [Ollama](https://ollama.com) running locally.
|
|
624
|
-
|
|
625
|
-
```bash
|
|
626
|
-
ollama pull nomic-embed-text
|
|
627
|
-
lore embed
|
|
136
|
+
### AI Integration (MCP)
|
|
137
|
+
Add the MCP server to your Claude Code config (`~/.claude/settings.json`):
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"mcpServers": {
|
|
141
|
+
"lore": { "command": "lore", "args": ["serve"] }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
628
144
|
```
|
|
629
145
|
|
|
630
|
-
What this enables:
|
|
631
|
-
- `lore search "why not redis"` finds entries that are semantically related, not just keyword matches
|
|
632
|
-
- `lore stale` adds similarity-based staleness (diff vs entry embedding)
|
|
633
|
-
- MCP `lore_why` ranks entries partly by semantic relevance
|
|
634
|
-
|
|
635
|
-
Lore works fully without this. All core features — capture, review, why, stale, score, MCP — work with no Ollama dependency.
|
|
636
|
-
|
|
637
146
|
---
|
|
638
147
|
|
|
639
|
-
##
|
|
148
|
+
## 🔒 Privacy & Data
|
|
640
149
|
|
|
641
|
-
|
|
642
|
-
.
|
|
643
|
-
|
|
644
|
-
invariants/ ← committed to git
|
|
645
|
-
graveyard/ ← committed to git
|
|
646
|
-
gotchas/ ← committed to git
|
|
647
|
-
modules/ ← committed to git
|
|
648
|
-
index.json ← committed to git
|
|
649
|
-
config.yaml ← committed to git
|
|
650
|
-
|
|
651
|
-
drafts/ ← gitignored (personal review queue)
|
|
652
|
-
graph.json ← gitignored (rebuilt automatically)
|
|
653
|
-
score.json ← gitignored (personal history)
|
|
654
|
-
watch-state.json ← gitignored (watcher bookkeeping)
|
|
655
|
-
watcher.pid ← gitignored (daemon PID)
|
|
656
|
-
watcher.log ← gitignored (daemon logs)
|
|
657
|
-
embeddings.db ← gitignored (SQLite, local only)
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
Entries are plain JSON. You can read, edit, or delete them directly. The index is rebuilt on next `lore status` or `lore why` if it drifts.
|
|
661
|
-
|
|
662
|
-
---
|
|
150
|
+
- **100% Free & Open Source:** No subscriptions.
|
|
151
|
+
- **Privacy-First (No Cloud API calls):** All semantic embeddings happen on your local machine via Ollama. No proprietary source code is ever sent to a third-party server.
|
|
152
|
+
- **Git Native:** All entries are stored as plain JSON in `.lore/` at the root of your project. They are committed alongside your codebase, so your team automatically shares the brain.
|
|
663
153
|
|
|
664
|
-
## Stack
|
|
154
|
+
## 🏗️ Stack
|
|
665
155
|
|
|
666
|
-
Node.js · CommonJS · Commander · Inquirer · Chalk · Chokidar · Babel Parser ·
|
|
156
|
+
Node.js · CommonJS · Commander · Inquirer · Express · Tailwind CSS · Chalk · Chokidar · Babel Parser · js-yaml · better-sqlite3 · Ollama (optional) · MCP SDK
|
package/bin/lore.js
CHANGED
|
@@ -8,7 +8,7 @@ const program = new Command();
|
|
|
8
8
|
program
|
|
9
9
|
.name('lore')
|
|
10
10
|
.description('Persistent project memory for developers')
|
|
11
|
-
.version('
|
|
11
|
+
.version(require('../package.json').version)
|
|
12
12
|
.action(async () => {
|
|
13
13
|
// Only launch the interactive menu if strictly NO arguments were provided
|
|
14
14
|
if (process.argv.length !== 2) return;
|
|
@@ -39,6 +39,7 @@ program
|
|
|
39
39
|
{ name: '👀 Review pending drafts (lore drafts)', value: 'drafts' },
|
|
40
40
|
{ name: '📊 View project health (lore score)', value: 'score' },
|
|
41
41
|
{ name: '🔍 Search knowledge base (lore search)', value: 'search' },
|
|
42
|
+
{ name: '🪄 Generate AI Prompt (lore prompt)', value: 'prompt' },
|
|
42
43
|
{ name: '🌐 Open Local Web Dashboard (lore ui)', value: 'ui' },
|
|
43
44
|
{ name: '⚙️ Start background watcher (lore watch --daemon)', value: 'watch --daemon' },
|
|
44
45
|
new inquirer.Separator(),
|
|
@@ -52,6 +53,18 @@ program
|
|
|
52
53
|
process.exit(0);
|
|
53
54
|
} else if (action === 'help') {
|
|
54
55
|
program.outputHelp();
|
|
56
|
+
} else if (action === 'prompt') {
|
|
57
|
+
const { query } = await inquirer.prompt([{
|
|
58
|
+
type: 'input',
|
|
59
|
+
name: 'query',
|
|
60
|
+
message: 'What are you trying to build or refactor? (e.g. "Add a login page")'
|
|
61
|
+
}]);
|
|
62
|
+
console.log();
|
|
63
|
+
if (query.trim()) {
|
|
64
|
+
try {
|
|
65
|
+
execSync(`node ${__filename} prompt "${query.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
|
|
66
|
+
} catch (e) { }
|
|
67
|
+
}
|
|
55
68
|
} else {
|
|
56
69
|
console.log();
|
|
57
70
|
try {
|
|
@@ -135,7 +148,7 @@ program
|
|
|
135
148
|
.action(require('../src/commands/stale'));
|
|
136
149
|
|
|
137
150
|
program
|
|
138
|
-
.command('search
|
|
151
|
+
.command('search [query...]')
|
|
139
152
|
.description('Search all entries for a keyword')
|
|
140
153
|
.action(require('../src/commands/search'));
|
|
141
154
|
|
|
@@ -202,4 +215,11 @@ program
|
|
|
202
215
|
.option('-p, --port <port>', 'Port to run the UI server on', '3333')
|
|
203
216
|
.action(require('../src/commands/ui'));
|
|
204
217
|
|
|
218
|
+
program
|
|
219
|
+
.command('prompt [query...]')
|
|
220
|
+
.description('Generate a perfectly formatted LLM context prompt from project memory')
|
|
221
|
+
.option('-t, --threshold <number>', 'Relevance threshold (0.0 to 1.0)', '0.4')
|
|
222
|
+
.option('-l, --limit <number>', 'Max number of entries to include', '10')
|
|
223
|
+
.action(require('../src/commands/prompt'));
|
|
224
|
+
|
|
205
225
|
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lore-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Persistent project memory for developers. Captures decisions, invariants, gotchas, and graveyard entries — automatically and manually — and injects them into AI coding sessions.",
|
|
5
5
|
"main": "bin/lore.js",
|
|
6
6
|
"bin": {
|
package/src/commands/init.js
CHANGED
|
@@ -67,9 +67,26 @@ async function init() {
|
|
|
67
67
|
const hookDir = path.join('.git', 'hooks');
|
|
68
68
|
if (fs.existsSync(hookDir)) {
|
|
69
69
|
const hookPath = path.join(hookDir, 'post-commit');
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
let writeHook = true;
|
|
71
|
+
|
|
72
|
+
if (fs.existsSync(hookPath)) {
|
|
73
|
+
const existingHook = await fs.readFile(hookPath, 'utf8');
|
|
74
|
+
if (existingHook.includes('Lore: Significant change detected')) {
|
|
75
|
+
writeHook = false;
|
|
76
|
+
console.log(chalk.dim('✓ Git post-commit hook already installed'));
|
|
77
|
+
} else {
|
|
78
|
+
// Append to existing hook
|
|
79
|
+
await fs.appendFile(hookPath, '\n' + HOOK_CONTENT.replace('#!/bin/bash\n', ''));
|
|
80
|
+
console.log(chalk.green('✓ Lore appended to existing Git post-commit hook'));
|
|
81
|
+
writeHook = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (writeHook) {
|
|
86
|
+
await fs.writeFile(hookPath, HOOK_CONTENT);
|
|
87
|
+
await fs.chmod(hookPath, '0755');
|
|
88
|
+
console.log(chalk.green('✓ Git post-commit hook installed'));
|
|
89
|
+
}
|
|
73
90
|
} else {
|
|
74
91
|
console.log(chalk.yellow('⚠ Not a git repo — hook not installed'));
|
|
75
92
|
}
|
|
@@ -95,7 +112,7 @@ async function init() {
|
|
|
95
112
|
console.log(chalk.dim('\n Scanning codebase for lore-worthy comments...'));
|
|
96
113
|
try {
|
|
97
114
|
const { mineDirectory } = require('../watcher/comments');
|
|
98
|
-
const count = mineDirectory(process.cwd(), process.cwd());
|
|
115
|
+
const count = await mineDirectory(process.cwd(), process.cwd());
|
|
99
116
|
if (count > 0) {
|
|
100
117
|
console.log(chalk.cyan(` Found ${count} draft entr${count === 1 ? 'y' : 'ies'} — run: lore drafts`));
|
|
101
118
|
}
|
package/src/commands/onboard.js
CHANGED
|
@@ -58,8 +58,9 @@ function onboard(options) {
|
|
|
58
58
|
for (const e of byType.decision.slice(0, 5)) {
|
|
59
59
|
console.log(chalk.white(` • ${e.title}`));
|
|
60
60
|
if (e.context) {
|
|
61
|
-
const
|
|
62
|
-
|
|
61
|
+
const firstLine = e.context.split('\n')[0];
|
|
62
|
+
const summary = firstLine.slice(0, 80);
|
|
63
|
+
console.log(chalk.dim(` ${summary}${firstLine.length > 80 ? '…' : ''}`));
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
if (byType.decision.length > 5) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { requireInit } = require('../lib/guard');
|
|
5
|
+
const { findSimilar } = require('../lib/embeddings');
|
|
6
|
+
const { readIndex } = require('../lib/index');
|
|
7
|
+
const { readEntry } = require('../lib/entries');
|
|
8
|
+
const { formatPromptContext } = require('../lib/format');
|
|
9
|
+
|
|
10
|
+
async function promptCmd(queryArgs, options) {
|
|
11
|
+
requireInit();
|
|
12
|
+
|
|
13
|
+
const queryInfo = Array.isArray(queryArgs) ? queryArgs.join(' ') : (queryArgs || '');
|
|
14
|
+
|
|
15
|
+
if (!queryInfo || queryInfo.trim().length === 0) {
|
|
16
|
+
console.error(chalk.red('\nError: You must provide a query to generate a prompt.'));
|
|
17
|
+
console.log(chalk.yellow('Example: lore prompt "I want to refactor the database"'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const query = queryInfo;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const threshold = parseFloat(options.threshold) || 0.4;
|
|
25
|
+
const limit = parseInt(options.limit, 10) || 10;
|
|
26
|
+
|
|
27
|
+
const index = readIndex();
|
|
28
|
+
const allIds = Object.keys(index.entries);
|
|
29
|
+
|
|
30
|
+
let results = [];
|
|
31
|
+
try {
|
|
32
|
+
const similar = await findSimilar(query, allIds, limit);
|
|
33
|
+
for (const { id, score } of similar) {
|
|
34
|
+
if (score >= threshold) {
|
|
35
|
+
const entry = readEntry(index.entries[id]);
|
|
36
|
+
if (entry) results.push(entry);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error(chalk.yellow(`⚠️ Warning: Semantic search failed (${e.message}). Ensure Ollama is running.\nFallback text search will be used.`));
|
|
41
|
+
// Basic text fallback
|
|
42
|
+
const q = query.toLowerCase();
|
|
43
|
+
for (const id of allIds) {
|
|
44
|
+
const entry = readEntry(index.entries[id]);
|
|
45
|
+
if (!entry) continue;
|
|
46
|
+
const searchable = [entry.title, entry.context].join(' ').toLowerCase();
|
|
47
|
+
if (searchable.includes(q)) results.push(entry);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The exact LLM-ready markdown string
|
|
52
|
+
const markdownPrompt = formatPromptContext(query, results);
|
|
53
|
+
|
|
54
|
+
// Print strictly to stdout without extra CLI fluff so it can be piped seamlessly
|
|
55
|
+
console.log(markdownPrompt);
|
|
56
|
+
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error(chalk.red(`\nFailed to generate prompt: ${e.message}\n`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = promptCmd;
|
package/src/commands/search.js
CHANGED
|
@@ -6,9 +6,16 @@ const { readEntry } = require('../lib/entries');
|
|
|
6
6
|
const { printEntry } = require('../lib/format');
|
|
7
7
|
const { requireInit } = require('../lib/guard');
|
|
8
8
|
|
|
9
|
-
function search(
|
|
9
|
+
function search(queryArgs) {
|
|
10
10
|
requireInit();
|
|
11
11
|
try {
|
|
12
|
+
const query = Array.isArray(queryArgs) ? queryArgs.join(' ') : (queryArgs || '');
|
|
13
|
+
|
|
14
|
+
if (!query || query.trim().length === 0) {
|
|
15
|
+
console.error(chalk.red('\nError: You must provide a search query.'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
const index = readIndex();
|
|
13
20
|
const q = query.toLowerCase();
|
|
14
21
|
const matches = [];
|
package/src/commands/stale.js
CHANGED
|
@@ -12,7 +12,7 @@ function stale() {
|
|
|
12
12
|
const index = readIndex();
|
|
13
13
|
let found = false;
|
|
14
14
|
|
|
15
|
-
for (const
|
|
15
|
+
for (const entryPath of Object.values(index.entries)) {
|
|
16
16
|
const entry = readEntry(entryPath);
|
|
17
17
|
if (!entry) continue;
|
|
18
18
|
|
package/src/lib/format.js
CHANGED
|
@@ -41,7 +41,7 @@ function printEntry(entry) {
|
|
|
41
41
|
content += `${titleColor(typeLabel)} ${titleColor(entry.title)} ${chalk.gray('(' + entry.date + ')')}\n\n`;
|
|
42
42
|
|
|
43
43
|
// Wrap context text roughly
|
|
44
|
-
const contextWords = entry.context.split(' ');
|
|
44
|
+
const contextWords = (entry.context || '').split(' ');
|
|
45
45
|
let currentLine = '';
|
|
46
46
|
for (const word of contextWords) {
|
|
47
47
|
if (currentLine.length + word.length > 70) {
|
|
@@ -71,4 +71,47 @@ function printEntry(entry) {
|
|
|
71
71
|
drawBox(content.trim(), colorFn, entry.id);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
function formatPromptContext(query, entries) {
|
|
75
|
+
if (!entries || entries.length === 0) {
|
|
76
|
+
return `# Project Context: Lore\nThe user is asking about: "${query}"\n\nNo specific historical rules or decisions were found in the project memory for this topic.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const invariants = entries.filter(e => e.type === 'invariant');
|
|
80
|
+
const gotchas = entries.filter(e => e.type === 'gotcha');
|
|
81
|
+
const decisions = entries.filter(e => e.type === 'decision');
|
|
82
|
+
const graveyard = entries.filter(e => e.type === 'graveyard');
|
|
83
|
+
|
|
84
|
+
let output = `# Project Context: Lore\nThe user is asking: "${query}"\n\nPlease adhere strictly to the following project architectural rules extracted from project memory:\n`;
|
|
85
|
+
|
|
86
|
+
if (invariants.length > 0) {
|
|
87
|
+
output += `\n## Invariants (CRITICAL: DO NOT BREAK)\n`;
|
|
88
|
+
for (const e of invariants) {
|
|
89
|
+
output += `- [${e.id}]: ${(e.context || '').trim().replace(/\n/g, ' ')}\n`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (gotchas.length > 0) {
|
|
94
|
+
output += `\n## Gotchas (WARNING: KNOWN TRAPS)\n`;
|
|
95
|
+
for (const e of gotchas) {
|
|
96
|
+
output += `- [${e.id}]: ${(e.context || '').trim().replace(/\n/g, ' ')}\n`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (decisions.length > 0) {
|
|
101
|
+
output += `\n## Relevant Decisions\n`;
|
|
102
|
+
for (const e of decisions) {
|
|
103
|
+
output += `- [${e.id}]: ${e.title} - ${(e.context || '').trim().replace(/\n/g, ' ')}\n`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (graveyard.length > 0) {
|
|
108
|
+
output += `\n## Graveyard (DO NOT SUGGEST OR USE)\n`;
|
|
109
|
+
for (const e of graveyard) {
|
|
110
|
+
output += `- [${e.id}]: ${(e.context || '').trim().replace(/\n/g, ' ')}\n`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return output.trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { printEntry, formatPromptContext };
|
package/src/lib/relevance.js
CHANGED
|
@@ -21,19 +21,21 @@ function scoreEntry(entry, context) {
|
|
|
21
21
|
let score = 0;
|
|
22
22
|
|
|
23
23
|
// Direct file match
|
|
24
|
+
let exactMatchFound = false;
|
|
24
25
|
if (context.filepath && entry.files && entry.files.length > 0) {
|
|
25
26
|
const normalizedQuery = context.filepath.replace(/^\.\//, '');
|
|
26
27
|
for (const f of entry.files) {
|
|
27
28
|
const normalizedFile = f.replace(/^\.\//, '');
|
|
28
29
|
if (normalizedFile === normalizedQuery) {
|
|
29
30
|
score += WEIGHTS.directFileMatch;
|
|
31
|
+
exactMatchFound = true;
|
|
30
32
|
break;
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// Parent dir match
|
|
36
|
-
if (context.filepath && entry.files && entry.files.length > 0) {
|
|
38
|
+
if (!exactMatchFound && context.filepath && entry.files && entry.files.length > 0) {
|
|
37
39
|
const queryDir = path.dirname(context.filepath.replace(/^\.\//, ''));
|
|
38
40
|
for (const f of entry.files) {
|
|
39
41
|
const fileDir = path.dirname(f.replace(/^\.\//, ''));
|
package/src/mcp/tools/search.js
CHANGED
|
@@ -89,11 +89,10 @@ async function handler(args) {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
const formatted = matches.map(e => formatEntry(e)).join('\n\n---\n\n');
|
|
93
92
|
const budgeted = enforceBudget(matches, budget);
|
|
94
93
|
|
|
95
94
|
return {
|
|
96
|
-
content: [{ type: 'text', text: budgeted ||
|
|
95
|
+
content: [{ type: 'text', text: budgeted || `Matches found, but they exceed the ${budget} token budget.` }],
|
|
97
96
|
};
|
|
98
97
|
} catch (e) {
|
|
99
98
|
return {
|
package/src/watcher/comments.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { detectType, extractTitle, scoreComment } = require('../lib/nlp');
|
|
6
|
-
const { saveDraft } = require('../lib/drafts');
|
|
6
|
+
const { saveDraft, listDrafts } = require('../lib/drafts');
|
|
7
|
+
const { readIndex } = require('../lib/index');
|
|
8
|
+
const { readEntry } = require('../lib/entries');
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Extract raw comment strings from source code.
|
|
@@ -57,7 +59,11 @@ async function mineFile(absFilePath, projectRoot) {
|
|
|
57
59
|
const comments = extractComments(code, absFilePath);
|
|
58
60
|
const created = [];
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
const existingDrafts = listDrafts();
|
|
63
|
+
const index = readIndex();
|
|
64
|
+
const existingEntries = Object.values(index.entries).map(p => readEntry(p)).filter(Boolean);
|
|
65
|
+
|
|
66
|
+
// Deduplicate: skip if we have a recent draft or entry from same file with same title
|
|
61
67
|
for (const comment of comments) {
|
|
62
68
|
const score = scoreComment(comment);
|
|
63
69
|
if (score < 0.5) continue;
|
|
@@ -66,6 +72,11 @@ async function mineFile(absFilePath, projectRoot) {
|
|
|
66
72
|
const title = extractTitle(comment);
|
|
67
73
|
if (!title || title.length < 3) continue;
|
|
68
74
|
|
|
75
|
+
const isDuplicateDraft = existingDrafts.some(d => d.suggestedTitle === title && (d.files || []).includes(relativePath));
|
|
76
|
+
const isDuplicateEntry = existingEntries.some(e => e.title === title && (e.files || []).includes(relativePath));
|
|
77
|
+
|
|
78
|
+
if (isDuplicateDraft || isDuplicateEntry) continue;
|
|
79
|
+
|
|
69
80
|
const draft = {
|
|
70
81
|
draftId: `draft-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
71
82
|
suggestedType: type,
|