mindlore 0.7.0 → 0.7.2
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 +30 -3
- package/dist/scripts/bundle-hooks.d.ts +2 -0
- package/dist/scripts/bundle-hooks.d.ts.map +1 -0
- package/dist/scripts/bundle-hooks.js +70 -0
- package/dist/scripts/bundle-hooks.js.map +1 -0
- package/dist/scripts/init.js +0 -3
- package/dist/scripts/init.js.map +1 -1
- package/dist/scripts/lib/all-migrations.d.ts.map +1 -1
- package/dist/scripts/lib/all-migrations.js +3 -0
- package/dist/scripts/lib/all-migrations.js.map +1 -1
- package/dist/scripts/lib/constants.d.ts +7 -2
- package/dist/scripts/lib/constants.d.ts.map +1 -1
- package/dist/scripts/lib/constants.js +17 -22
- package/dist/scripts/lib/constants.js.map +1 -1
- package/dist/scripts/lib/mcp-tools.d.ts.map +1 -1
- package/dist/scripts/lib/mcp-tools.js +63 -78
- package/dist/scripts/lib/mcp-tools.js.map +1 -1
- package/dist/scripts/lib/migrations-v072.d.ts +3 -0
- package/dist/scripts/lib/migrations-v072.d.ts.map +1 -0
- package/dist/scripts/lib/migrations-v072.js +25 -0
- package/dist/scripts/lib/migrations-v072.js.map +1 -0
- package/dist/scripts/lib/relation-helpers.d.ts +15 -0
- package/dist/scripts/lib/relation-helpers.d.ts.map +1 -0
- package/dist/scripts/lib/relation-helpers.js +30 -0
- package/dist/scripts/lib/relation-helpers.js.map +1 -0
- package/dist/scripts/lib/tool-adapters/get-adapter.d.ts +21 -0
- package/dist/scripts/lib/tool-adapters/get-adapter.d.ts.map +1 -0
- package/dist/scripts/lib/tool-adapters/get-adapter.js +51 -0
- package/dist/scripts/lib/tool-adapters/get-adapter.js.map +1 -0
- package/dist/scripts/lib/tool-adapters/relate-adapter.d.ts +34 -0
- package/dist/scripts/lib/tool-adapters/relate-adapter.d.ts.map +1 -0
- package/dist/scripts/lib/tool-adapters/relate-adapter.js +43 -0
- package/dist/scripts/lib/tool-adapters/relate-adapter.js.map +1 -0
- package/dist/scripts/lib/tool-adapters/search-adapter.d.ts +5 -0
- package/dist/scripts/lib/tool-adapters/search-adapter.d.ts.map +1 -1
- package/dist/scripts/lib/tool-adapters/search-adapter.js +37 -0
- package/dist/scripts/lib/tool-adapters/search-adapter.js.map +1 -1
- package/dist/scripts/mcp-server.js +1 -1
- package/dist/scripts/mcp-server.js.map +1 -1
- package/dist/tests/dont-repeat-dedup.test.d.ts +2 -0
- package/dist/tests/dont-repeat-dedup.test.d.ts.map +1 -0
- package/dist/tests/dont-repeat-dedup.test.js +93 -0
- package/dist/tests/dont-repeat-dedup.test.js.map +1 -0
- package/dist/tests/e2e-kg-pipeline.test.d.ts +2 -0
- package/dist/tests/e2e-kg-pipeline.test.d.ts.map +1 -0
- package/dist/tests/e2e-kg-pipeline.test.js +59 -0
- package/dist/tests/e2e-kg-pipeline.test.js.map +1 -0
- package/dist/tests/helpers/db.d.ts.map +1 -1
- package/dist/tests/helpers/db.js +2 -1
- package/dist/tests/helpers/db.js.map +1 -1
- package/dist/tests/hook-smoke.test.js +1 -1
- package/dist/tests/hook-smoke.test.js.map +1 -1
- package/dist/tests/mcp-get-tool.test.d.ts +2 -0
- package/dist/tests/mcp-get-tool.test.d.ts.map +1 -0
- package/dist/tests/mcp-get-tool.test.js +93 -0
- package/dist/tests/mcp-get-tool.test.js.map +1 -0
- package/dist/tests/mcp-relate-tool.test.d.ts +2 -0
- package/dist/tests/mcp-relate-tool.test.d.ts.map +1 -0
- package/dist/tests/mcp-relate-tool.test.js +85 -0
- package/dist/tests/mcp-relate-tool.test.js.map +1 -0
- package/dist/tests/mcp-server.test.js +3 -1
- package/dist/tests/mcp-server.test.js.map +1 -1
- package/dist/tests/mcp-tools.test.js +20 -0
- package/dist/tests/mcp-tools.test.js.map +1 -1
- package/dist/tests/memory-relate.test.d.ts +2 -0
- package/dist/tests/memory-relate.test.d.ts.map +1 -0
- package/dist/tests/memory-relate.test.js +70 -0
- package/dist/tests/memory-relate.test.js.map +1 -0
- package/dist/tests/migrations-v063.test.js +1 -1
- package/dist/tests/migrations-v072.test.d.ts +2 -0
- package/dist/tests/migrations-v072.test.d.ts.map +1 -0
- package/dist/tests/migrations-v072.test.js +74 -0
- package/dist/tests/migrations-v072.test.js.map +1 -0
- package/dist/tests/plugin-cache-regression.test.d.ts +2 -0
- package/dist/tests/plugin-cache-regression.test.d.ts.map +1 -0
- package/dist/tests/plugin-cache-regression.test.js +19 -0
- package/dist/tests/plugin-cache-regression.test.js.map +1 -0
- package/dist/tests/search-hook.test.js +1 -1
- package/dist/tests/search-hook.test.js.map +1 -1
- package/hooks/cc-memory-bulk-sync.cjs +606 -0
- package/hooks/cc-session-sync.cjs +856 -0
- package/hooks/hooks.json +149 -0
- package/hooks/lib/mindlore-common.cjs +2 -2
- package/hooks/lib/secure-io.cjs +17 -0
- package/hooks/mindlore-cwd-changed.cjs +19 -34
- package/hooks/mindlore-decision-detector.cjs +40 -31
- package/hooks/mindlore-dont-repeat.cjs +75 -115
- package/hooks/mindlore-fts5-sync.cjs +15 -44
- package/hooks/mindlore-index.cjs +100 -101
- package/hooks/mindlore-model-router.cjs +20 -32
- package/hooks/mindlore-post-compact.cjs +26 -42
- package/hooks/mindlore-post-read.cjs +35 -60
- package/hooks/mindlore-pre-compact.cjs +55 -73
- package/hooks/mindlore-read-guard.cjs +28 -51
- package/hooks/mindlore-research-guard.cjs +63 -101
- package/hooks/mindlore-search.cjs +1156 -93
- package/hooks/mindlore-session-end.cjs +155 -276
- package/hooks/mindlore-session-focus.cjs +672 -110
- package/hooks/src/lib/constants.cjs +15 -0
- package/hooks/src/lib/mindlore-common.cjs +975 -0
- package/hooks/src/lib/mindlore-common.d.cts +72 -0
- package/hooks/src/lib/secure-io.cjs +17 -0
- package/hooks/src/lib/types.d.ts +58 -0
- package/hooks/src/mindlore-cwd-changed.cjs +57 -0
- package/hooks/src/mindlore-decision-detector.cjs +54 -0
- package/hooks/src/mindlore-dont-repeat.cjs +243 -0
- package/hooks/src/mindlore-fts5-sync.cjs +98 -0
- package/hooks/src/mindlore-index.cjs +230 -0
- package/hooks/src/mindlore-model-router.cjs +54 -0
- package/hooks/src/mindlore-post-compact.cjs +69 -0
- package/hooks/src/mindlore-post-read.cjs +106 -0
- package/hooks/src/mindlore-pre-compact.cjs +154 -0
- package/hooks/src/mindlore-read-guard.cjs +105 -0
- package/hooks/src/mindlore-research-guard.cjs +176 -0
- package/hooks/src/mindlore-search.cjs +200 -0
- package/hooks/src/mindlore-session-end.cjs +511 -0
- package/hooks/src/mindlore-session-focus.cjs +256 -0
- package/package.json +8 -3
- package/plugin.json +5 -4
- package/templates/config.json +1 -1
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* mindlore-session-end — SessionEnd hook
|
|
6
|
+
*
|
|
7
|
+
* Writes a basic delta file to diary/ with session timestamp.
|
|
8
|
+
* v0.1: minimal delta (timestamp + marker)
|
|
9
|
+
* v0.2: structured delta with stats, decisions, learnings
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const { execFileSync, spawn } = require('child_process');
|
|
16
|
+
const { safeWriteFile, safeWriteJson } = require('../dist/scripts/lib/secure-io.js');
|
|
17
|
+
const { findMindloreDir, globalDir, getProjectName, openDatabase, ensureEpisodesTable, hasEpisodesTable, insertBareEpisode, insertFtsRow, hookLog, SHARED_EXPORT_DIRS, resolveWin32Bin, withTelemetry, getUnpromotedRawFiles, cleanupExpiredInjectLog } = require('./lib/mindlore-common.cjs');
|
|
18
|
+
|
|
19
|
+
const EXPORT_DIRS = SHARED_EXPORT_DIRS;
|
|
20
|
+
|
|
21
|
+
// --worker mode: heavy ops run in detached child process (survives parent exit)
|
|
22
|
+
if (process.argv.includes('--worker')) {
|
|
23
|
+
hookLog('session-end', 'info', 'worker started, pid=' + process.pid);
|
|
24
|
+
const dataPath = process.argv[process.argv.indexOf('--worker') + 1];
|
|
25
|
+
let payload;
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(dataPath, 'utf8');
|
|
28
|
+
fs.unlinkSync(dataPath);
|
|
29
|
+
payload = JSON.parse(raw);
|
|
30
|
+
} catch (_err) {
|
|
31
|
+
hookLog('session-end', 'error', 'payload read failed: ' + (_err?.message ?? _err));
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
const { baseDir, project, commits, changedFiles, reads } = payload;
|
|
35
|
+
|
|
36
|
+
async function safeRunAsync(fn, label) {
|
|
37
|
+
try {
|
|
38
|
+
await fn();
|
|
39
|
+
hookLog('session-end', 'info', label + ' OK');
|
|
40
|
+
} catch (e) {
|
|
41
|
+
hookLog('session-end', 'error', label + ' FAIL: ' + e?.message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
(async () => {
|
|
46
|
+
// Episode writes share DB — run sequentially first
|
|
47
|
+
await safeRunAsync(() => writeBareEpisode(baseDir, project, commits, changedFiles, reads), 'episode');
|
|
48
|
+
await safeRunAsync(() => writeEpisodeFile(baseDir, project, commits, changedFiles, reads), 'episode-file');
|
|
49
|
+
|
|
50
|
+
const nodeExe = resolveWin32Bin('node') || process.execPath;
|
|
51
|
+
function runSyncScript(scriptName, args, timeoutMs, label) {
|
|
52
|
+
const cjsName = scriptName.replace(/\.js$/, '.cjs');
|
|
53
|
+
const scriptPath = [path.join(__dirname, cjsName), path.join(__dirname, '..', 'dist', 'scripts', scriptName)].find(p => fs.existsSync(p));
|
|
54
|
+
if (!scriptPath) return;
|
|
55
|
+
try {
|
|
56
|
+
execFileSync(nodeExe, [scriptPath, ...args], {
|
|
57
|
+
timeout: timeoutMs,
|
|
58
|
+
env: { ...process.env, MINDLORE_HOME: baseDir },
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
});
|
|
61
|
+
hookLog('session-end', 'info', label + ' completed');
|
|
62
|
+
} catch (err) {
|
|
63
|
+
hookLog('session-end', 'warn', `${label} failed: ${err?.message || err}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await safeRunAsync(() => runSyncScript('cc-memory-bulk-sync.js', ['--auto'], 10000, 'CC memory sync'), 'cc-memory-sync');
|
|
68
|
+
await safeRunAsync(() => runSyncScript('cc-session-sync.js', [], 30000, 'CC session sync'), 'cc-session-sync');
|
|
69
|
+
|
|
70
|
+
// Raw accumulation warning (moved from main to worker — off hot path)
|
|
71
|
+
await safeRunAsync(() => {
|
|
72
|
+
const unpromoted = getUnpromotedRawFiles(baseDir);
|
|
73
|
+
if (unpromoted.length >= 5) {
|
|
74
|
+
hookLog('session-end', 'info', `${unpromoted.length} raw files unpromoted`);
|
|
75
|
+
}
|
|
76
|
+
}, 'raw-check');
|
|
77
|
+
|
|
78
|
+
// Obsidian + git-sync are independent — run in parallel
|
|
79
|
+
await Promise.allSettled([
|
|
80
|
+
safeRunAsync(() => syncObsidian(baseDir), 'obsidian'),
|
|
81
|
+
safeRunAsync(() => syncGlobalRepo(), 'git-sync'),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
hookLog('session-end', 'info', 'worker done');
|
|
85
|
+
process.exit(0);
|
|
86
|
+
})();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatDate(date) {
|
|
90
|
+
const y = date.getFullYear();
|
|
91
|
+
const m = String(date.getMonth() + 1).padStart(2, '0');
|
|
92
|
+
const d = String(date.getDate()).padStart(2, '0');
|
|
93
|
+
const h = String(date.getHours()).padStart(2, '0');
|
|
94
|
+
const min = String(date.getMinutes()).padStart(2, '0');
|
|
95
|
+
return `${y}-${m}-${d}-${h}${min}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get recent commits and changed files in a single git call.
|
|
100
|
+
* Returns { commits: string[], changedFiles: string[] }
|
|
101
|
+
*/
|
|
102
|
+
function getRecentGitInfo() {
|
|
103
|
+
try {
|
|
104
|
+
// --name-only includes file names after each commit entry
|
|
105
|
+
const raw = execFileSync('git', ['log', '--oneline', '-5', '--name-only'], {
|
|
106
|
+
encoding: 'utf8',
|
|
107
|
+
timeout: 5000,
|
|
108
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
109
|
+
windowsHide: true,
|
|
110
|
+
}).trim();
|
|
111
|
+
if (!raw) return { commits: [], changedFiles: [] };
|
|
112
|
+
|
|
113
|
+
const commits = [];
|
|
114
|
+
const fileSet = new Set();
|
|
115
|
+
for (const line of raw.split('\n')) {
|
|
116
|
+
if (!line) continue;
|
|
117
|
+
// Commit lines start with a short hash (7+ hex chars)
|
|
118
|
+
if (/^[0-9a-f]{7,}\s/.test(line)) {
|
|
119
|
+
commits.push(line);
|
|
120
|
+
} else {
|
|
121
|
+
fileSet.add(line);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { commits, changedFiles: [...fileSet].slice(0, 20) };
|
|
125
|
+
} catch (_err) {
|
|
126
|
+
return { commits: [], changedFiles: [] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getSessionReads(baseDir) {
|
|
131
|
+
const readsPath = path.join(baseDir, 'diary', `_session-reads-${getProjectName()}.json`);
|
|
132
|
+
if (!fs.existsSync(readsPath)) return null;
|
|
133
|
+
try {
|
|
134
|
+
const data = JSON.parse(fs.readFileSync(readsPath, 'utf8'));
|
|
135
|
+
const count = Object.keys(data).length;
|
|
136
|
+
const repeats = Object.values(data).filter((v) => {
|
|
137
|
+
if (typeof v === 'number') return v > 1;
|
|
138
|
+
if (v && typeof v === 'object') return (v.count || 0) > 1;
|
|
139
|
+
return false;
|
|
140
|
+
}).length;
|
|
141
|
+
// Clean up session file
|
|
142
|
+
fs.unlinkSync(readsPath);
|
|
143
|
+
return { count, repeats };
|
|
144
|
+
} catch (_err) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function main() {
|
|
150
|
+
const baseDir = findMindloreDir();
|
|
151
|
+
if (!baseDir) return;
|
|
152
|
+
|
|
153
|
+
const diaryDir = path.join(baseDir, 'diary');
|
|
154
|
+
if (!fs.existsSync(diaryDir)) {
|
|
155
|
+
fs.mkdirSync(diaryDir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const now = new Date();
|
|
159
|
+
const dateStr = formatDate(now);
|
|
160
|
+
const deltaPath = path.join(diaryDir, `delta-${dateStr}.md`);
|
|
161
|
+
|
|
162
|
+
// Don't overwrite existing delta (idempotent)
|
|
163
|
+
if (fs.existsSync(deltaPath)) return;
|
|
164
|
+
|
|
165
|
+
// Gather structured data (single git call)
|
|
166
|
+
const { commits, changedFiles } = getRecentGitInfo();
|
|
167
|
+
const reads = getSessionReads(baseDir);
|
|
168
|
+
|
|
169
|
+
const project = getProjectName();
|
|
170
|
+
|
|
171
|
+
const sections = [
|
|
172
|
+
'---',
|
|
173
|
+
`slug: delta-${dateStr}`,
|
|
174
|
+
'type: diary',
|
|
175
|
+
`date: ${now.toISOString().slice(0, 10)}`,
|
|
176
|
+
`project: ${project}`,
|
|
177
|
+
'---',
|
|
178
|
+
'',
|
|
179
|
+
`# Session Delta — ${dateStr}`,
|
|
180
|
+
'',
|
|
181
|
+
`Session ended: ${now.toISOString()}`,
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
// Commits section
|
|
185
|
+
sections.push('', '## Commits');
|
|
186
|
+
if (commits.length > 0) {
|
|
187
|
+
for (const c of commits) sections.push(`- ${c}`);
|
|
188
|
+
} else {
|
|
189
|
+
sections.push('- _(no commits)_');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Changed files section
|
|
193
|
+
sections.push('', '## Changed Files');
|
|
194
|
+
if (changedFiles.length > 0) {
|
|
195
|
+
for (const f of changedFiles) sections.push(`- ${f}`);
|
|
196
|
+
} else {
|
|
197
|
+
sections.push('- _(no file changes)_');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Read stats (from read-guard, if active)
|
|
201
|
+
if (reads) {
|
|
202
|
+
sections.push('', '## Read Stats');
|
|
203
|
+
sections.push(`- ${reads.count} files read, ${reads.repeats} repeated reads`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
sections.push('');
|
|
207
|
+
|
|
208
|
+
safeWriteFile(deltaPath, sections.join('\n'));
|
|
209
|
+
|
|
210
|
+
// Append to log.md
|
|
211
|
+
const logPath = path.join(baseDir, 'log.md');
|
|
212
|
+
if (fs.existsSync(logPath)) {
|
|
213
|
+
const logEntry = `| ${now.toISOString().slice(0, 10)} | session-end | delta-${dateStr}.md |\n`;
|
|
214
|
+
fs.appendFileSync(logPath, logEntry, 'utf8');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Heavy ops: detach into child process so CC can exit immediately.
|
|
218
|
+
// Fixes "Hook cancelled" when CC kills the hook before completion.
|
|
219
|
+
// See: https://github.com/anthropics/claude-code/issues/41577
|
|
220
|
+
try {
|
|
221
|
+
const workerData = JSON.stringify({ baseDir, project, commits, changedFiles, reads });
|
|
222
|
+
const tmpFile = path.join(os.tmpdir(), `mindlore-worker-${Date.now()}.json`);
|
|
223
|
+
safeWriteFile(tmpFile, workerData);
|
|
224
|
+
// Use system node instead of process.execPath — CC's embedded Node
|
|
225
|
+
// may not work as a standalone binary for detached worker processes.
|
|
226
|
+
// Resolve full path to avoid shell:true deprecation warning on Windows.
|
|
227
|
+
const nodeBin = resolveWin32Bin('node');
|
|
228
|
+
const child = spawn(nodeBin, [__filename, '--worker', tmpFile], {
|
|
229
|
+
detached: true,
|
|
230
|
+
stdio: 'ignore',
|
|
231
|
+
cwd: process.cwd(),
|
|
232
|
+
windowsHide: true,
|
|
233
|
+
});
|
|
234
|
+
child.unref();
|
|
235
|
+
} catch (_err) {
|
|
236
|
+
// Fallback: run inline if spawn fails
|
|
237
|
+
writeBareEpisode(baseDir, project, commits, changedFiles, reads);
|
|
238
|
+
writeEpisodeFile(baseDir, project, commits, changedFiles, reads);
|
|
239
|
+
syncObsidian(baseDir);
|
|
240
|
+
syncGlobalRepo();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Write a bare session episode to the episodes table.
|
|
246
|
+
* Deterministic — no LLM needed. Captures commits, files, read stats.
|
|
247
|
+
*/
|
|
248
|
+
function writeBareEpisode(baseDir, project, commits, changedFiles, reads) {
|
|
249
|
+
try {
|
|
250
|
+
const dbPath = path.join(baseDir, 'mindlore.db');
|
|
251
|
+
const db = openDatabase(dbPath);
|
|
252
|
+
if (!db) return;
|
|
253
|
+
|
|
254
|
+
if (!hasEpisodesTable(db)) {
|
|
255
|
+
ensureEpisodesTable(db);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const commitList = commits.length > 0 ? commits.join(', ') : 'no commits';
|
|
259
|
+
const fileCount = changedFiles.length;
|
|
260
|
+
const summary = `Session: ${commitList} (${fileCount} files)`;
|
|
261
|
+
|
|
262
|
+
const bodyParts = [];
|
|
263
|
+
if (commits.length > 0) {
|
|
264
|
+
bodyParts.push('## Commits\n' + commits.map(c => `- ${c}`).join('\n'));
|
|
265
|
+
}
|
|
266
|
+
if (changedFiles.length > 0) {
|
|
267
|
+
bodyParts.push('## Changed Files\n' + changedFiles.map(f => `- ${f}`).join('\n'));
|
|
268
|
+
}
|
|
269
|
+
if (reads) {
|
|
270
|
+
bodyParts.push(`## Read Stats\n- ${reads.count} files read, ${reads.repeats} repeated`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const entities = changedFiles.slice(0, 10);
|
|
274
|
+
const body = bodyParts.join('\n\n') || null;
|
|
275
|
+
const truncatedSummary = summary.slice(0, 300);
|
|
276
|
+
|
|
277
|
+
// Atomic: episode + FTS5 mirror in single transaction
|
|
278
|
+
const writeBoth = db.transaction(() => {
|
|
279
|
+
const epId = insertBareEpisode(db, {
|
|
280
|
+
kind: 'session',
|
|
281
|
+
scope: 'project',
|
|
282
|
+
project: project,
|
|
283
|
+
summary: truncatedSummary,
|
|
284
|
+
body: body,
|
|
285
|
+
tags: 'session',
|
|
286
|
+
entities: entities.length > 0 ? entities : null,
|
|
287
|
+
source: 'hook',
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// FTS5 mirror — episode searchable via mindlore-search hook
|
|
291
|
+
try {
|
|
292
|
+
insertFtsRow(db, {
|
|
293
|
+
path: `episodes/${epId}`,
|
|
294
|
+
slug: `ep-${epId}`,
|
|
295
|
+
description: truncatedSummary,
|
|
296
|
+
type: 'episode',
|
|
297
|
+
category: 'episodes',
|
|
298
|
+
title: truncatedSummary,
|
|
299
|
+
content: [truncatedSummary, body ?? ''].join('\n').trim(),
|
|
300
|
+
tags: 'session',
|
|
301
|
+
quality: null,
|
|
302
|
+
dateCaptured: new Date().toISOString().slice(0, 10),
|
|
303
|
+
project: project,
|
|
304
|
+
});
|
|
305
|
+
} catch (_ftsErr) {
|
|
306
|
+
// FTS5 mirror optional — don't break the transaction
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
writeBoth();
|
|
310
|
+
|
|
311
|
+
// TTL cleanup for episode_inject_log (R4)
|
|
312
|
+
try { cleanupExpiredInjectLog(db); } catch (_err) { /* cleanup is optional */ }
|
|
313
|
+
|
|
314
|
+
db.close();
|
|
315
|
+
} catch (err) {
|
|
316
|
+
hookLog('session-end', 'error', `episode write failed: ${err?.message ?? err}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Write episode as .md file to diary/{project}/ for human-readable browsing.
|
|
322
|
+
* Complements the DB episode — same content, different medium.
|
|
323
|
+
*/
|
|
324
|
+
function writeEpisodeFile(baseDir, project, commits, changedFiles, reads) {
|
|
325
|
+
const projDir = path.join(baseDir, 'diary', project || 'unknown');
|
|
326
|
+
if (!fs.existsSync(projDir)) fs.mkdirSync(projDir, { recursive: true });
|
|
327
|
+
|
|
328
|
+
const now = process.env.MINDLORE_EPISODE_TS ? new Date(process.env.MINDLORE_EPISODE_TS) : new Date();
|
|
329
|
+
const ts = formatDate(now);
|
|
330
|
+
const filePath = path.join(projDir, `episode-${ts}.md`);
|
|
331
|
+
if (fs.existsSync(filePath)) return; // idempotent
|
|
332
|
+
|
|
333
|
+
const lines = [
|
|
334
|
+
'---',
|
|
335
|
+
`slug: episode-${ts}`,
|
|
336
|
+
'type: episode',
|
|
337
|
+
`date: ${now.toISOString().slice(0, 10)}`,
|
|
338
|
+
`project: ${project || 'unknown'}`,
|
|
339
|
+
'---',
|
|
340
|
+
'',
|
|
341
|
+
`# Episode — ${ts}`,
|
|
342
|
+
'',
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
if (commits.length > 0) {
|
|
346
|
+
lines.push('## Commits');
|
|
347
|
+
for (const c of commits) lines.push(`- ${c}`);
|
|
348
|
+
lines.push('');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (changedFiles.length > 0) {
|
|
352
|
+
lines.push('## Changed Files');
|
|
353
|
+
for (const f of changedFiles) lines.push(`- ${f}`);
|
|
354
|
+
lines.push('');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (reads) {
|
|
358
|
+
lines.push('## Read Stats');
|
|
359
|
+
lines.push(`- ${reads.count} files read, ${reads.repeats} repeated`);
|
|
360
|
+
lines.push('');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (commits.length === 0 && changedFiles.length === 0) {
|
|
364
|
+
lines.push('_Read-only session — no commits or file changes._');
|
|
365
|
+
lines.push('');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
safeWriteFile(filePath, lines.join('\n'));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let _obsidianHelpersCache = undefined; // undefined = not yet attempted
|
|
372
|
+
/**
|
|
373
|
+
* Load obsidian-helpers from compiled dist (single source of truth for wikilink conversion).
|
|
374
|
+
* Returns null if helpers not available (e.g. dev environment without build).
|
|
375
|
+
* Result is cached — require() runs at most once per process.
|
|
376
|
+
*/
|
|
377
|
+
function getObsidianHelpers() {
|
|
378
|
+
if (_obsidianHelpersCache !== undefined) return _obsidianHelpersCache;
|
|
379
|
+
try {
|
|
380
|
+
const hookDir = __dirname;
|
|
381
|
+
const pkgRoot = path.dirname(hookDir);
|
|
382
|
+
const helpersPath = path.join(pkgRoot, 'dist', 'scripts', 'lib', 'obsidian-helpers.js');
|
|
383
|
+
_obsidianHelpersCache = require(helpersPath);
|
|
384
|
+
return _obsidianHelpersCache;
|
|
385
|
+
} catch (err) {
|
|
386
|
+
if (process.env.MINDLORE_DEBUG === '1') {
|
|
387
|
+
process.stderr.write(`[mindlore] obsidian-helpers not available: ${err.message}\n`);
|
|
388
|
+
}
|
|
389
|
+
_obsidianHelpersCache = null;
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Export a single .md file to Obsidian vault with wikilink conversion.
|
|
396
|
+
* Uses obsidian-helpers.convertToWikilinks for consistent behavior.
|
|
397
|
+
* Returns true if file was exported.
|
|
398
|
+
*/
|
|
399
|
+
function exportMdFile(srcPath, destPath, convertFn) {
|
|
400
|
+
try {
|
|
401
|
+
const destStat = fs.statSync(destPath);
|
|
402
|
+
const srcStat = fs.statSync(srcPath);
|
|
403
|
+
if (srcStat.mtimeMs <= destStat.mtimeMs) return false;
|
|
404
|
+
} catch (_err) {
|
|
405
|
+
// dest doesn't exist — proceed with export
|
|
406
|
+
}
|
|
407
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
408
|
+
content = convertFn(content);
|
|
409
|
+
fs.writeFileSync(destPath, content, 'utf8');
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Auto-export .md files to Obsidian vault if configured.
|
|
415
|
+
* Skips if no vault configured, vault missing, or nothing changed since last export.
|
|
416
|
+
*/
|
|
417
|
+
function syncObsidian(baseDir) {
|
|
418
|
+
try {
|
|
419
|
+
const configPath = path.join(baseDir, 'config.json');
|
|
420
|
+
if (!fs.existsSync(configPath)) return;
|
|
421
|
+
|
|
422
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
423
|
+
const vaultPath = config?.obsidian?.vault;
|
|
424
|
+
if (!vaultPath || typeof vaultPath !== 'string') return;
|
|
425
|
+
if (!fs.existsSync(vaultPath)) return;
|
|
426
|
+
|
|
427
|
+
const helpers = getObsidianHelpers();
|
|
428
|
+
// Fallback regex if helpers unavailable (strips path prefixes like the canonical version)
|
|
429
|
+
const convertFn = helpers?.convertToWikilinks
|
|
430
|
+
?? ((c) => c.replace(/\[([^\]]+)\]\((?:\.\.?\/)?(?:[\w-]+\/)*([^/)]+)\.md\)/g, '[[$2]]'));
|
|
431
|
+
|
|
432
|
+
const destBase = path.join(vaultPath, 'mindlore');
|
|
433
|
+
let exported = 0;
|
|
434
|
+
|
|
435
|
+
function walkAndExport(srcDir, destDir) {
|
|
436
|
+
if (!fs.existsSync(srcDir)) return;
|
|
437
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
438
|
+
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
439
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
440
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
441
|
+
const destPath = path.join(destDir, entry.name);
|
|
442
|
+
if (entry.isDirectory()) {
|
|
443
|
+
walkAndExport(srcPath, destPath);
|
|
444
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
445
|
+
if (exportMdFile(srcPath, destPath, convertFn)) exported++;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (const dir of EXPORT_DIRS) {
|
|
451
|
+
walkAndExport(path.join(baseDir, dir), path.join(destBase, dir));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const rootFile of ['INDEX.md', 'log.md']) {
|
|
455
|
+
const srcPath = path.join(baseDir, rootFile);
|
|
456
|
+
if (!fs.existsSync(srcPath)) continue;
|
|
457
|
+
fs.mkdirSync(destBase, { recursive: true });
|
|
458
|
+
if (exportMdFile(srcPath, path.join(destBase, rootFile), convertFn)) exported++;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
hookLog('session-end', 'info', `obsidian exported=${exported}, dirs=${EXPORT_DIRS.length}, vault=${vaultPath}`);
|
|
462
|
+
if (exported > 0) {
|
|
463
|
+
config.obsidian.lastExport = new Date().toISOString();
|
|
464
|
+
config.obsidian.lastExportCount = exported;
|
|
465
|
+
safeWriteJson(configPath, config);
|
|
466
|
+
}
|
|
467
|
+
} catch (err) {
|
|
468
|
+
hookLog('session-end', 'error', `obsidian internal: ${err?.message ?? err}`);
|
|
469
|
+
throw err; // re-throw so safeRun logs FAIL
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Auto-commit and push ~/.mindlore/ if it has a .git directory.
|
|
475
|
+
* Only runs for the global scope — project .mindlore/ is in the project's own git.
|
|
476
|
+
* Push failure is graceful (offline support).
|
|
477
|
+
*/
|
|
478
|
+
function resolveGitBin() {
|
|
479
|
+
return resolveWin32Bin('git');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function syncGlobalRepo() {
|
|
483
|
+
const gDir = globalDir();
|
|
484
|
+
const gitDir = path.join(gDir, '.git');
|
|
485
|
+
if (!fs.existsSync(gitDir)) return;
|
|
486
|
+
|
|
487
|
+
const git = resolveGitBin();
|
|
488
|
+
const execOpts = (timeout) => ({ cwd: gDir, encoding: 'utf8', timeout, stdio: 'pipe', windowsHide: true });
|
|
489
|
+
|
|
490
|
+
// Check for changes
|
|
491
|
+
const status = execFileSync(git, ['status', '--porcelain'], execOpts(5000)).trim();
|
|
492
|
+
if (!status) return; // nothing to commit
|
|
493
|
+
|
|
494
|
+
execFileSync(git, ['add', '*.md', 'mindlore.db', 'diary/', 'sources/', 'domains/', 'analyses/', 'decisions/', 'raw/', 'connections/', 'insights/', 'learnings/'], execOpts(10000));
|
|
495
|
+
const now = new Date().toISOString().slice(0, 19);
|
|
496
|
+
execFileSync(git, ['commit', '-m', `mindlore auto-sync ${now}`], execOpts(15000));
|
|
497
|
+
|
|
498
|
+
// Push — graceful fail if no remote or offline
|
|
499
|
+
try {
|
|
500
|
+
execFileSync(git, ['push'], execOpts(30000));
|
|
501
|
+
} catch (_pushErr) {
|
|
502
|
+
hookLog('session-end', 'warn', 'git push failed (offline?): ' + (_pushErr?.message ?? '').slice(0, 100));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (!process.argv.includes('--worker')) {
|
|
507
|
+
withTelemetry('mindlore-session-end', main).catch(err => {
|
|
508
|
+
hookLog('mindlore-session-end', 'error', err?.message ?? String(err));
|
|
509
|
+
process.exit(0);
|
|
510
|
+
});
|
|
511
|
+
}
|