mindlore 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,7 +184,7 @@ Skills spawn subagents with `[mindlore:SKILL]` markers — the model-router hook
184
184
 
185
185
  ## Hooks
186
186
 
187
- 13 Claude Code lifecycle hooks (v0.4.2):
187
+ 14 Claude Code lifecycle hooks (v0.4.3):
188
188
 
189
189
  | Event | Hook | What it does |
190
190
  |-------|------|-------------|
@@ -193,7 +193,7 @@ Skills spawn subagents with `[mindlore:SKILL]` markers — the model-router hook
193
193
  | UserPromptSubmit | decision-detector | TR+EN decision signal detection |
194
194
  | FileChanged | index | Sync changed files to FTS5 |
195
195
  | FileChanged | fts5-sync | Incremental batch re-index |
196
- | SessionEnd | session-end | Structured delta + bare episode + FTS5 mirror + git sync |
196
+ | SessionEnd | session-end | Structured delta + bare episode + FTS5 mirror + detached git sync |
197
197
  | PreCompact | pre-compact | FTS5 flush before compaction |
198
198
  | PostCompact | post-compact | Re-inject context |
199
199
  | PreToolUse (Read) | read-guard | Repeated-read warning, blocks 3+ repeats |
@@ -201,6 +201,7 @@ Skills spawn subagents with `[mindlore:SKILL]` markers — the model-router hook
201
201
  | PreToolUse (Write\|Edit) | dont-repeat | LESSONS/learnings rule enforcement |
202
202
  | CwdChanged | cwd-changed | Scope detection + _scope.json write |
203
203
  | PreToolUse (Agent) | model-router | Cost-optimized model routing via markers |
204
+ | PreToolUse (Agent) | research-guard | FTS5 check before research — blocks if recent+high quality exists |
204
205
 
205
206
  ## Uninstall
206
207
 
@@ -48,11 +48,11 @@ describe('Hook Smoke Tests', () => {
48
48
  expect(file).toMatch(/\.cjs$/);
49
49
  }
50
50
  });
51
- test('hook count should match plan (13 hooks: 7 v0.1 + 2 v0.2 + 3 v0.3 + 1 v0.3.1)', () => {
51
+ test('hook count should match plan (14 hooks)', () => {
52
52
  const hookFiles = fs_1.default
53
53
  .readdirSync(HOOKS_DIR)
54
54
  .filter((f) => f.startsWith('mindlore-') && f.endsWith('.cjs'));
55
- expect(hookFiles).toHaveLength(13);
55
+ expect(hookFiles).toHaveLength(14);
56
56
  });
57
57
  });
58
58
  //# sourceMappingURL=hook-smoke.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hook-smoke.test.js","sourceRoot":"","sources":["../../tests/hook-smoke.test.ts"],"names":[],"mappings":";;;;;AAAA,4CAAoB;AACpB,gDAAwB;AACxB,4CAAoB;AAEpB,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAEtD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,MAAM,aAAa,GAAG;QACpB,4BAA4B;QAC5B,qBAAqB;QACrB,oBAAoB;QACpB,wBAAwB;QACxB,0BAA0B;QAC1B,0BAA0B;QAC1B,2BAA2B;KAC5B,CAAC;IAEF,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC9C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAElD,2EAA2E;YAC3E,MAAM,CAAC,GAAG,EAAE;gBACV,IAAI,YAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjD,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAClD,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAChD,MAAM,SAAS,GAAG,YAAE;aACjB,WAAW,CAAC,SAAS,CAAC;aACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;QAE5C,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,8EAA8E,EAAE,GAAG,EAAE;QACxF,MAAM,SAAS,GAAG,YAAE;aACjB,WAAW,CAAC,SAAS,CAAC;aACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAElE,MAAM,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"hook-smoke.test.js","sourceRoot":"","sources":["../../tests/hook-smoke.test.ts"],"names":[],"mappings":";;;;;AAAA,4CAAoB;AACpB,gDAAwB;AACxB,4CAAoB;AAEpB,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;AAEtD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,MAAM,aAAa,GAAG;QACpB,4BAA4B;QAC5B,qBAAqB;QACrB,oBAAoB;QACpB,wBAAwB;QACxB,0BAA0B;QAC1B,0BAA0B;QAC1B,2BAA2B;KAC5B,CAAC;IAEF,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC9C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAElD,2EAA2E;YAC3E,MAAM,CAAC,GAAG,EAAE;gBACV,IAAI,YAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;YACjD,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,MAAM,OAAO,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAClD,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAChD,MAAM,SAAS,GAAG,YAAE;aACjB,WAAW,CAAC,SAAS,CAAC;aACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,CAAC;QAE5C,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACnD,MAAM,SAAS,GAAG,YAAE;aACjB,WAAW,CAAC,SAAS,CAAC;aACtB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAElE,MAAM,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=research-guard.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"research-guard.test.d.ts","sourceRoot":"","sources":["../../tests/research-guard.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const child_process_1 = require("child_process");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const db_1 = require("./helpers/db");
11
+ const HOOK_PATH = path_1.default.join(__dirname, '..', 'hooks', 'mindlore-research-guard.cjs');
12
+ let tmpDir;
13
+ let dbPath;
14
+ beforeAll(() => {
15
+ // Create isolated .mindlore/ with test DB
16
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'mindlore-rg-'));
17
+ dbPath = path_1.default.join(tmpDir, 'mindlore.db');
18
+ const db = (0, db_1.createTestDb)(dbPath);
19
+ // Insert a high-quality recent source
20
+ (0, db_1.insertFts)(db, {
21
+ path: 'sources/cc-hooks-reference.md',
22
+ slug: 'cc-hooks-reference',
23
+ description: 'Claude Code Hooks Reference',
24
+ type: 'source',
25
+ category: 'sources',
26
+ title: 'Claude Code Hooks — Comprehensive Reference',
27
+ content: 'Claude Code hooks SessionEnd timeout behavior exit code PreToolUse PostToolUse lifecycle',
28
+ tags: 'claude-code, hooks, timeout, SessionEnd',
29
+ quality: 'high',
30
+ dateCaptured: new Date().toISOString().slice(0, 10),
31
+ project: 'mindlore',
32
+ });
33
+ // Insert a medium-quality source
34
+ (0, db_1.insertFts)(db, {
35
+ path: 'sources/obsidian-integration.md',
36
+ slug: 'obsidian-integration',
37
+ description: 'Obsidian integration notes',
38
+ type: 'source',
39
+ category: 'sources',
40
+ title: 'Obsidian Vault Integration',
41
+ content: 'obsidian vault wikilinks export integration markdown',
42
+ tags: 'obsidian, vault, export',
43
+ quality: 'medium',
44
+ dateCaptured: new Date().toISOString().slice(0, 10),
45
+ project: 'mindlore',
46
+ });
47
+ db.close();
48
+ });
49
+ afterAll(() => {
50
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
51
+ });
52
+ function runHook(input) {
53
+ const result = (0, child_process_1.spawnSync)('node', [HOOK_PATH], {
54
+ input: JSON.stringify(input),
55
+ encoding: 'utf8',
56
+ timeout: 10000,
57
+ env: { ...process.env, MINDLORE_HOME: tmpDir },
58
+ });
59
+ return {
60
+ stdout: result.stdout || '',
61
+ stderr: result.stderr || '',
62
+ exitCode: result.status ?? 1,
63
+ };
64
+ }
65
+ describe('mindlore-research-guard', () => {
66
+ test('should pass silently for non-Agent tools', () => {
67
+ const result = runHook({ tool_name: 'Read', tool_input: { file_path: '/tmp/test.md' } });
68
+ expect(result.exitCode).toBe(0);
69
+ expect(result.stdout).toBe('');
70
+ });
71
+ test('should pass silently for non-research Agent prompts', () => {
72
+ const result = runHook({
73
+ tool_name: 'Agent',
74
+ tool_input: { prompt: 'Fix the bug in auth module', description: 'coder fix' },
75
+ });
76
+ expect(result.exitCode).toBe(0);
77
+ expect(result.stdout).toBe('');
78
+ });
79
+ test('should pass silently for mindlore ingest operations', () => {
80
+ const result = runHook({
81
+ tool_name: 'Agent',
82
+ tool_input: { prompt: '[mindlore:ingest] Fetch URL and save to raw/', description: 'ingest' },
83
+ });
84
+ expect(result.exitCode).toBe(0);
85
+ expect(result.stdout).toBe('');
86
+ });
87
+ test('should pass silently for [research-override] bypass', () => {
88
+ const result = runHook({
89
+ tool_name: 'Agent',
90
+ tool_input: {
91
+ prompt: '[research-override] Research the Claude Code hooks timeout',
92
+ description: 'research',
93
+ },
94
+ });
95
+ expect(result.exitCode).toBe(0);
96
+ expect(result.stdout).toBe('');
97
+ });
98
+ test('should block (exit 2) when high-quality recent match exists', () => {
99
+ const result = runHook({
100
+ tool_name: 'Agent',
101
+ tool_input: {
102
+ prompt: 'Research the Claude Code SessionEnd hook timeout behavior',
103
+ description: 'SessionEnd hook research',
104
+ },
105
+ });
106
+ expect(result.exitCode).toBe(2);
107
+ expect(result.stderr).toContain('BLOK');
108
+ expect(result.stderr).toContain('cc-hooks-reference');
109
+ });
110
+ test('should pass silently when keywords are too few', () => {
111
+ const result = runHook({
112
+ tool_name: 'Agent',
113
+ tool_input: { prompt: 'Research it', description: 'research' },
114
+ });
115
+ expect(result.exitCode).toBe(0);
116
+ expect(result.stdout).toBe('');
117
+ });
118
+ test('should return additionalContext for medium-quality matches', () => {
119
+ const result = runHook({
120
+ tool_name: 'Agent',
121
+ tool_input: {
122
+ prompt: 'Research obsidian vault integration wikilinks export',
123
+ description: 'obsidian research',
124
+ },
125
+ });
126
+ expect(result.exitCode).toBe(0);
127
+ if (result.stdout) {
128
+ const parsed = JSON.parse(result.stdout);
129
+ expect(parsed.hookSpecificOutput).toBeDefined();
130
+ expect(parsed.hookSpecificOutput.additionalContext).toContain('mindlore-research-guard');
131
+ }
132
+ });
133
+ test('should handle empty stdin gracefully', () => {
134
+ const result = (0, child_process_1.spawnSync)('node', [HOOK_PATH], {
135
+ input: '',
136
+ encoding: 'utf8',
137
+ timeout: 5000,
138
+ env: { ...process.env, MINDLORE_HOME: tmpDir },
139
+ });
140
+ expect(result.status).toBe(0);
141
+ });
142
+ });
143
+ //# sourceMappingURL=research-guard.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"research-guard.test.js","sourceRoot":"","sources":["../../tests/research-guard.test.ts"],"names":[],"mappings":"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"}
@@ -532,6 +532,64 @@ function formatMultiSessionEpisodes(episodes) {
532
532
  return lines.join('\n');
533
533
  }
534
534
 
535
+ // Shared FTS5 search utilities (used by mindlore-search + mindlore-research-guard)
536
+ const STOP_WORDS = new Set([
537
+ // English
538
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
539
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
540
+ 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
541
+ 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
542
+ 'it', 'its', 'this', 'that', 'these', 'those', 'what', 'which', 'who',
543
+ 'whom', 'how', 'when', 'where', 'why', 'not', 'no', 'nor', 'so',
544
+ 'if', 'or', 'but', 'all', 'each', 'every', 'both', 'few', 'more',
545
+ 'most', 'other', 'some', 'such', 'only', 'own', 'same', 'than',
546
+ 'and', 'about', 'between', 'after', 'before', 'above', 'below',
547
+ 'up', 'down', 'out', 'very', 'just', 'also', 'now', 'then',
548
+ 'here', 'there', 'too', 'yet', 'my', 'your', 'his', 'her', 'our',
549
+ 'their', 'me', 'him', 'us', 'them', 'i', 'you', 'he', 'she', 'we', 'they',
550
+ // Turkish
551
+ 'bir', 'bu', 'su', 'ne', 'nasil', 'neden', 'var', 'yok', 'mi', 'mu',
552
+ 'ile', 'icin', 'de', 'da', 've', 'veya', 'ama', 'ise', 'hem',
553
+ 'bakalim', 'gel', 'git', 'yap', 'et', 'al', 'ver',
554
+ 'evet', 'hayir', 'tamam', 'ok', 'oldu', 'olur', 'dur',
555
+ 'simdi', 'sonra', 'once', 'hemen', 'biraz',
556
+ 'lan', 'ya', 'ki', 'abi', 'hadi', 'hey', 'selam',
557
+ 'olarak', 'olan', 'gibi', 'kadar', 'daha', 'cok', 'hem',
558
+ 'bunu', 'buna', 'icinde', 'uzerinde', 'arasinda',
559
+ 'sonucu', 'tarafindan', 'zaten', 'gayet',
560
+ 'acaba', 'nedir', 'midir', 'mudur',
561
+ // Generic technical (appears everywhere, not distinctive)
562
+ 'hook', 'file', 'dosya', 'kullan', 'ekle', 'yaz', 'oku', 'calistir',
563
+ 'kontrol', 'test', 'check', 'run', 'add', 'update', 'config',
564
+ 'setup', 'install', 'start', 'stop', 'create', 'delete', 'remove', 'set',
565
+ 'get', 'list', 'show', 'view', 'open', 'close', 'save', 'load',
566
+ ]);
567
+
568
+ /**
569
+ * Extract topic keywords from text. Preserves Turkish chars.
570
+ * @param {string} text - Input text
571
+ * @param {number} [maxKeywords=8] - Max keywords to return
572
+ * @returns {string[]} Unique keywords
573
+ */
574
+ function extractKeywords(text, maxKeywords = 8) {
575
+ const words = text
576
+ .toLowerCase()
577
+ .replace(/[^\w\s\u00e7\u011f\u0131\u00f6\u015f\u00fc-]/g, ' ')
578
+ .split(/\s+/)
579
+ .filter((w) => w.length >= 3 && !STOP_WORDS.has(w) && !/^\d+$/.test(w));
580
+ return [...new Set(words)].slice(0, maxKeywords);
581
+ }
582
+
583
+ /**
584
+ * Sanitize keyword for FTS5 MATCH — strip special chars, quote-wrap.
585
+ * @param {string} kw - Raw keyword
586
+ * @returns {string|null} Quoted keyword or null if too short
587
+ */
588
+ function sanitizeKeyword(kw) {
589
+ const clean = kw.replace(/["*(){}[\]^~:]/g, '').replace(/-/g, ' ').trim();
590
+ return clean.length >= 2 ? `"${clean}"` : null;
591
+ }
592
+
535
593
  module.exports = {
536
594
  MINDLORE_DIR,
537
595
  GLOBAL_MINDLORE_DIR,
@@ -571,4 +629,8 @@ module.exports = {
571
629
  formatSupersededChains,
572
630
  queryMultiSessionEpisodes,
573
631
  formatMultiSessionEpisodes,
632
+ // FTS5 search utilities (v0.4.3)
633
+ STOP_WORDS,
634
+ extractKeywords,
635
+ sanitizeKeyword,
574
636
  };
@@ -0,0 +1,144 @@
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 } = 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
+ const prompt = (toolInput.prompt || '') + ' ' + (toolInput.description || '');
107
+
108
+ // Skip mindlore internal operations and explicit overrides
109
+ if (EXCLUDE_REGEX.test(prompt)) return;
110
+
111
+ // Only trigger on research-intent agents
112
+ if (!RESEARCH_REGEX.test(prompt)) return;
113
+
114
+ const keywords = extractKeywords(prompt, 10);
115
+ if (keywords.length < 2) return;
116
+
117
+ const matches = searchDbs(keywords);
118
+ if (matches.length === 0) return;
119
+
120
+ // Check for high-quality recent matches
121
+ const strongMatches = matches.filter((m) => m.quality === 'high' && m.recent);
122
+
123
+ if (strongMatches.length > 0) {
124
+ // BLOCK: high quality + recent knowledge exists
125
+ const slugList = strongMatches.map((m) => ` - ${m.slug} (${m.title})`).join('\n');
126
+ const msg = `[mindlore-research-guard] BLOK: Bu konuda guncel, yuksek kaliteli bilgi DB'de zaten var.\n` +
127
+ `Once mevcut bilgiyi oku:\n${slugList}\n` +
128
+ `Eger bilgi yetersizse, prompt'a "[research-override]" ekleyerek tekrar dene.`;
129
+ process.stderr.write(msg);
130
+ process.exit(2);
131
+ }
132
+
133
+ // WARN: old or low-quality matches exist
134
+ const slugList = matches.map((m) => `${m.slug} (${m.quality}, ${m.date_captured || 'tarih yok'})`).join(', ');
135
+ const output = {
136
+ hookSpecificOutput: {
137
+ hookEventName: 'PreToolUse',
138
+ additionalContext: `[mindlore-research-guard] DB'de ilgili bilgi var ama eski/dusuk kalite: ${slugList}. Guncelleme gerekebilir — arastirma sonrasi DB'yi guncelle.`,
139
+ },
140
+ };
141
+ process.stdout.write(JSON.stringify(output));
142
+ }
143
+
144
+ main();
@@ -10,55 +10,12 @@
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
- const { getAllDbs, requireDatabase, extractHeadings, readHookStdin } = require('./lib/mindlore-common.cjs');
13
+ const { getAllDbs, requireDatabase, extractHeadings, readHookStdin, extractKeywords } = require('./lib/mindlore-common.cjs');
14
14
 
15
15
  const MAX_RESULTS = 3;
16
16
  const MIN_QUERY_WORDS = 3;
17
17
  const MIN_KEYWORD_HITS = 2;
18
18
 
19
- // Extended stop words (~70 TR + EN) matching old knowledge system
20
- const STOP_WORDS = new Set([
21
- // English
22
- 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
23
- 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
24
- 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
25
- 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during',
26
- 'it', 'its', 'this', 'that', 'these', 'those', 'what', 'which', 'who',
27
- 'whom', 'how', 'when', 'where', 'why', 'not', 'no', 'nor', 'so',
28
- 'if', 'or', 'but', 'all', 'each', 'every', 'both', 'few', 'more',
29
- 'most', 'other', 'some', 'such', 'only', 'own', 'same', 'than',
30
- 'and', 'about', 'between', 'after', 'before', 'above', 'below',
31
- 'up', 'down', 'out', 'very', 'just', 'also', 'now', 'then',
32
- 'here', 'there', 'too', 'yet', 'my', 'your', 'his', 'her', 'our',
33
- 'their', 'me', 'him', 'us', 'them', 'i', 'you', 'he', 'she', 'we', 'they',
34
- // Turkish
35
- 'bir', 'bu', 'su', 'ne', 'nasil', 'neden', 'var', 'yok', 'mi', 'mu',
36
- 'ile', 'icin', 'de', 'da', 've', 'veya', 'ama', 'ise', 'hem',
37
- 'bakalim', 'gel', 'git', 'yap', 'et', 'al', 'ver',
38
- 'evet', 'hayir', 'tamam', 'ok', 'oldu', 'olur', 'dur',
39
- 'simdi', 'sonra', 'once', 'hemen', 'biraz',
40
- 'lan', 'ya', 'ki', 'abi', 'hadi', 'hey', 'selam',
41
- 'olarak', 'olan', 'gibi', 'kadar', 'daha', 'cok', 'hem',
42
- 'bunu', 'buna', 'icinde', 'uzerinde', 'arasinda',
43
- 'sonucu', 'tarafindan', 'zaten', 'gayet',
44
- 'acaba', 'nedir', 'midir', 'mudur',
45
- // Generic technical (appears everywhere, not distinctive)
46
- 'hook', 'file', 'dosya', 'kullan', 'ekle', 'yaz', 'oku', 'calistir',
47
- 'kontrol', 'test', 'check', 'run', 'add', 'update', 'config',
48
- 'setup', 'install', 'start', 'stop', 'create', 'delete', 'remove', 'set',
49
- 'get', 'list', 'show', 'view', 'open', 'close', 'save', 'load',
50
- ]);
51
-
52
- function extractKeywords(text) {
53
- const words = text
54
- .toLowerCase()
55
- .replace(/[^\w\s\u00e7\u011f\u0131\u00f6\u015f\u00fc-]/g, ' ')
56
- .split(/\s+/)
57
- .filter((w) => w.length >= 3 && !STOP_WORDS.has(w) && !/^\d+$/.test(w));
58
-
59
- return [...new Set(words)].slice(0, 8);
60
- }
61
-
62
19
  /**
63
20
  * Search a single DB and return scored results with their baseDir.
64
21
  */
@@ -11,9 +11,26 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const { execSync } = require('child_process');
14
+ const os = require('os');
15
+ const { execSync, spawn } = require('child_process');
15
16
  const { findMindloreDir, globalDir, getProjectName, openDatabase, ensureEpisodesTable, hasEpisodesTable, insertBareEpisode, insertFtsRow } = require('./lib/mindlore-common.cjs');
16
17
 
18
+ // --worker mode: heavy ops run in detached child process (survives parent exit)
19
+ if (process.argv.includes('--worker')) {
20
+ const dataPath = process.argv[process.argv.indexOf('--worker') + 1];
21
+ try {
22
+ 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();
28
+ } catch (_err) {
29
+ // Graceful fail — worker errors are silent
30
+ }
31
+ process.exit(0);
32
+ }
33
+
17
34
  function formatDate(date) {
18
35
  const y = date.getFullYear();
19
36
  const m = String(date.getMonth() + 1).padStart(2, '0');
@@ -140,14 +157,25 @@ function main() {
140
157
  fs.appendFileSync(logPath, logEntry, 'utf8');
141
158
  }
142
159
 
143
- // v0.4.0: Write bare episode to episodes table
144
- writeBareEpisode(baseDir, project, commits, changedFiles, reads);
145
-
146
- // Obsidian auto-export (if vault configured)
147
- syncObsidian(baseDir);
148
-
149
- // Git auto-commit + push for global ~/.mindlore/ only
150
- syncGlobalRepo();
160
+ // Heavy ops: detach into child process so CC can exit immediately.
161
+ // Fixes "Hook cancelled" when CC kills the hook before completion.
162
+ // See: https://github.com/anthropics/claude-code/issues/41577
163
+ try {
164
+ const workerData = JSON.stringify({ baseDir, project, commits, changedFiles, reads });
165
+ const tmpFile = path.join(os.tmpdir(), `mindlore-worker-${Date.now()}.json`);
166
+ fs.writeFileSync(tmpFile, workerData, 'utf8');
167
+ const child = spawn(process.execPath, [__filename, '--worker', tmpFile], {
168
+ detached: true,
169
+ stdio: 'ignore',
170
+ cwd: process.cwd(),
171
+ });
172
+ child.unref();
173
+ } catch (_err) {
174
+ // Fallback: run inline if spawn fails
175
+ writeBareEpisode(baseDir, project, commits, changedFiles, reads);
176
+ syncObsidian(baseDir);
177
+ syncGlobalRepo();
178
+ }
151
179
  }
152
180
 
153
181
  /**
@@ -204,7 +232,7 @@ function writeBareEpisode(baseDir, project, commits, changedFiles, reads) {
204
232
  description: truncatedSummary,
205
233
  type: 'episode',
206
234
  category: 'episodes',
207
- title: truncatedSummary.slice(0, 100),
235
+ title: truncatedSummary,
208
236
  content: [truncatedSummary, body ?? ''].join('\n').trim(),
209
237
  tags: 'session',
210
238
  quality: null,
@@ -218,8 +246,8 @@ function writeBareEpisode(baseDir, project, commits, changedFiles, reads) {
218
246
  writeBoth();
219
247
 
220
248
  db.close();
221
- } catch (_err) {
222
- // Graceful fail never break session end
249
+ } catch (err) {
250
+ process.stderr.write(`[mindlore] episode write failed: ${err?.message ?? err}\n`);
223
251
  }
224
252
  }
225
253
 
@@ -332,7 +360,7 @@ function syncGlobalRepo() {
332
360
 
333
361
  if (!status) return; // nothing to commit
334
362
 
335
- execSync('git add -A', { cwd: gDir, timeout: 5000, stdio: 'pipe' });
363
+ execSync('git add *.md mindlore.db config.json diary/ sources/ domains/ analyses/ decisions/ raw/ connections/ insights/ learnings/', { cwd: gDir, timeout: 5000, stdio: 'pipe' });
336
364
  const now = new Date().toISOString().slice(0, 19);
337
365
  execSync(`git commit -m "mindlore auto-sync ${now}"`, {
338
366
  cwd: gDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindlore",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "AI-native knowledge system for Claude Code",
5
5
  "type": "commonjs",
6
6
  "bin": {
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.2",
4
+ "version": "0.4.3",
5
5
  "skills": [
6
6
  {
7
7
  "name": "mindlore-ingest",
@@ -94,6 +94,11 @@
94
94
  "event": "PreToolUse",
95
95
  "script": "hooks/mindlore-model-router.cjs",
96
96
  "if": "Agent"
97
+ },
98
+ {
99
+ "event": "PreToolUse",
100
+ "script": "hooks/mindlore-research-guard.cjs",
101
+ "if": "Agent"
97
102
  }
98
103
  ]
99
104
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.2",
2
+ "version": "0.4.3",
3
3
  "models": {
4
4
  "ingest": "haiku",
5
5
  "evolve": "sonnet",