@dinoxx/dinox-cli 1.0.19 → 1.0.21

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 (88) hide show
  1. package/README.md +756 -472
  2. package/dist/auth/userInfo.d.ts +10 -1
  3. package/dist/auth/userInfo.js +66 -17
  4. package/dist/cli.js +0 -2
  5. package/dist/commands/auth/index.js +29 -14
  6. package/dist/commands/boxes/index.js +272 -52
  7. package/dist/commands/boxes/model.d.ts +34 -0
  8. package/dist/commands/boxes/model.js +78 -0
  9. package/dist/commands/boxes/operations.d.ts +86 -0
  10. package/dist/commands/boxes/operations.js +415 -0
  11. package/dist/commands/boxes/repo.d.ts +13 -6
  12. package/dist/commands/boxes/repo.js +272 -149
  13. package/dist/commands/boxes/store.d.ts +22 -0
  14. package/dist/commands/boxes/store.js +143 -0
  15. package/dist/commands/graph/index.js +2 -36
  16. package/dist/commands/mcp/noteContentTools.d.ts +222 -0
  17. package/dist/commands/mcp/noteContentTools.js +390 -0
  18. package/dist/commands/mcp/server.d.ts +3 -0
  19. package/dist/commands/mcp/server.js +45 -16
  20. package/dist/commands/mcp/tools.js +3 -1
  21. package/dist/commands/mcp/types.d.ts +6 -2
  22. package/dist/commands/meta/index.js +6 -22
  23. package/dist/commands/meta/repo.d.ts +0 -3
  24. package/dist/commands/meta/repo.js +1 -4
  25. package/dist/commands/notes/actions.d.ts +2 -0
  26. package/dist/commands/notes/actions.js +149 -55
  27. package/dist/commands/notes/bulk.d.ts +34 -0
  28. package/dist/commands/notes/bulk.js +204 -0
  29. package/dist/commands/notes/contentEngine.d.ts +95 -0
  30. package/dist/commands/notes/contentEngine.js +652 -0
  31. package/dist/commands/notes/contentPatch.d.ts +84 -0
  32. package/dist/commands/notes/contentPatch.js +337 -0
  33. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  34. package/dist/commands/notes/contentPatchCommand.js +126 -0
  35. package/dist/commands/notes/descriptors.js +192 -40
  36. package/dist/commands/notes/export.d.ts +21 -0
  37. package/dist/commands/notes/export.js +224 -0
  38. package/dist/commands/notes/metadataMutation.d.ts +28 -0
  39. package/dist/commands/notes/metadataMutation.js +127 -0
  40. package/dist/commands/notes/repo.d.ts +20 -0
  41. package/dist/commands/notes/repo.js +135 -18
  42. package/dist/commands/notes/runtime.d.ts +3 -12
  43. package/dist/commands/notes/runtime.js +4 -30
  44. package/dist/commands/notes/searchSql.js +1 -1
  45. package/dist/commands/prompt/index.js +15 -45
  46. package/dist/commands/prompt/repo.js +2 -2
  47. package/dist/commands/runtime.d.ts +26 -0
  48. package/dist/commands/runtime.js +76 -0
  49. package/dist/commands/schema/renderSkills.js +20 -7
  50. package/dist/commands/schema/spec.d.ts +1 -2
  51. package/dist/commands/schema/spec.js +2 -3
  52. package/dist/commands/schema/types.d.ts +0 -13
  53. package/dist/commands/storage/index.js +30 -60
  54. package/dist/commands/storage/repo.d.ts +1 -1
  55. package/dist/commands/storage/repo.js +20 -1
  56. package/dist/commands/sync.js +4 -2
  57. package/dist/commands/tags/content.d.ts +32 -0
  58. package/dist/commands/tags/content.js +243 -0
  59. package/dist/commands/tags/index.js +285 -49
  60. package/dist/commands/tags/model.d.ts +38 -0
  61. package/dist/commands/tags/model.js +100 -0
  62. package/dist/commands/tags/operations.d.ts +105 -0
  63. package/dist/commands/tags/operations.js +438 -0
  64. package/dist/commands/tags/repo.d.ts +5 -1
  65. package/dist/commands/tags/repo.js +142 -218
  66. package/dist/commands/tags/store.d.ts +28 -0
  67. package/dist/commands/tags/store.js +166 -0
  68. package/dist/commands/todo/index.js +30 -61
  69. package/dist/commands/writeDryRun.d.ts +25 -0
  70. package/dist/commands/writeDryRun.js +31 -1
  71. package/dist/config/paths.d.ts +2 -1
  72. package/dist/config/paths.js +11 -1
  73. package/dist/config/resolve.js +9 -7
  74. package/dist/daemon/index.d.ts +2 -0
  75. package/dist/daemon/index.js +176 -58
  76. package/dist/powersync/runtime.d.ts +2 -1
  77. package/dist/powersync/runtime.js +15 -3
  78. package/dist/powersync/schema/content.d.ts +2 -0
  79. package/dist/powersync/schema/content.js +2 -0
  80. package/dist/powersync/schema/index.d.ts +2 -0
  81. package/dist/skills/check.d.ts +1 -1
  82. package/dist/skills/check.js +27 -2
  83. package/dist/utils/authToken.d.ts +2 -0
  84. package/dist/utils/authToken.js +7 -2
  85. package/dist/utils/noteContentEngine.d.ts +333 -0
  86. package/dist/utils/noteContentEngine.js +740 -0
  87. package/dist/utils/tiptapMarkdownSchema.js +31 -0
  88. package/package.json +4 -8
@@ -0,0 +1,652 @@
1
+ import crypto from 'node:crypto';
2
+ import { DinoxError } from '../../utils/errors.js';
3
+ import { generateId } from '../../utils/id.js';
4
+ import { convertMarkdownToNoteContent } from '../../utils/tiptapMarkdown.js';
5
+ const CONTENT_TEXT_MAX_LENGTH = 300;
6
+ const MAX_REPLACE_BLOCK_RANGE = 50;
7
+ const ANCHOR_ATTRIBUTE_KEYS = ['anchorKey', 'data-toc-id', 'id', 'uniqueId'];
8
+ const TEXT_BLOCK_TYPES = new Set([
9
+ 'paragraph',
10
+ 'heading',
11
+ 'bulletList',
12
+ 'orderedList',
13
+ 'taskList',
14
+ 'blockquote',
15
+ 'codeBlock',
16
+ 'horizontalRule',
17
+ ]);
18
+ const ATOMIC_BLOCK_TYPES = new Set([
19
+ 'image',
20
+ 'video',
21
+ 'audio',
22
+ 'file',
23
+ 'bookmark',
24
+ 'iframe',
25
+ 'mermaid',
26
+ 'mindgraph',
27
+ 'blockMath',
28
+ 'math',
29
+ 'frontmatter',
30
+ ]);
31
+ const CONTAINER_BLOCK_TYPES = new Set(['table', 'audiosegment', 'pdfAnnotation']);
32
+ export function parseStoredNoteDocument(input) {
33
+ const parsed = parseMaybeJson(input.contentJson);
34
+ if (isNode(parsed)) {
35
+ const document = normalizeDocument(parsed);
36
+ if (hasMeaningfulDocumentContent(document) || !(input.contentMd ?? '').trim()) {
37
+ return document;
38
+ }
39
+ }
40
+ return normalizeDocument(convertMarkdownToNoteContent(input.contentMd ?? '').contentJson);
41
+ }
42
+ export function buildNoteContentBlocks(document) {
43
+ return topLevelNodes(document).map((node, index) => {
44
+ const anchorId = resolveAnchorId(node);
45
+ const text = extractPlainText(node).trim();
46
+ return {
47
+ anchorId: anchorId ?? `pos-${index}`,
48
+ hasStableAnchor: Boolean(anchorId),
49
+ nodeIndex: index,
50
+ nodeRange: `${index}:${index + 1}`,
51
+ type: node.type,
52
+ editMode: getNodeEditMode(node.type),
53
+ level: node.type === 'heading' ? normalizeHeadingLevel(node.attrs?.level) : null,
54
+ text,
55
+ textPreview: text.slice(0, 1000),
56
+ textHash: hashText(text),
57
+ nodeHash: hashNode(node),
58
+ charCount: text.length,
59
+ attrs: safeAttrs(node),
60
+ };
61
+ });
62
+ }
63
+ export function buildNoteContentOutline(document) {
64
+ const blocks = buildNoteContentBlocks(document);
65
+ return buildHeadingRanges(document).map((heading) => ({
66
+ path: heading.path,
67
+ level: heading.level,
68
+ nodeRange: `${heading.index}:${heading.endIndex}`,
69
+ contentHash: hashStableValue(blocks.slice(heading.index, heading.endIndex).map((block) => block.nodeHash)),
70
+ }));
71
+ }
72
+ export function getNoteContentHash(document) {
73
+ return hashStableValue(normalizeDocument(document));
74
+ }
75
+ export function serializeNoteContentMarkdown(document) {
76
+ return topLevelNodes(document).map((node) => blockToMarkdown(node, 0)).filter(Boolean).join('\n\n').trim();
77
+ }
78
+ export function projectNoteContent(document, options = {}) {
79
+ const normalized = normalizeHashtagReferences(normalizeDocument(document), options.resolveHashtagReference);
80
+ const contentMd = serializeNoteContentMarkdown(normalized);
81
+ const metadata = extractMetadata(normalized);
82
+ return {
83
+ document: normalized,
84
+ contentJson: stableStringify(normalized),
85
+ contentMd,
86
+ contentText: normalizeContentText(extractPlainText(normalized), CONTENT_TEXT_MAX_LENGTH),
87
+ hashtags: metadata.hashtags,
88
+ images: metadata.images,
89
+ bidLinks: metadata.bidLinks,
90
+ };
91
+ }
92
+ export function applyJsonPatchToDocument(document, operation, options = {}) {
93
+ const beforeDocument = normalizeDocument(document);
94
+ const beforeProjection = projectNoteContent(beforeDocument, options);
95
+ const beforeHash = getNoteContentHash(beforeDocument);
96
+ const result = patchDocument(beforeDocument, operation);
97
+ const afterDocument = normalizeDocument({ ...beforeDocument, content: result.nodes });
98
+ const afterProjection = projectNoteContent(afterDocument, options);
99
+ const afterHash = getNoteContentHash(afterProjection.document);
100
+ return {
101
+ operation: operation.kind,
102
+ changed: beforeHash !== afterHash,
103
+ beforeHash,
104
+ afterHash,
105
+ beforeContent: beforeProjection.contentMd,
106
+ afterContent: afterProjection.contentMd,
107
+ beforeBlocks: buildNoteContentBlocks(beforeDocument),
108
+ afterBlocks: buildNoteContentBlocks(afterProjection.document),
109
+ beforeProjection,
110
+ afterProjection,
111
+ target: result.target,
112
+ };
113
+ }
114
+ export function withStableBlockAnchors(document) {
115
+ const content = topLevelNodes(document).map((node) => {
116
+ if (resolveAnchorId(node)) {
117
+ return node;
118
+ }
119
+ return {
120
+ ...node,
121
+ attrs: {
122
+ ...(node.attrs ?? {}),
123
+ anchorKey: generateId(),
124
+ },
125
+ };
126
+ });
127
+ return normalizeDocument({ ...document, content });
128
+ }
129
+ function patchDocument(document, operation) {
130
+ const nodes = topLevelNodes(document);
131
+ switch (operation.kind) {
132
+ case 'appendSection':
133
+ return appendSection(nodes, operation.heading, operation.content);
134
+ case 'appendToHeading':
135
+ return appendToHeading(document, nodes, operation.headingPath, operation.content);
136
+ case 'replaceSection':
137
+ return replaceSection(document, nodes, operation);
138
+ case 'replaceBlock':
139
+ return replaceBlock(nodes, operation);
140
+ }
141
+ }
142
+ function appendSection(nodes, heading, markdown) {
143
+ const normalizedHeading = heading.trim();
144
+ if (!normalizedHeading) {
145
+ throw new DinoxError('--append-section requires a heading');
146
+ }
147
+ if (/^(#{1,6})\s+/.test(normalizedHeading)) {
148
+ throw new DinoxError('--append-section expects heading text without leading # markers');
149
+ }
150
+ return {
151
+ nodes: [
152
+ ...nodes,
153
+ headingNode(normalizedHeading, 2),
154
+ ...markdownToBlocks(markdown),
155
+ ],
156
+ target: { heading: normalizedHeading, nodeRange: `${nodes.length}:${nodes.length + 1}` },
157
+ };
158
+ }
159
+ function appendToHeading(document, nodes, headingPath, markdown) {
160
+ const heading = findHeadingRange(document, headingPath);
161
+ const insertAt = heading.endIndex;
162
+ const blocks = markdownToBlocks(markdown);
163
+ return {
164
+ nodes: [...nodes.slice(0, insertAt), ...blocks, ...nodes.slice(insertAt)],
165
+ target: { headingPath: heading.path, nodeRange: `${heading.index}:${heading.endIndex}` },
166
+ };
167
+ }
168
+ function replaceSection(document, nodes, operation) {
169
+ const heading = findHeadingRange(document, operation.headingPath);
170
+ const start = heading.index + 1;
171
+ const end = heading.endIndex;
172
+ assertProtectedReplacementAllowed(nodes.slice(start, end), Boolean(operation.allowProtectedReplace));
173
+ return {
174
+ nodes: [...nodes.slice(0, start), ...markdownToBlocks(operation.content), ...nodes.slice(end)],
175
+ target: { headingPath: heading.path, nodeRange: `${start}:${end}` },
176
+ };
177
+ }
178
+ function replaceBlock(nodes, operation) {
179
+ const match = normalizeMarkdownMatch(operation.match);
180
+ if (!match) {
181
+ throw new DinoxError('--match requires non-empty content for replace-block');
182
+ }
183
+ const ranges = findSerializedBlockRanges(nodes, match);
184
+ if (ranges.length === 0) {
185
+ throw new DinoxError('Block match was not found');
186
+ }
187
+ if (ranges.length > 1) {
188
+ throw new DinoxError('Block match is ambiguous; provide a larger exact match');
189
+ }
190
+ const range = ranges[0];
191
+ assertProtectedReplacementAllowed(nodes.slice(range.start, range.end), Boolean(operation.allowProtectedReplace));
192
+ return {
193
+ nodes: [...nodes.slice(0, range.start), ...markdownToBlocks(operation.content), ...nodes.slice(range.end)],
194
+ target: { nodeRange: `${range.start}:${range.end}`, matchHash: hashText(match) },
195
+ };
196
+ }
197
+ function findSerializedBlockRanges(nodes, match) {
198
+ const ranges = [];
199
+ for (let start = 0; start < nodes.length; start += 1) {
200
+ const maxEnd = Math.min(nodes.length, start + MAX_REPLACE_BLOCK_RANGE);
201
+ for (let end = start + 1; end <= maxEnd; end += 1) {
202
+ const markdown = normalizeMarkdownMatch(serializeNoteContentMarkdown({ type: 'doc', content: nodes.slice(start, end) }));
203
+ if (markdown === match) {
204
+ ranges.push({ start, end });
205
+ }
206
+ }
207
+ }
208
+ return ranges;
209
+ }
210
+ function assertProtectedReplacementAllowed(nodes, allowed) {
211
+ const protectedNodes = nodes.filter((node) => isProtectedBlock(node.type));
212
+ if (!protectedNodes.length || allowed) {
213
+ return;
214
+ }
215
+ const types = Array.from(new Set(protectedNodes.map((node) => node.type))).join(', ');
216
+ throw new DinoxError(`Target includes protected block(s): ${types}. Re-run with --allow-protected-replace after content-read if this replacement is intentional.`);
217
+ }
218
+ function buildHeadingRanges(document) {
219
+ const headings = [];
220
+ const stack = [];
221
+ const nodes = topLevelNodes(document);
222
+ nodes.forEach((node, index) => {
223
+ if (node.type !== 'heading') {
224
+ return;
225
+ }
226
+ const level = normalizeHeadingLevel(node.attrs?.level);
227
+ const title = extractPlainText(node).trim();
228
+ if (!title) {
229
+ return;
230
+ }
231
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) {
232
+ stack.pop();
233
+ }
234
+ stack.push({ level, title });
235
+ headings.push({ path: stack.map((item) => item.title).join('/'), level, index, endIndex: nodes.length });
236
+ });
237
+ return headings.map((heading, index) => {
238
+ const boundary = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);
239
+ return { ...heading, endIndex: boundary?.index ?? nodes.length };
240
+ });
241
+ }
242
+ function findHeadingRange(document, headingPath) {
243
+ const normalized = normalizeHeadingPath(headingPath);
244
+ if (!normalized) {
245
+ throw new DinoxError('Heading path is required');
246
+ }
247
+ const matches = buildHeadingRanges(document).filter((heading) => normalizeHeadingPath(heading.path) === normalized);
248
+ if (matches.length === 0) {
249
+ throw new DinoxError(`Heading not found: ${headingPath}`);
250
+ }
251
+ if (matches.length > 1) {
252
+ throw new DinoxError(`Heading path is ambiguous: ${headingPath}`);
253
+ }
254
+ return matches[0];
255
+ }
256
+ function markdownToBlocks(markdown) {
257
+ const contentJson = convertMarkdownToNoteContent(normalizeLineEndings(markdown)).contentJson;
258
+ return topLevelNodes(normalizeDocument(contentJson));
259
+ }
260
+ function headingNode(text, level) {
261
+ return {
262
+ type: 'heading',
263
+ attrs: { level },
264
+ content: text ? [{ type: 'text', text }] : [],
265
+ };
266
+ }
267
+ function normalizeDocument(node) {
268
+ if (!node || node.type !== 'doc') {
269
+ return { type: 'doc', content: [node].filter(isNode) };
270
+ }
271
+ const content = Array.isArray(node.content) ? node.content.filter(isNode) : [];
272
+ return { ...node, type: 'doc', content };
273
+ }
274
+ function normalizeHashtagReferences(node, resolve) {
275
+ if (!resolve) {
276
+ return node;
277
+ }
278
+ const walk = (current) => {
279
+ const content = Array.isArray(current.content) ? current.content.filter(isNode).map(walk) : undefined;
280
+ let next = content ? { ...current, content } : current;
281
+ if (next.type !== 'hashtag') {
282
+ return next;
283
+ }
284
+ const id = stringAttr(next, 'id');
285
+ const path = canonicalPath(stringAttr(next, 'fullPath') ||
286
+ stringAttr(next, 'path') ||
287
+ stringAttr(next, 'label') ||
288
+ id);
289
+ const resolved = resolve({ id, path });
290
+ const nextPath = resolved?.path ?? path;
291
+ if (!nextPath) {
292
+ return next;
293
+ }
294
+ next = {
295
+ ...next,
296
+ attrs: {
297
+ ...(next.attrs ?? {}),
298
+ id: (resolved?.id ?? id) || nextPath,
299
+ label: nextPath,
300
+ path: nextPath,
301
+ fullPath: nextPath,
302
+ },
303
+ };
304
+ return next;
305
+ };
306
+ return walk(node);
307
+ }
308
+ function topLevelNodes(document) {
309
+ return Array.isArray(document.content) ? document.content.filter(isNode) : [];
310
+ }
311
+ function hasMeaningfulDocumentContent(document) {
312
+ const nodes = topLevelNodes(document);
313
+ if (nodes.length === 0)
314
+ return false;
315
+ if (nodes.length > 1)
316
+ return true;
317
+ const only = nodes[0];
318
+ if (only.type !== 'paragraph')
319
+ return true;
320
+ return extractPlainText(only).trim().length > 0;
321
+ }
322
+ function parseMaybeJson(raw, depth = 0) {
323
+ const text = raw?.trim() ?? '';
324
+ if (!text || text === 'null' || text === 'undefined') {
325
+ return null;
326
+ }
327
+ try {
328
+ const parsed = JSON.parse(text);
329
+ return typeof parsed === 'string' && depth < 2 ? parseMaybeJson(parsed, depth + 1) : parsed;
330
+ }
331
+ catch {
332
+ return null;
333
+ }
334
+ }
335
+ function isNode(value) {
336
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value) && typeof value.type === 'string');
337
+ }
338
+ function getNodeEditMode(type) {
339
+ if (TEXT_BLOCK_TYPES.has(type))
340
+ return 'text';
341
+ if (ATOMIC_BLOCK_TYPES.has(type))
342
+ return 'atomicBlock';
343
+ if (CONTAINER_BLOCK_TYPES.has(type))
344
+ return 'container';
345
+ if (['text', 'hardBreak', 'mention', 'emoji', 'bidlink', 'hashtag', 'location', 'time', 'inlineMath'].includes(type))
346
+ return 'inlineAtom';
347
+ return 'unsupported';
348
+ }
349
+ function isProtectedBlock(type) {
350
+ const mode = getNodeEditMode(type);
351
+ return mode === 'atomicBlock' || mode === 'container' || mode === 'unsupported';
352
+ }
353
+ function resolveAnchorId(node) {
354
+ for (const key of ANCHOR_ATTRIBUTE_KEYS) {
355
+ const value = node.attrs?.[key];
356
+ if (typeof value === 'string' && value.trim()) {
357
+ return value.trim();
358
+ }
359
+ }
360
+ return null;
361
+ }
362
+ function safeAttrs(node) {
363
+ const attrs = node.attrs;
364
+ if (!attrs || typeof attrs !== 'object') {
365
+ return undefined;
366
+ }
367
+ const allowed = safeAttrKeys(node.type);
368
+ const safe = {};
369
+ for (const key of allowed) {
370
+ const value = attrs[key];
371
+ if (typeof value === 'string' && value.trim())
372
+ safe[key] = value.trim().slice(0, 2000);
373
+ if (typeof value === 'number' && Number.isFinite(value))
374
+ safe[key] = value;
375
+ if (typeof value === 'boolean')
376
+ safe[key] = value;
377
+ }
378
+ return Object.keys(safe).length ? safe : undefined;
379
+ }
380
+ function safeAttrKeys(type) {
381
+ const common = ['anchorKey', 'id', 'uniqueId'];
382
+ if (type === 'heading')
383
+ return [...common, 'level'];
384
+ if (type === 'image')
385
+ return [...common, 'src', 'resourceId', 'storageKey', 'alt', 'title', 'caption', 'showCaption'];
386
+ if (type === 'video')
387
+ return [...common, 'src', 'resourceId', 'storageKey', 'poster', 'title', 'autoplay', 'loop', 'muted', 'playsInline'];
388
+ if (type === 'audio')
389
+ return [...common, 'src', 'resourceId', 'storageKey', 'title', 'preload'];
390
+ if (type === 'file')
391
+ return [...common, 'src', 'resourceId', 'storageKey', 'name', 'type', 'mimeType', 'size'];
392
+ if (type === 'bookmark')
393
+ return [...common, 'url', 'title', 'description', 'siteName', 'domain', 'imageUrl', 'iconUrl'];
394
+ if (type === 'iframe')
395
+ return [...common, 'src'];
396
+ if (type === 'mermaid')
397
+ return [...common, 'code'];
398
+ if (type === 'mindgraph')
399
+ return [...common, 'markdown'];
400
+ if (type === 'audiosegment')
401
+ return [...common, 'text', 't0', 't1', 'speaker'];
402
+ if (type === 'pdfAnnotation')
403
+ return [...common, 'resourceId', 'pdfUrl', 'page', 'text', 'note'];
404
+ return common;
405
+ }
406
+ function extractPlainText(node) {
407
+ if (!node)
408
+ return '';
409
+ if (Array.isArray(node))
410
+ return node.map(extractPlainText).filter(Boolean).join('\n');
411
+ if (node.type === 'text')
412
+ return typeof node.text === 'string' ? node.text : '';
413
+ if (node.type === 'hardBreak')
414
+ return '\n';
415
+ const semantic = semanticNodeText(node);
416
+ if (semantic)
417
+ return semantic;
418
+ const children = Array.isArray(node.content) ? node.content : [];
419
+ const separator = joinsInlineContent(node.type) ? '' : isBlockNode(node.type) ? '\n' : '';
420
+ return children.map(extractPlainText).filter(Boolean).join(separator);
421
+ }
422
+ function blockToMarkdown(node, depth) {
423
+ const text = inlineMarkdown(node.content ?? []);
424
+ switch (node.type) {
425
+ case 'heading': return `${'#'.repeat(normalizeHeadingLevel(node.attrs?.level))} ${text}`.trim();
426
+ case 'paragraph': return text;
427
+ case 'blockquote': return childBlocks(node).map((child) => blockToMarkdown(child, depth)).join('\n').split('\n').map((line) => `> ${line}`).join('\n').trim();
428
+ case 'codeBlock': return `\`\`\`${stringAttr(node, 'language') || stringAttr(node, 'lang')}\n${extractPlainText(node)}\n\`\`\``;
429
+ case 'bulletList': return listToMarkdown(node, depth, '-');
430
+ case 'orderedList': return listToMarkdown(node, depth, '1.');
431
+ case 'taskList': return taskListToMarkdown(node, depth);
432
+ case 'horizontalRule': return '---';
433
+ case 'table': return tableToMarkdown(node);
434
+ case 'image': return imageToMarkdown(node);
435
+ case 'mermaid': return fencedAttrMarkdown('mermaid', stringAttr(node, 'code'));
436
+ case 'mindgraph': return fencedAttrMarkdown('mindgraph', stringAttr(node, 'markdown'));
437
+ case 'blockMath': return fencedAttrMarkdown('math', stringAttr(node, 'text') || stringAttr(node, 'latex') || stringAttr(node, 'code'));
438
+ case 'frontmatter': return stringAttr(node, 'raw') ? `---\n${stringAttr(node, 'raw')}\n---` : '';
439
+ default: return atomicReferenceMarkdown(node) || text || childBlocks(node).map((child) => blockToMarkdown(child, depth)).filter(Boolean).join('\n\n');
440
+ }
441
+ }
442
+ function inlineMarkdown(nodes) {
443
+ return nodes.map((node) => {
444
+ if (node.type === 'text')
445
+ return markedText(node);
446
+ if (node.type === 'hardBreak')
447
+ return '\n';
448
+ if (node.type === 'hashtag')
449
+ return `#${stringAttr(node, 'fullPath') || stringAttr(node, 'path') || stringAttr(node, 'label') || stringAttr(node, 'id')}`;
450
+ if (node.type === 'bidlink') {
451
+ const id = stringAttr(node, 'id');
452
+ const label = stringAttr(node, 'label');
453
+ return label && id ? `[[${label}|${id}]]` : label || `[[${id}]]`;
454
+ }
455
+ if (node.type === 'location')
456
+ return [stringAttr(node, 'name'), stringAttr(node, 'address')].filter(Boolean).join(' ');
457
+ if (node.type === 'time')
458
+ return stringAttr(node, 'iso') || stringAttr(node, 'displayFormat');
459
+ if (node.type === 'inlineMath')
460
+ return `$${stringAttr(node, 'text') || stringAttr(node, 'latex') || stringAttr(node, 'code') || extractPlainText(node)}$`;
461
+ return extractPlainText(node);
462
+ }).join('');
463
+ }
464
+ function markedText(node) {
465
+ let text = node.text ?? '';
466
+ for (const mark of Array.isArray(node.marks) ? node.marks : []) {
467
+ if (mark?.type === 'bold')
468
+ text = `**${text}**`;
469
+ if (mark?.type === 'italic')
470
+ text = `*${text}*`;
471
+ if (mark?.type === 'strike')
472
+ text = `~~${text}~~`;
473
+ if (mark?.type === 'code')
474
+ text = `\`${text.replace(/`/g, '\\`')}\``;
475
+ if (mark?.type === 'link') {
476
+ const href = stringFromAttrs(mark.attrs, 'href');
477
+ const title = stringFromAttrs(mark.attrs, 'title');
478
+ if (href)
479
+ text = `[${text}](${href}${title ? ` "${title.replace(/"/g, '\\"')}"` : ''})`;
480
+ }
481
+ }
482
+ return text;
483
+ }
484
+ function listToMarkdown(node, depth, marker) {
485
+ return childBlocks(node).map((item) => {
486
+ const [first, ...rest] = childBlocks(item);
487
+ const prefix = `${' '.repeat(depth)}${marker} `;
488
+ const head = first ? blockToMarkdown(first, depth + 1).replace(/\n/g, '\n ') : '';
489
+ const tail = rest.map((child) => blockToMarkdown(child, depth + 1)).filter(Boolean).join('\n');
490
+ return [prefix + head, tail].filter(Boolean).join('\n');
491
+ }).join('\n');
492
+ }
493
+ function taskListToMarkdown(node, depth) {
494
+ return childBlocks(node).map((item) => {
495
+ const checked = item.attrs?.checked === true ? 'x' : ' ';
496
+ const [first, ...rest] = childBlocks(item);
497
+ const prefix = `${' '.repeat(depth)}- [${checked}] `;
498
+ const head = first ? blockToMarkdown(first, depth + 1).replace(/\n/g, '\n ') : '';
499
+ const tail = rest.map((child) => blockToMarkdown(child, depth + 1)).filter(Boolean).join('\n');
500
+ return [prefix + head, tail].filter(Boolean).join('\n');
501
+ }).join('\n');
502
+ }
503
+ function tableToMarkdown(node) {
504
+ const matrix = childBlocks(node).filter((row) => row.type === 'tableRow').map((row) => childBlocks(row).filter((cell) => cell.type === 'tableCell' || cell.type === 'tableHeader'));
505
+ const width = Math.max(0, ...matrix.map((row) => row.length));
506
+ if (!width)
507
+ return '';
508
+ const rows = matrix.map((row) => Array.from({ length: width }, (_, index) => cellToMarkdown(row[index] ?? { type: 'tableCell', content: [] })));
509
+ return [rows[0], Array.from({ length: width }, () => '---'), ...rows.slice(1)].map((row) => `| ${row.join(' | ')} |`).join('\n');
510
+ }
511
+ function cellToMarkdown(cell) {
512
+ return childBlocks(cell).map((child) => blockToMarkdown(child, 0)).filter(Boolean).join('<br>').replace(/\|/g, '\\|').trim();
513
+ }
514
+ function imageToMarkdown(node) {
515
+ const src = stringAttr(node, 'src');
516
+ const alt = stringAttr(node, 'alt') || stringAttr(node, 'title') || inlineMarkdown(node.content ?? []);
517
+ const caption = stringAttr(node, 'caption') || inlineMarkdown(node.content ?? []);
518
+ return [src ? `![${alt}](${src})` : placeholderMarkdown('image', node), caption && caption !== alt ? caption : ''].filter(Boolean).join('\n');
519
+ }
520
+ function atomicReferenceMarkdown(node) {
521
+ if (node.type === 'video' || node.type === 'audio' || node.type === 'file' || node.type === 'bookmark' || node.type === 'iframe' || node.type === 'audiosegment' || node.type === 'pdfAnnotation') {
522
+ return placeholderMarkdown(node.type, node, semanticNodeText({ ...node, content: [] }));
523
+ }
524
+ return '';
525
+ }
526
+ function fencedAttrMarkdown(language, text) {
527
+ return text ? `\`\`\`${language}\n${text}\n\`\`\`` : `[${language}]`;
528
+ }
529
+ function placeholderMarkdown(type, node, label) {
530
+ const attrs = [
531
+ ['id', stringAttr(node, 'id')],
532
+ ['resourceId', stringAttr(node, 'resourceId')],
533
+ ['url', stringAttr(node, 'url') || stringAttr(node, 'src')],
534
+ ['title', stringAttr(node, 'title') || stringAttr(node, 'name')],
535
+ ].filter(([, value]) => value).map(([key, value]) => `${key}="${String(value).replace(/"/g, '\\"')}"`).join(' ');
536
+ return `[${type}${attrs ? ` ${attrs}` : ''}]${label ? ` ${label}` : ''}`;
537
+ }
538
+ function extractMetadata(document) {
539
+ const hashtags = new Set();
540
+ const images = new Set();
541
+ const bidLinks = new Map();
542
+ visit(document, (node) => {
543
+ if (node.type === 'hashtag') {
544
+ const tag = canonicalPath(stringAttr(node, 'fullPath') || stringAttr(node, 'path') || stringAttr(node, 'label') || stringAttr(node, 'id'));
545
+ if (tag)
546
+ hashtags.add(tag);
547
+ }
548
+ if ((node.type === 'image' || node.type === 'video') && /^https?:\/\//i.test(stringAttr(node, 'src'))) {
549
+ images.add(stripUrlQuery(stringAttr(node, 'src')));
550
+ }
551
+ if (node.type === 'bidlink') {
552
+ const id = stringAttr(node, 'id');
553
+ const type = stringAttr(node, 'type') || 'reference';
554
+ if (id)
555
+ bidLinks.set(`${id}::${type}`, { id, type: type === 'note' ? 'reference' : type });
556
+ }
557
+ });
558
+ return { hashtags: Array.from(hashtags), images: Array.from(images), bidLinks: Array.from(bidLinks.values()) };
559
+ }
560
+ function visit(node, callback) {
561
+ callback(node);
562
+ for (const child of Array.isArray(node.content) ? node.content : [])
563
+ visit(child, callback);
564
+ }
565
+ function semanticNodeText(node) {
566
+ switch (node.type) {
567
+ case 'hashtag': return stringAttr(node, 'fullPath') || stringAttr(node, 'path') || stringAttr(node, 'label') || stringAttr(node, 'id');
568
+ case 'bidlink': return stringAttr(node, 'label') || stringAttr(node, 'id');
569
+ case 'image': return stringAttr(node, 'caption') || stringAttr(node, 'alt') || stringAttr(node, 'title') || inlineMarkdown(node.content ?? []);
570
+ case 'video':
571
+ case 'audio': return stringAttr(node, 'title') || stringAttr(node, 'src');
572
+ case 'file': return stringAttr(node, 'name') || stringAttr(node, 'src');
573
+ case 'bookmark': return stringAttr(node, 'title') || stringAttr(node, 'description') || stringAttr(node, 'url');
574
+ case 'iframe': return stringAttr(node, 'src');
575
+ case 'mermaid': return stringAttr(node, 'code');
576
+ case 'mindgraph': return stringAttr(node, 'markdown');
577
+ case 'blockMath':
578
+ case 'inlineMath':
579
+ case 'math': return stringAttr(node, 'text') || stringAttr(node, 'latex') || stringAttr(node, 'code');
580
+ case 'audiosegment': return [stringAttr(node, 'speaker'), stringAttr(node, 'text'), extractPlainText(node.content)].filter(Boolean).join(' ');
581
+ case 'pdfAnnotation': return [stringAttr(node, 'text'), stringAttr(node, 'note'), extractPlainText(node.content)].filter(Boolean).join('\n');
582
+ default: return '';
583
+ }
584
+ }
585
+ function normalizeContentText(value, maxLength) {
586
+ const cleaned = normalizeLineEndings(value).split('\n').map((line) => line.trim()).filter(Boolean).join(' ').replace(/\bhttps?:\/\/\S+/gi, '').replace(/\s{2,}/g, ' ').trim();
587
+ return cleaned.length > maxLength ? cleaned.slice(0, maxLength) : cleaned;
588
+ }
589
+ function stableStringify(value) {
590
+ return JSON.stringify(stableValue(value));
591
+ }
592
+ function hashStableValue(value) {
593
+ return hashText(stableStringify(value));
594
+ }
595
+ function hashNode(node) {
596
+ return hashStableValue(node);
597
+ }
598
+ function hashText(value) {
599
+ return crypto.createHash('sha256').update(value, 'utf8').digest('hex');
600
+ }
601
+ function stableValue(value) {
602
+ if (Array.isArray(value))
603
+ return value.map(stableValue);
604
+ if (!value || typeof value !== 'object')
605
+ return value;
606
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, stableValue(item)]));
607
+ }
608
+ function childBlocks(node) {
609
+ return Array.isArray(node.content) ? node.content.filter(isNode) : [];
610
+ }
611
+ function stringAttr(node, key) {
612
+ return stringFromAttrs(node.attrs, key);
613
+ }
614
+ function stringFromAttrs(attrs, key) {
615
+ if (!attrs || typeof attrs !== 'object' || Array.isArray(attrs))
616
+ return '';
617
+ const value = attrs[key];
618
+ return typeof value === 'string' ? value.trim() : '';
619
+ }
620
+ function normalizeHeadingLevel(value) {
621
+ const level = Number(value);
622
+ return Number.isInteger(level) && level >= 1 && level <= 6 ? level : 1;
623
+ }
624
+ function normalizeHeadingPath(value) {
625
+ return value.split('/').map((segment) => segment.trim()).filter(Boolean).join('/');
626
+ }
627
+ function normalizeLineEndings(value) {
628
+ return String(value ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
629
+ }
630
+ function normalizeMarkdownMatch(value) {
631
+ return normalizeLineEndings(value).trim();
632
+ }
633
+ function canonicalPath(value) {
634
+ return value.split('/').map((segment) => segment.trim()).filter(Boolean).join('/');
635
+ }
636
+ function stripUrlQuery(input) {
637
+ try {
638
+ const url = new URL(input);
639
+ url.search = '';
640
+ url.hash = '';
641
+ return url.toString();
642
+ }
643
+ catch {
644
+ return input;
645
+ }
646
+ }
647
+ function joinsInlineContent(type) {
648
+ return ['paragraph', 'heading', 'text', 'blockquote'].includes(type);
649
+ }
650
+ function isBlockNode(type) {
651
+ return TEXT_BLOCK_TYPES.has(type) || ATOMIC_BLOCK_TYPES.has(type) || CONTAINER_BLOCK_TYPES.has(type);
652
+ }