Hermes PWA
A mobile-first Progressive Web App front end for a self-hosted Hermes Agent (Nous Research). It talks directly to the Hermes Agent API Server — the OpenAI-compatible HTTP + SSE endpoint — so you can chat with your agent, browse sessions, and use text, voice, and images from an installable app.
Branding (logo, colour scheme, app icons) follows derez.ai; the logo in the sidebar links to the homepage.
Which URL do I use? (dashboard vs API server)
Hermes exposes two different servers. This app needs the second one.
| URL | What it is | Auth | Works with this app? |
|---|---|---|---|
https://your-host/ (e.g. beaver.derez.ai) |
The web dashboard | username + password → session cookie | ❌ No. It only allows CORS from its own origin and never sends Access-Control-Allow-Credentials: true, so a separately-hosted app can't use its cookie. |
https://api.your-host/ (e.g. api.beaver.derez.ai, default port 8642) |
The API Server | Authorization: Bearer <API_SERVER_KEY> |
✅ Yes — this is the one. |
So enter the API server URL and your API_SERVER_KEY on the config
screen. There is no QR login and no username/password here — the API server uses
a bearer token.
Enabling the API server (on the Hermes host)
If it isn't already running, add to ~/.hermes/.env and start the gateway:
API_SERVER_ENABLED=true
API_SERVER_KEY=choose-a-long-random-secret
# Only needed if a browser calls the API directly (see CORS below):
# API_SERVER_CORS_ORIGINS=https://your-pwa-origin
hermes gateway # → [API Server] API server listening on http://127.0.0.1:8642
Verify from anywhere:
curl -s https://api.your-host/health
# {"status": "ok", "platform": "hermes-agent", "version": "..."}
curl -s -H "Authorization: Bearer $API_SERVER_KEY" https://api.your-host/v1/capabilities
Reaching the API server (CORS)
The API server has no browser CORS by default. Two options:
Option A — same origin (recommended): use the bundled proxy
server/proxy.mjs serves the built app and proxies /api/* and /v1/* to
the API server, so everything is same-origin and CORS never applies.
You normally paste the API key into the app in the browser (it's saved in
that browser's localStorage). The proxy does not need the key:
HERMES_TARGET=https://api.beaver.derez.ai PORT=8787 ./run.sh serve
# open http://localhost:8787
Then in the app's config screen, tick "Same origin (behind the bundled proxy)" and paste your API key. Done — no key on the server, no CORS.
HERMES_API_KEYis optional. Only set it if you'd rather keep the key on the proxy so it never reaches the browser:HERMES_TARGET=https://api.beaver.derez.ai HERMES_API_KEY=your-key PORT=8787 ./run.sh serve
Option B — direct, cross-origin
Set an explicit allowlist on the Hermes host and enter the API URL + key in the app's config screen:
API_SERVER_CORS_ORIGINS=https://your-pwa-origin
For local development you can skip CORS entirely with a dev proxy:
# .env
HERMES_DEV_TARGET=https://api.beaver.derez.ai
Quick start
./run.sh # dev server -> http://localhost:5173
Then on the config screen enter your API server URL and API key.
run.sh modes
./run.sh # dev server (http://localhost:5173)
./run.sh build # production build into dist/
./run.sh serve # build if needed, then host + proxy /api/*, /v1/* (needs HERMES_TARGET)
./run.sh preview # build, then serve the build with vite preview
./run.sh icons # regenerate the PWA icon set
./run.sh typecheck # type-check without emitting
| Variable | Used by | Meaning |
|---|---|---|
HERMES_TARGET |
serve |
API server base URL to proxy to |
HERMES_API_KEY |
serve |
Optional — injected as Authorization: Bearer … |
PORT |
serve |
Port to listen on (default 8787) |
HERMES_DEV_TARGET |
dev |
API server base URL to proxy to during development |
Features
- Config screen — add one or more API-server connections (URL, API key, optional profile prefix, custom headers) with a "Test" probe.
- Sessions — list, open, create, rename, fork, and delete; filter by title.
- Streaming chat — token-by-token replies over SSE, with live tool-call cards, cancel, and mid-turn steer.
- Images — attach/paste/drag-drop; sent inline as
data:URLs. - Voice — dictation via the browser's Web Speech API and spoken replies via
on-device speech synthesis. Tap the mic to speak your prompt; tap again to stop.
Requires a secure context (HTTPS or
localhost) and a browser with the Web Speech API (Chrome, Edge, Safari). Note: Chrome's dictation sends audio to Google's servers. - Installable PWA — offline app shell, home-screen install, standalone display.
Notes & limitations
- Voice is client-side. The API server has no
/transcribeor/ttsendpoint, so dictation uses the browser'sSpeechRecognitionand replies usespeechSynthesis. Support varies by browser (best on Chrome/Edge/Safari). - Profiles. The API server routes profiles via a
/p/<profile>/…prefix, and each profile has its ownAPI_SERVER_KEY. Add one connection per profile to see them all; the sidebar switches between connections. - Session ownership. The API server can create and drive its own sessions, but it refuses to write to a session owned by another surface (CLI, TUI, dashboard, messaging) — those open read-only.
- Response shapes. The Sessions REST API's exact JSON envelope isn't fully
documented, so the client normalises several shapes (
sessions/data/items,id/session_id, string/array content). If a field looks off on your version, it's a one-line tweak insrc/api/client.ts.
Project layout
src/
api/
client.ts # HermesClient: bearer auth, sessions REST, SSE chat
types.ts # API server types + normalised UI types
hooks/
useSpeechRecognition.ts # browser dictation
useTts.ts # browser speech synthesis
lib/
config.ts # saved connections (localStorage)
format.ts # time / token formatting
markdown.tsx # tiny safe markdown renderer
state/
store.ts # zustand store: connections, sessions, streaming
components/ # ConfigScreen, ConnectionForm, Sidebar, ChatView, Composer, …
server/
proxy.mjs # optional static host + /api,/v1 reverse proxy (no deps)
scripts/
gen-icons.mjs # dependency-free PNG icon generator
License
Licensed under the PolyForm Internal Use License 1.0.0 — internal
business use only; redistribution is not permitted. The license text is also
shipped in the build output as dist/LICENSE.
API endpoints used
GET /health, GET /v1/capabilities, GET /v1/models,
GET|POST /api/sessions, GET|PATCH|DELETE /api/sessions/{id},
GET /api/sessions/{id}/messages, POST /api/sessions/{id}/fork,
POST /api/sessions/{id}/chat/stream (SSE), and
POST /v1/runs/{id}/stop|steer|approval|clarify.