Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
---
|
||||
title: "Actual Setup — Set up Actual Computer (actual.inc) inference in Hermes"
|
||||
sidebar_label: "Actual Setup"
|
||||
description: "Set up Actual Computer (actual.inc) inference in Hermes"
|
||||
---
|
||||
|
||||
{/* 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. */}
|
||||
|
||||
# Actual Setup
|
||||
|
||||
Set up Actual Computer (actual.inc) inference in Hermes.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/devops/actual-setup` |
|
||||
| Path | `optional-skills/devops\actual-setup` |
|
||||
| Version | `2.0.0` |
|
||||
| Author | shl0ms + Hermes Agent |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos, windows |
|
||||
| Tags | `actual`, `actual-inc`, `provider`, `local-inference`, `relay`, `gguf`, `setup` |
|
||||
|
||||
## 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.
|
||||
:::
|
||||
|
||||
# Actual Computer Setup Skill
|
||||
|
||||
Sets up [actual.inc](https://actual.inc) (Actual Computer) as a Hermes inference
|
||||
provider. Actual turns the user's own hardware into a private inference cluster
|
||||
and exposes an OpenAI-compatible API two ways: a hosted end-to-end-encrypted
|
||||
relay at `https://api.actual.inc` (authenticated with an `ac_` key), and a local
|
||||
on-device daemon at `http://127.0.0.1:8080` (no auth on loopback). This skill
|
||||
does not install the Actual daemon for the user — device authorization requires
|
||||
a human in a browser.
|
||||
|
||||
## When to Use
|
||||
|
||||
- User wants to add actual.inc as an inference provider (cloud relay or local).
|
||||
- User has an `ac_` key and wants Hermes routed through their Actual cluster.
|
||||
- User wants fully-local, on-device inference via the Actual daemon.
|
||||
- Troubleshooting: Actual requests failing with cryptic 400s or empty streams.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Hermes has **first-class `actual` provider support** (provider id `actual`,
|
||||
aliases `actual-computer`, `actualcomputer`, `aci`). Do NOT configure Actual
|
||||
as a `custom_providers` / `providers.actual.*` entry on current Hermes — the
|
||||
built-in provider owns the name and handles base-url normalization, the
|
||||
Responses transport, and local no-auth automatically.
|
||||
- Relay mode: an Actual account and an `ac_` inference key from
|
||||
https://actual.inc/user/keys.
|
||||
- Local mode: the user has installed the daemon
|
||||
(`curl -fsSL "https://actual.inc/install" | bash`) and completed device
|
||||
authorization by running `actual` once and opening the printed
|
||||
`https://actual.inc/device?code=...` URL in a browser. Relay that URL to the
|
||||
user and WAIT — never invent an email or authorize on their behalf. Codes
|
||||
expire in 5 minutes; re-run `actual` for a fresh one.
|
||||
|
||||
## How to Run
|
||||
|
||||
### Relay / API mode
|
||||
|
||||
1. Put the key in `.env` (secrets only — never config.yaml):
|
||||
append `ACTUAL_API_KEY=ac_...` to `~/.hermes/.env`.
|
||||
2. Verify the key and discover models with `terminal`:
|
||||
```bash
|
||||
curl -s https://api.actual.inc/v1/models -H "Authorization: Bearer $ACTUAL_API_KEY"
|
||||
```
|
||||
3. Select provider + model:
|
||||
```bash
|
||||
hermes config set model.provider actual
|
||||
hermes config set model.default "MODEL_ID_FROM_DISCOVERY"
|
||||
```
|
||||
4. Verify end-to-end:
|
||||
```bash
|
||||
hermes chat -Q -q "Reply with exactly: ACTUAL_OK" --provider actual -m MODEL_ID
|
||||
```
|
||||
|
||||
### Local mode
|
||||
|
||||
1. Human has installed + authorized the daemon (see Prerequisites).
|
||||
2. Download and load a model (scriptable once authorized):
|
||||
```bash
|
||||
actual models search "qwen2.5 0.5b instruct gguf" --limit 8 --no-prompt
|
||||
# Downloads REQUIRE an explicit quantization (409 ambiguous_model_download otherwise):
|
||||
actual models download "Qwen/Qwen2.5-0.5B-Instruct-GGUF/Q4_K_M"
|
||||
actual models list # note the INSTALLED name (differs from download id)
|
||||
actual models load "qwen2.5-0.5b-instruct-q4_k_m" # load by installed name
|
||||
```
|
||||
3. Point Hermes at the daemon. `ACTUAL_BASE_URL` with a loopback host flips the
|
||||
built-in provider into local no-auth mode automatically — no key needed:
|
||||
append `ACTUAL_BASE_URL=http://127.0.0.1:8080` to `~/.hermes/.env`, then:
|
||||
```bash
|
||||
hermes config set model.provider actual
|
||||
hermes config set model.default "INSTALLED_MODEL_NAME"
|
||||
```
|
||||
4. Verify (reduced toolset — see context-window pitfall below):
|
||||
```bash
|
||||
hermes chat -Q -q "Reply with exactly: LOCAL_OK" --provider actual -m INSTALLED_NAME -t file,web
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Thing | Value |
|
||||
|---|---|
|
||||
| Hosted relay | `https://api.actual.inc/v1` (normalized from bare host automatically) |
|
||||
| Local daemon | `http://127.0.0.1:8080/v1` (no auth on loopback) |
|
||||
| Key env var | `ACTUAL_API_KEY` (`ac_...`) |
|
||||
| Base URL env var | `ACTUAL_BASE_URL` (loopback host ⇒ local no-auth mode) |
|
||||
| Provider id / aliases | `actual` / `actual-computer`, `actualcomputer`, `aci` |
|
||||
| Transport | Responses API (`codex_responses`) — built-in, do not override |
|
||||
| Cluster pinning | `X-Cluster-ID` header via `providers.actual.extra_headers` in config.yaml |
|
||||
| Model size guide | 0.5B Q4_K_M ~470MB (toy), 7-8B Q4_K_M ~4.5GB (daily driver), 32B ~20GB |
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **reasoning_effort trap (handled by Hermes since the first-class provider).**
|
||||
Actual's SGLang/vLLM backends accept only `none/low/medium/high/max`;
|
||||
`xhigh`/`ultra` used to fail with a cryptic
|
||||
`Expecting value: line 1 column 1 (char 0)` (a wrapped HTTP 400). The
|
||||
built-in provider clamps `xhigh→high` and `ultra→max` on the wire. If a
|
||||
request still 400s this way on an old Hermes, set a per-model cap:
|
||||
`agent.reasoning_overrides.<model>: high` in config.yaml.
|
||||
2. **Context-window overflow on small local models.** Hermes' default toolset
|
||||
is ~26k tokens of schemas plus a ~9k-token system prompt. A model loaded
|
||||
with a 32k context overflows before the first turn, and llama.cpp-family
|
||||
servers emit a bare `data: [DONE]` — Hermes reports
|
||||
`Provider returned an empty stream with no finish_reason`. This is NOT an
|
||||
SSE bug. Fixes: restrict tools (`-t file,web`), load the model with a
|
||||
larger `n_ctx`, or pick a >=64k-context model for the full toolset.
|
||||
Upstream tracking: #51448 (do not file new issues; add evidence there).
|
||||
Related but distinct: #65631 (HTTP-200 SSE carrying a 400), #56516
|
||||
(reasoning-only streams).
|
||||
3. **Download ids vs installed names.** `actual models download` takes
|
||||
`repo/QUANT` and 409s without an explicit quantization;
|
||||
`actual models load` takes the INSTALLED name from `actual models list`.
|
||||
4. **Reasoning models returning empty content.** GLM/Qwen reasoning variants
|
||||
emit thinking in a separate `reasoning` field and can burn a small
|
||||
`max_tokens` entirely on reasoning. Give generous max_tokens before
|
||||
assuming failure.
|
||||
5. **Do not create a custom provider named `actual`.** Older setup guides
|
||||
(pre first-class support) wrote `providers.actual.*` config blocks. On
|
||||
current Hermes the built-in provider wins the name; stale custom blocks
|
||||
are ignored or conflict. Remove them and use the env vars + model.provider
|
||||
flow above.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Relay:
|
||||
hermes chat -Q -q "Reply with exactly: ACTUAL_OK" --provider actual -m MODEL
|
||||
# Local (small model — reduced toolset):
|
||||
hermes chat -Q -q "Reply with exactly: LOCAL_OK" --provider actual -m MODEL -t file,web
|
||||
# Provider status (local no-auth shows key_source=local-offline):
|
||||
hermes status
|
||||
```
|
||||
|
||||
For other OpenAI-compatible clients (e.g. OpenCode), see
|
||||
`references/opencode.md`.
|
||||
@@ -0,0 +1,298 @@
|
||||
---
|
||||
title: "Docker Management — Manage Docker containers, images, volumes, and Compose"
|
||||
sidebar_label: "Docker Management"
|
||||
description: "Manage Docker containers, images, volumes, and Compose"
|
||||
---
|
||||
|
||||
{/* 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. */}
|
||||
|
||||
# Docker Management
|
||||
|
||||
Manage Docker containers, images, volumes, and Compose.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/devops/docker-management` |
|
||||
| Path | `optional-skills/devops\docker-management` |
|
||||
| Version | `1.0.0` |
|
||||
| Author | sprmn24 |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos, windows |
|
||||
| Tags | `docker`, `containers`, `devops`, `infrastructure`, `compose`, `images`, `volumes`, `networks`, `debugging` |
|
||||
|
||||
## 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.
|
||||
:::
|
||||
|
||||
# Docker Management
|
||||
|
||||
Manage Docker containers, images, volumes, networks, and Compose stacks using standard Docker CLI commands. No additional dependencies beyond Docker itself.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Run, stop, restart, remove, or inspect containers
|
||||
- Build, pull, push, tag, or clean up Docker images
|
||||
- Work with Docker Compose (multi-service stacks)
|
||||
- Manage volumes or networks
|
||||
- Debug a crashing container or analyze logs
|
||||
- Check Docker disk usage or free up space
|
||||
- Review or optimize a Dockerfile
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker Engine installed and running
|
||||
- User added to the `docker` group (or use `sudo`)
|
||||
- Docker Compose v2 (included with modern Docker installations)
|
||||
|
||||
Quick check:
|
||||
|
||||
```bash
|
||||
docker --version && docker compose version
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| Run container (background) | `docker run -d --name NAME IMAGE` |
|
||||
| Stop + remove | `docker stop NAME && docker rm NAME` |
|
||||
| View logs (follow) | `docker logs --tail 50 -f NAME` |
|
||||
| Shell into container | `docker exec -it NAME /bin/sh` |
|
||||
| List all containers | `docker ps -a` |
|
||||
| Build image | `docker build -t TAG .` |
|
||||
| Compose up | `docker compose up -d` |
|
||||
| Compose down | `docker compose down` |
|
||||
| Disk usage | `docker system df` |
|
||||
| Cleanup dangling | `docker image prune && docker container prune` |
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Identify the domain
|
||||
|
||||
Figure out which area the request falls into:
|
||||
|
||||
- **Container lifecycle** → run, stop, start, restart, rm, pause/unpause
|
||||
- **Container interaction** → exec, cp, logs, inspect, stats
|
||||
- **Image management** → build, pull, push, tag, rmi, save/load
|
||||
- **Docker Compose** → up, down, ps, logs, exec, build, config
|
||||
- **Volumes & networks** → create, inspect, rm, prune, connect
|
||||
- **Troubleshooting** → log analysis, exit codes, resource issues
|
||||
|
||||
### 2. Container operations
|
||||
|
||||
**Run a new container:**
|
||||
|
||||
```bash
|
||||
# Detached service with port mapping
|
||||
docker run -d --name web -p 8080:80 nginx
|
||||
|
||||
# With environment variables
|
||||
docker run -d -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=mydb --name db postgres:16
|
||||
|
||||
# With persistent data (named volume)
|
||||
docker run -d -v pgdata:/var/lib/postgresql/data --name db postgres:16
|
||||
|
||||
# For development (bind mount source code)
|
||||
docker run -d -v $(pwd)/src:/app/src -p 3000:3000 --name dev my-app
|
||||
|
||||
# Interactive debugging (auto-remove on exit)
|
||||
docker run -it --rm ubuntu:22.04 /bin/bash
|
||||
|
||||
# With resource limits and restart policy
|
||||
docker run -d --memory=512m --cpus=1.5 --restart=unless-stopped --name app my-app
|
||||
```
|
||||
|
||||
Key flags: `-d` detached, `-it` interactive+tty, `--rm` auto-remove, `-p` port (host:container), `-e` env var, `-v` volume, `--name` name, `--restart` restart policy.
|
||||
|
||||
**Manage running containers:**
|
||||
|
||||
```bash
|
||||
docker ps # running containers
|
||||
docker ps -a # all (including stopped)
|
||||
docker stop NAME # graceful stop
|
||||
docker start NAME # start stopped container
|
||||
docker restart NAME # stop + start
|
||||
docker rm NAME # remove stopped container
|
||||
docker rm -f NAME # force remove running container
|
||||
docker container prune # remove ALL stopped containers
|
||||
```
|
||||
|
||||
**Interact with containers:**
|
||||
|
||||
```bash
|
||||
docker exec -it NAME /bin/sh # shell access (use /bin/bash if available)
|
||||
docker exec NAME env # view environment variables
|
||||
docker exec -u root NAME apt update # run as specific user
|
||||
docker logs --tail 100 -f NAME # follow last 100 lines
|
||||
docker logs --since 2h NAME # logs from last 2 hours
|
||||
docker cp NAME:/path/file ./local # copy file from container
|
||||
docker cp ./file NAME:/path/ # copy file to container
|
||||
docker inspect NAME # full container details (JSON)
|
||||
docker stats --no-stream # resource usage snapshot
|
||||
docker top NAME # running processes
|
||||
```
|
||||
|
||||
### 3. Image management
|
||||
|
||||
```bash
|
||||
# Build
|
||||
docker build -t my-app:latest .
|
||||
docker build -t my-app:prod -f Dockerfile.prod .
|
||||
docker build --no-cache -t my-app . # clean rebuild
|
||||
DOCKER_BUILDKIT=1 docker build -t my-app . # faster with BuildKit
|
||||
|
||||
# Pull and push
|
||||
docker pull node:20-alpine
|
||||
docker login ghcr.io
|
||||
docker tag my-app:latest registry/my-app:v1.0
|
||||
docker push registry/my-app:v1.0
|
||||
|
||||
# Inspect
|
||||
docker images # list local images
|
||||
docker history IMAGE # see layers
|
||||
docker inspect IMAGE # full details
|
||||
|
||||
# Cleanup
|
||||
docker image prune # remove dangling (untagged) images
|
||||
docker image prune -a # remove ALL unused images (careful!)
|
||||
docker image prune -a --filter "until=168h" # unused images older than 7 days
|
||||
```
|
||||
|
||||
### 4. Docker Compose
|
||||
|
||||
```bash
|
||||
# Start/stop
|
||||
docker compose up -d # start all services detached
|
||||
docker compose up -d --build # rebuild images before starting
|
||||
docker compose down # stop and remove containers
|
||||
docker compose down -v # also remove volumes (DESTROYS DATA)
|
||||
|
||||
# Monitoring
|
||||
docker compose ps # list services
|
||||
docker compose logs -f api # follow logs for specific service
|
||||
docker compose logs --tail 50 # last 50 lines all services
|
||||
|
||||
# Interaction
|
||||
docker compose exec api /bin/sh # shell into running service
|
||||
docker compose run --rm api npm test # one-off command (new container)
|
||||
docker compose restart api # restart specific service
|
||||
|
||||
# Validation
|
||||
docker compose config # validate and view resolved config
|
||||
```
|
||||
|
||||
**Minimal compose.yml example:**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
# Password comes from the POSTGRES_PASSWORD secret, not the URL
|
||||
- DATABASE_URL=postgres://mydb_user@db:5432/mydb
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: pass
|
||||
POSTGRES_DB: mydb
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U user"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
### 5. Volumes and networks
|
||||
|
||||
```bash
|
||||
# Volumes
|
||||
docker volume ls # list volumes
|
||||
docker volume create mydata # create named volume
|
||||
docker volume inspect mydata # details (mount point, etc.)
|
||||
docker volume rm mydata # remove (fails if in use)
|
||||
docker volume prune # remove unused volumes
|
||||
|
||||
# Networks
|
||||
docker network ls # list networks
|
||||
docker network create mynet # create bridge network
|
||||
docker network inspect mynet # details (connected containers)
|
||||
docker network connect mynet NAME # attach container to network
|
||||
docker network disconnect mynet NAME # detach container
|
||||
docker network rm mynet # remove network
|
||||
docker network prune # remove unused networks
|
||||
```
|
||||
|
||||
### 6. Disk usage and cleanup
|
||||
|
||||
Always start with a diagnostic before cleaning:
|
||||
|
||||
```bash
|
||||
# Check what's using space
|
||||
docker system df # summary
|
||||
docker system df -v # detailed breakdown
|
||||
|
||||
# Targeted cleanup (safe)
|
||||
docker container prune # stopped containers
|
||||
docker image prune # dangling images
|
||||
docker volume prune # unused volumes
|
||||
docker network prune # unused networks
|
||||
|
||||
# Aggressive cleanup (confirm with user first!)
|
||||
docker system prune # containers + images + networks
|
||||
docker system prune -a # also unused images
|
||||
docker system prune -a --volumes # EVERYTHING — named volumes too
|
||||
```
|
||||
|
||||
**Warning:** Never run `docker system prune -a --volumes` without confirming with the user. This removes named volumes with potentially important data.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
| Problem | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| Container exits immediately | Main process finished or crashed | Check `docker logs NAME`, try `docker run -it --entrypoint /bin/sh IMAGE` |
|
||||
| "port is already allocated" | Another process using that port | `docker ps` or `lsof -i :PORT` to find it |
|
||||
| "no space left on device" | Docker disk full | `docker system df` then targeted prune |
|
||||
| Can't connect to container | App binds to 127.0.0.1 inside container | App must bind to `0.0.0.0`, check `-p` mapping |
|
||||
| Permission denied on volume | UID/GID mismatch host vs container | Use `--user $(id -u):$(id -g)` or fix permissions |
|
||||
| Compose services can't reach each other | Wrong network or service name | Services use service name as hostname, check `docker compose config` |
|
||||
| Build cache not working | Layer order wrong in Dockerfile | Put rarely-changing layers first (deps before source code) |
|
||||
| Image too large | No multi-stage build, no .dockerignore | Use multi-stage builds, add `.dockerignore` |
|
||||
|
||||
## Verification
|
||||
|
||||
After any Docker operation, verify the result:
|
||||
|
||||
- **Container started?** → `docker ps` (check status is "Up")
|
||||
- **Logs clean?** → `docker logs --tail 20 NAME` (no errors)
|
||||
- **Port accessible?** → `curl -s http://localhost:PORT` or `docker port NAME`
|
||||
- **Image built?** → `docker images | grep TAG`
|
||||
- **Compose stack healthy?** → `docker compose ps` (all services "running" or "healthy")
|
||||
- **Disk freed?** → `docker system df` (compare before/after)
|
||||
|
||||
## Dockerfile Optimization Tips
|
||||
|
||||
When reviewing or creating a Dockerfile, suggest these improvements:
|
||||
|
||||
1. **Multi-stage builds** — separate build environment from runtime to reduce final image size
|
||||
2. **Layer ordering** — put dependencies before source code so changes don't invalidate cached layers
|
||||
3. **Combine RUN commands** — fewer layers, smaller image
|
||||
4. **Use .dockerignore** — exclude `node_modules`, `.git`, `__pycache__`, etc.
|
||||
5. **Pin base image versions** — `node:20-alpine` not `node:latest`
|
||||
6. **Run as non-root** — add `USER` instruction for security
|
||||
7. **Use slim/alpine bases** — `python:3.12-slim` not `python:3.12`
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
---
|
||||
title: "Hermes S6 Container Supervision — Modify or debug s6 services in the Hermes Docker image"
|
||||
sidebar_label: "Hermes S6 Container Supervision"
|
||||
description: "Modify or debug s6 services in the Hermes Docker image"
|
||||
---
|
||||
|
||||
{/* 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. */}
|
||||
|
||||
# Hermes S6 Container Supervision
|
||||
|
||||
Modify or debug s6 services in the Hermes Docker image.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/devops/hermes-s6-container-supervision` |
|
||||
| Path | `optional-skills/devops\hermes-s6-container-supervision` |
|
||||
| Version | `1.0.0` |
|
||||
| Author | Hermes Agent |
|
||||
| License | MIT |
|
||||
| Platforms | linux |
|
||||
| Tags | `docker`, `s6`, `supervision`, `gateway`, `profiles` |
|
||||
| Related skills | [`hermes-agent`](/docs/user-guide/skills/bundled/autonomous-ai-agents/autonomous-ai-agents-hermes-agent) |
|
||||
|
||||
## 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.
|
||||
:::
|
||||
|
||||
# Hermes s6-overlay Container Supervision
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Load this skill when you're working on:
|
||||
- Adding or removing a static service in the Hermes Docker image (something that should be supervised at every container start, like the dashboard)
|
||||
- Diagnosing why a per-profile gateway isn't starting, restarting, or surviving `docker restart`
|
||||
- Understanding why the container's CMD is `/opt/hermes/docker/main-wrapper.sh` and how leading-dash args reach the user's program
|
||||
- Modifying `cont-init.d` boot scripts (UID remap, volume seeding, profile reconciliation)
|
||||
- Changing the rendered run-script for per-profile gateways (Phase 4)
|
||||
|
||||
If you're just running the Hermes Agent and want to use Docker, see `website/docs/user-guide/docker.md` instead.
|
||||
|
||||
## Architecture at a glance
|
||||
|
||||
<!-- ascii-guard-ignore -->
|
||||
```
|
||||
/init ← PID 1 (s6-overlay v3.2.3.0)
|
||||
├── cont-init.d ← oneshot setup, runs as root
|
||||
│ ├── 01-hermes-setup ← docker/stage2-hook.sh
|
||||
│ │ ├── UID/GID remap
|
||||
│ │ ├── chown /opt/data
|
||||
│ │ ├── chown /opt/data/profiles (every boot)
|
||||
│ │ ├── seed .env / config.yaml / SOUL.md
|
||||
│ │ └── skills_sync.py
|
||||
│ └── 02-reconcile-profiles ← hermes_cli.container_boot
|
||||
│ ├── chown /run/service (hermes-writable for runtime register)
|
||||
│ └── walk $HERMES_HOME/profiles/<name>/gateway_state.json
|
||||
│ → recreate /run/service/gateway-<name>/
|
||||
│ → auto-start only those with prior_state == "running"
|
||||
│
|
||||
├── s6-rc.d (static services, in /etc/s6-overlay/s6-rc.d/)
|
||||
│ ├── main-hermes/run ← exec sleep infinity (no-op slot)
|
||||
│ └── dashboard/run ← if HERMES_DASHBOARD=1, runs `hermes dashboard`
|
||||
│
|
||||
├── /run/service (s6-svscan watches; tmpfs)
|
||||
│ ├── gateway-coder/ ← runtime-registered per-profile
|
||||
│ │ ├── type ("longrun")
|
||||
│ │ ├── run ("#!/command/with-contenv sh ... exec s6-setuidgid hermes hermes -p coder gateway run")
|
||||
│ │ ├── down (marker — present means "registered but don't auto-start")
|
||||
│ │ └── log/run (s6-log → $HERMES_HOME/logs/gateways/coder/current)
|
||||
│ └── ...
|
||||
│
|
||||
└── CMD ("main program") ← /opt/hermes/docker/main-wrapper.sh
|
||||
└── routes user args: bare exec | hermes subcommand | hermes (no args)
|
||||
— exec'd by /init with stdin/stdout/stderr inherited (TTY for --tui)
|
||||
```
|
||||
<!-- ascii-guard-ignore-end -->
|
||||
|
||||
## Key files
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `Dockerfile` | s6-overlay install + cont-init.d wiring + `ENTRYPOINT ["/opt/hermes/docker/entrypoint-dispatch.sh"]` |
|
||||
| `docker/entrypoint-dispatch.sh` | PID-1 dispatcher: exec's `/init` + main-wrapper when the image owns PID 1; on wrapped runtimes (Fly Machines, `docker run --init`) falls back to stage2-hook + main-wrapper directly, restoring the s6 helper PATH first (#38349). |
|
||||
| `docker/stage2-hook.sh` | The "old entrypoint logic" — UID remap, chown, seed, skills sync. Runs as cont-init.d/01-hermes-setup. |
|
||||
| `docker/cont-init.d/02-reconcile-profiles` | Calls `hermes_cli.container_boot` on every boot to restore profile gateway slots from the persistent volume. |
|
||||
| `docker/main-wrapper.sh` | The container's CMD. Routes user args, drops to hermes via `s6-setuidgid`, exec's the chosen program. |
|
||||
| `docker/s6-rc.d/main-hermes/run` | No-op `sleep infinity` — slot exists so the s6-rc user bundle is valid; main hermes runs as the CMD, not as a supervised service. |
|
||||
| `docker/s6-rc.d/dashboard/run` | Conditional service — `exec sleep infinity` unless `HERMES_DASHBOARD` is truthy. |
|
||||
| `docker/entrypoint.sh` | Back-compat shim that `exec`s the stage2 hook. External scripts that hard-coded the old entrypoint path still work. |
|
||||
| `hermes_cli/service_manager.py` | `S6ServiceManager`: `register_profile_gateway`, `unregister_profile_gateway`, `start/stop/restart/is_running`, `list_profile_gateways`. |
|
||||
| `hermes_cli/container_boot.py` | `reconcile_profile_gateways()` — walks persistent profiles, regenerates s6 slots, emits `container-boot.log`. |
|
||||
| `hermes_cli/gateway.py::_dispatch_via_service_manager_if_s6` | Intercepts `hermes gateway start/stop/restart` and routes to s6 when running in a container. |
|
||||
|
||||
## Why Architecture B (CMD as main program, not s6-supervised)
|
||||
|
||||
The original plan (v1–v3) called for main hermes to run as a supervised s6-rc service. Two real s6-overlay v3 mechanics blocked that:
|
||||
|
||||
1. **cont-init.d scripts receive no CMD args** — so the stage2 hook can't parse `docker run <image> chat -q "hi"` to set `HERMES_ARGS` for a service `run` script to consume.
|
||||
2. **`/run/s6/basedir/bin/halt` does NOT propagate the exit code** written to `/run/s6-linux-init-container-results/exitcode`. Containers always exit 143 (SIGTERM) regardless. Confirmed by skarnet (s6 author) in [issue #477](https://github.com/just-containers/s6-overlay/issues/477): _"if you want a container shutdown, you need to either have your CMD exit, or, if you have no CMD, write the container exit code you want then call halt"_.
|
||||
|
||||
So we use the s6-overlay-native CMD pattern via the dispatcher: `ENTRYPOINT ["/opt/hermes/docker/entrypoint-dispatch.sh"]`, which under PID 1 exec's `/init /opt/hermes/docker/main-wrapper.sh "$@"`. The wrapper is prepended to user args automatically — so `docker run <image> --version` becomes `/init main-wrapper.sh --version`, and `--version` doesn't get intercepted by /init's POSIX shell. The wrapper drops to hermes via `s6-setuidgid`, then exec's the chosen program. The program's exit code becomes the container exit code, exactly matching the pre-s6 tini contract. When the entrypoint is NOT PID 1 (Fly Machines, `docker run --init`), the dispatcher skips `/init` entirely (it would abort with `can only run as pid 1`), restores the s6 helper PATH, runs stage2-hook.sh, and exec's main-wrapper.sh directly — no supervised services on that path (#38349).
|
||||
|
||||
Trade-off: main hermes is unsupervised under s6. That exactly matches its behavior under tini (the pre-s6 image). Dashboard supervision is the only **new** guarantee — and per-profile gateways under `/run/service/` get full supervision.
|
||||
|
||||
## Quick recipes
|
||||
|
||||
### Verify s6 is PID 1 in a running container
|
||||
|
||||
```sh
|
||||
docker exec <c> sh -c 'cat /proc/1/comm; readlink /proc/1/exe'
|
||||
# Expect: s6-svscan or init / /package/admin/s6/.../s6-svscan
|
||||
```
|
||||
|
||||
### Inspect a profile gateway service
|
||||
|
||||
```sh
|
||||
# /command/ isn't on docker-exec PATH — use absolute path
|
||||
docker exec <c> /command/s6-svstat /run/service/gateway-<name>
|
||||
# "up (pid …) … seconds" → running
|
||||
# "down (exitcode N) … seconds, normally up, want up, …" → s6 wants it up but the process keeps exiting (crash loop)
|
||||
# "down … normally up, ready …" → user stopped it
|
||||
```
|
||||
|
||||
### Bring a service up/down manually
|
||||
|
||||
```sh
|
||||
docker exec <c> /command/s6-svc -u /run/service/gateway-<name> # up
|
||||
docker exec <c> /command/s6-svc -d /run/service/gateway-<name> # down
|
||||
docker exec <c> /command/s6-svc -t /run/service/gateway-<name> # SIGTERM (restart)
|
||||
```
|
||||
|
||||
### Watch the cont-init reconciler log
|
||||
|
||||
```sh
|
||||
docker exec <c> tail -n 50 /opt/data/logs/container-boot.log
|
||||
# 2026-05-21T06:18:05+0000 profile=coder prior_state=running action=started
|
||||
# 2026-05-21T06:18:05+0000 profile=writer prior_state=stopped action=registered
|
||||
```
|
||||
|
||||
### Add a new static service
|
||||
|
||||
1. Create `docker/s6-rc.d/<name>/type` with `longrun\n` and `docker/s6-rc.d/<name>/run` (use `#!/command/with-contenv sh` + `# shellcheck shell=sh`).
|
||||
2. Drop to hermes via `s6-setuidgid hermes` at the top of run (unless you specifically need root).
|
||||
3. Create empty `docker/s6-rc.d/<name>/dependencies.d/base` so it waits for the base bundle.
|
||||
4. Create empty `docker/s6-rc.d/user/contents.d/<name>` so it joins the user bundle.
|
||||
5. The `COPY docker/s6-rc.d/` in the Dockerfile picks it up automatically — no other changes.
|
||||
|
||||
### Change the per-profile gateway run command
|
||||
|
||||
Edit `S6ServiceManager._render_run_script` in `hermes_cli/service_manager.py`. The function is also called by `hermes_cli/container_boot.py::_register_service` during boot reconciliation, so it's the single source of truth. Update the corresponding assertion in `tests/hermes_cli/test_service_manager.py::test_s6_register_creates_service_dir_and_triggers_scan`.
|
||||
|
||||
### Run the docker test harness
|
||||
|
||||
```sh
|
||||
docker build -t hermes-agent-harness:latest .
|
||||
HERMES_TEST_IMAGE=hermes-agent-harness:latest scripts/run_tests.sh tests/docker/ -v
|
||||
# Expect 19 passed, 0 xfailed against the s6 image
|
||||
```
|
||||
|
||||
The harness lives in `tests/docker/` and skips when Docker isn't available. The per-test timeout is bumped to 180s (see `tests/docker/conftest.py`).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
### "command not found" via `docker exec`
|
||||
|
||||
`/command/` (where s6-overlay puts its binaries) is on PATH only for processes spawned by the supervision tree — services, cont-init.d, main-wrapper.sh. `docker exec <c> s6-svstat …` will fail with "command not found"; always use the absolute path `/command/s6-svstat`. The `hermes` binary works because the Dockerfile adds `/opt/hermes/.venv/bin` to the runtime `ENV PATH`.
|
||||
|
||||
### Profile directory ownership
|
||||
|
||||
The cont-init reconciler runs as hermes (`s6-setuidgid hermes` in `02-reconcile-profiles`). If a profile dir ends up root-owned (e.g. because `docker exec <c> hermes profile create …` ran as root by default), the reconciler can't read SOUL.md and fails with `PermissionError`. Mitigation: `stage2-hook.sh` chowns `$HERMES_HOME/profiles` to hermes on **every** boot, idempotently. Don't remove that block.
|
||||
|
||||
### Files written by `docker exec` are root-owned
|
||||
|
||||
`docker exec` defaults to root. Either pass `--user hermes` or rely on the stage2 chown sweep next reboot. Don't write files under `$HERMES_HOME/profiles/<name>/` as root manually — the next reconcile pass will sweep them but in-flight operations may hit perm errors.
|
||||
|
||||
### Service slot exists but s6-svstat says "s6-supervise not running"
|
||||
|
||||
The service directory is on tmpfs and was wiped on container restart. Either the cont-init reconciler hasn't run yet (give it a moment after `docker restart`) or it failed. Check `docker logs <c> | grep '02-reconcile'`.
|
||||
|
||||
### Gateway starts then immediately exits (`down (exitcode 1)` in svstat)
|
||||
|
||||
Most likely the profile has no model or auth configured. The service slot is correct — the gateway itself is unconfigured. Run `hermes -p <profile> setup` first. The s6 supervisor will keep restarting it; that's the desired behavior (when you fix the config, the next attempt succeeds and stays up).
|
||||
|
||||
### Reconciler skipped a profile
|
||||
|
||||
The reconciler keys on the **presence of `SOUL.md`** as the "real profile" marker. `hermes profile create` always seeds it. If a profile dir is missing SOUL.md (stray directory, partial restore, backup-in-progress), the reconciler skips it intentionally. Add a `SOUL.md` (even empty) to opt back in.
|
||||
|
||||
### "Help, the container exits 143!"
|
||||
|
||||
Check whether something is invoking `s6-svscanctl -t` or `/run/s6/basedir/bin/halt` — both cause /init to begin stage 3 shutdown but return 143 (SIGTERM) rather than the desired exit code. This was the Phase 2 architecture pivot from A to B. For container shutdown with a real exit code, you must let the CMD (main-wrapper.sh) exit normally; do **not** try to control exit from a finish script.
|
||||
|
||||
## Related skills
|
||||
|
||||
- `hermes-agent-dev`: General hermes-agent codebase navigation
|
||||
- `hermes-tool-quirks`: Specific Hermes-tool workarounds (sed/grep/etc.) — load when debugging the s6 stack's interaction with hermes built-in tools.
|
||||
@@ -0,0 +1,173 @@
|
||||
---
|
||||
title: "Inference Sh Cli — Run 150+ AI apps (image, video, LLM) via inference.sh CLI"
|
||||
sidebar_label: "Inference Sh Cli"
|
||||
description: "Run 150+ AI apps (image, video, LLM) via inference.sh CLI"
|
||||
---
|
||||
|
||||
{/* 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. */}
|
||||
|
||||
# Inference Sh Cli
|
||||
|
||||
Run 150+ AI apps (image, video, LLM) via inference.sh CLI.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/devops/inference-sh-cli` |
|
||||
| Path | `optional-skills/devops\inference-sh-cli` |
|
||||
| Version | `1.0.0` |
|
||||
| Author | okaris |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos, windows |
|
||||
| Tags | `AI`, `image-generation`, `video`, `LLM`, `search`, `inference`, `FLUX`, `Veo`, `Claude` |
|
||||
|
||||
## 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.
|
||||
:::
|
||||
|
||||
# inference.sh CLI
|
||||
|
||||
Run 150+ AI apps in the cloud with a simple CLI. No GPU required.
|
||||
|
||||
All commands use the **terminal tool** to run `infsh` commands.
|
||||
|
||||
## When to Use
|
||||
|
||||
- User asks to generate images (FLUX, Reve, Seedream, Grok, Gemini image)
|
||||
- User asks to generate video (Veo, Wan, Seedance, OmniHuman)
|
||||
- User asks about inference.sh or infsh
|
||||
- User wants to run AI apps without managing individual provider APIs
|
||||
- User asks for AI-powered search (Tavily, Exa)
|
||||
- User needs avatar/lipsync generation
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The `infsh` CLI must be installed and authenticated. Check with:
|
||||
|
||||
```bash
|
||||
infsh me
|
||||
```
|
||||
|
||||
If not installed:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://cli.inference.sh | sh
|
||||
infsh login
|
||||
```
|
||||
|
||||
See `references/authentication.md` for full setup details.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Always Search First
|
||||
|
||||
Never guess app names — always search to find the correct app ID:
|
||||
|
||||
```bash
|
||||
infsh app list --search flux
|
||||
infsh app list --search video
|
||||
infsh app list --search image
|
||||
```
|
||||
|
||||
### 2. Run an App
|
||||
|
||||
Use the exact app ID from the search results. Always use `--json` for machine-readable output:
|
||||
|
||||
```bash
|
||||
infsh app run <app-id> --input '{"prompt": "your prompt here"}' --json
|
||||
```
|
||||
|
||||
### 3. Parse the Output
|
||||
|
||||
The JSON output contains URLs to generated media. Present these to the user with `MEDIA:<url>` for inline display.
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Image Generation
|
||||
|
||||
```bash
|
||||
# Search for image apps
|
||||
infsh app list --search image
|
||||
|
||||
# FLUX Dev with LoRA
|
||||
infsh app run falai/flux-dev-lora --input '{"prompt": "sunset over mountains", "num_images": 1}' --json
|
||||
|
||||
# Gemini image generation
|
||||
infsh app run google/gemini-2-5-flash-image --input '{"prompt": "futuristic city", "num_images": 1}' --json
|
||||
|
||||
# Seedream (ByteDance)
|
||||
infsh app run bytedance/seedream-5-lite --input '{"prompt": "nature scene"}' --json
|
||||
|
||||
# Grok Imagine (xAI)
|
||||
infsh app run xai/grok-imagine-image --input '{"prompt": "abstract art"}' --json
|
||||
```
|
||||
|
||||
### Video Generation
|
||||
|
||||
```bash
|
||||
# Search for video apps
|
||||
infsh app list --search video
|
||||
|
||||
# Veo 3.1 (Google)
|
||||
infsh app run google/veo-3-1-fast --input '{"prompt": "drone shot of coastline"}' --json
|
||||
|
||||
# Seedance (ByteDance)
|
||||
infsh app run bytedance/seedance-1-5-pro --input '{"prompt": "dancing figure", "resolution": "1080p"}' --json
|
||||
|
||||
# Wan 2.5
|
||||
infsh app run falai/wan-2-5 --input '{"prompt": "person walking through city"}' --json
|
||||
```
|
||||
|
||||
### Local File Uploads
|
||||
|
||||
The CLI automatically uploads local files when you provide a path:
|
||||
|
||||
```bash
|
||||
# Upscale a local image
|
||||
infsh app run falai/topaz-image-upscaler --input '{"image": "/path/to/photo.jpg", "upscale_factor": 2}' --json
|
||||
|
||||
# Image-to-video from local file
|
||||
infsh app run falai/wan-2-5-i2v --input '{"image": "/path/to/image.png", "prompt": "make it move"}' --json
|
||||
|
||||
# Avatar with audio
|
||||
infsh app run bytedance/omnihuman-1-5 --input '{"audio": "/path/to/audio.mp3", "image": "/path/to/face.jpg"}' --json
|
||||
```
|
||||
|
||||
### Search & Research
|
||||
|
||||
```bash
|
||||
infsh app list --search search
|
||||
infsh app run tavily/tavily-search --input '{"query": "latest AI news"}' --json
|
||||
infsh app run exa/exa-search --input '{"query": "machine learning papers"}' --json
|
||||
```
|
||||
|
||||
### Other Categories
|
||||
|
||||
```bash
|
||||
# 3D generation
|
||||
infsh app list --search 3d
|
||||
|
||||
# Audio / TTS
|
||||
infsh app list --search tts
|
||||
|
||||
# Twitter/X automation
|
||||
infsh app list --search twitter
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **Never guess app IDs** — always run `infsh app list --search <term>` first. App IDs change and new apps are added frequently.
|
||||
2. **Always use `--json`** — raw output is hard to parse. The `--json` flag gives structured output with URLs.
|
||||
3. **Check authentication** — if commands fail with auth errors, run `infsh login` or verify `INFSH_API_KEY` is set.
|
||||
4. **Long-running apps** — video generation can take 30-120 seconds. The terminal tool timeout should be sufficient, but warn the user it may take a moment.
|
||||
5. **Input format** — the `--input` flag takes a JSON string. Make sure to properly escape quotes.
|
||||
|
||||
## Reference Docs
|
||||
|
||||
- `references/authentication.md` — Setup, login, API keys
|
||||
- `references/app-discovery.md` — Searching and browsing the app catalog
|
||||
- `references/running-apps.md` — Running apps, input formats, output handling
|
||||
- `references/cli-reference.md` — Complete CLI command reference
|
||||
@@ -0,0 +1,326 @@
|
||||
---
|
||||
title: "Pinggy Tunnel — Zero-install localhost tunnels over SSH via Pinggy"
|
||||
sidebar_label: "Pinggy Tunnel"
|
||||
description: "Zero-install localhost tunnels over SSH via Pinggy"
|
||||
---
|
||||
|
||||
{/* 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. */}
|
||||
|
||||
# Pinggy Tunnel
|
||||
|
||||
Zero-install localhost tunnels over SSH via Pinggy.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/devops/pinggy-tunnel` |
|
||||
| Path | `optional-skills/devops\pinggy-tunnel` |
|
||||
| Version | `0.1.0` |
|
||||
| Author | Teknium (teknium1), Hermes Agent |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos, windows |
|
||||
| Tags | `Pinggy`, `Tunnel`, `Networking`, `SSH`, `Webhook`, `Localhost` |
|
||||
|
||||
## 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.
|
||||
:::
|
||||
|
||||
# Pinggy Tunnel Skill
|
||||
|
||||
Expose a local service (dev server, webhook receiver, MCP endpoint, demo) to the public internet using a Pinggy SSH reverse tunnel. No daemon to install — the user's stock SSH client connects to `a.pinggy.io:443` and Pinggy hands back a public HTTP/HTTPS URL.
|
||||
|
||||
Free tier: 60-minute tunnels, random subdomain, no signup. Pro tier ($3/mo) is an opt-in with a token.
|
||||
|
||||
## When to Use
|
||||
|
||||
- User asks to "expose this locally", "share my dev server", "make this URL public", "tunnel port N", "get a public URL for a webhook"
|
||||
- Need to receive a webhook callback during a local task (Stripe, GitHub, Discord, AgentMail)
|
||||
- Sharing a one-off HTTP demo (MCP server, Ollama/vLLM endpoint, dashboard) with a remote party
|
||||
- The host has SSH but no `cloudflared` / `ngrok` binary, and installing one would be overkill
|
||||
|
||||
If the host already has `cloudflared` configured, prefer the `cloudflared-quick-tunnel` skill — Cloudflare quick tunnels don't expire after 60 minutes.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `ssh` on PATH (`ssh -V`). Default on Linux, macOS, and Windows 10+. No other install.
|
||||
- A local service listening on `127.0.0.1:<port>` before the tunnel starts. Pinggy will return URLs but they'll 502 until the local origin is up.
|
||||
|
||||
Optional:
|
||||
|
||||
- `PINGGY_TOKEN` env var for paid Pro features (persistent subdomain, custom domain, multiple tunnels, no 60-minute cap). Free tier needs no credentials.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Plain HTTP/HTTPS tunnel for port 8000 (free tier)
|
||||
ssh -p 443 -o StrictHostKeyChecking=no -o ServerAliveInterval=30 \
|
||||
-R0:localhost:8000 free@a.pinggy.io
|
||||
|
||||
# TCP tunnel (databases, raw SSH, etc.)
|
||||
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:5432 tcp@a.pinggy.io
|
||||
|
||||
# TLS tunnel (Pinggy can't decrypt — bring your own certs at origin)
|
||||
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:443 tls@a.pinggy.io
|
||||
|
||||
# Basic auth gate (b:user:pass)
|
||||
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
|
||||
"b:admin:secret+free@a.pinggy.io"
|
||||
|
||||
# Bearer token gate (k:token)
|
||||
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
|
||||
"k:mysecrettoken+free@a.pinggy.io"
|
||||
|
||||
# IP whitelist (w:CIDR)
|
||||
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
|
||||
"w:203.0.113.0/24+free@a.pinggy.io"
|
||||
|
||||
# Enable CORS + force HTTPS redirect
|
||||
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 \
|
||||
"co+x:https+free@a.pinggy.io"
|
||||
|
||||
# Pro tier (persistent URL, no 60-min cap)
|
||||
ssh -p 443 -o StrictHostKeyChecking=no -R0:localhost:8000 "$PINGGY_TOKEN+a.pinggy.io"
|
||||
```
|
||||
|
||||
## Procedure — Start a Tunnel and Get the URL
|
||||
|
||||
The model SHOULD use the `terminal` tool. The tunnel must stay alive for the duration of the share, so run it as a background process and parse the public URL from stdout.
|
||||
|
||||
### 1. Confirm a local origin is up
|
||||
|
||||
```bash
|
||||
curl -sI http://127.0.0.1:8000/ | head -1
|
||||
# expect HTTP/1.x 200 (or any non-connection-refused response)
|
||||
```
|
||||
|
||||
If nothing is listening yet, start it first (e.g. `python -m http.server 8000 --bind 127.0.0.1`). Pinggy will happily return a URL pointed at nothing — the user will see 502 until the origin comes up.
|
||||
|
||||
### 2. Launch the tunnel as a background process
|
||||
|
||||
Use `terminal(background=True)` and capture output to a logfile (Pinggy prints the URLs on stdout, then keeps the connection open):
|
||||
|
||||
```bash
|
||||
LOG=/tmp/pinggy-8000.log
|
||||
nohup ssh -p 443 \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o ServerAliveInterval=30 \
|
||||
-o ServerAliveCountMax=3 \
|
||||
-R0:localhost:8000 free@a.pinggy.io \
|
||||
> "$LOG" 2>&1 &
|
||||
echo $! > /tmp/pinggy-8000.pid
|
||||
```
|
||||
|
||||
`StrictHostKeyChecking=no` + `UserKnownHostsFile=/dev/null` skips the first-run host-key prompt. `ServerAliveInterval=30` keeps the SSH session from getting torn down by an idle NAT.
|
||||
|
||||
### 3. Parse the URL out of the log
|
||||
|
||||
```bash
|
||||
sleep 4
|
||||
grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/pinggy-8000.log | head -1
|
||||
```
|
||||
|
||||
Expected output looks like:
|
||||
|
||||
```
|
||||
You are not authenticated.
|
||||
Your tunnel will expire in 60 minutes.
|
||||
http://yqycl-98-162-69-48.a.free.pinggy.link
|
||||
https://yqycl-98-162-69-48.a.free.pinggy.link
|
||||
```
|
||||
|
||||
Hand the `https://...pinggy.link` URL to the user.
|
||||
|
||||
### 4. Verify
|
||||
|
||||
```bash
|
||||
curl -sI https://<the-url>/ | head -3
|
||||
# expect 200/302/whatever the local origin actually returns
|
||||
```
|
||||
|
||||
If you get `502 Bad Gateway`, the SSH session is up but the local origin isn't listening — fix step 1 first.
|
||||
|
||||
### 5. Teardown
|
||||
|
||||
```bash
|
||||
kill "$(cat /tmp/pinggy-8000.pid)"
|
||||
# or, if the pid file got lost:
|
||||
pkill -f 'ssh -p 443 .* free@a\.pinggy\.io'
|
||||
```
|
||||
|
||||
If you have a session_id from `terminal(background=True)`, prefer `process(action='kill', session_id=...)`.
|
||||
|
||||
## Access Control via Username Keywords
|
||||
|
||||
Pinggy stacks control flags into the SSH username separated by `+`. Always quote the whole `user@host` argument when it contains a `+`:
|
||||
|
||||
| Keyword | Effect |
|
||||
|---------|--------|
|
||||
| `b:user:pass` | HTTP Basic auth gate |
|
||||
| `k:token` | Bearer-token header gate (`Authorization: Bearer <token>`) |
|
||||
| `w:CIDR` | IP whitelist (single IP or CIDR, repeatable) |
|
||||
| `co` | Add `Access-Control-Allow-Origin: *` (CORS) |
|
||||
| `x:https` | Force HTTPS — auto-redirect HTTP to HTTPS |
|
||||
| `a:Name:Value` | Add request header |
|
||||
| `u:Name:Value` | Update request header |
|
||||
| `r:Name` | Remove request header |
|
||||
| `qr` | Print a QR code of the URL to stdout (handy for mobile sharing) |
|
||||
|
||||
Combine freely: `"b:admin:secret+co+x:https+free@a.pinggy.io"`.
|
||||
|
||||
## Web Debugger (optional)
|
||||
|
||||
Pinggy can mirror the inbound traffic to `localhost:4300` for inspection. Add a local forward to the SSH command:
|
||||
|
||||
```bash
|
||||
ssh -p 443 -L4300:localhost:4300 -R0:localhost:8000 free@a.pinggy.io
|
||||
```
|
||||
|
||||
Then open `http://localhost:4300` in a browser to see live request/response pairs.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **60-minute hard cap on the free tier.** The SSH session terminates at the 60-minute mark; the URL goes dead. For longer shares, either use `PINGGY_TOKEN` (Pro) or auto-restart with a shell loop (note that the URL changes on every restart for free-tier).
|
||||
- **Free-tier URL is random and changes on restart.** Don't bookmark it, don't paste it into a config file. Re-parse from the log each time.
|
||||
- **Concurrent free tunnels are limited to one per source IP.** Starting a second tunnel from the same machine usually kills the first. Pro tier lifts this.
|
||||
- **`+` in usernames must be quoted.** Bare `ssh ... b:admin:secret+free@a.pinggy.io` works in bash but breaks under shells that treat `+` specially or when assembled programmatically. Always wrap in double quotes.
|
||||
- **Don't tunnel anything sensitive without an access-control flag.** A bare HTTP tunnel is reachable by anyone with the URL. Use `b:`, `k:`, or `w:` for non-public services.
|
||||
- **`process(action='log')` may miss SSH banner output.** Pinggy prints the URLs and then the SSH session goes interactive. Always redirect to a logfile and `grep` the file directly — same pattern as `cloudflared-quick-tunnel`.
|
||||
- **Host-key prompt on first run.** Default OpenSSH config asks the user to accept Pinggy's host key. Always pass `-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null` for unattended runs.
|
||||
- **TCP and TLS tunnels return a `<subdomain>.a.pinggy.online:<port>` pair, not an https URL.** Parse with a different regex (`tcp://` and a port). Don't assume every Pinggy tunnel is HTTP.
|
||||
- **Pro mode requires the token as the username, not a flag.** Use `"$PINGGY_TOKEN+a.pinggy.io"` (no `free@`). With a token you can also add `:persistent` for a stable subdomain — see `pinggy.io/docs/`.
|
||||
|
||||
## Recipes
|
||||
|
||||
Composite patterns combining a local origin with a Pinggy tunnel. Each recipe is self-contained — start the origin, start the tunnel, parse the URL, hand it back to the user.
|
||||
|
||||
### Recipe 1 — Receive a webhook callback
|
||||
|
||||
Use this when an external service (Stripe, GitHub, Discord, AgentMail, etc.) needs to POST to a publicly reachable URL during a local task.
|
||||
|
||||
```bash
|
||||
# 1. Tiny capturing server: every request gets appended to /tmp/webhook-hits.log
|
||||
cat >/tmp/webhook-server.py <<'PY'
|
||||
import http.server, json, datetime, pathlib
|
||||
LOG = pathlib.Path("/tmp/webhook-hits.log")
|
||||
class H(http.server.BaseHTTPRequestHandler):
|
||||
def _capture(self):
|
||||
n = int(self.headers.get("content-length") or 0)
|
||||
body = self.rfile.read(n).decode("utf-8", "replace") if n else ""
|
||||
rec = {"t": datetime.datetime.utcnow().isoformat(), "path": self.path,
|
||||
"method": self.command, "headers": dict(self.headers), "body": body}
|
||||
with LOG.open("a") as f: f.write(json.dumps(rec) + "\n")
|
||||
self.send_response(200); self.send_header("content-type","application/json")
|
||||
self.end_headers(); self.wfile.write(b'{"ok":true}\n')
|
||||
def do_GET(self): self._capture()
|
||||
def do_POST(self): self._capture()
|
||||
def log_message(self,*a,**k): pass
|
||||
http.server.HTTPServer(("127.0.0.1", 18080), H).serve_forever()
|
||||
PY
|
||||
nohup python /tmp/webhook-server.py >/tmp/webhook-server.log 2>&1 &
|
||||
echo $! >/tmp/webhook-server.pid
|
||||
|
||||
# 2. Tunnel — bearer-token-gate so randos can't pollute the capture log
|
||||
nohup ssh -p 443 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o ServerAliveInterval=30 \
|
||||
-R0:localhost:18080 "k:$(openssl rand -hex 12)+free@a.pinggy.io" \
|
||||
>/tmp/webhook-pinggy.log 2>&1 &
|
||||
echo $! >/tmp/webhook-pinggy.pid
|
||||
sleep 5
|
||||
URL=$(grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/webhook-pinggy.log | head -1)
|
||||
echo "Webhook URL: $URL"
|
||||
|
||||
# 3. While the agent works, watch hits land
|
||||
tail -f /tmp/webhook-hits.log
|
||||
```
|
||||
|
||||
Hand `$URL` to the service that needs to call you. Teardown: `kill $(cat /tmp/webhook-server.pid) $(cat /tmp/webhook-pinggy.pid)`.
|
||||
|
||||
### Recipe 2 — Expose an MCP server over HTTP/SSE
|
||||
|
||||
Use when a remote MCP client (Claude Desktop on another machine, a teammate's editor, etc.) needs to reach an MCP server running on the local box. Only works for MCP servers that speak HTTP transport — stdio-mode servers can't be tunneled.
|
||||
|
||||
```bash
|
||||
# 1. Start the MCP server in HTTP mode (example: a FastMCP server on port 8765)
|
||||
nohup python my_mcp_server.py --transport http --port 8765 \
|
||||
>/tmp/mcp-server.log 2>&1 &
|
||||
echo $! >/tmp/mcp-server.pid
|
||||
|
||||
# 2. Tunnel with a bearer token — MCP traffic should not be open to the internet
|
||||
TOKEN=$(openssl rand -hex 16)
|
||||
nohup ssh -p 443 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o ServerAliveInterval=30 \
|
||||
-R0:localhost:8765 "k:$TOKEN+free@a.pinggy.io" \
|
||||
>/tmp/mcp-pinggy.log 2>&1 &
|
||||
echo $! >/tmp/mcp-pinggy.pid
|
||||
sleep 5
|
||||
URL=$(grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/mcp-pinggy.log | head -1)
|
||||
echo "MCP URL: $URL"
|
||||
echo "Bearer token: $TOKEN"
|
||||
```
|
||||
|
||||
The remote client connects to `$URL` with `Authorization: Bearer $TOKEN`. Hermes' own native MCP client config: `{"transport": "http", "url": "<URL>", "headers": {"Authorization": "Bearer <TOKEN>"}}`.
|
||||
|
||||
### Recipe 3 — Expose a local LLM endpoint (Ollama / vLLM / llama.cpp)
|
||||
|
||||
Share a local model with a remote caller (another agent, a phone, a teammate). Ollama listens on `:11434`, vLLM and llama.cpp typically on `:8000`.
|
||||
|
||||
```bash
|
||||
# Pre-req: the model server is already running on 127.0.0.1:11434 (Ollama default)
|
||||
TOKEN=$(openssl rand -hex 16)
|
||||
nohup ssh -p 443 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o ServerAliveInterval=30 \
|
||||
-R0:localhost:11434 "k:$TOKEN+co+free@a.pinggy.io" \
|
||||
>/tmp/llm-pinggy.log 2>&1 &
|
||||
echo $! >/tmp/llm-pinggy.pid
|
||||
sleep 5
|
||||
URL=$(grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/llm-pinggy.log | head -1)
|
||||
echo "Endpoint: $URL"
|
||||
echo "Token: $TOKEN"
|
||||
|
||||
# Verify
|
||||
curl -s "$URL/api/tags" -H "Authorization: Bearer $TOKEN" | head
|
||||
```
|
||||
|
||||
`co` enables CORS so a browser caller can hit the endpoint. Drop `co` for backend-only callers. For an OpenAI-compatible vLLM/llama.cpp endpoint, callers use base URL `$URL/v1` with `Authorization: Bearer $TOKEN` — but note Pinggy strips/replaces nothing in the body, so the model server itself sees Pinggy's token; the local server should be configured to ignore auth (it's already on `127.0.0.1`) and let Pinggy do the gating.
|
||||
|
||||
### Recipe 4 — Share a dev server with a one-shot password
|
||||
|
||||
The fastest "let a teammate poke at my running app" pattern. Random password, prints once, dies when you Ctrl-C.
|
||||
|
||||
```bash
|
||||
PASS=$(openssl rand -base64 12 | tr -d '+/=' | head -c 12)
|
||||
echo "Dev server password: $PASS"
|
||||
ssh -p 443 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
-o ServerAliveInterval=30 \
|
||||
-R0:localhost:3000 "b:dev:$PASS+co+x:https+free@a.pinggy.io"
|
||||
# URL prints to the terminal. Share URL + password. Ctrl-C to tear down.
|
||||
```
|
||||
|
||||
`b:dev:$PASS` gates the URL with HTTP Basic auth. `x:https` forces TLS. `co` adds CORS for SPA frontends.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# End-to-end: spin up a trivial origin, tunnel it, hit it, tear down
|
||||
python -m http.server 18000 --bind 127.0.0.1 >/tmp/origin.log 2>&1 &
|
||||
ORIGIN_PID=$!
|
||||
|
||||
nohup ssh -p 443 \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-R0:localhost:18000 free@a.pinggy.io >/tmp/pinggy-verify.log 2>&1 &
|
||||
SSH_PID=$!
|
||||
|
||||
sleep 5
|
||||
URL=$(grep -oE 'https://[a-z0-9-]+\.[a-z]+\.pinggy\.link' /tmp/pinggy-verify.log | head -1)
|
||||
echo "URL: $URL"
|
||||
curl -sI "$URL/" | head -1
|
||||
|
||||
kill "$SSH_PID" "$ORIGIN_PID"
|
||||
```
|
||||
|
||||
Expected: a `pinggy.link` URL and `HTTP/2 200` on the curl head.
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Setup Wizard Generator — Generate a bash wizard guiding a human through manual setup"
|
||||
sidebar_label: "Setup Wizard Generator"
|
||||
description: "Generate a bash wizard guiding a human through manual setup"
|
||||
---
|
||||
|
||||
{/* 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. */}
|
||||
|
||||
# Setup Wizard Generator
|
||||
|
||||
Generate a bash wizard guiding a human through manual setup.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/devops/setup-wizard-generator` |
|
||||
| Path | `optional-skills/devops\setup-wizard-generator` |
|
||||
| Version | `1.0.0` |
|
||||
| Author | Matt Pocock (mattpocock/skills, wizard) + Hermes Agent |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos |
|
||||
| Tags | `wizard`, `setup`, `onboarding`, `credentials`, `secrets`, `migration`, `bash`, `human-in-the-loop` |
|
||||
|
||||
## 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.
|
||||
:::
|
||||
|
||||
# Setup Wizard Generator
|
||||
|
||||
Generates an interactive bash **wizard**: a script that walks a human, step
|
||||
by step, through a manual procedure that is tedious to do by hand and tedious
|
||||
to re-explain every time. It opens each URL, says exactly what to click and
|
||||
copy, captures the values, writes them where they belong (`.env`, GitHub
|
||||
secrets), confirms at every stage, and shows how many stages are left.
|
||||
|
||||
Ported from mattpocock/skills' MIT-licensed `wizard` skill.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Provisioning infrastructure or third-party services (Stripe, Supabase,
|
||||
DNS, OAuth apps) where only a human can click through the dashboard
|
||||
- Setting up credentials, CI secrets, or repo variables
|
||||
- One-off migrations or cutovers with irreversible human-gated steps
|
||||
- Any procedure the user will hand to a teammate to run
|
||||
|
||||
Do NOT use for steps the agent can perform itself — do those directly.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `bash`; `gh` CLI only if stages write GitHub secrets/variables
|
||||
- The library template: `templates/template.sh` in this skill's directory
|
||||
|
||||
## Procedure
|
||||
|
||||
### 1. Scope the procedure
|
||||
|
||||
Work out every manual step the human must take and every value captured
|
||||
along the way. Read the repo first, don't ask cold:
|
||||
|
||||
- Setup: `.env`, `.env.example`, `README`, `docker-compose*`, framework
|
||||
config, and `.github/workflows/*` (every `secrets.*` / `vars.*` reference
|
||||
is a value the wizard must produce).
|
||||
- Migration/cutover: the current state, the target state, and the
|
||||
irreversible actions between them.
|
||||
|
||||
Show the user the ordered stage list and the values each produces; they may
|
||||
add, drop, or reorder. Done when every stage is named in order and, for each
|
||||
captured value, you know (a) where the human gets it, (b) where it's written
|
||||
(`.env`, a GitHub secret, both, or nowhere), and (c) whether it's secret
|
||||
(hidden entry) or public.
|
||||
|
||||
### 2. Map each stage's journey
|
||||
|
||||
For each stage, write the precise path a human follows: which URL to open,
|
||||
what to do there, where the value is shown — e.g. "Dashboard → Developers →
|
||||
API keys → Reveal test key → copy". Where you don't know the current UI or
|
||||
exact command, say so and check the docs or ask — never invent steps that
|
||||
may not exist.
|
||||
|
||||
### 3. Author the wizard
|
||||
|
||||
Copy `templates/template.sh` (from this skill's directory) to the target
|
||||
path. Replace the example stage with one `stage` per step, in dependency
|
||||
order. Set `TOTAL_STAGES` to the number of stages you wrote.
|
||||
|
||||
Library helpers: `stage`, `say`/`step`/`note`/`warn`, `open_url`,
|
||||
`ask`/`ask_secret`, `write_env`, `set_secret`/`set_var`, `pause`/`confirm`,
|
||||
`banner`, `finish`. The library above the `STAGES` marker is identical in
|
||||
every wizard — never hand-edit it; that consistency is the point.
|
||||
|
||||
Hold the bar the template sets: open the URL before asking for its value,
|
||||
`ask_secret` for anything secret, `write_env` every persisted value,
|
||||
`set_secret` only what CI actually needs, and `confirm` before anything
|
||||
irreversible. Each `stage` clears the screen — keep one focused task per
|
||||
stage so nothing the human needs scrolls away.
|
||||
|
||||
A wizard is ephemeral by default: save it to a scratch or `scripts/` path,
|
||||
delete it when the job's done. Commit it only when the user wants a
|
||||
repeatable setup path living in the repo.
|
||||
|
||||
### 4. Verify and hand off
|
||||
|
||||
- `bash -n <script>`; run `shellcheck` if available; `chmod +x <script>`.
|
||||
- Do NOT run it end-to-end yourself: it opens browsers and blocks on human
|
||||
input. Trace it statically: every value from step 1 is captured and lands
|
||||
where step 1 said, and every `set_secret` name exactly matches a
|
||||
`secrets.*` reference in CI.
|
||||
- Tell the user how to run it. If it's repeatable, commit it and link it
|
||||
from the README.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **Editing the library section.** Everything above the `STAGES` marker is
|
||||
the wizard library; author only below it.
|
||||
2. **Inventing dashboard paths.** Third-party UIs drift. If unsure of the
|
||||
click path, verify against current docs or flag it as approximate.
|
||||
3. **`set_secret` for values CI doesn't use.** Only push to GitHub secrets
|
||||
what a workflow actually references.
|
||||
4. **Running the wizard yourself.** It blocks on human input; static tracing
|
||||
plus `bash -n` is the verification.
|
||||
5. **One mega-stage.** Screen clearing per stage means a long stage scrolls
|
||||
critical instructions away; split it.
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] Stage list confirmed with the user before authoring
|
||||
- [ ] `bash -n` passes; script is executable
|
||||
- [ ] Every captured value traced to its declared destination
|
||||
- [ ] Every `set_secret` name matches a CI `secrets.*` reference
|
||||
- [ ] Library section untouched from the template
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
title: "Watchers — Poll RSS, JSON APIs, and GitHub with watermark dedup"
|
||||
sidebar_label: "Watchers"
|
||||
description: "Poll RSS, JSON APIs, and GitHub with watermark dedup"
|
||||
---
|
||||
|
||||
{/* 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. */}
|
||||
|
||||
# Watchers
|
||||
|
||||
Poll RSS, JSON APIs, and GitHub with watermark dedup.
|
||||
|
||||
## Skill metadata
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Source | Optional — install with `hermes skills install official/devops/watchers` |
|
||||
| Path | `optional-skills/devops\watchers` |
|
||||
| Version | `1.0.0` |
|
||||
| Author | Hermes Agent |
|
||||
| License | MIT |
|
||||
| Platforms | linux, macos |
|
||||
| Tags | `cron`, `polling`, `rss`, `github`, `http`, `automation`, `monitoring` |
|
||||
|
||||
## 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.
|
||||
:::
|
||||
|
||||
# Watchers
|
||||
|
||||
Poll external sources on an interval and react only to new items. Three ready-made scripts plus a shared watermark helper; wire them into a cron job (or run them ad-hoc from the terminal).
|
||||
|
||||
## When to Use
|
||||
|
||||
- User wants to watch an RSS/Atom feed and be notified of new entries
|
||||
- User wants to watch a GitHub repo's issues / pulls / releases / commits
|
||||
- User wants to poll an arbitrary JSON endpoint and get notified on new items
|
||||
- User asks for "a watcher for X" or "notify me when X changes"
|
||||
|
||||
## Mental model
|
||||
|
||||
A watcher is just a script that:
|
||||
|
||||
1. Fetches data from the external source
|
||||
2. Compares against a watermark file of previously-seen IDs
|
||||
3. Writes the new watermark back
|
||||
4. Prints new items to stdout (or nothing on no-change)
|
||||
|
||||
The scripts below handle all three. The agent runs them via the terminal tool — from a cron job, a webhook, or an interactive chat — and reports what's new.
|
||||
|
||||
## Ready-made scripts
|
||||
|
||||
All three live in `$HERMES_HOME/skills/devops/watchers/scripts/` once the skill is installed. Each reads `WATCHER_STATE_DIR` (defaults to `$HERMES_HOME/watcher-state/`) for its state file, keyed by the `--name` argument.
|
||||
|
||||
| Script | What it watches | Dedup key |
|
||||
|---|---|---|
|
||||
| `watch_rss.py` | RSS 2.0 or Atom feed URL | `<guid>` / `<id>` |
|
||||
| `watch_http_json.py` | Any JSON endpoint returning a list of objects | Configurable id field |
|
||||
| `watch_github.py` | GitHub issues / pulls / releases / commits for a repo | `id` / `sha` |
|
||||
|
||||
All three:
|
||||
|
||||
- First run records a baseline — never replays existing feed
|
||||
- Watermark is a bounded ID set (max 500) to cap memory
|
||||
- Output format: `## <title>\n<url>\n\n<optional body>` per item
|
||||
- Empty stdout on no-new — the caller treats that as silent
|
||||
- Non-zero exit on fetch errors
|
||||
|
||||
## Usage
|
||||
|
||||
Run a watcher directly from the terminal tool:
|
||||
|
||||
```bash
|
||||
python $HERMES_HOME/skills/devops/watchers/scripts/watch_rss.py \
|
||||
--name hn --url https://news.ycombinator.com/rss --max 5
|
||||
```
|
||||
|
||||
Watch a GitHub repo (set `GITHUB_TOKEN` in `${HERMES_HOME:-~/.hermes}/.env` to avoid the 60 req/hr anonymous rate limit):
|
||||
|
||||
```bash
|
||||
python $HERMES_HOME/skills/devops/watchers/scripts/watch_github.py \
|
||||
--name hermes-issues --repo NousResearch/hermes-agent --scope issues
|
||||
```
|
||||
|
||||
Poll an arbitrary JSON API:
|
||||
|
||||
```bash
|
||||
python $HERMES_HOME/skills/devops/watchers/scripts/watch_http_json.py \
|
||||
--name api --url https://api.example.com/events \
|
||||
--id-field event_id --items-path data.events
|
||||
```
|
||||
|
||||
## Wiring into cron
|
||||
|
||||
Ask the agent to schedule a cron job with a prompt like:
|
||||
|
||||
> Every 15 minutes, run `watch_rss.py --name hn --url https://news.ycombinator.com/rss`. If it prints anything, summarize the headlines and deliver them. If it prints nothing, stay silent.
|
||||
|
||||
The agent invokes the script via the terminal tool inside the cron job's agent loop; no changes to cron's built-in `--script` flag are needed.
|
||||
|
||||
## State files
|
||||
|
||||
Every watcher writes `$HERMES_HOME/watcher-state/<name>.json`. Inspect:
|
||||
|
||||
```bash
|
||||
cat $HERMES_HOME/watcher-state/hn.json
|
||||
```
|
||||
|
||||
Force a replay (next run treated as first poll):
|
||||
|
||||
```bash
|
||||
rm $HERMES_HOME/watcher-state/hn.json
|
||||
```
|
||||
|
||||
## Writing your own
|
||||
|
||||
All three scripts use the same template: load watermark, fetch, diff, save, emit. `scripts/_watermark.py` is the shared helper; import it to get atomic writes + bounded ID set + first-run baseline for free. See any of the three reference scripts for how little boilerplate it takes.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Printing a "no new items" header every tick.** Callers rely on empty stdout = silent. If you print anything on an empty delta, you spam the channel. The shipped scripts handle this; custom scripts must too.
|
||||
2. **Expecting the first run to emit items.** It won't — first run records a baseline. If you need an initial digest, delete the state file after the first run or add a `--prime-with-latest N` flag in your own script.
|
||||
3. **Unbounded watermark growth.** The shared helper caps at 500 IDs. Raise it for high-churn feeds; lower it on constrained filesystems.
|
||||
4. **Putting the state dir where the agent's sandbox can't write.** `$HERMES_HOME/watcher-state/` is always writable. Docker/Modal backends may not see arbitrary host paths.
|
||||
Reference in New Issue
Block a user