@moxn/kb-migrate 0.4.17 → 0.4.19

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.
@@ -137,7 +137,12 @@ export async function runNotionExport(options) {
137
137
  // Use the parent page ID as the target for database creation
138
138
  const parentPageId = options.parentPageId;
139
139
  const result = await exportDatabase(dbCtx, dbId, parentPageId);
140
- const icon = { created: '\u2713', updated: '\u21bb', skipped: '-', failed: '\u2717' }[result.status];
140
+ const icon = {
141
+ created: '\u2713',
142
+ updated: '\u21bb',
143
+ skipped: '-',
144
+ failed: '\u2717',
145
+ }[result.status];
141
146
  console.log(` ${icon} ${result.status}: ${result.kbDatabaseName} (${result.entriesCreated} entries)`);
142
147
  if (result.error)
143
148
  console.log(` Error: ${result.error}`);
@@ -165,7 +170,8 @@ export async function runNotionExport(options) {
165
170
  // ============================================
166
171
  if (!options.dryRun) {
167
172
  // Find documents that have references and were successfully exported
168
- const successfulExports = results.filter((r) => (r.status === 'created' || r.status === 'updated' || r.status === 'skipped') && r.externalId);
173
+ const successfulExports = results.filter((r) => (r.status === 'created' || r.status === 'updated' || r.status === 'skipped') &&
174
+ r.externalId);
169
175
  if (successfulExports.length > 0) {
170
176
  console.log(`\nPass 2: Resolving cross-references for ${successfulExports.length} documents...`);
171
177
  let totalResolved = 0;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Target registry and factory
3
3
  */
4
- export { ExportTarget, type ExportTargetConfig, type ExportTargetResult, type ExportTargetLog } from './base.js';
4
+ export { ExportTarget, type ExportTargetConfig, type ExportTargetResult, type ExportTargetLog, } from './base.js';
5
5
  export { NotionExportTarget, type NotionExportConfig } from './notion.js';
6
- export { exportDatabase, type DatabaseExportResult, type DatabaseExportContext } from './notion-database-export.js';
6
+ export { exportDatabase, type DatabaseExportResult, type DatabaseExportContext, } from './notion-database-export.js';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Target registry and factory
3
3
  */
4
- export { ExportTarget } from './base.js';
4
+ export { ExportTarget, } from './base.js';
5
5
  export { NotionExportTarget } from './notion.js';
6
- export { exportDatabase } from './notion-database-export.js';
6
+ export { exportDatabase, } from './notion-database-export.js';
@@ -28,7 +28,7 @@ const HEX_TO_NOTION_COLOR = {
28
28
  function hexToNotionColor(hex) {
29
29
  if (!hex)
30
30
  return 'default';
31
- return HEX_TO_NOTION_COLOR[hex.toUpperCase()] ?? HEX_TO_NOTION_COLOR[hex] ?? 'default';
31
+ return (HEX_TO_NOTION_COLOR[hex.toUpperCase()] ?? HEX_TO_NOTION_COLOR[hex] ?? 'default');
32
32
  }
33
33
  /**
34
34
  * Export a single KB database to Notion.
@@ -187,22 +187,49 @@ async function uploadFileToNotion(client, url, filename, contentType) {
187
187
  /**
188
188
  * Post-process martian output to reconstruct callout blocks.
189
189
  *
190
- * Callouts are stored as blockquotes with an emoji prefix (e.g., `> 💡 tip text`).
191
- * Martian converts these to Notion `quote` blocks with the actual text inside
192
- * `children[].paragraph.rich_text` (not the top-level `rich_text`).
193
- * This function detects the emoji prefix pattern in the first child paragraph
194
- * and converts matching quotes to native Notion `callout` blocks.
190
+ * Callouts may arrive as either:
191
+ * 1. `quote` blocks (when stored with `> ` prefix) — text in children[0].paragraph.rich_text
192
+ * 2. `paragraph` blocks (when stored as plain text) — text in paragraph.rich_text
193
+ *
194
+ * In both cases, if the first text segment starts with an emoji followed by a space,
195
+ * the block is converted to a native Notion `callout` with that emoji as the icon.
195
196
  */
196
197
  function reconstructCallouts(blocks) {
197
198
  const emojiPrefixRe = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F?)\s/u;
198
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
199
199
  return blocks.map((block) => {
200
+ // Case 1: paragraph with emoji prefix
201
+ if (block.type === 'paragraph') {
202
+ const richText = block.paragraph?.rich_text;
203
+ if (!richText?.length)
204
+ return block;
205
+ const firstSegment = richText[0];
206
+ if (firstSegment.type !== 'text' || !firstSegment.text?.content)
207
+ return block;
208
+ const match = firstSegment.text.content.match(emojiPrefixRe);
209
+ if (!match)
210
+ return block;
211
+ const emoji = match[1];
212
+ const strippedContent = firstSegment.text.content.slice(match[0].length);
213
+ const calloutRichText = [
214
+ { ...firstSegment, text: { ...firstSegment.text, content: strippedContent } },
215
+ ...richText.slice(1),
216
+ ].filter((rt) => 'text' in rt && rt.text?.content);
217
+ return {
218
+ object: 'block',
219
+ type: 'callout',
220
+ callout: {
221
+ rich_text: calloutRichText,
222
+ icon: { type: 'emoji', emoji },
223
+ color: 'default',
224
+ },
225
+ };
226
+ }
227
+ // Case 2: quote with emoji prefix in first child paragraph
200
228
  if (block.type !== 'quote')
201
229
  return block;
202
230
  const children = block.quote?.children;
203
231
  if (!children?.length)
204
232
  return block;
205
- // Find the first child paragraph with text
206
233
  const firstChild = children[0];
207
234
  if (firstChild.type !== 'paragraph' || !firstChild.paragraph?.rich_text?.length)
208
235
  return block;
@@ -211,15 +238,13 @@ function reconstructCallouts(blocks) {
211
238
  return block;
212
239
  const match = firstSegment.text.content.match(emojiPrefixRe);
213
240
  if (!match)
214
- return block; // No emoji prefix — keep as regular quote
241
+ return block;
215
242
  const emoji = match[1];
216
243
  const strippedContent = firstSegment.text.content.slice(match[0].length);
217
- // Build the callout's rich_text from the first child paragraph (stripped of emoji)
218
244
  const calloutRichText = [
219
245
  { ...firstSegment, text: { ...firstSegment.text, content: strippedContent } },
220
246
  ...firstChild.paragraph.rich_text.slice(1),
221
247
  ].filter((rt) => rt.text?.content);
222
- // Remaining children become the callout's children
223
248
  const remainingChildren = children.slice(1);
224
249
  return {
225
250
  object: 'block',
@@ -285,7 +310,11 @@ async function sectionsToNotionBlocks(sections, options) {
285
310
  for (let i = 0; i < parts.length; i++) {
286
311
  if (i > 0) {
287
312
  flushText();
288
- allBlocks.push({ object: 'block', type: 'divider', divider: {} });
313
+ allBlocks.push({
314
+ object: 'block',
315
+ type: 'divider',
316
+ divider: {},
317
+ });
289
318
  }
290
319
  const part = parts[i];
291
320
  if (part.trim()) {
@@ -297,9 +326,12 @@ async function sectionsToNotionBlocks(sections, options) {
297
326
  else if (block.blockType === 'image' && block.url) {
298
327
  flushText();
299
328
  if (block.storageKey && options?.notionClient) {
300
- const fname = block.filename || deriveFilename(block.storageKey, block.mimeType || 'image/png');
329
+ const fname = block.filename ||
330
+ deriveFilename(block.storageKey, block.mimeType || 'image/png');
301
331
  const uploadId = await uploadFileToNotion(options.notionClient, block.url, fname, block.mimeType || 'image/png');
302
- allBlocks.push(uploadId ? notionImageUploadBlock(uploadId, block.alt) : notionImageBlock(block.url, block.alt));
332
+ allBlocks.push(uploadId
333
+ ? notionImageUploadBlock(uploadId, block.alt)
334
+ : notionImageBlock(block.url, block.alt));
303
335
  }
304
336
  else {
305
337
  allBlocks.push(notionImageBlock(block.url, block.alt));
@@ -308,10 +340,13 @@ async function sectionsToNotionBlocks(sections, options) {
308
340
  else if (block.blockType === 'document' && block.url) {
309
341
  flushText();
310
342
  if (block.storageKey && options?.notionClient) {
311
- const fname = block.filename || deriveFilename(block.storageKey, block.mimeType || 'application/octet-stream');
343
+ const fname = block.filename ||
344
+ deriveFilename(block.storageKey, block.mimeType || 'application/octet-stream');
312
345
  const uploadId = await uploadFileToNotion(options.notionClient, block.url, fname, block.mimeType || 'application/octet-stream');
313
346
  if (uploadId) {
314
- allBlocks.push(block.mimeType === 'application/pdf' ? notionPdfUploadBlock(uploadId, block.filename) : notionFileUploadBlock(uploadId, block.filename));
347
+ allBlocks.push(block.mimeType === 'application/pdf'
348
+ ? notionPdfUploadBlock(uploadId, block.filename)
349
+ : notionFileUploadBlock(uploadId, block.filename));
315
350
  }
316
351
  else if (block.mimeType === 'application/pdf') {
317
352
  allBlocks.push(notionPdfBlock(block.url, block.filename));
@@ -332,7 +367,9 @@ async function sectionsToNotionBlocks(sections, options) {
332
367
  if (block.storageKey && options?.notionClient) {
333
368
  const fname = block.filename || deriveFilename(block.storageKey, 'text/csv');
334
369
  const uploadId = await uploadFileToNotion(options.notionClient, block.url, fname, 'text/csv');
335
- allBlocks.push(uploadId ? notionFileUploadBlock(uploadId, block.filename || 'data.csv') : notionFileBlock(block.url, block.filename || 'data.csv'));
370
+ allBlocks.push(uploadId
371
+ ? notionFileUploadBlock(uploadId, block.filename || 'data.csv')
372
+ : notionFileBlock(block.url, block.filename || 'data.csv'));
336
373
  }
337
374
  else {
338
375
  allBlocks.push(notionFileBlock(block.url, block.filename || 'data.csv'));
@@ -427,7 +464,9 @@ export class NotionExportTarget extends ExportTarget {
427
464
  */
428
465
  async resolveAndAppendReferences(doc, notionPageId) {
429
466
  // Extract references from section content (no upload needed for reference pass)
430
- const { references } = await sectionsToNotionBlocks(doc.sections, { extractReferences: true });
467
+ const { references } = await sectionsToNotionBlocks(doc.sections, {
468
+ extractReferences: true,
469
+ });
431
470
  if (references.length === 0) {
432
471
  return { resolved: 0, unresolved: 0 };
433
472
  }
@@ -597,7 +636,10 @@ export class NotionExportTarget extends ExportTarget {
597
636
  // Notion page creation / update
598
637
  // ============================================
599
638
  async createNotionPage(doc) {
600
- const { blocks } = await sectionsToNotionBlocks(doc.sections, { extractReferences: true, notionClient: this.client });
639
+ const { blocks } = await sectionsToNotionBlocks(doc.sections, {
640
+ extractReferences: true,
641
+ notionClient: this.client,
642
+ });
601
643
  // First batch: up to 100 blocks as children of the new page
602
644
  const firstBatch = blocks.slice(0, MAX_BLOCKS_PER_APPEND);
603
645
  const remainingBlocks = blocks.slice(MAX_BLOCKS_PER_APPEND);
@@ -629,7 +671,10 @@ export class NotionExportTarget extends ExportTarget {
629
671
  },
630
672
  });
631
673
  await this.clearPageContent(notionPageId);
632
- const { blocks } = await sectionsToNotionBlocks(doc.sections, { extractReferences: true, notionClient: this.client });
674
+ const { blocks } = await sectionsToNotionBlocks(doc.sections, {
675
+ extractReferences: true,
676
+ notionClient: this.client,
677
+ });
633
678
  await this.appendRemainingBlocks(notionPageId, blocks);
634
679
  }
635
680
  async clearPageContent(pageId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.17",
3
+ "version": "0.4.19",
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",