@infograb/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 +1386 -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/batch/retrieve.d.ts +43 -0
- package/dist/commands/batch/retrieve.js +265 -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 +94 -0
- package/dist/commands/block/retrieve/children.d.ts +31 -0
- package/dist/commands/block/retrieve/children.js +174 -0
- package/dist/commands/block/retrieve.d.ts +30 -0
- package/dist/commands/block/retrieve.js +98 -0
- package/dist/commands/block/update.d.ts +45 -0
- package/dist/commands/block/update.js +241 -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 +30 -0
- package/dist/commands/config/set-token.js +201 -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 +355 -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 +57 -0
- package/dist/commands/init.js +471 -0
- package/dist/commands/list.d.ts +29 -0
- package/dist/commands/list.js +184 -0
- package/dist/commands/page/create.d.ts +33 -0
- package/dist/commands/page/create.js +240 -0
- package/dist/commands/page/retrieve/property_item.d.ts +24 -0
- package/dist/commands/page/retrieve/property_item.js +72 -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/search.d.ts +40 -0
- package/dist/commands/search.js +348 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.js +183 -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 +570 -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/disk-cache.d.ts +80 -0
- package/dist/utils/disk-cache.js +291 -0
- package/dist/utils/markdown-to-blocks.d.ts +19 -0
- package/dist/utils/markdown-to-blocks.js +259 -0
- package/dist/utils/notion-resolver.d.ts +48 -0
- package/dist/utils/notion-resolver.js +262 -0
- package/dist/utils/notion-url-parser.d.ts +46 -0
- package/dist/utils/notion-url-parser.js +111 -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/table-formatter.d.ts +36 -0
- package/dist/utils/table-formatter.js +122 -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 +55 -0
- package/dist/utils/token-validator.js +85 -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 +4497 -0
- package/package.json +115 -0
- package/scripts/banner.js +38 -0
- package/scripts/postinstall.js +56 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Disk Cache Manager
|
|
4
|
+
*
|
|
5
|
+
* Provides persistent caching to disk, maintaining cache across CLI invocations.
|
|
6
|
+
* Cache entries are stored in ~/.notion-cli/cache/ directory.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.diskCacheManager = exports.DiskCacheManager = void 0;
|
|
10
|
+
const fs = require("fs/promises");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const os = require("os");
|
|
13
|
+
const crypto = require("crypto");
|
|
14
|
+
const CACHE_DIR_NAME = '.notion-cli';
|
|
15
|
+
const CACHE_SUBDIR = 'cache';
|
|
16
|
+
const DEFAULT_MAX_SIZE = 100 * 1024 * 1024; // 100MB
|
|
17
|
+
const DEFAULT_SYNC_INTERVAL = 5000; // 5 seconds
|
|
18
|
+
class DiskCacheManager {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.dirtyKeys = new Set();
|
|
21
|
+
this.syncTimer = null;
|
|
22
|
+
this.initialized = false;
|
|
23
|
+
this.cacheDir = options.cacheDir || path.join(os.homedir(), CACHE_DIR_NAME, CACHE_SUBDIR);
|
|
24
|
+
this.maxSize = options.maxSize || parseInt(process.env.NOTION_CLI_DISK_CACHE_MAX_SIZE || String(DEFAULT_MAX_SIZE), 10);
|
|
25
|
+
this.syncInterval = options.syncInterval || parseInt(process.env.NOTION_CLI_DISK_CACHE_SYNC_INTERVAL || String(DEFAULT_SYNC_INTERVAL), 10);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Initialize disk cache (create directory, start sync timer)
|
|
29
|
+
*/
|
|
30
|
+
async initialize() {
|
|
31
|
+
if (this.initialized) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await this.ensureCacheDir();
|
|
35
|
+
await this.enforceMaxSize();
|
|
36
|
+
// Start periodic sync timer
|
|
37
|
+
if (this.syncInterval > 0) {
|
|
38
|
+
this.syncTimer = setInterval(() => {
|
|
39
|
+
this.sync().catch(error => {
|
|
40
|
+
if (process.env.DEBUG) {
|
|
41
|
+
console.warn('Disk cache sync error:', error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}, this.syncInterval);
|
|
45
|
+
// Don't keep the process alive
|
|
46
|
+
if (this.syncTimer.unref) {
|
|
47
|
+
this.syncTimer.unref();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.initialized = true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get a cache entry from disk
|
|
54
|
+
*/
|
|
55
|
+
async get(key) {
|
|
56
|
+
try {
|
|
57
|
+
const filePath = this.getFilePath(key);
|
|
58
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
59
|
+
const entry = JSON.parse(content);
|
|
60
|
+
// Check if expired
|
|
61
|
+
if (Date.now() > entry.expiresAt) {
|
|
62
|
+
// Delete expired entry
|
|
63
|
+
await this.invalidate(key);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return entry;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === 'ENOENT') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (process.env.DEBUG) {
|
|
73
|
+
console.warn(`Failed to read cache entry ${key}:`, error.message);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Set a cache entry to disk
|
|
80
|
+
*/
|
|
81
|
+
async set(key, data, ttl) {
|
|
82
|
+
const entry = {
|
|
83
|
+
key,
|
|
84
|
+
data,
|
|
85
|
+
expiresAt: Date.now() + ttl,
|
|
86
|
+
createdAt: Date.now(),
|
|
87
|
+
size: JSON.stringify(data).length,
|
|
88
|
+
};
|
|
89
|
+
const filePath = this.getFilePath(key);
|
|
90
|
+
const tmpPath = `${filePath}.tmp`;
|
|
91
|
+
try {
|
|
92
|
+
// Write to temporary file
|
|
93
|
+
await fs.writeFile(tmpPath, JSON.stringify(entry), 'utf-8');
|
|
94
|
+
// Atomic rename
|
|
95
|
+
await fs.rename(tmpPath, filePath);
|
|
96
|
+
this.dirtyKeys.delete(key);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// Clean up temp file if it exists
|
|
100
|
+
try {
|
|
101
|
+
await fs.unlink(tmpPath);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Ignore cleanup errors
|
|
105
|
+
}
|
|
106
|
+
if (process.env.DEBUG) {
|
|
107
|
+
console.warn(`Failed to write cache entry ${key}:`, error.message);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Check if we need to enforce size limits
|
|
111
|
+
const stats = await this.getStats();
|
|
112
|
+
if (stats.totalSize > this.maxSize) {
|
|
113
|
+
await this.enforceMaxSize();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Invalidate (delete) a cache entry
|
|
118
|
+
*/
|
|
119
|
+
async invalidate(key) {
|
|
120
|
+
try {
|
|
121
|
+
const filePath = this.getFilePath(key);
|
|
122
|
+
await fs.unlink(filePath);
|
|
123
|
+
this.dirtyKeys.delete(key);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code !== 'ENOENT') {
|
|
127
|
+
if (process.env.DEBUG) {
|
|
128
|
+
console.warn(`Failed to delete cache entry ${key}:`, error.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Clear all cache entries
|
|
135
|
+
*/
|
|
136
|
+
async clear() {
|
|
137
|
+
try {
|
|
138
|
+
const files = await fs.readdir(this.cacheDir);
|
|
139
|
+
await Promise.all(files
|
|
140
|
+
.filter(file => !file.endsWith('.tmp'))
|
|
141
|
+
.map(file => fs.unlink(path.join(this.cacheDir, file)).catch(() => { })));
|
|
142
|
+
this.dirtyKeys.clear();
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error.code !== 'ENOENT') {
|
|
146
|
+
if (process.env.DEBUG) {
|
|
147
|
+
console.warn('Failed to clear cache:', error.message);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Sync dirty entries to disk
|
|
154
|
+
*/
|
|
155
|
+
async sync() {
|
|
156
|
+
// In our implementation, writes are immediate (no write buffering)
|
|
157
|
+
// This method is here for API compatibility
|
|
158
|
+
this.dirtyKeys.clear();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Shutdown (flush and cleanup)
|
|
162
|
+
*/
|
|
163
|
+
async shutdown() {
|
|
164
|
+
if (this.syncTimer) {
|
|
165
|
+
clearInterval(this.syncTimer);
|
|
166
|
+
this.syncTimer = null;
|
|
167
|
+
}
|
|
168
|
+
await this.sync();
|
|
169
|
+
this.initialized = false;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get cache statistics
|
|
173
|
+
*/
|
|
174
|
+
async getStats() {
|
|
175
|
+
try {
|
|
176
|
+
const files = await fs.readdir(this.cacheDir);
|
|
177
|
+
const entries = [];
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
if (file.endsWith('.tmp')) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const content = await fs.readFile(path.join(this.cacheDir, file), 'utf-8');
|
|
184
|
+
const entry = JSON.parse(content);
|
|
185
|
+
entries.push(entry);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Skip corrupted entries
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
192
|
+
const timestamps = entries.map(e => e.createdAt);
|
|
193
|
+
return {
|
|
194
|
+
totalEntries: entries.length,
|
|
195
|
+
totalSize,
|
|
196
|
+
oldestEntry: timestamps.length > 0 ? Math.min(...timestamps) : null,
|
|
197
|
+
newestEntry: timestamps.length > 0 ? Math.max(...timestamps) : null,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
return {
|
|
202
|
+
totalEntries: 0,
|
|
203
|
+
totalSize: 0,
|
|
204
|
+
oldestEntry: null,
|
|
205
|
+
newestEntry: null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Enforce maximum cache size by removing oldest entries
|
|
211
|
+
*/
|
|
212
|
+
async enforceMaxSize() {
|
|
213
|
+
try {
|
|
214
|
+
const files = await fs.readdir(this.cacheDir);
|
|
215
|
+
const entries = [];
|
|
216
|
+
// Load all entries
|
|
217
|
+
for (const file of files) {
|
|
218
|
+
if (file.endsWith('.tmp')) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const filePath = path.join(this.cacheDir, file);
|
|
223
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
224
|
+
const entry = JSON.parse(content);
|
|
225
|
+
// Remove expired entries
|
|
226
|
+
if (Date.now() > entry.expiresAt) {
|
|
227
|
+
await fs.unlink(filePath);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
entries.push({ file, entry });
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Skip corrupted entries
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Calculate total size
|
|
237
|
+
const totalSize = entries.reduce((sum, { entry }) => sum + entry.size, 0);
|
|
238
|
+
// If under limit, we're done
|
|
239
|
+
if (totalSize <= this.maxSize) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
// Sort by creation time (oldest first)
|
|
243
|
+
entries.sort((a, b) => a.entry.createdAt - b.entry.createdAt);
|
|
244
|
+
// Remove oldest entries until under limit
|
|
245
|
+
let currentSize = totalSize;
|
|
246
|
+
for (const { file, entry } of entries) {
|
|
247
|
+
if (currentSize <= this.maxSize) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
await fs.unlink(path.join(this.cacheDir, file));
|
|
252
|
+
currentSize -= entry.size;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Skip deletion errors
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (process.env.DEBUG) {
|
|
261
|
+
console.warn('Failed to enforce max size:', error.message);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Ensure cache directory exists
|
|
267
|
+
*/
|
|
268
|
+
async ensureCacheDir() {
|
|
269
|
+
try {
|
|
270
|
+
await fs.mkdir(this.cacheDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
if (error.code !== 'EEXIST') {
|
|
274
|
+
throw new Error(`Failed to create cache directory: ${error.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Get file path for a cache key
|
|
280
|
+
*/
|
|
281
|
+
getFilePath(key) {
|
|
282
|
+
// Hash the key to create a safe filename
|
|
283
|
+
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
|
284
|
+
return path.join(this.cacheDir, `${hash}.json`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
exports.DiskCacheManager = DiskCacheManager;
|
|
288
|
+
/**
|
|
289
|
+
* Global singleton instance
|
|
290
|
+
*/
|
|
291
|
+
exports.diskCacheManager = new DiskCacheManager();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts markdown text to Notion block objects
|
|
3
|
+
*
|
|
4
|
+
* This is a simple, secure replacement for @tryfabric/martian's markdownToBlocks
|
|
5
|
+
* to eliminate security vulnerabilities from the katex dependency chain.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - Headings (h1, h2, h3)
|
|
9
|
+
* - Paragraphs
|
|
10
|
+
* - Bulleted lists
|
|
11
|
+
* - Numbered lists
|
|
12
|
+
* - Code blocks
|
|
13
|
+
* - Quotes
|
|
14
|
+
* - Bold, italic, and inline code formatting
|
|
15
|
+
*
|
|
16
|
+
* @param markdown - The markdown string to convert
|
|
17
|
+
* @returns Array of Notion block objects
|
|
18
|
+
*/
|
|
19
|
+
export declare function markdownToBlocks(markdown: string): any[];
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
* - Code blocks
|
|
16
|
+
* - Quotes
|
|
17
|
+
* - Bold, italic, and inline code formatting
|
|
18
|
+
*
|
|
19
|
+
* @param markdown - The markdown string to convert
|
|
20
|
+
* @returns Array of Notion block objects
|
|
21
|
+
*/
|
|
22
|
+
function markdownToBlocks(markdown) {
|
|
23
|
+
const blocks = [];
|
|
24
|
+
const lines = markdown.split('\n');
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (i < lines.length) {
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
const trimmedLine = line.trim();
|
|
29
|
+
// Skip empty lines at the top level
|
|
30
|
+
if (!trimmedLine) {
|
|
31
|
+
i++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Headings
|
|
35
|
+
const headingMatch = trimmedLine.match(/^(#{1,3})\s+(.+)$/);
|
|
36
|
+
if (headingMatch) {
|
|
37
|
+
const level = headingMatch[1].length;
|
|
38
|
+
const text = headingMatch[2];
|
|
39
|
+
const headingType = level === 1 ? 'heading_1' : level === 2 ? 'heading_2' : 'heading_3';
|
|
40
|
+
blocks.push({
|
|
41
|
+
object: 'block',
|
|
42
|
+
type: headingType,
|
|
43
|
+
[headingType]: {
|
|
44
|
+
rich_text: parseRichText(text),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
// Code blocks
|
|
51
|
+
if (trimmedLine.startsWith('```')) {
|
|
52
|
+
const language = trimmedLine.slice(3).trim() || 'plain text';
|
|
53
|
+
const codeLines = [];
|
|
54
|
+
i++;
|
|
55
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
56
|
+
codeLines.push(lines[i]);
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
blocks.push({
|
|
60
|
+
object: 'block',
|
|
61
|
+
type: 'code',
|
|
62
|
+
code: {
|
|
63
|
+
rich_text: [{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: { content: codeLines.join('\n') },
|
|
66
|
+
}],
|
|
67
|
+
language: language,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
i++; // Skip closing ```
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Block quotes
|
|
74
|
+
if (trimmedLine.startsWith('>')) {
|
|
75
|
+
const quoteText = trimmedLine.slice(1).trim();
|
|
76
|
+
blocks.push({
|
|
77
|
+
object: 'block',
|
|
78
|
+
type: 'quote',
|
|
79
|
+
quote: {
|
|
80
|
+
rich_text: parseRichText(quoteText),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Bulleted lists
|
|
87
|
+
if (trimmedLine.match(/^[-*]\s+/)) {
|
|
88
|
+
const text = trimmedLine.replace(/^[-*]\s+/, '');
|
|
89
|
+
blocks.push({
|
|
90
|
+
object: 'block',
|
|
91
|
+
type: 'bulleted_list_item',
|
|
92
|
+
bulleted_list_item: {
|
|
93
|
+
rich_text: parseRichText(text),
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
// Numbered lists
|
|
100
|
+
if (trimmedLine.match(/^\d+\.\s+/)) {
|
|
101
|
+
const text = trimmedLine.replace(/^\d+\.\s+/, '');
|
|
102
|
+
blocks.push({
|
|
103
|
+
object: 'block',
|
|
104
|
+
type: 'numbered_list_item',
|
|
105
|
+
numbered_list_item: {
|
|
106
|
+
rich_text: parseRichText(text),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
i++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Horizontal rule
|
|
113
|
+
if (trimmedLine.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
|
|
114
|
+
blocks.push({
|
|
115
|
+
object: 'block',
|
|
116
|
+
type: 'divider',
|
|
117
|
+
divider: {},
|
|
118
|
+
});
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Regular paragraph
|
|
123
|
+
blocks.push({
|
|
124
|
+
object: 'block',
|
|
125
|
+
type: 'paragraph',
|
|
126
|
+
paragraph: {
|
|
127
|
+
rich_text: parseRichText(trimmedLine),
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
return blocks;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Parse markdown text into Notion rich text format
|
|
136
|
+
* Supports: **bold**, *italic*, `code`, and [links](url)
|
|
137
|
+
*/
|
|
138
|
+
function parseRichText(text) {
|
|
139
|
+
if (!text) {
|
|
140
|
+
return [{ type: 'text', text: { content: '' } }];
|
|
141
|
+
}
|
|
142
|
+
const richText = [];
|
|
143
|
+
let currentText = '';
|
|
144
|
+
let i = 0;
|
|
145
|
+
while (i < text.length) {
|
|
146
|
+
// Bold: **text**
|
|
147
|
+
if (text[i] === '*' && text[i + 1] === '*') {
|
|
148
|
+
// Save any accumulated plain text
|
|
149
|
+
if (currentText) {
|
|
150
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
151
|
+
currentText = '';
|
|
152
|
+
}
|
|
153
|
+
// Find closing **
|
|
154
|
+
i += 2;
|
|
155
|
+
let boldText = '';
|
|
156
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '*')) {
|
|
157
|
+
boldText += text[i];
|
|
158
|
+
i++;
|
|
159
|
+
}
|
|
160
|
+
richText.push({
|
|
161
|
+
type: 'text',
|
|
162
|
+
text: { content: boldText },
|
|
163
|
+
annotations: { bold: true },
|
|
164
|
+
});
|
|
165
|
+
i += 2; // Skip closing **
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Italic: *text* or _text_
|
|
169
|
+
if ((text[i] === '*' || text[i] === '_') && text[i + 1] !== text[i]) {
|
|
170
|
+
const marker = text[i];
|
|
171
|
+
// Save any accumulated plain text
|
|
172
|
+
if (currentText) {
|
|
173
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
174
|
+
currentText = '';
|
|
175
|
+
}
|
|
176
|
+
// Find closing marker
|
|
177
|
+
i++;
|
|
178
|
+
let italicText = '';
|
|
179
|
+
while (i < text.length && text[i] !== marker) {
|
|
180
|
+
italicText += text[i];
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
richText.push({
|
|
184
|
+
type: 'text',
|
|
185
|
+
text: { content: italicText },
|
|
186
|
+
annotations: { italic: true },
|
|
187
|
+
});
|
|
188
|
+
i++; // Skip closing marker
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// Inline code: `text`
|
|
192
|
+
if (text[i] === '`') {
|
|
193
|
+
// Save any accumulated plain text
|
|
194
|
+
if (currentText) {
|
|
195
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
196
|
+
currentText = '';
|
|
197
|
+
}
|
|
198
|
+
// Find closing `
|
|
199
|
+
i++;
|
|
200
|
+
let codeText = '';
|
|
201
|
+
while (i < text.length && text[i] !== '`') {
|
|
202
|
+
codeText += text[i];
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
richText.push({
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: { content: codeText },
|
|
208
|
+
annotations: { code: true },
|
|
209
|
+
});
|
|
210
|
+
i++; // Skip closing `
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// Links: [text](url)
|
|
214
|
+
if (text[i] === '[') {
|
|
215
|
+
const linkStart = i;
|
|
216
|
+
let linkText = '';
|
|
217
|
+
i++;
|
|
218
|
+
// Find closing ]
|
|
219
|
+
while (i < text.length && text[i] !== ']') {
|
|
220
|
+
linkText += text[i];
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
// Check if followed by (url)
|
|
224
|
+
if (i < text.length && text[i] === ']' && text[i + 1] === '(') {
|
|
225
|
+
i += 2; // Skip ](
|
|
226
|
+
let url = '';
|
|
227
|
+
while (i < text.length && text[i] !== ')') {
|
|
228
|
+
url += text[i];
|
|
229
|
+
i++;
|
|
230
|
+
}
|
|
231
|
+
// Save any accumulated plain text
|
|
232
|
+
if (currentText) {
|
|
233
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
234
|
+
currentText = '';
|
|
235
|
+
}
|
|
236
|
+
richText.push({
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: { content: linkText, link: { url } },
|
|
239
|
+
});
|
|
240
|
+
i++; // Skip closing )
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Not a link, treat as plain text
|
|
245
|
+
currentText += text.slice(linkStart, i + 1);
|
|
246
|
+
i++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Regular character
|
|
251
|
+
currentText += text[i];
|
|
252
|
+
i++;
|
|
253
|
+
}
|
|
254
|
+
// Add any remaining text
|
|
255
|
+
if (currentText) {
|
|
256
|
+
richText.push({ type: 'text', text: { content: currentText } });
|
|
257
|
+
}
|
|
258
|
+
return richText.length > 0 ? richText : [{ type: 'text', text: { content: '' } }];
|
|
259
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
* 3. Cache lookup (exact + aliases)
|
|
14
|
+
* 4. API search fallback
|
|
15
|
+
* 5. Smart database_id → data_source_id resolution (for databases)
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Resolve Notion input (URL, ID, or name) to a clean Notion ID
|
|
19
|
+
*
|
|
20
|
+
* Supports URLs, IDs, and name-based lookups via cache and API search.
|
|
21
|
+
* For databases, automatically detects and converts database_id to data_source_id.
|
|
22
|
+
*
|
|
23
|
+
* @param input - Database/page name, ID, or URL
|
|
24
|
+
* @param type - Resource type (for better error messages)
|
|
25
|
+
* @returns Clean Notion ID (32 hex characters without dashes)
|
|
26
|
+
* @throws NotionCLIError if input cannot be resolved
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // URL
|
|
30
|
+
* await resolveNotionId('https://notion.so/1fb79d4c71bb8032b722c82305b63a00')
|
|
31
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Direct ID
|
|
35
|
+
* await resolveNotionId('1fb79d4c71bb8032b722c82305b63a00')
|
|
36
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Name (via cache or API)
|
|
40
|
+
* await resolveNotionId('Tasks Database', 'database')
|
|
41
|
+
* // Returns: '1fb79d4c71bb8032b722c82305b63a00'
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // database_id auto-conversion
|
|
45
|
+
* await resolveNotionId('abc123...', 'database')
|
|
46
|
+
* // If abc123 is a database_id, auto-resolves to data_source_id
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveNotionId(input: string, type?: 'database' | 'page'): Promise<string>;
|