hermes-plugin-clawrouter 0.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.
Files changed (32) hide show
  1. hermes_plugin_clawrouter-0.1.0/LICENSE +21 -0
  2. hermes_plugin_clawrouter-0.1.0/PKG-INFO +115 -0
  3. hermes_plugin_clawrouter-0.1.0/README.md +85 -0
  4. hermes_plugin_clawrouter-0.1.0/pyproject.toml +63 -0
  5. hermes_plugin_clawrouter-0.1.0/setup.cfg +4 -0
  6. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/__init__.py +122 -0
  7. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/cli.py +457 -0
  8. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/commands.py +85 -0
  9. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/models.py +27 -0
  10. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/plugin.yaml +10 -0
  11. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/provider_template/init.py.tmpl +97 -0
  12. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/provider_template/plugin.yaml +5 -0
  13. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/proxy_supervisor.py +286 -0
  14. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/schemas.py +116 -0
  15. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/skills/clawrouter/SKILL.md +202 -0
  16. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/state.py +111 -0
  17. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/tools.py +145 -0
  18. hermes_plugin_clawrouter-0.1.0/src/clawrouter_hermes/wallet.py +261 -0
  19. hermes_plugin_clawrouter-0.1.0/src/hermes_plugin_clawrouter.egg-info/PKG-INFO +115 -0
  20. hermes_plugin_clawrouter-0.1.0/src/hermes_plugin_clawrouter.egg-info/SOURCES.txt +30 -0
  21. hermes_plugin_clawrouter-0.1.0/src/hermes_plugin_clawrouter.egg-info/dependency_links.txt +1 -0
  22. hermes_plugin_clawrouter-0.1.0/src/hermes_plugin_clawrouter.egg-info/entry_points.txt +5 -0
  23. hermes_plugin_clawrouter-0.1.0/src/hermes_plugin_clawrouter.egg-info/requires.txt +8 -0
  24. hermes_plugin_clawrouter-0.1.0/src/hermes_plugin_clawrouter.egg-info/top_level.txt +1 -0
  25. hermes_plugin_clawrouter-0.1.0/tests/test_commands.py +45 -0
  26. hermes_plugin_clawrouter-0.1.0/tests/test_doctor_checks.py +103 -0
  27. hermes_plugin_clawrouter-0.1.0/tests/test_live_rpc.py +75 -0
  28. hermes_plugin_clawrouter-0.1.0/tests/test_provider_template.py +140 -0
  29. hermes_plugin_clawrouter-0.1.0/tests/test_proxy_supervisor.py +69 -0
  30. hermes_plugin_clawrouter-0.1.0/tests/test_state.py +48 -0
  31. hermes_plugin_clawrouter-0.1.0/tests/test_tools.py +118 -0
  32. hermes_plugin_clawrouter-0.1.0/tests/test_wallet.py +80 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BlockRun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: hermes-plugin-clawrouter
3
+ Version: 0.1.0
4
+ Summary: ClawRouter for Hermes — 55+ models, x402 USDC micropayments, smart routing
5
+ Author: BlockRun
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/BlockRunAI/ClawRouter
8
+ Project-URL: Repository, https://github.com/BlockRunAI/ClawRouter-Hermes
9
+ Project-URL: Bug Tracker, https://github.com/BlockRunAI/ClawRouter-Hermes/issues
10
+ Keywords: hermes,hermes-agent,clawrouter,x402,llm-router,blockrun
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: POSIX
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: bip_utils>=2.9
24
+ Requires-Dist: PyYAML>=6.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
28
+ Requires-Dist: respx>=0.21; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # hermes-plugin-clawrouter
32
+
33
+ ClawRouter for [Hermes](https://github.com/NousResearch/hermes-agent) — 55+ LLMs, x402 USDC micropayments, smart routing.
34
+
35
+ Wraps the existing [ClawRouter](https://github.com/BlockRunAI/ClawRouter) TypeScript proxy as a Hermes plugin. Wallet (BIP-39, Base + Solana), routing (15-dimension scorer), and x402 payment all stay in the canonical TS implementation — this is a thin Python adapter.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install hermes-plugin-clawrouter
41
+ hermes plugins enable clawrouter
42
+ hermes-clawrouter setup
43
+ hermes-clawrouter doctor
44
+ ```
45
+
46
+ `setup` writes the model-provider plugin to `~/.hermes/plugins/model-providers/clawrouter/`, seeds `CLAWROUTER_API_KEY=clawrouter-local` in `~/.hermes/.env`, and registers ClawRouter in `~/.hermes/config.yaml` so Hermes' `/model` picker can show the provider and curated BlockRun chat models.
47
+
48
+ `hermes-clawrouter` is provided because some Hermes releases do not add plugin-defined top-level CLI commands before the plugin is enabled. Once the plugin is loaded, `hermes clawrouter <setup|wallet|doctor|route|stats>` may also be available.
49
+
50
+ ## Usage
51
+
52
+ In a Hermes chat:
53
+
54
+ - Set model to `blockrun/auto` to use ClawRouter's smart routing.
55
+ - `/clawrouter wallet` — address + USDC balance
56
+ - `/clawrouter stats` — proxy usage stats
57
+ - `/clawrouter status` — proxy health
58
+ - `/clawrouter route <eco|auto|premium>` — switch routing profile
59
+
60
+ Tools (callable from chat):
61
+
62
+ - `clawrouter_image_generate` — 55+ models incl. DALL-E 3, Flux, Nano Banana
63
+ - `clawrouter_video_generate` — Seedance, Grok Imagine
64
+ - `clawrouter_web_search` — Exa-powered
65
+
66
+ ## Wallet
67
+
68
+ The plugin **reads** the canonical wallet at `~/.openclaw/blockrun/mnemonic` (24-word BIP-39 phrase, mode 0o600). To create one:
69
+
70
+ ```bash
71
+ npx @blockrun/clawrouter setup
72
+ ```
73
+
74
+ Then fund USDC on Base or Solana — $5 covers thousands of requests, non-custodial. The plugin never writes to the wallet.
75
+
76
+ ### Headless / CI
77
+
78
+ Set `BLOCKRUN_WALLET_KEY=<0x raw EVM hex>` to bypass the mnemonic file (EVM-only — Solana derivation unavailable).
79
+
80
+ ## Environment variables
81
+
82
+ | Variable | Effect |
83
+ |---|---|
84
+ | `CLAWROUTER_PROXY_URL` | Point at an externally-managed proxy (e.g. `https://my-host/v1`). Skips local spawn entirely. |
85
+ | `HERMES_CLAWROUTER_AUTOSPAWN=0` | Disable lazy spawn; require `npx @blockrun/clawrouter` to be running already. |
86
+ | `BLOCKRUN_WALLET_KEY` | Raw EVM hex private key — overrides the mnemonic file. |
87
+ | `CLAWROUTER_ROUTING_PROFILE` | `eco` / `auto` / `premium`. Forwarded to the proxy on spawn. |
88
+
89
+ ## How it works
90
+
91
+ 1. `hermes` starts → the entry-point plugin is loaded → `register(ctx)` wires tools, slash commands, CLI, and the skill.
92
+ 2. `hermes-clawrouter setup` materializes `~/.hermes/plugins/model-providers/clawrouter/{plugin.yaml,__init__.py}` from bundled package data and writes Hermes config/env hints needed by current Hermes provider and gateway model-picker paths.
93
+ 3. Hermes' `providers/__init__.py` discovers the materialized directory and registers the `ClawRouterProfile`, pointing `base_url` at `http://127.0.0.1:<port>/v1`.
94
+ 4. First tool call or chat turn → the supervisor probes `:8402`, spawns `npx -y @blockrun/clawrouter --port <port>` if needed, waits ≤30s for `/v1/models`, then forwards the request.
95
+ 5. A heartbeat thread restarts the subprocess on death (max 3 restarts/min).
96
+
97
+ ## Distribution
98
+
99
+ The Python package ships **both** logical plugins:
100
+
101
+ - **Standalone** plugin (this PyPI entry point): tools, slash commands, CLI, skill.
102
+ - **Model-provider** plugin (materialized into `~/.hermes/plugins/model-providers/clawrouter/` by `hermes clawrouter setup`): `ProviderProfile` registration.
103
+
104
+ This split is required because Hermes' PluginManager (`hermes_cli/plugins.py`) skips `register(ctx)` for `kind: model-provider`, and entry-point plugins always load as `kind: standalone`.
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ pip install -e ".[dev]"
110
+ pytest
111
+ ```
112
+
113
+ ## License
114
+
115
+ MIT. © BlockRun.
@@ -0,0 +1,85 @@
1
+ # hermes-plugin-clawrouter
2
+
3
+ ClawRouter for [Hermes](https://github.com/NousResearch/hermes-agent) — 55+ LLMs, x402 USDC micropayments, smart routing.
4
+
5
+ Wraps the existing [ClawRouter](https://github.com/BlockRunAI/ClawRouter) TypeScript proxy as a Hermes plugin. Wallet (BIP-39, Base + Solana), routing (15-dimension scorer), and x402 payment all stay in the canonical TS implementation — this is a thin Python adapter.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install hermes-plugin-clawrouter
11
+ hermes plugins enable clawrouter
12
+ hermes-clawrouter setup
13
+ hermes-clawrouter doctor
14
+ ```
15
+
16
+ `setup` writes the model-provider plugin to `~/.hermes/plugins/model-providers/clawrouter/`, seeds `CLAWROUTER_API_KEY=clawrouter-local` in `~/.hermes/.env`, and registers ClawRouter in `~/.hermes/config.yaml` so Hermes' `/model` picker can show the provider and curated BlockRun chat models.
17
+
18
+ `hermes-clawrouter` is provided because some Hermes releases do not add plugin-defined top-level CLI commands before the plugin is enabled. Once the plugin is loaded, `hermes clawrouter <setup|wallet|doctor|route|stats>` may also be available.
19
+
20
+ ## Usage
21
+
22
+ In a Hermes chat:
23
+
24
+ - Set model to `blockrun/auto` to use ClawRouter's smart routing.
25
+ - `/clawrouter wallet` — address + USDC balance
26
+ - `/clawrouter stats` — proxy usage stats
27
+ - `/clawrouter status` — proxy health
28
+ - `/clawrouter route <eco|auto|premium>` — switch routing profile
29
+
30
+ Tools (callable from chat):
31
+
32
+ - `clawrouter_image_generate` — 55+ models incl. DALL-E 3, Flux, Nano Banana
33
+ - `clawrouter_video_generate` — Seedance, Grok Imagine
34
+ - `clawrouter_web_search` — Exa-powered
35
+
36
+ ## Wallet
37
+
38
+ The plugin **reads** the canonical wallet at `~/.openclaw/blockrun/mnemonic` (24-word BIP-39 phrase, mode 0o600). To create one:
39
+
40
+ ```bash
41
+ npx @blockrun/clawrouter setup
42
+ ```
43
+
44
+ Then fund USDC on Base or Solana — $5 covers thousands of requests, non-custodial. The plugin never writes to the wallet.
45
+
46
+ ### Headless / CI
47
+
48
+ Set `BLOCKRUN_WALLET_KEY=<0x raw EVM hex>` to bypass the mnemonic file (EVM-only — Solana derivation unavailable).
49
+
50
+ ## Environment variables
51
+
52
+ | Variable | Effect |
53
+ |---|---|
54
+ | `CLAWROUTER_PROXY_URL` | Point at an externally-managed proxy (e.g. `https://my-host/v1`). Skips local spawn entirely. |
55
+ | `HERMES_CLAWROUTER_AUTOSPAWN=0` | Disable lazy spawn; require `npx @blockrun/clawrouter` to be running already. |
56
+ | `BLOCKRUN_WALLET_KEY` | Raw EVM hex private key — overrides the mnemonic file. |
57
+ | `CLAWROUTER_ROUTING_PROFILE` | `eco` / `auto` / `premium`. Forwarded to the proxy on spawn. |
58
+
59
+ ## How it works
60
+
61
+ 1. `hermes` starts → the entry-point plugin is loaded → `register(ctx)` wires tools, slash commands, CLI, and the skill.
62
+ 2. `hermes-clawrouter setup` materializes `~/.hermes/plugins/model-providers/clawrouter/{plugin.yaml,__init__.py}` from bundled package data and writes Hermes config/env hints needed by current Hermes provider and gateway model-picker paths.
63
+ 3. Hermes' `providers/__init__.py` discovers the materialized directory and registers the `ClawRouterProfile`, pointing `base_url` at `http://127.0.0.1:<port>/v1`.
64
+ 4. First tool call or chat turn → the supervisor probes `:8402`, spawns `npx -y @blockrun/clawrouter --port <port>` if needed, waits ≤30s for `/v1/models`, then forwards the request.
65
+ 5. A heartbeat thread restarts the subprocess on death (max 3 restarts/min).
66
+
67
+ ## Distribution
68
+
69
+ The Python package ships **both** logical plugins:
70
+
71
+ - **Standalone** plugin (this PyPI entry point): tools, slash commands, CLI, skill.
72
+ - **Model-provider** plugin (materialized into `~/.hermes/plugins/model-providers/clawrouter/` by `hermes clawrouter setup`): `ProviderProfile` registration.
73
+
74
+ This split is required because Hermes' PluginManager (`hermes_cli/plugins.py`) skips `register(ctx)` for `kind: model-provider`, and entry-point plugins always load as `kind: standalone`.
75
+
76
+ ## Development
77
+
78
+ ```bash
79
+ pip install -e ".[dev]"
80
+ pytest
81
+ ```
82
+
83
+ ## License
84
+
85
+ MIT. © BlockRun.
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hermes-plugin-clawrouter"
7
+ version = "0.1.0"
8
+ description = "ClawRouter for Hermes — 55+ models, x402 USDC micropayments, smart routing"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [
12
+ { name = "BlockRun" }
13
+ ]
14
+ requires-python = ">=3.9"
15
+ keywords = ["hermes", "hermes-agent", "clawrouter", "x402", "llm-router", "blockrun"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: POSIX",
21
+ "Operating System :: MacOS",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ ]
26
+ dependencies = [
27
+ "httpx>=0.27",
28
+ "bip_utils>=2.9",
29
+ "PyYAML>=6.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=7",
35
+ "pytest-asyncio>=0.23",
36
+ "respx>=0.21",
37
+ ]
38
+
39
+ [project.entry-points."hermes_agent.plugins"]
40
+ clawrouter = "clawrouter_hermes"
41
+
42
+ [project.scripts]
43
+ hermes-clawrouter = "clawrouter_hermes.cli:main"
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/BlockRunAI/ClawRouter"
47
+ Repository = "https://github.com/BlockRunAI/ClawRouter-Hermes"
48
+ "Bug Tracker" = "https://github.com/BlockRunAI/ClawRouter-Hermes/issues"
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+
53
+ [tool.setuptools.package-data]
54
+ clawrouter_hermes = [
55
+ "plugin.yaml",
56
+ "provider_template/*.yaml",
57
+ "provider_template/*.tmpl",
58
+ "skills/clawrouter/*.md",
59
+ ]
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
63
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,122 @@
1
+ """ClawRouter Hermes plugin — entry point.
2
+
3
+ Hermes' PluginManager discovers this module via the ``hermes_agent.plugins``
4
+ entry-point group and calls :func:`register` once at startup. We register:
5
+
6
+ - Tools (image / video / web_search) forwarded to the local ClawRouter proxy
7
+ - A single slash command ``/clawrouter`` with several subcommands
8
+ - A CLI subcommand ``hermes clawrouter <setup|wallet|doctor|route|stats>``
9
+ - A read-only skill ``clawrouter:guide``
10
+
11
+ The model-provider half (``ProviderProfile`` registration) is NOT done here
12
+ because Hermes loads model-provider plugins from
13
+ ``~/.hermes/plugins/model-providers/<name>/`` only, not from entry-point
14
+ plugins. ``hermes clawrouter setup`` materializes that directory from the
15
+ bundled ``provider_template/`` resources.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from pathlib import Path
22
+
23
+ from . import cli as _cli
24
+ from . import commands, proxy_supervisor, schemas, tools
25
+
26
+ __all__ = ["register"]
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ _VERSION = "0.1.0"
31
+
32
+
33
+ def register(ctx) -> None:
34
+ """Wire all surfaces into the Hermes plugin context."""
35
+ _install_compat()
36
+ _register_tools(ctx)
37
+ _register_slash_command(ctx)
38
+ _register_cli(ctx)
39
+ _register_skill(ctx)
40
+ # Best-effort, non-blocking probe so users see whether the proxy is up
41
+ # without paying spawn latency at startup.
42
+ try:
43
+ status = proxy_supervisor.ensure_running(autospawn=False)
44
+ if status.reachable:
45
+ logger.info("clawrouter: proxy reachable at %s", status.base_url)
46
+ else:
47
+ logger.debug("clawrouter: proxy not yet running (will spawn on first use)")
48
+ except Exception as exc:
49
+ logger.debug("clawrouter: startup probe failed: %s", exc)
50
+
51
+
52
+ def _install_compat() -> None:
53
+ """Best-effort setup for Hermes versions that need provider/config hints."""
54
+ try:
55
+ _cli.install_hermes_compat()
56
+ _cli.patch_hermes_model_catalog()
57
+ except Exception as exc:
58
+ logger.debug("clawrouter: compatibility setup skipped: %s", exc)
59
+
60
+
61
+ def _register_tools(ctx) -> None:
62
+ ctx.register_tool(
63
+ name="clawrouter_image_generate",
64
+ toolset="clawrouter",
65
+ schema=schemas.IMAGE_GENERATE,
66
+ handler=tools.image_generate,
67
+ description="Generate images via ClawRouter (x402-billed)",
68
+ emoji="🎨",
69
+ )
70
+ ctx.register_tool(
71
+ name="clawrouter_video_generate",
72
+ toolset="clawrouter",
73
+ schema=schemas.VIDEO_GENERATE,
74
+ handler=tools.video_generate,
75
+ description="Generate short videos via ClawRouter (x402-billed)",
76
+ emoji="🎬",
77
+ )
78
+ ctx.register_tool(
79
+ name="clawrouter_web_search",
80
+ toolset="clawrouter",
81
+ schema=schemas.WEB_SEARCH,
82
+ handler=tools.web_search,
83
+ description="Web search via ClawRouter Exa (x402-billed)",
84
+ emoji="🔎",
85
+ )
86
+
87
+
88
+ def _register_slash_command(ctx) -> None:
89
+ ctx.register_command(
90
+ name="clawrouter",
91
+ handler=commands.clawrouter_dispatch,
92
+ description="ClawRouter wallet / stats / routing controls",
93
+ args_hint="<wallet|stats|status|route|help>",
94
+ )
95
+
96
+
97
+ def _register_cli(ctx) -> None:
98
+ ctx.register_cli_command(
99
+ name="clawrouter",
100
+ help="ClawRouter setup, wallet, doctor, routing",
101
+ setup_fn=_cli.register_cli,
102
+ handler_fn=_cli.clawrouter_command,
103
+ description=(
104
+ "Manage the ClawRouter for Hermes plugin — install the model-provider "
105
+ "plugin, inspect the wallet, diagnose health, and set the routing profile."
106
+ ),
107
+ )
108
+
109
+
110
+ def _register_skill(ctx) -> None:
111
+ skill_path = Path(__file__).parent / "skills" / "clawrouter" / "SKILL.md"
112
+ if not skill_path.exists():
113
+ logger.debug("clawrouter: skill file missing at %s", skill_path)
114
+ return
115
+ try:
116
+ ctx.register_skill(
117
+ name="guide",
118
+ path=skill_path,
119
+ description="ClawRouter usage guide — models, pricing, wallet, slash commands",
120
+ )
121
+ except Exception as exc:
122
+ logger.debug("clawrouter: skill registration failed: %s", exc)