@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.
Files changed (114) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1386 -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/batch/retrieve.d.ts +43 -0
  14. package/dist/commands/batch/retrieve.js +265 -0
  15. package/dist/commands/block/append.d.ts +42 -0
  16. package/dist/commands/block/append.js +219 -0
  17. package/dist/commands/block/delete.d.ts +30 -0
  18. package/dist/commands/block/delete.js +94 -0
  19. package/dist/commands/block/retrieve/children.d.ts +31 -0
  20. package/dist/commands/block/retrieve/children.js +174 -0
  21. package/dist/commands/block/retrieve.d.ts +30 -0
  22. package/dist/commands/block/retrieve.js +98 -0
  23. package/dist/commands/block/update.d.ts +45 -0
  24. package/dist/commands/block/update.js +241 -0
  25. package/dist/commands/cache/info.d.ts +19 -0
  26. package/dist/commands/cache/info.js +145 -0
  27. package/dist/commands/config/set-token.d.ts +30 -0
  28. package/dist/commands/config/set-token.js +201 -0
  29. package/dist/commands/db/create.d.ts +31 -0
  30. package/dist/commands/db/create.js +124 -0
  31. package/dist/commands/db/query.d.ts +41 -0
  32. package/dist/commands/db/query.js +355 -0
  33. package/dist/commands/db/retrieve.d.ts +33 -0
  34. package/dist/commands/db/retrieve.js +134 -0
  35. package/dist/commands/db/schema.d.ts +32 -0
  36. package/dist/commands/db/schema.js +308 -0
  37. package/dist/commands/db/update.d.ts +31 -0
  38. package/dist/commands/db/update.js +117 -0
  39. package/dist/commands/doctor.d.ts +50 -0
  40. package/dist/commands/doctor.js +420 -0
  41. package/dist/commands/init.d.ts +57 -0
  42. package/dist/commands/init.js +471 -0
  43. package/dist/commands/list.d.ts +29 -0
  44. package/dist/commands/list.js +184 -0
  45. package/dist/commands/page/create.d.ts +33 -0
  46. package/dist/commands/page/create.js +240 -0
  47. package/dist/commands/page/retrieve/property_item.d.ts +24 -0
  48. package/dist/commands/page/retrieve/property_item.js +72 -0
  49. package/dist/commands/page/retrieve.d.ts +36 -0
  50. package/dist/commands/page/retrieve.js +244 -0
  51. package/dist/commands/page/update.d.ts +34 -0
  52. package/dist/commands/page/update.js +184 -0
  53. package/dist/commands/search.d.ts +40 -0
  54. package/dist/commands/search.js +348 -0
  55. package/dist/commands/sync.d.ts +24 -0
  56. package/dist/commands/sync.js +183 -0
  57. package/dist/commands/user/list.d.ts +27 -0
  58. package/dist/commands/user/list.js +99 -0
  59. package/dist/commands/user/retrieve/bot.d.ts +28 -0
  60. package/dist/commands/user/retrieve/bot.js +96 -0
  61. package/dist/commands/user/retrieve.d.ts +30 -0
  62. package/dist/commands/user/retrieve.js +103 -0
  63. package/dist/commands/whoami.d.ts +19 -0
  64. package/dist/commands/whoami.js +175 -0
  65. package/dist/deduplication.d.ts +41 -0
  66. package/dist/deduplication.js +71 -0
  67. package/dist/envelope.d.ts +169 -0
  68. package/dist/envelope.js +257 -0
  69. package/dist/errors/enhanced-errors.d.ts +168 -0
  70. package/dist/errors/enhanced-errors.js +570 -0
  71. package/dist/errors/index.d.ts +18 -0
  72. package/dist/errors/index.js +33 -0
  73. package/dist/examples/cache-retry-examples.d.ts +64 -0
  74. package/dist/examples/cache-retry-examples.js +375 -0
  75. package/dist/helper.d.ts +102 -0
  76. package/dist/helper.js +885 -0
  77. package/dist/http-agent.d.ts +38 -0
  78. package/dist/http-agent.js +60 -0
  79. package/dist/index.d.ts +1 -0
  80. package/dist/index.js +4 -0
  81. package/dist/interface.d.ts +4 -0
  82. package/dist/interface.js +2 -0
  83. package/dist/notion.d.ts +144 -0
  84. package/dist/notion.js +547 -0
  85. package/dist/retry.d.ts +72 -0
  86. package/dist/retry.js +381 -0
  87. package/dist/utils/disk-cache.d.ts +80 -0
  88. package/dist/utils/disk-cache.js +291 -0
  89. package/dist/utils/markdown-to-blocks.d.ts +19 -0
  90. package/dist/utils/markdown-to-blocks.js +259 -0
  91. package/dist/utils/notion-resolver.d.ts +48 -0
  92. package/dist/utils/notion-resolver.js +262 -0
  93. package/dist/utils/notion-url-parser.d.ts +46 -0
  94. package/dist/utils/notion-url-parser.js +111 -0
  95. package/dist/utils/property-expander.d.ts +45 -0
  96. package/dist/utils/property-expander.js +323 -0
  97. package/dist/utils/schema-examples.d.ts +40 -0
  98. package/dist/utils/schema-examples.js +359 -0
  99. package/dist/utils/schema-extractor.d.ts +65 -0
  100. package/dist/utils/schema-extractor.js +235 -0
  101. package/dist/utils/table-formatter.d.ts +36 -0
  102. package/dist/utils/table-formatter.js +122 -0
  103. package/dist/utils/terminal-banner.d.ts +24 -0
  104. package/dist/utils/terminal-banner.js +34 -0
  105. package/dist/utils/token-validator.d.ts +55 -0
  106. package/dist/utils/token-validator.js +85 -0
  107. package/dist/utils/update-notifier.d.ts +26 -0
  108. package/dist/utils/update-notifier.js +54 -0
  109. package/dist/utils/workspace-cache.d.ts +58 -0
  110. package/dist/utils/workspace-cache.js +185 -0
  111. package/oclif.manifest.json +4497 -0
  112. package/package.json +115 -0
  113. package/scripts/banner.js +38 -0
  114. package/scripts/postinstall.js +56 -0
package/dist/retry.js ADDED
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+ /**
3
+ * Enhanced retry logic with exponential backoff and jitter
4
+ * Handles rate limiting, network errors, and transient failures
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.CircuitBreaker = void 0;
8
+ exports.isRetryableError = isRetryableError;
9
+ exports.calculateDelay = calculateDelay;
10
+ exports.fetchWithRetry = fetchWithRetry;
11
+ exports.batchWithRetry = batchWithRetry;
12
+ /**
13
+ * Default retry configuration
14
+ */
15
+ const DEFAULT_CONFIG = {
16
+ maxRetries: parseInt(process.env.NOTION_CLI_MAX_RETRIES || '3', 10),
17
+ baseDelay: parseInt(process.env.NOTION_CLI_BASE_DELAY || '1000', 10), // 1 second
18
+ maxDelay: parseInt(process.env.NOTION_CLI_MAX_DELAY || '30000', 10), // 30 seconds
19
+ exponentialBase: parseFloat(process.env.NOTION_CLI_EXP_BASE || '2'),
20
+ jitterFactor: parseFloat(process.env.NOTION_CLI_JITTER_FACTOR || '0.1'),
21
+ // HTTP status codes that should trigger a retry
22
+ retryableStatusCodes: [408, 429, 500, 502, 503, 504],
23
+ // Notion API error codes that are retryable
24
+ retryableErrorCodes: [
25
+ 'rate_limited',
26
+ 'service_unavailable',
27
+ 'internal_server_error',
28
+ 'conflict_error',
29
+ ],
30
+ };
31
+ /**
32
+ * Check if verbose logging is enabled
33
+ */
34
+ function isVerboseEnabled() {
35
+ return process.env.DEBUG === 'true' ||
36
+ process.env.NOTION_CLI_DEBUG === 'true' ||
37
+ process.env.NOTION_CLI_VERBOSE === 'true';
38
+ }
39
+ /**
40
+ * Log structured retry event to stderr
41
+ * Never pollutes stdout - safe for JSON output
42
+ */
43
+ function logRetryEvent(event) {
44
+ // Only log if verbose mode is enabled
45
+ if (!isVerboseEnabled()) {
46
+ return;
47
+ }
48
+ // Always write to stderr, never stdout
49
+ console.error(JSON.stringify(event));
50
+ }
51
+ /**
52
+ * Extract error reason from error object
53
+ */
54
+ function getErrorReason(error) {
55
+ if (error.code === 'rate_limited' || error.status === 429)
56
+ return 'RATE_LIMITED';
57
+ if (error.status === 503)
58
+ return 'SERVICE_UNAVAILABLE';
59
+ if (error.status === 502)
60
+ return 'BAD_GATEWAY';
61
+ if (error.status === 504)
62
+ return 'GATEWAY_TIMEOUT';
63
+ if (error.status === 500)
64
+ return 'INTERNAL_SERVER_ERROR';
65
+ if (error.status === 408)
66
+ return 'REQUEST_TIMEOUT';
67
+ if (error.code === 'ECONNRESET')
68
+ return 'CONNECTION_RESET';
69
+ if (error.code === 'ETIMEDOUT')
70
+ return 'TIMEOUT';
71
+ if (error.code === 'ENOTFOUND')
72
+ return 'DNS_ERROR';
73
+ if (error.code === 'EAI_AGAIN')
74
+ return 'DNS_LOOKUP_FAILED';
75
+ if (error.code === 'service_unavailable')
76
+ return 'SERVICE_UNAVAILABLE';
77
+ if (error.code === 'internal_server_error')
78
+ return 'INTERNAL_SERVER_ERROR';
79
+ if (error.code === 'conflict_error')
80
+ return 'CONFLICT';
81
+ return 'UNKNOWN';
82
+ }
83
+ /**
84
+ * Extract URL/endpoint from error object
85
+ */
86
+ function extractUrl(error, context) {
87
+ var _a, _b;
88
+ if (error.url)
89
+ return error.url;
90
+ if ((_a = error.request) === null || _a === void 0 ? void 0 : _a.url)
91
+ return error.request.url;
92
+ if ((_b = error.config) === null || _b === void 0 ? void 0 : _b.url)
93
+ return error.config.url;
94
+ return context;
95
+ }
96
+ /**
97
+ * Categorize errors into retryable and non-retryable
98
+ */
99
+ function isRetryableError(error, config = DEFAULT_CONFIG) {
100
+ // Network errors (no response)
101
+ if (!error.status && (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' ||
102
+ error.code === 'ENOTFOUND' || error.code === 'EAI_AGAIN')) {
103
+ return true;
104
+ }
105
+ // HTTP status codes
106
+ if (error.status && config.retryableStatusCodes.includes(error.status)) {
107
+ return true;
108
+ }
109
+ // Notion API error codes
110
+ if (error.code && config.retryableErrorCodes.includes(error.code)) {
111
+ return true;
112
+ }
113
+ // Don't retry client errors (400-499, except 408 and 429)
114
+ if (error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429) {
115
+ return false;
116
+ }
117
+ return false;
118
+ }
119
+ /**
120
+ * Calculate delay with exponential backoff and jitter
121
+ */
122
+ function calculateDelay(attempt, config = DEFAULT_CONFIG, retryAfterHeader) {
123
+ // If we have a Retry-After header from rate limiting, use it
124
+ if (retryAfterHeader) {
125
+ const retryAfter = parseInt(retryAfterHeader, 10);
126
+ if (!isNaN(retryAfter)) {
127
+ return Math.min(retryAfter * 1000, config.maxDelay);
128
+ }
129
+ }
130
+ // Calculate exponential backoff: baseDelay * (exponentialBase ^ attempt)
131
+ const exponentialDelay = config.baseDelay * Math.pow(config.exponentialBase, attempt - 1);
132
+ // Cap at maxDelay
133
+ const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
134
+ // Add jitter: random value between -jitterFactor and +jitterFactor
135
+ const jitter = cappedDelay * config.jitterFactor * (Math.random() * 2 - 1);
136
+ const finalDelay = Math.max(0, cappedDelay + jitter);
137
+ return Math.round(finalDelay);
138
+ }
139
+ /**
140
+ * Sleep for specified milliseconds
141
+ */
142
+ function sleep(ms) {
143
+ return new Promise(resolve => setTimeout(resolve, ms));
144
+ }
145
+ /**
146
+ * Enhanced retry wrapper with exponential backoff and jitter
147
+ */
148
+ async function fetchWithRetry(fn, options = {}) {
149
+ var _a, _b;
150
+ const config = { ...DEFAULT_CONFIG, ...options.config };
151
+ const { onRetry, context } = options;
152
+ let lastError;
153
+ let totalDelay = 0;
154
+ for (let attempt = 1; attempt <= config.maxRetries + 1; attempt++) {
155
+ try {
156
+ // Log attempt start (if verbose and not first attempt)
157
+ if (attempt > 1 && isVerboseEnabled()) {
158
+ logRetryEvent({
159
+ level: 'info',
160
+ event: 'retry_attempt',
161
+ attempt,
162
+ max_retries: config.maxRetries,
163
+ context,
164
+ timestamp: new Date().toISOString(),
165
+ });
166
+ }
167
+ return await fn();
168
+ }
169
+ catch (error) {
170
+ lastError = error;
171
+ // Check if we should retry
172
+ const shouldRetry = attempt <= config.maxRetries && isRetryableError(error, config);
173
+ if (!shouldRetry) {
174
+ // Log non-retryable error if verbose
175
+ if (isVerboseEnabled() && attempt > 1) {
176
+ logRetryEvent({
177
+ level: 'error',
178
+ event: 'retry_exhausted',
179
+ attempt,
180
+ max_retries: config.maxRetries,
181
+ reason: getErrorReason(error),
182
+ context,
183
+ status_code: error.status,
184
+ error_code: error.code,
185
+ timestamp: new Date().toISOString(),
186
+ });
187
+ }
188
+ throw error;
189
+ }
190
+ // Calculate delay
191
+ const retryAfter = ((_a = error.headers) === null || _a === void 0 ? void 0 : _a['retry-after']) || ((_b = error.headers) === null || _b === void 0 ? void 0 : _b['Retry-After']);
192
+ const delay = calculateDelay(attempt, config, retryAfter);
193
+ totalDelay += delay;
194
+ // Log rate limit event specifically
195
+ if (error.status === 429 || error.code === 'rate_limited') {
196
+ logRetryEvent({
197
+ level: 'warn',
198
+ event: 'rate_limited',
199
+ attempt,
200
+ max_retries: config.maxRetries,
201
+ reason: 'RATE_LIMITED',
202
+ retry_after_ms: delay,
203
+ url: extractUrl(error, context),
204
+ context,
205
+ status_code: error.status,
206
+ timestamp: new Date().toISOString(),
207
+ });
208
+ }
209
+ else {
210
+ // Log general retry event
211
+ logRetryEvent({
212
+ level: 'warn',
213
+ event: 'retry',
214
+ attempt,
215
+ max_retries: config.maxRetries,
216
+ reason: getErrorReason(error),
217
+ retry_after_ms: delay,
218
+ url: extractUrl(error, context),
219
+ context,
220
+ status_code: error.status,
221
+ error_code: error.code,
222
+ timestamp: new Date().toISOString(),
223
+ });
224
+ }
225
+ // Create retry context
226
+ const retryContext = {
227
+ attempt,
228
+ maxRetries: config.maxRetries,
229
+ lastError: error,
230
+ totalDelay,
231
+ };
232
+ // Call retry callback if provided (for custom logging/monitoring)
233
+ if (onRetry) {
234
+ onRetry(retryContext);
235
+ }
236
+ // Wait before retrying
237
+ await sleep(delay);
238
+ }
239
+ }
240
+ // Should never reach here, but TypeScript needs it
241
+ throw lastError;
242
+ }
243
+ /**
244
+ * Batch retry wrapper for multiple operations
245
+ * Executes operations with retry logic and collects results
246
+ */
247
+ async function batchWithRetry(operations, options = {}) {
248
+ const { concurrency = 5 } = options;
249
+ const results = [];
250
+ // Process operations in batches
251
+ for (let i = 0; i < operations.length; i += concurrency) {
252
+ const batch = operations.slice(i, i + concurrency);
253
+ const batchPromises = batch.map(async (op, index) => {
254
+ try {
255
+ const data = await fetchWithRetry(op, {
256
+ ...options,
257
+ context: `Operation ${i + index + 1}/${operations.length}`,
258
+ });
259
+ return { success: true, data };
260
+ }
261
+ catch (error) {
262
+ return { success: false, error };
263
+ }
264
+ });
265
+ const batchResults = await Promise.all(batchPromises);
266
+ results.push(...batchResults);
267
+ }
268
+ return results;
269
+ }
270
+ /**
271
+ * Retry wrapper with circuit breaker pattern
272
+ * Prevents cascading failures by stopping retries after too many failures
273
+ */
274
+ class CircuitBreaker {
275
+ constructor(failureThreshold = 5, successThreshold = 2, timeout = 60000 // 1 minute
276
+ ) {
277
+ this.failureThreshold = failureThreshold;
278
+ this.successThreshold = successThreshold;
279
+ this.timeout = timeout;
280
+ this.failures = 0;
281
+ this.successes = 0;
282
+ this.state = 'closed';
283
+ this.nextAttempt = 0;
284
+ }
285
+ async execute(fn, retryOptions) {
286
+ if (this.state === 'open') {
287
+ if (Date.now() < this.nextAttempt) {
288
+ // Log circuit breaker open event
289
+ if (isVerboseEnabled()) {
290
+ logRetryEvent({
291
+ level: 'error',
292
+ event: 'retry_exhausted',
293
+ attempt: 0,
294
+ max_retries: 0,
295
+ reason: 'CIRCUIT_OPEN',
296
+ context: 'Circuit breaker is open',
297
+ timestamp: new Date().toISOString(),
298
+ });
299
+ }
300
+ throw new Error('Circuit breaker is open. Too many failures.');
301
+ }
302
+ this.state = 'half-open';
303
+ // Log circuit breaker half-open event
304
+ if (isVerboseEnabled()) {
305
+ logRetryEvent({
306
+ level: 'info',
307
+ event: 'retry_attempt',
308
+ attempt: 1,
309
+ max_retries: this.successThreshold,
310
+ context: 'Circuit breaker entering half-open state',
311
+ timestamp: new Date().toISOString(),
312
+ });
313
+ }
314
+ }
315
+ try {
316
+ const result = await fetchWithRetry(fn, retryOptions);
317
+ this.onSuccess();
318
+ return result;
319
+ }
320
+ catch (error) {
321
+ this.onFailure();
322
+ throw error;
323
+ }
324
+ }
325
+ onSuccess() {
326
+ this.failures = 0;
327
+ if (this.state === 'half-open') {
328
+ this.successes++;
329
+ if (this.successes >= this.successThreshold) {
330
+ this.state = 'closed';
331
+ this.successes = 0;
332
+ // Log circuit breaker closed event
333
+ if (isVerboseEnabled()) {
334
+ logRetryEvent({
335
+ level: 'info',
336
+ event: 'retry_attempt',
337
+ attempt: this.successThreshold,
338
+ max_retries: this.successThreshold,
339
+ context: 'Circuit breaker closed - service recovered',
340
+ timestamp: new Date().toISOString(),
341
+ });
342
+ }
343
+ }
344
+ }
345
+ }
346
+ onFailure() {
347
+ this.failures++;
348
+ this.successes = 0;
349
+ if (this.failures >= this.failureThreshold) {
350
+ this.state = 'open';
351
+ this.nextAttempt = Date.now() + this.timeout;
352
+ // Log circuit breaker open event
353
+ if (isVerboseEnabled()) {
354
+ logRetryEvent({
355
+ level: 'error',
356
+ event: 'retry_exhausted',
357
+ attempt: this.failures,
358
+ max_retries: this.failureThreshold,
359
+ reason: 'CIRCUIT_OPENED',
360
+ retry_after_ms: this.timeout,
361
+ context: `Circuit breaker opened after ${this.failures} failures`,
362
+ timestamp: new Date().toISOString(),
363
+ });
364
+ }
365
+ }
366
+ }
367
+ getState() {
368
+ return {
369
+ state: this.state,
370
+ failures: this.failures,
371
+ successes: this.successes,
372
+ };
373
+ }
374
+ reset() {
375
+ this.state = 'closed';
376
+ this.failures = 0;
377
+ this.successes = 0;
378
+ this.nextAttempt = 0;
379
+ }
380
+ }
381
+ exports.CircuitBreaker = CircuitBreaker;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Disk Cache Manager
3
+ *
4
+ * Provides persistent caching to disk, maintaining cache across CLI invocations.
5
+ * Cache entries are stored in ~/.notion-cli/cache/ directory.
6
+ */
7
+ export interface DiskCacheEntry<T = any> {
8
+ key: string;
9
+ data: T;
10
+ expiresAt: number;
11
+ createdAt: number;
12
+ size: number;
13
+ }
14
+ export interface DiskCacheStats {
15
+ totalEntries: number;
16
+ totalSize: number;
17
+ oldestEntry: number | null;
18
+ newestEntry: number | null;
19
+ }
20
+ export declare class DiskCacheManager {
21
+ private cacheDir;
22
+ private maxSize;
23
+ private syncInterval;
24
+ private dirtyKeys;
25
+ private syncTimer;
26
+ private initialized;
27
+ constructor(options?: {
28
+ cacheDir?: string;
29
+ maxSize?: number;
30
+ syncInterval?: number;
31
+ });
32
+ /**
33
+ * Initialize disk cache (create directory, start sync timer)
34
+ */
35
+ initialize(): Promise<void>;
36
+ /**
37
+ * Get a cache entry from disk
38
+ */
39
+ get<T>(key: string): Promise<DiskCacheEntry<T> | null>;
40
+ /**
41
+ * Set a cache entry to disk
42
+ */
43
+ set<T>(key: string, data: T, ttl: number): Promise<void>;
44
+ /**
45
+ * Invalidate (delete) a cache entry
46
+ */
47
+ invalidate(key: string): Promise<void>;
48
+ /**
49
+ * Clear all cache entries
50
+ */
51
+ clear(): Promise<void>;
52
+ /**
53
+ * Sync dirty entries to disk
54
+ */
55
+ sync(): Promise<void>;
56
+ /**
57
+ * Shutdown (flush and cleanup)
58
+ */
59
+ shutdown(): Promise<void>;
60
+ /**
61
+ * Get cache statistics
62
+ */
63
+ getStats(): Promise<DiskCacheStats>;
64
+ /**
65
+ * Enforce maximum cache size by removing oldest entries
66
+ */
67
+ private enforceMaxSize;
68
+ /**
69
+ * Ensure cache directory exists
70
+ */
71
+ private ensureCacheDir;
72
+ /**
73
+ * Get file path for a cache key
74
+ */
75
+ private getFilePath;
76
+ }
77
+ /**
78
+ * Global singleton instance
79
+ */
80
+ export declare const diskCacheManager: DiskCacheManager;