AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
79
archive/inactive-skills/notion-apikeys/SKILL.md
Normal file
79
archive/inactive-skills/notion-apikeys/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: notion-apikeys
|
||||
description: Look up API keys from Anthony's Notion 'API' database. Use this skill whenever you need an API key, token, or secret for any service (Brave, OpenAI, Notion, etc.) and it isn't already configured. Instead of asking Anthony for the key manually, query his Notion database to retrieve it automatically. Triggers on missing API key errors, needing to configure a service, or any request to find/use a stored API key.
|
||||
---
|
||||
|
||||
# Notion API Key Lookup
|
||||
|
||||
Query Anthony's Notion 'API' database to retrieve API keys and secrets.
|
||||
|
||||
## Config File
|
||||
|
||||
Credentials are stored at `~/.config/notion-apikeys.json`:
|
||||
```json
|
||||
{
|
||||
"token": "secret_...",
|
||||
"database_id": "..."
|
||||
}
|
||||
```
|
||||
|
||||
## Script
|
||||
|
||||
```
|
||||
skills/notion-apikeys/scripts/query.py
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Look up a specific service
|
||||
python3 scripts/query.py "Brave"
|
||||
|
||||
# List all entries (keys are partially masked)
|
||||
python3 scripts/query.py --list
|
||||
|
||||
# First-time setup (interactive)
|
||||
python3 scripts/query.py --setup
|
||||
|
||||
# Discover what columns exist in the database
|
||||
python3 scripts/query.py --find-db
|
||||
|
||||
# Pass token inline without saving
|
||||
python3 scripts/query.py "Brave" --token secret_xxx
|
||||
```
|
||||
|
||||
## Workflow: Retrieve a Key and Configure OpenClaw
|
||||
|
||||
1. Run `query.py <service>` with the service name
|
||||
2. Extract the key from the output
|
||||
3. Apply to OpenClaw config via `gateway config.patch` or edit `~/.openclaw/openclaw.json` directly
|
||||
4. Restart gateway if needed
|
||||
|
||||
## Setup (First Time)
|
||||
|
||||
Anthony must:
|
||||
1. Create a Notion integration at https://www.notion.so/my-integrations
|
||||
2. Share the 'API' database page with that integration (Share → Invite)
|
||||
3. Copy the integration token (starts with `secret_`)
|
||||
4. Run: `python3 scripts/query.py --setup`
|
||||
|
||||
If the Notion token is already known, pass it directly:
|
||||
```bash
|
||||
python3 scripts/query.py "Brave" --token secret_xxxx
|
||||
```
|
||||
|
||||
## Notion Database Structure
|
||||
|
||||
The script auto-discovers column names. Common layouts:
|
||||
- **Name** (title) — service/provider name
|
||||
- **API Key** / **Key** / **Token** — the actual secret
|
||||
- **URL** / **Endpoint** — optional base URL
|
||||
- **Notes** — optional context
|
||||
|
||||
If the property names differ, run `--find-db` to inspect them.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- "Could not find 'API' database" → integration hasn't been shared with the database
|
||||
- "Notion API error 401" → token is invalid or expired
|
||||
- "No entries found for X" → check spelling; use `--list` to see all available entries
|
||||
199
archive/inactive-skills/notion-apikeys/scripts/query.py
Normal file
199
archive/inactive-skills/notion-apikeys/scripts/query.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
notion-apikeys - Query Notion 'API' database for API keys
|
||||
Usage:
|
||||
query.py <service_name> Look up a service by name
|
||||
query.py --list List all entries
|
||||
query.py --find-db Discover database structure/properties
|
||||
query.py --setup Interactive setup (save token + db-id)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import argparse
|
||||
|
||||
CONFIG_FILE = os.path.expanduser("~/.config/notion-apikeys.json")
|
||||
NOTION_VERSION = "2022-06-28"
|
||||
NOTION_BASE = "https://api.notion.com/v1"
|
||||
|
||||
|
||||
def get_headers(token):
|
||||
return {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Notion-Version": NOTION_VERSION,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def notion_request(method, path, token, payload=None):
|
||||
try:
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
url = f"{NOTION_BASE}{path}"
|
||||
data = json.dumps(payload).encode() if payload is not None else None
|
||||
req = urllib.request.Request(url, data=data, headers=get_headers(token), method=method)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())
|
||||
except Exception as e:
|
||||
if hasattr(e, "read"):
|
||||
err = e.read().decode()
|
||||
try:
|
||||
err = json.loads(err)
|
||||
except Exception:
|
||||
pass
|
||||
print(f"Notion API error: {err}", file=sys.stderr)
|
||||
else:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def find_database(token, name="API"):
|
||||
result = notion_request("POST", "/search", token, {
|
||||
"query": name,
|
||||
"filter": {"value": "database", "property": "object"},
|
||||
"page_size": 20,
|
||||
})
|
||||
dbs = result.get("results", [])
|
||||
for db in dbs:
|
||||
title_parts = db.get("title", [])
|
||||
db_name = "".join(t.get("plain_text", "") for t in title_parts).strip()
|
||||
if db_name.lower() == name.lower():
|
||||
return db["id"], db_name, db.get("properties", {})
|
||||
# If no exact match, return all found
|
||||
return None, None, dbs
|
||||
|
||||
|
||||
def query_database(token, db_id, filter_text=None):
|
||||
payload = {"page_size": 100}
|
||||
if filter_text:
|
||||
payload["filter"] = {
|
||||
"or": [
|
||||
{"property": "Name", "title": {"contains": filter_text}},
|
||||
]
|
||||
}
|
||||
return notion_request("POST", f"/databases/{db_id}/query", token, payload)
|
||||
|
||||
|
||||
def extract_value(prop):
|
||||
ptype = prop.get("type", "")
|
||||
if ptype == "title":
|
||||
return "".join(t.get("plain_text", "") for t in prop.get("title", []))
|
||||
elif ptype == "rich_text":
|
||||
return "".join(t.get("plain_text", "") for t in prop.get("rich_text", []))
|
||||
elif ptype in ("url", "email", "phone_number"):
|
||||
return prop.get(ptype) or ""
|
||||
elif ptype == "select":
|
||||
s = prop.get("select")
|
||||
return s.get("name", "") if s else ""
|
||||
elif ptype == "multi_select":
|
||||
return ", ".join(s.get("name", "") for s in prop.get("multi_select", []))
|
||||
elif ptype == "number":
|
||||
v = prop.get("number")
|
||||
return str(v) if v is not None else ""
|
||||
elif ptype == "checkbox":
|
||||
return "✓" if prop.get("checkbox") else "✗"
|
||||
elif ptype == "date":
|
||||
d = prop.get("date")
|
||||
return d.get("start", "") if d else ""
|
||||
return ""
|
||||
|
||||
|
||||
def load_config():
|
||||
if os.path.exists(CONFIG_FILE):
|
||||
with open(CONFIG_FILE) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
|
||||
|
||||
def save_config(config):
|
||||
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Notion API key lookup")
|
||||
parser.add_argument("service", nargs="?", help="Service name to look up")
|
||||
parser.add_argument("--list", action="store_true", help="List all entries")
|
||||
parser.add_argument("--find-db", action="store_true", help="Discover DB structure")
|
||||
parser.add_argument("--setup", action="store_true", help="Save token + db-id to config")
|
||||
parser.add_argument("--token", help="Notion token (overrides config/env)")
|
||||
parser.add_argument("--db-id", help="Notion database ID (overrides config)")
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_config()
|
||||
token = args.token or os.environ.get("NOTION_TOKEN") or config.get("token")
|
||||
db_id = args.db_id or config.get("database_id")
|
||||
|
||||
# Setup mode
|
||||
if args.setup:
|
||||
if not token:
|
||||
token = input("Notion Integration Token (secret_...): ").strip()
|
||||
if not db_id:
|
||||
print("Finding 'API' database automatically...")
|
||||
found_id, found_name, _ = find_database(token, "API")
|
||||
if found_id:
|
||||
print(f"Found: '{found_name}' (ID: {found_id})")
|
||||
db_id = found_id
|
||||
else:
|
||||
db_id = input("Database ID (or leave blank to auto-find each time): ").strip()
|
||||
config["token"] = token
|
||||
if db_id:
|
||||
config["database_id"] = db_id
|
||||
save_config(config)
|
||||
print(f"✓ Config saved to {CONFIG_FILE}")
|
||||
return
|
||||
|
||||
if not token:
|
||||
print("ERROR: Notion token required.", file=sys.stderr)
|
||||
print(" Run: query.py --setup", file=sys.stderr)
|
||||
print(" Or set: NOTION_TOKEN env var", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Auto-discover database if needed
|
||||
if not db_id:
|
||||
found_id, found_name, found_dbs = find_database(token, "API")
|
||||
if found_id:
|
||||
db_id = found_id
|
||||
else:
|
||||
print("ERROR: Could not find 'API' database.", file=sys.stderr)
|
||||
print(f"Found databases: {[d.get('id') for d in found_dbs]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Find database structure
|
||||
if args.find_db:
|
||||
found_id, found_name, props = find_database(token, "API")
|
||||
if found_id:
|
||||
print(f"Database: '{found_name}' (ID: {found_id})")
|
||||
print("Properties:")
|
||||
for name, prop in props.items():
|
||||
print(f" • {name} ({prop.get('type', '?')})")
|
||||
return
|
||||
|
||||
# Query
|
||||
filter_text = args.service if not args.list else None
|
||||
result = query_database(token, db_id, filter_text)
|
||||
pages = result.get("results", [])
|
||||
|
||||
if not pages:
|
||||
label = f" for '{args.service}'" if args.service else ""
|
||||
print(f"No entries found{label}.")
|
||||
return
|
||||
|
||||
for page in pages:
|
||||
props = page.get("properties", {})
|
||||
values = {k: extract_value(v) for k, v in props.items()}
|
||||
name = next((values[k] for k, v in props.items() if v.get("type") == "title"), "Unknown")
|
||||
print(f"\n── {name} ──")
|
||||
for k, val in values.items():
|
||||
if val and props[k].get("type") != "title":
|
||||
# Mask long keys for security in list mode
|
||||
if args.list and len(val) > 20 and k.lower() in ("key", "api key", "token", "secret", "value"):
|
||||
val = val[:8] + "..." + val[-4:]
|
||||
print(f" {k}: {val}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user