@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/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
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ # Install notioncli globally
3
+ npm install -g notioncli
4
+ echo "✅ notioncli installed. Run: notion init --key \$NOTION_API_KEY"