npm package: @pan-enso/nctl
Description: Headless Notion CLI for Markdown-driven page and database operations.
Latest version: 0.4.2

<div align="center">

# nctl

**A headless Notion CLI from Japan**

[![npm version](https://img.shields.io/npm/v/@pan-enso/nctl)](https://www.npmjs.com/package/@pan-enso/nctl)
[![license](https://img.shields.io/github/license/pan-enso/nctl)](./LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-Bun-blue)](https://bun.sh)

</div>

`nctl` is a CLI for writing to Notion from CI/CD pipelines, cron jobs, and shell scripts — without OAuth, without a browser, without interactive login.

It uses only **integration tokens**, accepts **Markdown files** as input, and is safe to run headlessly at any time.

```bash
$ nctl page upsert --md report.md
✓ updated  abc123  sha256:e3b0c44...
```

---

## Table of Contents

- [Install](#install)
- [Authentication](#authentication)
- [Commands](#commands)
  - [page upsert](#nctl-page-upsert)
  - [db query](#nctl-db-query)
  - [block marker-replace](#nctl-block-marker-replace)
  - [api](#nctl-api)
- [Real-world Examples](#real-world-examples)
- [Markdown Format](#markdown-format)
- [Exit Codes](#exit-codes)
- [Contributing](#contributing)

---

## Install

```bash
# npm
npm install -g @pan-enso/nctl

# bun
bun add -g @pan-enso/nctl
```

Requires [Bun](https://bun.sh) at runtime.

---

## Authentication

nctl resolves your Notion integration token in this order:

| Method | Example |
|--------|---------|
| **Environment variable** (default) | `NOTION_API_KEY=ntn_xxx nctl ...` |
| **Google Cloud Secret Manager** | `nctl --token-from gcloud:NOTION_TOKEN ...` |
| **Any shell command** | `nctl --token-from "command:vault read secret/notion" ...` |

> [!TIP]
> For CI/cron environments, the `gcloud:` method keeps tokens out of environment files entirely.

> [!WARNING]
> `command:` executes an arbitrary shell command on the host. Use it only with trusted commands and avoid embedding secrets in shell history or process lists.

Security note: The `command:` token source passes the provided string directly to `bash -lc`, enabling arbitrary shell execution. Only use trusted commands here.

```bash
# Simplest: env var
NOTION_API_KEY=ntn_xxx nctl db query DB_ID

# Google Cloud Secret Manager
nctl --token-from gcloud:NOTION_TOKEN page upsert --md report.md

# Custom command (any secret manager)
nctl --token-from "command:gcloud secrets versions access latest --secret=NOTION_TOKEN" \
  page upsert --md report.md
```

---

## Commands

### `nctl page upsert`

Create or update a Notion page from a Markdown file. Uses content hashing — if nothing changed, the page is not touched (**no-op**).

```bash
nctl page upsert --md report.md
nctl page upsert --md report.md --dry-run   # preview without writing
```

The Markdown file must have a YAML frontmatter block:

```markdown
---
parent_page_id: PAGE_ID    # for pages under another page
# or:
database_id: DB_ID         # for database entries

title: Weekly Report
properties:
  Status: Done
  Tags:
    - engineering
    - weekly
---

# Heading

Body content goes here.
```

**Output:**

```
✓ created  abc123  sha256:e3b0c44...
✓ updated  abc123  sha256:e3b0c44...
- noop     abc123  sha256:e3b0c44...   ← content unchanged, skipped
```

---

### `nctl db query`

Query a Notion database and output results in various formats.

```bash
# Human-readable table
nctl db query DB_ID --format table

# JSON Lines (for piping)
nctl db query DB_ID --format jsonl --limit 10

# Page IDs only
nctl db query DB_ID --format ids

# With filter (Notion filter JSON)
nctl db query DB_ID \
  --filter '{"property":"Status","status":{"equals":"Done"}}' \
  --format table
```

| Flag | Description |
|------|-------------|
| `--format` | `table` \| `jsonl` \| `ids` \| `json` |
| `--filter` | Notion filter object (JSON string) |
| `--limit` | Max results (default: all) |

---

### `nctl block marker-replace`

Replace a named section inside an existing Notion page — without touching anything outside the markers. Safe to run repeatedly (**idempotent**).

**Step 1.** Add markers to your Notion page (as code blocks or paragraph blocks containing):

```
<!-- nctl:marker:morning-patrol -->
... this section will be replaced ...
<!-- /nctl:marker:morning-patrol -->
```

**Step 2.** Run:

```bash
nctl block marker-replace PAGE_ID --marker morning-patrol --md update.md
nctl block marker-replace PAGE_ID --marker morning-patrol --md update.md --dry-run
```

---

### `nctl api`

Low-level escape hatch for direct Notion REST API calls.

```bash
# GET
nctl api get /users/me

# POST with body
nctl api post /search --body '{"query":"Roadmap"}'

# PATCH
nctl api patch /blocks/BLOCK_ID \
  --body '{"paragraph":{"rich_text":[{"text":{"content":"Updated"}}]}}'
```

---

## Real-world Examples

### GitHub Actions: publish a daily report to Notion

```yaml
name: Daily Report
on:
  schedule:
    - cron: "0 9 * * *"

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v1
      - run: npm install -g @pan-enso/nctl
      - run: nctl page upsert --md reports/daily.md
        env:
          NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
```

### cron + Google Cloud Secret Manager

```bash
# /etc/cron.d/notion-sync
0 6 * * * user nctl --token-from gcloud:NOTION_TOKEN page upsert --md /opt/reports/morning.md
```

### Scripting with `db query`

```bash
# Get all Done items and process them
nctl db query "$DB_ID" --format jsonl --filter '{"property":"Status","status":{"equals":"Done"}}' \
  | jq -r '.id' \
  | while read id; do
      echo "Processing $id..."
    done
```

---

## Markdown Format

nctl parses standard Markdown with YAML frontmatter.

**Supported frontmatter fields:**

| Field | Type | Description |
|-------|------|-------------|
| `title` | string | Page title (required) |
| `parent_page_id` | string | Parent page UUID |
| `database_id` | string | Target database UUID |
| `properties` | object | Database properties (key: value) |

**Supported block types:**

| Markdown | Notion Block |
|----------|-------------|
| `# H1` / `## H2` / `### H3` | Heading 1/2/3 |
| Paragraph text | Paragraph |
| `> quote` | Quote |
| `- item` | Bulleted list item |
| `1. item` | Ordered list item (not supported) |
| ` ```lang ``` ` | Code block |
| `---` | Divider |

---

## Exit Codes

| Code | Name | Meaning |
|------|------|---------|
| `0` | success | OK |
| `1` | api-error | Notion API returned an error |
| `2` | auth-error | Token missing or invalid |
| `3` | validation-error | Bad input (missing frontmatter, invalid args) |
| `4` | not-found | Page or block not found |

---

## Contributing

Issues and PRs are welcome at [github.com/pan-enso/nctl](https://github.com/pan-enso/nctl).

Please open an issue before submitting large changes.

---

## Author

Built by [@AgentGymLeader](https://x.com/AgentGymLeader) · [pan-enso](https://github.com/pan-enso)

Inspired by [@sakasegawa/ncli](https://github.com/sakasegawa/ncli).

---

## License

MIT