AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
425
archive/inactive-skills/notion-sync/SKILL.md
Normal file
425
archive/inactive-skills/notion-sync/SKILL.md
Normal file
@@ -0,0 +1,425 @@
|
||||
---
|
||||
name: notion-sync
|
||||
description: Bi-directional sync and management for Notion pages and databases. Use when working with Notion workspaces for collaborative editing, research tracking, project management, or when you need to sync markdown files to/from Notion pages or monitor Notion pages for changes.
|
||||
---
|
||||
|
||||
# Notion Sync
|
||||
|
||||
Bi-directional sync between markdown files and Notion pages, plus database management utilities for research tracking and project management.
|
||||
|
||||
## Setup
|
||||
|
||||
### API Key Configuration
|
||||
|
||||
Store the Notion API key in macOS Keychain:
|
||||
|
||||
```bash
|
||||
# Add to keychain (will prompt for the secret)
|
||||
security add-generic-password -a "$USER" -s "openclaw.notion_api_key" -w
|
||||
|
||||
# Add to environment loader (e.g., ~/.openclaw/bin/openclaw-env.sh)
|
||||
export NOTION_API_KEY="$(security find-generic-password -a "$USER" -s "openclaw.notion_api_key" -w)"
|
||||
|
||||
# Restart gateway to load the key
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
### Integration Setup
|
||||
|
||||
1. Go to https://www.notion.so/my-integrations
|
||||
2. Create a new integration or use an existing one
|
||||
3. Copy the "Internal Integration Token" (starts with `secret_`)
|
||||
4. Store it in Keychain as shown above
|
||||
5. Share your Notion pages/databases with the integration:
|
||||
- Open the page/database in Notion
|
||||
- Click "Share" → "Invite"
|
||||
- Select your integration
|
||||
|
||||
## Core Operations
|
||||
|
||||
### 1. Search Pages and Databases
|
||||
|
||||
Search across your Notion workspace by title or content.
|
||||
|
||||
```bash
|
||||
node scripts/search-notion.js "<query>" [--filter page|database] [--limit 10]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Search for newsletter-related pages
|
||||
node scripts/search-notion.js "newsletter"
|
||||
|
||||
# Find only databases
|
||||
node scripts/search-notion.js "research" --filter database
|
||||
|
||||
# Limit results
|
||||
node scripts/search-notion.js "AI" --limit 5
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "page-id-here",
|
||||
"object": "page",
|
||||
"title": "Newsletter Draft",
|
||||
"url": "https://notion.so/...",
|
||||
"lastEdited": "2026-02-01T09:00:00.000Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 2. Query Databases with Filters
|
||||
|
||||
Query database contents with advanced filters and sorting.
|
||||
|
||||
```bash
|
||||
node scripts/query-database.js <database-id> [--filter <json>] [--sort <json>] [--limit 10]
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Get all items
|
||||
node scripts/query-database.js xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
# Filter by Status = "Complete"
|
||||
node scripts/query-database.js <db-id> \
|
||||
--filter '{"property": "Status", "select": {"equals": "Complete"}}'
|
||||
|
||||
# Filter by Tags containing "AI"
|
||||
node scripts/query-database.js <db-id> \
|
||||
--filter '{"property": "Tags", "multi_select": {"contains": "AI"}}'
|
||||
|
||||
# Sort by Date descending
|
||||
node scripts/query-database.js <db-id> \
|
||||
--sort '[{"property": "Date", "direction": "descending"}]'
|
||||
|
||||
# Combine filter + sort
|
||||
node scripts/query-database.js <db-id> \
|
||||
--filter '{"property": "Status", "select": {"equals": "Complete"}}' \
|
||||
--sort '[{"property": "Date", "direction": "descending"}]'
|
||||
```
|
||||
|
||||
**Common filter patterns:**
|
||||
- Select equals: `{"property": "Status", "select": {"equals": "Done"}}`
|
||||
- Multi-select contains: `{"property": "Tags", "multi_select": {"contains": "AI"}}`
|
||||
- Date after: `{"property": "Date", "date": {"after": "2024-01-01"}}`
|
||||
- Checkbox is true: `{"property": "Published", "checkbox": {"equals": true}}`
|
||||
- Number greater than: `{"property": "Count", "number": {"greater_than": 100}}`
|
||||
|
||||
### 3. Update Page Properties
|
||||
|
||||
Update properties for database pages (status, tags, dates, etc.).
|
||||
|
||||
```bash
|
||||
node scripts/update-page-properties.js <page-id> <property-name> <value> [--type <type>]
|
||||
```
|
||||
|
||||
**Supported types:** select, multi_select, checkbox, number, url, email, date, rich_text
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Set status
|
||||
node scripts/update-page-properties.js <page-id> Status "Complete" --type select
|
||||
|
||||
# Add multiple tags
|
||||
node scripts/update-page-properties.js <page-id> Tags "AI,Leadership,Research" --type multi_select
|
||||
|
||||
# Set checkbox
|
||||
node scripts/update-page-properties.js <page-id> Published true --type checkbox
|
||||
|
||||
# Set date
|
||||
node scripts/update-page-properties.js <page-id> "Publish Date" "2024-02-01" --type date
|
||||
|
||||
# Set URL
|
||||
node scripts/update-page-properties.js <page-id> "Source URL" "https://example.com" --type url
|
||||
|
||||
# Set number
|
||||
node scripts/update-page-properties.js <page-id> "Word Count" 1200 --type number
|
||||
```
|
||||
|
||||
### 4. Markdown → Notion Sync
|
||||
|
||||
Push markdown content to Notion with full formatting support.
|
||||
|
||||
```bash
|
||||
node scripts/md-to-notion.js \
|
||||
"<markdown-file-path>" \
|
||||
"<notion-parent-page-id>" \
|
||||
"<page-title>"
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
node scripts/md-to-notion.js \
|
||||
"projects/newsletter-draft.md" \
|
||||
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
|
||||
"Newsletter Draft - Feb 2026"
|
||||
```
|
||||
|
||||
**Supported formatting:**
|
||||
- Headings (H1-H3)
|
||||
- Bold/italic text
|
||||
- Links
|
||||
- Bullet lists
|
||||
- Code blocks with syntax highlighting
|
||||
- Horizontal dividers
|
||||
- Paragraphs
|
||||
|
||||
**Features:**
|
||||
- Batched uploads (100 blocks per request)
|
||||
- Automatic rate limiting (350ms between batches)
|
||||
- Returns Notion page URL and ID
|
||||
|
||||
**Output:**
|
||||
```
|
||||
Parsed 294 blocks from markdown
|
||||
✓ Created page: https://www.notion.so/[title-and-id]
|
||||
✓ Appended 100 blocks (100-200)
|
||||
✓ Appended 94 blocks (200-294)
|
||||
|
||||
✅ Successfully created Notion page!
|
||||
```
|
||||
|
||||
### 5. Notion → Markdown Sync
|
||||
|
||||
Pull Notion page content and convert to markdown.
|
||||
|
||||
```bash
|
||||
node scripts/notion-to-md.js <page-id> [output-file]
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
node scripts/notion-to-md.js \
|
||||
"abc123-example-page-id-456def" \
|
||||
"newsletter-updated.md"
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Converts Notion blocks to markdown
|
||||
- Preserves formatting (headings, lists, code, quotes)
|
||||
- Optional file output (writes to file or stdout)
|
||||
|
||||
### 6. Change Detection & Monitoring
|
||||
|
||||
Monitor Notion pages for edits and compare with local markdown files.
|
||||
|
||||
```bash
|
||||
node scripts/watch-notion.js "<page-id>" "<local-markdown-path>"
|
||||
|
||||
# Or use environment variables
|
||||
export NOTION_WATCH_PAGE_ID="<your-page-id>"
|
||||
export NOTION_WATCH_LOCAL_PATH="/path/to/your/draft.md"
|
||||
node scripts/watch-notion.js
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
node scripts/watch-notion.js \
|
||||
"abc123-example-page-id-456def" \
|
||||
"projects/newsletter-draft.md"
|
||||
```
|
||||
|
||||
**State tracking:** Maintains state in `memory/notion-watch-state.json`:
|
||||
```json
|
||||
{
|
||||
"pages": {
|
||||
"<page-id>": {
|
||||
"lastEditedTime": "2026-01-30T08:57:00.000Z",
|
||||
"lastChecked": "2026-01-31T19:41:54.000Z",
|
||||
"title": "Your Page Title"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"pageId": "<page-id>",
|
||||
"title": "Your Page Title",
|
||||
"lastEditedTime": "2026-01-30T08:57:00.000Z",
|
||||
"hasChanges": false,
|
||||
"localPath": "/path/to/your-draft.md",
|
||||
"actions": ["✓ No changes since last check"]
|
||||
}
|
||||
```
|
||||
|
||||
**Integration with heartbeat:** Add to `HEARTBEAT.md` for automated monitoring:
|
||||
```markdown
|
||||
## Notion Page Monitoring (Every 2-3 hours during work hours)
|
||||
|
||||
Check if enough time has passed since last Notion check.
|
||||
|
||||
If >2 hours since last check AND during work hours (9 AM - 9 PM):
|
||||
1. Run: `node scripts/watch-notion.js "<your-page-id>" "<your-local-path>"`
|
||||
2. If `hasChanges: true` → notify user via message tool
|
||||
3. Update check timestamp
|
||||
```
|
||||
|
||||
### 7. Database Management
|
||||
|
||||
#### Add Markdown Content to Database
|
||||
|
||||
Add a markdown file as a new page in any Notion database.
|
||||
|
||||
```bash
|
||||
node scripts/add-to-database.js <database-id> "<page-title>" <markdown-file-path>
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Add research output
|
||||
node scripts/add-to-database.js \
|
||||
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
|
||||
"Research Report - Feb 2026" \
|
||||
projects/research-insights.md
|
||||
|
||||
# Add project notes
|
||||
node scripts/add-to-database.js \
|
||||
<project-db-id> \
|
||||
"Sprint Retrospective" \
|
||||
docs/retro-2026-02.md
|
||||
|
||||
# Add meeting notes
|
||||
node scripts/add-to-database.js \
|
||||
<notes-db-id> \
|
||||
"Weekly Team Sync" \
|
||||
notes/sync-2026-02-06.md
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Creates database page with title property
|
||||
- Converts markdown to Notion blocks (headings, paragraphs, dividers)
|
||||
- Handles large files with batched uploads
|
||||
- Returns page URL for immediate access
|
||||
|
||||
**Note:** Additional properties (Type, Tags, Status, etc.) must be set manually in Notion UI after creation.
|
||||
|
||||
#### Inspect Database Schema
|
||||
|
||||
```bash
|
||||
node scripts/get-database-schema.js <database-id>
|
||||
```
|
||||
|
||||
**Example output:**
|
||||
```json
|
||||
{
|
||||
"object": "database",
|
||||
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"title": [{"plain_text": "Ax Resources"}],
|
||||
"properties": {
|
||||
"Name": {"type": "title"},
|
||||
"Type": {"type": "select"},
|
||||
"Tags": {"type": "multi_select"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
- Setting up new database integrations
|
||||
- Debugging property names/types
|
||||
- Understanding database structure
|
||||
|
||||
#### Archive Pages
|
||||
|
||||
```bash
|
||||
node scripts/delete-notion-page.js <page-id>
|
||||
```
|
||||
|
||||
**Note:** This archives the page (sets `archived: true`), not permanent deletion.
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Collaborative Editing Workflow
|
||||
|
||||
1. **Push local draft to Notion:**
|
||||
```bash
|
||||
node scripts/md-to-notion.js draft.md <parent-id> "Draft Title"
|
||||
```
|
||||
|
||||
2. **User edits in Notion** (anywhere, any device)
|
||||
|
||||
3. **Monitor for changes:**
|
||||
```bash
|
||||
node scripts/watch-notion.js
|
||||
# Returns hasChanges: true when edited
|
||||
```
|
||||
|
||||
4. **Pull updates back:**
|
||||
```bash
|
||||
node scripts/notion-to-md.js <page-id> draft-updated.md
|
||||
```
|
||||
|
||||
5. **Repeat as needed** (update same page, don't create v2/v3/etc.)
|
||||
|
||||
### Research Output Tracking
|
||||
|
||||
1. **Generate research locally** (e.g., via sub-agent)
|
||||
|
||||
2. **Sync to Notion database:**
|
||||
```bash
|
||||
node scripts/add-research-to-db.js
|
||||
```
|
||||
|
||||
3. **User adds metadata in Notion UI** (Type, Tags, Status properties)
|
||||
|
||||
4. **Access from anywhere** via Notion web/mobile
|
||||
|
||||
### Page ID Extraction
|
||||
|
||||
From Notion URL: `https://notion.so/Page-Title-abc123-example-page-id-456def`
|
||||
|
||||
Extract: `abc123-example-page-id-456def` (last part after title)
|
||||
|
||||
Or use the 32-char format: `abc123examplepageid456def` (hyphens optional)
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Property updates:** Database properties (Type, Tags, Status) must be added manually in Notion UI after page creation. API property updates can be temperamental with inline databases.
|
||||
- **Block limits:** Very large markdown files (>1000 blocks) may take several minutes to sync due to rate limiting.
|
||||
- **Formatting:** Some complex markdown (tables, nested lists >3 levels) may not convert perfectly.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Could not find page" error:**
|
||||
- Ensure page/database is shared with your integration
|
||||
- Check page ID format (32 chars, alphanumeric + hyphens)
|
||||
|
||||
**"Module not found" error:**
|
||||
- Scripts use built-in Node.js https module (no npm install needed)
|
||||
- Ensure running from correct directory with `cd ~/clawd`
|
||||
|
||||
**Rate limiting:**
|
||||
- Notion API has rate limits (~3 requests/second)
|
||||
- Scripts handle this automatically with 350ms delays between batches
|
||||
|
||||
## Resources
|
||||
|
||||
### scripts/
|
||||
|
||||
**Core Sync:**
|
||||
- **md-to-notion.js** - Markdown → Notion sync with full formatting
|
||||
- **notion-to-md.js** - Notion → Markdown conversion
|
||||
- **watch-notion.js** - Change detection and monitoring
|
||||
|
||||
**Search & Query:**
|
||||
- **search-notion.js** - Search pages and databases by query
|
||||
- **query-database.js** - Query databases with filters and sorting
|
||||
- **update-page-properties.js** - Update database page properties
|
||||
|
||||
**Database Management:**
|
||||
- **add-to-database.js** - Add markdown files as database pages
|
||||
- **get-database-schema.js** - Inspect database structure
|
||||
- **delete-notion-page.js** - Archive pages
|
||||
|
||||
**Utilities:**
|
||||
- **notion-utils.js** - Shared utilities (error handling, property formatting, API requests)
|
||||
|
||||
All scripts use only built-in Node.js modules (https, fs) - no external dependencies required.
|
||||
|
||||
### references/
|
||||
|
||||
- **database-patterns.md** - Common database schemas and property patterns
|
||||
6
archive/inactive-skills/notion-sync/_meta.json
Normal file
6
archive/inactive-skills/notion-sync/_meta.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7ey4tpcw1w2ermtp9a4hs19x80bmmg",
|
||||
"slug": "notion-sync",
|
||||
"version": "1.0.3",
|
||||
"publishedAt": 1770493761411
|
||||
}
|
||||
258
archive/inactive-skills/notion-sync/references/API-REFERENCE.md
Normal file
258
archive/inactive-skills/notion-sync/references/API-REFERENCE.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Notion Sync API Reference
|
||||
|
||||
Detailed technical reference for all Notion sync scripts and utilities.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### Required Environment Variable
|
||||
|
||||
```bash
|
||||
export NOTION_API_KEY="secret_..."
|
||||
```
|
||||
|
||||
### Keychain Storage (macOS)
|
||||
|
||||
```bash
|
||||
# Store in keychain
|
||||
security add-generic-password -a "$USER" -s "openclaw.notion_api_key" -w
|
||||
|
||||
# Load from keychain
|
||||
export NOTION_API_KEY="$(security find-generic-password -a "$USER" -s "openclaw.notion_api_key" -w)"
|
||||
```
|
||||
|
||||
## Scripts Reference
|
||||
|
||||
### search-notion.js
|
||||
|
||||
Search pages and databases by title or content.
|
||||
|
||||
**Signature:**
|
||||
```bash
|
||||
node scripts/search-notion.js "<query>" [--filter page|database] [--limit 10]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `query` (required): Search term
|
||||
- `--filter`: Restrict to `page` or `database`
|
||||
- `--limit`: Max results (default: 10)
|
||||
|
||||
**Returns:** JSON array of matching pages/databases with id, title, url, lastEdited
|
||||
|
||||
### query-database.js
|
||||
|
||||
Query database contents with advanced filters and sorting.
|
||||
|
||||
**Signature:**
|
||||
```bash
|
||||
node scripts/query-database.js <database-id> [--filter <json>] [--sort <json>] [--limit 10]
|
||||
```
|
||||
|
||||
**Filter Patterns:**
|
||||
|
||||
| Type | Example |
|
||||
|------|---------|
|
||||
| Select equals | `{"property": "Status", "select": {"equals": "Done"}}` |
|
||||
| Multi-select contains | `{"property": "Tags", "multi_select": {"contains": "AI"}}` |
|
||||
| Date after | `{"property": "Date", "date": {"after": "2024-01-01"}}` |
|
||||
| Checkbox | `{"property": "Published", "checkbox": {"equals": true}}` |
|
||||
| Number | `{"property": "Count", "number": {"greater_than": 100}}` |
|
||||
|
||||
**Sort Format:**
|
||||
```json
|
||||
[{"property": "Date", "direction": "descending"}]
|
||||
```
|
||||
|
||||
### update-page-properties.js
|
||||
|
||||
Update database page properties.
|
||||
|
||||
**Signature:**
|
||||
```bash
|
||||
node scripts/update-page-properties.js <page-id> <property-name> <value> [--type <type>]
|
||||
```
|
||||
|
||||
**Property Types:**
|
||||
- `select`: Single selection (e.g., Status)
|
||||
- `multi_select`: Multiple tags (comma-separated)
|
||||
- `checkbox`: Boolean (true/false)
|
||||
- `number`: Numeric value
|
||||
- `url`: URL string
|
||||
- `email`: Email address
|
||||
- `date`: ISO date (YYYY-MM-DD)
|
||||
- `rich_text`: Plain text
|
||||
|
||||
### md-to-notion.js
|
||||
|
||||
Convert markdown to Notion page.
|
||||
|
||||
**Signature:**
|
||||
```bash
|
||||
node scripts/md-to-notion.js "<markdown-file>" "<parent-page-id>" "<title>"
|
||||
```
|
||||
|
||||
**Supported Markdown:**
|
||||
- Headings: `#`, `##`, `###`
|
||||
- Bold: `**text**`
|
||||
- Italic: `*text*`
|
||||
- Links: `[text](url)`
|
||||
- Lists: `- item`
|
||||
- Code: ` ```lang ... ``` `
|
||||
- Dividers: `---`
|
||||
|
||||
**Output:** Notion page URL and ID
|
||||
|
||||
**Rate Limiting:** 350ms between batch uploads (100 blocks per batch)
|
||||
|
||||
### notion-to-md.js
|
||||
|
||||
Convert Notion page to markdown.
|
||||
|
||||
**Signature:**
|
||||
```bash
|
||||
node scripts/notion-to-md.js <page-id> [output-file]
|
||||
```
|
||||
|
||||
**Output:** Writes markdown to file or stdout
|
||||
|
||||
### watch-notion.js
|
||||
|
||||
Monitor page for changes.
|
||||
|
||||
**Signature:**
|
||||
```bash
|
||||
node scripts/watch-notion.js
|
||||
```
|
||||
|
||||
**State File:** `memory/notion-watch-state.json`
|
||||
|
||||
**State Schema:**
|
||||
```json
|
||||
{
|
||||
"pages": {
|
||||
"<page-id>": {
|
||||
"lastEditedTime": "ISO timestamp",
|
||||
"lastChecked": "ISO timestamp",
|
||||
"title": "Page Title"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Returns:** JSON with `hasChanges`, `localDiffers`, `actions`
|
||||
|
||||
### get-database-schema.js
|
||||
|
||||
Inspect database structure.
|
||||
|
||||
**Signature:**
|
||||
```bash
|
||||
node scripts/get-database-schema.js <database-id>
|
||||
```
|
||||
|
||||
**Returns:** JSON with database properties and their types
|
||||
|
||||
### delete-notion-page.js
|
||||
|
||||
Archive page (soft delete).
|
||||
|
||||
**Signature:**
|
||||
```bash
|
||||
node scripts/delete-notion-page.js <page-id>
|
||||
```
|
||||
|
||||
**Note:** Sets `archived: true`, doesn't permanently delete
|
||||
|
||||
## Notion API Utilities
|
||||
|
||||
### notion-utils.js
|
||||
|
||||
Shared utilities for all scripts.
|
||||
|
||||
**Exports:**
|
||||
|
||||
#### `notionRequest(path, method, body)`
|
||||
Makes authenticated API requests to Notion.
|
||||
|
||||
**Parameters:**
|
||||
- `path`: API endpoint (e.g., `/v1/pages`)
|
||||
- `method`: HTTP method (GET, POST, PATCH, DELETE)
|
||||
- `body`: Optional request body (object)
|
||||
|
||||
**Returns:** Promise resolving to response JSON
|
||||
|
||||
**Error Handling:** Throws with detailed Notion API error messages
|
||||
|
||||
#### `formatProperty(type, value)`
|
||||
Formats property values for Notion API.
|
||||
|
||||
**Supported Types:**
|
||||
- select, multi_select, checkbox, number, url, email, date, rich_text
|
||||
|
||||
**Returns:** Notion API property object
|
||||
|
||||
#### `extractPageId(input)`
|
||||
Extracts clean page ID from URL or ID string.
|
||||
|
||||
**Input Formats:**
|
||||
- URL: `https://notion.so/Title-abc123...`
|
||||
- With hyphens: `abc123-example-page-id-456def`
|
||||
- Without hyphens: `abc123examplepageid456def`
|
||||
|
||||
**Returns:** 32-char UUID with hyphens
|
||||
|
||||
## Rate Limits
|
||||
|
||||
Notion API limits:
|
||||
- ~3 requests/second
|
||||
- Scripts implement 350ms delays between batches
|
||||
- Large operations (>100 blocks) auto-batch with delays
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Authentication Errors
|
||||
|
||||
**Error:** `"Could not find page"`
|
||||
|
||||
**Solutions:**
|
||||
1. Verify page/database is shared with your integration
|
||||
2. Check page ID format (32 chars, no extra characters)
|
||||
3. Confirm `NOTION_API_KEY` is set and valid
|
||||
|
||||
### Property Update Failures
|
||||
|
||||
**Issue:** Property updates don't persist
|
||||
|
||||
**Cause:** Database is inline (embedded in a page) rather than standalone
|
||||
|
||||
**Solution:** Create standalone database or update properties manually in Notion UI
|
||||
|
||||
### Module Not Found
|
||||
|
||||
**Error:** `Cannot find module 'https'`
|
||||
|
||||
**Solution:** Ensure using Node.js v18+ (built-in modules)
|
||||
|
||||
## Page ID Extraction
|
||||
|
||||
From Notion URL:
|
||||
```
|
||||
https://notion.so/Page-Title-abc123-example-page-id-456def
|
||||
└─────────── Extract this part ──────────┘
|
||||
```
|
||||
|
||||
Both formats work:
|
||||
- With hyphens: `abc123-example-page-id-456def`
|
||||
- Without hyphens: `abc123examplepageid456def`
|
||||
|
||||
## Integration Permissions
|
||||
|
||||
Required Notion integration capabilities:
|
||||
- ✓ Read content
|
||||
- ✓ Update content
|
||||
- ✓ Insert content
|
||||
- ✓ Read comments (optional)
|
||||
|
||||
Share settings:
|
||||
- Must explicitly share each page/database with the integration
|
||||
- Child pages inherit parent permissions
|
||||
- Databases require explicit sharing even if parent is shared
|
||||
185
archive/inactive-skills/notion-sync/scripts/add-to-database.js
Normal file
185
archive/inactive-skills/notion-sync/scripts/add-to-database.js
Normal file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Add markdown file as a page in a Notion database
|
||||
* Usage: node add-to-database.js <database-id> <page-title> <markdown-file-path>
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
|
||||
const NOTION_API_KEY = process.env.NOTION_API_KEY;
|
||||
const NOTION_VERSION = '2025-09-03';
|
||||
|
||||
function notionRequest(path, method, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (data) req.write(JSON.stringify(data));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function parseRichText(text) {
|
||||
const richText = [];
|
||||
const maxLength = 2000;
|
||||
|
||||
if (text.length <= maxLength) {
|
||||
richText.push({
|
||||
type: 'text',
|
||||
text: { content: text }
|
||||
});
|
||||
} else {
|
||||
richText.push({
|
||||
type: 'text',
|
||||
text: { content: text.substring(0, maxLength) }
|
||||
});
|
||||
}
|
||||
|
||||
return richText;
|
||||
}
|
||||
|
||||
function parseMarkdown(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
const blocks = [];
|
||||
let currentParagraph = [];
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (currentParagraph.length > 0) {
|
||||
const text = currentParagraph.join('\n').trim();
|
||||
if (text && text.length > 0) {
|
||||
blocks.push({
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
rich_text: parseRichText(text)
|
||||
}
|
||||
});
|
||||
}
|
||||
currentParagraph = [];
|
||||
}
|
||||
};
|
||||
|
||||
for (let line of lines) {
|
||||
if (line.startsWith('### ')) {
|
||||
flushParagraph();
|
||||
blocks.push({
|
||||
type: 'heading_3',
|
||||
heading_3: { rich_text: parseRichText(line.substring(4)) }
|
||||
});
|
||||
} else if (line.startsWith('## ')) {
|
||||
flushParagraph();
|
||||
blocks.push({
|
||||
type: 'heading_2',
|
||||
heading_2: { rich_text: parseRichText(line.substring(3)) }
|
||||
});
|
||||
} else if (line.startsWith('# ')) {
|
||||
flushParagraph();
|
||||
blocks.push({
|
||||
type: 'heading_1',
|
||||
heading_1: { rich_text: parseRichText(line.substring(2)) }
|
||||
});
|
||||
} else if (line.startsWith('---')) {
|
||||
flushParagraph();
|
||||
blocks.push({ type: 'divider', divider: {} });
|
||||
} else if (line.trim() === '') {
|
||||
flushParagraph();
|
||||
} else {
|
||||
currentParagraph.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
flushParagraph();
|
||||
return blocks;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
// Parse arguments
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length < 3) {
|
||||
console.error('Usage: node add-to-database.js <database-id> <page-title> <markdown-file-path>');
|
||||
console.error('\nExample:');
|
||||
console.error(' node add-to-database.js abc123-db-id "Research Report" research.md');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [dbId, title, mdPath] = args;
|
||||
|
||||
// Validate inputs
|
||||
if (!fs.existsSync(mdPath)) {
|
||||
console.error(`Error: File not found: ${mdPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Adding page to database...');
|
||||
console.log(` Database: ${dbId}`);
|
||||
console.log(` Title: ${title}`);
|
||||
console.log(` Source: ${mdPath}\n`);
|
||||
|
||||
// Create database page
|
||||
const pageData = {
|
||||
parent: {
|
||||
type: 'database_id',
|
||||
database_id: dbId
|
||||
},
|
||||
properties: {
|
||||
'Name': {
|
||||
title: [{ text: { content: title } }]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
console.log('Creating database entry...');
|
||||
const page = await notionRequest('/v1/pages', 'POST', pageData);
|
||||
console.log(`✓ Page created: ${page.id}`);
|
||||
console.log(` URL: https://notion.so/${page.id.replace(/-/g, '')}`);
|
||||
|
||||
// Read and parse markdown
|
||||
const markdown = fs.readFileSync(mdPath, 'utf8');
|
||||
const blocks = parseMarkdown(markdown);
|
||||
console.log(`\nParsed ${blocks.length} blocks from markdown`);
|
||||
|
||||
// Add content in batches
|
||||
const batchSize = 100;
|
||||
for (let i = 0; i < blocks.length; i += batchSize) {
|
||||
const batch = blocks.slice(i, i + batchSize);
|
||||
console.log(`Adding blocks ${i + 1}-${Math.min(i + batchSize, blocks.length)}...`);
|
||||
|
||||
await notionRequest(`/v1/blocks/${page.id}/children`, 'PATCH', {
|
||||
children: batch
|
||||
});
|
||||
|
||||
if (i + batchSize < blocks.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 350));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Successfully added to database!`);
|
||||
console.log(`📄 URL: https://notion.so/${page.id.replace(/-/g, '')}`);
|
||||
console.log(`\n💡 Add additional properties (Type, Tags, Status, etc.) manually in Notion`);
|
||||
|
||||
})().catch(error => {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Archive/delete a Notion page
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const NOTION_API_KEY = process.env.NOTION_API_KEY;
|
||||
const NOTION_VERSION = '2025-09-03';
|
||||
|
||||
function notionRequest(path, method, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (data) req.write(JSON.stringify(data));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const pageId = process.argv[2];
|
||||
|
||||
if (!pageId) {
|
||||
console.error('Usage: delete-notion-page.js <page-id>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Archiving page: ${pageId}`);
|
||||
|
||||
const result = await notionRequest(`/v1/pages/${pageId}`, 'PATCH', {
|
||||
archived: true
|
||||
});
|
||||
|
||||
console.log('✓ Page archived successfully');
|
||||
console.log('Page ID:', result.id);
|
||||
console.log('Archived:', result.archived);
|
||||
})().catch(error => {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
const https = require('https');
|
||||
|
||||
const NOTION_API_KEY = process.env.NOTION_API_KEY;
|
||||
const NOTION_VERSION = '2025-09-03';
|
||||
|
||||
function notionRequest(path, method) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const dbId = process.argv[2];
|
||||
if (!dbId) {
|
||||
console.error('Usage: get-database-schema.js <database-id>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const db = await notionRequest(`/v1/databases/${dbId}`, 'GET');
|
||||
console.log(JSON.stringify(db, null, 2));
|
||||
})();
|
||||
287
archive/inactive-skills/notion-sync/scripts/md-to-notion.js
Normal file
287
archive/inactive-skills/notion-sync/scripts/md-to-notion.js
Normal file
@@ -0,0 +1,287 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Smart Markdown to Notion Converter
|
||||
* Batches blocks efficiently and handles common markdown structures
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
|
||||
const NOTION_API_KEY = process.env.NOTION_API_KEY;
|
||||
const NOTION_VERSION = '2025-09-03';
|
||||
|
||||
if (!NOTION_API_KEY) {
|
||||
console.error('Error: NOTION_API_KEY environment variable not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse markdown into structured blocks
|
||||
function parseMarkdown(markdown) {
|
||||
const lines = markdown.split('\n');
|
||||
const blocks = [];
|
||||
let currentParagraph = [];
|
||||
let inCodeBlock = false;
|
||||
let codeLanguage = '';
|
||||
let codeContent = [];
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (currentParagraph.length > 0) {
|
||||
const text = currentParagraph.join('\n').trim();
|
||||
if (text) {
|
||||
blocks.push({
|
||||
type: 'paragraph',
|
||||
paragraph: {
|
||||
rich_text: parseRichText(text)
|
||||
}
|
||||
});
|
||||
}
|
||||
currentParagraph = [];
|
||||
}
|
||||
};
|
||||
|
||||
for (let line of lines) {
|
||||
// Code blocks
|
||||
if (line.startsWith('```')) {
|
||||
if (!inCodeBlock) {
|
||||
flushParagraph();
|
||||
inCodeBlock = true;
|
||||
codeLanguage = line.slice(3).trim() || 'plain text';
|
||||
codeContent = [];
|
||||
} else {
|
||||
blocks.push({
|
||||
type: 'code',
|
||||
code: {
|
||||
language: codeLanguage,
|
||||
rich_text: [{ type: 'text', text: { content: codeContent.join('\n') } }]
|
||||
}
|
||||
});
|
||||
inCodeBlock = false;
|
||||
codeLanguage = '';
|
||||
codeContent = [];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
codeContent.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Headings
|
||||
if (line.match(/^#{1,3}\s+/)) {
|
||||
flushParagraph();
|
||||
const level = line.match(/^#+/)[0].length;
|
||||
const text = line.replace(/^#+\s+/, '').trim();
|
||||
blocks.push({
|
||||
type: level === 1 ? 'heading_1' : level === 2 ? 'heading_2' : 'heading_3',
|
||||
[level === 1 ? 'heading_1' : level === 2 ? 'heading_2' : 'heading_3']: {
|
||||
rich_text: parseRichText(text)
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rules
|
||||
if (line.match(/^---+$/)) {
|
||||
flushParagraph();
|
||||
blocks.push({
|
||||
type: 'divider',
|
||||
divider: {}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bullet lists
|
||||
if (line.match(/^[-*]\s+/)) {
|
||||
flushParagraph();
|
||||
const text = line.replace(/^[-*]\s+/, '').trim();
|
||||
blocks.push({
|
||||
type: 'bulleted_list_item',
|
||||
bulleted_list_item: {
|
||||
rich_text: parseRichText(text)
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Empty lines end paragraphs
|
||||
if (line.trim() === '') {
|
||||
flushParagraph();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular text accumulates into paragraphs
|
||||
currentParagraph.push(line);
|
||||
}
|
||||
|
||||
flushParagraph();
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// Parse rich text with basic markdown formatting
|
||||
function parseRichText(text) {
|
||||
const richText = [];
|
||||
|
||||
// Simple parser for bold, italic, links
|
||||
// This is a basic implementation - can be enhanced
|
||||
const parts = text.split(/(\*\*.*?\*\*|\*.*?\*|\[.*?\]\(.*?\))/);
|
||||
|
||||
for (let part of parts) {
|
||||
if (!part) continue;
|
||||
|
||||
if (part.startsWith('**') && part.endsWith('**')) {
|
||||
// Bold
|
||||
richText.push({
|
||||
type: 'text',
|
||||
text: { content: part.slice(2, -2) },
|
||||
annotations: { bold: true }
|
||||
});
|
||||
} else if (part.startsWith('*') && part.endsWith('*') && !part.startsWith('**')) {
|
||||
// Italic
|
||||
richText.push({
|
||||
type: 'text',
|
||||
text: { content: part.slice(1, -1) },
|
||||
annotations: { italic: true }
|
||||
});
|
||||
} else if (part.match(/\[(.*?)\]\((.*?)\)/)) {
|
||||
// Link
|
||||
const match = part.match(/\[(.*?)\]\((.*?)\)/);
|
||||
richText.push({
|
||||
type: 'text',
|
||||
text: { content: match[1], link: { url: match[2] } }
|
||||
});
|
||||
} else {
|
||||
// Plain text
|
||||
richText.push({
|
||||
type: 'text',
|
||||
text: { content: part }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return richText.length > 0 ? richText : [{ type: 'text', text: { content: text } }];
|
||||
}
|
||||
|
||||
// Create a page in Notion
|
||||
function createNotionPage(parentId, title, blocks) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = JSON.stringify({
|
||||
parent: { page_id: parentId },
|
||||
properties: {
|
||||
title: {
|
||||
title: [{ text: { content: title } }]
|
||||
}
|
||||
},
|
||||
children: blocks.slice(0, 100) // Notion API limit: 100 blocks per request
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
path: '/v1/pages',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(data)
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
reject(new Error(`API error: ${res.statusCode} ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Append remaining blocks to a page
|
||||
function appendBlocks(pageId, blocks) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = JSON.stringify({
|
||||
children: blocks.slice(0, 100)
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
path: `/v1/blocks/${pageId}/children`,
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(data)
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
reject(new Error(`API error: ${res.statusCode} ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(data);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 3) {
|
||||
console.error('Usage: md-to-notion.js <markdown-file> <parent-page-id> <page-title>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [mdFile, parentId, pageTitle] = args;
|
||||
|
||||
try {
|
||||
const markdown = fs.readFileSync(mdFile, 'utf8');
|
||||
const blocks = parseMarkdown(markdown);
|
||||
|
||||
console.log(`Parsed ${blocks.length} blocks from markdown`);
|
||||
|
||||
// Create page with first 100 blocks
|
||||
const page = await createNotionPage(parentId, pageTitle, blocks);
|
||||
console.log(`✓ Created page: ${page.url}`);
|
||||
|
||||
// Append remaining blocks in batches of 100
|
||||
for (let i = 100; i < blocks.length; i += 100) {
|
||||
const batch = blocks.slice(i, i + 100);
|
||||
await appendBlocks(page.id, batch);
|
||||
console.log(`✓ Appended ${batch.length} blocks (${i}-${i + batch.length})`);
|
||||
|
||||
// Rate limiting: wait 350ms between requests
|
||||
await new Promise(resolve => setTimeout(resolve, 350));
|
||||
}
|
||||
|
||||
console.log(`\n✅ Successfully created Notion page!`);
|
||||
console.log(`📄 URL: ${page.url}`);
|
||||
console.log(`🆔 Page ID: ${page.id}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
267
archive/inactive-skills/notion-sync/scripts/notion-to-md.js
Normal file
267
archive/inactive-skills/notion-sync/scripts/notion-to-md.js
Normal file
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Notion to Markdown Converter
|
||||
* Fetches a Notion page and converts it back to markdown
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const NOTION_API_KEY = process.env.NOTION_API_KEY;
|
||||
const NOTION_VERSION = '2025-09-03';
|
||||
|
||||
// Only check API key when running as main script (not when imported as module)
|
||||
if (require.main === module && !NOTION_API_KEY) {
|
||||
console.error('Error: NOTION_API_KEY environment variable not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Normalize and encode page/block IDs for URL safety
|
||||
function normalizeId(id) {
|
||||
// Remove hyphens if present, then add them back in UUID format
|
||||
const clean = id.replace(/-/g, '');
|
||||
if (clean.length === 32) {
|
||||
return `${clean.slice(0,8)}-${clean.slice(8,12)}-${clean.slice(12,16)}-${clean.slice(16,20)}-${clean.slice(20)}`;
|
||||
}
|
||||
return id; // Return as-is if not standard length
|
||||
}
|
||||
|
||||
// Fetch page metadata
|
||||
function getPage(pageId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const normalizedId = normalizeId(pageId);
|
||||
const encodedId = encodeURIComponent(normalizedId);
|
||||
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
path: `/v1/pages/${encodedId}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
reject(new Error(`API error: ${res.statusCode} ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch blocks from a page
|
||||
function getBlocks(blockId, cursor = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const normalizedId = normalizeId(blockId);
|
||||
const encodedId = encodeURIComponent(normalizedId);
|
||||
const encodedCursor = cursor ? encodeURIComponent(cursor) : null;
|
||||
const path = `/v1/blocks/${encodedId}/children${encodedCursor ? `?start_cursor=${encodedCursor}` : ''}`;
|
||||
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
path: path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
reject(new Error(`API error: ${res.statusCode} ${body}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch all blocks (handling pagination)
|
||||
async function getAllBlocks(blockId) {
|
||||
let allBlocks = [];
|
||||
let cursor = null;
|
||||
|
||||
do {
|
||||
const response = await getBlocks(blockId, cursor);
|
||||
allBlocks = allBlocks.concat(response.results);
|
||||
cursor = response.has_more ? response.next_cursor : null;
|
||||
} while (cursor);
|
||||
|
||||
return allBlocks;
|
||||
}
|
||||
|
||||
// Extract plain text from rich_text array
|
||||
function richTextToPlain(richText) {
|
||||
if (!richText || richText.length === 0) return '';
|
||||
return richText.map(rt => rt.plain_text || '').join('');
|
||||
}
|
||||
|
||||
// Extract formatted text from rich_text array (with markdown)
|
||||
function richTextToMarkdown(richText) {
|
||||
if (!richText || richText.length === 0) return '';
|
||||
|
||||
return richText.map(rt => {
|
||||
let text = rt.plain_text || '';
|
||||
const ann = rt.annotations || {};
|
||||
|
||||
// Apply formatting
|
||||
if (ann.code) text = `\`${text}\``;
|
||||
if (ann.bold) text = `**${text}**`;
|
||||
if (ann.italic) text = `*${text}*`;
|
||||
if (ann.strikethrough) text = `~~${text}~~`;
|
||||
|
||||
// Handle links
|
||||
if (rt.href) {
|
||||
text = `[${text}](${rt.href})`;
|
||||
} else if (rt.text && rt.text.link) {
|
||||
text = `[${text}](${rt.text.link.url})`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Convert Notion blocks to markdown
|
||||
function blocksToMarkdown(blocks) {
|
||||
const lines = [];
|
||||
|
||||
for (const block of blocks) {
|
||||
const type = block.type;
|
||||
const content = block[type];
|
||||
|
||||
switch (type) {
|
||||
case 'heading_1':
|
||||
lines.push(`# ${richTextToMarkdown(content.rich_text)}`);
|
||||
lines.push('');
|
||||
break;
|
||||
|
||||
case 'heading_2':
|
||||
lines.push(`## ${richTextToMarkdown(content.rich_text)}`);
|
||||
lines.push('');
|
||||
break;
|
||||
|
||||
case 'heading_3':
|
||||
lines.push(`### ${richTextToMarkdown(content.rich_text)}`);
|
||||
lines.push('');
|
||||
break;
|
||||
|
||||
case 'paragraph':
|
||||
const text = richTextToMarkdown(content.rich_text);
|
||||
if (text.trim()) {
|
||||
lines.push(text);
|
||||
lines.push('');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'bulleted_list_item':
|
||||
lines.push(`- ${richTextToMarkdown(content.rich_text)}`);
|
||||
break;
|
||||
|
||||
case 'numbered_list_item':
|
||||
lines.push(`1. ${richTextToMarkdown(content.rich_text)}`);
|
||||
break;
|
||||
|
||||
case 'code':
|
||||
const code = richTextToPlain(content.rich_text);
|
||||
const lang = content.language || 'plain text';
|
||||
lines.push(`\`\`\`${lang}`);
|
||||
lines.push(code);
|
||||
lines.push('```');
|
||||
lines.push('');
|
||||
break;
|
||||
|
||||
case 'divider':
|
||||
lines.push('---');
|
||||
lines.push('');
|
||||
break;
|
||||
|
||||
case 'quote':
|
||||
lines.push(`> ${richTextToMarkdown(content.rich_text)}`);
|
||||
lines.push('');
|
||||
break;
|
||||
|
||||
case 'callout':
|
||||
const emoji = content.icon?.emoji || '📌';
|
||||
lines.push(`${emoji} ${richTextToMarkdown(content.rich_text)}`);
|
||||
lines.push('');
|
||||
break;
|
||||
|
||||
// Skip unsupported block types silently
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 1) {
|
||||
console.error('Usage: notion-to-md.js <page-id> [output-file]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pageId = normalizeId(args[0]);
|
||||
const outputFile = args[1] || null;
|
||||
|
||||
try {
|
||||
// Fetch page metadata
|
||||
const page = await getPage(pageId);
|
||||
const title = page.properties?.title?.title?.[0]?.plain_text || 'Untitled';
|
||||
|
||||
// Fetch all blocks
|
||||
const blocks = await getAllBlocks(pageId);
|
||||
|
||||
// Convert to markdown
|
||||
const markdown = blocksToMarkdown(blocks);
|
||||
|
||||
// Output
|
||||
if (outputFile) {
|
||||
const fs = require('fs');
|
||||
const fullMarkdown = `# ${title}\n\n${markdown}`;
|
||||
fs.writeFileSync(outputFile, fullMarkdown, 'utf8');
|
||||
console.log(`✓ Saved to ${outputFile}`);
|
||||
} else {
|
||||
console.log(markdown);
|
||||
}
|
||||
|
||||
// Return metadata for programmatic use
|
||||
return {
|
||||
title,
|
||||
lastEditedTime: page.last_edited_time,
|
||||
markdown,
|
||||
blockCount: blocks.length
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow import as module or CLI use
|
||||
if (require.main === module) {
|
||||
main();
|
||||
} else {
|
||||
module.exports = { getPage, getAllBlocks, blocksToMarkdown, normalizeId };
|
||||
}
|
||||
258
archive/inactive-skills/notion-sync/scripts/notion-utils.js
Normal file
258
archive/inactive-skills/notion-sync/scripts/notion-utils.js
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Shared utilities for Notion API scripts
|
||||
* Common functions for HTTP requests, error handling, and data extraction
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const NOTION_API_KEY = process.env.NOTION_API_KEY;
|
||||
const NOTION_VERSION = '2025-09-03';
|
||||
|
||||
/**
|
||||
* Make a Notion API request with proper error handling
|
||||
*/
|
||||
function notionRequest(path, method, data = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestData = data ? JSON.stringify(data) : null;
|
||||
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (requestData) {
|
||||
options.headers['Content-Length'] = Buffer.byteLength(requestData);
|
||||
}
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
reject(createDetailedError(res.statusCode, body));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (requestData) {
|
||||
req.write(requestData);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create detailed error message based on status code and response
|
||||
*/
|
||||
function createDetailedError(statusCode, body) {
|
||||
let error;
|
||||
try {
|
||||
error = JSON.parse(body);
|
||||
} catch (e) {
|
||||
return new Error(`API error (${statusCode}): ${body}`);
|
||||
}
|
||||
|
||||
const errorCode = error.code;
|
||||
const errorMessage = error.message;
|
||||
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
if (errorCode === 'validation_error') {
|
||||
return new Error(`Validation error: ${errorMessage}. Check your input data.`);
|
||||
}
|
||||
return new Error(`Bad request: ${errorMessage}`);
|
||||
|
||||
case 401:
|
||||
return new Error('Authentication failed. Check your NOTION_API_KEY environment variable.');
|
||||
|
||||
case 404:
|
||||
if (errorCode === 'object_not_found') {
|
||||
return new Error('Page/database not found. Make sure it is shared with your integration.');
|
||||
}
|
||||
return new Error(`Not found: ${errorMessage}`);
|
||||
|
||||
case 429:
|
||||
return new Error('Rate limit exceeded. Wait a moment and try again.');
|
||||
|
||||
case 500:
|
||||
case 503:
|
||||
return new Error(`Notion server error (${statusCode}). Try again later.`);
|
||||
|
||||
default:
|
||||
return new Error(`API error (${statusCode}): ${errorMessage || body}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract title from a page or database object
|
||||
*/
|
||||
function extractTitle(item) {
|
||||
if (item.object === 'page') {
|
||||
const titleProp = Object.values(item.properties || {}).find(p => p.type === 'title');
|
||||
if (titleProp && titleProp.title && titleProp.title.length > 0) {
|
||||
return titleProp.title[0].plain_text;
|
||||
}
|
||||
} else if (item.object === 'database' || item.object === 'data_source') {
|
||||
if (item.title && item.title.length > 0) {
|
||||
return item.title[0].plain_text;
|
||||
}
|
||||
}
|
||||
return '(Untitled)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract value from a property based on its type
|
||||
*/
|
||||
function extractPropertyValue(property) {
|
||||
switch (property.type) {
|
||||
case 'title':
|
||||
return property.title.map(t => t.plain_text).join('');
|
||||
case 'rich_text':
|
||||
return property.rich_text.map(t => t.plain_text).join('');
|
||||
case 'number':
|
||||
return property.number;
|
||||
case 'select':
|
||||
return property.select?.name || null;
|
||||
case 'multi_select':
|
||||
return property.multi_select.map(s => s.name);
|
||||
case 'date':
|
||||
return property.date ? {
|
||||
start: property.date.start,
|
||||
end: property.date.end
|
||||
} : null;
|
||||
case 'checkbox':
|
||||
return property.checkbox;
|
||||
case 'url':
|
||||
return property.url;
|
||||
case 'email':
|
||||
return property.email;
|
||||
case 'phone_number':
|
||||
return property.phone_number;
|
||||
case 'relation':
|
||||
return property.relation.map(r => r.id);
|
||||
case 'created_time':
|
||||
return property.created_time;
|
||||
case 'last_edited_time':
|
||||
return property.last_edited_time;
|
||||
default:
|
||||
return property[property.type];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse rich text with basic markdown formatting
|
||||
*/
|
||||
function parseRichText(text) {
|
||||
const richText = [];
|
||||
const maxLength = 2000;
|
||||
|
||||
// Split text into chunks if needed
|
||||
if (text.length <= maxLength) {
|
||||
richText.push({
|
||||
type: 'text',
|
||||
text: { content: text }
|
||||
});
|
||||
} else {
|
||||
// Split into chunks
|
||||
for (let i = 0; i < text.length; i += maxLength) {
|
||||
richText.push({
|
||||
type: 'text',
|
||||
text: { content: text.substring(i, i + maxLength) }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return richText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NOTION_API_KEY is set
|
||||
*/
|
||||
function checkApiKey() {
|
||||
if (!NOTION_API_KEY) {
|
||||
console.error('Error: NOTION_API_KEY environment variable not set');
|
||||
console.error('');
|
||||
console.error('Setup:');
|
||||
console.error(' 1. Create a Notion integration at https://www.notion.so/my-integrations');
|
||||
console.error(' 2. Store the API key in macOS Keychain');
|
||||
console.error(' 3. Add to environment loader (e.g., ~/.openclaw/bin/openclaw-env.sh):');
|
||||
console.error(' export NOTION_API_KEY="$(security find-generic-password -a "$USER" -s "openclaw.notion_api_key" -w)"');
|
||||
console.error(' 4. Restart gateway: openclaw gateway restart');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format property value for database operations
|
||||
*/
|
||||
function formatPropertyValue(propertyType, value) {
|
||||
switch (propertyType) {
|
||||
case 'select':
|
||||
return { select: { name: value } };
|
||||
|
||||
case 'multi_select':
|
||||
const tags = Array.isArray(value) ? value : value.split(',').map(t => t.trim());
|
||||
return { multi_select: tags.map(name => ({ name })) };
|
||||
|
||||
case 'checkbox':
|
||||
const boolValue = typeof value === 'boolean' ? value :
|
||||
(value.toLowerCase() === 'true' || value === '1');
|
||||
return { checkbox: boolValue };
|
||||
|
||||
case 'number':
|
||||
return { number: typeof value === 'number' ? value : parseFloat(value) };
|
||||
|
||||
case 'url':
|
||||
return { url: value };
|
||||
|
||||
case 'email':
|
||||
return { email: value };
|
||||
|
||||
case 'date':
|
||||
if (typeof value === 'string') {
|
||||
const dates = value.split(',').map(d => d.trim());
|
||||
return {
|
||||
date: {
|
||||
start: dates[0],
|
||||
end: dates[1] || null
|
||||
}
|
||||
};
|
||||
}
|
||||
return { date: value };
|
||||
|
||||
case 'rich_text':
|
||||
return {
|
||||
rich_text: [{ type: 'text', text: { content: value } }]
|
||||
};
|
||||
|
||||
case 'title':
|
||||
return {
|
||||
title: [{ type: 'text', text: { content: value } }]
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported property type: ${propertyType}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
notionRequest,
|
||||
createDetailedError,
|
||||
extractTitle,
|
||||
extractPropertyValue,
|
||||
parseRichText,
|
||||
checkApiKey,
|
||||
formatPropertyValue,
|
||||
NOTION_API_KEY,
|
||||
NOTION_VERSION
|
||||
};
|
||||
201
archive/inactive-skills/notion-sync/scripts/query-database.js
Normal file
201
archive/inactive-skills/notion-sync/scripts/query-database.js
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Query Notion database with filters and sorts
|
||||
* Usage: query-database.js <database-id> [--filter <json>] [--sort <json>] [--limit 10]
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const NOTION_API_KEY = process.env.NOTION_API_KEY;
|
||||
const NOTION_VERSION = '2025-09-03';
|
||||
|
||||
if (!NOTION_API_KEY) {
|
||||
console.error('Error: NOTION_API_KEY environment variable not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function notionRequest(path, method, data = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestData = data ? JSON.stringify(data) : null;
|
||||
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (requestData) {
|
||||
options.headers['Content-Length'] = Buffer.byteLength(requestData);
|
||||
}
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
const error = JSON.parse(body);
|
||||
if (res.statusCode === 404 && error.code === 'object_not_found') {
|
||||
reject(new Error('Database not found. Make sure it is shared with your integration.'));
|
||||
} else if (res.statusCode === 401) {
|
||||
reject(new Error('Authentication failed. Check your NOTION_API_KEY.'));
|
||||
} else if (res.statusCode === 429) {
|
||||
reject(new Error('Rate limit exceeded. Wait a moment and try again.'));
|
||||
} else {
|
||||
reject(new Error(`API error (${res.statusCode}): ${error.message || body}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (requestData) {
|
||||
req.write(requestData);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function queryDatabase(databaseId, filter = null, sorts = null, pageSize = 10) {
|
||||
// In Notion API 2025-09-03, we need the data_source_id for queries
|
||||
// Get the database first to extract data_source_id
|
||||
console.error(`Fetching database info: ${databaseId}`);
|
||||
const dbInfo = await notionRequest(`/v1/databases/${databaseId}`, 'GET');
|
||||
|
||||
const dataSourceId = dbInfo.data_sources && dbInfo.data_sources.length > 0
|
||||
? dbInfo.data_sources[0].id
|
||||
: databaseId; // Fallback to database_id if no data_source found
|
||||
|
||||
console.error(`Querying data source: ${dataSourceId}`);
|
||||
|
||||
const queryPayload = {
|
||||
page_size: pageSize
|
||||
};
|
||||
|
||||
if (filter) {
|
||||
queryPayload.filter = filter;
|
||||
console.error('Filter:', JSON.stringify(filter, null, 2));
|
||||
}
|
||||
|
||||
if (sorts) {
|
||||
queryPayload.sorts = sorts;
|
||||
console.error('Sort:', JSON.stringify(sorts, null, 2));
|
||||
}
|
||||
|
||||
const result = await notionRequest(`/v1/data_sources/${dataSourceId}/query`, 'POST', queryPayload);
|
||||
|
||||
return result.results.map(page => {
|
||||
const properties = {};
|
||||
for (const [key, value] of Object.entries(page.properties)) {
|
||||
properties[key] = extractPropertyValue(value);
|
||||
}
|
||||
|
||||
return {
|
||||
id: page.id,
|
||||
url: page.url,
|
||||
lastEdited: page.last_edited_time,
|
||||
properties: properties
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function extractPropertyValue(property) {
|
||||
switch (property.type) {
|
||||
case 'title':
|
||||
return property.title.map(t => t.plain_text).join('');
|
||||
case 'rich_text':
|
||||
return property.rich_text.map(t => t.plain_text).join('');
|
||||
case 'number':
|
||||
return property.number;
|
||||
case 'select':
|
||||
return property.select?.name || null;
|
||||
case 'multi_select':
|
||||
return property.multi_select.map(s => s.name);
|
||||
case 'date':
|
||||
return property.date ? {
|
||||
start: property.date.start,
|
||||
end: property.date.end
|
||||
} : null;
|
||||
case 'checkbox':
|
||||
return property.checkbox;
|
||||
case 'url':
|
||||
return property.url;
|
||||
case 'email':
|
||||
return property.email;
|
||||
case 'phone_number':
|
||||
return property.phone_number;
|
||||
case 'relation':
|
||||
return property.relation.map(r => r.id);
|
||||
default:
|
||||
return property[property.type];
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
(async () => {
|
||||
try {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args[0] === '--help') {
|
||||
console.log('Usage: query-database.js <database-id> [options]');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --filter <json> Filter expression (JSON)');
|
||||
console.log(' --sort <json> Sort expression (JSON)');
|
||||
console.log(' --limit <num> Maximum results (default: 10)');
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' # Get all items');
|
||||
console.log(' query-database.js xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
|
||||
console.log('');
|
||||
console.log(' # Filter by status');
|
||||
console.log(' query-database.js <db-id> --filter \'{"property": "Status", "select": {"equals": "Complete"}}\'');
|
||||
console.log('');
|
||||
console.log(' # Filter by tag (multi-select contains)');
|
||||
console.log(' query-database.js <db-id> --filter \'{"property": "Tags", "multi_select": {"contains": "AI"}}\'');
|
||||
console.log('');
|
||||
console.log(' # Sort by date descending');
|
||||
console.log(' query-database.js <db-id> --sort \'[{"property": "Date", "direction": "descending"}]\'');
|
||||
console.log('');
|
||||
console.log(' # Combine filter + sort');
|
||||
console.log(' query-database.js <db-id> \\');
|
||||
console.log(' --filter \'{"property": "Status", "select": {"equals": "Complete"}}\' \\');
|
||||
console.log(' --sort \'[{"property": "Date", "direction": "descending"}]\'');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const databaseId = args[0];
|
||||
let filter = null;
|
||||
let sorts = null;
|
||||
let limit = 10;
|
||||
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
if (args[i] === '--filter' && args[i + 1]) {
|
||||
filter = JSON.parse(args[i + 1]);
|
||||
i++;
|
||||
} else if (args[i] === '--sort' && args[i + 1]) {
|
||||
sorts = JSON.parse(args[i + 1]);
|
||||
i++;
|
||||
} else if (args[i] === '--limit' && args[i + 1]) {
|
||||
limit = parseInt(args[i + 1]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
const results = await queryDatabase(databaseId, filter, sorts, limit);
|
||||
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
console.error(`\n✓ Found ${results.length} result(s)`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
144
archive/inactive-skills/notion-sync/scripts/search-notion.js
Normal file
144
archive/inactive-skills/notion-sync/scripts/search-notion.js
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Search Notion for pages and databases
|
||||
* Usage: search-notion.js <query> [--filter page|database] [--limit 10]
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const NOTION_API_KEY = process.env.NOTION_API_KEY;
|
||||
const NOTION_VERSION = '2025-09-03';
|
||||
|
||||
if (!NOTION_API_KEY) {
|
||||
console.error('Error: NOTION_API_KEY environment variable not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function notionRequest(path, method, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestData = JSON.stringify(data);
|
||||
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(requestData)
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
const error = JSON.parse(body);
|
||||
if (res.statusCode === 404 && error.code === 'object_not_found') {
|
||||
reject(new Error('Page/database not found. Make sure it is shared with your integration.'));
|
||||
} else if (res.statusCode === 401) {
|
||||
reject(new Error('Authentication failed. Check your NOTION_API_KEY.'));
|
||||
} else if (res.statusCode === 429) {
|
||||
reject(new Error('Rate limit exceeded. Wait a moment and try again.'));
|
||||
} else {
|
||||
reject(new Error(`API error (${res.statusCode}): ${error.message || body}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(requestData);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function searchNotion(query, filter = null, pageSize = 10) {
|
||||
const searchPayload = {
|
||||
query: query,
|
||||
page_size: pageSize
|
||||
};
|
||||
|
||||
if (filter) {
|
||||
searchPayload.filter = {
|
||||
property: 'object',
|
||||
value: filter // 'page' or 'database'
|
||||
};
|
||||
}
|
||||
|
||||
console.error(`Searching for: "${query}"${filter ? ` (filter: ${filter})` : ''}`);
|
||||
|
||||
const result = await notionRequest('/v1/search', 'POST', searchPayload);
|
||||
|
||||
return result.results.map(item => ({
|
||||
id: item.id,
|
||||
object: item.object,
|
||||
title: extractTitle(item),
|
||||
url: item.url,
|
||||
lastEdited: item.last_edited_time,
|
||||
parent: item.parent
|
||||
}));
|
||||
}
|
||||
|
||||
function extractTitle(item) {
|
||||
if (item.object === 'page') {
|
||||
const titleProp = Object.values(item.properties || {}).find(p => p.type === 'title');
|
||||
if (titleProp && titleProp.title && titleProp.title.length > 0) {
|
||||
return titleProp.title[0].plain_text;
|
||||
}
|
||||
} else if (item.object === 'database' || item.object === 'data_source') {
|
||||
if (item.title && item.title.length > 0) {
|
||||
return item.title[0].plain_text;
|
||||
}
|
||||
}
|
||||
return '(Untitled)';
|
||||
}
|
||||
|
||||
// Main execution
|
||||
(async () => {
|
||||
try {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args[0] === '--help') {
|
||||
console.log('Usage: search-notion.js <query> [options]');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --filter <page|database> Filter by object type');
|
||||
console.log(' --limit <number> Maximum results (default: 10)');
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' search-notion.js "newsletter"');
|
||||
console.log(' search-notion.js "research" --filter page');
|
||||
console.log(' search-notion.js "AI" --limit 20');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const query = args[0];
|
||||
let filter = null;
|
||||
let limit = 10;
|
||||
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
if (args[i] === '--filter' && args[i + 1]) {
|
||||
filter = args[i + 1];
|
||||
i++;
|
||||
} else if (args[i] === '--limit' && args[i + 1]) {
|
||||
limit = parseInt(args[i + 1]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
const results = await searchNotion(query, filter, limit);
|
||||
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
console.error(`\n✓ Found ${results.length} result(s)`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test for normalizeId function
|
||||
*/
|
||||
|
||||
const { normalizeId } = require('./notion-to-md.js');
|
||||
|
||||
// Test cases
|
||||
const testCases = [
|
||||
// With hyphens (standard UUID format)
|
||||
{
|
||||
input: 'abc12345-6789-0123-4567-890abcdef012',
|
||||
expected: 'abc12345-6789-0123-4567-890abcdef012',
|
||||
desc: 'Standard UUID with hyphens'
|
||||
},
|
||||
// Without hyphens (compact format)
|
||||
{
|
||||
input: 'abc12345678901234567890abcdef012',
|
||||
expected: 'abc12345-6789-0123-4567-890abcdef012',
|
||||
desc: 'Compact format without hyphens'
|
||||
},
|
||||
// Mixed case
|
||||
{
|
||||
input: 'ABC12345678901234567890ABCDEF012',
|
||||
expected: 'ABC12345-6789-0123-4567-890ABCDEF012',
|
||||
desc: 'Uppercase compact format'
|
||||
},
|
||||
// Already normalized with hyphens
|
||||
{
|
||||
input: '12a85c78-1e0b-481a-98d5-e122e8e9c5f3',
|
||||
expected: '12a85c78-1e0b-481a-98d5-e122e8e9c5f3',
|
||||
desc: 'Real Notion UUID'
|
||||
},
|
||||
// Real Notion ID without hyphens
|
||||
{
|
||||
input: '12a85c781e0b481a98d5e122e8e9c5f3',
|
||||
expected: '12a85c78-1e0b-481a-98d5-e122e8e9c5f3',
|
||||
desc: 'Real Notion UUID without hyphens'
|
||||
},
|
||||
// Invalid length (should return as-is)
|
||||
{
|
||||
input: 'tooshort',
|
||||
expected: 'tooshort',
|
||||
desc: 'Invalid format (too short)'
|
||||
}
|
||||
];
|
||||
|
||||
console.log('Testing normalizeId function:\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
testCases.forEach((test, idx) => {
|
||||
const result = normalizeId(test.input);
|
||||
const success = result === test.expected;
|
||||
|
||||
if (success) {
|
||||
console.log(`✓ Test ${idx + 1}: ${test.desc}`);
|
||||
console.log(` Input: ${test.input}`);
|
||||
console.log(` Output: ${result}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(`✗ Test ${idx + 1}: ${test.desc}`);
|
||||
console.log(` Input: ${test.input}`);
|
||||
console.log(` Expected: ${test.expected}`);
|
||||
console.log(` Got: ${result}`);
|
||||
failed++;
|
||||
}
|
||||
console.log();
|
||||
});
|
||||
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Update Notion page properties (for database pages)
|
||||
* Usage: update-page-properties.js <page-id> <property-name> <value> [--type select|multi_select|checkbox|number|url|email|date]
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
|
||||
const NOTION_API_KEY = process.env.NOTION_API_KEY;
|
||||
const NOTION_VERSION = '2025-09-03';
|
||||
|
||||
if (!NOTION_API_KEY) {
|
||||
console.error('Error: NOTION_API_KEY environment variable not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function notionRequest(path, method, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestData = JSON.stringify(data);
|
||||
|
||||
const options = {
|
||||
hostname: 'api.notion.com',
|
||||
port: 443,
|
||||
path: path,
|
||||
method: method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${NOTION_API_KEY}`,
|
||||
'Notion-Version': NOTION_VERSION,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(requestData)
|
||||
}
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk) => body += chunk);
|
||||
res.on('end', () => {
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(JSON.parse(body));
|
||||
} else {
|
||||
const error = JSON.parse(body);
|
||||
if (res.statusCode === 404 && error.code === 'object_not_found') {
|
||||
reject(new Error('Page not found. Make sure it is shared with your integration.'));
|
||||
} else if (res.statusCode === 401) {
|
||||
reject(new Error('Authentication failed. Check your NOTION_API_KEY.'));
|
||||
} else if (res.statusCode === 400 && error.code === 'validation_error') {
|
||||
reject(new Error(`Validation error: ${error.message}. Check property name and type.`));
|
||||
} else if (res.statusCode === 429) {
|
||||
reject(new Error('Rate limit exceeded. Wait a moment and try again.'));
|
||||
} else {
|
||||
reject(new Error(`API error (${res.statusCode}): ${error.message || body}`));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
req.write(requestData);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function formatPropertyValue(propertyType, value) {
|
||||
switch (propertyType) {
|
||||
case 'select':
|
||||
return { select: { name: value } };
|
||||
|
||||
case 'multi_select':
|
||||
// Value can be comma-separated: "AI,Leadership,Research"
|
||||
const tags = value.split(',').map(t => t.trim());
|
||||
return { multi_select: tags.map(name => ({ name })) };
|
||||
|
||||
case 'checkbox':
|
||||
const boolValue = value.toLowerCase() === 'true' || value === '1';
|
||||
return { checkbox: boolValue };
|
||||
|
||||
case 'number':
|
||||
return { number: parseFloat(value) };
|
||||
|
||||
case 'url':
|
||||
return { url: value };
|
||||
|
||||
case 'email':
|
||||
return { email: value };
|
||||
|
||||
case 'date':
|
||||
// Value can be "2024-01-15" or "2024-01-15,2024-01-20" for range
|
||||
const dates = value.split(',').map(d => d.trim());
|
||||
return {
|
||||
date: {
|
||||
start: dates[0],
|
||||
end: dates[1] || null
|
||||
}
|
||||
};
|
||||
|
||||
case 'rich_text':
|
||||
return {
|
||||
rich_text: [{ type: 'text', text: { content: value } }]
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported property type: ${propertyType}. Supported: select, multi_select, checkbox, number, url, email, date, rich_text`);
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePageProperties(pageId, propertyName, value, propertyType = 'select') {
|
||||
const properties = {};
|
||||
properties[propertyName] = formatPropertyValue(propertyType, value);
|
||||
|
||||
console.error(`Updating page: ${pageId}`);
|
||||
console.error(`Property: ${propertyName} (${propertyType})`);
|
||||
console.error(`Value: ${value}`);
|
||||
|
||||
const result = await notionRequest(`/v1/pages/${pageId}`, 'PATCH', { properties });
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
url: result.url,
|
||||
updated: result.last_edited_time
|
||||
};
|
||||
}
|
||||
|
||||
// Main execution
|
||||
(async () => {
|
||||
try {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 3 || args[0] === '--help') {
|
||||
console.log('Usage: update-page-properties.js <page-id> <property-name> <value> [--type <type>]');
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(' --type <type> Property type (default: select)');
|
||||
console.log('');
|
||||
console.log('Supported types:');
|
||||
console.log(' select Single choice (e.g., "Complete")');
|
||||
console.log(' multi_select Multiple choices, comma-separated (e.g., "AI,Leadership")');
|
||||
console.log(' checkbox Boolean (e.g., "true" or "false")');
|
||||
console.log(' number Numeric value (e.g., "1200")');
|
||||
console.log(' url URL string');
|
||||
console.log(' email Email address');
|
||||
console.log(' date Date or range (e.g., "2024-01-15" or "2024-01-15,2024-01-20")');
|
||||
console.log(' rich_text Text content');
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' # Set status to Complete');
|
||||
console.log(' update-page-properties.js <page-id> Status Complete --type select');
|
||||
console.log('');
|
||||
console.log(' # Add multiple tags');
|
||||
console.log(' update-page-properties.js <page-id> Tags "AI,Leadership,Research" --type multi_select');
|
||||
console.log('');
|
||||
console.log(' # Set checkbox');
|
||||
console.log(' update-page-properties.js <page-id> Published true --type checkbox');
|
||||
console.log('');
|
||||
console.log(' # Set date');
|
||||
console.log(' update-page-properties.js <page-id> "Publish Date" 2024-02-01 --type date');
|
||||
console.log('');
|
||||
console.log(' # Set URL');
|
||||
console.log(' update-page-properties.js <page-id> "Source URL" "https://example.com" --type url');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const pageId = args[0];
|
||||
const propertyName = args[1];
|
||||
const value = args[2];
|
||||
let propertyType = 'select';
|
||||
|
||||
for (let i = 3; i < args.length; i++) {
|
||||
if (args[i] === '--type' && args[i + 1]) {
|
||||
propertyType = args[i + 1];
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await updatePageProperties(pageId, propertyName, value, propertyType);
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
console.error(`\n✓ Page updated successfully`);
|
||||
console.error(` URL: ${result.url}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
156
archive/inactive-skills/notion-sync/scripts/watch-notion.js
Normal file
156
archive/inactive-skills/notion-sync/scripts/watch-notion.js
Normal file
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Notion Page Watcher
|
||||
* Monitors a Notion page for changes and suggests next actions
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getPage, getAllBlocks, blocksToMarkdown, normalizeId } = require('./notion-to-md.js');
|
||||
|
||||
const STATE_FILE = path.join(__dirname, '../memory/notion-watch-state.json');
|
||||
|
||||
// Load watch state
|
||||
function loadState() {
|
||||
if (!fs.existsSync(STATE_FILE)) {
|
||||
return { pages: {} };
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
||||
}
|
||||
|
||||
// Save watch state
|
||||
function saveState(state) {
|
||||
const dir = path.dirname(STATE_FILE);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
// Check a specific page for changes
|
||||
async function checkPage(pageId, localPath) {
|
||||
try {
|
||||
// Normalize pageId to handle different formats
|
||||
const normalizedPageId = normalizeId(pageId);
|
||||
|
||||
const state = loadState();
|
||||
const pageState = state.pages[normalizedPageId] || {};
|
||||
|
||||
// Fetch current page state
|
||||
const page = await getPage(normalizedPageId);
|
||||
const lastEditedTime = page.last_edited_time;
|
||||
const title = page.properties?.title?.title?.[0]?.plain_text || 'Untitled';
|
||||
|
||||
// Check if page was edited since last check
|
||||
const hasChanges = !pageState.lastEditedTime ||
|
||||
new Date(lastEditedTime) > new Date(pageState.lastEditedTime);
|
||||
|
||||
const result = {
|
||||
pageId: normalizedPageId,
|
||||
title,
|
||||
lastEditedTime,
|
||||
hasChanges,
|
||||
localPath,
|
||||
actions: []
|
||||
};
|
||||
|
||||
if (hasChanges) {
|
||||
// Fetch blocks and convert to markdown
|
||||
const blocks = await getAllBlocks(normalizedPageId);
|
||||
const notionMarkdown = blocksToMarkdown(blocks);
|
||||
|
||||
// Compare with local file if it exists
|
||||
let localMarkdown = '';
|
||||
let localDiffers = false;
|
||||
|
||||
if (fs.existsSync(localPath)) {
|
||||
localMarkdown = fs.readFileSync(localPath, 'utf8');
|
||||
// Simple comparison (could be enhanced with proper diff)
|
||||
localDiffers = localMarkdown.trim() !== notionMarkdown.trim();
|
||||
}
|
||||
|
||||
result.notionMarkdown = notionMarkdown;
|
||||
result.localDiffers = localDiffers;
|
||||
result.blockCount = blocks.length;
|
||||
|
||||
// Suggest actions
|
||||
if (pageState.lastEditedTime) {
|
||||
result.actions.push(`📝 Page edited since last check (${new Date(pageState.lastEditedTime).toLocaleString()})`);
|
||||
} else {
|
||||
result.actions.push('🆕 First time checking this page');
|
||||
}
|
||||
|
||||
if (localDiffers) {
|
||||
result.actions.push(`⚠️ Local markdown differs from Notion version`);
|
||||
result.actions.push(`💡 Suggested: Sync Notion → markdown to update local file`);
|
||||
}
|
||||
|
||||
// Update state
|
||||
pageState.lastEditedTime = lastEditedTime;
|
||||
pageState.lastChecked = new Date().toISOString();
|
||||
pageState.title = title;
|
||||
state.pages[normalizedPageId] = pageState;
|
||||
saveState(state);
|
||||
|
||||
} else {
|
||||
result.actions.push('✓ No changes since last check');
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
pageId: normalizeId(pageId),
|
||||
error: error.message,
|
||||
actions: [`❌ Error checking page: ${error.message}`]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Get page ID and local path from args or environment
|
||||
let pageId = args[0];
|
||||
let localPath = args[1];
|
||||
|
||||
// Fallback to environment variables if not provided
|
||||
if (!pageId) pageId = process.env.NOTION_WATCH_PAGE_ID;
|
||||
if (!localPath) localPath = process.env.NOTION_WATCH_LOCAL_PATH;
|
||||
|
||||
if (!pageId || !localPath) {
|
||||
console.error(`Usage: watch-notion.js <page-id> <local-path>
|
||||
|
||||
Arguments:
|
||||
page-id Notion page ID to monitor
|
||||
local-path Local markdown file path for comparison
|
||||
|
||||
Environment variables (optional):
|
||||
NOTION_WATCH_PAGE_ID Default page ID
|
||||
NOTION_WATCH_LOCAL_PATH Default local path
|
||||
|
||||
Examples:
|
||||
node watch-notion.js "abc123..." "/path/to/draft.md"
|
||||
|
||||
# Using environment variables
|
||||
export NOTION_WATCH_PAGE_ID="abc123..."
|
||||
export NOTION_WATCH_LOCAL_PATH="/path/to/draft.md"
|
||||
node watch-notion.js
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = await checkPage(pageId, localPath);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(err => {
|
||||
console.error('Fatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
module.exports = { checkPage };
|
||||
}
|
||||
Reference in New Issue
Block a user