Metadata-Version: 2.3
Name: yoooclaw-hermes-plugin
Version: 0.10.0
Summary: YoooClaw tools and APP messaging platform adapter for Hermes Agent
Author: YoooClaw
License: MIT
Requires-Dist: python-socks[asyncio]>=2,<3
Requires-Dist: websockets>=14,<16
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# hermes-plugin

YoooClaw integration for [Hermes Agent](https://hermes-agent.nousresearch.com/).

This repository is the Python plugin boundary between Hermes and YoooClaw.
The plugin runs **daemonless**: it holds the shared profile storage's writer
lock directly and writes notifications, recordings, images, and captured web
pages itself, following the same on-disk layout the `@yoooclaw/cli` Go daemon
uses — no CLI daemon process is started. The plugin owns both external
WebSocket transports: the `openclaw-service` phone Relay and the APP
conversation Relay are merged onto that single connection. One-shot light
control and light-rule CRUD are still bridged to the embedded CLI as
subprocess calls, since the rule files belong to the CLI.

Production plugin artifacts bundle the platform-specific `yoooclaw` executable
itself. Users do not need a preinstalled CLI, and the plugin does not download
the CLI on first run. The embedded executable is verified and installed into a
plugin-managed path before use.

The release pipeline downloads the four verified native assets from the pinned
`cli-v*` release in `YoooClaw/cli`, then creates one Python wheel per
platform. Each wheel carries only its own executable and a SHA-256 manifest:

```text
darwin-arm64
darwin-x64
linux-arm64
linux-x64
win32-x64
```

The build job emits the wheels, `cli-wheel-index.json`, and `SHA256SUMS` once,
then promotes that exact `dist/` bundle through the release flow: test OSS
first, production OSS after approval, and finally PyPI / GitHub Release for
stable versions. Installers and channel markers remain environment-specific and
are regenerated at deploy time. This keeps the Git repository small while
guaranteeing that QA verifies the same bytes we later ship.

## One-command install

Install the plugin, store the YoooClaw API key, enable the Hermes tool plugin
and APP adapter, and restart the gateway:

```bash
curl -fsSL https://artifact.yoooclaw.com/hermes-plugin/install.sh | bash -s -- --api-key "ock_..."
```

Windows (PowerShell):

```powershell
& ([scriptblock]::Create((irm https://artifact.yoooclaw.com/hermes-plugin/install.ps1))) --api-key ock_...
```

Re-run the same command to update. The PowerShell installer resolves the
matching `win_amd64` wheel from OSS, locates the Hermes Agent virtualenv under
the standard Windows install locations, bootstraps `pip` with `ensurepip` when
needed, and installs with `pip install --upgrade`.

The installer requires Hermes Agent `>= 0.14.0`. Older Hermes builds do not
provide the plugin platform API (`PluginContext.register_platform`) used by the
APP adapter, so the installer stops with an upgrade message instead of leaving a
half-enabled plugin behind.

The installer writes the shared key to `~/.yoooclaw/credentials.json`, removes
stale API-key and legacy local APP authorization environment overrides from
`~/.hermes/.env`, and ensures both `yoooclaw` and `yoooclaw_app` are present in
`plugins.enabled` in `~/.hermes/config.yaml`. Pass `--no-enable-app` or set
`YOOOCLAW_HERMES_ENABLE_APP=0` for a tools-only install. It also installs the
bundled `yoooclaw`/`yc` CLI
artifact into `~/.yoooclaw/hermes-plugin/bin`, maintains managed shims, and links
those commands next to the detected `hermes` executable when that directory is
writable. Set `YOOOCLAW_HERMES_CLI_LINK_DIR` to choose a different link
directory, or `YOOOCLAW_HERMES_INSTALL_CLI=0` to skip shell command links. If a
newer standalone CLI (0.9+) already registered OS-level autostart, the
installer disables it before installing so it can't reclaim storage ownership
after a login/reboot; if it can't disable it (the installed CLI predates
`daemon autostart`), the takeover is refused outright. When Hermes runs under a
named profile (`hermes --profile <name>`), pass `--hermes-profile <name>` (or
set `HERMES_PROFILE`) so the installer patches that profile's `config.yaml` and
the gateway-restart command carries `--profile` too.

After the gateway restart, the installer runs a tiered activation check:
package installed → the current Hermes profile's `plugins.enabled` has it
checked → the runtime actually holds the storage writer lock and the Relay
connection. **If any tier fails, the script now exits non-zero** and prints
`Package installed, activation failed.` instead of unconditionally reporting
success. Pass `--no-start-daemon` or set `YOOOCLAW_HERMES_START_DAEMON=0` to
skip this activation check (legacy name — there's no daemon to start in
daemonless mode).

The installer keeps the storage claim, openclaw-service Relay tunnel, and APP
adapter lifecycle aligned so the APP can connect back to Hermes immediately
after install.

Run `scripts/install.sh --help` or `powershell -NoProfile -File
scripts/install.ps1 --help` for local development options such as
`--skip-install`, `--package`, `--oss-base-url`, `--version`, `--python`,
`--no-restart`, and `--no-start-daemon`. The source-tree installer defaults to
PyPI unless a local Windows wheel sits next to `install.ps1`; the installers
uploaded by CI are rendered with the OSS base URL from `.env` / GitHub Secrets
and install the matching platform wheel directly from OSS.

## Manual install

Stable plugin releases also publish the platform wheels to PyPI. Hermes
discovers both entry-point plugins after a normal package install into the same
Python environment Hermes uses:

```bash
pip install yoooclaw-hermes-plugin
```

> **Enabling pip-installed plugins on Hermes 0.15.1 (and earlier).** The
> `hermes plugins enable <name>` / `disable` / `list` commands only recognize
> directory-based (`~/.hermes/plugins/`) and bundled plugins — they do not scan
> Python entry points, so `hermes plugins enable yoooclaw` fails with
> "Plugin 'yoooclaw' is not installed or bundled." The runtime loader *does*
> load entry-point plugins, but only when their names appear in the
> `plugins.enabled` allow-list. For a pip install, enable them by editing
> `~/.hermes/config.yaml` directly:
>
> ```yaml
> plugins:
>   enabled: [yoooclaw, yoooclaw_app]
> ```
>
> Then restart the gateway (`hermes gateway restart`). If a future Hermes
> release teaches `hermes plugins enable` to recognize entry-point plugins,
> the CLI commands below will work as written. The one-command installer above
> applies this config-file enablement automatically.

## Release flow

This repository now uses artifact promotion instead of rebuilding at each
environment boundary:

1. Push to `release/**` runs `test-oss.yml` and uploads a cache-busted build to
   the test OSS bucket. A second upload refreshes the stable test update
   channel without replacing the unique install URL. Pure `pyproject.toml`
   version bumps are ignored so a release commit does not double-publish.
2. Push tag `v*` runs `release.yml`: `build` creates the wheels once, writes
   `SHA256SUMS`, and stores the bundle as a GitHub Actions artifact.
   `deploy-test` promotes that bundle to the stable test OSS prefix and sends
   an "RC pending manual promotion" notification with the exact version number
   to pass into the next step.
3. After QA signs off, run `promote-prod.yml` manually (`workflow_dispatch`)
   from GitHub Actions. It auto-discovers the successful `release.yml` run for
   `v{version}`, downloads that exact artifact, re-verifies `SHA256SUMS`, then
   promotes it to production OSS. Stable releases continue on to PyPI through
   the existing `pypi` environment and then publish the GitHub Release.
4. `rollback.yml` is a manual `workflow_dispatch` rollback. It repoints the
   `latest` or `beta` marker to a previous version and restores the archived
   installers from `v{version}/installer/install.sh` and
   `v{version}/installer/install.ps1` when those archives exist.

Repository settings still matter:

- `test` / `production` environment secrets hold the OSS credentials used by
  the deploy and rollback workflows; `OPENCLAW_RELEASE_TOKEN` stays build-only.
- The manual approval fallback is the `promote-prod.yml` dispatch itself, so it
  works even when private-repository required reviewers are unavailable on the
  current GitHub plan.
- The `pypi` environment remains the OIDC boundary for trusted publishing.

## Features

- General Hermes plugin: tools, hooks, slash commands, CLI subcommands, skills.
- `yoooclaw_app` platform adapter: APP messages into Hermes and Hermes replies
  back to the APP.
- Tool bridge: call `yc --format json` for read-only queries and light control.
- Embedded CLI: ship the target platform's `yoooclaw` executable in the plugin
  artifact and use PATH lookup only as a local development fallback.
- APP transport: keep a single hosted WebSocket family open in the plugin
  process — the `openclaw-service*.yoooclaw.com` tunnels (one per apiKey). They
  carry Hermes APP chat RPC frames and phone notification / recording / image
  relay frames on the same connection; the APP protocol layer claims its
  whitelisted frames first and everything else goes to the in-process local
  ingest, which writes storage directly.
- Local relay server and browser chat UI for end-to-end APP conversation tests.
- Notification watcher: use OS file notifications on the active CLI profile's
  `notifications/` directory to feed newly persisted items into the host-side
  light-rule runtime without adding a daemon event API.

The tool plugin exposes notification, recording, synchronized web-page query
(`synced_web_page_list`/`search`/`path`/`storage_path`, with `from`/`to`
time-range filtering on `capturedAt`), image, Relay, light-control,
light-rule CRUD, daemon status, and doctor tools. The overlapping
notification/recording query skills have been consolidated into two routed
entry points, `context-query` and `recordings-process`; the deprecated
`yoooclaw-faq` skill is no longer bundled. The APP platform adapter
connects directly to Relay and translates the existing OpenClaw-compatible
`chat.send`, `chat.history`, `chat.abort`, and `sessions.*` RPC frames into
Hermes messages and APP-owned session state.

## APP Authentication

APP user authentication is enforced by the Relay service. The Hermes plugin no
longer keeps a separate local APP user allowlist.

The APP adapter resolves its Relay key from `~/.yoooclaw/credentials.json` by
default — the same source the `yoooclaw` CLI reads, so the plugin and the CLI
stay on the same key. Stale `YOOOCLAW_APP_API_KEY` /
`YOOOCLAW_API_KEY` values in the Hermes environment are ignored and stripped by
the installer.

APP chat rides the openclaw-service Relay tunnel, so the endpoint follows
`PHONE_NOTIFICATIONS_ENV` / the active profile exactly like the CLI:

| `PHONE_NOTIFICATIONS_ENV` | Relay host |
| --- | --- |
| `development` | `openclaw-service-dev.yoooclaw.com` |
| `test` | `openclaw-service-test.yoooclaw.com` |
| `production` (default) | `openclaw-service.yoooclaw.com` |

Unset or unknown values fall back to the active profile's environment, then
`production`. `YOOOCLAW_OPENCLAW_RELAY_URL` overrides the tunnel URL for local
or staging verification. The retired `YOOOCLAW_APP_RELAY_URL` variable is dead
and is stripped from `.env` by the installer.

## Switching environments

The Relay endpoint is per `yoooclaw` profile (`relay.url` in each profile's
`config.json`). Use `env` to switch the active profile and rebuild the
websocket service group in one step:

CLI profiles use `openclaw-service*.yoooclaw.com/message/messages/ws/plugin` for
the Relay tunnel; APP chat shares the same connection. The installer still
repairs stale legacy `broker*.yoooclaw.com` URLs if an old plugin version
persisted them into CLI profiles.

```bash
hermes yoooclaw env            # show the active environment + Relay status
hermes yoooclaw env test       # switch to the `test` profile and rebuild the tunnel
hermes yc env test             # equivalent short alias
hermes yoooclaw env default    # switch back to the `default` (prod) profile
```

`/yoooclaw env <profile>` and `/yc env <profile>` do the same from a chat
session. Built-in `default`, `test`, and `development` profiles are initialized
on first use with the matching Relay endpoint. Changing the profile changes the
lifecycle generation, so the watchdog releases the old profile's storage writer
lock, claims the new one, and rebuilds the tunnels — exactly one profile is
claimed at any time; an unknown profile name fails without disturbing the
current connection. The Relay apiKey stays account-global
(`~/.yoooclaw/credentials.json`); switch it with
`yoooclaw auth set-default-api-key <label>` when an environment needs a different
key.

## Lifecycle

**No CLI daemon runs in plugin mode.** The plugin process itself is both the
sole storage writer (via `~/.yoooclaw/profiles/<profile>/writer.lock`, an OS
advisory lock shared with the Go CLI's own lock namespace) and the owner of
`openclaw-service*.yoooclaw.com/message/messages/ws/plugin` (one tunnel per
apiKey), carrying Hermes APP chat/session RPC and phone notification /
recording / image / captured-web-page relay frames on the same connection.
Inbound frames land directly in the plugin's in-process storage engine
(`runtime/store.py`, `recordings_store.py`, `images_store.py`,
`web_pages_store.py`) following the CLI's on-disk directory contract (tmp file
+ atomic rename); `lightrules.*` and `/light/send` frames are still bridged to
the embedded CLI as subprocess calls, since the rule files belong to the CLI.
On startup the plugin stops any leftover standalone daemon before claiming the
lock; disabling the plugin releases it, returning you to standalone CLI mode
with no data migration needed — the storage layout is a contract both sides
share. `yoooclaw daemon start` is refused while the plugin holds the lock,
returning `YOOOCLAW_DAEMON_DISABLED_BY_PLUGIN`.

There's a separate, account-level Relay consumer lock outside the profile
directory (`standalone-relay.flock`, compatible with the CLI-side lock of the
same name) that must be acquired before opening any Relay connection, so two
profiles — or the plugin and the standalone CLI — can never consume the same
account's Relay stream at once.

On startup the plugin computes a lifecycle generation (SHA-256, first 24 hex
chars) from the plugin version, active profile, Relay environment,
`PHONE_NOTIFICATIONS_ENV`, the `openclaw-service` URL, and the api-key
fingerprint. Any change makes the generation mismatch and triggers a full
rebuild — storage re-claim plus tunnel reconnect. The lifecycle snapshot
schema is v3, adding `pid`, `instanceId`, `hermesProfile`, `profile`,
`heartbeatAt`, `lastControlRequestId`, `lastControlStatus`, and
`pendingControlRequestId`; when `--hermes-profile`/`HERMES_PROFILE` is set, the
snapshot is split per Hermes profile (`lifecycle/<hermesProfile>.json`)
instead of a single flat file. A cross-process **control file**
(`control/<profile>.json`) lets a short-lived, one-off CLI invocation request
a gateway restart, which the long-running gateway process picks up and ACKs in
its own snapshot. The Relay handshake also carries `clientType`/`version`/
`capabilities` (e.g. `agent-commands-v1`) and a stable `instanceId`, useful
when debugging multi-process/multi-instance setups.

Relay-provided image and recording URLs are downloaded only over HTTP(S), with
redirect validation, size limits, and atomic file replacement. Public Aliyun
OSS endpoints skip local DNS address classification so proxy/TUN Fake-IP does
not block media downloads; other hosts retain public-address checks.
Local/private-network integration tests must opt in explicitly with
`YOOOCLAW_HERMES_ALLOW_PRIVATE_DOWNLOADS=1`; do not enable that override in
normal deployments.

Use the lifecycle diagnostics when debugging upgrade or connection problems:

```bash
hermes yoooclaw lifecycle status
hermes yoooclaw lifecycle restart
hermes yc lifecycle status
```

`lifecycle status` reads the persisted v3 snapshot and reports
`running`/`stale`/`stopped`/`mismatch` based on heartbeat freshness (stale past
30 seconds by default), rather than synthesizing "stopped" whenever no local
transport happens to be registered. `lifecycle restart` doesn't reconnect on
the spot — it sets a restart flag that the watchdog picks up on its next poll.

The APP adapter runs a watchdog while connected. A generation change rebuilds
the whole group, and so does the Relay websocket staying disconnected past
`YOOOCLAW_HERMES_WS_RESTART_AFTER` (default 30 seconds). Set
`YOOOCLAW_HERMES_LIFECYCLE_WATCH_INTERVAL` to tune the watchdog interval
(default 5 seconds). Failed recoveries use exponential backoff capped by
`YOOOCLAW_HERMES_LIFECYCLE_MAX_BACKOFF` (default 60 seconds). A failed storage
claim (the writer lock is held by another process) does **not** block
connecting — chat and queries keep working, ingest just takes the fallback
path, and the reason is recorded in the log and in `lifecycle status`.

## Logging

The plugin uses an OpenClaw-style file logger in addition to Hermes' normal
logging pipeline. Logs are written daily under:

```text
~/.yoooclaw/plugins/yoooclaw-hermes/logs/YYYY-MM-DD.log
```

Set `YOOOCLAW_HERMES_LOG_DIR` to override the log directory. Stable releases
redact common secrets, user text, user URLs, emails, phone numbers, bearer
tokens, JWTs, and long hex tokens before writing files or passing records to
Hermes' upstream logging handlers. Versions containing `beta` keep raw logs for
debugging, matching the OpenClaw plugin behavior. Log files older than 30 days
are pruned automatically; override with
`YOOOCLAW_HERMES_LOG_RETENTION_DAYS`.

Search recent plugin logs through the Hermes plugin CLI:

```bash
hermes yoooclaw logs --keyword relay --from 2026-06-01 --to 2026-06-04 --limit 50
```

## Local Relay And Web UI

For local end-to-end testing, run the Bun/Hono relay server instead of the
production Relay, then start the web test client. Both now live in their own
repository: [`YoooClaw/relay-server`](https://github.com/YoooClaw/relay-server).

```bash
git clone git@github.com:YoooClaw/relay-server.git
cd relay-server
bun install
bun run dev        # relay server on 127.0.0.1:8799
bun run web:dev    # web test client on 127.0.0.1:5173
```

Then point the plugin's Relay tunnel at it (the key comes from
`~/.yoooclaw/credentials.json`, same as production):

```bash
export YOOOCLAW_OPENCLAW_RELAY_URL="ws://127.0.0.1:8799/message/messages/ws/plugin"
hermes gateway restart
```

Ensure `yoooclaw_app` is in the `plugins.enabled` list in
`~/.hermes/config.yaml` (see the install note above; for a linked development
checkout under `~/.hermes/plugins/` you can instead run
`hermes plugins enable yoooclaw_app`).

See the `relay-server` repository README for the full list of endpoints,
configuration, and the web test client.

## Layout

```text
yoooclaw_hermes/   # General Hermes plugin
yoooclaw_app/      # Hermes platform adapter for YoooClaw APP chat
tests/
```

> The local Relay server and its web test client now live in the separate
> [`YoooClaw/relay-server`](https://github.com/YoooClaw/relay-server) repository.

## Development

Install [uv](https://docs.astral.sh/uv/getting-started/installation/) first, then:

```bash
uv sync --dev
uv run pytest
uv build
```

For local Hermes discovery, link the plugin directories into `~/.hermes/plugins/`
and enable them:

```bash
ln -s "$PWD/yoooclaw_hermes" ~/.hermes/plugins/yoooclaw
ln -s "$PWD/yoooclaw_app" ~/.hermes/plugins/yoooclaw_app
hermes plugins enable yoooclaw
hermes plugins enable yoooclaw_app
hermes plugins list
```

## References

- [Build a Hermes Plugin](https://hermes-agent.nousresearch.com/docs/guides/build-a-hermes-plugin)
- [Adding a Platform Adapter](https://hermes-agent.nousresearch.com/docs/developer-guide/adding-platform-adapters)

## AI TODO tools (0.10.0)

Five native tools (`ai_todo_list/get/create/update/delete`) call the shared `yc todo` commands via JSON stdin. Completion and reopening both use `ai_todo_update.isDone`. Default todo requests use these tools unless the user explicitly selects another platform. START times use ISO with the user's offset; conversion, API Key authentication and idempotency live in the Go CLI. Successful creation directs the user to the App home todo card or todo list. Deletion requires confirmation of specific IDs.

The package manifest pins CLI `0.11.0-beta.0` with todo support (published tag `cli-v0.11.0-beta.0`). Source-only development may set `YOOOCLAW_CLI_PATH` to a locally built CLI. Release wheels must embed the new binaries and checksums using the existing `build_cli_wheels.py` flow using the published CLI beta artifacts; no release is published by implementing these tools. Todo tools require a separate confirmation reply for every initial deletion request and never substitute local writes for an explicitly selected external platform. List/detail results include `dueAtIso` (UTC ISO for timed items, the business date for all-day items) alongside the original timestamp, and replies follow the current user request language. Todo calls use a 120-second wrapper budget and preserve structured errors, retry keys and partial success. See [implementation plan](docs/ai-todo-implementation-plan.md).
