@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.
Files changed (162) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +552 -0
  3. package/bin/dev +17 -0
  4. package/bin/dev.cmd +3 -0
  5. package/bin/run +14 -0
  6. package/bin/run.cmd +3 -0
  7. package/dist/base-command.d.ts +73 -0
  8. package/dist/base-command.js +179 -0
  9. package/dist/base-flags.d.ts +14 -0
  10. package/dist/base-flags.js +59 -0
  11. package/dist/cache.d.ts +84 -0
  12. package/dist/cache.js +351 -0
  13. package/dist/commands/append.d.ts +37 -0
  14. package/dist/commands/append.js +120 -0
  15. package/dist/commands/batch/delete.d.ts +42 -0
  16. package/dist/commands/batch/delete.js +199 -0
  17. package/dist/commands/batch/retrieve.d.ts +43 -0
  18. package/dist/commands/batch/retrieve.js +272 -0
  19. package/dist/commands/block/append.d.ts +42 -0
  20. package/dist/commands/block/append.js +219 -0
  21. package/dist/commands/block/delete.d.ts +30 -0
  22. package/dist/commands/block/delete.js +97 -0
  23. package/dist/commands/block/retrieve/children.d.ts +31 -0
  24. package/dist/commands/block/retrieve/children.js +177 -0
  25. package/dist/commands/block/retrieve.d.ts +30 -0
  26. package/dist/commands/block/retrieve.js +101 -0
  27. package/dist/commands/block/update.d.ts +45 -0
  28. package/dist/commands/block/update.js +242 -0
  29. package/dist/commands/bookmark/list.d.ts +30 -0
  30. package/dist/commands/bookmark/list.js +60 -0
  31. package/dist/commands/bookmark/remove.d.ts +26 -0
  32. package/dist/commands/bookmark/remove.js +47 -0
  33. package/dist/commands/bookmark/set.d.ts +29 -0
  34. package/dist/commands/bookmark/set.js +96 -0
  35. package/dist/commands/browse.d.ts +13 -0
  36. package/dist/commands/browse.js +44 -0
  37. package/dist/commands/cache/info.d.ts +19 -0
  38. package/dist/commands/cache/info.js +145 -0
  39. package/dist/commands/config/set-token.d.ts +22 -0
  40. package/dist/commands/config/set-token.js +137 -0
  41. package/dist/commands/daily/index.d.ts +32 -0
  42. package/dist/commands/daily/index.js +135 -0
  43. package/dist/commands/daily/setup.d.ts +42 -0
  44. package/dist/commands/daily/setup.js +149 -0
  45. package/dist/commands/db/create.d.ts +31 -0
  46. package/dist/commands/db/create.js +124 -0
  47. package/dist/commands/db/query.d.ts +41 -0
  48. package/dist/commands/db/query.js +360 -0
  49. package/dist/commands/db/retrieve.d.ts +33 -0
  50. package/dist/commands/db/retrieve.js +134 -0
  51. package/dist/commands/db/schema.d.ts +32 -0
  52. package/dist/commands/db/schema.js +308 -0
  53. package/dist/commands/db/update.d.ts +31 -0
  54. package/dist/commands/db/update.js +117 -0
  55. package/dist/commands/doctor.d.ts +50 -0
  56. package/dist/commands/doctor.js +420 -0
  57. package/dist/commands/init.d.ts +65 -0
  58. package/dist/commands/init.js +479 -0
  59. package/dist/commands/list.d.ts +29 -0
  60. package/dist/commands/list.js +219 -0
  61. package/dist/commands/open.d.ts +29 -0
  62. package/dist/commands/open.js +100 -0
  63. package/dist/commands/page/create.d.ts +33 -0
  64. package/dist/commands/page/create.js +261 -0
  65. package/dist/commands/page/delete.d.ts +36 -0
  66. package/dist/commands/page/delete.js +107 -0
  67. package/dist/commands/page/export.d.ts +38 -0
  68. package/dist/commands/page/export.js +120 -0
  69. package/dist/commands/page/retrieve/property_item.d.ts +24 -0
  70. package/dist/commands/page/retrieve/property_item.js +75 -0
  71. package/dist/commands/page/retrieve.d.ts +36 -0
  72. package/dist/commands/page/retrieve.js +244 -0
  73. package/dist/commands/page/update.d.ts +34 -0
  74. package/dist/commands/page/update.js +184 -0
  75. package/dist/commands/quick.d.ts +35 -0
  76. package/dist/commands/quick.js +168 -0
  77. package/dist/commands/search.d.ts +43 -0
  78. package/dist/commands/search.js +361 -0
  79. package/dist/commands/stats.d.ts +35 -0
  80. package/dist/commands/stats.js +274 -0
  81. package/dist/commands/sync.d.ts +24 -0
  82. package/dist/commands/sync.js +183 -0
  83. package/dist/commands/template/get.d.ts +28 -0
  84. package/dist/commands/template/get.js +59 -0
  85. package/dist/commands/template/list.d.ts +32 -0
  86. package/dist/commands/template/list.js +62 -0
  87. package/dist/commands/template/remove.d.ts +27 -0
  88. package/dist/commands/template/remove.js +48 -0
  89. package/dist/commands/template/save.d.ts +32 -0
  90. package/dist/commands/template/save.js +92 -0
  91. package/dist/commands/template/use.d.ts +34 -0
  92. package/dist/commands/template/use.js +142 -0
  93. package/dist/commands/user/list.d.ts +27 -0
  94. package/dist/commands/user/list.js +99 -0
  95. package/dist/commands/user/retrieve/bot.d.ts +28 -0
  96. package/dist/commands/user/retrieve/bot.js +96 -0
  97. package/dist/commands/user/retrieve.d.ts +30 -0
  98. package/dist/commands/user/retrieve.js +103 -0
  99. package/dist/commands/whoami.d.ts +19 -0
  100. package/dist/commands/whoami.js +175 -0
  101. package/dist/deduplication.d.ts +41 -0
  102. package/dist/deduplication.js +71 -0
  103. package/dist/envelope.d.ts +169 -0
  104. package/dist/envelope.js +257 -0
  105. package/dist/errors/enhanced-errors.d.ts +168 -0
  106. package/dist/errors/enhanced-errors.js +567 -0
  107. package/dist/errors/index.d.ts +18 -0
  108. package/dist/errors/index.js +33 -0
  109. package/dist/examples/cache-retry-examples.d.ts +64 -0
  110. package/dist/examples/cache-retry-examples.js +375 -0
  111. package/dist/helper.d.ts +102 -0
  112. package/dist/helper.js +885 -0
  113. package/dist/http-agent.d.ts +38 -0
  114. package/dist/http-agent.js +60 -0
  115. package/dist/index.d.ts +1 -0
  116. package/dist/index.js +4 -0
  117. package/dist/interface.d.ts +4 -0
  118. package/dist/interface.js +2 -0
  119. package/dist/notion.d.ts +144 -0
  120. package/dist/notion.js +547 -0
  121. package/dist/retry.d.ts +72 -0
  122. package/dist/retry.js +381 -0
  123. package/dist/utils/bookmarks.d.ts +32 -0
  124. package/dist/utils/bookmarks.js +98 -0
  125. package/dist/utils/daily-config.d.ts +22 -0
  126. package/dist/utils/daily-config.js +60 -0
  127. package/dist/utils/disk-cache.d.ts +80 -0
  128. package/dist/utils/disk-cache.js +291 -0
  129. package/dist/utils/fuzzy.d.ts +36 -0
  130. package/dist/utils/fuzzy.js +69 -0
  131. package/dist/utils/interactive-navigator.d.ts +63 -0
  132. package/dist/utils/interactive-navigator.js +123 -0
  133. package/dist/utils/markdown-to-blocks.d.ts +21 -0
  134. package/dist/utils/markdown-to-blocks.js +333 -0
  135. package/dist/utils/notion-resolver.d.ts +49 -0
  136. package/dist/utils/notion-resolver.js +278 -0
  137. package/dist/utils/notion-url-parser.d.ts +48 -0
  138. package/dist/utils/notion-url-parser.js +121 -0
  139. package/dist/utils/property-expander.d.ts +45 -0
  140. package/dist/utils/property-expander.js +323 -0
  141. package/dist/utils/schema-examples.d.ts +40 -0
  142. package/dist/utils/schema-examples.js +359 -0
  143. package/dist/utils/schema-extractor.d.ts +65 -0
  144. package/dist/utils/schema-extractor.js +235 -0
  145. package/dist/utils/shell-config.d.ts +30 -0
  146. package/dist/utils/shell-config.js +84 -0
  147. package/dist/utils/table-formatter.d.ts +36 -0
  148. package/dist/utils/table-formatter.js +125 -0
  149. package/dist/utils/templates.d.ts +30 -0
  150. package/dist/utils/templates.js +82 -0
  151. package/dist/utils/terminal-banner.d.ts +24 -0
  152. package/dist/utils/terminal-banner.js +34 -0
  153. package/dist/utils/token-validator.d.ts +42 -0
  154. package/dist/utils/token-validator.js +66 -0
  155. package/dist/utils/update-notifier.d.ts +26 -0
  156. package/dist/utils/update-notifier.js +54 -0
  157. package/dist/utils/workspace-cache.d.ts +58 -0
  158. package/dist/utils/workspace-cache.js +185 -0
  159. package/oclif.manifest.json +6471 -0
  160. package/package.json +118 -0
  161. package/scripts/banner.js +38 -0
  162. 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
+ }