lore-memory 0.1.0 → 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/README.md +2 -2
- package/bin/lore.js +88 -1
- package/package.json +4 -4
- package/src/commands/drafts.js +61 -85
- package/src/commands/init.js +7 -4
- package/src/commands/log.js +1 -1
- package/src/lib/format.js +56 -5
- package/src/mcp/tools/overview.js +5 -0
- package/src/mcp/tools/search.js +11 -1
- package/src/watcher/comments.js +3 -3
- package/src/watcher/index.js +18 -18
- package/src/watcher/signals.js +12 -12
package/README.md
CHANGED
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(() =>
|
|
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.
|
|
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": {
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
33
|
-
"url": "https://github.com/
|
|
33
|
+
"url": "https://github.com/Tjindl/Lore"
|
|
34
34
|
},
|
|
35
|
-
"homepage": "https://github.com/
|
|
35
|
+
"homepage": "https://github.com/Tjindl/Lore#readme",
|
|
36
36
|
"bugs": {
|
|
37
|
-
"url": "https://github.com/
|
|
37
|
+
"url": "https://github.com/Tjindl/Lore/issues"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@babel/parser": "^7.29.0",
|
package/src/commands/drafts.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
84
|
+
}
|
|
58
85
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(
|
|
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('✓
|
|
117
|
+
console.log(chalk.green('\n✓ Draft review complete'));
|
|
142
118
|
}
|
|
143
119
|
|
|
144
120
|
module.exports = drafts;
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
`;
|
package/src/commands/log.js
CHANGED
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
59
|
+
content += chalk.yellow(` → ${alt}\n`);
|
|
13
60
|
}
|
|
14
61
|
}
|
|
15
62
|
|
|
16
63
|
if (entry.tradeoffs) {
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
package/src/mcp/tools/search.js
CHANGED
|
@@ -49,10 +49,20 @@ async function handler(args) {
|
|
|
49
49
|
].join(' ').toLowerCase();
|
|
50
50
|
|
|
51
51
|
if (searchable.includes(q)) {
|
|
52
|
-
|
|
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 {
|
package/src/watcher/comments.js
CHANGED
|
@@ -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.
|
|
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);
|
package/src/watcher/index.js
CHANGED
|
@@ -29,9 +29,9 @@ function startWatcher(options = {}) {
|
|
|
29
29
|
|
|
30
30
|
const log = options.logFile
|
|
31
31
|
? (msg) => {
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
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
|
|
package/src/watcher/signals.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
204
|
+
try { await fs.writeJson(statePath, state, { spaces: 2 }); } catch (e) { }
|
|
205
205
|
return draft;
|
|
206
206
|
}
|
|
207
207
|
return null;
|