Metadata-Version: 2.5
Name: galvanize
Version: 0.2.6
Summary: Event-driven activation points for AI agents: wake a fresh Hermes (or any CLI agent) session when something happens.
Project-URL: Homepage, https://github.com/lychee888/galvanize
Author: Jarvis Lai
License: MIT
License-File: LICENSE
Keywords: agents,automation,events,hermes,triggers,webhooks
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.10
Requires-Dist: imap-tools<2,>=1.10
Requires-Dist: keyring<26,>=24
Requires-Dist: pyyaml<7,>=6.0
Requires-Dist: tomli>=2.0; python_version < '3.11'
Requires-Dist: watchdog<7,>=4.0
Provides-Extra: dev
Requires-Dist: py-spy>=0.3.8; extra == 'dev'
Requires-Dist: pytest-timeout>=2.4; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Provides-Extra: imap
Requires-Dist: imap-tools<2,>=1.10; extra == 'imap'
Requires-Dist: keyring<26,>=24; extra == 'imap'
Description-Content-Type: text/markdown

![galvanize: wake your agents when the world moves](assets/galvanize-infographic.png)

**Wake your AI agent when something happens.**

galvanize watches the real world (new mail, a file landing in a folder, a git commit, a webhook call, an event from a script) and starts a fresh AI agent session with a prompt you wrote the moment one occurs. Event triggers sit directly in the agent's own tool list (native plugin for Hermes, MCP server for Claude Code and Codex), so "wake me when resumes land" wires an actual push trigger. Passwords go to your OS keyring, each trigger starts watching in seconds, and results arrive wherever you asked (Telegram, Discord, or the log).

### What your harness doesn't already do

- **Claude Code and Codex can't be woken from outside at all.** Their hooks run
  inside a live session. galvanize supplies the missing inbound door: the daemon
  watches the world and spawns the session when something happens.
- **Hermes has inbound webhooks, but only as admin config**, never in the
  agent's tool list. So agents fall back to cron: an hourly poller that spends
  an LLM run to announce that nothing arrived.
- **Watching is free; thinking costs tokens.** Source state (IMAP IDLE, folder,
  relay) lives in a small daemon with reconnect, dedupe, and catch-up. A model
  runs exactly once per real event, in a fresh one-shot session you scoped.

## Install

Works on Windows, macOS, and Linux (Python 3.10+). No cloning needed; one command installs straight from GitHub:

```bash
pipx install "git+https://github.com/lychee888/galvanize.git"
# or: uv tool install --from "git+https://github.com/lychee888/galvanize.git" galvanize
# or: pip install git+https://github.com/lychee888/galvanize.git
```

Then run setup once:

```bash
galvanize init
```

`init` enables the Hermes webhook platform (backs up your config first), installs the agent plugin, registers the daemon to start at login, and auto-registers the MCP tool surface into Claude Code / Codex if it finds them on this machine. Everything is confirmed on screen and reversible; `--yes` accepts defaults.

### The daemon's own runtime

`daemon install` does **not** adopt the interpreter you happened to run it
from. It provisions (or repairs) an app-owned venv under
`~/.galvanize/runtime/venv`, validates it *launch-equivalently* (spawned
isolated, at the launch working directory, with `PYTHONPATH`/user-site
scrubbed: imports, package versions, a real OS-keyring set/get/delete
roundtrip, windowless target), and records the result in
`~/.galvanize/install.json`. Only that recorded, probed identity goes into
the persistence entries (Startup `.lnk`, watchdog task, systemd unit,
LaunchAgent). Falling back to your invoker's interpreter requires passing
the same probe, and install says so out loud; a failed probe changes
nothing on disk. `galvanize probe` prints the manifest for this
interpreter; `galvanize doctor` diffs install.json vs the persistence
target vs the live daemon three ways.

Testing/automation seams (env vars): `GALVANIZE_RUNTIME_VENV` (location),
`GALVANIZE_RUNTIME_DISABLE=1` (skip the app venv entirely),
`GALVANIZE_RUNTIME_SKIP_PROVISION=1` (validate, never pip),
`GALVANIZE_INSTALL_INTERP=<exe>` (override the fallback candidate).

After that, the whole surface is five verbs:

```bash
galvanize add folder ~/watch --wake hermes             # + a live test fire
galvanize add imap you@gmail.com --wake hermes         # app-password to keyring
galvanize status           # watching? last fire, errors, health
galvanize doctor           # deep health check
galvanize test cad-drops   # inject a synthetic event through the real path
```

## Why

Polling is what agents reach for because it's the only event surface in their tool list; everything above makes the push version the default.

- **Fresh sessions.** Each event spawns a clean one-shot run; results are delivered where you asked.
- **Management everywhere cron is managed.** Dashboard `/triggers` tab, `/triggers` slash command, `hermes triggers` CLI, agent tools, plus `galvanize status` / `doctor` / `daemon`: all surfaces, one ops core.
- **Push email.** IMAP IDLE with 25-min re-arm, UID dedupe, reconnect catch-up, keyring-held credentials, and a `migrate hermes-cron` command that converts your existing email pollers.
- **Zero-inbound-port webhooks.** Optional user-owned Cloudflare Worker relay (`relay/worker.js`): services POST to your URL, your laptop pulls the queue.

## Working with each harness

### Hermes

`galvanize init` does the setup: enables `platforms.webhook` in your `config.yaml` (backup saved first), pip-installs the package into the interpreter Hermes runs in, copies the plugin into `~/.hermes/plugins` and enables it. Restart the Hermes gateway once to load the webhook platform; the `trigger_*` tools appear in new sessions. From then on, "wake me when a file lands in ~/cad-drops" creates a real trigger in conversation.

### Claude Code / Codex

`init` writes the MCP server entry into `~/.claude.json` / `~/.codex/config.toml` automatically when it finds them (Codex gets `default_tools_approval_mode = "approve"` pre-declared, as newer Codex builds otherwise hide the tools). New session: `trigger_add` and friends are in the tool list. Wake presets:

```bash
galvanize add folder ~/inbox --wake claude   # claude -p "{prompt}"
galvanize add folder ~/inbox --wake codex    # codex exec (sandbox pre-declared)
galvanize add git-hook ~/code/myrepo --wake codex   # wake Codex on commits
```

### Any other CLI agent

`galvanize add ... --wake dsh` defaults to `dsh --profile headless "{prompt}"`.
The DSH plugin installer saves a custom `--wake-profile` into both its runtime
profiles and the core's `dsh_wake_profile` setting, so future CLI and plugin
triggers agree. The CLI also accepts `--wake-profile <name>` for one trigger.
Existing triggers retain their saved commands. MCP `trigger_add` requires an
explicit `wake` target and accepts `command`, `workdir`, `wake_profile`,
`relay_url`, and `relay_token`; it does not silently choose Hermes.

```bash
galvanize add folder ~/watch --wake shell --command 'myagent run "{prompt}"'
```

Commands containing `{prompt}` or `{payload}` run as an executable plus literal arguments. Event text is never interpolated into a shell command. Use a script for pipelines or redirection, and read `GALVANIZE_PROMPT` / `GALVANIZE_PAYLOAD` inside it. On Windows, npm command shims are resolved to their Node entry point; other batch files need an explicit executable or interpreter.

Shell wakes run invisibly on Windows: every child is spawned through one
launch primitive (`galvanize/winlaunch.py`) with a hidden console inside a
kill-on-close Job Object, so no window ever appears and a timeout or a
surviving descendant takes the whole process tree down with it. The honest
limit: invisibility covers CONSOLE windows only — a shell wake executes
user-declared commands, and any GUI descendant (an editor, a message box,
an Electron app) can still draw its own window.

### Relay webhooks

DSH and other shell wakes require a relay for webhooks. Deploy `relay/worker.js` with a `RELAY` KV binding and two distinct secrets: `RELAY_TOKEN` for the daemon to read events, and `INGEST_TOKEN` for senders to submit events. Set both using `wrangler secret put` before deploying.

```bash
galvanize add webhook --name incoming --wake dsh \
  --relay https://your-relay.workers.dev --relay-token "$RELAY_TOKEN"
curl -X POST https://your-relay.workers.dev/ingest/incoming \
  -H "Authorization: Bearer $INGEST_TOKEN" \
  -H 'Content-Type: application/json' -d '{"message":"hello"}'
```

The supplied worker requires the ingestion bearer header. Providers that cannot send it need an adapter that validates their native signature before forwarding; pointing their unauthenticated webhook directly at the worker will return 401. Existing deployments must configure `INGEST_TOKEN` and update senders when installing this worker revision.

Relay receipt and delivery are persisted separately in per-URL SQLite inboxes
under `GALVANIZE_HOME` (default `~/.galvanize`). A page and its receipt cursor
commit together before dispatch. The delivery acknowledgement advances only
past successful or explicitly quarantined events. A failed wake never advances
that acknowledgement; later received events can still run. Normal restarts
resume pending work and do not replay acknowledged IDs, including retained
history returned again by the relay.

Delivery is **at least once**, not exactly once: a crash after a wake's external
side effect but before the acknowledgement commit can repeat it. Every payload
includes the stable relay `event_id`; use that ID for idempotent side effects.
Keep the inbox files when restarting. Deleting them intentionally resets replay
protection. IDs are retained without automatic pruning; monitor disk usage for
long-running, high-volume deployments. Events not received before the Worker's
retention period expires cannot be recovered locally. Cloudflare KV is eventually
consistent; this change does not make its timestamp cursor a transactional queue.

Failed dispatches retry up to five times with bounded exponential delay (2, 4,
8, 16 seconds before exhaustion). `galvanize status` and `trigger_status` expose
event ID, attempts, and error. After fixing the cause, use
`galvanize relay-retry <event-id>` to grant another five attempts. Cooldown and
in-flight deduplication deferrals remain pending and are checked again after
two seconds without spending a failed-dispatch attempt. Malformed records are
quarantined individually and remain visible. The Worker wraps
non-object JSON as `{"value": ...}` and non-JSON text as `{"raw": ...}`; the
reader also normalizes retained records from older Workers.

`watching` requires a fresh, connected watcher and a live daemon for folder,
IMAP, and relay sources. Direct Hermes webhooks use gateway health instead;
manual emit triggers have no watcher. IMAP exits IDLE with DONE before searching
or fetching mail, then re-enters IDLE after draining.

## Daily use

The agent's own `trigger_add` tool is the primary creation path; the CLI is the fallback for use without an agent. Every editor — agent tool, CLI, dashboard, `galvanize serve` — writes through the same transactional SQLite authority (`~/.galvanize/galvanize.db`) with config revisions, so concurrent edits cannot silently clobber each other. `~/.galvanize/triggers.yaml` is an import/export mirror (and the first-seed source): hand-edit it, then `galvanize config import triggers.yaml` to activate, or `config export` to start fresh from the live set.

### Wake sessions & tool access

A galvanize wake is an event-based cron session: the woken Hermes session
runs with the toolsets named by its webhook **route**. Since v0.2.4 every
managed route carries an explicit `toolsets` key defaulting to main-session
parity: the gateway's `default` distribution ("all available tools, all the
time"), i.e. `browser, file, image_gen, terminal, vision, web`. Without the
key the gateway falls back to the restricted `hermes-webhook` composite
(web_search, web_extract, vision_analyze, clarify), which silently cripples
any prompt containing real steps.

Narrow the grant per trigger (CLI; the plugin/MCP `trigger_add` tool takes
the same list as `wake_tools`):

```bash
galvanize add folder ~/inbox --wake-tools terminal,file   # exactly these
galvanize add imap me@x.com --wake-tools none             # alert-only posture
```

`--wake-tools none` is the security knob for high-volume or untrusted feeds:
inbound content is attacker-influable, and a wake session with autonomous
shell access turns a prompt injection into code execution. Choose it when the
trigger only needs to notify. The explicit choice is recorded on the trigger,
so re-adding or updating it never silently widens or revokes the grant.

Valid names are the Hermes toolset registry keys (checked against the
gateway's allow-list for the webhook platform as of this release):
`bot_room, browser, catalog, clarify, code_execution, coding, computer_use,
connections, context_engine, cronjob, debugging, delegation, desktop_ui,
feishu_doc, feishu_drive, file, image_gen, kanban, memory, onboarding,
project, safe, search, session_search, skills, terminal, todo, tts, video,
video_gen, vision, web, x_search, yuanbao` (plus composites like
`hermes-webhook`). The gateway re-validates names at spawn and drops unknown
ones, so a stale entry degrades to a no-op.

`galvanize doctor` prints one `wake tools '<name>'` row per hermes trigger
(parity / override list / restricted) and ⚠-warns on key-less routes left by
pre-0.2.4 installs. Fix them (recorded choices and manual grants survive):

```bash
galvanize doctor --fix    # also runs automatically on `galvanize daemon install`
```

### Importing mailbox passwords from himalaya

If your mail already works via [himalaya](https://pimalaya.org/), don't type the
app-password a second time:

```bash
galvanize secrets import-himalaya --dry-run   # list candidates; writes nothing
galvanize secrets import-himalaya             # store plaintext IMAP passwords in your OS keyring
galvanize secrets import-himalaya --overwrite # replace galvanize secrets that differ
```

Passwords land under the same `imap:<user>` keys `galvanize add imap` writes, so
existing imap triggers pick the values up on their next reconnect. The himalaya
config is read, never written, and password values are never printed. Accounts
whose password comes from a `cmd` store are reported but skipped. The config is
auto-detected (`~/.config/himalaya/config.toml`, `%APPDATA%\himalaya`, `$XDG_CONFIG_HOME`);
`--config PATH` overrides, and an empty `GALVANIZE_HIMALAYA_CONFIG` disables
discovery entirely.

`galvanize doctor` adds a drift check for each himalaya account: a password that
is missing from (or differs from) the galvanize store surfaces as a ⚠ warning
with the exact import command to run — non-blocking, and never auto-resynced.

## Development

```bash
git clone https://github.com/lychee888/galvanize && cd galvanize
python -m venv .venv && .venv/bin/pip install -e ".[dev]"   # Scripts/ on Windows
.venv/bin/pytest                     # unit suite; live-lane + docker tests skip cleanly
```

The live Hermes-lane test runs inside a Hermes checkout's venv (`pytest tests/test_live_hermes_lane.py`); the IMAP suite drives a GreenMail container and skips when Docker is unavailable. `test_imap_protocol.py` always exercises the real IMAP client over TCP against a strict test server that rejects commands during IDLE. Relay Worker tests execute the actual JavaScript with a memory-backed KV fixture, not a Cloudflare deployment.

MIT licensed. Built for the Hermes ecosystem; architecture is harness-neutral.
