Skill: vincentdchan/solid-notion (version 0.1.1)

--- name: solid-notion description: Read, edit, and write Notion pages as Markdown using the solid-notion CLI. Use when pulling Notion content, editing pages, managing changesets, submitting edits, restoring changes, or setting up Notion API authentication. Keywords: solid-notion, Notion, Notion API, markdown, pull, edit, write, submit, restore, changeset, notion page, notion block. metadata: author: DZ Chen version: "0.1" --- # solid-notion CLI Guide `solid-notion` is a CLI for reading, editing, and writing Notion pages as Markdown with local reversible changesets. --- ## 0. Installation For normal usage (published package): ```bash npm install -g solid-notion solid-notion --version ``` For local development from source: ```bash pnpm install pnpm build ``` --- ## 1. Authentication Setup Before using any command that talks to Notion, a token must be configured. ### Getting a Notion Token Create a Notion integration to get an API token: 1. Visit [https://www.notion.so/profile/integrations/internal](https://www.notion.so/profile/integrations/internal) 2. Click "New integration" and give it a name 3. Copy the "Internal Integration Token" 4. Share your pages with this integration (via page "Share" settings) ### Check current auth status ```bash solid-notion auth status --json ``` Returns: ```json { "ok": true, "profile": "default", "config_path": "...", "token_present": true, "token_fingerprint": "a1b2c3d4", "token_valid": null } ``` If `token_present` is `false`, run init first. ### Save a token (agent-recommended method) ```bash printf "%s" "$NOTION_TOKEN" | solid-notion init --token-stdin --json ``` Returns on success: ```json { "ok": true, "action": "init", "profile": "default", "config_path": "...", "token_saved": true, "overwritten": false, "ignored_inputs": [], "dry_run": false } ``` Other token input methods (in precedence order): | Method | Flag | Notes | |--------|------|-------| | Direct | `--token <value>` | Visible in `ps` / shell history | | Stdin | `--token-stdin` | **Recommended** for agents | | JSON | `--input-json '{"token":"..."}'` | Useful for structured protocols | Additional flags: | Flag | Effect | |------|--------| | `--json` | Machine-readable JSON output only | | `--dry-run` | Preview without writing | | `--force` | Overwrite existing token | | `--profile <name>` | Use a named profile (default: `"default"`) | ### Remove a token ```bash solid-notion auth logout --json ``` --- ## 2. Command Reference ### 2.1 Browse #### List locally pulled pages ```bash solid-notion ls solid-notion ls --json ``` Lists all pages that have been pulled to `$SOLID_NOTION_HOME/`. Does **not** call the Notion API. Default output: tab-separated `<pulled_at>\t<page_id>\t<title>`, sorted newest first. JSON output (`--json`): array of objects with `page_id`, `title`, `pulled_at`, `path`. #### List all pages (remote) ```bash solid-notion pages ``` Output: tab-separated lines of `<last_edited>\t<page_id>\t<title>`. #### Search pages ```bash solid-notion search <query> ``` Output: same tab-separated format as `pages`. ### 2.2 Read #### Show a page (non-recursive, stdout) ```bash solid-notion show page <page_id_or_name> --format markdown solid-notion show page <page_id_or_name> --format json ``` `<page_id_or_name>` can be a UUID or a page title. Default format: `markdown`. #### Show a block (JSON only) ```bash solid-notion show block <block_id> --format json ``` #### Pull a page to local files ```bash solid-notion pull page <page_id_or_name> --format markdown --outdir ./output ``` Options: | Flag | Default | Description | |------|---------|-------------| | `--format <format>` | `json` | `json` or `markdown` | | `--outdir <dir>` | `$SOLID_NOTION_HOME/<page_id>` | Output directory | | `--no-local-images` | (images downloaded) | Skip downloading images | | `--no-local-videos` | (videos downloaded) | Skip downloading videos | | `--no-recursive` | (recursive) | Only first-level blocks | Outputs the path of the written file. If the page was already pulled locally, running `pull page` again fetches the latest content from Notion and overwrites local output files in that directory. #### Pull a block to local file ```bash solid-notion pull block <block_id> --outdir ./output ``` Options: `--outdir <dir>`, `--no-recursive` ### 2.3 Edit and Write All edit/write/submit commands require a **strict Notion page ID** (UUID format like [REDACTED_SECRET]). Page names are NOT accepted. #### Apply a JSON patch Pipe a JSON patch object to stdin: ```bash echo '{"ops": [...]}' | solid-notion edit <page_id> ``` Also accepts a markdown file path (`notion-page-<uuid>.md`) to resolve the page ID. **CRITICAL: Valid Patch Schema** The patch object must have exactly these keys: `ops` (array) and `notes` (string). No other keys allowed. ```json { "ops": [ { "op": "replace_block_text", "block_id": "...", "new_markdown": "...", "reason": "..." }, { "op": "append_blocks", "parent_block_id": "...", "blocks": [...], "reason": "..." }, { "op": "set_props", "page_id": "...", "set": {...}, "reason": "..." } ], "notes": "optional notes" } ``` **Three allowed operation types:** ##### `replace_block_text` Replaces rich_text content of a block. Supported block types: `paragraph`, `heading_1`, `heading_2`, `heading_3`, `bulleted_list_item`, `numbered_list_item`, `to_do`, `quote`, `callout` ```json { "op": "replace_block_text", "block_id": "block-uuid", "new_markdown": "## New heading", "reason": "Clarified section title" } ``` ##### `append_blocks` Appends new blocks under a parent block. - Most block types need `type` + `rich_text_md` - `divider` needs only `type` - `code` supports optional `language` (defaults to `plain text`) ```json { "op": "append_blocks", "parent_block_id": "parent-uuid", "blocks": [ { "type": "paragraph", "rich_text_md": "New paragraph content" }, { "type": "heading_2", "rich_text_md": "New section" } ], "reason": "Added conclusion section" } ``` Allowed block types: - `paragraph`, `heading_1`, `heading_2`, `heading_3` - `bulleted_list_item`, `numbered_list_item`, `to_do`, `quote`, `callout` - `code`, `divider` ##### `set_props` Updates page properties (title, rich_text, number, checkbox, select, multi_select, date). ```json { "op": "set_props", "page_id": "page-uuid", "set": { "Status": { "type": "select", "name": "Done" }, "Priority": { "type": "number", "value": 3 }, "Done": { "type": "checkbox", "value": true }, "Tags": { "type": "multi_select", "names": ["urgent", "important"] }, "Due Date": { "type": "date", "start": "2026-03-02", "end": null }, "Title": { "type": "title", "md": "New page title" }, "Notes": { "type": "rich_text", "md": "Some notes" } }, "reason": "Updated status to Done" } ``` **Property value types:** | Type | Shape | Example | |------|-------|---------| | `number` | `{ "type": "number", "value": 42 }` | - | | `checkbox` | `{ "type": "checkbox", "value": true }` | - | | `select` | `{ "type": "select", "name": "Option" }` | - | | `multi_select` | `{ "type": "multi_select", "names": ["A", "B"] }` | - | | `date` | `{ "type": "date", "start": "2026-03-02", "end": null }` | end optional | | `title` | `{ "type": "title", "md": "markdown" }` | - | | `rich_text` | `{ "type": "rich_text", "md": "markdown" }` | - | #### Write workspace changes back to Notion ```bash solid-notion write <page_id> ``` Reads the workspace files (`page.md`, `.original.md`), replaces the page content in Notion, creates a changeset, and cleans up the workspace. #### Submit pending edits with rollback ```bash solid-notion submit <page_id> -m "description of changes" ``` **CRITICAL: How submit works** `submit` consumes **pending edit logs** (created by previous `edit` commands) and applies them to Notion with transaction-like semantics: 1. **Phase 0: Load** — Reads all edit log files from `edit-logs/<page_id>/` that don't have `.submitted` markers 2. **Phase 1: Prepare** — Fetches before-snapshots (current block content, property values) so rollback is possible 3. **Phase 2: Apply** — Applies ops sequentially to Notion 4. **Phase 3: Finalize** — On success, saves commit to `versions/`. On failure, rolls back already-applied ops using before-snapshots. **Exit statuses:** | `status` | Meaning | `ok` | |----------|---------|------| | `pushed` | All ops applied successfully | `true` | | `nothing_to_submit` | No pending edits found | `false` | | `rolled_back` | Apply failed but rollback succeeded | `false` | | `failed_needs_reconcile` | Apply failed AND rollback partially failed | `false` | **Submit result format:** ```json { "ok": true, "commit_id": "cmt_20260302_143022", "notion_id": "3d1b-...", "status": "pushed", "applied_ops": 3, "included_edits": 2 } ``` **Best practices:** - Always run `submit` after `edit` operations to publish changes - If submit returns `rolled_back`, the page is back to original state — no manual cleanup needed - If submit returns `failed_needs_reconcile`, manual intervention may be required - The `-m` (message) flag is **required** ### 2.4 Create New Pages **CRITICAL: Use the `new` command to create Notion pages programmatically** The `new` command creates a new page under a parent (page or database) with **metadata only** (title, icon, cover, props) via stdin as JSON. After creation, the page is **automatically pulled** locally as markdown. To add content (blocks), use the existing `edit` + `submit` workflow. #### Create a new page ```bash echo '{"title":"My New Page","notes":""}' | solid-notion new --parent <parent_id> -m "Create page" --json ``` **Required flags:** - `--parent <parent_id>` — Parent page or database ID - `-m, --message <message>` — Commit message **Optional flags:** - `--database` — Parent is a database (creates as database entry) - `--json` — Output JSON only - `--dry-run` — Validate without creating **CRITICAL: Valid Payload Schema** The JSON payload via stdin must have exactly these keys: ```json { "title": "Page Title", "icon": { "type": "emoji", "emoji": "📝" }, "cover": { "type": "external", "url": "https://..." }, "props": { "Status": { "type": "select", "name": "In Progress" } }, "notes": "optional notes about this creation" } ``` **Required fields:** - `title` (string, non-empty) - `notes` (string, can be empty) **Optional fields:** - `icon` — `{ "type": "emoji", "emoji": "🎉" }` or `{ "type": "external", "url": "..." }` - `cover` — `{ "type": "external", "url": "..." }` - `props` — Page properties (database entries), same types as `set_props` plus `url`, `email`, `phone_number` **No `blocks` field.** Content is added via `edit` + `submit` after creation. **Property types for `props` (database entries):** All types from `set_props` plus: - `url`: `{ "type": "url", "value": "https://..." }` - `email`: `{ "type": "email", "value": "[REDACTED_SECRET]" }` - `phone_number`: `{ "type": "phone_number", "value": "+1234567890" }` **CRITICAL rules:** - Payload must have exactly `title`, `notes`, and optional fields — no extra keys - For database pages, `props` must match the database schema - After creation, the page is auto-pulled locally as markdown **Transaction semantics:** `new` follows the same 4-phase pipeline as `submit`: 1. **Phase 0: Parse** — Read and validate JSON from stdin 2. **Phase 1: Prepare** — Verify parent exists, derive internal ops, create commit draft 3. **Phase 2: Apply** — Create page on Notion (metadata only) 4. **Phase 3: Finalize** — Persist version record, auto-pull page. On failure, rollback (archive page). Rollback for `new`: - Archives the created page (soft-delete) **Success output (JSON mode):** ```json { "ok": true, "commit_id": "parent-id-20260304T120000Z", "action": "new", "status": "pushed", "created_page_id": "abc123-def456", "page_url": "https://notion.so/abc123def456", "title": "My New Page", "parent_id": "parent-id", "parent_type": "page", "pulled_to": "/path/to/notion-page-abc123-def456.md" } ``` **Dry run output:** ```json { "ok": true, "action": "new", "status": "dry_run", "dry_run": true, "validation": "passed", "title": "My New Page", "parent_id": "parent-id" } ``` ### 2.5 History and Restore #### View history ```bash solid-notion history <page_id> ``` Output: tab-separated `<id>\t<created_at>\t<type>`. Types: - `changeset` — Created by `write` or `restore` - `new` — Created by `new` command - `submit` — Created by `submit` command #### Restore to a previous changeset or version ```bash solid-notion restore <page_id> <changeset_or_commit_id> solid-notion restore <changeset_or_commit_id> ``` **Behavior depends on type:** - **Changeset** (from `write`/`restore`): Restores page to that changeset's state - **Version (`submit` or `new`)**: Restore to the target hash by undoing only the later `submit` versions, then writes a new changeset Hash-only lookup is supported. If the hash exists in multiple pages, CLI asks you to disambiguate with page ID. After restore-to-version, local version files after the target hash are deleted. Outputs the new changeset ID created by restore. --- ## 3. Agent Workflows ### Workflow: Read a page as Markdown ```bash solid-notion pull page <page_id_or_name> --format markdown --outdir /tmp/notion-work ``` Then read the output file path printed to stdout. ### Workflow: Full edit cycle (Markdown mode) 1. **Pull** the page to a workspace: ```bash solid-notion pull page <page_id> --format markdown --outdir ~/.local/share/solid-notion-cli/<page_id> ``` 2. **Edit** the local `page.md` file as needed. 3. **Write** changes back: ```bash solid-notion write <page_id> ``` 4. **Submit** with a message: ```bash solid-notion submit <page_id> -m "Updated section headings" ``` ### Workflow: Edit via JSON patch (Programmatic mode) Use this workflow when making precise, targeted edits without pulling full markdown. 1. **Get the target block IDs** (you may need to pull JSON first): ```bash solid-notion pull page <page_id> --format json --outdir /tmp # or inspect specific block: solid-notion show block <block_id> --format json ``` 2. **Construct a valid patch** with `ops` array and `notes`: ```json { "ops": [ { "op": "replace_block_text", "block_id": "block-uuid-here", "new_markdown": "Updated content here", "reason": "Clarified the explanation" }, { "op": "set_props", "page_id": "page-uuid-here", "set": { "Status": { "type": "select", "name": "In Progress" } }, "reason": "Moving to next phase" } ], "notes": "Batch update from review session" } ``` 3. **Apply the patch** via stdin: ```bash cat patch.json | solid-notion edit <page_id> ``` 4. **Submit** the pending edits: ```bash solid-notion submit <page_id> -m "Applied review feedback" ``` **CRITICAL rules for JSON patch editing:** - The patch must have exactly `ops` and `notes` — no extra keys - Each op must have `op`, `reason`, and type-specific fields - `block_id` in `replace_block_text` must be a valid Notion block ID - `page_id` in `set_props` must be the target page UUID - `append_blocks` creates new blocks — the `created_block_ids` are recorded in edit logs for rollback ### Workflow: Batch multiple edits before submit You can run multiple `edit` commands before a single `submit`. All pending edits are aggregated: ```bash # First edit echo '{"ops":[...],"notes":"First change"}' | solid-notion edit <page_id> # Second edit echo '{"ops":[...],"notes":"Second change"}' | solid-notion edit <page_id> # Third edit echo '{"ops":[...],"notes":"Third change"}' | solid-notion edit <page_id> # Submit all at once solid-notion submit <page_id> -m "Batch: three related updates" ``` Submit reads all edit logs from `edit-logs/<page_id>/`, marks them `.submitted` on success, and creates a single commit record. ### Workflow: Restore a change 1. **List** changesets: ```bash solid-notion history <page_id> ``` 2. **Restore** to a specific hash: ```bash solid-notion restore <page_id> <changeset_or_commit_id> # or hash-only when unique: solid-notion restore <changeset_or_commit_id> ``` --- ## 4. Storage Concepts & Layout All data is stored under `$SOLID_NOTION_HOME` (default: `~/.local/share/solid-notion-cli`). ### Storage Concepts Explained **Workspace files** (`<page_id>/`) — Created by `pull`, used by `write`: - `page.md` — The markdown you edit - `.original.md` — Snapshot before editing (for diff/comparison) - `page.meta.md` — Page metadata (properties, etc.) - **Lifecycle**: Created on `pull`, deleted on successful `write` **Edit logs** (`edit-logs/<page_id>/`) — Created by `edit`, consumed by `submit`: - JSONL files recording each patch operation with before/after snapshots - **Purpose**: Enable rollback if submit fails - **Format**: Each line is a JSON object with `op`, `block_id`/`page_id`, `reason`, and before/after values - **Lifecycle**: Created on `edit`, marked `.submitted` on successful `submit` **Versions** (`<page_id>/versions/`) — Created by `submit`: - Commit records with full operation history and snapshots - **Purpose**: Immutable history of what was published to Notion - **Format**: JSON with `commitId`, `status`, `ops`, `beforeSnapshots`, `applyState` - **Lifecycle**: Created on every `submit` attempt (even failures) **Changesets** (`changesets/<page_id>/`) — Created by `write` and `restore`: - Reversible markdown diffs for the "write -> restore" workflow - **Purpose**: Track full page content changes for `history` and `restore` - **Format**: Markdown files with YAML frontmatter containing before/after content - **Lifecycle**: Created on `write`, referenced by `history` and `restore` ### Directory Layout ``` $SOLID_NOTION_HOME/ config.json # Auth tokens (mode 0600) # Workspace (markdown editing workflow) <page_id>/ page.md # Edited markdown page.meta.md # Page metadata .original.md # Original for diff versions/ # Submit commits (JSON) # Edit logs (JSON patch workflow) edit-logs/<page_id>/ <page_id>-<timestamp>.jsonl # Pending edits (before submit) <page_id>-<timestamp>.jsonl.submitted # Marker file (after submit) # Changesets (restorable full-page versions) changesets/<page_id>/ <page_id>-<timestamp>.md # Changeset with YAML frontmatter ``` Override the base directory with `SOLID_NOTION_HOME` environment variable. --- ## 5. Global Flags | Flag | Effect | |------|--------| | `-v, --verbose` | Print debug logs to stderr | | `-V, --version` | Print version | --- ## 6. Error Handling ### init command error codes | Exit code | Error | Meaning | |-----------|-------|---------| | 2 | `missing_arguments` | No token provided | | 3 | `invalid_token` | Token is empty or invalid | | 4 | `token_already_exists` | Token exists, use `--force` | | 5 | `write_failed` | Could not write config file | ### General errors All other commands exit `1` on failure and print diagnostics to stderr including error name, message, metadata (status, code, errno, syscall, hostname), cause chain, and stack trace. --- ## 7. Anti-Patterns ### Authentication - **Do NOT** use `--token <value>` in automated flows. Use `--token-stdin` to avoid leaking tokens in process lists. - **Do NOT** skip `solid-notion auth status --json` before running commands. If there is no token, all Notion API calls will fail. ### Page IDs - **Do NOT** pass page names to `edit`, `write`, `submit`, `history`, or `restore`. These commands require strict page UUIDs. ### Output parsing - **Do NOT** forget `--json` when you need to parse command output programmatically. ### Submit command - **Do NOT** call `submit` without `-m`. The message flag is **required**. - **Do NOT** assume `submit` takes stdin — it reads from local `edit-logs/`, not from stdin. - **Do NOT** ignore `status: "rolled_back"` — this means Notion writes failed but were undone. - **Do NOT** ignore `status: "failed_needs_reconcile"` — this means both apply AND rollback failed. ### Edit patch schema - **Do NOT** include extra keys in the patch object (only `ops` and `notes` allowed). - **Do NOT** forget the `reason` field in each op — it is required. - **Do NOT** use unsupported block types in `replace_block_text` or `append_blocks`. - **Do NOT** use `set_props` on properties with unsupported types (only: number, checkbox, select, multi_select, date, title, rich_text). - **Do NOT** pass Notion block objects as `new_markdown` — pass simple Markdown text (e.g., "## Heading" or "Paragraph with **bold**").


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)