argus-cli 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 (138) hide show
  1. argus_cli/__init__.py +10 -0
  2. argus_cli/__main__.py +6 -0
  3. argus_cli/_console.py +47 -0
  4. argus_cli/client.py +446 -0
  5. argus_cli/commands/__init__.py +1 -0
  6. argus_cli/commands/audit.py +119 -0
  7. argus_cli/commands/auth.py +112 -0
  8. argus_cli/commands/backends.py +395 -0
  9. argus_cli/commands/batch.py +121 -0
  10. argus_cli/commands/config_cmd.py +171 -0
  11. argus_cli/commands/config_server.py +305 -0
  12. argus_cli/commands/containers.py +392 -0
  13. argus_cli/commands/events.py +93 -0
  14. argus_cli/commands/health.py +150 -0
  15. argus_cli/commands/operations.py +198 -0
  16. argus_cli/commands/pods.py +299 -0
  17. argus_cli/commands/prompts.py +128 -0
  18. argus_cli/commands/registry.py +255 -0
  19. argus_cli/commands/resources.py +78 -0
  20. argus_cli/commands/secrets.py +194 -0
  21. argus_cli/commands/server.py +269 -0
  22. argus_cli/commands/skills.py +204 -0
  23. argus_cli/commands/tools.py +208 -0
  24. argus_cli/commands/workflows.py +147 -0
  25. argus_cli/config.py +242 -0
  26. argus_cli/daemon_client.py +394 -0
  27. argus_cli/design.py +77 -0
  28. argus_cli/main.py +253 -0
  29. argus_cli/output.py +290 -0
  30. argus_cli/repl/__init__.py +29 -0
  31. argus_cli/repl/completions.py +166 -0
  32. argus_cli/repl/dispatch.py +105 -0
  33. argus_cli/repl/handlers.py +292 -0
  34. argus_cli/repl/loop.py +170 -0
  35. argus_cli/repl/state.py +109 -0
  36. argus_cli/repl/toolbar.py +79 -0
  37. argus_cli/theme.py +256 -0
  38. argus_cli/themes/catppuccin-frappe.yaml +18 -0
  39. argus_cli/themes/catppuccin-latte.yaml +18 -0
  40. argus_cli/themes/catppuccin-macchiato.yaml +18 -0
  41. argus_cli/themes/catppuccin-mocha.yaml +18 -0
  42. argus_cli/themes/dracula.yaml +18 -0
  43. argus_cli/themes/everforest.yaml +18 -0
  44. argus_cli/themes/gruvbox.yaml +18 -0
  45. argus_cli/themes/kanagawa.yaml +18 -0
  46. argus_cli/themes/monokai.yaml +18 -0
  47. argus_cli/themes/nord.yaml +18 -0
  48. argus_cli/themes/one-dark.yaml +18 -0
  49. argus_cli/themes/rose-pine-moon.yaml +18 -0
  50. argus_cli/themes/rose-pine.yaml +18 -0
  51. argus_cli/themes/solarized-dark.yaml +18 -0
  52. argus_cli/themes/solarized-light.yaml +18 -0
  53. argus_cli/themes/tokyo-night.yaml +18 -0
  54. argus_cli/tui/__init__.py +19 -0
  55. argus_cli/tui/_config_ops.py +119 -0
  56. argus_cli/tui/_constants.py +23 -0
  57. argus_cli/tui/_dev_launch.py +81 -0
  58. argus_cli/tui/_error_utils.py +81 -0
  59. argus_cli/tui/api_client.py +17 -0
  60. argus_cli/tui/app.py +1258 -0
  61. argus_cli/tui/argus.tcss +589 -0
  62. argus_cli/tui/commands.py +125 -0
  63. argus_cli/tui/events.py +74 -0
  64. argus_cli/tui/screens/__init__.py +25 -0
  65. argus_cli/tui/screens/_base_log.py +143 -0
  66. argus_cli/tui/screens/audit_log.py +226 -0
  67. argus_cli/tui/screens/backend_config.py +461 -0
  68. argus_cli/tui/screens/backend_detail.py +205 -0
  69. argus_cli/tui/screens/base.py +59 -0
  70. argus_cli/tui/screens/catalog_browser.py +227 -0
  71. argus_cli/tui/screens/client_config.py +244 -0
  72. argus_cli/tui/screens/containers.py +248 -0
  73. argus_cli/tui/screens/dashboard.py +67 -0
  74. argus_cli/tui/screens/elicitation.py +83 -0
  75. argus_cli/tui/screens/exit_modal.py +104 -0
  76. argus_cli/tui/screens/export_import.py +376 -0
  77. argus_cli/tui/screens/health.py +440 -0
  78. argus_cli/tui/screens/kubernetes.py +260 -0
  79. argus_cli/tui/screens/operations.py +144 -0
  80. argus_cli/tui/screens/registry.py +262 -0
  81. argus_cli/tui/screens/security.py +240 -0
  82. argus_cli/tui/screens/server_detail.py +170 -0
  83. argus_cli/tui/screens/server_logs.py +232 -0
  84. argus_cli/tui/screens/settings.py +494 -0
  85. argus_cli/tui/screens/setup_wizard.py +714 -0
  86. argus_cli/tui/screens/skills.py +472 -0
  87. argus_cli/tui/screens/theme_picker.py +116 -0
  88. argus_cli/tui/screens/tool_editor.py +327 -0
  89. argus_cli/tui/screens/tools.py +268 -0
  90. argus_cli/tui/server_manager.py +312 -0
  91. argus_cli/tui/settings.py +92 -0
  92. argus_cli/tui/widgets/__init__.py +15 -0
  93. argus_cli/tui/widgets/backend_status.py +223 -0
  94. argus_cli/tui/widgets/capability_tables.py +204 -0
  95. argus_cli/tui/widgets/container_logs.py +112 -0
  96. argus_cli/tui/widgets/container_stats.py +125 -0
  97. argus_cli/tui/widgets/container_table.py +104 -0
  98. argus_cli/tui/widgets/elicitation_form.py +133 -0
  99. argus_cli/tui/widgets/event_log.py +124 -0
  100. argus_cli/tui/widgets/filter_bar.py +176 -0
  101. argus_cli/tui/widgets/filter_toggle.py +64 -0
  102. argus_cli/tui/widgets/health_panel.py +263 -0
  103. argus_cli/tui/widgets/install_panel.py +125 -0
  104. argus_cli/tui/widgets/jump_overlay.py +180 -0
  105. argus_cli/tui/widgets/middleware_panel.py +124 -0
  106. argus_cli/tui/widgets/module_container.py +57 -0
  107. argus_cli/tui/widgets/network_panel.py +135 -0
  108. argus_cli/tui/widgets/optimizer_panel.py +213 -0
  109. argus_cli/tui/widgets/otel_panel.py +169 -0
  110. argus_cli/tui/widgets/param_editor.py +156 -0
  111. argus_cli/tui/widgets/percentage_bar.py +110 -0
  112. argus_cli/tui/widgets/pod_table.py +50 -0
  113. argus_cli/tui/widgets/quick_actions.py +94 -0
  114. argus_cli/tui/widgets/registry_browser.py +299 -0
  115. argus_cli/tui/widgets/registry_panel.py +137 -0
  116. argus_cli/tui/widgets/secrets_panel.py +276 -0
  117. argus_cli/tui/widgets/server_connections_panel.py +125 -0
  118. argus_cli/tui/widgets/server_groups.py +108 -0
  119. argus_cli/tui/widgets/server_info.py +127 -0
  120. argus_cli/tui/widgets/server_selector.py +98 -0
  121. argus_cli/tui/widgets/sessions_panel.py +201 -0
  122. argus_cli/tui/widgets/sync_status.py +164 -0
  123. argus_cli/tui/widgets/tool_ops_panel.py +167 -0
  124. argus_cli/tui/widgets/tool_preview.py +65 -0
  125. argus_cli/tui/widgets/toolbar.py +101 -0
  126. argus_cli/tui/widgets/tplot.py +288 -0
  127. argus_cli/tui/widgets/version_badge.py +47 -0
  128. argus_cli/tui/widgets/version_drift.py +216 -0
  129. argus_cli/tui/widgets/workflows_panel.py +590 -0
  130. argus_cli/widgets/__init__.py +23 -0
  131. argus_cli/widgets/banner.py +35 -0
  132. argus_cli/widgets/panels.py +80 -0
  133. argus_cli/widgets/spinners.py +83 -0
  134. argus_cli/widgets/tables.py +84 -0
  135. argus_cli-0.1.0.dist-info/METADATA +99 -0
  136. argus_cli-0.1.0.dist-info/RECORD +138 -0
  137. argus_cli-0.1.0.dist-info/WHEEL +4 -0
  138. argus_cli-0.1.0.dist-info/entry_points.txt +4 -0
argus_cli/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Argus CLI — Interactive command-line interface for Argus MCP."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ try:
8
+ __version__ = version("argus-cli")
9
+ except PackageNotFoundError:
10
+ __version__ = "0.0.0-dev"
argus_cli/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow ``python -m argus_cli`` invocation."""
2
+
3
+ from argus_cli.main import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
argus_cli/_console.py ADDED
@@ -0,0 +1,47 @@
1
+ """Console singleton — shared by theme.py and output.py to avoid circular imports.
2
+
3
+ The module-global singleton pattern is intentional: Rich Console configuration
4
+ (theme, no_color) must be consistent across all output paths. ``reset_console()``
5
+ exists to support tests that need to reconfigure the console between runs.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __all__ = ["get_console", "reset_console"]
11
+
12
+ from rich.console import Console
13
+
14
+ _console: Console | None = None
15
+
16
+
17
+ def get_console(no_color: bool | None = None) -> Console:
18
+ """Get or create the Rich console singleton.
19
+
20
+ Args:
21
+ no_color: Override color setting. When ``None``, reads from
22
+ the active CLI config.
23
+ """
24
+ global _console
25
+ if _console is None:
26
+ from argus_cli.theme import ARGUS_THEME, _ensure_loaded
27
+
28
+ _ensure_loaded()
29
+ if no_color is None:
30
+ from argus_cli.config import get_config
31
+
32
+ try:
33
+ no_color = get_config().no_color
34
+ except RuntimeError:
35
+ no_color = False
36
+ _console = Console(
37
+ theme=ARGUS_THEME,
38
+ no_color=no_color,
39
+ stderr=False,
40
+ )
41
+ return _console
42
+
43
+
44
+ def reset_console() -> None:
45
+ """Reset console singleton so the next call picks up a new theme."""
46
+ global _console
47
+ _console = None
argus_cli/client.py ADDED
@@ -0,0 +1,446 @@
1
+ """HTTP API client for the Argus MCP Management API.
2
+
3
+ Thin adapter around :mod:`argus_mcp.api.client` that provides:
4
+
5
+ - **Sync** (``ArgusClient``) and **async** (``AsyncArgusClient``) wrappers
6
+ that return raw ``dict`` results for CLI output rendering.
7
+ - Retry with exponential back-off for transient transport errors.
8
+ - SSE streaming via ``httpx-sse``.
9
+ - ``CliConfig``-based initialisation so callers don't construct URLs manually.
10
+
11
+ The underlying HTTP contract (schemas, error model, endpoint paths) is shared
12
+ with the TUI client via ``argus_mcp.api``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ __all__ = ["ArgusClient", "ArgusClientError", "AsyncArgusClient"]
18
+
19
+ import json
20
+ import sys
21
+ import time
22
+ from collections.abc import AsyncGenerator
23
+ from typing import Any
24
+
25
+ if sys.version_info >= (3, 11):
26
+ from typing import Self
27
+ else:
28
+ from typing_extensions import Self
29
+
30
+ import httpx
31
+ from argus_mcp.api.client import ApiClientError
32
+
33
+ from argus_cli.config import CliConfig
34
+
35
+ # ── Timeout configuration ──────────────────────────────────────────────
36
+
37
+ DEFAULT_TIMEOUT = httpx.Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0)
38
+ SSE_TIMEOUT = httpx.Timeout(connect=5.0, read=None, write=10.0, pool=5.0)
39
+
40
+ # ── Retry configuration ────────────────────────────────────────────────
41
+
42
+ MAX_RETRIES = 3
43
+ RETRY_BACKOFF_BASE = 0.5 # seconds; doubles each attempt
44
+
45
+
46
+ class ArgusClientError(ApiClientError):
47
+ """CLI-specific API error with structured status_code / error / message."""
48
+
49
+ def __init__(self, status_code: int, error: str, message: str) -> None:
50
+ self.status_code = status_code
51
+ self.error = error
52
+ self.message = message
53
+ super().__init__(f"[{status_code}] {error}: {message}", status_code=status_code or None)
54
+
55
+
56
+ def _build_headers(config: CliConfig) -> dict[str, str]:
57
+ """Build request headers including optional auth token."""
58
+ headers: dict[str, str] = {"Accept": "application/json"}
59
+ if config.token:
60
+ headers["Authorization"] = f"Bearer {config.token}"
61
+ return headers
62
+
63
+
64
+ def _build_group_params(group: str | None) -> dict[str, str]:
65
+ """Build query params for the /groups endpoint."""
66
+ return {"group": group} if group else {}
67
+
68
+
69
+ def _build_capabilities_params(
70
+ *,
71
+ type_filter: str | None = None,
72
+ backend: str | None = None,
73
+ search: str | None = None,
74
+ ) -> dict[str, str]:
75
+ """Build query params for the /capabilities endpoint."""
76
+ params: dict[str, str] = {}
77
+ if type_filter:
78
+ params["type"] = type_filter
79
+ if backend:
80
+ params["backend"] = backend
81
+ if search:
82
+ params["search"] = search
83
+ return params
84
+
85
+
86
+ def _build_events_params(
87
+ *,
88
+ limit: int = 100,
89
+ since: str | None = None,
90
+ severity: str | None = None,
91
+ ) -> dict[str, str]:
92
+ """Build query params for the /events endpoint."""
93
+ params: dict[str, str] = {"limit": str(limit)}
94
+ if since:
95
+ params["since"] = since
96
+ if severity:
97
+ params["severity"] = severity
98
+ return params
99
+
100
+
101
+ def _handle_response(response: httpx.Response) -> dict[str, Any]:
102
+ """Parse response JSON and raise on error status codes."""
103
+ if response.status_code >= 400:
104
+ try:
105
+ body = response.json()
106
+ raise ArgusClientError(
107
+ status_code=response.status_code,
108
+ error=body.get("error", "unknown_error"),
109
+ message=body.get("message", response.text),
110
+ )
111
+ except (json.JSONDecodeError, KeyError) as exc:
112
+ raise ArgusClientError(
113
+ status_code=response.status_code,
114
+ error="http_error",
115
+ message=response.text,
116
+ ) from exc
117
+ try:
118
+ result: dict[str, Any] = response.json()
119
+ return result
120
+ except json.JSONDecodeError as exc:
121
+ raise ArgusClientError(
122
+ status_code=response.status_code,
123
+ error="parse_error",
124
+ message="Response is not valid JSON",
125
+ ) from exc
126
+
127
+
128
+ # ── Sync client (one-shot commands) ────────────────────────────────────
129
+
130
+
131
+ class ArgusClient:
132
+ """Synchronous httpx client for one-shot CLI commands."""
133
+
134
+ def __init__(self, config: CliConfig) -> None:
135
+ """Initialise the client with resolved CLI configuration.
136
+
137
+ Args:
138
+ config: Resolved CLI configuration containing the server URL,
139
+ auth token, and timeout settings.
140
+ """
141
+ self._config = config
142
+ self._client = httpx.Client(
143
+ base_url=config.base_url,
144
+ headers=_build_headers(config),
145
+ timeout=DEFAULT_TIMEOUT,
146
+ )
147
+
148
+ def close(self) -> None:
149
+ self._client.close()
150
+
151
+ def __enter__(self) -> Self:
152
+ return self
153
+
154
+ def __exit__(self, *args: object) -> None:
155
+ self.close()
156
+
157
+ def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
158
+ """Send an HTTP request with transport error handling and retry."""
159
+ last_exc: ArgusClientError | None = None
160
+ for attempt in range(MAX_RETRIES):
161
+ try:
162
+ response = getattr(self._client, method)(path, **kwargs)
163
+ return _handle_response(response)
164
+ except httpx.ConnectError as exc:
165
+ last_exc = ArgusClientError(
166
+ 0,
167
+ "connection_error",
168
+ f"Cannot connect to {self._config.server_url}",
169
+ )
170
+ last_exc.__cause__ = exc
171
+ except httpx.TimeoutException as exc:
172
+ last_exc = ArgusClientError(
173
+ 0,
174
+ "timeout_error",
175
+ "Request timed out",
176
+ )
177
+ last_exc.__cause__ = exc
178
+ except httpx.TransportError as exc:
179
+ last_exc = ArgusClientError(
180
+ 0,
181
+ "transport_error",
182
+ f"Network error: {exc}",
183
+ )
184
+ last_exc.__cause__ = exc
185
+ if attempt < MAX_RETRIES - 1:
186
+ time.sleep(RETRY_BACKOFF_BASE * (2**attempt))
187
+ raise last_exc # type: ignore[misc]
188
+
189
+ # ── GET endpoints ──────────────────────────────────────────────────
190
+
191
+ def health(self) -> dict[str, Any]:
192
+ return self._request("get", "/health")
193
+
194
+ def status(self) -> dict[str, Any]:
195
+ return self._request("get", "/status")
196
+
197
+ def backends(self) -> dict[str, Any]:
198
+ return self._request("get", "/backends")
199
+
200
+ def groups(self, group: str | None = None) -> dict[str, Any]:
201
+ return self._request("get", "/groups", params=_build_group_params(group))
202
+
203
+ def capabilities(
204
+ self,
205
+ *,
206
+ type_filter: str | None = None,
207
+ backend: str | None = None,
208
+ search: str | None = None,
209
+ ) -> dict[str, Any]:
210
+ params = _build_capabilities_params(
211
+ type_filter=type_filter,
212
+ backend=backend,
213
+ search=search,
214
+ )
215
+ return self._request("get", "/capabilities", params=params)
216
+
217
+ def sessions(self) -> dict[str, Any]:
218
+ return self._request("get", "/sessions")
219
+
220
+ def events(
221
+ self,
222
+ *,
223
+ limit: int = 100,
224
+ since: str | None = None,
225
+ severity: str | None = None,
226
+ ) -> dict[str, Any]:
227
+ params = _build_events_params(limit=limit, since=since, severity=severity)
228
+ return self._request("get", "/events", params=params)
229
+
230
+ # ── POST endpoints ─────────────────────────────────────────────────
231
+
232
+ def reload(self) -> dict[str, Any]:
233
+ return self._request("post", "/reload")
234
+
235
+ def reconnect(self, name: str) -> dict[str, Any]:
236
+ return self._request("post", f"/reconnect/{name}")
237
+
238
+ def shutdown(self, timeout_seconds: int = 30) -> dict[str, Any]:
239
+ return self._request("post", "/shutdown", json={"timeout_seconds": timeout_seconds})
240
+
241
+ # ── Registry endpoints ─────────────────────────────────────────────
242
+
243
+ def registry_search(
244
+ self,
245
+ query: str,
246
+ *,
247
+ limit: int = 20,
248
+ registry: str | None = None,
249
+ ) -> dict[str, Any]:
250
+ params: dict[str, Any] = {"q": query, "limit": limit}
251
+ if registry:
252
+ params["registry"] = registry
253
+ return self._request("get", "/registry/search", params=params)
254
+
255
+ # ── Skills endpoints ───────────────────────────────────────────────
256
+
257
+ def skills_list(self) -> dict[str, Any]:
258
+ return self._request("get", "/skills")
259
+
260
+ def skills_enable(self, name: str) -> dict[str, Any]:
261
+ return self._request("post", f"/skills/{name}/enable")
262
+
263
+ def skills_disable(self, name: str) -> dict[str, Any]:
264
+ return self._request("post", f"/skills/{name}/disable")
265
+
266
+ def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
267
+ payload = {"tool": name, "arguments": arguments or {}}
268
+ return self._request("post", "/tools/call", json=payload)
269
+
270
+ def read_resource(self, uri: str) -> dict[str, Any]:
271
+ return self._request("post", "/resources/read", json={"uri": uri})
272
+
273
+
274
+ # ── Async client (REPL mode) ──────────────────────────────────────────
275
+
276
+
277
+ class AsyncArgusClient:
278
+ """Async httpx client for REPL mode."""
279
+
280
+ def __init__(self, config: CliConfig) -> None:
281
+ """Initialise the async client with resolved CLI configuration.
282
+
283
+ Args:
284
+ config: Resolved CLI configuration containing the server URL,
285
+ auth token, and timeout settings.
286
+ """
287
+ self._config = config
288
+ self._client = httpx.AsyncClient(
289
+ base_url=config.base_url,
290
+ headers=_build_headers(config),
291
+ timeout=DEFAULT_TIMEOUT,
292
+ )
293
+
294
+ async def close(self) -> None:
295
+ await self._client.aclose()
296
+
297
+ async def __aenter__(self) -> Self:
298
+ return self
299
+
300
+ async def __aexit__(self, *args: object) -> None:
301
+ await self.close()
302
+
303
+ async def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
304
+ """Send an async HTTP request with transport error handling and retry."""
305
+ import asyncio
306
+
307
+ last_exc: ArgusClientError | None = None
308
+ for attempt in range(MAX_RETRIES):
309
+ try:
310
+ response = await getattr(self._client, method)(path, **kwargs)
311
+ return _handle_response(response)
312
+ except httpx.ConnectError as exc:
313
+ last_exc = ArgusClientError(
314
+ 0,
315
+ "connection_error",
316
+ f"Cannot connect to {self._config.server_url}",
317
+ )
318
+ last_exc.__cause__ = exc
319
+ except httpx.TimeoutException as exc:
320
+ last_exc = ArgusClientError(
321
+ 0,
322
+ "timeout_error",
323
+ "Request timed out",
324
+ )
325
+ last_exc.__cause__ = exc
326
+ except httpx.TransportError as exc:
327
+ last_exc = ArgusClientError(
328
+ 0,
329
+ "transport_error",
330
+ f"Network error: {exc}",
331
+ )
332
+ last_exc.__cause__ = exc
333
+ if attempt < MAX_RETRIES - 1:
334
+ await asyncio.sleep(RETRY_BACKOFF_BASE * (2**attempt))
335
+ raise last_exc # type: ignore[misc]
336
+
337
+ # ── GET endpoints ──────────────────────────────────────────────────
338
+
339
+ async def health(self) -> dict[str, Any]:
340
+ return await self._request("get", "/health")
341
+
342
+ async def status(self) -> dict[str, Any]:
343
+ return await self._request("get", "/status")
344
+
345
+ async def backends(self) -> dict[str, Any]:
346
+ return await self._request("get", "/backends")
347
+
348
+ async def groups(self, group: str | None = None) -> dict[str, Any]:
349
+ return await self._request("get", "/groups", params=_build_group_params(group))
350
+
351
+ async def capabilities(
352
+ self,
353
+ *,
354
+ type_filter: str | None = None,
355
+ backend: str | None = None,
356
+ search: str | None = None,
357
+ ) -> dict[str, Any]:
358
+ params = _build_capabilities_params(
359
+ type_filter=type_filter,
360
+ backend=backend,
361
+ search=search,
362
+ )
363
+ return await self._request("get", "/capabilities", params=params)
364
+
365
+ async def sessions(self) -> dict[str, Any]:
366
+ return await self._request("get", "/sessions")
367
+
368
+ async def events(
369
+ self,
370
+ *,
371
+ limit: int = 100,
372
+ since: str | None = None,
373
+ severity: str | None = None,
374
+ ) -> dict[str, Any]:
375
+ params = _build_events_params(limit=limit, since=since, severity=severity)
376
+ return await self._request("get", "/events", params=params)
377
+
378
+ # ── POST endpoints ─────────────────────────────────────────────────
379
+
380
+ async def reload(self) -> dict[str, Any]:
381
+ return await self._request("post", "/reload")
382
+
383
+ async def reconnect(self, name: str) -> dict[str, Any]:
384
+ return await self._request("post", f"/reconnect/{name}")
385
+
386
+ async def shutdown(self, timeout_seconds: int = 30) -> dict[str, Any]:
387
+ return await self._request("post", "/shutdown", json={"timeout_seconds": timeout_seconds})
388
+
389
+ # ── Registry endpoints ─────────────────────────────────────────────
390
+
391
+ async def registry_search(
392
+ self,
393
+ query: str,
394
+ *,
395
+ limit: int = 20,
396
+ registry: str | None = None,
397
+ ) -> dict[str, Any]:
398
+ params: dict[str, Any] = {"q": query, "limit": limit}
399
+ if registry:
400
+ params["registry"] = registry
401
+ return await self._request("get", "/registry/search", params=params)
402
+
403
+ # ── Skills endpoints ───────────────────────────────────────────────
404
+
405
+ async def skills_list(self) -> dict[str, Any]:
406
+ return await self._request("get", "/skills")
407
+
408
+ async def skills_enable(self, name: str) -> dict[str, Any]:
409
+ return await self._request("post", f"/skills/{name}/enable")
410
+
411
+ async def skills_disable(self, name: str) -> dict[str, Any]:
412
+ return await self._request("post", f"/skills/{name}/disable")
413
+
414
+ async def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
415
+ payload = {"tool": name, "arguments": arguments or {}}
416
+ return await self._request("post", "/tools/call", json=payload)
417
+
418
+ async def read_resource(self, uri: str) -> dict[str, Any]:
419
+ return await self._request("post", "/resources/read", json={"uri": uri})
420
+
421
+ # ── SSE streaming ──────────────────────────────────────────────────
422
+
423
+ async def events_stream(self) -> AsyncGenerator[dict[str, Any], None]:
424
+ """Yield SSE events from /events/stream as dicts.
425
+
426
+ Yields:
427
+ dict with keys: event, data, id (parsed from SSE format)
428
+ """
429
+ from httpx_sse import aconnect_sse
430
+
431
+ async with aconnect_sse(
432
+ self._client,
433
+ "GET",
434
+ "/events/stream",
435
+ timeout=SSE_TIMEOUT,
436
+ ) as event_source:
437
+ async for sse in event_source.aiter_sse():
438
+ try:
439
+ data = json.loads(sse.data)
440
+ except (json.JSONDecodeError, TypeError):
441
+ data = sse.data
442
+ yield {
443
+ "event": sse.event,
444
+ "data": data,
445
+ "id": sse.id,
446
+ }
@@ -0,0 +1 @@
1
+ """Commands package — Typer sub-apps for each command group."""
@@ -0,0 +1,119 @@
1
+ """Audit log commands — list, export."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__ = ["app"]
6
+
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ from argus_cli.output import OutputOption
12
+
13
+ app = typer.Typer(no_args_is_help=True)
14
+
15
+
16
+ @app.command("list")
17
+ def list_audit(
18
+ ctx: typer.Context,
19
+ limit: Annotated[int, typer.Option(help="Maximum entries to return.")] = 100,
20
+ type_filter: Annotated[str | None, typer.Option("--type", help="Filter by event type.")] = None,
21
+ search: Annotated[str | None, typer.Option(help="Search pattern.")] = None,
22
+ output_fmt: OutputOption = None,
23
+ ) -> None:
24
+ """List audit log entries."""
25
+ from argus_cli.client import ArgusClient, ArgusClientError
26
+ from argus_cli.output import OutputSpec, apply_output_option, output, print_error
27
+
28
+ apply_output_option(output_fmt)
29
+ config = ctx.obj
30
+ try:
31
+ with ArgusClient(config) as client:
32
+ data = client.events(limit=limit)
33
+ events = data.get("events", data)
34
+
35
+ # Client-side type filter (--type)
36
+ if type_filter:
37
+ tf = type_filter.lower()
38
+ events = [e for e in events if tf in str(e.get("type", "")).lower()]
39
+
40
+ # Client-side search filter
41
+ if search:
42
+ q = search.lower()
43
+ events = [
44
+ e
45
+ for e in events
46
+ if q in str(e.get("message", "")).lower()
47
+ or q in str(e.get("stage", "")).lower()
48
+ or q in str(e.get("backend", "")).lower()
49
+ ]
50
+
51
+ output(
52
+ events,
53
+ fmt=config.output_format,
54
+ spec=OutputSpec(
55
+ title="Audit Log",
56
+ columns=["timestamp", "stage", "severity", "message"],
57
+ key_field="severity",
58
+ ),
59
+ )
60
+ except ArgusClientError as e:
61
+ print_error(f"Failed to get audit log: {e.message}")
62
+ raise typer.Exit(1) from None
63
+
64
+
65
+ @app.command()
66
+ def export(
67
+ ctx: typer.Context,
68
+ fmt: Annotated[
69
+ str, typer.Option("--format", "-f", help="Export format: json or csv.")
70
+ ] = "json",
71
+ since: Annotated[str | None, typer.Option(help="Export since timestamp (ISO 8601).")] = None,
72
+ limit: Annotated[int, typer.Option(help="Maximum entries to export.")] = 1000,
73
+ output_file: Annotated[
74
+ str | None, typer.Option("--output", "-o", help="Output file (default: stdout).")
75
+ ] = None,
76
+ ) -> None:
77
+ """Export audit log entries as JSON or CSV."""
78
+ import csv
79
+ import io
80
+ import json
81
+ import sys
82
+
83
+ from argus_cli.client import ArgusClient, ArgusClientError
84
+ from argus_cli.output import print_error, print_success
85
+
86
+ config = ctx.obj
87
+ try:
88
+ with ArgusClient(config) as client:
89
+ data = client.events(limit=limit, since=since)
90
+ events = data.get("events", [])
91
+ except ArgusClientError as e:
92
+ print_error(f"Failed to export audit log: {e.message}")
93
+ raise typer.Exit(1) from None
94
+
95
+ if fmt == "json":
96
+ content = json.dumps(events, indent=2, default=str)
97
+ elif fmt == "csv":
98
+ if not events:
99
+ content = ""
100
+ else:
101
+ buf = io.StringIO()
102
+ fieldnames = ["id", "timestamp", "stage", "severity", "message", "backend"]
103
+ writer = csv.DictWriter(buf, fieldnames=fieldnames, extrasaction="ignore")
104
+ writer.writeheader()
105
+ for event in events:
106
+ writer.writerow(event)
107
+ content = buf.getvalue()
108
+ else:
109
+ print_error(f"Unsupported format: {fmt}. Use 'json' or 'csv'.")
110
+ raise typer.Exit(1) from None
111
+
112
+ if output_file:
113
+ with open(output_file, "w", encoding="utf-8") as f:
114
+ f.write(content)
115
+ print_success(f"Exported {len(events)} entries to {output_file}.")
116
+ else:
117
+ sys.stdout.write(content)
118
+ if not content.endswith("\n"):
119
+ sys.stdout.write("\n")