@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,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[];