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.
@@ -4,75 +4,64 @@
4
4
  /**
5
5
  * mindlore-search — UserPromptSubmit hook
6
6
  *
7
- * Extracts keywords from user prompt, searches FTS5, injects top 3 results.
8
- * Results: file path + first 2 headings via stderr additionalContext.
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 MIN_QUERY_LENGTH = 3;
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(/[^a-z0-9\u00e7\u011f\u0131\u00f6\u015f\u00fc\s-]/g, ' ')
55
+ .replace(/[^\w\s\u00e7\u011f\u0131\u00f6\u015f\u00fc-]/g, ' ')
40
56
  .split(/\s+/)
41
- .filter((w) => w.length >= MIN_QUERY_LENGTH && !stopWords.has(w));
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
- function extractHeadings(content, max) {
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
- // Read user prompt from stdin
60
- let input = '';
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 === 0) return;
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
- // Build FTS5 query OR between keywords
93
- const ftsQuery = keywords.join(' OR ');
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
- const results = db
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
- if (results.length === 0) return;
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 results) {
109
- const relativePath = path.relative(baseDir, r.path);
110
- let headings = [];
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, 2);
127
+ headings = extractHeadings(content, 5);
115
128
  }
116
129
 
117
- const headingStr = headings.length > 0 ? ` — ${headings.join(', ')}` : '';
118
- output.push(`${relativePath}${headingStr}`);
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.stderr.write(
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
- const content = [
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
- '## Changes',
53
- '',
54
- '_No structured changes tracked in v0.1. Upgrade to v0.2 for detailed deltas._',
55
- '',
56
- ].join('\n');
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, content, 'utf8');
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 stderr additionalContext.
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.stderr.write(output.join('\n\n') + '\n');
38
+ process.stdout.write(output.join('\n\n') + '\n');
39
39
  }
40
40
  }
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindlore",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "AI-native knowledge system for Claude Code",
5
5
  "type": "commonjs",
6
6
  "bin": {
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.1",
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 16-point structural health check on .mindlore/ knowledge base"
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
- log('Database already exists, skipping');
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
- require('./lib/constants.cjs').homedir(),
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: Add .mindlore/ to .gitignore ───────────────────────────────
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: Database
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
- // Step 4: Hooks
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 5: SCHEMA.md in projectDocFiles
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 6: .gitignore
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
- let Database;
30
- try {
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('INSERT INTO mindlore_fts (path, content) VALUES (?, ?)');
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, content);
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);