@catafal/notion-cli 5.9.0
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.
- package/LICENSE +21 -0
- package/README.md +552 -0
- package/bin/dev +17 -0
- package/bin/dev.cmd +3 -0
- package/bin/run +14 -0
- package/bin/run.cmd +3 -0
- package/dist/base-command.d.ts +73 -0
- package/dist/base-command.js +179 -0
- package/dist/base-flags.d.ts +14 -0
- package/dist/base-flags.js +59 -0
- package/dist/cache.d.ts +84 -0
- package/dist/cache.js +351 -0
- package/dist/commands/append.d.ts +37 -0
- package/dist/commands/append.js +120 -0
- package/dist/commands/batch/delete.d.ts +42 -0
- package/dist/commands/batch/delete.js +199 -0
- package/dist/commands/batch/retrieve.d.ts +43 -0
- package/dist/commands/batch/retrieve.js +272 -0
- package/dist/commands/block/append.d.ts +42 -0
- package/dist/commands/block/append.js +219 -0
- package/dist/commands/block/delete.d.ts +30 -0
- package/dist/commands/block/delete.js +97 -0
- package/dist/commands/block/retrieve/children.d.ts +31 -0
- package/dist/commands/block/retrieve/children.js +177 -0
- package/dist/commands/block/retrieve.d.ts +30 -0
- package/dist/commands/block/retrieve.js +101 -0
- package/dist/commands/block/update.d.ts +45 -0
- package/dist/commands/block/update.js +242 -0
- package/dist/commands/bookmark/list.d.ts +30 -0
- package/dist/commands/bookmark/list.js +60 -0
- package/dist/commands/bookmark/remove.d.ts +26 -0
- package/dist/commands/bookmark/remove.js +47 -0
- package/dist/commands/bookmark/set.d.ts +29 -0
- package/dist/commands/bookmark/set.js +96 -0
- package/dist/commands/browse.d.ts +13 -0
- package/dist/commands/browse.js +44 -0
- package/dist/commands/cache/info.d.ts +19 -0
- package/dist/commands/cache/info.js +145 -0
- package/dist/commands/config/set-token.d.ts +22 -0
- package/dist/commands/config/set-token.js +137 -0
- package/dist/commands/daily/index.d.ts +32 -0
- package/dist/commands/daily/index.js +135 -0
- package/dist/commands/daily/setup.d.ts +42 -0
- package/dist/commands/daily/setup.js +149 -0
- package/dist/commands/db/create.d.ts +31 -0
- package/dist/commands/db/create.js +124 -0
- package/dist/commands/db/query.d.ts +41 -0
- package/dist/commands/db/query.js +360 -0
- package/dist/commands/db/retrieve.d.ts +33 -0
- package/dist/commands/db/retrieve.js +134 -0
- package/dist/commands/db/schema.d.ts +32 -0
- package/dist/commands/db/schema.js +308 -0
- package/dist/commands/db/update.d.ts +31 -0
- package/dist/commands/db/update.js +117 -0
- package/dist/commands/doctor.d.ts +50 -0
- package/dist/commands/doctor.js +420 -0
- package/dist/commands/init.d.ts +65 -0
- package/dist/commands/init.js +479 -0
- package/dist/commands/list.d.ts +29 -0
- package/dist/commands/list.js +219 -0
- package/dist/commands/open.d.ts +29 -0
- package/dist/commands/open.js +100 -0
- package/dist/commands/page/create.d.ts +33 -0
- package/dist/commands/page/create.js +261 -0
- package/dist/commands/page/delete.d.ts +36 -0
- package/dist/commands/page/delete.js +107 -0
- package/dist/commands/page/export.d.ts +38 -0
- package/dist/commands/page/export.js +120 -0
- package/dist/commands/page/retrieve/property_item.d.ts +24 -0
- package/dist/commands/page/retrieve/property_item.js +75 -0
- package/dist/commands/page/retrieve.d.ts +36 -0
- package/dist/commands/page/retrieve.js +244 -0
- package/dist/commands/page/update.d.ts +34 -0
- package/dist/commands/page/update.js +184 -0
- package/dist/commands/quick.d.ts +35 -0
- package/dist/commands/quick.js +168 -0
- package/dist/commands/search.d.ts +43 -0
- package/dist/commands/search.js +361 -0
- package/dist/commands/stats.d.ts +35 -0
- package/dist/commands/stats.js +274 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.js +183 -0
- package/dist/commands/template/get.d.ts +28 -0
- package/dist/commands/template/get.js +59 -0
- package/dist/commands/template/list.d.ts +32 -0
- package/dist/commands/template/list.js +62 -0
- package/dist/commands/template/remove.d.ts +27 -0
- package/dist/commands/template/remove.js +48 -0
- package/dist/commands/template/save.d.ts +32 -0
- package/dist/commands/template/save.js +92 -0
- package/dist/commands/template/use.d.ts +34 -0
- package/dist/commands/template/use.js +142 -0
- package/dist/commands/user/list.d.ts +27 -0
- package/dist/commands/user/list.js +99 -0
- package/dist/commands/user/retrieve/bot.d.ts +28 -0
- package/dist/commands/user/retrieve/bot.js +96 -0
- package/dist/commands/user/retrieve.d.ts +30 -0
- package/dist/commands/user/retrieve.js +103 -0
- package/dist/commands/whoami.d.ts +19 -0
- package/dist/commands/whoami.js +175 -0
- package/dist/deduplication.d.ts +41 -0
- package/dist/deduplication.js +71 -0
- package/dist/envelope.d.ts +169 -0
- package/dist/envelope.js +257 -0
- package/dist/errors/enhanced-errors.d.ts +168 -0
- package/dist/errors/enhanced-errors.js +567 -0
- package/dist/errors/index.d.ts +18 -0
- package/dist/errors/index.js +33 -0
- package/dist/examples/cache-retry-examples.d.ts +64 -0
- package/dist/examples/cache-retry-examples.js +375 -0
- package/dist/helper.d.ts +102 -0
- package/dist/helper.js +885 -0
- package/dist/http-agent.d.ts +38 -0
- package/dist/http-agent.js +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/interface.d.ts +4 -0
- package/dist/interface.js +2 -0
- package/dist/notion.d.ts +144 -0
- package/dist/notion.js +547 -0
- package/dist/retry.d.ts +72 -0
- package/dist/retry.js +381 -0
- package/dist/utils/bookmarks.d.ts +32 -0
- package/dist/utils/bookmarks.js +98 -0
- package/dist/utils/daily-config.d.ts +22 -0
- package/dist/utils/daily-config.js +60 -0
- package/dist/utils/disk-cache.d.ts +80 -0
- package/dist/utils/disk-cache.js +291 -0
- package/dist/utils/fuzzy.d.ts +36 -0
- package/dist/utils/fuzzy.js +69 -0
- package/dist/utils/interactive-navigator.d.ts +63 -0
- package/dist/utils/interactive-navigator.js +123 -0
- package/dist/utils/markdown-to-blocks.d.ts +21 -0
- package/dist/utils/markdown-to-blocks.js +333 -0
- package/dist/utils/notion-resolver.d.ts +49 -0
- package/dist/utils/notion-resolver.js +278 -0
- package/dist/utils/notion-url-parser.d.ts +48 -0
- package/dist/utils/notion-url-parser.js +121 -0
- package/dist/utils/property-expander.d.ts +45 -0
- package/dist/utils/property-expander.js +323 -0
- package/dist/utils/schema-examples.d.ts +40 -0
- package/dist/utils/schema-examples.js +359 -0
- package/dist/utils/schema-extractor.d.ts +65 -0
- package/dist/utils/schema-extractor.js +235 -0
- package/dist/utils/shell-config.d.ts +30 -0
- package/dist/utils/shell-config.js +84 -0
- package/dist/utils/table-formatter.d.ts +36 -0
- package/dist/utils/table-formatter.js +125 -0
- package/dist/utils/templates.d.ts +30 -0
- package/dist/utils/templates.js +82 -0
- package/dist/utils/terminal-banner.d.ts +24 -0
- package/dist/utils/terminal-banner.js +34 -0
- package/dist/utils/token-validator.d.ts +42 -0
- package/dist/utils/token-validator.js +66 -0
- package/dist/utils/update-notifier.d.ts +26 -0
- package/dist/utils/update-notifier.js +54 -0
- package/dist/utils/workspace-cache.d.ts +58 -0
- package/dist/utils/workspace-cache.js +185 -0
- package/oclif.manifest.json +6471 -0
- package/package.json +118 -0
- package/scripts/banner.js +38 -0
- package/scripts/postinstall.js +44 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.markdownToBlocks = markdownToBlocks;
|
|
4
|
+
/**
|
|
5
|
+
* Converts markdown text to Notion block objects
|
|
6
|
+
*
|
|
7
|
+
* This is a simple, secure replacement for @tryfabric/martian's markdownToBlocks
|
|
8
|
+
* to eliminate security vulnerabilities from the katex dependency chain.
|
|
9
|
+
*
|
|
10
|
+
* Supports:
|
|
11
|
+
* - Headings (h1, h2, h3)
|
|
12
|
+
* - Paragraphs
|
|
13
|
+
* - Bulleted lists
|
|
14
|
+
* - Numbered lists
|
|
15
|
+
* - Checkboxes (to_do blocks)
|
|
16
|
+
* - Code blocks
|
|
17
|
+
* - Tables (with header detection)
|
|
18
|
+
* - Quotes
|
|
19
|
+
* - Bold, italic, strikethrough, and inline code formatting
|
|
20
|
+
*
|
|
21
|
+
* @param markdown - The markdown string to convert
|
|
22
|
+
* @returns Array of Notion block objects
|
|
23
|
+
*/
|
|
24
|
+
function markdownToBlocks(markdown) {
|
|
25
|
+
var _a;
|
|
26
|
+
const blocks = [];
|
|
27
|
+
const lines = markdown.split('\n');
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < lines.length) {
|
|
30
|
+
const line = lines[i];
|
|
31
|
+
const trimmedLine = line.trim();
|
|
32
|
+
// Skip empty lines at the top level
|
|
33
|
+
if (!trimmedLine) {
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Headings
|
|
38
|
+
const headingMatch = trimmedLine.match(/^(#{1,3})\s+(.+)$/);
|
|
39
|
+
if (headingMatch) {
|
|
40
|
+
const level = headingMatch[1].length;
|
|
41
|
+
const text = headingMatch[2];
|
|
42
|
+
const headingType = level === 1 ? 'heading_1' : level === 2 ? 'heading_2' : 'heading_3';
|
|
43
|
+
blocks.push({
|
|
44
|
+
object: 'block',
|
|
45
|
+
type: headingType,
|
|
46
|
+
[headingType]: {
|
|
47
|
+
rich_text: parseRichText(text),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
i++;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
// Code blocks
|
|
54
|
+
if (trimmedLine.startsWith('```')) {
|
|
55
|
+
const language = trimmedLine.slice(3).trim() || 'plain text';
|
|
56
|
+
const codeLines = [];
|
|
57
|
+
i++;
|
|
58
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
59
|
+
codeLines.push(lines[i]);
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
blocks.push({
|
|
63
|
+
object: 'block',
|
|
64
|
+
type: 'code',
|
|
65
|
+
code: {
|
|
66
|
+
rich_text: [{
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: { content: codeLines.join('\n') },
|
|
69
|
+
}],
|
|
70
|
+
language: language,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
i++; // Skip closing ```
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Tables: collect consecutive | rows, detect header via separator (|---|)
|
|
77
|
+
if (trimmedLine.match(/^\|.+\|$/)) {
|
|
78
|
+
const tableLines = [trimmedLine];
|
|
79
|
+
let j = i + 1;
|
|
80
|
+
while (j < lines.length && lines[j].trim().match(/^\|.+\|$/)) {
|
|
81
|
+
tableLines.push(lines[j].trim());
|
|
82
|
+
j++;
|
|
83
|
+
}
|
|
84
|
+
// Separator row like |---|---| means the first row is a header
|
|
85
|
+
const hasHeader = tableLines.length > 1 && /^\|[\s\-:|]+\|$/.test(tableLines[1]);
|
|
86
|
+
// Filter out separator rows — they're structural, not data
|
|
87
|
+
const dataRows = tableLines.filter(l => !/^\|[\s\-:|]+\|$/.test(l));
|
|
88
|
+
// Split each row into cells: "| A | B |" → ["A", "B"]
|
|
89
|
+
const parsedRows = dataRows.map(row => row.split('|').slice(1, -1).map(cell => cell.trim()));
|
|
90
|
+
const columnCount = ((_a = parsedRows[0]) === null || _a === void 0 ? void 0 : _a.length) || 1;
|
|
91
|
+
blocks.push({
|
|
92
|
+
object: 'block',
|
|
93
|
+
type: 'table',
|
|
94
|
+
table: {
|
|
95
|
+
table_width: columnCount,
|
|
96
|
+
has_column_header: hasHeader,
|
|
97
|
+
has_row_header: false,
|
|
98
|
+
children: parsedRows.map(cells => ({
|
|
99
|
+
object: 'block',
|
|
100
|
+
type: 'table_row',
|
|
101
|
+
table_row: {
|
|
102
|
+
cells: cells.map(cell => parseRichText(cell)),
|
|
103
|
+
},
|
|
104
|
+
})),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
i = j;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Block quotes
|
|
111
|
+
if (trimmedLine.startsWith('>')) {
|
|
112
|
+
const quoteText = trimmedLine.slice(1).trim();
|
|
113
|
+
blocks.push({
|
|
114
|
+
object: 'block',
|
|
115
|
+
type: 'quote',
|
|
116
|
+
quote: {
|
|
117
|
+
rich_text: parseRichText(quoteText),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// Checkboxes: - [ ] or - [x] / - [X] → Notion to_do block
|
|
124
|
+
// Must be checked before bullet lists since "- [ ]" also matches "^[-*]\s+"
|
|
125
|
+
const checkboxMatch = trimmedLine.match(/^-\s+\[([ xX])\]\s+(.*)$/);
|
|
126
|
+
if (checkboxMatch) {
|
|
127
|
+
const checked = checkboxMatch[1].toLowerCase() === 'x';
|
|
128
|
+
const text = checkboxMatch[2];
|
|
129
|
+
blocks.push({
|
|
130
|
+
object: 'block',
|
|
131
|
+
type: 'to_do',
|
|
132
|
+
to_do: {
|
|
133
|
+
rich_text: parseRichText(text),
|
|
134
|
+
checked,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
i++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Bulleted lists
|
|
141
|
+
if (trimmedLine.match(/^[-*]\s+/)) {
|
|
142
|
+
const text = trimmedLine.replace(/^[-*]\s+/, '');
|
|
143
|
+
blocks.push({
|
|
144
|
+
object: 'block',
|
|
145
|
+
type: 'bulleted_list_item',
|
|
146
|
+
bulleted_list_item: {
|
|
147
|
+
rich_text: parseRichText(text),
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
i++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// Numbered lists
|
|
154
|
+
if (trimmedLine.match(/^\d+\.\s+/)) {
|
|
155
|
+
const text = trimmedLine.replace(/^\d+\.\s+/, '');
|
|
156
|
+
blocks.push({
|
|
157
|
+
object: 'block',
|
|
158
|
+
type: 'numbered_list_item',
|
|
159
|
+
numbered_list_item: {
|
|
160
|
+
rich_text: parseRichText(text),
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// Horizontal rule
|
|
167
|
+
if (trimmedLine.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
|
|
168
|
+
blocks.push({
|
|
169
|
+
object: 'block',
|
|
170
|
+
type: 'divider',
|
|
171
|
+
divider: {},
|
|
172
|
+
});
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// Regular paragraph
|
|
177
|
+
blocks.push({
|
|
178
|
+
object: 'block',
|
|
179
|
+
type: 'paragraph',
|
|
180
|
+
paragraph: {
|
|
181
|
+
rich_text: parseRichText(trimmedLine),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
i++;
|
|
185
|
+
}
|
|
186
|
+
return blocks;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Parse markdown text into Notion rich text format
|
|
190
|
+
* Supports: **bold**, *italic*, ~~strikethrough~~, `code`, and [links](url)
|
|
191
|
+
*/
|
|
192
|
+
function parseRichText(text) {
|
|
193
|
+
if (!text) {
|
|
194
|
+
return [{ type: 'text', text: { content: '' } }];
|
|
195
|
+
}
|
|
196
|
+
const richText = [];
|
|
197
|
+
let currentText = '';
|
|
198
|
+
let i = 0;
|
|
199
|
+
while (i < text.length) {
|
|
200
|
+
// Bold: **text**
|
|
201
|
+
if (text[i] === '*' && text[i + 1] === '*') {
|
|
202
|
+
// Save any accumulated plain text
|
|
203
|
+
if (currentText) {
|
|
204
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
205
|
+
currentText = '';
|
|
206
|
+
}
|
|
207
|
+
// Find closing **
|
|
208
|
+
i += 2;
|
|
209
|
+
let boldText = '';
|
|
210
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '*')) {
|
|
211
|
+
boldText += text[i];
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
richText.push({
|
|
215
|
+
type: 'text',
|
|
216
|
+
text: { content: boldText },
|
|
217
|
+
annotations: { bold: true },
|
|
218
|
+
});
|
|
219
|
+
i += 2; // Skip closing **
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Strikethrough: ~~text~~ (same two-char delimiter pattern as bold)
|
|
223
|
+
if (text[i] === '~' && text[i + 1] === '~') {
|
|
224
|
+
if (currentText) {
|
|
225
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
226
|
+
currentText = '';
|
|
227
|
+
}
|
|
228
|
+
i += 2;
|
|
229
|
+
let stText = '';
|
|
230
|
+
while (i < text.length && !(text[i] === '~' && text[i + 1] === '~')) {
|
|
231
|
+
stText += text[i];
|
|
232
|
+
i++;
|
|
233
|
+
}
|
|
234
|
+
richText.push({
|
|
235
|
+
type: 'text',
|
|
236
|
+
text: { content: stText },
|
|
237
|
+
annotations: { strikethrough: true },
|
|
238
|
+
});
|
|
239
|
+
i += 2; // Skip closing ~~
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
// Italic: *text* or _text_
|
|
243
|
+
if ((text[i] === '*' || text[i] === '_') && text[i + 1] !== text[i]) {
|
|
244
|
+
const marker = text[i];
|
|
245
|
+
// Save any accumulated plain text
|
|
246
|
+
if (currentText) {
|
|
247
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
248
|
+
currentText = '';
|
|
249
|
+
}
|
|
250
|
+
// Find closing marker
|
|
251
|
+
i++;
|
|
252
|
+
let italicText = '';
|
|
253
|
+
while (i < text.length && text[i] !== marker) {
|
|
254
|
+
italicText += text[i];
|
|
255
|
+
i++;
|
|
256
|
+
}
|
|
257
|
+
richText.push({
|
|
258
|
+
type: 'text',
|
|
259
|
+
text: { content: italicText },
|
|
260
|
+
annotations: { italic: true },
|
|
261
|
+
});
|
|
262
|
+
i++; // Skip closing marker
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// Inline code: `text`
|
|
266
|
+
if (text[i] === '`') {
|
|
267
|
+
// Save any accumulated plain text
|
|
268
|
+
if (currentText) {
|
|
269
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
270
|
+
currentText = '';
|
|
271
|
+
}
|
|
272
|
+
// Find closing `
|
|
273
|
+
i++;
|
|
274
|
+
let codeText = '';
|
|
275
|
+
while (i < text.length && text[i] !== '`') {
|
|
276
|
+
codeText += text[i];
|
|
277
|
+
i++;
|
|
278
|
+
}
|
|
279
|
+
richText.push({
|
|
280
|
+
type: 'text',
|
|
281
|
+
text: { content: codeText },
|
|
282
|
+
annotations: { code: true },
|
|
283
|
+
});
|
|
284
|
+
i++; // Skip closing `
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// Links: [text](url)
|
|
288
|
+
if (text[i] === '[') {
|
|
289
|
+
const linkStart = i;
|
|
290
|
+
let linkText = '';
|
|
291
|
+
i++;
|
|
292
|
+
// Find closing ]
|
|
293
|
+
while (i < text.length && text[i] !== ']') {
|
|
294
|
+
linkText += text[i];
|
|
295
|
+
i++;
|
|
296
|
+
}
|
|
297
|
+
// Check if followed by (url)
|
|
298
|
+
if (i < text.length && text[i] === ']' && text[i + 1] === '(') {
|
|
299
|
+
i += 2; // Skip ](
|
|
300
|
+
let url = '';
|
|
301
|
+
while (i < text.length && text[i] !== ')') {
|
|
302
|
+
url += text[i];
|
|
303
|
+
i++;
|
|
304
|
+
}
|
|
305
|
+
// Save any accumulated plain text
|
|
306
|
+
if (currentText) {
|
|
307
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
308
|
+
currentText = '';
|
|
309
|
+
}
|
|
310
|
+
richText.push({
|
|
311
|
+
type: 'text',
|
|
312
|
+
text: { content: linkText, link: { url } },
|
|
313
|
+
});
|
|
314
|
+
i++; // Skip closing )
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// Not a link, treat as plain text
|
|
319
|
+
currentText += text.slice(linkStart, i + 1);
|
|
320
|
+
i++;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Regular character
|
|
325
|
+
currentText += text[i];
|
|
326
|
+
i++;
|
|
327
|
+
}
|
|
328
|
+
// Add any remaining text
|
|
329
|
+
if (currentText) {
|
|
330
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
331
|
+
}
|
|
332
|
+
return richText.length > 0 ? richText : [{ type: 'text', text: { content: '' } }];
|
|
333
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notion ID Resolver
|
|
3
|
+
*
|
|
4
|
+
* Hybrid resolution system that supports:
|
|
5
|
+
* - URLs: https://www.notion.so/database-id
|
|
6
|
+
* - Direct IDs: database-id
|
|
7
|
+
* - Names: "Tasks Database" (via cache lookup and API fallback)
|
|
8
|
+
* - Smart database_id → data_source_id conversion
|
|
9
|
+
*
|
|
10
|
+
* Resolution stages:
|
|
11
|
+
* 1. URL extraction
|
|
12
|
+
* 2. Direct ID validation
|
|
13
|
+
* 2.5. Bookmark lookup (user-defined shortcuts)
|
|
14
|
+
* 3. Cache lookup (exact → alias → substring → fuzzy)
|
|
15
|
+
* 4. API search fallback
|
|
16
|
+
* 5. Smart database_id → data_source_id resolution (for databases)
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Resolve Notion input (URL, ID, or name) to a clean Notion ID
|
|
20
|
+
*
|
|
21
|
+
* Supports URLs, IDs, and name-based lookups via cache and API search.
|
|
22
|
+
* For databases, automatically detects and converts database_id to data_source_id.
|
|
23
|
+
*
|
|
24
|
+
* @param input - Database/page name, ID, or URL
|
|
25
|
+
* @param type - Resource type (for better error messages)
|
|
26
|
+
* @returns Clean Notion ID (32 hex characters without dashes)
|
|
27
|
+
* @throws NotionCLIError if input cannot be resolved
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // URL
|
|
31
|
+
* await resolveNotionId('https://notion.so/1fb79d4c71bb8032b722c82305b63a00')
|
|
32
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Direct ID
|
|
36
|
+
* await resolveNotionId('1fb79d4c71bb8032b722c82305b63a00')
|
|
37
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Name (via cache or API)
|
|
41
|
+
* await resolveNotionId('Tasks Database', 'database')
|
|
42
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // database_id auto-conversion
|
|
46
|
+
* await resolveNotionId('abc123...', 'database')
|
|
47
|
+
* // If abc123 is a database_id, auto-resolves to data_source_id
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveNotionId(input: string, type?: 'database' | 'page'): Promise<string>;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Notion ID Resolver
|
|
4
|
+
*
|
|
5
|
+
* Hybrid resolution system that supports:
|
|
6
|
+
* - URLs: https://www.notion.so/database-id
|
|
7
|
+
* - Direct IDs: database-id
|
|
8
|
+
* - Names: "Tasks Database" (via cache lookup and API fallback)
|
|
9
|
+
* - Smart database_id → data_source_id conversion
|
|
10
|
+
*
|
|
11
|
+
* Resolution stages:
|
|
12
|
+
* 1. URL extraction
|
|
13
|
+
* 2. Direct ID validation
|
|
14
|
+
* 2.5. Bookmark lookup (user-defined shortcuts)
|
|
15
|
+
* 3. Cache lookup (exact → alias → substring → fuzzy)
|
|
16
|
+
* 4. API search fallback
|
|
17
|
+
* 5. Smart database_id → data_source_id resolution (for databases)
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.resolveNotionId = resolveNotionId;
|
|
21
|
+
const notion_url_parser_1 = require("./notion-url-parser");
|
|
22
|
+
const errors_1 = require("../errors");
|
|
23
|
+
const workspace_cache_1 = require("./workspace-cache");
|
|
24
|
+
const fuzzy_1 = require("./fuzzy");
|
|
25
|
+
const bookmarks_1 = require("./bookmarks");
|
|
26
|
+
const notion_1 = require("../notion");
|
|
27
|
+
const client_1 = require("@notionhq/client");
|
|
28
|
+
/**
|
|
29
|
+
* Resolve Notion input (URL, ID, or name) to a clean Notion ID
|
|
30
|
+
*
|
|
31
|
+
* Supports URLs, IDs, and name-based lookups via cache and API search.
|
|
32
|
+
* For databases, automatically detects and converts database_id to data_source_id.
|
|
33
|
+
*
|
|
34
|
+
* @param input - Database/page name, ID, or URL
|
|
35
|
+
* @param type - Resource type (for better error messages)
|
|
36
|
+
* @returns Clean Notion ID (32 hex characters without dashes)
|
|
37
|
+
* @throws NotionCLIError if input cannot be resolved
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // URL
|
|
41
|
+
* await resolveNotionId('https://notion.so/1fb79d4c71bb8032b722c82305b63a00')
|
|
42
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Direct ID
|
|
46
|
+
* await resolveNotionId('1fb79d4c71bb8032b722c82305b63a00')
|
|
47
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Name (via cache or API)
|
|
51
|
+
* await resolveNotionId('Tasks Database', 'database')
|
|
52
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // database_id auto-conversion
|
|
56
|
+
* await resolveNotionId('abc123...', 'database')
|
|
57
|
+
* // If abc123 is a database_id, auto-resolves to data_source_id
|
|
58
|
+
*/
|
|
59
|
+
async function resolveNotionId(input, type = 'database') {
|
|
60
|
+
if (!input || typeof input !== 'string') {
|
|
61
|
+
throw new errors_1.NotionCLIError(errors_1.NotionCLIErrorCode.VALIDATION_ERROR, `Invalid input: expected a ${type} name, ID, or URL`, [], { resourceType: type, userInput: String(input) });
|
|
62
|
+
}
|
|
63
|
+
const trimmed = input.trim();
|
|
64
|
+
// Stage 1: URL extraction
|
|
65
|
+
if ((0, notion_url_parser_1.isNotionUrl)(trimmed)) {
|
|
66
|
+
try {
|
|
67
|
+
const extractedId = (0, notion_url_parser_1.extractNotionId)(trimmed);
|
|
68
|
+
// For databases, try smart resolution in case URL contains database_id
|
|
69
|
+
if (type === 'database') {
|
|
70
|
+
return await trySmartDatabaseResolution(extractedId);
|
|
71
|
+
}
|
|
72
|
+
return extractedId;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw errors_1.NotionCLIErrorFactory.invalidIdFormat(trimmed, type);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Stage 2: Direct ID validation
|
|
79
|
+
if (isValidNotionId(trimmed)) {
|
|
80
|
+
const extractedId = (0, notion_url_parser_1.extractNotionId)(trimmed);
|
|
81
|
+
// For databases, try smart resolution in case it's a database_id
|
|
82
|
+
if (type === 'database') {
|
|
83
|
+
return await trySmartDatabaseResolution(extractedId);
|
|
84
|
+
}
|
|
85
|
+
return extractedId;
|
|
86
|
+
}
|
|
87
|
+
// Stage 2.5: Bookmark lookup — user-defined shortcuts (e.g. "inbox", "tasks")
|
|
88
|
+
const bookmark = await (0, bookmarks_1.getBookmark)(trimmed);
|
|
89
|
+
if (bookmark)
|
|
90
|
+
return bookmark.id;
|
|
91
|
+
// Stage 3: Cache lookup (exact + aliases)
|
|
92
|
+
const fromCache = await searchCache(trimmed);
|
|
93
|
+
if (fromCache)
|
|
94
|
+
return fromCache;
|
|
95
|
+
// Stage 4: API search as fallback
|
|
96
|
+
const fromApi = await searchNotionApi(trimmed, type);
|
|
97
|
+
if (fromApi)
|
|
98
|
+
return fromApi;
|
|
99
|
+
// Nothing found - throw helpful error
|
|
100
|
+
if (type === 'database') {
|
|
101
|
+
throw errors_1.NotionCLIErrorFactory.workspaceNotSynced(trimmed);
|
|
102
|
+
}
|
|
103
|
+
throw errors_1.NotionCLIErrorFactory.resourceNotFound(type, trimmed);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Smart database resolution: handles database_id → data_source_id conversion
|
|
107
|
+
*
|
|
108
|
+
* When a user provides a database_id (from parent.database_id field),
|
|
109
|
+
* this function detects the error and automatically resolves it to the
|
|
110
|
+
* correct data_source_id.
|
|
111
|
+
*
|
|
112
|
+
* @param databaseId - Potential database_id or data_source_id
|
|
113
|
+
* @returns data_source_id if valid, throws error otherwise
|
|
114
|
+
*/
|
|
115
|
+
async function trySmartDatabaseResolution(databaseId) {
|
|
116
|
+
try {
|
|
117
|
+
// Try direct lookup with data_source_id
|
|
118
|
+
await (0, notion_1.retrieveDataSource)(databaseId);
|
|
119
|
+
// If successful, it's a valid data_source_id
|
|
120
|
+
return databaseId;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// Check if this is an object_not_found error (404)
|
|
124
|
+
const isNotFound = error.status === 404 ||
|
|
125
|
+
error.code === 'object_not_found' ||
|
|
126
|
+
(error.notionError && error.notionError.code === 'object_not_found');
|
|
127
|
+
if (isNotFound) {
|
|
128
|
+
// Try to resolve database_id → data_source_id
|
|
129
|
+
const dataSourceId = await resolveDatabaseIdToDataSourceId(databaseId);
|
|
130
|
+
if (dataSourceId) {
|
|
131
|
+
// Log helpful message about conversion
|
|
132
|
+
console.log(`\nInfo: Resolved database_id to data_source_id`);
|
|
133
|
+
console.log(` database_id: ${databaseId}`);
|
|
134
|
+
console.log(` data_source_id: ${dataSourceId}`);
|
|
135
|
+
console.log(`\nNote: Use data_source_id for database operations.`);
|
|
136
|
+
console.log(` The database_id from parent.database_id won't work directly.\n`);
|
|
137
|
+
return dataSourceId;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// If we can't resolve it, throw the original error
|
|
141
|
+
throw (0, errors_1.wrapNotionError)(error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Resolve database_id to data_source_id by searching for pages
|
|
146
|
+
*
|
|
147
|
+
* When a user provides a database_id (from parent.database_id field),
|
|
148
|
+
* we search for pages that have this database as their parent, and
|
|
149
|
+
* extract the data_source_id from the parent field.
|
|
150
|
+
*
|
|
151
|
+
* @param databaseId - The database_id to resolve
|
|
152
|
+
* @returns data_source_id if found, null otherwise
|
|
153
|
+
*/
|
|
154
|
+
async function resolveDatabaseIdToDataSourceId(databaseId) {
|
|
155
|
+
try {
|
|
156
|
+
// Search for pages with this database_id as parent
|
|
157
|
+
const response = await (0, notion_1.search)({
|
|
158
|
+
filter: {
|
|
159
|
+
property: 'object',
|
|
160
|
+
value: 'page'
|
|
161
|
+
},
|
|
162
|
+
page_size: 100 // Search more pages to increase chance of finding one
|
|
163
|
+
});
|
|
164
|
+
if (!response || !response.results || response.results.length === 0) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
// Look through results for a page with matching parent.database_id
|
|
168
|
+
for (const result of response.results) {
|
|
169
|
+
if (result.object !== 'page')
|
|
170
|
+
continue;
|
|
171
|
+
// Use type guard to ensure we have a full page with parent
|
|
172
|
+
if (!(0, client_1.isFullPage)(result))
|
|
173
|
+
continue;
|
|
174
|
+
// Check if parent type is database_id and matches our search
|
|
175
|
+
if (result.parent &&
|
|
176
|
+
result.parent.type === 'database_id' &&
|
|
177
|
+
result.parent.database_id === databaseId) {
|
|
178
|
+
// Extract data_source_id from the same parent object
|
|
179
|
+
// In the Notion API v5, pages have both database_id and data_source_id in parent
|
|
180
|
+
if ('data_source_id' in result.parent) {
|
|
181
|
+
return result.parent.data_source_id;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
// If search fails, return null and let the main error handling deal with it
|
|
189
|
+
if (process.env.DEBUG) {
|
|
190
|
+
console.error('Debug: Failed to resolve database_id to data_source_id:', error);
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if a string is a valid Notion ID (32 hex chars with optional dashes)
|
|
197
|
+
*/
|
|
198
|
+
function isValidNotionId(input) {
|
|
199
|
+
const cleaned = input.replace(/-/g, '');
|
|
200
|
+
return /^[a-f0-9]{32}$/i.test(cleaned);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Search cache for database/page by name
|
|
204
|
+
*
|
|
205
|
+
* Searches in this order:
|
|
206
|
+
* 1. Exact title match (case-insensitive)
|
|
207
|
+
* 2. Alias match (case-insensitive)
|
|
208
|
+
* 3. Partial title match (case-insensitive substring)
|
|
209
|
+
* 4. Fuzzy match (Levenshtein distance for typo tolerance)
|
|
210
|
+
*
|
|
211
|
+
* @param query - Search query (database/page name)
|
|
212
|
+
* @returns Database/page ID if found, null otherwise
|
|
213
|
+
*/
|
|
214
|
+
async function searchCache(query) {
|
|
215
|
+
const cache = await (0, workspace_cache_1.loadCache)();
|
|
216
|
+
if (!cache)
|
|
217
|
+
return null;
|
|
218
|
+
const normalized = query.toLowerCase().trim();
|
|
219
|
+
// 1. Try exact title match
|
|
220
|
+
for (const db of cache.databases) {
|
|
221
|
+
if (db.titleNormalized === normalized) {
|
|
222
|
+
return db.id;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// 2. Try alias match
|
|
226
|
+
for (const db of cache.databases) {
|
|
227
|
+
if (db.aliases.includes(normalized)) {
|
|
228
|
+
return db.id;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// 3. Try partial match (substring in title)
|
|
232
|
+
for (const db of cache.databases) {
|
|
233
|
+
if (db.titleNormalized.includes(normalized)) {
|
|
234
|
+
return db.id;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 4. Try fuzzy match (typo tolerance via Levenshtein distance)
|
|
238
|
+
const candidates = cache.databases.flatMap(db => [
|
|
239
|
+
{ key: db.id, value: db.titleNormalized },
|
|
240
|
+
...db.aliases.map(alias => ({ key: db.id, value: alias })),
|
|
241
|
+
]);
|
|
242
|
+
const fuzzyResult = (0, fuzzy_1.fuzzyMatch)(normalized, candidates);
|
|
243
|
+
if (fuzzyResult)
|
|
244
|
+
return fuzzyResult.match;
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Search Notion API for database/page by name
|
|
249
|
+
*
|
|
250
|
+
* Uses Notion's search API as a fallback when cache lookup fails.
|
|
251
|
+
*
|
|
252
|
+
* @param query - Search query (database/page name)
|
|
253
|
+
* @param type - Resource type ('database' or 'page')
|
|
254
|
+
* @returns Database/page ID if found, null otherwise
|
|
255
|
+
*/
|
|
256
|
+
async function searchNotionApi(query, type) {
|
|
257
|
+
try {
|
|
258
|
+
// Search Notion API
|
|
259
|
+
const response = await (0, notion_1.search)({
|
|
260
|
+
query,
|
|
261
|
+
filter: {
|
|
262
|
+
property: 'object',
|
|
263
|
+
value: type === 'database' ? 'data_source' : 'page'
|
|
264
|
+
},
|
|
265
|
+
page_size: 10
|
|
266
|
+
});
|
|
267
|
+
// Return first match
|
|
268
|
+
if (response && response.results && response.results.length > 0) {
|
|
269
|
+
return response.results[0].id;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// API search failed, return null
|
|
275
|
+
// The caller will throw a more helpful error message
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|