agent-circus 0.1.0__py3-none-any.whl

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 (37) hide show
  1. agent_circus/__init__.py +5 -0
  2. agent_circus/agent_config.py +357 -0
  3. agent_circus/cli.py +73 -0
  4. agent_circus/commands/__init__.py +5 -0
  5. agent_circus/commands/build.py +83 -0
  6. agent_circus/commands/config_store.py +104 -0
  7. agent_circus/commands/destroy.py +123 -0
  8. agent_circus/commands/exec_.py +140 -0
  9. agent_circus/commands/init.py +498 -0
  10. agent_circus/commands/ps.py +113 -0
  11. agent_circus/commands/remove.py +127 -0
  12. agent_circus/commands/up.py +86 -0
  13. agent_circus/compose.py +481 -0
  14. agent_circus/config.py +1478 -0
  15. agent_circus/context.py +599 -0
  16. agent_circus/exceptions.py +29 -0
  17. agent_circus/mcp.py +164 -0
  18. agent_circus/runtime.py +109 -0
  19. agent_circus/state.py +343 -0
  20. agent_circus/templates/__init__.py +113 -0
  21. agent_circus/templates/agent-circus/Dockerfile +164 -0
  22. agent_circus/templates/agent-circus/compose.yaml +92 -0
  23. agent_circus/templates/agent-circus/docker-entrypoint.sh +57 -0
  24. agent_circus/templates/agent-circus/hooks/base-root.sh +3 -0
  25. agent_circus/templates/agent-circus/hooks/base-user.sh +3 -0
  26. agent_circus/templates/agent-circus/init-firewall.sh +147 -0
  27. agent_circus/templates/agent-circus/install-ca-certs.sh +9 -0
  28. agent_circus/templates/agent-circus/pyproject.toml +16 -0
  29. agent_circus/templates/agent-circus/setup-claude-mem.sh +24 -0
  30. agent_circus/templates/agent-circus/uv.lock +1531 -0
  31. agent_circus/update_versions.py +388 -0
  32. agent_circus/utils.py +28 -0
  33. agent_circus-0.1.0.dist-info/METADATA +1195 -0
  34. agent_circus-0.1.0.dist-info/RECORD +37 -0
  35. agent_circus-0.1.0.dist-info/WHEEL +4 -0
  36. agent_circus-0.1.0.dist-info/entry_points.txt +3 -0
  37. agent_circus-0.1.0.dist-info/licenses/LICENSE +9 -0
@@ -0,0 +1,5 @@
1
+ """Agent Circus - CLI for managing agent containers."""
2
+
3
+ from importlib.metadata import version as distribution_version
4
+
5
+ __version__ = distribution_version("agent-circus")
@@ -0,0 +1,357 @@
1
+ """Agent configuration templating for Agent Circus.
2
+
3
+ Reads each agent's original configuration file, merges in additions
4
+ (e.g. MCP servers), writes the result to the state directory, and
5
+ generates a Docker Compose override that bind-mounts the merged
6
+ files into agent containers.
7
+
8
+ The user's host files are never modified.
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ import tomllib
14
+ from pathlib import Path
15
+ from typing import Any, Protocol, runtime_checkable
16
+
17
+ import tomli_w
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Handler protocol
24
+ # ---------------------------------------------------------------------------
25
+
26
+
27
+ @runtime_checkable
28
+ class AgentConfigHandler(Protocol):
29
+ """Structural interface for agent-specific configuration handlers."""
30
+
31
+ agent_name: str
32
+ """Docker Compose service name (e.g. ``"claude-code"``)."""
33
+
34
+ host_config_path: Path
35
+ """Path to the user's original config file on the host."""
36
+
37
+ container_config_path: str
38
+ """Absolute path where the config is expected inside the container."""
39
+
40
+ output_filename: str
41
+ """Filename for the merged config in the state directory."""
42
+
43
+ def read(self) -> dict[str, Any]:
44
+ """Read and deserialize the user's original config.
45
+
46
+ Returns an empty dict if the file does not exist.
47
+ """
48
+ ...
49
+
50
+ def merge(self, base: dict[str, Any], additions: dict[str, Any]) -> dict[str, Any]:
51
+ """Merge *additions* into *base* config.
52
+
53
+ :param base: The user's original (deserialized) config.
54
+ :param additions: Values to merge in (e.g. ``{"mcp_servers": [...]}``)
55
+ :returns: Merged config.
56
+ """
57
+ ...
58
+
59
+ def write(self, config: dict[str, Any], output_path: Path) -> None:
60
+ """Serialize *config* and write it to *output_path*."""
61
+ ...
62
+
63
+
64
+ def build_handler(
65
+ handler: AgentConfigHandler,
66
+ additions: dict[str, Any],
67
+ output_dir: Path,
68
+ ) -> Path:
69
+ """Read → merge → write using the given handler.
70
+
71
+ :param handler: Agent config handler to use.
72
+ :param additions: Values to merge into the user's config.
73
+ :param output_dir: Directory to write the merged config to.
74
+ :returns: Path to the written file.
75
+ """
76
+ base = handler.read()
77
+ merged = handler.merge(base, additions)
78
+ output_path = output_dir / handler.output_filename
79
+ handler.write(merged, output_path)
80
+ logger.debug("Built merged config for %s: %s", handler.agent_name, output_path)
81
+ return output_path
82
+
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # JSON-based handlers (Claude Code, OpenCode)
86
+ # ---------------------------------------------------------------------------
87
+
88
+
89
+ class ClaudeCodeConfigHandler:
90
+ """Handler for Claude Code configuration (JSON)."""
91
+
92
+ agent_name = "claude-code"
93
+ container_config_path = "/home/node/.claude/.claude.json"
94
+ output_filename = "claude-code.json"
95
+
96
+ def __init__(self) -> None:
97
+ self.host_config_path = Path.home() / ".claude" / ".claude.json"
98
+
99
+ def read(self) -> dict[str, Any]:
100
+ if not self.host_config_path.is_file():
101
+ return {}
102
+ with open(self.host_config_path) as f:
103
+ return json.load(f)
104
+
105
+ def merge(self, base: dict[str, Any], additions: dict[str, Any]) -> dict[str, Any]:
106
+ merged = base.copy()
107
+ for key, value in additions.items():
108
+ if (
109
+ key in merged
110
+ and isinstance(merged[key], dict)
111
+ and isinstance(value, dict)
112
+ ):
113
+ # Dict-level merge (e.g. mcpServers): additions overwrite conflicts.
114
+ merged[key] = {**merged[key], **value}
115
+ else:
116
+ merged[key] = value
117
+ return merged
118
+
119
+ def write(self, config: dict[str, Any], output_path: Path) -> None:
120
+ output_path.parent.mkdir(parents=True, exist_ok=True)
121
+ with open(output_path, "w") as f:
122
+ json.dump(config, f, indent=2)
123
+ f.write("\n")
124
+
125
+
126
+ class OpenCodeConfigHandler:
127
+ """Handler for OpenCode configuration (JSON)."""
128
+
129
+ agent_name = "opencode"
130
+ container_config_path = "/home/node/.config/opencode/opencode.json"
131
+ output_filename = "opencode.json"
132
+
133
+ def __init__(self) -> None:
134
+ self.host_config_path = Path.home() / ".config" / "opencode" / "opencode.json"
135
+
136
+ def read(self) -> dict[str, Any]:
137
+ if not self.host_config_path.is_file():
138
+ return {}
139
+ with open(self.host_config_path) as f:
140
+ return json.load(f)
141
+
142
+ def merge(self, base: dict[str, Any], additions: dict[str, Any]) -> dict[str, Any]:
143
+ merged = base.copy()
144
+ for key, value in additions.items():
145
+ if (
146
+ key in merged
147
+ and isinstance(merged[key], dict)
148
+ and isinstance(value, dict)
149
+ ):
150
+ # Dict-level merge (e.g. mcp): additions overwrite conflicts.
151
+ merged[key] = {**merged[key], **value}
152
+ else:
153
+ merged[key] = value
154
+ return merged
155
+
156
+ def write(self, config: dict[str, Any], output_path: Path) -> None:
157
+ output_path.parent.mkdir(parents=True, exist_ok=True)
158
+ with open(output_path, "w") as f:
159
+ json.dump(config, f, indent=2)
160
+ f.write("\n")
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # TOML-based handlers (Codex, Vibe)
165
+ # ---------------------------------------------------------------------------
166
+
167
+
168
+ class _TomlConfigHandler:
169
+ """Base handler for TOML-based agent configs (Codex, Vibe)."""
170
+
171
+ host_config_path: Path
172
+
173
+ def read(self) -> dict[str, Any]:
174
+ if not self.host_config_path.is_file():
175
+ return {}
176
+ with open(self.host_config_path, "rb") as f:
177
+ return tomllib.load(f)
178
+
179
+ def merge(self, base: dict[str, Any], additions: dict[str, Any]) -> dict[str, Any]:
180
+ merged = base.copy()
181
+ for key, value in additions.items():
182
+ if (
183
+ key in merged
184
+ and isinstance(merged[key], list)
185
+ and isinstance(value, list)
186
+ ):
187
+ # Array merge by "name" field: additions overwrite entries
188
+ # with the same name, new entries are appended.
189
+ merged[key] = _merge_named_arrays(merged[key], value)
190
+ elif (
191
+ key in merged
192
+ and isinstance(merged[key], dict)
193
+ and isinstance(value, dict)
194
+ ):
195
+ merged[key] = {**merged[key], **value}
196
+ else:
197
+ merged[key] = value
198
+ return merged
199
+
200
+ def write(self, config: dict[str, Any], output_path: Path) -> None:
201
+ output_path.parent.mkdir(parents=True, exist_ok=True)
202
+ with open(output_path, "wb") as f:
203
+ tomli_w.dump(config, f)
204
+
205
+
206
+ class CodexConfigHandler(_TomlConfigHandler):
207
+ """Handler for Codex configuration (TOML)."""
208
+
209
+ agent_name = "codex"
210
+ container_config_path = "/home/node/.codex/config.toml"
211
+ output_filename = "codex.toml"
212
+
213
+ def __init__(self) -> None:
214
+ self.host_config_path = Path.home() / ".codex" / "config.toml"
215
+
216
+
217
+ class VibeConfigHandler(_TomlConfigHandler):
218
+ """Handler for Mistral Vibe configuration (TOML)."""
219
+
220
+ agent_name = "mistral-vibe"
221
+ container_config_path = "/home/node/.vibe/config.toml"
222
+ output_filename = "mistral-vibe.toml"
223
+
224
+ def __init__(self) -> None:
225
+ self.host_config_path = Path.home() / ".vibe" / "config.toml"
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Merge helpers
230
+ # ---------------------------------------------------------------------------
231
+
232
+
233
+ def _merge_named_arrays(
234
+ base: list[dict[str, Any]], additions: list[dict[str, Any]]
235
+ ) -> list[dict[str, Any]]:
236
+ """Merge two lists of dicts by the ``name`` field.
237
+
238
+ Entries in *additions* overwrite base entries with the same name.
239
+ New entries are appended. Order: base entries first (updated
240
+ in-place), then new additions.
241
+
242
+ Falls back to simple concatenation if entries lack ``name`` fields.
243
+
244
+ :param base: Original list of entries.
245
+ :param additions: Entries to merge in.
246
+ :returns: Merged list.
247
+ """
248
+ # Check if entries have "name" fields for keyed merging.
249
+ if not all(isinstance(e, dict) and "name" in e for e in [*base, *additions]):
250
+ return [*base, *additions]
251
+
252
+ by_name: dict[str, dict[str, Any]] = {e["name"]: e for e in base}
253
+ order: list[str] = [e["name"] for e in base]
254
+
255
+ for entry in additions:
256
+ name = entry["name"]
257
+ if name not in by_name:
258
+ order.append(name)
259
+ by_name[name] = entry
260
+
261
+ return [by_name[name] for name in order]
262
+
263
+
264
+ # ---------------------------------------------------------------------------
265
+ # All handlers
266
+ # ---------------------------------------------------------------------------
267
+
268
+
269
+ HANDLERS: list[type[AgentConfigHandler]] = [
270
+ ClaudeCodeConfigHandler,
271
+ CodexConfigHandler,
272
+ VibeConfigHandler,
273
+ OpenCodeConfigHandler,
274
+ ]
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # Compose override generation
279
+ # ---------------------------------------------------------------------------
280
+
281
+
282
+ def build_agent_configs_override(
283
+ additions: dict[str, dict[str, Any]],
284
+ output_dir: Path,
285
+ excluded_agents: set[str] | None = None,
286
+ ) -> str:
287
+ """Build merged agent configs and return a Compose override JSON string.
288
+
289
+ For each agent, reads the user's original config, merges in
290
+ *additions*, writes the result to *output_dir*, and produces a
291
+ Compose override that bind-mounts the merged files into the
292
+ corresponding containers.
293
+
294
+ :param additions: Per-agent additions dict, keyed by agent name.
295
+ Example::
296
+
297
+ {
298
+ "claude-code": {"mcpServers": {...}},
299
+ "codex": {"mcp_servers": [...]},
300
+ "mistral-vibe": {"mcp_servers": [...]},
301
+ "opencode": {"mcp": {...}},
302
+ }
303
+
304
+ :param output_dir: Directory to write merged config files to.
305
+ :type output_dir: Path
306
+ :param excluded_agents: Agents whose config is managed through another
307
+ writable mount, such as a project data store.
308
+ :returns: Compose override as a JSON string.
309
+ :rtype: str
310
+ """
311
+ output_dir.mkdir(parents=True, exist_ok=True)
312
+ services: dict[str, Any] = {}
313
+ excluded_agents = excluded_agents or set()
314
+
315
+ for handler_cls in HANDLERS:
316
+ handler = handler_cls()
317
+ if handler.agent_name in excluded_agents:
318
+ continue
319
+ agent_additions = additions.get(handler.agent_name, {})
320
+ if not agent_additions:
321
+ continue
322
+
323
+ output_path = build_handler(handler, agent_additions, output_dir)
324
+ services[handler.agent_name] = {
325
+ "volumes": [
326
+ f"{output_path}:{handler.container_config_path}",
327
+ ],
328
+ }
329
+
330
+ return json.dumps({"services": services})
331
+
332
+
333
+ def merge_agent_config_store(
334
+ agent_name: str,
335
+ additions: dict[str, Any],
336
+ store_dir: Path,
337
+ ) -> Path:
338
+ """Merge generated additions into an agent config data store.
339
+
340
+ :param agent_name: Agent service whose native config should be updated.
341
+ :param additions: Values to merge into the native agent configuration.
342
+ :param store_dir: Host directory mounted at the agent's config directory.
343
+ :returns: Path to the updated native configuration file.
344
+ :raises ValueError: If ``agent_name`` has no configuration handler.
345
+ """
346
+ for handler_cls in HANDLERS:
347
+ handler = handler_cls()
348
+ if handler.agent_name != agent_name:
349
+ continue
350
+
351
+ config_path = store_dir / Path(handler.container_config_path).name
352
+ handler.host_config_path = config_path
353
+ merged = handler.merge(handler.read(), additions)
354
+ handler.write(merged, config_path)
355
+ return config_path
356
+
357
+ raise ValueError(f"Unsupported agent config store: {agent_name}")
agent_circus/cli.py ADDED
@@ -0,0 +1,73 @@
1
+ """Main CLI entry point for Agent Circus."""
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import typer
7
+
8
+ from agent_circus.commands import (
9
+ build,
10
+ config_store,
11
+ destroy,
12
+ exec_,
13
+ init,
14
+ ps,
15
+ remove,
16
+ up,
17
+ )
18
+ from agent_circus.config import load_user_config
19
+ from agent_circus.utils import setup_logging
20
+
21
+ app = typer.Typer(
22
+ name="agent-circus",
23
+ help="CLI for managing agent containers.",
24
+ no_args_is_help=True,
25
+ )
26
+
27
+
28
+ @app.callback()
29
+ def main(
30
+ log_level: Annotated[
31
+ str | None,
32
+ typer.Option(
33
+ "--log-level",
34
+ envvar="LOGLEVEL",
35
+ help="Set the log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).",
36
+ ),
37
+ ] = None,
38
+ log_file: Annotated[
39
+ Path | None,
40
+ typer.Option(
41
+ "--log-file",
42
+ envvar="LOGFILE",
43
+ help="Path to a log file. Logs are written to both stdout and this file.",
44
+ ),
45
+ ] = None,
46
+ ) -> None:
47
+ """CLI for managing agent containers."""
48
+ logging_cfg = load_user_config().get("logging", {})
49
+ setup_logging(
50
+ level=log_level or logging_cfg.get("level", "INFO"),
51
+ log_file=log_file
52
+ or (Path(logging_cfg["file"]) if logging_cfg.get("file") else None),
53
+ )
54
+
55
+
56
+ app.command()(init.init)
57
+ app.command()(build.build)
58
+ app.command()(up.up)
59
+ app.command()(ps.ps)
60
+ app.command(name="exec")(exec_.exec_cmd)
61
+ app.command()(remove.remove)
62
+ app.command(name="rm", hidden=True)(remove.remove)
63
+ app.command()(destroy.destroy)
64
+ app.add_typer(config_store.app, name="config")
65
+
66
+
67
+ def run_cli() -> None:
68
+ """Entry point for the CLI."""
69
+ app()
70
+
71
+
72
+ if __name__ == "__main__":
73
+ run_cli()
@@ -0,0 +1,5 @@
1
+ """Command modules for Agent Circus CLI."""
2
+
3
+ from agent_circus.commands import build, config_store, destroy, exec_, init, remove, up
4
+
5
+ __all__ = ["build", "config_store", "destroy", "exec_", "init", "remove", "up"]
@@ -0,0 +1,83 @@
1
+ """Build agent container images."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from agent_circus.compose import compose_build
10
+ from agent_circus.config import (
11
+ AVAILABLE_SERVICES,
12
+ get_workspace_path,
13
+ validate_services,
14
+ )
15
+ from agent_circus.context import build_compose_context
16
+ from agent_circus.exceptions import AgentCircusError
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def build(
22
+ services: Annotated[
23
+ list[str] | None,
24
+ typer.Argument(
25
+ help=f"Services to build. Available: {', '.join(AVAILABLE_SERVICES)}",
26
+ ),
27
+ ] = None,
28
+ workspace: Annotated[
29
+ Path | None,
30
+ typer.Option(
31
+ "--workspace",
32
+ "-w",
33
+ help="Workspace directory path.",
34
+ exists=True,
35
+ file_okay=False,
36
+ resolve_path=True,
37
+ ),
38
+ ] = None,
39
+ no_cache: Annotated[
40
+ bool,
41
+ typer.Option(
42
+ "--no-cache",
43
+ help="Build without using cache.",
44
+ ),
45
+ ] = False,
46
+ runtime: Annotated[
47
+ str | None,
48
+ typer.Option(
49
+ "--runtime",
50
+ help="Container runtime backend to use: docker or podman.",
51
+ ),
52
+ ] = None,
53
+ ) -> None:
54
+ """Build agent container images.
55
+
56
+ Builds container images for the specified services using the selected
57
+ runtime's Compose implementation.
58
+ If no services are specified, all services will be built.
59
+
60
+ Examples:
61
+ agent-circus build # Build all services
62
+ agent-circus build claude-code # Build only claude-code
63
+ agent-circus build codex mistral-vibe # Build multiple services
64
+ agent-circus build --no-cache # Build without cache
65
+ """
66
+ workspace = workspace or get_workspace_path()
67
+
68
+ try:
69
+ services_to_build = validate_services(services or [])
70
+
71
+ if services:
72
+ typer.echo(f"Building services: {', '.join(services_to_build)}")
73
+ else:
74
+ typer.echo("Building all services...")
75
+
76
+ context_kwargs = {"runtime": runtime} if runtime is not None else {}
77
+ with build_compose_context(workspace, **context_kwargs) as ctx:
78
+ compose_build(ctx, services_to_build, no_cache=no_cache)
79
+ typer.echo("Build completed successfully.")
80
+
81
+ except AgentCircusError as e:
82
+ typer.echo(f"Error: {e}", err=True)
83
+ raise typer.Exit(code=1) from e
@@ -0,0 +1,104 @@
1
+ """Manage project-local writable agent configuration stores."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from agent_circus.compose import compose_is_service_running
10
+ from agent_circus.config import (
11
+ AVAILABLE_SERVICES,
12
+ get_agent_config_data_stores,
13
+ get_workspace_path,
14
+ load_config,
15
+ validate_services,
16
+ )
17
+ from agent_circus.context import build_compose_context
18
+ from agent_circus.exceptions import AgentCircusError, ConfigurationError
19
+ from agent_circus.state import get_agent_config_store_dir, get_data_store_dir
20
+
21
+ app = typer.Typer(help="Manage writable agent configuration stores.")
22
+
23
+
24
+ def _config_store_path(workspace: Path, agent_name: str) -> Path:
25
+ """Return the active writable config store path for an agent.
26
+
27
+ :param workspace: Workspace directory.
28
+ :param agent_name: Agent service name.
29
+ :returns: Built-in or explicitly configured store path.
30
+ """
31
+ owners = get_agent_config_data_stores(load_config(workspace).get("data_stores", []))
32
+ explicit_store = owners.get(agent_name)
33
+ if explicit_store:
34
+ return get_data_store_dir(workspace, explicit_store)
35
+ return get_agent_config_store_dir(workspace, agent_name)
36
+
37
+
38
+ @app.command()
39
+ def reset(
40
+ agent: Annotated[
41
+ str | None,
42
+ typer.Argument(
43
+ help=f"Agent to reset. Available: {', '.join(AVAILABLE_SERVICES)}"
44
+ ),
45
+ ] = None,
46
+ all_agents: Annotated[
47
+ bool,
48
+ typer.Option("--all", help="Reset configuration stores for every agent."),
49
+ ] = False,
50
+ workspace: Annotated[
51
+ Path | None,
52
+ typer.Option(
53
+ "--workspace",
54
+ "-w",
55
+ help="Workspace directory path.",
56
+ exists=True,
57
+ file_okay=False,
58
+ resolve_path=True,
59
+ ),
60
+ ] = None,
61
+ force: Annotated[
62
+ bool,
63
+ typer.Option("--force", "-f", help="Skip the confirmation prompt."),
64
+ ] = False,
65
+ runtime: Annotated[
66
+ str | None,
67
+ typer.Option(
68
+ "--runtime",
69
+ help="Container runtime backend to use: docker or podman.",
70
+ ),
71
+ ] = None,
72
+ ) -> None:
73
+ """Delete writable config stores so the next start seeds them again."""
74
+ workspace = workspace or get_workspace_path()
75
+
76
+ try:
77
+ if all_agents == (agent is not None):
78
+ raise ConfigurationError("Specify exactly one agent or --all")
79
+ agents = AVAILABLE_SERVICES if all_agents else validate_services([agent or ""])
80
+
81
+ context_kwargs = {"runtime": runtime} if runtime is not None else {}
82
+ with build_compose_context(workspace, **context_kwargs) as ctx:
83
+ running = [name for name in agents if compose_is_service_running(ctx, name)]
84
+ if running:
85
+ raise ConfigurationError(
86
+ "Cannot reset config for running services: " + ", ".join(running)
87
+ )
88
+
89
+ if not force:
90
+ typer.echo(
91
+ "This will delete writable config for: " + ", ".join(agents) + "."
92
+ )
93
+ if not typer.confirm("Are you sure you want to continue?"):
94
+ typer.echo("Aborted.")
95
+ raise typer.Exit(code=0)
96
+
97
+ for name in agents:
98
+ store_path = _config_store_path(workspace, name)
99
+ if store_path.exists():
100
+ shutil.rmtree(store_path)
101
+ typer.echo(f"Reset {name} config store: {store_path}")
102
+ except AgentCircusError as e:
103
+ typer.echo(f"Error: {e}", err=True)
104
+ raise typer.Exit(code=1) from e