qbraid-cli 0.13.1__tar.gz → 0.13.2__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 (64) hide show
  1. {qbraid_cli-0.13.1/qbraid_cli.egg-info → qbraid_cli-0.13.2}/PKG-INFO +1 -1
  2. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/pyproject.toml +1 -1
  3. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/admin/app.py +3 -3
  4. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/admin/validation.py +2 -1
  5. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/agents/app.py +604 -4
  6. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/envs/app.py +90 -25
  7. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/lab/app.py +5 -3
  8. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2/qbraid_cli.egg-info}/PKG-INFO +1 -1
  9. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/LICENSE +0 -0
  10. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/README.md +0 -0
  11. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/__init__.py +0 -0
  12. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/_build_info.py +0 -0
  13. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/_version.py +0 -0
  14. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/account/__init__.py +0 -0
  15. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/account/app.py +0 -0
  16. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/admin/__init__.py +0 -0
  17. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/admin/headers.py +0 -0
  18. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/agents/__init__.py +0 -0
  19. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/compute/__init__.py +0 -0
  20. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/compute/app.py +0 -0
  21. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/compute/profiles.py +0 -0
  22. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/compute/server.py +0 -0
  23. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/compute/ssh.py +0 -0
  24. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/configure/__init__.py +0 -0
  25. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/configure/actions.py +0 -0
  26. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/configure/app.py +0 -0
  27. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/devices/__init__.py +0 -0
  28. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/devices/app.py +0 -0
  29. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/devices/validation.py +0 -0
  30. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/envs/__init__.py +0 -0
  31. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/envs/activate.py +0 -0
  32. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/envs/create.py +0 -0
  33. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/envs/data_handling.py +0 -0
  34. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/envs/groups.py +0 -0
  35. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/envs/publish.py +0 -0
  36. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/envs/registry.py +0 -0
  37. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/envs/share.py +0 -0
  38. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/exceptions.py +0 -0
  39. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/handlers.py +0 -0
  40. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/jobs/__init__.py +0 -0
  41. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/jobs/app.py +0 -0
  42. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/jobs/groups.py +0 -0
  43. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/kernels/__init__.py +0 -0
  44. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/kernels/app.py +0 -0
  45. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/lab/__init__.py +0 -0
  46. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/main.py +0 -0
  47. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/pip/__init__.py +0 -0
  48. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/pip/app.py +0 -0
  49. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/pip/hooks.py +0 -0
  50. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/projects/__init__.py +0 -0
  51. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/projects/app.py +0 -0
  52. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/py.typed +0 -0
  53. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/skills/__init__.py +0 -0
  54. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/skills/app.py +0 -0
  55. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/troubleshoot/__init__.py +0 -0
  56. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli/troubleshoot/app.py +0 -0
  57. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli.egg-info/SOURCES.txt +0 -0
  58. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli.egg-info/dependency_links.txt +0 -0
  59. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli.egg-info/entry_points.txt +0 -0
  60. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli.egg-info/requires.txt +0 -0
  61. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/qbraid_cli.egg-info/top_level.txt +0 -0
  62. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/setup.cfg +0 -0
  63. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/tests/test_cli_e2e.py +0 -0
  64. {qbraid_cli-0.13.1 → qbraid_cli-0.13.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.13.1
3
+ Version: 0.13.2
4
4
  Summary: Command Line Interface for interacting with all parts of the qBraid platform.
5
5
  Author-email: qBraid Development Team <contact@qbraid.com>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qbraid-cli"
7
- version = "0.13.1"
7
+ version = "0.13.2"
8
8
  description = "Command Line Interface for interacting with all parts of the qBraid platform."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "qBraid Development Team", email = "contact@qbraid.com" }]
@@ -6,7 +6,7 @@ Module defining commands in the 'qbraid admin' namespace.
6
6
 
7
7
  """
8
8
 
9
- from typing import Optional
9
+ from typing import List, Optional
10
10
 
11
11
  import typer
12
12
 
@@ -22,11 +22,11 @@ admin_app = typer.Typer(
22
22
 
23
23
  @admin_app.command(name="headers")
24
24
  def admin_headers(
25
- src_paths: list[str] = typer.Argument(
25
+ src_paths: List[str] = typer.Argument(
26
26
  ..., help="Source file or directory paths to verify.", callback=validate_paths_exist
27
27
  ),
28
28
  header_type: HeaderType = typer.Option("default", "--type", "-t", help="Type of header to use."),
29
- skip_files: list[str] = typer.Option([], "--skip", "-s", help="Files to skip during verification."),
29
+ skip_files: List[str] = typer.Option([], "--skip", "-s", help="Files to skip during verification."),
30
30
  fix: bool = typer.Option(
31
31
  False, "--fix", "-f", help="Whether to fix the headers instead of just verifying."
32
32
  ),
@@ -7,13 +7,14 @@ Module for validating command arguments for qBraid admin commands.
7
7
  """
8
8
 
9
9
  import os
10
+ from typing import List
10
11
 
11
12
  import typer
12
13
 
13
14
  from qbraid_cli.handlers import _format_list_items
14
15
 
15
16
 
16
- def validate_paths_exist(paths: list[str]) -> list[str]:
17
+ def validate_paths_exist(paths: List[str]) -> List[str]:
17
18
  """Verifies that each path in the provided list exists."""
18
19
  non_existent_paths = [path for path in paths if not os.path.exists(path)]
19
20
  if non_existent_paths:
@@ -9,7 +9,7 @@ import signal
9
9
  import time
10
10
  from datetime import datetime
11
11
  from pathlib import Path
12
- from typing import Optional
12
+ from typing import List, Optional
13
13
 
14
14
  import click
15
15
  import typer
@@ -49,6 +49,20 @@ hooks_app = typer.Typer(
49
49
 
50
50
  agents_app.add_typer(hooks_app, name="hooks")
51
51
 
52
+ inbox_app = typer.Typer(
53
+ help=(
54
+ "Read messages sent to this agent's inbox.\n\n"
55
+ "Follow, filter unread, and mark-read messages sent to this agent\n"
56
+ "session's inbox. Inboxes are per-session JSONL files at\n"
57
+ "~/.qbraid/inboxes/. Every agent session has one; peers use\n"
58
+ "``qbraid agents send-to`` to write to it."
59
+ ),
60
+ no_args_is_help=False,
61
+ invoke_without_command=True,
62
+ )
63
+
64
+ agents_app.add_typer(inbox_app, name="inbox")
65
+
52
66
  console = Console()
53
67
 
54
68
 
@@ -267,7 +281,7 @@ def agents_deregister():
267
281
  def agents_list(
268
282
  tool: Optional[str] = typer.Option(None, "--tool", "-t", help="Filter by tool (claude, codeq, codex)"),
269
283
  status: Optional[str] = typer.Option(None, "--status", help="Filter by status"),
270
- tag: Optional[list[str]] = typer.Option(None, "--tag", help="Filter by tag (repeatable)"),
284
+ tag: Optional[List[str]] = typer.Option(None, "--tag", help="Filter by tag (repeatable)"),
271
285
  since: Optional[str] = typer.Option(None, "--since", help="Time window (e.g., '1h', '30m', '2d')"),
272
286
  compute: Optional[str] = typer.Option(
273
287
  None, "--compute", "-c", help="SSH alias for remote compute (runs list on remote)"
@@ -704,7 +718,7 @@ def agents_status(
704
718
  @agents_app.command("update")
705
719
  def agents_update(
706
720
  session_id: str = typer.Argument(..., help="Session ID (full or prefix)"),
707
- tags: Optional[list[str]] = typer.Option(
721
+ tags: Optional[List[str]] = typer.Option(
708
722
  None, "--tag", help="Tag to add (can be repeated)", show_default=False
709
723
  ),
710
724
  name: Optional[str] = typer.Option(None, "--name", help="Set terminal name for the session"),
@@ -1098,7 +1112,7 @@ def agents_launch(
1098
1112
  ),
1099
1113
  parent: Optional[str] = typer.Option(None, "--parent", help="Parent session ID"),
1100
1114
  team: Optional[str] = typer.Option(None, "--team", help="Team ID"),
1101
- tag: Optional[list[str]] = typer.Option(None, "--tag", help="Tags (repeatable)"),
1115
+ tag: Optional[List[str]] = typer.Option(None, "--tag", help="Tags (repeatable)"),
1102
1116
  compute: Optional[str] = typer.Option(
1103
1117
  None, "--compute", "-c", help="SSH alias for remote compute server"
1104
1118
  ),
@@ -2261,3 +2275,589 @@ def hooks_uninstall():
2261
2275
  except Exception as e:
2262
2276
  handle_error(message=str(e))
2263
2277
  raise typer.Exit(code=1)
2278
+
2279
+
2280
+ # =============================================================================
2281
+ # Messaging: send-to / inbox / inbox mark-read
2282
+ # =============================================================================
2283
+
2284
+
2285
+ def _resolve_from_session(explicit: Optional[str]) -> str:
2286
+ """Determine the sender session id.
2287
+
2288
+ Priority: explicit --from > QBRAID_SESSION_ID env var (set by the qBraid
2289
+ launcher for every agent it spawns) > fail with a helpful error.
2290
+ """
2291
+ if explicit:
2292
+ return explicit
2293
+ env_sid = os.environ.get("QBRAID_SESSION_ID")
2294
+ if env_sid:
2295
+ return env_sid
2296
+ console.print(
2297
+ "[red]No sender session id available.[/red] "
2298
+ "Pass --from <session-id>, or run this inside a qBraid-managed "
2299
+ "agent shell (which sets QBRAID_SESSION_ID)."
2300
+ )
2301
+ raise typer.Exit(code=1)
2302
+
2303
+
2304
+ def _resolve_session_id(
2305
+ explicit: Optional[str], registry=None, allow_missing_in_registry: bool = True
2306
+ ) -> str:
2307
+ """Determine which session to operate on (for inbox read/mark-read).
2308
+
2309
+ Priority:
2310
+ 1. ``explicit`` (from --session): full id, prefix, or tmux/terminal name.
2311
+ Resolved via the registry so prefixes and names work (fixes Persona A's
2312
+ "``inbox --session <prefix>`` returned empty" finding).
2313
+ 2. ``$QBRAID_SESSION_ID`` env var (set by the launcher for managed shells).
2314
+ 3. Fail with a helpful error.
2315
+
2316
+ When ``allow_missing_in_registry`` is True (the default for inbox ops),
2317
+ a name/prefix that doesn't resolve is returned as-is — the inbox file
2318
+ might still exist even if the registry entry has been pruned. This
2319
+ preserves the "list old inbox that outlived its session" case.
2320
+ """
2321
+ if explicit:
2322
+ if registry is not None:
2323
+ match = _find_session_by_prefix(registry, explicit)
2324
+ if match is not None:
2325
+ return match.session_id
2326
+ if allow_missing_in_registry:
2327
+ return explicit
2328
+ console.print(
2329
+ f"[red]No session matched '{explicit}'.[/red] "
2330
+ "Try 'qbraid agents list' to see available sessions."
2331
+ )
2332
+ raise typer.Exit(code=1)
2333
+
2334
+ env_sid = os.environ.get("QBRAID_SESSION_ID")
2335
+ if env_sid:
2336
+ return env_sid
2337
+ console.print(
2338
+ "[red]No session id.[/red] Pass --session <id>, or run this inside a "
2339
+ "qBraid-managed agent shell (which sets QBRAID_SESSION_ID)."
2340
+ )
2341
+ raise typer.Exit(code=1)
2342
+
2343
+
2344
+ def _resolve_target_session(registry, identifier: str) -> str:
2345
+ """Resolve a target-session identifier (full id / prefix / name) to a
2346
+ session_id. Prints suggestions on miss.
2347
+ """
2348
+ match = _find_session_by_prefix(registry, identifier)
2349
+ if match is None:
2350
+ console.print(
2351
+ f"[red]No session matched '{identifier}'.[/red] "
2352
+ "Try 'qbraid agents list' to see available sessions."
2353
+ )
2354
+ raise typer.Exit(code=1)
2355
+ return match.session_id
2356
+
2357
+
2358
+ def _resolve_team_targets(registry, team_id: str, exclude_session_id: Optional[str] = None) -> list:
2359
+ """Return every session in a team, optionally excluding one (usually the sender).
2360
+
2361
+ Returns a list of ``AgentSession`` objects. Empty list if the team has no
2362
+ members other than the excluded one.
2363
+ """
2364
+ all_sessions = registry.load_registry().sessions
2365
+ members = [s for s in all_sessions if s.team_id == team_id and s.session_id != exclude_session_id]
2366
+ return members
2367
+
2368
+
2369
+ def _parse_target_csv(csv: str, registry) -> list[str]:
2370
+ """Parse ``--to A,B,C`` into a list of resolved session_ids.
2371
+
2372
+ Each token goes through ``_find_session_by_prefix`` — id, prefix, or
2373
+ tmux/terminal name. Fails the whole call if ANY token doesn't resolve
2374
+ (bulk sends are all-or-nothing to avoid partial delivery surprises).
2375
+ """
2376
+ tokens = [t.strip() for t in csv.split(",") if t.strip()]
2377
+ resolved: list[str] = []
2378
+ unresolved: list[str] = []
2379
+ for tok in tokens:
2380
+ match = _find_session_by_prefix(registry, tok)
2381
+ if match is None:
2382
+ unresolved.append(tok)
2383
+ else:
2384
+ resolved.append(match.session_id)
2385
+ if unresolved:
2386
+ console.print(
2387
+ f"[red]No session matched: {', '.join(unresolved)}[/red]. " "Broadcast aborted; nothing sent."
2388
+ )
2389
+ raise typer.Exit(code=1)
2390
+ return resolved
2391
+
2392
+
2393
+ def _deliver_to_many(
2394
+ targets: list[str],
2395
+ text: str,
2396
+ from_sid: str,
2397
+ message_type: str,
2398
+ registry,
2399
+ validate_permissions: bool,
2400
+ ) -> int:
2401
+ """Deliver one message to each target session_id; render results.
2402
+
2403
+ Returns a shell exit code: 0 = all delivered, 1 = zero delivered,
2404
+ 3 = partial success. Kept out of the command bodies so send-to and
2405
+ broadcast can share it without duplicating error rendering.
2406
+ """
2407
+ # pylint: disable-next=import-outside-toplevel
2408
+ from qbraid_core.services.agents import send_message
2409
+
2410
+ # pylint: disable-next=import-outside-toplevel
2411
+ from qbraid_core.services.agents.exceptions import AgentMessagingError
2412
+
2413
+ delivered: list[tuple[str, str]] = []
2414
+ failed: list[tuple[str, str]] = []
2415
+ for target_sid in targets:
2416
+ try:
2417
+ msg = send_message(
2418
+ from_session_id=from_sid,
2419
+ to_session_id=target_sid,
2420
+ text=text,
2421
+ message_type=message_type,
2422
+ registry=registry,
2423
+ validate_permissions=validate_permissions,
2424
+ )
2425
+ delivered.append((target_sid, msg.id))
2426
+ except AgentMessagingError as err:
2427
+ failed.append((target_sid, str(err)))
2428
+ except Exception as err: # pylint: disable=broad-exception-caught
2429
+ failed.append((target_sid, f"{type(err).__name__}: {err}"))
2430
+
2431
+ for sid, mid in delivered:
2432
+ console.print(
2433
+ f"[green]→[/green] Sent to [bold]{sid}[/bold] " f"[dim](id={mid}, type={message_type})[/dim]"
2434
+ )
2435
+ for sid, reason in failed:
2436
+ console.print(f"[red]✗[/red] [bold]{sid}[/bold]: {reason}")
2437
+
2438
+ if failed and not delivered:
2439
+ return 1
2440
+ if failed:
2441
+ return 3
2442
+ return 0
2443
+
2444
+
2445
+ @agents_app.command("send-to")
2446
+ def agents_send_to(
2447
+ to: str = typer.Argument(
2448
+ ...,
2449
+ help="Target session id (full or prefix), tmux window name, or terminal name.",
2450
+ ),
2451
+ text: str = typer.Argument(..., help="Message body."),
2452
+ from_: Optional[str] = typer.Option(
2453
+ None,
2454
+ "--from",
2455
+ help=(
2456
+ "Sender session id. Defaults to $QBRAID_SESSION_ID (set by the "
2457
+ "qBraid launcher for every managed agent shell)."
2458
+ ),
2459
+ ),
2460
+ message_type: str = typer.Option(
2461
+ "message",
2462
+ "--type",
2463
+ help=(
2464
+ "One of: message, task_assignment, status_update, shutdown_request. "
2465
+ "Controls how the recipient's UI/hooks categorize it."
2466
+ ),
2467
+ ),
2468
+ no_permission_check: bool = typer.Option(
2469
+ False,
2470
+ "--no-permission-check",
2471
+ help=(
2472
+ "Skip the sender→target permission check (agent-type rules from "
2473
+ "qbraid_core.services.agents.types.can_message). Use for testing."
2474
+ ),
2475
+ ),
2476
+ ) -> None:
2477
+ """Send a message to another agent's inbox.
2478
+
2479
+ For multi-recipient / team broadcast, use ``qbraid agents broadcast``.
2480
+
2481
+ Delivery is a JSONL append at ~/.qbraid/inboxes/<to-session-id>.jsonl
2482
+ (local host only — cross-host messaging isn't yet supported and will raise
2483
+ a clear error). Permission rules (leader/worker/observer/reviewer) are
2484
+ enforced by default.
2485
+ """
2486
+ try:
2487
+ # pylint: disable-next=import-outside-toplevel,unused-import
2488
+ from qbraid_core.services.agents import send_message # noqa: F401
2489
+ except ImportError as err:
2490
+ handle_error(message=f"qbraid-core messaging unavailable: {err}", include_traceback=False)
2491
+ raise typer.Exit(code=1) from err
2492
+
2493
+ registry = _get_registry()
2494
+ from_sid = _resolve_from_session(from_)
2495
+ to_sid = _resolve_target_session(registry, to)
2496
+
2497
+ code = _deliver_to_many(
2498
+ [to_sid],
2499
+ text,
2500
+ from_sid=from_sid,
2501
+ message_type=message_type,
2502
+ registry=registry,
2503
+ validate_permissions=not no_permission_check,
2504
+ )
2505
+ if code:
2506
+ raise typer.Exit(code=code)
2507
+
2508
+
2509
+ @agents_app.command("broadcast")
2510
+ def agents_broadcast(
2511
+ text: str = typer.Argument(..., help="Message body — delivered to every target."),
2512
+ team: Optional[str] = typer.Option(
2513
+ None,
2514
+ "--team",
2515
+ help=(
2516
+ "Deliver to every session with this team_id (excluding the sender). " "Use one of --team or --to."
2517
+ ),
2518
+ ),
2519
+ to_csv: Optional[str] = typer.Option(
2520
+ None,
2521
+ "--to",
2522
+ help=(
2523
+ "Comma-separated list of target session ids/prefixes/names for "
2524
+ "multi-recipient send. Use one of --team or --to."
2525
+ ),
2526
+ ),
2527
+ from_: Optional[str] = typer.Option(
2528
+ None,
2529
+ "--from",
2530
+ help="Sender session id. Defaults to $QBRAID_SESSION_ID.",
2531
+ ),
2532
+ message_type: str = typer.Option(
2533
+ "message",
2534
+ "--type",
2535
+ help=(
2536
+ "One of: message, task_assignment, status_update, shutdown_request. "
2537
+ "Controls how the recipient's UI/hooks categorize it."
2538
+ ),
2539
+ ),
2540
+ no_permission_check: bool = typer.Option(
2541
+ False,
2542
+ "--no-permission-check",
2543
+ help="Skip the sender→target permission check. Use for testing.",
2544
+ ),
2545
+ ) -> None:
2546
+ """Broadcast to multiple agent inboxes.
2547
+
2548
+ Two selectors — pick exactly one:
2549
+ * ``--team <team_id>`` — every session with that team_id (except sender)
2550
+ * ``--to A,B,C`` — explicit id/prefix/name list
2551
+
2552
+ Delivery is all-or-nothing on target resolution: if any target can't be
2553
+ resolved, NOTHING is sent (avoids partial-delivery surprises). Once
2554
+ resolved, per-target send failures are reported individually; a partial
2555
+ delivery exits with code 3.
2556
+ """
2557
+ try:
2558
+ # pylint: disable-next=import-outside-toplevel,unused-import
2559
+ from qbraid_core.services.agents import send_message # noqa: F401
2560
+ except ImportError as err:
2561
+ handle_error(message=f"qbraid-core messaging unavailable: {err}", include_traceback=False)
2562
+ raise typer.Exit(code=1) from err
2563
+
2564
+ modes_used = sum(bool(x) for x in (team, to_csv))
2565
+ if modes_used != 1:
2566
+ # Use typer's own error path so exit code is 2 (usage error) not 1.
2567
+ console.print("[red]Error:[/red] Use exactly one of --team or --to.")
2568
+ raise typer.Exit(code=2)
2569
+
2570
+ registry = _get_registry()
2571
+ from_sid = _resolve_from_session(from_)
2572
+
2573
+ if team is not None:
2574
+ members = _resolve_team_targets(registry, team, exclude_session_id=from_sid)
2575
+ if not members:
2576
+ handle_error(
2577
+ message=(
2578
+ f"No sessions found in team '{team}' (other than the sender). "
2579
+ "Check 'qbraid agents list' for team_id values."
2580
+ ),
2581
+ include_traceback=False,
2582
+ )
2583
+ raise typer.Exit(code=1)
2584
+ targets = [m.session_id for m in members]
2585
+ else:
2586
+ # to_csv is not None due to the modes_used check above
2587
+ assert to_csv is not None
2588
+ targets = _parse_target_csv(to_csv, registry)
2589
+
2590
+ code = _deliver_to_many(
2591
+ targets,
2592
+ text,
2593
+ from_sid=from_sid,
2594
+ message_type=message_type,
2595
+ registry=registry,
2596
+ validate_permissions=not no_permission_check,
2597
+ )
2598
+ if code:
2599
+ raise typer.Exit(code=code)
2600
+
2601
+
2602
+ def _render_messages_table(messages, session_id: str, full_ids: bool = False) -> None:
2603
+ """Render a list of AgentMessage to the console as a table.
2604
+
2605
+ Args:
2606
+ messages: list of AgentMessage.
2607
+ session_id: session whose inbox we're rendering (for the title).
2608
+ full_ids: when True, show full session_ids in the ``From`` column and
2609
+ the table title. Default is a 12-char prefix so the table stays
2610
+ narrow — but when ``list`` shows multiple sessions with the same
2611
+ prefix, the prefix version is ambiguous (Persona A + B finding).
2612
+ """
2613
+ if not messages:
2614
+ display_sid = session_id if full_ids else session_id[:12]
2615
+ console.print(f"[dim]Inbox for {display_sid} is empty.[/dim]")
2616
+ return
2617
+
2618
+ title_sid = session_id if full_ids else session_id[:12]
2619
+ table = Table(show_header=True, header_style="bold", title=f"Inbox: {title_sid}")
2620
+ table.add_column("Time", style="dim", no_wrap=True)
2621
+ table.add_column("From", style="cyan", no_wrap=full_ids)
2622
+ table.add_column("Type", style="yellow", no_wrap=True)
2623
+ table.add_column("R", justify="center", no_wrap=True)
2624
+ table.add_column("Message")
2625
+
2626
+ for msg in messages:
2627
+ ts = datetime.fromtimestamp(msg.timestamp).strftime("%m-%d %H:%M")
2628
+ read_marker = "[dim]✓[/dim]" if msg.read else "[bold red]•[/bold red]"
2629
+ from_display = msg.from_session if full_ids else msg.from_session[:12]
2630
+ table.add_row(
2631
+ ts,
2632
+ from_display,
2633
+ msg.type.value,
2634
+ read_marker,
2635
+ msg.text,
2636
+ )
2637
+ console.print(table)
2638
+
2639
+
2640
+ @inbox_app.callback()
2641
+ def inbox_default( # pylint: disable=too-many-arguments
2642
+ ctx: typer.Context,
2643
+ session: Optional[str] = typer.Option(
2644
+ None,
2645
+ "--session",
2646
+ "-s",
2647
+ help=(
2648
+ "Session whose inbox to read. Accepts full id, prefix, or "
2649
+ "tmux/terminal name (resolved via the registry). Defaults to "
2650
+ "$QBRAID_SESSION_ID."
2651
+ ),
2652
+ ),
2653
+ follow: bool = typer.Option(False, "--follow", "-f", help="Stream new messages as they arrive."),
2654
+ unread: bool = typer.Option(False, "--unread", "-u", help="Show only unread messages."),
2655
+ limit: int = typer.Option(50, "--limit", "-n", help="Max messages to show (most recent)."),
2656
+ mark_read_flag: bool = typer.Option(
2657
+ True,
2658
+ "--mark-read/--no-mark-read",
2659
+ help="Mark shown messages as read after display. Default on.",
2660
+ ),
2661
+ wide: bool = typer.Option(
2662
+ False,
2663
+ "--wide",
2664
+ "-w",
2665
+ help=(
2666
+ "Show full session ids in the From column and title (not the "
2667
+ "12-char prefix). Use when multiple senders share a prefix."
2668
+ ),
2669
+ ),
2670
+ ) -> None:
2671
+ """Read messages from an agent session's inbox.
2672
+
2673
+ Without a subcommand this reads the inbox (optionally streams with
2674
+ --follow). Use ``qbraid agents inbox mark-read`` to mark manually.
2675
+ """
2676
+ if ctx.invoked_subcommand is not None:
2677
+ return
2678
+
2679
+ try:
2680
+ # pylint: disable-next=import-outside-toplevel
2681
+ from qbraid_core.services.agents import mark_read, read_inbox, watch_inbox
2682
+ except ImportError as err:
2683
+ handle_error(message=f"qbraid-core messaging unavailable: {err}", include_traceback=False)
2684
+ raise typer.Exit(code=1) from err
2685
+
2686
+ # Registry-aware resolution so `--session <prefix>` and tmux names work
2687
+ # — Persona A's #1 finding was that inbox silently returned "empty"
2688
+ # for a valid prefix. Registry is only touched when an explicit
2689
+ # --session was passed, so the env-var-fallback code path stays cheap
2690
+ # and testable without mocking the registry.
2691
+ session_id = _resolve_session_id(session, registry=_get_registry() if session else None)
2692
+
2693
+ if follow:
2694
+ existing = read_inbox(session_id, unread_only=unread, limit=limit)
2695
+ _render_messages_table(existing, session_id, full_ids=wide)
2696
+ if mark_read_flag and existing:
2697
+ mark_read(session_id, message_ids=[m.id for m in existing if not m.read])
2698
+
2699
+ console.print("[dim]Watching for new messages… Ctrl-C to stop.[/dim]")
2700
+ try:
2701
+ for msg in watch_inbox(session_id):
2702
+ ts = datetime.fromtimestamp(msg.timestamp).strftime("%H:%M:%S")
2703
+ from_display = msg.from_session if wide else msg.from_session[:12]
2704
+ console.print(
2705
+ f"[dim]{ts}[/dim] "
2706
+ f"[cyan]{from_display}[/cyan] "
2707
+ f"[yellow]({msg.type.value})[/yellow] {msg.text}"
2708
+ )
2709
+ if mark_read_flag:
2710
+ mark_read(session_id, message_ids=[msg.id])
2711
+ except KeyboardInterrupt:
2712
+ console.print("[dim]Stopped watching.[/dim]")
2713
+ return
2714
+
2715
+ messages = read_inbox(session_id, unread_only=unread, limit=limit)
2716
+ _render_messages_table(messages, session_id, full_ids=wide)
2717
+
2718
+ if mark_read_flag and messages:
2719
+ unread_ids = [m.id for m in messages if not m.read]
2720
+ if unread_ids:
2721
+ n = mark_read(session_id, message_ids=unread_ids)
2722
+ if n:
2723
+ console.print(f"[dim]Marked {n} message(s) as read.[/dim]")
2724
+
2725
+
2726
+ @inbox_app.command("mark-read")
2727
+ def inbox_mark_read(
2728
+ session: Optional[str] = typer.Option(
2729
+ None,
2730
+ "--session",
2731
+ "-s",
2732
+ help=(
2733
+ "Session whose inbox to update. Accepts full id, prefix, or "
2734
+ "tmux/terminal name. Defaults to $QBRAID_SESSION_ID."
2735
+ ),
2736
+ ),
2737
+ message_id: Optional[List[str]] = typer.Option(
2738
+ None,
2739
+ "--message-id",
2740
+ "-m",
2741
+ help=(
2742
+ "Specific message id(s) to mark. May be passed multiple times. "
2743
+ "Omit to mark ALL unread messages in the inbox."
2744
+ ),
2745
+ ),
2746
+ ) -> None:
2747
+ """Mark inbox messages as read.
2748
+
2749
+ Without --message-id, marks every unread message in the session's inbox.
2750
+ """
2751
+ try:
2752
+ # pylint: disable-next=import-outside-toplevel
2753
+ from qbraid_core.services.agents import mark_read
2754
+ except ImportError as err:
2755
+ handle_error(message=f"qbraid-core messaging unavailable: {err}", include_traceback=False)
2756
+ raise typer.Exit(code=1) from err
2757
+
2758
+ session_id = _resolve_session_id(session, registry=_get_registry() if session else None)
2759
+ ids = list(message_id) if message_id else None
2760
+
2761
+ n = mark_read(session_id, message_ids=ids)
2762
+ console.print(f"[green]Marked {n} message(s) as read[/green] for {session_id[:12]}.")
2763
+
2764
+
2765
+ # =============================================================================
2766
+ # `qbraid agents team <id>` — leader-centric roster view
2767
+ # =============================================================================
2768
+
2769
+
2770
+ @agents_app.command("team")
2771
+ def agents_team(
2772
+ team_id: str = typer.Argument(..., help="Team identifier (matches AgentSession.team_id)."),
2773
+ wide: bool = typer.Option(
2774
+ False,
2775
+ "--wide",
2776
+ "-w",
2777
+ help="Show full session ids instead of the 12-char prefix.",
2778
+ ),
2779
+ ) -> None:
2780
+ """Show a team roster with unread counts.
2781
+
2782
+ Answers the leader-orchestrator question "who is on my team, how many
2783
+ unread messages do they have, and what was the last thing they heard from
2784
+ anyone?" — which today requires peeking each worker's inbox individually.
2785
+
2786
+ Rows are sorted with the current session first (identified via
2787
+ $QBRAID_SESSION_ID), then by agent_type (leader → worker → observer →
2788
+ reviewer), then by session_id.
2789
+ """
2790
+ try:
2791
+ # pylint: disable-next=import-outside-toplevel
2792
+ from qbraid_core.services.agents import read_inbox
2793
+ except ImportError as err:
2794
+ handle_error(message=f"qbraid-core messaging unavailable: {err}", include_traceback=False)
2795
+ raise typer.Exit(code=1) from err
2796
+
2797
+ registry = _get_registry()
2798
+ all_sessions = registry.load_registry().sessions
2799
+ members = [s for s in all_sessions if s.team_id == team_id]
2800
+
2801
+ if not members:
2802
+ console.print(
2803
+ f"[yellow]No sessions found with team_id '{team_id}'.[/yellow] "
2804
+ "Check 'qbraid agents list' for known team_ids."
2805
+ )
2806
+ raise typer.Exit(code=0)
2807
+
2808
+ me_sid = os.environ.get("QBRAID_SESSION_ID")
2809
+ role_order = {"leader": 0, "worker": 1, "reviewer": 2, "observer": 3}
2810
+
2811
+ def _sort_key(session):
2812
+ return (
2813
+ 0 if session.session_id == me_sid else 1,
2814
+ role_order.get(session.agent_type or "worker", 99),
2815
+ session.session_id,
2816
+ )
2817
+
2818
+ members.sort(key=_sort_key)
2819
+
2820
+ table = Table(
2821
+ show_header=True,
2822
+ header_style="bold",
2823
+ title=f"Team: {team_id} ({len(members)} member{'s' if len(members) != 1 else ''})",
2824
+ )
2825
+ table.add_column("You", justify="center", no_wrap=True)
2826
+ table.add_column("Session", style="cyan", no_wrap=wide)
2827
+ table.add_column("Type", style="yellow", no_wrap=True)
2828
+ table.add_column("Status", no_wrap=True)
2829
+ table.add_column("Unread", justify="right", no_wrap=True)
2830
+ table.add_column("Last msg from", style="dim")
2831
+ table.add_column("Last msg text", style="dim")
2832
+
2833
+ status_styles = {"idle": "green", "active": "green", "stopped": "red"}
2834
+
2835
+ for s in members:
2836
+ try:
2837
+ # Read up to 10K so the unread count reflects the full inbox
2838
+ # for realistic sizes (matches watch_inbox's cap). Members with
2839
+ # >10K messages will still undercount, but that's the same
2840
+ # boundary the follower uses.
2841
+ inbox = read_inbox(s.session_id, limit=10_000)
2842
+ except Exception: # pylint: disable=broad-exception-caught
2843
+ inbox = []
2844
+ unread_count = sum(1 for m in inbox if not m.read)
2845
+ last = inbox[-1] if inbox else None
2846
+
2847
+ session_display = s.session_id if wide else s.session_id[:12]
2848
+ last_from = (last.from_session if wide else last.from_session[:12]) if last else "-"
2849
+ last_text = (last.text[:60] + "…") if last and len(last.text) > 60 else (last.text if last else "-")
2850
+ status_style = status_styles.get(s.status or "", "dim")
2851
+ unread_style = "bold red" if unread_count else "dim"
2852
+
2853
+ table.add_row(
2854
+ "★" if s.session_id == me_sid else "",
2855
+ session_display,
2856
+ s.agent_type or "worker",
2857
+ f"[{status_style}]{s.status or 'unknown'}[/{status_style}]",
2858
+ f"[{unread_style}]{unread_count}[/{unread_style}]",
2859
+ last_from,
2860
+ last_text,
2861
+ )
2862
+
2863
+ console.print(table)
@@ -10,7 +10,7 @@ import shlex
10
10
  import subprocess
11
11
  import sys
12
12
  from pathlib import Path
13
- from typing import TYPE_CHECKING, Optional
13
+ from typing import TYPE_CHECKING, List, Optional
14
14
 
15
15
  import typer
16
16
  from qbraid_core.services.environments.schema import EnvironmentConfig
@@ -359,11 +359,23 @@ def envs_create( # pylint: disable=too-many-statements
359
359
  @envs_app.command(name="uninstall")
360
360
  def envs_uninstall(
361
361
  name: str = typer.Option(..., "-n", "--name", help="Name of the environment to remove"),
362
+ delete_cloud: bool = typer.Option(
363
+ False,
364
+ "--delete-cloud",
365
+ "-c",
366
+ help=(
367
+ "Also delete the environment from qBraid cloud (requires ownership). "
368
+ "By default only the local copy is removed and the cloud slug is preserved, "
369
+ "so you can reinstall it later."
370
+ ),
371
+ ),
362
372
  auto_confirm: bool = typer.Option(False, "--yes", "-y", help="Automatically answer 'yes' to all prompts"),
363
373
  ) -> None:
364
374
  """Remove a local environment.
365
375
 
366
- Removes a qBraid environment from your filesystem.
376
+ By default this removes only the local copy — the cloud environment is
377
+ preserved so you can reinstall it later. Pass ``--delete-cloud`` to also
378
+ retire the cloud environment (requires ownership).
367
379
  """
368
380
  import asyncio
369
381
 
@@ -374,38 +386,55 @@ def envs_uninstall(
374
386
  emc = EnvironmentManagerClient()
375
387
 
376
388
  # Run async uninstall method
377
- result = asyncio.run(emc.uninstall_environment_local(slug, delete_metadata=True, force=auto_confirm))
389
+ result = asyncio.run(
390
+ emc.uninstall_environment_local(slug, delete_metadata=delete_cloud, force=auto_confirm)
391
+ )
378
392
  return result
379
393
 
380
- def gather_local_data(env_name: str) -> tuple[Path | None, str | None]:
381
- """Get environment path and env_id from name, env_id, or slug."""
394
+ def gather_local_data(env_name: str) -> tuple[Path | None, str | None, str | None]:
395
+ """Return ``(path, env_id, slug)`` for a name/env_id/slug lookup.
396
+
397
+ ``slug`` is ``None`` when the env was never uploaded (local-only env);
398
+ the ``--delete-cloud`` hint below uses this to avoid pointing users at
399
+ a flag that would be a no-op for their env.
400
+ """
401
+ from qbraid_core.services.environments.registry import EnvironmentRegistryManager
402
+
382
403
  installed, aliases = installed_envs_data()
404
+ registry_mgr = None
405
+
406
+ def _slug_for(env_id: str) -> str | None:
407
+ nonlocal registry_mgr
408
+ try:
409
+ if registry_mgr is None:
410
+ registry_mgr = EnvironmentRegistryManager()
411
+ entry = registry_mgr.registry.environments.get(env_id)
412
+ return entry.slug if entry else None
413
+ except Exception:
414
+ return None
415
+
383
416
  # Try to find by name first, then by env_id
384
417
  if env_name in aliases:
385
418
  env_id = aliases[env_name]
386
- path = installed[env_id]
387
- return path, env_id
419
+ return installed[env_id], env_id, _slug_for(env_id)
388
420
  if env_name in installed:
389
- # Direct env_id lookup
390
- path = installed[env_name]
391
- return path, env_name
421
+ return installed[env_name], env_name, _slug_for(env_name)
392
422
 
393
423
  # Fall back to slug lookup via registry
394
424
  try:
395
- from qbraid_core.services.environments.registry import EnvironmentRegistryManager
396
-
397
- registry_mgr = EnvironmentRegistryManager()
425
+ if registry_mgr is None:
426
+ registry_mgr = EnvironmentRegistryManager()
398
427
  match = registry_mgr.find_by_slug(env_name)
399
428
  if match:
400
429
  env_id, entry = match
401
430
  if entry.path.exists():
402
- return entry.path, env_id
431
+ return entry.path, env_id, entry.slug
403
432
  except Exception:
404
433
  pass
405
434
 
406
- return None, None
435
+ return None, None, None
407
436
 
408
- env_path, env_id = gather_local_data(name)
437
+ env_path, env_id, env_slug = gather_local_data(name)
409
438
 
410
439
  if env_path is None:
411
440
  console = Console()
@@ -414,12 +443,28 @@ def envs_uninstall(
414
443
  console.print("[dim]Run 'qbraid envs list' to see all installed environments.[/dim]")
415
444
  raise typer.Exit(0)
416
445
 
446
+ # Only hint at --delete-cloud when there's actually a cloud copy to delete.
447
+ # Local-only envs (never uploaded → no slug) would treat --delete-cloud as
448
+ # a no-op, so mentioning it there is confusing.
449
+ if delete_cloud:
450
+ scope_line = (
451
+ "This will remove the local install AND permanently delete the environment "
452
+ "from qBraid cloud. This operation CANNOT be undone."
453
+ )
454
+ elif env_slug:
455
+ scope_line = (
456
+ "This will remove only the LOCAL copy. The cloud environment is preserved "
457
+ "so you can reinstall it later — pass --delete-cloud to also delete from "
458
+ "qBraid cloud."
459
+ )
460
+ else:
461
+ scope_line = (
462
+ "This will remove the local copy. (No cloud environment associated — this "
463
+ "env was never uploaded.)"
464
+ )
417
465
  confirmation_message = (
418
- f"⚠️ Warning: You are about to delete the environment '{name}' "
419
- f"located at '{env_path}'.\n"
420
- "This will remove all local packages and permanently delete all "
421
- "of its associated qBraid environment metadata.\n"
422
- "This operation CANNOT be undone.\n\n"
466
+ f"⚠️ About to remove the environment '{name}' at '{env_path}'.\n"
467
+ f"{scope_line}\n\n"
423
468
  "Are you sure you want to continue?"
424
469
  )
425
470
 
@@ -433,9 +478,28 @@ def envs_uninstall(
433
478
  )
434
479
 
435
480
  if result.get("deleted_metadata"):
436
- typer.echo(f"✅ Environment '{name}' successfully removed (local files and metadata).")
481
+ typer.echo(f"✅ Environment '{name}' removed (local files and cloud metadata).")
482
+ elif delete_cloud:
483
+ # User asked for cloud delete but it didn't happen. Point them at
484
+ # the actionable diagnostic — a missing slug is the common cause;
485
+ # otherwise re-run with QBRAID_LOG_LEVEL=DEBUG to surface the API
486
+ # error the core swallowed at line 3098 of environments/client.py.
487
+ reason = (
488
+ "no cloud slug is registered for this environment"
489
+ if not env_slug
490
+ else "re-run with QBRAID_LOG_LEVEL=DEBUG to see the underlying API error"
491
+ )
492
+ typer.echo(
493
+ f"✅ Environment '{name}' removed locally. Cloud delete was requested "
494
+ f"but skipped ({reason})."
495
+ )
496
+ elif env_slug:
497
+ typer.echo(
498
+ f"✅ Environment '{name}' removed locally. "
499
+ "Cloud copy preserved (pass --delete-cloud to also remove from qBraid cloud)."
500
+ )
437
501
  else:
438
- typer.echo(f"✅ Environment '{name}' successfully removed (local files only).")
502
+ typer.echo(f"✅ Environment '{name}' removed locally.")
439
503
 
440
504
 
441
505
  @envs_app.command(name="delete")
@@ -1243,6 +1307,7 @@ def envs_install(
1243
1307
  overwrite=yes,
1244
1308
  progress_callback=progress_callback,
1245
1309
  auto_install_python=False, # We handled Python above
1310
+ env_info=env_info, # skip the internal fetch — we already have it
1246
1311
  )
1247
1312
 
1248
1313
  console.print("")
@@ -1698,8 +1763,8 @@ def envs_config(
1698
1763
  tags: Optional[str] = typer.Option(
1699
1764
  None, "--tags", help="Replace tags (comma-separated, use '' to clear)"
1700
1765
  ),
1701
- add_tag: Optional[list[str]] = typer.Option(None, "--add-tag", help="Append a tag (repeatable)"),
1702
- remove_tag: Optional[list[str]] = typer.Option(None, "--remove-tag", help="Remove a tag (repeatable)"),
1766
+ add_tag: Optional[List[str]] = typer.Option(None, "--add-tag", help="Append a tag (repeatable)"),
1767
+ remove_tag: Optional[List[str]] = typer.Option(None, "--remove-tag", help="Remove a tag (repeatable)"),
1703
1768
  kernel_name: Optional[str] = typer.Option(
1704
1769
  None, "--kernel-name", help="Update Jupyter kernel display name"
1705
1770
  ),
@@ -12,7 +12,6 @@ rejected before our code runs.
12
12
  import os
13
13
  import sys
14
14
 
15
- import click
16
15
  import typer
17
16
 
18
17
  app = typer.Typer(help="Launch qBraid Lab (JupyterLab).")
@@ -43,6 +42,9 @@ def lab_command(_ctx: typer.Context):
43
42
  try:
44
43
  os.execvp("jupyter-lab", ["jupyter-lab"] + jupyter_args)
45
44
  except FileNotFoundError:
46
- raise click.UsageError(
47
- "jupyter-lab is not installed or not on PATH. Install qBraid Lab first: pip install qbraid-lab"
45
+ typer.echo(
46
+ "Error: jupyter-lab is not installed or not on PATH. "
47
+ "Install qBraid Lab first: pip install qbraid-lab",
48
+ err=True,
48
49
  )
50
+ raise typer.Exit(code=1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.13.1
3
+ Version: 0.13.2
4
4
  Summary: Command Line Interface for interacting with all parts of the qBraid platform.
5
5
  Author-email: qBraid Development Team <contact@qbraid.com>
6
6
  License-Expression: LicenseRef-Proprietary
File without changes
File without changes
File without changes