mindlore 0.6.1 → 0.6.3

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.
Files changed (177) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +3 -3
  3. package/dist/scripts/cc-session-sync.d.ts +2 -0
  4. package/dist/scripts/cc-session-sync.d.ts.map +1 -1
  5. package/dist/scripts/cc-session-sync.js +56 -0
  6. package/dist/scripts/cc-session-sync.js.map +1 -1
  7. package/dist/scripts/fetch-raw.js +133 -8
  8. package/dist/scripts/fetch-raw.js.map +1 -1
  9. package/dist/scripts/init.js +28 -43
  10. package/dist/scripts/init.js.map +1 -1
  11. package/dist/scripts/lib/chunker.d.ts +12 -0
  12. package/dist/scripts/lib/chunker.d.ts.map +1 -0
  13. package/dist/scripts/lib/chunker.js +94 -0
  14. package/dist/scripts/lib/chunker.js.map +1 -0
  15. package/dist/scripts/lib/episodes.d.ts +1 -1
  16. package/dist/scripts/lib/episodes.d.ts.map +1 -1
  17. package/dist/scripts/lib/episodes.js +1 -1
  18. package/dist/scripts/lib/episodes.js.map +1 -1
  19. package/dist/scripts/lib/fuzzy.d.ts +10 -0
  20. package/dist/scripts/lib/fuzzy.d.ts.map +1 -0
  21. package/dist/scripts/lib/fuzzy.js +88 -0
  22. package/dist/scripts/lib/fuzzy.js.map +1 -0
  23. package/dist/scripts/lib/hybrid-search.d.ts +1 -0
  24. package/dist/scripts/lib/hybrid-search.d.ts.map +1 -1
  25. package/dist/scripts/lib/hybrid-search.js.map +1 -1
  26. package/dist/scripts/lib/merge-defaults.d.ts +5 -0
  27. package/dist/scripts/lib/merge-defaults.d.ts.map +1 -0
  28. package/dist/scripts/lib/merge-defaults.js +25 -0
  29. package/dist/scripts/lib/merge-defaults.js.map +1 -0
  30. package/dist/scripts/lib/migrations-v062.d.ts +3 -0
  31. package/dist/scripts/lib/migrations-v062.d.ts.map +1 -0
  32. package/dist/scripts/lib/migrations-v062.js +35 -0
  33. package/dist/scripts/lib/migrations-v062.js.map +1 -0
  34. package/dist/scripts/lib/migrations-v063.d.ts +7 -0
  35. package/dist/scripts/lib/migrations-v063.d.ts.map +1 -0
  36. package/dist/scripts/lib/migrations-v063.js +58 -0
  37. package/dist/scripts/lib/migrations-v063.js.map +1 -0
  38. package/dist/scripts/lib/proximity.d.ts +3 -0
  39. package/dist/scripts/lib/proximity.d.ts.map +1 -0
  40. package/dist/scripts/lib/proximity.js +53 -0
  41. package/dist/scripts/lib/proximity.js.map +1 -0
  42. package/dist/scripts/lib/rrf.d.ts +23 -0
  43. package/dist/scripts/lib/rrf.d.ts.map +1 -0
  44. package/dist/scripts/lib/rrf.js +63 -0
  45. package/dist/scripts/lib/rrf.js.map +1 -0
  46. package/dist/scripts/lib/search-cache.d.ts +20 -0
  47. package/dist/scripts/lib/search-cache.d.ts.map +1 -0
  48. package/dist/scripts/lib/search-cache.js +60 -0
  49. package/dist/scripts/lib/search-cache.js.map +1 -0
  50. package/dist/scripts/lib/search-engine.d.ts +22 -0
  51. package/dist/scripts/lib/search-engine.d.ts.map +1 -0
  52. package/dist/scripts/lib/search-engine.js +105 -0
  53. package/dist/scripts/lib/search-engine.js.map +1 -0
  54. package/dist/scripts/lib/session-payload.d.ts +2 -4
  55. package/dist/scripts/lib/session-payload.d.ts.map +1 -1
  56. package/dist/scripts/lib/session-payload.js +43 -43
  57. package/dist/scripts/lib/session-payload.js.map +1 -1
  58. package/dist/scripts/lib/snippet.d.ts +2 -0
  59. package/dist/scripts/lib/snippet.d.ts.map +1 -0
  60. package/dist/scripts/lib/snippet.js +32 -0
  61. package/dist/scripts/lib/snippet.js.map +1 -0
  62. package/dist/scripts/lib/triage.d.ts +18 -0
  63. package/dist/scripts/lib/triage.d.ts.map +1 -0
  64. package/dist/scripts/lib/triage.js +81 -0
  65. package/dist/scripts/lib/triage.js.map +1 -0
  66. package/dist/scripts/mindlore-doctor.d.ts +1 -0
  67. package/dist/scripts/mindlore-doctor.d.ts.map +1 -1
  68. package/dist/scripts/mindlore-doctor.js +26 -1
  69. package/dist/scripts/mindlore-doctor.js.map +1 -1
  70. package/dist/scripts/mindlore-fts5-index.js +44 -3
  71. package/dist/scripts/mindlore-fts5-index.js.map +1 -1
  72. package/dist/scripts/mindlore-fts5-search.d.ts +3 -5
  73. package/dist/scripts/mindlore-fts5-search.d.ts.map +1 -1
  74. package/dist/scripts/mindlore-fts5-search.js +41 -116
  75. package/dist/scripts/mindlore-fts5-search.js.map +1 -1
  76. package/dist/scripts/mindlore-health-check.d.ts +10 -0
  77. package/dist/scripts/mindlore-health-check.d.ts.map +1 -1
  78. package/dist/scripts/mindlore-health-check.js +26 -1
  79. package/dist/scripts/mindlore-health-check.js.map +1 -1
  80. package/dist/scripts/mindlore-perf.d.ts +3 -3
  81. package/dist/scripts/mindlore-perf.d.ts.map +1 -1
  82. package/dist/scripts/mindlore-perf.js +10 -8
  83. package/dist/scripts/mindlore-perf.js.map +1 -1
  84. package/dist/tests/chunker.test.d.ts +2 -0
  85. package/dist/tests/chunker.test.d.ts.map +1 -0
  86. package/dist/tests/chunker.test.js +40 -0
  87. package/dist/tests/chunker.test.js.map +1 -0
  88. package/dist/tests/chunks-migration.test.d.ts +2 -0
  89. package/dist/tests/chunks-migration.test.d.ts.map +1 -0
  90. package/dist/tests/chunks-migration.test.js +55 -0
  91. package/dist/tests/chunks-migration.test.js.map +1 -0
  92. package/dist/tests/compaction-snapshot.test.d.ts +2 -0
  93. package/dist/tests/compaction-snapshot.test.d.ts.map +1 -0
  94. package/dist/tests/compaction-snapshot.test.js +102 -0
  95. package/dist/tests/compaction-snapshot.test.js.map +1 -0
  96. package/dist/tests/daemon-integration.test.js +5 -5
  97. package/dist/tests/daemon-integration.test.js.map +1 -1
  98. package/dist/tests/diary.test.js +3 -3
  99. package/dist/tests/diary.test.js.map +1 -1
  100. package/dist/tests/doctor.test.js +13 -0
  101. package/dist/tests/doctor.test.js.map +1 -1
  102. package/dist/tests/fetch-raw.test.js +12 -5
  103. package/dist/tests/fetch-raw.test.js.map +1 -1
  104. package/dist/tests/fuzzy.test.d.ts +2 -0
  105. package/dist/tests/fuzzy.test.d.ts.map +1 -0
  106. package/dist/tests/fuzzy.test.js +70 -0
  107. package/dist/tests/fuzzy.test.js.map +1 -0
  108. package/dist/tests/health-check-memory.test.js +27 -0
  109. package/dist/tests/health-check-memory.test.js.map +1 -1
  110. package/dist/tests/helpers/db.d.ts.map +1 -1
  111. package/dist/tests/helpers/db.js +4 -1
  112. package/dist/tests/helpers/db.js.map +1 -1
  113. package/dist/tests/init.test.js +20 -0
  114. package/dist/tests/init.test.js.map +1 -1
  115. package/dist/tests/merge-defaults.test.d.ts +2 -0
  116. package/dist/tests/merge-defaults.test.d.ts.map +1 -0
  117. package/dist/tests/merge-defaults.test.js +35 -0
  118. package/dist/tests/merge-defaults.test.js.map +1 -0
  119. package/dist/tests/migrations-v062.test.d.ts +2 -0
  120. package/dist/tests/migrations-v062.test.d.ts.map +1 -0
  121. package/dist/tests/migrations-v062.test.js +61 -0
  122. package/dist/tests/migrations-v062.test.js.map +1 -0
  123. package/dist/tests/migrations-v063.test.d.ts +2 -0
  124. package/dist/tests/migrations-v063.test.d.ts.map +1 -0
  125. package/dist/tests/migrations-v063.test.js +84 -0
  126. package/dist/tests/migrations-v063.test.js.map +1 -0
  127. package/dist/tests/nomination.test.js +1 -1
  128. package/dist/tests/proximity.test.d.ts +2 -0
  129. package/dist/tests/proximity.test.d.ts.map +1 -0
  130. package/dist/tests/proximity.test.js +31 -0
  131. package/dist/tests/proximity.test.js.map +1 -0
  132. package/dist/tests/rrf.test.d.ts +2 -0
  133. package/dist/tests/rrf.test.d.ts.map +1 -0
  134. package/dist/tests/rrf.test.js +100 -0
  135. package/dist/tests/rrf.test.js.map +1 -0
  136. package/dist/tests/savings.test.d.ts +2 -0
  137. package/dist/tests/savings.test.d.ts.map +1 -0
  138. package/dist/tests/savings.test.js +87 -0
  139. package/dist/tests/savings.test.js.map +1 -0
  140. package/dist/tests/search-cache.test.d.ts +2 -0
  141. package/dist/tests/search-cache.test.d.ts.map +1 -0
  142. package/dist/tests/search-cache.test.js +95 -0
  143. package/dist/tests/search-cache.test.js.map +1 -0
  144. package/dist/tests/search-engine.test.d.ts +2 -0
  145. package/dist/tests/search-engine.test.d.ts.map +1 -0
  146. package/dist/tests/search-engine.test.js +125 -0
  147. package/dist/tests/search-engine.test.js.map +1 -0
  148. package/dist/tests/search-hook.test.js +3 -3
  149. package/dist/tests/search-hook.test.js.map +1 -1
  150. package/dist/tests/session-payload.test.d.ts +1 -1
  151. package/dist/tests/session-payload.test.js +1 -14
  152. package/dist/tests/session-payload.test.js.map +1 -1
  153. package/dist/tests/session-summary.test.d.ts +2 -0
  154. package/dist/tests/session-summary.test.d.ts.map +1 -0
  155. package/dist/tests/session-summary.test.js +138 -0
  156. package/dist/tests/session-summary.test.js.map +1 -0
  157. package/dist/tests/snippet.test.d.ts +2 -0
  158. package/dist/tests/snippet.test.d.ts.map +1 -0
  159. package/dist/tests/snippet.test.js +30 -0
  160. package/dist/tests/snippet.test.js.map +1 -0
  161. package/dist/tests/telemetry-perf.test.js +7 -0
  162. package/dist/tests/telemetry-perf.test.js.map +1 -1
  163. package/dist/tests/triage.test.d.ts +2 -0
  164. package/dist/tests/triage.test.d.ts.map +1 -0
  165. package/dist/tests/triage.test.js +69 -0
  166. package/dist/tests/triage.test.js.map +1 -0
  167. package/hooks/lib/mindlore-common.cjs +74 -13
  168. package/hooks/mindlore-index.cjs +6 -0
  169. package/hooks/mindlore-post-compact.cjs +23 -3
  170. package/hooks/mindlore-pre-compact.cjs +86 -3
  171. package/hooks/mindlore-search.cjs +90 -214
  172. package/hooks/mindlore-session-end.cjs +22 -18
  173. package/hooks/mindlore-session-focus.cjs +92 -71
  174. package/package.json +6 -5
  175. package/plugin.json +1 -1
  176. package/skills/mindlore-maintain/SKILL.md +1 -0
  177. package/templates/config.json +1 -1
@@ -11,7 +11,63 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const { findMindloreDir, hookLog, withTelemetry } = require('./lib/mindlore-common.cjs');
14
+ const { findMindloreDir, openDatabase, hookLog, withTelemetry, listSnapshots } = require('./lib/mindlore-common.cjs');
15
+
16
+ function collectRecentEpisodes(baseDir) {
17
+ try {
18
+ const dbPath = path.join(baseDir, 'mindlore.db');
19
+ const db = openDatabase(dbPath, { readonly: true });
20
+ if (!db) return [];
21
+ try {
22
+ const episodes = db.prepare(
23
+ "SELECT kind, summary FROM episodes WHERE created_at > datetime('now', '-4 hours') ORDER BY created_at DESC LIMIT 20"
24
+ ).all();
25
+ if (episodes.length === 0) return [];
26
+ const grouped = {};
27
+ for (const ep of episodes) {
28
+ const kind = ep.kind || 'other';
29
+ if (!grouped[kind]) grouped[kind] = [];
30
+ grouped[kind].push(ep.summary);
31
+ }
32
+ const lines = ['## Session Episodes'];
33
+ for (const [kind, items] of Object.entries(grouped)) {
34
+ lines.push(`### ${kind}`);
35
+ for (const item of items) lines.push(`- ${item}`);
36
+ }
37
+ return lines;
38
+ } finally {
39
+ db.close();
40
+ }
41
+ } catch (_err) {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ function collectGitDiff() {
47
+ try {
48
+ const { execSync } = require('child_process');
49
+ const diffStat = execSync('git diff --stat HEAD 2>/dev/null || echo ""', {
50
+ encoding: 'utf8', timeout: 2000, windowsHide: true,
51
+ }).trim();
52
+ if (diffStat) return ['## Changed Files (uncommitted)', '```', diffStat, '```'];
53
+ return [];
54
+ } catch (_err) {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ function getActivePlan() {
60
+ try {
61
+ const plansDir = path.join(process.cwd(), '.claude', 'plans');
62
+ if (!fs.existsSync(plansDir)) return [];
63
+ const plans = fs.readdirSync(plansDir).filter(f => f.endsWith('.md'));
64
+ if (plans.length === 0) return [];
65
+ const latestPlan = plans.sort().pop();
66
+ return [`## Active Plan: ${latestPlan}`];
67
+ } catch (_err) {
68
+ return [];
69
+ }
70
+ }
15
71
 
16
72
  function main() {
17
73
  const baseDir = findMindloreDir();
@@ -34,11 +90,10 @@ function main() {
34
90
 
35
91
  const now = new Date();
36
92
  const iso = now.toISOString();
93
+ const ts = iso.replace(/[:.]/g, '-');
37
94
 
38
- // Write pre-compact episode
39
95
  const episodesDir = path.join(baseDir, 'episodes');
40
96
  try {
41
- const ts = iso.replace(/[:.]/g, '-');
42
97
  const episodePath = path.join(episodesDir, `pre-compact-${ts}.md`);
43
98
  const content = [
44
99
  '---',
@@ -61,6 +116,34 @@ function main() {
61
116
  fs.appendFileSync(logPath, entry, 'utf8');
62
117
  } catch (_err) { /* log file may not exist */ }
63
118
 
119
+ // Build compaction snapshot (#17)
120
+ const diaryDir = path.join(baseDir, 'diary');
121
+ try {
122
+ const sections = [];
123
+ sections.push(...collectRecentEpisodes(baseDir));
124
+ sections.push(...collectGitDiff());
125
+ sections.push(...getActivePlan());
126
+
127
+ if (sections.length > 0) {
128
+ const snapshotContent = [
129
+ '---',
130
+ 'type: compaction-snapshot',
131
+ `date: ${iso.slice(0, 10)}`,
132
+ `project: ${path.basename(process.cwd())}`,
133
+ '---',
134
+ '',
135
+ ...sections,
136
+ ].join('\n');
137
+ fs.writeFileSync(path.join(diaryDir, `compaction-snapshot-${ts}.md`), snapshotContent);
138
+ }
139
+
140
+ const snapshots = listSnapshots(diaryDir).filter(f => f.startsWith('compaction-'));
141
+ while (snapshots.length > 5) {
142
+ const oldest = snapshots.shift();
143
+ if (oldest) fs.unlinkSync(path.join(diaryDir, oldest));
144
+ }
145
+ } catch (_err) { /* snapshot is best-effort */ }
146
+
64
147
  process.stdout.write('[Mindlore: pre-compact FTS5 flush complete]\n');
65
148
  }
66
149
 
@@ -4,206 +4,114 @@
4
4
  /**
5
5
  * mindlore-search — UserPromptSubmit hook
6
6
  *
7
- * Extracts keywords from user prompt, searches FTS5 with per-keyword scoring,
8
- * injects top results with description + headings (matching old knowledge system quality).
7
+ * Thin wrapper over search-engine.ts pipeline.
8
+ * Extracts keywords from user prompt, delegates search to modular engine,
9
+ * injects top results with description + headings.
9
10
  */
10
11
 
11
12
  const fs = require('fs');
12
13
  const path = require('path');
13
- const { getAllDbs, openDatabase, extractHeadings, readHookStdin, extractKeywords, sanitizeKeyword, readConfig, loadSqliteVecCjs, hasVecTableCjs, hookLog, incrementRecallCount, getDaemonPortFile, withTelemetry, fixVersionTokens } = require('./lib/mindlore-common.cjs');
14
-
15
- const { execFileSync } = require('child_process');
14
+ const { getAllDbs, openDatabase, extractHeadings, readHookStdin, readConfig, hookLog, incrementRecallCount, withTelemetry } = require('./lib/mindlore-common.cjs');
16
15
 
17
16
  const MAX_RESULTS = 3;
18
17
  const MIN_QUERY_WORDS = 3;
19
18
 
20
- // Try to load hybrid search module (built TS)
21
- let hybridSearchMod;
19
+ let searchEngineMod;
22
20
  try {
23
- hybridSearchMod = require('../dist/scripts/lib/hybrid-search.js');
21
+ searchEngineMod = require('../dist/scripts/lib/search-engine.js');
24
22
  } catch (_err) {
25
- // hybrid-search not built yet — pure FTS5 mode
23
+ // search-engine not built yet
26
24
  }
27
25
 
28
- // v0.5.5: Request embedding from daemon via execFileSync bridge
29
- function requestEmbeddingSync(query) {
30
- try {
31
- const portFile = getDaemonPortFile();
32
- if (!fs.existsSync(portFile)) return null;
33
- const clientScript = path.join(__dirname, '..', 'scripts', 'lib', 'daemon-client.js');
34
- if (!fs.existsSync(clientScript)) return null;
35
- const result = execFileSync(process.execPath, [clientScript, portFile, query, '300'], {
36
- timeout: 500, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true,
37
- });
38
- const parsed = JSON.parse(result.trim());
39
- return parsed.type === 'embedding' ? parsed.embedding : null;
40
- } catch {
41
- return null;
42
- }
26
+ let SearchCacheMod;
27
+ try {
28
+ SearchCacheMod = require('../dist/scripts/lib/search-cache.js');
29
+ } catch (_err) {
30
+ // search-cache not built yet
43
31
  }
44
32
 
45
- /**
46
- * Search a single DB and return scored results with their baseDir.
47
- */
48
- function searchDb(dbPath, keywords) {
49
- const baseDir = path.dirname(dbPath);
50
- const db = openDatabase(dbPath, { readonly: true });
51
- if (!db) return [];
52
- const results = [];
33
+ function main() {
34
+ const userMessage = readHookStdin(['prompt', 'content', 'message', 'query']);
35
+ if (!userMessage || userMessage.length < MIN_QUERY_WORDS) return;
53
36
 
54
- // v0.5.0: Try hybrid search with synonym expansion (no embedding — hooks are sync)
55
- if (!hybridSearchMod) {
56
- hookLog('search', 'info', 'No hybridSearchMod — FTS5-only mode');
57
- }
58
- if (hybridSearchMod && loadSqliteVecCjs(db) && hasVecTableCjs(db)) {
59
- try {
60
- const config = readConfig(baseDir);
61
- const synonyms = (config && config.synonyms) ? config.synonyms : {};
37
+ const dbPaths = getAllDbs();
38
+ if (dbPaths.length === 0) return;
62
39
 
63
- // Expand keywords with synonyms
64
- const expandedTerms = keywords.slice();
65
- for (const kw of keywords) {
66
- const lower = kw.toLowerCase();
67
- if (synonyms[lower]) {
68
- expandedTerms.push(...synonyms[lower]);
69
- }
70
- }
40
+ if (!searchEngineMod) {
41
+ hookLog('search', 'warn', 'search-engine module not available — skipping');
42
+ return;
43
+ }
71
44
 
72
- // v0.5.5: Try to get queryEmbedding from daemon
73
- let queryEmbedding = null;
74
- try {
75
- queryEmbedding = requestEmbeddingSync(expandedTerms.join(' '));
76
- if (!queryEmbedding) {
77
- hookLog('search', 'info', 'Daemon not available — FTS5-only hybrid mode');
78
- }
79
- } catch {
80
- hookLog('search', 'info', 'Daemon connection failed — FTS5-only hybrid mode');
81
- }
45
+ const project = path.basename(process.cwd());
46
+ const config = readConfig(path.dirname(dbPaths[0]));
47
+ const synonyms = (config && config.synonyms) ? config.synonyms : {};
82
48
 
83
- const fusedResults = hybridSearchMod.hybridSearch(db, expandedTerms.join(' '), {
84
- maxResults: MAX_RESULTS,
85
- project: path.basename(process.cwd()),
86
- queryEmbedding,
87
- });
49
+ // Read session_id from stdin for throttling
50
+ let sessionId;
51
+ try {
52
+ const stdinData = JSON.parse(process.env.CLAUDE_HOOK_STDIN || '{}');
53
+ sessionId = stdinData.session_id || 'unknown';
54
+ } catch (_) {
55
+ sessionId = 'unknown';
56
+ }
88
57
 
89
- if (fusedResults.length > 0) {
90
- for (const r of fusedResults) {
91
- const filePath = r.path || '';
92
- let headings = [];
93
- if (filePath) {
94
- try {
95
- const content = fs.readFileSync(filePath, 'utf8');
96
- headings = extractHeadings(content, 3);
97
- } catch (_err) { /* file may have been deleted */ }
98
- }
99
- results.push({
100
- path: filePath,
101
- slug: r.slug,
102
- description: r.description || '',
103
- category: r.category || '',
104
- title: r.title || '',
105
- tags: r.tags || '',
106
- headings,
107
- hits: 1,
108
- rank: r.score,
109
- baseDir,
110
- });
58
+ const allResults = [];
59
+ for (const dbPath of dbPaths) {
60
+ const db = openDatabase(dbPath);
61
+ if (!db) continue;
62
+ try {
63
+ // Cache + throttle
64
+ let cache;
65
+ let effectiveMax = MAX_RESULTS;
66
+ if (SearchCacheMod) {
67
+ cache = new SearchCacheMod.SearchCache(db, { ttlMs: 300000 });
68
+ const callCount = cache.incrementCallCount(sessionId);
69
+ effectiveMax = cache.getMaxResults(callCount);
70
+ if (effectiveMax === 0) {
71
+ hookLog('search', 'info', `Throttled (call #${callCount})`);
72
+ db.close();
73
+ continue;
74
+ }
75
+ const cached = cache.get(userMessage);
76
+ if (cached) {
77
+ const baseDir = path.dirname(dbPath);
78
+ for (const r of cached) allResults.push({ ...r, baseDir });
79
+ db.close();
80
+ continue;
111
81
  }
112
- db.close();
113
- return results;
114
82
  }
115
- } catch (hybridErr) {
116
- hookLog('search', 'warn', `Hybrid search fallback to FTS5: ${hybridErr?.message || hybridErr}`);
117
- }
118
- }
119
83
 
120
- // FTS5-only fallback: OR-joined single query (replaces O(docs×keywords) nested loop)
121
- try {
122
- const sanitized = keywords.map(sanitizeKeyword).filter(Boolean);
123
- if (sanitized.length === 0) { db.close(); return results; }
124
-
125
- const ftsQuery = fixVersionTokens(sanitized.join(' OR '));
126
- const project = path.basename(process.cwd());
127
-
128
- // v0.6.1: Project-scoped search with global fallback
129
- let rows = db.prepare(
130
- `SELECT path, slug, description, category, title, tags, rank
131
- FROM mindlore_fts WHERE project = ? AND mindlore_fts MATCH ? ORDER BY rank LIMIT ?`
132
- ).all(project, ftsQuery, MAX_RESULTS * 2);
133
-
134
- if (rows.length === 0) {
135
- rows = db.prepare(
136
- `SELECT path, slug, description, category, title, tags, rank
137
- FROM mindlore_fts WHERE mindlore_fts MATCH ? ORDER BY rank LIMIT ?`
138
- ).all(ftsQuery, MAX_RESULTS * 2);
139
- }
140
-
141
- for (const r of rows) {
142
- results.push({
143
- path: r.path || '',
144
- slug: r.slug,
145
- description: r.description || '',
146
- category: r.category || '',
147
- title: r.title || '',
148
- tags: r.tags || '',
149
- headings: [], // populated later in main() after slicing
150
- hits: sanitized.length,
151
- rank: r.rank,
152
- baseDir,
84
+ const results = searchEngineMod.search(db, userMessage, {
85
+ project,
86
+ maxResults: effectiveMax,
87
+ synonyms,
153
88
  });
154
- }
155
- } catch (_err) {
156
- // FTS5 query error — silently skip
157
- } finally {
158
- db.close();
159
- }
160
-
161
- return results;
162
- }
163
89
 
164
- /**
165
- * Search episodes via FTS5 mirror (type = 'episode').
166
- * Reuses an already-open DB handle — no extra sqlite3_open.
167
- */
168
- function searchEpisodesFts(db, keywords) {
169
- try {
170
- const ftsQuery = keywords.map(sanitizeKeyword).filter(Boolean).join(' OR ');
171
- const rows = db.prepare(
172
- "SELECT title, category, slug, tags FROM mindlore_fts WHERE type = 'episode' AND mindlore_fts MATCH ? LIMIT 2"
173
- ).all(ftsQuery);
174
-
175
- return rows.map(r => {
176
- const tags = r.tags || '';
177
- const kind = tags.split(',')[0]?.trim() || 'episode';
178
- return `[episode] ${kind}: ${r.title || r.slug}`;
179
- });
180
- } catch (_err) {
181
- return [];
182
- }
183
- }
184
-
185
- function main() {
186
- const userMessage = readHookStdin(['prompt', 'content', 'message', 'query']);
187
- if (!userMessage || userMessage.length < MIN_QUERY_WORDS) return;
90
+ if (cache) cache.set(userMessage, results);
188
91
 
189
- const dbPaths = getAllDbs();
190
- if (dbPaths.length === 0) return;
191
-
192
- const keywords = extractKeywords(userMessage);
193
- if (keywords.length < MIN_QUERY_WORDS) return;
92
+ const baseDir = path.dirname(dbPath);
93
+ for (const r of results) {
94
+ allResults.push({ ...r, baseDir });
95
+ }
194
96
 
195
- const allScores = [];
196
- for (const dbPath of dbPaths) {
197
- allScores.push(...searchDb(dbPath, keywords));
97
+ // Recall count inside loop — avoid reopening DB
98
+ try {
99
+ const txn = db.transaction(() => {
100
+ for (const r of results) incrementRecallCount(db, r.path);
101
+ });
102
+ txn();
103
+ } catch (_e) { /* graceful */ }
104
+ } catch (err) {
105
+ hookLog('search', 'warn', `Search error: ${err?.message || err}`);
106
+ } finally {
107
+ db.close();
108
+ }
198
109
  }
199
110
 
200
- // Sort: most keyword hits first, then best rank
201
- allScores.sort((a, b) => b.hits - a.hits || a.rank - b.rank);
202
-
203
- // Deduplicate by full path (project version wins — appears first in sort)
111
+ // Deduplicate by full path
204
112
  const seen = new Set();
205
113
  const unique = [];
206
- for (const r of allScores) {
114
+ for (const r of allResults) {
207
115
  const normalized = path.resolve(r.path);
208
116
  if (!seen.has(normalized)) {
209
117
  seen.add(normalized);
@@ -211,45 +119,30 @@ function main() {
211
119
  }
212
120
  }
213
121
 
122
+ // Sort by score descending, take top N
123
+ unique.sort((a, b) => b.score - a.score);
214
124
  const relevant = unique.slice(0, MAX_RESULTS);
215
125
  if (relevant.length === 0) return;
216
126
 
217
- try {
218
- const db = openDatabase(dbPaths[0]);
219
- if (db) {
220
- const txn = db.transaction(() => {
221
- for (const r of relevant) incrementRecallCount(db, r.path);
222
- });
223
- txn();
224
- db.close();
225
- }
226
- } catch (_e) { /* graceful — never block search output */ }
227
-
228
- // Populate headings only for final results (avoid reading extra files)
229
- for (const r of relevant) {
230
- if (r.path && r.headings.length === 0 && fs.existsSync(r.path)) {
231
- try {
232
- const content = fs.readFileSync(r.path, 'utf8');
233
- r.headings = extractHeadings(content, 3);
234
- } catch (_err) { /* skip */ }
235
- }
236
- }
237
-
238
127
  // Token budget from config
239
- const config = readConfig(path.dirname(dbPaths[0]));
240
128
  const budget = (config && config.tokenBudget) || {};
241
- // Defaults match DEFAULT_TOKEN_BUDGET in scripts/lib/constants.ts
242
- const perResultChars = ((budget.perResult || 500) * 4); // ~4 chars/token
129
+ const perResultChars = ((budget.perResult || 500) * 4);
243
130
  const totalChars = ((budget.searchResults || 1500) * 4);
244
131
 
245
- // Build rich inject output
132
+ // Build output
246
133
  const output = [];
247
134
  let totalUsed = 0;
248
135
  for (const r of relevant) {
249
136
  if (totalUsed >= totalChars) break;
250
137
  const relativePath = path.relative(r.baseDir, r.path).replace(/\\/g, '/');
251
138
 
252
- const headings = r.headings || [];
139
+ let headings = [];
140
+ const contentStr = r.content || '';
141
+ if (contentStr) {
142
+ try {
143
+ headings = extractHeadings(contentStr, 3);
144
+ } catch (_err) { /* skip */ }
145
+ }
253
146
 
254
147
  const category = r.category || path.dirname(relativePath).split('/')[0];
255
148
  const title = r.title || r.slug || path.basename(r.path, '.md');
@@ -263,32 +156,15 @@ function main() {
263
156
  output.push(truncated);
264
157
  }
265
158
 
266
- // v0.4.0: Search episode mirrors in FTS5 (reuses searchDb's DB path, no extra open)
267
- if (relevant.length < MAX_RESULTS) {
268
- for (const dbPath of dbPaths) {
269
- try {
270
- const db = openDatabase(dbPath, { readonly: true });
271
- if (!db) continue;
272
- const episodeResults = searchEpisodesFts(db, keywords);
273
- db.close();
274
- if (episodeResults.length > 0) {
275
- output.push(`[Mindlore Episodes]\n${episodeResults.join('\n')}`);
276
- break;
277
- }
278
- } catch (_err) { /* skip */ }
279
- }
280
- }
281
-
282
159
  if (output.length > 0) {
283
160
  let outputStr = output.join('\n\n') + '\n';
284
161
 
285
- const OFFLOAD_THRESHOLD = 10240; // 10KB
162
+ const OFFLOAD_THRESHOLD = 10240;
286
163
  if (outputStr.length > OFFLOAD_THRESHOLD) {
287
164
  const baseDir = path.dirname(dbPaths[0]);
288
165
  const tmpDir = path.join(baseDir, 'tmp');
289
166
  fs.mkdirSync(tmpDir, { recursive: true });
290
167
 
291
- // Cleanup stale tmp files before writing new one (>1h old, keep max 20)
292
168
  try {
293
169
  const oneHourAgo = Date.now() - 3600000;
294
170
  const files = fs.readdirSync(tmpDir)
@@ -13,7 +13,7 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const os = require('os');
15
15
  const { execSync, execFileSync, spawn } = require('child_process');
16
- const { findMindloreDir, globalDir, getProjectName, openDatabase, ensureEpisodesTable, hasEpisodesTable, insertBareEpisode, insertFtsRow, hookLog, SHARED_EXPORT_DIRS, resolveWin32Bin, withTelemetry } = require('./lib/mindlore-common.cjs');
16
+ const { findMindloreDir, globalDir, getProjectName, openDatabase, ensureEpisodesTable, hasEpisodesTable, insertBareEpisode, insertFtsRow, hookLog, SHARED_EXPORT_DIRS, resolveWin32Bin, withTelemetry, getUnpromotedRawFiles } = require('./lib/mindlore-common.cjs');
17
17
 
18
18
  const EXPORT_DIRS = SHARED_EXPORT_DIRS;
19
19
 
@@ -82,6 +82,14 @@ if (process.argv.includes('--worker')) {
82
82
  }
83
83
  }, 'embed-trigger');
84
84
 
85
+ // Raw accumulation warning (moved from main to worker — off hot path)
86
+ await safeRunAsync(() => {
87
+ const unpromoted = getUnpromotedRawFiles(baseDir);
88
+ if (unpromoted.length >= 5) {
89
+ hookLog('session-end', 'info', `${unpromoted.length} raw files unpromoted`);
90
+ }
91
+ }, 'raw-check');
92
+
85
93
  // Obsidian + git-sync are independent — run in parallel
86
94
  await Promise.allSettled([
87
95
  safeRunAsync(() => syncObsidian(baseDir), 'obsidian'),
@@ -436,29 +444,25 @@ function syncObsidian(baseDir) {
436
444
  const destBase = path.join(vaultPath, 'mindlore');
437
445
  let exported = 0;
438
446
 
439
- for (const dir of EXPORT_DIRS) {
440
- const srcDir = path.join(baseDir, dir);
441
- if (!fs.existsSync(srcDir)) continue;
442
-
443
- const destDir = path.join(destBase, dir);
447
+ function walkAndExport(srcDir, destDir) {
448
+ if (!fs.existsSync(srcDir)) return;
444
449
  fs.mkdirSync(destDir, { recursive: true });
445
-
446
- for (const file of fs.readdirSync(srcDir).filter(f => f.endsWith('.md') && !f.startsWith('_'))) {
447
- if (exportMdFile(path.join(srcDir, file), path.join(destDir, file), convertFn)) exported++;
448
- }
449
-
450
- // One-level subdirectories (e.g. diary/mindlore/)
451
450
  for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
452
- if (!entry.isDirectory() || entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
453
- const subSrc = path.join(srcDir, entry.name);
454
- const subDest = path.join(destDir, entry.name);
455
- fs.mkdirSync(subDest, { recursive: true });
456
- for (const file of fs.readdirSync(subSrc).filter(f => f.endsWith('.md') && !f.startsWith('_'))) {
457
- if (exportMdFile(path.join(subSrc, file), path.join(subDest, file), convertFn)) exported++;
451
+ if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
452
+ const srcPath = path.join(srcDir, entry.name);
453
+ const destPath = path.join(destDir, entry.name);
454
+ if (entry.isDirectory()) {
455
+ walkAndExport(srcPath, destPath);
456
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
457
+ if (exportMdFile(srcPath, destPath, convertFn)) exported++;
458
458
  }
459
459
  }
460
460
  }
461
461
 
462
+ for (const dir of EXPORT_DIRS) {
463
+ walkAndExport(path.join(baseDir, dir), path.join(destBase, dir));
464
+ }
465
+
462
466
  for (const rootFile of ['INDEX.md', 'log.md']) {
463
467
  const srcPath = path.join(baseDir, rootFile);
464
468
  if (!fs.existsSync(srcPath)) continue;