mindlore 0.6.2 → 0.6.4

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 (134) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +3 -3
  3. package/dist/scripts/cc-session-sync.d.ts +1 -0
  4. package/dist/scripts/cc-session-sync.d.ts.map +1 -1
  5. package/dist/scripts/cc-session-sync.js +3 -2
  6. package/dist/scripts/cc-session-sync.js.map +1 -1
  7. package/dist/scripts/fetch-raw.js +96 -7
  8. package/dist/scripts/fetch-raw.js.map +1 -1
  9. package/dist/scripts/init.js +6 -25
  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 +97 -0
  14. package/dist/scripts/lib/chunker.js.map +1 -0
  15. package/dist/scripts/lib/constants.d.ts +5 -0
  16. package/dist/scripts/lib/constants.d.ts.map +1 -1
  17. package/dist/scripts/lib/constants.js +35 -1
  18. package/dist/scripts/lib/constants.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 +90 -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-v063.d.ts +7 -0
  31. package/dist/scripts/lib/migrations-v063.d.ts.map +1 -0
  32. package/dist/scripts/lib/migrations-v063.js +58 -0
  33. package/dist/scripts/lib/migrations-v063.js.map +1 -0
  34. package/dist/scripts/lib/proximity.d.ts +3 -0
  35. package/dist/scripts/lib/proximity.d.ts.map +1 -0
  36. package/dist/scripts/lib/proximity.js +59 -0
  37. package/dist/scripts/lib/proximity.js.map +1 -0
  38. package/dist/scripts/lib/rrf.d.ts +28 -0
  39. package/dist/scripts/lib/rrf.d.ts.map +1 -0
  40. package/dist/scripts/lib/rrf.js +67 -0
  41. package/dist/scripts/lib/rrf.js.map +1 -0
  42. package/dist/scripts/lib/search-cache.d.ts +34 -0
  43. package/dist/scripts/lib/search-cache.d.ts.map +1 -0
  44. package/dist/scripts/lib/search-cache.js +102 -0
  45. package/dist/scripts/lib/search-cache.js.map +1 -0
  46. package/dist/scripts/lib/search-engine.d.ts +22 -0
  47. package/dist/scripts/lib/search-engine.d.ts.map +1 -0
  48. package/dist/scripts/lib/search-engine.js +104 -0
  49. package/dist/scripts/lib/search-engine.js.map +1 -0
  50. package/dist/scripts/lib/session-payload.d.ts +2 -4
  51. package/dist/scripts/lib/session-payload.d.ts.map +1 -1
  52. package/dist/scripts/lib/session-payload.js +31 -43
  53. package/dist/scripts/lib/session-payload.js.map +1 -1
  54. package/dist/scripts/lib/snippet.d.ts +2 -0
  55. package/dist/scripts/lib/snippet.d.ts.map +1 -0
  56. package/dist/scripts/lib/snippet.js +32 -0
  57. package/dist/scripts/lib/snippet.js.map +1 -0
  58. package/dist/scripts/mindlore-fts5-index.js +44 -3
  59. package/dist/scripts/mindlore-fts5-index.js.map +1 -1
  60. package/dist/scripts/mindlore-fts5-search.d.ts +3 -5
  61. package/dist/scripts/mindlore-fts5-search.d.ts.map +1 -1
  62. package/dist/scripts/mindlore-fts5-search.js +41 -116
  63. package/dist/scripts/mindlore-fts5-search.js.map +1 -1
  64. package/dist/scripts/mindlore-rrf-bench.d.ts +2 -0
  65. package/dist/scripts/mindlore-rrf-bench.d.ts.map +1 -0
  66. package/dist/scripts/mindlore-rrf-bench.js +30 -0
  67. package/dist/scripts/mindlore-rrf-bench.js.map +1 -0
  68. package/dist/tests/chunker.test.d.ts +2 -0
  69. package/dist/tests/chunker.test.d.ts.map +1 -0
  70. package/dist/tests/chunker.test.js +40 -0
  71. package/dist/tests/chunker.test.js.map +1 -0
  72. package/dist/tests/chunks-migration.test.d.ts +2 -0
  73. package/dist/tests/chunks-migration.test.d.ts.map +1 -0
  74. package/dist/tests/chunks-migration.test.js +55 -0
  75. package/dist/tests/chunks-migration.test.js.map +1 -0
  76. package/dist/tests/compaction-snapshot.test.js +47 -0
  77. package/dist/tests/compaction-snapshot.test.js.map +1 -1
  78. package/dist/tests/daemon-integration.test.js +5 -5
  79. package/dist/tests/daemon-integration.test.js.map +1 -1
  80. package/dist/tests/fuzzy.test.d.ts +2 -0
  81. package/dist/tests/fuzzy.test.d.ts.map +1 -0
  82. package/dist/tests/fuzzy.test.js +70 -0
  83. package/dist/tests/fuzzy.test.js.map +1 -0
  84. package/dist/tests/helpers/db.d.ts.map +1 -1
  85. package/dist/tests/helpers/db.js +2 -1
  86. package/dist/tests/helpers/db.js.map +1 -1
  87. package/dist/tests/merge-defaults.test.d.ts +2 -0
  88. package/dist/tests/merge-defaults.test.d.ts.map +1 -0
  89. package/dist/tests/merge-defaults.test.js +35 -0
  90. package/dist/tests/merge-defaults.test.js.map +1 -0
  91. package/dist/tests/migrations-v063.test.d.ts +2 -0
  92. package/dist/tests/migrations-v063.test.d.ts.map +1 -0
  93. package/dist/tests/migrations-v063.test.js +84 -0
  94. package/dist/tests/migrations-v063.test.js.map +1 -0
  95. package/dist/tests/proximity.test.d.ts +2 -0
  96. package/dist/tests/proximity.test.d.ts.map +1 -0
  97. package/dist/tests/proximity.test.js +31 -0
  98. package/dist/tests/proximity.test.js.map +1 -0
  99. package/dist/tests/rrf.test.d.ts +2 -0
  100. package/dist/tests/rrf.test.d.ts.map +1 -0
  101. package/dist/tests/rrf.test.js +139 -0
  102. package/dist/tests/rrf.test.js.map +1 -0
  103. package/dist/tests/search-cache.test.d.ts +2 -0
  104. package/dist/tests/search-cache.test.d.ts.map +1 -0
  105. package/dist/tests/search-cache.test.js +127 -0
  106. package/dist/tests/search-cache.test.js.map +1 -0
  107. package/dist/tests/search-engine.test.d.ts +2 -0
  108. package/dist/tests/search-engine.test.d.ts.map +1 -0
  109. package/dist/tests/search-engine.test.js +125 -0
  110. package/dist/tests/search-engine.test.js.map +1 -0
  111. package/dist/tests/search-hook.test.js +3 -3
  112. package/dist/tests/search-hook.test.js.map +1 -1
  113. package/dist/tests/session-payload.test.d.ts +1 -1
  114. package/dist/tests/session-payload.test.js +1 -14
  115. package/dist/tests/session-payload.test.js.map +1 -1
  116. package/dist/tests/session-summary.test.js +40 -4
  117. package/dist/tests/session-summary.test.js.map +1 -1
  118. package/dist/tests/snippet.test.d.ts +2 -0
  119. package/dist/tests/snippet.test.d.ts.map +1 -0
  120. package/dist/tests/snippet.test.js +30 -0
  121. package/dist/tests/snippet.test.js.map +1 -0
  122. package/dist/tests/stop-words.test.d.ts +2 -0
  123. package/dist/tests/stop-words.test.d.ts.map +1 -0
  124. package/dist/tests/stop-words.test.js +21 -0
  125. package/dist/tests/stop-words.test.js.map +1 -0
  126. package/hooks/lib/mindlore-common.cjs +72 -35
  127. package/hooks/mindlore-index.cjs +6 -0
  128. package/hooks/mindlore-pre-compact.cjs +61 -52
  129. package/hooks/mindlore-search.cjs +91 -214
  130. package/hooks/mindlore-session-end.cjs +9 -15
  131. package/hooks/mindlore-session-focus.cjs +11 -26
  132. package/package.json +2 -2
  133. package/plugin.json +1 -1
  134. package/templates/config.json +1 -1
@@ -12,6 +12,10 @@ const fs = require('fs');
12
12
  const path = require('path');
13
13
  const { MINDLORE_DIR, DB_NAME, SKIP_FILES, sha256, openDatabase, parseFrontmatter, extractFtsMetadata, insertFtsRow, readHookStdin, getProjectName, resolveProject, globalDir, hookLog, withTelemetry } = require('./lib/mindlore-common.cjs');
14
14
 
15
+ function invalidateSearchCache(db) {
16
+ try { db.exec('DELETE FROM search_cache'); } catch (_) { /* table may not exist */ }
17
+ }
18
+
15
19
  function main() {
16
20
  const filePath = readHookStdin(['path', 'file_path']);
17
21
  if (!filePath) return;
@@ -55,6 +59,7 @@ function main() {
55
59
  try {
56
60
  db.prepare('DELETE FROM mindlore_fts WHERE path = ?').run(filePath);
57
61
  db.prepare('DELETE FROM file_hashes WHERE path = ?').run(filePath);
62
+ invalidateSearchCache(db);
58
63
  } finally {
59
64
  db.close();
60
65
  }
@@ -92,6 +97,7 @@ function main() {
92
97
  ).run(filePath, hash, new Date().toISOString());
93
98
  });
94
99
  updateIndex();
100
+ invalidateSearchCache(db);
95
101
  } finally {
96
102
  db.close();
97
103
  }
@@ -11,7 +11,63 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const { findMindloreDir, openDatabase, 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();
@@ -64,54 +120,9 @@ function main() {
64
120
  const diaryDir = path.join(baseDir, 'diary');
65
121
  try {
66
122
  const sections = [];
67
-
68
- try {
69
- const dbPath = path.join(baseDir, 'mindlore.db');
70
- const db = openDatabase(dbPath, { readonly: true });
71
- if (db) {
72
- try {
73
- const episodes = db.prepare(
74
- "SELECT kind, summary FROM episodes WHERE created_at > datetime('now', '-4 hours') ORDER BY created_at DESC LIMIT 20"
75
- ).all();
76
- if (episodes.length > 0) {
77
- const grouped = {};
78
- for (const ep of episodes) {
79
- const kind = ep.kind || 'other';
80
- if (!grouped[kind]) grouped[kind] = [];
81
- grouped[kind].push(ep.summary);
82
- }
83
- sections.push('## Session Episodes');
84
- for (const [kind, items] of Object.entries(grouped)) {
85
- sections.push(`### ${kind}`);
86
- for (const item of items) sections.push(`- ${item}`);
87
- }
88
- }
89
- } finally {
90
- db.close();
91
- }
92
- }
93
- } catch (_err) { /* graceful skip */ }
94
-
95
- try {
96
- const { execSync } = require('child_process');
97
- const diffStat = execSync('git diff --stat HEAD 2>/dev/null || echo ""', {
98
- encoding: 'utf8', timeout: 2000, windowsHide: true,
99
- }).trim();
100
- if (diffStat) {
101
- sections.push('## Changed Files (uncommitted)', '```', diffStat, '```');
102
- }
103
- } catch (_err) { /* no git */ }
104
-
105
- try {
106
- const plansDir = path.join(process.cwd(), '.claude', 'plans');
107
- if (fs.existsSync(plansDir)) {
108
- const plans = fs.readdirSync(plansDir).filter(f => f.endsWith('.md'));
109
- if (plans.length > 0) {
110
- const latestPlan = plans.sort().pop();
111
- sections.push(`## Active Plan: ${latestPlan}`);
112
- }
113
- }
114
- } catch (_err) { /* skip */ }
123
+ sections.push(...collectRecentEpisodes(baseDir));
124
+ sections.push(...collectGitDiff());
125
+ sections.push(...getActivePlan());
115
126
 
116
127
  if (sections.length > 0) {
117
128
  const snapshotContent = [
@@ -126,9 +137,7 @@ function main() {
126
137
  fs.writeFileSync(path.join(diaryDir, `compaction-snapshot-${ts}.md`), snapshotContent);
127
138
  }
128
139
 
129
- const snapshots = fs.readdirSync(diaryDir)
130
- .filter(f => f.startsWith('compaction-snapshot-'))
131
- .sort();
140
+ const snapshots = listSnapshots(diaryDir).filter(f => f.startsWith('compaction-'));
132
141
  while (snapshots.length > 5) {
133
142
  const oldest = snapshots.shift();
134
143
  if (oldest) fs.unlinkSync(path.join(diaryDir, oldest));
@@ -4,206 +4,115 @@
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 throttle = new SearchCacheMod.SearchThrottle(db);
69
+ const callCount = throttle.incrementCallCount(sessionId);
70
+ effectiveMax = throttle.getMaxResults(callCount);
71
+ if (effectiveMax === 0) {
72
+ hookLog('search', 'info', `Throttled (call #${callCount})`);
73
+ db.close();
74
+ continue;
75
+ }
76
+ const cached = cache.get(userMessage);
77
+ if (cached) {
78
+ const baseDir = path.dirname(dbPath);
79
+ for (const r of cached) allResults.push({ ...r, baseDir });
80
+ db.close();
81
+ continue;
111
82
  }
112
- db.close();
113
- return results;
114
83
  }
115
- } catch (hybridErr) {
116
- hookLog('search', 'warn', `Hybrid search fallback to FTS5: ${hybridErr?.message || hybridErr}`);
117
- }
118
- }
119
84
 
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,
85
+ const results = searchEngineMod.search(db, userMessage, {
86
+ project,
87
+ maxResults: effectiveMax,
88
+ synonyms,
153
89
  });
154
- }
155
- } catch (_err) {
156
- // FTS5 query error — silently skip
157
- } finally {
158
- db.close();
159
- }
160
-
161
- return results;
162
- }
163
90
 
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;
91
+ if (cache) cache.set(userMessage, results);
188
92
 
189
- const dbPaths = getAllDbs();
190
- if (dbPaths.length === 0) return;
191
-
192
- const keywords = extractKeywords(userMessage);
193
- if (keywords.length < MIN_QUERY_WORDS) return;
93
+ const baseDir = path.dirname(dbPath);
94
+ for (const r of results) {
95
+ allResults.push({ ...r, baseDir });
96
+ }
194
97
 
195
- const allScores = [];
196
- for (const dbPath of dbPaths) {
197
- allScores.push(...searchDb(dbPath, keywords));
98
+ // Recall count inside loop — avoid reopening DB
99
+ try {
100
+ const txn = db.transaction(() => {
101
+ for (const r of results) incrementRecallCount(db, r.path);
102
+ });
103
+ txn();
104
+ } catch (_e) { /* graceful */ }
105
+ } catch (err) {
106
+ hookLog('search', 'warn', `Search error: ${err?.message || err}`);
107
+ } finally {
108
+ db.close();
109
+ }
198
110
  }
199
111
 
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)
112
+ // Deduplicate by full path
204
113
  const seen = new Set();
205
114
  const unique = [];
206
- for (const r of allScores) {
115
+ for (const r of allResults) {
207
116
  const normalized = path.resolve(r.path);
208
117
  if (!seen.has(normalized)) {
209
118
  seen.add(normalized);
@@ -211,45 +120,30 @@ function main() {
211
120
  }
212
121
  }
213
122
 
123
+ // Sort by score descending, take top N
124
+ unique.sort((a, b) => b.score - a.score);
214
125
  const relevant = unique.slice(0, MAX_RESULTS);
215
126
  if (relevant.length === 0) return;
216
127
 
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
128
  // Token budget from config
239
- const config = readConfig(path.dirname(dbPaths[0]));
240
129
  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
130
+ const perResultChars = ((budget.perResult || 500) * 4);
243
131
  const totalChars = ((budget.searchResults || 1500) * 4);
244
132
 
245
- // Build rich inject output
133
+ // Build output
246
134
  const output = [];
247
135
  let totalUsed = 0;
248
136
  for (const r of relevant) {
249
137
  if (totalUsed >= totalChars) break;
250
138
  const relativePath = path.relative(r.baseDir, r.path).replace(/\\/g, '/');
251
139
 
252
- const headings = r.headings || [];
140
+ let headings = [];
141
+ const contentStr = r.content || '';
142
+ if (contentStr) {
143
+ try {
144
+ headings = extractHeadings(contentStr, 3);
145
+ } catch (_err) { /* skip */ }
146
+ }
253
147
 
254
148
  const category = r.category || path.dirname(relativePath).split('/')[0];
255
149
  const title = r.title || r.slug || path.basename(r.path, '.md');
@@ -263,32 +157,15 @@ function main() {
263
157
  output.push(truncated);
264
158
  }
265
159
 
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
160
  if (output.length > 0) {
283
161
  let outputStr = output.join('\n\n') + '\n';
284
162
 
285
- const OFFLOAD_THRESHOLD = 10240; // 10KB
163
+ const OFFLOAD_THRESHOLD = 10240;
286
164
  if (outputStr.length > OFFLOAD_THRESHOLD) {
287
165
  const baseDir = path.dirname(dbPaths[0]);
288
166
  const tmpDir = path.join(baseDir, 'tmp');
289
167
  fs.mkdirSync(tmpDir, { recursive: true });
290
168
 
291
- // Cleanup stale tmp files before writing new one (>1h old, keep max 20)
292
169
  try {
293
170
  const oneHourAgo = Date.now() - 3600000;
294
171
  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'),
@@ -221,20 +229,6 @@ function main() {
221
229
  fs.appendFileSync(logPath, logEntry, 'utf8');
222
230
  }
223
231
 
224
- // Raw accumulation warning
225
- try {
226
- const rawDir = path.join(baseDir, 'raw');
227
- const sourcesDir = path.join(baseDir, 'sources');
228
- if (fs.existsSync(rawDir) && fs.existsSync(sourcesDir)) {
229
- const rawFiles = fs.readdirSync(rawDir).filter(f => f.endsWith('.md'));
230
- const sourceFiles = new Set(fs.readdirSync(sourcesDir).filter(f => f.endsWith('.md')));
231
- const unpromoted = rawFiles.filter(f => !sourceFiles.has(f)).length;
232
- if (unpromoted >= 5) {
233
- process.stdout.write(`[Mindlore] ${unpromoted} raw dosya promote bekliyor — \`/mindlore-maintain triage\` ile listele\n`);
234
- }
235
- }
236
- } catch (_err) { /* graceful skip */ }
237
-
238
232
  // Heavy ops: detach into child process so CC can exit immediately.
239
233
  // Fixes "Hook cancelled" when CC kills the hook before completion.
240
234
  // See: https://github.com/anthropics/claude-code/issues/41577
@@ -10,38 +10,22 @@
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
- const { findMindloreDir, readConfig, openDatabase, hasEpisodesTable, querySupersededChains, formatSupersededChains, hookLog, getProjectName, parseFrontmatter, withTelemetry, withTimeoutDb } = require('./lib/mindlore-common.cjs');
14
-
15
- function isCorruptionError(err) {
16
- const code = err?.code ?? '';
17
- const msg = String(err?.message ?? err);
18
- return code === 'SQLITE_CORRUPT' || code === 'SQLITE_NOTADB' || /corrupt|malformed/i.test(msg);
19
- }
20
-
21
- function recoverCorruptDb(db, dbPath, reason) {
22
- try { db.close(); } catch { /* already closed */ }
23
- const bakPath = dbPath + '.corrupt.bak';
24
- try { fs.copyFileSync(dbPath, bakPath); } catch { /* best effort */ }
25
- try { fs.unlinkSync(dbPath); } catch { /* best effort */ }
26
- hookLog('session-focus', 'warn', reason);
27
- }
13
+ const { findMindloreDir, readConfig, openDatabase, hasEpisodesTable, querySupersededChains, formatSupersededChains, hookLog, getProjectName, parseFrontmatter, withTelemetry, withTimeoutDb, listSnapshots, isCorruptionError, recoverCorruptDb } = require('./lib/mindlore-common.cjs');
28
14
 
29
15
  function tryOpenDb(dbPath) {
30
16
  return openDatabase(dbPath, { readonly: true });
31
17
  }
32
18
 
33
- function loadDbContent(db, baseDir, config, output, timings) {
19
+ function loadDbContent(db, baseDir, config, output, timings, latestDeltaContent) {
34
20
  // Session payload: Session summary, Decisions, Friction, Learnings
35
21
  const tPayload = Date.now();
36
22
  try {
37
23
  const { buildSessionPayload } = require('../dist/scripts/lib/session-payload.js');
38
24
  const project = path.basename(process.cwd());
39
25
  const payloadBudget = config?.tokenBudget?.sessionInject ?? 2000;
40
- const payload = buildSessionPayload(db, baseDir, project, payloadBudget);
41
- if (!payload.skipInjection) {
42
- for (const section of payload.sections) {
43
- output.push(`[Mindlore ${section.label}]\n${section.content}`);
44
- }
26
+ const payload = buildSessionPayload(db, baseDir, project, payloadBudget, latestDeltaContent);
27
+ for (const section of payload.sections) {
28
+ output.push(`[Mindlore ${section.label}]\n${section.content}`);
45
29
  }
46
30
  } catch (_payloadErr) {
47
31
  // Session payload is optional — don't break session start
@@ -108,16 +92,17 @@ function main() {
108
92
  // Inject latest delta + reflect trigger (single readdirSync)
109
93
  const tDiary = Date.now();
110
94
  const diaryDir = path.join(baseDir, 'diary');
95
+ let latestDeltaContent = undefined;
111
96
  if (fs.existsSync(diaryDir)) {
112
97
  try {
113
- const diaryFiles = fs.readdirSync(diaryDir).filter(f => f.startsWith('delta-') && f.endsWith('.md'));
98
+ const diaryFiles = listSnapshots(diaryDir).filter(f => f.startsWith('delta-'));
114
99
 
115
100
  if (diaryFiles.length > 0) {
116
- const sorted = [...diaryFiles].sort();
117
- const latestName = sorted[sorted.length - 1];
101
+ const latestName = diaryFiles[diaryFiles.length - 1];
118
102
  const latestPath = path.join(diaryDir, latestName);
119
103
  sourceChars += fs.statSync(latestPath).size;
120
104
  const deltaContent = fs.readFileSync(latestPath, 'utf8').trim();
105
+ latestDeltaContent = deltaContent;
121
106
  const { meta } = parseFrontmatter(deltaContent);
122
107
  const deltaProject = meta.project || null;
123
108
  const currentProject = getProjectName();
@@ -163,10 +148,10 @@ function main() {
163
148
 
164
149
  if (db) {
165
150
  try {
166
- loadDbContent(db, baseDir, config, output, timings);
151
+ loadDbContent(db, baseDir, config, output, timings, latestDeltaContent);
167
152
  } catch (err) {
168
153
  if (isCorruptionError(err)) {
169
- recoverCorruptDb(db, dbPath, `Corrupt DB detected during query: ${err?.message ?? err}`);
154
+ recoverCorruptDb(db, dbPath, 'session-focus');
170
155
  }
171
156
  } finally {
172
157
  try { db.close(); } catch { /* already closed by recovery */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindlore",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "AI-native knowledge system for Claude Code",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "wiki"
37
37
  ],
38
38
  "author": "omrfc",
39
- "license": "MIT",
39
+ "license": "AGPL-3.0-only",
40
40
  "repository": {
41
41
  "type": "git",
42
42
  "url": "git+https://github.com/mindlore/mindlore.git"