mindlore 0.4.3 → 0.5.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.
Files changed (120) hide show
  1. package/README.md +2 -1
  2. package/dist/scripts/init.js +45 -3
  3. package/dist/scripts/init.js.map +1 -1
  4. package/dist/scripts/lib/constants.d.ts +15 -0
  5. package/dist/scripts/lib/constants.d.ts.map +1 -1
  6. package/dist/scripts/lib/constants.js +16 -1
  7. package/dist/scripts/lib/constants.js.map +1 -1
  8. package/dist/scripts/lib/db-helpers.d.ts +15 -0
  9. package/dist/scripts/lib/db-helpers.d.ts.map +1 -1
  10. package/dist/scripts/lib/db-helpers.js +51 -0
  11. package/dist/scripts/lib/db-helpers.js.map +1 -1
  12. package/dist/scripts/lib/embedding.d.ts +5 -0
  13. package/dist/scripts/lib/embedding.d.ts.map +1 -0
  14. package/dist/scripts/lib/embedding.js +44 -0
  15. package/dist/scripts/lib/embedding.js.map +1 -0
  16. package/dist/scripts/lib/hybrid-search.d.ts +62 -0
  17. package/dist/scripts/lib/hybrid-search.d.ts.map +1 -0
  18. package/dist/scripts/lib/hybrid-search.js +150 -0
  19. package/dist/scripts/lib/hybrid-search.js.map +1 -0
  20. package/dist/scripts/lib/migrations-v051.d.ts +3 -0
  21. package/dist/scripts/lib/migrations-v051.d.ts.map +1 -0
  22. package/dist/scripts/lib/migrations-v051.js +24 -0
  23. package/dist/scripts/lib/migrations-v051.js.map +1 -0
  24. package/dist/scripts/lib/migrations.d.ts +4 -0
  25. package/dist/scripts/lib/migrations.d.ts.map +1 -0
  26. package/dist/scripts/lib/migrations.js +40 -0
  27. package/dist/scripts/lib/migrations.js.map +1 -0
  28. package/dist/scripts/lib/privacy-filter.d.ts +3 -0
  29. package/dist/scripts/lib/privacy-filter.d.ts.map +1 -0
  30. package/dist/scripts/lib/privacy-filter.js +28 -0
  31. package/dist/scripts/lib/privacy-filter.js.map +1 -0
  32. package/dist/scripts/lib/schema-version.d.ts +13 -0
  33. package/dist/scripts/lib/schema-version.d.ts.map +1 -0
  34. package/dist/scripts/lib/schema-version.js +37 -0
  35. package/dist/scripts/lib/schema-version.js.map +1 -0
  36. package/dist/scripts/lib/similarity.d.ts +12 -0
  37. package/dist/scripts/lib/similarity.d.ts.map +1 -0
  38. package/dist/scripts/lib/similarity.js +64 -0
  39. package/dist/scripts/lib/similarity.js.map +1 -0
  40. package/dist/scripts/lib/synonym.d.ts +4 -0
  41. package/dist/scripts/lib/synonym.d.ts.map +1 -0
  42. package/dist/scripts/lib/synonym.js +37 -0
  43. package/dist/scripts/lib/synonym.js.map +1 -0
  44. package/dist/scripts/mindlore-fts5-index.d.ts +2 -1
  45. package/dist/scripts/mindlore-fts5-index.d.ts.map +1 -1
  46. package/dist/scripts/mindlore-fts5-index.js +71 -5
  47. package/dist/scripts/mindlore-fts5-index.js.map +1 -1
  48. package/dist/scripts/mindlore-fts5-search.d.ts +3 -2
  49. package/dist/scripts/mindlore-fts5-search.d.ts.map +1 -1
  50. package/dist/scripts/mindlore-fts5-search.js +89 -35
  51. package/dist/scripts/mindlore-fts5-search.js.map +1 -1
  52. package/dist/scripts/mindlore-health-check.js +105 -0
  53. package/dist/scripts/mindlore-health-check.js.map +1 -1
  54. package/dist/scripts/quality-populate.js +8 -4
  55. package/dist/scripts/quality-populate.js.map +1 -1
  56. package/dist/tests/cc-memory-sync.test.d.ts +2 -0
  57. package/dist/tests/cc-memory-sync.test.d.ts.map +1 -0
  58. package/dist/tests/cc-memory-sync.test.js +121 -0
  59. package/dist/tests/cc-memory-sync.test.js.map +1 -0
  60. package/dist/tests/embedding.test.d.ts +6 -0
  61. package/dist/tests/embedding.test.d.ts.map +1 -0
  62. package/dist/tests/embedding.test.js +71 -0
  63. package/dist/tests/embedding.test.js.map +1 -0
  64. package/dist/tests/episode-file.test.d.ts +2 -0
  65. package/dist/tests/episode-file.test.d.ts.map +1 -0
  66. package/dist/tests/episode-file.test.js +79 -0
  67. package/dist/tests/episode-file.test.js.map +1 -0
  68. package/dist/tests/fts5.test.js +82 -0
  69. package/dist/tests/fts5.test.js.map +1 -1
  70. package/dist/tests/helpers/db.d.ts +6 -0
  71. package/dist/tests/helpers/db.d.ts.map +1 -1
  72. package/dist/tests/helpers/db.js +29 -0
  73. package/dist/tests/helpers/db.js.map +1 -1
  74. package/dist/tests/hook-logging.test.d.ts +2 -0
  75. package/dist/tests/hook-logging.test.d.ts.map +1 -0
  76. package/dist/tests/hook-logging.test.js +108 -0
  77. package/dist/tests/hook-logging.test.js.map +1 -0
  78. package/dist/tests/hybrid-search.test.d.ts +2 -0
  79. package/dist/tests/hybrid-search.test.d.ts.map +1 -0
  80. package/dist/tests/hybrid-search.test.js +114 -0
  81. package/dist/tests/hybrid-search.test.js.map +1 -0
  82. package/dist/tests/index-cli-embed.test.d.ts +7 -0
  83. package/dist/tests/index-cli-embed.test.d.ts.map +1 -0
  84. package/dist/tests/index-cli-embed.test.js +128 -0
  85. package/dist/tests/index-cli-embed.test.js.map +1 -0
  86. package/dist/tests/privacy-filter.test.d.ts +2 -0
  87. package/dist/tests/privacy-filter.test.d.ts.map +1 -0
  88. package/dist/tests/privacy-filter.test.js +56 -0
  89. package/dist/tests/privacy-filter.test.js.map +1 -0
  90. package/dist/tests/schema-version.test.d.ts +2 -0
  91. package/dist/tests/schema-version.test.d.ts.map +1 -0
  92. package/dist/tests/schema-version.test.js +127 -0
  93. package/dist/tests/schema-version.test.js.map +1 -0
  94. package/dist/tests/search-cli-hybrid.test.d.ts +6 -0
  95. package/dist/tests/search-cli-hybrid.test.d.ts.map +1 -0
  96. package/dist/tests/search-cli-hybrid.test.js +103 -0
  97. package/dist/tests/search-cli-hybrid.test.js.map +1 -0
  98. package/dist/tests/search-hook.test.js +44 -0
  99. package/dist/tests/search-hook.test.js.map +1 -1
  100. package/dist/tests/similarity.test.d.ts +2 -0
  101. package/dist/tests/similarity.test.d.ts.map +1 -0
  102. package/dist/tests/similarity.test.js +61 -0
  103. package/dist/tests/similarity.test.js.map +1 -0
  104. package/dist/tests/synonym.test.d.ts +2 -0
  105. package/dist/tests/synonym.test.d.ts.map +1 -0
  106. package/dist/tests/synonym.test.js +47 -0
  107. package/dist/tests/synonym.test.js.map +1 -0
  108. package/dist/tests/token-budget.test.d.ts +2 -0
  109. package/dist/tests/token-budget.test.d.ts.map +1 -0
  110. package/dist/tests/token-budget.test.js +32 -0
  111. package/dist/tests/token-budget.test.js.map +1 -0
  112. package/hooks/lib/mindlore-common.cjs +120 -0
  113. package/hooks/mindlore-index.cjs +82 -2
  114. package/hooks/mindlore-search.cjs +102 -35
  115. package/hooks/mindlore-session-end.cjs +129 -39
  116. package/hooks/mindlore-session-focus.cjs +24 -3
  117. package/package.json +6 -4
  118. package/plugin.json +1 -1
  119. package/skills/mindlore-ingest/SKILL.md +7 -1
  120. package/templates/config.json +20 -1
@@ -10,11 +10,18 @@
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
- const { getAllDbs, requireDatabase, extractHeadings, readHookStdin, extractKeywords } = require('./lib/mindlore-common.cjs');
13
+ const { getAllDbs, requireDatabase, extractHeadings, readHookStdin, extractKeywords, sanitizeKeyword, readConfig, loadSqliteVecCjs, hasVecTableCjs, hookLog } = require('./lib/mindlore-common.cjs');
14
14
 
15
15
  const MAX_RESULTS = 3;
16
16
  const MIN_QUERY_WORDS = 3;
17
- const MIN_KEYWORD_HITS = 2;
17
+
18
+ // Try to load hybrid search module (built TS)
19
+ let hybridSearchMod;
20
+ try {
21
+ hybridSearchMod = require('../dist/scripts/lib/hybrid-search.js');
22
+ } catch (_err) {
23
+ // hybrid-search not built yet — pure FTS5 mode
24
+ }
18
25
 
19
26
  /**
20
27
  * Search a single DB and return scored results with their baseDir.
@@ -24,35 +31,79 @@ function searchDb(dbPath, keywords, Database) {
24
31
  const db = new Database(dbPath, { readonly: true });
25
32
  const results = [];
26
33
 
27
- try {
28
- const allPaths = db.prepare('SELECT DISTINCT path FROM mindlore_fts').all();
29
- const matchStmt = db.prepare('SELECT rank FROM mindlore_fts WHERE path = ? AND mindlore_fts MATCH ?');
30
- const metaStmt = db.prepare(
31
- 'SELECT slug, description, category, title, tags FROM mindlore_fts WHERE path = ?'
32
- );
33
-
34
- for (const row of allPaths) {
35
- let hits = 0;
36
- let totalRank = 0;
34
+ // v0.5.0: Try hybrid search with synonym expansion (no embedding — hooks are sync)
35
+ if (hybridSearchMod && loadSqliteVecCjs(db) && hasVecTableCjs(db)) {
36
+ try {
37
+ const config = readConfig(baseDir);
38
+ const synonyms = (config && config.synonyms) ? config.synonyms : {};
37
39
 
40
+ // Expand keywords with synonyms
41
+ const expandedTerms = keywords.slice();
38
42
  for (const kw of keywords) {
39
- try {
40
- const sanitized = kw.replace(/["*(){}[\]^~:]/g, '');
41
- if (!sanitized) continue;
42
- const r = matchStmt.get(row.path, '"' + sanitized + '"');
43
- if (r) {
44
- hits++;
45
- totalRank += r.rank;
46
- }
47
- } catch (_err) {
48
- // FTS5 query error for this keyword — skip
43
+ const lower = kw.toLowerCase();
44
+ if (synonyms[lower]) {
45
+ expandedTerms.push(...synonyms[lower]);
49
46
  }
50
47
  }
51
48
 
52
- if (hits >= MIN_KEYWORD_HITS) {
53
- const meta = metaStmt.get(row.path) || {};
54
- results.push({ path: row.path, hits, totalRank, baseDir, meta });
49
+ const fusedResults = hybridSearchMod.hybridSearch(db, expandedTerms.join(' '), {
50
+ maxResults: MAX_RESULTS,
51
+ project: path.basename(process.cwd()),
52
+ });
53
+
54
+ if (fusedResults.length > 0) {
55
+ for (const r of fusedResults) {
56
+ const filePath = r.path || '';
57
+ let headings = [];
58
+ if (filePath && fs.existsSync(filePath)) {
59
+ const content = fs.readFileSync(filePath, 'utf8');
60
+ headings = extractHeadings(content, 3);
61
+ }
62
+ results.push({
63
+ path: filePath,
64
+ slug: r.slug,
65
+ description: r.description || '',
66
+ category: r.category || '',
67
+ title: r.title || '',
68
+ tags: r.tags || '',
69
+ headings,
70
+ hits: 1,
71
+ rank: r.score,
72
+ baseDir,
73
+ });
74
+ }
75
+ db.close();
76
+ return results;
55
77
  }
78
+ } catch (_err) {
79
+ // Hybrid search failed — fall through to FTS5
80
+ }
81
+ }
82
+
83
+ // FTS5-only fallback: OR-joined single query (replaces O(docs×keywords) nested loop)
84
+ try {
85
+ const sanitized = keywords.map(sanitizeKeyword).filter(Boolean);
86
+ if (sanitized.length === 0) { db.close(); return results; }
87
+
88
+ const ftsQuery = sanitized.join(' OR ');
89
+ const rows = db.prepare(
90
+ `SELECT path, slug, description, category, title, tags, rank
91
+ FROM mindlore_fts WHERE mindlore_fts MATCH ? ORDER BY rank LIMIT ?`
92
+ ).all(ftsQuery, MAX_RESULTS * 2);
93
+
94
+ for (const r of rows) {
95
+ results.push({
96
+ path: r.path || '',
97
+ slug: r.slug,
98
+ description: r.description || '',
99
+ category: r.category || '',
100
+ title: r.title || '',
101
+ tags: r.tags || '',
102
+ headings: [], // populated later in main() after slicing
103
+ hits: sanitized.length,
104
+ rank: r.rank,
105
+ baseDir,
106
+ });
56
107
  }
57
108
  } catch (_err) {
58
109
  // FTS5 query error — silently skip
@@ -69,7 +120,7 @@ function searchDb(dbPath, keywords, Database) {
69
120
  */
70
121
  function searchEpisodesFts(db, keywords) {
71
122
  try {
72
- const ftsQuery = keywords.map(kw => '"' + kw.replace(/["*(){}[\]^~:]/g, '') + '"').filter(q => q !== '""').join(' OR ');
123
+ const ftsQuery = keywords.map(sanitizeKeyword).filter(Boolean).join(' OR ');
73
124
  const rows = db.prepare(
74
125
  "SELECT title, category, slug, tags FROM mindlore_fts WHERE type = 'episode' AND mindlore_fts MATCH ? LIMIT 2"
75
126
  ).all(ftsQuery);
@@ -119,17 +170,32 @@ function main() {
119
170
  const relevant = unique.slice(0, MAX_RESULTS);
120
171
  if (relevant.length === 0) return;
121
172
 
173
+ // Populate headings only for final results (avoid reading extra files)
174
+ for (const r of relevant) {
175
+ if (r.path && r.headings.length === 0 && fs.existsSync(r.path)) {
176
+ try {
177
+ const content = fs.readFileSync(r.path, 'utf8');
178
+ r.headings = extractHeadings(content, 3);
179
+ } catch (_err) { /* skip */ }
180
+ }
181
+ }
182
+
183
+ // Token budget from config
184
+ const config = readConfig(path.dirname(dbPaths[0]));
185
+ const budget = (config && config.tokenBudget) || {};
186
+ // Defaults match DEFAULT_TOKEN_BUDGET in scripts/lib/constants.ts
187
+ const perResultChars = ((budget.perResult || 500) * 4); // ~4 chars/token
188
+ const totalChars = ((budget.searchResults || 1500) * 4);
189
+
122
190
  // Build rich inject output
123
191
  const output = [];
192
+ let totalUsed = 0;
124
193
  for (const r of relevant) {
194
+ if (totalUsed >= totalChars) break;
125
195
  const meta = r.meta || {};
126
196
  const relativePath = path.relative(r.baseDir, r.path).replace(/\\/g, '/');
127
197
 
128
- let headings = [];
129
- if (fs.existsSync(r.path)) {
130
- const content = fs.readFileSync(r.path, 'utf8');
131
- headings = extractHeadings(content, 5);
132
- }
198
+ const headings = r.headings || [];
133
199
 
134
200
  const category = meta.category || path.dirname(relativePath).split('/')[0];
135
201
  const title = meta.title || meta.slug || path.basename(r.path, '.md');
@@ -137,9 +203,10 @@ function main() {
137
203
 
138
204
  const headingStr = headings.length > 0 ? `\nBasliklar: ${headings.join(', ')}` : '';
139
205
  const tagsStr = meta.tags ? `\nTags: ${meta.tags}` : '';
140
- output.push(
141
- `[Mindlore: ${category}/${title}] ${description}\nDosya: ${relativePath}${tagsStr}${headingStr}`
142
- );
206
+ const entry = `[Mindlore: ${category}/${title}] ${description}\nDosya: ${relativePath}${tagsStr}${headingStr}`;
207
+ const truncated = entry.slice(0, perResultChars);
208
+ totalUsed += truncated.length;
209
+ output.push(truncated);
143
210
  }
144
211
 
145
212
  // v0.4.0: Search episode mirrors in FTS5 (reuses searchDb's DB path, no extra open)
@@ -162,4 +229,4 @@ function main() {
162
229
  }
163
230
  }
164
231
 
165
- main();
232
+ try { main(); } catch (err) { hookLog('search', 'error', err?.message ?? String(err)); }
@@ -13,22 +13,48 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const os = require('os');
15
15
  const { execSync, spawn } = require('child_process');
16
- const { findMindloreDir, globalDir, getProjectName, openDatabase, ensureEpisodesTable, hasEpisodesTable, insertBareEpisode, insertFtsRow } = require('./lib/mindlore-common.cjs');
16
+ const { findMindloreDir, globalDir, getProjectName, openDatabase, ensureEpisodesTable, hasEpisodesTable, insertBareEpisode, insertFtsRow, hookLog, SHARED_EXPORT_DIRS, resolveWin32Bin } = require('./lib/mindlore-common.cjs');
17
+
18
+ const EXPORT_DIRS = SHARED_EXPORT_DIRS;
17
19
 
18
20
  // --worker mode: heavy ops run in detached child process (survives parent exit)
19
21
  if (process.argv.includes('--worker')) {
22
+ hookLog('session-end', 'info', 'worker started, pid=' + process.pid);
20
23
  const dataPath = process.argv[process.argv.indexOf('--worker') + 1];
24
+ let payload;
21
25
  try {
22
26
  const raw = fs.readFileSync(dataPath, 'utf8');
23
- fs.unlinkSync(dataPath); // cleanup temp file before any processing
24
- const { baseDir, project, commits, changedFiles, reads } = JSON.parse(raw);
25
- writeBareEpisode(baseDir, project, commits, changedFiles, reads);
26
- syncObsidian(baseDir);
27
- syncGlobalRepo();
27
+ fs.unlinkSync(dataPath);
28
+ payload = JSON.parse(raw);
28
29
  } catch (_err) {
29
- // Graceful fail worker errors are silent
30
+ hookLog('session-end', 'error', 'payload read failed: ' + (_err?.message ?? _err));
31
+ process.exit(0);
30
32
  }
31
- process.exit(0);
33
+ const { baseDir, project, commits, changedFiles, reads } = payload;
34
+
35
+ async function safeRunAsync(fn, label) {
36
+ try {
37
+ await fn();
38
+ hookLog('session-end', 'info', label + ' OK');
39
+ } catch (e) {
40
+ hookLog('session-end', 'error', label + ' FAIL: ' + e?.message);
41
+ }
42
+ }
43
+
44
+ (async () => {
45
+ // Episode writes share DB — run sequentially first
46
+ await safeRunAsync(() => writeBareEpisode(baseDir, project, commits, changedFiles, reads), 'episode');
47
+ await safeRunAsync(() => writeEpisodeFile(baseDir, project, commits, changedFiles, reads), 'episode-file');
48
+
49
+ // Obsidian + git-sync are independent — run in parallel
50
+ await Promise.allSettled([
51
+ safeRunAsync(() => syncObsidian(baseDir), 'obsidian'),
52
+ safeRunAsync(() => syncGlobalRepo(), 'git-sync'),
53
+ ]);
54
+
55
+ hookLog('session-end', 'info', 'worker done');
56
+ process.exit(0);
57
+ })();
32
58
  }
33
59
 
34
60
  function formatDate(date) {
@@ -47,9 +73,10 @@ function formatDate(date) {
47
73
  function getRecentGitInfo() {
48
74
  try {
49
75
  // --name-only includes file names after each commit entry
50
- const raw = execSync('git log --oneline -5 --name-only 2>/dev/null', {
76
+ const raw = execSync('git log --oneline -5 --name-only', {
51
77
  encoding: 'utf8',
52
78
  timeout: 5000,
79
+ stdio: ['pipe', 'pipe', 'pipe'],
53
80
  }).trim();
54
81
  if (!raw) return { commits: [], changedFiles: [] };
55
82
 
@@ -164,7 +191,11 @@ function main() {
164
191
  const workerData = JSON.stringify({ baseDir, project, commits, changedFiles, reads });
165
192
  const tmpFile = path.join(os.tmpdir(), `mindlore-worker-${Date.now()}.json`);
166
193
  fs.writeFileSync(tmpFile, workerData, 'utf8');
167
- const child = spawn(process.execPath, [__filename, '--worker', tmpFile], {
194
+ // Use system node instead of process.execPath CC's embedded Node
195
+ // may not work as a standalone binary for detached worker processes.
196
+ // Resolve full path to avoid shell:true deprecation warning on Windows.
197
+ const nodeBin = resolveWin32Bin('node');
198
+ const child = spawn(nodeBin, [__filename, '--worker', tmpFile], {
168
199
  detached: true,
169
200
  stdio: 'ignore',
170
201
  cwd: process.cwd(),
@@ -173,6 +204,7 @@ function main() {
173
204
  } catch (_err) {
174
205
  // Fallback: run inline if spawn fails
175
206
  writeBareEpisode(baseDir, project, commits, changedFiles, reads);
207
+ writeEpisodeFile(baseDir, project, commits, changedFiles, reads);
176
208
  syncObsidian(baseDir);
177
209
  syncGlobalRepo();
178
210
  }
@@ -247,11 +279,60 @@ function writeBareEpisode(baseDir, project, commits, changedFiles, reads) {
247
279
 
248
280
  db.close();
249
281
  } catch (err) {
250
- process.stderr.write(`[mindlore] episode write failed: ${err?.message ?? err}\n`);
282
+ hookLog('session-end', 'error', `episode write failed: ${err?.message ?? err}`);
251
283
  }
252
284
  }
253
285
 
254
- const EXPORT_DIRS = ['analyses', 'decisions', 'diary', 'raw', 'sources', 'domains', 'connections', 'insights', 'learnings'];
286
+ /**
287
+ * Write episode as .md file to diary/{project}/ for human-readable browsing.
288
+ * Complements the DB episode — same content, different medium.
289
+ */
290
+ function writeEpisodeFile(baseDir, project, commits, changedFiles, reads) {
291
+ const projDir = path.join(baseDir, 'diary', project || 'unknown');
292
+ if (!fs.existsSync(projDir)) fs.mkdirSync(projDir, { recursive: true });
293
+
294
+ const now = new Date();
295
+ const ts = formatDate(now);
296
+ const filePath = path.join(projDir, `episode-${ts}.md`);
297
+ if (fs.existsSync(filePath)) return; // idempotent
298
+
299
+ const lines = [
300
+ '---',
301
+ `slug: episode-${ts}`,
302
+ 'type: episode',
303
+ `date: ${now.toISOString().slice(0, 10)}`,
304
+ `project: ${project || 'unknown'}`,
305
+ '---',
306
+ '',
307
+ `# Episode — ${ts}`,
308
+ '',
309
+ ];
310
+
311
+ if (commits.length > 0) {
312
+ lines.push('## Commits');
313
+ for (const c of commits) lines.push(`- ${c}`);
314
+ lines.push('');
315
+ }
316
+
317
+ if (changedFiles.length > 0) {
318
+ lines.push('## Changed Files');
319
+ for (const f of changedFiles) lines.push(`- ${f}`);
320
+ lines.push('');
321
+ }
322
+
323
+ if (reads) {
324
+ lines.push('## Read Stats');
325
+ lines.push(`- ${reads.count} files read, ${reads.repeats} repeated`);
326
+ lines.push('');
327
+ }
328
+
329
+ if (commits.length === 0 && changedFiles.length === 0) {
330
+ lines.push('_Read-only session — no commits or file changes._');
331
+ lines.push('');
332
+ }
333
+
334
+ fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
335
+ }
255
336
 
256
337
  /**
257
338
  * Load obsidian-helpers from compiled dist (single source of truth for wikilink conversion).
@@ -316,27 +397,40 @@ function syncObsidian(baseDir) {
316
397
  if (!fs.existsSync(srcDir)) continue;
317
398
 
318
399
  const destDir = path.join(destBase, dir);
319
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
400
+ fs.mkdirSync(destDir, { recursive: true });
320
401
 
321
402
  for (const file of fs.readdirSync(srcDir).filter(f => f.endsWith('.md') && !f.startsWith('_'))) {
322
403
  if (exportMdFile(path.join(srcDir, file), path.join(destDir, file), convertFn)) exported++;
323
404
  }
405
+
406
+ // One-level subdirectories (e.g. diary/mindlore/)
407
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
408
+ if (!entry.isDirectory() || entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
409
+ const subSrc = path.join(srcDir, entry.name);
410
+ const subDest = path.join(destDir, entry.name);
411
+ fs.mkdirSync(subDest, { recursive: true });
412
+ for (const file of fs.readdirSync(subSrc).filter(f => f.endsWith('.md') && !f.startsWith('_'))) {
413
+ if (exportMdFile(path.join(subSrc, file), path.join(subDest, file), convertFn)) exported++;
414
+ }
415
+ }
324
416
  }
325
417
 
326
418
  for (const rootFile of ['INDEX.md', 'log.md']) {
327
419
  const srcPath = path.join(baseDir, rootFile);
328
420
  if (!fs.existsSync(srcPath)) continue;
329
- if (!fs.existsSync(destBase)) fs.mkdirSync(destBase, { recursive: true });
421
+ fs.mkdirSync(destBase, { recursive: true });
330
422
  if (exportMdFile(srcPath, path.join(destBase, rootFile), convertFn)) exported++;
331
423
  }
332
424
 
425
+ hookLog('session-end', 'info', `obsidian exported=${exported}, dirs=${EXPORT_DIRS.length}, vault=${vaultPath}`);
333
426
  if (exported > 0) {
334
427
  config.obsidian.lastExport = new Date().toISOString();
335
428
  config.obsidian.lastExportCount = exported;
336
429
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
337
430
  }
338
431
  } catch (err) {
339
- process.stderr.write(`[mindlore] obsidian sync failed: ${err?.message ?? err}\n`);
432
+ hookLog('session-end', 'error', `obsidian internal: ${err?.message ?? err}`);
433
+ throw err; // re-throw so safeRun logs FAIL
340
434
  }
341
435
  }
342
436
 
@@ -345,38 +439,34 @@ function syncObsidian(baseDir) {
345
439
  * Only runs for the global scope — project .mindlore/ is in the project's own git.
346
440
  * Push failure is graceful (offline support).
347
441
  */
442
+ function resolveGitBin() {
443
+ return resolveWin32Bin('git');
444
+ }
445
+
348
446
  function syncGlobalRepo() {
349
447
  const gDir = globalDir();
350
448
  const gitDir = path.join(gDir, '.git');
351
449
  if (!fs.existsSync(gitDir)) return;
352
450
 
353
- try {
354
- // Check for changes
355
- const status = execSync('git status --porcelain', {
356
- cwd: gDir,
357
- encoding: 'utf8',
358
- timeout: 5000,
359
- }).trim();
451
+ const git = resolveGitBin();
452
+ const execOpts = (timeout) => ({ cwd: gDir, encoding: 'utf8', timeout, stdio: 'pipe' });
360
453
 
361
- if (!status) return; // nothing to commit
454
+ // Check for changes
455
+ const status = execSync(`"${git}" status --porcelain`, execOpts(5000)).trim();
456
+ if (!status) return; // nothing to commit
362
457
 
363
- execSync('git add *.md mindlore.db config.json diary/ sources/ domains/ analyses/ decisions/ raw/ connections/ insights/ learnings/', { cwd: gDir, timeout: 5000, stdio: 'pipe' });
364
- const now = new Date().toISOString().slice(0, 19);
365
- execSync(`git commit -m "mindlore auto-sync ${now}"`, {
366
- cwd: gDir,
367
- timeout: 10000,
368
- stdio: 'pipe',
369
- });
458
+ execSync(`"${git}" add *.md mindlore.db diary/ sources/ domains/ analyses/ decisions/ raw/ connections/ insights/ learnings/`, execOpts(10000));
459
+ const now = new Date().toISOString().slice(0, 19);
460
+ execSync(`"${git}" commit -m "mindlore auto-sync ${now}"`, execOpts(15000));
370
461
 
371
- // Push — graceful fail if no remote or offline
372
- try {
373
- execSync('git push', { cwd: gDir, timeout: 15000, stdio: 'pipe' });
374
- } catch (_pushErr) {
375
- // Offline or no remote silently continue
376
- }
377
- } catch (_err) {
378
- // Git not available or commit failed — silently continue
462
+ // Push — graceful fail if no remote or offline
463
+ try {
464
+ execSync(`"${git}" push`, execOpts(15000));
465
+ } catch (_pushErr) {
466
+ hookLog('session-end', 'warn', 'git push failed (offline?): ' + (_pushErr?.message ?? '').slice(0, 100));
379
467
  }
380
468
  }
381
469
 
382
- main();
470
+ if (!process.argv.includes('--worker')) {
471
+ main();
472
+ }
@@ -10,7 +10,7 @@
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
- const { findMindloreDir, readConfig, openDatabase, hasEpisodesTable, queryRecentEpisodes, querySupersededChains, formatSupersededChains, queryMultiSessionEpisodes, formatMultiSessionEpisodes, getAllMdFiles } = require('./lib/mindlore-common.cjs');
13
+ const { findMindloreDir, readConfig, openDatabase, hasEpisodesTable, queryRecentEpisodes, querySupersededChains, formatSupersededChains, queryMultiSessionEpisodes, formatMultiSessionEpisodes, getAllMdFiles, getRecentHookErrors, hookLog } = require('./lib/mindlore-common.cjs');
14
14
 
15
15
  function main() {
16
16
  const baseDir = findMindloreDir();
@@ -117,8 +117,29 @@ function main() {
117
117
  }
118
118
  } catch (_healthErr) { /* skip */ }
119
119
 
120
- if (output.length > 0) {
121
- process.stdout.write(output.join('\n\n') + '\n');
120
+ // Check for recent hook errors — inject warnings into CC context
121
+ try {
122
+ const errors = getRecentHookErrors();
123
+ if (errors.length > 0) {
124
+ const lines = errors.map(e => `- [${e.ts.slice(0, 19)}] **${e.hook}** (${e.level}): ${e.msg}`);
125
+ output.push(`[Mindlore Hook Alerts]\n${lines.join('\n')}`);
126
+ }
127
+ } catch (_hookLogErr) { /* skip */ }
128
+
129
+ hookLog('session-focus', 'info', 'session started');
130
+
131
+ // Token budget for session inject
132
+ // Defaults match DEFAULT_TOKEN_BUDGET in scripts/lib/constants.ts
133
+ const budgetConfig = config?.tokenBudget ?? {};
134
+ const maxInjectChars = (budgetConfig.sessionInject || 2000) * 4;
135
+
136
+ let joined = output.join('\n\n');
137
+ if (joined.length > maxInjectChars) {
138
+ joined = joined.slice(0, maxInjectChars) + '\n[...truncated by token budget]';
139
+ }
140
+
141
+ if (joined.length > 0) {
142
+ process.stdout.write(joined + '\n');
122
143
  }
123
144
  }
124
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindlore",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "AI-native knowledge system for Claude Code",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -43,7 +43,7 @@
43
43
  "node": ">=20.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "better-sqlite3": "^11.0.0",
46
+ "better-sqlite3": "^12.9.0",
47
47
  "zod": "^4.3.6"
48
48
  },
49
49
  "devDependencies": {
@@ -52,9 +52,11 @@
52
52
  "@types/node": "^25.6.0",
53
53
  "@typescript-eslint/eslint-plugin": "^8.58.1",
54
54
  "@typescript-eslint/parser": "^8.58.1",
55
- "eslint": "^9.0.0",
56
- "globals": "^15.0.0",
55
+ "@xenova/transformers": "^2.17.2",
56
+ "eslint": "^10.2.0",
57
+ "globals": "^17.5.0",
57
58
  "jest": "^29.7.0",
59
+ "sqlite-vec": "^0.1.9",
58
60
  "ts-jest": "^29.4.9",
59
61
  "typescript": "^6.0.2"
60
62
  },
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.4.3",
4
+ "version": "0.5.1",
5
5
  "skills": [
6
6
  {
7
7
  "name": "mindlore-ingest",
@@ -129,8 +129,14 @@ N source, N analysis, N total
129
129
 
130
130
  ## Post-Ingest Quality Gate
131
131
 
132
- After every ingest, verify all 6 checkpoints before reporting success:
132
+ After every ingest, verify all 7 checkpoints before reporting success:
133
133
 
134
+ 0. **Duplicate check** — Ingest öncesi mevcut DB'de benzer içerik ara:
135
+ ```bash
136
+ node dist/scripts/lib/similarity.js "<title or first 100 chars>"
137
+ ```
138
+ Eğer score > 0.7 olan sonuç varsa KULLANICIYA SOR: "Bu içerik '${slug}' ile benzer görünüyor. Yine de eklensin mi?"
139
+ Kullanıcı onaylarsa devam et, yoksa atla.
134
140
  1. **raw/ file exists** — immutable capture written with frontmatter (slug, type, source_url)
135
141
  2. **sources/ summary exists** — processed summary with full frontmatter (slug, type, title, tags, quality, description)
136
142
  3. **INDEX.md updated** — stats line incremented, Recent section has new entry
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.3",
2
+ "version": "0.5.1",
3
3
  "models": {
4
4
  "ingest": "haiku",
5
5
  "evolve": "sonnet",
@@ -12,5 +12,24 @@
12
12
  "session_focus": {
13
13
  "max_episodes": 3,
14
14
  "multi_session_days": 3
15
+ },
16
+ "synonyms": {
17
+ "auth": ["authentication", "login", "kimlik doğrulama"],
18
+ "güvenlik": ["security", "hardening", "sertleştirme"],
19
+ "db": ["database", "veritabanı", "sqlite"],
20
+ "api": ["endpoint", "rest", "graphql"],
21
+ "ui": ["frontend", "arayüz", "interface"],
22
+ "test": ["testing", "jest", "unit test"]
23
+ },
24
+ "hybrid": {
25
+ "enabled": true,
26
+ "ftsWeight": 0.4,
27
+ "vecWeight": 0.6,
28
+ "k": 60
29
+ },
30
+ "tokenBudget": {
31
+ "sessionInject": 2000,
32
+ "searchResults": 1500,
33
+ "perResult": 500
15
34
  }
16
35
  }