@moxn/kb-migrate 0.4.10 → 0.4.12

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.
@@ -62,17 +62,11 @@ function stripInvalidLinks(text) {
62
62
  return displayText;
63
63
  });
64
64
  }
65
- /**
66
- * Convert a KB document's sections into a single markdown string.
67
- * Section names become H2 headings (mirrors the import convention).
68
- *
69
- * If extractReferences is true, also extracts KB path references from
70
- * the content and returns them separately (for two-pass export).
71
- */
72
65
  function sectionsToMarkdown(sections, options) {
73
66
  const parts = [];
74
67
  const allReferences = [];
75
68
  const databaseIds = [];
69
+ const media = [];
76
70
  const extractRefs = options?.extractReferences ?? false;
77
71
  for (const section of sections) {
78
72
  parts.push(`## ${section.name}\n`);
@@ -91,15 +85,21 @@ function sectionsToMarkdown(sections, options) {
91
85
  parts.push('');
92
86
  }
93
87
  else if (block.blockType === 'image' && block.url) {
94
- parts.push(`![${block.alt || ''}](${block.url})`);
88
+ const token = `MOXNMEDIA${media.length}PLACEHOLDER`;
89
+ media.push({ token, type: 'image', url: block.url, alt: block.alt });
90
+ parts.push(token);
95
91
  parts.push('');
96
92
  }
97
93
  else if (block.blockType === 'document' && block.url) {
98
- parts.push(`[${block.filename || 'document'}](${block.url})`);
94
+ const token = `MOXNMEDIA${media.length}PLACEHOLDER`;
95
+ media.push({ token, type: 'file', url: block.url, filename: block.filename });
96
+ parts.push(token);
99
97
  parts.push('');
100
98
  }
101
99
  else if (block.blockType === 'csv' && block.url) {
102
- parts.push(`[${block.filename || 'data.csv'}](${block.url})`);
100
+ const token = `MOXNMEDIA${media.length}PLACEHOLDER`;
101
+ media.push({ token, type: 'embed', url: block.url, filename: block.filename || 'data.csv' });
102
+ parts.push(token);
103
103
  parts.push('');
104
104
  }
105
105
  else if (block.blockType === 'database_embed' && block.databaseId) {
@@ -111,7 +111,60 @@ function sectionsToMarkdown(sections, options) {
111
111
  }
112
112
  }
113
113
  }
114
- return { markdown: parts.join('\n').trim(), references: allReferences, databaseIds };
114
+ return { markdown: parts.join('\n').trim(), references: allReferences, databaseIds, media };
115
+ }
116
+ /**
117
+ * Replace media placeholder paragraphs in Notion blocks with proper
118
+ * image/file/embed blocks. Martian doesn't support images, so we
119
+ * post-process the converted blocks.
120
+ */
121
+ function injectMediaBlocks(blocks, media) {
122
+ if (media.length === 0)
123
+ return blocks;
124
+ // Build a lookup from token to media info
125
+ const tokenMap = new Map(media.map((m) => [m.token, m]));
126
+ return blocks.map((block) => {
127
+ // Check if this is a paragraph containing a media placeholder
128
+ const b = block;
129
+ if (b.type !== 'paragraph' || !b.paragraph?.rich_text)
130
+ return block;
131
+ const text = b.paragraph.rich_text.map((rt) => rt.text?.content ?? '').join('').trim();
132
+ const mediaInfo = tokenMap.get(text);
133
+ if (!mediaInfo)
134
+ return block;
135
+ // Replace with proper Notion block
136
+ if (mediaInfo.type === 'image') {
137
+ return {
138
+ object: 'block',
139
+ type: 'image',
140
+ image: {
141
+ type: 'external',
142
+ external: { url: mediaInfo.url },
143
+ ...(mediaInfo.alt ? { caption: [{ type: 'text', text: { content: mediaInfo.alt } }] } : {}),
144
+ },
145
+ };
146
+ }
147
+ if (mediaInfo.type === 'file') {
148
+ return {
149
+ object: 'block',
150
+ type: 'file',
151
+ file: {
152
+ type: 'external',
153
+ external: { url: mediaInfo.url },
154
+ caption: [{ type: 'text', text: { content: mediaInfo.filename || 'document' } }],
155
+ },
156
+ };
157
+ }
158
+ // For CSV/embeds, use a bookmark block (Notion doesn't have native CSV embed)
159
+ return {
160
+ object: 'block',
161
+ type: 'bookmark',
162
+ bookmark: {
163
+ url: mediaInfo.url,
164
+ caption: [{ type: 'text', text: { content: mediaInfo.filename || 'file' } }],
165
+ },
166
+ };
167
+ });
115
168
  }
116
169
  // Max 100 blocks per API call
117
170
  const MAX_BLOCKS_PER_APPEND = 100;
@@ -360,8 +413,8 @@ export class NotionExportTarget extends ExportTarget {
360
413
  // Notion page creation / update
361
414
  // ============================================
362
415
  async createNotionPage(doc) {
363
- const { markdown } = sectionsToMarkdown(doc.sections, { extractReferences: true });
364
- const blocks = markdownToBlocks(markdown);
416
+ const { markdown, media } = sectionsToMarkdown(doc.sections, { extractReferences: true });
417
+ const blocks = injectMediaBlocks(markdownToBlocks(markdown), media);
365
418
  // First batch: up to 100 blocks as children of the new page
366
419
  const firstBatch = blocks.slice(0, MAX_BLOCKS_PER_APPEND);
367
420
  const remainingBlocks = blocks.slice(MAX_BLOCKS_PER_APPEND);
@@ -393,8 +446,8 @@ export class NotionExportTarget extends ExportTarget {
393
446
  },
394
447
  });
395
448
  await this.clearPageContent(notionPageId);
396
- const { markdown } = sectionsToMarkdown(doc.sections, { extractReferences: true });
397
- const blocks = markdownToBlocks(markdown);
449
+ const { markdown, media } = sectionsToMarkdown(doc.sections, { extractReferences: true });
450
+ const blocks = injectMediaBlocks(markdownToBlocks(markdown), media);
398
451
  await this.appendRemainingBlocks(notionPageId, blocks);
399
452
  }
400
453
  async clearPageContent(pageId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Migration tool for importing documents into Moxn Knowledge Base from local files, Notion, Google Docs, and more",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",