lore-memory 0.1.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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +666 -0
  3. package/bin/lore.js +108 -0
  4. package/package.json +53 -0
  5. package/src/commands/drafts.js +144 -0
  6. package/src/commands/edit.js +30 -0
  7. package/src/commands/embed.js +63 -0
  8. package/src/commands/export.js +76 -0
  9. package/src/commands/graph.js +80 -0
  10. package/src/commands/init.js +110 -0
  11. package/src/commands/log.js +149 -0
  12. package/src/commands/mine.js +38 -0
  13. package/src/commands/onboard.js +112 -0
  14. package/src/commands/score.js +88 -0
  15. package/src/commands/search.js +49 -0
  16. package/src/commands/serve.js +21 -0
  17. package/src/commands/stale.js +41 -0
  18. package/src/commands/status.js +59 -0
  19. package/src/commands/watch.js +67 -0
  20. package/src/commands/why.js +58 -0
  21. package/src/lib/budget.js +57 -0
  22. package/src/lib/config.js +52 -0
  23. package/src/lib/drafts.js +104 -0
  24. package/src/lib/embeddings.js +97 -0
  25. package/src/lib/entries.js +59 -0
  26. package/src/lib/format.js +23 -0
  27. package/src/lib/git.js +18 -0
  28. package/src/lib/graph.js +51 -0
  29. package/src/lib/guard.js +13 -0
  30. package/src/lib/index.js +84 -0
  31. package/src/lib/nlp.js +106 -0
  32. package/src/lib/relevance.js +81 -0
  33. package/src/lib/scorer.js +188 -0
  34. package/src/lib/sessions.js +51 -0
  35. package/src/lib/stale.js +27 -0
  36. package/src/mcp/server.js +52 -0
  37. package/src/mcp/tools/drafts.js +54 -0
  38. package/src/mcp/tools/log.js +93 -0
  39. package/src/mcp/tools/overview.js +141 -0
  40. package/src/mcp/tools/search.js +96 -0
  41. package/src/mcp/tools/stale.js +88 -0
  42. package/src/mcp/tools/why.js +91 -0
  43. package/src/watcher/comments.js +113 -0
  44. package/src/watcher/graph.js +149 -0
  45. package/src/watcher/index.js +134 -0
  46. package/src/watcher/signals.js +217 -0
  47. package/src/watcher/staleness.js +104 -0
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+ const { detectType, extractTitle } = require('../lib/nlp');
7
+ const { saveDraft } = require('../lib/drafts');
8
+ const { LORE_DIR } = require('../lib/index');
9
+
10
+ function makeDraft(overrides) {
11
+ return {
12
+ draftId: `draft-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
13
+ suggestedType: 'decision',
14
+ suggestedTitle: '',
15
+ evidence: '',
16
+ files: [],
17
+ confidence: 0.6,
18
+ createdAt: new Date().toISOString(),
19
+ status: 'pending',
20
+ source: 'signal',
21
+ ...overrides,
22
+ };
23
+ }
24
+
25
+ function onFileDeletion(filepath, projectRoot) {
26
+ const relativePath = path.relative(projectRoot, filepath).replace(/\\/g, '/');
27
+
28
+ // Try to check line count via git
29
+ let lines = 0;
30
+ try {
31
+ const out = execSync(`git show HEAD:"${relativePath}" 2>/dev/null | wc -l`, {
32
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
33
+ });
34
+ lines = parseInt(out.trim(), 10) || 0;
35
+ } catch (e) {}
36
+
37
+ if (lines > 0 && lines < 100) return null;
38
+
39
+ const name = path.basename(relativePath, path.extname(relativePath));
40
+ const draft = makeDraft({
41
+ suggestedType: 'graveyard',
42
+ suggestedTitle: `Removed ${name}`,
43
+ evidence: `File deleted: ${relativePath}`,
44
+ files: [relativePath],
45
+ confidence: 0.6,
46
+ });
47
+ saveDraft(draft);
48
+ return draft;
49
+ }
50
+
51
+ function onDirectoryDeletion(dirpath, projectRoot) {
52
+ const relativePath = path.relative(projectRoot, dirpath).replace(/\\/g, '/');
53
+ const name = path.basename(relativePath);
54
+ const draft = makeDraft({
55
+ suggestedType: 'graveyard',
56
+ suggestedTitle: `Removed ${name} module`,
57
+ evidence: `Directory deleted: ${relativePath}`,
58
+ files: [relativePath + '/'],
59
+ confidence: 0.6,
60
+ });
61
+ saveDraft(draft);
62
+ return draft;
63
+ }
64
+
65
+ function onNewFile(filepath, projectRoot) {
66
+ const relativePath = path.relative(projectRoot, filepath).replace(/\\/g, '/');
67
+ const name = path.basename(relativePath);
68
+ const lower = name.toLowerCase();
69
+
70
+ const configRe = /^(\.env|config\.|settings\.|\.eslintrc|\.prettierrc|\.babelrc|jest\.config|webpack\.config|vite\.config|tsconfig)/;
71
+ if (configRe.test(lower)) {
72
+ const draft = makeDraft({
73
+ suggestedType: 'decision',
74
+ suggestedTitle: `Added ${name}`,
75
+ evidence: `New config file detected: ${relativePath}`,
76
+ files: [relativePath],
77
+ confidence: 0.6,
78
+ });
79
+ saveDraft(draft);
80
+ return draft;
81
+ }
82
+
83
+ const adapterRe = /(adapter|provider|connector|driver|handler|strategy|middleware)\.(js|ts|jsx|tsx)$/;
84
+ if (adapterRe.test(lower)) {
85
+ const base = name.replace(/\.(js|ts|jsx|tsx)$/, '');
86
+ const draft = makeDraft({
87
+ suggestedType: 'decision',
88
+ suggestedTitle: `Added ${base}`,
89
+ evidence: `New adapter/provider file added: ${relativePath}`,
90
+ files: [relativePath],
91
+ confidence: 0.6,
92
+ });
93
+ saveDraft(draft);
94
+ return draft;
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ function onPackageJsonChange(filepath, projectRoot) {
101
+ const relativePath = path.relative(projectRoot, filepath).replace(/\\/g, '/');
102
+ if (!relativePath.endsWith('package.json')) return [];
103
+
104
+ let prev = {};
105
+ let curr = {};
106
+ try {
107
+ const prevRaw = execSync(`git show HEAD:"${relativePath}"`, {
108
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
109
+ });
110
+ prev = JSON.parse(prevRaw);
111
+ } catch (e) {}
112
+
113
+ try { curr = fs.readJsonSync(filepath); } catch (e) { return []; }
114
+
115
+ const prevDeps = Object.assign({}, prev.dependencies || {}, prev.devDependencies || {});
116
+ const currDeps = Object.assign({}, curr.dependencies || {}, curr.devDependencies || {});
117
+
118
+ const drafts = [];
119
+ for (const pkg of Object.keys(currDeps)) {
120
+ if (!prevDeps[pkg]) {
121
+ drafts.push(makeDraft({
122
+ suggestedType: 'decision',
123
+ suggestedTitle: `Added ${pkg} dependency`,
124
+ evidence: `New package added to package.json: ${pkg}@${currDeps[pkg]}`,
125
+ files: [relativePath],
126
+ confidence: 0.6,
127
+ }));
128
+ }
129
+ }
130
+ for (const pkg of Object.keys(prevDeps)) {
131
+ if (!currDeps[pkg]) {
132
+ drafts.push(makeDraft({
133
+ suggestedType: 'graveyard',
134
+ suggestedTitle: `Removed ${pkg} dependency`,
135
+ evidence: `Package removed from package.json: ${pkg}`,
136
+ files: [relativePath],
137
+ confidence: 0.6,
138
+ }));
139
+ }
140
+ }
141
+
142
+ for (const d of drafts) saveDraft(d);
143
+ return drafts;
144
+ }
145
+
146
+ function onCommitMessage(message, projectRoot) {
147
+ const lower = message.toLowerCase();
148
+ const signals = [
149
+ { re: /\b(replac|switch(ed|ing)|migrat)\b/, type: 'decision', confidence: 0.8 },
150
+ { re: /\b(remov|delet|drop(ped|ping))\b/, type: 'graveyard', confidence: 0.8 },
151
+ { re: /\b(revert|undo|rollback)\b/, type: 'graveyard', confidence: 0.8 },
152
+ { re: /\b(never|always|must|shall)\b/, type: 'invariant', confidence: 0.8 },
153
+ ];
154
+
155
+ for (const { re, type, confidence } of signals) {
156
+ if (re.test(lower)) {
157
+ const title = extractTitle(message) || message.slice(0, 50);
158
+ const draft = makeDraft({
159
+ suggestedType: type,
160
+ suggestedTitle: title,
161
+ evidence: `Commit message: "${message}"`,
162
+ files: [],
163
+ confidence,
164
+ source: 'commit-signal',
165
+ });
166
+ saveDraft(draft);
167
+ return [draft];
168
+ }
169
+ }
170
+ return [];
171
+ }
172
+
173
+ // Track repeated edits to detect gotcha-worthy files
174
+ function trackFileEdit(filepath, projectRoot) {
175
+ const relativePath = path.relative(projectRoot, filepath).replace(/\\/g, '/');
176
+ const statePath = path.join(LORE_DIR, 'watch-state.json');
177
+
178
+ let state = { edits: {} };
179
+ try { state = fs.readJsonSync(statePath); } catch (e) {}
180
+ if (!state.edits) state.edits = {};
181
+
182
+ const now = Date.now();
183
+ const weekAgo = now - 7 * 24 * 60 * 60 * 1000;
184
+
185
+ if (!state.edits[relativePath]) state.edits[relativePath] = [];
186
+ state.edits[relativePath].push(now);
187
+ state.edits[relativePath] = state.edits[relativePath].filter(t => t > weekAgo);
188
+
189
+ try { fs.writeJsonSync(statePath, state, { spaces: 2 }); } catch (e) {}
190
+
191
+ if (state.edits[relativePath].length >= 5) {
192
+ const name = path.basename(relativePath);
193
+ const draft = makeDraft({
194
+ suggestedType: 'gotcha',
195
+ suggestedTitle: `Frequent edits to ${name}`,
196
+ evidence: `${relativePath} edited ${state.edits[relativePath].length}× this week — may be a tricky area`,
197
+ files: [relativePath],
198
+ confidence: 0.4,
199
+ source: 'repeated-edit',
200
+ });
201
+ saveDraft(draft);
202
+ // Reset to avoid spam
203
+ state.edits[relativePath] = [];
204
+ try { fs.writeJsonSync(statePath, state, { spaces: 2 }); } catch (e) {}
205
+ return draft;
206
+ }
207
+ return null;
208
+ }
209
+
210
+ module.exports = {
211
+ onFileDeletion,
212
+ onDirectoryDeletion,
213
+ onNewFile,
214
+ onPackageJsonChange,
215
+ onCommitMessage,
216
+ trackFileEdit,
217
+ };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
5
+ const { readIndex } = require('../lib/index');
6
+ const { readEntry } = require('../lib/entries');
7
+
8
+ /**
9
+ * Pattern-based staleness checks — no Ollama needed.
10
+ * @param {object} entry
11
+ * @param {string} changedFile - relative path of changed file
12
+ * @param {string} changedCode - current content of changed file
13
+ * @returns {string[]} array of reason strings
14
+ */
15
+ function checkPatternStaleness(entry, changedFile, changedCode) {
16
+ const reasons = [];
17
+ const context = (entry.context || '').toLowerCase();
18
+ const filename = path.basename(changedFile).toLowerCase();
19
+ const code = (changedCode || '').toLowerCase();
20
+
21
+ // Performance invariant + external HTTP added
22
+ if (entry.type === 'invariant' && /\d+ms/.test(context)) {
23
+ if (/fetch\(|axios\.|http\.get|https\.get|request\(/.test(code)) {
24
+ reasons.push('External HTTP call added to a performance-critical path');
25
+ }
26
+ }
27
+
28
+ // Decision about polling + websocket import
29
+ if (context.includes('polling') && /websocket|ws\.|socket\.io/.test(code)) {
30
+ reasons.push('Architecture may have shifted — is this decision still valid?');
31
+ }
32
+
33
+ // Decision mentions websocket + new websocket file added
34
+ if (context.includes('websocket') && filename.includes('websocket')) {
35
+ reasons.push('Your infra constraint may have changed');
36
+ }
37
+
38
+ // Graveyard entry for a package that may be re-added
39
+ if (entry.type === 'graveyard' && changedFile.endsWith('package.json')) {
40
+ const titleLower = entry.title.toLowerCase();
41
+ // Extract package name from graveyard title
42
+ const match = titleLower.match(/removed?\s+([\w-@/]+)\s+dep/);
43
+ if (match && code.includes(match[1])) {
44
+ reasons.push('This abandoned approach may have been re-introduced');
45
+ }
46
+ }
47
+
48
+ return reasons;
49
+ }
50
+
51
+ /**
52
+ * Semantic staleness check using embeddings.
53
+ * @param {object} entry
54
+ * @param {string} diff - summary of changes (function names + imports)
55
+ * @returns {Promise<string[]>}
56
+ */
57
+ async function checkSemanticStaleness(entry, diff) {
58
+ try {
59
+ const { generateEmbedding, cosineSimilarity, getEmbedding } = require('../lib/embeddings');
60
+ const entryVec = getEmbedding(entry.id);
61
+ if (!entryVec) return [];
62
+
63
+ const diffVec = await generateEmbedding(diff);
64
+ const sim = cosineSimilarity(diffVec, entryVec);
65
+
66
+ if (sim > 0.6 && (entry.type === 'invariant' || entry.type === 'decision')) {
67
+ return [`Diff is semantically related to this ${entry.type} (${(sim * 100).toFixed(0)}% similarity)`];
68
+ }
69
+ } catch (e) {
70
+ // Ollama not available
71
+ }
72
+ return [];
73
+ }
74
+
75
+ /**
76
+ * Check all entries linked to a changed file for staleness signals.
77
+ * @param {string} changedFile - absolute path
78
+ * @param {string} projectRoot
79
+ * @param {string} changedCode
80
+ * @param {string|null} diff - optional diff text for semantic check
81
+ * @returns {Promise<Array<{entry, reasons}>>}
82
+ */
83
+ async function checkFileStaleness(changedFile, projectRoot, changedCode, diff) {
84
+ const index = readIndex();
85
+ const relativePath = path.relative(projectRoot, path.resolve(changedFile)).replace(/\\/g, '/');
86
+
87
+ const entryIds = new Set();
88
+ if (index.files[relativePath]) index.files[relativePath].forEach(id => entryIds.add(id));
89
+
90
+ const results = [];
91
+ for (const id of entryIds) {
92
+ const entry = readEntry(index.entries[id]);
93
+ if (!entry) continue;
94
+
95
+ const patternReasons = checkPatternStaleness(entry, relativePath, changedCode);
96
+ const semanticReasons = diff ? await checkSemanticStaleness(entry, diff) : [];
97
+ const reasons = [...patternReasons, ...semanticReasons];
98
+ if (reasons.length > 0) results.push({ entry, reasons });
99
+ }
100
+
101
+ return results;
102
+ }
103
+
104
+ module.exports = { checkFileStaleness, checkPatternStaleness, checkSemanticStaleness };