humanbound-cli 0.3.2__tar.gz → 0.3.3__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 (53) hide show
  1. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/PKG-INFO +1 -1
  2. humanbound_cli-0.3.3/humanbound_cli/__init__.py +8 -0
  3. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/client.py +19 -7
  4. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/api_keys.py +2 -2
  5. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/auth.py +12 -2
  6. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/campaigns.py +1 -1
  7. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/experiments.py +106 -3
  8. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/findings.py +2 -2
  9. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/logs.py +1 -1
  10. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/members.py +2 -2
  11. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/providers.py +1 -1
  12. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/test.py +13 -13
  13. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/main.py +16 -1
  14. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/PKG-INFO +1 -1
  15. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/pyproject.toml +1 -1
  16. humanbound_cli-0.3.2/humanbound_cli/__init__.py +0 -3
  17. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/LICENSE +0 -0
  18. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/README.md +0 -0
  19. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/__init__.py +0 -0
  20. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/coverage.py +0 -0
  21. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/docs.py +0 -0
  22. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/guardrails.py +0 -0
  23. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/init.py +0 -0
  24. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/orgs.py +0 -0
  25. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/posture.py +0 -0
  26. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/projects.py +0 -0
  27. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/scan.py +0 -0
  28. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/upload_logs.py +0 -0
  29. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/config.py +0 -0
  30. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/exceptions.py +0 -0
  31. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/extractors/__init__.py +0 -0
  32. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/extractors/openapi.py +0 -0
  33. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/extractors/repo.py +0 -0
  34. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/pytest_plugin/__init__.py +0 -0
  35. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/pytest_plugin/fixtures.py +0 -0
  36. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/pytest_plugin/report.py +0 -0
  37. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/report.py +0 -0
  38. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/__init__.py +0 -0
  39. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/config_builder.py +0 -0
  40. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/local_server.py +0 -0
  41. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/runtime_detector.py +0 -0
  42. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/tunnel_client.py +0 -0
  43. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/SOURCES.txt +0 -0
  44. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/dependency_links.txt +0 -0
  45. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/entry_points.txt +0 -0
  46. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/requires.txt +0 -0
  47. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/top_level.txt +0 -0
  48. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/relay/relay.py +0 -0
  49. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/setup.cfg +0 -0
  50. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/tests/__init__.py +0 -0
  51. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/tests/cli_integration_test.py +0 -0
  52. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/tests/conftest.py +0 -0
  53. {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/tests/test_cli_commands.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: humanbound-cli
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Humanbound CLI - command line interface for AI agent security testing.
5
5
  Author-email: Kostas Siabanis <hello@humanbound.ai>, Demetris Gerogiannis <hello@humanbound.ai>
6
6
  License: Apache-2.0
@@ -0,0 +1,8 @@
1
+ """Humanbound CLI - command line interface for AI agent security testing."""
2
+
3
+ from importlib.metadata import version as _v
4
+
5
+ try:
6
+ __version__ = _v("humanbound-cli")
7
+ except Exception:
8
+ __version__ = "dev"
@@ -471,16 +471,24 @@ class HumanboundClient:
471
471
 
472
472
  def _exchange_for_api_token(self) -> None:
473
473
  """Exchange Auth0 token for API session token."""
474
- response = requests.get(
475
- f"{self.base_url}/auth",
476
- headers={"Authorization": f"Bearer {self._auth0_token}"},
477
- timeout=DEFAULT_TIMEOUT,
478
- )
474
+ try:
475
+ response = requests.get(
476
+ f"{self.base_url}/auth",
477
+ headers={"Authorization": f"Bearer {self._auth0_token}"},
478
+ timeout=DEFAULT_TIMEOUT,
479
+ )
480
+ except requests.ConnectionError:
481
+ raise AuthenticationError(f"Could not connect to {self.base_url}. Is the server running?")
482
+ except requests.Timeout:
483
+ raise AuthenticationError(f"Connection to {self.base_url} timed out.")
479
484
 
480
485
  if response.status_code != 200:
481
- raise AuthenticationError(f"API authentication failed: {response.text}")
486
+ raise AuthenticationError(f"API authentication failed ({response.status_code}): {response.text}")
482
487
 
483
- data = response.json()
488
+ try:
489
+ data = response.json()
490
+ except Exception:
491
+ raise AuthenticationError(f"API returned invalid response from {self.base_url}/auth")
484
492
  self._api_token = data.get("access_token") or data.get("token")
485
493
  if not self._api_token:
486
494
  raise AuthenticationError("No access token in API response")
@@ -537,6 +545,10 @@ class HumanboundClient:
537
545
  self._username = credentials.get("username")
538
546
  self._email = credentials.get("email")
539
547
  self._default_organisation_id = credentials.get("default_organisation_id")
548
+ # Restore saved base_url unless explicitly overridden via --base-url or env var
549
+ saved_url = credentials.get("base_url")
550
+ if saved_url and self.base_url == get_base_url().rstrip("/"):
551
+ self.base_url = saved_url.rstrip("/")
540
552
 
541
553
  def _load_credentials_file(self) -> dict:
542
554
  """Load credentials file."""
@@ -54,7 +54,7 @@ def list_keys(as_json: bool):
54
54
  return
55
55
 
56
56
  table = Table(title="API Keys")
57
- table.add_column("ID", style="dim", width=10)
57
+ table.add_column("ID", style="dim")
58
58
  table.add_column("Name", style="bold")
59
59
  table.add_column("Scopes")
60
60
  table.add_column("Active", justify="center")
@@ -67,7 +67,7 @@ def list_keys(as_json: bool):
67
67
  prefix = (key_val[:12] + "...") if len(str(key_val)) > 12 else str(key_val)
68
68
 
69
69
  table.add_row(
70
- str(key.get("id", ""))[:10],
70
+ str(key.get("id", "")),
71
71
  key.get("name", ""),
72
72
  key.get("scopes", ""),
73
73
  active,
@@ -5,6 +5,7 @@ from rich.console import Console
5
5
  from rich.panel import Panel
6
6
 
7
7
  from ..client import HumanboundClient
8
+ from ..config import DEFAULT_BASE_URL
8
9
  from ..exceptions import AuthenticationError, APIError
9
10
 
10
11
  console = Console()
@@ -50,9 +51,14 @@ def login(base_url: str, port: int, force: bool):
50
51
  except Exception as e:
51
52
  org_display = f"{client.default_organisation_id} (could not resolve name: {e})"
52
53
 
54
+ base_url_line = ""
55
+ if client.base_url.rstrip("/") != DEFAULT_BASE_URL.rstrip("/"):
56
+ base_url_line = f"Base URL: {client.base_url}\n"
57
+
53
58
  console.print(Panel(
54
59
  f"[green]Login successful![/green]\n\n"
55
60
  f"User: {client.username or 'unknown'}\n"
61
+ f"{base_url_line}"
56
62
  f"Organisation: {org_display}",
57
63
  title="Humanbound",
58
64
  ))
@@ -109,7 +115,7 @@ def logout(revoke: bool, port: int):
109
115
  finally:
110
116
  server.server_close()
111
117
 
112
- console.print("[green]Browser session revoked.[/green]")
118
+ console.print("[green]Browser session revoked. Base URL reset to default.[/green]")
113
119
  else:
114
120
  console.print(
115
121
  "[dim]Note: Your browser may still have an active Auth0 session. "
@@ -135,11 +141,15 @@ def whoami():
135
141
  except Exception as e:
136
142
  org_display = f"{client.organisation_id} [dim](could not resolve name: {e})[/dim]"
137
143
 
144
+ base_url_line = ""
145
+ if client.base_url.rstrip("/") != DEFAULT_BASE_URL.rstrip("/"):
146
+ base_url_line = f"Base URL: {client.base_url}\n"
147
+
138
148
  console.print(Panel(
139
149
  f"[green]Authenticated[/green]\n\n"
140
150
  f"User: {client.username or '[dim]unknown[/dim]'}\n"
141
151
  f"Email: {client.email or '[dim]unknown[/dim]'}\n"
142
- f"Base URL: {client.base_url}\n"
152
+ f"{base_url_line}"
143
153
  f"Organisation: {org_display}\n"
144
154
  f"Project: {client.project_id or '[dim]not set[/dim]'}",
145
155
  title="Humanbound Status",
@@ -156,7 +156,7 @@ def break_campaign(force: bool):
156
156
  return
157
157
 
158
158
  if not force:
159
- if not Confirm.ask(f"Break campaign [bold]{campaign_id[:10]}...[/bold]? Running experiments will be stopped"):
159
+ if not Confirm.ask(f"Break campaign [bold]{campaign_id}[/bold]? Running experiments will be stopped"):
160
160
  console.print("[dim]Cancelled.[/dim]")
161
161
  return
162
162
 
@@ -142,13 +142,20 @@ def show_experiment(experiment_id: str):
142
142
 
143
143
 
144
144
  @experiments_group.command("status")
145
- @click.argument("experiment_id")
145
+ @click.argument("experiment_id", required=False)
146
146
  @click.option("--watch", "-w", is_flag=True, help="Watch status until completion")
147
147
  @click.option("--interval", default=10, help="Polling interval in seconds (with --watch)")
148
- def experiment_status(experiment_id: str, watch: bool, interval: int):
148
+ @click.option("--all", "show_all", is_flag=True, help="Show status of all project experiments (polls every 60s)")
149
+ def experiment_status(experiment_id: str, watch: bool, interval: int, show_all: bool):
149
150
  """Check experiment status.
150
151
 
151
- EXPERIMENT_ID: Experiment UUID.
152
+ EXPERIMENT_ID: Experiment UUID (optional with --all).
153
+
154
+ \b
155
+ Examples:
156
+ hb experiments status <id> # One-shot status
157
+ hb experiments status <id> --watch # Poll single experiment
158
+ hb experiments status --all # Dashboard of all experiments (polls 60s)
152
159
  """
153
160
  client = HumanboundClient()
154
161
 
@@ -156,6 +163,14 @@ def experiment_status(experiment_id: str, watch: bool, interval: int):
156
163
  console.print("[yellow]No project selected.[/yellow]")
157
164
  raise SystemExit(1)
158
165
 
166
+ if show_all:
167
+ _poll_all_experiments(client)
168
+ return
169
+
170
+ if not experiment_id:
171
+ console.print("[red]Provide an experiment ID or use --all.[/red]")
172
+ raise SystemExit(1)
173
+
159
174
  try:
160
175
  if not watch:
161
176
  status = client.get_experiment_status(experiment_id)
@@ -201,6 +216,94 @@ def experiment_status(experiment_id: str, watch: bool, interval: int):
201
216
  console.print("\n[yellow]Stopped watching.[/yellow]")
202
217
 
203
218
 
219
+ ACTIVE_STATUSES = ["Running", "Generating", "Generated", "Pending"]
220
+
221
+
222
+ def _poll_all_experiments(client: HumanboundClient):
223
+ """Poll all project experiments every 60s until none are active."""
224
+ try:
225
+ cycle = 0
226
+ while True:
227
+ # Fetch all experiments (up to 200)
228
+ all_exps = []
229
+ page = 1
230
+ while True:
231
+ response = client.list_experiments(page=page, size=100)
232
+ all_exps.extend(response.get("data", []))
233
+ if not response.get("has_next_page"):
234
+ break
235
+ page += 1
236
+
237
+ if not all_exps:
238
+ console.print("[yellow]No experiments found.[/yellow]")
239
+ return
240
+
241
+ active = [e for e in all_exps if e.get("status") in ACTIVE_STATUSES]
242
+ finished = [e for e in all_exps if e.get("status") == "Finished"]
243
+ failed = [e for e in all_exps if e.get("status") == "Failed"]
244
+
245
+ # Clear screen on subsequent cycles
246
+ if cycle > 0:
247
+ console.clear()
248
+
249
+ timestamp = time.strftime("%H:%M:%S")
250
+ console.print(f"[bold]Project Experiments[/bold] [dim]{timestamp}[/dim]\n")
251
+ console.print(
252
+ f" Total: {len(all_exps)} "
253
+ f"[green]Finished: {len(finished)}[/green] "
254
+ f"[yellow]Active: {len(active)}[/yellow] "
255
+ f"[red]Failed: {len(failed)}[/red]\n"
256
+ )
257
+
258
+ table = Table()
259
+ table.add_column("Name", style="bold", max_width=35)
260
+ table.add_column("Status", width=12)
261
+ table.add_column("Category", width=20)
262
+ table.add_column("ID", style="dim")
263
+
264
+ for exp in all_exps:
265
+ status = exp.get("status", "Unknown")
266
+ status_style = {
267
+ "Finished": "[green]Finished[/green]",
268
+ "Running": "[yellow]Running[/yellow]",
269
+ "Failed": "[red]Failed[/red]",
270
+ "Generating": "[cyan]Generating[/cyan]",
271
+ "Generated": "[blue]Generated[/blue]",
272
+ "Pending": "[dim]Pending[/dim]",
273
+ "Terminated": "[red]Terminated[/red]",
274
+ }.get(status, status)
275
+
276
+ cat = exp.get("test_category", "")
277
+ if "/" in cat:
278
+ cat = cat.split("/")[-1]
279
+
280
+ table.add_row(
281
+ exp.get("name", ""),
282
+ status_style,
283
+ cat,
284
+ exp.get("id", ""),
285
+ )
286
+
287
+ console.print(table)
288
+
289
+ if not active:
290
+ console.print(f"\n[green]All experiments completed.[/green]")
291
+ return
292
+
293
+ console.print(f"\n[dim]{len(active)} active — polling every 60s (Ctrl+C to stop)[/dim]")
294
+ time.sleep(60)
295
+ cycle += 1
296
+
297
+ except NotAuthenticatedError:
298
+ console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
299
+ raise SystemExit(1)
300
+ except APIError as e:
301
+ console.print(f"[red]Error:[/red] {e}")
302
+ raise SystemExit(1)
303
+ except KeyboardInterrupt:
304
+ console.print("\n[yellow]Stopped polling.[/yellow]")
305
+
306
+
204
307
  def _print_status(status: dict):
205
308
  """Print experiment status."""
206
309
  current_status = status.get("status", "Unknown")
@@ -76,7 +76,7 @@ def findings_group(ctx, status, severity, page, size, as_json):
76
76
  return
77
77
 
78
78
  table = Table(title="Findings")
79
- table.add_column("ID", style="dim", width=10)
79
+ table.add_column("ID", style="dim")
80
80
  table.add_column("Title", max_width=40)
81
81
  table.add_column("Severity", width=10)
82
82
  table.add_column("Status", width=10)
@@ -89,7 +89,7 @@ def findings_group(ctx, status, severity, page, size, as_json):
89
89
  stat = str(finding.get("status", "")).lower()
90
90
 
91
91
  table.add_row(
92
- str(finding.get("id", ""))[:10],
92
+ str(finding.get("id", "")),
93
93
  finding.get("title", finding.get("description", ""))[:40],
94
94
  SEVERITY_STYLES.get(sev, sev),
95
95
  STATUS_STYLES.get(stat, stat),
@@ -127,7 +127,7 @@ def _show_table(client: HumanboundClient, experiment_id: str, verdict: str, page
127
127
  return
128
128
 
129
129
  table = Table(title=f"Experiment Logs (page {page})")
130
- table.add_column("ID", style="dim", width=10)
130
+ table.add_column("ID", style="dim")
131
131
  table.add_column("Verdict", width=6)
132
132
  table.add_column("Severity", width=8)
133
133
  table.add_column("Category", width=15)
@@ -55,7 +55,7 @@ def list_members(as_json: bool):
55
55
  return
56
56
 
57
57
  table = Table(title="Organisation Members")
58
- table.add_column("ID", style="dim", width=10)
58
+ table.add_column("ID", style="dim")
59
59
  table.add_column("Email", style="bold")
60
60
  table.add_column("Username")
61
61
  table.add_column("Role")
@@ -70,7 +70,7 @@ def list_members(as_json: bool):
70
70
  }.get(str(role).lower(), str(role))
71
71
 
72
72
  table.add_row(
73
- str(member.get("id", ""))[:10],
73
+ str(member.get("id", "")),
74
74
  member.get("email", ""),
75
75
  member.get("username", ""),
76
76
  role_style,
@@ -47,7 +47,7 @@ def list_providers():
47
47
  return
48
48
 
49
49
  table = Table(title="Model Providers")
50
- table.add_column("ID", style="dim", width=12)
50
+ table.add_column("ID", style="dim")
51
51
  table.add_column("Name", style="bold")
52
52
  table.add_column("Model", width=25)
53
53
  table.add_column("Default", justify="center")
@@ -13,13 +13,8 @@ from ..exceptions import NotAuthenticatedError, APIError
13
13
 
14
14
  console = Console()
15
15
 
16
- # Available test categories
17
- TEST_CATEGORIES = [
18
- "humanbound/adversarial/owasp_single_turn",
19
- "humanbound/adversarial/owasp_multi_turn",
20
- "humanbound/adversarial/owasp_agentic_multi_turn",
21
- "humanbound/behavioral/behavioral",
22
- ]
16
+ # Default test category
17
+ DEFAULT_TEST_CATEGORY = "humanbound/adversarial/owasp_multi_turn"
23
18
 
24
19
  # Testing levels (must match backend TestingLevel enum)
25
20
  # unit (~20 min), system (~45 min), acceptance (~90 min)
@@ -62,7 +57,7 @@ def _load_integration(value: str) -> dict:
62
57
  raise SystemExit(1)
63
58
 
64
59
  try:
65
- return json.loads(value)
60
+ return json.loads(value.strip())
66
61
  except json.JSONDecodeError:
67
62
  console.print(f"[red]--endpoint must be a JSON string or path to a JSON file.[/red]")
68
63
  console.print("[dim]Example: --endpoint ./bot-config.json[/dim]")
@@ -73,9 +68,8 @@ def _load_integration(value: str) -> dict:
73
68
  @click.command("test")
74
69
  @click.option(
75
70
  "--test-category", "-t",
76
- type=click.Choice(TEST_CATEGORIES, case_sensitive=False),
77
- default="humanbound/adversarial/owasp_multi_turn",
78
- help="Test category to run"
71
+ default=DEFAULT_TEST_CATEGORY,
72
+ help="Test category to run (e.g. humanbound/adversarial/owasp_multi_turn, humanbound/behavioral/qa)"
79
73
  )
80
74
  @click.option(
81
75
  "--testing-level", "-l",
@@ -87,6 +81,11 @@ def _load_integration(value: str) -> dict:
87
81
  "--name", "-n",
88
82
  help="Experiment name (auto-generated if not provided)"
89
83
  )
84
+ @click.option(
85
+ "--description", "-d",
86
+ default="",
87
+ help="Experiment description"
88
+ )
90
89
  @click.option(
91
90
  "--lang",
92
91
  default="english",
@@ -117,8 +116,8 @@ def _load_integration(value: str) -> dict:
117
116
  type=click.Choice(["critical", "high", "medium", "low", "any"]),
118
117
  help="Exit with error if findings of this severity or higher are found"
119
118
  )
120
- def test_command(test_category: str, testing_level: str, name: str, lang: str,
121
- provider_id: str, endpoint: str,
119
+ def test_command(test_category: str, testing_level: str, name: str, description: str,
120
+ lang: str, provider_id: str, endpoint: str,
122
121
  no_auto_start: bool,
123
122
  wait: bool, fail_on: str):
124
123
  """Run security tests on the current project.
@@ -185,6 +184,7 @@ def test_command(test_category: str, testing_level: str, name: str, lang: str,
185
184
  # Create experiment
186
185
  experiment_data = {
187
186
  "name": name,
187
+ "description": description,
188
188
  "test_category": test_category,
189
189
  "testing_level": testing_level,
190
190
  "lang": lang,
@@ -3,6 +3,7 @@
3
3
  import click
4
4
  from rich.console import Console
5
5
 
6
+ from . import __version__
6
7
  from .client import HumanboundClient
7
8
  from .config import get_base_url
8
9
 
@@ -35,6 +36,7 @@ def get_client() -> HumanboundClient:
35
36
 
36
37
 
37
38
  @click.group()
39
+ @click.version_option(version=__version__, prog_name="hb")
38
40
  @click.option(
39
41
  "--base-url",
40
42
  envvar="HUMANBOUND_BASE_URL",
@@ -149,12 +151,24 @@ def switch_org(org_id: str):
149
151
  @cli.command("status")
150
152
  @click.argument("experiment_id", required=False)
151
153
  @click.option("--watch", "-w", is_flag=True, help="Watch status until completion")
154
+ @click.option("--all", "show_all", is_flag=True, help="Show all project experiments (polls every 60s)")
152
155
  @click.pass_context
153
- def status_alias(ctx, experiment_id: str, watch: bool):
156
+ def status_alias(ctx, experiment_id: str, watch: bool, show_all: bool):
154
157
  """Check experiment status (alias for 'experiments status').
155
158
 
156
159
  If no experiment_id is provided, shows the most recent experiment.
160
+ Use --all to see a dashboard of all project experiments.
157
161
  """
162
+ if show_all:
163
+ ctx.invoke(
164
+ experiments.experiment_status,
165
+ experiment_id=None,
166
+ watch=False,
167
+ interval=10,
168
+ show_all=True,
169
+ )
170
+ return
171
+
158
172
  client = HumanboundClient()
159
173
 
160
174
  if not experiment_id:
@@ -180,6 +194,7 @@ def status_alias(ctx, experiment_id: str, watch: bool):
180
194
  experiment_id=experiment_id,
181
195
  watch=watch,
182
196
  interval=10,
197
+ show_all=False,
183
198
  )
184
199
 
185
200
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: humanbound-cli
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Humanbound CLI - command line interface for AI agent security testing.
5
5
  Author-email: Kostas Siabanis <hello@humanbound.ai>, Demetris Gerogiannis <hello@humanbound.ai>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "humanbound-cli"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  authors = [
9
9
  { name="Kostas Siabanis", email="hello@humanbound.ai" },
10
10
  { name="Demetris Gerogiannis", email="hello@humanbound.ai" },
@@ -1,3 +0,0 @@
1
- """Humanbound CLI - command line interface for AI agent security testing."""
2
-
3
- __version__ = "0.2.0"
File without changes
File without changes
File without changes