ermes-cli 1.0.0__tar.gz → 1.1.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 → ermes_cli-1.1.0}/PKG-INFO +40 -43
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/README.md +38 -40
- ermes_cli-1.1.0/ermes_cli/__init__.py +1 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/auth_cmds.py +1 -1
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/projects.py +1 -1
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/config.py +1 -1
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/main.py +48 -0
- ermes_cli-1.1.0/ermes_cli/update.py +225 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/write_mode.py +1 -5
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/pyproject.toml +2 -7
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/scripts/install-cli.sh +1 -11
- ermes_cli-1.1.0/tests/test_update.py +366 -0
- ermes_cli-1.0.0/ermes_cli/__init__.py +0 -1
- ermes_cli-1.0.0/ermes_cli/manifest.py +0 -218
- ermes_cli-1.0.0/ermes_cli/mcp_server.py +0 -167
- ermes_cli-1.0.0/ermes_cli/routes.yaml +0 -163
- ermes_cli-1.0.0/scripts/install-mcp.sh +0 -348
- ermes_cli-1.0.0/tests/test_install_mcp_script.py +0 -256
- ermes_cli-1.0.0/tests/test_install_mcp_uninstall.py +0 -168
- ermes_cli-1.0.0/tests/test_manifest.py +0 -229
- ermes_cli-1.0.0/tests/test_mcp_server.py +0 -160
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/.gitignore +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/admin_api.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/auth0.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/client.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/__init__.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/campaigns.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/config_cmd.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/data_paths.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/export.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/ingest.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/journeys.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/keys.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/lists.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/metrics.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/providers.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/subscriptions.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/tags.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/templates.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/users.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/commands/write_cmds.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/errors.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/export_utils.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/keyring_store.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/output.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/ermes_cli/rules.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/__init__.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/conftest.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_admin_api.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_auth0.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_auth_cmds.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_client.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/__init__.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_campaigns.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_data_paths.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_export.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_ingest.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_journeys.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_keys.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_lists.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_metrics.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_projects.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_providers.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_subscriptions.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_tags.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_templates.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_commands/test_users.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_config.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_export_utils.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_keyring_store.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_output.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_projects_config.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_rules.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_version.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_whoami.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.0}/tests/test_write_cmds.py +0 -0
- {ermes_cli-1.0.0 → ermes_cli-1.1.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.
|
|
4
|
-
Summary: CLI-first access to Ermes marketing automation
|
|
3
|
+
Version: 1.1.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,12 @@ 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
|
|
30
|
+
# Ermes CLI
|
|
32
31
|
|
|
33
|
-
This package gives Ermes operators a CLI
|
|
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-
|
|
34
|
+
Version 1.1.0. It removed the `ermes-mcp` compatibility server that 1.0.0 shipped (DEV-2326); a
|
|
35
|
+
machine that registered it must drop that registration (e.g. `claude mcp remove ermes`).
|
|
38
36
|
|
|
39
37
|
## Install the CLI
|
|
40
38
|
|
|
@@ -55,22 +53,44 @@ Releases are cut by pushing a tag `ermes-cli-vX.Y.Z` equal to `cli/pyproject.tom
|
|
|
55
53
|
(`.github/workflows/publish-cli.yml` runs the tests, builds the wheel, publishes to PyPI and attaches
|
|
56
54
|
it to a GitHub Release). `install-cli.sh` is idempotent and safe to re-run for upgrades.
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
explicit project:
|
|
56
|
+
## Update the CLI
|
|
60
57
|
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
**Automatic prompt.** In an interactive terminal, the CLI checks PyPI for a newer `ermes-cli` at
|
|
59
|
+
most once every 24 hours (2-second timeout, cached in `~/.ermes/update-check.json`; failed checks
|
|
60
|
+
are cached too, so an offline laptop is not slowed down). When one exists it asks before running
|
|
61
|
+
your command:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
ermes-cli 1.2.0 is available (you have 1.1.0). Update now? [Y]es / [n]ot now / [s]kip this version
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
- **Yes** (Enter): runs `pipx upgrade ermes-cli`, checks the new version in a fresh process, then
|
|
68
|
+
re-runs your command on it.
|
|
69
|
+
- **Not now**: runs your command; asks again on the next run.
|
|
70
|
+
- **Skip this version**: never asks about that version again; a newer release asks once more.
|
|
71
|
+
|
|
72
|
+
A failed upgrade or Ctrl-C at the prompt prints one line and runs your command on the current
|
|
73
|
+
version. The check never runs with `--json`, when `CI` is set, when stdin/stdout/stderr is not a
|
|
74
|
+
TTY, before `ermes version` / `ermes update`, or with `ERMES_NO_UPDATE_CHECK=1`.
|
|
75
|
+
|
|
76
|
+
**Manual.**
|
|
67
77
|
|
|
68
78
|
```bash
|
|
69
|
-
|
|
79
|
+
ermes update # check, then ask before upgrading
|
|
80
|
+
ermes update --check # report only, never install
|
|
81
|
+
ermes update --yes # upgrade without asking
|
|
82
|
+
ermes --json update [--yes] # --json installs only with --yes
|
|
70
83
|
```
|
|
71
84
|
|
|
72
|
-
|
|
73
|
-
`
|
|
85
|
+
**Status values** (`--json` `status`): `up_to_date`, `ahead`, `update_available`, `updated`,
|
|
86
|
+
`lookup_failed` (exit 1), `update_failed` (exit 1). Installer output goes to stderr; stdout stays
|
|
87
|
+
one JSON document.
|
|
88
|
+
|
|
89
|
+
**Supported install.** Only the pipx install of the PyPI package (`install-cli.sh`, or
|
|
90
|
+
`pipx install ermes-cli`) upgrades itself. `pipx upgrade` keeps the old venv if pip fails, so a
|
|
91
|
+
failed upgrade never leaves you without a CLI. Editable checkouts (`git pull`), git or local-dir
|
|
92
|
+
installs, non-pipx environments and an `ermes` that is not the venv pipx manages get the manual
|
|
93
|
+
command instead of a prompt. Keychain tokens and `~/.ermes` are untouched.
|
|
74
94
|
|
|
75
95
|
## Auth — Auth0 device login (recommended)
|
|
76
96
|
|
|
@@ -161,8 +181,8 @@ State lives in `~/.ermes/state.yml` (chmod 600), separate from `projects.yml`
|
|
|
161
181
|
so refreshing projects doesn't clobber the write window. `ermes whoami` shows
|
|
162
182
|
the current write status.
|
|
163
183
|
|
|
164
|
-
|
|
165
|
-
|
|
184
|
+
Every agent write must carry explicit project scope and one-process `ERMES_ALLOW_WRITES=1`
|
|
185
|
+
authorization.
|
|
166
186
|
|
|
167
187
|
## Metrics
|
|
168
188
|
|
|
@@ -243,35 +263,12 @@ is legible.
|
|
|
243
263
|
|
|
244
264
|
Full surface: `ermes --help`.
|
|
245
265
|
|
|
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
266
|
## Where in the wider stack
|
|
269
267
|
|
|
270
268
|
Ermes is a CLI-first integration in the
|
|
271
269
|
[`Martech-DM/martech-mcps`](https://github.com/Martech-DM/martech-mcps) registry.
|
|
272
270
|
That repo's `ermes/README.md` and `ermes/skills/ermes-ops/SKILL.md` are the
|
|
273
|
-
cross-fleet setup and operating runbooks.
|
|
274
|
-
unregistered during normal operation.
|
|
271
|
+
cross-fleet setup and operating runbooks.
|
|
275
272
|
|
|
276
273
|
## CLI source history
|
|
277
274
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
# Ermes CLI
|
|
1
|
+
# Ermes CLI
|
|
2
2
|
|
|
3
|
-
This package gives Ermes operators a CLI
|
|
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-
|
|
5
|
+
Version 1.1.0. It removed the `ermes-mcp` compatibility server that 1.0.0 shipped (DEV-2326); a
|
|
6
|
+
machine that registered it must drop that registration (e.g. `claude mcp remove ermes`).
|
|
8
7
|
|
|
9
8
|
## Install the CLI
|
|
10
9
|
|
|
@@ -25,22 +24,44 @@ Releases are cut by pushing a tag `ermes-cli-vX.Y.Z` equal to `cli/pyproject.tom
|
|
|
25
24
|
(`.github/workflows/publish-cli.yml` runs the tests, builds the wheel, publishes to PyPI and attaches
|
|
26
25
|
it to a GitHub Release). `install-cli.sh` is idempotent and safe to re-run for upgrades.
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
explicit project:
|
|
27
|
+
## Update the CLI
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
**Automatic prompt.** In an interactive terminal, the CLI checks PyPI for a newer `ermes-cli` at
|
|
30
|
+
most once every 24 hours (2-second timeout, cached in `~/.ermes/update-check.json`; failed checks
|
|
31
|
+
are cached too, so an offline laptop is not slowed down). When one exists it asks before running
|
|
32
|
+
your command:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
ermes-cli 1.2.0 is available (you have 1.1.0). Update now? [Y]es / [n]ot now / [s]kip this version
|
|
33
36
|
```
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
- **Yes** (Enter): runs `pipx upgrade ermes-cli`, checks the new version in a fresh process, then
|
|
39
|
+
re-runs your command on it.
|
|
40
|
+
- **Not now**: runs your command; asks again on the next run.
|
|
41
|
+
- **Skip this version**: never asks about that version again; a newer release asks once more.
|
|
42
|
+
|
|
43
|
+
A failed upgrade or Ctrl-C at the prompt prints one line and runs your command on the current
|
|
44
|
+
version. The check never runs with `--json`, when `CI` is set, when stdin/stdout/stderr is not a
|
|
45
|
+
TTY, before `ermes version` / `ermes update`, or with `ERMES_NO_UPDATE_CHECK=1`.
|
|
46
|
+
|
|
47
|
+
**Manual.**
|
|
37
48
|
|
|
38
49
|
```bash
|
|
39
|
-
|
|
50
|
+
ermes update # check, then ask before upgrading
|
|
51
|
+
ermes update --check # report only, never install
|
|
52
|
+
ermes update --yes # upgrade without asking
|
|
53
|
+
ermes --json update [--yes] # --json installs only with --yes
|
|
40
54
|
```
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
`
|
|
56
|
+
**Status values** (`--json` `status`): `up_to_date`, `ahead`, `update_available`, `updated`,
|
|
57
|
+
`lookup_failed` (exit 1), `update_failed` (exit 1). Installer output goes to stderr; stdout stays
|
|
58
|
+
one JSON document.
|
|
59
|
+
|
|
60
|
+
**Supported install.** Only the pipx install of the PyPI package (`install-cli.sh`, or
|
|
61
|
+
`pipx install ermes-cli`) upgrades itself. `pipx upgrade` keeps the old venv if pip fails, so a
|
|
62
|
+
failed upgrade never leaves you without a CLI. Editable checkouts (`git pull`), git or local-dir
|
|
63
|
+
installs, non-pipx environments and an `ermes` that is not the venv pipx manages get the manual
|
|
64
|
+
command instead of a prompt. Keychain tokens and `~/.ermes` are untouched.
|
|
44
65
|
|
|
45
66
|
## Auth — Auth0 device login (recommended)
|
|
46
67
|
|
|
@@ -131,8 +152,8 @@ State lives in `~/.ermes/state.yml` (chmod 600), separate from `projects.yml`
|
|
|
131
152
|
so refreshing projects doesn't clobber the write window. `ermes whoami` shows
|
|
132
153
|
the current write status.
|
|
133
154
|
|
|
134
|
-
|
|
135
|
-
|
|
155
|
+
Every agent write must carry explicit project scope and one-process `ERMES_ALLOW_WRITES=1`
|
|
156
|
+
authorization.
|
|
136
157
|
|
|
137
158
|
## Metrics
|
|
138
159
|
|
|
@@ -213,35 +234,12 @@ is legible.
|
|
|
213
234
|
|
|
214
235
|
Full surface: `ermes --help`.
|
|
215
236
|
|
|
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
237
|
## Where in the wider stack
|
|
239
238
|
|
|
240
239
|
Ermes is a CLI-first integration in the
|
|
241
240
|
[`Martech-DM/martech-mcps`](https://github.com/Martech-DM/martech-mcps) registry.
|
|
242
241
|
That repo's `ermes/README.md` and `ermes/skills/ermes-ops/SKILL.md` are the
|
|
243
|
-
cross-fleet setup and operating runbooks.
|
|
244
|
-
unregistered during normal operation.
|
|
242
|
+
cross-fleet setup and operating runbooks.
|
|
245
243
|
|
|
246
244
|
## CLI source history
|
|
247
245
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.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
|
|
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
|
|
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.")
|
|
@@ -304,7 +304,7 @@ def get_auth0_email() -> str | None:
|
|
|
304
304
|
|
|
305
305
|
|
|
306
306
|
# ---------------------------------------------------------------------------
|
|
307
|
-
# Profile resolution — what the CLI
|
|
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(
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Newer-version check, upgrade prompt and `ermes update` (DEV-1972).
|
|
2
|
+
|
|
3
|
+
ermes-cli ships to PyPI on `ermes-cli-v*` tags (DEV-2262), so "latest" is PyPI's current
|
|
4
|
+
`ermes-cli` release: a public JSON endpoint, no GitHub or Ermes credentials. Interactive runs check
|
|
5
|
+
at most once a day and ask before upgrading, like Codex / Claude Code. Only the pipx install made by
|
|
6
|
+
`install-cli.sh` is upgraded in place (`pipx upgrade`); anything else gets the manual command.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from importlib import metadata
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
import typer
|
|
24
|
+
|
|
25
|
+
from ermes_cli import __version__
|
|
26
|
+
|
|
27
|
+
PACKAGE = "ermes-cli"
|
|
28
|
+
PYPI_URL = f"https://pypi.org/pypi/{PACKAGE}/json"
|
|
29
|
+
|
|
30
|
+
CACHE_FILE = "update-check.json"
|
|
31
|
+
CHECK_INTERVAL_S = 24 * 3600
|
|
32
|
+
AUTO_TIMEOUT_S = 2.0
|
|
33
|
+
EXPLICIT_TIMEOUT_S = 10.0
|
|
34
|
+
OPT_OUT_ENV = "ERMES_NO_UPDATE_CHECK"
|
|
35
|
+
# Never prompt before these: `version` must stay a pure read, `update` does its own check.
|
|
36
|
+
SKIP_COMMANDS = {"update", "version"}
|
|
37
|
+
|
|
38
|
+
MANUAL_INSTALL = f"Upgrade manually: `pipx upgrade {PACKAGE}`, or re-run cli/scripts/install-cli.sh."
|
|
39
|
+
|
|
40
|
+
_SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UpdateError(Exception):
|
|
44
|
+
"""Lookup, environment or install failure. The message is shown to the operator."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_version(value: str | None) -> tuple[int, int, int] | None:
|
|
48
|
+
"""MAJOR.MINOR.PATCH -> int tuple; anything else (pre-release, junk, None) -> None."""
|
|
49
|
+
m = _SEMVER_RE.match((value or "").strip())
|
|
50
|
+
return (int(m[1]), int(m[2]), int(m[3])) if m else None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_newer(latest: str | None, current: str | None) -> bool:
|
|
54
|
+
"""True only when both parse and latest > current. Malformed never counts as newer."""
|
|
55
|
+
a, b = parse_version(latest), parse_version(current)
|
|
56
|
+
return a is not None and b is not None and a > b
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def fetch_latest(timeout: float = EXPLICIT_TIMEOUT_S) -> str:
|
|
60
|
+
"""PyPI's current release. `info.version` already excludes yanked and pre-releases."""
|
|
61
|
+
try:
|
|
62
|
+
r = httpx.get(PYPI_URL, timeout=timeout, follow_redirects=True)
|
|
63
|
+
r.raise_for_status()
|
|
64
|
+
version = r.json()["info"]["version"]
|
|
65
|
+
except httpx.HTTPStatusError as e:
|
|
66
|
+
raise UpdateError(f"PyPI lookup failed: HTTP {e.response.status_code}") from e
|
|
67
|
+
except httpx.HTTPError as e:
|
|
68
|
+
raise UpdateError(f"PyPI lookup failed: {e.__class__.__name__}") from e
|
|
69
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
70
|
+
raise UpdateError("PyPI returned an unexpected response") from e
|
|
71
|
+
if parse_version(version) is None:
|
|
72
|
+
raise UpdateError(f"PyPI's latest {PACKAGE} is not MAJOR.MINOR.PATCH: {version!r}")
|
|
73
|
+
return version
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------- cache
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _cache_path() -> Path:
|
|
80
|
+
from ermes_cli.config import _config_dir
|
|
81
|
+
|
|
82
|
+
return _config_dir() / CACHE_FILE
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def read_cache() -> dict:
|
|
86
|
+
try:
|
|
87
|
+
data = json.loads(_cache_path().read_text())
|
|
88
|
+
return data if isinstance(data, dict) else {}
|
|
89
|
+
except (OSError, ValueError):
|
|
90
|
+
return {}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def write_cache(**fields) -> None:
|
|
94
|
+
"""Merge into the cache. Failed lookups are cached too, so an offline laptop backs off."""
|
|
95
|
+
try:
|
|
96
|
+
path = _cache_path()
|
|
97
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
path.write_text(json.dumps({**read_cache(), **fields}))
|
|
99
|
+
except OSError:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------- install detection + upgrade
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class Install:
|
|
108
|
+
venv: Path
|
|
109
|
+
pipx: str
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def detect_install() -> Install:
|
|
113
|
+
"""Prove the RUNNING CLI is the pipx venv `pipx upgrade ermes-cli` would touch, else refuse."""
|
|
114
|
+
try:
|
|
115
|
+
raw = metadata.distribution(PACKAGE).read_text("direct_url.json")
|
|
116
|
+
except metadata.PackageNotFoundError as e:
|
|
117
|
+
raise UpdateError(f"{PACKAGE} is not installed as a package. {MANUAL_INSTALL}") from e
|
|
118
|
+
if raw: # PyPI installs carry no direct_url.json; git, local-dir and editable installs do.
|
|
119
|
+
du = json.loads(raw)
|
|
120
|
+
if du.get("dir_info", {}).get("editable"):
|
|
121
|
+
raise UpdateError(f"this ermes runs from an editable checkout ({du.get('url')}); update it with git pull.")
|
|
122
|
+
raise UpdateError(f"this ermes was installed from {du.get('url')}, not PyPI. {MANUAL_INSTALL}")
|
|
123
|
+
|
|
124
|
+
venv = Path(sys.prefix)
|
|
125
|
+
if not (venv / "pipx_metadata.json").is_file():
|
|
126
|
+
raise UpdateError(f"this ermes is not a pipx install ({venv}). {MANUAL_INSTALL}")
|
|
127
|
+
pipx = shutil.which("pipx")
|
|
128
|
+
if not pipx:
|
|
129
|
+
raise UpdateError(f"pipx is not on PATH. {MANUAL_INSTALL}")
|
|
130
|
+
out = subprocess.run(
|
|
131
|
+
[pipx, "environment", "--value", "PIPX_LOCAL_VENVS"],
|
|
132
|
+
capture_output=True, text=True, timeout=30, check=False,
|
|
133
|
+
)
|
|
134
|
+
expected = Path(out.stdout.strip() or Path.home() / ".local/pipx/venvs") / PACKAGE
|
|
135
|
+
if not expected.exists() or not expected.samefile(venv):
|
|
136
|
+
raise UpdateError(
|
|
137
|
+
f"the running ermes ({venv}) is not the venv pipx manages ({expected}); "
|
|
138
|
+
"refusing to update a different install."
|
|
139
|
+
)
|
|
140
|
+
return Install(venv=venv, pipx=pipx)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def upgrade(inst: Install, target: str) -> str:
|
|
144
|
+
"""`pipx upgrade`, then check a FRESH process reports `target`; raise otherwise.
|
|
145
|
+
|
|
146
|
+
`pipx upgrade` keeps the old venv when pip fails, so a failed upgrade never leaves the operator
|
|
147
|
+
without a CLI (the uninstall-first trap DEV-2070 describes for install-cli.sh).
|
|
148
|
+
"""
|
|
149
|
+
# Installer output goes to stderr so `--json` stdout stays one parseable document.
|
|
150
|
+
subprocess.run([inst.pipx, "upgrade", PACKAGE], stdout=sys.stderr, stderr=sys.stderr, check=False)
|
|
151
|
+
out = subprocess.run(
|
|
152
|
+
[str(inst.venv / "bin" / "ermes"), "--json", "version"],
|
|
153
|
+
capture_output=True, text=True, timeout=60, check=False,
|
|
154
|
+
env={**os.environ, OPT_OUT_ENV: "1"},
|
|
155
|
+
)
|
|
156
|
+
try:
|
|
157
|
+
installed = json.loads(out.stdout)["version"]
|
|
158
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
159
|
+
raise UpdateError(
|
|
160
|
+
f"the upgraded ermes did not report a version (exit {out.returncode}). {MANUAL_INSTALL}"
|
|
161
|
+
) from e
|
|
162
|
+
if installed != target:
|
|
163
|
+
raise UpdateError(f"upgrade left {installed} installed, expected {target}. {MANUAL_INSTALL}")
|
|
164
|
+
return installed
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------- automatic prompt
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def auto_check_enabled(use_json: bool, command: str | None) -> bool:
|
|
171
|
+
"""Interactive humans only: never in --json, CI, pipes, scripts, or with the opt-out set."""
|
|
172
|
+
if use_json or command in SKIP_COMMANDS or os.environ.get(OPT_OUT_ENV) or os.environ.get("CI"):
|
|
173
|
+
return False
|
|
174
|
+
return sys.stdin.isatty() and sys.stdout.isatty() and sys.stderr.isatty()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _cached_latest() -> str | None:
|
|
178
|
+
cache = read_cache()
|
|
179
|
+
if time.time() - float(cache.get("checked_at") or 0) < CHECK_INTERVAL_S:
|
|
180
|
+
return cache.get("latest")
|
|
181
|
+
try:
|
|
182
|
+
latest = fetch_latest(timeout=AUTO_TIMEOUT_S)
|
|
183
|
+
except UpdateError as e:
|
|
184
|
+
write_cache(checked_at=time.time(), latest=None, error=str(e))
|
|
185
|
+
return None
|
|
186
|
+
write_cache(checked_at=time.time(), latest=latest, error=None)
|
|
187
|
+
return latest
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def maybe_prompt(use_json: bool, command: str | None) -> None:
|
|
191
|
+
"""Ask (at most daily) whether to upgrade; on yes, upgrade and re-run the command on the new code.
|
|
192
|
+
|
|
193
|
+
Never raises: a broken check must not break the command the operator actually asked for.
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
if not auto_check_enabled(use_json, command):
|
|
197
|
+
return
|
|
198
|
+
latest = _cached_latest()
|
|
199
|
+
if not is_newer(latest, __version__) or read_cache().get("skipped") == latest:
|
|
200
|
+
return
|
|
201
|
+
try:
|
|
202
|
+
inst = detect_install()
|
|
203
|
+
except UpdateError as e:
|
|
204
|
+
typer.echo(f"ermes-cli {latest} is available (you have {__version__}). {e}", err=True)
|
|
205
|
+
return
|
|
206
|
+
# typer >= 0.24 vendors click, so there is no Choice type: anything but y/s means "not now".
|
|
207
|
+
choice = typer.prompt(
|
|
208
|
+
f"ermes-cli {latest} is available (you have {__version__}). "
|
|
209
|
+
"Update now? [Y]es / [n]ot now / [s]kip this version",
|
|
210
|
+
default="y", show_default=False, err=True,
|
|
211
|
+
).strip().lower()[:1]
|
|
212
|
+
if choice == "s":
|
|
213
|
+
write_cache(skipped=latest)
|
|
214
|
+
return
|
|
215
|
+
if choice != "y":
|
|
216
|
+
return
|
|
217
|
+
upgrade(inst, latest)
|
|
218
|
+
typer.echo(f"Updated ermes-cli to {latest}. Re-running your command...", err=True)
|
|
219
|
+
os.execv(str(inst.venv / "bin" / "ermes"), ["ermes", *sys.argv[1:]])
|
|
220
|
+
except typer.Abort: # Ctrl-C / EOF at the prompt: carry on with the command
|
|
221
|
+
typer.echo("", err=True)
|
|
222
|
+
except UpdateError as e:
|
|
223
|
+
typer.echo(f"Update failed, continuing on {__version__}: {e}", err=True)
|
|
224
|
+
except Exception: # noqa: BLE001 - the check must never break the real command
|
|
225
|
+
return
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Write-mode guard: block mutating CLI
|
|
1
|
+
"""Write-mode guard: block mutating CLI calls by default.
|
|
2
2
|
|
|
3
3
|
Why: most CLI sessions are read-only investigation ("what campaigns ran
|
|
4
4
|
last week?"). The mutating commands (`campaigns create`, `lists delete`,
|
|
@@ -20,10 +20,6 @@ Behavior:
|
|
|
20
20
|
State lives in `~/.ermes/state.yml` (chmod 600). It's deliberately
|
|
21
21
|
separate from projects.yml so refreshing projects doesn't accidentally
|
|
22
22
|
clobber the write window.
|
|
23
|
-
|
|
24
|
-
The MCP server's tool set is read-only by design (see PR #395 TODO), so
|
|
25
|
-
this guard primarily protects the `ermes` CLI surface. When MCP gains
|
|
26
|
-
mutating tools (v0.4), they'll be gated through the same helper.
|
|
27
23
|
"""
|
|
28
24
|
|
|
29
25
|
from __future__ import annotations
|
|
@@ -5,8 +5,8 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ermes-cli"
|
|
7
7
|
# Keep in lockstep with ermes_cli/__init__.py::__version__ — tests/test_version.py fails if they drift.
|
|
8
|
-
version = "1.
|
|
9
|
-
description = "CLI-first access to Ermes marketing automation
|
|
8
|
+
version = "1.1.0"
|
|
9
|
+
description = "CLI-first access to Ermes marketing automation with Auth0 multi-project auth."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
license = { text = "Proprietary" }
|
|
@@ -25,7 +25,6 @@ dependencies = [
|
|
|
25
25
|
"httpx>=0.28",
|
|
26
26
|
"rich>=14.0",
|
|
27
27
|
"pyyaml>=6.0",
|
|
28
|
-
"fastmcp>=2.0",
|
|
29
28
|
"keyring>=24.0",
|
|
30
29
|
"requests>=2.31",
|
|
31
30
|
# zoneinfo data — guarantees `projects create --timezone` validation works on
|
|
@@ -36,7 +35,6 @@ dependencies = [
|
|
|
36
35
|
|
|
37
36
|
[project.scripts]
|
|
38
37
|
ermes = "ermes_cli.main:app"
|
|
39
|
-
ermes-mcp = "ermes_cli.mcp_server:main"
|
|
40
38
|
|
|
41
39
|
[project.urls]
|
|
42
40
|
Homepage = "https://ermes.publinker.com"
|
|
@@ -55,6 +53,3 @@ exclude = ["uv.lock"]
|
|
|
55
53
|
|
|
56
54
|
[tool.hatch.build.targets.wheel]
|
|
57
55
|
packages = ["ermes_cli"]
|
|
58
|
-
|
|
59
|
-
[tool.hatch.build.targets.wheel.force-include]
|
|
60
|
-
"ermes_cli/routes.yaml" = "ermes_cli/routes.yaml"
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# One-shot installer for the
|
|
3
|
-
# The package includes an optional compatibility MCP, but this script never
|
|
4
|
-
# registers it.
|
|
2
|
+
# One-shot installer for the Ermes CLI.
|
|
5
3
|
#
|
|
6
4
|
# Usage:
|
|
7
5
|
# ./cli/scripts/install-cli.sh # install latest release from PyPI + verify
|
|
@@ -121,15 +119,12 @@ fi
|
|
|
121
119
|
step "Verifying"
|
|
122
120
|
ERMES_VENV="$(ermes_venv_dir)"
|
|
123
121
|
ERMES_BIN="$ERMES_VENV/bin/ermes"
|
|
124
|
-
ERMES_MCP_BIN="$ERMES_VENV/bin/ermes-mcp"
|
|
125
122
|
|
|
126
123
|
[ -x "$ERMES_BIN" ] || die "ermes binary missing from pipx venv $ERMES_VENV."
|
|
127
|
-
[ -x "$ERMES_MCP_BIN" ] || die "ermes-mcp binary missing from pipx venv $ERMES_VENV."
|
|
128
124
|
|
|
129
125
|
# Typer apps expose a `version` subcommand (no --version flag).
|
|
130
126
|
VERSION_OUTPUT="$("$ERMES_BIN" version 2>&1)" || die "ermes version failed: $VERSION_OUTPUT"
|
|
131
127
|
ok "$VERSION_OUTPUT"
|
|
132
|
-
ok "ermes-mcp at $ERMES_MCP_BIN"
|
|
133
128
|
|
|
134
129
|
PATH_ERMES="$(command -v ermes || true)"
|
|
135
130
|
if [ -z "$PATH_ERMES" ]; then
|
|
@@ -162,10 +157,5 @@ else
|
|
|
162
157
|
ermes whoami --check proves the selected key against the API. CI or a box
|
|
163
158
|
without a browser: ERMES_API_KEY=sk_... ermes --json campaigns list.
|
|
164
159
|
|
|
165
|
-
The optional compatibility MCP is not registered by default. If a temporary
|
|
166
|
-
rollback needs its 10 fixed schemas, pin its project at registration:
|
|
167
|
-
|
|
168
|
-
./cli/scripts/install-mcp.sh --project=<project-name-or-id>
|
|
169
|
-
|
|
170
160
|
EOF
|
|
171
161
|
fi
|