Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,13 @@ src/clayde/
__init__.py
app.py # FastAPI app, /webhook/pebble, /health, OTel enqueue span
auth.py # constant-time bearer-token verification
notify.py # send_ntfy + NotificationPayload model
queue.py # PebbleJob, JobQueue (in-memory asyncio.Queue), QueueFullError
runner.py # invoke_claude_pebble — async CLI subprocess, fresh session
skills.py # Skill model, /skills/ discovery, system + user prompt builders
worker.py # worker_loop, process_job — pop jobs, OTel process span
skills_builtin/
ping.md # built-in health-check skill (baked into image)

# Container paths
/opt/clayde/ # application code (WORKDIR)
Expand Down Expand Up @@ -115,8 +118,12 @@ Plain `KEY=VALUE` file (no shell quoting). All keys use `CLAYDE_` prefix and are
| `CLAYDE_PEBBLE_TOKEN` | Bearer token the Pebble app sends |
| `CLAYDE_PEBBLE_HOST` | Public hostname for Traefik routing |
| `CLAYDE_PEBBLE_PORT` | Internal HTTP port (default 8080) |
| `CLAYDE_PEBBLE_TIMEOUT` | Per-request CLI timeout seconds (default 600) |
| `CLAYDE_PEBBLE_TIMEOUT` | Per-request CLI timeout seconds (default 300) |
| `CLAYDE_PEBBLE_QUEUE_MAX` | Max queued jobs before 503 (default 100) |
| `CLAYDE_NTFY_TOPIC` | ntfy.sh topic for Pebble outcome notifications |
| `CLAYDE_NTFY_BASE_URL` | ntfy base URL (override for self-host) |
| `CLAYDE_NTFY_TIMEOUT_S` | ntfy POST timeout seconds (default 10) |
| `CLAYDE_KB_PATH` | In-container KB path; Pebble per-request cwd (default `/home/clayde/knowledge_base`) |

Config is loaded via `get_settings()` (singleton). `GH_TOKEN` is exported at startup for the `gh` CLI.

Expand Down Expand Up @@ -313,25 +320,26 @@ webhook for a Pebble watch app, alongside the existing GitHub poll loop

The text is dispatched to the Claude CLI with a system prompt listing
*skills* found under the in-container path `/skills/`. Each skill is a
single markdown file with frontmatter:

```markdown
---
name: my-skill
description: One-line description used in skill catalog.
---

(Body: instructions for Claude.)
```

Mount one or more host directories read-only under `/skills/` in
`docker-compose.yml`. Discovery is recursive; subdirectory layout is
free. Duplicate `name` fields are logged and only the first-discovered
skill is used.

Claude must pick AT MOST ONE skill per request, or respond exactly
"No matching skill". Each request gets a fresh `claude` session — no
context carries between requests.
single markdown file with `name` + `description` frontmatter. Built-in
skills live at `/skills/builtin/` (baked into the image — currently
`ping`); host-mounted skill directories sit alongside (e.g.
`/skills/personal/`, `/skills/shared/`).

Claude is free to use any number of skills per request — there is no
single-skill cap. If no skill fits, Claude uses judgement (typically
capturing into the knowledge base inbox).

Per-request `cwd` is `${CLAYDE_KB_PATH}` (default
`/home/clayde/knowledge_base`), mounted RW from the host
`~/knowledge_base/`. Sync to other devices is handled by Syncthing on
the host — the container performs no `git` operations against the KB.

Every terminal outcome (success, claude-reported failure, timeout, usage
limit, CLI error, auth error, worker exception, queue full) emits an ntfy
notification on `${CLAYDE_NTFY_BASE_URL}/${CLAYDE_NTFY_TOPIC}`. Claude
produces the title/body via a fenced JSON tail in its output; the
framework falls back to a synthetic "no summary" payload when parsing
fails.

Traefik handles TLS (Let's Encrypt) and routes
`https://<CLAYDE_PEBBLE_HOST>/webhook/pebble` over a private docker
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ RUN uv sync --frozen --no-dev --no-install-project

# Copy source and install project
COPY src/ src/
COPY src/clayde/skills_builtin/ /skills/builtin/
COPY CLAUDE.md ./
RUN uv sync --frozen --no-dev

Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,12 @@ In any repository the bot has access to, assign issues to the bot account. Clayd
| `CLAYDE_PEBBLE_TOKEN` | Bearer token the Pebble app sends |
| `CLAYDE_PEBBLE_HOST` | Public hostname for Traefik routing |
| `CLAYDE_PEBBLE_PORT` | Internal HTTP port (default `8080`) |
| `CLAYDE_PEBBLE_TIMEOUT` | Per-request CLI timeout seconds (default `600`) |
| `CLAYDE_PEBBLE_TIMEOUT` | Per-request CLI timeout seconds (default `300`) |
| `CLAYDE_PEBBLE_QUEUE_MAX` | Max queued jobs before 503 (default `100`) |
| `CLAYDE_NTFY_TOPIC` | ntfy.sh topic for Pebble outcome notifications |
| `CLAYDE_NTFY_BASE_URL` | ntfy base URL (override for self-host) |
| `CLAYDE_NTFY_TIMEOUT_S` | ntfy POST timeout seconds (default `10`) |
| `CLAYDE_KB_PATH` | In-container KB path; Pebble per-request cwd (default `/home/clayde/knowledge_base`) |

---

Expand All @@ -224,11 +228,21 @@ To enable:
3. Mount one or more skill directories under `/skills/` in
`docker-compose.yml`. Each skill is a markdown file with frontmatter
`name` and `description` (see `CLAUDE.md` for the full format).
4. Configure the Pebble app to POST to
Built-in skills (currently `ping`) are baked into the image at
`/skills/builtin/`.
4. Mount `~/knowledge_base` to `/home/clayde/knowledge_base` (already
wired in `docker-compose.yml`) so Claude has a writable working
directory. Sync across devices is handled by Syncthing on the host —
the container performs no `git` against the KB.
5. Set `CLAYDE_NTFY_TOPIC` (and optionally `CLAYDE_NTFY_BASE_URL` for
self-hosted ntfy) to receive outcome notifications on your phone for
every Pebble request.
6. Configure the Pebble app to POST to
`https://<CLAYDE_PEBBLE_HOST>/webhook/pebble` with the bearer token.

The webhook is fire-and-forget: requests return `200` with a job id and
work happens asynchronously in a single serial worker. Each request
spawns a fresh Claude CLI session (no context carries between requests)
and the system prompt instructs Claude to choose at most one matching
skill or respond `"No matching skill"`.
with `cwd` set to the knowledge-base mount. Claude is free to use any
number of skills per request; every terminal outcome (success, failure,
timeout, usage limit, queue full, etc.) emits an ntfy notification.
14 changes: 12 additions & 2 deletions config.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@ CLAYDE_PEBBLE_TOKEN=
CLAYDE_PEBBLE_HOST=
# Internal HTTP port (default 8080; Traefik backend target).
CLAYDE_PEBBLE_PORT=8080
# Per-request CLI timeout in seconds.
CLAYDE_PEBBLE_TIMEOUT=600
# Per-request CLI timeout in seconds (default 300).
CLAYDE_PEBBLE_TIMEOUT=300
# Maximum queued Pebble jobs before 503.
CLAYDE_PEBBLE_QUEUE_MAX=100

# --- ntfy notifications (Pebble outcome feedback) ---
# Default topic is public on ntfy.sh; anyone with the string can read transcripts.
CLAYDE_NTFY_TOPIC=7yuau0vyes
CLAYDE_NTFY_BASE_URL=https://ntfy.sh
CLAYDE_NTFY_TIMEOUT_S=10

# --- Knowledge base (default cwd for Pebble runs) ---
# Mounted from host ~/knowledge_base/. Synced by Syncthing — no git in container.
CLAYDE_KB_PATH=/home/clayde/knowledge_base
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ services:
# under /skills/. Subdirectory layout is free; discovery is recursive.
- ~/skills/personal:/skills/personal:ro
- ~/skills/shared:/skills/shared:ro
# Pebble knowledge-base working directory — Syncthing on the host
# handles cross-device sync; container performs no git on the KB.
- ~/knowledge_base:/home/clayde/knowledge_base
labels:
- "com.centurylinklabs.watchtower.enable=true"
- "traefik.enable=true"
Expand Down
Loading
Loading