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.
- package/LICENSE +21 -0
- package/README.md +666 -0
- package/bin/lore.js +108 -0
- package/package.json +53 -0
- package/src/commands/drafts.js +144 -0
- package/src/commands/edit.js +30 -0
- package/src/commands/embed.js +63 -0
- package/src/commands/export.js +76 -0
- package/src/commands/graph.js +80 -0
- package/src/commands/init.js +110 -0
- package/src/commands/log.js +149 -0
- package/src/commands/mine.js +38 -0
- package/src/commands/onboard.js +112 -0
- package/src/commands/score.js +88 -0
- package/src/commands/search.js +49 -0
- package/src/commands/serve.js +21 -0
- package/src/commands/stale.js +41 -0
- package/src/commands/status.js +59 -0
- package/src/commands/watch.js +67 -0
- package/src/commands/why.js +58 -0
- package/src/lib/budget.js +57 -0
- package/src/lib/config.js +52 -0
- package/src/lib/drafts.js +104 -0
- package/src/lib/embeddings.js +97 -0
- package/src/lib/entries.js +59 -0
- package/src/lib/format.js +23 -0
- package/src/lib/git.js +18 -0
- package/src/lib/graph.js +51 -0
- package/src/lib/guard.js +13 -0
- package/src/lib/index.js +84 -0
- package/src/lib/nlp.js +106 -0
- package/src/lib/relevance.js +81 -0
- package/src/lib/scorer.js +188 -0
- package/src/lib/sessions.js +51 -0
- package/src/lib/stale.js +27 -0
- package/src/mcp/server.js +52 -0
- package/src/mcp/tools/drafts.js +54 -0
- package/src/mcp/tools/log.js +93 -0
- package/src/mcp/tools/overview.js +141 -0
- package/src/mcp/tools/search.js +96 -0
- package/src/mcp/tools/stale.js +88 -0
- package/src/mcp/tools/why.js +91 -0
- package/src/watcher/comments.js +113 -0
- package/src/watcher/graph.js +149 -0
- package/src/watcher/index.js +134 -0
- package/src/watcher/signals.js +217 -0
- 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 };
|