ermes-cli 1.0.0__tar.gz → 1.2.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 (80) hide show
  1. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/PKG-INFO +51 -42
  2. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/README.md +49 -39
  3. ermes_cli-1.2.0/ermes_cli/__init__.py +1 -0
  4. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/auth_cmds.py +1 -1
  5. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/projects.py +1 -1
  6. ermes_cli-1.2.0/ermes_cli/commands/skill.py +40 -0
  7. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/config.py +1 -1
  8. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/main.py +50 -0
  9. ermes_cli-1.2.0/ermes_cli/skill/SKILL.md +69 -0
  10. ermes_cli-1.2.0/ermes_cli/update.py +225 -0
  11. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/write_mode.py +1 -5
  12. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/pyproject.toml +2 -7
  13. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/scripts/install-cli.sh +1 -11
  14. ermes_cli-1.2.0/tests/test_skill_command.py +20 -0
  15. ermes_cli-1.2.0/tests/test_update.py +366 -0
  16. ermes_cli-1.0.0/ermes_cli/__init__.py +0 -1
  17. ermes_cli-1.0.0/ermes_cli/manifest.py +0 -218
  18. ermes_cli-1.0.0/ermes_cli/mcp_server.py +0 -167
  19. ermes_cli-1.0.0/ermes_cli/routes.yaml +0 -163
  20. ermes_cli-1.0.0/scripts/install-mcp.sh +0 -348
  21. ermes_cli-1.0.0/tests/test_install_mcp_script.py +0 -256
  22. ermes_cli-1.0.0/tests/test_install_mcp_uninstall.py +0 -168
  23. ermes_cli-1.0.0/tests/test_manifest.py +0 -229
  24. ermes_cli-1.0.0/tests/test_mcp_server.py +0 -160
  25. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/.gitignore +0 -0
  26. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/admin_api.py +0 -0
  27. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/auth0.py +0 -0
  28. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/client.py +0 -0
  29. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/__init__.py +0 -0
  30. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/campaigns.py +0 -0
  31. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/config_cmd.py +0 -0
  32. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/data_paths.py +0 -0
  33. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/export.py +0 -0
  34. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/ingest.py +0 -0
  35. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/journeys.py +0 -0
  36. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/keys.py +0 -0
  37. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/lists.py +0 -0
  38. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/metrics.py +0 -0
  39. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/providers.py +0 -0
  40. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/subscriptions.py +0 -0
  41. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/tags.py +0 -0
  42. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/templates.py +0 -0
  43. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/users.py +0 -0
  44. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/commands/write_cmds.py +0 -0
  45. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/errors.py +0 -0
  46. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/export_utils.py +0 -0
  47. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/keyring_store.py +0 -0
  48. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/output.py +0 -0
  49. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/ermes_cli/rules.py +0 -0
  50. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/__init__.py +0 -0
  51. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/conftest.py +0 -0
  52. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_admin_api.py +0 -0
  53. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_auth0.py +0 -0
  54. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_auth_cmds.py +0 -0
  55. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_client.py +0 -0
  56. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/__init__.py +0 -0
  57. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_campaigns.py +0 -0
  58. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_data_paths.py +0 -0
  59. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_export.py +0 -0
  60. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_ingest.py +0 -0
  61. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_journeys.py +0 -0
  62. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_keys.py +0 -0
  63. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_lists.py +0 -0
  64. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_metrics.py +0 -0
  65. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_projects.py +0 -0
  66. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_providers.py +0 -0
  67. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_subscriptions.py +0 -0
  68. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_tags.py +0 -0
  69. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_templates.py +0 -0
  70. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_commands/test_users.py +0 -0
  71. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_config.py +0 -0
  72. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_export_utils.py +0 -0
  73. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_keyring_store.py +0 -0
  74. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_output.py +0 -0
  75. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_projects_config.py +0 -0
  76. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_rules.py +0 -0
  77. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_version.py +0 -0
  78. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_whoami.py +0 -0
  79. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_write_cmds.py +0 -0
  80. {ermes_cli-1.0.0 → ermes_cli-1.2.0}/tests/test_write_mode.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
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.
3
+ Version: 1.2.0
4
+ Summary: CLI-first access to Ermes marketing automation with Auth0 multi-project auth.
5
5
  Project-URL: Homepage, https://ermes.publinker.com
6
6
  Project-URL: Repository, https://github.com/Martech-DM/ermes
7
7
  Author: Martech-DM
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3 :: Only
15
15
  Classifier: Topic :: Communications :: Email
16
16
  Requires-Python: >=3.10
17
- Requires-Dist: fastmcp>=2.0
18
17
  Requires-Dist: httpx>=0.28
19
18
  Requires-Dist: keyring>=24.0
20
19
  Requires-Dist: pyyaml>=6.0
@@ -28,13 +27,13 @@ Requires-Dist: pytest>=8.0; extra == 'dev'
28
27
  Requires-Dist: responses>=0.25; extra == 'dev'
29
28
  Description-Content-Type: text/markdown
30
29
 
31
- # Ermes CLI and optional compatibility MCP
30
+ # Ermes CLI
32
31
 
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.
32
+ This package gives Ermes operators a CLI surface for explicit-project API reads and guarded writes.
36
33
 
37
- Version 1.0.0 (DEV-2261). Verified 2026-10-20.
34
+ Version 1.2.0. It adds `ermes skill install` (DEV-2329). 1.1.0 removed the `ermes-mcp`
35
+ compatibility server that 1.0.0 shipped (DEV-2326); a machine that registered it must drop that
36
+ registration (e.g. `claude mcp remove ermes`).
38
37
 
39
38
  ## Install the CLI
40
39
 
@@ -55,22 +54,54 @@ Releases are cut by pushing a tag `ermes-cli-vX.Y.Z` equal to `cli/pyproject.tom
55
54
  (`.github/workflows/publish-cli.yml` runs the tests, builds the wheel, publishes to PyPI and attaches
56
55
  it to a GitHub Release). `install-cli.sh` is idempotent and safe to re-run for upgrades.
57
56
 
58
- If a temporary rollback needs fixed typed schemas, register the compatibility MCP only for one
59
- explicit project:
57
+ ## Teach your AI assistant
60
58
 
61
59
  ```bash
62
- ./cli/scripts/install-mcp.sh --project=<project-name-or-id>
60
+ ermes skill install # writes ~/.claude/skills/ermes/SKILL.md (Claude Code) and ~/.agents/skills/ermes/SKILL.md (Codex)
61
+ ermes skill path # prints the packaged SKILL.md, for any other assistant
63
62
  ```
64
63
 
65
- Remove it after the rollback window. Removal preserves Auth0 tokens, project keys, and every file
66
- under `~/.ermes`:
64
+ The skill ships inside the package, so it matches the installed version. Re-run `ermes skill
65
+ install` after `ermes update`, then restart the assistant.
66
+
67
+ ## Update the CLI
68
+
69
+ **Automatic prompt.** In an interactive terminal, the CLI checks PyPI for a newer `ermes-cli` at
70
+ most once every 24 hours (2-second timeout, cached in `~/.ermes/update-check.json`; failed checks
71
+ are cached too, so an offline laptop is not slowed down). When one exists it asks before running
72
+ your command:
73
+
74
+ ```
75
+ ermes-cli 1.2.0 is available (you have 1.1.0). Update now? [Y]es / [n]ot now / [s]kip this version
76
+ ```
77
+
78
+ - **Yes** (Enter): runs `pipx upgrade ermes-cli`, checks the new version in a fresh process, then
79
+ re-runs your command on it.
80
+ - **Not now**: runs your command; asks again on the next run.
81
+ - **Skip this version**: never asks about that version again; a newer release asks once more.
82
+
83
+ A failed upgrade or Ctrl-C at the prompt prints one line and runs your command on the current
84
+ version. The check never runs with `--json`, when `CI` is set, when stdin/stdout/stderr is not a
85
+ TTY, before `ermes version` / `ermes update`, or with `ERMES_NO_UPDATE_CHECK=1`.
86
+
87
+ **Manual.**
67
88
 
68
89
  ```bash
69
- ./cli/scripts/install-mcp.sh --uninstall
90
+ ermes update # check, then ask before upgrading
91
+ ermes update --check # report only, never install
92
+ ermes update --yes # upgrade without asking
93
+ ermes --json update [--yes] # --json installs only with --yes
70
94
  ```
71
95
 
72
- Never run `ermes logout` as part of registration removal; logout deletes the Keychain session and
73
- `projects.yml`.
96
+ **Status values** (`--json` `status`): `up_to_date`, `ahead`, `update_available`, `updated`,
97
+ `lookup_failed` (exit 1), `update_failed` (exit 1). Installer output goes to stderr; stdout stays
98
+ one JSON document.
99
+
100
+ **Supported install.** Only the pipx install of the PyPI package (`install-cli.sh`, or
101
+ `pipx install ermes-cli`) upgrades itself. `pipx upgrade` keeps the old venv if pip fails, so a
102
+ failed upgrade never leaves you without a CLI. Editable checkouts (`git pull`), git or local-dir
103
+ installs, non-pipx environments and an `ermes` that is not the venv pipx manages get the manual
104
+ command instead of a prompt. Keychain tokens and `~/.ermes` are untouched.
74
105
 
75
106
  ## Auth — Auth0 device login (recommended)
76
107
 
@@ -161,8 +192,8 @@ State lives in `~/.ermes/state.yml` (chmod 600), separate from `projects.yml`
161
192
  so refreshing projects doesn't clobber the write window. `ermes whoami` shows
162
193
  the current write status.
163
194
 
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.
195
+ Every agent write must carry explicit project scope and one-process `ERMES_ALLOW_WRITES=1`
196
+ authorization.
166
197
 
167
198
  ## Metrics
168
199
 
@@ -239,39 +270,17 @@ is legible.
239
270
  | `subscriptions`, `tags`, `providers`, `ingest`, `export`, `config` | … |
240
271
  | `login`, `logout`, `whoami`, `use`, `projects` | Auth0 device-flow auth + multi-project context |
241
272
  | `metrics` | Campaign metrics, email or push (mirrors UI Metrics view) |
273
+ | `skill install / path` | Install the packaged agent skill for Claude Code / Codex |
242
274
  | `write allow / deny / status` | Control the write-mode guard (mutations blocked by default) |
243
275
 
244
276
  Full surface: `ermes --help`.
245
277
 
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
278
  ## Where in the wider stack
269
279
 
270
280
  Ermes is a CLI-first integration in the
271
281
  [`Martech-DM/martech-mcps`](https://github.com/Martech-DM/martech-mcps) registry.
272
282
  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.
283
+ cross-fleet setup and operating runbooks.
275
284
 
276
285
  ## CLI source history
277
286
 
@@ -1,10 +1,10 @@
1
- # Ermes CLI and optional compatibility MCP
1
+ # Ermes CLI
2
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.
3
+ This package gives Ermes operators a CLI surface for explicit-project API reads and guarded writes.
6
4
 
7
- Version 1.0.0 (DEV-2261). Verified 2026-10-20.
5
+ Version 1.2.0. It adds `ermes skill install` (DEV-2329). 1.1.0 removed the `ermes-mcp`
6
+ compatibility server that 1.0.0 shipped (DEV-2326); a machine that registered it must drop that
7
+ registration (e.g. `claude mcp remove ermes`).
8
8
 
9
9
  ## Install the CLI
10
10
 
@@ -25,22 +25,54 @@ Releases are cut by pushing a tag `ermes-cli-vX.Y.Z` equal to `cli/pyproject.tom
25
25
  (`.github/workflows/publish-cli.yml` runs the tests, builds the wheel, publishes to PyPI and attaches
26
26
  it to a GitHub Release). `install-cli.sh` is idempotent and safe to re-run for upgrades.
27
27
 
28
- If a temporary rollback needs fixed typed schemas, register the compatibility MCP only for one
29
- explicit project:
28
+ ## Teach your AI assistant
30
29
 
31
30
  ```bash
32
- ./cli/scripts/install-mcp.sh --project=<project-name-or-id>
31
+ ermes skill install # writes ~/.claude/skills/ermes/SKILL.md (Claude Code) and ~/.agents/skills/ermes/SKILL.md (Codex)
32
+ ermes skill path # prints the packaged SKILL.md, for any other assistant
33
33
  ```
34
34
 
35
- Remove it after the rollback window. Removal preserves Auth0 tokens, project keys, and every file
36
- under `~/.ermes`:
35
+ The skill ships inside the package, so it matches the installed version. Re-run `ermes skill
36
+ install` after `ermes update`, then restart the assistant.
37
+
38
+ ## Update the CLI
39
+
40
+ **Automatic prompt.** In an interactive terminal, the CLI checks PyPI for a newer `ermes-cli` at
41
+ most once every 24 hours (2-second timeout, cached in `~/.ermes/update-check.json`; failed checks
42
+ are cached too, so an offline laptop is not slowed down). When one exists it asks before running
43
+ your command:
44
+
45
+ ```
46
+ ermes-cli 1.2.0 is available (you have 1.1.0). Update now? [Y]es / [n]ot now / [s]kip this version
47
+ ```
48
+
49
+ - **Yes** (Enter): runs `pipx upgrade ermes-cli`, checks the new version in a fresh process, then
50
+ re-runs your command on it.
51
+ - **Not now**: runs your command; asks again on the next run.
52
+ - **Skip this version**: never asks about that version again; a newer release asks once more.
53
+
54
+ A failed upgrade or Ctrl-C at the prompt prints one line and runs your command on the current
55
+ version. The check never runs with `--json`, when `CI` is set, when stdin/stdout/stderr is not a
56
+ TTY, before `ermes version` / `ermes update`, or with `ERMES_NO_UPDATE_CHECK=1`.
57
+
58
+ **Manual.**
37
59
 
38
60
  ```bash
39
- ./cli/scripts/install-mcp.sh --uninstall
61
+ ermes update # check, then ask before upgrading
62
+ ermes update --check # report only, never install
63
+ ermes update --yes # upgrade without asking
64
+ ermes --json update [--yes] # --json installs only with --yes
40
65
  ```
41
66
 
42
- Never run `ermes logout` as part of registration removal; logout deletes the Keychain session and
43
- `projects.yml`.
67
+ **Status values** (`--json` `status`): `up_to_date`, `ahead`, `update_available`, `updated`,
68
+ `lookup_failed` (exit 1), `update_failed` (exit 1). Installer output goes to stderr; stdout stays
69
+ one JSON document.
70
+
71
+ **Supported install.** Only the pipx install of the PyPI package (`install-cli.sh`, or
72
+ `pipx install ermes-cli`) upgrades itself. `pipx upgrade` keeps the old venv if pip fails, so a
73
+ failed upgrade never leaves you without a CLI. Editable checkouts (`git pull`), git or local-dir
74
+ installs, non-pipx environments and an `ermes` that is not the venv pipx manages get the manual
75
+ command instead of a prompt. Keychain tokens and `~/.ermes` are untouched.
44
76
 
45
77
  ## Auth — Auth0 device login (recommended)
46
78
 
@@ -131,8 +163,8 @@ State lives in `~/.ermes/state.yml` (chmod 600), separate from `projects.yml`
131
163
  so refreshing projects doesn't clobber the write window. `ermes whoami` shows
132
164
  the current write status.
133
165
 
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.
166
+ Every agent write must carry explicit project scope and one-process `ERMES_ALLOW_WRITES=1`
167
+ authorization.
136
168
 
137
169
  ## Metrics
138
170
 
@@ -209,39 +241,17 @@ is legible.
209
241
  | `subscriptions`, `tags`, `providers`, `ingest`, `export`, `config` | … |
210
242
  | `login`, `logout`, `whoami`, `use`, `projects` | Auth0 device-flow auth + multi-project context |
211
243
  | `metrics` | Campaign metrics, email or push (mirrors UI Metrics view) |
244
+ | `skill install / path` | Install the packaged agent skill for Claude Code / Codex |
212
245
  | `write allow / deny / status` | Control the write-mode guard (mutations blocked by default) |
213
246
 
214
247
  Full surface: `ermes --help`.
215
248
 
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
249
  ## Where in the wider stack
239
250
 
240
251
  Ermes is a CLI-first integration in the
241
252
  [`Martech-DM/martech-mcps`](https://github.com/Martech-DM/martech-mcps) registry.
242
253
  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.
254
+ cross-fleet setup and operating runbooks.
245
255
 
246
256
  ## CLI source history
247
257
 
@@ -0,0 +1 @@
1
+ __version__ = "1.2.0"
@@ -80,7 +80,7 @@ def _hostname() -> str:
80
80
  def ensure_project_key(project_id: int) -> str:
81
81
  """Return the cached CLI key for `project_id`, minting it on first use.
82
82
 
83
- Raises `ConfigError` (never SystemExit — the MCP server calls this too)
83
+ Raises `ConfigError` (never SystemExit)
84
84
  when there is no Auth0 session or the server refuses.
85
85
  """
86
86
  payload = load_projects_file()
@@ -246,7 +246,7 @@ def use(
246
246
  ..., help="Project name or numeric id (from `ermes projects list`)"
247
247
  ),
248
248
  ) -> None:
249
- """Switch the active project. Affects every subsequent CLI + MCP call."""
249
+ """Switch the active project. Affects every subsequent CLI call."""
250
250
  projects = get_projects()
251
251
  if not projects:
252
252
  console.print("[yellow]No cached projects.[/yellow] Run `ermes login` first.")
@@ -0,0 +1,40 @@
1
+ """`ermes skill` — install the agent skill that teaches AI assistants the CLI.
2
+
3
+ The skill is a plain SKILL.md packaged with the CLI, so it always matches the
4
+ installed version. `install` copies it to the user-level skill folders that
5
+ Claude Code and Codex read; `path` prints it for any other assistant.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from importlib import resources
11
+ from pathlib import Path
12
+
13
+ import typer
14
+
15
+ skill_app = typer.Typer(help="Install the Ermes skill for Claude Code and Codex.", no_args_is_help=True)
16
+
17
+ SKILL_NAME = "ermes"
18
+ # User-level skill folders: Claude Code reads ~/.claude/skills, Codex reads
19
+ # ~/.agents/skills. Both take <name>/SKILL.md.
20
+ TARGETS = (Path(".claude/skills"), Path(".agents/skills"))
21
+
22
+
23
+ def packaged_skill() -> Path:
24
+ return Path(str(resources.files("ermes_cli") / "skill" / "SKILL.md"))
25
+
26
+
27
+ @skill_app.command("install", help="Copy the skill into ~/.claude/skills and ~/.agents/skills.")
28
+ def install_cmd() -> None:
29
+ text = packaged_skill().read_text(encoding="utf-8")
30
+ for target in TARGETS:
31
+ dest = Path.home() / target / SKILL_NAME / "SKILL.md"
32
+ dest.parent.mkdir(parents=True, exist_ok=True)
33
+ dest.write_text(text, encoding="utf-8")
34
+ typer.echo(f"installed {dest}")
35
+ typer.echo("Restart Claude Code / Codex to pick it up.")
36
+
37
+
38
+ @skill_app.command("path", help="Print the packaged SKILL.md path (for other assistants).")
39
+ def path_cmd() -> None:
40
+ typer.echo(packaged_skill())
@@ -304,7 +304,7 @@ def get_auth0_email() -> str | None:
304
304
 
305
305
 
306
306
  # ---------------------------------------------------------------------------
307
- # Profile resolution — what the CLI / MCP actually consume at runtime
307
+ # Profile resolution — what the CLI actually consumes at runtime
308
308
  # ---------------------------------------------------------------------------
309
309
 
310
310
 
@@ -32,11 +32,15 @@ atexit.register(_close_client)
32
32
 
33
33
  @app.callback()
34
34
  def main(
35
+ ctx: typer.Context,
35
36
  profile: Optional[str] = typer.Option(None, "--profile", "-p", help="Profile name"),
36
37
  use_json: bool = typer.Option(False, "--json", help="Output as JSON"),
37
38
  ):
38
39
  state.profile_name = profile
39
40
  state.use_json = use_json
41
+ from ermes_cli.update import maybe_prompt
42
+
43
+ maybe_prompt(use_json, ctx.invoked_subcommand)
40
44
 
41
45
 
42
46
  def get_client() -> ErmesClient:
@@ -61,6 +65,50 @@ def version():
61
65
  typer.echo(f"ermes-cli {__version__}")
62
66
 
63
67
 
68
+ @app.command()
69
+ def update(
70
+ check: bool = typer.Option(False, "--check", help="Only report whether a newer version exists."),
71
+ yes: bool = typer.Option(False, "--yes", "-y", help="Upgrade without asking."),
72
+ ):
73
+ """Check PyPI for a newer ermes-cli and upgrade the pipx install. Disable the daily prompt with ERMES_NO_UPDATE_CHECK=1."""
74
+ import json
75
+ import time
76
+
77
+ from ermes_cli import update as upd
78
+
79
+ def done(status: str, code: int = 0, **extra) -> None:
80
+ if state.use_json:
81
+ print(json.dumps({"status": status, "installed": __version__, **extra}))
82
+ raise typer.Exit(code=code)
83
+
84
+ try:
85
+ latest = upd.fetch_latest()
86
+ except upd.UpdateError as e:
87
+ typer.echo(f"Update check failed: {e}", err=True)
88
+ done("lookup_failed", 1, error=str(e))
89
+ upd.write_cache(checked_at=time.time(), latest=latest, error=None)
90
+
91
+ if not upd.is_newer(latest, __version__):
92
+ status = "ahead" if upd.is_newer(__version__, latest) else "up_to_date"
93
+ if not state.use_json:
94
+ typer.echo(f"ermes-cli {__version__} is {'newer than PyPI ' + latest if status == 'ahead' else 'up to date'}.")
95
+ done(status, latest=latest)
96
+ if check or (state.use_json and not yes):
97
+ if not state.use_json:
98
+ typer.echo(f"ermes-cli {latest} is available (you have {__version__}). Run `ermes update` to install.")
99
+ done("update_available", latest=latest)
100
+ if not yes and not typer.confirm(f"Upgrade ermes-cli {__version__} -> {latest}?", default=True, err=True):
101
+ done("update_available", latest=latest)
102
+ try:
103
+ installed = upd.upgrade(upd.detect_install(), latest)
104
+ except upd.UpdateError as e:
105
+ typer.echo(f"Update failed: {e}", err=True)
106
+ done("update_failed", 1, latest=latest, error=str(e))
107
+ if not state.use_json:
108
+ typer.echo(f"Updated ermes-cli to {installed}.")
109
+ done("updated", latest=latest)
110
+
111
+
64
112
  @app.command()
65
113
  def whoami(
66
114
  check: bool = typer.Option(
@@ -171,6 +219,7 @@ from ermes_cli.commands.auth_cmds import login as _login, logout as _logout
171
219
  from ermes_cli.commands.projects import projects_app, use as _use
172
220
  from ermes_cli.commands.write_cmds import write_app
173
221
  from ermes_cli.commands.metrics import metrics_app
222
+ from ermes_cli.commands.skill import skill_app
174
223
 
175
224
  app.add_typer(campaigns_app, name="campaigns")
176
225
  app.add_typer(templates_app, name="templates")
@@ -188,6 +237,7 @@ app.add_typer(export_app, name="export")
188
237
  app.add_typer(projects_app, name="projects")
189
238
  app.add_typer(write_app, name="write")
190
239
  app.add_typer(metrics_app, name="metrics")
240
+ app.add_typer(skill_app, name="skill")
191
241
  app.command(name="login", help="Auth0 device login + auto-discover projects")(_login)
192
242
  app.command(name="logout", help="Forget Auth0 tokens + cached projects")(_logout)
193
243
  app.command(name="use", help="Switch active project (by name or id)")(_use)
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: ermes
3
+ description: Read and manage Ermes marketing-automation data (campaigns, journeys, lists, templates, users, events, metrics, subscriptions) with the `ermes` CLI. Use when the user asks about their Ermes projects, campaigns, email metrics, contacts or lists, or mentions ermes.
4
+ ---
5
+
6
+ # Ermes CLI
7
+
8
+ The `ermes` command talks to the Ermes API for one project at a time. It only
9
+ sees the projects the logged-in user has access to.
10
+
11
+ ## Before anything
12
+
13
+ 1. `ermes whoami` shows the selected project (it does not prove the login is
14
+ still valid). If there is no login, ask the user to run `ermes login`
15
+ themselves (it opens a browser to confirm a code). Never run `ermes login`
16
+ for them, and never print API keys or the contents of `~/.ermes`.
17
+ 2. `ermes projects list` to see the projects. Name the project on every
18
+ command with `ERMES_PROJECT=<name-or-id>` in front of it. Do not run
19
+ `ermes use` to switch projects: it changes the user's shared default.
20
+
21
+ ```bash
22
+ ERMES_PROJECT=<project> ermes --json campaigns list --limit 5
23
+ ```
24
+
25
+ ## Find the right command
26
+
27
+ Every group and command has `--help`; read it instead of guessing flags.
28
+
29
+ ```bash
30
+ ermes --help
31
+ ermes campaigns --help
32
+ ermes metrics --help
33
+ ```
34
+
35
+ `--json` is a root option: `ermes --json campaigns list`, not
36
+ `ermes campaigns list --json`. Bound list reads with `--limit`.
37
+
38
+ ## Common reads
39
+
40
+ ```bash
41
+ ERMES_PROJECT=<project> ermes --json campaigns get <id>
42
+ ERMES_PROJECT=<project> ermes --json journeys list --limit 20
43
+ ERMES_PROJECT=<project> ermes --json users get <external-id>
44
+ ERMES_PROJECT=<project> ermes --json users events <external-id> --limit 20
45
+ ERMES_PROJECT=<project> ermes --json metrics --help
46
+ ```
47
+
48
+ User commands take the user's `external_id`, not an email address.
49
+
50
+ ## Changes (writes)
51
+
52
+ Commands that create, change or delete data are blocked by default. Before a write:
53
+
54
+ 1. Read the current value first, then tell the user exactly what will change
55
+ and get a clear yes for that exact command.
56
+ 2. Run that one command with `ERMES_ALLOW_WRITES=1` and an explicit project:
57
+ `ERMES_PROJECT=<project> ERMES_ALLOW_WRITES=1 ermes --json campaigns update <id> ...`
58
+ 3. Never `export ERMES_ALLOW_WRITES` and never run `ermes write allow`;
59
+ nothing should stay unlocked afterwards.
60
+ 4. Read the value again and show the user the result. If a write times out,
61
+ read before retrying; it may have gone through.
62
+
63
+ ## Errors
64
+
65
+ - `401` / no login: the user runs `ermes login`.
66
+ - `403` or a missing project: the user has no access to it, or runs
67
+ `ermes projects refresh` after being added. Do not retry on other projects.
68
+ - A newer version is announced: tell the user to run `ermes update`, then
69
+ `ermes skill install` to refresh this skill.