Migration

From Hermes to Flowly

Flowly's plugin API is intentionally close to Hermes Agent's — the same register(ctx) entry point, the same hook event names, the same registration methods. Most Hermes plugins port over with import-path and conventional-path renames, no logic changes.

What changes

The differences fall into four buckets:

ConcernHermesFlowly
Importshermes_cli.*, plugins.*flowly.*, .* (relative)
Home dir$HERMES_HOME$FLOWLY_HOME
Temp paths/tmp/hermes-*/tmp/flowly-*
Manifest kindbackend / standalone / exclusivestandalone (only one supported)
Hook events and contexts are identical
The 14 lifecycle events, their payload shapes, and the action protocols (BlockAction, RewriteAction, SkipAction) are 1:1 with Hermes. Hooks don't need rewriting.

Imports

Most Hermes plugins do absolute imports against their own package:

python
# Hermes
from plugins.spotify.tools import SPOTIFY_PLAYBACK_SCHEMA, _handle_spotify_playback
from plugins.spotify.client import SpotifyClient
from hermes_cli.auth import get_auth_status, resolve_spotify_runtime_credentials
from tools.registry import tool_error, tool_result

Flowly loads plugins under a synthetic namespace, so absolute imports inside the plugin won't resolve. Convert to relative imports for intra-plugin references; replace Hermes core helpers with the Flowly equivalent or inline them.

python
# Flowly
from .tools import SPOTIFY_PLAYBACK_SCHEMA, _handle_spotify_playback
from .client import SpotifyClient
# Auth helpers don't have a 1:1 in Flowly v1 — see Auth section below.
# tool_error/tool_result are just JSON wrappers; inline them as small helpers.

The find/replace pass for most plugins:

bash
# Inside your plugin source
sed -i '' 's/from plugins\.<your-plugin>\./from \./g' *.py
sed -i '' 's/HERMES_HOME/FLOWLY_HOME/g' *.py
sed -i '' 's|/tmp/hermes-|/tmp/flowly-|g' *.py

Path conventions

Replace the home-directory helper:

python
# Hermes
from hermes_constants import get_hermes_home

def _state_dir() -> Path:
    return get_hermes_home() / "my-plugin"
python
# Flowly
from flowly.profile import get_flowly_home

def _state_dir() -> Path:
    return get_flowly_home() / "my-plugin"

Anywhere your plugin scans for paths inside HERMES_HOME (e.g. disk-cleanup's safety check), update the safety filter to match the Flowly profile-aware home + the new /tmp/flowly-*convention.

python
def is_safe_path(path: Path) -> bool:
    """Accept only paths under FLOWLY_HOME or /tmp/flowly-*."""
    flowly_home = get_flowly_home()
    try:
        path.resolve().relative_to(flowly_home)
        return True
    except (ValueError, OSError):
        pass
    parts = path.parts
    if len(parts) >= 3 and parts[1] == "tmp" and parts[2].startswith("flowly-"):
        return True
    return False

Manifest

Most fields are identical. Two adjustments for v1:

  • kind: Hermes supports standalone, backend, and exclusive. Flowly v1 only supports standalone. If your plugin used backend (e.g. an image_gen provider), change to standalone and lift the runtime registration into your register(ctx)directly — Flowly doesn't have a separate backend registry yet.
  • manifest_version: Flowly v1 supports manifests up to version 1. Hermes uses the same value, so usually no change.
yaml
# Hermes — for an image_gen backend
kind: backend

# Flowly v1 — for the same plugin
kind: standalone
# (and inside register(ctx), invoke whatever your provider needs to do
#  to make itself usable — usually just register a tool)

Auth & credentials

This is the largest gap. Hermes ships a shared auth layer in hermes_cli/auth.py with helpers for OAuth flows (PKCE callback servers, token persistence, refresh logic). Plugins like spotify import resolve_spotify_runtime_credentials() and never see the underlying flow.

Flowly v1 does not have an equivalent shared auth layer. If you're porting a plugin that uses hermes_cli.auth, you have two options:

  • Vendor the auth helpers into the plugin. Copy the relevant functions from hermes_cli/auth.py into your plugin's package. The PKCE OAuth flow is around 200 lines and self-contained.
  • Simplify to env-var or pre-filled credentials. Drop the OAuth dance and require the user to bring an existing access token via env var or a pre-populated credentials file under $FLOWLY_HOME/<plugin>/credentials.json. Less polish, much smaller patch.
Production OAuth requires a server-side proxy
If you ship a plugin to non-developer end users, the cleanest auth UX is the same one Flowly uses for Gmail: a server-side endpoint at your domain that holds the OAuth client credentials, runs the flow, and returns the user's tokens to the desktop client. Hermes' local-callback approach works for developers but exposes the OAuth client_id everywhere a user installs the plugin.

Already-ported plugins

Two Hermes plugins are already in Flowly's bundled set, both with public source you can read for porting patterns:

  • disk-cleanup standalone, hook-only, no auth. Smallest possible port: imports updated, paths renamed, safety filter adjusted. Source at flowly/plugins_bundled/disk-cleanup/.
  • auto-commit — written for Flowly directly but follows the Hermes plugin pattern. Useful as a reference for the audit-log + slash-command + config.json convention if you're porting a plugin that needs persistent state. Source at flowly/plugins_bundled/auto-commit/.

For larger ports (Hermes' spotify, google_meet), the work item count grows but the pattern is the same: imports first, paths next, then auth, then test against an isolated FLOWLY_HOME.