mindlore 0.0.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/LICENSE +21 -21
- package/README.md +61 -4
- package/SCHEMA.md +71 -12
- package/hooks/lib/mindlore-common.cjs +106 -0
- package/hooks/mindlore-decision-detector.cjs +51 -0
- package/hooks/mindlore-fts5-sync.cjs +13 -24
- package/hooks/mindlore-index.cjs +11 -27
- package/hooks/mindlore-post-compact.cjs +2 -2
- package/hooks/mindlore-pre-compact.cjs +1 -1
- package/hooks/mindlore-read-guard.cjs +62 -0
- package/hooks/mindlore-search.cjs +94 -76
- package/hooks/mindlore-session-end.cjs +74 -8
- package/hooks/mindlore-session-focus.cjs +2 -2
- package/package.json +1 -1
- package/plugin.json +26 -2
- package/scripts/init.cjs +126 -13
- package/scripts/mindlore-fts5-index.cjs +7 -10
- package/scripts/mindlore-fts5-search.cjs +3 -6
- package/scripts/mindlore-health-check.cjs +98 -29
- package/scripts/uninstall.cjs +186 -0
- package/skills/mindlore-decide/SKILL.md +71 -0
- package/skills/mindlore-ingest/SKILL.md +13 -2
- package/skills/mindlore-log/SKILL.md +79 -0
- package/skills/mindlore-query/SKILL.md +125 -0
- package/templates/SCHEMA.md +280 -0
|
@@ -4,75 +4,64 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* mindlore-search — UserPromptSubmit hook
|
|
6
6
|
*
|
|
7
|
-
* Extracts keywords from user prompt, searches FTS5
|
|
8
|
-
*
|
|
7
|
+
* Extracts keywords from user prompt, searches FTS5 with per-keyword scoring,
|
|
8
|
+
* injects top results with description + headings (matching old knowledge system quality).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
-
const { findMindloreDir, DB_NAME, requireDatabase } = require('./lib/mindlore-common.cjs');
|
|
13
|
+
const { findMindloreDir, DB_NAME, requireDatabase, extractHeadings, readHookStdin } = require('./lib/mindlore-common.cjs');
|
|
14
14
|
|
|
15
15
|
const MAX_RESULTS = 3;
|
|
16
|
-
const
|
|
16
|
+
const MIN_QUERY_WORDS = 3;
|
|
17
|
+
const MIN_KEYWORD_HITS = 2;
|
|
18
|
+
|
|
19
|
+
// Extended stop words (~70 TR + EN) matching old knowledge system
|
|
20
|
+
const STOP_WORDS = new Set([
|
|
21
|
+
// English
|
|
22
|
+
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
23
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
24
|
+
'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
|
|
25
|
+
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
|
|
26
|
+
'it', 'its', 'this', 'that', 'these', 'those', 'what', 'which', 'who',
|
|
27
|
+
'whom', 'how', 'when', 'where', 'why', 'not', 'no', 'nor', 'so',
|
|
28
|
+
'if', 'or', 'but', 'all', 'each', 'every', 'both', 'few', 'more',
|
|
29
|
+
'most', 'other', 'some', 'such', 'only', 'own', 'same', 'than',
|
|
30
|
+
'and', 'about', 'between', 'after', 'before', 'above', 'below',
|
|
31
|
+
'up', 'down', 'out', 'very', 'just', 'also', 'now', 'then',
|
|
32
|
+
'here', 'there', 'too', 'yet', 'my', 'your', 'his', 'her', 'our',
|
|
33
|
+
'their', 'me', 'him', 'us', 'them', 'i', 'you', 'he', 'she', 'we', 'they',
|
|
34
|
+
// Turkish
|
|
35
|
+
'bir', 'bu', 'su', 'ne', 'nasil', 'neden', 'var', 'yok', 'mi', 'mu',
|
|
36
|
+
'ile', 'icin', 'de', 'da', 've', 'veya', 'ama', 'ise', 'hem',
|
|
37
|
+
'bakalim', 'gel', 'git', 'yap', 'et', 'al', 'ver',
|
|
38
|
+
'evet', 'hayir', 'tamam', 'ok', 'oldu', 'olur', 'dur',
|
|
39
|
+
'simdi', 'sonra', 'once', 'hemen', 'biraz',
|
|
40
|
+
'lan', 'ya', 'ki', 'abi', 'hadi', 'hey', 'selam',
|
|
41
|
+
'olarak', 'olan', 'gibi', 'kadar', 'daha', 'cok', 'hem',
|
|
42
|
+
'bunu', 'buna', 'icinde', 'uzerinde', 'arasinda',
|
|
43
|
+
'sonucu', 'tarafindan', 'zaten', 'gayet',
|
|
44
|
+
'acaba', 'nedir', 'midir', 'mudur',
|
|
45
|
+
// Generic technical (appears everywhere, not distinctive)
|
|
46
|
+
'hook', 'file', 'dosya', 'kullan', 'ekle', 'yaz', 'oku', 'calistir',
|
|
47
|
+
'kontrol', 'test', 'check', 'run', 'add', 'update', 'config',
|
|
48
|
+
'setup', 'install', 'start', 'stop', 'create', 'delete', 'remove', 'set',
|
|
49
|
+
'get', 'list', 'show', 'view', 'open', 'close', 'save', 'load',
|
|
50
|
+
]);
|
|
17
51
|
|
|
18
52
|
function extractKeywords(text) {
|
|
19
|
-
// Remove common stop words and short words
|
|
20
|
-
const stopWords = new Set([
|
|
21
|
-
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
22
|
-
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
23
|
-
'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
|
|
24
|
-
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'about', 'between',
|
|
25
|
-
'through', 'after', 'before', 'above', 'below', 'up', 'down', 'out',
|
|
26
|
-
'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either',
|
|
27
|
-
'neither', 'each', 'every', 'all', 'any', 'few', 'more', 'most',
|
|
28
|
-
'other', 'some', 'such', 'no', 'only', 'own', 'same', 'than', 'too',
|
|
29
|
-
'very', 'just', 'also', 'now', 'then', 'here', 'there', 'when',
|
|
30
|
-
'where', 'why', 'how', 'what', 'which', 'who', 'whom', 'this',
|
|
31
|
-
'that', 'these', 'those', 'it', 'its', 'my', 'your', 'his', 'her',
|
|
32
|
-
'our', 'their', 'me', 'him', 'us', 'them', 'i', 'you', 'he', 'she',
|
|
33
|
-
'we', 'they', 'bu', 'su', 'bir', 'de', 'da', 've', 'ile', 'icin',
|
|
34
|
-
'var', 'mi', 'ne', 'nasil', 'nedir', 'evet', 'hayir',
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
53
|
const words = text
|
|
38
54
|
.toLowerCase()
|
|
39
|
-
.replace(/[
|
|
55
|
+
.replace(/[^\w\s\u00e7\u011f\u0131\u00f6\u015f\u00fc-]/g, ' ')
|
|
40
56
|
.split(/\s+/)
|
|
41
|
-
.filter((w) => w.length >=
|
|
42
|
-
|
|
43
|
-
// Deduplicate and limit
|
|
44
|
-
return [...new Set(words)].slice(0, 5);
|
|
45
|
-
}
|
|
57
|
+
.filter((w) => w.length >= 3 && !STOP_WORDS.has(w) && !/^\d+$/.test(w));
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
const headings = [];
|
|
49
|
-
for (const line of content.split('\n')) {
|
|
50
|
-
if (line.startsWith('#')) {
|
|
51
|
-
headings.push(line.replace(/^#+\s*/, '').trim());
|
|
52
|
-
if (headings.length >= max) break;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return headings;
|
|
59
|
+
return [...new Set(words)].slice(0, 8);
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function main() {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
input = fs.readFileSync(0, 'utf8');
|
|
63
|
-
} catch (_err) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let userMessage = '';
|
|
68
|
-
try {
|
|
69
|
-
const parsed = JSON.parse(input);
|
|
70
|
-
userMessage = parsed.content || parsed.message || parsed.query || input;
|
|
71
|
-
} catch (_err) {
|
|
72
|
-
userMessage = input;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!userMessage || userMessage.length < MIN_QUERY_LENGTH) return;
|
|
63
|
+
const userMessage = readHookStdin(['prompt', 'content', 'message', 'query']);
|
|
64
|
+
if (!userMessage || userMessage.length < MIN_QUERY_WORDS) return;
|
|
76
65
|
|
|
77
66
|
const baseDir = findMindloreDir();
|
|
78
67
|
if (!baseDir) return;
|
|
@@ -81,7 +70,7 @@ function main() {
|
|
|
81
70
|
if (!fs.existsSync(dbPath)) return;
|
|
82
71
|
|
|
83
72
|
const keywords = extractKeywords(userMessage);
|
|
84
|
-
if (keywords.length
|
|
73
|
+
if (keywords.length < MIN_QUERY_WORDS) return;
|
|
85
74
|
|
|
86
75
|
const Database = requireDatabase();
|
|
87
76
|
if (!Database) return;
|
|
@@ -89,39 +78,68 @@ function main() {
|
|
|
89
78
|
const db = new Database(dbPath, { readonly: true });
|
|
90
79
|
|
|
91
80
|
try {
|
|
92
|
-
//
|
|
93
|
-
|
|
81
|
+
// Per-keyword scoring (like old knowledge-search.cjs)
|
|
82
|
+
// Count how many keywords match each document
|
|
83
|
+
const allPaths = db.prepare('SELECT DISTINCT path FROM mindlore_fts').all();
|
|
84
|
+
const scores = [];
|
|
85
|
+
const matchStmt = db.prepare('SELECT rank FROM mindlore_fts WHERE path = ? AND mindlore_fts MATCH ?');
|
|
86
|
+
|
|
87
|
+
for (const row of allPaths) {
|
|
88
|
+
let hits = 0;
|
|
89
|
+
let totalRank = 0;
|
|
90
|
+
|
|
91
|
+
for (const kw of keywords) {
|
|
92
|
+
try {
|
|
93
|
+
const r = matchStmt.get(row.path, '"' + kw + '"');
|
|
94
|
+
if (r) {
|
|
95
|
+
hits++;
|
|
96
|
+
totalRank += r.rank;
|
|
97
|
+
}
|
|
98
|
+
} catch (_err) {
|
|
99
|
+
// FTS5 query error for this keyword — skip
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (hits >= MIN_KEYWORD_HITS) {
|
|
104
|
+
scores.push({ path: row.path, hits, totalRank });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Sort: most keyword hits first, then best rank
|
|
109
|
+
scores.sort((a, b) => b.hits - a.hits || a.totalRank - b.totalRank);
|
|
110
|
+
const relevant = scores.slice(0, MAX_RESULTS);
|
|
94
111
|
|
|
95
|
-
|
|
96
|
-
.prepare(
|
|
97
|
-
`SELECT path, rank
|
|
98
|
-
FROM mindlore_fts
|
|
99
|
-
WHERE mindlore_fts MATCH ?
|
|
100
|
-
ORDER BY rank
|
|
101
|
-
LIMIT ?`
|
|
102
|
-
)
|
|
103
|
-
.all(ftsQuery, MAX_RESULTS);
|
|
112
|
+
if (relevant.length === 0) return;
|
|
104
113
|
|
|
105
|
-
|
|
114
|
+
// Build rich inject output
|
|
115
|
+
const metaStmt = db.prepare(
|
|
116
|
+
'SELECT slug, description, category, title, tags FROM mindlore_fts WHERE path = ?'
|
|
117
|
+
);
|
|
106
118
|
|
|
107
119
|
const output = [];
|
|
108
|
-
for (const r of
|
|
109
|
-
const
|
|
110
|
-
|
|
120
|
+
for (const r of relevant) {
|
|
121
|
+
const meta = metaStmt.get(r.path) || {};
|
|
122
|
+
const relativePath = path.relative(baseDir, r.path).replace(/\\/g, '/');
|
|
111
123
|
|
|
124
|
+
let headings = [];
|
|
112
125
|
if (fs.existsSync(r.path)) {
|
|
113
126
|
const content = fs.readFileSync(r.path, 'utf8');
|
|
114
|
-
headings = extractHeadings(content,
|
|
127
|
+
headings = extractHeadings(content, 5);
|
|
115
128
|
}
|
|
116
129
|
|
|
117
|
-
const
|
|
118
|
-
|
|
130
|
+
const category = meta.category || path.dirname(relativePath).split('/')[0];
|
|
131
|
+
const title = meta.title || meta.slug || path.basename(r.path, '.md');
|
|
132
|
+
const description = meta.description || '';
|
|
133
|
+
|
|
134
|
+
const headingStr = headings.length > 0 ? `\nBasliklar: ${headings.join(', ')}` : '';
|
|
135
|
+
const tagsStr = meta.tags ? `\nTags: ${meta.tags}` : '';
|
|
136
|
+
output.push(
|
|
137
|
+
`[Mindlore: ${category}/${title}] ${description}\nDosya: ${relativePath}${tagsStr}${headingStr}`
|
|
138
|
+
);
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
if (output.length > 0) {
|
|
122
|
-
process.
|
|
123
|
-
`[Mindlore Search: ${keywords.join(', ')}]\n${output.join('\n')}\n`
|
|
124
|
-
);
|
|
142
|
+
process.stdout.write(output.join('\n\n') + '\n');
|
|
125
143
|
}
|
|
126
144
|
} catch (_err) {
|
|
127
145
|
// FTS5 query error — silently skip
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
14
15
|
const { findMindloreDir } = require('./lib/mindlore-common.cjs');
|
|
15
16
|
|
|
16
17
|
function formatDate(date) {
|
|
@@ -22,6 +23,47 @@ function formatDate(date) {
|
|
|
22
23
|
return `${y}-${m}-${d}-${h}${min}`;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function getRecentGitChanges() {
|
|
27
|
+
try {
|
|
28
|
+
const raw = execSync('git diff --name-only HEAD~5..HEAD 2>/dev/null', {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
timeout: 5000,
|
|
31
|
+
}).trim();
|
|
32
|
+
if (!raw) return [];
|
|
33
|
+
return raw.split('\n').filter(Boolean).slice(0, 20);
|
|
34
|
+
} catch (_err) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getRecentCommits() {
|
|
40
|
+
try {
|
|
41
|
+
const raw = execSync('git log --oneline -5 2>/dev/null', {
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
timeout: 5000,
|
|
44
|
+
}).trim();
|
|
45
|
+
if (!raw) return [];
|
|
46
|
+
return raw.split('\n').filter(Boolean);
|
|
47
|
+
} catch (_err) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getSessionReads(baseDir) {
|
|
53
|
+
const readsPath = path.join(baseDir, 'diary', '_session-reads.json');
|
|
54
|
+
if (!fs.existsSync(readsPath)) return null;
|
|
55
|
+
try {
|
|
56
|
+
const data = JSON.parse(fs.readFileSync(readsPath, 'utf8'));
|
|
57
|
+
const count = Object.keys(data).length;
|
|
58
|
+
const repeats = Object.values(data).filter((v) => v > 1).length;
|
|
59
|
+
// Clean up session file
|
|
60
|
+
fs.unlinkSync(readsPath);
|
|
61
|
+
return { count, repeats };
|
|
62
|
+
} catch (_err) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
25
67
|
function main() {
|
|
26
68
|
const baseDir = findMindloreDir();
|
|
27
69
|
if (!baseDir) return;
|
|
@@ -38,7 +80,12 @@ function main() {
|
|
|
38
80
|
// Don't overwrite existing delta (idempotent)
|
|
39
81
|
if (fs.existsSync(deltaPath)) return;
|
|
40
82
|
|
|
41
|
-
|
|
83
|
+
// Gather structured data
|
|
84
|
+
const commits = getRecentCommits();
|
|
85
|
+
const changedFiles = getRecentGitChanges();
|
|
86
|
+
const reads = getSessionReads(baseDir);
|
|
87
|
+
|
|
88
|
+
const sections = [
|
|
42
89
|
'---',
|
|
43
90
|
`slug: delta-${dateStr}`,
|
|
44
91
|
'type: diary',
|
|
@@ -48,14 +95,33 @@ function main() {
|
|
|
48
95
|
`# Session Delta — ${dateStr}`,
|
|
49
96
|
'',
|
|
50
97
|
`Session ended: ${now.toISOString()}`,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// Commits section
|
|
101
|
+
sections.push('', '## Commits');
|
|
102
|
+
if (commits.length > 0) {
|
|
103
|
+
for (const c of commits) sections.push(`- ${c}`);
|
|
104
|
+
} else {
|
|
105
|
+
sections.push('- _(no commits)_');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Changed files section
|
|
109
|
+
sections.push('', '## Changed Files');
|
|
110
|
+
if (changedFiles.length > 0) {
|
|
111
|
+
for (const f of changedFiles) sections.push(`- ${f}`);
|
|
112
|
+
} else {
|
|
113
|
+
sections.push('- _(no file changes)_');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Read stats (from read-guard, if active)
|
|
117
|
+
if (reads) {
|
|
118
|
+
sections.push('', '## Read Stats');
|
|
119
|
+
sections.push(`- ${reads.count} files read, ${reads.repeats} repeated reads`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
sections.push('');
|
|
57
123
|
|
|
58
|
-
fs.writeFileSync(deltaPath,
|
|
124
|
+
fs.writeFileSync(deltaPath, sections.join('\n'), 'utf8');
|
|
59
125
|
|
|
60
126
|
// Append to log.md
|
|
61
127
|
const logPath = path.join(baseDir, 'log.md');
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* mindlore-session-focus — SessionStart hook
|
|
6
6
|
*
|
|
7
7
|
* Injects last delta file content + INDEX.md into session context.
|
|
8
|
-
* Fires once at session start via
|
|
8
|
+
* Fires once at session start via stdout additionalContext.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
@@ -35,7 +35,7 @@ function main() {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (output.length > 0) {
|
|
38
|
-
process.
|
|
38
|
+
process.stdout.write(output.join('\n\n') + '\n');
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mindlore",
|
|
3
3
|
"description": "AI-native knowledge system for Claude Code. Persistent, searchable, evolving knowledge base with FTS5.",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"name": "mindlore-ingest",
|
|
@@ -11,7 +11,22 @@
|
|
|
11
11
|
{
|
|
12
12
|
"name": "mindlore-health",
|
|
13
13
|
"path": "skills/mindlore-health/SKILL.md",
|
|
14
|
-
"description": "Run
|
|
14
|
+
"description": "Run 18-point structural health check on .mindlore/ knowledge base"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "mindlore-query",
|
|
18
|
+
"path": "skills/mindlore-query/SKILL.md",
|
|
19
|
+
"description": "Search, ask, stats, brief — compounding knowledge pipeline"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "mindlore-log",
|
|
23
|
+
"path": "skills/mindlore-log/SKILL.md",
|
|
24
|
+
"description": "Session logging, pattern extraction, wiki updates"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "mindlore-decide",
|
|
28
|
+
"path": "skills/mindlore-decide/SKILL.md",
|
|
29
|
+
"description": "Record and list decisions with context, alternatives, rationale"
|
|
15
30
|
}
|
|
16
31
|
],
|
|
17
32
|
"hooks": [
|
|
@@ -23,6 +38,10 @@
|
|
|
23
38
|
"event": "UserPromptSubmit",
|
|
24
39
|
"script": "hooks/mindlore-search.cjs"
|
|
25
40
|
},
|
|
41
|
+
{
|
|
42
|
+
"event": "UserPromptSubmit",
|
|
43
|
+
"script": "hooks/mindlore-decision-detector.cjs"
|
|
44
|
+
},
|
|
26
45
|
{
|
|
27
46
|
"event": "FileChanged",
|
|
28
47
|
"script": "hooks/mindlore-index.cjs"
|
|
@@ -42,6 +61,11 @@
|
|
|
42
61
|
{
|
|
43
62
|
"event": "PostCompact",
|
|
44
63
|
"script": "hooks/mindlore-post-compact.cjs"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"event": "PreToolUse",
|
|
67
|
+
"script": "hooks/mindlore-read-guard.cjs",
|
|
68
|
+
"if": "Read"
|
|
45
69
|
}
|
|
46
70
|
]
|
|
47
71
|
}
|
package/scripts/init.cjs
CHANGED
|
@@ -14,7 +14,8 @@ const path = require('path');
|
|
|
14
14
|
|
|
15
15
|
// ── Constants ──────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
|
-
const { MINDLORE_DIR, DB_NAME, DIRECTORIES } = require('./lib/constants.cjs');
|
|
17
|
+
const { MINDLORE_DIR, DB_NAME, DIRECTORIES, homedir } = require('./lib/constants.cjs');
|
|
18
|
+
const { SQL_FTS_CREATE } = require('../hooks/lib/mindlore-common.cjs');
|
|
18
19
|
|
|
19
20
|
const TEMPLATE_FILES = ['INDEX.md', 'log.md'];
|
|
20
21
|
|
|
@@ -84,11 +85,51 @@ function copyTemplates(baseDir, packageRoot) {
|
|
|
84
85
|
|
|
85
86
|
// ── Step 3: Create FTS5 database ───────────────────────────────────────
|
|
86
87
|
|
|
88
|
+
function migrateDatabase(dbPath, Database) {
|
|
89
|
+
const db = new Database(dbPath);
|
|
90
|
+
try {
|
|
91
|
+
// Check if FTS5 table has the new schema (7 columns with slug, description, etc.)
|
|
92
|
+
const info = db.pragma('table_info(mindlore_fts)');
|
|
93
|
+
const columns = info.map((r) => r.name);
|
|
94
|
+
if (!columns.includes('slug') || !columns.includes('description')) {
|
|
95
|
+
log('Upgrading FTS5 schema (2 → 9 columns, porter stemmer)...');
|
|
96
|
+
db.exec('DROP TABLE IF EXISTS mindlore_fts');
|
|
97
|
+
db.exec(SQL_FTS_CREATE);
|
|
98
|
+
db.exec('DELETE FROM file_hashes');
|
|
99
|
+
db.close();
|
|
100
|
+
return true;
|
|
101
|
+
} else if (!columns.includes('tags')) {
|
|
102
|
+
log('Upgrading FTS5 schema (7 → 9 columns, +tags +quality)...');
|
|
103
|
+
db.exec('DROP TABLE IF EXISTS mindlore_fts');
|
|
104
|
+
db.exec(SQL_FTS_CREATE);
|
|
105
|
+
db.exec('DELETE FROM file_hashes');
|
|
106
|
+
db.close();
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
} catch (_err) {
|
|
110
|
+
// table_info fails on FTS5 virtual tables in some versions — recreate
|
|
111
|
+
db.exec('DROP TABLE IF EXISTS mindlore_fts');
|
|
112
|
+
db.exec(SQL_FTS_CREATE);
|
|
113
|
+
db.exec('DELETE FROM file_hashes');
|
|
114
|
+
db.close();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
db.close();
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
function createDatabase(baseDir) {
|
|
88
122
|
const dbPath = path.join(baseDir, DB_NAME);
|
|
89
123
|
if (fs.existsSync(dbPath)) {
|
|
90
|
-
|
|
91
|
-
return false;
|
|
124
|
+
let Database;
|
|
125
|
+
try { Database = require('better-sqlite3'); } catch (_err) { return false; }
|
|
126
|
+
const migrated = migrateDatabase(dbPath, Database);
|
|
127
|
+
if (migrated) {
|
|
128
|
+
log('FTS5 schema upgraded — run index to rebuild');
|
|
129
|
+
} else {
|
|
130
|
+
log('Database already exists, schema OK');
|
|
131
|
+
}
|
|
132
|
+
return migrated;
|
|
92
133
|
}
|
|
93
134
|
|
|
94
135
|
let Database;
|
|
@@ -103,10 +144,7 @@ function createDatabase(baseDir) {
|
|
|
103
144
|
const db = new Database(dbPath);
|
|
104
145
|
db.pragma('journal_mode = WAL');
|
|
105
146
|
|
|
106
|
-
db.exec(
|
|
107
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS mindlore_fts
|
|
108
|
-
USING fts5(path, content, tokenize='unicode61');
|
|
109
|
-
`);
|
|
147
|
+
db.exec(SQL_FTS_CREATE);
|
|
110
148
|
|
|
111
149
|
db.exec(`
|
|
112
150
|
CREATE TABLE IF NOT EXISTS file_hashes (
|
|
@@ -124,7 +162,7 @@ function createDatabase(baseDir) {
|
|
|
124
162
|
|
|
125
163
|
function mergeHooks(packageRoot) {
|
|
126
164
|
const settingsPath = path.join(
|
|
127
|
-
|
|
165
|
+
homedir(),
|
|
128
166
|
'.claude',
|
|
129
167
|
'settings.json'
|
|
130
168
|
);
|
|
@@ -239,7 +277,59 @@ function addSchemaToProjectDocs() {
|
|
|
239
277
|
return false;
|
|
240
278
|
}
|
|
241
279
|
|
|
242
|
-
// ── Step 6:
|
|
280
|
+
// ── Step 6: Register skills ────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
function registerSkills(packageRoot, plugin) {
|
|
283
|
+
const skillsDir = path.join(homedir(), '.claude', 'skills');
|
|
284
|
+
ensureDir(skillsDir);
|
|
285
|
+
|
|
286
|
+
if (!plugin.skills || plugin.skills.length === 0) return 0;
|
|
287
|
+
|
|
288
|
+
let added = 0;
|
|
289
|
+
for (const skill of plugin.skills) {
|
|
290
|
+
const skillSrcDir = path.join(packageRoot, path.dirname(skill.path));
|
|
291
|
+
const skillDestDir = path.join(skillsDir, skill.name);
|
|
292
|
+
|
|
293
|
+
ensureDir(skillDestDir);
|
|
294
|
+
const entries = fs.readdirSync(skillSrcDir, { withFileTypes: true });
|
|
295
|
+
for (const entry of entries) {
|
|
296
|
+
if (!entry.isFile()) continue;
|
|
297
|
+
fs.copyFileSync(
|
|
298
|
+
path.join(skillSrcDir, entry.name),
|
|
299
|
+
path.join(skillDestDir, entry.name)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
added++;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return added;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Step 7: Install better-sqlite3 if needed ──────────────────────────
|
|
309
|
+
|
|
310
|
+
function ensureBetterSqlite3() {
|
|
311
|
+
try {
|
|
312
|
+
require('better-sqlite3');
|
|
313
|
+
return true;
|
|
314
|
+
} catch (_err) {
|
|
315
|
+
try {
|
|
316
|
+
const { execSync } = require('child_process');
|
|
317
|
+
log('Installing better-sqlite3 (native dependency)...');
|
|
318
|
+
execSync('npm install better-sqlite3 --no-save', {
|
|
319
|
+
cwd: process.cwd(),
|
|
320
|
+
stdio: 'pipe',
|
|
321
|
+
timeout: 120000,
|
|
322
|
+
});
|
|
323
|
+
return true;
|
|
324
|
+
} catch (_installErr) {
|
|
325
|
+
log('WARNING: Could not install better-sqlite3. FTS5 search disabled.');
|
|
326
|
+
log(' Run manually: npm install better-sqlite3');
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Step 8: Add .mindlore/ to .gitignore ───────────────────────────────
|
|
243
333
|
|
|
244
334
|
function addToGitignore() {
|
|
245
335
|
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
@@ -263,9 +353,15 @@ function main() {
|
|
|
263
353
|
const args = process.argv.slice(2);
|
|
264
354
|
const command = args[0];
|
|
265
355
|
|
|
356
|
+
if (command === 'uninstall') {
|
|
357
|
+
require('./uninstall.cjs');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
266
361
|
if (command && command !== 'init') {
|
|
267
362
|
console.log(`Unknown command: ${command}`);
|
|
268
363
|
console.log('Usage: npx mindlore init [--recommended]');
|
|
364
|
+
console.log(' npx mindlore uninstall [--all]');
|
|
269
365
|
process.exit(1);
|
|
270
366
|
}
|
|
271
367
|
|
|
@@ -291,11 +387,20 @@ function main() {
|
|
|
291
387
|
: 'All templates already in place'
|
|
292
388
|
);
|
|
293
389
|
|
|
294
|
-
// Step 3:
|
|
390
|
+
// Step 3: better-sqlite3 (before DB creation so it's available)
|
|
391
|
+
ensureBetterSqlite3();
|
|
392
|
+
|
|
393
|
+
// Step 4: Database
|
|
295
394
|
const dbCreated = createDatabase(baseDir);
|
|
296
395
|
log(dbCreated ? 'Created FTS5 database' : 'Database already exists');
|
|
297
396
|
|
|
298
|
-
//
|
|
397
|
+
// Read plugin.json once for hooks + skills
|
|
398
|
+
const pluginPath = path.join(packageRoot, 'plugin.json');
|
|
399
|
+
const plugin = fs.existsSync(pluginPath)
|
|
400
|
+
? JSON.parse(fs.readFileSync(pluginPath, 'utf8'))
|
|
401
|
+
: {};
|
|
402
|
+
|
|
403
|
+
// Step 5: Hooks
|
|
299
404
|
const hooksAdded = mergeHooks(packageRoot);
|
|
300
405
|
if (typeof hooksAdded === 'number' && hooksAdded > 0) {
|
|
301
406
|
log(`Registered ${hooksAdded} hooks in ~/.claude/settings.json`);
|
|
@@ -303,7 +408,7 @@ function main() {
|
|
|
303
408
|
log('Hooks already registered (or settings.json not found)');
|
|
304
409
|
}
|
|
305
410
|
|
|
306
|
-
// Step
|
|
411
|
+
// Step 6: SCHEMA.md in projectDocFiles
|
|
307
412
|
const schemaAdded = addSchemaToProjectDocs();
|
|
308
413
|
log(
|
|
309
414
|
schemaAdded
|
|
@@ -311,7 +416,15 @@ function main() {
|
|
|
311
416
|
: 'SCHEMA.md already in project settings'
|
|
312
417
|
);
|
|
313
418
|
|
|
314
|
-
// Step
|
|
419
|
+
// Step 7: Skills
|
|
420
|
+
const skillsAdded = registerSkills(packageRoot, plugin);
|
|
421
|
+
log(
|
|
422
|
+
skillsAdded > 0
|
|
423
|
+
? `Registered ${skillsAdded} skills in ~/.claude/skills/`
|
|
424
|
+
: 'Skills already registered'
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Step 8: .gitignore
|
|
315
428
|
const gitignoreAdded = addToGitignore();
|
|
316
429
|
log(
|
|
317
430
|
gitignoreAdded
|
|
@@ -13,7 +13,7 @@ const path = require('path');
|
|
|
13
13
|
// ── Constants ──────────────────────────────────────────────────────────
|
|
14
14
|
|
|
15
15
|
const { DB_NAME } = require('./lib/constants.cjs');
|
|
16
|
-
const { sha256, getAllMdFiles } = require('../hooks/lib/mindlore-common.cjs');
|
|
16
|
+
const { sha256, getAllMdFiles, openDatabase, parseFrontmatter, extractFtsMetadata, SQL_FTS_INSERT } = require('../hooks/lib/mindlore-common.cjs');
|
|
17
17
|
|
|
18
18
|
// ── Main ───────────────────────────────────────────────────────────────
|
|
19
19
|
|
|
@@ -26,17 +26,12 @@ function main() {
|
|
|
26
26
|
process.exit(1);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
Database = require('better-sqlite3');
|
|
32
|
-
} catch (_err) {
|
|
29
|
+
const db = openDatabase(dbPath);
|
|
30
|
+
if (!db) {
|
|
33
31
|
console.error(' better-sqlite3 not installed. Run: npm install better-sqlite3');
|
|
34
32
|
process.exit(1);
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
const db = new Database(dbPath);
|
|
38
|
-
db.pragma('journal_mode = WAL');
|
|
39
|
-
|
|
40
35
|
// Prepare statements
|
|
41
36
|
const getHash = db.prepare('SELECT content_hash FROM file_hashes WHERE path = ?');
|
|
42
37
|
const upsertHash = db.prepare(`
|
|
@@ -47,7 +42,7 @@ function main() {
|
|
|
47
42
|
last_indexed = excluded.last_indexed
|
|
48
43
|
`);
|
|
49
44
|
const deleteFts = db.prepare('DELETE FROM mindlore_fts WHERE path = ?');
|
|
50
|
-
const insertFts = db.prepare(
|
|
45
|
+
const insertFts = db.prepare(SQL_FTS_INSERT);
|
|
51
46
|
|
|
52
47
|
// Get all .md files
|
|
53
48
|
const mdFiles = getAllMdFiles(baseDir);
|
|
@@ -71,8 +66,10 @@ function main() {
|
|
|
71
66
|
}
|
|
72
67
|
|
|
73
68
|
// Update FTS5
|
|
69
|
+
const { meta, body } = parseFrontmatter(content);
|
|
70
|
+
const { slug, description, type, category, title, tags, quality } = extractFtsMetadata(meta, body, filePath, baseDir);
|
|
74
71
|
deleteFts.run(filePath);
|
|
75
|
-
insertFts.run(filePath,
|
|
72
|
+
insertFts.run(filePath, slug, description, type, category, title, body, tags, quality);
|
|
76
73
|
|
|
77
74
|
// Update hash
|
|
78
75
|
upsertHash.run(filePath, hash, now);
|