lore-memory 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/lore.js CHANGED
@@ -9,7 +9,94 @@ program
9
9
  .name('lore')
10
10
  .description('Persistent project memory for developers')
11
11
  .version('0.1.0')
12
- .action(() => program.outputHelp());
12
+ .action(async () => {
13
+ const inquirer = require('inquirer');
14
+ const chalk = require('chalk');
15
+ const { execSync } = require('child_process');
16
+
17
+ const LORE_LOGO = `
18
+ ██╗ ██████╗ ██████╗ ███████╗
19
+ ██║ ██╔═══██╗██╔══██╗██╔════╝
20
+ ██║ ██║ ██║██████╔╝█████╗
21
+ ██║ ██║ ██║██╔══██╗██╔══╝
22
+ ███████╗╚██████╔╝██║ ██║███████╗
23
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
24
+ `;
25
+
26
+ console.log(chalk.cyan(LORE_LOGO));
27
+ console.log(chalk.dim(' Project Memory for Developers\n'));
28
+
29
+ const { action } = await inquirer.prompt([
30
+ {
31
+ type: 'list',
32
+ name: 'action',
33
+ message: 'What would you like to do?',
34
+ choices: [
35
+ { name: '📝 Log new knowledge (lore log)', value: 'log' },
36
+ { name: '👀 Review pending drafts (lore drafts)', value: 'drafts' },
37
+ { name: '📊 View project health (lore score)', value: 'score' },
38
+ { name: '🔍 Search knowledge base (lore search)', value: 'search' },
39
+ { name: '⚙️ Start background watcher (lore watch --daemon)', value: 'watch --daemon' },
40
+ new inquirer.Separator(),
41
+ { name: '❓ Show Help', value: 'help' },
42
+ { name: '❌ Exit', value: 'exit' }
43
+ ],
44
+ },
45
+ ]);
46
+
47
+ if (action === 'exit') {
48
+ process.exit(0);
49
+ } else if (action === 'help') {
50
+ program.outputHelp();
51
+ } else {
52
+ console.log();
53
+ try {
54
+ execSync(`node ${__filename} ${action}`, { stdio: 'inherit' });
55
+ } catch (e) { }
56
+ }
57
+ });
58
+
59
+ // Fuzzy matching for unknown commands
60
+ program.on('command:*', function (operands) {
61
+ const chalk = require('chalk');
62
+ console.error(chalk.red(`error: unknown command '${operands[0]}'`));
63
+ const availableCommands = program.commands.map(cmd => cmd.name());
64
+
65
+ // Simple Levenshtein distance check for did-you-mean
66
+ let closest = null;
67
+ let minDistance = 3; // only suggest if distance < 3
68
+
69
+ for (const cmd of availableCommands) {
70
+ let distance = 0;
71
+ const a = operands[0];
72
+ const b = cmd;
73
+ const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(null));
74
+ for (let i = 0; i <= a.length; i += 1) { matrix[0][i] = i; }
75
+ for (let j = 0; j <= b.length; j += 1) { matrix[j][0] = j; }
76
+ for (let j = 1; j <= b.length; j += 1) {
77
+ for (let i = 1; i <= a.length; i += 1) {
78
+ const indicator = a[i - 1] === b[j - 1] ? 0 : 1;
79
+ matrix[j][i] = Math.min(
80
+ matrix[j][i - 1] + 1,
81
+ matrix[j - 1][i] + 1,
82
+ matrix[j - 1][i - 1] + indicator
83
+ );
84
+ }
85
+ }
86
+ distance = matrix[b.length][a.length];
87
+
88
+ if (distance < minDistance) {
89
+ minDistance = distance;
90
+ closest = cmd;
91
+ }
92
+ }
93
+
94
+ if (closest) {
95
+ console.log();
96
+ console.log(chalk.yellow(`Did you mean ${chalk.bold('lore ' + closest)}?`));
97
+ }
98
+ process.exitCode = 1;
99
+ });
13
100
 
14
101
  program
15
102
  .command('init')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lore-memory",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Persistent project memory for developers. Captures decisions, invariants, gotchas, and graveyard entries — automatically and manually — and injects them into AI coding sessions.",
5
5
  "main": "bin/lore.js",
6
6
  "bin": {
@@ -38,8 +38,7 @@ async function drafts(options) {
38
38
 
39
39
  console.log(chalk.cyan(`\n📖 ${pending.length} pending draft${pending.length === 1 ? '' : 's'}\n`));
40
40
 
41
- for (let i = 0; i < pending.length; i++) {
42
- const draft = pending[i];
41
+ const choices = pending.map((draft, i) => {
43
42
  const conf = Math.round((draft.confidence || 0) * 100);
44
43
  const typeColor = {
45
44
  decision: chalk.blue,
@@ -48,97 +47,74 @@ async function drafts(options) {
48
47
  graveyard: chalk.dim,
49
48
  }[draft.suggestedType] || chalk.white;
50
49
 
51
- 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(', ')}`);
50
+ // Create a nicely formatted display string
51
+ const title = draft.suggestedTitle.length > 50 ? draft.suggestedTitle.slice(0, 47) + '...' : draft.suggestedTitle;
52
+ const display = `${typeColor(draft.suggestedType.toUpperCase().padEnd(9))} | ${title.padEnd(50)} | Conf: ${conf}%`;
53
+
54
+ return {
55
+ name: display,
56
+ value: draft,
57
+ checked: conf >= 80 // Pre-check high confidence ones
58
+ };
59
+ });
60
+
61
+ let selectedDrafts;
62
+ try {
63
+ const ans = await inquirer.prompt([{
64
+ type: 'checkbox',
65
+ name: 'selected',
66
+ message: 'Select drafts to ACCEPT (unselected drafts will be kept pending):',
67
+ choices: choices,
68
+ pageSize: 15
69
+ }]);
70
+ selectedDrafts = ans.selected;
71
+ } catch (e) {
72
+ console.log(chalk.yellow('\nAborted.'));
73
+ return;
74
+ }
75
+
76
+ if (selectedDrafts.length === 0) {
77
+ console.log(chalk.yellow('\nNo drafts selected. Keeping all drafts pending.'));
78
+ } else {
79
+ console.log(chalk.cyan(`\nAccepting ${selectedDrafts.length} drafts...`));
80
+ for (const draft of selectedDrafts) {
81
+ const entry = acceptDraft(draft.draftId);
82
+ console.log(chalk.green(` ✓ ${draft.suggestedTitle} -> Saved as ${entry.id}`));
56
83
  }
57
- console.log();
84
+ }
58
85
 
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
- }
86
+ // Ask about deletions for remaining
87
+ const remainingDrafts = pending.filter(p => !selectedDrafts.find(s => s.draftId === p.draftId));
88
+
89
+ if (remainingDrafts.length > 0) {
90
+ console.log();
91
+ let deleteSelection;
92
+ try {
93
+ const deleteAns = await inquirer.prompt([{
94
+ type: 'checkbox',
95
+ name: 'deletes',
96
+ message: 'Select drafts to permanently DELETE:',
97
+ choices: remainingDrafts.map(draft => ({
98
+ name: `${draft.suggestedType.toUpperCase().padEnd(9)} | ${draft.suggestedTitle}`,
99
+ value: draft
100
+ })),
101
+ pageSize: 10
102
+ }]);
103
+ deleteSelection = deleteAns.deletes;
104
+ } catch (e) {
105
+ return;
106
+ }
80
107
 
81
- 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') {
108
+ if (deleteSelection.length > 0) {
109
+ console.log(chalk.yellow(`\nDeleting ${deleteSelection.length} drafts...`));
110
+ for (const draft of deleteSelection) {
130
111
  deleteDraft(draft.draftId);
131
- console.log(chalk.dim(' Deleted'));
132
- done = true;
133
- } else if (action === 'quit') {
134
- console.log(chalk.cyan('\n Remaining drafts saved. Run: lore drafts'));
135
- return;
112
+ console.log(chalk.dim(` Deleted: ${draft.suggestedTitle}`));
136
113
  }
137
114
  }
138
- console.log();
139
115
  }
140
116
 
141
- console.log(chalk.green('✓ All drafts reviewed'));
117
+ console.log(chalk.green('\nDraft review complete'));
142
118
  }
143
119
 
144
120
  module.exports = drafts;
@@ -9,10 +9,13 @@ const { LORE_DIR, emptyIndex } = require('../lib/index');
9
9
  const HOOK_CONTENT = `#!/bin/bash
10
10
  LINECOUNT=$(git diff HEAD~1 --shortstat 2>/dev/null | grep -o '[0-9]* insertion' | grep -o '[0-9]*' || echo 0)
11
11
  if [ "\${LINECOUNT:-0}" -gt 50 ]; then
12
- 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
12
+ # Only prompt if running in an interactive terminal
13
+ if [ -t 1 ] || [ -t 0 ]; then
14
+ echo "📖 Lore: Significant change detected. Log it? (y/n)"
15
+ read -r answer </dev/tty
16
+ if [ "$answer" = "y" ]; then
17
+ lore log </dev/tty
18
+ fi
16
19
  fi
17
20
  fi
18
21
  `;
@@ -108,7 +108,7 @@ async function log(options) {
108
108
  id,
109
109
  type,
110
110
  title,
111
- date: new Date().toISOString().split('T')[0],
111
+ date: new Date().toISOString(),
112
112
  files,
113
113
  context,
114
114
  alternatives: alternatives.filter(Boolean),
package/src/lib/format.js CHANGED
@@ -2,22 +2,73 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
 
5
+ function drawBox(content, colorFn, title) {
6
+ const lines = content.split('\n');
7
+ const maxLen = Math.max(
8
+ ...lines.map(l => l.replace(/\x1B\[[0-9;]*m/g, '').length),
9
+ title ? title.length + 4 : 0
10
+ );
11
+
12
+ const width = Math.min(80, Math.max(40, maxLen + 2)); // keep between 40 and 80 width
13
+
14
+ const top = title
15
+ ? `╭─ ${title} ${'─'.repeat(Math.max(0, width - title.length - 4))}╮`
16
+ : `╭${'─'.repeat(width)}╮`;
17
+
18
+ console.log(colorFn(top));
19
+
20
+ for (const line of lines) {
21
+ const cleanLen = line.replace(/\x1B\[[0-9;]*m/g, '').length;
22
+ const padding = ' '.repeat(Math.max(0, width - cleanLen - 2));
23
+ console.log(colorFn('│ ') + line + padding + colorFn(' │'));
24
+ }
25
+
26
+ console.log(colorFn(`╰${'─'.repeat(width)}╯\n`));
27
+ }
28
+
5
29
  function printEntry(entry) {
30
+ let content = '';
31
+
32
+ let colorFn = chalk.white;
33
+ let titleColor = chalk.bold.white;
34
+
35
+ if (entry.type === 'decision') { colorFn = chalk.cyan; titleColor = chalk.bold.cyan; }
36
+ else if (entry.type === 'invariant') { colorFn = chalk.red; titleColor = chalk.bold.red; }
37
+ else if (entry.type === 'gotcha') { colorFn = chalk.yellow; titleColor = chalk.bold.yellow; }
38
+ else if (entry.type === 'graveyard') { colorFn = chalk.gray; titleColor = chalk.bold.gray; }
39
+
6
40
  const typeLabel = `[${entry.type.toUpperCase()}]`;
7
- console.log(chalk.bold(`${typeLabel} ${entry.title}`) + chalk.gray(` (${entry.date})`));
8
- console.log(` → ${entry.context}`);
41
+ content += `${titleColor(typeLabel)} ${titleColor(entry.title)} ${chalk.gray('(' + entry.date + ')')}\n\n`;
42
+
43
+ // Wrap context text roughly
44
+ const contextWords = entry.context.split(' ');
45
+ let currentLine = '';
46
+ for (const word of contextWords) {
47
+ if (currentLine.length + word.length > 70) {
48
+ content += chalk.white(currentLine) + '\n';
49
+ currentLine = word + ' ';
50
+ } else {
51
+ currentLine += word + ' ';
52
+ }
53
+ }
54
+ if (currentLine) content += chalk.white(currentLine) + '\n';
9
55
 
10
56
  if (entry.alternatives && entry.alternatives.length > 0) {
57
+ content += '\n' + chalk.yellow('Alternatives Considered:\n');
11
58
  for (const alt of entry.alternatives) {
12
- console.log(chalk.yellow(` → Rejected: ${alt}`));
59
+ content += chalk.yellow(` → ${alt}\n`);
13
60
  }
14
61
  }
15
62
 
16
63
  if (entry.tradeoffs) {
17
- console.log(chalk.gray(`Tradeoffs: ${entry.tradeoffs}`));
64
+ content += '\n' + chalk.magenta(`Tradeoffs: ${entry.tradeoffs}\n`);
65
+ }
66
+
67
+ if (entry.files && entry.files.length > 0) {
68
+ content += '\n' + chalk.dim(`Files: ${entry.files.join(', ')}\n`);
18
69
  }
19
70
 
20
- console.log();
71
+ drawBox(content.trim(), colorFn, entry.id);
21
72
  }
22
73
 
23
74
  module.exports = { printEntry };
@@ -45,6 +45,11 @@ async function handler(args) {
45
45
  }
46
46
  }
47
47
 
48
+ // Sort entries newest-first
49
+ for (const type of Object.keys(byType)) {
50
+ byType[type].sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0));
51
+ }
52
+
48
53
  const lines = [];
49
54
  const projectName = config.project || 'this project';
50
55
 
@@ -49,10 +49,20 @@ async function handler(args) {
49
49
  ].join(' ').toLowerCase();
50
50
 
51
51
  if (searchable.includes(q)) {
52
- matches.push(entry);
52
+ // Calculate a basic relevance score for text search:
53
+ // 1. Term frequency of query
54
+ const tf = (searchable.match(new RegExp(q, 'g')) || []).length;
55
+ // 2. Bonus for newer entries
56
+ const ageDays = (Date.now() - new Date(entry.date || 0)) / (1000 * 60 * 60 * 24);
57
+ const recencyBonus = Math.max(0, 5 - (ageDays / 30)); // up to +5 points for newest
58
+
59
+ matches.push(Object.assign({}, entry, { _score: tf + recencyBonus }));
53
60
  }
54
61
  }
55
62
 
63
+ // Sort matches by relevance score descending
64
+ matches.sort((a, b) => (b._score || 0) - (a._score || 0));
65
+
56
66
  // Try semantic search if text search found nothing and embeddings exist
57
67
  if (matches.length === 0) {
58
68
  try {
@@ -47,11 +47,11 @@ function extractComments(code, filePath) {
47
47
  * Saves passing comments as drafts.
48
48
  * @param {string} absFilePath
49
49
  * @param {string} projectRoot
50
- * @returns {object[]} created drafts
50
+ * @returns {Promise<object[]>} created drafts
51
51
  */
52
- function mineFile(absFilePath, projectRoot) {
52
+ async function mineFile(absFilePath, projectRoot) {
53
53
  let code = '';
54
- try { code = fs.readFileSync(absFilePath, 'utf8'); } catch (e) { return []; }
54
+ try { code = await fs.readFile(absFilePath, 'utf8'); } catch (e) { return []; }
55
55
 
56
56
  const relativePath = path.relative(projectRoot, absFilePath).replace(/\\/g, '/');
57
57
  const comments = extractComments(code, absFilePath);
@@ -29,9 +29,9 @@ function startWatcher(options = {}) {
29
29
 
30
30
  const log = options.logFile
31
31
  ? (msg) => {
32
- const plain = msg.replace(/\x1B\[[0-9;]*m/g, '');
33
- fs.appendFileSync(options.logFile, `${new Date().toISOString()} ${plain}\n`);
34
- }
32
+ const plain = msg.replace(/\x1B\[[0-9;]*m/g, '');
33
+ fs.appendFileSync(options.logFile, `${new Date().toISOString()} ${plain}\n`);
34
+ }
35
35
  : (msg) => console.log(msg);
36
36
 
37
37
  if (!options.quiet) {
@@ -56,46 +56,46 @@ function startWatcher(options = {}) {
56
56
  awaitWriteFinish: { stabilityThreshold: 500, pollInterval: 100 },
57
57
  });
58
58
 
59
- watcher.on('unlink', (relPath) => {
59
+ watcher.on('unlink', async (relPath) => {
60
60
  const abs = path.join(projectRoot, relPath);
61
- const draft = signals.onFileDeletion(abs, projectRoot);
61
+ const draft = await signals.onFileDeletion(abs, projectRoot);
62
62
  if (draft) recordDraft(draft, abs);
63
63
  });
64
64
 
65
- watcher.on('unlinkDir', (relPath) => {
65
+ watcher.on('unlinkDir', async (relPath) => {
66
66
  const abs = path.join(projectRoot, relPath);
67
- const draft = signals.onDirectoryDeletion(abs, projectRoot);
67
+ const draft = await signals.onDirectoryDeletion(abs, projectRoot);
68
68
  if (draft) recordDraft(draft, abs);
69
69
  });
70
70
 
71
- watcher.on('add', (relPath) => {
71
+ watcher.on('add', async (relPath) => {
72
72
  const abs = path.join(projectRoot, relPath);
73
- const draft = signals.onNewFile(abs, projectRoot);
73
+ const draft = await signals.onNewFile(abs, projectRoot);
74
74
  if (draft) recordDraft(draft, abs);
75
75
  });
76
76
 
77
- watcher.on('change', (relPath) => {
77
+ watcher.on('change', async (relPath) => {
78
78
  const abs = path.join(projectRoot, relPath);
79
79
 
80
80
  // Repeated edit tracking
81
- const editDraft = signals.trackFileEdit(abs, projectRoot);
81
+ const editDraft = await signals.trackFileEdit(abs, projectRoot);
82
82
  if (editDraft) recordDraft(editDraft, abs);
83
83
 
84
84
  // package.json changes
85
85
  if (relPath.endsWith('package.json')) {
86
- const pkgDrafts = signals.onPackageJsonChange(abs, projectRoot);
86
+ const pkgDrafts = await signals.onPackageJsonChange(abs, projectRoot);
87
87
  for (const d of pkgDrafts) recordDraft(d, abs);
88
88
  }
89
89
 
90
90
  // Comment mining + graph update for source files
91
91
  if (/\.(js|ts|jsx|tsx|py|go|rs)$/.test(relPath)) {
92
- const commentDrafts = mineFile(abs, projectRoot);
92
+ const commentDrafts = await mineFile(abs, projectRoot);
93
93
  if (commentDrafts.length > 0) {
94
94
  draftCount += commentDrafts.length;
95
95
  log(`${chalk.dim(`[${timestamp()}]`)} Mined ${commentDrafts.length} comment${commentDrafts.length === 1 ? '' : 's'} from ${chalk.yellow(relPath)} — queued for review`);
96
96
  }
97
97
 
98
- try { updateGraphForFile(abs, projectRoot); } catch (e) {}
98
+ try { updateGraphForFile(abs, projectRoot); } catch (e) { }
99
99
  }
100
100
  });
101
101
 
@@ -104,15 +104,15 @@ function startWatcher(options = {}) {
104
104
  let gitWatcher = null;
105
105
  if (fs.existsSync(path.join(projectRoot, '.git'))) {
106
106
  gitWatcher = chokidar.watch(commitMsgPath, { persistent: true, ignoreInitial: true });
107
- gitWatcher.on('change', () => {
107
+ gitWatcher.on('change', async () => {
108
108
  try {
109
- const message = fs.readFileSync(commitMsgPath, 'utf8').trim();
110
- const drafts = signals.onCommitMessage(message, projectRoot);
109
+ const message = await fs.readFile(commitMsgPath, 'utf8');
110
+ const drafts = await signals.onCommitMessage(message.trim(), projectRoot);
111
111
  for (const d of drafts) {
112
112
  draftCount++;
113
113
  log(`${chalk.dim(`[${timestamp()}]`)} Commit signal: "${message.slice(0, 60)}" — queued for review`);
114
114
  }
115
- } catch (e) {}
115
+ } catch (e) { }
116
116
  });
117
117
  }
118
118
 
@@ -22,7 +22,7 @@ function makeDraft(overrides) {
22
22
  };
23
23
  }
24
24
 
25
- function onFileDeletion(filepath, projectRoot) {
25
+ async function onFileDeletion(filepath, projectRoot) {
26
26
  const relativePath = path.relative(projectRoot, filepath).replace(/\\/g, '/');
27
27
 
28
28
  // Try to check line count via git
@@ -32,7 +32,7 @@ function onFileDeletion(filepath, projectRoot) {
32
32
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
33
33
  });
34
34
  lines = parseInt(out.trim(), 10) || 0;
35
- } catch (e) {}
35
+ } catch (e) { }
36
36
 
37
37
  if (lines > 0 && lines < 100) return null;
38
38
 
@@ -48,7 +48,7 @@ function onFileDeletion(filepath, projectRoot) {
48
48
  return draft;
49
49
  }
50
50
 
51
- function onDirectoryDeletion(dirpath, projectRoot) {
51
+ async function onDirectoryDeletion(dirpath, projectRoot) {
52
52
  const relativePath = path.relative(projectRoot, dirpath).replace(/\\/g, '/');
53
53
  const name = path.basename(relativePath);
54
54
  const draft = makeDraft({
@@ -62,7 +62,7 @@ function onDirectoryDeletion(dirpath, projectRoot) {
62
62
  return draft;
63
63
  }
64
64
 
65
- function onNewFile(filepath, projectRoot) {
65
+ async function onNewFile(filepath, projectRoot) {
66
66
  const relativePath = path.relative(projectRoot, filepath).replace(/\\/g, '/');
67
67
  const name = path.basename(relativePath);
68
68
  const lower = name.toLowerCase();
@@ -97,7 +97,7 @@ function onNewFile(filepath, projectRoot) {
97
97
  return null;
98
98
  }
99
99
 
100
- function onPackageJsonChange(filepath, projectRoot) {
100
+ async function onPackageJsonChange(filepath, projectRoot) {
101
101
  const relativePath = path.relative(projectRoot, filepath).replace(/\\/g, '/');
102
102
  if (!relativePath.endsWith('package.json')) return [];
103
103
 
@@ -108,9 +108,9 @@ function onPackageJsonChange(filepath, projectRoot) {
108
108
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
109
109
  });
110
110
  prev = JSON.parse(prevRaw);
111
- } catch (e) {}
111
+ } catch (e) { }
112
112
 
113
- try { curr = fs.readJsonSync(filepath); } catch (e) { return []; }
113
+ try { curr = await fs.readJson(filepath); } catch (e) { return []; }
114
114
 
115
115
  const prevDeps = Object.assign({}, prev.dependencies || {}, prev.devDependencies || {});
116
116
  const currDeps = Object.assign({}, curr.dependencies || {}, curr.devDependencies || {});
@@ -143,7 +143,7 @@ function onPackageJsonChange(filepath, projectRoot) {
143
143
  return drafts;
144
144
  }
145
145
 
146
- function onCommitMessage(message, projectRoot) {
146
+ async function onCommitMessage(message, projectRoot) {
147
147
  const lower = message.toLowerCase();
148
148
  const signals = [
149
149
  { re: /\b(replac|switch(ed|ing)|migrat)\b/, type: 'decision', confidence: 0.8 },
@@ -171,12 +171,12 @@ function onCommitMessage(message, projectRoot) {
171
171
  }
172
172
 
173
173
  // Track repeated edits to detect gotcha-worthy files
174
- function trackFileEdit(filepath, projectRoot) {
174
+ async function trackFileEdit(filepath, projectRoot) {
175
175
  const relativePath = path.relative(projectRoot, filepath).replace(/\\/g, '/');
176
176
  const statePath = path.join(LORE_DIR, 'watch-state.json');
177
177
 
178
178
  let state = { edits: {} };
179
- try { state = fs.readJsonSync(statePath); } catch (e) {}
179
+ try { state = await fs.readJson(statePath); } catch (e) { }
180
180
  if (!state.edits) state.edits = {};
181
181
 
182
182
  const now = Date.now();
@@ -186,7 +186,7 @@ function trackFileEdit(filepath, projectRoot) {
186
186
  state.edits[relativePath].push(now);
187
187
  state.edits[relativePath] = state.edits[relativePath].filter(t => t > weekAgo);
188
188
 
189
- try { fs.writeJsonSync(statePath, state, { spaces: 2 }); } catch (e) {}
189
+ try { await fs.writeJson(statePath, state, { spaces: 2 }); } catch (e) { }
190
190
 
191
191
  if (state.edits[relativePath].length >= 5) {
192
192
  const name = path.basename(relativePath);
@@ -201,7 +201,7 @@ function trackFileEdit(filepath, projectRoot) {
201
201
  saveDraft(draft);
202
202
  // Reset to avoid spam
203
203
  state.edits[relativePath] = [];
204
- try { fs.writeJsonSync(statePath, state, { spaces: 2 }); } catch (e) {}
204
+ try { await fs.writeJson(statePath, state, { spaces: 2 }); } catch (e) { }
205
205
  return draft;
206
206
  }
207
207
  return null;