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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +666 -0
  3. package/bin/lore.js +108 -0
  4. package/package.json +53 -0
  5. package/src/commands/drafts.js +144 -0
  6. package/src/commands/edit.js +30 -0
  7. package/src/commands/embed.js +63 -0
  8. package/src/commands/export.js +76 -0
  9. package/src/commands/graph.js +80 -0
  10. package/src/commands/init.js +110 -0
  11. package/src/commands/log.js +149 -0
  12. package/src/commands/mine.js +38 -0
  13. package/src/commands/onboard.js +112 -0
  14. package/src/commands/score.js +88 -0
  15. package/src/commands/search.js +49 -0
  16. package/src/commands/serve.js +21 -0
  17. package/src/commands/stale.js +41 -0
  18. package/src/commands/status.js +59 -0
  19. package/src/commands/watch.js +67 -0
  20. package/src/commands/why.js +58 -0
  21. package/src/lib/budget.js +57 -0
  22. package/src/lib/config.js +52 -0
  23. package/src/lib/drafts.js +104 -0
  24. package/src/lib/embeddings.js +97 -0
  25. package/src/lib/entries.js +59 -0
  26. package/src/lib/format.js +23 -0
  27. package/src/lib/git.js +18 -0
  28. package/src/lib/graph.js +51 -0
  29. package/src/lib/guard.js +13 -0
  30. package/src/lib/index.js +84 -0
  31. package/src/lib/nlp.js +106 -0
  32. package/src/lib/relevance.js +81 -0
  33. package/src/lib/scorer.js +188 -0
  34. package/src/lib/sessions.js +51 -0
  35. package/src/lib/stale.js +27 -0
  36. package/src/mcp/server.js +52 -0
  37. package/src/mcp/tools/drafts.js +54 -0
  38. package/src/mcp/tools/log.js +93 -0
  39. package/src/mcp/tools/overview.js +141 -0
  40. package/src/mcp/tools/search.js +96 -0
  41. package/src/mcp/tools/stale.js +88 -0
  42. package/src/mcp/tools/why.js +91 -0
  43. package/src/watcher/comments.js +113 -0
  44. package/src/watcher/graph.js +149 -0
  45. package/src/watcher/index.js +134 -0
  46. package/src/watcher/signals.js +217 -0
  47. 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;