mindlore 0.3.5 → 0.4.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.
Files changed (37) hide show
  1. package/README.md +9 -6
  2. package/dist/scripts/init.js +9 -1
  3. package/dist/scripts/init.js.map +1 -1
  4. package/dist/scripts/lib/episodes.d.ts +66 -0
  5. package/dist/scripts/lib/episodes.d.ts.map +1 -0
  6. package/dist/scripts/lib/episodes.js +180 -0
  7. package/dist/scripts/lib/episodes.js.map +1 -0
  8. package/dist/scripts/mindlore-episodes.d.ts +12 -0
  9. package/dist/scripts/mindlore-episodes.d.ts.map +1 -0
  10. package/dist/scripts/mindlore-episodes.js +193 -0
  11. package/dist/scripts/mindlore-episodes.js.map +1 -0
  12. package/dist/tests/diary.test.d.ts +6 -0
  13. package/dist/tests/diary.test.d.ts.map +1 -0
  14. package/dist/tests/diary.test.js +169 -0
  15. package/dist/tests/diary.test.js.map +1 -0
  16. package/dist/tests/episodes-inject.test.d.ts +6 -0
  17. package/dist/tests/episodes-inject.test.d.ts.map +1 -0
  18. package/dist/tests/episodes-inject.test.js +161 -0
  19. package/dist/tests/episodes-inject.test.js.map +1 -0
  20. package/dist/tests/episodes.test.d.ts +5 -0
  21. package/dist/tests/episodes.test.d.ts.map +1 -0
  22. package/dist/tests/episodes.test.js +254 -0
  23. package/dist/tests/episodes.test.js.map +1 -0
  24. package/dist/tests/helpers/db.d.ts +7 -0
  25. package/dist/tests/helpers/db.d.ts.map +1 -1
  26. package/dist/tests/helpers/db.js +20 -1
  27. package/dist/tests/helpers/db.js.map +1 -1
  28. package/hooks/lib/mindlore-common.cjs +119 -0
  29. package/hooks/mindlore-post-read.cjs +11 -2
  30. package/hooks/mindlore-read-guard.cjs +12 -4
  31. package/hooks/mindlore-search.cjs +36 -0
  32. package/hooks/mindlore-session-end.cjs +105 -22
  33. package/hooks/mindlore-session-focus.cjs +28 -1
  34. package/package.json +1 -1
  35. package/skills/mindlore-log/SKILL.md +74 -14
  36. package/skills/mindlore-query/SKILL.md +8 -5
  37. package/templates/config.json +4 -1
@@ -12,7 +12,7 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
  const { execSync } = require('child_process');
15
- const { findMindloreDir, globalDir, getProjectName } = require('./lib/mindlore-common.cjs');
15
+ const { findMindloreDir, globalDir, getProjectName, openDatabase, ensureEpisodesTable, hasEpisodesTable, insertBareEpisode, insertFtsRow } = require('./lib/mindlore-common.cjs');
16
16
 
17
17
  function formatDate(date) {
18
18
  const y = date.getFullYear();
@@ -23,29 +23,33 @@ function formatDate(date) {
23
23
  return `${y}-${m}-${d}-${h}${min}`;
24
24
  }
25
25
 
26
- function getRecentGitChanges() {
26
+ /**
27
+ * Get recent commits and changed files in a single git call.
28
+ * Returns { commits: string[], changedFiles: string[] }
29
+ */
30
+ function getRecentGitInfo() {
27
31
  try {
28
- const raw = execSync('git diff --name-only HEAD~5..HEAD 2>/dev/null', {
32
+ // --name-only includes file names after each commit entry
33
+ const raw = execSync('git log --oneline -5 --name-only 2>/dev/null', {
29
34
  encoding: 'utf8',
30
35
  timeout: 5000,
31
36
  }).trim();
32
- if (!raw) return [];
33
- return raw.split('\n').filter(Boolean).slice(0, 20);
34
- } catch (_err) {
35
- return [];
36
- }
37
- }
37
+ if (!raw) return { commits: [], changedFiles: [] };
38
38
 
39
- function getRecentCommits() {
40
- try {
41
- const raw = execSync('git log --oneline -5 2>/dev/null', {
42
- encoding: 'utf8',
43
- timeout: 5000,
44
- }).trim();
45
- if (!raw) return [];
46
- return raw.split('\n').filter(Boolean);
39
+ const commits = [];
40
+ const fileSet = new Set();
41
+ for (const line of raw.split('\n')) {
42
+ if (!line) continue;
43
+ // Commit lines start with a short hash (7+ hex chars)
44
+ if (/^[0-9a-f]{7,}\s/.test(line)) {
45
+ commits.push(line);
46
+ } else {
47
+ fileSet.add(line);
48
+ }
49
+ }
50
+ return { commits, changedFiles: [...fileSet].slice(0, 20) };
47
51
  } catch (_err) {
48
- return [];
52
+ return { commits: [], changedFiles: [] };
49
53
  }
50
54
  }
51
55
 
@@ -55,7 +59,11 @@ function getSessionReads(baseDir) {
55
59
  try {
56
60
  const data = JSON.parse(fs.readFileSync(readsPath, 'utf8'));
57
61
  const count = Object.keys(data).length;
58
- const repeats = Object.values(data).filter((v) => v > 1).length;
62
+ const repeats = Object.values(data).filter((v) => {
63
+ if (typeof v === 'number') return v > 1;
64
+ if (v && typeof v === 'object') return (v.count || 0) > 1;
65
+ return false;
66
+ }).length;
59
67
  // Clean up session file
60
68
  fs.unlinkSync(readsPath);
61
69
  return { count, repeats };
@@ -80,9 +88,8 @@ function main() {
80
88
  // Don't overwrite existing delta (idempotent)
81
89
  if (fs.existsSync(deltaPath)) return;
82
90
 
83
- // Gather structured data
84
- const commits = getRecentCommits();
85
- const changedFiles = getRecentGitChanges();
91
+ // Gather structured data (single git call)
92
+ const { commits, changedFiles } = getRecentGitInfo();
86
93
  const reads = getSessionReads(baseDir);
87
94
 
88
95
  const project = getProjectName();
@@ -133,10 +140,86 @@ function main() {
133
140
  fs.appendFileSync(logPath, logEntry, 'utf8');
134
141
  }
135
142
 
143
+ // v0.4.0: Write bare episode to episodes table
144
+ writeBareEpisode(baseDir, project, commits, changedFiles, reads);
145
+
136
146
  // Git auto-commit + push for global ~/.mindlore/ only
137
147
  syncGlobalRepo();
138
148
  }
139
149
 
150
+ /**
151
+ * Write a bare session episode to the episodes table.
152
+ * Deterministic — no LLM needed. Captures commits, files, read stats.
153
+ */
154
+ function writeBareEpisode(baseDir, project, commits, changedFiles, reads) {
155
+ try {
156
+ const dbPath = path.join(baseDir, 'mindlore.db');
157
+ const db = openDatabase(dbPath);
158
+ if (!db) return;
159
+
160
+ if (!hasEpisodesTable(db)) {
161
+ ensureEpisodesTable(db);
162
+ }
163
+
164
+ const commitList = commits.length > 0 ? commits.join(', ') : 'no commits';
165
+ const fileCount = changedFiles.length;
166
+ const summary = `Session: ${commitList} (${fileCount} files)`;
167
+
168
+ const bodyParts = [];
169
+ if (commits.length > 0) {
170
+ bodyParts.push('## Commits\n' + commits.map(c => `- ${c}`).join('\n'));
171
+ }
172
+ if (changedFiles.length > 0) {
173
+ bodyParts.push('## Changed Files\n' + changedFiles.map(f => `- ${f}`).join('\n'));
174
+ }
175
+ if (reads) {
176
+ bodyParts.push(`## Read Stats\n- ${reads.count} files read, ${reads.repeats} repeated`);
177
+ }
178
+
179
+ const entities = changedFiles.slice(0, 10);
180
+ const body = bodyParts.join('\n\n') || null;
181
+ const truncatedSummary = summary.slice(0, 300);
182
+
183
+ // Atomic: episode + FTS5 mirror in single transaction
184
+ const writeBoth = db.transaction(() => {
185
+ const epId = insertBareEpisode(db, {
186
+ kind: 'session',
187
+ scope: 'project',
188
+ project: project,
189
+ summary: truncatedSummary,
190
+ body: body,
191
+ tags: 'session',
192
+ entities: entities.length > 0 ? entities : null,
193
+ source: 'hook',
194
+ });
195
+
196
+ // FTS5 mirror — episode searchable via mindlore-search hook
197
+ try {
198
+ insertFtsRow(db, {
199
+ path: `episodes/${epId}`,
200
+ slug: `ep-${epId}`,
201
+ description: truncatedSummary,
202
+ type: 'episode',
203
+ category: 'episodes',
204
+ title: truncatedSummary.slice(0, 100),
205
+ content: [truncatedSummary, body ?? ''].join('\n').trim(),
206
+ tags: 'session',
207
+ quality: null,
208
+ dateCaptured: new Date().toISOString().slice(0, 10),
209
+ project: project,
210
+ });
211
+ } catch (_ftsErr) {
212
+ // FTS5 mirror optional — don't break the transaction
213
+ }
214
+ });
215
+ writeBoth();
216
+
217
+ db.close();
218
+ } catch (_err) {
219
+ // Graceful fail — never break session end
220
+ }
221
+ }
222
+
140
223
  /**
141
224
  * Auto-commit and push ~/.mindlore/ if it has a .git directory.
142
225
  * Only runs for the global scope — project .mindlore/ is in the project's own git.
@@ -10,7 +10,7 @@
10
10
 
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
- const { findMindloreDir, readConfig } = require('./lib/mindlore-common.cjs');
13
+ const { findMindloreDir, readConfig, openDatabase, hasEpisodesTable, queryRecentEpisodes } = require('./lib/mindlore-common.cjs');
14
14
 
15
15
  function main() {
16
16
  const baseDir = findMindloreDir();
@@ -63,6 +63,33 @@ function main() {
63
63
  }
64
64
  } catch (_err) { /* skip */ }
65
65
 
66
+ // v0.4.0: Inject recent episodes
67
+ try {
68
+ const dbPath = path.join(baseDir, 'mindlore.db');
69
+ const db = openDatabase(dbPath, { readonly: true });
70
+ if (db) {
71
+ try {
72
+ if (hasEpisodesTable(db)) {
73
+ const config = readConfig(baseDir);
74
+ const maxEpisodes = config?.session_focus?.max_episodes ?? 3;
75
+ const project = path.basename(process.cwd());
76
+ const episodes = queryRecentEpisodes(db, { project, limit: maxEpisodes });
77
+
78
+ if (episodes.length > 0) {
79
+ const lines = episodes.map(ep => {
80
+ const date = (ep.created_at || '').slice(0, 10);
81
+ const summary = String(ep.summary || '').slice(0, 100);
82
+ return `- [${date}] ${ep.kind}: ${summary}`;
83
+ });
84
+ output.push(`[Mindlore Episodes]\n${lines.join('\n')}`);
85
+ }
86
+ }
87
+ } finally {
88
+ db.close();
89
+ }
90
+ }
91
+ } catch (_err) { /* graceful skip */ }
92
+
66
93
  if (output.length > 0) {
67
94
  process.stdout.write(output.join('\n\n') + '\n');
68
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindlore",
3
- "version": "0.3.5",
3
+ "version": "0.4.0",
4
4
  "description": "AI-native knowledge system for Claude Code",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -12,7 +12,7 @@ Determine target using `getActiveMindloreDir()` logic:
12
12
 
13
13
  ## Trigger
14
14
 
15
- `/mindlore-log <mode>` where mode is `log`, `reflect`, `status`, or `save`.
15
+ `/mindlore-log <mode>` where mode is `log`, `diary`, `reflect`, `status`, or `save`.
16
16
 
17
17
  ## Modes
18
18
 
@@ -36,30 +36,90 @@ date: 2026-04-11
36
36
  4. Body: user's note as-is
37
37
  5. Append to `log.md`: `| {date} | log | {slug}.md |`
38
38
 
39
+ ### diary
40
+
41
+ LLM-driven session analysis → enriched episodes in the episodes table.
42
+
43
+ **Trigger:** User runs `/mindlore-log diary` or Stop hook asks "Diary analizi yapayım mı?"
44
+
45
+ **Model:** `[mindlore:diary]` marker → sonnet (analysis needed)
46
+
47
+ **Flow:**
48
+ 1. Open `~/.mindlore/mindlore.db`, ensure episodes table exists
49
+ 2. Find the latest bare session episode for current project: `WHERE kind = 'session' AND project = ? AND source = 'hook' ORDER BY created_at DESC LIMIT 1`
50
+ 3. Gather context:
51
+ - The bare episode's body (commits, files, read stats)
52
+ - Git log last 10 commits
53
+ - Decision-detector captures (if any in session)
54
+ 4. LLM analyzes and extracts structured episodes:
55
+ - **Decisions** → `kind: 'decision'` — architectural/tool/format choices
56
+ - **Discoveries** → `kind: 'discovery'` — assumption vs reality findings
57
+ - **Frictions** → `kind: 'friction'` — tool errors, blockers, recurring issues
58
+ - **Learnings** → `kind: 'learning'` — reusable knowledge
59
+ - **Preferences** → `kind: 'preference'` — user behavioral preferences
60
+ - **Events** → `kind: 'event'` — releases, incidents, milestones
61
+ 5. **Deduplication rule:** Each finding belongs to exactly ONE kind. Priority: `decision > discovery > friction > learning > preference > event`. Never write the same finding to multiple kinds.
62
+ 6. Present to user, get approval
63
+ 7. Write approved episodes to DB:
64
+ - `source: 'diary'`
65
+ - `parent_id: {bare_session_episode_id}` — links enriched episodes to source session
66
+ - `scope: 'project'` (default) or `'global'` if cross-project
67
+ 8. Optionally mirror to FTS5 for text search
68
+ 9. Append to `log.md`: `| {date} | diary | {N} episodes extracted from session |`
69
+
70
+ **Rules:**
71
+ - NEVER write episodes without user approval
72
+ - parent_id always points to the source session episode
73
+ - Each episode gets its own summary (max 100 chars) and body (markdown, unbounded)
74
+ - entities field: JSON array of relevant file paths (max 10)
75
+
39
76
  ### reflect
40
77
 
41
- LLM-driven pattern extraction from diary deltas → persistent learnings.
78
+ LLM-driven pattern extraction from episodes → persistent learnings.
42
79
 
43
- **Flow (v0.3LLM-driven):**
44
- 1. Read last N non-archived delta files in `diary/` (no `archived: true` frontmatter)
45
- 2. Present summary: "Found N unprocessed deltas spanning DATE1 to DATE2"
46
- 3. LLM analyzes deltas for repeating patterns:
47
- - Recurring topics/themes across sessions
48
- - Repeated decisions (same choice made multiple times)
49
- - Common mistakes or friction points
80
+ **Flow (v0.4episodes-powered):**
81
+ 1. Read active episodes: `WHERE status = 'active' AND source IN ('hook', 'diary')`
82
+ 2. Optionally filter by time: `--days 7` (default 7), `--days 30`
83
+ 3. Present summary: "Found N episodes spanning DATE1 to DATE2"
84
+ 4. LLM analyzes episodes (not deltas) for patterns:
85
+ - Repeated decisions (same choice 2+ times)
86
+ - Recurring frictions (same blocker/error)
87
+ - Discovery patterns (assumptions that keep breaking)
50
88
  - Workflow patterns that worked well
51
- 4. Categorize found patterns by topic (git, testing, architecture, etc.)
52
- 5. Present findings to user with proposed learnings
53
- 6. User approves → write to `learnings/{topic}.md` (append if exists, create if not)
89
+ 5. **Structured report output:**
90
+
91
+ ```
92
+ ── Reflect Raporu (son {days} gün, {N} episode) ──
93
+
94
+ Friction ({count}):
95
+ - {summary} — {repeat_count}x tekrar
96
+
97
+ Discoveries ({count}):
98
+ - {summary}
99
+
100
+ Decisions ({count}):
101
+ - {summary}
102
+
103
+ Patterns:
104
+ - "{pattern_description}" → kural adayı
105
+
106
+ Önerilen:
107
+ [ ] {rule} ({repeat_count}x, {confidence} confidence)
108
+ ```
109
+
110
+ 6. User approves → write to `learnings/{topic}.md`
54
111
  7. Format: `YAPMA:` / `BEST PRACTICE:` / `KRITIK:` prefixed rules
55
112
  8. Update relevant domain page if pattern relates to an existing domain
56
- 9. Mark processed deltas: add `archived: true` to their frontmatter
57
- 10. Append to `log.md`: `| {date} | reflect | {N} deltas processed, {M} learnings written |`
113
+ 9. Mark processed episodes: future reflect skips already-processed timeranges
114
+ 10. Append to `log.md`: `| {date} | reflect | {N} episodes processed, {M} learnings written |`
115
+
116
+ **Fallback:** Also reads non-archived delta files if episodes table is empty (backward compat with v0.3 deltas).
58
117
 
59
118
  **Rules:**
60
119
  - NEVER write learnings without user approval
61
120
  - Group related patterns into existing topic files (don't create one file per pattern)
62
121
  - Reflect scans both project + global diary/ in `--all` mode
122
+ - Deduplication: same pattern found in both episodes and deltas → episodes win
63
123
 
64
124
  ### status
65
125
 
@@ -18,14 +18,17 @@ Determine search scope using `getActiveMindloreDir()` / `getAllDbs()` logic:
18
18
 
19
19
  ### search
20
20
 
21
- FTS5 keyword search with direct results.
21
+ FTS5 keyword search + episodes recall with unified results.
22
22
 
23
23
  **Flow:**
24
24
  1. Parse user query into keywords (strip stop words)
25
- 2. Run FTS5 MATCH on `mindlore_fts` table
26
- 3. Return top 5 results (configurable) with: path, title, description, category, rank score
27
- 4. Display as table with snippet preview
28
- 5. If `--tags <tag>` flag provided: `WHERE tags MATCH '<tag>'` filter
25
+ 2. Run FTS5 MATCH on `mindlore_fts` table (knowledge: "ne biliyorum")
26
+ 3. Run LIKE search on `episodes` table (memory: "ne oldu, ne karar aldım")
27
+ 4. Merge results: FTS5 results first, then matching episodes
28
+ 5. Return top 5 knowledge results + top 3 episode results
29
+ 6. Display knowledge as table with snippet preview, episodes as timeline
30
+ 7. If `--tags <tag>` flag provided: `WHERE tags MATCH '<tag>'` filter (FTS5 only)
31
+ 8. If `--episodes-only` flag: skip FTS5, show only episode matches
29
32
 
30
33
  **Output format:**
31
34
  ```
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.3.5",
2
+ "version": "0.4.0",
3
3
  "models": {
4
4
  "ingest": "haiku",
5
5
  "evolve": "sonnet",
@@ -8,5 +8,8 @@
8
8
  },
9
9
  "reflect": {
10
10
  "threshold": 5
11
+ },
12
+ "session_focus": {
13
+ "max_episodes": 3
11
14
  }
12
15
  }