|
Download code/moth/integrations/README.md from darioooooo0o/moth-112m: direct link, hf CLI and curl.
- Browser
- Download file 44.8 kB
-
https://huggingface.co/darioooooo0o/moth-112m/resolve/main/code/moth/integrations/README.md
- Command line
-
hf download hf://darioooooo0o/moth-112m/code/moth/integrations/README.md
-
curl -L -o README.md https://huggingface.co/darioooooo0o/moth-112m/resolve/main/code/moth/integrations/README.md
44.8 kB
| # moth harness adapters | |
| Each adapter sends the coding agent's **final reply of a turn** to the running moth server, one way: | |
| `POST $MOTH_URL/api/agent` with `{"text": "<raw markdown>", "source": "claude-code", "session": "<id>", "label": | |
| "moth-repo"}`. `source` is the harness name; `session` is a stable id of the agent session and `label` a short name for | |
| it, the basename of its working directory (else the first 8 characters of the session id). Cards show them as | |
| "claude · moth-repo said…" in moth's page and `moth tail`. `session` and `label` are optional: an older adapter | |
| without them counts as one session per harness. `MOTH_URL` defaults to `http://127.0.0.1:4747`. | |
| Every adapter follows the same rules: | |
| - It uses a 1 s timeout and never throws or prints. The JS and Hermes adapters never wait inside the agent's own code | |
| path; the Claude Code and Codex hooks run in the foreground, so a one-shot run can't kill them before they post | |
| (the wait is the hook's start-up plus one local POST that moth only queues, 1 s at most). | |
| - It posts nothing when moth is down. If moth is slow, it hangs up after 1 s, but moth keeps the text: `serve.py` | |
| reads the request body first and the page polls `/api/log`. | |
| - It posts nothing for subagent turns, aborted turns, or turns with no assistant text. It never re-sends an older reply. | |
| - It needs no dependencies beyond Python's stdlib or the harness's own JS runtime (`fetch`). | |
| - It sends moth's per-install token (`X-Moth-Token`, read from `~/.moth/token` on every post; moth makes the file on | |
| its first run, `MOTH_TOKEN_FILE` overrides). Python adapters bypass any HTTP proxy set in the environment. | |
| moth doesn't react to every turn: at most one reaction per 2 minutes (`--react-every`), except when the agent | |
| apologises, reports a failure or an error, or writes a very long message. While you're chatting with moth (the last | |
| 60 s, `--quiet-after`) it holds its reaction until you stop. Every turn still shows up as a card; only ~25 words of | |
| each one stay in moth's memory after it reacted, so agent turns don't push your conversation out. | |
| Several sessions at once (any mix of harnesses): | |
| - **Batching.** When moth may react, it first waits `--batch-window` seconds (default 4) for other sessions to finish | |
| too. Then it reacts to the most interesting message: a failure, apology or error first, then a question to you, | |
| then a long message, then a routine "done" (the newest wins a tie). The others show as skipped cards. Batched cards | |
| appear when the window closes, the chosen one last, so moth's reply sits right under it. `--batch-window 0` turns | |
| this off, and `--react-every 0 --quiet-after 0` (the judge tooling) still reacts to every message at once, with no | |
| window. | |
| - **No mix-ups.** moth's history keeps one agent turn per session: when it reacts to session B, the older agent | |
| turns of B, and all but the newest of every other session, are dropped together with moth's replies to them (agent | |
| turns carry a session marker in the history that the model never sees). moth's input stays exactly what it | |
| trained on (`[the agent said]` + the message); session names never go into it. | |
| - **Seeing them.** The page shows "2 agents active · 1 skipped" (sessions that posted in the last 30 min) and, with | |
| two or more sessions, a dropdown to show one session's cards (moth's replies always show). `moth tail --session | |
| <label>` does the same in a terminal. When two active sessions share a folder name, moth adds the end of the | |
| session id: "moth (3f2a)". | |
| - Synthetic batch turns ("three tasks finished, one says sorry") were left out on purpose: moth only ever trained on | |
| real agent messages, and a counted summary would be out of distribution. It couldn't be tested here without | |
| running the model. | |
| | Harness | `session` | `label` from | | |
| |---|---|---| | |
| | Claude Code | Stop input `session_id` | Stop input `cwd` | | |
| | Codex CLI | Stop `session_id` / notify `thread-id` | `cwd` (both) | | |
| | Hermes Agent | `post_llm_call` `session_id` | `agent.runtime_cwd`: gateway session cwd, else `TERMINAL_CWD`, else launch dir | | |
| | Pi | `ctx.sessionManager.getSessionId()` | `ctx.cwd` | | |
| | OpenCode | the event's `sessionID` | `Session.directory` (from `session.created/updated`), else the plugin's `directory` | | |
| | dsh | `session.id` | `process.cwd()` (dsh is started in the project) | | |
| moth has to be awake for the adapters to reach it. The one-command way is the desktop pet, | |
| `cd moth-112m/code && python -m moth.pet`: it starts the server itself and stops it when you quit the pet. For the | |
| snippets, or without a desktop, run `cd moth-112m/code && python -m moth` (just the server, in the terminal; the pet | |
| uses it if it's already running). It prints the snippets below with the real absolute paths | |
| of your copy filled in. The chat page is served at `http://127.0.0.1:4747/` but only opened in your browser with | |
| `python -m moth --open` (or `--browser`; the old `--no-browser` is accepted and does nothing). Below, paths are | |
| relative to the Hugging Face download (`hf download darioooooo0o/moth-112m --local-dir moth-112m`); hooks need | |
| absolute paths, so put your own folder in front of `moth-112m/`. | |
| | Harness | File | Mechanism | Turn-end event | | |
| |---|---|---|---| | |
| | Claude Code | `claude_code.py` | command hook | `Stop` | | |
| | Codex CLI | `codex.py` | hooks.json command hook, or legacy `notify` | `Stop` / `agent-turn-complete` | | |
| | Hermes Agent | `hermes/moth/` | plugin | `post_llm_call` | | |
| | Pi | `pi/moth.ts` | extension | `agent_end` + `agent_settled` | | |
| | OpenCode | `opencode/moth.js` | plugin | `session.status` idle (+ deprecated `session.idle`) | | |
| | dsh | `dsh/moth.mjs` | cordis plugin | `session/event` → `turn/end` | | |
| Test them all offline against a stub moth: `python3 moth/integrations/test_integrations.py` (about 50 s, 127.0.0.1 only). | |
| **Or let moth install them** (from `moth-112m/code`, or anywhere after `pip install`, as `moth ...`): | |
| - `python -m moth hooks` lists the harnesses found on this machine and whether moth is hooked into each. It also | |
| flags copies that are outdated, broken or point to another moth folder. | |
| - `python -m moth hook claude-code` (or `codex`, `hermes`, `pi`, `opencode`, `dsh`, or `--all` for every harness | |
| found) does what the sections below describe. It shows the plan and asks first (`--yes` skips the question, | |
| `--dry-run` only shows the plan), then prints what changed. | |
| - Every config file it edits is backed up first as `<file>.bak-moth-<timestamp>`. JSON, YAML and TOML configs are | |
| merged, so other hooks and keys stay. Installing twice changes nothing. | |
| - Copied adapters (Pi, OpenCode, Hermes) that differ from this folder's copy are replaced, for example after an | |
| update. The old Hermes folder is kept in `~/.moth/backups/`, outside the plugins folder. | |
| - Codex: the `hooks.json` hook by default, or `--legacy-notify`. moth never installs both. Approve the hook once in | |
| `codex` (`/hooks`). | |
| - `--project DIR`: Claude Code's `DIR/.claude/settings.json`. `--profile NAME`: the dsh profile. | |
| - `python -m moth unhook <harness>|--all` removes moth's entries only. It deletes a copied adapter only if it is | |
| moth's. | |
| ## Privacy: what moth sees, and where it goes | |
| - **What moth sees: two things only.** | |
| - The final message of each agent turn, posted by the adapters above. Each post carries a session id and the | |
| project folder's name as its label. | |
| - What you type to moth: in the chat page, the pet, `moth tail`, Pi's `/moth` or the Claude Code band. | |
| It never gets your prompts to the agent, the agent's tool calls, or your files. Its own tools reach only its home | |
| folder. | |
| - **Everything stays on your machine.** The server listens on 127.0.0.1 only, unless you start it with another | |
| `--host`. The adapters post to `http://127.0.0.1:4747`, unless you set `MOTH_URL`. | |
| - moth's code goes online for two things only, both from the Hugging Face Hub: downloading the weights (first run | |
| of a pip install) and `moth update`. | |
| - The browser chat page embeds its two fonts, so opening it doesn't fetch anything from outside either. | |
| - The training and eval scripts in `code/moth/` talk to whatever LLM server you point them at. Running moth never | |
| uses them. | |
| - **Where it's stored: `~/.moth/`.** That holds the token, `config.json`, pidfiles while moth runs, `backups/` from | |
| `moth hook`, and moth's home folder `home/`: its files, its diary, and anything it wrote down from your chats. | |
| - `serve.log` holds the server's output when the pet starts it: status lines and errors, no chat text. | |
| - The conversation itself lives only in RAM. Stopping moth wipes it, and so does the page's "new chat" button. | |
| moth's memory of its own last 16 reactions (token ids, so it doesn't repeat a joke) is RAM-only too. "New chat" | |
| keeps it; stopping moth or `moth forget` wipes it. | |
| - In the dev checkout, the home is `live/home/` and the logs are `live/*.log`. | |
| - **The agent never receives anything back.** The adapters only post. They ignore the answer, print nothing and | |
| exit 0, so nothing reaches the agent's context. | |
| - moth's lines show only in displays you turn on: the pet, `moth tail`, the Claude Code band, and the Pi widget or | |
| OpenCode toast with `show_in_harness on`. None of them feed the model. | |
| - What you say to moth goes to moth only. | |
| - **Wiping it.** Stop moth first, then: | |
| - `moth forget` deletes its logs. | |
| - `moth forget --all` also resets its home folder, but only a folder moth made itself. | |
| - `moth forget --all --token` also deletes the token and settings. | |
| - On a moth that's still running, `moth forget` deletes no files but makes it forget the chat and its last | |
| reactions (`POST /api/reset {"everything": true}`). | |
| `moth unhook --all` removes the hooks. | |
| ## Seeing and talking to moth | |
| The adapters above only *tell* moth about each turn. They hang up after 1 s and never see its reply. The pieces | |
| below are the other half: they **show** what moth says back, and they let you talk to it. Each one polls | |
| `GET /api/says` (or the full `/api/log`) for new `say` events and **never posts to `/api/agent`**, so moth never | |
| hears a turn twice. Install | |
| the adapter for your harness as well. | |
| **Each in-harness display shows moth's reaction to its own session only.** With several agents running, moth's | |
| newest line is often its reply to another one (seen live: Claude Code's band showed moth's reply to Hermes). So the | |
| Claude Code band, the Pi widget and the OpenCode toast ask `/api/says?source=<harness>&session=<id>` for the | |
| session they belong to, and show nothing rather than another session's line. The Hermes Desktop pet asks for | |
| `source=hermes` (every Hermes session in that app: the renderer can't map its sessions to the agent's ids). The | |
| desktop pet, `moth tail` and the chat page still show everything. Codex and dsh have no in-harness display. | |
| moth needs a moment to think. A display that waits after a turn (Pi, OpenCode, when turned on) waits up to about | |
| 4 s. A slower line is not lost: it shows on the next poll, after the next turn, or right away in `moth tail` and the | |
| pet. | |
| **Venting stays private.** Whatever you say to moth, and what moth says back, never enters the agent's context. | |
| That is why there is no `/moth` in Claude Code or Hermes. In Claude Code a command's output row is read by the model. | |
| In the Hermes gateway, a plugin command sent mid-turn is handed to the running agent. Pi's `/moth` is kept, because | |
| Pi runs extension commands outside the conversation (details per harness below). | |
| **The server only talks to this machine, and to whoever has the token.** | |
| - It answers only requests whose `Host` is `127.0.0.1:<port>` or `localhost:<port>`, so a website that points its | |
| own domain at 127.0.0.1 (DNS rebinding) gets a 403. | |
| - `POST /api/chat`, `/api/agent` and `/api/reset`, and the full log (`GET /api/log`, every agent reply and everything | |
| you typed) need the token in `X-Moth-Token`. The page moth serves has it built in; the adapters and `moth tail` | |
| read `~/.moth/token`. | |
| - `GET /api/says?since=n` is moth's own lines and nothing else, without a token. That is all a display needs, and | |
| what the Hermes Desktop pet and the Pi / OpenCode / Claude Code displays use. `&source=<harness>` and/or | |
| `&session=<id>` keep only moth's reactions to that harness / session (moth tags each reaction in its log with the | |
| turn it answers; chat replies have no tag, so they drop out). The answer is the same `{"say"}` list either way, | |
| with no ids or labels in it, and `next` still counts every event. | |
| - CORS: local pages (`http(s)://localhost` or `127.0.0.1`, any port) may read answers. Desktop shells | |
| (`app://…`, `file://`, `null`: Hermes Desktop packaged loads `file://…/index.html`, which sends `Origin: null`) | |
| may read `/api/says` only. Any other origin gets no CORS header, and a POST from a foreign origin, `null` | |
| included, is refused outright. | |
| - POSTs must be `Content-Type: application/json`, at most 64 KB, a JSON object; anything else gets a 4xx, not a crash. | |
| Every answer carries `X-Frame-Options: DENY`. | |
| - Every poll answer has an `epoch` that changes when moth restarts or the chat is reset. Pass it back | |
| (`?since=n&epoch=e`) and a changed epoch gets you the new log from its start. | |
| ### In-harness display: off by default (`show_in_harness`) | |
| The Pi widget and the OpenCode toast come inside adapters you install anyway, so they are **off** until you turn | |
| them on. Off, those adapters still post every turn (the desktop pet and `moth tail` react as usual); they just don't | |
| poll moth or draw anything in the harness. | |
| ```bash | |
| cd moth-112m/code && python -m moth config show_in_harness on # or off | |
| cd moth-112m/code && python -m moth config # print the settings and where they come from | |
| ``` | |
| - Stored as `"show_in_harness": true|false` in `~/.moth/config.json` (`MOTH_CONFIG_FILE` overrides the path). The | |
| CLI writes it atomically and keeps any other keys. | |
| - The adapters read the file at **every turn**, so a change applies from the next turn without restarting Pi or | |
| OpenCode. Turning it off also takes down a Pi widget that is on screen. | |
| - `MOTH_SHOW_IN_HARNESS=1` / `0` (also on/off, true/false, yes/no) in the harness's environment beats the file. The | |
| environment is fixed when the harness starts. | |
| - **Pi `/moth <text>`** is always registered, because Pi registers commands when it loads the extension and so can't | |
| follow a live toggle. Typing it is an explicit request, so it always answers. With the setting on the answer goes in | |
| the widget; with it off it comes as a one-off notice, and no widget is left behind. | |
| - **Not governed by it**: the Claude Code band mod (loaded only with `--plugin-dir` or `CLAUDE_CODE_PLUGIN_DIRS`) and | |
| the Hermes Desktop pet half (starts disabled; you enable it in the desktop's Plugins page). Loading or enabling them | |
| is their opt-in, and unloading or disabling them turns them off. Honouring a default-off setting would hide them | |
| from someone who just loaded them on purpose. The Hermes renderer also has no file or environment access to read | |
| it. `moth tail` and the desktop pet are separate programs you start yourself (the pet starts the server too). | |
| ### `moth tail`: works with every harness (Codex included) | |
| Run it in a tmux split or a terminal next to the harness: | |
| ```bash | |
| cd moth-112m/code && python -m moth.tail # moth's lines + a "you>" prompt to chat with it | |
| cd moth-112m/code && python -m moth.tail --watch # status pane only, no prompt | |
| tmux split-window -h -l 45 'cd moth-112m/code && python -m moth.tail' # e.g. beside codex / claude | |
| ``` | |
| - It prints moth's new lines (`moth>` in lamp yellow, wrapped to the pane), a dim `agent said: ...` one-liner for | |
| each turn moth heard, and a dim `$ cmd` line when moth uses a tool. It shows the last 3 events on connect | |
| (`--history N`). | |
| - Lines you type go to `/api/chat`, and the reply arrives through the same poll. A moth line that arrives while you | |
| type is printed above the prompt, and your half-typed text is kept. | |
| - When moth is down it prints "moth is asleep" once and retries with backoff (up to 10 s). It follows moth across | |
| restarts and `/api/reset`. | |
| - Stdlib only. It doesn't import torch, so any `python3` works. `--url` or `MOTH_URL` changes the address. | |
| ### Desktop pet: a moth flying around your screen (`python -m moth.pet`, any harness) | |
| ```bash | |
| pip install PySide6 # optional: only the pet needs it, moth itself doesn't | |
| cd moth-112m/code && python -m moth.pet # --size 64, --no-wander (sits in a corner), --url, --token-file | |
| python -m moth.pet -- --react-every 0 # flags after -- go to the server the pet starts | |
| python -m moth.pet --export-petdex ./moth-pet # the same moth as a Petdex / Codex pet (pet.json + spritesheet.png) | |
| python -m moth.pet --skin <pet-id> # wear an installed Petdex pet instead (or --skin ./some-pet-folder) | |
| python -m moth config pet_skin <pet-id> # ... every time (default moth); update_check on|off for the bubble | |
| ``` | |
| - **One command.** At start the pet asks `GET /api/health` on its URL. If a moth answers, it uses that one and never | |
| stops it. If nothing listens, it starts `python -m moth.serve` itself (same Python, CPU, this download's weights), | |
| dozes with "waking up…" while the model loads, and stops that server again when the pet quits (menu quit, | |
| Ctrl-C, SIGTERM/SIGHUP, the window closed; on Linux also if the pet is killed). The server runs in its own process | |
| group and logs to `~/.moth/serve.log`. If something that isn't moth holds the port, or less than ~3 GB of memory | |
| is free, it starts nothing and says why in the bubble. | |
| - Flags: `--no-server` (never start one, just wait for moth like before), `--model DIR` (the weights for a server | |
| the pet starts), `--server-arg=--flag` (repeatable; `--server-arg=--react-every --server-arg=0`) or anything after | |
| `--` for that server. `python -m moth` / `python -m moth.serve` are still the way to run moth headless or in a | |
| terminal. | |
| - A small moth, drawn in the page's style, flies around on top of your windows. It drifts, is drawn to the mouse | |
| cursor, lands on screen edges to rest, and dozes after 3 idle minutes. When a new line arrives it flutters and says | |
| the line in a speech bubble (wrapped at 32 characters, shown for 6-10 s, queued). When `~/.moth/token` is | |
| readable, a reaction gets a small `claude · repo` caption, taken from `/api/log`. Without the token the pet uses | |
| `/api/says`. | |
| - Click the moth to type to it (`/api/chat`, needs the token); the reply comes back in the bubble. Drag it to move | |
| it. Right-click for: open chat page, mute bubbles, sleep, show moth in harnesses, skin, check for updates, quit. | |
| It never takes focus, except for that input box. Where the desktop has a system tray, a tray icon has the same | |
| menu (no tray, e.g. on some Wayland setups: no icon, the right-click menu is still there). | |
| - **Skins.** `--skin ID|FOLDER`, `python -m moth config pet_skin ID` or right-click → skin: the pet can look like any | |
| Petdex pet you have installed (`npx petdex install <pet>`). Ids are looked up in `~/.petdex/pets/<id>/`, then | |
| `$CODEX_HOME/pets/<id>/` and `~/.hermes/pets/<id>/`; the menu lists them all. The pet's sheet must be the Petdex | |
| 1536x1872 grid. Flying uses running-right / running-left, resting idle, dozing waiting, excited jumping or waving, | |
| waiting for a reply review; the bubble sits on the pet's visible top. A missing or broken pet means moth, and the | |
| bubble says why. moth ships no third-party pets; they keep their own licences. | |
| - **Update bubble.** Once a day (and on "check for updates") the pet runs `python -m moth update --check` in the | |
| background and, if there's a new version, says so: "run `python -m moth update`", or `pip install -U | |
| moth-companion` for a pip install. It never updates anything itself. Off: `python -m moth config update_check | |
| off`; `HF_HUB_OFFLINE=1` or `MOTH_OFFLINE=1` also skips it. | |
| - When moth is down it dozes with "zzz… (moth server asleep)" and retries with backoff (up to 30 s). If the server | |
| the pet started dies, the bubble points at its log instead. | |
| - Wayland doesn't let an app place its own window. On a Wayland session the pet asks Qt for XWayland | |
| (`QT_QPA_PLATFORM=xcb;wayland`, only if you haven't set it); without XWayland it stays where the compositor puts | |
| it (drag still works). Under XWayland the cursor position only updates over X windows, so the pull to the | |
| cursor fades when the cursor seems to stop. It lands on screen edges, not on other windows' edges. | |
| - `--export-petdex DIR` writes a 1536x1872 sheet (8x9 cells of 192x208: idle, running-right/left, waving, jumping, | |
| failed, waiting, running, review). Copy DIR to `$CODEX_HOME/pets/moth` (Codex, pick it in `/pets`), to | |
| `~/.hermes/pets/moth` (`hermes pets select moth`), or to `~/.petdex/pets/moth` (the Petdex desktop app). Codex picks pets by folder name | |
| (`custom:moth`). | |
| - The pet only reads moth and posts what you type; it never posts to `/api/agent`. | |
| ### Claude Code: band above the prompt, with a field to talk to moth (mod) | |
| `claude_code_mod/` is a mod (function hooks, early-access API). It draws moth's newest reaction to this session in | |
| the band above the prompt (`/api/says?source=claude-code&session=<$.session.id()>`, the Stop hook's `session_id`, | |
| read on every poll so `/clear` follows), polling every 2 s, with a text field under it. The reply to what you type | |
| there comes back with the send. Focus the band with ctrl+x tab or a click, type, and press | |
| Enter. Esc returns to the prompt. Keep the `Stop` hook above as well, since it is what tells moth about each turn. | |
| Try it for one session: | |
| ```bash | |
| claude --plugin-dir moth-112m/code/moth/integrations/claude_code_mod | |
| ``` | |
| To load it in every session, add the folder to `CLAUDE_CODE_PLUGIN_DIRS` in the `env` block of | |
| `~/.claude/settings.json` (`"CLAUDE_CODE_PLUGIN_DIRS": "/abs/path/to/moth-112m/code/moth/integrations/claude_code_mod"`). | |
| - `MOTH_URL` changes the address. It is read when the session starts. | |
| - **Kept out of Claude's context.** The field's Enter goes to the mod's own `onSubmit`, which POSTs to moth | |
| directly. The type definitions say an Input's submit starts "No model turn unless it asks one" (`InputProps.onSubmit` | |
| in the 2.1.302 `claude-code.d.ts`), and the band is drawn UI, not a transcript row. A slash command was rejected | |
| because its output is "the row the model also reads" (`CommandRunResult.text`), and a `prompt.submit` `{ drop }` | |
| comes too late for a plugin command, which has already run by then. | |
| - Mobile has no `Input`, so there the band shows moth's line only. | |
| - It reads moth's lines from `/api/says`; a send reads the token from `~/.moth/token` (`$.fs.read`). | |
| - `claude plugin validate moth-112m/code/moth/integrations/claude_code_mod` passes on 2.1.302. | |
| ### Hermes: desktop pet (chat in `moth tail`) | |
| - **No `/moth`.** A `register_command` reply is not added to the agent history when Hermes is idle (CLI | |
| `_run_plugin_slash_command` only prints it; the gateway's idle path returns before a turn). But in the messaging | |
| gateway, a plugin command sent while a turn runs never reaches the plugin. `resolve_command` knows only built-ins, | |
| so `_hm_busy_slash_or_photo` lets it fall through to `_hm_busy_steer` / `_hm_busy_interrupt`, which hand | |
| `/moth …` to the running agent as text (`gateway/run_inbound.py`). The hooks that run earlier don't fix this: | |
| `pre_gateway_dispatch` runs before auth, and `post_gateway_admission` runs after the busy lane. So chat with moth | |
| in `moth tail`. | |
| - **`desktop/plugin.js`**, the Hermes Desktop half. It shows moth's lines in the pet's speech bubble | |
| (`ctx.pet.say`): one line at a time under a stable id, so each new line replaces the last; lines over 120 | |
| characters are cut into up to 3 pieces shown one after another, each up for 4–30 s. It polls every 2.5 s with | |
| `ctx.setInterval`, for moth's reactions to Hermes turns only (`source=hermes`). It can't narrow that to one Hermes | |
| session: the desktop's gateway events carry the TUI session id, while the agent posts its session key | |
| (`tui_gateway` `_make_agent(sid, key, session_id=key)`). | |
| The install is the same `cp -r` as above. Hermes Desktop copies `plugins/moth/desktop/` into | |
| `<hermes home>/desktop-plugins/moth/`, and such a half starts **disabled**. Enable "moth" in the desktop's Plugins | |
| page, and turn the pet on in Settings. The desktop half reads `http://127.0.0.1:4747` (the renderer has no | |
| environment, so edit `MOTH` in `plugin.js` to change it). Hermes's own desktop lint | |
| (`hermes_cli/plugin_validate_desktop.py`) finds nothing in it. | |
| ### Pi: widget above the editor + `/moth` | |
| `pi/moth.ts` now also: | |
| - with `show_in_harness` on (see above), polls the log for up to 4 s after each posted turn and puts moth's | |
| reaction to this session in a widget above the editor (`ctx.ui.setWidget("moth", ...)`, interactive sessions | |
| only; `source=pi&session=<getSessionId()>`, nothing without a session id); | |
| - registers `/moth <text>`, which shows the reply in the same widget (or as a notice where there is no widget, or | |
| when `show_in_harness` is off). | |
| **Kept out of the agent's context.** `AgentSession.prompt()` runs an extension command's handler and returns | |
| before any message is made (`dist/core/agent-session.js` `_tryExecuteExtensionCommand`), and interactive mode | |
| sends input typed mid-stream and during compaction through `prompt()` too (`dist/modes/interactive/interactive-mode.js`). | |
| The handler only calls `ctx.ui`. The typed line is kept only in the editor's local up-arrow history. | |
| The install is unchanged. The poll timers are unref'd, so `pi -p` doesn't wait on them. | |
| ### OpenCode: toast | |
| With `show_in_harness` on (see above), `opencode/moth.js` also polls the log for up to 4 s after posting and shows | |
| moth's reaction to that session as a TUI toast (`client.tui.showToast`, title "moth", 8 s; | |
| `source=opencode&session=<sessionID>`, with a cursor per session, so two sessions in one OpenCode each get theirs). The install is unchanged. There is no `/moth` here: use `moth tail` to | |
| chat. | |
| ### dsh: not built (options) | |
| - **A client slot in our cordis plugin.** `dsh/moth.mjs` could poll `/api/says` after `turn/end` and hand moth's | |
| line to whatever display surface dsh exposes to plugins, the same way as Pi's widget. | |
| - **`@linxin666/dsh-pet`.** If that pet plugin is installed, its announce call could carry moth's line. | |
| Neither was checked against dsh sources (dsh isn't installed here). Until then, use `moth tail` next to dsh. | |
| --- | |
| ## Claude Code | |
| Add this to `~/.claude/settings.json`, or to a project's `.claude/settings.json`, merging with any `hooks` already there: | |
| ```json | |
| { | |
| "hooks": { | |
| "Stop": [ | |
| { "hooks": [ { "type": "command", "command": "python3 /abs/path/to/moth-112m/code/moth/integrations/claude_code.py", "timeout": 3 } ] } | |
| ] | |
| } | |
| } | |
| ``` | |
| - The Stop input carries `last_assistant_message`, "the text content of Claude's final response". The docs advise | |
| using it instead of `transcript_path`, which "isn't guaranteed to include the final message at Stop time". | |
| The transcript is only a fallback for older versions that don't send the key, and even then only the | |
| current turn is read. | |
| - Subagents fire `SubagentStop`, not `Stop`, so they are skipped. | |
| - **No `"async": true`.** A background hook works in the interactive TUI, but a one-shot `claude -p` kills it when | |
| it exits, before it posts (seen live on 2.1.302). In the foreground the hook posts first. It is quick: Python's | |
| start-up plus one POST to 127.0.0.1, which moth only queues, and it gives up after 1 s (`timeout: 3` is Claude | |
| Code's hard stop). `moth hook claude-code` rewrites an older `async: true` entry, and `moth hooks` flags it as | |
| outdated. | |
| - Source: <https://code.claude.com/docs/en/hooks> (Stop input, SubagentStop input, command hook fields). The field | |
| is also present in the local binary, 2.1.302. | |
| ## Codex CLI | |
| **Install one of the two options below, not both**, or every reply will be posted twice. | |
| **A. Stop hook (preferred).** Create or merge `~/.codex/hooks.json`: | |
| ```json | |
| { | |
| "hooks": { | |
| "Stop": [ | |
| { "hooks": [ { "type": "command", "command": "python3 /abs/path/to/moth-112m/code/moth/integrations/codex.py", "timeout": 3 } ] } | |
| ] | |
| } | |
| } | |
| ``` | |
| Then start the interactive `codex` once and approve the hook in the startup hook review, or later with `/hooks`. | |
| Untrusted or modified user hooks are silently skipped, including in `codex exec`. Approval stores a | |
| `trusted_hash` in the config, so **any edit of the entry needs a new approval** (even `async` true → false). | |
| - **No `"async": true`**, as for Claude Code: `codex exec` drops a background hook when it exits, before it posts (seen | |
| live on 0.163.0; the interactive TUI posted either way). `moth hook codex` rewrites an older `async: true` entry; | |
| approve it again afterwards. | |
| - **Windows:** Codex runs hook commands through PowerShell (`powershell -NoProfile -Command <command>`, the session's | |
| shell: `core/src/session/mod.rs` `build_hooks_config`). There a command that starts with a quoted path is a string, | |
| not a call, so a Python path with a space (`C:/Program Files/...`, a venv under `C:/Users/First Last`) needs the call | |
| operator: `& "C:/Program Files/Python312/python.exe" "C:/.../codex.py"`. `moth hook codex` writes that (and | |
| `moth hooks` flags an older entry without it); without it Codex ran nothing, silently (seen in `tests/e2e`). | |
| - Hooks are a stable feature, enabled by default (`features/src/lib.rs`, `CodexHooks`, key `hooks`, at | |
| `rust-v0.163.0`). They are loaded from `<config folder>/hooks.json` (`hooks/src/engine/discovery.rs`). | |
| - The Stop input carries `last_assistant_message` (`hooks/src/events/stop.rs`). | |
| - User Stop hooks run only for root turns: | |
| - Thread-spawned subagents fire `SubagentStop`. | |
| - Internal subagents dispatch no hook at all. | |
| - Memory consolidation runs only managed hooks. | |
| Sources: `core/src/hook_runtime.rs` `run_turn_stop_hooks`, `stop.rs` `select_handlers`. | |
| - Trust: `hooks/src/engine/discovery.rs` (`hook_trust_status`, handlers pushed only when Trusted or Managed), | |
| `tui/src/hooks_rpc.rs`, and `tui/src/startup_hooks_review.rs`. | |
| **B. Legacy `notify`.** Add this to `~/.codex/config.toml`. There is only one `notify` slot, so this replaces any | |
| existing notifier: | |
| ```toml | |
| notify = ["python3", "/abs/path/to/moth-112m/code/moth/integrations/codex.py"] | |
| ``` | |
| - Codex appends `{"type":"agent-turn-complete", ..., "last-assistant-message": ...}` as the last argv argument and | |
| spawns the program with null stdio, without waiting for it (`hooks/src/legacy_notify.rs`). | |
| - It is marked for removal ("TODO: Remove this hook ... when legacy `notify` support is removed"). | |
| - It is **not** gated by session source: `core/src/session/turn.rs` calls `run_legacy_after_agent_hook` for every | |
| turn. So subagent and memory-consolidation turns are probably posted too. | |
| - Source: <https://github.com/openai/codex/tree/rust-v0.163.0/codex-rs> | |
| ## Hermes Agent | |
| ```bash | |
| cp -r moth-112m/code/moth/integrations/hermes/moth ~/.hermes/plugins/moth | |
| hermes plugins enable moth # or add `moth` under plugins.enabled in ~/.hermes/config.yaml | |
| ``` | |
| - `post_llm_call` fires once per turn after the tool loop, with `assistant_response` set to the final text. It fires | |
| only on turns that produced a response and were not interrupted. | |
| - `delegate_task` children run with `platform="subagent"` and are skipped. | |
| - The POST runs on a daemon thread, so the hook returns at once. | |
| - Sources: `website/docs/user-guide/features/hooks.md#post_llm_call` and | |
| `website/docs/developer-guide/plugins/index.md` in the Hermes repo. The firing site is | |
| `agent/turn_finalizer.py` `_apply_output_hooks`; the subagent platform is set in `tools/delegate_tool.py`. | |
| Checked against a local hermes-agent checkout at `c57331677d`, 2026-10-10. | |
| - User plugins are opt-in (`plugins.enabled`). | |
| ## Pi | |
| ```bash | |
| cp moth-112m/code/moth/integrations/pi/moth.ts ~/.pi/agent/extensions/moth.ts | |
| # or for one run: pi -e /abs/path/to/moth-112m/code/moth/integrations/pi/moth.ts | |
| ``` | |
| - `agent_end` (`messages` = the messages generated by that run) can be followed by retries, overflow recovery or | |
| queued work, so the adapter only remembers the last assistant text there. | |
| - It posts on `agent_settled`, which means "Pi will not continue automatically", unless that event has `aborted`. | |
| - `agent_start` clears the remembered text. | |
| - Assistant messages with `stopReason` `error` or `aborted` are ignored. Only `text` blocks count. | |
| - Sources: `docs/extensions.md`, `docs/json.md` and `docs/cli-integration.md` in the package, and | |
| <https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts> | |
| (`AgentStartEvent`, `AgentEndEvent`, `AgentSettledEvent`). Checked against main (v1.3.0) and the local 1.1.0 | |
| `dist/core/extensions/types.d.ts`. | |
| - Pi has no built-in subagents. Subagent extensions that spawn child `pi` processes will load this extension too, | |
| and those children will post. | |
| ## OpenCode | |
| ```bash | |
| cp moth-112m/code/moth/integrations/opencode/moth.js ~/.config/opencode/plugins/moth.js # global | |
| # or: <project>/.opencode/plugins/moth.js | |
| ``` | |
| - Idle events carry only `sessionID`, so the adapter collects state from the event stream: | |
| - From `message.updated`, it records which message ids are assistant messages. Compaction (`summary`) messages | |
| are skipped; an `error` such as an abort means no reply. | |
| - From `message.part.updated`, it takes `text` parts that aren't `synthetic` or `ignored`. `part.text` is the | |
| full text, and the final update has `time.end`. | |
| - From `session.created` / `session.updated`, it treats sessions with an `info.parentID` as subagent (task tool) | |
| sessions and skips them. | |
| - On `session.status` with `status.type === "idle"`, and on the deprecated `session.idle` fired at the same moment, | |
| it posts the newest assistant message's text and then forgets that session. | |
| - Sources: <https://opencode.ai/docs/plugins/> (plugin dirs, `event` hook, `session.idle` / `session.status`), plus | |
| `packages/schema/src/session-status-event.ts` and `packages/schema/src/v1/session.ts` at tag `v1.19.3` in | |
| <https://github.com/anomalyco/opencode>, plus the local SDK types 1.2.10 (`@opencode-ai/sdk` `types.gen.d.ts`). | |
| ## dsh (DeepSeek Harness) | |
| Add this to `~/.dsh/profiles/<profile>/cordis.patch.yml`, a YAML list, next to any existing entries: | |
| ```yaml | |
| - insert: [{ id: moth, name: '/abs/path/to/moth-112m/code/moth/integrations/dsh/moth.mjs' }] | |
| ``` | |
| Or pass it for one run: `dsh --patch /path/to/patch.yml ...`, where the patch file holds the same list. | |
| - `session/event` is the "post-commit, fire-and-forget append feed" of every session-log event. From it the adapter: | |
| - remembers the text blocks of the newest `assistant/message` in the turn, skipping `interrupted: true`; | |
| - on `turn/end` with `reason.kind` `completed` or `max-tokens`, posts that same turn's text; | |
| - posts nothing for aborted, error and blocked turns. | |
| - Child sessions announced by `subagent/start` (`info.id`) are skipped. | |
| - Sources: <https://github.com/deepseek-ai/deepseek-harness> `docs/subsystems/session.md` (`session/event`, | |
| `SessionEventMap`, `TurnEndReasonMap`), `docs/subsystems/subagent.md` and | |
| `packages/subagent/subagent/src/types.ts` (`SubagentRunInfo.id`), `packages/llm/llm/src/types.ts` (`TextBlock`). | |
| Checked at tags `dsh-v0.2.0-rc.2` (npm `latest`) and `dsh-v0.2.1-rc.2`. The `agent/turn-stopping` hook was not | |
| used because it can steer another step, while `turn/end` is the committed close. | |
| ## Updating | |
| From `moth-112m/code`, with moth stopped: `python -m moth update --check` shows what changed (exit 10 = update | |
| available), `python -m moth update` fetches only the changed files into the same folder, and `python -m moth update | |
| --rollback` puts back the previous version. `python -m moth --version` prints the version and Hub revision. Hook | |
| commands that point into `code/moth/integrations` keep working; re-copy any adapter you copied elsewhere (Pi | |
| `moth.ts`, OpenCode `moth.js`, the Hermes plugin folder) when the update says integration files changed: | |
| `python -m moth hooks` flags outdated copies and `python -m moth hook <harness>` replaces them. `~/.moth/` | |
| (home, token) is never touched. | |
| --- | |
| ## Not verified live | |
| All six adapters, the Claude Code band, the Pi widget and the OpenCode toast have since run live once, on | |
| 2026-10-18 (`../../video/raw/INDEX.md` in the dev checkout). That run found the `async` one-shot bug and the | |
| cross-session display bug, both fixed since; the fixes were checked offline as described below, not in a new live run. | |
| Each adapter is also checked with fake payloads shaped from the sources above (`test_integrations.py`): | |
| - correct text posted; | |
| - subagent, aborted and empty turns skipped; | |
| - no output, exit 0; | |
| - under 1.8 s against a moth that answers after 2 s; | |
| - no crash with moth down or a bad URL. | |
| Open points: | |
| - **Session ids and labels** come from the payload fields in the table above, checked against sources (Claude Code | |
| docs, Codex `stop.rs` / `legacy_notify.rs` at `rust-v0.163.0`, Hermes `agent/turn_finalizer.py` and | |
| `agent/runtime_cwd.py` at c57331677d, Pi 1.1.0 `ExtensionContext` types, OpenCode SDK 1.2.10 `Session`), and only | |
| with fake payloads. dsh's session object was not checked for a cwd field, so its label is `process.cwd()`. Hermes | |
| in a messaging gateway without a configured cwd labels sessions with the gateway's launch folder. | |
| - **Claude Code and Codex Stop**: if another Stop hook blocks and makes the agent keep going, the reply from before | |
| the block has already been posted, and the later reply posts too. An async observer can't tell them apart. | |
| - **Pi**: a follow-up you queue mid-run runs before `agent_settled`, so only the last reply in that chain posts. | |
| - **Claude Code / Codex Stop hooks** were run live on 2026-10-18 (Claude Code 2.1.302, Codex 0.163.0, | |
| `../../video/raw/INDEX.md` in the dev checkout): `async: true` posted from the interactive TUIs but not from | |
| `claude -p` / `codex exec`, so the hooks now run in the foreground. | |
| - **Real CLIs, fake model** (`tests/e2e/`, `.github/workflows/e2e.yml`, GitHub-hosted Linux, macOS and Windows): | |
| `run_e2e.py` installs the hooks with `moth hook ... --yes` into a scratch home from a folder with a space (and | |
| again with a venv Python in a folder with a space), runs `claude -p` (2.1.302), `codex exec` (0.163.0), `pi -p` | |
| (1.3.0) and `opencode run` (newest) against a fake Anthropic / OpenAI API, and checks that serve.py's real handler | |
| got the final reply with the harness's own session id and the folder label, before the CLI exited. Codex's run | |
| uses `--dangerously-bypass-hook-trust`; a control run without it must post nothing (the unapproved hook is | |
| skipped). The approval itself was checked by hand in the interactive `codex` (`tui_live.py --tui codex`, Linux): | |
| the startup review's "trust" writes the `trusted_hash`, and `codex exec` then posts without the flag. | |
| - **dsh**: | |
| - Not confirmed: whether a root-level `ctx.on('session/event')` receives events with no `inject`, and whether | |
| `session.id` equals `SubagentRunInfo.id` for in-process children. | |
| - Remote (`local: false`) subagents emit no local session events anyway. | |
| - **OpenCode**: | |
| - Not confirmed live: that the assistant `message.updated` arrives before its idle event. | |
| - In one-shot `opencode run` / `pi -p`, the process may exit before the POST lands. In `tests/e2e` (one short | |
| reply, local fake model) both posted before exit on Linux, macOS and Windows; a slow machine may still lose it. | |
| - **Pi**: child `pi` processes spawned by subagent extensions are not filtered. | |
| - **Hermes**: in one-shot runs (`hermes -q`), the daemon thread can die at exit before posting. | |
| Display layer: everything was checked against a stub moth with a real log (`Display` in `test_integrations.py`): | |
| - `moth tail`: the watch output, wrapping, history, restart, chat echo, and "asleep" said once. Interactive | |
| redraw was checked by hand in a pty (libedit Python 3.14). | |
| - Hermes registers no slash command. | |
| - Pet `plugin.js` under node with a fake `ctx`: module shape, history skipped, the `moth` id, ttl bounds, 120-char | |
| pieces, moth down, and no `ctx.pet`. | |
| - Pi widget and `/moth` with a fake `ctx.ui`. | |
| - OpenCode toast with a fake `client.tui`. The event hook doesn't wait on the poll. | |
| - Per-session displays: another harness's reaction landing last is not shown by the Pi widget, two OpenCode | |
| sessions in one process each get their own toast, the Hermes Desktop pet voices Hermes reactions only, and | |
| serve.py's `/api/says?source=&session=` filter (stripped answer, `next` unchanged, no ids leaked). | |
| - The Claude Code band, live in the real interactive `claude` 2.1.302 (`tests/e2e/tui_live.py`, Linux, tmux, scratch | |
| home, fake model, serve.py's real handler): `$.session.id()` equals the Stop hook's `session_id` (the band polls | |
| `/api/says?...&session=<id>` every 2 s, the hook posts the same id), also after `/clear` (both switch to the new | |
| id, the old line is cleared). With this session's reaction shown, two decoys posted last (another session id; the | |
| same id under `source=codex`) don't replace it. The field: ctrl+x tab focuses it, Enter posts `/api/chat` with the | |
| token from `~/.moth/token` (`$.fs.read`) and shows the answer, no model request follows, and the field comes back | |
| empty. | |
| - Pi's `/moth`: a reply slower than 4 s is picked up by an unfiltered poll (chat replies carry no session), so in | |
| that rare case another session's reaction could show instead. | |
| - `show_in_harness`: off by default in Pi and OpenCode (the turn is still posted, no `/api/says` poll, no widget or | |
| toast; Pi's `/moth` answers with a notice); the env beating the file both ways; a live toggle on → off → on of the | |
| file in one Pi / OpenCode process (the widget is taken down, and turning it back on never shows a line from the off | |
| turn); Pi's reader and `moth/config.py` agreeing on every spelling. The `python -m moth config` CLI and the browser | |
| opt-in of `python -m moth` are in `tests/test_config.py`. | |
| - `serve.py` on the real handler, with torch and the model faked out (`Serve`): the Host allow-list (look-alikes, | |
| a LAN IP, no port, no Host), the token on every guarded route, CORS (local pages everywhere, desktop-shell origins | |
| on `/api/says` only, look-alikes such as `localhost.evil.example` nowhere), foreign-origin / wrong-type / oversized | |
| / malformed POSTs, a bad `since`, `X-Frame-Options`, the token in the page but no CORS on it, `source` filtering, and | |
| "new home" refusing a folder without moth's marker. | |
| - Every adapter sends the token and its `source`; the Claude Code hook posts past an `http_proxy` in its environment. | |
| - Every adapter's `session` and `label` from fake payloads, end to end through the real handler and the real | |
| `Moth.hear_agent` onto the log's cards (`AdapterToLog`); their cleaning in serve.py (id charset, 40-char one-line | |
| label, ANSI stripped); the session list only in the token-guarded `/api/log`, never in `/api/says`; and | |
| `moth tail --session`. | |
| Not verified: | |
| - **Claude Code mod** (the rest was run live, see above): a click to focus the field; the band on macOS / Windows | |
| terminals; whether the poller survives a mid-session reload of the mod (it is started in `session.start`, and a | |
| reload drops timers). | |
| - **Hermes Desktop**: | |
| - that the packaged renderer really sends `Origin: null`, or `file://`, both of which are allowed; | |
| - that the renderer's `fetch` to `http://127.0.0.1:4747` isn't blocked by a CSP or by Chromium's | |
| private-network rules. serve.py deliberately does not send `Access-Control-Allow-Private-Network`; | |
| - that the copied desktop half shows up in the Plugins page as described in `electron/desktop-plugins-root.ts`. | |
| - **Pi**: how ANSI color looks inside a string-array widget. The interactive renderer wraps each string in a `Text`. | |
| - **OpenCode**: that `client.tui.showToast` works from a plugin in 1.2.10 (the SDK types have it), and that | |
| `opencode run` exits promptly with the poll pending. | |
| - **tail**: readline's redraw is called from the poll thread. It is cosmetic, but GNU readline was not tried. | |
| - **Token reads inside harnesses**: checked (the Claude Code mod's `$.fs.read` of `~/.moth/token` live; Pi's and | |
| OpenCode's `node:fs` reads in `tests/e2e`, where moth refuses a post without the right token), but only with no | |
| other fs hook installed that might object. | |
| - **Proxies in JS**: Node's `fetch` ignores `HTTP_PROXY`, but Bun's (OpenCode) honours it; with a proxy set and no | |
| `NO_PROXY=127.0.0.1`, OpenCode's posts may go to the proxy. | |