@jordancoin/notioncli 1.0.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/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/bin/notion.js +847 -0
- package/lib/helpers.js +291 -0
- package/package.json +44 -0
- package/skill/SKILL.md +262 -0
- package/skill/install.sh +4 -0
- package/test/integration.test.js +110 -0
- package/test/mock.test.js +378 -0
- package/test/unit.test.js +663 -0
package/lib/helpers.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// lib/helpers.js — Pure functions extracted from bin/notion.js for testability
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// ─── Config file system ────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
function getConfigPaths(overrideDir) {
|
|
9
|
+
const configDir = overrideDir || path.join(
|
|
10
|
+
process.env.XDG_CONFIG_HOME || path.join(require('os').homedir(), '.config'),
|
|
11
|
+
'notioncli'
|
|
12
|
+
);
|
|
13
|
+
return {
|
|
14
|
+
CONFIG_DIR: configDir,
|
|
15
|
+
CONFIG_PATH: path.join(configDir, 'config.json'),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function loadConfig(configPath) {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(configPath)) {
|
|
22
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
23
|
+
}
|
|
24
|
+
} catch (err) {
|
|
25
|
+
// Corrupted config — start fresh
|
|
26
|
+
}
|
|
27
|
+
return { aliases: {} };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveConfig(config, configDir, configPath) {
|
|
31
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
32
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/** Extract plain text from rich_text array */
|
|
38
|
+
function richTextToPlain(rt) {
|
|
39
|
+
if (!rt) return '';
|
|
40
|
+
if (Array.isArray(rt)) return rt.map(r => r.plain_text || '').join('');
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Extract a readable value from a Notion property */
|
|
45
|
+
function propValue(prop) {
|
|
46
|
+
if (!prop) return '';
|
|
47
|
+
switch (prop.type) {
|
|
48
|
+
case 'title':
|
|
49
|
+
return richTextToPlain(prop.title);
|
|
50
|
+
case 'rich_text':
|
|
51
|
+
return richTextToPlain(prop.rich_text);
|
|
52
|
+
case 'number':
|
|
53
|
+
return prop.number != null ? String(prop.number) : '';
|
|
54
|
+
case 'select':
|
|
55
|
+
return prop.select ? prop.select.name : '';
|
|
56
|
+
case 'multi_select':
|
|
57
|
+
return (prop.multi_select || []).map(s => s.name).join(', ');
|
|
58
|
+
case 'date':
|
|
59
|
+
if (!prop.date) return '';
|
|
60
|
+
return prop.date.end ? `${prop.date.start} → ${prop.date.end}` : prop.date.start;
|
|
61
|
+
case 'checkbox':
|
|
62
|
+
return prop.checkbox ? '✓' : '✗';
|
|
63
|
+
case 'url':
|
|
64
|
+
return prop.url || '';
|
|
65
|
+
case 'email':
|
|
66
|
+
return prop.email || '';
|
|
67
|
+
case 'phone_number':
|
|
68
|
+
return prop.phone_number || '';
|
|
69
|
+
case 'status':
|
|
70
|
+
return prop.status ? prop.status.name : '';
|
|
71
|
+
case 'formula':
|
|
72
|
+
if (prop.formula) {
|
|
73
|
+
const f = prop.formula;
|
|
74
|
+
return f.string || f.number?.toString() || f.boolean?.toString() || f.date?.start || '';
|
|
75
|
+
}
|
|
76
|
+
return '';
|
|
77
|
+
case 'relation':
|
|
78
|
+
return (prop.relation || []).map(r => r.id).join(', ');
|
|
79
|
+
case 'rollup':
|
|
80
|
+
return JSON.stringify(prop.rollup);
|
|
81
|
+
case 'people':
|
|
82
|
+
return (prop.people || []).map(p => p.name || p.id).join(', ');
|
|
83
|
+
case 'files':
|
|
84
|
+
return (prop.files || []).map(f => f.name || f.external?.url || '').join(', ');
|
|
85
|
+
case 'created_time':
|
|
86
|
+
return prop.created_time || '';
|
|
87
|
+
case 'last_edited_time':
|
|
88
|
+
return prop.last_edited_time || '';
|
|
89
|
+
case 'created_by':
|
|
90
|
+
return prop.created_by?.name || prop.created_by?.id || '';
|
|
91
|
+
case 'last_edited_by':
|
|
92
|
+
return prop.last_edited_by?.name || prop.last_edited_by?.id || '';
|
|
93
|
+
default:
|
|
94
|
+
return JSON.stringify(prop[prop.type] ?? '');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build property value for Notion API based on schema type */
|
|
99
|
+
function buildPropValue(type, value) {
|
|
100
|
+
switch (type) {
|
|
101
|
+
case 'title':
|
|
102
|
+
return { title: [{ text: { content: value } }] };
|
|
103
|
+
case 'rich_text':
|
|
104
|
+
return { rich_text: [{ text: { content: value } }] };
|
|
105
|
+
case 'number':
|
|
106
|
+
return { number: Number(value) };
|
|
107
|
+
case 'select':
|
|
108
|
+
return { select: { name: value } };
|
|
109
|
+
case 'multi_select':
|
|
110
|
+
return { multi_select: value.split(',').map(v => ({ name: v.trim() })) };
|
|
111
|
+
case 'date':
|
|
112
|
+
return { date: { start: value } };
|
|
113
|
+
case 'checkbox':
|
|
114
|
+
return { checkbox: value === 'true' || value === '1' || value === 'yes' };
|
|
115
|
+
case 'url':
|
|
116
|
+
return { url: value };
|
|
117
|
+
case 'email':
|
|
118
|
+
return { email: value };
|
|
119
|
+
case 'phone_number':
|
|
120
|
+
return { phone_number: value };
|
|
121
|
+
case 'status':
|
|
122
|
+
return { status: { name: value } };
|
|
123
|
+
default:
|
|
124
|
+
return { [type]: value };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Simple table formatter */
|
|
129
|
+
function printTable(rows, columns) {
|
|
130
|
+
if (!rows || rows.length === 0) {
|
|
131
|
+
console.log('(no results)');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const widths = {};
|
|
136
|
+
for (const col of columns) {
|
|
137
|
+
widths[col] = col.length;
|
|
138
|
+
}
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
for (const col of columns) {
|
|
141
|
+
const val = String(row[col] ?? '');
|
|
142
|
+
widths[col] = Math.max(widths[col], val.length);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const col of columns) {
|
|
146
|
+
widths[col] = Math.min(widths[col], 50);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const header = columns.map(c => c.padEnd(widths[c])).join(' │ ');
|
|
150
|
+
const separator = columns.map(c => '─'.repeat(widths[c])).join('─┼─');
|
|
151
|
+
console.log(header);
|
|
152
|
+
console.log(separator);
|
|
153
|
+
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
const line = columns.map(c => {
|
|
156
|
+
let val = String(row[c] ?? '');
|
|
157
|
+
if (val.length > 50) val = val.slice(0, 47) + '...';
|
|
158
|
+
return val.padEnd(widths[c]);
|
|
159
|
+
}).join(' │ ');
|
|
160
|
+
console.log(line);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(`\n${rows.length} result${rows.length !== 1 ? 's' : ''}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Convert pages to table rows */
|
|
167
|
+
function pagesToRows(pages) {
|
|
168
|
+
return pages.map(page => {
|
|
169
|
+
const row = { id: page.id };
|
|
170
|
+
if (page.properties) {
|
|
171
|
+
for (const [key, prop] of Object.entries(page.properties)) {
|
|
172
|
+
row[key] = propValue(prop);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return row;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Format rows as CSV string */
|
|
180
|
+
function formatCsv(rows, columns) {
|
|
181
|
+
if (!rows || rows.length === 0) return '(no results)';
|
|
182
|
+
const escape = (val) => {
|
|
183
|
+
const s = String(val ?? '');
|
|
184
|
+
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
|
|
185
|
+
return '"' + s.replace(/"/g, '""') + '"';
|
|
186
|
+
}
|
|
187
|
+
return s;
|
|
188
|
+
};
|
|
189
|
+
const lines = [columns.map(escape).join(',')];
|
|
190
|
+
for (const row of rows) {
|
|
191
|
+
lines.push(columns.map(c => escape(row[c])).join(','));
|
|
192
|
+
}
|
|
193
|
+
return lines.join('\n');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Format rows as YAML string */
|
|
197
|
+
function formatYaml(rows, columns) {
|
|
198
|
+
if (!rows || rows.length === 0) return '(no results)';
|
|
199
|
+
const lines = [];
|
|
200
|
+
for (let i = 0; i < rows.length; i++) {
|
|
201
|
+
if (i > 0) lines.push('');
|
|
202
|
+
lines.push(`- # result ${i + 1}`);
|
|
203
|
+
for (const col of columns) {
|
|
204
|
+
const val = String(rows[i][col] ?? '');
|
|
205
|
+
const needsQuote = val.includes(':') || val.includes('#') || val.includes('"') || val.includes("'") || val.includes('\n') || val === '';
|
|
206
|
+
lines.push(` ${col}: ${needsQuote ? '"' + val.replace(/"/g, '\\"') + '"' : val}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return lines.join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Output rows in the specified format */
|
|
213
|
+
function outputFormatted(rows, columns, format) {
|
|
214
|
+
switch (format) {
|
|
215
|
+
case 'csv':
|
|
216
|
+
console.log(formatCsv(rows, columns));
|
|
217
|
+
break;
|
|
218
|
+
case 'yaml':
|
|
219
|
+
console.log(formatYaml(rows, columns));
|
|
220
|
+
break;
|
|
221
|
+
case 'json':
|
|
222
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
223
|
+
break;
|
|
224
|
+
case 'table':
|
|
225
|
+
default:
|
|
226
|
+
printTable(rows, columns);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** UUID regex pattern used for validation */
|
|
232
|
+
const UUID_REGEX = /^[0-9a-f-]{32,36}$/i;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Build a Notion filter object from a schema entry and key=value string.
|
|
236
|
+
* Pure logic portion — does not call the API (schema must be provided).
|
|
237
|
+
*/
|
|
238
|
+
function buildFilterFromSchema(schema, filterStr) {
|
|
239
|
+
const eqIdx = filterStr.indexOf('=');
|
|
240
|
+
if (eqIdx === -1) {
|
|
241
|
+
return { error: `Invalid filter format: ${filterStr} (expected key=value)` };
|
|
242
|
+
}
|
|
243
|
+
const key = filterStr.slice(0, eqIdx);
|
|
244
|
+
const value = filterStr.slice(eqIdx + 1);
|
|
245
|
+
const schemaEntry = schema[key.toLowerCase()];
|
|
246
|
+
if (!schemaEntry) {
|
|
247
|
+
return {
|
|
248
|
+
error: `Filter property "${key}" not found in database schema.`,
|
|
249
|
+
available: Object.values(schema).map(s => s.name),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const propName = schemaEntry.name;
|
|
254
|
+
const type = schemaEntry.type;
|
|
255
|
+
|
|
256
|
+
switch (type) {
|
|
257
|
+
case 'title':
|
|
258
|
+
case 'rich_text':
|
|
259
|
+
return { filter: { property: propName, [type]: { contains: value } } };
|
|
260
|
+
case 'select':
|
|
261
|
+
return { filter: { property: propName, select: { equals: value } } };
|
|
262
|
+
case 'multi_select':
|
|
263
|
+
return { filter: { property: propName, multi_select: { contains: value } } };
|
|
264
|
+
case 'number':
|
|
265
|
+
return { filter: { property: propName, number: { equals: Number(value) } } };
|
|
266
|
+
case 'checkbox':
|
|
267
|
+
return { filter: { property: propName, checkbox: { equals: value === 'true' || value === '1' } } };
|
|
268
|
+
case 'date':
|
|
269
|
+
return { filter: { property: propName, date: { equals: value } } };
|
|
270
|
+
case 'status':
|
|
271
|
+
return { filter: { property: propName, status: { equals: value } } };
|
|
272
|
+
default:
|
|
273
|
+
return { filter: { property: propName, [type]: { equals: value } } };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
getConfigPaths,
|
|
279
|
+
loadConfig,
|
|
280
|
+
saveConfig,
|
|
281
|
+
richTextToPlain,
|
|
282
|
+
propValue,
|
|
283
|
+
buildPropValue,
|
|
284
|
+
printTable,
|
|
285
|
+
pagesToRows,
|
|
286
|
+
formatCsv,
|
|
287
|
+
formatYaml,
|
|
288
|
+
outputFormatted,
|
|
289
|
+
buildFilterFromSchema,
|
|
290
|
+
UUID_REGEX,
|
|
291
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jordancoin/notioncli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A powerful CLI for the Notion API — query databases, manage pages, and automate your workspace from the terminal.",
|
|
5
|
+
"main": "bin/notion.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"notion": "./bin/notion.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/unit.test.js test/mock.test.js",
|
|
11
|
+
"test:coverage": "c8 --reporter=lcov --reporter=text node --test test/unit.test.js test/mock.test.js",
|
|
12
|
+
"test:live": "node --test test/integration.test.js",
|
|
13
|
+
"test:all": "node --test test/"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/JordanCoin/notioncli.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"notion",
|
|
21
|
+
"cli",
|
|
22
|
+
"api",
|
|
23
|
+
"database",
|
|
24
|
+
"productivity",
|
|
25
|
+
"workspace",
|
|
26
|
+
"automation"
|
|
27
|
+
],
|
|
28
|
+
"author": "JordanCoin",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/JordanCoin/notioncli/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/JordanCoin/notioncli#readme",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"c8": "^10.1.3"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@notionhq/client": "^5.9.0",
|
|
39
|
+
"commander": "^14.0.3"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: notion
|
|
3
|
+
description: Notion API for creating and managing pages, databases, and blocks via the notioncli CLI tool.
|
|
4
|
+
homepage: https://github.com/JordanCoin/notioncli
|
|
5
|
+
metadata:
|
|
6
|
+
openclaw:
|
|
7
|
+
emoji: "📝"
|
|
8
|
+
requires:
|
|
9
|
+
env: ["NOTION_API_KEY"]
|
|
10
|
+
primaryEnv: NOTION_API_KEY
|
|
11
|
+
install: "npm install -g notioncli"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# notioncli — Notion API Skill
|
|
15
|
+
|
|
16
|
+
A powerful CLI for the Notion API. Query databases, manage pages, add comments, and automate your workspace from the terminal. Built for AI agents and humans alike.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g notioncli
|
|
22
|
+
notion init --key $NOTION_API_KEY
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The `init` command saves your API key and **auto-discovers all databases** shared with your integration. Each database gets an alias (a short slug derived from the database title) so you never need to type raw UUIDs.
|
|
26
|
+
|
|
27
|
+
> **Tip:** In Notion, you must share each database with your integration first: open the database → ••• menu → Connections → Add your integration.
|
|
28
|
+
|
|
29
|
+
## Auto-Aliases
|
|
30
|
+
|
|
31
|
+
When you run `notion init`, every shared database is automatically assigned a slug alias:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Found 3 databases:
|
|
35
|
+
|
|
36
|
+
✅ projects → Projects
|
|
37
|
+
✅ tasks → Tasks
|
|
38
|
+
✅ reading-list → Reading List
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You can then use `projects` instead of `a1b2c3d4-e5f6-...` everywhere. Manage aliases manually with:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
notion alias list # Show all aliases
|
|
45
|
+
notion alias add mydb <db-id> # Add a custom alias
|
|
46
|
+
notion alias rename old new # Rename an alias
|
|
47
|
+
notion alias remove mydb # Remove an alias
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands Reference
|
|
51
|
+
|
|
52
|
+
### Database Discovery
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
notion dbs # List all databases shared with your integration
|
|
56
|
+
notion alias list # Show configured aliases with IDs
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Querying Data
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
notion query tasks # Query all rows
|
|
63
|
+
notion query tasks --filter Status=Active # Filter by property
|
|
64
|
+
notion query tasks --sort Date:desc # Sort results
|
|
65
|
+
notion query tasks --filter Status=Active --limit 10 # Combine options
|
|
66
|
+
notion query tasks --output csv # CSV output
|
|
67
|
+
notion query tasks --output yaml # YAML output
|
|
68
|
+
notion query tasks --output json # JSON output
|
|
69
|
+
notion --json query tasks # JSON (shorthand)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Output formats:**
|
|
73
|
+
- `table` — formatted ASCII table (default)
|
|
74
|
+
- `csv` — header row + comma-separated values
|
|
75
|
+
- `json` — full API response as JSON
|
|
76
|
+
- `yaml` — simple key/value YAML format
|
|
77
|
+
|
|
78
|
+
### Creating Pages
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
notion add tasks --prop "Name=Buy groceries" --prop "Status=Todo"
|
|
82
|
+
notion add projects --prop "Name=New Feature" --prop "Priority=High" --prop "Due=2026-03-01"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Multiple `--prop` flags for multiple properties. Property names are case-insensitive and matched against the database schema.
|
|
86
|
+
|
|
87
|
+
### Updating Pages
|
|
88
|
+
|
|
89
|
+
By page ID:
|
|
90
|
+
```bash
|
|
91
|
+
notion update <page-id> --prop "Status=Done"
|
|
92
|
+
notion update <page-id> --prop "Priority=Low" --prop "Notes=Updated by CLI"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
By alias + filter (zero UUIDs):
|
|
96
|
+
```bash
|
|
97
|
+
notion update tasks --filter "Name=Ship feature" --prop "Status=Done"
|
|
98
|
+
notion update workouts --filter "Name=LEGS #5" --prop "Notes=Great session"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Reading Pages & Content
|
|
102
|
+
|
|
103
|
+
By page ID:
|
|
104
|
+
```bash
|
|
105
|
+
notion get <page-id> # Page properties
|
|
106
|
+
notion blocks <page-id> # Page content (headings, text, lists, etc.)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
By alias + filter:
|
|
110
|
+
```bash
|
|
111
|
+
notion get tasks --filter "Name=Ship feature"
|
|
112
|
+
notion blocks tasks --filter "Name=Ship feature"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Deleting (Archiving) Pages
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
notion delete <page-id> # By page ID
|
|
119
|
+
notion delete tasks --filter "Name=Old task" # By alias + filter
|
|
120
|
+
notion delete workouts --filter "Date=2026-02-09" # By alias + filter
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Appending Content
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
notion append <page-id> "Meeting notes: discussed Q2 roadmap"
|
|
127
|
+
notion append tasks "Status update: phase 1 complete" --filter "Name=Ship feature"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Appends a paragraph block to the page.
|
|
131
|
+
|
|
132
|
+
### Users
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
notion users # List all workspace users
|
|
136
|
+
notion user <user-id> # Get details for a specific user
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Comments
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
notion comments <page-id> # By page ID
|
|
143
|
+
notion comments tasks --filter "Name=Ship feature" # By alias + filter
|
|
144
|
+
notion comment <page-id> "Looks good, shipping this!" # By page ID
|
|
145
|
+
notion comment tasks "AI review complete ✅" --filter "Name=Ship feature" # By alias + filter
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Search
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
notion search "quarterly report" # Search across all pages and databases
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### JSON Output
|
|
155
|
+
|
|
156
|
+
Add `--json` before any command to get the raw Notion API response:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
notion --json query tasks
|
|
160
|
+
notion --json get <page-id>
|
|
161
|
+
notion --json users
|
|
162
|
+
notion --json comments <page-id>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Common Patterns for AI Agents
|
|
166
|
+
|
|
167
|
+
### 1. Discover available databases
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
notion dbs
|
|
171
|
+
notion alias list
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 2. Query and filter data
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
notion query tasks --filter Status=Active --sort Date:desc
|
|
178
|
+
notion --json query tasks --filter Status=Active # Parse JSON programmatically
|
|
179
|
+
notion query tasks --output csv # CSV for spreadsheet tools
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 3. Create a new entry
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
notion add tasks --prop "Name=Review PR #42" --prop "Status=Todo" --prop "Priority=High"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 4. Update an existing entry (zero UUIDs)
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# By alias + filter — no page ID needed
|
|
192
|
+
notion update tasks --filter "Name=Review PR #42" --prop "Status=Done"
|
|
193
|
+
|
|
194
|
+
# Or by page ID if you already have it
|
|
195
|
+
notion update <page-id> --prop "Status=Done"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### 5. Read page content (zero UUIDs)
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# By alias + filter
|
|
202
|
+
notion get tasks --filter "Name=Review PR #42"
|
|
203
|
+
notion blocks tasks --filter "Name=Review PR #42"
|
|
204
|
+
|
|
205
|
+
# Or by page ID
|
|
206
|
+
notion get <page-id>
|
|
207
|
+
notion blocks <page-id>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 6. Append notes to a page
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
notion append tasks "Status update: completed phase 1" --filter "Name=Review PR #42"
|
|
214
|
+
notion append <page-id> "Status update: completed phase 1"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 7. Collaborate with comments
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
notion comments tasks --filter "Name=Review PR #42" # Check existing
|
|
221
|
+
notion comment tasks "AI review complete ✅" --filter "Name=Review PR #42" # Add comment
|
|
222
|
+
|
|
223
|
+
# Or by page ID
|
|
224
|
+
notion comments <page-id>
|
|
225
|
+
notion comment <page-id> "AI review complete ✅"
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 8. Delete by alias + filter
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
notion delete tasks --filter "Name=Old task"
|
|
232
|
+
notion delete workouts --filter "Date=2026-02-09"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Property Type Reference
|
|
236
|
+
|
|
237
|
+
When using `--prop key=value`, the CLI auto-detects the property type from the database schema:
|
|
238
|
+
|
|
239
|
+
| Type | Example Value | Notes |
|
|
240
|
+
|------|-------------|-------|
|
|
241
|
+
| `title` | `Name=Hello World` | Main title property |
|
|
242
|
+
| `rich_text` | `Notes=Some text` | Plain text content |
|
|
243
|
+
| `number` | `Amount=42.5` | Numeric values |
|
|
244
|
+
| `select` | `Status=Active` | Single select option |
|
|
245
|
+
| `multi_select` | `Tags=bug,urgent` | Comma-separated options |
|
|
246
|
+
| `date` | `Due=2026-03-01` | ISO 8601 date string |
|
|
247
|
+
| `checkbox` | `Done=true` | `true`, `1`, or `yes` |
|
|
248
|
+
| `url` | `Link=https://example.com` | Full URL |
|
|
249
|
+
| `email` | `Contact=user@example.com` | Email address |
|
|
250
|
+
| `phone_number` | `Phone=+1234567890` | Phone number string |
|
|
251
|
+
| `status` | `Status=In Progress` | Status property |
|
|
252
|
+
|
|
253
|
+
## Notion API 2025 — Dual IDs
|
|
254
|
+
|
|
255
|
+
The Notion API (2025-09-03) uses dual IDs for databases: a `database_id` and a `data_source_id`. notioncli handles this automatically — when you run `notion init` or `notion alias add`, both IDs are discovered and stored. You never need to think about it.
|
|
256
|
+
|
|
257
|
+
## Troubleshooting
|
|
258
|
+
|
|
259
|
+
- **"No Notion API key found"** — Run `notion init --key ntn_...` or `export NOTION_API_KEY=ntn_...`
|
|
260
|
+
- **"Unknown database alias"** — Run `notion alias list` to see available aliases, or `notion init` to rediscover
|
|
261
|
+
- **"Not found" errors** — Make sure the database/page is shared with your integration in Notion
|
|
262
|
+
- **Filter/sort property not found** — Property names are case-insensitive; run `notion --json query <alias> --limit 1` to see available properties
|
package/skill/install.sh
ADDED