lore-memory 0.1.1 → 0.3.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/bin/lore.js +98 -1
- package/package.json +4 -2
- package/src/commands/drafts.js +61 -85
- package/src/commands/init.js +7 -4
- package/src/commands/log.js +1 -1
- package/src/commands/mine.js +4 -3
- package/src/commands/ui.js +171 -0
- package/src/lib/format.js +56 -5
- package/src/mcp/tools/overview.js +5 -0
- package/src/mcp/tools/search.js +11 -1
- 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 +6 -5
- package/src/watcher/index.js +18 -18
- package/src/watcher/signals.js +12 -12
package/bin/lore.js
CHANGED
|
@@ -9,7 +9,98 @@ program
|
|
|
9
9
|
.name('lore')
|
|
10
10
|
.description('Persistent project memory for developers')
|
|
11
11
|
.version('0.1.0')
|
|
12
|
-
.action(() =>
|
|
12
|
+
.action(async () => {
|
|
13
|
+
// Only launch the interactive menu if strictly NO arguments were provided
|
|
14
|
+
if (process.argv.length !== 2) return;
|
|
15
|
+
|
|
16
|
+
const inquirer = require('inquirer');
|
|
17
|
+
const chalk = require('chalk');
|
|
18
|
+
const { execSync } = require('child_process');
|
|
19
|
+
|
|
20
|
+
const LORE_LOGO = `
|
|
21
|
+
██╗ ██████╗ ██████╗ ███████╗
|
|
22
|
+
██║ ██╔═══██╗██╔══██╗██╔════╝
|
|
23
|
+
██║ ██║ ██║██████╔╝█████╗
|
|
24
|
+
██║ ██║ ██║██╔══██╗██╔══╝
|
|
25
|
+
███████╗╚██████╔╝██║ ██║███████╗
|
|
26
|
+
╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
console.log(chalk.cyan(LORE_LOGO));
|
|
30
|
+
console.log(chalk.dim(' Project Memory for Developers\n'));
|
|
31
|
+
|
|
32
|
+
const { action } = await inquirer.prompt([
|
|
33
|
+
{
|
|
34
|
+
type: 'list',
|
|
35
|
+
name: 'action',
|
|
36
|
+
message: 'What would you like to do?',
|
|
37
|
+
choices: [
|
|
38
|
+
{ name: '📝 Log new knowledge (lore log)', value: 'log' },
|
|
39
|
+
{ name: '👀 Review pending drafts (lore drafts)', value: 'drafts' },
|
|
40
|
+
{ name: '📊 View project health (lore score)', value: 'score' },
|
|
41
|
+
{ name: '🔍 Search knowledge base (lore search)', value: 'search' },
|
|
42
|
+
{ name: '🌐 Open Local Web Dashboard (lore ui)', value: 'ui' },
|
|
43
|
+
{ name: '⚙️ Start background watcher (lore watch --daemon)', value: 'watch --daemon' },
|
|
44
|
+
new inquirer.Separator(),
|
|
45
|
+
{ name: '❓ Show Help', value: 'help' },
|
|
46
|
+
{ name: '❌ Exit', value: 'exit' }
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
if (action === 'exit') {
|
|
52
|
+
process.exit(0);
|
|
53
|
+
} else if (action === 'help') {
|
|
54
|
+
program.outputHelp();
|
|
55
|
+
} else {
|
|
56
|
+
console.log();
|
|
57
|
+
try {
|
|
58
|
+
execSync(`node ${__filename} ${action}`, { stdio: 'inherit' });
|
|
59
|
+
} catch (e) { }
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Fuzzy matching for unknown commands
|
|
64
|
+
program.on('command:*', function (operands) {
|
|
65
|
+
const chalk = require('chalk');
|
|
66
|
+
console.error(chalk.red(`error: unknown command '${operands[0]}'`));
|
|
67
|
+
const availableCommands = program.commands.map(cmd => cmd.name());
|
|
68
|
+
|
|
69
|
+
// Simple Levenshtein distance check for did-you-mean
|
|
70
|
+
let closest = null;
|
|
71
|
+
let minDistance = 3; // only suggest if distance < 3
|
|
72
|
+
|
|
73
|
+
for (const cmd of availableCommands) {
|
|
74
|
+
let distance = 0;
|
|
75
|
+
const a = operands[0];
|
|
76
|
+
const b = cmd;
|
|
77
|
+
const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));
|
|
78
|
+
for (let i = 0; i <= a.length; i += 1) { matrix[0][i] = i; }
|
|
79
|
+
for (let j = 0; j <= b.length; j += 1) { matrix[j][0] = j; }
|
|
80
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
81
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
82
|
+
const indicator = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
83
|
+
matrix[j][i] = Math.min(
|
|
84
|
+
matrix[j][i - 1] + 1,
|
|
85
|
+
matrix[j - 1][i] + 1,
|
|
86
|
+
matrix[j - 1][i - 1] + indicator
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
distance = matrix[b.length][a.length];
|
|
91
|
+
|
|
92
|
+
if (distance < minDistance) {
|
|
93
|
+
minDistance = distance;
|
|
94
|
+
closest = cmd;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (closest) {
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(chalk.yellow(`Did you mean ${chalk.bold('lore ' + closest)}?`));
|
|
101
|
+
}
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
});
|
|
13
104
|
|
|
14
105
|
program
|
|
15
106
|
.command('init')
|
|
@@ -105,4 +196,10 @@ program
|
|
|
105
196
|
.option('--build', 'Rebuild the full graph from source')
|
|
106
197
|
.action(require('../src/commands/graph'));
|
|
107
198
|
|
|
199
|
+
program
|
|
200
|
+
.command('ui')
|
|
201
|
+
.description('Start the local Lore web dashboard')
|
|
202
|
+
.option('-p, --port <port>', 'Port to run the UI server on', '3333')
|
|
203
|
+
.action(require('../src/commands/ui'));
|
|
204
|
+
|
|
108
205
|
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.3.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/drafts.js
CHANGED
|
@@ -38,8 +38,7 @@ async function drafts(options) {
|
|
|
38
38
|
|
|
39
39
|
console.log(chalk.cyan(`\n📖 ${pending.length} pending draft${pending.length === 1 ? '' : 's'}\n`));
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
const draft = pending[i];
|
|
41
|
+
const choices = pending.map((draft, i) => {
|
|
43
42
|
const conf = Math.round((draft.confidence || 0) * 100);
|
|
44
43
|
const typeColor = {
|
|
45
44
|
decision: chalk.blue,
|
|
@@ -48,97 +47,74 @@ async function drafts(options) {
|
|
|
48
47
|
graveyard: chalk.dim,
|
|
49
48
|
}[draft.suggestedType] || chalk.white;
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
// Create a nicely formatted display string
|
|
51
|
+
const title = draft.suggestedTitle.length > 50 ? draft.suggestedTitle.slice(0, 47) + '...' : draft.suggestedTitle;
|
|
52
|
+
const display = `${typeColor(draft.suggestedType.toUpperCase().padEnd(9))} | ${title.padEnd(50)} | Conf: ${conf}%`;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: display,
|
|
56
|
+
value: draft,
|
|
57
|
+
checked: conf >= 80 // Pre-check high confidence ones
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let selectedDrafts;
|
|
62
|
+
try {
|
|
63
|
+
const ans = await inquirer.prompt([{
|
|
64
|
+
type: 'checkbox',
|
|
65
|
+
name: 'selected',
|
|
66
|
+
message: 'Select drafts to ACCEPT (unselected drafts will be kept pending):',
|
|
67
|
+
choices: choices,
|
|
68
|
+
pageSize: 15
|
|
69
|
+
}]);
|
|
70
|
+
selectedDrafts = ans.selected;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.log(chalk.yellow('\nAborted.'));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (selectedDrafts.length === 0) {
|
|
77
|
+
console.log(chalk.yellow('\nNo drafts selected. Keeping all drafts pending.'));
|
|
78
|
+
} else {
|
|
79
|
+
console.log(chalk.cyan(`\nAccepting ${selectedDrafts.length} drafts...`));
|
|
80
|
+
for (const draft of selectedDrafts) {
|
|
81
|
+
const entry = acceptDraft(draft.draftId);
|
|
82
|
+
console.log(chalk.green(` ✓ ${draft.suggestedTitle} -> Saved as ${entry.id}`));
|
|
56
83
|
}
|
|
57
|
-
|
|
84
|
+
}
|
|
58
85
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
// Ask about deletions for remaining
|
|
87
|
+
const remainingDrafts = pending.filter(p => !selectedDrafts.find(s => s.draftId === p.draftId));
|
|
88
|
+
|
|
89
|
+
if (remainingDrafts.length > 0) {
|
|
90
|
+
console.log();
|
|
91
|
+
let deleteSelection;
|
|
92
|
+
try {
|
|
93
|
+
const deleteAns = await inquirer.prompt([{
|
|
94
|
+
type: 'checkbox',
|
|
95
|
+
name: 'deletes',
|
|
96
|
+
message: 'Select drafts to permanently DELETE:',
|
|
97
|
+
choices: remainingDrafts.map(draft => ({
|
|
98
|
+
name: `${draft.suggestedType.toUpperCase().padEnd(9)} | ${draft.suggestedTitle}`,
|
|
99
|
+
value: draft
|
|
100
|
+
})),
|
|
101
|
+
pageSize: 10
|
|
102
|
+
}]);
|
|
103
|
+
deleteSelection = deleteAns.deletes;
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
80
107
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
done = true;
|
|
85
|
-
} else if (action === 'edit') {
|
|
86
|
-
let edited;
|
|
87
|
-
try {
|
|
88
|
-
edited = await inquirer.prompt([
|
|
89
|
-
{
|
|
90
|
-
type: 'list',
|
|
91
|
-
name: 'type',
|
|
92
|
-
message: 'Type:',
|
|
93
|
-
choices: ['decision', 'invariant', 'gotcha', 'graveyard'],
|
|
94
|
-
default: draft.suggestedType,
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
type: 'input',
|
|
98
|
-
name: 'title',
|
|
99
|
-
message: 'Title:',
|
|
100
|
-
default: draft.suggestedTitle,
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
type: 'input',
|
|
104
|
-
name: 'context',
|
|
105
|
-
message: 'Context:',
|
|
106
|
-
default: draft.evidence,
|
|
107
|
-
},
|
|
108
|
-
]);
|
|
109
|
-
} catch (e) {
|
|
110
|
-
console.log(chalk.yellow('\nAborted.'));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Update draft on disk, then accept
|
|
115
|
-
const draftPath = path.join(LORE_DIR, 'drafts', `${draft.draftId}.json`);
|
|
116
|
-
fs.writeJsonSync(draftPath, {
|
|
117
|
-
...draft,
|
|
118
|
-
suggestedType: edited.type,
|
|
119
|
-
suggestedTitle: edited.title,
|
|
120
|
-
evidence: edited.context,
|
|
121
|
-
}, { spaces: 2 });
|
|
122
|
-
|
|
123
|
-
const entry = acceptDraft(draft.draftId);
|
|
124
|
-
console.log(chalk.green(` ✓ Saved as ${entry.id}`));
|
|
125
|
-
done = true;
|
|
126
|
-
} else if (action === 'skip') {
|
|
127
|
-
console.log(chalk.dim(' Skipped'));
|
|
128
|
-
done = true;
|
|
129
|
-
} else if (action === 'delete') {
|
|
108
|
+
if (deleteSelection.length > 0) {
|
|
109
|
+
console.log(chalk.yellow(`\nDeleting ${deleteSelection.length} drafts...`));
|
|
110
|
+
for (const draft of deleteSelection) {
|
|
130
111
|
deleteDraft(draft.draftId);
|
|
131
|
-
console.log(chalk.dim(
|
|
132
|
-
done = true;
|
|
133
|
-
} else if (action === 'quit') {
|
|
134
|
-
console.log(chalk.cyan('\n Remaining drafts saved. Run: lore drafts'));
|
|
135
|
-
return;
|
|
112
|
+
console.log(chalk.dim(` ✗ Deleted: ${draft.suggestedTitle}`));
|
|
136
113
|
}
|
|
137
114
|
}
|
|
138
|
-
console.log();
|
|
139
115
|
}
|
|
140
116
|
|
|
141
|
-
console.log(chalk.green('✓
|
|
117
|
+
console.log(chalk.green('\n✓ Draft review complete'));
|
|
142
118
|
}
|
|
143
119
|
|
|
144
120
|
module.exports = drafts;
|
package/src/commands/init.js
CHANGED
|
@@ -9,10 +9,13 @@ const { LORE_DIR, emptyIndex } = require('../lib/index');
|
|
|
9
9
|
const HOOK_CONTENT = `#!/bin/bash
|
|
10
10
|
LINECOUNT=$(git diff HEAD~1 --shortstat 2>/dev/null | grep -o '[0-9]* insertion' | grep -o '[0-9]*' || echo 0)
|
|
11
11
|
if [ "\${LINECOUNT:-0}" -gt 50 ]; then
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
# Only prompt if running in an interactive terminal
|
|
13
|
+
if [ -t 1 ] || [ -t 0 ]; then
|
|
14
|
+
echo "📖 Lore: Significant change detected. Log it? (y/n)"
|
|
15
|
+
read -r answer </dev/tty
|
|
16
|
+
if [ "$answer" = "y" ]; then
|
|
17
|
+
lore log </dev/tty
|
|
18
|
+
fi
|
|
16
19
|
fi
|
|
17
20
|
fi
|
|
18
21
|
`;
|
package/src/commands/log.js
CHANGED
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) {
|
|
@@ -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
|
@@ -2,22 +2,73 @@
|
|
|
2
2
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
|
|
5
|
+
function drawBox(content, colorFn, title) {
|
|
6
|
+
const lines = content.split('\n');
|
|
7
|
+
const maxLen = Math.max(
|
|
8
|
+
...lines.map(l => l.replace(/\x1B\[[0-9;]*m/g, '').length),
|
|
9
|
+
title ? title.length + 4 : 0
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const width = Math.min(80, Math.max(40, maxLen + 2)); // keep between 40 and 80 width
|
|
13
|
+
|
|
14
|
+
const top = title
|
|
15
|
+
? `╭─ ${title} ${'─'.repeat(Math.max(0, width - title.length - 4))}╮`
|
|
16
|
+
: `╭${'─'.repeat(width)}╮`;
|
|
17
|
+
|
|
18
|
+
console.log(colorFn(top));
|
|
19
|
+
|
|
20
|
+
for (const line of lines) {
|
|
21
|
+
const cleanLen = line.replace(/\x1B\[[0-9;]*m/g, '').length;
|
|
22
|
+
const padding = ' '.repeat(Math.max(0, width - cleanLen - 2));
|
|
23
|
+
console.log(colorFn('│ ') + line + padding + colorFn(' │'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log(colorFn(`╰${'─'.repeat(width)}╯\n`));
|
|
27
|
+
}
|
|
28
|
+
|
|
5
29
|
function printEntry(entry) {
|
|
30
|
+
let content = '';
|
|
31
|
+
|
|
32
|
+
let colorFn = chalk.white;
|
|
33
|
+
let titleColor = chalk.bold.white;
|
|
34
|
+
|
|
35
|
+
if (entry.type === 'decision') { colorFn = chalk.cyan; titleColor = chalk.bold.cyan; }
|
|
36
|
+
else if (entry.type === 'invariant') { colorFn = chalk.red; titleColor = chalk.bold.red; }
|
|
37
|
+
else if (entry.type === 'gotcha') { colorFn = chalk.yellow; titleColor = chalk.bold.yellow; }
|
|
38
|
+
else if (entry.type === 'graveyard') { colorFn = chalk.gray; titleColor = chalk.bold.gray; }
|
|
39
|
+
|
|
6
40
|
const typeLabel = `[${entry.type.toUpperCase()}]`;
|
|
7
|
-
|
|
8
|
-
|
|
41
|
+
content += `${titleColor(typeLabel)} ${titleColor(entry.title)} ${chalk.gray('(' + entry.date + ')')}\n\n`;
|
|
42
|
+
|
|
43
|
+
// Wrap context text roughly
|
|
44
|
+
const contextWords = entry.context.split(' ');
|
|
45
|
+
let currentLine = '';
|
|
46
|
+
for (const word of contextWords) {
|
|
47
|
+
if (currentLine.length + word.length > 70) {
|
|
48
|
+
content += chalk.white(currentLine) + '\n';
|
|
49
|
+
currentLine = word + ' ';
|
|
50
|
+
} else {
|
|
51
|
+
currentLine += word + ' ';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (currentLine) content += chalk.white(currentLine) + '\n';
|
|
9
55
|
|
|
10
56
|
if (entry.alternatives && entry.alternatives.length > 0) {
|
|
57
|
+
content += '\n' + chalk.yellow('Alternatives Considered:\n');
|
|
11
58
|
for (const alt of entry.alternatives) {
|
|
12
|
-
|
|
59
|
+
content += chalk.yellow(` → ${alt}\n`);
|
|
13
60
|
}
|
|
14
61
|
}
|
|
15
62
|
|
|
16
63
|
if (entry.tradeoffs) {
|
|
17
|
-
|
|
64
|
+
content += '\n' + chalk.magenta(`Tradeoffs: ${entry.tradeoffs}\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (entry.files && entry.files.length > 0) {
|
|
68
|
+
content += '\n' + chalk.dim(`Files: ${entry.files.join(', ')}\n`);
|
|
18
69
|
}
|
|
19
70
|
|
|
20
|
-
|
|
71
|
+
drawBox(content.trim(), colorFn, entry.id);
|
|
21
72
|
}
|
|
22
73
|
|
|
23
74
|
module.exports = { printEntry };
|
|
@@ -45,6 +45,11 @@ async function handler(args) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Sort entries newest-first
|
|
49
|
+
for (const type of Object.keys(byType)) {
|
|
50
|
+
byType[type].sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0));
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
const lines = [];
|
|
49
54
|
const projectName = config.project || 'this project';
|
|
50
55
|
|
package/src/mcp/tools/search.js
CHANGED
|
@@ -49,10 +49,20 @@ async function handler(args) {
|
|
|
49
49
|
].join(' ').toLowerCase();
|
|
50
50
|
|
|
51
51
|
if (searchable.includes(q)) {
|
|
52
|
-
|
|
52
|
+
// Calculate a basic relevance score for text search:
|
|
53
|
+
// 1. Term frequency of query
|
|
54
|
+
const tf = (searchable.match(new RegExp(q, 'g')) || []).length;
|
|
55
|
+
// 2. Bonus for newer entries
|
|
56
|
+
const ageDays = (Date.now() - new Date(entry.date || 0)) / (1000 * 60 * 60 * 24);
|
|
57
|
+
const recencyBonus = Math.max(0, 5 - (ageDays / 30)); // up to +5 points for newest
|
|
58
|
+
|
|
59
|
+
matches.push(Object.assign({}, entry, { _score: tf + recencyBonus }));
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
// Sort matches by relevance score descending
|
|
64
|
+
matches.sort((a, b) => (b._score || 0) - (a._score || 0));
|
|
65
|
+
|
|
56
66
|
// Try semantic search if text search found nothing and embeddings exist
|
|
57
67
|
if (matches.length === 0) {
|
|
58
68
|
try {
|