mindlore 0.0.1
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 -0
- package/README.md +128 -0
- package/SCHEMA.md +221 -0
- package/hooks/lib/mindlore-common.cjs +90 -0
- package/hooks/mindlore-fts5-sync.cjs +91 -0
- package/hooks/mindlore-index.cjs +96 -0
- package/hooks/mindlore-post-compact.cjs +46 -0
- package/hooks/mindlore-pre-compact.cjs +48 -0
- package/hooks/mindlore-search.cjs +133 -0
- package/hooks/mindlore-session-end.cjs +68 -0
- package/hooks/mindlore-session-focus.cjs +42 -0
- package/package.json +55 -0
- package/plugin.json +47 -0
- package/scripts/init.cjs +335 -0
- package/scripts/lib/constants.cjs +49 -0
- package/scripts/mindlore-fts5-index.cjs +115 -0
- package/scripts/mindlore-fts5-search.cjs +122 -0
- package/scripts/mindlore-health-check.cjs +320 -0
- package/skills/mindlore-health/SKILL.md +55 -0
- package/skills/mindlore-ingest/SKILL.md +102 -0
- package/templates/INDEX.md +12 -0
- package/templates/log.md +4 -0
package/scripts/init.cjs
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* mindlore init — Initialize .mindlore/ knowledge base in current project.
|
|
6
|
+
*
|
|
7
|
+
* Usage: npx mindlore init [--recommended]
|
|
8
|
+
*
|
|
9
|
+
* Idempotent: running again does not destroy existing data.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const { MINDLORE_DIR, DB_NAME, DIRECTORIES } = require('./lib/constants.cjs');
|
|
18
|
+
|
|
19
|
+
const TEMPLATE_FILES = ['INDEX.md', 'log.md'];
|
|
20
|
+
|
|
21
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function log(msg) {
|
|
24
|
+
console.log(` ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolvePackageRoot() {
|
|
28
|
+
// When installed globally via npm, __dirname is inside the package
|
|
29
|
+
// Look for templates/ relative to this script
|
|
30
|
+
return path.resolve(__dirname, '..');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureDir(dirPath) {
|
|
34
|
+
if (!fs.existsSync(dirPath)) {
|
|
35
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Step 1: Create .mindlore/ directories ──────────────────────────────
|
|
42
|
+
|
|
43
|
+
function createDirectories(baseDir) {
|
|
44
|
+
let created = 0;
|
|
45
|
+
for (const dir of DIRECTORIES) {
|
|
46
|
+
if (ensureDir(path.join(baseDir, dir))) {
|
|
47
|
+
created++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return created;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Step 2: Copy template files ────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function copyTemplates(baseDir, packageRoot) {
|
|
56
|
+
const templatesDir = path.join(packageRoot, 'templates');
|
|
57
|
+
let copied = 0;
|
|
58
|
+
|
|
59
|
+
for (const file of TEMPLATE_FILES) {
|
|
60
|
+
const dest = path.join(baseDir, file);
|
|
61
|
+
if (!fs.existsSync(dest)) {
|
|
62
|
+
const src = path.join(templatesDir, file);
|
|
63
|
+
if (fs.existsSync(src)) {
|
|
64
|
+
fs.copyFileSync(src, dest);
|
|
65
|
+
copied++;
|
|
66
|
+
} else {
|
|
67
|
+
log(`WARNING: template not found: ${src}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Copy SCHEMA.md
|
|
73
|
+
const schemaSrc = path.join(packageRoot, 'SCHEMA.md');
|
|
74
|
+
const schemaDest = path.join(baseDir, 'SCHEMA.md');
|
|
75
|
+
if (!fs.existsSync(schemaDest)) {
|
|
76
|
+
if (fs.existsSync(schemaSrc)) {
|
|
77
|
+
fs.copyFileSync(schemaSrc, schemaDest);
|
|
78
|
+
copied++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return copied;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Step 3: Create FTS5 database ───────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function createDatabase(baseDir) {
|
|
88
|
+
const dbPath = path.join(baseDir, DB_NAME);
|
|
89
|
+
if (fs.existsSync(dbPath)) {
|
|
90
|
+
log('Database already exists, skipping');
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let Database;
|
|
95
|
+
try {
|
|
96
|
+
Database = require('better-sqlite3');
|
|
97
|
+
} catch (_err) {
|
|
98
|
+
log('WARNING: better-sqlite3 not installed. Run: npm install better-sqlite3');
|
|
99
|
+
log('Database creation skipped — run mindlore init again after installing.');
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const db = new Database(dbPath);
|
|
104
|
+
db.pragma('journal_mode = WAL');
|
|
105
|
+
|
|
106
|
+
db.exec(`
|
|
107
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS mindlore_fts
|
|
108
|
+
USING fts5(path, content, tokenize='unicode61');
|
|
109
|
+
`);
|
|
110
|
+
|
|
111
|
+
db.exec(`
|
|
112
|
+
CREATE TABLE IF NOT EXISTS file_hashes (
|
|
113
|
+
path TEXT PRIMARY KEY,
|
|
114
|
+
content_hash TEXT NOT NULL,
|
|
115
|
+
last_indexed TEXT NOT NULL
|
|
116
|
+
);
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
db.close();
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Step 4: Merge hooks into settings.json ─────────────────────────────
|
|
124
|
+
|
|
125
|
+
function mergeHooks(packageRoot) {
|
|
126
|
+
const settingsPath = path.join(
|
|
127
|
+
require('./lib/constants.cjs').homedir(),
|
|
128
|
+
'.claude',
|
|
129
|
+
'settings.json'
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (!fs.existsSync(settingsPath)) {
|
|
133
|
+
log('WARNING: ~/.claude/settings.json not found. Hooks not registered.');
|
|
134
|
+
log('Create it manually or install Claude Code first.');
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let settings;
|
|
139
|
+
try {
|
|
140
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
141
|
+
settings = JSON.parse(raw);
|
|
142
|
+
} catch (_err) {
|
|
143
|
+
log('WARNING: Could not parse settings.json. Hooks not registered.');
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Read plugin.json for hook definitions
|
|
148
|
+
const pluginPath = path.join(packageRoot, 'plugin.json');
|
|
149
|
+
if (!fs.existsSync(pluginPath)) {
|
|
150
|
+
log('WARNING: plugin.json not found. Hooks not registered.');
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const plugin = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
|
|
155
|
+
if (!plugin.hooks || plugin.hooks.length === 0) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!settings.hooks) {
|
|
160
|
+
settings.hooks = {};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let added = 0;
|
|
164
|
+
for (const hook of plugin.hooks) {
|
|
165
|
+
const event = hook.event;
|
|
166
|
+
if (!settings.hooks[event]) {
|
|
167
|
+
settings.hooks[event] = [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check if this hook already exists (by script path containing 'mindlore-')
|
|
171
|
+
const hookScript = path.join(packageRoot, hook.script);
|
|
172
|
+
const hookName = path.basename(hook.script, '.cjs');
|
|
173
|
+
|
|
174
|
+
const exists = settings.hooks[event].some((entry) => {
|
|
175
|
+
// CC format: each entry is { hooks: [{ type, command }] }
|
|
176
|
+
if (entry.hooks && Array.isArray(entry.hooks)) {
|
|
177
|
+
return entry.hooks.some((h) => (h.command || '').includes(hookName));
|
|
178
|
+
}
|
|
179
|
+
// Legacy flat format check
|
|
180
|
+
return (entry.command || '').includes(hookName);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!exists) {
|
|
184
|
+
settings.hooks[event].push({
|
|
185
|
+
hooks: [
|
|
186
|
+
{
|
|
187
|
+
type: 'command',
|
|
188
|
+
command: `node "${hookScript}"`,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
added++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (added > 0) {
|
|
197
|
+
// Backup before writing
|
|
198
|
+
const backupPath = settingsPath + '.mindlore-backup';
|
|
199
|
+
if (!fs.existsSync(backupPath)) {
|
|
200
|
+
fs.copyFileSync(settingsPath, backupPath);
|
|
201
|
+
}
|
|
202
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return added;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Step 5: Add SCHEMA.md to projectDocFiles ───────────────────────────
|
|
209
|
+
|
|
210
|
+
function addSchemaToProjectDocs() {
|
|
211
|
+
const projectSettingsDir = path.join(process.cwd(), '.claude');
|
|
212
|
+
const projectSettingsPath = path.join(projectSettingsDir, 'settings.json');
|
|
213
|
+
|
|
214
|
+
let settings = {};
|
|
215
|
+
if (fs.existsSync(projectSettingsPath)) {
|
|
216
|
+
try {
|
|
217
|
+
settings = JSON.parse(fs.readFileSync(projectSettingsPath, 'utf8'));
|
|
218
|
+
} catch (_err) {
|
|
219
|
+
settings = {};
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
ensureDir(projectSettingsDir);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!settings.projectDocFiles) {
|
|
226
|
+
settings.projectDocFiles = [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const schemaPath = path.join(MINDLORE_DIR, 'SCHEMA.md');
|
|
230
|
+
if (!settings.projectDocFiles.includes(schemaPath)) {
|
|
231
|
+
settings.projectDocFiles.push(schemaPath);
|
|
232
|
+
fs.writeFileSync(
|
|
233
|
+
projectSettingsPath,
|
|
234
|
+
JSON.stringify(settings, null, 2),
|
|
235
|
+
'utf8'
|
|
236
|
+
);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── Step 6: Add .mindlore/ to .gitignore ───────────────────────────────
|
|
243
|
+
|
|
244
|
+
function addToGitignore() {
|
|
245
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
246
|
+
const entry = '.mindlore/';
|
|
247
|
+
|
|
248
|
+
if (fs.existsSync(gitignorePath)) {
|
|
249
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
250
|
+
if (content.includes(entry)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
fs.appendFileSync(gitignorePath, `\n${entry}\n`, 'utf8');
|
|
254
|
+
} else {
|
|
255
|
+
fs.writeFileSync(gitignorePath, `${entry}\n`, 'utf8');
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Main ───────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
function main() {
|
|
263
|
+
const args = process.argv.slice(2);
|
|
264
|
+
const command = args[0];
|
|
265
|
+
|
|
266
|
+
if (command && command !== 'init') {
|
|
267
|
+
console.log(`Unknown command: ${command}`);
|
|
268
|
+
console.log('Usage: npx mindlore init [--recommended]');
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const isRecommended = args.includes('--recommended');
|
|
273
|
+
const packageRoot = resolvePackageRoot();
|
|
274
|
+
const baseDir = path.join(process.cwd(), MINDLORE_DIR);
|
|
275
|
+
|
|
276
|
+
console.log('\n Mindlore — AI-native knowledge system\n');
|
|
277
|
+
|
|
278
|
+
// Step 1: Directories
|
|
279
|
+
const dirsCreated = createDirectories(baseDir);
|
|
280
|
+
log(
|
|
281
|
+
dirsCreated > 0
|
|
282
|
+
? `Created ${dirsCreated} directories in ${MINDLORE_DIR}/`
|
|
283
|
+
: 'All directories already exist'
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Step 2: Templates
|
|
287
|
+
const filesCopied = copyTemplates(baseDir, packageRoot);
|
|
288
|
+
log(
|
|
289
|
+
filesCopied > 0
|
|
290
|
+
? `Copied ${filesCopied} template files`
|
|
291
|
+
: 'All templates already in place'
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Step 3: Database
|
|
295
|
+
const dbCreated = createDatabase(baseDir);
|
|
296
|
+
log(dbCreated ? 'Created FTS5 database' : 'Database already exists');
|
|
297
|
+
|
|
298
|
+
// Step 4: Hooks
|
|
299
|
+
const hooksAdded = mergeHooks(packageRoot);
|
|
300
|
+
if (typeof hooksAdded === 'number' && hooksAdded > 0) {
|
|
301
|
+
log(`Registered ${hooksAdded} hooks in ~/.claude/settings.json`);
|
|
302
|
+
} else {
|
|
303
|
+
log('Hooks already registered (or settings.json not found)');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Step 5: SCHEMA.md in projectDocFiles
|
|
307
|
+
const schemaAdded = addSchemaToProjectDocs();
|
|
308
|
+
log(
|
|
309
|
+
schemaAdded
|
|
310
|
+
? 'Added SCHEMA.md to project settings'
|
|
311
|
+
: 'SCHEMA.md already in project settings'
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Step 6: .gitignore
|
|
315
|
+
const gitignoreAdded = addToGitignore();
|
|
316
|
+
log(
|
|
317
|
+
gitignoreAdded
|
|
318
|
+
? 'Added .mindlore/ to .gitignore'
|
|
319
|
+
: '.mindlore/ already in .gitignore'
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Recommended profile tips
|
|
323
|
+
if (isRecommended) {
|
|
324
|
+
console.log('\n Recommended setup:');
|
|
325
|
+
log('Install markitdown for better web/doc extraction:');
|
|
326
|
+
log(' pip install markitdown');
|
|
327
|
+
log('');
|
|
328
|
+
log('Install context-mode for token savings:');
|
|
329
|
+
log(' See: https://github.com/context-mode/context-mode');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log('\n Done! Start with: /mindlore-ingest\n');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
main();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared constants and utilities for mindlore scripts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const MINDLORE_DIR = '.mindlore';
|
|
10
|
+
const DB_NAME = 'mindlore.db';
|
|
11
|
+
|
|
12
|
+
const DIRECTORIES = [
|
|
13
|
+
'raw',
|
|
14
|
+
'sources',
|
|
15
|
+
'domains',
|
|
16
|
+
'analyses',
|
|
17
|
+
'insights',
|
|
18
|
+
'connections',
|
|
19
|
+
'learnings',
|
|
20
|
+
'diary',
|
|
21
|
+
'decisions',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const SKIP_FILES = new Set(['INDEX.md', 'SCHEMA.md', 'log.md']);
|
|
25
|
+
|
|
26
|
+
const TYPE_TO_DIR = {
|
|
27
|
+
raw: 'raw',
|
|
28
|
+
source: 'sources',
|
|
29
|
+
domain: 'domains',
|
|
30
|
+
analysis: 'analyses',
|
|
31
|
+
insight: 'insights',
|
|
32
|
+
connection: 'connections',
|
|
33
|
+
learning: 'learnings',
|
|
34
|
+
decision: 'decisions',
|
|
35
|
+
diary: 'diary',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function homedir() {
|
|
39
|
+
return os.homedir();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
MINDLORE_DIR,
|
|
44
|
+
DB_NAME,
|
|
45
|
+
DIRECTORIES,
|
|
46
|
+
SKIP_FILES,
|
|
47
|
+
TYPE_TO_DIR,
|
|
48
|
+
homedir,
|
|
49
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* mindlore-fts5-index — Full re-index of .mindlore/ into FTS5 database.
|
|
6
|
+
*
|
|
7
|
+
* Scans all .md files, computes SHA256 content-hash, skips unchanged files.
|
|
8
|
+
* Usage: node scripts/mindlore-fts5-index.cjs [path-to-mindlore-dir]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const { DB_NAME } = require('./lib/constants.cjs');
|
|
16
|
+
const { sha256, getAllMdFiles } = require('../hooks/lib/mindlore-common.cjs');
|
|
17
|
+
|
|
18
|
+
// ── Main ───────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function main() {
|
|
21
|
+
const baseDir = process.argv[2] || path.join(process.cwd(), '.mindlore');
|
|
22
|
+
const dbPath = path.join(baseDir, DB_NAME);
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(dbPath)) {
|
|
25
|
+
console.error(' Database not found. Run: npx mindlore init');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let Database;
|
|
30
|
+
try {
|
|
31
|
+
Database = require('better-sqlite3');
|
|
32
|
+
} catch (_err) {
|
|
33
|
+
console.error(' better-sqlite3 not installed. Run: npm install better-sqlite3');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const db = new Database(dbPath);
|
|
38
|
+
db.pragma('journal_mode = WAL');
|
|
39
|
+
|
|
40
|
+
// Prepare statements
|
|
41
|
+
const getHash = db.prepare('SELECT content_hash FROM file_hashes WHERE path = ?');
|
|
42
|
+
const upsertHash = db.prepare(`
|
|
43
|
+
INSERT INTO file_hashes (path, content_hash, last_indexed)
|
|
44
|
+
VALUES (?, ?, ?)
|
|
45
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
46
|
+
content_hash = excluded.content_hash,
|
|
47
|
+
last_indexed = excluded.last_indexed
|
|
48
|
+
`);
|
|
49
|
+
const deleteFts = db.prepare('DELETE FROM mindlore_fts WHERE path = ?');
|
|
50
|
+
const insertFts = db.prepare('INSERT INTO mindlore_fts (path, content) VALUES (?, ?)');
|
|
51
|
+
|
|
52
|
+
// Get all .md files
|
|
53
|
+
const mdFiles = getAllMdFiles(baseDir);
|
|
54
|
+
let indexed = 0;
|
|
55
|
+
let skipped = 0;
|
|
56
|
+
let errors = 0;
|
|
57
|
+
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
|
|
60
|
+
const transaction = db.transaction(() => {
|
|
61
|
+
for (const filePath of mdFiles) {
|
|
62
|
+
try {
|
|
63
|
+
const content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
64
|
+
const hash = sha256(content);
|
|
65
|
+
|
|
66
|
+
// Check if content changed
|
|
67
|
+
const existing = getHash.get(filePath);
|
|
68
|
+
if (existing && existing.content_hash === hash) {
|
|
69
|
+
skipped++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Update FTS5
|
|
74
|
+
deleteFts.run(filePath);
|
|
75
|
+
insertFts.run(filePath, content);
|
|
76
|
+
|
|
77
|
+
// Update hash
|
|
78
|
+
upsertHash.run(filePath, hash, now);
|
|
79
|
+
indexed++;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error(` Error indexing ${path.basename(filePath)}: ${err.message}`);
|
|
82
|
+
errors++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
transaction();
|
|
88
|
+
|
|
89
|
+
// Clean up entries for deleted files
|
|
90
|
+
const allIndexed = db.prepare('SELECT path FROM file_hashes').all();
|
|
91
|
+
const existingPaths = new Set(mdFiles);
|
|
92
|
+
let removed = 0;
|
|
93
|
+
|
|
94
|
+
const deleteHash = db.prepare('DELETE FROM file_hashes WHERE path = ?');
|
|
95
|
+
const cleanupTransaction = db.transaction(() => {
|
|
96
|
+
for (const row of allIndexed) {
|
|
97
|
+
if (!existingPaths.has(row.path)) {
|
|
98
|
+
deleteFts.run(row.path);
|
|
99
|
+
deleteHash.run(row.path);
|
|
100
|
+
removed++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
cleanupTransaction();
|
|
106
|
+
db.close();
|
|
107
|
+
|
|
108
|
+
console.log(
|
|
109
|
+
`\n FTS5 Index: ${indexed} indexed, ${skipped} unchanged, ${removed} removed, ${errors} errors\n`
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
process.exit(errors > 0 ? 1 : 0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* mindlore-fts5-search — Search .mindlore/ knowledge base via FTS5.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node scripts/mindlore-fts5-search.cjs "query" [path-to-mindlore-dir]
|
|
8
|
+
*
|
|
9
|
+
* Returns top 3 results ranked by BM25 with file path and snippet.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const { DB_NAME } = require('./lib/constants.cjs');
|
|
16
|
+
const MAX_RESULTS = 3;
|
|
17
|
+
|
|
18
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function extractHeadings(content, maxHeadings) {
|
|
21
|
+
const lines = content.split('\n');
|
|
22
|
+
const headings = [];
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (line.startsWith('#')) {
|
|
25
|
+
headings.push(line.replace(/^#+\s*/, '').trim());
|
|
26
|
+
if (headings.length >= maxHeadings) break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return headings;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Main ───────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function main() {
|
|
35
|
+
const query = process.argv[2];
|
|
36
|
+
if (!query) {
|
|
37
|
+
console.error('Usage: node mindlore-fts5-search.cjs "query" [mindlore-dir]');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const baseDir = process.argv[3] || path.join(process.cwd(), '.mindlore');
|
|
42
|
+
const dbPath = path.join(baseDir, DB_NAME);
|
|
43
|
+
|
|
44
|
+
if (!fs.existsSync(dbPath)) {
|
|
45
|
+
console.error(' Database not found. Run: npx mindlore init && npm run index');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let Database;
|
|
50
|
+
try {
|
|
51
|
+
Database = require('better-sqlite3');
|
|
52
|
+
} catch (_err) {
|
|
53
|
+
console.error(' better-sqlite3 not installed.');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const db = new Database(dbPath, { readonly: true });
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Sanitize query for FTS5 (escape special chars)
|
|
61
|
+
const sanitized = query.replace(/['"(){}[\]*:^~!]/g, ' ').trim();
|
|
62
|
+
if (!sanitized) {
|
|
63
|
+
console.log(' No valid search terms.');
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const results = db
|
|
68
|
+
.prepare(
|
|
69
|
+
`SELECT path, snippet(mindlore_fts, 1, '>>>', '<<<', '...', 40) as snippet,
|
|
70
|
+
rank
|
|
71
|
+
FROM mindlore_fts
|
|
72
|
+
WHERE mindlore_fts MATCH ?
|
|
73
|
+
ORDER BY rank
|
|
74
|
+
LIMIT ?`
|
|
75
|
+
)
|
|
76
|
+
.all(sanitized, MAX_RESULTS);
|
|
77
|
+
|
|
78
|
+
if (results.length === 0) {
|
|
79
|
+
console.log(` No results for: "${query}"`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`\n Mindlore Search: "${query}" (${results.length} results)\n`);
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < results.length; i++) {
|
|
86
|
+
const r = results[i];
|
|
87
|
+
const relativePath = path.relative(baseDir, r.path);
|
|
88
|
+
const fileName = path.basename(r.path, '.md');
|
|
89
|
+
|
|
90
|
+
// Try to get headings from the actual file
|
|
91
|
+
let headings = [];
|
|
92
|
+
if (fs.existsSync(r.path)) {
|
|
93
|
+
const content = fs.readFileSync(r.path, 'utf8');
|
|
94
|
+
headings = extractHeadings(content, 2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(` ${i + 1}. ${relativePath}`);
|
|
98
|
+
if (headings.length > 0) {
|
|
99
|
+
console.log(` ${headings.join(' > ')}`);
|
|
100
|
+
}
|
|
101
|
+
console.log(` ${r.snippet || fileName}`);
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// FTS5 query syntax error — try simpler query
|
|
106
|
+
if (err.message.includes('fts5')) {
|
|
107
|
+
const words = query.split(/\s+/).filter((w) => w.length >= 2);
|
|
108
|
+
if (words.length > 0) {
|
|
109
|
+
console.error(` Search syntax error. Try simpler terms: ${words.join(' ')}`);
|
|
110
|
+
} else {
|
|
111
|
+
console.error(` Search error: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
console.error(` Error: ${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
} finally {
|
|
118
|
+
db.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main();
|