@theglitchking/semantic-memory 1.0.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 (68) hide show
  1. package/.claude-plugin/marketplace.json +71 -0
  2. package/.claude-plugin/plugin.json +34 -0
  3. package/CHANGELOG.md +233 -0
  4. package/LICENSE +21 -0
  5. package/README.md +91 -0
  6. package/bin/semantic-memory +2 -0
  7. package/bin/semantic-sidekick +2 -0
  8. package/commands/healthcheck.md +6 -0
  9. package/commands/mode.md +65 -0
  10. package/commands/normalize-config.md +6 -0
  11. package/commands/policy.md +16 -0
  12. package/commands/relink.md +6 -0
  13. package/commands/status.md +6 -0
  14. package/commands/update.md +6 -0
  15. package/commands/vault.md +21 -0
  16. package/dist/chunk-25EIQWXL.js +651 -0
  17. package/dist/chunk-25EIQWXL.js.map +1 -0
  18. package/dist/chunk-CAN2GWUG.js +88 -0
  19. package/dist/chunk-CAN2GWUG.js.map +1 -0
  20. package/dist/chunk-CXTRTCIK.js +762 -0
  21. package/dist/chunk-CXTRTCIK.js.map +1 -0
  22. package/dist/chunk-E2Z4J4MK.js +180 -0
  23. package/dist/chunk-E2Z4J4MK.js.map +1 -0
  24. package/dist/chunk-HODDRXJK.js +169 -0
  25. package/dist/chunk-HODDRXJK.js.map +1 -0
  26. package/dist/chunk-JPQHNG3R.js +147 -0
  27. package/dist/chunk-JPQHNG3R.js.map +1 -0
  28. package/dist/chunk-PZ5AY32C.js +10 -0
  29. package/dist/chunk-PZ5AY32C.js.map +1 -0
  30. package/dist/cli/index.d.ts +1 -0
  31. package/dist/cli/index.js +505 -0
  32. package/dist/cli/index.js.map +1 -0
  33. package/dist/core/embed-worker.d.ts +2 -0
  34. package/dist/core/embed-worker.js +121 -0
  35. package/dist/core/embed-worker.js.map +1 -0
  36. package/dist/core/index.d.ts +486 -0
  37. package/dist/core/index.js +62 -0
  38. package/dist/core/index.js.map +1 -0
  39. package/dist/indexer-KD6UTXZB.js +8 -0
  40. package/dist/indexer-KD6UTXZB.js.map +1 -0
  41. package/dist/lint-GMXQF4RW.js +11 -0
  42. package/dist/lint-GMXQF4RW.js.map +1 -0
  43. package/dist/log-LQXIJXIA.js +10 -0
  44. package/dist/log-LQXIJXIA.js.map +1 -0
  45. package/dist/mcp/server.d.ts +16 -0
  46. package/dist/mcp/server.js +4925 -0
  47. package/dist/mcp/server.js.map +1 -0
  48. package/dist/schema-F3RTY6SQ.js +12 -0
  49. package/dist/schema-F3RTY6SQ.js.map +1 -0
  50. package/dist/search-27WYAI24.js +202 -0
  51. package/dist/search-27WYAI24.js.map +1 -0
  52. package/docs/compat-matrix.md +127 -0
  53. package/docs/corpora-json.md +150 -0
  54. package/docs/regression-snapshot.md +122 -0
  55. package/docs/smart-middle-activation.md +97 -0
  56. package/hooks/hooks.json +39 -0
  57. package/hooks/reconcile.js +171 -0
  58. package/hooks/session-start.js +81 -0
  59. package/hooks/vault-context.js +503 -0
  60. package/package.json +97 -0
  61. package/scripts/link-skills.js +25 -0
  62. package/scripts/pre-commit-lint.sh +42 -0
  63. package/skills/outage-silence/SKILL.md +72 -0
  64. package/skills/research-mode/SKILL.md +84 -0
  65. package/skills/semantic-first/SKILL.md +117 -0
  66. package/skills/semantic-first/evals/evals.json +37 -0
  67. package/skills/semantic-first/references/vault-frontmatter.md +133 -0
  68. package/skills/vault-first/SKILL.md +100 -0
@@ -0,0 +1,762 @@
1
+ import {
2
+ loadSchema,
3
+ validateNote
4
+ } from "./chunk-E2Z4J4MK.js";
5
+
6
+ // src/core/crud.ts
7
+ import { readFile, writeFile, unlink, rename, mkdir } from "fs/promises";
8
+ import { dirname, join } from "path";
9
+ import { existsSync } from "fs";
10
+ import { glob } from "glob";
11
+ import matter from "gray-matter";
12
+ var NoteCrud = class {
13
+ notesPath;
14
+ constructor(notesPath) {
15
+ this.notesPath = notesPath;
16
+ }
17
+ async create(relativePath, content, frontmatter) {
18
+ const absPath = join(this.notesPath, relativePath);
19
+ if (existsSync(absPath)) {
20
+ throw new Error(`Note already exists: ${relativePath}`);
21
+ }
22
+ await mkdir(dirname(absPath), { recursive: true });
23
+ const stamps = computeStamps(content);
24
+ const merged = { ...stamps, ...frontmatter ?? {} };
25
+ const fileContent = matter.stringify(content, merged);
26
+ await writeFile(absPath, fileContent, "utf-8");
27
+ return relativePath;
28
+ }
29
+ async read(relativePath) {
30
+ const absPath = join(this.notesPath, relativePath);
31
+ return readFile(absPath, "utf-8");
32
+ }
33
+ async readMultiple(paths) {
34
+ const results = /* @__PURE__ */ new Map();
35
+ await Promise.all(
36
+ paths.map(async (p) => {
37
+ try {
38
+ const content = await this.read(p);
39
+ results.set(p, content);
40
+ } catch {
41
+ results.set(p, `[Error: could not read ${p}]`);
42
+ }
43
+ })
44
+ );
45
+ return results;
46
+ }
47
+ async update(relativePath, content, options) {
48
+ const absPath = join(this.notesPath, relativePath);
49
+ if (!existsSync(absPath)) {
50
+ throw new Error(`Note does not exist: ${relativePath}`);
51
+ }
52
+ const existing = await readFile(absPath, "utf-8");
53
+ let updated;
54
+ switch (options.mode) {
55
+ case "overwrite":
56
+ updated = content;
57
+ break;
58
+ case "append":
59
+ updated = existing + "\n" + content;
60
+ break;
61
+ case "prepend": {
62
+ const { data, content: body2 } = matter(existing);
63
+ const newBody = content + "\n" + body2;
64
+ updated = Object.keys(data).length > 0 ? matter.stringify(newBody, data) : newBody;
65
+ break;
66
+ }
67
+ case "patch-by-heading": {
68
+ if (!options.heading) throw new Error("patch-by-heading requires a heading");
69
+ updated = this.patchByHeading(existing, options.heading, content);
70
+ break;
71
+ }
72
+ }
73
+ const { data: existingFm, content: body } = matter(updated);
74
+ const stamps = computeStamps(body);
75
+ const newFm = { ...existingFm, ...stamps };
76
+ await writeFile(absPath, matter.stringify(body, newFm), "utf-8");
77
+ }
78
+ async delete(relativePath) {
79
+ const absPath = join(this.notesPath, relativePath);
80
+ if (!existsSync(absPath)) {
81
+ throw new Error(`Note does not exist: ${relativePath}`);
82
+ }
83
+ await unlink(absPath);
84
+ }
85
+ async move(fromPath, toPath) {
86
+ const absFrom = join(this.notesPath, fromPath);
87
+ const absTo = join(this.notesPath, toPath);
88
+ if (!existsSync(absFrom)) {
89
+ throw new Error(`Note does not exist: ${fromPath}`);
90
+ }
91
+ if (existsSync(absTo)) {
92
+ throw new Error(`Destination already exists: ${toPath}`);
93
+ }
94
+ await mkdir(dirname(absTo), { recursive: true });
95
+ await rename(absFrom, absTo);
96
+ await this.updateWikilinksAfterMove(fromPath, toPath);
97
+ }
98
+ async updateWikilinksAfterMove(oldPath, newPath) {
99
+ const oldName = oldPath.replace(/\.md$/, "").split("/").pop();
100
+ const newName = newPath.replace(/\.md$/, "").split("/").pop();
101
+ if (oldName === newName) return;
102
+ const files = await glob("**/*.md", { cwd: this.notesPath });
103
+ for (const file of files) {
104
+ const absPath = join(this.notesPath, file);
105
+ const content = await readFile(absPath, "utf-8");
106
+ const pattern = new RegExp(`\\[\\[${escapeRegex(oldName)}(\\|[^\\]]*)?\\]\\]`, "g");
107
+ if (!pattern.test(content)) continue;
108
+ const updated = content.replace(pattern, `[[${newName}$1]]`);
109
+ await writeFile(absPath, updated, "utf-8");
110
+ }
111
+ }
112
+ patchByHeading(content, heading, newContent) {
113
+ const lines = content.split("\n");
114
+ const headingPattern = new RegExp(`^#{1,6}\\s+${escapeRegex(heading)}\\s*$`, "i");
115
+ let headingIndex = -1;
116
+ let headingLevel = 0;
117
+ for (let i = 0; i < lines.length; i++) {
118
+ if (headingPattern.test(lines[i])) {
119
+ headingIndex = i;
120
+ const match = lines[i].match(/^(#{1,6})\s+/);
121
+ headingLevel = match ? match[1].length : 1;
122
+ break;
123
+ }
124
+ }
125
+ if (headingIndex === -1) {
126
+ throw new Error(`Heading not found: ${heading}`);
127
+ }
128
+ let endIndex = lines.length;
129
+ for (let i = headingIndex + 1; i < lines.length; i++) {
130
+ const match = lines[i].match(/^(#{1,6})\s+/);
131
+ if (match && match[1].length <= headingLevel) {
132
+ endIndex = i;
133
+ break;
134
+ }
135
+ }
136
+ const before = lines.slice(0, headingIndex + 1);
137
+ const after = lines.slice(endIndex);
138
+ return [...before, "", newContent, "", ...after].join("\n");
139
+ }
140
+ };
141
+ function escapeRegex(str) {
142
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
143
+ }
144
+ function computeStamps(body) {
145
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
146
+ const wordCount = body.trim() ? body.trim().split(/\s+/).length : 0;
147
+ const minutes = Math.max(1, Math.ceil(wordCount / 200));
148
+ return {
149
+ last_updated: today,
150
+ word_count: wordCount,
151
+ estimated_read_time: `${minutes} min read`
152
+ };
153
+ }
154
+
155
+ // src/core/frontmatter.ts
156
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
157
+ import { join as join2 } from "path";
158
+ import { glob as glob2 } from "glob";
159
+ import matter2 from "gray-matter";
160
+ var FrontmatterManager = class {
161
+ notesPath;
162
+ constructor(notesPath) {
163
+ this.notesPath = notesPath;
164
+ }
165
+ async get(relativePath) {
166
+ const absPath = join2(this.notesPath, relativePath);
167
+ const raw = await readFile2(absPath, "utf-8");
168
+ const { data } = matter2(raw);
169
+ return data;
170
+ }
171
+ async update(relativePath, fields) {
172
+ const absPath = join2(this.notesPath, relativePath);
173
+ const raw = await readFile2(absPath, "utf-8");
174
+ const { data, content } = matter2(raw);
175
+ for (const [key, value] of Object.entries(fields)) {
176
+ if (value === null || value === void 0) {
177
+ delete data[key];
178
+ } else {
179
+ data[key] = value;
180
+ }
181
+ }
182
+ const updated = matter2.stringify(content, data);
183
+ await writeFile2(absPath, updated, "utf-8");
184
+ }
185
+ };
186
+ var TagManager = class {
187
+ notesPath;
188
+ constructor(notesPath) {
189
+ this.notesPath = notesPath;
190
+ }
191
+ async list(relativePath) {
192
+ const absPath = join2(this.notesPath, relativePath);
193
+ const raw = await readFile2(absPath, "utf-8");
194
+ const { data, content } = matter2(raw);
195
+ const fmTags = Array.isArray(data.tags) ? data.tags : [];
196
+ const inlineTags = [...content.matchAll(/(?:^|\s)#([a-zA-Z][\w-/]*)/g)].map(
197
+ (m) => m[1]
198
+ );
199
+ return [.../* @__PURE__ */ new Set([...fmTags, ...inlineTags])];
200
+ }
201
+ async add(relativePath, tags) {
202
+ const absPath = join2(this.notesPath, relativePath);
203
+ const raw = await readFile2(absPath, "utf-8");
204
+ const { data, content } = matter2(raw);
205
+ const existing = Array.isArray(data.tags) ? data.tags : [];
206
+ const merged = [.../* @__PURE__ */ new Set([...existing, ...tags])];
207
+ data.tags = merged;
208
+ const updated = matter2.stringify(content, data);
209
+ await writeFile2(absPath, updated, "utf-8");
210
+ }
211
+ async remove(relativePath, tags) {
212
+ const absPath = join2(this.notesPath, relativePath);
213
+ const raw = await readFile2(absPath, "utf-8");
214
+ const { data, content } = matter2(raw);
215
+ if (Array.isArray(data.tags)) {
216
+ data.tags = data.tags.filter((t) => !tags.includes(t));
217
+ }
218
+ let updatedContent = content;
219
+ for (const tag of tags) {
220
+ const pattern = new RegExp(`(^|\\s)#${escapeRegex2(tag)}(?=\\s|$)`, "g");
221
+ updatedContent = updatedContent.replace(pattern, "$1");
222
+ }
223
+ const updated = matter2.stringify(updatedContent, data);
224
+ await writeFile2(absPath, updated, "utf-8");
225
+ }
226
+ async renameVaultWide(oldTag, newTag) {
227
+ const files = await glob2("**/*.md", { cwd: this.notesPath });
228
+ let count = 0;
229
+ for (const file of files) {
230
+ const absPath = join2(this.notesPath, file);
231
+ const raw = await readFile2(absPath, "utf-8");
232
+ const { data, content } = matter2(raw);
233
+ let changed = false;
234
+ if (Array.isArray(data.tags)) {
235
+ const idx = data.tags.indexOf(oldTag);
236
+ if (idx >= 0) {
237
+ data.tags[idx] = newTag;
238
+ changed = true;
239
+ }
240
+ }
241
+ const pattern = new RegExp(`(^|\\s)#${escapeRegex2(oldTag)}(?=\\s|$)`, "g");
242
+ const updatedContent = content.replace(pattern, `$1#${newTag}`);
243
+ if (updatedContent !== content) changed = true;
244
+ if (changed) {
245
+ const updated = matter2.stringify(updatedContent, data);
246
+ await writeFile2(absPath, updated, "utf-8");
247
+ count++;
248
+ }
249
+ }
250
+ return count;
251
+ }
252
+ };
253
+ function escapeRegex2(str) {
254
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
255
+ }
256
+
257
+ // src/core/watcher.ts
258
+ import { watch } from "chokidar";
259
+ import { EventEmitter } from "events";
260
+ var Watcher = class extends EventEmitter {
261
+ notesPath;
262
+ fsWatcher = null;
263
+ debounceMs;
264
+ pendingChanges = /* @__PURE__ */ new Set();
265
+ debounceTimer = null;
266
+ readyPromise = null;
267
+ usePolling;
268
+ constructor(notesPath, debounceMs = 500, usePolling = false) {
269
+ super();
270
+ this.notesPath = notesPath;
271
+ this.debounceMs = debounceMs;
272
+ this.usePolling = usePolling;
273
+ }
274
+ start() {
275
+ if (this.fsWatcher) return;
276
+ this.fsWatcher = watch("**/*.md", {
277
+ cwd: this.notesPath,
278
+ ignoreInitial: true,
279
+ followSymlinks: true,
280
+ ignored: [
281
+ "**/node_modules/**",
282
+ "**/.semantic-sidekick-index/**",
283
+ "**/.git/**"
284
+ ],
285
+ ...this.usePolling ? { usePolling: true, interval: 100 } : {}
286
+ });
287
+ this.readyPromise = new Promise((resolve) => {
288
+ this.fsWatcher.on("ready", resolve);
289
+ });
290
+ this.fsWatcher.on("add", (path) => this.enqueue(path));
291
+ this.fsWatcher.on("change", (path) => this.enqueue(path));
292
+ this.fsWatcher.on("unlink", (path) => this.enqueue(path));
293
+ this.fsWatcher.on("error", (err) => this.emit("error", err));
294
+ }
295
+ async ready() {
296
+ if (this.readyPromise) await this.readyPromise;
297
+ }
298
+ stop() {
299
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
300
+ this.fsWatcher?.close();
301
+ this.fsWatcher = null;
302
+ this.pendingChanges.clear();
303
+ }
304
+ enqueue(path) {
305
+ this.pendingChanges.add(path);
306
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
307
+ this.debounceTimer = setTimeout(() => {
308
+ const paths = [...this.pendingChanges];
309
+ this.pendingChanges.clear();
310
+ this.emit("changed", paths);
311
+ }, this.debounceMs);
312
+ }
313
+ };
314
+
315
+ // src/core/ingest.ts
316
+ import { basename } from "path";
317
+ function buildIngestChangeSet(input) {
318
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
319
+ const sourcePath = input.source.source_path ?? defaultSourcePath(input.source);
320
+ const sourceNote = {
321
+ path: sourcePath,
322
+ content: buildSourceBody(input.source),
323
+ frontmatter: {
324
+ title: input.source.source_title,
325
+ type: input.source.source_type ?? "source",
326
+ status: "active",
327
+ source: input.source.source_uri,
328
+ last_verified: today,
329
+ confidence: "high",
330
+ ...input.source.source_tags && input.source.source_tags.length > 0 && { tags: input.source.source_tags }
331
+ }
332
+ };
333
+ const unitCreates = input.units.map((u) => {
334
+ const title = u.title ?? titleFromPath(u.path);
335
+ const derivedFrom = [sourcePath, ...u.extra_derived_from ?? []];
336
+ return {
337
+ path: u.path.endsWith(".md") ? u.path : u.path + ".md",
338
+ content: u.content.startsWith("#") ? u.content : `# ${title}
339
+
340
+ ${u.content.trim()}
341
+ `,
342
+ frontmatter: {
343
+ title,
344
+ type: u.type ?? "note",
345
+ status: "active",
346
+ derived_from: derivedFrom,
347
+ last_verified: today,
348
+ confidence: u.confidence ?? "medium",
349
+ ...u.extra_frontmatter ?? {}
350
+ }
351
+ };
352
+ });
353
+ return {
354
+ changeset: { creates: [sourceNote, ...unitCreates] },
355
+ sourcePath,
356
+ unitPaths: unitCreates.map((c) => c.path)
357
+ };
358
+ }
359
+ function buildSourceBody(source) {
360
+ const lines = [`# ${source.source_title}`, "", `**Source:** ${source.source_uri}`, ""];
361
+ if (source.source_summary) {
362
+ lines.push(source.source_summary.trim(), "");
363
+ } else {
364
+ lines.push("_Summary not provided \u2014 fill in when referencing this source._", "");
365
+ }
366
+ return lines.join("\n");
367
+ }
368
+ function defaultSourcePath(source) {
369
+ const slug = slugify(source.source_title);
370
+ return `sources/${slug}.md`;
371
+ }
372
+ function titleFromPath(p) {
373
+ return basename(p).replace(/\.md$/, "").replace(/[-_]/g, " ");
374
+ }
375
+ function slugify(s) {
376
+ return s.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 80);
377
+ }
378
+
379
+ // src/core/index-regen.ts
380
+ import { readdir, readFile as readFile3, writeFile as writeFile3, stat } from "fs/promises";
381
+ import { existsSync as existsSync2 } from "fs";
382
+ import { join as join3, dirname as dirname2, basename as basename2, sep } from "path";
383
+ import matter3 from "gray-matter";
384
+ var INDEX_FILE = "INDEX.md";
385
+ var INDEX_OVERFLOW_THRESHOLD = 100;
386
+ async function regenDirectoryIndex(vaultPath, dirRel) {
387
+ const absDir = join3(vaultPath, dirRel);
388
+ const indexPath = join3(absDir, INDEX_FILE);
389
+ const relIndex = join3(dirRel, INDEX_FILE);
390
+ if (!existsSync2(absDir)) {
391
+ return { directory: dirRel, indexPath: relIndex, entryCount: 0, overflowed: false, wrote: false };
392
+ }
393
+ const files = (await readdir(absDir)).filter(
394
+ (f) => f.endsWith(".md") && f !== INDEX_FILE && f !== "README.md"
395
+ );
396
+ const entries = [];
397
+ for (const f of files) {
398
+ const full = join3(absDir, f);
399
+ try {
400
+ const st = await stat(full);
401
+ if (!st.isFile()) continue;
402
+ const raw = await readFile3(full, "utf-8");
403
+ const { data, content } = matter3(raw);
404
+ const title = typeof data.title === "string" ? data.title : firstHeading(content) ?? basenameNoExt(f);
405
+ const summary = typeof data.purpose === "string" ? data.purpose : firstParagraph(content);
406
+ entries.push({ path: f, title, summary });
407
+ } catch {
408
+ }
409
+ }
410
+ entries.sort((a, b) => a.title.localeCompare(b.title));
411
+ const body = renderIndex(dirRel, entries);
412
+ const overflowed = entries.length > INDEX_OVERFLOW_THRESHOLD;
413
+ let wrote = false;
414
+ if (existsSync2(indexPath)) {
415
+ const current = await readFile3(indexPath, "utf-8");
416
+ if (current !== body) {
417
+ await writeFile3(indexPath, body, "utf-8");
418
+ wrote = true;
419
+ }
420
+ } else {
421
+ await writeFile3(indexPath, body, "utf-8");
422
+ wrote = true;
423
+ }
424
+ return { directory: dirRel, indexPath: relIndex, entryCount: entries.length, overflowed, wrote };
425
+ }
426
+ async function regenIndexesForPaths(vaultPath, paths) {
427
+ const dirs = /* @__PURE__ */ new Set();
428
+ for (const p of paths) {
429
+ const d = dirname2(p);
430
+ if (d === "." || d === "/") continue;
431
+ dirs.add(d);
432
+ }
433
+ const results = [];
434
+ for (const d of dirs) {
435
+ results.push(await regenDirectoryIndex(vaultPath, d));
436
+ }
437
+ return results;
438
+ }
439
+ function renderIndex(dirRel, entries) {
440
+ const header = `# ${toTitle(dirRel)} Index
441
+
442
+ > Auto-generated by semantic-sidekick. Changes are overwritten on apply_patch. Add prose to per-note files, not here.
443
+
444
+ `;
445
+ if (entries.length === 0) return header + "_No notes in this directory yet._\n";
446
+ const overflowNote = entries.length > INDEX_OVERFLOW_THRESHOLD ? `> \u26A0\uFE0F ${entries.length} entries \u2014 consider splitting this directory into subfolders (threshold: ${INDEX_OVERFLOW_THRESHOLD}).
447
+
448
+ ` : "";
449
+ const lines = entries.map((e) => {
450
+ const linkText = e.title;
451
+ const target = e.path.replace(/\.md$/, "");
452
+ const summary = e.summary ? ` \u2014 ${truncate(e.summary, 140)}` : "";
453
+ return `- [${linkText}](./${e.path}) \u2014 \`[[${target}]]\`${summary}`;
454
+ });
455
+ return header + overflowNote + lines.join("\n") + "\n";
456
+ }
457
+ function toTitle(dirRel) {
458
+ return dirRel.split(sep).pop().replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
459
+ }
460
+ function basenameNoExt(p) {
461
+ return basename2(p).replace(/\.md$/, "");
462
+ }
463
+ function firstHeading(content) {
464
+ const m = content.match(/^\s*#{1,6}\s+(.+)$/m);
465
+ return m ? m[1].trim() : null;
466
+ }
467
+ function firstParagraph(content) {
468
+ const lines = content.split("\n");
469
+ for (const line of lines) {
470
+ const trimmed = line.trim();
471
+ if (!trimmed) continue;
472
+ if (trimmed.startsWith("#")) continue;
473
+ if (trimmed.startsWith(">")) continue;
474
+ if (trimmed.startsWith("```")) continue;
475
+ return trimmed.replace(/\s+/g, " ");
476
+ }
477
+ return void 0;
478
+ }
479
+ function truncate(s, n) {
480
+ return s.length <= n ? s : s.slice(0, n - 1).trimEnd() + "\u2026";
481
+ }
482
+
483
+ // src/core/patch.ts
484
+ import { readFile as readFile4, writeFile as writeFile4, unlink as unlink2, rename as rename2, mkdir as mkdir2 } from "fs/promises";
485
+ import { existsSync as existsSync3 } from "fs";
486
+ import { dirname as dirname3, join as join4 } from "path";
487
+ import matter4 from "gray-matter";
488
+ async function applyPatch(notesPath, changeset, options = {}) {
489
+ const validate = options.validate !== false;
490
+ const dryRun = options.dryRun === true;
491
+ const allowWarn = options.allowLintWarnings !== false;
492
+ const errors = [];
493
+ const lint = [];
494
+ const applied = [];
495
+ const snapshots = [];
496
+ const crud = new NoteCrud(notesPath);
497
+ let schema = null;
498
+ if (validate) {
499
+ try {
500
+ schema = await loadSchema(notesPath);
501
+ } catch (e) {
502
+ errors.push(`failed to load schema: ${e instanceof Error ? e.message : String(e)}`);
503
+ return { ok: false, applied, rolledBack: [], lint, errors };
504
+ }
505
+ }
506
+ for (const c of changeset.creates ?? []) {
507
+ if (existsSync3(join4(notesPath, c.path))) {
508
+ errors.push(`create: already exists: ${c.path}`);
509
+ }
510
+ }
511
+ for (const u of changeset.updates ?? []) {
512
+ if (!existsSync3(join4(notesPath, u.path))) {
513
+ errors.push(`update: does not exist: ${u.path}`);
514
+ }
515
+ }
516
+ for (const d of changeset.deletes ?? []) {
517
+ if (!existsSync3(join4(notesPath, d.path))) {
518
+ errors.push(`delete: does not exist: ${d.path}`);
519
+ }
520
+ }
521
+ for (const m of changeset.moves ?? []) {
522
+ if (!existsSync3(join4(notesPath, m.from))) {
523
+ errors.push(`move: source does not exist: ${m.from}`);
524
+ }
525
+ if (existsSync3(join4(notesPath, m.to))) {
526
+ errors.push(`move: destination exists: ${m.to}`);
527
+ }
528
+ }
529
+ if (errors.length > 0) {
530
+ return { ok: false, applied, rolledBack: [], lint, errors };
531
+ }
532
+ if (validate && schema) {
533
+ for (const c of changeset.creates ?? []) {
534
+ const proposed = matter4.stringify(c.content, c.frontmatter ?? {});
535
+ lint.push(...validateNote(c.path, proposed, schema));
536
+ }
537
+ for (const u of changeset.updates ?? []) {
538
+ const abs = join4(notesPath, u.path);
539
+ let proposed;
540
+ try {
541
+ const existing = await readFile4(abs, "utf-8");
542
+ proposed = applyUpdateInMemory(existing, u);
543
+ } catch (e) {
544
+ errors.push(`update precheck: ${u.path}: ${e instanceof Error ? e.message : String(e)}`);
545
+ continue;
546
+ }
547
+ lint.push(...validateNote(u.path, proposed, schema));
548
+ }
549
+ const schemaErrors = lint.filter((f) => f.rule === "schema_violations" && f.severity === "error");
550
+ if (schemaErrors.length > 0) {
551
+ errors.push(`${schemaErrors.length} schema violation(s) blocked the patch`);
552
+ }
553
+ if (!allowWarn) {
554
+ const warns = lint.filter((f) => f.severity === "warn");
555
+ if (warns.length > 0) errors.push(`${warns.length} lint warning(s) blocked the patch (allowLintWarnings=false)`);
556
+ }
557
+ }
558
+ if (errors.length > 0 || dryRun) {
559
+ return { ok: errors.length === 0, applied, rolledBack: [], lint, errors };
560
+ }
561
+ try {
562
+ for (const c of changeset.creates ?? []) {
563
+ const abs = join4(notesPath, c.path);
564
+ await crud.create(c.path, c.content, c.frontmatter);
565
+ snapshots.push({ op: "create", path: c.path, absPath: abs });
566
+ applied.push({ op: "create", path: c.path });
567
+ }
568
+ for (const u of changeset.updates ?? []) {
569
+ const abs = join4(notesPath, u.path);
570
+ const prev = await readFile4(abs, "utf-8");
571
+ await crud.update(u.path, u.content, { mode: u.mode, heading: u.heading });
572
+ snapshots.push({ op: "update", path: u.path, absPath: abs, prevContent: prev });
573
+ applied.push({ op: "update", path: u.path });
574
+ }
575
+ for (const m of changeset.moves ?? []) {
576
+ await crud.move(m.from, m.to);
577
+ snapshots.push({
578
+ op: "move",
579
+ path: m.to,
580
+ absPath: join4(notesPath, m.to),
581
+ prevAbsPath: join4(notesPath, m.from)
582
+ });
583
+ applied.push({ op: "move", path: `${m.from} -> ${m.to}` });
584
+ }
585
+ for (const d of changeset.deletes ?? []) {
586
+ const abs = join4(notesPath, d.path);
587
+ const prev = await readFile4(abs, "utf-8");
588
+ await crud.delete(d.path);
589
+ snapshots.push({ op: "delete", path: d.path, absPath: abs, prevContent: prev });
590
+ applied.push({ op: "delete", path: d.path });
591
+ }
592
+ } catch (e) {
593
+ errors.push(`op failed: ${e instanceof Error ? e.message : String(e)} \u2014 rolling back`);
594
+ const rolledBack = await rollback(snapshots);
595
+ return { ok: false, applied, rolledBack, lint, errors };
596
+ }
597
+ let indexes_regenerated = [];
598
+ if (options.regenIndexes !== false) {
599
+ const touched = [
600
+ ...(changeset.creates ?? []).map((c) => c.path),
601
+ ...(changeset.deletes ?? []).map((d) => d.path),
602
+ ...(changeset.moves ?? []).flatMap((m) => [m.from, m.to])
603
+ ];
604
+ try {
605
+ indexes_regenerated = await regenIndexesForPaths(notesPath, touched);
606
+ } catch (e) {
607
+ errors.push(`index regen failed (patch still applied): ${e instanceof Error ? e.message : String(e)}`);
608
+ }
609
+ }
610
+ return { ok: true, applied, rolledBack: [], lint, errors, indexes_regenerated };
611
+ }
612
+ async function rollback(snapshots) {
613
+ const reverted = [];
614
+ for (const s of snapshots.slice().reverse()) {
615
+ try {
616
+ switch (s.op) {
617
+ case "create":
618
+ if (existsSync3(s.absPath)) await unlink2(s.absPath);
619
+ reverted.push({ op: "create", path: s.path });
620
+ break;
621
+ case "update":
622
+ if (s.prevContent !== void 0) {
623
+ await writeFile4(s.absPath, s.prevContent, "utf-8");
624
+ }
625
+ reverted.push({ op: "update", path: s.path });
626
+ break;
627
+ case "delete":
628
+ if (s.prevContent !== void 0) {
629
+ await mkdir2(dirname3(s.absPath), { recursive: true });
630
+ await writeFile4(s.absPath, s.prevContent, "utf-8");
631
+ }
632
+ reverted.push({ op: "delete", path: s.path });
633
+ break;
634
+ case "move":
635
+ if (s.prevAbsPath && existsSync3(s.absPath)) {
636
+ await mkdir2(dirname3(s.prevAbsPath), { recursive: true });
637
+ await rename2(s.absPath, s.prevAbsPath);
638
+ }
639
+ reverted.push({ op: "move", path: s.path });
640
+ break;
641
+ }
642
+ } catch {
643
+ }
644
+ }
645
+ return reverted;
646
+ }
647
+ function applyUpdateInMemory(existing, u) {
648
+ switch (u.mode) {
649
+ case "overwrite":
650
+ return u.content;
651
+ case "append":
652
+ return existing + "\n" + u.content;
653
+ case "prepend": {
654
+ const { data, content: body } = matter4(existing);
655
+ const newBody = u.content + "\n" + body;
656
+ return Object.keys(data).length > 0 ? matter4.stringify(newBody, data) : newBody;
657
+ }
658
+ case "patch-by-heading": {
659
+ if (!u.heading) throw new Error("patch-by-heading requires heading");
660
+ return patchByHeadingInMemory(existing, u.heading, u.content);
661
+ }
662
+ }
663
+ }
664
+ function patchByHeadingInMemory(content, heading, newContent) {
665
+ const lines = content.split("\n");
666
+ const re = new RegExp(`^#{1,6}\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "i");
667
+ let idx = -1;
668
+ let level = 0;
669
+ for (let i = 0; i < lines.length; i++) {
670
+ if (re.test(lines[i])) {
671
+ idx = i;
672
+ const m = lines[i].match(/^(#{1,6})\s+/);
673
+ level = m ? m[1].length : 1;
674
+ break;
675
+ }
676
+ }
677
+ if (idx === -1) throw new Error(`Heading not found: ${heading}`);
678
+ let end = lines.length;
679
+ for (let i = idx + 1; i < lines.length; i++) {
680
+ const m = lines[i].match(/^(#{1,6})\s+/);
681
+ if (m && m[1].length <= level) {
682
+ end = i;
683
+ break;
684
+ }
685
+ }
686
+ return [...lines.slice(0, idx + 1), "", newContent, "", ...lines.slice(end)].join("\n");
687
+ }
688
+
689
+ // src/core/synthesize.ts
690
+ import { basename as basename3 } from "path";
691
+ function buildSynthesizeChangeSet(input) {
692
+ const path = input.suggested_path.endsWith(".md") ? input.suggested_path : input.suggested_path + ".md";
693
+ const title = input.topic.trim();
694
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
695
+ const frontmatter = {
696
+ title,
697
+ type: input.type ?? "note",
698
+ status: input.status ?? "active",
699
+ last_verified: today,
700
+ confidence: input.confidence ?? "medium",
701
+ ...input.sources && input.sources.length > 0 && { sources: input.sources },
702
+ ...input.derived_from && input.derived_from.length > 0 && { derived_from: input.derived_from },
703
+ ...input.decision_maker && { decision_maker: input.decision_maker },
704
+ ...input.decided_on && { decided_on: input.decided_on },
705
+ ...input.severity && { severity: input.severity },
706
+ ...input.extra_frontmatter ?? {}
707
+ };
708
+ const body = buildBody(title, input.answer, input.related_notes ?? []);
709
+ const create = { path, content: body, frontmatter };
710
+ const changeset = { creates: [create] };
711
+ return { changeset, path, title };
712
+ }
713
+ function buildBody(title, answer, relatedNotes) {
714
+ let body = `# ${title}
715
+
716
+ ${answer.trim()}
717
+ `;
718
+ if (relatedNotes.length > 0) {
719
+ body = autoWikilink(body, relatedNotes);
720
+ const links = relatedNotes.map((r) => `- [[${wikilinkName(r)}]]`).join("\n");
721
+ body += `
722
+ ## Related
723
+
724
+ ${links}
725
+ `;
726
+ }
727
+ return body;
728
+ }
729
+ function autoWikilink(body, relatedNotes) {
730
+ const parts = body.split(/(```[\s\S]*?```)/g);
731
+ return parts.map((part, i) => {
732
+ if (i % 2 === 1) return part;
733
+ let out = part;
734
+ for (const r of relatedNotes) {
735
+ const name = wikilinkName(r);
736
+ if (/[\[\]]/.test(out) && out.includes(`[[${name}]]`)) continue;
737
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
738
+ const re = new RegExp(`\\b${escaped}\\b`, "i");
739
+ out = out.replace(re, `[[${name}]]`);
740
+ }
741
+ return out;
742
+ }).join("");
743
+ }
744
+ function wikilinkName(pathOrName) {
745
+ const base = basename3(pathOrName).replace(/\.md$/, "");
746
+ return base;
747
+ }
748
+
749
+ export {
750
+ NoteCrud,
751
+ FrontmatterManager,
752
+ TagManager,
753
+ Watcher,
754
+ buildIngestChangeSet,
755
+ INDEX_FILE,
756
+ INDEX_OVERFLOW_THRESHOLD,
757
+ regenDirectoryIndex,
758
+ regenIndexesForPaths,
759
+ applyPatch,
760
+ buildSynthesizeChangeSet
761
+ };
762
+ //# sourceMappingURL=chunk-CXTRTCIK.js.map