mindlore 0.7.0 → 0.7.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 (56) hide show
  1. package/README.md +30 -3
  2. package/dist/scripts/bundle-hooks.d.ts +2 -0
  3. package/dist/scripts/bundle-hooks.d.ts.map +1 -0
  4. package/dist/scripts/bundle-hooks.js +68 -0
  5. package/dist/scripts/bundle-hooks.js.map +1 -0
  6. package/dist/scripts/init.js +0 -3
  7. package/dist/scripts/init.js.map +1 -1
  8. package/dist/scripts/lib/constants.d.ts +0 -2
  9. package/dist/scripts/lib/constants.d.ts.map +1 -1
  10. package/dist/scripts/lib/constants.js +0 -21
  11. package/dist/scripts/lib/constants.js.map +1 -1
  12. package/dist/tests/hook-smoke.test.js +1 -1
  13. package/dist/tests/hook-smoke.test.js.map +1 -1
  14. package/dist/tests/search-hook.test.js +1 -1
  15. package/dist/tests/search-hook.test.js.map +1 -1
  16. package/hooks/cc-memory-bulk-sync.cjs +592 -0
  17. package/hooks/cc-session-sync.cjs +842 -0
  18. package/hooks/hooks.json +149 -0
  19. package/hooks/lib/mindlore-common.cjs +2 -2
  20. package/hooks/lib/secure-io.cjs +17 -0
  21. package/hooks/mindlore-cwd-changed.cjs +19 -34
  22. package/hooks/mindlore-decision-detector.cjs +40 -31
  23. package/hooks/mindlore-dont-repeat.cjs +57 -115
  24. package/hooks/mindlore-fts5-sync.cjs +15 -44
  25. package/hooks/mindlore-index.cjs +100 -101
  26. package/hooks/mindlore-model-router.cjs +20 -32
  27. package/hooks/mindlore-post-compact.cjs +26 -42
  28. package/hooks/mindlore-post-read.cjs +35 -60
  29. package/hooks/mindlore-pre-compact.cjs +55 -73
  30. package/hooks/mindlore-read-guard.cjs +28 -51
  31. package/hooks/mindlore-research-guard.cjs +63 -101
  32. package/hooks/mindlore-search.cjs +1142 -93
  33. package/hooks/mindlore-session-end.cjs +155 -276
  34. package/hooks/mindlore-session-focus.cjs +639 -110
  35. package/hooks/src/lib/constants.cjs +15 -0
  36. package/hooks/src/lib/mindlore-common.cjs +975 -0
  37. package/hooks/src/lib/mindlore-common.d.cts +72 -0
  38. package/hooks/src/lib/secure-io.cjs +17 -0
  39. package/hooks/src/lib/types.d.ts +58 -0
  40. package/hooks/src/mindlore-cwd-changed.cjs +57 -0
  41. package/hooks/src/mindlore-decision-detector.cjs +54 -0
  42. package/hooks/src/mindlore-dont-repeat.cjs +222 -0
  43. package/hooks/src/mindlore-fts5-sync.cjs +98 -0
  44. package/hooks/src/mindlore-index.cjs +230 -0
  45. package/hooks/src/mindlore-model-router.cjs +54 -0
  46. package/hooks/src/mindlore-post-compact.cjs +69 -0
  47. package/hooks/src/mindlore-post-read.cjs +106 -0
  48. package/hooks/src/mindlore-pre-compact.cjs +154 -0
  49. package/hooks/src/mindlore-read-guard.cjs +105 -0
  50. package/hooks/src/mindlore-research-guard.cjs +176 -0
  51. package/hooks/src/mindlore-search.cjs +200 -0
  52. package/hooks/src/mindlore-session-end.cjs +511 -0
  53. package/hooks/src/mindlore-session-focus.cjs +256 -0
  54. package/package.json +7 -3
  55. package/plugin.json +3 -3
  56. package/templates/config.json +1 -1
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
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
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { findMindloreDir, readHookStdin, getProjectName, hookLog, extractSkeleton, withTelemetrySync } = require('./lib/mindlore-common.cjs');
18
+
19
+ function main() {
20
+ const baseDir = findMindloreDir();
21
+ if (!baseDir) return;
22
+
23
+ const filePath = readHookStdin(['file_path', 'path']);
24
+ if (!filePath) return;
25
+
26
+ // Only track CWD-relative files, skip .mindlore/ internals
27
+ const cwd = process.cwd();
28
+ const resolved = path.resolve(filePath);
29
+ if (!resolved.startsWith(cwd)) return;
30
+ if (resolved.startsWith(path.resolve(baseDir))) return;
31
+
32
+ // Load or create session reads tracker
33
+ const diaryDir = path.join(baseDir, 'diary');
34
+ if (!fs.existsSync(diaryDir)) {
35
+ fs.mkdirSync(diaryDir, { recursive: true });
36
+ }
37
+
38
+ const readsPath = path.join(diaryDir, `_session-reads-${getProjectName()}.json`);
39
+ let reads = {};
40
+ if (fs.existsSync(readsPath)) {
41
+ try {
42
+ reads = JSON.parse(fs.readFileSync(readsPath, 'utf8'));
43
+ } catch (_err) {
44
+ reads = {};
45
+ }
46
+ }
47
+
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)` : '';
73
+
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.`);
78
+ process.exit(2);
79
+ }
80
+
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.` : '';
84
+ let skeletonSection = '';
85
+ try {
86
+ const ext = path.extname(filePath).slice(1);
87
+ const fileContent = fs.readFileSync(filePath, 'utf8');
88
+ if (fileContent.length < 500_000) {
89
+ const skeleton = extractSkeleton(fileContent, ext);
90
+ if (skeleton !== fileContent) {
91
+ const truncated = skeleton.length > 2000 ? skeleton.slice(0, 2000) + '\n...[truncated]' : skeleton;
92
+ skeletonSection = '\n\n' + truncated;
93
+ }
94
+ }
95
+ } catch (_e) { /* unreadable/binary — skip */ }
96
+ process.stdout.write(JSON.stringify({
97
+ hookSpecificOutput: {
98
+ hookEventName: 'PreToolUse',
99
+ additionalContext: `[Mindlore: ${basename}${tokenInfo} bu session'da ${count}. kez okunuyor.${totalWaste} Bir sonraki okuma engellenecek — Edit gerekiyorsa simdi yap.]${skeletonSection}`
100
+ }
101
+ }));
102
+ }
103
+ }
104
+
105
+ try { withTelemetrySync('mindlore-read-guard', main); } catch (err) { hookLog('read-guard', 'error', err?.message ?? String(err)); }
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * mindlore-research-guard — PreToolUse (Agent) hook
6
+ *
7
+ * Before spawning a researcher agent, checks FTS5 for existing knowledge.
8
+ * - High quality + recent (30 days) match → exit 2 (block)
9
+ * - Old or low quality match → exit 0 with warning (additionalContext)
10
+ * - No match → silent pass
11
+ *
12
+ * Prevents redundant web research when knowledge already exists in DB.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { getAllDbs, requireDatabase, extractKeywords, sanitizeKeyword, hookLog, withTelemetrySync } = require('./lib/mindlore-common.cjs');
18
+
19
+ // Keywords that signal a research/web-search intent in agent prompts
20
+ // Note: entries with dots/stars are regex patterns, rest are literals
21
+ const RESEARCH_SIGNALS = [
22
+ 'research', 'araştır', 'arastir', 'investigate', 'search for',
23
+ 'web search', 'websearch', 'webfetch', 'fetch.*url', 'look up',
24
+ 'find out', 'check.*docs', 'documentation.*for',
25
+ ];
26
+ const RESEARCH_REGEX = new RegExp(RESEARCH_SIGNALS.join('|'), 'i');
27
+
28
+ // Exclude ingest/internal operations (they intentionally fetch URLs)
29
+ const EXCLUDE_REGEX = /\[mindlore:|\bmindlore-ingest\b|ingest.*url|save.*raw|\[research-override\]/i;
30
+
31
+ const MAX_AGE_DAYS = 30;
32
+
33
+ function isRecent(dateStr) {
34
+ if (!dateStr) return false;
35
+ const d = new Date(dateStr);
36
+ if (isNaN(d.getTime())) return false;
37
+ const diff = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24);
38
+ return diff <= MAX_AGE_DAYS;
39
+ }
40
+
41
+ /**
42
+ * Search FTS5 using a single OR query instead of per-path×keyword loop.
43
+ * Returns top matches with quality and date from FTS5 columns (no file I/O).
44
+ */
45
+ function searchDbs(keywords) {
46
+ const Database = requireDatabase();
47
+ if (!Database) return [];
48
+
49
+ const sanitized = keywords.map(sanitizeKeyword).filter(Boolean);
50
+ if (sanitized.length === 0) return [];
51
+
52
+ const matchQuery = sanitized.join(' OR ');
53
+ const dbPaths = getAllDbs();
54
+ const results = [];
55
+
56
+ for (const dbPath of dbPaths) {
57
+ try {
58
+ const db = new Database(dbPath, { readonly: true });
59
+
60
+ // Single FTS5 query — O(1) instead of O(paths × keywords)
61
+ const rows = db.prepare(
62
+ `SELECT path, slug, title, description, quality, date_captured, rank
63
+ FROM mindlore_fts
64
+ WHERE mindlore_fts MATCH ?
65
+ ORDER BY rank
66
+ LIMIT 10`
67
+ ).all(matchQuery);
68
+
69
+ for (const row of rows) {
70
+ const quality = (row.quality || 'medium').toLowerCase();
71
+ const date_captured = row.date_captured || null;
72
+
73
+ results.push({
74
+ slug: row.slug || path.basename(row.path, '.md'),
75
+ title: row.title || row.description || row.slug || '',
76
+ quality,
77
+ date_captured,
78
+ recent: isRecent(date_captured),
79
+ rank: row.rank,
80
+ });
81
+ }
82
+
83
+ db.close();
84
+ } catch (_err) { /* db open or query failed */ }
85
+ }
86
+
87
+ // Sort by rank (lower = better match in FTS5)
88
+ results.sort((a, b) => a.rank - b.rank);
89
+ return results.slice(0, 5);
90
+ }
91
+
92
+ function main() {
93
+ let input;
94
+ try {
95
+ const raw = fs.readFileSync(0, 'utf8').trim();
96
+ if (!raw) return;
97
+ input = JSON.parse(raw);
98
+ } catch (_err) {
99
+ return;
100
+ }
101
+
102
+ const toolName = input.tool_name || '';
103
+ if (toolName !== 'Agent') return;
104
+
105
+ const toolInput = input.tool_input || {};
106
+
107
+ // Only block agents with web access — let local-only agents pass
108
+ const WEB_CAPABLE_TYPES = ['researcher', 'general-purpose'];
109
+ const LOCAL_ONLY_TYPES = [
110
+ 'Explore', 'coder', 'code-reviewer', 'Plan',
111
+ 'bug-analyzer', 'security-reviewer', 'contrarian',
112
+ 'scope-guardian', 'quality-gate', 'test-runner',
113
+ ];
114
+ const subagentType = toolInput.subagent_type || '';
115
+ const description = (toolInput.description || '').toLowerCase();
116
+
117
+ // Known local-only agent → always pass
118
+ if (LOCAL_ONLY_TYPES.includes(subagentType)) return;
119
+
120
+ // Known web-capable agent → continue to FTS5 check
121
+ // Unknown or empty subagent_type → check description for research intent
122
+ if (subagentType && !WEB_CAPABLE_TYPES.includes(subagentType)) return;
123
+
124
+ // If no subagent_type, check description for web research intent (reuse RESEARCH_REGEX)
125
+ if (!subagentType && !RESEARCH_REGEX.test(description)) return;
126
+
127
+ const prompt = (toolInput.prompt || '') + ' ' + (toolInput.description || '');
128
+
129
+ // Skip mindlore internal operations and explicit overrides
130
+ if (EXCLUDE_REGEX.test(prompt)) return;
131
+
132
+ // If subagent_type is a known research type, skip prompt-level regex check
133
+ // Otherwise require research signals in the prompt text
134
+ const isKnownResearchType = WEB_CAPABLE_TYPES.includes(subagentType);
135
+ if (!isKnownResearchType && !RESEARCH_REGEX.test(prompt)) return;
136
+
137
+ const keywords = extractKeywords(prompt, 10);
138
+ if (keywords.length < 2) return;
139
+
140
+ const matches = searchDbs(keywords);
141
+ if (matches.length === 0) return;
142
+
143
+ // Prevents false positives like "claude-code-repo" matching on generic words
144
+ const lcKeywords = keywords.map((k) => k.toLowerCase());
145
+ const relevantMatches = matches.filter((m) => {
146
+ const haystack = `${m.slug} ${m.title}`.toLowerCase();
147
+ const overlap = lcKeywords.filter((k) => haystack.includes(k));
148
+ return overlap.length >= 2;
149
+ });
150
+
151
+ if (relevantMatches.length === 0) return;
152
+
153
+ // Check for high-quality recent matches among relevant ones
154
+ const strongMatches = relevantMatches.filter((m) => m.quality === 'high' && m.recent);
155
+
156
+ if (strongMatches.length > 0) {
157
+ const slugList = strongMatches.map((m) => ` - ${m.slug} (${m.title})`).join('\n');
158
+ const msg = `[mindlore-research-guard] BLOK: Bu konuda guncel, yuksek kaliteli bilgi DB'de zaten var.\n` +
159
+ `Once mevcut bilgiyi oku:\n${slugList}\n` +
160
+ `Eger bilgi yetersizse, prompt'a "[research-override]" ekleyerek tekrar dene.`;
161
+ process.stderr.write(msg);
162
+ process.exit(2);
163
+ }
164
+
165
+ // WARN: relevant but old or low-quality matches exist
166
+ const slugList = relevantMatches.map((m) => `${m.slug} (${m.quality}, ${m.date_captured || 'tarih yok'})`).join(', ');
167
+ const output = {
168
+ hookSpecificOutput: {
169
+ hookEventName: 'PreToolUse',
170
+ additionalContext: `[mindlore-research-guard] DB'de ilgili bilgi var ama eski/dusuk kalite: ${slugList}. Guncelleme gerekebilir — arastirma sonrasi DB'yi guncelle.`,
171
+ },
172
+ };
173
+ process.stdout.write(JSON.stringify(output));
174
+ }
175
+
176
+ try { withTelemetrySync('mindlore-research-guard', main); } catch (err) { hookLog('research-guard', 'error', err?.message ?? String(err)); }
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * mindlore-search — UserPromptSubmit hook
6
+ *
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.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { getAllDbs, openDatabase, extractHeadings, readHookStdin, readConfig, hookLog, incrementRecallCount, withTelemetry } = require('./lib/mindlore-common.cjs');
15
+
16
+ const MAX_RESULTS = 3;
17
+ const MIN_QUERY_WORDS = 3;
18
+
19
+ let searchEngineMod;
20
+ try {
21
+ searchEngineMod = require('../dist/scripts/lib/search-engine.js');
22
+ } catch (_err) {
23
+ // search-engine not built yet
24
+ }
25
+
26
+ let SearchCacheMod;
27
+ try {
28
+ SearchCacheMod = require('../dist/scripts/lib/search-cache.js');
29
+ } catch (_err) {
30
+ // search-cache not built yet
31
+ }
32
+
33
+ function main() {
34
+ const userMessage = readHookStdin(['prompt', 'content', 'message', 'query']);
35
+ if (!userMessage || userMessage.length < MIN_QUERY_WORDS) return;
36
+
37
+ const dbPaths = getAllDbs();
38
+ if (dbPaths.length === 0) return;
39
+
40
+ if (!searchEngineMod) {
41
+ hookLog('search', 'warn', 'search-engine module not available — skipping');
42
+ return;
43
+ }
44
+
45
+ const project = path.basename(process.cwd());
46
+ const config = readConfig(path.dirname(dbPaths[0]));
47
+ const synonyms = (config && config.synonyms) ? config.synonyms : {};
48
+
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
+ }
57
+
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;
82
+ }
83
+ }
84
+
85
+ const results = searchEngineMod.search(db, userMessage, {
86
+ project,
87
+ maxResults: effectiveMax,
88
+ synonyms,
89
+ });
90
+
91
+ if (cache) cache.set(userMessage, results);
92
+
93
+ const baseDir = path.dirname(dbPath);
94
+ for (const r of results) {
95
+ allResults.push({ ...r, baseDir });
96
+ }
97
+
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
+ }
110
+ }
111
+
112
+ // Deduplicate by full path
113
+ const seen = new Set();
114
+ const unique = [];
115
+ for (const r of allResults) {
116
+ const normalized = path.resolve(r.path);
117
+ if (!seen.has(normalized)) {
118
+ seen.add(normalized);
119
+ unique.push(r);
120
+ }
121
+ }
122
+
123
+ // Sort by score descending, take top N
124
+ unique.sort((a, b) => b.score - a.score);
125
+ const relevant = unique.slice(0, MAX_RESULTS);
126
+ if (relevant.length === 0) return;
127
+
128
+ // Token budget from config
129
+ const budget = (config && config.tokenBudget) || {};
130
+ const perResultChars = ((budget.perResult || 500) * 4);
131
+ const totalChars = ((budget.searchResults || 1500) * 4);
132
+
133
+ // Build output
134
+ const output = [];
135
+ let totalUsed = 0;
136
+ for (const r of relevant) {
137
+ if (totalUsed >= totalChars) break;
138
+ const relativePath = path.relative(r.baseDir, r.path).replace(/\\/g, '/');
139
+
140
+ let headings = [];
141
+ const contentStr = r.content || '';
142
+ if (contentStr) {
143
+ try {
144
+ headings = extractHeadings(contentStr, 3);
145
+ } catch (_err) { /* skip */ }
146
+ }
147
+
148
+ const category = r.category || path.dirname(relativePath).split('/')[0];
149
+ const title = r.title || r.slug || path.basename(r.path, '.md');
150
+ const description = r.description || '';
151
+
152
+ const headingStr = headings.length > 0 ? `\nBasliklar: ${headings.join(', ')}` : '';
153
+ const tagsStr = r.tags ? `\nTags: ${r.tags}` : '';
154
+ const snippetOrDesc = r.snippet || description;
155
+ const entry = `[Mindlore: ${category}/${title}] ${snippetOrDesc}\nDosya: ${relativePath}${tagsStr}${headingStr}`;
156
+ const truncated = entry.slice(0, perResultChars);
157
+ totalUsed += truncated.length;
158
+ output.push(truncated);
159
+ }
160
+
161
+ if (output.length > 0) {
162
+ let outputStr = output.join('\n\n') + '\n';
163
+
164
+ const OFFLOAD_THRESHOLD = 10240;
165
+ if (outputStr.length > OFFLOAD_THRESHOLD) {
166
+ const baseDir = path.dirname(dbPaths[0]);
167
+ const tmpDir = path.join(baseDir, 'tmp');
168
+ fs.mkdirSync(tmpDir, { recursive: true });
169
+
170
+ try {
171
+ const oneHourAgo = Date.now() - 3600000;
172
+ const files = fs.readdirSync(tmpDir)
173
+ .filter(f => f.startsWith('search-'))
174
+ .map(f => ({ name: f, mtime: fs.statSync(path.join(tmpDir, f)).mtimeMs }))
175
+ .sort((a, b) => b.mtime - a.mtime);
176
+ for (let i = 0; i < files.length; i++) {
177
+ if (i >= 20 || files[i].mtime < oneHourAgo) {
178
+ try { fs.unlinkSync(path.join(tmpDir, files[i].name)); } catch { /* ignore */ }
179
+ }
180
+ }
181
+ } catch { /* cleanup is best-effort */ }
182
+ const fileName = `search-${Date.now()}.md`;
183
+ const filePath = path.join(tmpDir, fileName);
184
+ fs.writeFileSync(filePath, outputStr, 'utf8');
185
+
186
+ const summary = outputStr.slice(0, 500).replace(/\n/g, ' ').trim();
187
+ outputStr = `[Mindlore Search: ${outputStr.length} chars offloaded to ${filePath}]\n` +
188
+ `Summary: ${summary}...\n` +
189
+ `[Read full results: ${filePath}]`;
190
+ hookLog('search', 'info', 'offloaded to tmp/ (' + outputStr.length + ' chars)');
191
+ }
192
+
193
+ process.stdout.write(outputStr);
194
+ }
195
+ }
196
+
197
+ withTelemetry('mindlore-search', main).catch(err => {
198
+ hookLog('mindlore-search', 'error', err?.message ?? String(err));
199
+ process.exit(0);
200
+ });