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.
Files changed (56) hide show
  1. package/README.md +30 -3
  2. package/dist/scripts/bundle-hooks.d.ts +2 -0
  3. package/dist/scripts/bundle-hooks.d.ts.map +1 -0
  4. package/dist/scripts/bundle-hooks.js +68 -0
  5. package/dist/scripts/bundle-hooks.js.map +1 -0
  6. package/dist/scripts/init.js +0 -3
  7. package/dist/scripts/init.js.map +1 -1
  8. package/dist/scripts/lib/constants.d.ts +0 -2
  9. package/dist/scripts/lib/constants.d.ts.map +1 -1
  10. package/dist/scripts/lib/constants.js +0 -21
  11. package/dist/scripts/lib/constants.js.map +1 -1
  12. package/dist/tests/hook-smoke.test.js +1 -1
  13. package/dist/tests/hook-smoke.test.js.map +1 -1
  14. package/dist/tests/search-hook.test.js +1 -1
  15. package/dist/tests/search-hook.test.js.map +1 -1
  16. package/hooks/cc-memory-bulk-sync.cjs +592 -0
  17. package/hooks/cc-session-sync.cjs +842 -0
  18. package/hooks/hooks.json +149 -0
  19. package/hooks/lib/mindlore-common.cjs +2 -2
  20. package/hooks/lib/secure-io.cjs +17 -0
  21. package/hooks/mindlore-cwd-changed.cjs +19 -34
  22. package/hooks/mindlore-decision-detector.cjs +40 -31
  23. package/hooks/mindlore-dont-repeat.cjs +57 -115
  24. package/hooks/mindlore-fts5-sync.cjs +15 -44
  25. package/hooks/mindlore-index.cjs +100 -101
  26. package/hooks/mindlore-model-router.cjs +20 -32
  27. package/hooks/mindlore-post-compact.cjs +26 -42
  28. package/hooks/mindlore-post-read.cjs +35 -60
  29. package/hooks/mindlore-pre-compact.cjs +55 -73
  30. package/hooks/mindlore-read-guard.cjs +28 -51
  31. package/hooks/mindlore-research-guard.cjs +63 -101
  32. package/hooks/mindlore-search.cjs +1142 -93
  33. package/hooks/mindlore-session-end.cjs +155 -276
  34. package/hooks/mindlore-session-focus.cjs +639 -110
  35. package/hooks/src/lib/constants.cjs +15 -0
  36. package/hooks/src/lib/mindlore-common.cjs +975 -0
  37. package/hooks/src/lib/mindlore-common.d.cts +72 -0
  38. package/hooks/src/lib/secure-io.cjs +17 -0
  39. package/hooks/src/lib/types.d.ts +58 -0
  40. package/hooks/src/mindlore-cwd-changed.cjs +57 -0
  41. package/hooks/src/mindlore-decision-detector.cjs +54 -0
  42. package/hooks/src/mindlore-dont-repeat.cjs +222 -0
  43. package/hooks/src/mindlore-fts5-sync.cjs +98 -0
  44. package/hooks/src/mindlore-index.cjs +230 -0
  45. package/hooks/src/mindlore-model-router.cjs +54 -0
  46. package/hooks/src/mindlore-post-compact.cjs +69 -0
  47. package/hooks/src/mindlore-post-read.cjs +106 -0
  48. package/hooks/src/mindlore-pre-compact.cjs +154 -0
  49. package/hooks/src/mindlore-read-guard.cjs +105 -0
  50. package/hooks/src/mindlore-research-guard.cjs +176 -0
  51. package/hooks/src/mindlore-search.cjs +200 -0
  52. package/hooks/src/mindlore-session-end.cjs +511 -0
  53. package/hooks/src/mindlore-session-focus.cjs +256 -0
  54. package/package.json +7 -3
  55. package/plugin.json +3 -3
  56. package/templates/config.json +1 -1
@@ -1,119 +1,91 @@
1
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];
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, 'utf8');
17
+ const raw = fs.readFileSync(dataPath, "utf8");
28
18
  fs.unlinkSync(dataPath);
29
19
  payload = JSON.parse(raw);
30
20
  } catch (_err) {
31
- hookLog('session-end', 'error', 'payload read failed: ' + (_err?.message ?? _err));
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('session-end', 'info', label + ' OK');
28
+ hookLog("session-end", "info", label + " OK");
40
29
  } catch (e) {
41
- hookLog('session-end', 'error', label + ' FAIL: ' + e?.message);
30
+ hookLog("session-end", "error", label + " FAIL: " + e?.message);
42
31
  }
43
32
  }
44
-
45
33
  (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
-
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 scriptPath = path.join(__dirname, '..', 'dist', 'scripts', scriptName);
52
- if (!fs.existsSync(scriptPath)) return;
53
- const nodeExe = resolveWin32Bin('node') || process.execPath;
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('session-end', 'info', label + ' completed');
47
+ hookLog("session-end", "info", label + " completed");
61
48
  } catch (err) {
62
- hookLog('session-end', 'warn', `${label} failed: ${err?.message || err}`);
49
+ hookLog("session-end", "warn", `${label} failed: ${err?.message || err}`);
63
50
  }
64
51
  }
65
-
66
- await safeRunAsync(() => runSyncScript('cc-memory-bulk-sync.js', ['--auto'], 10000, 'CC memory sync'), 'cc-memory-sync');
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('session-end', 'info', `${unpromoted.length} raw files unpromoted`);
57
+ hookLog("session-end", "info", `${unpromoted.length} raw files unpromoted`);
74
58
  }
75
- }, 'raw-check');
76
-
77
- // Obsidian + git-sync are independent — run in parallel
59
+ }, "raw-check");
78
60
  await Promise.allSettled([
79
- safeRunAsync(() => syncObsidian(baseDir), 'obsidian'),
80
- safeRunAsync(() => syncGlobalRepo(), 'git-sync'),
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, '0');
91
- const d = String(date.getDate()).padStart(2, '0');
92
- const h = String(date.getHours()).padStart(2, '0');
93
- const min = String(date.getMinutes()).padStart(2, '0');
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
- // --name-only includes file names after each commit entry
104
- const raw = execFileSync('git', ['log', '--oneline', '-5', '--name-only'], {
105
- encoding: 'utf8',
106
- timeout: 5000,
107
- stdio: ['pipe', 'pipe', 'pipe'],
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('\n')) {
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, 'diary', `_session-reads-${getProjectName()}.json`);
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, 'utf8'));
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 === 'number') return v > 1;
137
- if (v && typeof v === 'object') return (v.count || 0) > 1;
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
- 'type: diary',
134
+ "type: diary",
174
135
  `date: ${now.toISOString().slice(0, 10)}`,
175
136
  `project: ${project}`,
176
- '---',
177
- '',
178
- `# Session Delta ${dateStr}`,
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('- _(no commits)_');
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('- _(no file changes)_');
153
+ sections.push("- _(no file changes)_");
197
154
  }
198
-
199
- // Read stats (from read-guard, if active)
200
155
  if (reads) {
201
- sections.push('', '## Read Stats');
156
+ sections.push("", "## Read Stats");
202
157
  sections.push(`- ${reads.count} files read, ${reads.repeats} repeated reads`);
203
158
  }
204
-
205
- sections.push('');
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 |\n`;
213
- fs.appendFileSync(logPath, logEntry, 'utf8');
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
- // Use system node instead of process.execPath — CC's embedded Node
224
- // may not work as a standalone binary for detached worker processes.
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: 'ignore',
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, 'mindlore.db');
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('## Commits\n' + commits.map(c => `- ${c}`).join('\n'));
199
+ bodyParts.push("## Commits\n" + commits.map((c) => `- ${c}`).join("\n"));
264
200
  }
265
201
  if (changedFiles.length > 0) {
266
- bodyParts.push('## Changed Files\n' + changedFiles.map(f => `- ${f}`).join('\n'));
202
+ bodyParts.push("## Changed Files\n" + changedFiles.map((f) => `- ${f}`).join("\n"));
267
203
  }
268
204
  if (reads) {
269
- bodyParts.push(`## Read Stats\n- ${reads.count} files read, ${reads.repeats} repeated`);
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('\n\n') || null;
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: 'session',
280
- scope: 'project',
281
- project: project,
213
+ kind: "session",
214
+ scope: "project",
215
+ project,
282
216
  summary: truncatedSummary,
283
- body: body,
284
- tags: 'session',
217
+ body,
218
+ tags: "session",
285
219
  entities: entities.length > 0 ? entities : null,
286
- source: 'hook',
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: 'episode',
296
- category: 'episodes',
227
+ type: "episode",
228
+ category: "episodes",
297
229
  title: truncatedSummary,
298
- content: [truncatedSummary, body ?? ''].join('\n').trim(),
299
- tags: 'session',
230
+ content: [truncatedSummary, body ?? ""].join("\n").trim(),
231
+ tags: "session",
300
232
  quality: null,
301
- dateCaptured: new Date().toISOString().slice(0, 10),
302
- project: 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
- // TTL cleanup for episode_inject_log (R4)
311
- try { cleanupExpiredInjectLog(db); } catch (_err) { /* cleanup is optional */ }
312
-
240
+ try {
241
+ cleanupExpiredInjectLog(db);
242
+ } catch (_err) {
243
+ }
313
244
  db.close();
314
245
  } catch (err) {
315
- hookLog('session-end', 'error', `episode write failed: ${err?.message ?? err}`);
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, 'diary', project || 'unknown');
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; // idempotent
331
-
255
+ if (fs.existsSync(filePath)) return;
332
256
  const lines = [
333
- '---',
257
+ "---",
334
258
  `slug: episode-${ts}`,
335
- 'type: episode',
259
+ "type: episode",
336
260
  `date: ${now.toISOString().slice(0, 10)}`,
337
- `project: ${project || 'unknown'}`,
338
- '---',
339
- '',
340
- `# Episode ${ts}`,
341
- '',
261
+ `project: ${project || "unknown"}`,
262
+ "---",
263
+ "",
264
+ `# Episode \u2014 ${ts}`,
265
+ ""
342
266
  ];
343
-
344
267
  if (commits.length > 0) {
345
- lines.push('## Commits');
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('## Changed Files');
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('## Read Stats');
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('_Read-only session no commits or file changes._');
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 !== undefined) return _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, 'dist', 'scripts', 'lib', 'obsidian-helpers.js');
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 === '1') {
386
- process.stderr.write(`[mindlore] obsidian-helpers not available: ${err.message}\n`);
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, 'utf8');
313
+ let content = fs.readFileSync(srcPath, "utf8");
407
314
  content = convertFn(content);
408
- fs.writeFileSync(destPath, content, 'utf8');
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
- const configPath = path.join(baseDir, 'config.json');
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('_') || entry.name.startsWith('.')) continue;
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('.md')) {
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('session-end', 'error', `obsidian internal: ${err?.message ?? err}`);
468
- throw err; // re-throw so safeRun logs FAIL
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('git');
365
+ return resolveWin32Bin("git");
479
366
  }
480
-
481
367
  function syncGlobalRepo() {
482
368
  const gDir = globalDir();
483
- const gitDir = path.join(gDir, '.git');
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: 'utf8', timeout, stdio: 'pipe', windowsHide: true });
488
-
489
- // Check for changes
490
- const status = execFileSync(git, ['status', '--porcelain'], execOpts(5000)).trim();
491
- if (!status) return; // nothing to commit
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, ['push'], execOpts(30000));
379
+ execFileSync(git, ["push"], execOpts(3e4));
500
380
  } catch (_pushErr) {
501
- hookLog('session-end', 'warn', 'git push failed (offline?): ' + (_pushErr?.message ?? '').slice(0, 100));
381
+ hookLog("session-end", "warn", "git push failed (offline?): " + (_pushErr?.message ?? "").slice(0, 100));
502
382
  }
503
383
  }
504
-
505
- if (!process.argv.includes('--worker')) {
506
- withTelemetry('mindlore-session-end', main).catch(err => {
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
  }