Metadata-Version: 2.4
Name: wishee-hermes-connector
Version: 0.9.0
Summary: Connect a Hermes agent to the Wishee bridge via reverse WebSocket
License: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: websockets>=12.0
Requires-Dist: qrcode>=7.4
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"

# wishee-hermes-connector

Connect a [Hermes](https://github.com/NousResearch/hermes) agent to the Wishee bridge
(`openclaw-bridge`) over a reverse WebSocket — the Hermes counterpart of
`@wishee-ai/openclaw-connector`. The bridge drives the agent (`chat.send` /
`chat.history` / `sessions.reset`) and receives its replies (`task-result`).

Requires no changes to the bridge or to hermes-agent core.

**Requires Hermes >= 0.14.0.** The connector checks this in `register()` and refuses
to load below it — Hermes surfaces the error in the gateway log and in
`hermes plugins list`. See `version_gate.py` for how that floor was derived
(the binding constraint is `PlatformEntry.is_connected`, 2026-04-20).

## How it works

The connector is a Hermes **platform adapter** (runs inside the `hermes gateway`
process). It dials the bridge, does the `hello`/`welcome` handshake, then:

- `chat.send` → mints a `runId`, answers immediately, runs the agent turn, and
  emits a `task-result` event when the turn finishes (the bridge's device-push path).
- `chat.history` / `sessions.reset` → reads / clears the Hermes session store.

From the bridge's perspective a Hermes agent is indistinguishable from an
OpenClaw instance: same `/api/bind`, same `hello`, same RPCs, same event frames.

### Activity (usage duration)

The connector installs a Hermes hook at `~/.hermes/hooks/wishee-activity/` that
listens for `agent:start` / `agent:end` — the two events that bracket one
`_run_agent` call, i.e. the real boundary of a turn, **for every platform**. So
turns the user starts locally count too, not just the ones the bridge dispatches.

The hook handler only appends one self-contained interval per finished run to a
local journal (`~/.hermes/wishee-connector/wishee-activity-YYYYMMDD.jsonl`); it
never imports the connector, touches the network, or starts a timer. The
connector reads that journal by byte watermark and ships batches as
`agent-activity` frames, advancing the watermark **only after a successful
send** — the journal is what makes this survive a disconnect or a restart
(unlike an in-memory queue).

On first run it also seeds the journal once from `state.db`, deriving past turns
from message timestamps, so history predating the hook isn't lost.

> The hook is picked up by `discover_and_load()` at gateway startup, so a freshly
> installed hook **takes effect on the next gateway restart**.

### 阻塞式提示（询问选项 / 危险命令审批）

Hermes 的两类**会卡住 agent 线程等用户**的提示——`clarify`（提问 + 选项）与
危险命令审批——都经 bridge 现成的**输入类审批**通道推到
设备：`approval-request{kind:"user_input", inputSpec}` → 设备渲染 →
`approval.respond{approvalId, decision, input}` → `approval-resolved`。不新开
帧，所以 bridge 和 profile-service 无需改动。

提问卡的 `inputSpec` 与 codex 连接器的 `requestUserInput` **同构**（`questions[]`，
契约 v2），这样主机端只做一种渲染——见
`docs/superpowers/specs/2026-09-11-user-input-card-contract.md`。危险命令审批那半
（`kind:"command"`）不变，见
`docs/superpowers/specs/2026-09-10-approval-roundtrip-contract.md`。

> 不实现这两个方法的话都会落到 Hermes 的文本兜底：问题被拼成编号文本走
> `send()`，而 `send()` 只往 `_pending_reply` 里塞，要等整轮跑完才发——可这一轮
> 正卡在等回答上（默认 3600 秒）。主机端因此整整一小时收不到任何一帧，日志里
> 还什么都不报，因为兜底照样回 `success=True`。

一轮结束时还挂着的问题会回一条 `approval-resolved{outcome:"canceled"}`，设备
据此撤掉卡片——超时之后 agent 早就自己往下跑了，那张卡再答也没有去处。

The transport **auto-reconnects**: an unexpected socket close (bridge restart,
network drop) re-dials with capped exponential backoff and re-runs the
`hello`/`welcome` handshake. A deliberate shutdown never reconnects.

## Install

```bash
pip install wishee-hermes-connector   # into your Hermes environment
```

## Enable

1. Add `wishee` to `plugins.enabled` in your Hermes `config.yaml` (entry-point
   plugins are opt-in).
2. Optionally set `WISHEE_BRIDGE_URL` to override the default
   (`wss://ai-bridge.wishee.com.cn/ws`).
3. Run `hermes gateway`. On first run it prints a QR code — scan it to bind this
   instance to the bridge (via the bridge's `/api/bind`). Identity is stored at
   `<HERMES_HOME>/wishee-connector/binding.json`, i.e. `~/.hermes/...` unless
   `HERMES_HOME` says otherwise — the same directory the activity journal uses.
   Up to 0.8.0 this one path instead read `HOME` and fell back to `/tmp`, which
   on Windows (where a gateway cold start usually has no `HOME`) put the
   identity in `C:\tmp` and made the agent look unbound after an upgrade.
   0.9.0 adopts an identity left at that old location on first run, copying it
   rather than moving it so a rollback still finds one.

## Scope

v1 emits `task-result` on each completed turn (the bridge's device-push path).
Live streaming (`agent` progress events) is out of scope for v1.

## Development

```bash
uv venv --python 3.13 .venv
uv pip install --python .venv/bin/python -e /path/to/hermes-agent
uv pip install --python .venv/bin/python -e ".[dev]"
.venv/bin/python -m pytest -q
```
