@moxn/kb-migrate 0.4.28 → 0.4.30

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,131 @@
1
+ /**
2
+ * Discovery: walk Microsoft Graph's OneNote hierarchy and flatten into a
3
+ * list of pages with their computed KB path.
4
+ *
5
+ * The hierarchy is: Notebook → SectionGroup* → Section → Page.
6
+ * SectionGroups can nest; OneNote caps at 4 levels of nesting in practice
7
+ * and we cap recursion at 4 to avoid accidental cycles.
8
+ *
9
+ * Selection semantics: callers pass a set of selected Graph IDs (any mix of
10
+ * notebook / sectionGroup / section / page). A page is included iff its
11
+ * own ID or any ancestor's ID is in the set. If the set is empty, every
12
+ * page is included.
13
+ */
14
+ const MAX_GROUP_DEPTH = 4;
15
+ /**
16
+ * Normalize any human text into a ltree-safe slug segment.
17
+ * - lowercases
18
+ * - replaces spaces/underscores/periods with hyphens (ltree forbids _ and .)
19
+ * - drops anything outside [a-z0-9-]
20
+ * - collapses runs of hyphens; trims leading/trailing hyphens
21
+ * Falls back to `untitled` when the result is empty.
22
+ */
23
+ export function slugifyPathSegment(raw) {
24
+ const ascii = raw
25
+ .normalize('NFKD')
26
+ .replace(/[̀-ͯ]/g, '') // strip combining marks
27
+ .toLowerCase()
28
+ .replace(/[\s_.]+/g, '-')
29
+ .replace(/[^a-z0-9-]/g, '-')
30
+ .replace(/-+/g, '-')
31
+ .replace(/^-+|-+$/g, '');
32
+ return ascii.length > 0 ? ascii : 'untitled';
33
+ }
34
+ export async function discoverOneNotePages(client, opts = {}) {
35
+ const pathPrefix = opts.pathPrefix
36
+ ? opts.pathPrefix.replace(/^\/+|\/+$/g, '')
37
+ : 'imported/onenote';
38
+ const sel = opts.selectedIds && opts.selectedIds.size > 0 ? opts.selectedIds : null;
39
+ const notebooks = await client.listNotebooks();
40
+ const out = [];
41
+ for (const notebook of notebooks) {
42
+ const notebookSelected = !sel || sel.has(notebook.id);
43
+ // When nothing at notebook-or-below is selected we can skip entirely.
44
+ // Cheap upper bound: if sel is set and notebook isn't in it, we still
45
+ // need to walk in case a descendant is selected.
46
+ const notebookSlug = slugifyPathSegment(notebook.displayName);
47
+ await walkNotebook(client, notebook, [pathPrefix, notebookSlug], notebookSelected, sel, opts, out);
48
+ }
49
+ out.sort((a, b) => a.kbPath.localeCompare(b.kbPath));
50
+ return out;
51
+ }
52
+ async function walkNotebook(client, notebook, pathSegs, ancestorSelected, sel, opts, out) {
53
+ const [sectionGroups, sections] = await Promise.all([
54
+ client.listSectionGroups(notebook.id),
55
+ client.listSectionsInNotebook(notebook.id),
56
+ ]);
57
+ const ctx = {
58
+ notebookId: notebook.id,
59
+ notebookName: notebook.displayName,
60
+ sectionGroups: [],
61
+ };
62
+ for (const section of sections) {
63
+ const sectionSelected = ancestorSelected || (!!sel && sel.has(section.id));
64
+ await walkSection(client, section, pathSegs, ctx, sectionSelected, sel, opts, out);
65
+ }
66
+ for (const group of sectionGroups) {
67
+ await walkSectionGroup(client, group, pathSegs, ctx, ancestorSelected, sel, opts, out, 1);
68
+ }
69
+ }
70
+ async function walkSectionGroup(client, group, pathSegs, ctx, ancestorSelected, sel, opts, out, depth) {
71
+ if (depth > MAX_GROUP_DEPTH)
72
+ return;
73
+ const selfSelected = ancestorSelected || (!!sel && sel.has(group.id));
74
+ const nextSegs = [...pathSegs, slugifyPathSegment(group.displayName)];
75
+ const nextCtx = {
76
+ ...ctx,
77
+ sectionGroups: [...ctx.sectionGroups, { id: group.id, name: group.displayName }],
78
+ };
79
+ const [nestedGroups, groupSections] = await Promise.all([
80
+ client.listNestedSectionGroups(group.id),
81
+ client.listSectionsInGroup(group.id),
82
+ ]);
83
+ for (const section of groupSections) {
84
+ const sectionSelected = selfSelected || (!!sel && sel.has(section.id));
85
+ await walkSection(client, section, nextSegs, nextCtx, sectionSelected, sel, opts, out);
86
+ }
87
+ for (const nested of nestedGroups) {
88
+ await walkSectionGroup(client, nested, nextSegs, nextCtx, selfSelected, sel, opts, out, depth + 1);
89
+ }
90
+ }
91
+ async function walkSection(client, section, pathSegs, ctx, ancestorSelected, sel, opts, out) {
92
+ const pages = await client.listPages(section.id, {
93
+ modifiedAfter: opts.modifiedAfter,
94
+ modifiedBefore: opts.modifiedBefore,
95
+ });
96
+ const sectionSeg = slugifyPathSegment(section.displayName);
97
+ for (const page of pages) {
98
+ const pageSelected = ancestorSelected || (!!sel && sel.has(page.id));
99
+ if (!pageSelected)
100
+ continue;
101
+ const pageSeg = slugifyPathSegment(page.title ?? 'untitled');
102
+ out.push(toDiscovered(page, section, [...pathSegs, sectionSeg, pageSeg], ctx));
103
+ }
104
+ }
105
+ function toDiscovered(page, section, segs, ctx) {
106
+ return {
107
+ onenotePageId: page.id,
108
+ title: page.title ?? 'Untitled',
109
+ kbPath: '/' + segs.join('/'),
110
+ notebookId: ctx.notebookId,
111
+ notebookName: ctx.notebookName,
112
+ sectionId: section.id,
113
+ sectionName: section.displayName,
114
+ sectionGroups: ctx.sectionGroups,
115
+ onenoteWebUrl: page.links?.oneNoteWebUrl?.href,
116
+ createdDateTime: page.createdDateTime,
117
+ lastModifiedDateTime: page.lastModifiedDateTime,
118
+ createdByEmail: page.createdBy?.user?.email ?? page.createdBy?.user?.userPrincipalName,
119
+ createdByDisplayName: page.createdBy?.user?.displayName,
120
+ lastModifiedByEmail: page.lastModifiedBy?.user?.email ??
121
+ page.lastModifiedBy?.user?.userPrincipalName,
122
+ };
123
+ }
124
+ /**
125
+ * Join kbPath segments into `/a/b/c` — public helper used by the orchestrator
126
+ * when it needs to derive a path outside the discovery walk.
127
+ */
128
+ export function joinKbPath(...segments) {
129
+ const filtered = segments.filter((s) => Boolean(s && s.length));
130
+ return '/' + filtered.map((s) => s.replace(/^\/+|\/+$/g, '')).join('/');
131
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Shared types for the OneNote source (Microsoft Graph).
3
+ *
4
+ * These mirror the Graph API payloads we actually read. Graph's own TypeScript
5
+ * types are available via @microsoft/microsoft-graph-types but include every
6
+ * field of every shape; we trim to what the importer touches.
7
+ */
8
+ export interface GraphIdentity {
9
+ user?: {
10
+ id?: string;
11
+ displayName?: string;
12
+ email?: string;
13
+ /** work/school accounts — full UPN */
14
+ userPrincipalName?: string;
15
+ };
16
+ }
17
+ export interface OneNoteNotebook {
18
+ id: string;
19
+ displayName: string;
20
+ isDefault?: boolean;
21
+ isShared?: boolean;
22
+ createdDateTime?: string;
23
+ lastModifiedDateTime?: string;
24
+ createdBy?: GraphIdentity;
25
+ lastModifiedBy?: GraphIdentity;
26
+ links?: {
27
+ oneNoteClientUrl?: {
28
+ href: string;
29
+ };
30
+ oneNoteWebUrl?: {
31
+ href: string;
32
+ };
33
+ };
34
+ }
35
+ export interface OneNoteSectionGroup {
36
+ id: string;
37
+ displayName: string;
38
+ /** ID of the notebook this group lives in */
39
+ parentNotebookId?: string;
40
+ /** ID of parent section group (for nested groups) */
41
+ parentSectionGroupId?: string;
42
+ createdDateTime?: string;
43
+ lastModifiedDateTime?: string;
44
+ }
45
+ export interface OneNoteSection {
46
+ id: string;
47
+ displayName: string;
48
+ parentNotebookId?: string;
49
+ parentSectionGroupId?: string;
50
+ createdDateTime?: string;
51
+ lastModifiedDateTime?: string;
52
+ }
53
+ export interface OneNotePage {
54
+ id: string;
55
+ title: string;
56
+ createdDateTime?: string;
57
+ lastModifiedDateTime?: string;
58
+ contentUrl?: string;
59
+ parentSection?: {
60
+ id: string;
61
+ displayName?: string;
62
+ };
63
+ parentNotebook?: {
64
+ id: string;
65
+ displayName?: string;
66
+ };
67
+ createdBy?: GraphIdentity;
68
+ lastModifiedBy?: GraphIdentity;
69
+ links?: {
70
+ oneNoteClientUrl?: {
71
+ href: string;
72
+ };
73
+ oneNoteWebUrl?: {
74
+ href: string;
75
+ };
76
+ };
77
+ }
78
+ /**
79
+ * A page flattened with its full derived kbPath, ready for the fan-out task.
80
+ */
81
+ export interface DiscoveredOneNotePage {
82
+ onenotePageId: string;
83
+ title: string;
84
+ kbPath: string;
85
+ notebookId: string;
86
+ notebookName: string;
87
+ sectionId: string;
88
+ sectionName: string;
89
+ /** Zero or more nested section groups, outermost first */
90
+ sectionGroups: Array<{
91
+ id: string;
92
+ name: string;
93
+ }>;
94
+ onenoteWebUrl?: string;
95
+ createdDateTime?: string;
96
+ lastModifiedDateTime?: string;
97
+ createdByEmail?: string;
98
+ createdByDisplayName?: string;
99
+ lastModifiedByEmail?: string;
100
+ }
101
+ /**
102
+ * A cross-page reference captured during HTML conversion, to be resolved
103
+ * into a KB cross-link after all pages are imported.
104
+ */
105
+ export interface OneNoteExtractedReference {
106
+ sectionIndex: number;
107
+ targetOnenotePageId: string;
108
+ displayText: string;
109
+ }
110
+ /**
111
+ * Output of the HTML converter: what the fan-out task hands to
112
+ * `create_document` along with per-page cross-references.
113
+ */
114
+ export interface ConvertedOneNotePage {
115
+ title: string;
116
+ sections: import('../../types.js').SectionInput[];
117
+ extractedReferences: OneNoteExtractedReference[];
118
+ /** Count of images/attachments that were re-hosted */
119
+ mediaCount: number;
120
+ /** Count of content items skipped (unknown MIME, oversized, etc.) */
121
+ skippedItemCount: number;
122
+ /** Human-readable reasons for skipped items, logged back to the job. */
123
+ skippedReasons: string[];
124
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared types for the OneNote source (Microsoft Graph).
3
+ *
4
+ * These mirror the Graph API payloads we actually read. Graph's own TypeScript
5
+ * types are available via @microsoft/microsoft-graph-types but include every
6
+ * field of every shape; we trim to what the importer touches.
7
+ */
8
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.28",
3
+ "version": "0.4.30",
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",
@@ -38,6 +38,26 @@
38
38
  "types": "./dist/sources/local.d.ts",
39
39
  "default": "./dist/sources/local.js"
40
40
  },
41
+ "./sources/onenote": {
42
+ "types": "./dist/sources/onenote/index.d.ts",
43
+ "default": "./dist/sources/onenote/index.js"
44
+ },
45
+ "./sources/onenote/onenote-api": {
46
+ "types": "./dist/sources/onenote/onenote-api.d.ts",
47
+ "default": "./dist/sources/onenote/onenote-api.js"
48
+ },
49
+ "./sources/onenote/onenote-auth": {
50
+ "types": "./dist/sources/onenote/onenote-auth.d.ts",
51
+ "default": "./dist/sources/onenote/onenote-auth.js"
52
+ },
53
+ "./sources/onenote/onenote-html": {
54
+ "types": "./dist/sources/onenote/onenote-html.d.ts",
55
+ "default": "./dist/sources/onenote/onenote-html.js"
56
+ },
57
+ "./sources/onenote/onenote-tree": {
58
+ "types": "./dist/sources/onenote/onenote-tree.d.ts",
59
+ "default": "./dist/sources/onenote/onenote-tree.js"
60
+ },
41
61
  "./sources/base": {
42
62
  "types": "./dist/sources/base.d.ts",
43
63
  "default": "./dist/sources/base.js"
@@ -82,11 +102,14 @@
82
102
  "prepublishOnly": "npm run build"
83
103
  },
84
104
  "dependencies": {
105
+ "@azure/msal-node": "^3.8.10",
106
+ "@microsoft/microsoft-graph-client": "^3.0.7",
85
107
  "@moxn/kb-migrate": "^0.4.14",
86
108
  "@notionhq/client": "^5.9.0",
87
109
  "@tryfabric/martian": "^1.2.4",
88
110
  "commander": "^12.0.0",
89
111
  "glob": "^10.0.0",
112
+ "node-html-parser": "^6.1.13",
90
113
  "remark-parse": "^11.0.0",
91
114
  "unified": "^11.0.0"
92
115
  },