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
package/bin/lore.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { Command } = require('commander');
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('lore')
|
|
10
|
+
.description('Persistent project memory for developers')
|
|
11
|
+
.version('0.1.0')
|
|
12
|
+
.action(() => program.outputHelp());
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command('init')
|
|
16
|
+
.description('Initialize Lore in the current project')
|
|
17
|
+
.action(require('../src/commands/init'));
|
|
18
|
+
|
|
19
|
+
program
|
|
20
|
+
.command('log')
|
|
21
|
+
.description('Log a new decision, invariant, graveyard entry, or gotcha')
|
|
22
|
+
.option('--type <type>', 'Entry type (decision|invariant|graveyard|gotcha)')
|
|
23
|
+
.option('--title <title>', 'Entry title')
|
|
24
|
+
.option('--context <context>', 'Context/reason')
|
|
25
|
+
.option('--alternatives <alternatives>', 'Alternatives considered')
|
|
26
|
+
.option('--tradeoffs <tradeoffs>', 'Tradeoffs')
|
|
27
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
28
|
+
.option('--files <files>', 'Comma-separated file paths')
|
|
29
|
+
.action(require('../src/commands/log'));
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('why <filepath>')
|
|
33
|
+
.description('Show all Lore entries linked to a file or directory')
|
|
34
|
+
.action(require('../src/commands/why'));
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('status')
|
|
38
|
+
.description('Show entry counts and stale warnings')
|
|
39
|
+
.action(require('../src/commands/status'));
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command('stale')
|
|
43
|
+
.description('Show detailed stale entry report')
|
|
44
|
+
.action(require('../src/commands/stale'));
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.command('search <query>')
|
|
48
|
+
.description('Search all entries for a keyword')
|
|
49
|
+
.action(require('../src/commands/search'));
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command('export')
|
|
53
|
+
.description('Export all entries to CLAUDE.md at project root')
|
|
54
|
+
.action(require('../src/commands/export'));
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('edit <id>')
|
|
58
|
+
.description('Open an entry JSON in VSCode')
|
|
59
|
+
.action(require('../src/commands/edit'));
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command('serve')
|
|
63
|
+
.description('Start the Lore MCP server (stdio) for use with Claude Code')
|
|
64
|
+
.option('-q, --quiet', 'Suppress startup messages (use when piped into MCP client)')
|
|
65
|
+
.action(require('../src/commands/serve'));
|
|
66
|
+
|
|
67
|
+
program
|
|
68
|
+
.command('embed')
|
|
69
|
+
.description('Build semantic search index using Ollama (requires ollama pull nomic-embed-text)')
|
|
70
|
+
.action(require('../src/commands/embed'));
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command('onboard')
|
|
74
|
+
.description('Print a re-onboarding brief for this project')
|
|
75
|
+
.option('--days <n>', 'Only show if away for at least N days', '0')
|
|
76
|
+
.action(require('../src/commands/onboard'));
|
|
77
|
+
|
|
78
|
+
program
|
|
79
|
+
.command('watch')
|
|
80
|
+
.description('Watch project for decisions and mine comments passively')
|
|
81
|
+
.option('-d, --daemon', 'Run as background daemon')
|
|
82
|
+
.option('--stop', 'Stop the running background daemon')
|
|
83
|
+
.option('--daemon-worker', { hidden: true })
|
|
84
|
+
.action(require('../src/commands/watch'));
|
|
85
|
+
|
|
86
|
+
program
|
|
87
|
+
.command('mine [path]')
|
|
88
|
+
.description('Mine source files for lore-worthy comments and create drafts')
|
|
89
|
+
.action(require('../src/commands/mine'));
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.command('drafts')
|
|
93
|
+
.description('Review and approve auto-captured draft entries')
|
|
94
|
+
.option('--auto', 'Auto-accept drafts with confidence >= 0.8')
|
|
95
|
+
.action(require('../src/commands/drafts'));
|
|
96
|
+
|
|
97
|
+
program
|
|
98
|
+
.command('score')
|
|
99
|
+
.description('Show the Lore Score ā memory health metric for this project')
|
|
100
|
+
.action(require('../src/commands/score'));
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command('graph [filepath]')
|
|
104
|
+
.description('Show or build the module dependency graph')
|
|
105
|
+
.option('--build', 'Rebuild the full graph from source')
|
|
106
|
+
.action(require('../src/commands/graph'));
|
|
107
|
+
|
|
108
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lore-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"main": "bin/lore.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lore": "./bin/lore.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"lore",
|
|
19
|
+
"memory",
|
|
20
|
+
"decisions",
|
|
21
|
+
"architecture",
|
|
22
|
+
"ai",
|
|
23
|
+
"claude",
|
|
24
|
+
"mcp",
|
|
25
|
+
"context",
|
|
26
|
+
"knowledge-base",
|
|
27
|
+
"developer-tools",
|
|
28
|
+
"cli"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/YOUR_USERNAME/lore-cli.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/YOUR_USERNAME/lore-cli#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/YOUR_USERNAME/lore-cli/issues"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@babel/parser": "^7.29.0",
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
42
|
+
"better-sqlite3": "^12.6.2",
|
|
43
|
+
"chalk": "^4",
|
|
44
|
+
"chokidar": "^3.6.0",
|
|
45
|
+
"commander": "^11",
|
|
46
|
+
"fs-extra": "^11",
|
|
47
|
+
"glob": "^10.5.0",
|
|
48
|
+
"inquirer": "^8",
|
|
49
|
+
"js-yaml": "^4",
|
|
50
|
+
"natural": "^6.12.0",
|
|
51
|
+
"ollama": "^0.6.3"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const { listDrafts, acceptDraft, deleteDraft } = require('../lib/drafts');
|
|
8
|
+
const { LORE_DIR } = require('../lib/index');
|
|
9
|
+
const { requireInit } = require('../lib/guard');
|
|
10
|
+
|
|
11
|
+
async function drafts(options) {
|
|
12
|
+
requireInit();
|
|
13
|
+
|
|
14
|
+
const pending = listDrafts();
|
|
15
|
+
|
|
16
|
+
if (pending.length === 0) {
|
|
17
|
+
console.log(chalk.green('ā No pending drafts'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Auto mode: accept high-confidence, leave rest
|
|
22
|
+
if (options.auto) {
|
|
23
|
+
let accepted = 0;
|
|
24
|
+
for (const draft of pending) {
|
|
25
|
+
if ((draft.confidence || 0) >= 0.8) {
|
|
26
|
+
acceptDraft(draft.draftId);
|
|
27
|
+
console.log(chalk.green(` ā ${draft.suggestedTitle}`));
|
|
28
|
+
accepted++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const remaining = pending.length - accepted;
|
|
32
|
+
console.log(chalk.green(`\nš Auto-accepted ${accepted} draft${accepted === 1 ? '' : 's'}`));
|
|
33
|
+
if (remaining > 0) {
|
|
34
|
+
console.log(chalk.cyan(` ${remaining} remaining ā run: lore drafts`));
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(chalk.cyan(`\nš ${pending.length} pending draft${pending.length === 1 ? '' : 's'}\n`));
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < pending.length; i++) {
|
|
42
|
+
const draft = pending[i];
|
|
43
|
+
const conf = Math.round((draft.confidence || 0) * 100);
|
|
44
|
+
const typeColor = {
|
|
45
|
+
decision: chalk.blue,
|
|
46
|
+
invariant: chalk.red,
|
|
47
|
+
gotcha: chalk.yellow,
|
|
48
|
+
graveyard: chalk.dim,
|
|
49
|
+
}[draft.suggestedType] || chalk.white;
|
|
50
|
+
|
|
51
|
+
console.log(chalk.cyan(`[${i + 1}/${pending.length}] SUGGESTED: ${typeColor(draft.suggestedType.toUpperCase())} (confidence: ${conf}%)`));
|
|
52
|
+
console.log(` ${chalk.bold('Title:')} ${draft.suggestedTitle}`);
|
|
53
|
+
console.log(` ${chalk.bold('Evidence:')} ${draft.evidence}`);
|
|
54
|
+
if (draft.files && draft.files.length > 0) {
|
|
55
|
+
console.log(` ${chalk.bold('Files:')} ${draft.files.join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
console.log();
|
|
58
|
+
|
|
59
|
+
let done = false;
|
|
60
|
+
while (!done) {
|
|
61
|
+
let action;
|
|
62
|
+
try {
|
|
63
|
+
const ans = await inquirer.prompt([{
|
|
64
|
+
type: 'list',
|
|
65
|
+
name: 'action',
|
|
66
|
+
message: 'Action:',
|
|
67
|
+
choices: [
|
|
68
|
+
{ name: '[a] Accept', value: 'accept' },
|
|
69
|
+
{ name: '[e] Edit then save', value: 'edit' },
|
|
70
|
+
{ name: '[s] Skip', value: 'skip' },
|
|
71
|
+
{ name: '[d] Delete', value: 'delete' },
|
|
72
|
+
{ name: '[q] Quit', value: 'quit' },
|
|
73
|
+
],
|
|
74
|
+
}]);
|
|
75
|
+
action = ans.action;
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.log(chalk.yellow('\nAborted.'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (action === 'accept') {
|
|
82
|
+
const entry = acceptDraft(draft.draftId);
|
|
83
|
+
console.log(chalk.green(` ā Saved as ${entry.id}`));
|
|
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') {
|
|
130
|
+
deleteDraft(draft.draftId);
|
|
131
|
+
console.log(chalk.dim(' Deleted'));
|
|
132
|
+
done = true;
|
|
133
|
+
} else if (action === 'quit') {
|
|
134
|
+
console.log(chalk.cyan('\n Remaining drafts saved. Run: lore drafts'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
console.log();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log(chalk.green('ā All drafts reviewed'));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = drafts;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { readIndex } = require('../lib/index');
|
|
6
|
+
const { requireInit } = require('../lib/guard');
|
|
7
|
+
|
|
8
|
+
function edit(id) {
|
|
9
|
+
requireInit();
|
|
10
|
+
try {
|
|
11
|
+
const index = readIndex();
|
|
12
|
+
const entryPath = index.entries[id];
|
|
13
|
+
|
|
14
|
+
if (!entryPath) {
|
|
15
|
+
console.error(chalk.red(`Entry not found: ${id}`));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
execSync(`code "${entryPath}"`, { stdio: 'inherit' });
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.log(chalk.yellow(`Could not open VSCode. File path: ${entryPath}`));
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error(chalk.red(`Failed to edit: ${e.message}`));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = edit;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { readIndex } = require('../lib/index');
|
|
5
|
+
const { readEntry } = require('../lib/entries');
|
|
6
|
+
const { generateEmbedding, storeEmbedding } = require('../lib/embeddings');
|
|
7
|
+
const { requireInit } = require('../lib/guard');
|
|
8
|
+
|
|
9
|
+
async function embed() {
|
|
10
|
+
requireInit();
|
|
11
|
+
try {
|
|
12
|
+
const index = readIndex();
|
|
13
|
+
const ids = Object.keys(index.entries);
|
|
14
|
+
|
|
15
|
+
if (ids.length === 0) {
|
|
16
|
+
console.log(chalk.yellow('No entries to embed. Run lore log first.'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log(chalk.cyan(`š Embedding ${ids.length} entries using Ollama...`));
|
|
21
|
+
let success = 0;
|
|
22
|
+
let failed = 0;
|
|
23
|
+
|
|
24
|
+
for (const id of ids) {
|
|
25
|
+
const entryPath = index.entries[id];
|
|
26
|
+
const entry = readEntry(entryPath);
|
|
27
|
+
if (!entry) continue;
|
|
28
|
+
|
|
29
|
+
const text = [
|
|
30
|
+
entry.title,
|
|
31
|
+
entry.context,
|
|
32
|
+
...(entry.alternatives || []),
|
|
33
|
+
entry.tradeoffs || '',
|
|
34
|
+
...(entry.tags || []),
|
|
35
|
+
].join(' ');
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const vector = await generateEmbedding(text);
|
|
39
|
+
storeEmbedding(id, vector);
|
|
40
|
+
process.stdout.write(chalk.green('.'));
|
|
41
|
+
success++;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
process.stdout.write(chalk.red('x'));
|
|
44
|
+
failed++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(chalk.green(`ā Embedded ${success}/${ids.length} entries`));
|
|
50
|
+
if (failed > 0) {
|
|
51
|
+
console.log(chalk.yellow(`ā ${failed} failed (is Ollama running? Run: ollama pull nomic-embed-text)`));
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e.message && e.message.includes('Ollama')) {
|
|
55
|
+
console.error(chalk.red('Ollama not available. Start Ollama and run: ollama pull nomic-embed-text'));
|
|
56
|
+
} else {
|
|
57
|
+
console.error(chalk.red(`Failed to embed: ${e.message}`));
|
|
58
|
+
}
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = embed;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { readIndex } = require('../lib/index');
|
|
6
|
+
const { readEntry } = require('../lib/entries');
|
|
7
|
+
const { requireInit } = require('../lib/guard');
|
|
8
|
+
|
|
9
|
+
function exportLore() {
|
|
10
|
+
requireInit();
|
|
11
|
+
try {
|
|
12
|
+
const index = readIndex();
|
|
13
|
+
const allEntries = [];
|
|
14
|
+
|
|
15
|
+
for (const entryPath of Object.values(index.entries)) {
|
|
16
|
+
const entry = readEntry(entryPath);
|
|
17
|
+
if (entry) allEntries.push(entry);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const byType = {
|
|
21
|
+
decision: allEntries.filter(e => e.type === 'decision'),
|
|
22
|
+
invariant: allEntries.filter(e => e.type === 'invariant'),
|
|
23
|
+
graveyard: allEntries.filter(e => e.type === 'graveyard'),
|
|
24
|
+
gotcha: allEntries.filter(e => e.type === 'gotcha'),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let md = '# Project Lore\n';
|
|
28
|
+
md += '> Auto-generated by lore export. Do not edit manually ā run `lore export` to regenerate.\n\n';
|
|
29
|
+
|
|
30
|
+
if (byType.decision.length > 0) {
|
|
31
|
+
md += '## Architectural Decisions\n';
|
|
32
|
+
for (const e of byType.decision) {
|
|
33
|
+
md += `- **${e.title}** (\`${e.files.join(', ')}\`): ${e.context}\n`;
|
|
34
|
+
if (e.alternatives && e.alternatives.length > 0) {
|
|
35
|
+
md += ` - Alternatives considered: ${e.alternatives.join('; ')}\n`;
|
|
36
|
+
}
|
|
37
|
+
if (e.tradeoffs) {
|
|
38
|
+
md += ` - Tradeoffs: ${e.tradeoffs}\n`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
md += '\n';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (byType.invariant.length > 0) {
|
|
45
|
+
md += '## Invariants\n';
|
|
46
|
+
for (const e of byType.invariant) {
|
|
47
|
+
md += `- **${e.title}** (\`${e.files.join(', ')}\`): ${e.context}\n`;
|
|
48
|
+
}
|
|
49
|
+
md += '\n';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (byType.graveyard.length > 0) {
|
|
53
|
+
md += '## Graveyard (Abandoned Approaches)\n';
|
|
54
|
+
for (const e of byType.graveyard) {
|
|
55
|
+
md += `- **${e.title}** (\`${e.files.join(', ')}\`): ${e.context}\n`;
|
|
56
|
+
}
|
|
57
|
+
md += '\n';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (byType.gotcha.length > 0) {
|
|
61
|
+
md += '## Gotchas\n';
|
|
62
|
+
for (const e of byType.gotcha) {
|
|
63
|
+
md += `- **${e.title}** (\`${e.files.join(', ')}\`): ${e.context}\n`;
|
|
64
|
+
}
|
|
65
|
+
md += '\n';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fs.writeFileSync('CLAUDE.md', md);
|
|
69
|
+
console.log(chalk.green('ā CLAUDE.md written to project root ā Claude Code will read this on next session start'));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.error(chalk.red(`Failed to export: ${e.message}`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = exportLore;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { loadGraph, saveGraph } = require('../lib/graph');
|
|
6
|
+
const { readIndex } = require('../lib/index');
|
|
7
|
+
const { requireInit } = require('../lib/guard');
|
|
8
|
+
|
|
9
|
+
function graph(filepath, options) {
|
|
10
|
+
requireInit();
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
let g = loadGraph();
|
|
15
|
+
|
|
16
|
+
// Rebuild graph if requested or it's empty
|
|
17
|
+
if (options.build || (Object.keys(g.imports).length === 0 && filepath)) {
|
|
18
|
+
console.log(chalk.cyan('š Building dependency graph...'));
|
|
19
|
+
const { buildFullGraph } = require('../watcher/graph');
|
|
20
|
+
g = buildFullGraph(projectRoot);
|
|
21
|
+
saveGraph(g);
|
|
22
|
+
console.log(chalk.green(`ā Graph built: ${Object.keys(g.imports).length} files indexed`));
|
|
23
|
+
if (!filepath) return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!filepath) {
|
|
27
|
+
// Stats overview
|
|
28
|
+
const fileCount = Object.keys(g.imports).length;
|
|
29
|
+
const edgeCount = Object.values(g.imports).reduce((sum, arr) => sum + arr.length, 0);
|
|
30
|
+
console.log(chalk.cyan(`\nš Dependency Graph`));
|
|
31
|
+
console.log(` Files indexed: ${fileCount}`);
|
|
32
|
+
console.log(` Import edges: ${edgeCount}`);
|
|
33
|
+
if (g.lastUpdated) console.log(chalk.dim(` Last updated: ${g.lastUpdated}`));
|
|
34
|
+
console.log(chalk.dim('\n Run: lore graph <filepath> for file details'));
|
|
35
|
+
console.log(chalk.dim(' Run: lore graph --build to rebuild from scratch'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const index = readIndex();
|
|
40
|
+
const normalized = path.relative(projectRoot, path.resolve(filepath)).replace(/\\/g, '/');
|
|
41
|
+
|
|
42
|
+
const imports = g.imports[normalized] || [];
|
|
43
|
+
const importedBy = g.importedBy[normalized] || [];
|
|
44
|
+
|
|
45
|
+
if (imports.length === 0 && importedBy.length === 0) {
|
|
46
|
+
console.log(chalk.yellow(`No graph data for ${filepath}`));
|
|
47
|
+
console.log(chalk.dim(' Run: lore graph --build to build the dependency graph'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entryCount = (file) => (index.files[file] || []).length;
|
|
52
|
+
|
|
53
|
+
console.log(chalk.cyan(`\nš ${filepath}\n`));
|
|
54
|
+
|
|
55
|
+
if (imports.length > 0) {
|
|
56
|
+
console.log(chalk.bold('Imports:'));
|
|
57
|
+
for (const dep of imports) {
|
|
58
|
+
const n = entryCount(dep);
|
|
59
|
+
const badge = n > 0 ? chalk.green(` ā ${n} Lore entr${n === 1 ? 'y' : 'ies'}`) : chalk.dim(` ā 0 Lore entries`);
|
|
60
|
+
console.log(` ${dep}${badge}`);
|
|
61
|
+
}
|
|
62
|
+
console.log();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (importedBy.length > 0) {
|
|
66
|
+
console.log(chalk.bold('Imported by:'));
|
|
67
|
+
for (const dep of importedBy) {
|
|
68
|
+
const n = entryCount(dep);
|
|
69
|
+
const badge = n > 0 ? chalk.green(` ā ${n} Lore entr${n === 1 ? 'y' : 'ies'}`) : chalk.dim(` ā 0 Lore entries`);
|
|
70
|
+
console.log(` ${dep}${badge}`);
|
|
71
|
+
}
|
|
72
|
+
console.log();
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.error(chalk.red(`Failed: ${e.message}`));
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = graph;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const { LORE_DIR, emptyIndex } = require('../lib/index');
|
|
8
|
+
|
|
9
|
+
const HOOK_CONTENT = `#!/bin/bash
|
|
10
|
+
LINECOUNT=$(git diff HEAD~1 --shortstat 2>/dev/null | grep -o '[0-9]* insertion' | grep -o '[0-9]*' || echo 0)
|
|
11
|
+
if [ "\${LINECOUNT:-0}" -gt 50 ]; then
|
|
12
|
+
echo "š Lore: Significant change detected. Log it? (y/n)"
|
|
13
|
+
read -r answer </dev/tty
|
|
14
|
+
if [ "$answer" = "y" ]; then
|
|
15
|
+
lore log </dev/tty
|
|
16
|
+
fi
|
|
17
|
+
fi
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
async function init() {
|
|
21
|
+
try {
|
|
22
|
+
const dirs = ['decisions', 'invariants', 'graveyard', 'gotchas', 'modules', 'sessions', 'drafts'];
|
|
23
|
+
for (const dir of dirs) {
|
|
24
|
+
await fs.ensureDir(path.join(LORE_DIR, dir));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const indexPath = path.join(LORE_DIR, 'index.json');
|
|
28
|
+
if (!fs.existsSync(indexPath)) {
|
|
29
|
+
await fs.writeJson(indexPath, emptyIndex(), { spaces: 2 });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const configPath = path.join(LORE_DIR, 'config.yaml');
|
|
33
|
+
if (!fs.existsSync(configPath)) {
|
|
34
|
+
const config = {
|
|
35
|
+
version: '1.0',
|
|
36
|
+
project: path.basename(process.cwd()),
|
|
37
|
+
staleAfterDays: 30,
|
|
38
|
+
embed: {
|
|
39
|
+
model: 'nomic-embed-text',
|
|
40
|
+
autoEmbed: true,
|
|
41
|
+
},
|
|
42
|
+
mcp: {
|
|
43
|
+
tokenBudget: 4000,
|
|
44
|
+
},
|
|
45
|
+
watchMode: false,
|
|
46
|
+
watchIgnore: ['node_modules', 'dist', '.git', 'coverage'],
|
|
47
|
+
commentPatterns: ["don't", 'never', 'always', 'because', 'warning', 'hack', 'note:', 'important:'],
|
|
48
|
+
signalThreshold: 30,
|
|
49
|
+
semanticStaleness: true,
|
|
50
|
+
scoringWeights: { coverage: 0.4, freshness: 0.35, depth: 0.25 },
|
|
51
|
+
};
|
|
52
|
+
await fs.writeFile(configPath, yaml.dump(config));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Create a .lore/.gitignore so generated/ephemeral files are excluded
|
|
56
|
+
// but entry json files, config.yaml, and index.json are committed.
|
|
57
|
+
const loreGitignorePath = path.join(LORE_DIR, '.gitignore');
|
|
58
|
+
if (!fs.existsSync(loreGitignorePath)) {
|
|
59
|
+
await fs.writeFile(loreGitignorePath,
|
|
60
|
+
'# Auto-generated by lore init\nembeddings.db\nsessions/\nwatch-state.json\nwatcher.pid\nwatcher.log\nscore.json\n'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const hookDir = path.join('.git', 'hooks');
|
|
65
|
+
if (fs.existsSync(hookDir)) {
|
|
66
|
+
const hookPath = path.join(hookDir, 'post-commit');
|
|
67
|
+
await fs.writeFile(hookPath, HOOK_CONTENT);
|
|
68
|
+
await fs.chmod(hookPath, '755');
|
|
69
|
+
console.log(chalk.green('ā Git post-commit hook installed'));
|
|
70
|
+
} else {
|
|
71
|
+
console.log(chalk.yellow('ā Not a git repo ā hook not installed'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(chalk.green(`ā Lore initialized at ${LORE_DIR}/`));
|
|
75
|
+
console.log(chalk.cyan(' Run: lore mine . to scan codebase for lore-worthy comments'));
|
|
76
|
+
console.log(chalk.cyan(' Run: lore log to create your first entry manually'));
|
|
77
|
+
console.log(chalk.cyan(' Run: lore watch to start passive capture'));
|
|
78
|
+
console.log(chalk.cyan(' Run: lore serve to start the MCP server'));
|
|
79
|
+
|
|
80
|
+
// Auto-mine the codebase on first init if source files exist
|
|
81
|
+
const index = require('../lib/index').readIndex();
|
|
82
|
+
const entryCount = Object.keys(index.entries).length;
|
|
83
|
+
if (entryCount === 0) {
|
|
84
|
+
const { globSync } = require('glob');
|
|
85
|
+
const hasSource = globSync('**/*.{js,ts,jsx,tsx}', {
|
|
86
|
+
cwd: process.cwd(),
|
|
87
|
+
ignore: ['node_modules/**', 'dist/**', '.lore/**'],
|
|
88
|
+
absolute: false,
|
|
89
|
+
}).length > 0;
|
|
90
|
+
|
|
91
|
+
if (hasSource) {
|
|
92
|
+
console.log(chalk.dim('\n Scanning codebase for lore-worthy comments...'));
|
|
93
|
+
try {
|
|
94
|
+
const { mineDirectory } = require('../watcher/comments');
|
|
95
|
+
const count = mineDirectory(process.cwd(), process.cwd());
|
|
96
|
+
if (count > 0) {
|
|
97
|
+
console.log(chalk.cyan(` Found ${count} draft entr${count === 1 ? 'y' : 'ies'} ā run: lore drafts`));
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
// Non-fatal
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error(chalk.red(`Failed to initialize Lore: ${e.message}`));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = init;
|