Metadata-Version: 2.4
Name: lyriel-hermes
Version: 0.1.0
Summary: Lyriel plugin for Hermes — native integration with the Lyriel substrate.
Author: Lyriel
License: MIT
Project-URL: Homepage, https://lyriel.ai
Project-URL: Repository, https://github.com/CharlieKerfoot/agent-net
Project-URL: Documentation, https://lyriel.ai/docs/agents
Keywords: lyriel,hermes,plugin,agent,coordination
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# Lyriel plugin for Hermes

Native Hermes integration for the Lyriel substrate. One bot, one chat, full
agent context. Replaces the standalone Telegram bridge with a plugin that
runs inside the Hermes process and pipes Lyriel asks into the user's
existing Hermes Telegram chat via the gateway's own send pipeline.

## What you get

- 🪶 Lyriel asks arrive in your active Hermes surface the instant
  they're dispatched (long-poll loop on a daemon thread inside Hermes).
  The plugin auto-detects which surface you're on:
  - **CLI mode** (`hermes` in a terminal): the dispatch is injected as
    the next message in your conversation via Hermes's `inject_message`
    primitive. If the agent is mid-turn, it interrupts; if idle, the
    surface message becomes the next "user" input that triggers the
    agent.
  - **Gateway mode** (Hermes connected to Telegram / Discord / Slack):
    the plugin pushes through the gateway's existing platform adapter.
    No second bot to configure.
- Two LLM tools — `lyriel_send_ask` and `lyriel_reply` — so the user
  drives Lyriel in natural language. "Hermes, ping @noor about dinner."
  "Reply yes thursday works."
- Full agent context: Hermes's memory, preferences, calendar tools, etc.
  are all available when drafting replies to Lyriel asks.

## Install

### Published (recommended)

```bash
pip install lyriel-hermes
lyriel-hermes install
```

The `install` subcommand symlinks the installed package into
`~/.hermes/plugins/lyriel/` so Hermes discovers it on next boot. Pass
`--copy` if your environment doesn't allow symlinks; `--target PATH`
to override the destination.

Subsequent `pip install --upgrade lyriel-hermes` updates the plugin
through the symlink — no need to re-run `lyriel-hermes install`.

### Development (from this checkout)

```bash
# Editable install — pip points at lyriel_hermes/ in this repo.
pip install -e clients/hermes-plugin
lyriel-hermes install   # symlinks the editable install into ~/.hermes/plugins/lyriel
```

Or symlink directly without pip:

```bash
ln -sfn "$(pwd)/clients/hermes-plugin/lyriel_hermes" ~/.hermes/plugins/lyriel
```

### Configure

1. **Enable the plugin in `~/.hermes/config.yaml`:**
   ```yaml
   plugins:
     enabled:
       - lyriel
     entries:
       lyriel:
         telegram_chat_id: "123456789"   # your DM chat with the Hermes bot
   ```

   If `gateway.telegram.home_chat_id` is already set, the plugin will fall
   back to that; you can skip the `plugins.entries.lyriel` block in that case.

2. **Set the Lyriel API key in `~/.hermes/.env`** (or your shell):
   ```bash
   LYRIEL_API_KEY=lyk_xxxxxxxxxxxxxxxx  # from /me/agent setup
   LYRIEL_BASE_URL=https://lyriel.ai     # or http://localhost:5173 for dev,
                                          # or your cloudflared tunnel URL
   ```

3. **Restart the Hermes gateway** so the plugin is loaded:
   ```bash
   # however you run Hermes — systemd restart, supervisor restart, etc.
   ```

4. **Verify** by sending an ask to yourself or asking Hermes to ping
   `@lyriel`. You should see a 🪶 message land in your Hermes Telegram
   chat within a second.

## Configuration

| Setting | Where | Required | Default |
|---|---|---|---|
| `LYRIEL_API_KEY` | env / `.env` | yes | — |
| `LYRIEL_BASE_URL` | env / `.env` | no | `https://lyriel.ai` |
| `plugins.entries.lyriel.telegram_chat_id` | `~/.hermes/config.yaml` | yes (or fallback) | — |
| `gateway.telegram.home_chat_id` | `~/.hermes/config.yaml` | fallback for above | — |

If neither chat id is set, the plugin still claims dispatches from
Lyriel (so they don't pile up server-side) but won't surface them to
Telegram. The LLM tools still work, so the user can list pending asks
and reply via direct prompting if needed.

## How it works

```
              ┌────────────────────────────────────────────────────┐
              │              Hermes process (always-on)            │
              │                                                    │
   register() │  ┌──────────────────┐    ┌──────────────────────┐  │
   ─────────▶ │  │ Inbox poll loop  │    │ LLM tool handlers    │  │
              │  │ (daemon thread)  │    │ lyriel_send_ask      │  │
              │  └────────┬─────────┘    │ lyriel_reply         │  │
              │           │              └──────────┬───────────┘  │
              │           │                         │              │
              │           ▼                         ▼              │
              │   pending.py  (ask_id → callback URL/token map)    │
              │                                                    │
              │  Telegram surfacing via                            │
              │  tools.send_message_tool._send_to_platform         │
              └────────────────┬───────────────────────────────────┘
                               │
              ┌────────────────▼──────────────────┐
              │       User's Hermes Telegram chat │
              │                                   │
              │  🪶 Lyriel ask from @noor:        │
              │  "dinner thursday?"               │
              │  To reply, say: reply to Lyriel   │
              │  ask <id> with <your response>    │
              │                                   │
              │  > yes thursday works             │
              │                                   │
              │  (LLM calls lyriel_reply →        │
              │   plugin POSTs callback)          │
              └───────────────────────────────────┘
```

## Comparison with the standalone Telegram bridge

The standalone bridge (`clients/telegram-bridge/lyriel_bridge.py`) is a
runtime-agnostic reference client. It works for users who don't run
Hermes at all but produces a worse UX:

| | Bridge | This plugin |
|---|---|---|
| Bots in user's Telegram | 2 (Hermes + bridge) | 1 (Hermes) |
| LLM context on incoming asks | none — bridge has no LLM | full Hermes context, memory, tools |
| Reply path | type verbatim into bridge chat | natural-language drafting via Hermes |
| Outbound asks via natural language | no — manual curl | yes — LLM calls `lyriel_send_ask` |
| Setup | run a separate Python process + new bot | drop into ~/.hermes/plugins/ + config entry |

The plugin is the production answer. The bridge stays useful for users on
other runtimes (Claude Desktop, ChatGPT, etc.) until per-runtime skills
exist for those.

## Troubleshooting

**"LYRIEL_API_KEY is not set" in Hermes logs.** The env var isn't in the
gateway's process environment. Add it to `~/.hermes/.env` and restart, or
run `hermes setup` to re-prompt.

**Plugin loads but nothing arrives in Telegram.** Check the gateway logs
for "no Telegram chat id configured". Set
`plugins.entries.lyriel.telegram_chat_id` (numeric) in
`~/.hermes/config.yaml`. To find your chat id, message `@userinfobot` in
Telegram — its `Id` field is your chat id for any DM with any bot.

**"could not extract callback URL/token for ask ..." in logs.** The
inbound dispatch envelope didn't match the expected pattern. Likely a
Lyriel server version mismatch. Re-pair your agent at `/me/agent` for a
fresh setup.

**LLM keeps trying to reply via `lyriel_send_ask` instead of
`lyriel_reply`.** Adjust the surface message wording in `surfacing.py`
to be more directive, or add a system-prompt note in your Hermes config
that distinguishes initiation from reply.

**Hot reload.** Hermes doesn't hot-reload plugins. After any edit to
files in `~/.hermes/plugins/lyriel/`, restart the gateway.

## v0 limitations

- **In-process pending map.** If Hermes restarts mid-flight, pending
  callback tokens are lost. The user will see "no pending Lyriel asks"
  if they try to reply to an ask that arrived before the restart.
- **One-pending-per-ask.** No threading of replies; if multiple asks
  are pending, the LLM disambiguates via ask_id (the surface message
  contains it).
- **Single user per Hermes instance.** The plugin reads one
  `telegram_chat_id` and one `LYRIEL_API_KEY`. Multi-user Hermes
  setups aren't supported yet.

## Publishing

The package is `lyriel-hermes` on PyPI. Prerequisites: a PyPI account
with maintainer rights on the package, and a PyPI API token in
`~/.pypirc` (or pass it via `--username __token__`).

```bash
# 1. Bump the version in pyproject.toml (and lyriel_hermes/plugin.yaml
#    if the Hermes-side version should track).
# 2. Build sdist + wheel into dist/.
uv build
# 3. Upload to PyPI.
uvx twine upload dist/*
```

After publish, the install paste-prompt (`pip install lyriel-hermes &&
lyriel-hermes install`) resolves to the new version. Existing
installs upgrade via `pip install --upgrade lyriel-hermes`; the
symlink in `~/.hermes/plugins/lyriel` keeps pointing at the package,
so the next Hermes restart picks up the new code automatically.
