mindlore 0.0.1 → 0.1.0
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/LICENSE +21 -21
- package/README.md +56 -1
- package/SCHEMA.md +11 -8
- package/hooks/lib/mindlore-common.cjs +72 -0
- package/hooks/mindlore-fts5-sync.cjs +12 -9
- package/hooks/mindlore-index.cjs +10 -12
- package/hooks/mindlore-post-compact.cjs +2 -2
- package/hooks/mindlore-pre-compact.cjs +1 -1
- package/hooks/mindlore-search.cjs +93 -62
- package/hooks/mindlore-session-focus.cjs +2 -2
- package/package.json +1 -1
- package/scripts/init.cjs +125 -10
- package/scripts/mindlore-fts5-index.cjs +7 -10
- package/scripts/mindlore-fts5-search.cjs +3 -6
- package/scripts/mindlore-health-check.cjs +19 -3
- package/scripts/uninstall.cjs +186 -0
- package/templates/SCHEMA.md +224 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 mindlore
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mindlore
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -103,9 +103,51 @@ Also suggests installing:
|
|
|
103
103
|
- **markitdown** — better web/document extraction (URL, DOCX, YouTube)
|
|
104
104
|
- **context-mode** — token savings for large sessions
|
|
105
105
|
|
|
106
|
+
## How It Works
|
|
107
|
+
|
|
108
|
+
Mindlore operates through Claude Code lifecycle hooks — invisible background scripts
|
|
109
|
+
that fire automatically as you work. No commands to run, no workflow changes.
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
┌─────────────────────────────┐
|
|
113
|
+
│ Claude Code Session │
|
|
114
|
+
└──────────────┬──────────────┘
|
|
115
|
+
│
|
|
116
|
+
┌──────────────────────────────────────┼──────────────────────────────────────┐
|
|
117
|
+
│ │ │
|
|
118
|
+
▼ ▼ ▼
|
|
119
|
+
SESSION START DURING SESSION SESSION END
|
|
120
|
+
│ │ │
|
|
121
|
+
├─ session-focus hook ├─ search hook ├─ session-end hook
|
|
122
|
+
│ reads INDEX.md + last delta │ 7-col FTS5 + porter stemmer │ writes delta to diary/
|
|
123
|
+
│ injects into context │ per-keyword scoring, top 3 injected │
|
|
124
|
+
│ │ │
|
|
125
|
+
│ ├─ index + fts5-sync hooks │
|
|
126
|
+
│ │ file changes → FTS5 update │
|
|
127
|
+
│ │ │
|
|
128
|
+
│ ├─ /mindlore-ingest skill │
|
|
129
|
+
│ │ URL → raw/ → sources/ → FTS5 │
|
|
130
|
+
│ │ │
|
|
131
|
+
└────────────────────────────────┴──────────────────────────────────────┘
|
|
132
|
+
│
|
|
133
|
+
┌──────────────┴──────────────┐
|
|
134
|
+
│ NEXT SESSION │
|
|
135
|
+
│ session-focus injects delta │
|
|
136
|
+
│ → knowledge compounds │
|
|
137
|
+
└─────────────────────────────┘
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Key design decisions:**
|
|
141
|
+
|
|
142
|
+
- **Hooks are global** — registered in `~/.claude/settings.json`, active in all projects
|
|
143
|
+
- **Data is per-project** — `.mindlore/` lives in each project directory
|
|
144
|
+
- **No `.mindlore/`?** — hooks silently skip, zero overhead
|
|
145
|
+
- **FTS5 search** — SQLite full-text search with BM25 ranking, no external services
|
|
146
|
+
- **Content-hash dedup** — SHA256 prevents re-indexing unchanged files
|
|
147
|
+
|
|
106
148
|
## Hooks
|
|
107
149
|
|
|
108
|
-
|
|
150
|
+
7 Claude Code lifecycle hooks (v0.1):
|
|
109
151
|
|
|
110
152
|
| Event | Hook | What it does |
|
|
111
153
|
|-------|------|-------------|
|
|
@@ -117,6 +159,19 @@ Mindlore works through 7 Claude Code lifecycle hooks (v0.1):
|
|
|
117
159
|
| PreCompact | pre-compact | FTS5 flush before compaction |
|
|
118
160
|
| PostCompact | post-compact | Re-inject context |
|
|
119
161
|
|
|
162
|
+
## Uninstall
|
|
163
|
+
|
|
164
|
+
Remove Mindlore from your system:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx mindlore uninstall
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This removes:
|
|
171
|
+
- Hooks from `~/.claude/settings.json`
|
|
172
|
+
- Skills from `~/.claude/skills/`
|
|
173
|
+
- Optionally: `.mindlore/` project data (asks for confirmation)
|
|
174
|
+
|
|
120
175
|
## Inspired By
|
|
121
176
|
|
|
122
177
|
- [Andrej Karpathy](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) — LLM Knowledge Bases concept
|
package/SCHEMA.md
CHANGED
|
@@ -59,14 +59,14 @@ tags: [tag1, tag2]
|
|
|
59
59
|
|
|
60
60
|
| Type | Required Fields | Optional |
|
|
61
61
|
|------|----------------|----------|
|
|
62
|
-
| `raw` | slug, type, source_url | tags |
|
|
63
|
-
| `source` | slug, type, title, tags, quality, source_url, ingested | date_captured |
|
|
64
|
-
| `domain` | slug, type, title, tags |
|
|
65
|
-
| `analysis` | slug, type, title, tags, confidence, sources_used |
|
|
66
|
-
| `insight` | slug, type, title, tags | sources_used |
|
|
67
|
-
| `connection` | slug, type, title, tags | sources_used |
|
|
68
|
-
| `learning` | slug, type, title, tags |
|
|
69
|
-
| `decision` | slug, type, title, tags | supersedes, status |
|
|
62
|
+
| `raw` | slug, type, source_url | tags, description |
|
|
63
|
+
| `source` | slug, type, title, tags, quality, source_url, ingested | date_captured, description, raw_slug |
|
|
64
|
+
| `domain` | slug, type, title, tags | description, status |
|
|
65
|
+
| `analysis` | slug, type, title, tags, confidence, sources_used | description |
|
|
66
|
+
| `insight` | slug, type, title, tags | sources_used, description |
|
|
67
|
+
| `connection` | slug, type, title, tags | sources_used, description |
|
|
68
|
+
| `learning` | slug, type, title, tags | description |
|
|
69
|
+
| `decision` | slug, type, title, tags | supersedes, status, description |
|
|
70
70
|
| `diary` | slug, type, date | — (hook adds automatically) |
|
|
71
71
|
|
|
72
72
|
### Field Value Rules
|
|
@@ -77,6 +77,9 @@ tags: [tag1, tag2]
|
|
|
77
77
|
- `sources_used`: list of slugs referenced in the analysis
|
|
78
78
|
- `supersedes`: slug of the decision this one replaces
|
|
79
79
|
- `date`: ISO 8601 date (YYYY-MM-DD)
|
|
80
|
+
- `description`: one-line summary (15-30 words). Used in FTS5 search and inject output. Optional but strongly recommended for search quality
|
|
81
|
+
- `raw_slug`: slug of the raw/ file this source was processed from (source→raw traceability)
|
|
82
|
+
- `status`: `stub` | `active` | `archived` (domain maturity indicator)
|
|
80
83
|
|
|
81
84
|
## 4. Seven Operations
|
|
82
85
|
|
|
@@ -41,6 +41,74 @@ function sha256(content) {
|
|
|
41
41
|
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Parse YAML frontmatter from markdown content.
|
|
46
|
+
* Returns { meta: { key: value }, body: string }
|
|
47
|
+
*/
|
|
48
|
+
function parseFrontmatter(content) {
|
|
49
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
50
|
+
if (!match) return { meta: {}, body: content };
|
|
51
|
+
|
|
52
|
+
const meta = {};
|
|
53
|
+
const lines = match[1].split('\n');
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
const colonIdx = line.indexOf(':');
|
|
56
|
+
if (colonIdx === -1) continue;
|
|
57
|
+
const key = line.slice(0, colonIdx).trim();
|
|
58
|
+
let value = line.slice(colonIdx + 1).trim();
|
|
59
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
60
|
+
value = value.slice(1, -1).split(',').map((s) => s.trim().replace(/^["']|["']$/g, ''));
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === 'string') {
|
|
63
|
+
value = value.replace(/^["']|["']$/g, '');
|
|
64
|
+
}
|
|
65
|
+
meta[key] = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const bodyStart = content.indexOf('---', 3);
|
|
69
|
+
const body = bodyStart !== -1 ? content.slice(bodyStart + 3).replace(/^\r?\n/, '') : content;
|
|
70
|
+
|
|
71
|
+
return { meta, body };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract FTS5 metadata from parsed frontmatter + file path.
|
|
76
|
+
* Returns { slug, description, type, category, title }
|
|
77
|
+
*/
|
|
78
|
+
function extractFtsMetadata(meta, body, filePath, baseDir) {
|
|
79
|
+
const slug = meta.slug || path.basename(filePath, '.md');
|
|
80
|
+
const description = meta.description || '';
|
|
81
|
+
const type = meta.type || '';
|
|
82
|
+
const relativePath = baseDir ? path.relative(baseDir, filePath) : filePath;
|
|
83
|
+
const category = path.dirname(relativePath).split(path.sep)[0] || 'root';
|
|
84
|
+
let title = meta.title || meta.name || '';
|
|
85
|
+
if (!title) {
|
|
86
|
+
const headingMatch = body.match(/^#\s+(.+)/m);
|
|
87
|
+
title = headingMatch ? headingMatch[1].trim() : path.basename(filePath, '.md');
|
|
88
|
+
}
|
|
89
|
+
return { slug, description, type, category, title };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Shared SQL constants to prevent drift across indexing paths.
|
|
94
|
+
*/
|
|
95
|
+
const SQL_FTS_INSERT =
|
|
96
|
+
'INSERT INTO mindlore_fts (path, slug, description, type, category, title, content) VALUES (?, ?, ?, ?, ?, ?, ?)';
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Extract headings (h1-h3) from markdown content.
|
|
100
|
+
*/
|
|
101
|
+
function extractHeadings(content, max) {
|
|
102
|
+
const headings = [];
|
|
103
|
+
for (const line of content.split('\n')) {
|
|
104
|
+
if (/^#{1,3}\s/.test(line)) {
|
|
105
|
+
headings.push(line.replace(/^#+\s*/, '').trim());
|
|
106
|
+
if (headings.length >= max) break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return headings;
|
|
110
|
+
}
|
|
111
|
+
|
|
44
112
|
function requireDatabase() {
|
|
45
113
|
try {
|
|
46
114
|
return require('better-sqlite3');
|
|
@@ -84,6 +152,10 @@ module.exports = {
|
|
|
84
152
|
findMindloreDir,
|
|
85
153
|
getLatestDelta,
|
|
86
154
|
sha256,
|
|
155
|
+
parseFrontmatter,
|
|
156
|
+
extractFtsMetadata,
|
|
157
|
+
SQL_FTS_INSERT,
|
|
158
|
+
extractHeadings,
|
|
87
159
|
requireDatabase,
|
|
88
160
|
openDatabase,
|
|
89
161
|
getAllMdFiles,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
-
const { MINDLORE_DIR, DB_NAME, sha256,
|
|
16
|
+
const { MINDLORE_DIR, DB_NAME, sha256, openDatabase, getAllMdFiles, parseFrontmatter, extractFtsMetadata, SQL_FTS_INSERT } = require('./lib/mindlore-common.cjs');
|
|
17
17
|
|
|
18
18
|
function main() {
|
|
19
19
|
// Read stdin to check if this is a .mindlore/ file change
|
|
@@ -35,24 +35,25 @@ function main() {
|
|
|
35
35
|
// Only trigger on .mindlore/ changes (empty filePath = skip)
|
|
36
36
|
if (!filePath || !filePath.includes(MINDLORE_DIR)) return;
|
|
37
37
|
|
|
38
|
+
// Skip if this is a single .md file change — mindlore-index.cjs handles those.
|
|
39
|
+
// This hook is for bulk changes (git pull, manual batch edits).
|
|
40
|
+
if (filePath.endsWith('.md')) return;
|
|
41
|
+
|
|
38
42
|
const baseDir = path.join(process.cwd(), MINDLORE_DIR);
|
|
39
43
|
if (!fs.existsSync(baseDir)) return;
|
|
40
44
|
|
|
41
45
|
const dbPath = path.join(baseDir, DB_NAME);
|
|
42
46
|
if (!fs.existsSync(dbPath)) return;
|
|
43
47
|
|
|
44
|
-
const
|
|
45
|
-
if (!
|
|
46
|
-
|
|
47
|
-
const db = new Database(dbPath);
|
|
48
|
-
db.pragma('journal_mode = WAL');
|
|
48
|
+
const db = openDatabase(dbPath);
|
|
49
|
+
if (!db) return;
|
|
49
50
|
|
|
50
51
|
const mdFiles = getAllMdFiles(baseDir);
|
|
51
52
|
let synced = 0;
|
|
52
53
|
|
|
53
54
|
const getHash = db.prepare('SELECT content_hash FROM file_hashes WHERE path = ?');
|
|
54
55
|
const deleteFts = db.prepare('DELETE FROM mindlore_fts WHERE path = ?');
|
|
55
|
-
const insertFts = db.prepare(
|
|
56
|
+
const insertFts = db.prepare(SQL_FTS_INSERT);
|
|
56
57
|
const upsertHash = db.prepare(`
|
|
57
58
|
INSERT INTO file_hashes (path, content_hash, last_indexed)
|
|
58
59
|
VALUES (?, ?, ?)
|
|
@@ -72,8 +73,10 @@ function main() {
|
|
|
72
73
|
const existing = getHash.get(file);
|
|
73
74
|
if (existing && existing.content_hash === hash) continue;
|
|
74
75
|
|
|
76
|
+
const { meta, body } = parseFrontmatter(content);
|
|
77
|
+
const { slug, description, type, category, title } = extractFtsMetadata(meta, body, file, baseDir);
|
|
75
78
|
deleteFts.run(file);
|
|
76
|
-
insertFts.run(file,
|
|
79
|
+
insertFts.run(file, slug, description, type, category, title, body);
|
|
77
80
|
upsertHash.run(file, hash, now);
|
|
78
81
|
synced++;
|
|
79
82
|
}
|
|
@@ -84,7 +87,7 @@ function main() {
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
if (synced > 0) {
|
|
87
|
-
process.
|
|
90
|
+
process.stdout.write(`[Mindlore FTS5 Sync: ${synced} files re-indexed]\n`);
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
|
package/hooks/mindlore-index.cjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
-
const { MINDLORE_DIR, DB_NAME, SKIP_FILES, sha256,
|
|
13
|
+
const { MINDLORE_DIR, DB_NAME, SKIP_FILES, sha256, openDatabase, parseFrontmatter, extractFtsMetadata, SQL_FTS_INSERT } = require('./lib/mindlore-common.cjs');
|
|
14
14
|
|
|
15
15
|
function main() {
|
|
16
16
|
let input = '';
|
|
@@ -43,13 +43,10 @@ function main() {
|
|
|
43
43
|
|
|
44
44
|
if (!fs.existsSync(dbPath)) return;
|
|
45
45
|
|
|
46
|
-
const Database = requireDatabase();
|
|
47
|
-
if (!Database) return;
|
|
48
|
-
|
|
49
46
|
if (!fs.existsSync(filePath)) {
|
|
50
47
|
// File was deleted — remove from index
|
|
51
|
-
const db =
|
|
52
|
-
db
|
|
48
|
+
const db = openDatabase(dbPath);
|
|
49
|
+
if (!db) return;
|
|
53
50
|
try {
|
|
54
51
|
db.prepare('DELETE FROM mindlore_fts WHERE path = ?').run(filePath);
|
|
55
52
|
db.prepare('DELETE FROM file_hashes WHERE path = ?').run(filePath);
|
|
@@ -62,8 +59,8 @@ function main() {
|
|
|
62
59
|
const content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
63
60
|
const hash = sha256(content);
|
|
64
61
|
|
|
65
|
-
const db =
|
|
66
|
-
db
|
|
62
|
+
const db = openDatabase(dbPath);
|
|
63
|
+
if (!db) return;
|
|
67
64
|
|
|
68
65
|
try {
|
|
69
66
|
// Check if content changed
|
|
@@ -73,12 +70,13 @@ function main() {
|
|
|
73
70
|
|
|
74
71
|
if (existing && existing.content_hash === hash) return; // Unchanged
|
|
75
72
|
|
|
73
|
+
// Parse frontmatter for rich FTS5 columns
|
|
74
|
+
const { meta, body } = parseFrontmatter(content);
|
|
75
|
+
const { slug, description, type, category, title } = extractFtsMetadata(meta, body, filePath, baseDir);
|
|
76
|
+
|
|
76
77
|
// Update FTS5
|
|
77
78
|
db.prepare('DELETE FROM mindlore_fts WHERE path = ?').run(filePath);
|
|
78
|
-
db.prepare(
|
|
79
|
-
filePath,
|
|
80
|
-
content
|
|
81
|
-
);
|
|
79
|
+
db.prepare(SQL_FTS_INSERT).run(filePath, slug, description, type, category, title, body);
|
|
82
80
|
|
|
83
81
|
// Update hash
|
|
84
82
|
db.prepare(
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* After context compaction, re-inject session context:
|
|
8
8
|
* 1. Read INDEX.md
|
|
9
9
|
* 2. Read latest delta
|
|
10
|
-
* 3. Inject via
|
|
10
|
+
* 3. Inject via stdout (same as session-focus)
|
|
11
11
|
*
|
|
12
12
|
* This ensures the agent has knowledge context after compaction.
|
|
13
13
|
*/
|
|
@@ -39,7 +39,7 @@ function main() {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
if (output.length > 0) {
|
|
42
|
-
process.
|
|
42
|
+
process.stdout.write(output.join('\n\n') + '\n');
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -4,59 +4,62 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* mindlore-search — UserPromptSubmit hook
|
|
6
6
|
*
|
|
7
|
-
* Extracts keywords from user prompt, searches FTS5
|
|
8
|
-
*
|
|
7
|
+
* Extracts keywords from user prompt, searches FTS5 with per-keyword scoring,
|
|
8
|
+
* injects top results with description + headings (matching old knowledge system quality).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
-
const { findMindloreDir, DB_NAME, requireDatabase } = require('./lib/mindlore-common.cjs');
|
|
13
|
+
const { findMindloreDir, DB_NAME, requireDatabase, extractHeadings } = require('./lib/mindlore-common.cjs');
|
|
14
14
|
|
|
15
15
|
const MAX_RESULTS = 3;
|
|
16
|
-
const
|
|
16
|
+
const MIN_QUERY_WORDS = 3;
|
|
17
|
+
const MIN_KEYWORD_HITS = 2;
|
|
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
|
+
]);
|
|
17
51
|
|
|
18
52
|
function extractKeywords(text) {
|
|
19
|
-
// Remove common stop words and short words
|
|
20
|
-
const stopWords = new Set([
|
|
21
|
-
'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
22
|
-
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
23
|
-
'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for',
|
|
24
|
-
'on', 'with', 'at', 'by', 'from', 'as', 'into', 'about', 'between',
|
|
25
|
-
'through', 'after', 'before', 'above', 'below', 'up', 'down', 'out',
|
|
26
|
-
'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either',
|
|
27
|
-
'neither', 'each', 'every', 'all', 'any', 'few', 'more', 'most',
|
|
28
|
-
'other', 'some', 'such', 'no', 'only', 'own', 'same', 'than', 'too',
|
|
29
|
-
'very', 'just', 'also', 'now', 'then', 'here', 'there', 'when',
|
|
30
|
-
'where', 'why', 'how', 'what', 'which', 'who', 'whom', 'this',
|
|
31
|
-
'that', 'these', 'those', 'it', 'its', 'my', 'your', 'his', 'her',
|
|
32
|
-
'our', 'their', 'me', 'him', 'us', 'them', 'i', 'you', 'he', 'she',
|
|
33
|
-
'we', 'they', 'bu', 'su', 'bir', 'de', 'da', 've', 'ile', 'icin',
|
|
34
|
-
'var', 'mi', 'ne', 'nasil', 'nedir', 'evet', 'hayir',
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
53
|
const words = text
|
|
38
54
|
.toLowerCase()
|
|
39
|
-
.replace(/[
|
|
55
|
+
.replace(/[^\w\s\u00e7\u011f\u0131\u00f6\u015f\u00fc-]/g, ' ')
|
|
40
56
|
.split(/\s+/)
|
|
41
|
-
.filter((w) => w.length >=
|
|
42
|
-
|
|
43
|
-
// Deduplicate and limit
|
|
44
|
-
return [...new Set(words)].slice(0, 5);
|
|
45
|
-
}
|
|
57
|
+
.filter((w) => w.length >= 3 && !STOP_WORDS.has(w) && !/^\d+$/.test(w));
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
const headings = [];
|
|
49
|
-
for (const line of content.split('\n')) {
|
|
50
|
-
if (line.startsWith('#')) {
|
|
51
|
-
headings.push(line.replace(/^#+\s*/, '').trim());
|
|
52
|
-
if (headings.length >= max) break;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return headings;
|
|
59
|
+
return [...new Set(words)].slice(0, 8);
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function main() {
|
|
59
|
-
// Read user prompt from stdin
|
|
60
63
|
let input = '';
|
|
61
64
|
try {
|
|
62
65
|
input = fs.readFileSync(0, 'utf8');
|
|
@@ -67,12 +70,12 @@ function main() {
|
|
|
67
70
|
let userMessage = '';
|
|
68
71
|
try {
|
|
69
72
|
const parsed = JSON.parse(input);
|
|
70
|
-
userMessage = parsed.content || parsed.message || parsed.query || input;
|
|
73
|
+
userMessage = parsed.prompt || parsed.content || parsed.message || parsed.query || input;
|
|
71
74
|
} catch (_err) {
|
|
72
75
|
userMessage = input;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
if (!userMessage || userMessage.length <
|
|
78
|
+
if (!userMessage || userMessage.length < MIN_QUERY_WORDS) return;
|
|
76
79
|
|
|
77
80
|
const baseDir = findMindloreDir();
|
|
78
81
|
if (!baseDir) return;
|
|
@@ -81,7 +84,7 @@ function main() {
|
|
|
81
84
|
if (!fs.existsSync(dbPath)) return;
|
|
82
85
|
|
|
83
86
|
const keywords = extractKeywords(userMessage);
|
|
84
|
-
if (keywords.length
|
|
87
|
+
if (keywords.length < MIN_QUERY_WORDS) return;
|
|
85
88
|
|
|
86
89
|
const Database = requireDatabase();
|
|
87
90
|
if (!Database) return;
|
|
@@ -89,39 +92,67 @@ function main() {
|
|
|
89
92
|
const db = new Database(dbPath, { readonly: true });
|
|
90
93
|
|
|
91
94
|
try {
|
|
92
|
-
//
|
|
93
|
-
|
|
95
|
+
// Per-keyword scoring (like old knowledge-search.cjs)
|
|
96
|
+
// Count how many keywords match each document
|
|
97
|
+
const allPaths = db.prepare('SELECT DISTINCT path FROM mindlore_fts').all();
|
|
98
|
+
const scores = [];
|
|
99
|
+
const matchStmt = db.prepare('SELECT rank FROM mindlore_fts WHERE path = ? AND mindlore_fts MATCH ?');
|
|
100
|
+
|
|
101
|
+
for (const row of allPaths) {
|
|
102
|
+
let hits = 0;
|
|
103
|
+
let totalRank = 0;
|
|
104
|
+
|
|
105
|
+
for (const kw of keywords) {
|
|
106
|
+
try {
|
|
107
|
+
const r = matchStmt.get(row.path, '"' + kw + '"');
|
|
108
|
+
if (r) {
|
|
109
|
+
hits++;
|
|
110
|
+
totalRank += r.rank;
|
|
111
|
+
}
|
|
112
|
+
} catch (_err) {
|
|
113
|
+
// FTS5 query error for this keyword — skip
|
|
114
|
+
}
|
|
115
|
+
}
|
|
94
116
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
WHERE mindlore_fts MATCH ?
|
|
100
|
-
ORDER BY rank
|
|
101
|
-
LIMIT ?`
|
|
102
|
-
)
|
|
103
|
-
.all(ftsQuery, MAX_RESULTS);
|
|
117
|
+
if (hits >= MIN_KEYWORD_HITS) {
|
|
118
|
+
scores.push({ path: row.path, hits, totalRank });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
104
121
|
|
|
105
|
-
|
|
122
|
+
// Sort: most keyword hits first, then best rank
|
|
123
|
+
scores.sort((a, b) => b.hits - a.hits || a.totalRank - b.totalRank);
|
|
124
|
+
const relevant = scores.slice(0, MAX_RESULTS);
|
|
125
|
+
|
|
126
|
+
if (relevant.length === 0) return;
|
|
127
|
+
|
|
128
|
+
// Build rich inject output
|
|
129
|
+
const metaStmt = db.prepare(
|
|
130
|
+
'SELECT slug, description, category, title FROM mindlore_fts WHERE path = ?'
|
|
131
|
+
);
|
|
106
132
|
|
|
107
133
|
const output = [];
|
|
108
|
-
for (const r of
|
|
109
|
-
const
|
|
110
|
-
|
|
134
|
+
for (const r of relevant) {
|
|
135
|
+
const meta = metaStmt.get(r.path) || {};
|
|
136
|
+
const relativePath = path.relative(baseDir, r.path).replace(/\\/g, '/');
|
|
111
137
|
|
|
138
|
+
let headings = [];
|
|
112
139
|
if (fs.existsSync(r.path)) {
|
|
113
140
|
const content = fs.readFileSync(r.path, 'utf8');
|
|
114
|
-
headings = extractHeadings(content,
|
|
141
|
+
headings = extractHeadings(content, 5);
|
|
115
142
|
}
|
|
116
143
|
|
|
117
|
-
const
|
|
118
|
-
|
|
144
|
+
const category = meta.category || path.dirname(relativePath).split('/')[0];
|
|
145
|
+
const title = meta.title || meta.slug || path.basename(r.path, '.md');
|
|
146
|
+
const description = meta.description || '';
|
|
147
|
+
|
|
148
|
+
const headingStr = headings.length > 0 ? `\nBasliklar: ${headings.join(', ')}` : '';
|
|
149
|
+
output.push(
|
|
150
|
+
`[Mindlore: ${category}/${title}] ${description}\nDosya: ${relativePath}${headingStr}`
|
|
151
|
+
);
|
|
119
152
|
}
|
|
120
153
|
|
|
121
154
|
if (output.length > 0) {
|
|
122
|
-
process.
|
|
123
|
-
`[Mindlore Search: ${keywords.join(', ')}]\n${output.join('\n')}\n`
|
|
124
|
-
);
|
|
155
|
+
process.stdout.write(output.join('\n\n') + '\n');
|
|
125
156
|
}
|
|
126
157
|
} catch (_err) {
|
|
127
158
|
// FTS5 query error — silently skip
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* mindlore-session-focus — SessionStart hook
|
|
6
6
|
*
|
|
7
7
|
* Injects last delta file content + INDEX.md into session context.
|
|
8
|
-
* Fires once at session start via
|
|
8
|
+
* Fires once at session start via stdout additionalContext.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
@@ -35,7 +35,7 @@ function main() {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (output.length > 0) {
|
|
38
|
-
process.
|
|
38
|
+
process.stdout.write(output.join('\n\n') + '\n');
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|