lore-memory 0.2.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 +32 -2
- package/package.json +4 -2
- package/src/commands/init.js +21 -4
- package/src/commands/mine.js +4 -3
- 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/commands/ui.js +171 -0
- package/src/lib/format.js +45 -2
- package/src/lib/relevance.js +3 -1
- package/src/mcp/tools/search.js +1 -2
- package/src/ui/public/app.js +286 -0
- package/src/ui/public/index.html +118 -0
- package/src/ui/public/style.css +321 -0
- package/src/watcher/comments.js +16 -4
package/bin/lore.js
CHANGED
|
@@ -8,8 +8,11 @@ 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
|
+
// Only launch the interactive menu if strictly NO arguments were provided
|
|
14
|
+
if (process.argv.length !== 2) return;
|
|
15
|
+
|
|
13
16
|
const inquirer = require('inquirer');
|
|
14
17
|
const chalk = require('chalk');
|
|
15
18
|
const { execSync } = require('child_process');
|
|
@@ -36,6 +39,8 @@ program
|
|
|
36
39
|
{ name: 'š Review pending drafts (lore drafts)', value: 'drafts' },
|
|
37
40
|
{ name: 'š View project health (lore score)', value: 'score' },
|
|
38
41
|
{ name: 'š Search knowledge base (lore search)', value: 'search' },
|
|
42
|
+
{ name: 'šŖ Generate AI Prompt (lore prompt)', value: 'prompt' },
|
|
43
|
+
{ name: 'š Open Local Web Dashboard (lore ui)', value: 'ui' },
|
|
39
44
|
{ name: 'āļø Start background watcher (lore watch --daemon)', value: 'watch --daemon' },
|
|
40
45
|
new inquirer.Separator(),
|
|
41
46
|
{ name: 'ā Show Help', value: 'help' },
|
|
@@ -48,6 +53,18 @@ program
|
|
|
48
53
|
process.exit(0);
|
|
49
54
|
} else if (action === 'help') {
|
|
50
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
|
+
}
|
|
51
68
|
} else {
|
|
52
69
|
console.log();
|
|
53
70
|
try {
|
|
@@ -131,7 +148,7 @@ program
|
|
|
131
148
|
.action(require('../src/commands/stale'));
|
|
132
149
|
|
|
133
150
|
program
|
|
134
|
-
.command('search
|
|
151
|
+
.command('search [query...]')
|
|
135
152
|
.description('Search all entries for a keyword')
|
|
136
153
|
.action(require('../src/commands/search'));
|
|
137
154
|
|
|
@@ -192,4 +209,17 @@ program
|
|
|
192
209
|
.option('--build', 'Rebuild the full graph from source')
|
|
193
210
|
.action(require('../src/commands/graph'));
|
|
194
211
|
|
|
212
|
+
program
|
|
213
|
+
.command('ui')
|
|
214
|
+
.description('Start the local Lore web dashboard')
|
|
215
|
+
.option('-p, --port <port>', 'Port to run the UI server on', '3333')
|
|
216
|
+
.action(require('../src/commands/ui'));
|
|
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
|
+
|
|
195
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": {
|
|
@@ -43,11 +43,13 @@
|
|
|
43
43
|
"chalk": "^4",
|
|
44
44
|
"chokidar": "^3.6.0",
|
|
45
45
|
"commander": "^11",
|
|
46
|
+
"express": "^5.2.1",
|
|
46
47
|
"fs-extra": "^11",
|
|
47
48
|
"glob": "^10.5.0",
|
|
48
49
|
"inquirer": "^8",
|
|
49
50
|
"js-yaml": "^4",
|
|
50
51
|
"natural": "^6.12.0",
|
|
51
|
-
"ollama": "^0.6.3"
|
|
52
|
+
"ollama": "^0.6.3",
|
|
53
|
+
"open": "^11.0.0"
|
|
52
54
|
}
|
|
53
55
|
}
|
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/mine.js
CHANGED
|
@@ -6,7 +6,7 @@ const fs = require('fs-extra');
|
|
|
6
6
|
const { mineFile, mineDirectory } = require('../watcher/comments');
|
|
7
7
|
const { requireInit } = require('../lib/guard');
|
|
8
8
|
|
|
9
|
-
function mine(targetPath) {
|
|
9
|
+
async function mine(targetPath) {
|
|
10
10
|
requireInit();
|
|
11
11
|
const projectRoot = process.cwd();
|
|
12
12
|
const target = targetPath || '.';
|
|
@@ -18,10 +18,11 @@ function mine(targetPath) {
|
|
|
18
18
|
|
|
19
19
|
if (stat.isDirectory()) {
|
|
20
20
|
console.log(chalk.cyan(`š Mining comments in ${target} ...`));
|
|
21
|
-
count = mineDirectory(abs, projectRoot);
|
|
21
|
+
count = await mineDirectory(abs, projectRoot);
|
|
22
22
|
} else {
|
|
23
23
|
console.log(chalk.cyan(`š Mining comments in ${target} ...`));
|
|
24
|
-
|
|
24
|
+
const drafts = await mineFile(abs, projectRoot);
|
|
25
|
+
count = drafts.length;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
if (count === 0) {
|
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
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { requireInit } = require('../lib/guard');
|
|
7
|
+
const { readIndex, LORE_DIR } = require('../lib/index');
|
|
8
|
+
const { readEntry } = require('../lib/entries');
|
|
9
|
+
const { computeScore } = require('../lib/scorer');
|
|
10
|
+
const { listDrafts, acceptDraft, deleteDraft } = require('../lib/drafts');
|
|
11
|
+
|
|
12
|
+
// Only load 'open' dynamically to avoid overhead on other CLI commands if not needed
|
|
13
|
+
async function openBrowser(url) {
|
|
14
|
+
const open = (await import('open')).default;
|
|
15
|
+
await open(url);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ui(options) {
|
|
19
|
+
requireInit();
|
|
20
|
+
|
|
21
|
+
const app = express();
|
|
22
|
+
const PORT = options.port || 3333;
|
|
23
|
+
|
|
24
|
+
app.use(express.json());
|
|
25
|
+
|
|
26
|
+
// CORS for local dev
|
|
27
|
+
app.use((req, res, next) => {
|
|
28
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
29
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
|
30
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
|
|
31
|
+
next();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// API Endpoints
|
|
35
|
+
|
|
36
|
+
app.get('/api/stats', (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const scoreData = computeScore();
|
|
39
|
+
const drafts = listDrafts();
|
|
40
|
+
|
|
41
|
+
const index = readIndex();
|
|
42
|
+
const counts = { decision: 0, invariant: 0, graveyard: 0, gotcha: 0 };
|
|
43
|
+
for (const entryPath of Object.values(index.entries)) {
|
|
44
|
+
const entry = readEntry(entryPath);
|
|
45
|
+
if (entry && counts[entry.type] !== undefined) counts[entry.type]++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
res.json({
|
|
49
|
+
score: scoreData,
|
|
50
|
+
counts,
|
|
51
|
+
draftCount: drafts.length,
|
|
52
|
+
totalEntries: Object.keys(index.entries).length
|
|
53
|
+
});
|
|
54
|
+
} catch (e) {
|
|
55
|
+
res.status(500).json({ error: e.message });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
app.get('/api/entries', (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const index = readIndex();
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (const entryPath of Object.values(index.entries)) {
|
|
64
|
+
const entry = readEntry(entryPath);
|
|
65
|
+
if (entry) entries.push(entry);
|
|
66
|
+
}
|
|
67
|
+
// Sort newest first
|
|
68
|
+
entries.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
69
|
+
res.json(entries);
|
|
70
|
+
} catch (e) {
|
|
71
|
+
res.status(500).json({ error: e.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
app.get('/api/drafts', (req, res) => {
|
|
76
|
+
try {
|
|
77
|
+
const drafts = listDrafts();
|
|
78
|
+
res.json(drafts);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
res.status(500).json({ error: e.message });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.post('/api/drafts/:id/accept', (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const entry = acceptDraft(req.params.id);
|
|
87
|
+
res.json({ success: true, entry });
|
|
88
|
+
} catch (e) {
|
|
89
|
+
res.status(500).json({ error: e.message });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
app.delete('/api/drafts/:id', (req, res) => {
|
|
94
|
+
try {
|
|
95
|
+
deleteDraft(req.params.id);
|
|
96
|
+
res.json({ success: true });
|
|
97
|
+
} catch (e) {
|
|
98
|
+
res.status(500).json({ error: e.message });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.get('/api/graph', (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const { loadGraph, saveGraph } = require('../lib/graph');
|
|
105
|
+
let g = loadGraph();
|
|
106
|
+
|
|
107
|
+
if (Object.keys(g.imports).length === 0) {
|
|
108
|
+
const { buildFullGraph } = require('../watcher/graph');
|
|
109
|
+
g = buildFullGraph(process.cwd());
|
|
110
|
+
saveGraph(g);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const nodesSet = new Set();
|
|
114
|
+
const edges = [];
|
|
115
|
+
for (const [file, deps] of Object.entries(g.imports)) {
|
|
116
|
+
nodesSet.add(file);
|
|
117
|
+
for (const dep of deps) {
|
|
118
|
+
nodesSet.add(dep);
|
|
119
|
+
edges.push({ from: file, to: dep });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const nodes = Array.from(nodesSet).map(id => ({ id, label: id }));
|
|
124
|
+
res.json({ nodes, edges });
|
|
125
|
+
} catch (e) {
|
|
126
|
+
res.status(500).json({ error: e.message });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Handle unmapped API routes with 404 JSON (instead of serving index.html)
|
|
131
|
+
app.use('/api', (req, res) => {
|
|
132
|
+
res.status(404).json({ error: 'API route not found' });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Serve the frontend application
|
|
136
|
+
const uiPath = path.join(__dirname, '..', 'ui', 'public');
|
|
137
|
+
app.use(express.static(uiPath));
|
|
138
|
+
|
|
139
|
+
// Catch-all to serve index.html for SPA routing
|
|
140
|
+
app.use((req, res) => {
|
|
141
|
+
res.sendFile(path.join(uiPath, 'index.html'));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const server = app.listen(PORT, () => {
|
|
145
|
+
const url = `http://localhost:${PORT}`;
|
|
146
|
+
console.log(chalk.green(`\nš Lore UI Dashboard running at ${chalk.bold(url)}\n`));
|
|
147
|
+
console.log(chalk.cyan(` Press Ctrl+C to stop the server.`));
|
|
148
|
+
|
|
149
|
+
// Use native exec to open browser to avoid ESM import issues with 'open'
|
|
150
|
+
const { exec } = require('child_process');
|
|
151
|
+
const startPath = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
152
|
+
exec(`${startPath} ${url}`, (err) => {
|
|
153
|
+
if (err) {
|
|
154
|
+
console.log(chalk.dim(` (Could not open browser automatically. Please visit ${url} manually)`));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
server.on('error', (e) => {
|
|
160
|
+
if (e.code === 'EADDRINUSE') {
|
|
161
|
+
console.error(chalk.red(`\nPort ${PORT} is already in use by another process.`));
|
|
162
|
+
console.error(chalk.yellow(`Use 'lore ui --port <number>' to specify a different port.\n`));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
} else {
|
|
165
|
+
console.error(chalk.red(`\nFailed to start server: ${e.message}\n`));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = ui;
|
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 {
|