@jordancoin/notioncli 1.2.1 → 1.3.1

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.
@@ -0,0 +1,142 @@
1
+ module.exports = {
2
+ register(program, ctx) {
3
+ const {
4
+ getNotion,
5
+ resolvePageId,
6
+ paginate,
7
+ jsonOutput,
8
+ richTextToPlain,
9
+ runCommand,
10
+ } = ctx;
11
+
12
+ // ─── blocks ───────────────────────────────────────────────────────────────
13
+ program
14
+ .command('blocks <page-or-alias>')
15
+ .description('Get page content as rendered blocks by ID or alias + filter')
16
+ .option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
17
+ .option('--ids', 'Show block IDs alongside content (for editing/deleting)')
18
+ .action(async (target, opts, cmd) => runCommand('Blocks', async () => {
19
+ const notion = getNotion();
20
+ const { pageId } = await resolvePageId(target, opts.filter);
21
+ const { results, response } = await paginate(
22
+ ({ start_cursor, page_size }) => notion.blocks.children.list({
23
+ block_id: pageId,
24
+ start_cursor,
25
+ page_size,
26
+ }),
27
+ { pageSizeLimit: 100 },
28
+ );
29
+ if (jsonOutput(cmd, response)) return;
30
+ if (results.length === 0) {
31
+ console.log('(no blocks)');
32
+ return;
33
+ }
34
+ for (const block of results) {
35
+ const type = block.type;
36
+ const content = block[type];
37
+ let text = '';
38
+ if (content?.rich_text) {
39
+ text = richTextToPlain(content.rich_text);
40
+ } else if (content?.text) {
41
+ text = richTextToPlain(content.text);
42
+ }
43
+ const prefix = type === 'heading_1' ? '# '
44
+ : type === 'heading_2' ? '## '
45
+ : type === 'heading_3' ? '### '
46
+ : type === 'bulleted_list_item' ? '• '
47
+ : type === 'numbered_list_item' ? ' 1. '
48
+ : type === 'to_do' ? (content?.checked ? '☑ ' : '☐ ')
49
+ : type === 'code' ? '```\n'
50
+ : '';
51
+ const suffix = type === 'code' ? '\n```' : '';
52
+ const idTag = opts.ids ? `[${block.id.slice(0, 8)}] ` : '';
53
+ console.log(`${idTag}${prefix}${text}${suffix}`);
54
+ }
55
+ }));
56
+
57
+ // ─── block-edit ───────────────────────────────────────────────────────────
58
+ program
59
+ .command('block-edit <block-id> <text>')
60
+ .description('Update a block\'s text content')
61
+ .action(async (blockId, text, opts, cmd) => runCommand('Block edit', async () => {
62
+ const notion = getNotion();
63
+ // First retrieve the block to know its type
64
+ const block = await notion.blocks.retrieve({ block_id: blockId });
65
+ const type = block.type;
66
+
67
+ // Build the update payload based on block type
68
+ const supportedTextTypes = [
69
+ 'paragraph', 'heading_1', 'heading_2', 'heading_3',
70
+ 'bulleted_list_item', 'numbered_list_item', 'quote', 'callout', 'toggle',
71
+ ];
72
+
73
+ if (type === 'to_do') {
74
+ const res = await notion.blocks.update({
75
+ block_id: blockId,
76
+ to_do: {
77
+ rich_text: [{ text: { content: text } }],
78
+ checked: block.to_do?.checked || false,
79
+ },
80
+ });
81
+ if (jsonOutput(cmd, res)) return;
82
+ console.log(`✅ Updated ${type} block: ${blockId.slice(0, 8)}…`);
83
+ } else if (supportedTextTypes.includes(type)) {
84
+ const res = await notion.blocks.update({
85
+ block_id: blockId,
86
+ [type]: {
87
+ rich_text: [{ text: { content: text } }],
88
+ },
89
+ });
90
+ if (jsonOutput(cmd, res)) return;
91
+ console.log(`✅ Updated ${type} block: ${blockId.slice(0, 8)}…`);
92
+ } else if (type === 'code') {
93
+ const res = await notion.blocks.update({
94
+ block_id: blockId,
95
+ code: {
96
+ rich_text: [{ text: { content: text } }],
97
+ language: block.code?.language || 'plain text',
98
+ },
99
+ });
100
+ if (jsonOutput(cmd, res)) return;
101
+ console.log(`✅ Updated code block: ${blockId.slice(0, 8)}…`);
102
+ } else {
103
+ console.error(`Block type "${type}" doesn't support text editing.`);
104
+ console.error('Supported types: paragraph, headings, lists, to_do, quote, callout, toggle, code');
105
+ process.exit(1);
106
+ }
107
+ }));
108
+
109
+ // ─── block-delete ─────────────────────────────────────────────────────────
110
+ program
111
+ .command('block-delete <block-id>')
112
+ .description('Delete a block from a page')
113
+ .action(async (blockId, opts, cmd) => runCommand('Block delete', async () => {
114
+ const notion = getNotion();
115
+ const res = await notion.blocks.delete({ block_id: blockId });
116
+ if (jsonOutput(cmd, res)) return;
117
+ console.log(`🗑️ Deleted block: ${blockId.slice(0, 8)}…`);
118
+ }));
119
+
120
+ // ─── append ──────────────────────────────────────────────────────────────
121
+ program
122
+ .command('append <page-or-alias> <text>')
123
+ .description('Append a text block to a page by ID or alias + filter')
124
+ .option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
125
+ .action(async (target, text, opts, cmd) => runCommand('Append', async () => {
126
+ const notion = getNotion();
127
+ const { pageId } = await resolvePageId(target, opts.filter);
128
+ const res = await notion.blocks.children.append({
129
+ block_id: pageId,
130
+ children: [{
131
+ object: 'block',
132
+ type: 'paragraph',
133
+ paragraph: {
134
+ rich_text: [{ text: { content: text } }],
135
+ },
136
+ }],
137
+ });
138
+ if (jsonOutput(cmd, res)) return;
139
+ console.log(`✅ Appended text block to page ${pageId}`);
140
+ }));
141
+ },
142
+ };
@@ -0,0 +1,59 @@
1
+ module.exports = {
2
+ register(program, ctx) {
3
+ const {
4
+ getNotion,
5
+ resolvePageId,
6
+ paginate,
7
+ jsonOutput,
8
+ richTextToPlain,
9
+ printTable,
10
+ runCommand,
11
+ } = ctx;
12
+
13
+ // ─── comments ────────────────────────────────────────────────────────────
14
+ program
15
+ .command('comments <page-or-alias>')
16
+ .description('List comments on a page by ID or alias + filter')
17
+ .option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
18
+ .action(async (target, opts, cmd) => runCommand('Comments', async () => {
19
+ const notion = getNotion();
20
+ const { pageId } = await resolvePageId(target, opts.filter);
21
+ const { results, response } = await paginate(
22
+ ({ start_cursor, page_size }) => notion.comments.list({
23
+ block_id: pageId,
24
+ start_cursor,
25
+ page_size,
26
+ }),
27
+ { pageSizeLimit: 100 },
28
+ );
29
+ if (jsonOutput(cmd, response)) return;
30
+ if (results.length === 0) {
31
+ console.log('(no comments)');
32
+ return;
33
+ }
34
+ const rows = results.map(c => ({
35
+ id: c.id,
36
+ text: richTextToPlain(c.rich_text),
37
+ created: c.created_time || '',
38
+ author: c.created_by?.name || c.created_by?.id || '',
39
+ }));
40
+ printTable(rows, ['id', 'text', 'created', 'author']);
41
+ }));
42
+
43
+ // ─── comment ─────────────────────────────────────────────────────────────
44
+ program
45
+ .command('comment <page-or-alias> <text>')
46
+ .description('Add a comment to a page by ID or alias + filter')
47
+ .option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
48
+ .action(async (target, text, opts, cmd) => runCommand('Comment', async () => {
49
+ const notion = getNotion();
50
+ const { pageId } = await resolvePageId(target, opts.filter);
51
+ const res = await notion.comments.create({
52
+ parent: { page_id: pageId },
53
+ rich_text: [{ text: { content: text } }],
54
+ });
55
+ if (jsonOutput(cmd, res)) return;
56
+ console.log(`✅ Comment added: ${res.id}`);
57
+ }));
58
+ },
59
+ };
@@ -0,0 +1,328 @@
1
+ module.exports = {
2
+ register(program, ctx) {
3
+ const {
4
+ CONFIG_PATH,
5
+ loadConfig,
6
+ saveConfig,
7
+ getWorkspaceName,
8
+ getWorkspaceConfig,
9
+ getNotion,
10
+ createNotionClient,
11
+ richTextToPlain,
12
+ printTable,
13
+ runCommand,
14
+ } = ctx;
15
+
16
+ // ─── init ──────────────────────────────────────────────────────────────────
17
+ program
18
+ .command('init')
19
+ .description('Initialize notioncli with your API key and discover databases')
20
+ .option('--key <api-key>', 'Notion integration API key (starts with ntn_)')
21
+ .action(async (opts) => runCommand('Init', async () => {
22
+ const config = loadConfig();
23
+ const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
24
+ const apiKey = opts.key || process.env.NOTION_API_KEY;
25
+
26
+ if (!apiKey) {
27
+ console.error('Error: Provide an API key with --key or set NOTION_API_KEY env var.');
28
+ console.error('');
29
+ console.error('To create an integration:');
30
+ console.error(' 1. Go to https://www.notion.so/profile/integrations');
31
+ console.error(' 2. Click "New integration"');
32
+ console.error(' 3. Copy the API key (starts with ntn_)');
33
+ console.error(' 4. Share your databases with the integration');
34
+ console.error('');
35
+ console.error('Then run: notion init --key ntn_your_api_key');
36
+ console.error(' Or with workspace: notion init --workspace work --key ntn_your_api_key');
37
+ process.exit(1);
38
+ }
39
+
40
+ if (!config.workspaces) config.workspaces = {};
41
+ if (!config.workspaces[wsName]) config.workspaces[wsName] = { aliases: {} };
42
+ config.workspaces[wsName].apiKey = apiKey;
43
+ config.activeWorkspace = wsName;
44
+ saveConfig(config);
45
+ console.log(`✅ API key saved to workspace "${wsName}" in ${CONFIG_PATH}`);
46
+ console.log('');
47
+
48
+ // Discover databases
49
+ const notion = createNotionClient(apiKey);
50
+ try {
51
+ const res = await notion.search({
52
+ filter: { value: 'data_source', property: 'object' },
53
+ page_size: 100,
54
+ });
55
+
56
+ if (res.results.length === 0) {
57
+ console.log('No databases found. Make sure you\'ve shared databases with your integration.');
58
+ console.log('In Notion: open a database → ••• menu → Connections → Add your integration');
59
+ return;
60
+ }
61
+
62
+ const aliases = config.workspaces[wsName].aliases || {};
63
+
64
+ console.log(`Found ${res.results.length} database${res.results.length !== 1 ? 's' : ''}:\n`);
65
+
66
+ const added = [];
67
+ for (const db of res.results) {
68
+ const title = richTextToPlain(db.title) || '';
69
+ const dsId = db.id;
70
+ const dbId = (db.parent && db.parent.type === 'database_id' && db.parent.database_id) || db.database_id || dsId;
71
+
72
+ // Auto-generate a slug from the title
73
+ let slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 30);
74
+ if (!slug) slug = `db-${dsId.slice(0, 8)}`;
75
+
76
+ // Avoid collisions — append a number if needed
77
+ let finalSlug = slug;
78
+ let counter = 2;
79
+ while (aliases[finalSlug] && aliases[finalSlug].data_source_id !== dsId) {
80
+ finalSlug = `${slug}-${counter}`;
81
+ counter++;
82
+ }
83
+
84
+ aliases[finalSlug] = {
85
+ database_id: dbId,
86
+ data_source_id: dsId,
87
+ };
88
+
89
+ console.log(` ✅ ${finalSlug.padEnd(25)} → ${title || '(untitled)'}`);
90
+ added.push(finalSlug);
91
+ }
92
+
93
+ config.workspaces[wsName].aliases = aliases;
94
+ saveConfig(config);
95
+ console.log('');
96
+ console.log(`${added.length} alias${added.length !== 1 ? 'es' : ''} saved to workspace "${wsName}".`);
97
+ console.log('');
98
+ console.log('Ready! Try:');
99
+ if (added.length > 0) {
100
+ console.log(` notion query ${added[0]}`);
101
+ console.log(` notion add ${added[0]} --prop "Name=Hello World"`);
102
+ }
103
+ console.log('');
104
+ console.log('Manage aliases:');
105
+ console.log(' notion alias list — see all aliases');
106
+ console.log(' notion alias rename <old> <new> — rename an alias');
107
+ console.log(' notion alias remove <name> — remove an alias');
108
+ } catch (err) {
109
+ console.error(`Failed to discover databases: ${err.message}`);
110
+ console.error('Your API key was saved. You can add databases manually with: notion alias add <name> <id>');
111
+ }
112
+ }));
113
+
114
+ // ─── alias ─────────────────────────────────────────────────────────────────
115
+ const alias = program
116
+ .command('alias')
117
+ .description('Manage database aliases for quick access');
118
+
119
+ alias
120
+ .command('add <name> <database-id>')
121
+ .description('Add a database alias (auto-discovers data_source_id)')
122
+ .action(async (name, databaseId) => runCommand('Alias add', async () => {
123
+ const config = loadConfig();
124
+ const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
125
+ if (!config.workspaces[wsName]) config.workspaces[wsName] = { aliases: {} };
126
+ const aliases = config.workspaces[wsName].aliases || {};
127
+
128
+ // Try to discover the data_source_id by searching for this database
129
+ const notion = getNotion();
130
+ let dataSourceId = databaseId;
131
+
132
+ try {
133
+ const res = await notion.search({
134
+ filter: { value: 'data_source', property: 'object' },
135
+ page_size: 100,
136
+ });
137
+
138
+ const match = res.results.find(db => {
139
+ // Match by data_source_id or database_id
140
+ return db.id === databaseId ||
141
+ db.id.replace(/-/g, '') === databaseId.replace(/-/g, '');
142
+ });
143
+
144
+ if (match) {
145
+ dataSourceId = match.id;
146
+ // The database_id might differ from data_source_id — check parent
147
+ const dbId = (match.parent && match.parent.type === 'database_id' && match.parent.database_id) || match.database_id || databaseId;
148
+ aliases[name] = {
149
+ database_id: dbId,
150
+ data_source_id: dataSourceId,
151
+ };
152
+ const title = richTextToPlain(match.title) || '(untitled)';
153
+ console.log(`✅ Added alias "${name}" → ${title}`);
154
+ console.log(` database_id: ${dbId}`);
155
+ console.log(` data_source_id: ${dataSourceId}`);
156
+ } else {
157
+ // Couldn't find via search — use the ID for both
158
+ aliases[name] = {
159
+ database_id: databaseId,
160
+ data_source_id: databaseId,
161
+ };
162
+ console.log(`✅ Added alias "${name}" → ${databaseId}`);
163
+ console.log(' (Could not auto-discover data_source_id — using same ID for both)');
164
+ }
165
+ } catch (err) {
166
+ // Fallback: use same ID for both
167
+ aliases[name] = {
168
+ database_id: databaseId,
169
+ data_source_id: databaseId,
170
+ };
171
+ console.log(`✅ Added alias "${name}" → ${databaseId}`);
172
+ console.log(` (Auto-discovery failed: ${err.message})`);
173
+ }
174
+
175
+ config.workspaces[wsName].aliases = aliases;
176
+ saveConfig(config);
177
+ }));
178
+
179
+ alias
180
+ .command('list')
181
+ .description('Show all configured database aliases')
182
+ .action(() => {
183
+ const ws = getWorkspaceConfig();
184
+ const aliases = ws.aliases || {};
185
+ const names = Object.keys(aliases);
186
+
187
+ if (names.length === 0) {
188
+ console.log(`No aliases in workspace "${ws.name}".`);
189
+ console.log('Add one with: notion alias add <name> <database-id>');
190
+ return;
191
+ }
192
+
193
+ console.log(`Workspace: ${ws.name}\n`);
194
+ const rows = names.map(name => ({
195
+ alias: name,
196
+ database_id: aliases[name].database_id,
197
+ data_source_id: aliases[name].data_source_id,
198
+ }));
199
+ printTable(rows, ['alias', 'database_id', 'data_source_id']);
200
+ });
201
+
202
+ alias
203
+ .command('remove <name>')
204
+ .description('Remove a database alias')
205
+ .action((name) => {
206
+ const config = loadConfig();
207
+ const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
208
+ const aliases = config.workspaces[wsName]?.aliases || {};
209
+ if (!aliases[name]) {
210
+ console.error(`Alias "${name}" not found in workspace "${wsName}".`);
211
+ const names = Object.keys(aliases);
212
+ if (names.length > 0) {
213
+ console.error(`Available: ${names.join(', ')}`);
214
+ }
215
+ process.exit(1);
216
+ }
217
+ delete aliases[name];
218
+ config.workspaces[wsName].aliases = aliases;
219
+ saveConfig(config);
220
+ console.log(`✅ Removed alias "${name}" from workspace "${wsName}"`);
221
+ });
222
+
223
+ alias
224
+ .command('rename <old-name> <new-name>')
225
+ .description('Rename a database alias')
226
+ .action((oldName, newName) => {
227
+ const config = loadConfig();
228
+ const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
229
+ const aliases = config.workspaces[wsName]?.aliases || {};
230
+ if (!aliases[oldName]) {
231
+ console.error(`Alias "${oldName}" not found in workspace "${wsName}".`);
232
+ const names = Object.keys(aliases);
233
+ if (names.length > 0) {
234
+ console.error(`Available: ${names.join(', ')}`);
235
+ }
236
+ process.exit(1);
237
+ }
238
+ if (aliases[newName]) {
239
+ console.error(`Alias "${newName}" already exists. Remove it first or pick a different name.`);
240
+ process.exit(1);
241
+ }
242
+ aliases[newName] = aliases[oldName];
243
+ delete aliases[oldName];
244
+ config.workspaces[wsName].aliases = aliases;
245
+ saveConfig(config);
246
+ console.log(`✅ Renamed "${oldName}" → "${newName}" in workspace "${wsName}"`);
247
+ });
248
+
249
+ // ─── workspace ─────────────────────────────────────────────────────────────
250
+ const workspace = program
251
+ .command('workspace')
252
+ .description('Manage workspace profiles (multiple Notion accounts)');
253
+
254
+ workspace
255
+ .command('add <name>')
256
+ .description('Add a new workspace profile')
257
+ .requiredOption('--key <api-key>', 'Notion API key for this workspace')
258
+ .action(async (name, opts) => runCommand('Workspace add', async () => {
259
+ const config = loadConfig();
260
+ if (config.workspaces[name]) {
261
+ console.error(`Workspace "${name}" already exists. Use "notion init --workspace ${name} --key ..." to update it.`);
262
+ process.exit(1);
263
+ }
264
+ config.workspaces[name] = { apiKey: opts.key, aliases: {} };
265
+ saveConfig(config);
266
+ console.log(`✅ Added workspace "${name}"`);
267
+ console.log('');
268
+ console.log(`Discover databases: notion init --workspace ${name}`);
269
+ console.log(`Or set as active: notion workspace use ${name}`);
270
+ }));
271
+
272
+ workspace
273
+ .command('list')
274
+ .description('List all workspace profiles')
275
+ .action(() => {
276
+ const config = loadConfig();
277
+ const names = Object.keys(config.workspaces || {});
278
+ if (names.length === 0) {
279
+ console.log('No workspaces configured. Run: notion init --key ntn_...');
280
+ return;
281
+ }
282
+ for (const name of names) {
283
+ const ws = config.workspaces[name];
284
+ const active = name === config.activeWorkspace ? ' ← active' : '';
285
+ const aliasCount = Object.keys(ws.aliases || {}).length;
286
+ const keyPreview = ws.apiKey ? `${ws.apiKey.slice(0, 8)}...` : '(no key)';
287
+ console.log(` ${name}${active}`);
288
+ console.log(` Key: ${keyPreview} | Aliases: ${aliasCount}`);
289
+ }
290
+ });
291
+
292
+ workspace
293
+ .command('use <name>')
294
+ .description('Set the active workspace')
295
+ .action((name) => {
296
+ const config = loadConfig();
297
+ if (!config.workspaces[name]) {
298
+ console.error(`Workspace "${name}" not found.`);
299
+ const names = Object.keys(config.workspaces || {});
300
+ if (names.length > 0) {
301
+ console.error(`Available: ${names.join(', ')}`);
302
+ }
303
+ process.exit(1);
304
+ }
305
+ config.activeWorkspace = name;
306
+ saveConfig(config);
307
+ console.log(`✅ Active workspace: ${name}`);
308
+ });
309
+
310
+ workspace
311
+ .command('remove <name>')
312
+ .description('Remove a workspace profile')
313
+ .action((name) => {
314
+ const config = loadConfig();
315
+ if (!config.workspaces[name]) {
316
+ console.error(`Workspace "${name}" not found.`);
317
+ process.exit(1);
318
+ }
319
+ if (name === config.activeWorkspace) {
320
+ console.error('Cannot remove the active workspace. Switch first: notion workspace use <other>');
321
+ process.exit(1);
322
+ }
323
+ delete config.workspaces[name];
324
+ saveConfig(config);
325
+ console.log(`✅ Removed workspace "${name}"`);
326
+ });
327
+ },
328
+ };