ermes-cli 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ermes_cli-1.0.0/.gitignore +17 -0
- ermes_cli-1.0.0/PKG-INFO +281 -0
- ermes_cli-1.0.0/README.md +251 -0
- ermes_cli-1.0.0/ermes_cli/__init__.py +1 -0
- ermes_cli-1.0.0/ermes_cli/admin_api.py +285 -0
- ermes_cli-1.0.0/ermes_cli/auth0.py +220 -0
- ermes_cli-1.0.0/ermes_cli/client.py +100 -0
- ermes_cli-1.0.0/ermes_cli/commands/__init__.py +0 -0
- ermes_cli-1.0.0/ermes_cli/commands/auth_cmds.py +386 -0
- ermes_cli-1.0.0/ermes_cli/commands/campaigns.py +321 -0
- ermes_cli-1.0.0/ermes_cli/commands/config_cmd.py +105 -0
- ermes_cli-1.0.0/ermes_cli/commands/data_paths.py +103 -0
- ermes_cli-1.0.0/ermes_cli/commands/export.py +238 -0
- ermes_cli-1.0.0/ermes_cli/commands/ingest.py +101 -0
- ermes_cli-1.0.0/ermes_cli/commands/journeys.py +182 -0
- ermes_cli-1.0.0/ermes_cli/commands/keys.py +113 -0
- ermes_cli-1.0.0/ermes_cli/commands/lists.py +137 -0
- ermes_cli-1.0.0/ermes_cli/commands/metrics.py +398 -0
- ermes_cli-1.0.0/ermes_cli/commands/projects.py +286 -0
- ermes_cli-1.0.0/ermes_cli/commands/providers.py +184 -0
- ermes_cli-1.0.0/ermes_cli/commands/subscriptions.py +75 -0
- ermes_cli-1.0.0/ermes_cli/commands/tags.py +86 -0
- ermes_cli-1.0.0/ermes_cli/commands/templates.py +133 -0
- ermes_cli-1.0.0/ermes_cli/commands/users.py +133 -0
- ermes_cli-1.0.0/ermes_cli/commands/write_cmds.py +87 -0
- ermes_cli-1.0.0/ermes_cli/config.py +393 -0
- ermes_cli-1.0.0/ermes_cli/errors.py +34 -0
- ermes_cli-1.0.0/ermes_cli/export_utils.py +263 -0
- ermes_cli-1.0.0/ermes_cli/keyring_store.py +145 -0
- ermes_cli-1.0.0/ermes_cli/main.py +193 -0
- ermes_cli-1.0.0/ermes_cli/manifest.py +218 -0
- ermes_cli-1.0.0/ermes_cli/mcp_server.py +167 -0
- ermes_cli-1.0.0/ermes_cli/output.py +58 -0
- ermes_cli-1.0.0/ermes_cli/routes.yaml +163 -0
- ermes_cli-1.0.0/ermes_cli/rules.py +54 -0
- ermes_cli-1.0.0/ermes_cli/write_mode.py +178 -0
- ermes_cli-1.0.0/pyproject.toml +60 -0
- ermes_cli-1.0.0/scripts/install-cli.sh +171 -0
- ermes_cli-1.0.0/scripts/install-mcp.sh +348 -0
- ermes_cli-1.0.0/tests/__init__.py +0 -0
- ermes_cli-1.0.0/tests/conftest.py +73 -0
- ermes_cli-1.0.0/tests/test_admin_api.py +317 -0
- ermes_cli-1.0.0/tests/test_auth0.py +195 -0
- ermes_cli-1.0.0/tests/test_auth_cmds.py +440 -0
- ermes_cli-1.0.0/tests/test_client.py +106 -0
- ermes_cli-1.0.0/tests/test_commands/__init__.py +0 -0
- ermes_cli-1.0.0/tests/test_commands/test_campaigns.py +408 -0
- ermes_cli-1.0.0/tests/test_commands/test_data_paths.py +58 -0
- ermes_cli-1.0.0/tests/test_commands/test_export.py +305 -0
- ermes_cli-1.0.0/tests/test_commands/test_ingest.py +68 -0
- ermes_cli-1.0.0/tests/test_commands/test_journeys.py +104 -0
- ermes_cli-1.0.0/tests/test_commands/test_keys.py +87 -0
- ermes_cli-1.0.0/tests/test_commands/test_lists.py +46 -0
- ermes_cli-1.0.0/tests/test_commands/test_metrics.py +458 -0
- ermes_cli-1.0.0/tests/test_commands/test_projects.py +335 -0
- ermes_cli-1.0.0/tests/test_commands/test_providers.py +208 -0
- ermes_cli-1.0.0/tests/test_commands/test_subscriptions.py +22 -0
- ermes_cli-1.0.0/tests/test_commands/test_tags.py +20 -0
- ermes_cli-1.0.0/tests/test_commands/test_templates.py +46 -0
- ermes_cli-1.0.0/tests/test_commands/test_users.py +84 -0
- ermes_cli-1.0.0/tests/test_config.py +136 -0
- ermes_cli-1.0.0/tests/test_export_utils.py +179 -0
- ermes_cli-1.0.0/tests/test_install_mcp_script.py +256 -0
- ermes_cli-1.0.0/tests/test_install_mcp_uninstall.py +168 -0
- ermes_cli-1.0.0/tests/test_keyring_store.py +185 -0
- ermes_cli-1.0.0/tests/test_manifest.py +229 -0
- ermes_cli-1.0.0/tests/test_mcp_server.py +160 -0
- ermes_cli-1.0.0/tests/test_output.py +53 -0
- ermes_cli-1.0.0/tests/test_projects_config.py +342 -0
- ermes_cli-1.0.0/tests/test_rules.py +155 -0
- ermes_cli-1.0.0/tests/test_version.py +142 -0
- ermes_cli-1.0.0/tests/test_whoami.py +110 -0
- ermes_cli-1.0.0/tests/test_write_cmds.py +61 -0
- ermes_cli-1.0.0/tests/test_write_mode.py +145 -0
ermes_cli-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ermes-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI-first access to Ermes marketing automation, with Auth0 multi-project auth and an optional compatibility MCP.
|
|
5
|
+
Project-URL: Homepage, https://ermes.publinker.com
|
|
6
|
+
Project-URL: Repository, https://github.com/Martech-DM/ermes
|
|
7
|
+
Author: Martech-DM
|
|
8
|
+
License: Proprietary
|
|
9
|
+
Keywords: cli,email,ermes,marketing-automation
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Communications :: Email
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: fastmcp>=2.0
|
|
18
|
+
Requires-Dist: httpx>=0.28
|
|
19
|
+
Requires-Dist: keyring>=24.0
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: requests>=2.31
|
|
22
|
+
Requires-Dist: rich>=14.0
|
|
23
|
+
Requires-Dist: typer>=0.24
|
|
24
|
+
Requires-Dist: tzdata>=2024.1
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-httpx>=0.35; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: responses>=0.25; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Ermes CLI and optional compatibility MCP
|
|
32
|
+
|
|
33
|
+
This package gives Ermes operators a CLI-first surface for explicit-project API reads and guarded
|
|
34
|
+
writes. It also retains a 10-tool read-only MCP for temporary compatibility, but normal agent
|
|
35
|
+
sessions leave that server unregistered.
|
|
36
|
+
|
|
37
|
+
Version 1.0.0 (DEV-2261). Verified 2026-10-20.
|
|
38
|
+
|
|
39
|
+
## Install the CLI
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pipx install ermes-cli # from PyPI; `pipx upgrade ermes-cli` later
|
|
43
|
+
ermes version
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or run the installer, which also checks Python/pipx and verifies the entrypoints:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
./cli/scripts/install-cli.sh # latest release
|
|
50
|
+
./cli/scripts/install-cli.sh --version=1.0.0 # pinned
|
|
51
|
+
./cli/scripts/install-cli.sh --branch=main # developers: straight from git
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Releases are cut by pushing a tag `ermes-cli-vX.Y.Z` equal to `cli/pyproject.toml`'s version
|
|
55
|
+
(`.github/workflows/publish-cli.yml` runs the tests, builds the wheel, publishes to PyPI and attaches
|
|
56
|
+
it to a GitHub Release). `install-cli.sh` is idempotent and safe to re-run for upgrades.
|
|
57
|
+
|
|
58
|
+
If a temporary rollback needs fixed typed schemas, register the compatibility MCP only for one
|
|
59
|
+
explicit project:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
./cli/scripts/install-mcp.sh --project=<project-name-or-id>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Remove it after the rollback window. Removal preserves Auth0 tokens, project keys, and every file
|
|
66
|
+
under `~/.ermes`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
./cli/scripts/install-mcp.sh --uninstall
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Never run `ermes logout` as part of registration removal; logout deletes the Keychain session and
|
|
73
|
+
`projects.yml`.
|
|
74
|
+
|
|
75
|
+
## Auth — Auth0 device login (recommended)
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ermes login # Auth0 device flow + auto-discover projects
|
|
79
|
+
ermes projects list # show cached projects; keys stay masked
|
|
80
|
+
ermes use <project-name-or-id> # interactive active-project selection
|
|
81
|
+
ermes whoami # local selection only; not API liveness
|
|
82
|
+
ermes projects refresh # re-sync after admins add you to new projects
|
|
83
|
+
ermes logout # delete Keychain tokens + projects.yml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`ermes login` runs an Auth0 Device Authorization Flow (RFC 8628) against the
|
|
87
|
+
shared `dev-hqiwr-re.us.auth0.com` tenant (same one publinker-cli uses, so the
|
|
88
|
+
authenticated user is your Auth0 identity). After authentication:
|
|
89
|
+
|
|
90
|
+
1. Trades the Auth0 access token for an Ermes admin JWT via `/api/cli/exchange`
|
|
91
|
+
(the account's email must be verified in Auth0).
|
|
92
|
+
2. Lists every project you can access via `/api/admin/projects/all`.
|
|
93
|
+
3. Caches the list in `~/.ermes/projects.yml` (chmod 600) — **no keys are minted at login**.
|
|
94
|
+
4. Auto-selects the first project as active.
|
|
95
|
+
5. On the first command against a project, mints **your own** `sk_` key for it via
|
|
96
|
+
`POST /api/admin/projects/:id/cli-key`, named `cli-<email>-<hostname>` and bound to your
|
|
97
|
+
admin account (revoked when you are removed from the project). Keys from the old
|
|
98
|
+
`cli-managed-<hostname>` scheme are ignored.
|
|
99
|
+
|
|
100
|
+
Auth0 tokens live in the OS keychain (`ermes-cli` service). On a headless box with no keyring
|
|
101
|
+
backend they fall back to `~/.ermes/auth0-tokens.json` (chmod 600). Per-project `sk_` keys live
|
|
102
|
+
in `~/.ermes/projects.yml`.
|
|
103
|
+
|
|
104
|
+
For interactive work, `ermes use <project-name-or-id>` persists the active project. For every script
|
|
105
|
+
or agent command, pin scope without changing that shared default:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
ERMES_PROJECT=<project-name-or-id> ermes --json campaigns list --limit 1
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Resolution order: `--profile`, `ERMES_API_KEY`, `ERMES_PROJECT`, the active project, then the legacy
|
|
112
|
+
`ERMES_PROFILE` / `config.yml` default. `ERMES_PROFILE` is deprecated and prints a warning; it can no
|
|
113
|
+
longer override an explicit project or `projects.yml`.
|
|
114
|
+
|
|
115
|
+
For CI or a machine without a browser, skip `login` and pass a key directly:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
ERMES_API_KEY=sk_... ERMES_BASE_URL=https://ermes.publinker.com ermes --json campaigns list
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`ermes whoami` reports the local selection; `ermes whoami --check` additionally makes one
|
|
122
|
+
authenticated read and exits 1 if the key is rejected.
|
|
123
|
+
|
|
124
|
+
> **Requires:** `AUTH_CLI_AUDIENCE=https://ermes.publinker.com/api` set on the
|
|
125
|
+
> Ermes API (SSM env-file). Without it, `/api/cli/exchange` returns 404 and
|
|
126
|
+
> `ermes login` errors out with a clear hint.
|
|
127
|
+
|
|
128
|
+
### Legacy: single-profile manual setup
|
|
129
|
+
|
|
130
|
+
`ermes config init` (or `ermes config add`) still works for the one-project,
|
|
131
|
+
manual-paste flow. It writes `~/.ermes/config.yml` with the legacy profile
|
|
132
|
+
shape; `resolve_profile()` falls back to it when no `projects.yml` exists. The
|
|
133
|
+
command now emits a deprecation warning pointing at `ermes login`.
|
|
134
|
+
|
|
135
|
+
## Write-mode safety (default-blocked mutations)
|
|
136
|
+
|
|
137
|
+
Every `POST` / `PATCH` / `PUT` / `DELETE` through the CLI is **blocked by
|
|
138
|
+
default**. Reads work freely; mutations require an explicit unlock. These interactive commands act on
|
|
139
|
+
the project deliberately selected with `ermes use`:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
ermes write status # show current state
|
|
143
|
+
ermes write allow # unlock for 30 minutes
|
|
144
|
+
ermes write allow --minutes 5 # custom window (1-1440 min)
|
|
145
|
+
ermes write allow --persistent # no expiry (until `write deny`)
|
|
146
|
+
ermes write deny # close the window immediately
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For one reviewed scripted mutation, keep both project and write authorization process-scoped:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
ERMES_PROJECT=<owned-project-name-or-id> ERMES_ALLOW_WRITES=1 \
|
|
153
|
+
ermes --json <resource-group> <mutation> <arguments>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Ermes has no general server-side dry run. Read the current resource before the mutation, keep the
|
|
157
|
+
scope to an owned project, then read it again to verify the requested state. Never leave
|
|
158
|
+
`ERMES_ALLOW_WRITES` exported in a shared shell.
|
|
159
|
+
|
|
160
|
+
State lives in `~/.ermes/state.yml` (chmod 600), separate from `projects.yml`
|
|
161
|
+
so refreshing projects doesn't clobber the write window. `ermes whoami` shows
|
|
162
|
+
the current write status.
|
|
163
|
+
|
|
164
|
+
The optional compatibility MCP remains fixed at 10 read-only tools. Mutations stay CLI-only so every
|
|
165
|
+
agent write must carry explicit project scope and one-process `ERMES_ALLOW_WRITES=1` authorization.
|
|
166
|
+
|
|
167
|
+
## Metrics
|
|
168
|
+
|
|
169
|
+
These examples assume that you deliberately selected an interactive project with `ermes use`. In
|
|
170
|
+
scripts, prefix the same commands with `ERMES_PROJECT=<project-name-or-id>`.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
ermes metrics # last 7 days, type=trigger, channel=email (UI default)
|
|
174
|
+
ermes metrics --type blast --days 30 # last 30 days, blast campaigns only
|
|
175
|
+
ermes metrics --type all --start 2026-05-01 --end 2026-05-31
|
|
176
|
+
ermes metrics --channel push --days 7 # push campaigns
|
|
177
|
+
ermes metrics --channel all --days 7 # both, as two labelled blocks
|
|
178
|
+
ermes --json metrics --type all # raw JSON for piping
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Per-campaign breakdown: sends, opens, clicks, bounces, complaints, plus
|
|
182
|
+
computed open/click/CTOR rates. Aggregate summary row at the bottom. Server
|
|
183
|
+
caps at 100 rows / 90-day range; the CLI surfaces both as warnings.
|
|
184
|
+
|
|
185
|
+
### Channels
|
|
186
|
+
|
|
187
|
+
`--channel / -c` takes `email` (default) | `push` | `all`. The API accepts only
|
|
188
|
+
`email` and `push`, lowercase — the CLI folds the case so `--channel PUSH`
|
|
189
|
+
works instead of 400ing, and defaults to `email`, which is also what the server
|
|
190
|
+
assumes when the param is omitted, so the default output is unchanged.
|
|
191
|
+
|
|
192
|
+
There is **no combined mode server-side**: `--channel all` issues one request
|
|
193
|
+
per channel and prints two labelled blocks (`--json` nests them under
|
|
194
|
+
`channels`). The two are never summed — push has no opens or bounces, so the
|
|
195
|
+
rates aren't comparable.
|
|
196
|
+
|
|
197
|
+
> **`--json` is a ROOT flag, not a `metrics` flag** — `ermes --json metrics …`,
|
|
198
|
+
> never `ermes metrics --json` (that exits with `No such option: --json`). This
|
|
199
|
+
> catches people out because every example that shows JSON also shows a
|
|
200
|
+
> subcommand. The per-campaign rows land under **`results`** (not `campaigns`
|
|
201
|
+
> or `rows`), alongside `channel`, `summary`, `truncated` and
|
|
202
|
+
> `aggregated_through`:
|
|
203
|
+
>
|
|
204
|
+
> ```json
|
|
205
|
+
> {"channel": "push", "results": [{"campaign_id": 156139, "name": "...",
|
|
206
|
+
> "sends": 61, "failed": 0, "clicks": 0, "click_rate": 0, "opens": 0}],
|
|
207
|
+
> "summary": {"campaign_count": 13, "sends": 499, "failed": 67, "clicks": 25},
|
|
208
|
+
> "truncated": false, "aggregated_through": "2026-08-04"}
|
|
209
|
+
> ```
|
|
210
|
+
>
|
|
211
|
+
> For `--channel all` the shape nests instead: `{"channel": "all",
|
|
212
|
+
> "channels": {"email": {…}, "push": {…}}}`, so a consumer must branch on
|
|
213
|
+
> `channel` before reading `results`.
|
|
214
|
+
|
|
215
|
+
Push renders the UI's columns — `Sent` (= `sends + failed`), `Delivered`
|
|
216
|
+
(= `sends`), `Failed`, `Clicks`, `Click %`. Opens / Open % / CTOR / Bounces /
|
|
217
|
+
Spam are omitted: push has no open or bounce event, so they are always zero and
|
|
218
|
+
only read as breakage.
|
|
219
|
+
|
|
220
|
+
`aggregated_through` in the response is the newest local day with data for that
|
|
221
|
+
channel+type, capped at the day the hourly rollup has marked complete for the
|
|
222
|
+
project. Today, and yesterday until the first run past local midnight, are
|
|
223
|
+
returned as partial "so far" numbers, so the default window ends yesterday,
|
|
224
|
+
like the UI; pass `--end` to include today. The empty-state message prints the frontier so an empty window
|
|
225
|
+
is legible.
|
|
226
|
+
|
|
227
|
+
## Tool surface
|
|
228
|
+
|
|
229
|
+
### CLI (`ermes ...`)
|
|
230
|
+
|
|
231
|
+
| Group | Commands |
|
|
232
|
+
|---|---|
|
|
233
|
+
| `campaigns` | list, get, create, update, delete, trigger (`--external-id`, optional `--email`/`--phone`/`--device-token`/`--locale`/`--timezone`) |
|
|
234
|
+
| `templates` | list, get, create, update, delete |
|
|
235
|
+
| `lists` | list, get, create, update, delete |
|
|
236
|
+
| `journeys` | list, get, create, update, delete |
|
|
237
|
+
| `users` | list, get, delete, events, lists |
|
|
238
|
+
| `keys` | list, get, create, update, delete — uses your Auth0 admin session, never a project key |
|
|
239
|
+
| `subscriptions`, `tags`, `providers`, `ingest`, `export`, `config` | … |
|
|
240
|
+
| `login`, `logout`, `whoami`, `use`, `projects` | Auth0 device-flow auth + multi-project context |
|
|
241
|
+
| `metrics` | Campaign metrics, email or push (mirrors UI Metrics view) |
|
|
242
|
+
| `write allow / deny / status` | Control the write-mode guard (mutations blocked by default) |
|
|
243
|
+
|
|
244
|
+
Full surface: `ermes --help`.
|
|
245
|
+
|
|
246
|
+
### Optional compatibility MCP (`ermes-mcp`)
|
|
247
|
+
|
|
248
|
+
The 10-tool read-only MCP remains for temporary Claude Code rollback only. Normal sessions use the
|
|
249
|
+
CLI so they can pin project scope per command without loading fixed schemas at startup.
|
|
250
|
+
`install-mcp.sh --project=<name-or-id>` writes an explicit `ERMES_PROJECT`; `ermes use` doesn't
|
|
251
|
+
retarget that process.
|
|
252
|
+
|
|
253
|
+
The CLI covers every compatibility read and is the preferred path:
|
|
254
|
+
|
|
255
|
+
| Tool | Purpose |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `whoami` | Current profile + base URL (no API call) |
|
|
258
|
+
| `list_campaigns(limit?, status?)` | Campaigns; use CLI `campaigns list --status` when filtering |
|
|
259
|
+
| `get_campaign(campaign_id)` | One campaign |
|
|
260
|
+
| `list_templates(limit?)` | Templates |
|
|
261
|
+
| `get_template(template_id)` | One template |
|
|
262
|
+
| `list_lists(limit?)` | Contact lists |
|
|
263
|
+
| `list_journeys(limit?)` | Automation journeys |
|
|
264
|
+
| `list_users(limit?, email?)` | Users; use CLI `users list --q` for exact search |
|
|
265
|
+
| `get_user(user_id)` | One user; the CLI argument is the external ID |
|
|
266
|
+
| `get_user_events(user_id, limit?)` | Recent events; the CLI argument is the external ID |
|
|
267
|
+
|
|
268
|
+
## Where in the wider stack
|
|
269
|
+
|
|
270
|
+
Ermes is a CLI-first integration in the
|
|
271
|
+
[`Martech-DM/martech-mcps`](https://github.com/Martech-DM/martech-mcps) registry.
|
|
272
|
+
That repo's `ermes/README.md` and `ermes/skills/ermes-ops/SKILL.md` are the
|
|
273
|
+
cross-fleet setup and operating runbooks. The compatibility MCP stays
|
|
274
|
+
unregistered during normal operation.
|
|
275
|
+
|
|
276
|
+
## CLI source history
|
|
277
|
+
|
|
278
|
+
The `ermes_cli/` package was originally bundled inside the legacy
|
|
279
|
+
`ermes-skill`. It now lives here as the single code source of truth. The
|
|
280
|
+
versioned operating skill lives at `martech-mcps/ermes/skills/ermes-ops` and
|
|
281
|
+
invokes this installed package rather than carrying a copy.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Ermes CLI and optional compatibility MCP
|
|
2
|
+
|
|
3
|
+
This package gives Ermes operators a CLI-first surface for explicit-project API reads and guarded
|
|
4
|
+
writes. It also retains a 10-tool read-only MCP for temporary compatibility, but normal agent
|
|
5
|
+
sessions leave that server unregistered.
|
|
6
|
+
|
|
7
|
+
Version 1.0.0 (DEV-2261). Verified 2026-10-20.
|
|
8
|
+
|
|
9
|
+
## Install the CLI
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pipx install ermes-cli # from PyPI; `pipx upgrade ermes-cli` later
|
|
13
|
+
ermes version
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or run the installer, which also checks Python/pipx and verifies the entrypoints:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
./cli/scripts/install-cli.sh # latest release
|
|
20
|
+
./cli/scripts/install-cli.sh --version=1.0.0 # pinned
|
|
21
|
+
./cli/scripts/install-cli.sh --branch=main # developers: straight from git
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Releases are cut by pushing a tag `ermes-cli-vX.Y.Z` equal to `cli/pyproject.toml`'s version
|
|
25
|
+
(`.github/workflows/publish-cli.yml` runs the tests, builds the wheel, publishes to PyPI and attaches
|
|
26
|
+
it to a GitHub Release). `install-cli.sh` is idempotent and safe to re-run for upgrades.
|
|
27
|
+
|
|
28
|
+
If a temporary rollback needs fixed typed schemas, register the compatibility MCP only for one
|
|
29
|
+
explicit project:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
./cli/scripts/install-mcp.sh --project=<project-name-or-id>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Remove it after the rollback window. Removal preserves Auth0 tokens, project keys, and every file
|
|
36
|
+
under `~/.ermes`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
./cli/scripts/install-mcp.sh --uninstall
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Never run `ermes logout` as part of registration removal; logout deletes the Keychain session and
|
|
43
|
+
`projects.yml`.
|
|
44
|
+
|
|
45
|
+
## Auth — Auth0 device login (recommended)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
ermes login # Auth0 device flow + auto-discover projects
|
|
49
|
+
ermes projects list # show cached projects; keys stay masked
|
|
50
|
+
ermes use <project-name-or-id> # interactive active-project selection
|
|
51
|
+
ermes whoami # local selection only; not API liveness
|
|
52
|
+
ermes projects refresh # re-sync after admins add you to new projects
|
|
53
|
+
ermes logout # delete Keychain tokens + projects.yml
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`ermes login` runs an Auth0 Device Authorization Flow (RFC 8628) against the
|
|
57
|
+
shared `dev-hqiwr-re.us.auth0.com` tenant (same one publinker-cli uses, so the
|
|
58
|
+
authenticated user is your Auth0 identity). After authentication:
|
|
59
|
+
|
|
60
|
+
1. Trades the Auth0 access token for an Ermes admin JWT via `/api/cli/exchange`
|
|
61
|
+
(the account's email must be verified in Auth0).
|
|
62
|
+
2. Lists every project you can access via `/api/admin/projects/all`.
|
|
63
|
+
3. Caches the list in `~/.ermes/projects.yml` (chmod 600) — **no keys are minted at login**.
|
|
64
|
+
4. Auto-selects the first project as active.
|
|
65
|
+
5. On the first command against a project, mints **your own** `sk_` key for it via
|
|
66
|
+
`POST /api/admin/projects/:id/cli-key`, named `cli-<email>-<hostname>` and bound to your
|
|
67
|
+
admin account (revoked when you are removed from the project). Keys from the old
|
|
68
|
+
`cli-managed-<hostname>` scheme are ignored.
|
|
69
|
+
|
|
70
|
+
Auth0 tokens live in the OS keychain (`ermes-cli` service). On a headless box with no keyring
|
|
71
|
+
backend they fall back to `~/.ermes/auth0-tokens.json` (chmod 600). Per-project `sk_` keys live
|
|
72
|
+
in `~/.ermes/projects.yml`.
|
|
73
|
+
|
|
74
|
+
For interactive work, `ermes use <project-name-or-id>` persists the active project. For every script
|
|
75
|
+
or agent command, pin scope without changing that shared default:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ERMES_PROJECT=<project-name-or-id> ermes --json campaigns list --limit 1
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Resolution order: `--profile`, `ERMES_API_KEY`, `ERMES_PROJECT`, the active project, then the legacy
|
|
82
|
+
`ERMES_PROFILE` / `config.yml` default. `ERMES_PROFILE` is deprecated and prints a warning; it can no
|
|
83
|
+
longer override an explicit project or `projects.yml`.
|
|
84
|
+
|
|
85
|
+
For CI or a machine without a browser, skip `login` and pass a key directly:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ERMES_API_KEY=sk_... ERMES_BASE_URL=https://ermes.publinker.com ermes --json campaigns list
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`ermes whoami` reports the local selection; `ermes whoami --check` additionally makes one
|
|
92
|
+
authenticated read and exits 1 if the key is rejected.
|
|
93
|
+
|
|
94
|
+
> **Requires:** `AUTH_CLI_AUDIENCE=https://ermes.publinker.com/api` set on the
|
|
95
|
+
> Ermes API (SSM env-file). Without it, `/api/cli/exchange` returns 404 and
|
|
96
|
+
> `ermes login` errors out with a clear hint.
|
|
97
|
+
|
|
98
|
+
### Legacy: single-profile manual setup
|
|
99
|
+
|
|
100
|
+
`ermes config init` (or `ermes config add`) still works for the one-project,
|
|
101
|
+
manual-paste flow. It writes `~/.ermes/config.yml` with the legacy profile
|
|
102
|
+
shape; `resolve_profile()` falls back to it when no `projects.yml` exists. The
|
|
103
|
+
command now emits a deprecation warning pointing at `ermes login`.
|
|
104
|
+
|
|
105
|
+
## Write-mode safety (default-blocked mutations)
|
|
106
|
+
|
|
107
|
+
Every `POST` / `PATCH` / `PUT` / `DELETE` through the CLI is **blocked by
|
|
108
|
+
default**. Reads work freely; mutations require an explicit unlock. These interactive commands act on
|
|
109
|
+
the project deliberately selected with `ermes use`:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
ermes write status # show current state
|
|
113
|
+
ermes write allow # unlock for 30 minutes
|
|
114
|
+
ermes write allow --minutes 5 # custom window (1-1440 min)
|
|
115
|
+
ermes write allow --persistent # no expiry (until `write deny`)
|
|
116
|
+
ermes write deny # close the window immediately
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For one reviewed scripted mutation, keep both project and write authorization process-scoped:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
ERMES_PROJECT=<owned-project-name-or-id> ERMES_ALLOW_WRITES=1 \
|
|
123
|
+
ermes --json <resource-group> <mutation> <arguments>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Ermes has no general server-side dry run. Read the current resource before the mutation, keep the
|
|
127
|
+
scope to an owned project, then read it again to verify the requested state. Never leave
|
|
128
|
+
`ERMES_ALLOW_WRITES` exported in a shared shell.
|
|
129
|
+
|
|
130
|
+
State lives in `~/.ermes/state.yml` (chmod 600), separate from `projects.yml`
|
|
131
|
+
so refreshing projects doesn't clobber the write window. `ermes whoami` shows
|
|
132
|
+
the current write status.
|
|
133
|
+
|
|
134
|
+
The optional compatibility MCP remains fixed at 10 read-only tools. Mutations stay CLI-only so every
|
|
135
|
+
agent write must carry explicit project scope and one-process `ERMES_ALLOW_WRITES=1` authorization.
|
|
136
|
+
|
|
137
|
+
## Metrics
|
|
138
|
+
|
|
139
|
+
These examples assume that you deliberately selected an interactive project with `ermes use`. In
|
|
140
|
+
scripts, prefix the same commands with `ERMES_PROJECT=<project-name-or-id>`.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
ermes metrics # last 7 days, type=trigger, channel=email (UI default)
|
|
144
|
+
ermes metrics --type blast --days 30 # last 30 days, blast campaigns only
|
|
145
|
+
ermes metrics --type all --start 2026-05-01 --end 2026-05-31
|
|
146
|
+
ermes metrics --channel push --days 7 # push campaigns
|
|
147
|
+
ermes metrics --channel all --days 7 # both, as two labelled blocks
|
|
148
|
+
ermes --json metrics --type all # raw JSON for piping
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Per-campaign breakdown: sends, opens, clicks, bounces, complaints, plus
|
|
152
|
+
computed open/click/CTOR rates. Aggregate summary row at the bottom. Server
|
|
153
|
+
caps at 100 rows / 90-day range; the CLI surfaces both as warnings.
|
|
154
|
+
|
|
155
|
+
### Channels
|
|
156
|
+
|
|
157
|
+
`--channel / -c` takes `email` (default) | `push` | `all`. The API accepts only
|
|
158
|
+
`email` and `push`, lowercase — the CLI folds the case so `--channel PUSH`
|
|
159
|
+
works instead of 400ing, and defaults to `email`, which is also what the server
|
|
160
|
+
assumes when the param is omitted, so the default output is unchanged.
|
|
161
|
+
|
|
162
|
+
There is **no combined mode server-side**: `--channel all` issues one request
|
|
163
|
+
per channel and prints two labelled blocks (`--json` nests them under
|
|
164
|
+
`channels`). The two are never summed — push has no opens or bounces, so the
|
|
165
|
+
rates aren't comparable.
|
|
166
|
+
|
|
167
|
+
> **`--json` is a ROOT flag, not a `metrics` flag** — `ermes --json metrics …`,
|
|
168
|
+
> never `ermes metrics --json` (that exits with `No such option: --json`). This
|
|
169
|
+
> catches people out because every example that shows JSON also shows a
|
|
170
|
+
> subcommand. The per-campaign rows land under **`results`** (not `campaigns`
|
|
171
|
+
> or `rows`), alongside `channel`, `summary`, `truncated` and
|
|
172
|
+
> `aggregated_through`:
|
|
173
|
+
>
|
|
174
|
+
> ```json
|
|
175
|
+
> {"channel": "push", "results": [{"campaign_id": 156139, "name": "...",
|
|
176
|
+
> "sends": 61, "failed": 0, "clicks": 0, "click_rate": 0, "opens": 0}],
|
|
177
|
+
> "summary": {"campaign_count": 13, "sends": 499, "failed": 67, "clicks": 25},
|
|
178
|
+
> "truncated": false, "aggregated_through": "2026-08-04"}
|
|
179
|
+
> ```
|
|
180
|
+
>
|
|
181
|
+
> For `--channel all` the shape nests instead: `{"channel": "all",
|
|
182
|
+
> "channels": {"email": {…}, "push": {…}}}`, so a consumer must branch on
|
|
183
|
+
> `channel` before reading `results`.
|
|
184
|
+
|
|
185
|
+
Push renders the UI's columns — `Sent` (= `sends + failed`), `Delivered`
|
|
186
|
+
(= `sends`), `Failed`, `Clicks`, `Click %`. Opens / Open % / CTOR / Bounces /
|
|
187
|
+
Spam are omitted: push has no open or bounce event, so they are always zero and
|
|
188
|
+
only read as breakage.
|
|
189
|
+
|
|
190
|
+
`aggregated_through` in the response is the newest local day with data for that
|
|
191
|
+
channel+type, capped at the day the hourly rollup has marked complete for the
|
|
192
|
+
project. Today, and yesterday until the first run past local midnight, are
|
|
193
|
+
returned as partial "so far" numbers, so the default window ends yesterday,
|
|
194
|
+
like the UI; pass `--end` to include today. The empty-state message prints the frontier so an empty window
|
|
195
|
+
is legible.
|
|
196
|
+
|
|
197
|
+
## Tool surface
|
|
198
|
+
|
|
199
|
+
### CLI (`ermes ...`)
|
|
200
|
+
|
|
201
|
+
| Group | Commands |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `campaigns` | list, get, create, update, delete, trigger (`--external-id`, optional `--email`/`--phone`/`--device-token`/`--locale`/`--timezone`) |
|
|
204
|
+
| `templates` | list, get, create, update, delete |
|
|
205
|
+
| `lists` | list, get, create, update, delete |
|
|
206
|
+
| `journeys` | list, get, create, update, delete |
|
|
207
|
+
| `users` | list, get, delete, events, lists |
|
|
208
|
+
| `keys` | list, get, create, update, delete — uses your Auth0 admin session, never a project key |
|
|
209
|
+
| `subscriptions`, `tags`, `providers`, `ingest`, `export`, `config` | … |
|
|
210
|
+
| `login`, `logout`, `whoami`, `use`, `projects` | Auth0 device-flow auth + multi-project context |
|
|
211
|
+
| `metrics` | Campaign metrics, email or push (mirrors UI Metrics view) |
|
|
212
|
+
| `write allow / deny / status` | Control the write-mode guard (mutations blocked by default) |
|
|
213
|
+
|
|
214
|
+
Full surface: `ermes --help`.
|
|
215
|
+
|
|
216
|
+
### Optional compatibility MCP (`ermes-mcp`)
|
|
217
|
+
|
|
218
|
+
The 10-tool read-only MCP remains for temporary Claude Code rollback only. Normal sessions use the
|
|
219
|
+
CLI so they can pin project scope per command without loading fixed schemas at startup.
|
|
220
|
+
`install-mcp.sh --project=<name-or-id>` writes an explicit `ERMES_PROJECT`; `ermes use` doesn't
|
|
221
|
+
retarget that process.
|
|
222
|
+
|
|
223
|
+
The CLI covers every compatibility read and is the preferred path:
|
|
224
|
+
|
|
225
|
+
| Tool | Purpose |
|
|
226
|
+
|---|---|
|
|
227
|
+
| `whoami` | Current profile + base URL (no API call) |
|
|
228
|
+
| `list_campaigns(limit?, status?)` | Campaigns; use CLI `campaigns list --status` when filtering |
|
|
229
|
+
| `get_campaign(campaign_id)` | One campaign |
|
|
230
|
+
| `list_templates(limit?)` | Templates |
|
|
231
|
+
| `get_template(template_id)` | One template |
|
|
232
|
+
| `list_lists(limit?)` | Contact lists |
|
|
233
|
+
| `list_journeys(limit?)` | Automation journeys |
|
|
234
|
+
| `list_users(limit?, email?)` | Users; use CLI `users list --q` for exact search |
|
|
235
|
+
| `get_user(user_id)` | One user; the CLI argument is the external ID |
|
|
236
|
+
| `get_user_events(user_id, limit?)` | Recent events; the CLI argument is the external ID |
|
|
237
|
+
|
|
238
|
+
## Where in the wider stack
|
|
239
|
+
|
|
240
|
+
Ermes is a CLI-first integration in the
|
|
241
|
+
[`Martech-DM/martech-mcps`](https://github.com/Martech-DM/martech-mcps) registry.
|
|
242
|
+
That repo's `ermes/README.md` and `ermes/skills/ermes-ops/SKILL.md` are the
|
|
243
|
+
cross-fleet setup and operating runbooks. The compatibility MCP stays
|
|
244
|
+
unregistered during normal operation.
|
|
245
|
+
|
|
246
|
+
## CLI source history
|
|
247
|
+
|
|
248
|
+
The `ermes_cli/` package was originally bundled inside the legacy
|
|
249
|
+
`ermes-skill`. It now lives here as the single code source of truth. The
|
|
250
|
+
versioned operating skill lives at `martech-mcps/ermes/skills/ermes-ops` and
|
|
251
|
+
invokes this installed package rather than carrying a copy.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|