@andrzejchm/notion-cli 0.6.0 → 0.7.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.
@@ -0,0 +1,189 @@
1
+ # Feature Parity: notion-cli vs Notion MCP
2
+
3
+ > Compared against the official Notion MCP server tools (2026-03).
4
+ > This document tracks gaps and serves as a prioritized roadmap for closing them.
5
+
6
+ **Legend:**
7
+ - **CLI** = `@andrzejchm/notion-cli` (this repo, v0.6.0)
8
+ - **MCP** = Official Notion MCP server
9
+
10
+ ---
11
+
12
+ ## Current Parity Summary
13
+
14
+ | Area | CLI | MCP | Parity |
15
+ |------|-----|-----|--------|
16
+ | Search | Basic keyword | Semantic + AI + connected sources + date/creator filters | Partial |
17
+ | Page read | Markdown via API | Markdown + discussions + transcripts | Partial |
18
+ | Page create | Under pages only | Under pages, databases, data sources; batch; templates; icon/cover | Partial |
19
+ | Page edit | Surgical replace via `--range` | Search-and-replace (multi-op), full replace, template apply, verification | Partial |
20
+ | Page properties | Read-only (db query) | Full read + write (update any property) | Gap |
21
+ | Move pages | - | Batch move to any parent | Gap |
22
+ | Duplicate pages | - | Duplicate with async content copy | Gap |
23
+ | Archive/delete | - | Trash pages | Gap |
24
+ | Database create | - | SQL DDL `CREATE TABLE` syntax | Gap |
25
+ | Database schema update | Read-only schema | `ADD/DROP/RENAME/ALTER COLUMN` via DDL | Gap |
26
+ | Database views | - | Create + update 10 view types with DSL | Gap |
27
+ | Comments | Page-level add + list | Page-level + inline (selection-anchored) + reply to thread + rich text | Partial |
28
+ | Users | List all | List + search + fetch by ID + fetch self | Partial |
29
+ | Teams | - | List teamspaces + search | Gap |
30
+ | Create pages in DB | - | Full property support, date/place/checkbox expanded formats | Gap |
31
+ | Batch operations | - | Create up to 100 pages, move up to 100 pages | Gap |
32
+ | Icon / Cover | - | Set emoji/image icon + cover on create and update | Gap |
33
+
34
+ ---
35
+
36
+ ## Prioritized Gaps (Roadmap)
37
+
38
+ Ordered by impact for AI-agent workflows first, then developer productivity.
39
+
40
+ ### Tier 1 - High Impact (core agent workflows)
41
+
42
+ These gaps directly limit what an AI agent can accomplish through the CLI compared to using MCP directly.
43
+
44
+ #### 1. Update page properties
45
+ **MCP:** `update_properties` command — set title, status, dates, select, people, checkbox, numbers, etc. Supports `null` to clear.
46
+ **CLI:** No equivalent. Can only read properties via `db query`.
47
+ **Why first:** AI agents frequently need to update task status, dates, assignees. This is the single most impactful missing write operation.
48
+ **Suggested command:** `notion update <id> --prop "Status=Done" --prop "Priority=High"`
49
+
50
+ #### 2. Create pages in databases (with properties)
51
+ **MCP:** `create-pages` supports `data_source_id` parent with full property map including expanded date, place, checkbox formats.
52
+ **CLI:** `create-page` only supports `page_id` parent with title + markdown body. Cannot create database entries.
53
+ **Why second:** Agents need to create tasks, tickets, and records in databases — not just child pages.
54
+ **Suggested command:** `notion create-page --parent <db-id> --title "Task" --prop "Status=To Do" --prop "Due=2026-04-01"`
55
+
56
+ #### 3. Archive / trash pages
57
+ **MCP:** `update-page` with property changes or page operations; `update_data_source` with `in_trash: true`.
58
+ **CLI:** No equivalent.
59
+ **Why third:** Agents need to clean up after themselves — archive completed tasks, delete draft pages.
60
+ **Suggested command:** `notion archive <id>` / `notion delete <id>`
61
+
62
+ #### 4. Search filters (date range, creator)
63
+ **MCP:** `created_date_range` (start/end dates), `created_by_user_ids` filter, scoped search within page/database/teamspace.
64
+ **CLI:** Keyword-only search with `--type` filter.
65
+ **Why important:** Agents searching for "recent" items or "my tasks" need date and creator filters to get relevant results without scanning everything.
66
+ **Suggested flags:** `--created-after`, `--created-before`, `--created-by`
67
+
68
+ ### Tier 2 - Medium Impact (power-user and advanced agent workflows)
69
+
70
+ #### 5. Inline / anchored comments
71
+ **MCP:** `selection_with_ellipsis` targets a specific block; `discussion_id` replies to existing threads. Rich text with mentions, dates, links.
72
+ **CLI:** Page-level comments only. Plain text only.
73
+ **Why:** Agents doing code/doc review need to comment on specific sections, not just drop a page-level note. Thread replies keep conversations organized.
74
+ **Suggested flags:** `notion comment <id> -m "text" --anchor "## Section...content"` / `--reply-to <discussion-id>`
75
+
76
+ #### 6. Create databases
77
+ **MCP:** `CREATE TABLE` DDL with full type system (select, relation, rollup, formula, unique_id, etc.).
78
+ **CLI:** No equivalent.
79
+ **Why:** Agents building project scaffolds or workflows need to create structured databases, not just pages.
80
+ **Suggested command:** `notion create-db --parent <id> --title "Tasks" --schema "Name TITLE, Status SELECT(Todo,Done)"`
81
+
82
+ #### 7. Move pages
83
+ **MCP:** Batch move up to 100 pages/databases to a new parent (page, database, data source, or workspace).
84
+ **CLI:** No equivalent.
85
+ **Why:** Reorganizing content (moving completed items to archive, restructuring projects) is a common agent task.
86
+ **Suggested command:** `notion move <id...> --to <parent-id>`
87
+
88
+ #### 8. Update database schema
89
+ **MCP:** `ADD COLUMN`, `DROP COLUMN`, `RENAME COLUMN`, `ALTER COLUMN SET` via DDL.
90
+ **CLI:** Read-only schema via `notion db schema`.
91
+ **Why:** Evolving database structure (adding a new status option, renaming a field) without leaving the terminal.
92
+ **Suggested command:** `notion db alter <id> --add "Priority SELECT(High,Medium,Low)" --rename "Status:Project Status"`
93
+
94
+ #### 9. Multi-operation content editing
95
+ **MCP:** `update_content` accepts an array of `{ old_str, new_str }` pairs (up to 100) in a single call, with `replace_all_matches` option.
96
+ **CLI:** `notion edit-page` supports single `--range` replacement. No batch search-and-replace.
97
+ **Why:** Agents making multiple edits to a page (updating several sections) currently need multiple CLI invocations.
98
+ **Suggested approach:** Support multiple `--replace "old...new"` flags or a JSON patch file.
99
+
100
+ #### 10. Duplicate pages
101
+ **MCP:** `duplicate-page` — async copy of any accessible page.
102
+ **CLI:** No equivalent.
103
+ **Why:** Templating workflows — copy a template page to start a new project/sprint.
104
+ **Suggested command:** `notion duplicate <id>`
105
+
106
+ ### Tier 3 - Lower Impact (nice-to-have, niche workflows)
107
+
108
+ #### 11. Database views (create + update)
109
+ **MCP:** 10 view types (table, board, calendar, timeline, gallery, list, form, chart, map, dashboard) with DSL for filters, sorts, grouping.
110
+ **CLI:** No equivalent.
111
+ **Why:** Mostly a UI concern; agents rarely need to create views programmatically. But useful for project setup automation.
112
+
113
+ #### 12. Page icon and cover
114
+ **MCP:** Set emoji/custom emoji/image URL as icon; set image URL as cover on create and update.
115
+ **CLI:** No equivalent.
116
+ **Why:** Cosmetic but helps agents create polished pages. Low effort to add as flags.
117
+ **Suggested flags:** `--icon "🚀"` / `--cover "https://..."`
118
+
119
+ #### 13. Teams / teamspaces listing
120
+ **MCP:** `get-teams` with name search.
121
+ **CLI:** No equivalent.
122
+ **Why:** Rarely needed by agents. Useful for workspace discovery in large organizations.
123
+ **Suggested command:** `notion teams`
124
+
125
+ #### 14. Scoped search (within page / database / teamspace)
126
+ **MCP:** `page_url`, `data_source_url`, `teamspace_id` parameters scope search.
127
+ **CLI:** Global search only.
128
+ **Why:** Useful for agents working within a specific project area, but global search + filtering usually suffices.
129
+ **Suggested flags:** `--within <id>`
130
+
131
+ #### 15. Page verification
132
+ **MCP:** `update_verification` — mark pages as verified with optional expiry (Business/Enterprise only).
133
+ **CLI:** No equivalent.
134
+ **Why:** Enterprise-only feature, limited audience.
135
+
136
+ #### 16. Template application
137
+ **MCP:** Apply database templates on create and update (template content is async).
138
+ **CLI:** No equivalent.
139
+ **Why:** Useful for standardized page creation but requires database template discovery first.
140
+ **Suggested flags:** `--template <template-id>`
141
+
142
+ #### 17. Batch page creation
143
+ **MCP:** Create up to 100 pages in a single call.
144
+ **CLI:** One page per invocation.
145
+ **Why:** Performance optimization for bulk workflows. Can be scripted with shell loops for now.
146
+
147
+ #### 18. Advanced user lookup
148
+ **MCP:** Search users by name/email, fetch by ID, fetch authenticated user (`self`).
149
+ **CLI:** `notion users` lists all, no search or lookup.
150
+ **Suggested flags:** `notion users --search "john"` / `notion users --id <uuid>` / `notion users --me`
151
+
152
+ #### 19. Fetch page discussions inline
153
+ **MCP:** `include_discussions: true` on fetch shows discussion anchors in page content; `get-comments` with `include_all_blocks`, `include_resolved`.
154
+ **CLI:** `notion comments` shows page-level comments only, no block-level discussions, no resolved filter.
155
+ **Suggested flags:** `--all-blocks` / `--include-resolved`
156
+
157
+ ---
158
+
159
+ ## What CLI Does That MCP Doesn't
160
+
161
+ The CLI isn't just chasing MCP parity — it has unique strengths:
162
+
163
+ | CLI Feature | MCP Equivalent |
164
+ |---|---|
165
+ | `notion open <id>` — open in browser | No equivalent |
166
+ | `notion ls` — list all accessible content | Must use search with empty query |
167
+ | `notion db schema` — human-readable schema | Must fetch database and parse response |
168
+ | `notion auth` — multi-profile management with OAuth | N/A (MCP uses connection-level auth) |
169
+ | `notion completion bash/zsh/fish` — shell completions | N/A |
170
+ | `--verbose` — debug API requests | N/A |
171
+ | Pipe-friendly output (stdin → page, table → stdout) | N/A (MCP is programmatic, not pipe-based) |
172
+ | URL → ID normalization everywhere | Both support this |
173
+ | `.notion.yaml` per-project config | N/A |
174
+
175
+ ---
176
+
177
+ ## Implementation Notes
178
+
179
+ - Tier 1 items (1-4) should be tackled before any Tier 2 work
180
+ - Items 1 and 2 can share infrastructure (property value parsing, `--prop` flag syntax)
181
+ - Item 3 (archive) is likely a small addition once property updates work (archive is a property)
182
+ - Items 6 and 8 (database create/alter) can share a schema DSL parser
183
+ - The CLI should NOT try to replicate MCP's SQL DDL syntax — a simpler flag-based approach fits CLI ergonomics better
184
+
185
+ ---
186
+
187
+ *Last updated: 2026-03-23*
188
+ *CLI version compared: 0.6.0*
189
+ *MCP version compared: Official Notion MCP (2026-03)*
@@ -78,3 +78,45 @@ npm install -g @andrzejchm/notion-cli
78
78
  curl -fsSL https://raw.githubusercontent.com/andrzejchm/notion-cli/main/docs/skills/using-notion-cli/SKILL.md \
79
79
  -o ~/.config/opencode/skills/using-notion-cli/SKILL.md
80
80
  ```
81
+
82
+ ## Commands Reference
83
+
84
+ ### `notion update <id|url>`
85
+
86
+ Update properties on any Notion page (standalone or database entry).
87
+
88
+ ```bash
89
+ # Set a select property
90
+ notion update "$PAGE_ID" --prop "Status=Done"
91
+
92
+ # Set multiple properties at once
93
+ notion update "$PAGE_ID" --prop "Status=In Progress" --prop "Priority=High"
94
+
95
+ # Update the page title
96
+ notion update "$PAGE_ID" --title "New Page Title"
97
+
98
+ # Combine --title and --prop
99
+ notion update "$PAGE_ID" --title "Updated" --prop "Status=Done"
100
+
101
+ # Clear a property (set to empty)
102
+ notion update "$PAGE_ID" --prop "Status="
103
+
104
+ # Output the updated page as JSON
105
+ notion update "$PAGE_ID" --prop "Status=Done" --json
106
+ ```
107
+
108
+ **Supported property types:** `title`, `rich_text`, `select`, `status`, `multi_select`, `number`, `checkbox`, `url`, `email`, `phone_number`, `date`
109
+
110
+ **`--prop` format details:**
111
+
112
+ | Type | Example |
113
+ |------|---------|
114
+ | `title` / `rich_text` | `--prop "Name=My Title"` |
115
+ | `select` / `status` | `--prop "Status=Done"` |
116
+ | `multi_select` | `--prop "Tags=design,eng,qa"` |
117
+ | `number` | `--prop "Count=42"` |
118
+ | `checkbox` | `--prop "Done=true"` or `--prop "Done=yes"` |
119
+ | `url` | `--prop "Link=https://example.com"` |
120
+ | `date` | `--prop "Due=2024-12-25"` or `--prop "Range=2024-01-01,2024-01-31"` |
121
+
122
+ Required integration capabilities: **Read content**, **Update content**
@@ -119,26 +119,32 @@ notion comment <id|url> -m "Reviewed and approved." # add comment to a page
119
119
 
120
120
  #### Surgical Editing
121
121
 
122
- Insert content at a specific location or replace a targeted section instead of the whole page.
122
+ Search-and-replace specific text, replace the full page, or insert content at a specific location.
123
123
 
124
124
  ```bash
125
- # Append after a matched section (inserts new blocks right after the match)
126
- notion append <id|url> -m "New content" --after "## Status...end of status"
125
+ # Search-and-replace: find text and replace it
126
+ notion edit-page <id|url> --find "old text" --replace "new text"
127
+
128
+ # Multiple search-and-replace operations in one call
129
+ notion edit-page <id|url> --find "old1" --replace "new1" --find "old2" --replace "new2"
130
+
131
+ # Replace all occurrences (not just the first match)
132
+ notion edit-page <id|url> --find "TODO" --replace "DONE" --all
127
133
 
128
134
  # Replace an entire page's content
129
135
  notion edit-page <id|url> -m "# Replacement\nNew full-page content"
130
136
 
131
- # Replace only a matched section (leaves the rest of the page intact)
132
- notion edit-page <id|url> -m "## Updated Section\nNew text" --range "## Old Section...old last line"
137
+ # Pipe replacement content from a file
138
+ cat updated-section.md | notion edit-page <id|url>
133
139
 
134
- # Replace a section that contains child pages/databases (requires explicit opt-in)
135
- notion edit-page <id|url> -m "## Clean Slate" --range "## Archive...end" --allow-deleting-content
140
+ # Allow deletion of child pages/databases during replace
141
+ notion edit-page <id|url> -m "## Clean Slate" --allow-deleting-content
136
142
 
137
- # Pipe replacement content from a file
138
- cat updated-section.md | notion edit-page <id|url> --range "## Notes...end of notes"
143
+ # Append after a matched section (inserts new blocks right after the match)
144
+ notion append <id|url> -m "New content" --after "## Status...end of status"
139
145
  ```
140
146
 
141
- > **Ellipsis selector format:** Both `--after` and `--range` use the same selector syntax: `"start text...end text"`. Provide ~10 characters from the beginning of the target content, three dots (`...`), then ~10 characters from the end. The selector matches the first contiguous range of blocks whose text starts with the prefix and ends with the suffix. Run `notion read <id>` first to see the exact content and pick accurate selectors.
147
+ > **`--range` (deprecated):** The `--range` flag still works for backward compatibility but uses the older `replace_content_range` API. Prefer `--find`/`--replace` for targeted edits.
142
148
 
143
149
  ---
144
150
 
@@ -180,14 +186,16 @@ echo "Created: $URL"
180
186
  # Pipe command output into a new page
181
187
  my-report-command | notion create-page --parent "$PAGE_ID" --title "Auto Report"
182
188
 
183
- # Surgically update a specific section of a page
184
- # 1. Read the page to find the section boundaries
189
+ # Surgically update specific text on a page
190
+ # 1. Read the page to find the text you want to change
185
191
  notion read "$PAGE_ID"
186
- # 2. Identify a selector from the output, e.g. the section starts with "## Status"
187
- # and ends with "Blocked: none" build the ellipsis selector from those
188
- # 3. Replace just that section
189
- notion edit-page "$PAGE_ID" -m "## Status\nAll tasks complete.\nBlocked: none" \
190
- --range "## Status...Blocked: none"
192
+ # 2. Use --find/--replace to swap specific text
193
+ notion edit-page "$PAGE_ID" --find "Status: In Progress" --replace "Status: Done"
194
+
195
+ # Multiple replacements in one call
196
+ notion edit-page "$PAGE_ID" \
197
+ --find "Status: In Progress" --replace "Status: Done" \
198
+ --find "Blocked: yes" --replace "Blocked: none"
191
199
 
192
200
  # Insert a new sub-section after an existing section
193
201
  notion append "$PAGE_ID" -m "## New Sub-section\nContent here" \
@@ -214,4 +222,6 @@ notion append "$PAGE_ID" -m "## New Sub-section\nContent here" \
214
222
 
215
223
  **`notion edit-page` returns "Insufficient permissions"** — Enable **Update content** in integration capabilities.
216
224
 
217
- **`--range` / `--after` selector not found** — Run `notion read <id>` to see the exact page content. The selector must match real text: `"start...end"` with ~10 chars from the beginning and end of the target range.
225
+ **`--find` text not found** — Run `notion read <id>` to see the exact page content. The `--find` value must match text on the page exactly.
226
+
227
+ **`--after` selector not found** — Run `notion read <id>` to see the exact page content. The selector must match real text: `"start...end"` with ~10 chars from the beginning and end of the target range.
@@ -0,0 +1,250 @@
1
+ # Integration Testing Setup
2
+
3
+ This guide walks through setting up a Notion workspace for running integration tests against the `notion-cli` tool.
4
+
5
+ ## Prerequisites
6
+
7
+ - A Notion account (free plan works)
8
+ - Node.js >= 22
9
+ - This repository cloned locally
10
+
11
+ ## 1. Create a Notion Integration
12
+
13
+ 1. Go to [https://www.notion.so/profile/integrations](https://www.notion.so/profile/integrations)
14
+ 2. Click **"New integration"**
15
+ 3. Name it `notion-cli-tests`
16
+ 4. Select the workspace you want to use for testing
17
+ 5. Under **Capabilities**, enable:
18
+ - **Read content**
19
+ - **Update content**
20
+ - **Insert content**
21
+ - **Read comments**
22
+ - **Create comments**
23
+ 6. Click **Save**
24
+ 7. Copy the **Internal Integration Token** (starts with `ntn_`) — you'll need it later
25
+
26
+ ## 2. Create Fixture Pages and Databases
27
+
28
+ Create the following structure in your Notion workspace. All fixtures live under a single root page.
29
+
30
+ ### Root Page
31
+
32
+ Create a page called **`notion-cli-test-fixtures`**. This is the parent for everything below.
33
+
34
+ ### Fixture Table
35
+
36
+ | Fixture | Page/DB Name | Content |
37
+ |---|---|---|
38
+ | **Rich Content Page** | `Test: Rich Content` | See [Rich Content Page details](#rich-content-page) below |
39
+ | **Simple Page** | `Test: Simple Page` | Title + a single paragraph: `"This is a simple test page with a single paragraph."` |
40
+ | **Empty Page** | `Test: Empty Page` | Title only, no body content at all |
41
+ | **Task Database** | `Test: Task Database` | See [Task Database details](#task-database) below |
42
+ | **Empty Database** | `Test: Empty Database` | Same schema as Task Database, zero rows |
43
+ | **Comments Page** | `Test: Comments Page` | See [Comments Page details](#comments-page) below |
44
+
45
+ ### Rich Content Page
46
+
47
+ Create a page called `Test: Rich Content` with the following blocks (in order):
48
+
49
+ 1. **Heading 1**: `Main Heading`
50
+ 2. **Paragraph**: `This is a paragraph with **bold**, *italic*, and ~~strikethrough~~ text.`
51
+ 3. **Heading 2**: `Sub Heading`
52
+ 4. **Bulleted list** with 3 items:
53
+ - `First item`
54
+ - `Second item`
55
+ - `Third item`
56
+ 5. **Numbered list** with 3 items:
57
+ 1. `Step one`
58
+ 2. `Step two`
59
+ 3. `Step three`
60
+ 6. **Toggle block**: Summary `Click to expand`, content `Hidden content inside toggle`
61
+ 7. **Callout block**: `This is an important callout` (use any emoji icon)
62
+ 8. **Code block** (language: `javascript`):
63
+ ```javascript
64
+ function hello() {
65
+ return "world";
66
+ }
67
+ ```
68
+ 9. **Table**: 3 columns × 2 data rows
69
+
70
+ | Name | Value | Notes |
71
+ |------|-------|-------|
72
+ | Alpha | 1 | First entry |
73
+ | Beta | 2 | Second entry |
74
+
75
+ 10. **Child page**: `Nested Child Page` — with a single paragraph: `"Content inside nested page."`
76
+
77
+ ### Task Database
78
+
79
+ Create an inline database called `Test: Task Database` with these properties:
80
+
81
+ | Property | Type | Configuration |
82
+ |---|---|---|
83
+ | **Title** | Title | (default title column) |
84
+ | **Status** | Select | Options: `Not Started`, `In Progress`, `Done` |
85
+ | **Priority** | Number | Number format: Number |
86
+ | **Due Date** | Date | — |
87
+ | **Tags** | Multi-select | Options: `bug`, `feature`, `docs`, `urgent` |
88
+
89
+ Add 5–10 rows with varied data. Example rows:
90
+
91
+ | Title | Status | Priority | Due Date | Tags |
92
+ |---|---|---|---|---|
93
+ | Fix login bug | In Progress | 1 | 2025-03-01 | bug, urgent |
94
+ | Add search feature | Not Started | 2 | 2025-04-15 | feature |
95
+ | Update README | Done | 3 | 2025-02-01 | docs |
96
+ | Refactor auth module | In Progress | 1 | 2025-03-10 | feature |
97
+ | Write API docs | Not Started | 2 | 2025-05-01 | docs |
98
+
99
+ ### Empty Database
100
+
101
+ Create an inline database called `Test: Empty Database` with the **exact same schema** as the Task Database (Title, Status, Priority, Due Date, Tags) but **do not add any rows**.
102
+
103
+ ### Comments Page
104
+
105
+ Create a page called `Test: Comments Page` with:
106
+
107
+ - **Body**: A single paragraph: `"This page is used to test comment retrieval."`
108
+ - **Page-level comments** (add 2–3 via the page comment area):
109
+ 1. `"First test comment"`
110
+ 2. `"Second test comment"`
111
+ 3. `"Third test comment"` (optional)
112
+ - **Inline comment**: Select the word `"comment"` in the paragraph and add a discussion comment: `"Inline comment on selected text"`
113
+
114
+ ## 3. Share the Root Page with the Integration
115
+
116
+ 1. Open the `notion-cli-test-fixtures` page in Notion
117
+ 2. Click **"Share"** (top right) or the **"···"** menu → **"Connections"**
118
+ 3. Search for `notion-cli-tests` and add it
119
+ 4. Confirm the integration has access — all child pages and databases inherit the connection
120
+
121
+ ## 4. Extract Page and Database IDs
122
+
123
+ Each Notion page/database has a unique ID embedded in its URL.
124
+
125
+ **From a page URL:**
126
+ ```
127
+ https://www.notion.so/Your-Page-Title-1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
128
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
129
+ This is the page ID (32 hex chars)
130
+ ```
131
+
132
+ Format it as a UUID: `1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d`
133
+
134
+ **From a database URL:**
135
+ ```
136
+ https://www.notion.so/1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d?v=...
137
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
138
+ This is the database ID
139
+ ```
140
+
141
+ The 32-character hex string after the last `/` (and before any `?`) is the ID. You can use it with or without dashes.
142
+
143
+ Open each fixture page/database and extract its ID.
144
+
145
+ ## 5. Configure `.env.test`
146
+
147
+ Copy the example file and fill in real values:
148
+
149
+ ```bash
150
+ cp .env.test.example .env.test
151
+ ```
152
+
153
+ Edit `.env.test` with the IDs you extracted:
154
+
155
+ ```bash
156
+ # Internal integration token (starts with ntn_)
157
+ NOTION_TEST_TOKEN=ntn_your_real_token
158
+
159
+ # Root page containing all fixtures
160
+ NOTION_TEST_ROOT_PAGE_ID=<id-of-notion-cli-test-fixtures-page>
161
+
162
+ # Rich Content Page
163
+ NOTION_FIXTURE_RICH_PAGE_ID=<id-of-test-rich-content-page>
164
+
165
+ # Simple Page
166
+ NOTION_FIXTURE_SIMPLE_PAGE_ID=<id-of-test-simple-page>
167
+
168
+ # Empty Page
169
+ NOTION_FIXTURE_EMPTY_PAGE_ID=<id-of-test-empty-page>
170
+
171
+ # Task Database
172
+ NOTION_FIXTURE_TASK_DB_ID=<id-of-test-task-database>
173
+
174
+ # Empty Database
175
+ NOTION_FIXTURE_EMPTY_DB_ID=<id-of-test-empty-database>
176
+
177
+ # Comments Page
178
+ NOTION_FIXTURE_COMMENTS_PAGE_ID=<id-of-test-comments-page>
179
+ ```
180
+
181
+ ### Environment Variable Reference
182
+
183
+ | Variable | Description |
184
+ |---|---|
185
+ | `NOTION_TEST_TOKEN` | Internal integration token from step 1. The test runner passes this as `NOTION_API_TOKEN` to the CLI (the env var the CLI reads for auth — see `src/config/token.ts`). |
186
+ | `NOTION_TEST_ROOT_PAGE_ID` | ID of the `notion-cli-test-fixtures` root page. Used to verify integration access. |
187
+ | `NOTION_FIXTURE_RICH_PAGE_ID` | ID of the `Test: Rich Content` page. Tests heading, list, toggle, callout, code block, table, and nested child page rendering. |
188
+ | `NOTION_FIXTURE_SIMPLE_PAGE_ID` | ID of the `Test: Simple Page` page. Tests basic page read with title and a single paragraph. |
189
+ | `NOTION_FIXTURE_EMPTY_PAGE_ID` | ID of the `Test: Empty Page` page. Tests reading a page with no body content. |
190
+ | `NOTION_FIXTURE_TASK_DB_ID` | ID of the `Test: Task Database` database. Tests database queries, filtering, sorting across multiple property types. |
191
+ | `NOTION_FIXTURE_EMPTY_DB_ID` | ID of the `Test: Empty Database` database. Tests querying a database that returns zero results. |
192
+ | `NOTION_FIXTURE_COMMENTS_PAGE_ID` | ID of the `Test: Comments Page` page. Tests reading page-level and inline comments. |
193
+
194
+ > **Important:** The CLI authenticates via the `NOTION_API_TOKEN` environment variable (not `NOTION_TOKEN`). Integration tests store the token as `NOTION_TEST_TOKEN` and the test runner helper is responsible for passing it as `NOTION_API_TOKEN` when spawning CLI processes.
195
+
196
+ ## 6. Install Optional Dependencies
197
+
198
+ Install `dotenv-cli` to load `.env.test` when running tests locally:
199
+
200
+ ```bash
201
+ npm install --save-dev dotenv-cli
202
+ ```
203
+
204
+ This is already listed as a devDependency if you've run `npm install` after pulling this change.
205
+
206
+ ## 7. Run Integration Tests
207
+
208
+ Run integration tests locally with the test environment loaded:
209
+
210
+ ```bash
211
+ npx dotenv -e .env.test -- npm run test:integration
212
+ ```
213
+
214
+ This command:
215
+ 1. Loads variables from `.env.test` into the environment
216
+ 2. Runs `vitest run --config vitest.config.integration.ts`
217
+ 3. Vitest picks up test files from `tests/integration/**/*.test.ts`
218
+
219
+ ### Troubleshooting
220
+
221
+ | Problem | Solution |
222
+ |---|---|
223
+ | `Script "test:integration" not found` | Run `npm install` to refresh scripts |
224
+ | `401 Unauthorized` from Notion API | Verify `NOTION_TEST_TOKEN` is correct and the integration is still active |
225
+ | `404 Not Found` for a fixture | Verify the page/database ID is correct and the root page is shared with the integration |
226
+ | Tests time out | Default timeout is 30s per test. Check your network connection. |
227
+ | `NOTION_API_TOKEN` not set errors | Make sure you're using `npx dotenv -e .env.test --` before the npm command |
228
+
229
+ ## CI Configuration
230
+
231
+ Integration tests run automatically on **pull requests** via the `integration` job in `.github/workflows/ci.yml`. They do not run on push events, so pushes to `main` are never blocked by integration test failures.
232
+
233
+ The job depends on the `ci` job (typecheck, lint, unit tests) passing first, then builds the CLI and runs `npm run test:integration`.
234
+
235
+ ### Required GitHub Actions Secrets
236
+
237
+ Configure these secrets in your repository under **Settings → Secrets and variables → Actions**:
238
+
239
+ | Secret | Description |
240
+ |---|---|
241
+ | `NOTION_TEST_TOKEN` | Internal integration token (starts with `ntn_`) |
242
+ | `NOTION_TEST_ROOT_PAGE_ID` | ID of the `notion-cli-test-fixtures` root page |
243
+ | `NOTION_FIXTURE_RICH_PAGE_ID` | ID of the `Test: Rich Content` page |
244
+ | `NOTION_FIXTURE_SIMPLE_PAGE_ID` | ID of the `Test: Simple Page` page |
245
+ | `NOTION_FIXTURE_EMPTY_PAGE_ID` | ID of the `Test: Empty Page` page |
246
+ | `NOTION_FIXTURE_TASK_DB_ID` | ID of the `Test: Task Database` database |
247
+ | `NOTION_FIXTURE_EMPTY_DB_ID` | ID of the `Test: Empty Database` database |
248
+ | `NOTION_FIXTURE_COMMENTS_PAGE_ID` | ID of the `Test: Comments Page` page |
249
+
250
+ These are the same values from your `.env.test` file (see [section 5](#5-configure-envtest) above). Each secret maps to the corresponding environment variable that the test runner expects.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrzejchm/notion-cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Read Notion pages and databases from the terminal. Built for AI agents and developers.",
5
5
  "keywords": [
6
6
  "notion",
@@ -43,11 +43,12 @@
43
43
  "format:write": "biome format --write .",
44
44
  "check": "biome check .",
45
45
  "check:write": "biome check --write .",
46
- "ci": "npm run typecheck && npm run check"
46
+ "ci": "npm run typecheck && npm run check",
47
+ "test:integration": "vitest run --config vitest.config.integration.ts"
47
48
  },
48
49
  "dependencies": {
49
50
  "@inquirer/prompts": "^8.3.0",
50
- "@notionhq/client": "^5.11.1",
51
+ "@notionhq/client": "^5.14.0",
51
52
  "chalk": "^5.6.0",
52
53
  "commander": "^14.0.0",
53
54
  "yaml": "^2.8.0"
@@ -56,6 +57,7 @@
56
57
  "@biomejs/biome": "^2.4.4",
57
58
  "@commander-js/extra-typings": "^14.0.0",
58
59
  "@types/node": "^22",
60
+ "dotenv-cli": "^11.0.0",
59
61
  "tsup": "^8.5.0",
60
62
  "typescript": "~5.9.0",
61
63
  "vitest": "^4.0.0"