@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,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Base Command with Envelope Support
|
|
4
|
+
*
|
|
5
|
+
* Extends oclif Command with automatic envelope wrapping for consistent JSON output.
|
|
6
|
+
* All commands should extend this class to get automatic envelope support.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.EnvelopeFlags = exports.BaseCommand = void 0;
|
|
10
|
+
const core_1 = require("@oclif/core");
|
|
11
|
+
const envelope_1 = require("./envelope");
|
|
12
|
+
const index_1 = require("./errors/index");
|
|
13
|
+
const disk_cache_1 = require("./utils/disk-cache");
|
|
14
|
+
const http_agent_1 = require("./http-agent");
|
|
15
|
+
/**
|
|
16
|
+
* BaseCommand - Extends oclif Command with envelope support
|
|
17
|
+
*
|
|
18
|
+
* Features:
|
|
19
|
+
* - Automatic envelope wrapping for JSON output
|
|
20
|
+
* - Consistent error handling
|
|
21
|
+
* - Execution time tracking
|
|
22
|
+
* - Version metadata injection
|
|
23
|
+
* - Stdout/stderr separation
|
|
24
|
+
*/
|
|
25
|
+
class BaseCommand extends core_1.Command {
|
|
26
|
+
constructor() {
|
|
27
|
+
super(...arguments);
|
|
28
|
+
this.shouldUseEnvelope = false;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Initialize command and create envelope formatter
|
|
32
|
+
*/
|
|
33
|
+
async init() {
|
|
34
|
+
var _a;
|
|
35
|
+
await super.init();
|
|
36
|
+
// Initialize disk cache (load from disk)
|
|
37
|
+
const diskCacheEnabled = process.env.NOTION_CLI_DISK_CACHE_ENABLED !== 'false';
|
|
38
|
+
if (diskCacheEnabled) {
|
|
39
|
+
try {
|
|
40
|
+
await disk_cache_1.diskCacheManager.initialize();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// Silently ignore disk cache initialization errors
|
|
44
|
+
if (process.env.DEBUG) {
|
|
45
|
+
console.error('Failed to initialize disk cache:', error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Get command name from ID (e.g., "page:retrieve" -> "page retrieve")
|
|
50
|
+
const commandName = ((_a = this.id) === null || _a === void 0 ? void 0 : _a.replace(/:/g, ' ')) || 'unknown';
|
|
51
|
+
// Get version from config
|
|
52
|
+
const version = this.config.version;
|
|
53
|
+
// Initialize envelope formatter
|
|
54
|
+
this.envelope = new envelope_1.EnvelopeFormatter(commandName, version);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Cleanup hook - flushes disk cache and destroys HTTP agents before exit
|
|
58
|
+
*/
|
|
59
|
+
async finally(error) {
|
|
60
|
+
// Destroy HTTP agents to close all connections
|
|
61
|
+
try {
|
|
62
|
+
(0, http_agent_1.destroyAgents)();
|
|
63
|
+
}
|
|
64
|
+
catch (agentError) {
|
|
65
|
+
// Silently ignore agent cleanup errors
|
|
66
|
+
if (process.env.DEBUG) {
|
|
67
|
+
console.error('Failed to destroy HTTP agents:', agentError);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Flush disk cache before exit
|
|
71
|
+
const diskCacheEnabled = process.env.NOTION_CLI_DISK_CACHE_ENABLED !== 'false';
|
|
72
|
+
if (diskCacheEnabled) {
|
|
73
|
+
try {
|
|
74
|
+
await disk_cache_1.diskCacheManager.shutdown();
|
|
75
|
+
}
|
|
76
|
+
catch (shutdownError) {
|
|
77
|
+
// Silently ignore shutdown errors
|
|
78
|
+
if (process.env.DEBUG) {
|
|
79
|
+
console.error('Failed to shutdown disk cache:', shutdownError);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
await super.finally(error);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Determine if envelope should be used based on flags
|
|
87
|
+
*/
|
|
88
|
+
checkEnvelopeUsage(flags) {
|
|
89
|
+
return !!(flags.json || flags['compact-json']);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Output success response with automatic envelope wrapping
|
|
93
|
+
*
|
|
94
|
+
* @param data - Response data
|
|
95
|
+
* @param flags - Command flags
|
|
96
|
+
* @param additionalMetadata - Optional metadata to include
|
|
97
|
+
*/
|
|
98
|
+
outputSuccess(data, flags, additionalMetadata) {
|
|
99
|
+
// Check if we should use envelope
|
|
100
|
+
this.shouldUseEnvelope = this.checkEnvelopeUsage(flags);
|
|
101
|
+
if (this.shouldUseEnvelope) {
|
|
102
|
+
const envelope = this.envelope.wrapSuccess(data, additionalMetadata);
|
|
103
|
+
this.envelope.outputEnvelope(envelope, flags, this.log.bind(this));
|
|
104
|
+
process.exit(this.envelope.getExitCode(envelope));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Non-envelope output (table, markdown, etc.) - handled by caller
|
|
108
|
+
// This path should not normally be reached as caller handles non-JSON output
|
|
109
|
+
throw new Error('outputSuccess should only be called for JSON output');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Output error response with automatic envelope wrapping
|
|
114
|
+
*
|
|
115
|
+
* @param error - Error object
|
|
116
|
+
* @param flags - Command flags
|
|
117
|
+
* @param additionalContext - Optional error context
|
|
118
|
+
*/
|
|
119
|
+
outputError(error, flags, additionalContext) {
|
|
120
|
+
// Wrap raw errors in NotionCLIError
|
|
121
|
+
const cliError = error instanceof index_1.NotionCLIError ? error : (0, index_1.wrapNotionError)(error);
|
|
122
|
+
// Check if we should use envelope
|
|
123
|
+
this.shouldUseEnvelope = this.checkEnvelopeUsage(flags);
|
|
124
|
+
if (this.shouldUseEnvelope) {
|
|
125
|
+
const envelope = this.envelope.wrapError(cliError, additionalContext);
|
|
126
|
+
this.envelope.outputEnvelope(envelope, flags, this.log.bind(this));
|
|
127
|
+
process.exit(this.envelope.getExitCode(envelope));
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Non-JSON mode - use oclif's error handling
|
|
131
|
+
this.error(cliError.message, { exit: this.getExitCodeForError(cliError) });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get appropriate exit code for error
|
|
136
|
+
*/
|
|
137
|
+
getExitCodeForError(error) {
|
|
138
|
+
// CLI validation errors
|
|
139
|
+
if (error.code === 'VALIDATION_ERROR') {
|
|
140
|
+
return envelope_1.ExitCode.CLI_ERROR;
|
|
141
|
+
}
|
|
142
|
+
// API errors (default)
|
|
143
|
+
return envelope_1.ExitCode.API_ERROR;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Catch handler that ensures proper envelope error output
|
|
147
|
+
*/
|
|
148
|
+
async catch(error) {
|
|
149
|
+
// If command has already handled the error via outputError, just propagate
|
|
150
|
+
if (error.exitCode !== undefined) {
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
// Otherwise, wrap and handle the error
|
|
154
|
+
const cliError = (0, index_1.wrapNotionError)(error);
|
|
155
|
+
this.error(cliError.message, { exit: this.getExitCodeForError(cliError) });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.BaseCommand = BaseCommand;
|
|
159
|
+
/**
|
|
160
|
+
* Standard flags that all envelope-enabled commands should include
|
|
161
|
+
*/
|
|
162
|
+
exports.EnvelopeFlags = {
|
|
163
|
+
json: core_1.Flags.boolean({
|
|
164
|
+
char: 'j',
|
|
165
|
+
description: 'Output as JSON envelope (recommended for automation)',
|
|
166
|
+
default: false,
|
|
167
|
+
}),
|
|
168
|
+
'compact-json': core_1.Flags.boolean({
|
|
169
|
+
char: 'c',
|
|
170
|
+
description: 'Output as compact JSON envelope (single-line, ideal for piping)',
|
|
171
|
+
default: false,
|
|
172
|
+
exclusive: ['markdown', 'pretty'],
|
|
173
|
+
}),
|
|
174
|
+
raw: core_1.Flags.boolean({
|
|
175
|
+
char: 'r',
|
|
176
|
+
description: 'Output raw API response without envelope (legacy mode)',
|
|
177
|
+
default: false,
|
|
178
|
+
}),
|
|
179
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const AutomationFlags: {
|
|
2
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
3
|
+
'page-size': import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
4
|
+
retry: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
5
|
+
timeout: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
6
|
+
'no-cache': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
|
+
verbose: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
minimal: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
};
|
|
10
|
+
export declare const OutputFormatFlags: {
|
|
11
|
+
markdown: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
'compact-json': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
pretty: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OutputFormatFlags = exports.AutomationFlags = void 0;
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
exports.AutomationFlags = {
|
|
6
|
+
json: core_1.Flags.boolean({
|
|
7
|
+
char: 'j',
|
|
8
|
+
description: 'Output as JSON (recommended for automation)',
|
|
9
|
+
default: false,
|
|
10
|
+
}),
|
|
11
|
+
'page-size': core_1.Flags.integer({
|
|
12
|
+
description: 'Items per page (1-100, default: 100 for automation)',
|
|
13
|
+
min: 1,
|
|
14
|
+
max: 100,
|
|
15
|
+
default: 100,
|
|
16
|
+
}),
|
|
17
|
+
retry: core_1.Flags.boolean({
|
|
18
|
+
description: 'Auto-retry on rate limit (respects Retry-After header)',
|
|
19
|
+
default: true,
|
|
20
|
+
}),
|
|
21
|
+
timeout: core_1.Flags.integer({
|
|
22
|
+
description: 'Request timeout in milliseconds',
|
|
23
|
+
default: 30000,
|
|
24
|
+
}),
|
|
25
|
+
'no-cache': core_1.Flags.boolean({
|
|
26
|
+
description: 'Bypass cache and force fresh API calls',
|
|
27
|
+
default: false,
|
|
28
|
+
}),
|
|
29
|
+
verbose: core_1.Flags.boolean({
|
|
30
|
+
char: 'v',
|
|
31
|
+
description: 'Enable verbose logging to stderr (retry events, cache stats) - never pollutes stdout',
|
|
32
|
+
default: false,
|
|
33
|
+
env: 'NOTION_CLI_VERBOSE',
|
|
34
|
+
}),
|
|
35
|
+
minimal: core_1.Flags.boolean({
|
|
36
|
+
description: 'Strip unnecessary metadata (created_by, last_edited_by, object fields, request_id, etc.) - reduces response size by ~40%',
|
|
37
|
+
default: false,
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
exports.OutputFormatFlags = {
|
|
41
|
+
markdown: core_1.Flags.boolean({
|
|
42
|
+
char: 'm',
|
|
43
|
+
description: 'Output as markdown table (GitHub-flavored)',
|
|
44
|
+
default: false,
|
|
45
|
+
exclusive: ['compact-json', 'pretty'],
|
|
46
|
+
}),
|
|
47
|
+
'compact-json': core_1.Flags.boolean({
|
|
48
|
+
char: 'c',
|
|
49
|
+
description: 'Output as compact JSON (single-line, ideal for piping)',
|
|
50
|
+
default: false,
|
|
51
|
+
exclusive: ['markdown', 'pretty'],
|
|
52
|
+
}),
|
|
53
|
+
pretty: core_1.Flags.boolean({
|
|
54
|
+
char: 'P',
|
|
55
|
+
description: 'Output as pretty table with borders',
|
|
56
|
+
default: false,
|
|
57
|
+
exclusive: ['markdown', 'compact-json'],
|
|
58
|
+
}),
|
|
59
|
+
};
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple in-memory caching layer for Notion API responses
|
|
3
|
+
* Supports TTL (time-to-live) and cache invalidation
|
|
4
|
+
* Integrated with disk cache for persistence across CLI invocations
|
|
5
|
+
*/
|
|
6
|
+
export interface CacheEntry<T> {
|
|
7
|
+
data: T;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
ttl: number;
|
|
10
|
+
}
|
|
11
|
+
export interface CacheStats {
|
|
12
|
+
hits: number;
|
|
13
|
+
misses: number;
|
|
14
|
+
sets: number;
|
|
15
|
+
evictions: number;
|
|
16
|
+
size: number;
|
|
17
|
+
}
|
|
18
|
+
export interface CacheConfig {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
defaultTtl: number;
|
|
21
|
+
maxSize: number;
|
|
22
|
+
ttlByType: {
|
|
23
|
+
dataSource: number;
|
|
24
|
+
database: number;
|
|
25
|
+
user: number;
|
|
26
|
+
page: number;
|
|
27
|
+
block: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare class CacheManager {
|
|
31
|
+
private cache;
|
|
32
|
+
private stats;
|
|
33
|
+
private config;
|
|
34
|
+
constructor(config?: Partial<CacheConfig>);
|
|
35
|
+
/**
|
|
36
|
+
* Generate a cache key from resource type and identifiers
|
|
37
|
+
*/
|
|
38
|
+
private generateKey;
|
|
39
|
+
/**
|
|
40
|
+
* Check if a cache entry is still valid
|
|
41
|
+
*/
|
|
42
|
+
private isValid;
|
|
43
|
+
/**
|
|
44
|
+
* Evict expired entries
|
|
45
|
+
*/
|
|
46
|
+
private evictExpired;
|
|
47
|
+
/**
|
|
48
|
+
* Evict oldest entries if cache is full
|
|
49
|
+
*/
|
|
50
|
+
private evictOldest;
|
|
51
|
+
/**
|
|
52
|
+
* Get a value from cache (checks memory, then disk)
|
|
53
|
+
*/
|
|
54
|
+
get<T>(type: string, ...identifiers: Array<string | number | object>): Promise<T | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Set a value in cache with optional custom TTL (writes to memory and disk)
|
|
57
|
+
*/
|
|
58
|
+
set<T>(type: string, data: T, customTtl?: number, ...identifiers: Array<string | number | object>): void;
|
|
59
|
+
/**
|
|
60
|
+
* Invalidate specific cache entries by type and optional identifiers
|
|
61
|
+
*/
|
|
62
|
+
invalidate(type: string, ...identifiers: Array<string | number | object>): void;
|
|
63
|
+
/**
|
|
64
|
+
* Clear all cache entries (memory and disk)
|
|
65
|
+
*/
|
|
66
|
+
clear(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Get cache statistics
|
|
69
|
+
*/
|
|
70
|
+
getStats(): CacheStats;
|
|
71
|
+
/**
|
|
72
|
+
* Get cache hit rate
|
|
73
|
+
*/
|
|
74
|
+
getHitRate(): number;
|
|
75
|
+
/**
|
|
76
|
+
* Check if cache is enabled
|
|
77
|
+
*/
|
|
78
|
+
isEnabled(): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Get current configuration
|
|
81
|
+
*/
|
|
82
|
+
getConfig(): CacheConfig;
|
|
83
|
+
}
|
|
84
|
+
export declare const cacheManager: CacheManager;
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Simple in-memory caching layer for Notion API responses
|
|
4
|
+
* Supports TTL (time-to-live) and cache invalidation
|
|
5
|
+
* Integrated with disk cache for persistence across CLI invocations
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.cacheManager = exports.CacheManager = void 0;
|
|
9
|
+
const disk_cache_1 = require("./utils/disk-cache");
|
|
10
|
+
/**
|
|
11
|
+
* Check if verbose logging is enabled
|
|
12
|
+
*/
|
|
13
|
+
function isVerboseEnabled() {
|
|
14
|
+
return process.env.DEBUG === 'true' ||
|
|
15
|
+
process.env.NOTION_CLI_DEBUG === 'true' ||
|
|
16
|
+
process.env.NOTION_CLI_VERBOSE === 'true';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Log structured cache event to stderr
|
|
20
|
+
* Never pollutes stdout - safe for JSON output
|
|
21
|
+
*/
|
|
22
|
+
function logCacheEvent(event) {
|
|
23
|
+
// Only log if verbose mode is enabled
|
|
24
|
+
if (!isVerboseEnabled()) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Always write to stderr, never stdout
|
|
28
|
+
console.error(JSON.stringify(event));
|
|
29
|
+
}
|
|
30
|
+
class CacheManager {
|
|
31
|
+
constructor(config) {
|
|
32
|
+
this.cache = new Map();
|
|
33
|
+
this.stats = {
|
|
34
|
+
hits: 0,
|
|
35
|
+
misses: 0,
|
|
36
|
+
sets: 0,
|
|
37
|
+
evictions: 0,
|
|
38
|
+
size: 0,
|
|
39
|
+
};
|
|
40
|
+
// Default configuration
|
|
41
|
+
this.config = {
|
|
42
|
+
enabled: process.env.NOTION_CLI_CACHE_ENABLED !== 'false',
|
|
43
|
+
defaultTtl: parseInt(process.env.NOTION_CLI_CACHE_TTL || '300000', 10), // 5 minutes default
|
|
44
|
+
maxSize: parseInt(process.env.NOTION_CLI_CACHE_MAX_SIZE || '1000', 10),
|
|
45
|
+
ttlByType: {
|
|
46
|
+
dataSource: parseInt(process.env.NOTION_CLI_CACHE_DS_TTL || '600000', 10), // 10 min
|
|
47
|
+
database: parseInt(process.env.NOTION_CLI_CACHE_DB_TTL || '600000', 10), // 10 min
|
|
48
|
+
user: parseInt(process.env.NOTION_CLI_CACHE_USER_TTL || '3600000', 10), // 1 hour
|
|
49
|
+
page: parseInt(process.env.NOTION_CLI_CACHE_PAGE_TTL || '60000', 10), // 1 min
|
|
50
|
+
block: parseInt(process.env.NOTION_CLI_CACHE_BLOCK_TTL || '30000', 10), // 30 sec
|
|
51
|
+
},
|
|
52
|
+
...config,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generate a cache key from resource type and identifiers
|
|
57
|
+
*/
|
|
58
|
+
generateKey(type, ...identifiers) {
|
|
59
|
+
return `${type}:${identifiers.map(id => typeof id === 'object' ? JSON.stringify(id) : String(id)).join(':')}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if a cache entry is still valid
|
|
63
|
+
*/
|
|
64
|
+
isValid(entry) {
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
return now - entry.timestamp < entry.ttl;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Evict expired entries
|
|
70
|
+
*/
|
|
71
|
+
evictExpired() {
|
|
72
|
+
let evictedCount = 0;
|
|
73
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
74
|
+
if (!this.isValid(entry)) {
|
|
75
|
+
this.cache.delete(key);
|
|
76
|
+
this.stats.evictions++;
|
|
77
|
+
evictedCount++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.stats.size = this.cache.size;
|
|
81
|
+
// Log eviction event if any entries were evicted
|
|
82
|
+
if (evictedCount > 0 && isVerboseEnabled()) {
|
|
83
|
+
logCacheEvent({
|
|
84
|
+
level: 'debug',
|
|
85
|
+
event: 'cache_evict',
|
|
86
|
+
namespace: 'expired',
|
|
87
|
+
cache_size: this.cache.size,
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Evict oldest entries if cache is full
|
|
94
|
+
*/
|
|
95
|
+
evictOldest() {
|
|
96
|
+
if (this.cache.size >= this.config.maxSize) {
|
|
97
|
+
// Find and remove oldest entry
|
|
98
|
+
let oldestKey = null;
|
|
99
|
+
let oldestTime = Infinity;
|
|
100
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
101
|
+
if (entry.timestamp < oldestTime) {
|
|
102
|
+
oldestTime = entry.timestamp;
|
|
103
|
+
oldestKey = key;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (oldestKey) {
|
|
107
|
+
this.cache.delete(oldestKey);
|
|
108
|
+
this.stats.evictions++;
|
|
109
|
+
// Log LRU eviction
|
|
110
|
+
logCacheEvent({
|
|
111
|
+
level: 'debug',
|
|
112
|
+
event: 'cache_evict',
|
|
113
|
+
namespace: 'lru',
|
|
114
|
+
key: oldestKey,
|
|
115
|
+
cache_size: this.cache.size,
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get a value from cache (checks memory, then disk)
|
|
123
|
+
*/
|
|
124
|
+
async get(type, ...identifiers) {
|
|
125
|
+
if (!this.config.enabled) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const key = this.generateKey(type, ...identifiers);
|
|
129
|
+
const entry = this.cache.get(key);
|
|
130
|
+
// Check memory cache first
|
|
131
|
+
if (entry && this.isValid(entry)) {
|
|
132
|
+
this.stats.hits++;
|
|
133
|
+
// Log cache hit
|
|
134
|
+
logCacheEvent({
|
|
135
|
+
level: 'debug',
|
|
136
|
+
event: 'cache_hit',
|
|
137
|
+
namespace: type,
|
|
138
|
+
key: identifiers.join(':'),
|
|
139
|
+
age_ms: Date.now() - entry.timestamp,
|
|
140
|
+
ttl_ms: entry.ttl,
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
});
|
|
143
|
+
return entry.data;
|
|
144
|
+
}
|
|
145
|
+
// Remove invalid memory entry
|
|
146
|
+
if (entry) {
|
|
147
|
+
this.cache.delete(key);
|
|
148
|
+
this.stats.evictions++;
|
|
149
|
+
// Log eviction event
|
|
150
|
+
logCacheEvent({
|
|
151
|
+
level: 'debug',
|
|
152
|
+
event: 'cache_evict',
|
|
153
|
+
namespace: type,
|
|
154
|
+
key: identifiers.join(':'),
|
|
155
|
+
cache_size: this.cache.size,
|
|
156
|
+
timestamp: new Date().toISOString(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// Check disk cache (only if enabled)
|
|
160
|
+
const diskEnabled = process.env.NOTION_CLI_DISK_CACHE_ENABLED !== 'false';
|
|
161
|
+
if (diskEnabled) {
|
|
162
|
+
try {
|
|
163
|
+
const diskEntry = await disk_cache_1.diskCacheManager.get(key);
|
|
164
|
+
if (diskEntry && diskEntry.data) {
|
|
165
|
+
const entry = diskEntry.data;
|
|
166
|
+
// Validate disk entry
|
|
167
|
+
if (this.isValid(entry)) {
|
|
168
|
+
// Promote to memory cache
|
|
169
|
+
this.cache.set(key, entry);
|
|
170
|
+
this.stats.hits++;
|
|
171
|
+
// Log cache hit (from disk)
|
|
172
|
+
logCacheEvent({
|
|
173
|
+
level: 'debug',
|
|
174
|
+
event: 'cache_hit',
|
|
175
|
+
namespace: type,
|
|
176
|
+
key: identifiers.join(':'),
|
|
177
|
+
age_ms: Date.now() - entry.timestamp,
|
|
178
|
+
ttl_ms: entry.ttl,
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
});
|
|
181
|
+
return entry.data;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Remove expired disk entry
|
|
185
|
+
disk_cache_1.diskCacheManager.invalidate(key).catch(() => { });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
// Silently ignore disk cache errors
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Cache miss
|
|
194
|
+
this.stats.misses++;
|
|
195
|
+
// Log cache miss
|
|
196
|
+
logCacheEvent({
|
|
197
|
+
level: 'debug',
|
|
198
|
+
event: 'cache_miss',
|
|
199
|
+
namespace: type,
|
|
200
|
+
key: identifiers.join(':'),
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
});
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Set a value in cache with optional custom TTL (writes to memory and disk)
|
|
207
|
+
*/
|
|
208
|
+
set(type, data, customTtl, ...identifiers) {
|
|
209
|
+
if (!this.config.enabled) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Evict expired entries periodically
|
|
213
|
+
if (this.cache.size > 0 && Math.random() < 0.1) {
|
|
214
|
+
this.evictExpired();
|
|
215
|
+
}
|
|
216
|
+
// Evict oldest if at capacity
|
|
217
|
+
this.evictOldest();
|
|
218
|
+
const key = this.generateKey(type, ...identifiers);
|
|
219
|
+
const ttl = customTtl || this.config.ttlByType[type] || this.config.defaultTtl;
|
|
220
|
+
const entry = {
|
|
221
|
+
data,
|
|
222
|
+
timestamp: Date.now(),
|
|
223
|
+
ttl,
|
|
224
|
+
};
|
|
225
|
+
this.cache.set(key, entry);
|
|
226
|
+
this.stats.sets++;
|
|
227
|
+
this.stats.size = this.cache.size;
|
|
228
|
+
// Log cache set
|
|
229
|
+
logCacheEvent({
|
|
230
|
+
level: 'debug',
|
|
231
|
+
event: 'cache_set',
|
|
232
|
+
namespace: type,
|
|
233
|
+
key: identifiers.join(':'),
|
|
234
|
+
ttl_ms: ttl,
|
|
235
|
+
cache_size: this.cache.size,
|
|
236
|
+
timestamp: new Date().toISOString(),
|
|
237
|
+
});
|
|
238
|
+
// Async write to disk cache (fire-and-forget)
|
|
239
|
+
const diskEnabled = process.env.NOTION_CLI_DISK_CACHE_ENABLED !== 'false';
|
|
240
|
+
if (diskEnabled) {
|
|
241
|
+
disk_cache_1.diskCacheManager.set(key, entry, ttl).catch(() => {
|
|
242
|
+
// Silently ignore disk cache errors
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Invalidate specific cache entries by type and optional identifiers
|
|
248
|
+
*/
|
|
249
|
+
invalidate(type, ...identifiers) {
|
|
250
|
+
const diskEnabled = process.env.NOTION_CLI_DISK_CACHE_ENABLED !== 'false';
|
|
251
|
+
if (identifiers.length === 0) {
|
|
252
|
+
// Invalidate all entries of this type
|
|
253
|
+
const pattern = `${type}:`;
|
|
254
|
+
let invalidatedCount = 0;
|
|
255
|
+
for (const key of this.cache.keys()) {
|
|
256
|
+
if (key.startsWith(pattern)) {
|
|
257
|
+
this.cache.delete(key);
|
|
258
|
+
this.stats.evictions++;
|
|
259
|
+
invalidatedCount++;
|
|
260
|
+
// Also invalidate from disk (fire-and-forget)
|
|
261
|
+
if (diskEnabled) {
|
|
262
|
+
disk_cache_1.diskCacheManager.invalidate(key).catch(() => { });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Log bulk invalidation
|
|
267
|
+
if (invalidatedCount > 0) {
|
|
268
|
+
logCacheEvent({
|
|
269
|
+
level: 'debug',
|
|
270
|
+
event: 'cache_invalidate',
|
|
271
|
+
namespace: type,
|
|
272
|
+
cache_size: this.cache.size,
|
|
273
|
+
timestamp: new Date().toISOString(),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// Invalidate specific entry
|
|
279
|
+
const key = this.generateKey(type, ...identifiers);
|
|
280
|
+
if (this.cache.delete(key)) {
|
|
281
|
+
this.stats.evictions++;
|
|
282
|
+
// Also invalidate from disk (fire-and-forget)
|
|
283
|
+
if (diskEnabled) {
|
|
284
|
+
disk_cache_1.diskCacheManager.invalidate(key).catch(() => { });
|
|
285
|
+
}
|
|
286
|
+
// Log specific invalidation
|
|
287
|
+
logCacheEvent({
|
|
288
|
+
level: 'debug',
|
|
289
|
+
event: 'cache_invalidate',
|
|
290
|
+
namespace: type,
|
|
291
|
+
key: identifiers.join(':'),
|
|
292
|
+
cache_size: this.cache.size,
|
|
293
|
+
timestamp: new Date().toISOString(),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
this.stats.size = this.cache.size;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Clear all cache entries (memory and disk)
|
|
301
|
+
*/
|
|
302
|
+
clear() {
|
|
303
|
+
const previousSize = this.cache.size;
|
|
304
|
+
this.cache.clear();
|
|
305
|
+
this.stats.evictions += this.stats.size;
|
|
306
|
+
this.stats.size = 0;
|
|
307
|
+
// Also clear disk cache (fire-and-forget)
|
|
308
|
+
const diskEnabled = process.env.NOTION_CLI_DISK_CACHE_ENABLED !== 'false';
|
|
309
|
+
if (diskEnabled) {
|
|
310
|
+
disk_cache_1.diskCacheManager.clear().catch(() => { });
|
|
311
|
+
}
|
|
312
|
+
// Log cache clear
|
|
313
|
+
if (previousSize > 0) {
|
|
314
|
+
logCacheEvent({
|
|
315
|
+
level: 'info',
|
|
316
|
+
event: 'cache_invalidate',
|
|
317
|
+
namespace: 'all',
|
|
318
|
+
cache_size: 0,
|
|
319
|
+
timestamp: new Date().toISOString(),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Get cache statistics
|
|
325
|
+
*/
|
|
326
|
+
getStats() {
|
|
327
|
+
return { ...this.stats };
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Get cache hit rate
|
|
331
|
+
*/
|
|
332
|
+
getHitRate() {
|
|
333
|
+
const total = this.stats.hits + this.stats.misses;
|
|
334
|
+
return total > 0 ? this.stats.hits / total : 0;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Check if cache is enabled
|
|
338
|
+
*/
|
|
339
|
+
isEnabled() {
|
|
340
|
+
return this.config.enabled;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get current configuration
|
|
344
|
+
*/
|
|
345
|
+
getConfig() {
|
|
346
|
+
return { ...this.config };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
exports.CacheManager = CacheManager;
|
|
350
|
+
// Singleton instance
|
|
351
|
+
exports.cacheManager = new CacheManager();
|