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
@@ -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;