@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
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,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bookmarks Utility
|
|
3
|
+
*
|
|
4
|
+
* User-defined shortcuts to frequently-used Notion pages and databases.
|
|
5
|
+
* Stored at ~/.notion-cli/bookmarks.json alongside the workspace cache.
|
|
6
|
+
*
|
|
7
|
+
* Bookmarks integrate into the resolver — once saved, a bookmark name
|
|
8
|
+
* works anywhere an ID or URL does (e.g. `notion-cli db query inbox`).
|
|
9
|
+
*/
|
|
10
|
+
export interface Bookmark {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'database' | 'page';
|
|
13
|
+
}
|
|
14
|
+
export interface BookmarksData {
|
|
15
|
+
version: string;
|
|
16
|
+
default: string | null;
|
|
17
|
+
bookmarks: Record<string, Bookmark>;
|
|
18
|
+
}
|
|
19
|
+
/** Load bookmarks from disk. Returns empty structure if file doesn't exist. */
|
|
20
|
+
export declare function loadBookmarks(): Promise<BookmarksData>;
|
|
21
|
+
/** Save bookmarks to disk (atomic write via tmp + rename). */
|
|
22
|
+
export declare function saveBookmarks(data: BookmarksData): Promise<void>;
|
|
23
|
+
/** Get a single bookmark by name. Returns null if not found. */
|
|
24
|
+
export declare function getBookmark(name: string): Promise<Bookmark | null>;
|
|
25
|
+
/** Save or update a bookmark. */
|
|
26
|
+
export declare function setBookmark(name: string, id: string, type: 'database' | 'page'): Promise<void>;
|
|
27
|
+
/** Remove a bookmark. Returns true if it existed. */
|
|
28
|
+
export declare function removeBookmark(name: string): Promise<boolean>;
|
|
29
|
+
/** Get the default bookmark name (used by `quick` command). */
|
|
30
|
+
export declare function getDefaultBookmark(): Promise<string | null>;
|
|
31
|
+
/** Set the default bookmark name. Throws if bookmark doesn't exist. */
|
|
32
|
+
export declare function setDefaultBookmark(name: string): Promise<void>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Bookmarks Utility
|
|
4
|
+
*
|
|
5
|
+
* User-defined shortcuts to frequently-used Notion pages and databases.
|
|
6
|
+
* Stored at ~/.notion-cli/bookmarks.json alongside the workspace cache.
|
|
7
|
+
*
|
|
8
|
+
* Bookmarks integrate into the resolver — once saved, a bookmark name
|
|
9
|
+
* works anywhere an ID or URL does (e.g. `notion-cli db query inbox`).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.loadBookmarks = loadBookmarks;
|
|
13
|
+
exports.saveBookmarks = saveBookmarks;
|
|
14
|
+
exports.getBookmark = getBookmark;
|
|
15
|
+
exports.setBookmark = setBookmark;
|
|
16
|
+
exports.removeBookmark = removeBookmark;
|
|
17
|
+
exports.getDefaultBookmark = getDefaultBookmark;
|
|
18
|
+
exports.setDefaultBookmark = setDefaultBookmark;
|
|
19
|
+
const fs = require("fs/promises");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const workspace_cache_1 = require("./workspace-cache");
|
|
22
|
+
// -- Constants --
|
|
23
|
+
const BOOKMARKS_FILE = 'bookmarks.json';
|
|
24
|
+
const BOOKMARKS_VERSION = '1.0.0';
|
|
25
|
+
function getBookmarksPath() {
|
|
26
|
+
return path.join((0, workspace_cache_1.getCacheDir)(), BOOKMARKS_FILE);
|
|
27
|
+
}
|
|
28
|
+
// -- Core operations --
|
|
29
|
+
/** Load bookmarks from disk. Returns empty structure if file doesn't exist. */
|
|
30
|
+
async function loadBookmarks() {
|
|
31
|
+
try {
|
|
32
|
+
const content = await fs.readFile(getBookmarksPath(), 'utf-8');
|
|
33
|
+
const data = JSON.parse(content);
|
|
34
|
+
// Validate structure
|
|
35
|
+
if (!data.version || typeof data.bookmarks !== 'object') {
|
|
36
|
+
return createEmpty();
|
|
37
|
+
}
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error.code === 'ENOENT')
|
|
42
|
+
return createEmpty();
|
|
43
|
+
return createEmpty();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Save bookmarks to disk (atomic write via tmp + rename). */
|
|
47
|
+
async function saveBookmarks(data) {
|
|
48
|
+
await (0, workspace_cache_1.ensureCacheDir)();
|
|
49
|
+
const filePath = getBookmarksPath();
|
|
50
|
+
const tmpPath = `${filePath}.tmp`;
|
|
51
|
+
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
52
|
+
await fs.rename(tmpPath, filePath);
|
|
53
|
+
}
|
|
54
|
+
/** Get a single bookmark by name. Returns null if not found. */
|
|
55
|
+
async function getBookmark(name) {
|
|
56
|
+
var _a;
|
|
57
|
+
const data = await loadBookmarks();
|
|
58
|
+
return (_a = data.bookmarks[name.toLowerCase()]) !== null && _a !== void 0 ? _a : null;
|
|
59
|
+
}
|
|
60
|
+
/** Save or update a bookmark. */
|
|
61
|
+
async function setBookmark(name, id, type) {
|
|
62
|
+
const data = await loadBookmarks();
|
|
63
|
+
data.bookmarks[name.toLowerCase()] = { id, type };
|
|
64
|
+
await saveBookmarks(data);
|
|
65
|
+
}
|
|
66
|
+
/** Remove a bookmark. Returns true if it existed. */
|
|
67
|
+
async function removeBookmark(name) {
|
|
68
|
+
const data = await loadBookmarks();
|
|
69
|
+
const key = name.toLowerCase();
|
|
70
|
+
if (!(key in data.bookmarks))
|
|
71
|
+
return false;
|
|
72
|
+
delete data.bookmarks[key];
|
|
73
|
+
// Clear default if it pointed to the removed bookmark
|
|
74
|
+
if (data.default === key) {
|
|
75
|
+
data.default = null;
|
|
76
|
+
}
|
|
77
|
+
await saveBookmarks(data);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/** Get the default bookmark name (used by `quick` command). */
|
|
81
|
+
async function getDefaultBookmark() {
|
|
82
|
+
const data = await loadBookmarks();
|
|
83
|
+
return data.default;
|
|
84
|
+
}
|
|
85
|
+
/** Set the default bookmark name. Throws if bookmark doesn't exist. */
|
|
86
|
+
async function setDefaultBookmark(name) {
|
|
87
|
+
const data = await loadBookmarks();
|
|
88
|
+
const key = name.toLowerCase();
|
|
89
|
+
if (!(key in data.bookmarks)) {
|
|
90
|
+
throw new Error(`Bookmark "${name}" does not exist. Create it first with: bookmark set ${name} <ID>`);
|
|
91
|
+
}
|
|
92
|
+
data.default = key;
|
|
93
|
+
await saveBookmarks(data);
|
|
94
|
+
}
|
|
95
|
+
// -- Helpers --
|
|
96
|
+
function createEmpty() {
|
|
97
|
+
return { version: BOOKMARKS_VERSION, default: null, bookmarks: {} };
|
|
98
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily Config Utility
|
|
3
|
+
*
|
|
4
|
+
* Persistent configuration for the `daily` command.
|
|
5
|
+
* Stored at ~/.notion-cli/daily.json alongside bookmarks and workspace cache.
|
|
6
|
+
*
|
|
7
|
+
* Follows the same atomic-write pattern as bookmarks.ts:
|
|
8
|
+
* write to .tmp first, then rename (safe against mid-write crashes).
|
|
9
|
+
*/
|
|
10
|
+
export interface DailyConfig {
|
|
11
|
+
version: string;
|
|
12
|
+
database_id: string;
|
|
13
|
+
title_property: string;
|
|
14
|
+
date_property: string;
|
|
15
|
+
title_format: string;
|
|
16
|
+
}
|
|
17
|
+
/** Load daily config from disk. Returns null if not yet configured. */
|
|
18
|
+
export declare function loadDailyConfig(): Promise<DailyConfig | null>;
|
|
19
|
+
/** Save daily config to disk (atomic write via tmp + rename). */
|
|
20
|
+
export declare function saveDailyConfig(config: DailyConfig): Promise<void>;
|
|
21
|
+
/** Factory: create a DailyConfig with sensible defaults. */
|
|
22
|
+
export declare function createDailyConfig(databaseId: string, titleProperty: string, dateProperty: string, titleFormat?: string): DailyConfig;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Daily Config Utility
|
|
4
|
+
*
|
|
5
|
+
* Persistent configuration for the `daily` command.
|
|
6
|
+
* Stored at ~/.notion-cli/daily.json alongside bookmarks and workspace cache.
|
|
7
|
+
*
|
|
8
|
+
* Follows the same atomic-write pattern as bookmarks.ts:
|
|
9
|
+
* write to .tmp first, then rename (safe against mid-write crashes).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.loadDailyConfig = loadDailyConfig;
|
|
13
|
+
exports.saveDailyConfig = saveDailyConfig;
|
|
14
|
+
exports.createDailyConfig = createDailyConfig;
|
|
15
|
+
const fs = require("fs/promises");
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const workspace_cache_1 = require("./workspace-cache");
|
|
18
|
+
// -- Constants --
|
|
19
|
+
const DAILY_FILE = 'daily.json';
|
|
20
|
+
const DAILY_VERSION = '1.0.0';
|
|
21
|
+
const DEFAULT_TITLE_FORMAT = 'YYYY-MM-DD';
|
|
22
|
+
function getDailyConfigPath() {
|
|
23
|
+
return path.join((0, workspace_cache_1.getCacheDir)(), DAILY_FILE);
|
|
24
|
+
}
|
|
25
|
+
// -- Core operations --
|
|
26
|
+
/** Load daily config from disk. Returns null if not yet configured. */
|
|
27
|
+
async function loadDailyConfig() {
|
|
28
|
+
try {
|
|
29
|
+
const content = await fs.readFile(getDailyConfigPath(), 'utf-8');
|
|
30
|
+
const data = JSON.parse(content);
|
|
31
|
+
// Validate: must have the essential fields to be usable
|
|
32
|
+
if (!data.version || !data.database_id || !data.date_property) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === 'ENOENT')
|
|
39
|
+
return null;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Save daily config to disk (atomic write via tmp + rename). */
|
|
44
|
+
async function saveDailyConfig(config) {
|
|
45
|
+
await (0, workspace_cache_1.ensureCacheDir)();
|
|
46
|
+
const filePath = getDailyConfigPath();
|
|
47
|
+
const tmpPath = `${filePath}.tmp`;
|
|
48
|
+
await fs.writeFile(tmpPath, JSON.stringify(config, null, 2), { encoding: 'utf-8', mode: 0o600 });
|
|
49
|
+
await fs.rename(tmpPath, filePath);
|
|
50
|
+
}
|
|
51
|
+
/** Factory: create a DailyConfig with sensible defaults. */
|
|
52
|
+
function createDailyConfig(databaseId, titleProperty, dateProperty, titleFormat = DEFAULT_TITLE_FORMAT) {
|
|
53
|
+
return {
|
|
54
|
+
version: DAILY_VERSION,
|
|
55
|
+
database_id: databaseId,
|
|
56
|
+
title_property: titleProperty,
|
|
57
|
+
date_property: dateProperty,
|
|
58
|
+
title_format: titleFormat,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -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;
|