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