The webserver serves the repo checkout directly as static files, so index.html at the repo root must be the production build (pointing at hashed /assets/*.js) rather than the dev template (/src/main.tsx), which was being served with an unrecognized application/octet-stream MIME type and blocked by the browser. - Move src/, public/, scripts/, server/, package.json, vite.config.ts, tsconfig.json, .env(.example), and the dev index.html into app/. - Add app/scripts/deploy.mjs: copies app/dist/* into the repo root after every `vite build`, cleaning up stale hashed filenames. - Wire it into `npm run build` (app/package.json). - Update run.sh to operate from app/, and .gitignore for the new layout. - Commit the current build output (index.html, assets/, manifest, sw.js, workbox-*.js, favicon.svg, icons/, LICENSE) to the repo root. - Update README with the new project layout and deployment model.
234 lines
9.0 KiB
Markdown
234 lines
9.0 KiB
Markdown
# Hermes PWA
|
|
|
|
A mobile-first **Progressive Web App** front end for a self-hosted
|
|
[Hermes Agent](https://hermes-agent.nousresearch.com) (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](https://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:
|
|
|
|
```bash
|
|
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
|
|
```
|
|
|
|
```bash
|
|
hermes gateway # → [API Server] API server listening on http://127.0.0.1:8642
|
|
```
|
|
|
|
Verify from anywhere:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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_KEY` is **optional**. Only set it if you'd rather keep the key on
|
|
the proxy so it never reaches the browser:
|
|
>
|
|
> ```bash
|
|
> 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:
|
|
|
|
```bash
|
|
API_SERVER_CORS_ORIGINS=https://your-pwa-origin
|
|
```
|
|
|
|
For local development you can skip CORS entirely with a dev proxy:
|
|
|
|
```bash
|
|
# .env
|
|
HERMES_DEV_TARGET=https://api.beaver.derez.ai
|
|
```
|
|
|
|
---
|
|
|
|
## Quick start
|
|
|
|
```bash
|
|
./run.sh # dev server -> http://localhost:5173
|
|
```
|
|
|
|
Then on the config screen enter your **API server URL** and **API key**.
|
|
|
|
### `run.sh` modes
|
|
|
|
```bash
|
|
./run.sh # dev server (http://localhost:5173)
|
|
./run.sh build # production build, deployed straight into the repo root
|
|
./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
|
|
```
|
|
|
|
### Deployment model: the repo root *is* the web root
|
|
|
|
The Vite project lives in `app/`. Running a build (`./run.sh build` or
|
|
`cd app && npm run build`) compiles it and then copies the output
|
|
(`index.html`, `assets/`, `manifest.webmanifest`, `sw.js`, `workbox-*.js`,
|
|
`favicon.svg`, `icons/`, `LICENSE`) directly into the **repository root** —
|
|
see `app/scripts/deploy.mjs`. Those deployed files are committed to git.
|
|
|
|
This means a webserver that simply serves the raw repository checkout as
|
|
static files (no build step, no custom MIME types, no reverse-proxy config)
|
|
works out of the box: `index.html` in the repo root already references
|
|
hashed, plain `.js`/`.css` assets — nothing points at `/src/*.tsx` in
|
|
production, so there's no "unauthorized MIME type" issue.
|
|
|
|
To publish a new version:
|
|
|
|
```bash
|
|
cd app
|
|
npm run build # builds app/dist, then syncs it into the repo root
|
|
cd ..
|
|
git add -A
|
|
git commit -m "Deploy: <description>"
|
|
git push # or however the server pulls the repo
|
|
```
|
|
|
|
> Note: `app/` (source, dev config, `node_modules`) is still present in the
|
|
> checkout, so if the webserver serves the whole repo folder, the raw
|
|
> TypeScript source under `app/src` is technically reachable as plain text.
|
|
> It is never *executed* as a module by the deployed app (the deployed
|
|
> `index.html` only references the built `assets/*.js`), so this is not a
|
|
> functional issue — just something to be aware of if source privacy
|
|
> matters to you.
|
|
|
|
| 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 `/transcribe` or `/tts`
|
|
endpoint, so dictation uses the browser's `SpeechRecognition` and replies use
|
|
`speechSynthesis`. 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 own `API_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 in `app/src/api/client.ts`.
|
|
|
|
## Project layout
|
|
|
|
```
|
|
index.html, assets/, manifest.webmanifest, # repo root = deployed build
|
|
sw.js, workbox-*.js, favicon.svg, icons/, # output (see Deployment model above)
|
|
LICENSE
|
|
app/ # Vite project (source, dev config)
|
|
index.html # dev entry (-> /src/main.tsx); NOT served in production
|
|
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, …
|
|
scripts/
|
|
gen-icons.mjs # dependency-free PNG icon generator
|
|
deploy.mjs # copies app/dist/* into the repo root after `vite build`
|
|
server/
|
|
proxy.mjs # optional static host + /api,/v1 reverse proxy (no deps)
|
|
```
|
|
|
|
## License
|
|
|
|
Licensed under the [PolyForm Internal Use License 1.0.0](./LICENSE) — 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`.
|