@moxn/kb-migrate 0.4.3 → 0.4.7

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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * NotionExportTarget — exports KB documents to Notion pages.
3
+ *
4
+ * Converts KB section content (markdown) to Notion blocks using @tryfabric/martian,
5
+ * then creates or updates Notion pages via the Notion API.
6
+ *
7
+ * Supports a two-pass export algorithm:
8
+ * - Pass 1: Create all pages (without cross-references)
9
+ * - Pass 2: Append reference blocks using the KB doc ID -> Notion page ID mapping
10
+ *
11
+ * Conflict detection uses the MoxnClient's notion-mapping API endpoints
12
+ * (when available) to find existing Notion pages for a KB document.
13
+ */
14
+ import { ExportTarget, type ExportTargetConfig, type ExportTargetResult } from './base.js';
15
+ import type { DocumentDetail, DocumentListItem } from '../types.js';
16
+ export interface NotionExportConfig extends ExportTargetConfig {
17
+ /** Notion integration token */
18
+ notionToken: string;
19
+ /** Parent page ID under which to create pages */
20
+ parentPageId: string;
21
+ /** How to handle pages that already exist in Notion */
22
+ conflictStrategy: 'skip' | 'update';
23
+ /** Moxn API URL (for notion-mapping lookups) */
24
+ apiUrl: string;
25
+ /** Moxn API key */
26
+ apiKey: string;
27
+ }
28
+ /** A cross-reference extracted from section content */
29
+ export interface ExtractedKBReference {
30
+ /** The KB document ID of the source document */
31
+ sourceDocumentId: string;
32
+ /** Target KB path found in markdown links */
33
+ targetKbPath: string;
34
+ /** Display text for the reference */
35
+ displayText: string;
36
+ }
37
+ export declare class NotionExportTarget extends ExportTarget<NotionExportConfig> {
38
+ private client;
39
+ /** Cache: kbDocumentId -> notionPageId (from mapping API) */
40
+ private mappingCache;
41
+ /** Cache: notionPageId -> true (pages we've verified exist) */
42
+ private verifiedPages;
43
+ /** Map from KB path prefix -> Notion page ID (for nested page creation) */
44
+ private pathToNotionId;
45
+ /** KB path -> KB document ID mapping (built during two-pass export) */
46
+ private kbPathToDocId;
47
+ /** KB doc ID -> Notion page ID mapping (built during pass 1) */
48
+ private docIdToNotionPageId;
49
+ constructor(config: NotionExportConfig);
50
+ get targetType(): string;
51
+ get targetLocation(): string;
52
+ validate(): Promise<void>;
53
+ /**
54
+ * Register a KB document in the path -> ID index.
55
+ * Called before pass 1 so references can be resolved in pass 2.
56
+ */
57
+ registerDocument(doc: DocumentDetail): void;
58
+ /**
59
+ * After pass 1, record a successful export mapping.
60
+ * Called by the runner when a doc is created/updated successfully.
61
+ */
62
+ registerExportedPage(kbDocumentId: string, notionPageId: string): void;
63
+ /**
64
+ * Pass 2: Resolve references for a document and append link blocks to its Notion page.
65
+ *
66
+ * Finds KB path links in the document content, resolves them to Notion page IDs
67
+ * (from the current export batch or pre-existing mappings), and appends
68
+ * mention/link blocks to the Notion page.
69
+ *
70
+ * Returns the number of references resolved.
71
+ */
72
+ resolveAndAppendReferences(doc: DocumentDetail, notionPageId: string): Promise<{
73
+ resolved: number;
74
+ unresolved: number;
75
+ }>;
76
+ /**
77
+ * Resolve a KB path to a Notion page ID.
78
+ *
79
+ * Checks three sources in order:
80
+ * 1. Current export batch (docIdToNotionPageId from pass 1)
81
+ * 2. Local mapping cache
82
+ * 3. Remote notion-mapping API
83
+ */
84
+ private resolveKBPathToNotionPage;
85
+ exportDocument(doc: DocumentDetail, listItem: DocumentListItem, dryRun: boolean): Promise<ExportTargetResult>;
86
+ cleanup(): Promise<void>;
87
+ private createNotionPage;
88
+ private updateNotionPage;
89
+ private clearPageContent;
90
+ private appendRemainingBlocks;
91
+ private findExistingNotionPage;
92
+ private recordMapping;
93
+ }
@@ -0,0 +1,478 @@
1
+ /**
2
+ * NotionExportTarget — exports KB documents to Notion pages.
3
+ *
4
+ * Converts KB section content (markdown) to Notion blocks using @tryfabric/martian,
5
+ * then creates or updates Notion pages via the Notion API.
6
+ *
7
+ * Supports a two-pass export algorithm:
8
+ * - Pass 1: Create all pages (without cross-references)
9
+ * - Pass 2: Append reference blocks using the KB doc ID -> Notion page ID mapping
10
+ *
11
+ * Conflict detection uses the MoxnClient's notion-mapping API endpoints
12
+ * (when available) to find existing Notion pages for a KB document.
13
+ */
14
+ import { Client } from '@notionhq/client';
15
+ import { markdownToBlocks } from '@tryfabric/martian';
16
+ import { ExportTarget, } from './base.js';
17
+ // ============================================
18
+ // Helpers
19
+ // ============================================
20
+ const RATE_LIMIT_MS = 350;
21
+ function sleep(ms) {
22
+ return new Promise((resolve) => setTimeout(resolve, ms));
23
+ }
24
+ /** Strip <moxn:comment> tags from text, preserving the inner text. */
25
+ function stripCommentTags(text) {
26
+ return text.replace(/<moxn:comment[^>]*>([\s\S]*?)<\/moxn:comment>/g, '$1');
27
+ }
28
+ /**
29
+ * Extract KB path references from markdown text.
30
+ * Matches markdown links where the URL starts with / (KB internal paths).
31
+ * Returns the extracted references and the text with references removed.
32
+ */
33
+ function extractKBPathReferences(text) {
34
+ const references = [];
35
+ // Match [display text](/kb/path) - links starting with /
36
+ const KB_PATH_RE = /\[([^\]]+)\]\((\/[^)]+)\)/g;
37
+ const cleanedText = text.replace(KB_PATH_RE, (match, displayText, path) => {
38
+ // Only treat as KB reference if it looks like a KB document path
39
+ // (starts with / and doesn't look like an external URL fragment)
40
+ if (path.startsWith('/') && !path.startsWith('//')) {
41
+ references.push({ targetKbPath: path, displayText });
42
+ // Replace with plain text (the link text) since we'll add references separately
43
+ return displayText;
44
+ }
45
+ return match;
46
+ });
47
+ return { cleanedText, references };
48
+ }
49
+ /**
50
+ * Convert a KB document's sections into a single markdown string.
51
+ * Section names become H2 headings (mirrors the import convention).
52
+ *
53
+ * If extractReferences is true, also extracts KB path references from
54
+ * the content and returns them separately (for two-pass export).
55
+ */
56
+ function sectionsToMarkdown(sections, options) {
57
+ const parts = [];
58
+ const allReferences = [];
59
+ const extractRefs = options?.extractReferences ?? false;
60
+ for (const section of sections) {
61
+ parts.push(`## ${section.name}\n`);
62
+ for (const block of section.content) {
63
+ if (block.blockType === 'text' && block.text) {
64
+ let text = stripCommentTags(block.text);
65
+ if (extractRefs) {
66
+ const { cleanedText, references } = extractKBPathReferences(text);
67
+ text = cleanedText;
68
+ allReferences.push(...references);
69
+ }
70
+ parts.push(text);
71
+ parts.push('');
72
+ }
73
+ else if (block.blockType === 'image' && block.url) {
74
+ parts.push(`![${block.alt || ''}](${block.url})`);
75
+ parts.push('');
76
+ }
77
+ else if (block.blockType === 'document' && block.url) {
78
+ parts.push(`[${block.filename || 'document'}](${block.url})`);
79
+ parts.push('');
80
+ }
81
+ else if (block.blockType === 'csv' && block.url) {
82
+ parts.push(`[${block.filename || 'data.csv'}](${block.url})`);
83
+ parts.push('');
84
+ }
85
+ }
86
+ }
87
+ return { markdown: parts.join('\n').trim(), references: allReferences };
88
+ }
89
+ // Max 100 blocks per API call
90
+ const MAX_BLOCKS_PER_APPEND = 100;
91
+ // ============================================
92
+ // Target
93
+ // ============================================
94
+ export class NotionExportTarget extends ExportTarget {
95
+ client;
96
+ /** Cache: kbDocumentId -> notionPageId (from mapping API) */
97
+ mappingCache = new Map();
98
+ /** Cache: notionPageId -> true (pages we've verified exist) */
99
+ verifiedPages = new Set();
100
+ /** Map from KB path prefix -> Notion page ID (for nested page creation) */
101
+ pathToNotionId = new Map();
102
+ /** KB path -> KB document ID mapping (built during two-pass export) */
103
+ kbPathToDocId = new Map();
104
+ /** KB doc ID -> Notion page ID mapping (built during pass 1) */
105
+ docIdToNotionPageId = new Map();
106
+ constructor(config) {
107
+ super(config);
108
+ this.client = new Client({ auth: config.notionToken });
109
+ }
110
+ get targetType() {
111
+ return 'notion';
112
+ }
113
+ get targetLocation() {
114
+ return `Notion (parent: ${this.config.parentPageId})`;
115
+ }
116
+ // ============================================
117
+ // Validation
118
+ // ============================================
119
+ async validate() {
120
+ console.log('Validating Notion API token...');
121
+ try {
122
+ await this.client.users.me({});
123
+ console.log(' Token valid.');
124
+ }
125
+ catch (error) {
126
+ throw new Error(`Notion token invalid: ${error instanceof Error ? error.message : error}`);
127
+ }
128
+ console.log('Verifying parent page...');
129
+ try {
130
+ await sleep(RATE_LIMIT_MS);
131
+ await this.client.pages.retrieve({ page_id: this.config.parentPageId });
132
+ console.log(' Parent page accessible.');
133
+ }
134
+ catch (error) {
135
+ throw new Error(`Parent page not accessible: ${error instanceof Error ? error.message : error}`);
136
+ }
137
+ }
138
+ // ============================================
139
+ // Two-Pass Export Support
140
+ // ============================================
141
+ /**
142
+ * Register a KB document in the path -> ID index.
143
+ * Called before pass 1 so references can be resolved in pass 2.
144
+ */
145
+ registerDocument(doc) {
146
+ this.kbPathToDocId.set(doc.path, doc.id);
147
+ }
148
+ /**
149
+ * After pass 1, record a successful export mapping.
150
+ * Called by the runner when a doc is created/updated successfully.
151
+ */
152
+ registerExportedPage(kbDocumentId, notionPageId) {
153
+ this.docIdToNotionPageId.set(kbDocumentId, notionPageId);
154
+ }
155
+ /**
156
+ * Pass 2: Resolve references for a document and append link blocks to its Notion page.
157
+ *
158
+ * Finds KB path links in the document content, resolves them to Notion page IDs
159
+ * (from the current export batch or pre-existing mappings), and appends
160
+ * mention/link blocks to the Notion page.
161
+ *
162
+ * Returns the number of references resolved.
163
+ */
164
+ async resolveAndAppendReferences(doc, notionPageId) {
165
+ // Extract references from section content
166
+ const { references } = sectionsToMarkdown(doc.sections, { extractReferences: true });
167
+ if (references.length === 0) {
168
+ return { resolved: 0, unresolved: 0 };
169
+ }
170
+ // Deduplicate references by target path
171
+ const seen = new Set();
172
+ const uniqueRefs = references.filter((ref) => {
173
+ if (seen.has(ref.targetKbPath))
174
+ return false;
175
+ seen.add(ref.targetKbPath);
176
+ return true;
177
+ });
178
+ const referenceBlocks = [];
179
+ let resolved = 0;
180
+ let unresolved = 0;
181
+ for (const ref of uniqueRefs) {
182
+ const targetNotionPageId = await this.resolveKBPathToNotionPage(ref.targetKbPath);
183
+ if (targetNotionPageId) {
184
+ // Create a mention block linking to the Notion page
185
+ referenceBlocks.push({
186
+ object: 'block',
187
+ type: 'paragraph',
188
+ paragraph: {
189
+ rich_text: [
190
+ { type: 'text', text: { content: 'See also: ' } },
191
+ {
192
+ type: 'mention',
193
+ mention: {
194
+ type: 'page',
195
+ page: { id: targetNotionPageId },
196
+ },
197
+ },
198
+ ],
199
+ },
200
+ });
201
+ resolved++;
202
+ }
203
+ else {
204
+ // Graceful degradation: plain text reference
205
+ referenceBlocks.push({
206
+ object: 'block',
207
+ type: 'paragraph',
208
+ paragraph: {
209
+ rich_text: [
210
+ {
211
+ type: 'text',
212
+ text: { content: `See also: ${ref.displayText} (${ref.targetKbPath})` },
213
+ annotations: { italic: true, color: 'gray' },
214
+ },
215
+ ],
216
+ },
217
+ });
218
+ unresolved++;
219
+ }
220
+ }
221
+ if (referenceBlocks.length > 0) {
222
+ // Add a divider before references section
223
+ const blocksToAppend = [
224
+ { object: 'block', type: 'divider', divider: {} },
225
+ {
226
+ object: 'block',
227
+ type: 'heading_3',
228
+ heading_3: {
229
+ rich_text: [{ type: 'text', text: { content: 'References' } }],
230
+ },
231
+ },
232
+ ...referenceBlocks,
233
+ ];
234
+ await this.appendRemainingBlocks(notionPageId, blocksToAppend);
235
+ }
236
+ return { resolved, unresolved };
237
+ }
238
+ /**
239
+ * Resolve a KB path to a Notion page ID.
240
+ *
241
+ * Checks three sources in order:
242
+ * 1. Current export batch (docIdToNotionPageId from pass 1)
243
+ * 2. Local mapping cache
244
+ * 3. Remote notion-mapping API
245
+ */
246
+ async resolveKBPathToNotionPage(kbPath) {
247
+ // 1. Try to find the KB doc ID from path
248
+ const kbDocId = this.kbPathToDocId.get(kbPath);
249
+ if (kbDocId) {
250
+ // 2. Check if it was exported in this batch
251
+ const batchNotionId = this.docIdToNotionPageId.get(kbDocId);
252
+ if (batchNotionId)
253
+ return batchNotionId;
254
+ // 3. Check mapping cache / API
255
+ const mappedNotionId = await this.findExistingNotionPage(kbDocId);
256
+ if (mappedNotionId)
257
+ return mappedNotionId;
258
+ }
259
+ return null;
260
+ }
261
+ // ============================================
262
+ // Export (Pass 1)
263
+ // ============================================
264
+ async exportDocument(doc, listItem, dryRun) {
265
+ const startTime = Date.now();
266
+ try {
267
+ const existingNotionPageId = await this.findExistingNotionPage(doc.id);
268
+ if (existingNotionPageId) {
269
+ if (this.config.conflictStrategy === 'skip') {
270
+ this.registerExportedPage(doc.id, existingNotionPageId);
271
+ return {
272
+ documentId: doc.id,
273
+ documentPath: doc.path,
274
+ status: 'skipped',
275
+ externalId: existingNotionPageId,
276
+ duration: Date.now() - startTime,
277
+ };
278
+ }
279
+ if (dryRun) {
280
+ return {
281
+ documentId: doc.id,
282
+ documentPath: doc.path,
283
+ status: 'updated',
284
+ externalId: existingNotionPageId,
285
+ duration: Date.now() - startTime,
286
+ };
287
+ }
288
+ await this.updateNotionPage(existingNotionPageId, doc);
289
+ await this.recordMapping(doc.id, existingNotionPageId);
290
+ this.registerExportedPage(doc.id, existingNotionPageId);
291
+ return {
292
+ documentId: doc.id,
293
+ documentPath: doc.path,
294
+ status: 'updated',
295
+ externalId: existingNotionPageId,
296
+ duration: Date.now() - startTime,
297
+ };
298
+ }
299
+ if (dryRun) {
300
+ return {
301
+ documentId: doc.id,
302
+ documentPath: doc.path,
303
+ status: 'created',
304
+ duration: Date.now() - startTime,
305
+ };
306
+ }
307
+ const notionPageId = await this.createNotionPage(doc);
308
+ await this.recordMapping(doc.id, notionPageId);
309
+ this.registerExportedPage(doc.id, notionPageId);
310
+ return {
311
+ documentId: doc.id,
312
+ documentPath: doc.path,
313
+ status: 'created',
314
+ externalId: notionPageId,
315
+ duration: Date.now() - startTime,
316
+ };
317
+ }
318
+ catch (error) {
319
+ console.error(`Export failed for ${doc.path}: ${error instanceof Error ? error.message : error}`);
320
+ return {
321
+ documentId: doc.id,
322
+ documentPath: doc.path,
323
+ status: 'failed',
324
+ error: error instanceof Error ? error.message : String(error),
325
+ duration: Date.now() - startTime,
326
+ };
327
+ }
328
+ }
329
+ async cleanup() {
330
+ // No-op
331
+ }
332
+ // ============================================
333
+ // Notion page creation / update
334
+ // ============================================
335
+ async createNotionPage(doc) {
336
+ const { markdown } = sectionsToMarkdown(doc.sections, { extractReferences: true });
337
+ const blocks = markdownToBlocks(markdown);
338
+ // First batch: up to 100 blocks as children of the new page
339
+ const firstBatch = blocks.slice(0, MAX_BLOCKS_PER_APPEND);
340
+ const remainingBlocks = blocks.slice(MAX_BLOCKS_PER_APPEND);
341
+ await sleep(RATE_LIMIT_MS);
342
+ const response = await this.client.pages.create({
343
+ parent: { page_id: this.config.parentPageId },
344
+ properties: {
345
+ title: {
346
+ type: 'title',
347
+ title: [{ type: 'text', text: { content: doc.name } }],
348
+ },
349
+ },
350
+ children: firstBatch,
351
+ });
352
+ const pageId = response.id;
353
+ // Append remaining blocks in batches
354
+ await this.appendRemainingBlocks(pageId, remainingBlocks);
355
+ return pageId;
356
+ }
357
+ async updateNotionPage(notionPageId, doc) {
358
+ await sleep(RATE_LIMIT_MS);
359
+ await this.client.pages.update({
360
+ page_id: notionPageId,
361
+ properties: {
362
+ title: {
363
+ type: 'title',
364
+ title: [{ type: 'text', text: { content: doc.name } }],
365
+ },
366
+ },
367
+ });
368
+ await this.clearPageContent(notionPageId);
369
+ const { markdown } = sectionsToMarkdown(doc.sections, { extractReferences: true });
370
+ const blocks = markdownToBlocks(markdown);
371
+ await this.appendRemainingBlocks(notionPageId, blocks);
372
+ }
373
+ async clearPageContent(pageId) {
374
+ await sleep(RATE_LIMIT_MS);
375
+ const children = await this.client.blocks.children.list({
376
+ block_id: pageId,
377
+ page_size: 100,
378
+ });
379
+ for (const block of children.results) {
380
+ try {
381
+ await sleep(RATE_LIMIT_MS);
382
+ const deletePromise = this.client.blocks.delete({ block_id: block.id });
383
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Block delete timeout')), 10000));
384
+ await Promise.race([deletePromise, timeoutPromise]);
385
+ }
386
+ catch {
387
+ // Continue with other blocks even if one fails
388
+ }
389
+ }
390
+ // Handle pagination
391
+ if (children.has_more && children.next_cursor) {
392
+ let cursor = children.next_cursor;
393
+ while (cursor) {
394
+ try {
395
+ await sleep(RATE_LIMIT_MS);
396
+ const more = await this.client.blocks.children.list({
397
+ block_id: pageId,
398
+ start_cursor: cursor,
399
+ page_size: 100,
400
+ });
401
+ for (const block of more.results) {
402
+ try {
403
+ await sleep(RATE_LIMIT_MS);
404
+ const deletePromise = this.client.blocks.delete({ block_id: block.id });
405
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Block delete timeout')), 10000));
406
+ await Promise.race([deletePromise, timeoutPromise]);
407
+ }
408
+ catch {
409
+ // Continue on failure
410
+ }
411
+ }
412
+ cursor = more.has_more ? more.next_cursor : null;
413
+ }
414
+ catch {
415
+ break;
416
+ }
417
+ }
418
+ }
419
+ }
420
+ async appendRemainingBlocks(pageId, blocks) {
421
+ for (let i = 0; i < blocks.length; i += MAX_BLOCKS_PER_APPEND) {
422
+ const batch = blocks.slice(i, i + MAX_BLOCKS_PER_APPEND);
423
+ await sleep(RATE_LIMIT_MS);
424
+ await this.client.blocks.children.append({
425
+ block_id: pageId,
426
+ children: batch,
427
+ });
428
+ }
429
+ }
430
+ // ============================================
431
+ // Mapping lookups
432
+ // ============================================
433
+ async findExistingNotionPage(kbDocumentId) {
434
+ // Check cache first
435
+ const cached = this.mappingCache.get(kbDocumentId);
436
+ if (cached)
437
+ return cached;
438
+ // Try notion-mapping API
439
+ try {
440
+ const response = await fetch(`${this.config.apiUrl}/api/v1/kb/notion-mappings/by-document/${kbDocumentId}`, {
441
+ headers: { 'x-api-key': this.config.apiKey },
442
+ });
443
+ if (response.ok) {
444
+ const data = (await response.json());
445
+ if (data.mapping?.notionPageId) {
446
+ this.mappingCache.set(kbDocumentId, data.mapping.notionPageId);
447
+ return data.mapping.notionPageId;
448
+ }
449
+ }
450
+ }
451
+ catch {
452
+ // Mapping API might not be deployed yet - that's OK, treat as no mapping
453
+ }
454
+ return null;
455
+ }
456
+ async recordMapping(kbDocumentId, notionPageId) {
457
+ this.mappingCache.set(kbDocumentId, notionPageId);
458
+ // Try to save mapping via API (best-effort)
459
+ try {
460
+ await fetch(`${this.config.apiUrl}/api/v1/kb/notion-mappings`, {
461
+ method: 'POST',
462
+ headers: {
463
+ 'Content-Type': 'application/json',
464
+ 'x-api-key': this.config.apiKey,
465
+ },
466
+ body: JSON.stringify({
467
+ kbDocumentId,
468
+ notionPageId,
469
+ importSource: 'export',
470
+ syncDirection: 'kb_to_notion',
471
+ }),
472
+ });
473
+ }
474
+ catch {
475
+ // Best-effort — mapping API might not be available
476
+ }
477
+ }
478
+ }
package/dist/types.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * to kb-migrate (local filesystem paths) — MoxnClient converts them to
9
9
  * `type: 'storage'` before sending to the KB API.
10
10
  */
11
- export type ContentBlock = TextBlock | ImageRemoteBlock | ImageFileBlock | DocumentRemoteBlock | DocumentFileBlock | CsvRemoteBlock | CsvFileBlock;
11
+ export type ContentBlock = TextBlock | ImageRemoteBlock | ImageFileBlock | DocumentRemoteBlock | DocumentFileBlock | CsvRemoteBlock | CsvFileBlock | DatabaseEmbedBlock;
12
12
  export interface TextBlock {
13
13
  blockType: 'text';
14
14
  text: string;
@@ -65,6 +65,10 @@ export interface CsvFileBlock {
65
65
  headers?: string[];
66
66
  rowCount?: number;
67
67
  }
68
+ export interface DatabaseEmbedBlock {
69
+ blockType: 'database_embed';
70
+ databaseId: string;
71
+ }
68
72
  /**
69
73
  * A section to be created in a document
70
74
  */
@@ -98,6 +102,12 @@ export interface ExtractedDocument {
98
102
  sourcePath: string;
99
103
  /** Cross-references discovered during resolution */
100
104
  references?: ExtractedReference[];
105
+ /** Source-specific metadata (e.g., Notion page ID for bidirectional sync) */
106
+ metadata?: {
107
+ notionPageId?: string;
108
+ notionTitle?: string;
109
+ [key: string]: unknown;
110
+ };
101
111
  }
102
112
  /**
103
113
  * Status of a single document migration
@@ -162,6 +172,8 @@ export interface MigrationOptions {
162
172
  aiAccess?: 'edit' | 'read' | 'none';
163
173
  /** Convenience flag: 'team' = read, 'private' = none */
164
174
  visibility?: 'team' | 'private';
175
+ /** Date filter for source documents */
176
+ dateFilter?: import('./date-filter.js').DateFilter;
165
177
  }
166
178
  /**
167
179
  * Error response from API when document already exists
@@ -175,13 +187,14 @@ export interface ConflictError {
175
187
  * A content block as returned by the KB API
176
188
  */
177
189
  export interface ExportContentBlock {
178
- blockType: 'text' | 'image' | 'document' | 'csv';
190
+ blockType: 'text' | 'image' | 'document' | 'csv' | 'database_embed';
179
191
  text?: string;
180
192
  url?: string;
181
193
  mimeType?: string;
182
194
  alt?: string;
183
195
  filename?: string;
184
196
  storageKey?: string;
197
+ databaseId?: string;
185
198
  }
186
199
  /**
187
200
  * A section within a document (API response)
@@ -201,6 +214,7 @@ export interface DocumentListItem {
201
214
  name: string;
202
215
  description: string | null;
203
216
  createdAt: string;
217
+ updatedAt: string | null;
204
218
  }
205
219
  /**
206
220
  * Full document detail with sections
@@ -271,4 +285,6 @@ export interface ExportOptions {
271
285
  pdfDir: string;
272
286
  csvDir: string;
273
287
  dryRun: boolean;
288
+ /** Date filter for exported documents */
289
+ dateFilter?: import('./date-filter.js').DateFilter;
274
290
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.3",
3
+ "version": "0.4.7",
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",
@@ -49,6 +49,26 @@
49
49
  "./client": {
50
50
  "types": "./dist/client.d.ts",
51
51
  "default": "./dist/client.js"
52
+ },
53
+ "./date-filter": {
54
+ "types": "./dist/date-filter.d.ts",
55
+ "default": "./dist/date-filter.js"
56
+ },
57
+ "./targets/base": {
58
+ "types": "./dist/targets/base.d.ts",
59
+ "default": "./dist/targets/base.js"
60
+ },
61
+ "./targets/notion": {
62
+ "types": "./dist/targets/notion.d.ts",
63
+ "default": "./dist/targets/notion.js"
64
+ },
65
+ "./targets": {
66
+ "types": "./dist/targets/index.d.ts",
67
+ "default": "./dist/targets/index.js"
68
+ },
69
+ "./export-notion": {
70
+ "types": "./dist/export-notion.d.ts",
71
+ "default": "./dist/export-notion.js"
52
72
  }
53
73
  },
54
74
  "bin": {
@@ -62,6 +82,8 @@
62
82
  "prepublishOnly": "npm run build"
63
83
  },
64
84
  "dependencies": {
85
+ "@notionhq/client": "^2.3.0",
86
+ "@tryfabric/martian": "^1.2.4",
65
87
  "commander": "^12.0.0",
66
88
  "glob": "^10.0.0",
67
89
  "remark-parse": "^11.0.0",