ocean-brain 0.5.0 → 0.6.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 (81) hide show
  1. package/dist/mcp.js +660 -31
  2. package/package.json +2 -1
  3. package/server/client/dist/assets/{Calendar-BU9XgX2N.js → Calendar-BJHLksEZ.js} +1 -1
  4. package/server/client/dist/assets/{Callout-C6tVCxOT.js → Callout-wFx9oCas.js} +1 -1
  5. package/server/client/dist/assets/Graph-9piERaNR.js +1 -0
  6. package/server/client/dist/assets/{Image-DxWhlIDG.js → Image-DK0lnaK2.js} +1 -1
  7. package/server/client/dist/assets/{Image.es-BjGK7y6Y.js → Image.es-Be7par_j.js} +1 -1
  8. package/server/client/dist/assets/Note-CC9ybxXL.js +21 -0
  9. package/server/client/dist/assets/{Plus.es-EcmtS_uw.js → Plus.es-BKm7uDYu.js} +1 -1
  10. package/server/client/dist/assets/Reminders-DRxA8v98.js +1 -0
  11. package/server/client/dist/assets/Search-BunSQrqv.js +1 -0
  12. package/server/client/dist/assets/{SurfaceCard-CO0oVxeL.js → SurfaceCard-FhlC8VSR.js} +1 -1
  13. package/server/client/dist/assets/Tag-z_crJ5JP.js +1 -0
  14. package/server/client/dist/assets/TagNotes-D7wIJ4jy.js +1 -0
  15. package/server/client/dist/assets/{Trash.es-DmwfSy3l.js → Trash.es-ByMVwx0d.js} +1 -1
  16. package/server/client/dist/assets/ViewNotes-Duo9XmcJ.js +1 -0
  17. package/server/client/dist/assets/Views-D4Z_O9Kd.js +1 -0
  18. package/server/client/dist/assets/{image.api-DeF9ck7g.js → image.api-B5Qi-Txv.js} +1 -1
  19. package/server/client/dist/assets/{index-BZUGWdOU.css → index-Cd81UxEh.css} +1 -1
  20. package/server/client/dist/assets/index-cHJQCo8V.js +1 -0
  21. package/server/client/dist/assets/index-nYj7BL3a.js +327 -0
  22. package/server/client/dist/assets/manage-image-detail-DcZggB1M.js +1 -0
  23. package/server/client/dist/assets/manage-image-glbtBibn.js +1 -0
  24. package/server/client/dist/assets/{mcp-Bh743HBz.js → mcp-BBUpQdS9.js} +1 -1
  25. package/server/client/dist/assets/{note-core-Z6kqulGB.js → note-core-DMZcZ4yV.js} +53 -53
  26. package/server/client/dist/assets/{note-vendor-i1M2FSI2.js → note-vendor-uuJO2MTD.js} +1 -1
  27. package/server/client/dist/assets/{placeholder-XQqo1Fvr.js → placeholder-B_qb_qkc.js} +5 -5
  28. package/server/client/dist/assets/properties-CiyQv6Tb.js +1 -0
  29. package/server/client/dist/assets/trash-CQnj4Rmv.js +1 -0
  30. package/server/client/dist/assets/{useReminderMutate-D21PurIS.js → useReminderMutate-CSKliBcc.js} +1 -1
  31. package/server/client/dist/assets/view-dashboard-B5fKgvk_.js +99 -0
  32. package/server/client/dist/index.html +4 -4
  33. package/server/dist/features/note/graphql/note.field.resolver.js +2 -0
  34. package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
  35. package/server/dist/features/note/graphql/note.mutation.resolver.js +119 -0
  36. package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
  37. package/server/dist/features/note/graphql/note.query.resolver.js +14 -0
  38. package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
  39. package/server/dist/features/note/graphql/note.type-defs.js +100 -0
  40. package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
  41. package/server/dist/features/note/http/mcp.js +394 -1
  42. package/server/dist/features/note/http/mcp.js.map +1 -1
  43. package/server/dist/features/note/services/markdown-intent-write.js +495 -0
  44. package/server/dist/features/note/services/markdown-intent-write.js.map +1 -0
  45. package/server/dist/features/note/services/markdown-patch.js +601 -0
  46. package/server/dist/features/note/services/markdown-patch.js.map +1 -0
  47. package/server/dist/features/note/services/properties.js +874 -0
  48. package/server/dist/features/note/services/properties.js.map +1 -0
  49. package/server/dist/features/note/services/snapshot.js +94 -12
  50. package/server/dist/features/note/services/snapshot.js.map +1 -1
  51. package/server/dist/features/note/services/trash.js +70 -2
  52. package/server/dist/features/note/services/trash.js.map +1 -1
  53. package/server/dist/features/note/services/write.js +21 -7
  54. package/server/dist/features/note/services/write.js.map +1 -1
  55. package/server/dist/features/view/graphql/view.query.resolver.js +31 -1
  56. package/server/dist/features/view/graphql/view.query.resolver.js.map +1 -1
  57. package/server/dist/features/view/graphql/view.type-defs.js +57 -1
  58. package/server/dist/features/view/graphql/view.type-defs.js.map +1 -1
  59. package/server/dist/features/view/services/workspace.js +367 -43
  60. package/server/dist/features/view/services/workspace.js.map +1 -1
  61. package/server/dist/modules/blocknote.js +367 -7
  62. package/server/dist/modules/blocknote.js.map +1 -1
  63. package/server/dist/routes/mcp.js +21 -1
  64. package/server/dist/routes/mcp.js.map +1 -1
  65. package/server/prisma/migrations/20260527073000_0017_note_properties/migration.sql +105 -0
  66. package/server/prisma/migrations/20260531073000_0018_view_query/migration.sql +3 -0
  67. package/server/prisma/schema.prisma +226 -130
  68. package/server/client/dist/assets/Graph-Dond2hGK.js +0 -1
  69. package/server/client/dist/assets/Note-Cr6GtXUM.js +0 -21
  70. package/server/client/dist/assets/Reminders-BlLLT_x0.js +0 -1
  71. package/server/client/dist/assets/Search-CrZ0ap8x.js +0 -1
  72. package/server/client/dist/assets/Tag-vws5AfNW.js +0 -1
  73. package/server/client/dist/assets/TagNotes-Fmf1Kgim.js +0 -1
  74. package/server/client/dist/assets/ViewNotes-BALN7IfH.js +0 -1
  75. package/server/client/dist/assets/Views-Ccc_Ml5U.js +0 -1
  76. package/server/client/dist/assets/index-BSVE2cGd.js +0 -1
  77. package/server/client/dist/assets/index-BvKBTjQq.js +0 -246
  78. package/server/client/dist/assets/manage-image-IoTXaMBP.js +0 -1
  79. package/server/client/dist/assets/manage-image-detail-BRTXwOsh.js +0 -1
  80. package/server/client/dist/assets/trash-hiJIg6-c.js +0 -1
  81. package/server/client/dist/assets/view-dashboard-BuaZ4MvI.js +0 -90
@@ -0,0 +1,601 @@
1
+ import crypto from "crypto";
2
+ const HASH_ALGORITHM = "sha256";
3
+ const EXCERPT_CONTEXT_CHARS = 80;
4
+ const DIFF_CONTEXT_LINES = 3;
5
+ const calculateMarkdownSha256 = (value) => {
6
+ return crypto.createHash(HASH_ALGORITHM).update(value, "utf8").digest("hex");
7
+ };
8
+ const countNewlines = (value) => {
9
+ let count = 0;
10
+ for (const character of value) {
11
+ if (character === "\n") {
12
+ count += 1;
13
+ }
14
+ }
15
+ return count;
16
+ };
17
+ const getLineRange = (markdown, match) => {
18
+ const lineStart = countNewlines(markdown.slice(0, match.start)) + 1;
19
+ const lineEnd = lineStart + countNewlines(match.text);
20
+ return {
21
+ lineStart,
22
+ lineEnd
23
+ };
24
+ };
25
+ const buildExcerpt = (markdown, start, end) => {
26
+ const excerpt = markdown.slice(start, end).trim();
27
+ return excerpt.length > 0 ? excerpt : void 0;
28
+ };
29
+ const toPatchMatch = (markdown, match, totalMatches) => {
30
+ const { lineStart, lineEnd } = getLineRange(markdown, match);
31
+ const beforeStart = Math.max(0, match.start - EXCERPT_CONTEXT_CHARS);
32
+ const afterEnd = Math.min(markdown.length, match.end + EXCERPT_CONTEXT_CHARS);
33
+ const beforeText = markdown.slice(beforeStart, match.start);
34
+ const afterText = markdown.slice(match.end, afterEnd);
35
+ return {
36
+ matchIndex: match.matchIndex,
37
+ lineStart,
38
+ lineEnd,
39
+ text: match.text,
40
+ beforeExcerpt: buildExcerpt(markdown, beforeStart, match.start),
41
+ afterExcerpt: buildExcerpt(markdown, match.end, afterEnd),
42
+ matchSha256: calculateMarkdownSha256(match.text),
43
+ surroundingHash: calculateMarkdownSha256(JSON.stringify({ beforeText, targetText: match.text, afterText })),
44
+ ...match.matchIndex === 0 && totalMatches > 1 ? { positionHint: "first" } : {},
45
+ ...match.matchIndex === totalMatches - 1 && totalMatches > 1 ? { positionHint: "last" } : {}
46
+ };
47
+ };
48
+ const findExactTextMatches = (markdown, text) => {
49
+ const matches = [];
50
+ let searchStart = 0;
51
+ while (searchStart <= markdown.length) {
52
+ const start = markdown.indexOf(text, searchStart);
53
+ if (start === -1) {
54
+ break;
55
+ }
56
+ matches.push({
57
+ matchIndex: matches.length,
58
+ start,
59
+ end: start + text.length,
60
+ text
61
+ });
62
+ searchStart = start + text.length;
63
+ }
64
+ return matches;
65
+ };
66
+ const splitLines = (value) => {
67
+ return value.split("\n");
68
+ };
69
+ const findSharedLineEdges = (beforeLines, afterLines) => {
70
+ let prefix = 0;
71
+ const maxPrefix = Math.min(beforeLines.length, afterLines.length);
72
+ while (prefix < maxPrefix && beforeLines[prefix] === afterLines[prefix]) {
73
+ prefix += 1;
74
+ }
75
+ let suffix = 0;
76
+ const maxSuffix = Math.min(beforeLines.length - prefix, afterLines.length - prefix);
77
+ while (suffix < maxSuffix && beforeLines[beforeLines.length - 1 - suffix] === afterLines[afterLines.length - 1 - suffix]) {
78
+ suffix += 1;
79
+ }
80
+ return {
81
+ prefix,
82
+ suffix
83
+ };
84
+ };
85
+ const formatDiffRange = (lineIndex, lineCount) => {
86
+ const lineNumber = lineIndex + 1;
87
+ return lineCount === 1 ? String(lineNumber) : `${lineNumber},${lineCount}`;
88
+ };
89
+ const createUnifiedMarkdownDiff = (beforeMarkdown, afterMarkdown, contextLines = DIFF_CONTEXT_LINES) => {
90
+ const beforeLines = splitLines(beforeMarkdown);
91
+ const afterLines = splitLines(afterMarkdown);
92
+ const { prefix, suffix } = findSharedLineEdges(beforeLines, afterLines);
93
+ const changedBeforeStart = prefix;
94
+ const changedBeforeEnd = beforeLines.length - suffix;
95
+ const changedAfterStart = prefix;
96
+ const changedAfterEnd = afterLines.length - suffix;
97
+ const displayBeforeStart = contextLines === null ? 0 : Math.max(0, changedBeforeStart - contextLines);
98
+ const displayBeforeEnd = contextLines === null ? beforeLines.length : Math.min(beforeLines.length, changedBeforeEnd + contextLines);
99
+ const displayAfterStart = contextLines === null ? 0 : Math.max(0, changedAfterStart - contextLines);
100
+ const displayAfterEnd = contextLines === null ? afterLines.length : Math.min(afterLines.length, changedAfterEnd + contextLines);
101
+ const diffLines = [
102
+ "--- before.md",
103
+ "+++ after.md",
104
+ `@@ -${formatDiffRange(displayBeforeStart, displayBeforeEnd - displayBeforeStart)} +${formatDiffRange(displayAfterStart, displayAfterEnd - displayAfterStart)} @@`
105
+ ];
106
+ for (let index = displayBeforeStart; index < changedBeforeStart; index += 1) {
107
+ diffLines.push(` ${beforeLines[index]}`);
108
+ }
109
+ for (let index = changedBeforeStart; index < changedBeforeEnd; index += 1) {
110
+ diffLines.push(`-${beforeLines[index]}`);
111
+ }
112
+ for (let index = changedAfterStart; index < changedAfterEnd; index += 1) {
113
+ diffLines.push(`+${afterLines[index]}`);
114
+ }
115
+ for (let index = changedBeforeEnd; index < displayBeforeEnd; index += 1) {
116
+ diffLines.push(` ${beforeLines[index]}`);
117
+ }
118
+ return diffLines.join("\n");
119
+ };
120
+ const countChangedLines = (beforeMarkdown, afterMarkdown) => {
121
+ const beforeLines = splitLines(beforeMarkdown);
122
+ const afterLines = splitLines(afterMarkdown);
123
+ const { prefix, suffix } = findSharedLineEdges(beforeLines, afterLines);
124
+ const removedLineCount = beforeLines.length - prefix - suffix;
125
+ const addedLineCount = afterLines.length - prefix - suffix;
126
+ return Math.max(removedLineCount, addedLineCount);
127
+ };
128
+ const isMarkdownTokenWhitespace = (character) => /\s/.test(character);
129
+ const countExplicitTagTokens = (value) => {
130
+ let count = 0;
131
+ for (let index = 0; index < value.length; index += 1) {
132
+ if (value[index] !== "[" || value[index + 1] !== "@" && value[index + 1] !== "#") {
133
+ continue;
134
+ }
135
+ let cursor = index + 2;
136
+ let isValid = cursor < value.length;
137
+ while (cursor < value.length && value[cursor] !== "]") {
138
+ if (value[cursor] === "[" || isMarkdownTokenWhitespace(value[cursor])) {
139
+ isValid = false;
140
+ break;
141
+ }
142
+ cursor += 1;
143
+ }
144
+ if (isValid && cursor < value.length && cursor > index + 2) {
145
+ count += 1;
146
+ index = cursor;
147
+ continue;
148
+ }
149
+ if (!isValid) {
150
+ index = Math.max(index, cursor - 1);
151
+ }
152
+ }
153
+ return count;
154
+ };
155
+ const countMarkdownReferenceTokens = (value) => {
156
+ let count = 0;
157
+ let cursor = 0;
158
+ while (cursor < value.length) {
159
+ const start = value.indexOf("[[", cursor);
160
+ if (start === -1) {
161
+ break;
162
+ }
163
+ const end = value.indexOf("]]", start + 2);
164
+ if (end === -1) {
165
+ break;
166
+ }
167
+ if (end > start + 2) {
168
+ count += 1;
169
+ }
170
+ cursor = end + 2;
171
+ }
172
+ return count;
173
+ };
174
+ const parseVersionTimestamp = (value) => {
175
+ const timestamp = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
176
+ return Number.isFinite(timestamp) ? timestamp : null;
177
+ };
178
+ const noteVersionsMatch = (expectedUpdatedAt, currentUpdatedAt) => {
179
+ const expectedTimestamp = parseVersionTimestamp(expectedUpdatedAt);
180
+ const currentTimestamp = parseVersionTimestamp(currentUpdatedAt);
181
+ return expectedTimestamp !== null && currentTimestamp !== null && expectedTimestamp === currentTimestamp;
182
+ };
183
+ const getPreservationFailure = (beforeMarkdown, afterMarkdown, policy) => {
184
+ const beforeTagCount = countExplicitTagTokens(beforeMarkdown);
185
+ const afterTagCount = countExplicitTagTokens(afterMarkdown);
186
+ if (policy?.preserveTags === true && afterTagCount < beforeTagCount) {
187
+ return {
188
+ status: "failed",
189
+ reason: "TAG_TOKEN_DECREASED",
190
+ message: "The patch would reduce explicit tag tokens."
191
+ };
192
+ }
193
+ const beforeReferenceCount = countMarkdownReferenceTokens(beforeMarkdown);
194
+ const afterReferenceCount = countMarkdownReferenceTokens(afterMarkdown);
195
+ if (policy?.preserveReferences === true && afterReferenceCount < beforeReferenceCount) {
196
+ return {
197
+ status: "failed",
198
+ reason: "REFERENCE_TOKEN_DECREASED",
199
+ message: "The patch would reduce note reference tokens."
200
+ };
201
+ }
202
+ return null;
203
+ };
204
+ const collectWarnings = (beforeMarkdown, afterMarkdown, policy) => {
205
+ const warnings = [];
206
+ const beforeTagCount = countExplicitTagTokens(beforeMarkdown);
207
+ const afterTagCount = countExplicitTagTokens(afterMarkdown);
208
+ const shouldWarnTags = policy?.preserveTags === void 0 || policy.preserveTags === "warn";
209
+ if (shouldWarnTags && afterTagCount < beforeTagCount) {
210
+ warnings.push("TAG_TOKEN_COUNT_DECREASED");
211
+ }
212
+ const beforeReferenceCount = countMarkdownReferenceTokens(beforeMarkdown);
213
+ const afterReferenceCount = countMarkdownReferenceTokens(afterMarkdown);
214
+ const shouldWarnReferences = policy?.preserveReferences === void 0 || policy.preserveReferences === "warn";
215
+ if (shouldWarnReferences && afterReferenceCount < beforeReferenceCount) {
216
+ warnings.push("REFERENCE_TOKEN_COUNT_DECREASED");
217
+ }
218
+ return warnings;
219
+ };
220
+ const validateBaseline = (note, expectedUpdatedAt, baseMarkdownSha256) => {
221
+ const beforeMarkdownSha256 = calculateMarkdownSha256(note.markdown);
222
+ if (!expectedUpdatedAt && !baseMarkdownSha256) {
223
+ return {
224
+ status: "failed",
225
+ reason: "MISSING_BASELINE",
226
+ message: "expectedUpdatedAt or baseMarkdownSha256 is required for markdown writes."
227
+ };
228
+ }
229
+ if (expectedUpdatedAt && !noteVersionsMatch(expectedUpdatedAt, note.updatedAt) || baseMarkdownSha256 && baseMarkdownSha256 !== beforeMarkdownSha256) {
230
+ return {
231
+ status: "failed",
232
+ reason: "BASELINE_MISMATCH",
233
+ message: "The markdown write baseline does not match the current note."
234
+ };
235
+ }
236
+ return null;
237
+ };
238
+ const validateChangeLimits = (beforeMarkdown, afterMarkdown, changedCharCount, policy) => {
239
+ const changedLineCount = countChangedLines(beforeMarkdown, afterMarkdown);
240
+ if (policy?.maxChangedLines !== void 0 && changedLineCount > policy.maxChangedLines || policy?.maxChangedChars !== void 0 && changedCharCount > policy.maxChangedChars) {
241
+ return {
242
+ status: "failed",
243
+ reason: "CHANGE_LIMIT_EXCEEDED",
244
+ message: "The markdown write exceeds the configured change limit."
245
+ };
246
+ }
247
+ return null;
248
+ };
249
+ const createChangePlan = ({
250
+ note,
251
+ afterMarkdown,
252
+ changedCharCount,
253
+ policy,
254
+ diffContextLines = DIFF_CONTEXT_LINES,
255
+ match,
256
+ placement
257
+ }) => {
258
+ if (!policy?.allowNoop && afterMarkdown === note.markdown) {
259
+ return {
260
+ status: "failed",
261
+ reason: "NOOP",
262
+ message: "The markdown write would not change the note."
263
+ };
264
+ }
265
+ const preservationFailure = getPreservationFailure(note.markdown, afterMarkdown, policy);
266
+ if (preservationFailure) {
267
+ return preservationFailure;
268
+ }
269
+ const limitFailure = validateChangeLimits(note.markdown, afterMarkdown, changedCharCount, policy);
270
+ if (limitFailure) {
271
+ return limitFailure;
272
+ }
273
+ return {
274
+ status: "dry_run",
275
+ note: {
276
+ id: note.id,
277
+ title: note.title,
278
+ updatedAt: note.updatedAt
279
+ },
280
+ ...match ? { match } : {},
281
+ ...placement ? { placement } : {},
282
+ proposed: {
283
+ changedLineCount: countChangedLines(note.markdown, afterMarkdown),
284
+ changedCharCount,
285
+ beforeMarkdownSha256: calculateMarkdownSha256(note.markdown),
286
+ afterMarkdownSha256: calculateMarkdownSha256(afterMarkdown),
287
+ diff: createUnifiedMarkdownDiff(note.markdown, afterMarkdown, diffContextLines)
288
+ },
289
+ warnings: collectWarnings(note.markdown, afterMarkdown, policy),
290
+ afterMarkdown
291
+ };
292
+ };
293
+ const matchHasAnchors = (markdown, match, selector) => {
294
+ if (selector.before !== void 0 && !markdown.slice(0, match.start).endsWith(selector.before)) {
295
+ return false;
296
+ }
297
+ if (selector.after !== void 0 && !markdown.slice(match.end).startsWith(selector.after)) {
298
+ return false;
299
+ }
300
+ return true;
301
+ };
302
+ const selectExactTextMatch = (markdown, selector) => {
303
+ if (selector.text.length === 0) {
304
+ return {
305
+ status: "failed",
306
+ reason: "EMPTY_TARGET",
307
+ message: "Exact text selector must not be empty."
308
+ };
309
+ }
310
+ const rawMatches = findExactTextMatches(markdown, selector.text);
311
+ if (rawMatches.length === 0) {
312
+ return {
313
+ status: "failed",
314
+ reason: "TARGET_NOT_FOUND",
315
+ message: "No exact text match was found in the current note."
316
+ };
317
+ }
318
+ const anchoredMatches = rawMatches.filter((match2) => matchHasAnchors(markdown, match2, selector));
319
+ if (anchoredMatches.length === 0) {
320
+ return {
321
+ status: "failed",
322
+ reason: "ANCHOR_MISMATCH",
323
+ message: "Exact text matches were found, but before/after anchors did not match."
324
+ };
325
+ }
326
+ if (anchoredMatches.length > 1) {
327
+ return {
328
+ status: "needs_disambiguation",
329
+ reason: "TARGET_AMBIGUOUS",
330
+ matches: anchoredMatches.map((match2) => toPatchMatch(markdown, match2, rawMatches.length))
331
+ };
332
+ }
333
+ const match = anchoredMatches[0];
334
+ return {
335
+ match,
336
+ patchMatch: toPatchMatch(markdown, match, rawMatches.length)
337
+ };
338
+ };
339
+ const selectMatchCandidate = (markdown, selector) => {
340
+ if (selector.text.length === 0) {
341
+ return {
342
+ status: "failed",
343
+ reason: "EMPTY_TARGET",
344
+ message: "Match candidate selector text must not be empty."
345
+ };
346
+ }
347
+ const rawMatches = findExactTextMatches(markdown, selector.text);
348
+ if (rawMatches.length === 0) {
349
+ return {
350
+ status: "failed",
351
+ reason: "TARGET_NOT_FOUND",
352
+ message: "No match candidate text was found in the current note."
353
+ };
354
+ }
355
+ const match = rawMatches.find((candidate) => candidate.matchIndex === selector.matchIndex);
356
+ if (!match) {
357
+ return {
358
+ status: "failed",
359
+ reason: "CANDIDATE_MISMATCH",
360
+ message: "The selected match index does not exist in the current note."
361
+ };
362
+ }
363
+ const patchMatch = toPatchMatch(markdown, match, rawMatches.length);
364
+ const positionMatches = selector.positionHint === void 0 || patchMatch.positionHint === selector.positionHint;
365
+ if (patchMatch.lineStart !== selector.lineStart || patchMatch.matchSha256 !== selector.matchSha256 || patchMatch.surroundingHash !== selector.surroundingHash || !positionMatches) {
366
+ return {
367
+ status: "failed",
368
+ reason: "CANDIDATE_MISMATCH",
369
+ message: "The selected match candidate no longer matches the current note context."
370
+ };
371
+ }
372
+ return {
373
+ match,
374
+ patchMatch
375
+ };
376
+ };
377
+ const applyPatchOperation = (markdown, match, operation) => {
378
+ if (operation.type === "replace") {
379
+ return {
380
+ markdown: `${markdown.slice(0, match.start)}${operation.replacement}${markdown.slice(match.end)}`,
381
+ changedCharCount: match.text.length + operation.replacement.length
382
+ };
383
+ }
384
+ if (operation.type === "insert_before") {
385
+ return {
386
+ markdown: `${markdown.slice(0, match.start)}${operation.insertion}${markdown.slice(match.start)}`,
387
+ changedCharCount: operation.insertion.length
388
+ };
389
+ }
390
+ return {
391
+ markdown: `${markdown.slice(0, match.end)}${operation.insertion}${markdown.slice(match.end)}`,
392
+ changedCharCount: operation.insertion.length
393
+ };
394
+ };
395
+ const buildMarkdownPatchPlan = (input) => {
396
+ const baselineFailure = validateBaseline(input.note, input.expectedUpdatedAt, input.baseMarkdownSha256);
397
+ if (baselineFailure) {
398
+ return baselineFailure;
399
+ }
400
+ const selected = input.selector.type === "exact_text" ? selectExactTextMatch(input.note.markdown, input.selector) : selectMatchCandidate(input.note.markdown, input.selector);
401
+ if ("status" in selected) {
402
+ return selected;
403
+ }
404
+ const patched = applyPatchOperation(input.note.markdown, selected.match, input.operation);
405
+ const plan = createChangePlan({
406
+ note: input.note,
407
+ afterMarkdown: patched.markdown,
408
+ changedCharCount: patched.changedCharCount,
409
+ policy: input.policy,
410
+ match: {
411
+ count: 1,
412
+ lineStart: selected.patchMatch.lineStart,
413
+ lineEnd: selected.patchMatch.lineEnd,
414
+ selectorType: input.selector.type,
415
+ matchedTextSha256: selected.patchMatch.matchSha256,
416
+ surroundingHash: selected.patchMatch.surroundingHash
417
+ }
418
+ });
419
+ return plan;
420
+ };
421
+ const parseHeadingLine = (line) => {
422
+ const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
423
+ if (!match) {
424
+ return null;
425
+ }
426
+ return {
427
+ level: match[1].length,
428
+ heading: match[2].trim()
429
+ };
430
+ };
431
+ const getLineStartOffsets = (lines) => {
432
+ const offsets = [];
433
+ let offset = 0;
434
+ for (const line of lines) {
435
+ offsets.push(offset);
436
+ offset += line.length + 1;
437
+ }
438
+ return offsets;
439
+ };
440
+ const findHeadingMatches = (markdown, placement) => {
441
+ const lines = splitLines(markdown);
442
+ const offsets = getLineStartOffsets(lines);
443
+ const headingLines = lines.map((line, index) => {
444
+ const heading = parseHeadingLine(line);
445
+ if (!heading) {
446
+ return null;
447
+ }
448
+ return {
449
+ ...heading,
450
+ lineIndex: index,
451
+ start: offsets[index]
452
+ };
453
+ }).filter((heading) => heading !== null);
454
+ const matches = headingLines.filter((heading) => {
455
+ return heading.heading === placement.heading && (placement.level === void 0 || heading.level === placement.level);
456
+ });
457
+ return matches.map((match) => {
458
+ const nextHeading = headingLines.find(
459
+ (heading) => heading.lineIndex > match.lineIndex && heading.level <= match.level
460
+ );
461
+ return {
462
+ ...match,
463
+ nextSectionStart: nextHeading?.start ?? markdown.length
464
+ };
465
+ });
466
+ };
467
+ const appendWithSeparator = (prefix, insertion, separator) => {
468
+ if (prefix.length === 0) {
469
+ return insertion;
470
+ }
471
+ return `${prefix}${prefix.endsWith(separator) ? "" : separator}${insertion}`;
472
+ };
473
+ const applyAppendPlacement = (markdown, insertion, placement, separator) => {
474
+ if (insertion.length === 0) {
475
+ return {
476
+ status: "failed",
477
+ reason: "EMPTY_INSERTION",
478
+ message: "Append insertion must not be empty."
479
+ };
480
+ }
481
+ if (placement.type === "end") {
482
+ return {
483
+ markdown: appendWithSeparator(markdown, insertion, separator),
484
+ placement
485
+ };
486
+ }
487
+ if (placement.level !== void 0 && (!Number.isInteger(placement.level) || placement.level < 1 || placement.level > 6)) {
488
+ return {
489
+ status: "failed",
490
+ reason: "INVALID_HEADING_LEVEL",
491
+ message: "Heading level must be an integer from 1 to 6."
492
+ };
493
+ }
494
+ const headingMatches = findHeadingMatches(markdown, placement);
495
+ if (headingMatches.length === 0) {
496
+ return {
497
+ status: "failed",
498
+ reason: "HEADING_NOT_FOUND",
499
+ message: "The requested heading was not found."
500
+ };
501
+ }
502
+ if (headingMatches.length > 1) {
503
+ return {
504
+ status: "failed",
505
+ reason: "HEADING_AMBIGUOUS",
506
+ message: "The requested heading appears more than once."
507
+ };
508
+ }
509
+ const [headingMatch] = headingMatches;
510
+ const prefix = markdown.slice(0, headingMatch.nextSectionStart);
511
+ const suffix = markdown.slice(headingMatch.nextSectionStart);
512
+ const prefixWithInsertion = appendWithSeparator(prefix, insertion, separator);
513
+ const nextMarkdown = suffix.length === 0 ? prefixWithInsertion : `${prefixWithInsertion}${prefixWithInsertion.endsWith(separator) ? "" : separator}${suffix}`;
514
+ return {
515
+ markdown: nextMarkdown,
516
+ placement
517
+ };
518
+ };
519
+ const buildMarkdownAppendPlan = (input) => {
520
+ const baselineFailure = validateBaseline(input.note, input.expectedUpdatedAt, input.baseMarkdownSha256);
521
+ if (baselineFailure) {
522
+ return baselineFailure;
523
+ }
524
+ const placement = input.placement ?? { type: "end" };
525
+ const separator = input.separator ?? "\n\n";
526
+ const appended = applyAppendPlacement(input.note.markdown, input.insertion, placement, separator);
527
+ if ("status" in appended) {
528
+ return appended;
529
+ }
530
+ return createChangePlan({
531
+ note: input.note,
532
+ afterMarkdown: appended.markdown,
533
+ changedCharCount: input.insertion.length,
534
+ policy: input.policy,
535
+ placement: appended.placement
536
+ });
537
+ };
538
+ const buildMarkdownReplacePlan = (input) => {
539
+ const baselineFailure = validateBaseline(input.note, input.expectedUpdatedAt, input.baseMarkdownSha256);
540
+ if (baselineFailure) {
541
+ return baselineFailure;
542
+ }
543
+ if (input.replacement.length === 0) {
544
+ return {
545
+ status: "failed",
546
+ reason: "EMPTY_REPLACEMENT",
547
+ message: "Replacement markdown must not be empty."
548
+ };
549
+ }
550
+ return createChangePlan({
551
+ note: input.note,
552
+ afterMarkdown: input.replacement,
553
+ changedCharCount: input.note.markdown.length + input.replacement.length,
554
+ policy: input.policy,
555
+ diffContextLines: null
556
+ });
557
+ };
558
+ const toPreviewResult = (plan) => {
559
+ return {
560
+ status: "dry_run",
561
+ note: plan.note,
562
+ ...plan.match ? { match: plan.match } : {},
563
+ ...plan.placement ? { placement: plan.placement } : {},
564
+ proposed: plan.proposed,
565
+ warnings: plan.warnings
566
+ };
567
+ };
568
+ const previewMarkdownPatch = (input) => {
569
+ const plan = buildMarkdownPatchPlan(input);
570
+ if (plan.status !== "dry_run") {
571
+ return plan;
572
+ }
573
+ return toPreviewResult(plan);
574
+ };
575
+ const previewMarkdownAppend = (input) => {
576
+ const plan = buildMarkdownAppendPlan(input);
577
+ if (plan.status !== "dry_run") {
578
+ return plan;
579
+ }
580
+ return toPreviewResult(plan);
581
+ };
582
+ const previewMarkdownReplace = (input) => {
583
+ const plan = buildMarkdownReplacePlan(input);
584
+ if (plan.status !== "dry_run") {
585
+ return plan;
586
+ }
587
+ return toPreviewResult(plan);
588
+ };
589
+ export {
590
+ buildMarkdownAppendPlan,
591
+ buildMarkdownPatchPlan,
592
+ buildMarkdownReplacePlan,
593
+ calculateMarkdownSha256,
594
+ countExplicitTagTokens,
595
+ countMarkdownReferenceTokens,
596
+ createUnifiedMarkdownDiff,
597
+ previewMarkdownAppend,
598
+ previewMarkdownPatch,
599
+ previewMarkdownReplace
600
+ };
601
+ //# sourceMappingURL=markdown-patch.js.map