Metadata-Version: 2.5
Name: ermes-cli
Version: 1.0.0
Summary: CLI-first access to Ermes marketing automation, with Auth0 multi-project auth and an optional compatibility MCP.
Project-URL: Homepage, https://ermes.publinker.com
Project-URL: Repository, https://github.com/Martech-DM/ermes
Author: Martech-DM
License: Proprietary
Keywords: cli,email,ermes,marketing-automation
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Communications :: Email
Requires-Python: >=3.10
Requires-Dist: fastmcp>=2.0
Requires-Dist: httpx>=0.28
Requires-Dist: keyring>=24.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.31
Requires-Dist: rich>=14.0
Requires-Dist: typer>=0.24
Requires-Dist: tzdata>=2024.1
Provides-Extra: dev
Requires-Dist: pytest-httpx>=0.35; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: responses>=0.25; extra == 'dev'
Description-Content-Type: text/markdown

# Ermes CLI and optional compatibility MCP

This package gives Ermes operators a CLI-first surface for explicit-project API reads and guarded
writes. It also retains a 10-tool read-only MCP for temporary compatibility, but normal agent
sessions leave that server unregistered.

Version 1.0.0 (DEV-2261). Verified 2026-10-20.

## Install the CLI

```bash
pipx install ermes-cli              # from PyPI; `pipx upgrade ermes-cli` later
ermes version
```

Or run the installer, which also checks Python/pipx and verifies the entrypoints:

```bash
./cli/scripts/install-cli.sh                  # latest release
./cli/scripts/install-cli.sh --version=1.0.0  # pinned
./cli/scripts/install-cli.sh --branch=main    # developers: straight from git
```

Releases are cut by pushing a tag `ermes-cli-vX.Y.Z` equal to `cli/pyproject.toml`'s version
(`.github/workflows/publish-cli.yml` runs the tests, builds the wheel, publishes to PyPI and attaches
it to a GitHub Release). `install-cli.sh` is idempotent and safe to re-run for upgrades.

If a temporary rollback needs fixed typed schemas, register the compatibility MCP only for one
explicit project:

```bash
./cli/scripts/install-mcp.sh --project=<project-name-or-id>
```

Remove it after the rollback window. Removal preserves Auth0 tokens, project keys, and every file
under `~/.ermes`:

```bash
./cli/scripts/install-mcp.sh --uninstall
```

Never run `ermes logout` as part of registration removal; logout deletes the Keychain session and
`projects.yml`.

## Auth — Auth0 device login (recommended)

```bash
ermes login                             # Auth0 device flow + auto-discover projects
ermes projects list                     # show cached projects; keys stay masked
ermes use <project-name-or-id>           # interactive active-project selection
ermes whoami                            # local selection only; not API liveness
ermes projects refresh                  # re-sync after admins add you to new projects
ermes logout                            # delete Keychain tokens + projects.yml
```

`ermes login` runs an Auth0 Device Authorization Flow (RFC 8628) against the
shared `dev-hqiwr-re.us.auth0.com` tenant (same one publinker-cli uses, so the
authenticated user is your Auth0 identity). After authentication:

1. Trades the Auth0 access token for an Ermes admin JWT via `/api/cli/exchange`
   (the account's email must be verified in Auth0).
2. Lists every project you can access via `/api/admin/projects/all`.
3. Caches the list in `~/.ermes/projects.yml` (chmod 600) — **no keys are minted at login**.
4. Auto-selects the first project as active.
5. On the first command against a project, mints **your own** `sk_` key for it via
   `POST /api/admin/projects/:id/cli-key`, named `cli-<email>-<hostname>` and bound to your
   admin account (revoked when you are removed from the project). Keys from the old
   `cli-managed-<hostname>` scheme are ignored.

Auth0 tokens live in the OS keychain (`ermes-cli` service). On a headless box with no keyring
backend they fall back to `~/.ermes/auth0-tokens.json` (chmod 600). Per-project `sk_` keys live
in `~/.ermes/projects.yml`.

For interactive work, `ermes use <project-name-or-id>` persists the active project. For every script
or agent command, pin scope without changing that shared default:

```bash
ERMES_PROJECT=<project-name-or-id> ermes --json campaigns list --limit 1
```

Resolution order: `--profile`, `ERMES_API_KEY`, `ERMES_PROJECT`, the active project, then the legacy
`ERMES_PROFILE` / `config.yml` default. `ERMES_PROFILE` is deprecated and prints a warning; it can no
longer override an explicit project or `projects.yml`.

For CI or a machine without a browser, skip `login` and pass a key directly:

```bash
ERMES_API_KEY=sk_... ERMES_BASE_URL=https://ermes.publinker.com ermes --json campaigns list
```

`ermes whoami` reports the local selection; `ermes whoami --check` additionally makes one
authenticated read and exits 1 if the key is rejected.

> **Requires:** `AUTH_CLI_AUDIENCE=https://ermes.publinker.com/api` set on the
> Ermes API (SSM env-file). Without it, `/api/cli/exchange` returns 404 and
> `ermes login` errors out with a clear hint.

### Legacy: single-profile manual setup

`ermes config init` (or `ermes config add`) still works for the one-project,
manual-paste flow. It writes `~/.ermes/config.yml` with the legacy profile
shape; `resolve_profile()` falls back to it when no `projects.yml` exists. The
command now emits a deprecation warning pointing at `ermes login`.

## Write-mode safety (default-blocked mutations)

Every `POST` / `PATCH` / `PUT` / `DELETE` through the CLI is **blocked by
default**. Reads work freely; mutations require an explicit unlock. These interactive commands act on
the project deliberately selected with `ermes use`:

```bash
ermes write status                    # show current state
ermes write allow                     # unlock for 30 minutes
ermes write allow --minutes 5         # custom window (1-1440 min)
ermes write allow --persistent        # no expiry (until `write deny`)
ermes write deny                      # close the window immediately
```

For one reviewed scripted mutation, keep both project and write authorization process-scoped:

```bash
ERMES_PROJECT=<owned-project-name-or-id> ERMES_ALLOW_WRITES=1 \
  ermes --json <resource-group> <mutation> <arguments>
```

Ermes has no general server-side dry run. Read the current resource before the mutation, keep the
scope to an owned project, then read it again to verify the requested state. Never leave
`ERMES_ALLOW_WRITES` exported in a shared shell.

State lives in `~/.ermes/state.yml` (chmod 600), separate from `projects.yml`
so refreshing projects doesn't clobber the write window. `ermes whoami` shows
the current write status.

The optional compatibility MCP remains fixed at 10 read-only tools. Mutations stay CLI-only so every
agent write must carry explicit project scope and one-process `ERMES_ALLOW_WRITES=1` authorization.

## Metrics

These examples assume that you deliberately selected an interactive project with `ermes use`. In
scripts, prefix the same commands with `ERMES_PROJECT=<project-name-or-id>`.

```bash
ermes metrics                           # last 7 days, type=trigger, channel=email (UI default)
ermes metrics --type blast --days 30    # last 30 days, blast campaigns only
ermes metrics --type all --start 2026-05-01 --end 2026-05-31
ermes metrics --channel push --days 7   # push campaigns
ermes metrics --channel all --days 7    # both, as two labelled blocks
ermes --json metrics --type all         # raw JSON for piping
```

Per-campaign breakdown: sends, opens, clicks, bounces, complaints, plus
computed open/click/CTOR rates. Aggregate summary row at the bottom. Server
caps at 100 rows / 90-day range; the CLI surfaces both as warnings.

### Channels

`--channel / -c` takes `email` (default) | `push` | `all`. The API accepts only
`email` and `push`, lowercase — the CLI folds the case so `--channel PUSH`
works instead of 400ing, and defaults to `email`, which is also what the server
assumes when the param is omitted, so the default output is unchanged.

There is **no combined mode server-side**: `--channel all` issues one request
per channel and prints two labelled blocks (`--json` nests them under
`channels`). The two are never summed — push has no opens or bounces, so the
rates aren't comparable.

> **`--json` is a ROOT flag, not a `metrics` flag** — `ermes --json metrics …`,
> never `ermes metrics --json` (that exits with `No such option: --json`). This
> catches people out because every example that shows JSON also shows a
> subcommand. The per-campaign rows land under **`results`** (not `campaigns`
> or `rows`), alongside `channel`, `summary`, `truncated` and
> `aggregated_through`:
>
> ```json
> {"channel": "push", "results": [{"campaign_id": 156139, "name": "...",
>  "sends": 61, "failed": 0, "clicks": 0, "click_rate": 0, "opens": 0}],
>  "summary": {"campaign_count": 13, "sends": 499, "failed": 67, "clicks": 25},
>  "truncated": false, "aggregated_through": "2026-08-04"}
> ```
>
> For `--channel all` the shape nests instead: `{"channel": "all",
> "channels": {"email": {…}, "push": {…}}}`, so a consumer must branch on
> `channel` before reading `results`.

Push renders the UI's columns — `Sent` (= `sends + failed`), `Delivered`
(= `sends`), `Failed`, `Clicks`, `Click %`. Opens / Open % / CTOR / Bounces /
Spam are omitted: push has no open or bounce event, so they are always zero and
only read as breakage.

`aggregated_through` in the response is the newest local day with data for that
channel+type, capped at the day the hourly rollup has marked complete for the
project. Today, and yesterday until the first run past local midnight, are
returned as partial "so far" numbers, so the default window ends yesterday,
like the UI; pass `--end` to include today. The empty-state message prints the frontier so an empty window
is legible.

## Tool surface

### CLI (`ermes ...`)

| Group | Commands |
|---|---|
| `campaigns` | list, get, create, update, delete, trigger (`--external-id`, optional `--email`/`--phone`/`--device-token`/`--locale`/`--timezone`) |
| `templates` | list, get, create, update, delete |
| `lists` | list, get, create, update, delete |
| `journeys` | list, get, create, update, delete |
| `users` | list, get, delete, events, lists |
| `keys` | list, get, create, update, delete — uses your Auth0 admin session, never a project key |
| `subscriptions`, `tags`, `providers`, `ingest`, `export`, `config` | … |
| `login`, `logout`, `whoami`, `use`, `projects` | Auth0 device-flow auth + multi-project context |
| `metrics` | Campaign metrics, email or push (mirrors UI Metrics view) |
| `write allow / deny / status` | Control the write-mode guard (mutations blocked by default) |

Full surface: `ermes --help`.

### Optional compatibility MCP (`ermes-mcp`)

The 10-tool read-only MCP remains for temporary Claude Code rollback only. Normal sessions use the
CLI so they can pin project scope per command without loading fixed schemas at startup.
`install-mcp.sh --project=<name-or-id>` writes an explicit `ERMES_PROJECT`; `ermes use` doesn't
retarget that process.

The CLI covers every compatibility read and is the preferred path:

| Tool | Purpose |
|---|---|
| `whoami` | Current profile + base URL (no API call) |
| `list_campaigns(limit?, status?)` | Campaigns; use CLI `campaigns list --status` when filtering |
| `get_campaign(campaign_id)` | One campaign |
| `list_templates(limit?)` | Templates |
| `get_template(template_id)` | One template |
| `list_lists(limit?)` | Contact lists |
| `list_journeys(limit?)` | Automation journeys |
| `list_users(limit?, email?)` | Users; use CLI `users list --q` for exact search |
| `get_user(user_id)` | One user; the CLI argument is the external ID |
| `get_user_events(user_id, limit?)` | Recent events; the CLI argument is the external ID |

## Where in the wider stack

Ermes is a CLI-first integration in the
[`Martech-DM/martech-mcps`](https://github.com/Martech-DM/martech-mcps) registry.
That repo's `ermes/README.md` and `ermes/skills/ermes-ops/SKILL.md` are the
cross-fleet setup and operating runbooks. The compatibility MCP stays
unregistered during normal operation.

## CLI source history

The `ermes_cli/` package was originally bundled inside the legacy
`ermes-skill`. It now lives here as the single code source of truth. The
versioned operating skill lives at `martech-mcps/ermes/skills/ermes-ops` and
invokes this installed package rather than carrying a copy.
