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.
Files changed (74) hide show
  1. ermes_cli-1.0.0/.gitignore +17 -0
  2. ermes_cli-1.0.0/PKG-INFO +281 -0
  3. ermes_cli-1.0.0/README.md +251 -0
  4. ermes_cli-1.0.0/ermes_cli/__init__.py +1 -0
  5. ermes_cli-1.0.0/ermes_cli/admin_api.py +285 -0
  6. ermes_cli-1.0.0/ermes_cli/auth0.py +220 -0
  7. ermes_cli-1.0.0/ermes_cli/client.py +100 -0
  8. ermes_cli-1.0.0/ermes_cli/commands/__init__.py +0 -0
  9. ermes_cli-1.0.0/ermes_cli/commands/auth_cmds.py +386 -0
  10. ermes_cli-1.0.0/ermes_cli/commands/campaigns.py +321 -0
  11. ermes_cli-1.0.0/ermes_cli/commands/config_cmd.py +105 -0
  12. ermes_cli-1.0.0/ermes_cli/commands/data_paths.py +103 -0
  13. ermes_cli-1.0.0/ermes_cli/commands/export.py +238 -0
  14. ermes_cli-1.0.0/ermes_cli/commands/ingest.py +101 -0
  15. ermes_cli-1.0.0/ermes_cli/commands/journeys.py +182 -0
  16. ermes_cli-1.0.0/ermes_cli/commands/keys.py +113 -0
  17. ermes_cli-1.0.0/ermes_cli/commands/lists.py +137 -0
  18. ermes_cli-1.0.0/ermes_cli/commands/metrics.py +398 -0
  19. ermes_cli-1.0.0/ermes_cli/commands/projects.py +286 -0
  20. ermes_cli-1.0.0/ermes_cli/commands/providers.py +184 -0
  21. ermes_cli-1.0.0/ermes_cli/commands/subscriptions.py +75 -0
  22. ermes_cli-1.0.0/ermes_cli/commands/tags.py +86 -0
  23. ermes_cli-1.0.0/ermes_cli/commands/templates.py +133 -0
  24. ermes_cli-1.0.0/ermes_cli/commands/users.py +133 -0
  25. ermes_cli-1.0.0/ermes_cli/commands/write_cmds.py +87 -0
  26. ermes_cli-1.0.0/ermes_cli/config.py +393 -0
  27. ermes_cli-1.0.0/ermes_cli/errors.py +34 -0
  28. ermes_cli-1.0.0/ermes_cli/export_utils.py +263 -0
  29. ermes_cli-1.0.0/ermes_cli/keyring_store.py +145 -0
  30. ermes_cli-1.0.0/ermes_cli/main.py +193 -0
  31. ermes_cli-1.0.0/ermes_cli/manifest.py +218 -0
  32. ermes_cli-1.0.0/ermes_cli/mcp_server.py +167 -0
  33. ermes_cli-1.0.0/ermes_cli/output.py +58 -0
  34. ermes_cli-1.0.0/ermes_cli/routes.yaml +163 -0
  35. ermes_cli-1.0.0/ermes_cli/rules.py +54 -0
  36. ermes_cli-1.0.0/ermes_cli/write_mode.py +178 -0
  37. ermes_cli-1.0.0/pyproject.toml +60 -0
  38. ermes_cli-1.0.0/scripts/install-cli.sh +171 -0
  39. ermes_cli-1.0.0/scripts/install-mcp.sh +348 -0
  40. ermes_cli-1.0.0/tests/__init__.py +0 -0
  41. ermes_cli-1.0.0/tests/conftest.py +73 -0
  42. ermes_cli-1.0.0/tests/test_admin_api.py +317 -0
  43. ermes_cli-1.0.0/tests/test_auth0.py +195 -0
  44. ermes_cli-1.0.0/tests/test_auth_cmds.py +440 -0
  45. ermes_cli-1.0.0/tests/test_client.py +106 -0
  46. ermes_cli-1.0.0/tests/test_commands/__init__.py +0 -0
  47. ermes_cli-1.0.0/tests/test_commands/test_campaigns.py +408 -0
  48. ermes_cli-1.0.0/tests/test_commands/test_data_paths.py +58 -0
  49. ermes_cli-1.0.0/tests/test_commands/test_export.py +305 -0
  50. ermes_cli-1.0.0/tests/test_commands/test_ingest.py +68 -0
  51. ermes_cli-1.0.0/tests/test_commands/test_journeys.py +104 -0
  52. ermes_cli-1.0.0/tests/test_commands/test_keys.py +87 -0
  53. ermes_cli-1.0.0/tests/test_commands/test_lists.py +46 -0
  54. ermes_cli-1.0.0/tests/test_commands/test_metrics.py +458 -0
  55. ermes_cli-1.0.0/tests/test_commands/test_projects.py +335 -0
  56. ermes_cli-1.0.0/tests/test_commands/test_providers.py +208 -0
  57. ermes_cli-1.0.0/tests/test_commands/test_subscriptions.py +22 -0
  58. ermes_cli-1.0.0/tests/test_commands/test_tags.py +20 -0
  59. ermes_cli-1.0.0/tests/test_commands/test_templates.py +46 -0
  60. ermes_cli-1.0.0/tests/test_commands/test_users.py +84 -0
  61. ermes_cli-1.0.0/tests/test_config.py +136 -0
  62. ermes_cli-1.0.0/tests/test_export_utils.py +179 -0
  63. ermes_cli-1.0.0/tests/test_install_mcp_script.py +256 -0
  64. ermes_cli-1.0.0/tests/test_install_mcp_uninstall.py +168 -0
  65. ermes_cli-1.0.0/tests/test_keyring_store.py +185 -0
  66. ermes_cli-1.0.0/tests/test_manifest.py +229 -0
  67. ermes_cli-1.0.0/tests/test_mcp_server.py +160 -0
  68. ermes_cli-1.0.0/tests/test_output.py +53 -0
  69. ermes_cli-1.0.0/tests/test_projects_config.py +342 -0
  70. ermes_cli-1.0.0/tests/test_rules.py +155 -0
  71. ermes_cli-1.0.0/tests/test_version.py +142 -0
  72. ermes_cli-1.0.0/tests/test_whoami.py +110 -0
  73. ermes_cli-1.0.0/tests/test_write_cmds.py +61 -0
  74. ermes_cli-1.0.0/tests/test_write_mode.py +145 -0
@@ -0,0 +1,17 @@
1
+ # Python build artifacts
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+
8
+ # Virtualenvs
9
+ .venv/
10
+ venv/
11
+
12
+ # Test caches
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+
17
+ # Lock files (regenerated per Python version)
@@ -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"