@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,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), { encoding: 'utf-8', mode: 0o600 });
|
|
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, mode: 0o700 });
|
|
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,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fuzzy matching utilities for typo-tolerant name resolution.
|
|
3
|
+
*
|
|
4
|
+
* Uses Levenshtein distance (edit distance) to find the closest match
|
|
5
|
+
* when exact/substring lookups fail. No external dependencies — the
|
|
6
|
+
* algorithm is ~20 lines of classic dynamic programming.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Levenshtein distance — minimum single-char edits (insert, delete, replace)
|
|
10
|
+
* to transform string `a` into string `b`.
|
|
11
|
+
*
|
|
12
|
+
* Classic DP approach, O(m*n) time and O(min(m,n)) space.
|
|
13
|
+
* Both inputs should be pre-normalized (lowercased/trimmed) by the caller.
|
|
14
|
+
*/
|
|
15
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
16
|
+
/**
|
|
17
|
+
* Find the best fuzzy match from a list of candidates.
|
|
18
|
+
*
|
|
19
|
+
* Returns the candidate with the lowest Levenshtein distance,
|
|
20
|
+
* but ONLY if the distance is within a dynamic threshold:
|
|
21
|
+
* threshold = max(2, floor(query.length / 4))
|
|
22
|
+
*
|
|
23
|
+
* - Short names (≤8 chars): allows up to 2 typos
|
|
24
|
+
* - Longer names: ~25% of query length
|
|
25
|
+
*
|
|
26
|
+
* @param query - Normalized search query (lowercase, trimmed)
|
|
27
|
+
* @param candidates - Array of { key: unique ID, value: normalized name }
|
|
28
|
+
* @returns Best match { match: key, distance } or null if nothing is close enough
|
|
29
|
+
*/
|
|
30
|
+
export declare function fuzzyMatch(query: string, candidates: {
|
|
31
|
+
key: string;
|
|
32
|
+
value: string;
|
|
33
|
+
}[]): {
|
|
34
|
+
match: string;
|
|
35
|
+
distance: number;
|
|
36
|
+
} | null;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fuzzy matching utilities for typo-tolerant name resolution.
|
|
4
|
+
*
|
|
5
|
+
* Uses Levenshtein distance (edit distance) to find the closest match
|
|
6
|
+
* when exact/substring lookups fail. No external dependencies — the
|
|
7
|
+
* algorithm is ~20 lines of classic dynamic programming.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.levenshtein = levenshtein;
|
|
11
|
+
exports.fuzzyMatch = fuzzyMatch;
|
|
12
|
+
/**
|
|
13
|
+
* Levenshtein distance — minimum single-char edits (insert, delete, replace)
|
|
14
|
+
* to transform string `a` into string `b`.
|
|
15
|
+
*
|
|
16
|
+
* Classic DP approach, O(m*n) time and O(min(m,n)) space.
|
|
17
|
+
* Both inputs should be pre-normalized (lowercased/trimmed) by the caller.
|
|
18
|
+
*/
|
|
19
|
+
function levenshtein(a, b) {
|
|
20
|
+
// Optimization: ensure `a` is the shorter string so we use less memory
|
|
21
|
+
if (a.length > b.length)
|
|
22
|
+
[a, b] = [b, a];
|
|
23
|
+
const m = a.length;
|
|
24
|
+
const n = b.length;
|
|
25
|
+
// Single row of the DP matrix (+ previous value), reused per iteration
|
|
26
|
+
let prev = Array.from({ length: m + 1 }, (_, i) => i);
|
|
27
|
+
for (let j = 1; j <= n; j++) {
|
|
28
|
+
const curr = [j]; // first column = number of insertions
|
|
29
|
+
for (let i = 1; i <= m; i++) {
|
|
30
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
31
|
+
curr[i] = Math.min(curr[i - 1] + 1, // insert
|
|
32
|
+
prev[i] + 1, // delete
|
|
33
|
+
prev[i - 1] + cost);
|
|
34
|
+
}
|
|
35
|
+
prev = curr;
|
|
36
|
+
}
|
|
37
|
+
return prev[m];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Find the best fuzzy match from a list of candidates.
|
|
41
|
+
*
|
|
42
|
+
* Returns the candidate with the lowest Levenshtein distance,
|
|
43
|
+
* but ONLY if the distance is within a dynamic threshold:
|
|
44
|
+
* threshold = max(2, floor(query.length / 4))
|
|
45
|
+
*
|
|
46
|
+
* - Short names (≤8 chars): allows up to 2 typos
|
|
47
|
+
* - Longer names: ~25% of query length
|
|
48
|
+
*
|
|
49
|
+
* @param query - Normalized search query (lowercase, trimmed)
|
|
50
|
+
* @param candidates - Array of { key: unique ID, value: normalized name }
|
|
51
|
+
* @returns Best match { match: key, distance } or null if nothing is close enough
|
|
52
|
+
*/
|
|
53
|
+
function fuzzyMatch(query, candidates) {
|
|
54
|
+
const threshold = Math.max(2, Math.floor(query.length / 4));
|
|
55
|
+
let bestKey = null;
|
|
56
|
+
let bestDist = threshold + 1; // start above threshold so first valid match wins
|
|
57
|
+
for (const { key, value } of candidates) {
|
|
58
|
+
const dist = levenshtein(query, value);
|
|
59
|
+
if (dist < bestDist) {
|
|
60
|
+
bestDist = dist;
|
|
61
|
+
bestKey = key;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Only return if we found something within the allowed threshold
|
|
65
|
+
if (bestKey !== null && bestDist <= threshold) {
|
|
66
|
+
return { match: bestKey, distance: bestDist };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Page Navigator
|
|
3
|
+
*
|
|
4
|
+
* Provides arrow-key navigation through Notion page trees.
|
|
5
|
+
* Reuses mapPageStructure() for cached, parallel data fetching.
|
|
6
|
+
*
|
|
7
|
+
* Exported functions:
|
|
8
|
+
* - extractNavigableItems(): filters child_page/child_database from structure (pure)
|
|
9
|
+
* - buildChoices(): builds @inquirer/select choices array (pure)
|
|
10
|
+
* - startNavigator(): main navigation loop (side-effectful)
|
|
11
|
+
*/
|
|
12
|
+
import { mapPageStructure } from '../notion';
|
|
13
|
+
/** A navigable item extracted from page structure */
|
|
14
|
+
export interface NavigableItem {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
type: 'child_page' | 'child_database';
|
|
18
|
+
}
|
|
19
|
+
/** A choice for the select prompt */
|
|
20
|
+
export interface NavigatorChoice {
|
|
21
|
+
name: string;
|
|
22
|
+
value: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const NAV_BACK = "__nav_back__";
|
|
25
|
+
export declare const NAV_QUIT = "__nav_quit__";
|
|
26
|
+
/**
|
|
27
|
+
* Extract navigable items (child pages and databases) from page structure.
|
|
28
|
+
* Pure function — no side effects.
|
|
29
|
+
*
|
|
30
|
+
* @param structure - The structure array from mapPageStructure()
|
|
31
|
+
* @returns Filtered list of navigable items
|
|
32
|
+
*/
|
|
33
|
+
export declare function extractNavigableItems(structure: Array<{
|
|
34
|
+
type: string;
|
|
35
|
+
id: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
}>): NavigableItem[];
|
|
38
|
+
/**
|
|
39
|
+
* Build choices array for @inquirer/select.
|
|
40
|
+
* Pure function — no side effects.
|
|
41
|
+
*
|
|
42
|
+
* @param items - Navigable items to display
|
|
43
|
+
* @param hasParent - Whether to show ".. Back" option
|
|
44
|
+
* @returns Choices array with navigation items, Back, and Quit
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildChoices(items: NavigableItem[], hasParent: boolean): NavigatorChoice[];
|
|
47
|
+
/** Dependencies injectable for testing */
|
|
48
|
+
export interface NavigatorDeps {
|
|
49
|
+
selectFn: (config: {
|
|
50
|
+
message: string;
|
|
51
|
+
choices: NavigatorChoice[];
|
|
52
|
+
}) => Promise<string>;
|
|
53
|
+
fetchStructure: (pageId: string) => ReturnType<typeof mapPageStructure>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Main interactive navigation loop.
|
|
57
|
+
* Fetches page structure, displays select prompt, navigates on selection.
|
|
58
|
+
*
|
|
59
|
+
* @param startPageId - The page ID to start navigating from
|
|
60
|
+
* @param log - Logging function (typically this.log from oclif)
|
|
61
|
+
* @param deps - Injectable dependencies (for testing)
|
|
62
|
+
*/
|
|
63
|
+
export declare function startNavigator(startPageId: string, log: (msg: string) => void, deps?: NavigatorDeps): Promise<void>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Interactive Page Navigator
|
|
4
|
+
*
|
|
5
|
+
* Provides arrow-key navigation through Notion page trees.
|
|
6
|
+
* Reuses mapPageStructure() for cached, parallel data fetching.
|
|
7
|
+
*
|
|
8
|
+
* Exported functions:
|
|
9
|
+
* - extractNavigableItems(): filters child_page/child_database from structure (pure)
|
|
10
|
+
* - buildChoices(): builds @inquirer/select choices array (pure)
|
|
11
|
+
* - startNavigator(): main navigation loop (side-effectful)
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.NAV_QUIT = exports.NAV_BACK = void 0;
|
|
15
|
+
exports.extractNavigableItems = extractNavigableItems;
|
|
16
|
+
exports.buildChoices = buildChoices;
|
|
17
|
+
exports.startNavigator = startNavigator;
|
|
18
|
+
const select_1 = require("@inquirer/select");
|
|
19
|
+
const notion_1 = require("../notion");
|
|
20
|
+
const errors_1 = require("../errors");
|
|
21
|
+
// Sentinel values for navigation actions
|
|
22
|
+
exports.NAV_BACK = '__nav_back__';
|
|
23
|
+
exports.NAV_QUIT = '__nav_quit__';
|
|
24
|
+
/**
|
|
25
|
+
* Extract navigable items (child pages and databases) from page structure.
|
|
26
|
+
* Pure function — no side effects.
|
|
27
|
+
*
|
|
28
|
+
* @param structure - The structure array from mapPageStructure()
|
|
29
|
+
* @returns Filtered list of navigable items
|
|
30
|
+
*/
|
|
31
|
+
function extractNavigableItems(structure) {
|
|
32
|
+
return structure
|
|
33
|
+
.filter((item) => item.type === 'child_page' || item.type === 'child_database')
|
|
34
|
+
.map((item) => ({
|
|
35
|
+
id: item.id,
|
|
36
|
+
title: item.title || 'Untitled',
|
|
37
|
+
type: item.type,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build choices array for @inquirer/select.
|
|
42
|
+
* Pure function — no side effects.
|
|
43
|
+
*
|
|
44
|
+
* @param items - Navigable items to display
|
|
45
|
+
* @param hasParent - Whether to show ".. Back" option
|
|
46
|
+
* @returns Choices array with navigation items, Back, and Quit
|
|
47
|
+
*/
|
|
48
|
+
function buildChoices(items, hasParent) {
|
|
49
|
+
const choices = [];
|
|
50
|
+
// Back option at top when not at root
|
|
51
|
+
if (hasParent) {
|
|
52
|
+
choices.push({ name: '.. Back', value: exports.NAV_BACK });
|
|
53
|
+
}
|
|
54
|
+
// Child pages and databases
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
const prefix = item.type === 'child_database' ? '[DB] ' : '';
|
|
57
|
+
choices.push({ name: `${prefix}${item.title}`, value: item.id });
|
|
58
|
+
}
|
|
59
|
+
// Quit at bottom
|
|
60
|
+
choices.push({ name: '[Quit]', value: exports.NAV_QUIT });
|
|
61
|
+
return choices;
|
|
62
|
+
}
|
|
63
|
+
// Default deps use real implementations
|
|
64
|
+
const defaultDeps = {
|
|
65
|
+
selectFn: (config) => (0, select_1.default)(config),
|
|
66
|
+
fetchStructure: notion_1.mapPageStructure,
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Main interactive navigation loop.
|
|
70
|
+
* Fetches page structure, displays select prompt, navigates on selection.
|
|
71
|
+
*
|
|
72
|
+
* @param startPageId - The page ID to start navigating from
|
|
73
|
+
* @param log - Logging function (typically this.log from oclif)
|
|
74
|
+
* @param deps - Injectable dependencies (for testing)
|
|
75
|
+
*/
|
|
76
|
+
async function startNavigator(startPageId, log, deps = defaultDeps) {
|
|
77
|
+
// Guard: must be interactive terminal
|
|
78
|
+
if (!process.stdin.isTTY) {
|
|
79
|
+
throw new errors_1.NotionCLIError(errors_1.NotionCLIErrorCode.VALIDATION_ERROR, 'browse requires an interactive terminal (TTY)', [{ description: 'Run this command in a terminal, not piped or in CI' }]);
|
|
80
|
+
}
|
|
81
|
+
// Navigation history stack — push on enter, pop on back
|
|
82
|
+
const history = [];
|
|
83
|
+
let currentPageId = startPageId;
|
|
84
|
+
// Navigation loop
|
|
85
|
+
while (true) {
|
|
86
|
+
let pageData;
|
|
87
|
+
try {
|
|
88
|
+
pageData = await deps.fetchStructure(currentPageId);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw error instanceof errors_1.NotionCLIError ? error : (0, errors_1.wrapNotionError)(error, {
|
|
92
|
+
resourceType: 'page',
|
|
93
|
+
attemptedId: currentPageId,
|
|
94
|
+
endpoint: 'pages.retrieve',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const items = extractNavigableItems(pageData.structure);
|
|
98
|
+
const icon = pageData.icon || '';
|
|
99
|
+
const header = `${icon}${icon ? ' ' : ''}${pageData.title} (${items.length} children)`;
|
|
100
|
+
log(`\n${header}`);
|
|
101
|
+
const choices = buildChoices(items, history.length > 0);
|
|
102
|
+
// Nothing to navigate if no children and no parent
|
|
103
|
+
if (items.length === 0 && history.length === 0) {
|
|
104
|
+
log('No child pages or databases found.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const selected = await deps.selectFn({
|
|
108
|
+
message: 'Navigate to:',
|
|
109
|
+
choices,
|
|
110
|
+
});
|
|
111
|
+
if (selected === exports.NAV_QUIT) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (selected === exports.NAV_BACK) {
|
|
115
|
+
// Pop from history to go back
|
|
116
|
+
currentPageId = history.pop();
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Navigate into selected page
|
|
120
|
+
history.push(currentPageId);
|
|
121
|
+
currentPageId = selected;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
* - Checkboxes (to_do blocks)
|
|
13
|
+
* - Code blocks
|
|
14
|
+
* - Tables (with header detection)
|
|
15
|
+
* - Quotes
|
|
16
|
+
* - Bold, italic, strikethrough, and inline code formatting
|
|
17
|
+
*
|
|
18
|
+
* @param markdown - The markdown string to convert
|
|
19
|
+
* @returns Array of Notion block objects
|
|
20
|
+
*/
|
|
21
|
+
export declare function markdownToBlocks(markdown: string): any[];
|