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,149 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { readIndex, writeIndex, addEntryToIndex } = require('../lib/index');
|
|
6
|
+
const { requireInit } = require('../lib/guard');
|
|
7
|
+
const { writeEntry, generateId } = require('../lib/entries');
|
|
8
|
+
const { getRecentFiles } = require('../lib/git');
|
|
9
|
+
|
|
10
|
+
async function log(options) {
|
|
11
|
+
requireInit();
|
|
12
|
+
try {
|
|
13
|
+
const index = readIndex();
|
|
14
|
+
|
|
15
|
+
let type, title, context, alternatives, tradeoffs, tags, files;
|
|
16
|
+
|
|
17
|
+
// Inline mode: all three required fields provided as flags
|
|
18
|
+
if (options.type && options.title && options.context) {
|
|
19
|
+
type = options.type;
|
|
20
|
+
title = options.title;
|
|
21
|
+
context = options.context;
|
|
22
|
+
alternatives = options.alternatives ? [options.alternatives] : [];
|
|
23
|
+
tradeoffs = options.tradeoffs || '';
|
|
24
|
+
tags = options.tags ? options.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
25
|
+
files = options.files ? options.files.split(',').map(f => f.trim()).filter(Boolean) : [];
|
|
26
|
+
} else {
|
|
27
|
+
// Interactive mode
|
|
28
|
+
const recentFiles = getRecentFiles();
|
|
29
|
+
|
|
30
|
+
const prompts = [
|
|
31
|
+
{
|
|
32
|
+
type: 'list',
|
|
33
|
+
name: 'type',
|
|
34
|
+
message: 'Entry type:',
|
|
35
|
+
choices: ['decision', 'invariant', 'graveyard', 'gotcha'],
|
|
36
|
+
default: options.type || 'decision',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'input',
|
|
40
|
+
name: 'title',
|
|
41
|
+
message: 'Title:',
|
|
42
|
+
default: options.title || '',
|
|
43
|
+
validate: v => v.trim().length > 0 || 'Title is required',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: 'input',
|
|
47
|
+
name: 'context',
|
|
48
|
+
message: 'Context (why?):',
|
|
49
|
+
default: options.context || '',
|
|
50
|
+
validate: v => v.trim().length > 0 || 'Context is required',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: 'input',
|
|
54
|
+
name: 'alternatives',
|
|
55
|
+
message: 'Alternatives considered (optional, press Enter to skip):',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'input',
|
|
59
|
+
name: 'tradeoffs',
|
|
60
|
+
message: 'Tradeoffs (optional, press Enter to skip):',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'input',
|
|
64
|
+
name: 'tags',
|
|
65
|
+
message: 'Tags (comma-separated, optional):',
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
if (recentFiles.length > 0) {
|
|
70
|
+
prompts.push({
|
|
71
|
+
type: 'checkbox',
|
|
72
|
+
name: 'files',
|
|
73
|
+
message: 'Link to files (recent git changes — space to select):',
|
|
74
|
+
choices: recentFiles,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
prompts.push({
|
|
79
|
+
type: 'input',
|
|
80
|
+
name: 'extraFiles',
|
|
81
|
+
message: 'Additional file paths (comma-separated, optional):',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const answers = await inquirer.prompt(prompts);
|
|
85
|
+
|
|
86
|
+
type = answers.type;
|
|
87
|
+
title = answers.title;
|
|
88
|
+
context = answers.context;
|
|
89
|
+
alternatives = answers.alternatives ? [answers.alternatives] : [];
|
|
90
|
+
tradeoffs = answers.tradeoffs || '';
|
|
91
|
+
tags = answers.tags ? answers.tags.split(',').map(t => t.trim()).filter(Boolean) : [];
|
|
92
|
+
files = answers.files ? [...answers.files] : [];
|
|
93
|
+
|
|
94
|
+
if (answers.extraFiles) {
|
|
95
|
+
const extra = answers.extraFiles.split(',').map(f => f.trim()).filter(Boolean);
|
|
96
|
+
files = [...files, ...extra];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const validTypes = ['decision', 'invariant', 'graveyard', 'gotcha'];
|
|
101
|
+
if (!validTypes.includes(type)) {
|
|
102
|
+
console.error(chalk.red(`Invalid type "${type}". Must be one of: ${validTypes.join(', ')}`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const id = generateId(type, title);
|
|
107
|
+
const entry = {
|
|
108
|
+
id,
|
|
109
|
+
type,
|
|
110
|
+
title,
|
|
111
|
+
date: new Date().toISOString().split('T')[0],
|
|
112
|
+
files,
|
|
113
|
+
context,
|
|
114
|
+
alternatives: alternatives.filter(Boolean),
|
|
115
|
+
tradeoffs,
|
|
116
|
+
tags,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const entryPath = writeEntry(entry);
|
|
120
|
+
addEntryToIndex(index, entry);
|
|
121
|
+
writeIndex(index);
|
|
122
|
+
|
|
123
|
+
console.log(chalk.green(`✓ Saved to ${entryPath}`));
|
|
124
|
+
|
|
125
|
+
// Auto-embed if Ollama is available and autoEmbed is enabled
|
|
126
|
+
try {
|
|
127
|
+
const { readConfig } = require('../lib/config');
|
|
128
|
+
const config = readConfig();
|
|
129
|
+
if (config.embed && config.embed.autoEmbed) {
|
|
130
|
+
const { generateEmbedding, storeEmbedding } = require('../lib/embeddings');
|
|
131
|
+
const text = [title, context, ...alternatives.filter(Boolean), tradeoffs, ...tags].join(' ');
|
|
132
|
+
const vector = await generateEmbedding(text);
|
|
133
|
+
storeEmbedding(id, vector);
|
|
134
|
+
console.log(chalk.dim(' (embedding stored)'));
|
|
135
|
+
}
|
|
136
|
+
} catch (e) {
|
|
137
|
+
// Ollama not running — skip silently
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
if (e.message && e.message.includes('force closed')) {
|
|
141
|
+
console.log(chalk.yellow('\nAborted.'));
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
console.error(chalk.red(`Failed to log entry: ${e.message}`));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = log;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const { mineFile, mineDirectory } = require('../watcher/comments');
|
|
7
|
+
const { requireInit } = require('../lib/guard');
|
|
8
|
+
|
|
9
|
+
function mine(targetPath) {
|
|
10
|
+
requireInit();
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
const target = targetPath || '.';
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const abs = path.resolve(target);
|
|
16
|
+
const stat = fs.statSync(abs);
|
|
17
|
+
let count = 0;
|
|
18
|
+
|
|
19
|
+
if (stat.isDirectory()) {
|
|
20
|
+
console.log(chalk.cyan(`📖 Mining comments in ${target} ...`));
|
|
21
|
+
count = mineDirectory(abs, projectRoot);
|
|
22
|
+
} else {
|
|
23
|
+
console.log(chalk.cyan(`📖 Mining comments in ${target} ...`));
|
|
24
|
+
count = mineFile(abs, projectRoot).length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (count === 0) {
|
|
28
|
+
console.log(chalk.green('✓ No significant comments found'));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(chalk.green(`📖 Found ${count} draft entr${count === 1 ? 'y' : 'ies'} — review with: lore drafts`));
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(chalk.red(`Failed to mine: ${e.message}`));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = mine;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { readIndex } = require('../lib/index');
|
|
5
|
+
const { readEntry } = require('../lib/entries');
|
|
6
|
+
const { checkStaleness } = require('../lib/stale');
|
|
7
|
+
const { readConfig } = require('../lib/config');
|
|
8
|
+
const { getDaysSinceLastSession, updateLastSession } = require('../lib/sessions');
|
|
9
|
+
const { requireInit } = require('../lib/guard');
|
|
10
|
+
|
|
11
|
+
function onboard(options) {
|
|
12
|
+
requireInit();
|
|
13
|
+
try {
|
|
14
|
+
const config = readConfig();
|
|
15
|
+
const index = readIndex();
|
|
16
|
+
const minDays = parseInt(options.days, 10) || 0;
|
|
17
|
+
|
|
18
|
+
const daysSince = getDaysSinceLastSession();
|
|
19
|
+
|
|
20
|
+
// If --days is set, only show onboarding if away long enough
|
|
21
|
+
if (minDays > 0 && (daysSince === null || daysSince < minDays)) {
|
|
22
|
+
console.log(chalk.cyan(`📖 Skipping onboarding (last session ${daysSince !== null ? daysSince + ' day(s) ago' : 'recently'})`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Update session tracking
|
|
27
|
+
updateLastSession();
|
|
28
|
+
|
|
29
|
+
const projectName = config.project || 'this project';
|
|
30
|
+
const byType = { decision: [], invariant: [], gotcha: [], graveyard: [] };
|
|
31
|
+
const staleItems = [];
|
|
32
|
+
|
|
33
|
+
for (const entryPath of Object.values(index.entries)) {
|
|
34
|
+
const entry = readEntry(entryPath);
|
|
35
|
+
if (!entry) continue;
|
|
36
|
+
if (byType[entry.type]) byType[entry.type].push(entry);
|
|
37
|
+
const staleFiles = checkStaleness(entry);
|
|
38
|
+
if (staleFiles.length > 0) staleItems.push({ entry, staleFiles });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const total = Object.values(byType).reduce((sum, arr) => sum + arr.length, 0);
|
|
42
|
+
|
|
43
|
+
if (daysSince !== null && daysSince >= 3) {
|
|
44
|
+
console.log(chalk.cyan(`\n📖 Welcome back to ${projectName}!`));
|
|
45
|
+
console.log(chalk.dim(` You've been away for ${daysSince} day${daysSince === 1 ? '' : 's'}\n`));
|
|
46
|
+
} else {
|
|
47
|
+
console.log(chalk.cyan(`\n📖 Lore — ${projectName}\n`));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (total === 0) {
|
|
51
|
+
console.log(chalk.yellow(' No lore entries yet. Run: lore log'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Decisions
|
|
56
|
+
if (byType.decision.length > 0) {
|
|
57
|
+
console.log(chalk.bold(`Architectural Decisions (${byType.decision.length}):`));
|
|
58
|
+
for (const e of byType.decision.slice(0, 5)) {
|
|
59
|
+
console.log(chalk.white(` • ${e.title}`));
|
|
60
|
+
if (e.context) {
|
|
61
|
+
const summary = e.context.split('\n')[0].slice(0, 80);
|
|
62
|
+
console.log(chalk.dim(` ${summary}${e.context.length > 80 ? '…' : ''}`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (byType.decision.length > 5) {
|
|
66
|
+
console.log(chalk.dim(` …and ${byType.decision.length - 5} more`));
|
|
67
|
+
}
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Invariants
|
|
72
|
+
if (byType.invariant.length > 0) {
|
|
73
|
+
console.log(chalk.bold.red(`Invariants — Never Break These (${byType.invariant.length}):`));
|
|
74
|
+
for (const e of byType.invariant) {
|
|
75
|
+
console.log(chalk.white(` • ${e.title}`));
|
|
76
|
+
}
|
|
77
|
+
console.log();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Gotchas
|
|
81
|
+
if (byType.gotcha.length > 0) {
|
|
82
|
+
console.log(chalk.bold.yellow(`Gotchas (${byType.gotcha.length}):`));
|
|
83
|
+
for (const e of byType.gotcha.slice(0, 3)) {
|
|
84
|
+
console.log(chalk.white(` • ${e.title}`));
|
|
85
|
+
}
|
|
86
|
+
if (byType.gotcha.length > 3) {
|
|
87
|
+
console.log(chalk.dim(` …and ${byType.gotcha.length - 3} more`));
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Stale warnings
|
|
93
|
+
if (staleItems.length > 0) {
|
|
94
|
+
console.log(chalk.bold.yellow(`⚠ Stale Entries (${staleItems.length}):`));
|
|
95
|
+
for (const { entry, staleFiles } of staleItems.slice(0, 5)) {
|
|
96
|
+
const days = staleFiles[0].daysAgo;
|
|
97
|
+
const daysText = days === 0 ? 'today' : `${days}d ago`;
|
|
98
|
+
console.log(chalk.yellow(` • ${entry.title} — ${staleFiles[0].filepath} changed ${daysText}`));
|
|
99
|
+
}
|
|
100
|
+
console.log(chalk.cyan(' Run: lore stale for full details'));
|
|
101
|
+
console.log();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(chalk.dim(`Total: ${total} entries | lore search <query> | lore why <file>`));
|
|
105
|
+
console.log();
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error(chalk.red(`Failed to run onboard: ${e.message}`));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = onboard;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { computeScore, saveScore, loadHistory } = require('../lib/scorer');
|
|
5
|
+
const { requireInit } = require('../lib/guard');
|
|
6
|
+
|
|
7
|
+
function label(score) {
|
|
8
|
+
if (score >= 90) return 'Excellent';
|
|
9
|
+
if (score >= 75) return 'Good';
|
|
10
|
+
if (score >= 60) return 'Fair';
|
|
11
|
+
if (score >= 40) return 'Needs Work';
|
|
12
|
+
return 'Low';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function bar(value, width = 20) {
|
|
16
|
+
const filled = Math.round((value / 100) * width);
|
|
17
|
+
return '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, width - filled));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function trend(history) {
|
|
21
|
+
if (history.length < 2) return '';
|
|
22
|
+
const last = history[history.length - 1].score;
|
|
23
|
+
const prev = history[history.length - 2].score;
|
|
24
|
+
if (last > prev) return ' (improving)';
|
|
25
|
+
if (last < prev) return ' (declining)';
|
|
26
|
+
return ' (stable)';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function score() {
|
|
30
|
+
requireInit();
|
|
31
|
+
try {
|
|
32
|
+
const result = computeScore();
|
|
33
|
+
const history = saveScore(result);
|
|
34
|
+
|
|
35
|
+
const scoreLabel = label(result.score);
|
|
36
|
+
console.log(chalk.cyan(`\n📖 Lore Score: ${chalk.bold(result.score)}/100 (${scoreLabel})`));
|
|
37
|
+
console.log(chalk.dim('────────────────────────────────'));
|
|
38
|
+
console.log();
|
|
39
|
+
|
|
40
|
+
// Coverage
|
|
41
|
+
const cColor = result.coverage >= 70 ? chalk.green : result.coverage >= 40 ? chalk.yellow : chalk.red;
|
|
42
|
+
console.log(cColor(`Coverage ${result.coverage}/100`));
|
|
43
|
+
console.log(chalk.dim(` ${bar(result.coverage)} ${result.coveredModules}/${result.activeModules} active modules documented`));
|
|
44
|
+
if (result.topUnlogged.length > 0) {
|
|
45
|
+
console.log(chalk.yellow(' Highest risk unlogged modules:'));
|
|
46
|
+
for (const { module: mod, commits } of result.topUnlogged) {
|
|
47
|
+
console.log(chalk.yellow(` ${mod} — ${commits} commits`));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
console.log();
|
|
51
|
+
|
|
52
|
+
// Freshness
|
|
53
|
+
const fColor = result.freshness >= 70 ? chalk.green : result.freshness >= 40 ? chalk.yellow : chalk.red;
|
|
54
|
+
console.log(fColor(`Freshness ${result.freshness}/100`));
|
|
55
|
+
console.log(chalk.dim(` ${bar(result.freshness)}`));
|
|
56
|
+
console.log();
|
|
57
|
+
|
|
58
|
+
// Depth
|
|
59
|
+
const dColor = result.depth >= 70 ? chalk.green : result.depth >= 40 ? chalk.yellow : chalk.red;
|
|
60
|
+
console.log(dColor(`Depth ${result.depth}/100`));
|
|
61
|
+
console.log(chalk.dim(` ${bar(result.depth)}`));
|
|
62
|
+
console.log();
|
|
63
|
+
|
|
64
|
+
console.log(chalk.dim('────────────────────────────────'));
|
|
65
|
+
|
|
66
|
+
// History
|
|
67
|
+
if (history.length > 1) {
|
|
68
|
+
const recent = history.slice(-3).map(h => h.score).join(' → ');
|
|
69
|
+
console.log(chalk.dim(`Score history: ${recent}${trend(history)}`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Tip
|
|
73
|
+
if (result.topUnlogged.length > 0) {
|
|
74
|
+
console.log(chalk.cyan(`Tip: Log memory for ${result.topUnlogged[0].module} — highest risk unlogged module`));
|
|
75
|
+
} else if (result.freshness < 60) {
|
|
76
|
+
console.log(chalk.cyan('Tip: Review stale entries — run: lore stale'));
|
|
77
|
+
} else if (result.depth < 60) {
|
|
78
|
+
console.log(chalk.cyan('Tip: Add invariants and gotchas — run: lore log --type invariant'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error(chalk.red(`Failed to compute score: ${e.message}`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = score;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { readIndex } = require('../lib/index');
|
|
5
|
+
const { readEntry } = require('../lib/entries');
|
|
6
|
+
const { printEntry } = require('../lib/format');
|
|
7
|
+
const { requireInit } = require('../lib/guard');
|
|
8
|
+
|
|
9
|
+
function search(query) {
|
|
10
|
+
requireInit();
|
|
11
|
+
try {
|
|
12
|
+
const index = readIndex();
|
|
13
|
+
const q = query.toLowerCase();
|
|
14
|
+
const matches = [];
|
|
15
|
+
|
|
16
|
+
for (const entryPath of Object.values(index.entries)) {
|
|
17
|
+
const entry = readEntry(entryPath);
|
|
18
|
+
if (!entry) continue;
|
|
19
|
+
|
|
20
|
+
const searchable = [
|
|
21
|
+
entry.title,
|
|
22
|
+
entry.context,
|
|
23
|
+
...(entry.alternatives || []),
|
|
24
|
+
entry.tradeoffs || '',
|
|
25
|
+
...(entry.tags || []),
|
|
26
|
+
].join(' ').toLowerCase();
|
|
27
|
+
|
|
28
|
+
if (searchable.includes(q)) {
|
|
29
|
+
matches.push(entry);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (matches.length === 0) {
|
|
34
|
+
console.log(chalk.cyan(`📖 No entries found for "${query}"`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(chalk.cyan(`\n─── Search results for "${query}" ───\n`));
|
|
39
|
+
for (const entry of matches) {
|
|
40
|
+
printEntry(entry);
|
|
41
|
+
}
|
|
42
|
+
console.log(chalk.cyan('─── End results ───\n'));
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error(chalk.red(`Failed to search: ${e.message}`));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = search;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { requireInit } = require('../lib/guard');
|
|
5
|
+
|
|
6
|
+
async function serve(options) {
|
|
7
|
+
requireInit();
|
|
8
|
+
try {
|
|
9
|
+
const { startServer } = require('../mcp/server');
|
|
10
|
+
if (!options.quiet) {
|
|
11
|
+
process.stderr.write(chalk.green('📖 Lore MCP server starting on stdio\n'));
|
|
12
|
+
process.stderr.write(chalk.cyan(' Add to Claude Code settings: { "command": "lore serve", "args": [] }\n'));
|
|
13
|
+
}
|
|
14
|
+
await startServer();
|
|
15
|
+
} catch (e) {
|
|
16
|
+
process.stderr.write(chalk.red(`Failed to start server: ${e.message}\n`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = serve;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { readIndex } = require('../lib/index');
|
|
5
|
+
const { readEntry } = require('../lib/entries');
|
|
6
|
+
const { checkStaleness } = require('../lib/stale');
|
|
7
|
+
const { requireInit } = require('../lib/guard');
|
|
8
|
+
|
|
9
|
+
function stale() {
|
|
10
|
+
requireInit();
|
|
11
|
+
try {
|
|
12
|
+
const index = readIndex();
|
|
13
|
+
let found = false;
|
|
14
|
+
|
|
15
|
+
for (const [id, entryPath] of Object.entries(index.entries)) {
|
|
16
|
+
const entry = readEntry(entryPath);
|
|
17
|
+
if (!entry) continue;
|
|
18
|
+
|
|
19
|
+
const staleFiles = checkStaleness(entry);
|
|
20
|
+
if (staleFiles.length > 0) {
|
|
21
|
+
found = true;
|
|
22
|
+
console.log(chalk.yellow(`\n⚠ ${entry.id}`));
|
|
23
|
+
console.log(` Title: ${entry.title}`);
|
|
24
|
+
for (const s of staleFiles) {
|
|
25
|
+
const daysText = s.daysAgo === 0 ? 'today' : `${s.daysAgo} day${s.daysAgo === 1 ? '' : 's'} ago`;
|
|
26
|
+
console.log(chalk.yellow(` File changed: ${s.filepath} (${daysText})`));
|
|
27
|
+
}
|
|
28
|
+
console.log(chalk.cyan(` Suggest: lore edit ${entry.id}`));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!found) {
|
|
33
|
+
console.log(chalk.green('✓ No stale entries found'));
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(chalk.red(`Failed to run stale: ${e.message}`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = stale;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { readIndex } = require('../lib/index');
|
|
5
|
+
const { readEntry } = require('../lib/entries');
|
|
6
|
+
const { checkStaleness } = require('../lib/stale');
|
|
7
|
+
const { getDraftCount } = require('../lib/drafts');
|
|
8
|
+
const { requireInit } = require('../lib/guard');
|
|
9
|
+
|
|
10
|
+
function status() {
|
|
11
|
+
requireInit();
|
|
12
|
+
try {
|
|
13
|
+
const index = readIndex();
|
|
14
|
+
const counts = { decision: 0, invariant: 0, graveyard: 0, gotcha: 0 };
|
|
15
|
+
const staleItems = [];
|
|
16
|
+
|
|
17
|
+
for (const entryPath of Object.values(index.entries)) {
|
|
18
|
+
const entry = readEntry(entryPath);
|
|
19
|
+
if (!entry) continue;
|
|
20
|
+
|
|
21
|
+
if (counts[entry.type] !== undefined) {
|
|
22
|
+
counts[entry.type]++;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const stale of checkStaleness(entry)) {
|
|
26
|
+
staleItems.push({ entry, ...stale });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const draftCount = getDraftCount();
|
|
31
|
+
|
|
32
|
+
console.log(chalk.cyan('\n📖 Lore Status'));
|
|
33
|
+
console.log(` decisions: ${counts.decision}`);
|
|
34
|
+
console.log(` invariants: ${counts.invariant}`);
|
|
35
|
+
console.log(` graveyard: ${counts.graveyard}`);
|
|
36
|
+
console.log(` gotchas: ${counts.gotcha}`);
|
|
37
|
+
if (draftCount > 0) {
|
|
38
|
+
console.log(chalk.yellow(` drafts: ${draftCount} pending — run: lore drafts`));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (staleItems.length > 0) {
|
|
42
|
+
console.log(chalk.yellow('\n⚠️ Stale entries (linked files changed since entry was written):'));
|
|
43
|
+
for (const item of staleItems) {
|
|
44
|
+
const daysText = item.daysAgo === 0 ? 'today' : `${item.daysAgo} day${item.daysAgo === 1 ? '' : 's'} ago`;
|
|
45
|
+
console.log(chalk.yellow(` ${item.entry.id} → ${item.filepath} changed ${daysText}`));
|
|
46
|
+
}
|
|
47
|
+
console.log(chalk.cyan(' Run: lore stale for details'));
|
|
48
|
+
} else {
|
|
49
|
+
console.log(chalk.green('\n✓ All entries up to date'));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(chalk.red(`Failed to run status: ${e.message}`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = status;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { requireInit } = require('../lib/guard');
|
|
7
|
+
const { LORE_DIR } = require('../lib/index');
|
|
8
|
+
|
|
9
|
+
const pidFile = () => path.join(LORE_DIR, 'watcher.pid');
|
|
10
|
+
const logFile = () => path.join(LORE_DIR, 'watcher.log');
|
|
11
|
+
|
|
12
|
+
function watch(options) {
|
|
13
|
+
requireInit();
|
|
14
|
+
|
|
15
|
+
// Stop daemon
|
|
16
|
+
if (options.stop) {
|
|
17
|
+
const pf = pidFile();
|
|
18
|
+
if (!fs.existsSync(pf)) {
|
|
19
|
+
console.log(chalk.yellow('No watcher running'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const pid = parseInt(fs.readFileSync(pf, 'utf8').trim(), 10);
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 'SIGTERM');
|
|
25
|
+
fs.removeSync(pf);
|
|
26
|
+
console.log(chalk.green(`✓ Stopped watcher (PID ${pid})`));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.log(chalk.yellow(`Watcher process not found (PID ${pid}) — cleaning up`));
|
|
29
|
+
fs.removeSync(pf);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Internal worker mode (spawned as detached child)
|
|
35
|
+
if (options.daemonWorker) {
|
|
36
|
+
const { startWatcher } = require('../watcher/index');
|
|
37
|
+
startWatcher({ quiet: true, logFile: logFile() });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Daemon mode: spawn detached child
|
|
42
|
+
if (options.daemon) {
|
|
43
|
+
const { spawn } = require('child_process');
|
|
44
|
+
const lf = logFile();
|
|
45
|
+
fs.ensureFileSync(lf);
|
|
46
|
+
const logFd = fs.openSync(lf, 'a');
|
|
47
|
+
|
|
48
|
+
const child = spawn(process.execPath, [process.argv[1], 'watch', '--daemon-worker'], {
|
|
49
|
+
detached: true,
|
|
50
|
+
stdio: ['ignore', logFd, logFd],
|
|
51
|
+
});
|
|
52
|
+
child.unref();
|
|
53
|
+
fs.closeSync(logFd);
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(pidFile(), String(child.pid));
|
|
56
|
+
console.log(chalk.green(`✓ Lore watcher started (PID ${child.pid})`));
|
|
57
|
+
console.log(chalk.dim(` Logging to: ${lf}`));
|
|
58
|
+
console.log(chalk.dim(' Stop with: lore watch --stop'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Foreground mode
|
|
63
|
+
const { startWatcher } = require('../watcher/index');
|
|
64
|
+
startWatcher({});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = watch;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { readIndex } = require('../lib/index');
|
|
6
|
+
const { requireInit } = require('../lib/guard');
|
|
7
|
+
const { readEntry } = require('../lib/entries');
|
|
8
|
+
const { printEntry } = require('../lib/format');
|
|
9
|
+
|
|
10
|
+
function why(filepath) {
|
|
11
|
+
requireInit();
|
|
12
|
+
try {
|
|
13
|
+
const index = readIndex();
|
|
14
|
+
const normalized = filepath.replace(/^\.\//, '');
|
|
15
|
+
|
|
16
|
+
const entryIds = new Set();
|
|
17
|
+
|
|
18
|
+
// Exact match (also try with trailing slash for bare directory names)
|
|
19
|
+
if (index.files[normalized]) {
|
|
20
|
+
index.files[normalized].forEach(id => entryIds.add(id));
|
|
21
|
+
}
|
|
22
|
+
if (!normalized.endsWith('/') && index.files[normalized + '/']) {
|
|
23
|
+
index.files[normalized + '/'].forEach(id => entryIds.add(id));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Walk up parent directories
|
|
27
|
+
let dir = path.dirname(normalized.replace(/\/$/, ''));
|
|
28
|
+
while (dir && dir !== '.') {
|
|
29
|
+
const dirKey = dir + '/';
|
|
30
|
+
if (index.files[dirKey]) {
|
|
31
|
+
index.files[dirKey].forEach(id => entryIds.add(id));
|
|
32
|
+
}
|
|
33
|
+
dir = path.dirname(dir);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (entryIds.size === 0) {
|
|
37
|
+
console.log(chalk.cyan(`📖 No Lore entries linked to ${filepath} yet. Run: lore log`));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(chalk.cyan(`\n─── Lore Context: ${filepath} ───\n`));
|
|
42
|
+
|
|
43
|
+
for (const id of entryIds) {
|
|
44
|
+
const entryPath = index.entries[id];
|
|
45
|
+
if (!entryPath) continue;
|
|
46
|
+
const entry = readEntry(entryPath);
|
|
47
|
+
if (!entry) continue;
|
|
48
|
+
printEntry(entry);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.cyan('─── End Lore Context ───\n'));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error(chalk.red(`Failed to run why: ${e.message}`));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = why;
|