Merge branch 'main' into bb/gui
This commit is contained in:
@@ -820,7 +820,7 @@ Available providers for auxiliary tasks: `auto`, `main`, plus any provider in th
|
||||
:::
|
||||
|
||||
:::tip xAI Grok OAuth
|
||||
`xai-oauth` logs in via browser OAuth for SuperGrok subscribers (no API key needed). Run `hermes model` and select **xAI Grok OAuth (SuperGrok Subscription)** to authenticate. The same OAuth token is reused for every direct-to-xAI surface (chat, auxiliary tasks, TTS, image gen, video gen, transcription). See the [xAI Grok OAuth guide](../guides/xai-grok-oauth.md).
|
||||
`xai-oauth` logs in via browser OAuth for SuperGrok subscribers (no API key needed). Run `hermes model` and select **xAI Grok OAuth (SuperGrok Subscription)** to authenticate. The same OAuth token is reused for every direct-to-xAI surface (chat, auxiliary tasks, TTS, image gen, video gen, transcription). See the [xAI Grok OAuth guide](../guides/xai-grok-oauth.md), and if Hermes is on a remote host see [OAuth over SSH / Remote Hosts](../guides/oauth-over-ssh.md).
|
||||
:::
|
||||
|
||||
:::warning `"main"` is for auxiliary tasks only
|
||||
|
||||
@@ -68,7 +68,13 @@ Agree to the terms and click **Save**. On the next page click **Settings** → c
|
||||
|
||||
### Running over SSH / in a headless environment
|
||||
|
||||
If `SSH_CLIENT` or `SSH_TTY` is set, Hermes skips the automatic browser open during both the wizard and the OAuth step. Copy the dashboard URL and the authorization URL Hermes prints, open them in a browser on your local machine, and proceed normally — the local HTTP listener still runs on the remote host on port 43827. If you need to reach it through an SSH tunnel, forward that port: `ssh -L 43827:127.0.0.1:43827 remote`.
|
||||
If `SSH_CLIENT` or `SSH_TTY` is set, Hermes skips the automatic browser open during both the wizard and the OAuth step. Copy the dashboard URL and the authorization URL Hermes prints, open them in a browser on your local machine, and proceed normally — the local HTTP listener still runs on the remote host on port `43827`. Your laptop's browser can't reach the remote loopback without an SSH local-forward:
|
||||
|
||||
```bash
|
||||
ssh -N -L 43827:127.0.0.1:43827 user@remote-host
|
||||
```
|
||||
|
||||
For jump-box / bastion setups and other gotchas (mosh, tmux, port conflicts), see [OAuth over SSH / Remote Hosts](../../guides/oauth-over-ssh.md).
|
||||
|
||||
## Verify
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ Generate images, video, and audio with ComfyUI — install, launch, manage nodes
|
||||
|---|---|
|
||||
| Source | Bundled (installed by default) |
|
||||
| Path | `skills/creative/comfyui` |
|
||||
| Version | `5.0.0` |
|
||||
| Author | ['kshitijk4poor', 'alt-glitch'] |
|
||||
| Version | `5.1.0` |
|
||||
| Author | ['kshitijk4poor', 'alt-glitch', 'purzbeats'] |
|
||||
| License | MIT |
|
||||
| Platforms | macos, linux, windows |
|
||||
| Tags | `comfyui`, `image-generation`, `stable-diffusion`, `flux`, `sd3`, `wan-video`, `hunyuan-video`, `creative`, `generative-ai`, `video-generation` |
|
||||
@@ -42,6 +42,12 @@ for workflow execution.
|
||||
- `official-cli.md` — every `comfy ...` command, with flags
|
||||
- `rest-api.md` — REST + WebSocket endpoints (local + cloud), payload schemas
|
||||
- `workflow-format.md` — API-format JSON, common node types, param mapping
|
||||
- `template-integrity.md` — converting `comfyui-workflow-templates` from
|
||||
editor format to API format: Reroute bypass, dotted dynamic-input keys
|
||||
(`values.a`, `resize_type.width`), Cloud quirks (302 redirect, 1 concurrent
|
||||
free-tier job, 1080p VRAM ceiling), Discord-compatible ffmpeg stitch.
|
||||
Authored by [@purzbeats](https://github.com/purzbeats). Load this whenever
|
||||
you're starting from an official template.
|
||||
|
||||
**Scripts (`scripts/`):**
|
||||
|
||||
|
||||
@@ -65,6 +65,29 @@ kanban_complete(
|
||||
)
|
||||
```
|
||||
|
||||
**Coding task that needs human review (review-required):**
|
||||
|
||||
For most code-changing tasks, the work isn't truly *done* until a human reviewer has eyes on it. Block instead of complete, with `reason` prefixed `review-required: ` so the dashboard surfaces the row as needing review. Drop the structured metadata (changed files, test counts, diff/PR url) into a comment first, since `kanban_block` only carries the human-readable reason — comments are the durable annotation channel. Reviewer either approves and runs `hermes kanban unblock <id>` (which re-spawns you with the comment thread for any follow-ups) or asks for changes via another comment.
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
kanban_comment(
|
||||
body="review-required handoff:\n" + json.dumps({
|
||||
"changed_files": ["rate_limiter.py", "tests/test_rate_limiter.py"],
|
||||
"tests_run": 14,
|
||||
"tests_passed": 14,
|
||||
"diff_path": "/path/to/worktree", # or PR url if pushed
|
||||
"decisions": ["user_id primary, IP fallback for unauthenticated requests"],
|
||||
}, indent=2),
|
||||
)
|
||||
kanban_block(
|
||||
reason="review-required: rate limiter shipped, 14/14 tests pass — needs eyes on the user_id/IP fallback choice before merging",
|
||||
)
|
||||
```
|
||||
|
||||
Use `kanban_complete` only when the task is genuinely terminal — e.g. a one-line typo fix, a docs change with no functional consequences, or a research task where the artifact IS the writeup itself.
|
||||
|
||||
**Research task:**
|
||||
```python
|
||||
kanban_complete(
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
title: "Notion — Notion API via curl: pages, databases, blocks, search"
|
||||
title: "Notion — Notion API + ntn CLI: pages, databases, markdown, Workers"
|
||||
sidebar_label: "Notion"
|
||||
description: "Notion API via curl: pages, databases, blocks, search"
|
||||
description: "Notion API + ntn CLI: pages, databases, markdown, Workers"
|
||||
---
|
||||
|
||||
{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
|
||||
|
||||
# Notion
|
||||
|
||||
Notion API via curl: pages, databases, blocks, search.
|
||||
Notion API + ntn CLI: pages, databases, markdown, Workers.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
@@ -16,11 +16,11 @@ Notion API via curl: pages, databases, blocks, search.
|
||||
|---|---|
|
||||
| Source | Bundled (installed by default) |
|
||||
| Path | `skills/productivity/notion` |
|
||||
| Version | `1.0.0` |
|
||||
| Version | `2.0.0` |
|
||||
| Author | community |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos, windows |
|
||||
| Tags | `Notion`, `Productivity`, `Notes`, `Database`, `API` |
|
||||
| Tags | `Notion`, `Productivity`, `Notes`, `Database`, `API`, `CLI`, `Workers` |
|
||||
|
||||
## Reference: full SKILL.md
|
||||
|
||||
@@ -28,23 +28,146 @@ Notion API via curl: pages, databases, blocks, search.
|
||||
The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
|
||||
:::
|
||||
|
||||
# Notion API
|
||||
# Notion
|
||||
|
||||
Use the Notion API via curl to create, read, update pages, databases (data sources), and blocks. No extra tools needed — just curl and a Notion API key.
|
||||
Talk to Notion two ways. Same integration token works for both — pick by what's available.
|
||||
|
||||
## Prerequisites
|
||||
◆ **`ntn` CLI** — Notion's official CLI. Shorter syntax, one-line file uploads, required for Workers. macOS + Linux only as of May 2026 (Windows support "coming soon"). **Default when installed.**
|
||||
◆ **HTTP + curl** — works everywhere including Windows. **Default fallback** when `ntn` isn't installed.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Get an integration token (required for both paths)
|
||||
|
||||
1. Create an integration at https://notion.so/my-integrations
|
||||
2. Copy the API key (starts with `ntn_` or `secret_`)
|
||||
3. Store it in `~/.hermes/.env`:
|
||||
3. Store in `~/.hermes/.env`:
|
||||
```
|
||||
NOTION_API_KEY=ntn_your_key_here
|
||||
```
|
||||
4. **Important:** Share target pages/databases with your integration in Notion (click "..." → "Connect to" → your integration name)
|
||||
4. **Share target pages/databases with the integration** in Notion: page menu `...` → `Connect to` → your integration name. Without this, the API returns 404 for that page even though it exists.
|
||||
|
||||
### 2. Install `ntn` (preferred path on macOS / Linux)
|
||||
|
||||
```bash
|
||||
# Recommended
|
||||
curl -fsSL https://ntn.dev | bash
|
||||
|
||||
# Or via npm (needs Node 22+, npm 10+)
|
||||
npm install --global ntn
|
||||
|
||||
ntn --version # verify
|
||||
```
|
||||
|
||||
**Skip `ntn login` — use the integration token instead.** This works headlessly, no browser needed:
|
||||
```bash
|
||||
export NOTION_API_TOKEN=$NOTION_API_KEY # ntn reads NOTION_API_TOKEN
|
||||
export NOTION_KEYRING=0 # don't try to use the OS keychain
|
||||
```
|
||||
|
||||
Add those exports to your shell profile (or to `~/.hermes/.env`) so every session inherits them.
|
||||
|
||||
### 3. Choose path at runtime
|
||||
|
||||
```bash
|
||||
if command -v ntn >/dev/null 2>&1; then
|
||||
# use ntn
|
||||
else
|
||||
# fall back to curl
|
||||
fi
|
||||
```
|
||||
|
||||
Windows users: skip step 2 entirely until native `ntn` ships — Path B works fine. If you want CLI ergonomics now, install `ntn` inside WSL2.
|
||||
|
||||
## API Basics
|
||||
|
||||
All requests use this pattern:
|
||||
`Notion-Version: 2025-09-03` is required on all HTTP requests. `ntn` handles this for you. In this version, what users call "databases" are called **data sources** in the API.
|
||||
|
||||
## Path A — `ntn` CLI (preferred, macOS / Linux)
|
||||
|
||||
### Raw API calls (shorthand for curl)
|
||||
```bash
|
||||
ntn api v1/users # GET
|
||||
ntn api v1/pages parent[page_id]=abc123 \ # POST with inline body
|
||||
properties[title][0][text][content]="Notes"
|
||||
ntn api v1/pages/abc123 -X PATCH archived:=true # PATCH; := is non-string (bool/num/null)
|
||||
```
|
||||
|
||||
Syntax notes:
|
||||
- `key=value` — string fields
|
||||
- `key[nested]=value` — nested object fields
|
||||
- `key:=value` — typed assignment (booleans, numbers, null, arrays)
|
||||
|
||||
### Search
|
||||
```bash
|
||||
ntn api v1/search query="page title"
|
||||
```
|
||||
|
||||
### Read page metadata
|
||||
```bash
|
||||
ntn api v1/pages/{page_id}
|
||||
```
|
||||
|
||||
### Read page as Markdown (agent-friendly)
|
||||
```bash
|
||||
ntn api v1/pages/{page_id}/markdown
|
||||
```
|
||||
|
||||
### Read page content as blocks
|
||||
```bash
|
||||
ntn api v1/blocks/{page_id}/children
|
||||
```
|
||||
|
||||
### Create page from Markdown
|
||||
```bash
|
||||
ntn api v1/pages \
|
||||
parent[page_id]=xxx \
|
||||
properties[title][0][text][content]="Notes from meeting" \
|
||||
markdown="# Agenda
|
||||
|
||||
- Q3 roadmap
|
||||
- Hiring"
|
||||
```
|
||||
|
||||
### Patch a page with Markdown
|
||||
```bash
|
||||
ntn api v1/pages/{page_id}/markdown -X PATCH \
|
||||
markdown="## Update
|
||||
|
||||
Shipped the prototype."
|
||||
```
|
||||
|
||||
### Query a database (data source)
|
||||
```bash
|
||||
ntn api v1/data_sources/{data_source_id}/query -X POST \
|
||||
filter[property]=Status filter[select][equals]=Active
|
||||
```
|
||||
|
||||
For complex queries with `sorts`, multiple filter clauses, or compound logic, pipe JSON in:
|
||||
```bash
|
||||
echo '{"filter": {"property": "Status", "select": {"equals": "Active"}}, "sorts": [{"property": "Date", "direction": "descending"}]}' | \
|
||||
ntn api v1/data_sources/{data_source_id}/query -X POST --json -
|
||||
```
|
||||
|
||||
### File uploads (one-liner — biggest CLI win)
|
||||
```bash
|
||||
ntn files create < photo.png
|
||||
ntn files create --external-url https://example.com/photo.png
|
||||
ntn files list
|
||||
```
|
||||
|
||||
Compare to the 3-step HTTP flow (create upload → PUT bytes → reference).
|
||||
|
||||
### Useful env vars
|
||||
| Var | Effect |
|
||||
|---|---|
|
||||
| `NOTION_API_TOKEN` | Auth token (overrides keychain) — set this to your integration token |
|
||||
| `NOTION_KEYRING=0` | File-based creds at `~/.config/notion/auth.json` instead of OS keychain |
|
||||
| `NOTION_WORKSPACE_ID` | Skip the workspace picker prompt |
|
||||
|
||||
## Path B — HTTP + curl (cross-platform, default on Windows)
|
||||
|
||||
All requests share this pattern:
|
||||
|
||||
```bash
|
||||
curl -s -X GET "https://api.notion.com/v1/..." \
|
||||
@@ -53,12 +176,9 @@ curl -s -X GET "https://api.notion.com/v1/..." \
|
||||
-H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
The `Notion-Version` header is required. This skill uses `2025-09-03` (latest). In this version, databases are called "data sources" in the API.
|
||||
|
||||
## Common Operations
|
||||
On Windows the `curl` shipped with Windows 10+ works as-is. PowerShell users can also use `Invoke-RestMethod`.
|
||||
|
||||
### Search
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://api.notion.com/v1/search" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
@@ -67,24 +187,56 @@ curl -s -X POST "https://api.notion.com/v1/search" \
|
||||
-d '{"query": "page title"}'
|
||||
```
|
||||
|
||||
### Get Page
|
||||
|
||||
### Read page metadata
|
||||
```bash
|
||||
curl -s "https://api.notion.com/v1/pages/{page_id}" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
-H "Notion-Version: 2025-09-03"
|
||||
```
|
||||
|
||||
### Get Page Content (blocks)
|
||||
### Read page as Markdown (agent-friendly)
|
||||
|
||||
Easier to feed to a model than block JSON.
|
||||
|
||||
```bash
|
||||
curl -s "https://api.notion.com/v1/pages/{page_id}/markdown" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
-H "Notion-Version: 2025-09-03"
|
||||
```
|
||||
|
||||
### Read page content as blocks (when you need structure)
|
||||
```bash
|
||||
curl -s "https://api.notion.com/v1/blocks/{page_id}/children" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
-H "Notion-Version: 2025-09-03"
|
||||
```
|
||||
|
||||
### Create Page in a Database
|
||||
### Create page from Markdown
|
||||
|
||||
`POST /v1/pages` accepts a `markdown` body param.
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://api.notion.com/v1/pages" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
-H "Notion-Version: 2025-09-03" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"parent": {"page_id": "xxx"},
|
||||
"properties": {"title": [{"text": {"content": "Notes from meeting"}}]},
|
||||
"markdown": "# Agenda\n\n- Q3 roadmap\n- Hiring\n\n## Decisions\n- Ship MVP Friday"
|
||||
}'
|
||||
```
|
||||
|
||||
### Patch a page with Markdown
|
||||
```bash
|
||||
curl -s -X PATCH "https://api.notion.com/v1/pages/{page_id}/markdown" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
-H "Notion-Version: 2025-09-03" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"markdown": "## Update\n\nShipped the prototype."}'
|
||||
```
|
||||
|
||||
### Create page in a database (typed properties)
|
||||
```bash
|
||||
curl -s -X POST "https://api.notion.com/v1/pages" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
@@ -99,8 +251,7 @@ curl -s -X POST "https://api.notion.com/v1/pages" \
|
||||
}'
|
||||
```
|
||||
|
||||
### Query a Database
|
||||
|
||||
### Query a database (data source)
|
||||
```bash
|
||||
curl -s -X POST "https://api.notion.com/v1/data_sources/{data_source_id}/query" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
@@ -112,8 +263,7 @@ curl -s -X POST "https://api.notion.com/v1/data_sources/{data_source_id}/query"
|
||||
}'
|
||||
```
|
||||
|
||||
### Create a Database
|
||||
|
||||
### Create a database
|
||||
```bash
|
||||
curl -s -X POST "https://api.notion.com/v1/data_sources" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
@@ -130,8 +280,7 @@ curl -s -X POST "https://api.notion.com/v1/data_sources" \
|
||||
}'
|
||||
```
|
||||
|
||||
### Update Page Properties
|
||||
|
||||
### Update page properties
|
||||
```bash
|
||||
curl -s -X PATCH "https://api.notion.com/v1/pages/{page_id}" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
@@ -140,8 +289,7 @@ curl -s -X PATCH "https://api.notion.com/v1/pages/{page_id}" \
|
||||
-d '{"properties": {"Status": {"select": {"name": "Done"}}}}'
|
||||
```
|
||||
|
||||
### Add Content to a Page
|
||||
|
||||
### Append blocks to a page
|
||||
```bash
|
||||
curl -s -X PATCH "https://api.notion.com/v1/blocks/{page_id}/children" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
@@ -154,6 +302,21 @@ curl -s -X PATCH "https://api.notion.com/v1/blocks/{page_id}/children" \
|
||||
}'
|
||||
```
|
||||
|
||||
### File uploads (3-step flow)
|
||||
```bash
|
||||
# 1. Create upload
|
||||
curl -s -X POST "https://api.notion.com/v1/file_uploads" \
|
||||
-H "Authorization: Bearer $NOTION_API_KEY" \
|
||||
-H "Notion-Version: 2025-09-03" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"filename": "photo.png", "content_type": "image/png"}'
|
||||
|
||||
# 2. PUT bytes to the upload_url returned above
|
||||
curl -s -X PUT "{upload_url}" --data-binary @photo.png
|
||||
|
||||
# 3. Reference {file_upload_id} in a page/block payload
|
||||
```
|
||||
|
||||
## Property Types
|
||||
|
||||
Common property formats for database items:
|
||||
@@ -169,19 +332,132 @@ Common property formats for database items:
|
||||
- **Email:** `{"email": "user@example.com"}`
|
||||
- **Relation:** `{"relation": [{"id": "page_id"}]}`
|
||||
|
||||
## Key Differences in API Version 2025-09-03
|
||||
## API Version 2025-09-03 — Databases vs Data Sources
|
||||
|
||||
- **Databases → Data Sources:** Use `/data_sources/` endpoints for queries and retrieval
|
||||
- **Two IDs:** Each database has both a `database_id` and a `data_source_id`
|
||||
- Use `database_id` when creating pages (`parent: {"database_id": "..."}`)
|
||||
- Use `data_source_id` when querying (`POST /v1/data_sources/{id}/query`)
|
||||
- **Search results:** Databases return as `"object": "data_source"` with their `data_source_id`
|
||||
- **Databases became data sources.** Use `/data_sources/` endpoints for queries and retrieval.
|
||||
- **Two IDs per database:** `database_id` and `data_source_id`.
|
||||
- `database_id` when creating pages: `parent: {"database_id": "..."}`
|
||||
- `data_source_id` when querying: `POST /v1/data_sources/{id}/query`
|
||||
- Search returns databases as `"object": "data_source"` with the `data_source_id` field.
|
||||
|
||||
## Notion Workers (advanced, requires `ntn`)
|
||||
|
||||
Workers are TypeScript programs Notion hosts for you. One worker can expose any combination of:
|
||||
- **Syncs** — pull data from external APIs into a Notion database on a schedule (default 30 min).
|
||||
- **Tools** — appear as callable tools inside Notion's Custom Agents.
|
||||
- **Webhooks** — receive HTTP events from external services (GitHub, Stripe, etc.) and act in Notion.
|
||||
|
||||
**Plan / platform gating:**
|
||||
- CLI works on all plans. **Deploying Workers requires Business or Enterprise.**
|
||||
- `ntn` is macOS/Linux only as of May 2026. Windows users need WSL2 or to wait for native support.
|
||||
- Free through August 11, 2026; metered on Notion credits after.
|
||||
|
||||
### Minimal Worker
|
||||
|
||||
```bash
|
||||
ntn workers new my-worker # scaffold
|
||||
cd my-worker
|
||||
# Edit src/index.ts
|
||||
ntn workers deploy --name my-worker
|
||||
```
|
||||
|
||||
`src/index.ts`:
|
||||
```typescript
|
||||
import { Worker } from "@notionhq/workers";
|
||||
|
||||
const worker = new Worker();
|
||||
export default worker;
|
||||
|
||||
worker.tool("greet", {
|
||||
title: "Greet a User",
|
||||
description: "Returns a friendly greeting",
|
||||
inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
|
||||
execute: async ({ name }) => `Hello, ${name}!`,
|
||||
});
|
||||
```
|
||||
|
||||
### Webhook capability
|
||||
|
||||
```typescript
|
||||
worker.webhook("onGithubPush", {
|
||||
title: "GitHub Push Handler",
|
||||
execute: async (events, { notion }) => {
|
||||
for (const event of events) {
|
||||
// event.body, event.rawBody (for signature verification), event.headers
|
||||
console.log("got delivery", event.deliveryId);
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
After deploy: `ntn workers webhooks list` shows the URL Notion generates. Treat that URL as a secret — anyone with it can POST events unless you add signature verification.
|
||||
|
||||
### Worker lifecycle commands
|
||||
|
||||
```bash
|
||||
ntn workers deploy
|
||||
ntn workers list
|
||||
ntn workers exec <capability-key> -d '{"name": "world"}'
|
||||
ntn workers sync trigger <key> # run a sync now
|
||||
ntn workers sync pause <key>
|
||||
ntn workers env set GITHUB_WEBHOOK_SECRET=...
|
||||
ntn workers runs list # recent invocations
|
||||
ntn workers runs logs <run-id>
|
||||
ntn workers webhooks list
|
||||
```
|
||||
|
||||
When asked to build a Worker, scaffold with `ntn workers new`, write the code in `src/index.ts`, set any secrets with `ntn workers env set`, and deploy. Notion's docs at https://developers.notion.com/workers cover the full API surface.
|
||||
|
||||
## Notion-Flavored Markdown (used by `/markdown` endpoints)
|
||||
|
||||
Standard CommonMark plus XML-like tags for Notion-specific blocks. Use **tabs** for indentation.
|
||||
|
||||
**Blocks beyond CommonMark:**
|
||||
```
|
||||
<callout icon="🎯" color="blue_bg">
|
||||
Ship the MVP by **Friday**.
|
||||
</callout>
|
||||
|
||||
<details color="gray">
|
||||
<summary>Toggle title</summary>
|
||||
Children indented one tab
|
||||
</details>
|
||||
|
||||
<columns>
|
||||
<column>Left side</column>
|
||||
<column>Right side</column>
|
||||
</columns>
|
||||
|
||||
<table_of_contents color="gray"/>
|
||||
```
|
||||
|
||||
**Inline:**
|
||||
- Mentions: `<mention-user url="..."/>`, `<mention-page url="...">Title</mention-page>`, `<mention-date start="2026-05-15"/>`
|
||||
- Underline: `<span underline="true">text</span>`
|
||||
- Color: `<span color="blue">text</span>` or block-level `{color="blue"}` on the first line
|
||||
- Math: inline `$x^2$`, block `$$ ... $$`
|
||||
- Citations: `[^https://example.com]`
|
||||
|
||||
**Colors:** `gray brown orange yellow green blue purple pink red`, plus `*_bg` variants for backgrounds.
|
||||
|
||||
Headings 5/6 collapse to H4. Multiple `>` lines render as separate quote blocks — use `<br>` inside a single `>` for multi-line quotes.
|
||||
|
||||
## Choosing the Right Path
|
||||
|
||||
| Task | mac / Linux | Windows |
|
||||
|---|---|---|
|
||||
| Read/write pages, search, query databases | `ntn api ...` | curl |
|
||||
| Read a page for an agent to summarize | `ntn api v1/pages/{id}/markdown` | curl `/markdown` endpoint |
|
||||
| Upload a file | `ntn files create < file` | 3-step HTTP flow |
|
||||
| One-off API exploration | `ntn api ...` | curl |
|
||||
| Build a sync / webhook / agent tool hosted by Notion | `ntn workers ...` | WSL2 + `ntn workers ...` |
|
||||
|
||||
## Notes
|
||||
|
||||
- Page/database IDs are UUIDs (with or without dashes)
|
||||
- Rate limit: ~3 requests/second average
|
||||
- The API cannot set database view filters — that's UI-only
|
||||
- Use `is_inline: true` when creating data sources to embed them in pages
|
||||
- Add `-s` flag to curl to suppress progress bars (cleaner output for Hermes)
|
||||
- Pipe output through `jq` for readable JSON: `... | jq '.results[0].properties'`
|
||||
- Page/database IDs are UUIDs (with or without dashes — both accepted).
|
||||
- Rate limit: ~3 requests/second average. The CLI doesn't bypass this.
|
||||
- The API cannot set database **view** filters — that's UI-only.
|
||||
- Use `"is_inline": true` when creating data sources to embed them in a page.
|
||||
- Always pass `-s` to curl to suppress progress bars (cleaner agent output).
|
||||
- Pipe JSON through `jq` when reading: `... | jq '.results[0].properties'`.
|
||||
- Notion also ships an MCP server now (`Notion MCP`, ~91% more token-efficient on DB ops than the previous version) — wire it via Hermes' MCP support if you want streaming Notion access from inside a session, but the paths above are enough for most one-shot tasks.
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: "Hyperliquid — Hyperliquid market data, account history, trade review"
|
||||
sidebar_label: "Hyperliquid"
|
||||
description: "Hyperliquid market data, account history, trade review"
|
||||
---
|
||||
|
||||
{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
|
||||
|
||||
# Hyperliquid
|
||||
|
||||
Hyperliquid market data, account history, trade review.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/blockchain/hyperliquid` |
|
||||
| Path | `optional-skills/blockchain/hyperliquid` |
|
||||
| Version | `0.1.0` |
|
||||
| Author | Hugo Sequier (Hugo-SEQUIER), Hermes Agent |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos, windows |
|
||||
| Tags | `Hyperliquid`, `Blockchain`, `Crypto`, `Trading`, `Perpetuals`, `Spot`, `DeFi` |
|
||||
|
||||
## Reference: full SKILL.md
|
||||
|
||||
:::info
|
||||
The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
|
||||
:::
|
||||
|
||||
# Hyperliquid Skill
|
||||
|
||||
Query Hyperliquid market and account data through the public `/info` endpoint.
|
||||
Read-only — no API key, no signing, no order placement.
|
||||
|
||||
12 commands: `dexs`, `markets`, `spots`, `candles`, `funding`, `l2`, `state`,
|
||||
`spot-balances`, `fills`, `orders`, `review`, `export`. Stdlib only
|
||||
(`urllib`, `json`, `argparse`).
|
||||
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
- User asks for Hyperliquid perp or spot market data, candles, funding, or L2 book
|
||||
- User wants to inspect a wallet's perp positions, spot balances, fills, or orders
|
||||
- User wants a post-trade review combining recent fills with market context
|
||||
- User wants to inspect builder-deployed perp dexs or HIP-3 markets
|
||||
- User wants a normalized JSON export of candles + funding for backtesting prep
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Stdlib only — no external packages, no API key.
|
||||
|
||||
The script reads `~/.hermes/.env` for two optional defaults:
|
||||
|
||||
- `HYPERLIQUID_API_URL` — defaults to `https://api.hyperliquid.xyz`. Set to
|
||||
`https://api.hyperliquid-testnet.xyz` for testnet.
|
||||
- `HYPERLIQUID_USER_ADDRESS` — default address for `state`, `spot-balances`,
|
||||
`fills`, `orders`, and `review`. If unset, pass the address as the first
|
||||
positional argument.
|
||||
|
||||
A project `.env` in the current working directory is honored as a dev fallback.
|
||||
|
||||
Helper script: `~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py`
|
||||
|
||||
---
|
||||
|
||||
## How to Run
|
||||
|
||||
Invoke through the `terminal` tool:
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py <command> [args]
|
||||
```
|
||||
|
||||
Add `--json` to any command for machine-readable output.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
hyperliquid_client.py dexs
|
||||
hyperliquid_client.py markets [--dex DEX] [--limit N] [--sort volume|oi|funding_abs|change_abs|name]
|
||||
hyperliquid_client.py spots [--limit N]
|
||||
hyperliquid_client.py candles <coin> [--interval 1h] [--hours 24] [--limit N]
|
||||
hyperliquid_client.py funding <coin> [--hours 72] [--limit N]
|
||||
hyperliquid_client.py l2 <coin> [--levels N]
|
||||
hyperliquid_client.py state [address] [--dex DEX]
|
||||
hyperliquid_client.py spot-balances [address] [--limit N]
|
||||
hyperliquid_client.py fills [address] [--hours N] [--limit N] [--aggregate-by-time]
|
||||
hyperliquid_client.py orders [address] [--limit N]
|
||||
hyperliquid_client.py review [address] [--coin COIN] [--hours N] [--fills N]
|
||||
hyperliquid_client.py export <coin> [--interval 1h] [--hours N] [--output PATH]
|
||||
```
|
||||
|
||||
For `state`, `spot-balances`, `fills`, `orders`, and `review`, the address is
|
||||
optional when `HYPERLIQUID_USER_ADDRESS` is set in `~/.hermes/.env`.
|
||||
|
||||
---
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Discover DEXs and Markets
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py dexs
|
||||
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
markets --limit 15 --sort volume
|
||||
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
spots --limit 15
|
||||
```
|
||||
|
||||
- `--dex` only applies to perp endpoints; omit for the first perp dex.
|
||||
- Spot pairs may show as `PURR/USDC` or aliases like `@107`.
|
||||
- HIP-3 markets prefix the coin with the dex, e.g. `mydex:BTC`.
|
||||
|
||||
### 2. Pull Historical Market Data
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
candles BTC --interval 1h --hours 72 --limit 48
|
||||
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
funding BTC --hours 168 --limit 30
|
||||
```
|
||||
|
||||
Time-range endpoints paginate. For larger windows, repeat with a later
|
||||
`startTime` or use `export` (below).
|
||||
|
||||
### 3. Inspect Live Order Book
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
l2 BTC --levels 10
|
||||
```
|
||||
|
||||
Use when asked about book depth, near-term liquidity, or potential market
|
||||
impact of a large order.
|
||||
|
||||
### 4. Review an Account
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
state 0xabc...
|
||||
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
spot-balances
|
||||
```
|
||||
|
||||
`state` returns perp positions; `spot-balances` returns spot inventory.
|
||||
Use these for "how are my positions?", "what am I holding?", "how much is
|
||||
withdrawable?".
|
||||
|
||||
### 5. Review Fills and Orders
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
fills 0xabc... --hours 72 --limit 25
|
||||
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
orders --limit 25
|
||||
```
|
||||
|
||||
### 6. Generate a Trade Review
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
review 0xabc... --hours 72 --fills 50
|
||||
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
review --coin BTC --hours 168
|
||||
```
|
||||
|
||||
Reports realized PnL, fees, win/loss counts, coin breakdowns, market trend
|
||||
and average funding for each traded perp, plus heuristics (fee drag,
|
||||
concentration, counter-trend losses).
|
||||
|
||||
For deeper post-trade analysis: start with `review` to find problem coins
|
||||
or windows → pull `fills` and `orders` for that period → pull `candles`
|
||||
and `funding` for each traded coin → judge decision quality separately
|
||||
from outcome quality.
|
||||
|
||||
### 7. Export a Reusable Dataset
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
export BTC --interval 1h --hours 168 --output ./btc-1h-7d.json
|
||||
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
export BTC --interval 15m --hours 72 --end-time-ms 1760000000000
|
||||
```
|
||||
|
||||
Output JSON contains: schema version, source metadata, exact time window,
|
||||
normalized candle rows, normalized funding rows, summary stats. Use
|
||||
`--end-time-ms` for reproducible windows.
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Public info endpoints are rate-limited. Large historical queries may
|
||||
return capped windows; iterate with later `startTime` values.
|
||||
- `fills --hours ...` uses `userFillsByTime`, which only exposes a
|
||||
recent rolling window — not full archive history.
|
||||
- `historicalOrders` returns recent orders only; not a full export.
|
||||
- The `review` command is heuristic. It cannot reconstruct intent,
|
||||
order placement quality, or true slippage from fills alone.
|
||||
- The `export` command writes a normalized dataset, not a backtest
|
||||
engine. You still need your own slippage/fill model.
|
||||
- Spot aliases like `@107` are valid identifiers even when the UI shows
|
||||
a friendlier name.
|
||||
- `l2` is a point-in-time snapshot, not a time series.
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
python3 ~/.hermes/skills/blockchain/hyperliquid/scripts/hyperliquid_client.py \
|
||||
markets --limit 5
|
||||
```
|
||||
|
||||
Should print the top Hyperliquid perp markets by 24h notional volume.
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: "Stocks — Stock quotes, history, search, compare, crypto via Yahoo"
|
||||
sidebar_label: "Stocks"
|
||||
description: "Stock quotes, history, search, compare, crypto via Yahoo"
|
||||
---
|
||||
|
||||
{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
|
||||
|
||||
# Stocks
|
||||
|
||||
Stock quotes, history, search, compare, crypto via Yahoo.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/finance/stocks` |
|
||||
| Path | `optional-skills/finance/stocks` |
|
||||
| Version | `0.1.0` |
|
||||
| Author | Mibay (Mibayy), Hermes Agent |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos, windows |
|
||||
| Tags | `Stocks`, `Finance`, `Market`, `Crypto`, `Investing` |
|
||||
| Related skills | [`dcf-model`](/docs/user-guide/skills/optional/finance/finance-dcf-model), [`comps-analysis`](/docs/user-guide/skills/optional/finance/finance-comps-analysis), [`lbo-model`](/docs/user-guide/skills/optional/finance/finance-lbo-model) |
|
||||
|
||||
## Reference: full SKILL.md
|
||||
|
||||
:::info
|
||||
The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
|
||||
:::
|
||||
|
||||
# Stocks Skill
|
||||
|
||||
Read-only market data via Yahoo Finance. Five commands: `quote`, `search`,
|
||||
`history`, `compare`, `crypto`. Python stdlib only — no API key, no pip
|
||||
installs. Yahoo's endpoint is unofficial and may rate-limit or change.
|
||||
|
||||
## When to Use
|
||||
|
||||
- User asks for a current stock price (AAPL, TSLA, MSFT, ...)
|
||||
- User wants to look up a ticker by company name
|
||||
- User wants OHLCV history or performance over a date range
|
||||
- User wants to compare several tickers side by side
|
||||
- User asks for a crypto price (BTC, ETH, SOL, ...)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Python 3.8+ stdlib only. Optional: set `ALPHA_VANTAGE_KEY` to enrich
|
||||
`market_cap`, `pe_ratio`, and 52-week levels when Yahoo's crumb-protected
|
||||
fields come back null. Free key: https://www.alphavantage.co/support/#api-key
|
||||
|
||||
## How to Run
|
||||
|
||||
Invoke through the `terminal` tool. Once installed:
|
||||
|
||||
```
|
||||
SCRIPT=~/.hermes/skills/finance/stocks/scripts/stocks_client.py
|
||||
python3 $SCRIPT quote AAPL
|
||||
```
|
||||
|
||||
All output is JSON on stdout — pipe through `jq` if you want to slice it.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```
|
||||
python3 $SCRIPT quote AAPL
|
||||
python3 $SCRIPT quote AAPL MSFT GOOGL TSLA
|
||||
python3 $SCRIPT search "Tesla"
|
||||
python3 $SCRIPT history NVDA --range 6mo
|
||||
python3 $SCRIPT compare AAPL MSFT GOOGL
|
||||
python3 $SCRIPT crypto BTC ETH SOL
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `quote SYMBOL [SYMBOL2 ...]`
|
||||
|
||||
Current price, change, change%, volume, 52-week high/low.
|
||||
|
||||
### `search QUERY`
|
||||
|
||||
Find tickers by company name. Returns top 5: symbol, name, exchange, type.
|
||||
|
||||
### `history SYMBOL [--range RANGE]`
|
||||
|
||||
Daily OHLCV plus stats (min, max, avg, total return %). Ranges: `1mo`,
|
||||
`3mo`, `6mo`, `1y`, `5y`. Default: `1mo`.
|
||||
|
||||
### `compare SYMBOL1 SYMBOL2 [...]`
|
||||
|
||||
Side-by-side: price, change%, 52-week performance.
|
||||
|
||||
### `crypto SYMBOL [SYMBOL2 ...]`
|
||||
|
||||
Crypto prices. Pass `BTC` (the script appends `-USD` automatically).
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Yahoo Finance's API is unofficial. Endpoints can change or rate-limit
|
||||
without notice — if requests start failing, that's why.
|
||||
- `market_cap` and `pe_ratio` may return null on `quote` when Yahoo's
|
||||
crumb session isn't established. Set `ALPHA_VANTAGE_KEY` to backfill.
|
||||
- Add a small delay between bulk requests to avoid rate-limiting.
|
||||
- This is read-only — no order placement, no account integration.
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
python3 ~/.hermes/skills/finance/stocks/scripts/stocks_client.py quote AAPL
|
||||
```
|
||||
|
||||
Returns a JSON object with `symbol: "AAPL"` and a numeric `price` field.
|
||||
+531
@@ -0,0 +1,531 @@
|
||||
---
|
||||
title: "Rest Graphql Debug — Debug REST/GraphQL APIs: status codes, auth, schemas, repro"
|
||||
sidebar_label: "Rest Graphql Debug"
|
||||
description: "Debug REST/GraphQL APIs: status codes, auth, schemas, repro"
|
||||
---
|
||||
|
||||
{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
|
||||
|
||||
# Rest Graphql Debug
|
||||
|
||||
Debug REST/GraphQL APIs: status codes, auth, schemas, repro.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/software-development/rest-graphql-debug` |
|
||||
| Path | `optional-skills/software-development/rest-graphql-debug` |
|
||||
| Version | `1.2.0` |
|
||||
| Author | eren-karakus0 |
|
||||
| License | MIT |
|
||||
| Tags | `api`, `rest`, `graphql`, `http`, `debugging`, `testing`, `curl`, `integration` |
|
||||
| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`test-driven-development`](/docs/user-guide/skills/bundled/software-development/software-development-test-driven-development) |
|
||||
|
||||
## Reference: full SKILL.md
|
||||
|
||||
:::info
|
||||
The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
|
||||
:::
|
||||
|
||||
# API Testing & Debugging
|
||||
|
||||
Drive REST and GraphQL diagnosis through Hermes tools — `terminal` for `curl`, `execute_code` for Python `requests`, `web_extract` for vendor docs. Isolate the failing layer before guessing at the fix.
|
||||
|
||||
## When to Use
|
||||
|
||||
- API returns unexpected status or body
|
||||
- Auth fails (401/403 after token refresh, OAuth, API key)
|
||||
- Works in Postman but fails in code
|
||||
- Webhook / callback integration debugging
|
||||
- Building or reviewing API integration tests
|
||||
- Rate limiting or pagination issues
|
||||
|
||||
Skip for UI rendering, DB query tuning, or DNS/firewall infra (escalate).
|
||||
|
||||
## Core Principle
|
||||
|
||||
**Isolate the layer, then fix.** A 200 OK can hide broken data. A 500 can mask a one-character auth typo. Walk the chain in order; never skip a step.
|
||||
|
||||
```
|
||||
1. Connectivity → can we reach the host at all?
|
||||
1.5 Timeouts → connect-slow vs read-slow?
|
||||
2. TLS/SSL → cert valid and trusted?
|
||||
3. Auth → credentials correct and unexpired?
|
||||
4. Request format → payload shape match server expectations?
|
||||
5. Response parse → does our code accept what came back?
|
||||
6. Semantics → does the data mean what we assume?
|
||||
```
|
||||
|
||||
## 5-Minute Quickstart
|
||||
|
||||
### REST via terminal
|
||||
|
||||
```python
|
||||
# Verbose request/response exchange
|
||||
terminal('curl -v https://api.example.com/users/1')
|
||||
|
||||
# POST with JSON
|
||||
terminal("""curl -X POST https://api.example.com/users \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-H "Authorization: Bearer $TOKEN" \\
|
||||
-d '{"name":"test","email":"test@example.com"}'""")
|
||||
|
||||
# Headers only
|
||||
terminal('curl -sI https://api.example.com/health')
|
||||
|
||||
# Pretty-print JSON
|
||||
terminal('curl -s https://api.example.com/users | python3 -m json.tool')
|
||||
```
|
||||
|
||||
### GraphQL via terminal
|
||||
|
||||
```python
|
||||
terminal("""curl -X POST https://api.example.com/graphql \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-H "Authorization: Bearer $TOKEN" \\
|
||||
-d '{"query":"{ user(id: 1) { name email } }"}'""")
|
||||
```
|
||||
|
||||
**GraphQL gotcha:** servers often return HTTP 200 even when the query failed. Always inspect the `errors` field regardless of status code:
|
||||
|
||||
```python
|
||||
execute_code('''
|
||||
import os, requests
|
||||
resp = requests.post(
|
||||
"https://api.example.com/graphql",
|
||||
json={"query": "{ user(id: 1) { name email } }"},
|
||||
headers={"Authorization": f"Bearer {os.environ['TOKEN']}"},
|
||||
timeout=10,
|
||||
)
|
||||
data = resp.json()
|
||||
if data.get("errors"):
|
||||
for err in data["errors"]:
|
||||
print(f"GraphQL error: {err['message']} (path: {err.get('path')})")
|
||||
print(data.get("data"))
|
||||
''')
|
||||
```
|
||||
|
||||
### Python (requests) via execute_code
|
||||
|
||||
```python
|
||||
execute_code('''
|
||||
import requests
|
||||
resp = requests.get(
|
||||
"https://api.example.com/users/1",
|
||||
headers={"Authorization": "Bearer <TOKEN>"},
|
||||
timeout=(3.05, 30), # (connect, read)
|
||||
)
|
||||
print(resp.status_code, dict(resp.headers))
|
||||
print(resp.text[:500])
|
||||
''')
|
||||
```
|
||||
|
||||
## Layered Debug Flow
|
||||
|
||||
### Step 1 — Connectivity
|
||||
|
||||
```python
|
||||
terminal('nslookup api.example.com')
|
||||
terminal('curl -v --connect-timeout 5 https://api.example.com/health')
|
||||
```
|
||||
|
||||
Failures: DNS not resolving, firewall, VPN required, proxy missing.
|
||||
|
||||
### Step 1.5 — Timeouts
|
||||
|
||||
Distinguish *can't reach* from *reaches but slow*:
|
||||
|
||||
```python
|
||||
terminal('''curl -w "dns:%{time_namelookup}s connect:%{time_connect}s tls:%{time_appconnect}s ttfb:%{time_starttransfer}s total:%{time_total}s\\n" \\
|
||||
-o /dev/null -s https://api.example.com/endpoint''')
|
||||
```
|
||||
|
||||
In Python, always pass a tuple timeout — `requests` has no default and will hang forever:
|
||||
|
||||
```python
|
||||
execute_code('''
|
||||
import requests
|
||||
from requests.exceptions import ConnectTimeout, ReadTimeout
|
||||
try:
|
||||
requests.get(url, timeout=(3.05, 30))
|
||||
except ConnectTimeout:
|
||||
print("Cannot reach host — DNS, firewall, VPN")
|
||||
except ReadTimeout:
|
||||
print("Connected but server is slow")
|
||||
''')
|
||||
```
|
||||
|
||||
Diagnosis: high `time_connect` is network/firewall; high `time_starttransfer` with low `time_connect` is a slow server.
|
||||
|
||||
### Step 2 — TLS/SSL
|
||||
|
||||
```python
|
||||
terminal('curl -vI https://api.example.com 2>&1 | grep -E "SSL|subject|expire|issuer"')
|
||||
```
|
||||
|
||||
Failures: expired cert, self-signed, hostname mismatch, missing CA bundle. Use `-k` only for ad-hoc debug, never in code.
|
||||
|
||||
### Step 3 — Authentication
|
||||
|
||||
```python
|
||||
# Token validity check
|
||||
terminal('curl -s -o /dev/null -w "%{http_code}\\n" -H "Authorization: Bearer $TOKEN" https://api.example.com/me')
|
||||
|
||||
# Decode JWT exp claim — handles base64url padding correctly
|
||||
execute_code('''
|
||||
import json, base64, os
|
||||
tok = os.environ["TOKEN"]
|
||||
payload = tok.split(".")[1]
|
||||
payload += "=" * (-len(payload) % 4)
|
||||
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
|
||||
''')
|
||||
```
|
||||
|
||||
Checklist:
|
||||
- Token expired? (`exp` claim in JWT)
|
||||
- Right scheme? Bearer vs Basic vs Token vs `X-Api-Key`
|
||||
- Right environment? Staging key on prod is a classic
|
||||
- API key in header vs query param (`?api_key=…`)?
|
||||
|
||||
### Step 4 — Request Format
|
||||
|
||||
```python
|
||||
terminal("""curl -v -X POST https://api.example.com/endpoint \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '{"key":"value"}' 2>&1""")
|
||||
```
|
||||
|
||||
**Content-Type / body mismatch — the silent 415/400:**
|
||||
|
||||
```python
|
||||
# WRONG — data= sends form-encoded, header lies
|
||||
requests.post(url, data='{"k":"v"}', headers={"Content-Type": "application/json"})
|
||||
|
||||
# RIGHT — json= auto-sets header AND serializes
|
||||
requests.post(url, json={"k": "v"})
|
||||
|
||||
# WRONG — Accept says XML, code calls .json()
|
||||
requests.get(url, headers={"Accept": "text/xml"})
|
||||
|
||||
# RIGHT — let requests build multipart with boundary
|
||||
requests.post(url, files={"file": open("doc.pdf", "rb")})
|
||||
```
|
||||
|
||||
Common: form-encoded vs JSON, missing required fields, wrong HTTP method, unencoded query params.
|
||||
|
||||
### Step 5 — Response Parsing
|
||||
|
||||
Always inspect content-type before calling `.json()`:
|
||||
|
||||
```python
|
||||
execute_code('''
|
||||
import requests
|
||||
resp = requests.post(url, json=payload, timeout=10)
|
||||
print(f"status={resp.status_code}")
|
||||
print(f"headers={dict(resp.headers)}")
|
||||
ct = resp.headers.get("Content-Type", "")
|
||||
if "application/json" in ct:
|
||||
print(resp.json())
|
||||
else:
|
||||
print(f"unexpected content-type {ct!r}, body={resp.text[:500]!r}")
|
||||
''')
|
||||
```
|
||||
|
||||
Failures: HTML error page where JSON expected, empty body, wrong charset.
|
||||
|
||||
### Step 6 — Semantic Validation
|
||||
|
||||
Parsed cleanly — but is the data *correct*?
|
||||
|
||||
- Does `"status": "active"` mean what your code thinks?
|
||||
- ID in response matches the one requested?
|
||||
- Timestamps in expected timezone?
|
||||
- Pagination returning all results, or just page 1?
|
||||
|
||||
## HTTP Status Playbook
|
||||
|
||||
### 401 Unauthorized — credentials missing or invalid
|
||||
|
||||
1. `Authorization` header actually present? (`curl -v` to confirm)
|
||||
2. Token correct and unexpired?
|
||||
3. Right auth scheme? (`Bearer` vs `Basic` vs `Token`)
|
||||
4. Some APIs use query param (`?api_key=…`) instead of header.
|
||||
|
||||
### 403 Forbidden — authenticated but not authorized
|
||||
|
||||
1. Token has the required scopes/permissions?
|
||||
2. Resource owned by a different account?
|
||||
3. IP allowlist blocking you?
|
||||
4. CORS in browser? (check `Access-Control-Allow-Origin`)
|
||||
|
||||
### 404 Not Found — resource doesn't exist or URL is wrong
|
||||
|
||||
1. Path correct? (trailing slash, typo, version prefix)
|
||||
2. Resource ID exists?
|
||||
3. Right API version (`/v1/` vs `/v2/`)?
|
||||
4. Right base URL (staging vs prod)?
|
||||
|
||||
### 409 Conflict — state collision
|
||||
|
||||
1. Resource already exists (duplicate create)?
|
||||
2. Stale `ETag` / `If-Match`?
|
||||
3. Concurrent modification by another process?
|
||||
|
||||
### 422 Unprocessable Entity — valid JSON, invalid data
|
||||
|
||||
The error body usually names the bad fields. Check:
|
||||
- Field types (string vs int, date format)
|
||||
- Required vs optional
|
||||
- Enum values inside the allowed set
|
||||
|
||||
### 429 Too Many Requests — rate limited
|
||||
|
||||
Check `Retry-After` and `X-RateLimit-*` headers. Exponential backoff:
|
||||
|
||||
```python
|
||||
execute_code('''
|
||||
import time, requests
|
||||
|
||||
def with_backoff(method, url, **kwargs):
|
||||
for attempt in range(5):
|
||||
resp = requests.request(method, url, **kwargs)
|
||||
if resp.status_code != 429:
|
||||
return resp
|
||||
wait = int(resp.headers.get("Retry-After", 2 ** attempt))
|
||||
time.sleep(wait)
|
||||
return resp
|
||||
''')
|
||||
```
|
||||
|
||||
### 5xx — server-side, usually not your fault
|
||||
|
||||
- **500** — server bug. Capture correlation ID, file with provider.
|
||||
- **502** — upstream down. Backoff + retry.
|
||||
- **503** — overloaded / maintenance. Check status page.
|
||||
- **504** — upstream timeout. Reduce payload or raise timeout.
|
||||
|
||||
For all 5xx: backoff with jitter, alert on persistence.
|
||||
|
||||
## Pagination & Idempotency
|
||||
|
||||
**Pagination.** Verify you're getting *all* results. Look for `next_cursor`, `next_page`, `total_count`. Two patterns:
|
||||
- Offset (`?limit=100&offset=200`) — simple, can skip items if data shifts.
|
||||
- Cursor (`?cursor=abc123`) — preferred for live or large datasets.
|
||||
|
||||
**Idempotency.** For non-idempotent operations (POST), send `Idempotency-Key: <uuid>` so retries don't double-charge / double-create. Mandatory for payments and orders.
|
||||
|
||||
## Contract Validation
|
||||
|
||||
Catch schema drift before it hits production:
|
||||
|
||||
```python
|
||||
execute_code('''
|
||||
import requests
|
||||
|
||||
def validate_user(data: dict) -> list[str]:
|
||||
errors = []
|
||||
required = {"id": int, "email": str, "created_at": str}
|
||||
for field, expected in required.items():
|
||||
if field not in data:
|
||||
errors.append(f"missing field: {field}")
|
||||
elif not isinstance(data[field], expected):
|
||||
errors.append(f"{field}: want {expected.__name__}, got {type(data[field]).__name__}")
|
||||
return errors
|
||||
|
||||
resp = requests.get(f"{BASE}/users/1", headers=HEADERS, timeout=10)
|
||||
issues = validate_user(resp.json())
|
||||
if issues:
|
||||
print(f"contract violations: {issues}")
|
||||
''')
|
||||
```
|
||||
|
||||
Run after API upgrades, when integrating new third parties, or in CI smoke tests.
|
||||
|
||||
## Correlation IDs
|
||||
|
||||
Always capture the provider's request ID — fastest path to vendor support:
|
||||
|
||||
```python
|
||||
execute_code('''
|
||||
import requests
|
||||
resp = requests.post(url, json=payload, headers=headers, timeout=10)
|
||||
request_id = (
|
||||
resp.headers.get("X-Request-Id")
|
||||
or resp.headers.get("X-Trace-Id")
|
||||
or resp.headers.get("CF-Ray") # Cloudflare
|
||||
)
|
||||
if resp.status_code >= 400:
|
||||
print(f"failed status={resp.status_code} req_id={request_id} ts={resp.headers.get('Date')}")
|
||||
''')
|
||||
```
|
||||
|
||||
**Vendor bug-report template:**
|
||||
|
||||
```
|
||||
Endpoint: POST /api/v1/orders
|
||||
Request ID: req_abc123xyz
|
||||
Timestamp: 2026-03-17T14:30:00Z
|
||||
Status: 500
|
||||
Expected: 201 with order object
|
||||
Actual: 500 {"error":"internal server error"}
|
||||
Repro: curl -X POST … (auth: <REDACTED>)
|
||||
```
|
||||
|
||||
## Regression Test Template
|
||||
|
||||
Drop this into `tests/` and run via `terminal('pytest tests/test_api_smoke.py -v')`:
|
||||
|
||||
```python
|
||||
import os, requests, pytest
|
||||
|
||||
BASE_URL = os.environ.get("API_BASE_URL", "https://api.example.com")
|
||||
TOKEN = os.environ.get("API_TOKEN", "")
|
||||
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
|
||||
|
||||
class TestAPISmoke:
|
||||
def test_health(self):
|
||||
resp = requests.get(f"{BASE_URL}/health", timeout=5)
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_list_users_returns_array(self):
|
||||
resp = requests.get(f"{BASE_URL}/users", headers=HEADERS, timeout=10)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data.get("data", data), list)
|
||||
|
||||
def test_get_user_required_fields(self):
|
||||
resp = requests.get(f"{BASE_URL}/users/1", headers=HEADERS, timeout=10)
|
||||
assert resp.status_code in (200, 404)
|
||||
if resp.status_code == 200:
|
||||
user = resp.json()
|
||||
assert "id" in user and "email" in user
|
||||
|
||||
def test_invalid_auth_returns_401(self):
|
||||
resp = requests.get(
|
||||
f"{BASE_URL}/users",
|
||||
headers={"Authorization": "Bearer invalid-token"},
|
||||
timeout=10,
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Token handling
|
||||
- Never log full tokens. Redact: `Bearer <REDACTED>`.
|
||||
- Never hardcode tokens in scripts. Read from env (`os.environ["API_TOKEN"]`) or `~/.hermes/.env`.
|
||||
- Rotate immediately if a token surfaces in logs, error messages, or git history.
|
||||
|
||||
### Safe logging
|
||||
|
||||
```python
|
||||
def redact_auth(headers: dict) -> dict:
|
||||
sensitive = {"authorization", "x-api-key", "cookie", "set-cookie"}
|
||||
return {k: ("<REDACTED>" if k.lower() in sensitive else v) for k, v in headers.items()}
|
||||
```
|
||||
|
||||
### Leak checklist
|
||||
|
||||
- [ ] **Credentials in URLs.** API keys in query strings end up in server logs, browser history, referrer headers — use headers.
|
||||
- [ ] **PII in error responses.** `404 on /users/123` shouldn't reveal whether the user exists (enumeration).
|
||||
- [ ] **Stack traces in prod.** 500s shouldn't leak file paths, framework versions.
|
||||
- [ ] **Internal hostnames/IPs.** `10.x.x.x`, `internal-api.corp.local` in error bodies.
|
||||
- [ ] **Tokens echoed back.** Some APIs include the auth token in error details. Verify they don't.
|
||||
- [ ] **Verbose `Server` / `X-Powered-By`.** Stack-info leaks. Note for security review.
|
||||
|
||||
## Hermes Tool Patterns
|
||||
|
||||
### terminal — for curl, dig, openssl
|
||||
|
||||
```python
|
||||
terminal('curl -sI https://api.example.com')
|
||||
terminal('openssl s_client -connect api.example.com:443 -servername api.example.com </dev/null 2>/dev/null | openssl x509 -noout -dates')
|
||||
```
|
||||
|
||||
### execute_code — for multi-step Python flows
|
||||
|
||||
When debugging spans auth → fetch → paginate → validate, use `execute_code`. Variables persist for the script, results print to stdout, no risk of token spam in your context:
|
||||
|
||||
```python
|
||||
execute_code('''
|
||||
import os, requests
|
||||
|
||||
token = os.environ["API_TOKEN"]
|
||||
base = "https://api.example.com"
|
||||
H = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# 1. auth
|
||||
me = requests.get(f"{base}/me", headers=H, timeout=10)
|
||||
print(f"auth {me.status_code}")
|
||||
|
||||
# 2. paginate
|
||||
all_users, cursor = [], None
|
||||
while True:
|
||||
params = {"cursor": cursor} if cursor else {}
|
||||
r = requests.get(f"{base}/users", headers=H, params=params, timeout=10)
|
||||
body = r.json()
|
||||
all_users.extend(body["data"])
|
||||
cursor = body.get("next_cursor")
|
||||
if not cursor:
|
||||
break
|
||||
print(f"users={len(all_users)}")
|
||||
''')
|
||||
```
|
||||
|
||||
### web_extract — for vendor API docs
|
||||
|
||||
Pull the spec for the endpoint you're debugging instead of guessing:
|
||||
|
||||
```python
|
||||
web_extract(urls=["https://docs.example.com/api/v1/users"])
|
||||
```
|
||||
|
||||
### delegate_task — for full CRUD test sweeps
|
||||
|
||||
```python
|
||||
delegate_task(
|
||||
goal="Test all CRUD endpoints for /api/v1/users",
|
||||
context="""
|
||||
Follow the rest-graphql-debug skill (optional-skills/software-development/rest-graphql-debug).
|
||||
Base URL: https://api.example.com
|
||||
Auth: Bearer token from API_TOKEN env var.
|
||||
|
||||
For each verb (POST, GET, PATCH, DELETE):
|
||||
- happy path: assert status + response schema
|
||||
- error cases: 400, 404, 422
|
||||
- log a repro curl for any failure (redact tokens)
|
||||
|
||||
Output: pass/fail per endpoint + correlation IDs for failures.
|
||||
""",
|
||||
toolsets=["terminal", "file"],
|
||||
)
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
When reporting findings:
|
||||
|
||||
```
|
||||
## Finding
|
||||
Endpoint: POST /api/v1/users
|
||||
Status: 422 Unprocessable Entity
|
||||
Req ID: req_abc123xyz
|
||||
|
||||
## Repro
|
||||
curl -X POST https://api.example.com/api/v1/users \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <REDACTED>' \
|
||||
-d '{"name":"test"}'
|
||||
|
||||
## Root Cause
|
||||
Missing required field `email`. Server validation rejects before processing.
|
||||
|
||||
## Fix
|
||||
-d '{"name":"test","email":"test@example.com"}'
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- `systematic-debugging` — once the failing API layer is isolated, root-cause your code
|
||||
- `test-driven-development` — write the regression test before shipping the fix
|
||||
Reference in New Issue
Block a user