mindlore 0.7.6 → 0.7.7

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 (148) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/README.md +3 -1
  4. package/dist/scripts/bundle-hooks.js +5 -1
  5. package/dist/scripts/bundle-hooks.js.map +1 -1
  6. package/dist/scripts/init.js +49 -10
  7. package/dist/scripts/init.js.map +1 -1
  8. package/dist/scripts/lib/constants.d.ts +8 -0
  9. package/dist/scripts/lib/constants.d.ts.map +1 -1
  10. package/dist/scripts/lib/constants.js +9 -1
  11. package/dist/scripts/lib/constants.js.map +1 -1
  12. package/dist/scripts/lib/db-helpers.d.ts +6 -0
  13. package/dist/scripts/lib/db-helpers.d.ts.map +1 -1
  14. package/dist/scripts/lib/db-helpers.js +6 -0
  15. package/dist/scripts/lib/db-helpers.js.map +1 -1
  16. package/dist/scripts/lib/read-guard-core.d.ts +21 -0
  17. package/dist/scripts/lib/read-guard-core.d.ts.map +1 -0
  18. package/dist/scripts/lib/read-guard-core.js +40 -0
  19. package/dist/scripts/lib/read-guard-core.js.map +1 -0
  20. package/dist/scripts/lib/search-cache.d.ts +1 -7
  21. package/dist/scripts/lib/search-cache.d.ts.map +1 -1
  22. package/dist/scripts/lib/search-cache.js +3 -25
  23. package/dist/scripts/lib/search-cache.js.map +1 -1
  24. package/dist/scripts/lib/search-throttle.d.ts +10 -0
  25. package/dist/scripts/lib/search-throttle.d.ts.map +1 -0
  26. package/dist/scripts/lib/search-throttle.js +32 -0
  27. package/dist/scripts/lib/search-throttle.js.map +1 -0
  28. package/dist/scripts/lib/settings-cleanup.d.ts.map +1 -1
  29. package/dist/scripts/lib/settings-cleanup.js +2 -1
  30. package/dist/scripts/lib/settings-cleanup.js.map +1 -1
  31. package/dist/scripts/lib/setup-wizard.d.ts +13 -0
  32. package/dist/scripts/lib/setup-wizard.d.ts.map +1 -0
  33. package/dist/scripts/lib/setup-wizard.js +51 -0
  34. package/dist/scripts/lib/setup-wizard.js.map +1 -0
  35. package/dist/scripts/lib/tool-adapters/decide-adapter.d.ts.map +1 -1
  36. package/dist/scripts/lib/tool-adapters/decide-adapter.js +3 -3
  37. package/dist/scripts/lib/tool-adapters/decide-adapter.js.map +1 -1
  38. package/dist/scripts/lib/tool-adapters/get-adapter.d.ts.map +1 -1
  39. package/dist/scripts/lib/tool-adapters/get-adapter.js +10 -2
  40. package/dist/scripts/lib/tool-adapters/get-adapter.js.map +1 -1
  41. package/dist/scripts/lib/tool-adapters/ingest-adapter.d.ts.map +1 -1
  42. package/dist/scripts/lib/tool-adapters/ingest-adapter.js +3 -3
  43. package/dist/scripts/lib/tool-adapters/ingest-adapter.js.map +1 -1
  44. package/dist/scripts/lib/transcript-token-estimator.d.ts +8 -0
  45. package/dist/scripts/lib/transcript-token-estimator.d.ts.map +1 -0
  46. package/dist/scripts/lib/transcript-token-estimator.js +164 -0
  47. package/dist/scripts/lib/transcript-token-estimator.js.map +1 -0
  48. package/dist/scripts/maintain-cleanup.d.ts.map +1 -1
  49. package/dist/scripts/maintain-cleanup.js +2 -1
  50. package/dist/scripts/maintain-cleanup.js.map +1 -1
  51. package/dist/scripts/mcp-server.js +3 -3
  52. package/dist/scripts/mcp-server.js.map +1 -1
  53. package/dist/scripts/mindlore-backup.d.ts.map +1 -1
  54. package/dist/scripts/mindlore-backup.js +3 -2
  55. package/dist/scripts/mindlore-backup.js.map +1 -1
  56. package/dist/scripts/mindlore-clean-cache.d.ts +4 -1
  57. package/dist/scripts/mindlore-clean-cache.d.ts.map +1 -1
  58. package/dist/scripts/mindlore-clean-cache.js +47 -20
  59. package/dist/scripts/mindlore-clean-cache.js.map +1 -1
  60. package/dist/scripts/mindlore-doctor.d.ts +4 -0
  61. package/dist/scripts/mindlore-doctor.d.ts.map +1 -1
  62. package/dist/scripts/mindlore-doctor.js +9 -1
  63. package/dist/scripts/mindlore-doctor.js.map +1 -1
  64. package/dist/scripts/mindlore-health-check.d.ts +7 -0
  65. package/dist/scripts/mindlore-health-check.d.ts.map +1 -1
  66. package/dist/scripts/mindlore-health-check.js +57 -4
  67. package/dist/scripts/mindlore-health-check.js.map +1 -1
  68. package/dist/scripts/mindlore-learnings.d.ts +6 -0
  69. package/dist/scripts/mindlore-learnings.d.ts.map +1 -0
  70. package/dist/scripts/mindlore-learnings.js +106 -0
  71. package/dist/scripts/mindlore-learnings.js.map +1 -0
  72. package/dist/scripts/mindlore-obsidian.js +6 -5
  73. package/dist/scripts/mindlore-obsidian.js.map +1 -1
  74. package/dist/scripts/quality-populate.js +2 -1
  75. package/dist/scripts/quality-populate.js.map +1 -1
  76. package/dist/scripts/uninstall.js +2 -1
  77. package/dist/scripts/uninstall.js.map +1 -1
  78. package/dist/tests/clean-cache-eperm.test.d.ts +2 -0
  79. package/dist/tests/clean-cache-eperm.test.d.ts.map +1 -0
  80. package/dist/tests/clean-cache-eperm.test.js +74 -0
  81. package/dist/tests/clean-cache-eperm.test.js.map +1 -0
  82. package/dist/tests/doctor.test.js +18 -0
  83. package/dist/tests/doctor.test.js.map +1 -1
  84. package/dist/tests/health-check.test.d.ts +2 -0
  85. package/dist/tests/health-check.test.d.ts.map +1 -0
  86. package/dist/tests/health-check.test.js +61 -0
  87. package/dist/tests/health-check.test.js.map +1 -0
  88. package/dist/tests/helpers/db.d.ts +7 -2
  89. package/dist/tests/helpers/db.d.ts.map +1 -1
  90. package/dist/tests/helpers/db.js +16 -14
  91. package/dist/tests/helpers/db.js.map +1 -1
  92. package/dist/tests/mindlore-ingest-extraction.test.d.ts +2 -0
  93. package/dist/tests/mindlore-ingest-extraction.test.d.ts.map +1 -0
  94. package/dist/tests/mindlore-ingest-extraction.test.js +48 -0
  95. package/dist/tests/mindlore-ingest-extraction.test.js.map +1 -0
  96. package/dist/tests/mindlore-learnings.test.d.ts +2 -0
  97. package/dist/tests/mindlore-learnings.test.d.ts.map +1 -0
  98. package/dist/tests/mindlore-learnings.test.js +63 -0
  99. package/dist/tests/mindlore-learnings.test.js.map +1 -0
  100. package/dist/tests/read-guard-perf.test.d.ts +2 -0
  101. package/dist/tests/read-guard-perf.test.d.ts.map +1 -0
  102. package/dist/tests/read-guard-perf.test.js +31 -0
  103. package/dist/tests/read-guard-perf.test.js.map +1 -0
  104. package/dist/tests/reflect-trigger.test.js +8 -1
  105. package/dist/tests/reflect-trigger.test.js.map +1 -1
  106. package/dist/tests/search-cache.test.js +0 -35
  107. package/dist/tests/search-cache.test.js.map +1 -1
  108. package/dist/tests/search-throttle.test.d.ts +2 -0
  109. package/dist/tests/search-throttle.test.d.ts.map +1 -0
  110. package/dist/tests/search-throttle.test.js +53 -0
  111. package/dist/tests/search-throttle.test.js.map +1 -0
  112. package/dist/tests/session-focus-reflect-nudge.test.js +3 -3
  113. package/dist/tests/session-focus-reflect-nudge.test.js.map +1 -1
  114. package/dist/tests/setup-wizard.test.d.ts +2 -0
  115. package/dist/tests/setup-wizard.test.d.ts.map +1 -0
  116. package/dist/tests/setup-wizard.test.js +35 -0
  117. package/dist/tests/setup-wizard.test.js.map +1 -0
  118. package/dist/tests/transcript-token-estimator.test.d.ts +2 -0
  119. package/dist/tests/transcript-token-estimator.test.d.ts.map +1 -0
  120. package/dist/tests/transcript-token-estimator.test.js +122 -0
  121. package/dist/tests/transcript-token-estimator.test.js.map +1 -0
  122. package/hooks/cc-memory-bulk-sync.cjs +9 -1
  123. package/hooks/cc-session-sync.cjs +9 -1
  124. package/hooks/lib/learnings-loader.cjs +21 -11
  125. package/hooks/lib/reflect-trigger.cjs +6 -3
  126. package/hooks/mindlore-cwd-changed.cjs +4 -5
  127. package/hooks/mindlore-dont-repeat.cjs +2 -1
  128. package/hooks/mindlore-post-read.cjs +2 -1
  129. package/hooks/mindlore-pre-compact.cjs +3 -2
  130. package/hooks/mindlore-read-guard.cjs +62 -35
  131. package/hooks/mindlore-search.cjs +231 -31
  132. package/hooks/mindlore-session-end.cjs +6 -8
  133. package/hooks/mindlore-session-focus.cjs +472 -23
  134. package/hooks/src/mindlore-cwd-changed.cjs +4 -5
  135. package/hooks/src/mindlore-dont-repeat.cjs +2 -1
  136. package/hooks/src/mindlore-post-read.cjs +2 -1
  137. package/hooks/src/mindlore-pre-compact.cjs +3 -2
  138. package/hooks/src/mindlore-read-guard.cjs +15 -55
  139. package/hooks/src/mindlore-search.cjs +39 -10
  140. package/hooks/src/mindlore-session-end.cjs +6 -8
  141. package/hooks/src/mindlore-session-focus.cjs +29 -21
  142. package/mcp-server.cjs +41 -25
  143. package/package.json +1 -1
  144. package/plugin.json +6 -1
  145. package/skills/mindlore-ingest/SKILL.md +21 -0
  146. package/skills/mindlore-learnings/SKILL.md +30 -0
  147. package/start.cjs +1 -1
  148. package/templates/config.json +1 -1
@@ -1,20 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- /**
5
- * mindlore-read-guard — PreToolUse hook (if: "Read")
6
- *
7
- * Repeated-read detection: detects files read multiple times
8
- * in the same session and emits a soft warning.
9
- * Does NOT block (exit 0) — advisory only.
10
- *
11
- * Storage: .mindlore/diary/_session-reads.json
12
- * Cleanup: session-end hook writes stats to delta then deletes the file.
13
- */
14
-
15
4
  const fs = require('fs');
16
5
  const path = require('path');
17
6
  const { findMindloreDir, readHookStdin, getProjectName, hookLog, extractSkeleton, withTelemetrySync } = require('./lib/mindlore-common.cjs');
7
+ const { safeMkdir, safeWriteFile } = require('./lib/secure-io.cjs');
8
+ const { runReadGuard } = require('../../dist/scripts/lib/read-guard-core.js');
18
9
 
19
10
  function main() {
20
11
  const baseDir = findMindloreDir();
@@ -23,64 +14,33 @@ function main() {
23
14
  const filePath = readHookStdin(['file_path', 'path']);
24
15
  if (!filePath) return;
25
16
 
26
- // Only track CWD-relative files, skip .mindlore/ internals
27
17
  const cwd = process.cwd();
28
18
  const resolved = path.resolve(filePath);
29
19
  if (!resolved.startsWith(cwd)) return;
30
20
  if (resolved.startsWith(path.resolve(baseDir))) return;
31
21
 
32
- // Load or create session reads tracker
33
22
  const diaryDir = path.join(baseDir, 'diary');
34
- if (!fs.existsSync(diaryDir)) {
35
- fs.mkdirSync(diaryDir, { recursive: true });
36
- }
23
+ safeMkdir(diaryDir);
37
24
 
38
25
  const readsPath = path.join(diaryDir, `_session-reads-${getProjectName()}.json`);
39
26
  let reads = {};
40
- if (fs.existsSync(readsPath)) {
41
- try {
42
- reads = JSON.parse(fs.readFileSync(readsPath, 'utf8'));
43
- } catch (_err) {
44
- reads = {};
45
- }
27
+ try {
28
+ reads = JSON.parse(fs.readFileSync(readsPath, 'utf8'));
29
+ } catch (err) {
30
+ if (err.code !== 'ENOENT') hookLog('read-guard', 'warn', `read error: ${err.message}`);
31
+ reads = {};
46
32
  }
47
33
 
48
- const normalizedPath = path.resolve(filePath);
49
- const existing = reads[normalizedPath];
50
-
51
- // Support both old format (number) and new format (object with tokens)
52
- let count, tokens;
53
- if (typeof existing === 'number') {
54
- count = existing + 1;
55
- tokens = 0;
56
- reads[normalizedPath] = { count, tokens: 0, chars: 0 };
57
- } else if (existing && typeof existing === 'object') {
58
- count = (existing.count || 0) + 1;
59
- tokens = existing.tokens || 0;
60
- existing.count = count;
61
- reads[normalizedPath] = existing;
62
- } else {
63
- count = 1;
64
- tokens = 0;
65
- reads[normalizedPath] = { count, tokens: 0, chars: 0 };
66
- }
67
-
68
- // Write updated reads
69
- fs.writeFileSync(readsPath, JSON.stringify(reads, null, 2), 'utf8');
70
-
71
- const basename = path.basename(filePath);
72
- const tokenInfo = tokens > 0 ? ` (~${tokens} token)` : '';
34
+ const decision = runReadGuard({ filePath: resolved, basename: path.basename(filePath) }, reads);
35
+ reads[resolved] = decision.updatedReadsEntry;
36
+ safeWriteFile(readsPath, JSON.stringify(reads, null, 2));
73
37
 
74
- // Block on 3+ repeated reads (exit 2 = block tool call)
75
- if (count >= 3) {
76
- const totalWaste = tokens > 0 ? ` Toplam israf: ~${tokens * (count - 1)} token.` : '';
77
- process.stderr.write(`[Mindlore BLOCK] ${basename}${tokenInfo} bu session'da ${count}. kez okunuyor.${totalWaste} Edit icin gerekiyorsa once degisikligini yap, sonra tekrar oku. Analiz icin ctx_execute_file kullan.`);
38
+ if (decision.block) {
39
+ process.stderr.write(decision.warning);
78
40
  process.exit(2);
79
41
  }
80
42
 
81
- // Warn on 2nd read (exit 0 = allow but warn)
82
- if (count > 1) {
83
- const totalWaste = tokens > 0 ? ` Toplam tekrar: ~${tokens * (count - 1)} token.` : '';
43
+ if (decision.additionalContext) {
84
44
  let skeletonSection = '';
85
45
  try {
86
46
  const ext = path.extname(filePath).slice(1);
@@ -96,7 +56,7 @@ function main() {
96
56
  process.stdout.write(JSON.stringify({
97
57
  hookSpecificOutput: {
98
58
  hookEventName: 'PreToolUse',
99
- additionalContext: `[Mindlore: ${basename}${tokenInfo} bu session'da ${count}. kez okunuyor.${totalWaste} Bir sonraki okuma engellenecek — Edit gerekiyorsa simdi yap.]${skeletonSection}`
59
+ additionalContext: decision.additionalContext + skeletonSection
100
60
  }
101
61
  }));
102
62
  }
@@ -12,6 +12,7 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const { getAllDbs, openDatabase, extractHeadings, readConfig, hookLog, incrementRecallCount, withTelemetry } = require('./lib/mindlore-common.cjs');
15
+ const { safeMkdir, safeWriteFile } = require('./lib/secure-io.cjs');
15
16
 
16
17
  const MAX_RESULTS = 3;
17
18
  const MIN_QUERY_WORDS = 3;
@@ -30,22 +31,42 @@ try {
30
31
  // search-cache not built yet
31
32
  }
32
33
 
34
+ let TokenEstimatorMod;
35
+ try {
36
+ TokenEstimatorMod = require('../dist/scripts/lib/transcript-token-estimator.js');
37
+ } catch (_err) {
38
+ // estimator not built yet
39
+ }
40
+
33
41
  function parseStdin() {
34
42
  try {
35
43
  const raw = fs.readFileSync(0, 'utf8').trim();
36
- if (!raw) return { userMessage: '', sessionId: 'unknown' };
44
+ if (!raw) return { userMessage: '', sessionId: 'unknown', transcriptPath: undefined };
37
45
  const parsed = JSON.parse(raw);
38
46
  const userMessage = parsed.prompt || parsed.content || parsed.message || parsed.query || raw;
39
47
  const sessionId = parsed.session_id || 'unknown';
40
- return { userMessage, sessionId };
48
+ const transcriptPath = parsed.transcript_path || parsed.transcriptPath;
49
+ return { userMessage, sessionId, transcriptPath };
41
50
  } catch (_err) {
42
- return { userMessage: '', sessionId: 'unknown' };
51
+ return { userMessage: '', sessionId: 'unknown', transcriptPath: undefined };
52
+ }
53
+ }
54
+
55
+ function getBaseMax(transcriptPath) {
56
+ if (TokenEstimatorMod) {
57
+ try {
58
+ return TokenEstimatorMod.adaptiveResultCount(transcriptPath);
59
+ } catch (_err) {
60
+ // fall through to default
61
+ }
43
62
  }
63
+ return MAX_RESULTS;
44
64
  }
45
65
 
46
66
  function main() {
47
- const { userMessage, sessionId } = parseStdin();
67
+ const { userMessage, sessionId, transcriptPath } = parseStdin();
48
68
  if (!userMessage || userMessage.length < MIN_QUERY_WORDS) return;
69
+ const baseMax = getBaseMax(transcriptPath);
49
70
  let searchMs = 0;
50
71
 
51
72
  const dbPaths = getAllDbs();
@@ -61,18 +82,26 @@ function main() {
61
82
  const synonyms = (config && config.synonyms) ? config.synonyms : {};
62
83
 
63
84
  const allResults = [];
85
+ // Track tightest throttle cap across DBs — applied to the final result slice
86
+ // so cache hits (which return previously-stored arrays) still respect throttle
87
+ // state in subsequent calls of the same session.
88
+ let sessionEffectiveMax = baseMax;
64
89
  for (const dbPath of dbPaths) {
65
90
  const db = openDatabase(dbPath);
66
91
  if (!db) continue;
67
92
  try {
68
93
  // Cache + throttle
69
94
  let cache;
70
- let effectiveMax = MAX_RESULTS;
95
+ let effectiveMax = baseMax;
71
96
  if (SearchCacheMod) {
72
97
  cache = new SearchCacheMod.SearchCache(db, { ttlMs: 300000 });
73
98
  const throttle = new SearchCacheMod.SearchThrottle(db);
74
99
  const callCount = throttle.incrementCallCount(sessionId);
75
- effectiveMax = throttle.getMaxResults(callCount);
100
+ // Throttle is baseMax-aware: when callCount <= 10 it returns baseMax,
101
+ // so adaptive expansion (up to 5 in low-context sessions) is honored.
102
+ // Above 10 calls it clamps to 1, above 20 to 0 (skip).
103
+ effectiveMax = throttle.getMaxResults(callCount, baseMax);
104
+ if (effectiveMax < sessionEffectiveMax) sessionEffectiveMax = effectiveMax;
76
105
  if (effectiveMax === 0) {
77
106
  hookLog('search', 'info', `Throttled (call #${callCount})`);
78
107
  db.close();
@@ -81,7 +110,7 @@ function main() {
81
110
  const cached = cache.get(userMessage);
82
111
  if (cached) {
83
112
  const baseDir = path.dirname(dbPath);
84
- for (const r of cached) allResults.push({ ...r, baseDir });
113
+ for (const r of cached.slice(0, effectiveMax)) allResults.push({ ...r, baseDir });
85
114
  db.close();
86
115
  continue;
87
116
  }
@@ -129,7 +158,7 @@ function main() {
129
158
 
130
159
  // Sort by score descending, take top N
131
160
  unique.sort((a, b) => b.score - a.score);
132
- const relevant = unique.slice(0, MAX_RESULTS);
161
+ const relevant = unique.slice(0, sessionEffectiveMax);
133
162
  if (relevant.length === 0) return;
134
163
 
135
164
  // Token budget from config
@@ -172,7 +201,7 @@ function main() {
172
201
  if (outputStr.length > OFFLOAD_THRESHOLD) {
173
202
  const baseDir = path.dirname(dbPaths[0]);
174
203
  const tmpDir = path.join(baseDir, 'tmp');
175
- fs.mkdirSync(tmpDir, { recursive: true });
204
+ safeMkdir(tmpDir);
176
205
 
177
206
  try {
178
207
  const oneHourAgo = Date.now() - 3600000;
@@ -188,7 +217,7 @@ function main() {
188
217
  } catch { /* cleanup is best-effort */ }
189
218
  const fileName = `search-${Date.now()}.md`;
190
219
  const filePath = path.join(tmpDir, fileName);
191
- fs.writeFileSync(filePath, outputStr, 'utf8');
220
+ safeWriteFile(filePath, outputStr);
192
221
 
193
222
  const summary = outputStr.slice(0, 500).replace(/\n/g, ' ').trim();
194
223
  outputStr = `[Mindlore Search: ${outputStr.length} chars offloaded to ${filePath}]\n` +
@@ -13,7 +13,7 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const os = require('os');
15
15
  const { execFileSync, spawn } = require('child_process');
16
- const { safeWriteFile, safeWriteJson } = require('../dist/scripts/lib/secure-io.js');
16
+ const { safeMkdir, safeWriteFile, safeWriteJson } = require('./lib/secure-io.cjs');
17
17
  const { findMindloreDir, globalDir, getProjectName, openDatabase, ensureEpisodesTable, hasEpisodesTable, insertBareEpisode, insertFtsRow, hookLog, SHARED_EXPORT_DIRS, resolveWin32Bin, withTelemetry, getUnpromotedRawFiles, cleanupExpiredInjectLog } = require('./lib/mindlore-common.cjs');
18
18
 
19
19
  const EXPORT_DIRS = SHARED_EXPORT_DIRS;
@@ -151,9 +151,7 @@ function main() {
151
151
  if (!baseDir) return;
152
152
 
153
153
  const diaryDir = path.join(baseDir, 'diary');
154
- if (!fs.existsSync(diaryDir)) {
155
- fs.mkdirSync(diaryDir, { recursive: true });
156
- }
154
+ safeMkdir(diaryDir);
157
155
 
158
156
  const now = new Date();
159
157
  const dateStr = formatDate(now);
@@ -323,7 +321,7 @@ function writeBareEpisode(baseDir, project, commits, changedFiles, reads) {
323
321
  */
324
322
  function writeEpisodeFile(baseDir, project, commits, changedFiles, reads) {
325
323
  const projDir = path.join(baseDir, 'diary', project || 'unknown');
326
- if (!fs.existsSync(projDir)) fs.mkdirSync(projDir, { recursive: true });
324
+ safeMkdir(projDir);
327
325
 
328
326
  const now = process.env.MINDLORE_EPISODE_TS ? new Date(process.env.MINDLORE_EPISODE_TS) : new Date();
329
327
  const ts = formatDate(now);
@@ -406,7 +404,7 @@ function exportMdFile(srcPath, destPath, convertFn) {
406
404
  }
407
405
  let content = fs.readFileSync(srcPath, 'utf8');
408
406
  content = convertFn(content);
409
- fs.writeFileSync(destPath, content, 'utf8');
407
+ safeWriteFile(destPath, content);
410
408
  return true;
411
409
  }
412
410
 
@@ -434,7 +432,7 @@ function syncObsidian(baseDir) {
434
432
 
435
433
  function walkAndExport(srcDir, destDir) {
436
434
  if (!fs.existsSync(srcDir)) return;
437
- fs.mkdirSync(destDir, { recursive: true });
435
+ safeMkdir(destDir);
438
436
  for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
439
437
  if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
440
438
  const srcPath = path.join(srcDir, entry.name);
@@ -454,7 +452,7 @@ function syncObsidian(baseDir) {
454
452
  for (const rootFile of ['INDEX.md', 'log.md']) {
455
453
  const srcPath = path.join(baseDir, rootFile);
456
454
  if (!fs.existsSync(srcPath)) continue;
457
- fs.mkdirSync(destBase, { recursive: true });
455
+ safeMkdir(destBase);
458
456
  if (exportMdFile(srcPath, path.join(destBase, rootFile), convertFn)) exported++;
459
457
  }
460
458
 
@@ -12,7 +12,7 @@ const fs = require('fs');
12
12
  const path = require('path');
13
13
  const { findMindloreDir, readConfig, openDatabase, hasEpisodesTable, querySupersededChains, formatSupersededChains, hookLog, getProjectName, parseFrontmatter, withTelemetry, withTimeoutDb, listSnapshots, isCorruptionError, recoverCorruptDb, getNominationCounts, resolveMindloreHome } = require('./lib/mindlore-common.cjs');
14
14
  const { loadLearningsBlock } = require('./lib/learnings-loader.cjs');
15
- const { shouldNudgeReflect } = require('../lib/reflect-trigger.cjs');
15
+ const { shouldNudgeReflect, buildNudgeMessage } = require('../lib/reflect-trigger.cjs');
16
16
 
17
17
  function truncateSection(content, sectionRegex, keepCount, label) {
18
18
  const match = content.match(sectionRegex);
@@ -138,10 +138,16 @@ function main() {
138
138
  // Inject INDEX.md
139
139
  const tIndex = Date.now();
140
140
  const indexPath = path.join(baseDir, 'INDEX.md');
141
- if (fs.existsSync(indexPath)) {
142
- const content = fs.readFileSync(indexPath, 'utf8').trim();
143
- sourceChars += content.length;
144
- output.push(`[Mindlore INDEX]\n${content}`);
141
+ let indexContent;
142
+ try {
143
+ indexContent = fs.readFileSync(indexPath, 'utf8').trim();
144
+ } catch (err) {
145
+ if (err.code !== 'ENOENT') throw err;
146
+ indexContent = null;
147
+ }
148
+ if (indexContent !== null) {
149
+ sourceChars += indexContent.length;
150
+ output.push(`[Mindlore INDEX]\n${indexContent}`);
145
151
  }
146
152
  timings.index_read = Date.now() - tIndex;
147
153
 
@@ -181,15 +187,13 @@ function main() {
181
187
  // Both are flat strings written by init — no JSON parse needed on session start
182
188
  const versionPath = path.join(baseDir, '.version');
183
189
  const pkgVersionPath = path.join(baseDir, '.pkg-version');
184
- try {
185
- if (fs.existsSync(versionPath) && fs.existsSync(pkgVersionPath)) {
186
- const installed = fs.readFileSync(versionPath, 'utf8').trim();
187
- const pkgVersion = fs.readFileSync(pkgVersionPath, 'utf8').trim();
188
- if (pkgVersion && pkgVersion !== installed) {
189
- output.push(`[Mindlore: Guncelleme mevcut (${installed} → ${pkgVersion}). \`npx mindlore init\` calistirin.]`);
190
- }
191
- }
192
- } catch (_err) { /* skip */ }
190
+ let installed;
191
+ try { installed = fs.readFileSync(versionPath, 'utf8').trim(); } catch (err) { if (err.code !== 'ENOENT') throw err; }
192
+ let pkgVersion;
193
+ try { pkgVersion = fs.readFileSync(pkgVersionPath, 'utf8').trim(); } catch (err) { if (err.code !== 'ENOENT') throw err; }
194
+ if (installed !== undefined && pkgVersion !== undefined && pkgVersion !== installed) {
195
+ output.push(`[Mindlore: Guncelleme mevcut (${installed} → ${pkgVersion}). \`npx mindlore init\` calistirin.]`);
196
+ }
193
197
  timings.version_check = Date.now() - tVersion;
194
198
 
195
199
  // v0.5.4: Consolidated session payload (replaces scattered episodes/activity/alerts injection)
@@ -220,14 +224,18 @@ function main() {
220
224
 
221
225
  // Auto-reflect nudge (7-day threshold + 24h cooldown)
222
226
  try {
223
- const reflectRow = db.prepare(
224
- "SELECT value FROM skill_memory WHERE skill_name = 'mindlore-reflect' AND key = 'last_reflect_date'"
225
- ).get();
226
- const nudgeRow = db.prepare(
227
- "SELECT value FROM skill_memory WHERE skill_name = 'mindlore-reflect' AND key = 'last_nudge_date'"
228
- ).get();
227
+ const rows = db.prepare(
228
+ "SELECT key, value FROM skill_memory WHERE skill_name = 'mindlore-reflect' AND key IN ('last_reflect_date', 'last_nudge_date')"
229
+ ).all();
230
+ const byKey = Object.fromEntries(rows.map(r => [r.key, r.value]));
231
+ const reflectRow = byKey['last_reflect_date'] ? { value: byKey['last_reflect_date'] } : undefined;
232
+ const nudgeRow = byKey['last_nudge_date'] ? { value: byKey['last_nudge_date'] } : undefined;
229
233
  if (shouldNudgeReflect(reflectRow?.value ?? null, nudgeRow?.value ?? null, new Date())) {
230
- output.push('[Mindlore] 7+ gün reflect yapılmadı `/mindlore-reflect` çalıştır');
234
+ const daysSince = reflectRow?.value ? Math.floor((Date.now() - new Date(reflectRow.value).getTime()) / 86400000) : 999;
235
+ const episodeCount = hasEpisodesTable(db) ? db.prepare("SELECT count(*) AS c FROM episodes").get()?.c ?? 0 : 0;
236
+ const diaryDirPath = path.join(baseDir, 'diary');
237
+ const diaryCount = fs.existsSync(diaryDirPath) ? fs.readdirSync(diaryDirPath).length : 0;
238
+ output.push(buildNudgeMessage({ daysSince, episodeCount, diaryCount }));
231
239
  const nowIso = new Date().toISOString();
232
240
  db.prepare(`
233
241
  INSERT INTO skill_memory (skill_name, key, value, updated_at)
package/mcp-server.cjs CHANGED
@@ -12481,12 +12481,12 @@ var require_dist = __commonJS({
12481
12481
  throw new Error(`Unknown format "${name}"`);
12482
12482
  return f;
12483
12483
  };
12484
- function addFormats(ajv, list, fs9, exportName) {
12484
+ function addFormats(ajv, list, fs10, exportName) {
12485
12485
  var _a3;
12486
12486
  var _b;
12487
12487
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
12488
12488
  for (const f of list)
12489
- ajv.addFormat(f, fs9[f]);
12489
+ ajv.addFormat(f, fs10[f]);
12490
12490
  }
12491
12491
  module2.exports = exports2 = formatsPlugin;
12492
12492
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -36561,7 +36561,7 @@ var StdioServerTransport = class {
36561
36561
  };
36562
36562
 
36563
36563
  // scripts/mcp-server.ts
36564
- var import_fs8 = __toESM(require("fs"));
36564
+ var import_fs9 = __toESM(require("fs"));
36565
36565
  var import_path9 = __toESM(require("path"));
36566
36566
 
36567
36567
  // scripts/lib/mcp-namespace.ts
@@ -36798,6 +36798,7 @@ var PRIORITY_CASE = "WHEN 'supersedes' THEN 1 WHEN 'contradicts' THEN 2 WHEN 'ex
36798
36798
  var CC_PLUGIN_CACHE_DIR = import_path.default.join(import_os.default.homedir(), ".claude", "plugins", "cache");
36799
36799
  var CC_MEMORY_PATH_MARKER = import_path.default.join(".claude", "projects");
36800
36800
  var TELEMETRY_FILE_ROTATE_BYTES = 10 * 1024 * 1024;
36801
+ var CACHE_STALE_AGE_MS = 24 * 3600 * 1e3;
36801
36802
 
36802
36803
  // scripts/lib/mcp-namespace.ts
36803
36804
  function resolveMindloreHome() {
@@ -37488,7 +37489,7 @@ function handleBrief(ctx, input) {
37488
37489
  }
37489
37490
 
37490
37491
  // scripts/lib/tool-adapters/ingest-adapter.ts
37491
- var import_fs4 = __toESM(require("fs"));
37492
+ var import_fs5 = __toESM(require("fs"));
37492
37493
  var import_path6 = __toESM(require("path"));
37493
37494
 
37494
37495
  // scripts/lib/slugify.ts
@@ -37496,6 +37497,15 @@ function slugify2(text) {
37496
37497
  return text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 60).replace(/^-|-$/g, "");
37497
37498
  }
37498
37499
 
37500
+ // scripts/lib/secure-io.ts
37501
+ var import_fs4 = __toESM(require("fs"));
37502
+ function safeMkdir(dirPath) {
37503
+ import_fs4.default.mkdirSync(dirPath, { recursive: true, mode: 448 });
37504
+ }
37505
+ function safeWriteFile(filePath, data) {
37506
+ import_fs4.default.writeFileSync(filePath, data, { encoding: "utf8", mode: 384 });
37507
+ }
37508
+
37499
37509
  // scripts/lib/tool-adapters/ingest-adapter.ts
37500
37510
  function handleIngest(ctx, input) {
37501
37511
  if (!input.content || input.content.trim().length === 0) {
@@ -37506,7 +37516,7 @@ function handleIngest(ctx, input) {
37506
37516
  if (input.type === "file") {
37507
37517
  const filePath = import_path6.default.resolve(input.content);
37508
37518
  try {
37509
- body = import_fs4.default.readFileSync(filePath, "utf8").replace(/\r\n/g, "\n");
37519
+ body = import_fs5.default.readFileSync(filePath, "utf8").replace(/\r\n/g, "\n");
37510
37520
  } catch {
37511
37521
  throw new Error(`File not found: ${filePath}`);
37512
37522
  }
@@ -37534,8 +37544,8 @@ ${body}
37534
37544
  `;
37535
37545
  const outPath = import_path6.default.join(ctx.baseDir, "raw", `${slug}.md`);
37536
37546
  const rawDir = import_path6.default.join(ctx.baseDir, "raw");
37537
- if (!import_fs4.default.existsSync(rawDir)) import_fs4.default.mkdirSync(rawDir, { recursive: true });
37538
- import_fs4.default.writeFileSync(outPath, fullContent);
37547
+ safeMkdir(rawDir);
37548
+ safeWriteFile(outPath, fullContent);
37539
37549
  const wordCount = body.split(/\s+/).filter(Boolean).length;
37540
37550
  return {
37541
37551
  slug,
@@ -37547,7 +37557,7 @@ ${body}
37547
37557
  }
37548
37558
 
37549
37559
  // scripts/lib/tool-adapters/decide-adapter.ts
37550
- var import_fs5 = __toESM(require("fs"));
37560
+ var import_fs6 = __toESM(require("fs"));
37551
37561
  var import_path7 = __toESM(require("path"));
37552
37562
  function handleDecide(ctx, input) {
37553
37563
  if (input.action === "save") {
@@ -37556,7 +37566,7 @@ function handleDecide(ctx, input) {
37556
37566
  }
37557
37567
  }
37558
37568
  const decisionsDir = import_path7.default.join(ctx.baseDir, "decisions");
37559
- if (!import_fs5.default.existsSync(decisionsDir)) import_fs5.default.mkdirSync(decisionsDir, { recursive: true });
37569
+ safeMkdir(decisionsDir);
37560
37570
  if (input.action === "save") {
37561
37571
  const slug = slugify2(input.title);
37562
37572
  if (!slug) throw new Error("Cannot generate slug from title");
@@ -37578,15 +37588,15 @@ function handleDecide(ctx, input) {
37578
37588
  }
37579
37589
  lines.push("");
37580
37590
  const outPath = import_path7.default.join(decisionsDir, `${slug}.md`);
37581
- import_fs5.default.writeFileSync(outPath, lines.join("\n"));
37591
+ safeWriteFile(outPath, lines.join("\n"));
37582
37592
  return { slug, path: outPath };
37583
37593
  }
37584
- const files = import_fs5.default.readdirSync(decisionsDir).filter((f) => f.endsWith(".md")).sort().reverse();
37594
+ const files = import_fs6.default.readdirSync(decisionsDir).filter((f) => f.endsWith(".md")).sort().reverse();
37585
37595
  const limit = Math.min(input.limit ?? 10, 50);
37586
37596
  const decisions = [];
37587
37597
  for (const file2 of files) {
37588
37598
  if (decisions.length >= limit) break;
37589
- const raw = import_fs5.default.readFileSync(import_path7.default.join(decisionsDir, file2), "utf8").replace(/\r\n/g, "\n");
37599
+ const raw = import_fs6.default.readFileSync(import_path7.default.join(decisionsDir, file2), "utf8").replace(/\r\n/g, "\n");
37590
37600
  const match = raw.match(/^---\n([\s\S]*?)\n---/);
37591
37601
  if (!match) continue;
37592
37602
  const fmBlock = match[1];
@@ -37647,12 +37657,12 @@ function handleRelate(ctx, input) {
37647
37657
  }
37648
37658
 
37649
37659
  // scripts/lib/tool-adapters/get-adapter.ts
37650
- var import_fs6 = __toESM(require("fs"));
37660
+ var import_fs7 = __toESM(require("fs"));
37651
37661
  function handleGet(ctx, input) {
37652
37662
  const { path: sourcePath, title } = assertSlugExists(ctx.db, input.source);
37653
37663
  let raw;
37654
37664
  try {
37655
- raw = import_fs6.default.readFileSync(sourcePath, "utf8").replace(/\r\n/g, "\n");
37665
+ raw = import_fs7.default.readFileSync(sourcePath, "utf8").replace(/\r\n/g, "\n");
37656
37666
  } catch {
37657
37667
  throw new Error(`Source file not found on disk: ${sourcePath}`);
37658
37668
  }
@@ -37669,10 +37679,16 @@ function handleGet(ctx, input) {
37669
37679
  const match = chunks.find((c) => c.heading && slugify2(c.heading) === targetSlug);
37670
37680
  if (match) {
37671
37681
  result.content = match.content;
37672
- result.section = match.heading.replace(/^#+\s*/, "");
37682
+ const heading = match.heading;
37683
+ if (!heading) throw new Error("expected match.heading to exist");
37684
+ result.section = heading.replace(/^#+\s*/, "");
37673
37685
  } else {
37674
37686
  result.content = "";
37675
- const sections = chunks.filter((c) => c.heading).map((c) => c.heading.replace(/^#+\s*/, ""));
37687
+ const sections = chunks.filter((c) => c.heading).map((c) => {
37688
+ const heading = c.heading;
37689
+ if (!heading) throw new Error("expected c.heading to exist");
37690
+ return heading.replace(/^#+\s*/, "");
37691
+ });
37676
37692
  if (sections.length > 0) result.available_sections = sections;
37677
37693
  }
37678
37694
  }
@@ -37685,12 +37701,12 @@ function handleGet(ctx, input) {
37685
37701
  }
37686
37702
 
37687
37703
  // scripts/lib/mcp-telemetry.ts
37688
- var import_fs7 = __toESM(require("fs"));
37704
+ var import_fs8 = __toESM(require("fs"));
37689
37705
  var import_path8 = __toESM(require("path"));
37690
37706
  function writeMcpTelemetry(baseDir, entry) {
37691
37707
  try {
37692
37708
  const telemetryPath = import_path8.default.join(baseDir, "telemetry.jsonl");
37693
- import_fs7.default.appendFileSync(telemetryPath, JSON.stringify(entry) + "\n");
37709
+ import_fs8.default.appendFileSync(telemetryPath, JSON.stringify(entry) + "\n");
37694
37710
  } catch {
37695
37711
  }
37696
37712
  }
@@ -37836,7 +37852,7 @@ function registerAllTools(server, ctx) {
37836
37852
  function resolvePluginRoot() {
37837
37853
  let dir = __dirname;
37838
37854
  for (let i = 0; i < 5; i++) {
37839
- if (import_fs8.default.existsSync(import_path9.default.join(dir, "node_modules", "better-sqlite3"))) return dir;
37855
+ if (import_fs9.default.existsSync(import_path9.default.join(dir, "node_modules", "better-sqlite3"))) return dir;
37840
37856
  const parent = import_path9.default.dirname(dir);
37841
37857
  if (parent === dir) break;
37842
37858
  dir = parent;
@@ -37846,7 +37862,7 @@ function resolvePluginRoot() {
37846
37862
  function ensureSqliteBinding(root) {
37847
37863
  const modDir = import_path9.default.join(root, "node_modules", "better-sqlite3");
37848
37864
  const bindingPath = import_path9.default.join(modDir, "build", "Release", "better_sqlite3.node");
37849
- if (import_fs8.default.existsSync(modDir) && !import_fs8.default.existsSync(bindingPath)) {
37865
+ if (import_fs9.default.existsSync(modDir) && !import_fs9.default.existsSync(bindingPath)) {
37850
37866
  process.stderr.write("mindlore-mcp: rebuilding better-sqlite3 native binding...\n");
37851
37867
  try {
37852
37868
  (0, import_child_process.execSync)("npm rebuild better-sqlite3", {
@@ -37868,8 +37884,8 @@ ensureSqliteBinding(pluginRoot);
37868
37884
  var Database = require(import_path9.default.join(pluginRoot, "node_modules", "better-sqlite3"));
37869
37885
  var PACKAGE_VERSION = (() => {
37870
37886
  try {
37871
- const pkgPath = import_fs8.default.existsSync(import_path9.default.join(__dirname, "package.json")) ? import_path9.default.join(__dirname, "package.json") : import_path9.default.join(__dirname, "..", "package.json");
37872
- const pkg = JSON.parse(import_fs8.default.readFileSync(pkgPath, "utf8"));
37887
+ const pkgPath = import_fs9.default.existsSync(import_path9.default.join(__dirname, "package.json")) ? import_path9.default.join(__dirname, "package.json") : import_path9.default.join(__dirname, "..", "package.json");
37888
+ const pkg = JSON.parse(import_fs9.default.readFileSync(pkgPath, "utf8"));
37873
37889
  return pkg.version ?? "0.0.0";
37874
37890
  } catch {
37875
37891
  return "0.0.0";
@@ -37877,11 +37893,11 @@ var PACKAGE_VERSION = (() => {
37877
37893
  })();
37878
37894
  async function main() {
37879
37895
  const baseDir = resolveMindloreHome();
37880
- if (!import_fs8.default.existsSync(baseDir)) {
37881
- import_fs8.default.mkdirSync(baseDir, { recursive: true });
37896
+ if (!import_fs9.default.existsSync(baseDir)) {
37897
+ safeMkdir(baseDir);
37882
37898
  for (const sub of ["sources", "episodes", "decisions", "diary", "raw", "domains", "analyses", "learnings"]) {
37883
37899
  const dir = import_path9.default.join(baseDir, sub);
37884
- if (!import_fs8.default.existsSync(dir)) import_fs8.default.mkdirSync(dir, { recursive: true });
37900
+ if (!import_fs9.default.existsSync(dir)) safeMkdir(dir);
37885
37901
  }
37886
37902
  }
37887
37903
  const dbPath = import_path9.default.join(baseDir, DB_NAME);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindlore",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "AI-native knowledge system for Claude Code",
5
5
  "type": "commonjs",
6
6
  "bin": {
package/plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifestVersion": 2,
3
3
  "name": "mindlore",
4
4
  "description": "AI-native knowledge system for Claude Code. Persistent, searchable, evolving knowledge base with FTS5 and Knowledge Graph.",
5
- "version": "0.7.6",
5
+ "version": "0.7.7",
6
6
  "skills": [
7
7
  {
8
8
  "name": "mindlore-ingest",
@@ -58,6 +58,11 @@
58
58
  "name": "mindlore-stats",
59
59
  "path": "skills/mindlore-stats/SKILL.md",
60
60
  "description": "Show context contribution and cost per session — hook calls, durations, DB stats."
61
+ },
62
+ {
63
+ "name": "mindlore-learnings",
64
+ "path": "skills/mindlore-learnings/SKILL.md",
65
+ "description": "Show full content of a learning by slug, or list all learnings. Use when the session-focus hook truncates a learning and you need the full text."
61
66
  }
62
67
  ],
63
68
  "agents": [
@@ -193,3 +193,24 @@ Optional: run full health check for structural integrity:
193
193
  ```bash
194
194
  node "$MINDLORE_PKG/dist/scripts/lib/skill-runner.js" mindlore-ingest mindlore-health-check.js
195
195
  ```
196
+
197
+ ## Type-Aware Extraction Templates
198
+
199
+ The default extraction template produces generic summary + tags. For specific source types, use the matching template below.
200
+
201
+ ### github-repo
202
+ Required fields in the source summary:
203
+ - **Tech Stack:** languages, frameworks, key dependencies
204
+ - **Key Features:** 3-5 main features the README emphasizes
205
+ - **Setup:** install/run commands
206
+ - **License:** SPDX identifier
207
+
208
+ ### cc-session
209
+ Required fields:
210
+ - **Decisions:** numbered list of architectural/scope decisions made
211
+ - **Patterns:** recurring techniques, idioms, or anti-patterns observed
212
+ - **Actionable Items:** TODO / follow-up tasks named in the transcript
213
+ - **Open Questions:** unresolved items called out at session end
214
+
215
+ ### Other types (docs, blog, video, x-thread, text-paste, snippet, forum, cc-subagent)
216
+ Use the existing generic template (summary + tags + quality + related links).
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: mindlore-learnings
3
+ description: Show full content of a learning by slug, or list all learnings. Use when the session-focus hook truncates a learning and you need the full text.
4
+ ---
5
+
6
+ # mindlore-learnings
7
+
8
+ ## Commands
9
+
10
+ ### show <slug>
11
+ Returns full content of `~/.mindlore/learnings/<slug>.md`.
12
+
13
+ Example:
14
+ ```
15
+ /mindlore-learnings show dev-patterns-2026-04
16
+ ```
17
+
18
+ If slug not found, suggests closest match.
19
+
20
+ ### list
21
+ Lists all learnings with the first line of each.
22
+
23
+ ```
24
+ /mindlore-learnings list
25
+ ```
26
+
27
+ ## Implementation
28
+
29
+ Source: `scripts/mindlore-learnings.ts`
30
+ Use: `node "$MINDLORE_PKG/dist/scripts/lib/skill-runner.js" mindlore-learnings mindlore-learnings.js <command> [args]`
package/start.cjs CHANGED
@@ -85,7 +85,7 @@ function installMissing(packages) {
85
85
  try {
86
86
  execSync(
87
87
  `${NPM} install ${packages.join(' ')} --no-package-lock --no-save --silent`,
88
- { cwd: __dirname_, stdio: 'pipe', timeout: 180000, shell: true }
88
+ { cwd: __dirname_, stdio: 'pipe', timeout: 600000, shell: true }
89
89
  );
90
90
  log('install complete');
91
91
  return true;