lore-memory 0.3.0 ā 0.5.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 +24 -3
- package/package.json +1 -1
- package/src/commands/init.js +2 -25
- package/src/commands/log.js +27 -1
- package/src/commands/onboard.js +3 -2
- package/src/commands/prompt.js +63 -0
- package/src/commands/search.js +8 -1
- package/src/commands/serve.js +21 -0
- package/src/commands/stale.js +1 -1
- package/src/commands/ui.js +32 -4
- package/src/lib/config.js +1 -0
- package/src/lib/drafts.js +70 -29
- package/src/lib/entries.js +61 -0
- package/src/lib/format.js +45 -2
- package/src/lib/relevance.js +3 -1
- package/src/mcp/server.js +8 -2
- package/src/mcp/status.js +72 -0
- package/src/mcp/tools/log.js +39 -4
- package/src/mcp/tools/search.js +1 -2
- package/src/mcp/tools/update.js +143 -0
- package/src/watcher/comments.js +13 -2
package/src/mcp/server.js
CHANGED
|
@@ -10,15 +10,17 @@ const {
|
|
|
10
10
|
const why = require('./tools/why');
|
|
11
11
|
const search = require('./tools/search');
|
|
12
12
|
const log = require('./tools/log');
|
|
13
|
+
const update = require('./tools/update');
|
|
13
14
|
const stale = require('./tools/stale');
|
|
14
15
|
const overview = require('./tools/overview');
|
|
15
16
|
const drafts = require('./tools/drafts');
|
|
17
|
+
const { announceStartup } = require('./status');
|
|
16
18
|
|
|
17
|
-
const TOOLS = [why, search, log, stale, overview, drafts];
|
|
19
|
+
const TOOLS = [why, search, log, update, stale, overview, drafts];
|
|
18
20
|
|
|
19
21
|
async function startServer() {
|
|
20
22
|
const server = new Server(
|
|
21
|
-
{ name: 'lore', version: '0.
|
|
23
|
+
{ name: 'lore', version: '0.4.0' },
|
|
22
24
|
{ capabilities: { tools: {} } }
|
|
23
25
|
);
|
|
24
26
|
|
|
@@ -45,8 +47,12 @@ async function startServer() {
|
|
|
45
47
|
const transport = new StdioServerTransport();
|
|
46
48
|
await server.connect(transport);
|
|
47
49
|
|
|
50
|
+
announceStartup(TOOLS);
|
|
51
|
+
|
|
48
52
|
// Keep process alive
|
|
49
53
|
process.stdin.resume();
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
module.exports = { startServer };
|
|
57
|
+
|
|
58
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const LORE_DIR = path.join(process.cwd(), '.lore');
|
|
7
|
+
const PID_FILE = path.join(LORE_DIR, 'mcp.pid');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Write a status file so other tools can detect if the MCP server is running.
|
|
11
|
+
*/
|
|
12
|
+
function writePidFile(tools) {
|
|
13
|
+
try {
|
|
14
|
+
fs.ensureDirSync(LORE_DIR);
|
|
15
|
+
fs.writeJsonSync(PID_FILE, {
|
|
16
|
+
pid: process.pid,
|
|
17
|
+
startedAt: new Date().toISOString(),
|
|
18
|
+
tools: tools.map(t => t.toolDefinition.name),
|
|
19
|
+
}, { spaces: 2 });
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// Non-fatal
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Remove the status file on shutdown.
|
|
27
|
+
*/
|
|
28
|
+
function removePidFile() {
|
|
29
|
+
try { fs.removeSync(PID_FILE); } catch (e) {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Print startup banner to stderr (safe ā MCP only reads stdout).
|
|
34
|
+
* Register cleanup handlers.
|
|
35
|
+
*/
|
|
36
|
+
function announceStartup(tools) {
|
|
37
|
+
writePidFile(tools);
|
|
38
|
+
|
|
39
|
+
const toolNames = tools.map(t => t.toolDefinition.name).join(', ');
|
|
40
|
+
process.stderr.write(`\nš Lore MCP server active (PID: ${process.pid})\n`);
|
|
41
|
+
process.stderr.write(` ${tools.length} tools available: ${toolNames}\n`);
|
|
42
|
+
process.stderr.write(` Serving project: ${path.basename(process.cwd())}\n\n`);
|
|
43
|
+
|
|
44
|
+
process.on('SIGINT', () => { removePidFile(); process.exit(0); });
|
|
45
|
+
process.on('SIGTERM', () => { removePidFile(); process.exit(0); });
|
|
46
|
+
process.on('exit', removePidFile);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if the MCP server is currently running.
|
|
51
|
+
* @returns {{ pid: number, startedAt: string, tools: string[] }|null}
|
|
52
|
+
*/
|
|
53
|
+
function getServerStatus() {
|
|
54
|
+
try {
|
|
55
|
+
if (!fs.existsSync(PID_FILE)) return null;
|
|
56
|
+
const info = fs.readJsonSync(PID_FILE);
|
|
57
|
+
|
|
58
|
+
// Verify the process is actually alive
|
|
59
|
+
try {
|
|
60
|
+
process.kill(info.pid, 0); // Signal 0 = just check if process exists
|
|
61
|
+
return info;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Process is dead, clean up stale PID file
|
|
64
|
+
removePidFile();
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { announceStartup, getServerStatus };
|
package/src/mcp/tools/log.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { readIndex, writeIndex, addEntryToIndex } = require('../../lib/index');
|
|
4
|
-
const { generateId, writeEntry } = require('../../lib/entries');
|
|
4
|
+
const { generateId, writeEntry, findDuplicate } = require('../../lib/entries');
|
|
5
|
+
const { readConfig } = require('../../lib/config');
|
|
6
|
+
const { saveDraft } = require('../../lib/drafts');
|
|
5
7
|
|
|
6
8
|
const toolDefinition = {
|
|
7
9
|
name: 'lore_log',
|
|
8
|
-
description: 'Create a new Lore entry to record an architectural decision, invariant, gotcha, or graveyard item. Use this when you make a significant technical decision that future developers (or AI) should know about.',
|
|
10
|
+
description: 'Create a new Lore entry to record an architectural decision, invariant, gotcha, or graveyard item. Use this when you make a significant technical decision that future developers (or AI) should know about. Note: entries may be saved as drafts pending human review depending on project configuration.',
|
|
9
11
|
inputSchema: {
|
|
10
12
|
type: 'object',
|
|
11
13
|
properties: {
|
|
@@ -50,6 +52,40 @@ async function handler(args) {
|
|
|
50
52
|
const { type, title, context, files = [], tags = [], alternatives = [], tradeoffs = '' } = args;
|
|
51
53
|
|
|
52
54
|
try {
|
|
55
|
+
const index = readIndex();
|
|
56
|
+
|
|
57
|
+
// Deduplication check
|
|
58
|
+
const duplicate = findDuplicate(index, type, title);
|
|
59
|
+
if (duplicate) {
|
|
60
|
+
const matchLabel = duplicate.match === 'exact' ? 'Exact duplicate' : 'Similar entry';
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: 'text', text: `${matchLabel} already exists: "${duplicate.entry.title}" (${duplicate.entry.id}). No new entry created.` }],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const config = readConfig();
|
|
67
|
+
const requireConfirmation = config.mcp && config.mcp.confirmEntries !== false;
|
|
68
|
+
|
|
69
|
+
if (requireConfirmation) {
|
|
70
|
+
// Route to drafts for human review
|
|
71
|
+
const draftId = saveDraft({
|
|
72
|
+
suggestedType: type,
|
|
73
|
+
suggestedTitle: title,
|
|
74
|
+
evidence: context,
|
|
75
|
+
files,
|
|
76
|
+
tags,
|
|
77
|
+
alternatives,
|
|
78
|
+
tradeoffs,
|
|
79
|
+
confidence: 0.9,
|
|
80
|
+
source: 'mcp-log',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: 'text', text: `Draft created for human review: "${title}" (${draftId}). The developer can approve it with: lore drafts` }],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Direct entry creation (confirmEntries is explicitly false)
|
|
53
89
|
const id = generateId(type, title);
|
|
54
90
|
const entry = {
|
|
55
91
|
id,
|
|
@@ -64,8 +100,6 @@ async function handler(args) {
|
|
|
64
100
|
};
|
|
65
101
|
|
|
66
102
|
writeEntry(entry);
|
|
67
|
-
|
|
68
|
-
const index = readIndex();
|
|
69
103
|
addEntryToIndex(index, entry);
|
|
70
104
|
writeIndex(index);
|
|
71
105
|
|
|
@@ -91,3 +125,4 @@ async function handler(args) {
|
|
|
91
125
|
}
|
|
92
126
|
|
|
93
127
|
module.exports = { toolDefinition, handler };
|
|
128
|
+
|
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 {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { readIndex, writeIndex } = require('../../lib/index');
|
|
4
|
+
const { readEntry, writeEntry } = require('../../lib/entries');
|
|
5
|
+
const { saveDraft } = require('../../lib/drafts');
|
|
6
|
+
const { readConfig } = require('../../lib/config');
|
|
7
|
+
|
|
8
|
+
const toolDefinition = {
|
|
9
|
+
name: 'lore_update',
|
|
10
|
+
description: 'Update an existing Lore entry. Use this when a previous decision, invariant, gotcha, or graveyard item needs to be revised because the context has changed. Updates may be saved as drafts pending human review depending on project configuration.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
id: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'The ID of the existing entry to update (e.g. "decision-use-postgres-1709876543")',
|
|
17
|
+
},
|
|
18
|
+
context: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Updated context/explanation. If provided, replaces the existing context.',
|
|
21
|
+
},
|
|
22
|
+
title: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
description: 'Updated title. If provided, replaces the existing title.',
|
|
25
|
+
},
|
|
26
|
+
files: {
|
|
27
|
+
type: 'array',
|
|
28
|
+
items: { type: 'string' },
|
|
29
|
+
description: 'Updated list of related files. If provided, replaces the existing file list.',
|
|
30
|
+
},
|
|
31
|
+
tags: {
|
|
32
|
+
type: 'array',
|
|
33
|
+
items: { type: 'string' },
|
|
34
|
+
description: 'Updated tags. If provided, replaces existing tags.',
|
|
35
|
+
},
|
|
36
|
+
alternatives: {
|
|
37
|
+
type: 'array',
|
|
38
|
+
items: { type: 'string' },
|
|
39
|
+
description: 'Updated alternatives considered.',
|
|
40
|
+
},
|
|
41
|
+
tradeoffs: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Updated tradeoffs.',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ['id'],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
async function handler(args) {
|
|
51
|
+
const { id, context, title, files, tags, alternatives, tradeoffs } = args;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const index = readIndex();
|
|
55
|
+
const entryPath = index.entries[id];
|
|
56
|
+
|
|
57
|
+
if (!entryPath) {
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: 'text', text: `Entry not found: "${id}". Use lore_search to find the correct entry ID.` }],
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const entry = readEntry(entryPath);
|
|
65
|
+
if (!entry) {
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: 'text', text: `Failed to read entry: "${id}".` }],
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config = readConfig();
|
|
73
|
+
const requireConfirmation = config.mcp && config.mcp.confirmEntries !== false;
|
|
74
|
+
|
|
75
|
+
// Build a summary of what's changing
|
|
76
|
+
const changes = [];
|
|
77
|
+
if (title !== undefined && title !== entry.title) changes.push(`title: "${entry.title}" ā "${title}"`);
|
|
78
|
+
if (context !== undefined && context !== entry.context) changes.push('context updated');
|
|
79
|
+
if (files !== undefined) changes.push(`files: [${files.join(', ')}]`);
|
|
80
|
+
if (tags !== undefined) changes.push(`tags: [${tags.join(', ')}]`);
|
|
81
|
+
if (alternatives !== undefined) changes.push(`alternatives: [${alternatives.join(', ')}]`);
|
|
82
|
+
if (tradeoffs !== undefined) changes.push('tradeoffs updated');
|
|
83
|
+
|
|
84
|
+
if (changes.length === 0) {
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: 'text', text: `No changes provided for entry "${id}".` }],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (requireConfirmation) {
|
|
91
|
+
// Route update through drafts for human review
|
|
92
|
+
const draftId = saveDraft({
|
|
93
|
+
suggestedType: entry.type,
|
|
94
|
+
suggestedTitle: title || entry.title,
|
|
95
|
+
evidence: `[UPDATE to ${id}] ${context || entry.context}`,
|
|
96
|
+
files: files || entry.files,
|
|
97
|
+
tags: tags || entry.tags,
|
|
98
|
+
alternatives: alternatives || entry.alternatives,
|
|
99
|
+
tradeoffs: tradeoffs !== undefined ? tradeoffs : entry.tradeoffs,
|
|
100
|
+
confidence: 0.95,
|
|
101
|
+
source: 'mcp-update',
|
|
102
|
+
updatesEntryId: id,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: 'text', text: `Update drafted for human review (${draftId}). Changes: ${changes.join('; ')}. The developer can approve it with: lore drafts` }],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Direct update (confirmEntries is explicitly false)
|
|
111
|
+
if (title !== undefined) entry.title = title;
|
|
112
|
+
if (context !== undefined) entry.context = context;
|
|
113
|
+
if (files !== undefined) entry.files = files;
|
|
114
|
+
if (tags !== undefined) entry.tags = tags;
|
|
115
|
+
if (alternatives !== undefined) entry.alternatives = alternatives;
|
|
116
|
+
if (tradeoffs !== undefined) entry.tradeoffs = tradeoffs;
|
|
117
|
+
entry.lastUpdated = new Date().toISOString();
|
|
118
|
+
|
|
119
|
+
writeEntry(entry);
|
|
120
|
+
writeIndex(index);
|
|
121
|
+
|
|
122
|
+
// Re-embed
|
|
123
|
+
try {
|
|
124
|
+
const { generateEmbedding, storeEmbedding } = require('../../lib/embeddings');
|
|
125
|
+
const text = [entry.title, entry.context, ...(entry.alternatives || []), entry.tradeoffs || '', ...(entry.tags || [])].join(' ');
|
|
126
|
+
const vector = await generateEmbedding(text);
|
|
127
|
+
storeEmbedding(id, vector);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
// Ollama not available ā skip
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: 'text', text: `Updated entry "${id}". Changes: ${changes.join('; ')}` }],
|
|
134
|
+
};
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: 'text', text: `Error updating entry: ${e.message}` }],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { toolDefinition, handler };
|
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,
|