qbraid-cli 0.16.6__tar.gz → 0.16.8__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 (71) hide show
  1. {qbraid_cli-0.16.6/qbraid_cli.egg-info → qbraid_cli-0.16.8}/PKG-INFO +6 -6
  2. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/README.md +2 -2
  3. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/pyproject.toml +4 -4
  4. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/agents/app.py +403 -102
  5. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/comms/app.py +32 -3
  6. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8/qbraid_cli.egg-info}/PKG-INFO +6 -6
  7. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/requires.txt +3 -3
  8. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/LICENSE +0 -0
  9. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/__init__.py +0 -0
  10. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/_build_info.py +0 -0
  11. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/_version.py +0 -0
  12. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/account/__init__.py +0 -0
  13. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/account/app.py +0 -0
  14. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/admin/__init__.py +0 -0
  15. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/admin/app.py +0 -0
  16. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/admin/headers.py +0 -0
  17. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/admin/validation.py +0 -0
  18. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/agents/__init__.py +0 -0
  19. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/agents/permission_response.py +0 -0
  20. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/ai/__init__.py +0 -0
  21. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/ai/app.py +0 -0
  22. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/ai/token.py +0 -0
  23. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/comms/__init__.py +0 -0
  24. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/__init__.py +0 -0
  25. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/app.py +0 -0
  26. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/instances.py +0 -0
  27. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/profiles.py +0 -0
  28. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/server.py +0 -0
  29. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/compute/ssh.py +0 -0
  30. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/configure/__init__.py +0 -0
  31. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/configure/actions.py +0 -0
  32. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/configure/app.py +0 -0
  33. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/devices/__init__.py +0 -0
  34. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/devices/app.py +0 -0
  35. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/devices/validation.py +0 -0
  36. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/__init__.py +0 -0
  37. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/activate.py +0 -0
  38. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/app.py +0 -0
  39. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/create.py +0 -0
  40. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/data_handling.py +0 -0
  41. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/groups.py +0 -0
  42. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/publish.py +0 -0
  43. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/registry.py +0 -0
  44. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/envs/share.py +0 -0
  45. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/exceptions.py +0 -0
  46. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/handlers.py +0 -0
  47. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/jobs/__init__.py +0 -0
  48. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/jobs/app.py +0 -0
  49. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/jobs/groups.py +0 -0
  50. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/kernels/__init__.py +0 -0
  51. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/kernels/app.py +0 -0
  52. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/lab/__init__.py +0 -0
  53. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/lab/app.py +0 -0
  54. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/main.py +0 -0
  55. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/pip/__init__.py +0 -0
  56. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/pip/app.py +0 -0
  57. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/pip/hooks.py +0 -0
  58. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/projects/__init__.py +0 -0
  59. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/projects/app.py +0 -0
  60. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/py.typed +0 -0
  61. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/skills/__init__.py +0 -0
  62. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/skills/app.py +0 -0
  63. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/troubleshoot/__init__.py +0 -0
  64. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli/troubleshoot/app.py +0 -0
  65. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/SOURCES.txt +0 -0
  66. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/dependency_links.txt +0 -0
  67. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/entry_points.txt +0 -0
  68. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/top_level.txt +0 -0
  69. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/setup.cfg +0 -0
  70. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/tests/test_cli_e2e.py +0 -0
  71. {qbraid_cli-0.16.6 → qbraid_cli-0.16.8}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.16.6
3
+ Version: 0.16.8
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
@@ -30,11 +30,11 @@ Requires-Dist: typer>=0.12.1
30
30
  Requires-Dist: rich>=10.11.0
31
31
  Requires-Dist: click<=8.5.0
32
32
  Requires-Dist: questionary>=2.0.0
33
- Requires-Dist: qbraid-core>=0.6.15
33
+ Requires-Dist: qbraid-core>=0.6.19
34
34
  Provides-Extra: compute
35
- Requires-Dist: qbraid-core[compute]>=0.6.15; extra == "compute"
35
+ Requires-Dist: qbraid-core[compute]>=0.6.19; extra == "compute"
36
36
  Provides-Extra: envs
37
- Requires-Dist: qbraid-core[environments]>=0.6.15; extra == "envs"
37
+ Requires-Dist: qbraid-core[environments]>=0.6.19; extra == "envs"
38
38
  Provides-Extra: dev
39
39
  Requires-Dist: isort; extra == "dev"
40
40
  Requires-Dist: black; extra == "dev"
@@ -172,5 +172,5 @@ qbraid comms email send --subject "Progress" --file report.txt --json
172
172
  qbraid comms slack read --json
173
173
  ```
174
174
 
175
- See [communication commands](docs/comms-cli.md) for Slack connection, stdin input,
176
- pagination, and deployment requirements.
175
+ See [communication commands](https://docs.qbraid.com/v2/cli/api-reference/qbraid_comms) for Slack connection, stdin input,
176
+ message limits, and pagination.
@@ -127,5 +127,5 @@ qbraid comms email send --subject "Progress" --file report.txt --json
127
127
  qbraid comms slack read --json
128
128
  ```
129
129
 
130
- See [communication commands](docs/comms-cli.md) for Slack connection, stdin input,
131
- pagination, and deployment requirements.
130
+ See [communication commands](https://docs.qbraid.com/v2/cli/api-reference/qbraid_comms) for Slack connection, stdin input,
131
+ message limits, and pagination.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qbraid-cli"
7
- version = "0.16.6"
7
+ version = "0.16.8"
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" }]
@@ -32,7 +32,7 @@ dependencies = [
32
32
  "rich>=10.11.0",
33
33
  "click<=8.5.0",
34
34
  "questionary>=2.0.0",
35
- "qbraid-core>=0.6.15",
35
+ "qbraid-core>=0.6.19",
36
36
  ]
37
37
  requires-python = ">= 3.10"
38
38
 
@@ -43,8 +43,8 @@ Documentation = "https://docs.qbraid.com/v2/cli/api-reference/qbraid"
43
43
  Discord = "https://discord.gg/KugF6Cnncm"
44
44
 
45
45
  [project.optional-dependencies]
46
- compute = ["qbraid-core[compute]>=0.6.15"]
47
- envs = ["qbraid-core[environments]>=0.6.15"]
46
+ compute = ["qbraid-core[compute]>=0.6.19"]
47
+ envs = ["qbraid-core[environments]>=0.6.19"]
48
48
  dev = ["isort", "black", "pytest", "pytest-cov", "pytest-asyncio"]
49
49
 
50
50
  [project.scripts]
@@ -17,6 +17,7 @@ from typing import Any, Callable, Dict, List, Optional
17
17
  import click
18
18
  import typer
19
19
  from rich.console import Console
20
+ from rich.markup import escape
20
21
  from rich.panel import Panel
21
22
  from rich.table import Table
22
23
 
@@ -114,7 +115,6 @@ def _remote_failure(stdout: str, stderr: str) -> str:
114
115
  "Session not found" into "Warning: Permanently added ... to the list of
115
116
  known hosts", which names the one thing that went right.
116
117
  """
117
- from rich.markup import escape
118
118
 
119
119
  def useful(text: str) -> list[str]:
120
120
  # The remote's Rich error panel arrives as box-drawing borders around
@@ -204,12 +204,91 @@ def _since_to_timestamp(since_str: str) -> float:
204
204
  return time.time() - seconds
205
205
 
206
206
 
207
+ def _plain(value) -> str:
208
+ """A user- or agent-supplied value, escaped so Rich prints it literally."""
209
+ return escape(str(value))
210
+
211
+
212
+ def _core_method(registry, name: str):
213
+ """A registry method the installed qbraid-core actually defines, else None.
214
+
215
+ Checked on the class so older Core releases (and test doubles) fall back
216
+ to the previous behavior instead of calling a missing method.
217
+ """
218
+ if not callable(getattr(type(registry), name, None)):
219
+ return None
220
+ return getattr(registry, name)
221
+
222
+
223
+ def _calling_session(registry) -> Optional[str]:
224
+ """The live agent session this command runs inside, if any.
225
+
226
+ Newer qbraid-core finds it by process ancestry, so unsetting
227
+ QBRAID_SESSION_ID does not make an agent look like the person, and a
228
+ person typing in a pane that kept the variable is not mistaken for the
229
+ agent that exited. Older Core falls back to the variable, unless it names
230
+ a session the registry records as stopped or errored.
231
+ """
232
+ find = _core_method(registry, "calling_session")
233
+ if find is not None:
234
+ try:
235
+ return find()
236
+ except Exception as err: # pylint: disable=broad-exception-caught
237
+ if _is_registry_error(err):
238
+ # Core could not tell which agent this is (for example, the
239
+ # caller matches more than one live session). Refuse rather
240
+ # than fail open to "the person".
241
+ console.print(
242
+ f"[red]Cannot tell which agent session this command runs in: {_plain(err)}[/red]"
243
+ )
244
+ raise typer.Exit(code=1) from err
245
+ # any other failure: fall back to the variable, as older Core does
246
+ env_sid = os.environ.get("QBRAID_SESSION_ID") or None
247
+ if env_sid and _session_status(registry, env_sid) in ("stopped", "error"):
248
+ return None
249
+ return env_sid
250
+
251
+
252
+ def _is_registry_error(err: Exception) -> bool:
253
+ """Whether ``err`` is qbraid-core's AgentRegistryError (or a subclass)."""
254
+ try:
255
+ # pylint: disable-next=import-outside-toplevel
256
+ from qbraid_core.services.agents.exceptions import AgentRegistryError
257
+ except ImportError:
258
+ return False
259
+ return isinstance(err, AgentRegistryError)
260
+
261
+
262
+ def _session_status(registry, session_id: str) -> Optional[str]:
263
+ """The registry status of a session ("stopped", "working", ...), or None
264
+ when it is not registered or the registry cannot be read."""
265
+ try:
266
+ session = registry.get_session(session_id)
267
+ except Exception: # pylint: disable=broad-exception-caught
268
+ return None
269
+ status = getattr(session, "status", None)
270
+ value = getattr(status, "value", status)
271
+ return value if isinstance(value, str) else None
272
+
273
+
207
274
  def _find_session_by_prefix(registry, session_id: str):
208
275
  """Find a session by ID, prefix, or name from the registry.
209
276
 
210
277
  Searches session_id (exact/prefix), then tmux_window and terminal_name.
211
278
  Returns the matched AgentSession or None.
212
279
  """
280
+ resolve = _core_method(registry, "resolve_session")
281
+ if resolve is not None:
282
+ # Newer Core: exact id, unique prefix, then a name that prefers the live
283
+ # session over stopped namesakes; ambiguity is an error, not a guess.
284
+ try:
285
+ return resolve(session_id)
286
+ except Exception as err: # pylint: disable=broad-exception-caught
287
+ if type(err).__name__ != "AgentSessionAmbiguousError":
288
+ raise
289
+ console.print(f"[red]{_plain(err)}[/red]")
290
+ raise typer.Exit(code=1) from err
291
+
213
292
  all_sessions = registry.load_registry().sessions
214
293
 
215
294
  # Try session_id exact or prefix match first
@@ -766,9 +845,9 @@ def agents_status(
766
845
 
767
846
  # Team info
768
847
  if s.team_id:
769
- lines.append(f"[bold]Team ID:[/bold] {s.team_id}")
848
+ lines.append(f"[bold]Team ID:[/bold] {_plain(s.team_id)}")
770
849
  if s.role:
771
- lines.append(f"[bold]Role:[/bold] {s.role}")
850
+ lines.append(f"[bold]Role:[/bold] {_plain(s.role)}")
772
851
 
773
852
  # Transcript path
774
853
  if s.transcript_path:
@@ -807,8 +886,24 @@ def agents_update(
807
886
  status: Optional[str] = typer.Option(
808
887
  None, "--status", help="Set session status (working, waiting, idle, error, stopped)"
809
888
  ),
889
+ agent_type: Optional[str] = typer.Option(
890
+ None,
891
+ "--type",
892
+ help="Set the agent type (leader, worker, observer, reviewer); '' clears it.",
893
+ ),
894
+ team: Optional[str] = typer.Option(None, "--team", help="Join a team by id; '' leaves it."),
895
+ parent: Optional[str] = typer.Option(
896
+ None, "--parent", help="Set the parent session (id, prefix or name); '' clears it."
897
+ ),
810
898
  ):
811
- """Update metadata fields on an agent session."""
899
+ """Update metadata fields on an agent session.
900
+
901
+ --type, --team and --parent let agents started from a terminal join a
902
+ team after launch, so they can message each other. From your own shell
903
+ any change is allowed. Inside an agent, it may only place teamless
904
+ sessions or its teammates into a team it leads, never as or out of
905
+ leader. These are coordination guardrails, not a security boundary.
906
+ """
812
907
  try:
813
908
  from qbraid_core.services.agents import (
814
909
  AgentSessionNotFoundError,
@@ -820,7 +915,7 @@ def agents_update(
820
915
  # Support prefix and name matching
821
916
  match = _find_session_by_prefix(registry, session_id)
822
917
  if not match:
823
- console.print(f"[red]Session not found: {session_id}[/red]")
918
+ console.print(f"[red]Session not found: {_plain(session_id)}[/red]")
824
919
  console.print("[dim]Use 'qbraid agents list' to see active sessions.[/dim]")
825
920
  raise typer.Exit(code=1)
826
921
  matched_id = match.session_id
@@ -842,7 +937,9 @@ def agents_update(
842
937
  valid_statuses = {"working", "waiting", "idle", "error", "stopped"}
843
938
  if status not in valid_statuses:
844
939
  valid_opts = ", ".join(sorted(valid_statuses))
845
- console.print(f"[red]Invalid status: {status!r}." f" Must be one of: {valid_opts}[/red]")
940
+ console.print(
941
+ f"[red]Invalid status: {_plain(repr(status))}. Must be one of: {valid_opts}[/red]"
942
+ )
846
943
  raise typer.Exit(code=1)
847
944
  updates["status"] = status
848
945
 
@@ -854,31 +951,70 @@ def agents_update(
854
951
  merged.append(t)
855
952
  updates["tags"] = merged
856
953
 
857
- if not updates:
954
+ team_change = agent_type is not None or team is not None or parent is not None
955
+ if not updates and not team_change:
858
956
  console.print(
859
- "[yellow]No updates specified. Use --name, --model, --cwd, --status, or --tag.[/yellow]"
957
+ "[yellow]No updates specified. Use --name, --model, --cwd, --status, --tag, "
958
+ "--type, --team or --parent.[/yellow]"
860
959
  )
861
960
  raise typer.Exit(code=0)
862
961
 
863
- updated = registry.update_session(matched_id, **updates)
962
+ if team_change:
963
+ assign = _core_method(registry, "assign_team")
964
+ if assign is None:
965
+ console.print(
966
+ "[red]This qbraid-core cannot change a session's team or role.[/red] "
967
+ "Upgrade qbraid-core, or launch the agent with --type/--team/--parent."
968
+ )
969
+ raise typer.Exit(code=1)
970
+ parent_id = parent
971
+ if parent:
972
+ parent_match = _find_session_by_prefix(registry, parent)
973
+ if not parent_match:
974
+ console.print(f"[red]Parent session not found: {_plain(parent)}[/red]")
975
+ raise typer.Exit(code=1)
976
+ parent_id = parent_match.session_id
977
+ try:
978
+ assign(
979
+ matched_id,
980
+ team_id=team,
981
+ agent_type=agent_type,
982
+ parent_session_id=parent_id,
983
+ actor_session_id=_calling_session(registry),
984
+ )
985
+ except AgentSessionNotFoundError:
986
+ raise
987
+ except Exception as err: # pylint: disable=broad-exception-caught
988
+ console.print(f"[red]{_plain(err)}[/red]")
989
+ raise typer.Exit(code=1) from err
990
+
991
+ updated = (
992
+ registry.update_session(matched_id, **updates) if updates else registry.get_session(matched_id)
993
+ )
864
994
 
865
995
  # Display updated session info
866
996
  status_val = updated.status.value if updated.status else "idle"
867
997
  style = _status_style(status_val)
868
998
 
999
+ # Every value here can come from a user or an agent: escape it so a
1000
+ # name or team such as "[/]" prints literally instead of breaking Rich.
869
1001
  lines = []
870
- lines.append(f"[bold]Session ID:[/bold] {updated.session_id}")
1002
+ lines.append(f"[bold]Session ID:[/bold] {_plain(updated.session_id)}")
871
1003
  lines.append(f"[bold]Tool:[/bold] {updated.tool.value if updated.tool else '-'}")
872
- lines.append(f"[bold]Status:[/bold] [{style}]{status_val}[/{style}]")
873
- lines.append(f"[bold]Model:[/bold] {updated.model_name or '-'}")
874
- lines.append(f"[bold]Name:[/bold] {updated.terminal_name or '-'}")
875
- lines.append(f"[bold]CWD:[/bold] {updated.cwd or '-'}")
1004
+ lines.append(f"[bold]Status:[/bold] [{style}]{_plain(status_val)}[/{style}]")
1005
+ lines.append(f"[bold]Model:[/bold] {_plain(updated.model_name or '-')}")
1006
+ lines.append(f"[bold]Name:[/bold] {_plain(updated.terminal_name or '-')}")
1007
+ lines.append(f"[bold]CWD:[/bold] {_plain(updated.cwd or '-')}")
876
1008
  if updated.tags:
877
- lines.append(f"[bold]Tags:[/bold] {', '.join(updated.tags)}")
1009
+ lines.append(f"[bold]Tags:[/bold] {_plain(', '.join(updated.tags))}")
1010
+ if updated.agent_type or updated.team_id or updated.parent_session_id:
1011
+ lines.append(f"[bold]Type:[/bold] {_plain(updated.agent_type or 'worker (default)')}")
1012
+ lines.append(f"[bold]Team:[/bold] {_plain(updated.team_id or '-')}")
1013
+ lines.append(f"[bold]Parent:[/bold] {_plain(updated.parent_session_id or '-')}")
878
1014
 
879
1015
  panel = Panel(
880
1016
  "\n".join(lines),
881
- title=f"Updated Session [{updated.session_id[:8]}]",
1017
+ title=_plain(f"Updated Session [{updated.session_id[:8]}]"),
882
1018
  border_style="green",
883
1019
  )
884
1020
  console.print(panel)
@@ -886,7 +1022,7 @@ def agents_update(
886
1022
  except (typer.Exit, SystemExit):
887
1023
  raise
888
1024
  except AgentSessionNotFoundError:
889
- console.print(f"[red]Session not found: {session_id}[/red]")
1025
+ console.print(f"[red]Session not found: {_plain(session_id)}[/red]")
890
1026
  console.print("[dim]Use 'qbraid agents list' to see active sessions.[/dim]")
891
1027
  raise typer.Exit(code=1)
892
1028
  except Exception as e:
@@ -1278,6 +1414,27 @@ def _request_id_kwargs(method: Callable[..., Any], request_id: Optional[str]) ->
1278
1414
  return {"request_id": request_id}
1279
1415
 
1280
1416
 
1417
+ def _fork_execution(original: Any, approval: Optional[Any], execution_mode: Optional[Any]):
1418
+ """Execution mode for a fork of ``original``.
1419
+
1420
+ An explicit --execution-mode wins. Otherwise the original's mode is reused
1421
+ -- unless it was YOLO's forced container and the fork asks for a safer
1422
+ approval, or the fork asks for auto-review, which Core pins to the
1423
+ workspace sandbox. Inheriting there carried danger-full-access into a
1424
+ session labelled manual or auto-review (core#378 review F2).
1425
+ """
1426
+ if execution_mode:
1427
+ return execution_mode.value
1428
+ requested = approval.value if approval else None
1429
+ recorded = getattr(original, "approval", None)
1430
+ effective = requested or recorded
1431
+ if effective in ("auto-review", "yolo") or (recorded == "yolo" and requested not in (None, "yolo")):
1432
+ # auto-review is pinned to workspace and yolo to container by core;
1433
+ # passing the original's mode would make core refuse the fork.
1434
+ return None
1435
+ return getattr(original, "execution_mode", None)
1436
+
1437
+
1281
1438
  def _approval_kwargs(method: Callable[..., Any], approval: Optional[str]) -> Dict[str, Any]:
1282
1439
  """Forward ``approval`` only to a qbraid-core new enough to accept it.
1283
1440
 
@@ -1425,6 +1582,43 @@ def agents_settings(
1425
1582
  handle_error(error_type=type(error).__name__, message=str(error), include_traceback=False)
1426
1583
 
1427
1584
 
1585
+ def _hook_trust_kwargs(method, value):
1586
+ """Fail before launch when an explicit choice cannot be honored."""
1587
+ if value is None:
1588
+ return {}
1589
+
1590
+ from qbraid_core.services.agents import AgentLauncherError
1591
+
1592
+ params = inspect.signature(method).parameters
1593
+ if "trust_hooks" not in params and not any(
1594
+ p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values()
1595
+ ):
1596
+ raise AgentLauncherError("Hook trust requires a newer qbraid-core on this machine.")
1597
+ return {"trust_hooks": value}
1598
+
1599
+
1600
+ def _startup_pending(session):
1601
+ return session.tool.value in ("codex", "claude") and not getattr(session, "startup_confirmed_at", None)
1602
+
1603
+
1604
+ def _startup_notice(session, compute=None):
1605
+ import shlex
1606
+
1607
+ suffix = f" --compute {shlex.quote(compute)}" if compute else ""
1608
+ if _startup_pending(session):
1609
+ console.print(
1610
+ "Session created; startup is not yet confirmed. It may be waiting for sign-in, "
1611
+ "folder trust, or hook review. The initial task waits until setup is complete.",
1612
+ markup=False,
1613
+ )
1614
+ if session.tool.value == "codex":
1615
+ console.print(
1616
+ "Approve hooks in the agent's terminal (/hooks); Codex remembers unchanged ones.",
1617
+ markup=False,
1618
+ )
1619
+ console.print(f" qbraid agents attach {shlex.quote(session.session_id)}{suffix}", markup=False)
1620
+
1621
+
1428
1622
  @agents_app.command("launch")
1429
1623
  def agents_launch(
1430
1624
  backend: TerminalBackend = typer.Option(
@@ -1470,6 +1664,15 @@ def agents_launch(
1470
1664
  "decides. yolo: no permission checks. ask/auto: legacy modes, unchanged."
1471
1665
  ),
1472
1666
  ),
1667
+ trust_hooks: bool = typer.Option(
1668
+ False,
1669
+ "--trust-hooks",
1670
+ help=(
1671
+ "Codex only, DANGEROUS: run ALL enabled hooks (yours, the project's and "
1672
+ "plugins') without Codex's hook review, for this launch only. Not retained on "
1673
+ "resume. Refused with --repo. Independent of --approval and --yolo."
1674
+ ),
1675
+ ),
1473
1676
  yolo: bool = typer.Option(
1474
1677
  False, "--yolo", help="Claude: skip permissions. Codex: bypass approvals and its sandbox."
1475
1678
  ),
@@ -1498,6 +1701,14 @@ def agents_launch(
1498
1701
  tag = tag or result.get("tags")
1499
1702
  repo = repo or result.get("repo")
1500
1703
 
1704
+ if trust_hooks and repo:
1705
+ # Codex would run the clone's own .codex hooks unreviewed once its folder is
1706
+ # trusted. Core refuses this too; refuse here before cloning anything.
1707
+ raise AgentLauncherError(
1708
+ "--trust-hooks cannot be combined with --repo: the repository's project hooks "
1709
+ "would run without review. Clone and vet it, then launch with --cwd."
1710
+ )
1711
+
1501
1712
  if yolo:
1502
1713
  if tool not in ("claude", "claude-auto", "codex"):
1503
1714
  raise AgentLauncherError("--yolo is supported only for Claude and Codex.")
@@ -1535,6 +1746,7 @@ def agents_launch(
1535
1746
  tags=tag,
1536
1747
  **_request_id_kwargs(launcher.remote_launch, request_id),
1537
1748
  **_approval_kwargs(launcher.remote_launch, approval.value if approval else None),
1749
+ **_hook_trust_kwargs(launcher.remote_launch, True if trust_hooks else None),
1538
1750
  **_execution_kwargs(launcher.remote_launch, execution_mode.value if execution_mode else None),
1539
1751
  )
1540
1752
  else:
@@ -1553,11 +1765,16 @@ def agents_launch(
1553
1765
  team_id=team,
1554
1766
  tags=tag,
1555
1767
  **_approval_kwargs(launcher.launch, approval.value if approval else None),
1768
+ **_hook_trust_kwargs(launcher.launch, True if trust_hooks else None),
1556
1769
  **_execution_kwargs(launcher.launch, execution_mode.value if execution_mode else None),
1557
1770
  progress_callback=_clone_progress if repo else None,
1558
1771
  )
1559
1772
 
1560
- status_val = session.status.value if session.status else "idle"
1773
+ status_val = (
1774
+ "starting / check terminal"
1775
+ if _startup_pending(session)
1776
+ else (session.status.value if session.status else "idle")
1777
+ )
1561
1778
  style = _status_style(status_val)
1562
1779
 
1563
1780
  lines = []
@@ -1583,7 +1800,7 @@ def agents_launch(
1583
1800
  console.print(panel)
1584
1801
  console.print()
1585
1802
  console.print("[dim]Open from the Agent Dashboard or attach with:[/dim]")
1586
- console.print(f" qbraid agents attach {session.session_id}")
1803
+ _startup_notice(session, compute)
1587
1804
 
1588
1805
  except AgentLauncherError as e:
1589
1806
  console.print(
@@ -1686,6 +1903,14 @@ def agents_resume(
1686
1903
  approval: Optional[ApprovalMode] = typer.Option(
1687
1904
  None, "--approval", help="Override recorded permissions for this resume. Omit to preserve them."
1688
1905
  ),
1906
+ trust_hooks: bool = typer.Option(
1907
+ False,
1908
+ "--trust-hooks",
1909
+ help=(
1910
+ "Codex only, DANGEROUS: run ALL enabled hooks without Codex's hook review, "
1911
+ "for this resume only. Hooks are reviewed again on every run that omits it."
1912
+ ),
1913
+ ),
1689
1914
  compute: Optional[str] = typer.Option(None, "--compute", "-c", help="SSH alias for remote compute"),
1690
1915
  ):
1691
1916
  """Resume a previous agent session.
@@ -1716,6 +1941,8 @@ def agents_resume(
1716
1941
  flags += f" --execution-mode {shlex.quote(execution_mode.value)}"
1717
1942
  if approval:
1718
1943
  flags += f" --approval {shlex.quote(approval.value)}"
1944
+ if trust_hooks:
1945
+ flags += " --trust-hooks"
1719
1946
  # markup=False -- see agents_status.
1720
1947
  console.print(
1721
1948
  _ssh_cmd(compute, f"qbraid agents resume {shlex.quote(session_id)}{flags}"),
@@ -1735,6 +1962,7 @@ def agents_resume(
1735
1962
  session_id=session_id,
1736
1963
  name=name,
1737
1964
  **(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
1965
+ **_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
1738
1966
  **_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
1739
1967
  )
1740
1968
  except AgentLauncherError as e:
@@ -1779,6 +2007,12 @@ def agents_resume(
1779
2007
  cwd=original.cwd,
1780
2008
  name=fork_name,
1781
2009
  instructions=None,
2010
+ **_hook_trust_kwargs(
2011
+ launcher.launch,
2012
+ # Never inherited from the original: only an explicit
2013
+ # --trust-hooks on this command bypasses hook review.
2014
+ True if trust_hooks else None,
2015
+ ),
1782
2016
  # A fork of a detached agent must not quietly
1783
2017
  # become one that waits for a human.
1784
2018
  **_approval_kwargs(
@@ -1787,11 +2021,7 @@ def agents_resume(
1787
2021
  ),
1788
2022
  **_execution_kwargs(
1789
2023
  launcher.launch,
1790
- (
1791
- execution_mode.value
1792
- if execution_mode
1793
- else getattr(original, "execution_mode", None)
1794
- ),
2024
+ _fork_execution(original, approval, execution_mode),
1795
2025
  ),
1796
2026
  )
1797
2027
  else:
@@ -1806,60 +2036,68 @@ def agents_resume(
1806
2036
 
1807
2037
  elif "Multiple sessions match" not in err_msg:
1808
2038
  raise
2039
+ else:
2040
+ # Only an ambiguous prefix reaches the picker. A completed fork
2041
+ # used to fall through to it, find no matches and exit 1 after
2042
+ # the fork had already launched.
2043
+ # Show filtered picker with only matching sessions
2044
+ import questionary
2045
+
2046
+ registry = _get_registry()
2047
+ all_sessions = registry.search_sessions(include_stopped=True)
2048
+ matches = [
2049
+ s
2050
+ for s in all_sessions
2051
+ if (
2052
+ s.session_id == session_id
2053
+ or s.session_id.startswith(session_id)
2054
+ or (s.tmux_window and s.tmux_window == session_id)
2055
+ or (s.terminal_name and s.terminal_name == session_id)
2056
+ )
2057
+ ]
2058
+ matches.sort(key=lambda s: s.last_activity or s.started_at or 0, reverse=True)
1809
2059
 
1810
- # Show filtered picker with only matching sessions
1811
- import questionary
1812
-
1813
- registry = _get_registry()
1814
- all_sessions = registry.search_sessions(include_stopped=True)
1815
- matches = [
1816
- s
1817
- for s in all_sessions
1818
- if (
1819
- s.session_id == session_id
1820
- or s.session_id.startswith(session_id)
1821
- or (s.tmux_window and s.tmux_window == session_id)
1822
- or (s.terminal_name and s.terminal_name == session_id)
1823
- )
1824
- ]
1825
- matches.sort(key=lambda s: s.last_activity or s.started_at or 0, reverse=True)
2060
+ if not matches:
2061
+ console.print(
2062
+ "[yellow]No visible sessions match this prefix. "
2063
+ "Use a full session ID to resume a retained record.[/yellow]"
2064
+ )
2065
+ raise typer.Exit(code=1)
2066
+
2067
+ choices = []
2068
+ for s in matches:
2069
+ sid = s.session_id[:8]
2070
+ tool_val = s.tool.value if s.tool else "?"
2071
+ status_val = s.status.value if s.status else "?"
2072
+ cwd_display = s.cwd or ""
2073
+ if len(cwd_display) > 30:
2074
+ cwd_display = "..." + cwd_display[-27:]
2075
+ age_str = _time_ago(s.last_activity or s.started_at)
2076
+ label = f"{sid} {tool_val:<8} {status_val:<8} {cwd_display:<30} {age_str}"
2077
+ choices.append(questionary.Choice(title=label, value=s.session_id))
2078
+
2079
+ console.print(f"[yellow]Multiple sessions match '{session_id}':[/yellow]")
2080
+ selected = questionary.select(
2081
+ "Select a session to resume:",
2082
+ choices=choices,
2083
+ instruction="(use arrow keys)",
2084
+ ).ask()
2085
+ if selected is None:
2086
+ raise typer.Exit()
1826
2087
 
1827
- if not matches:
1828
- console.print(
1829
- "[yellow]No visible sessions match this prefix. "
1830
- "Use a full session ID to resume a retained record.[/yellow]"
2088
+ session = launcher.resume(
2089
+ session_id=selected,
2090
+ name=name,
2091
+ **(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
2092
+ **_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
2093
+ **_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
1831
2094
  )
1832
- raise typer.Exit(code=1)
1833
-
1834
- choices = []
1835
- for s in matches:
1836
- sid = s.session_id[:8]
1837
- tool_val = s.tool.value if s.tool else "?"
1838
- status_val = s.status.value if s.status else "?"
1839
- cwd_display = s.cwd or ""
1840
- if len(cwd_display) > 30:
1841
- cwd_display = "..." + cwd_display[-27:]
1842
- age_str = _time_ago(s.last_activity or s.started_at)
1843
- label = f"{sid} {tool_val:<8} {status_val:<8} {cwd_display:<30} {age_str}"
1844
- choices.append(questionary.Choice(title=label, value=s.session_id))
1845
2095
 
1846
- console.print(f"[yellow]Multiple sessions match '{session_id}':[/yellow]")
1847
- selected = questionary.select(
1848
- "Select a session to resume:",
1849
- choices=choices,
1850
- instruction="(use arrow keys)",
1851
- ).ask()
1852
- if selected is None:
1853
- raise typer.Exit()
1854
-
1855
- session = launcher.resume(
1856
- session_id=selected,
1857
- name=name,
1858
- **(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
1859
- **_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
1860
- )
1861
-
1862
- status_val = session.status.value if session.status else "idle"
2096
+ status_val = (
2097
+ "starting / check terminal"
2098
+ if _startup_pending(session)
2099
+ else (session.status.value if session.status else "idle")
2100
+ )
1863
2101
  style = _status_style(status_val)
1864
2102
 
1865
2103
  lines = []
@@ -1880,7 +2118,7 @@ def agents_resume(
1880
2118
  console.print(panel)
1881
2119
  console.print()
1882
2120
  console.print("[dim]Attach with:[/dim]")
1883
- console.print(f" qbraid agents attach {session.tmux_window or session.session_id[:8]}")
2121
+ _startup_notice(session)
1884
2122
 
1885
2123
  except AgentLauncherError as e:
1886
2124
  console.print(Panel(str(e), title="Resume Error", border_style="red"))
@@ -2845,17 +3083,44 @@ def hooks_uninstall():
2845
3083
  # =============================================================================
2846
3084
 
2847
3085
 
2848
- def _resolve_from_session(explicit: Optional[str]) -> str:
3086
+ def _resolve_from_session(explicit: Optional[str], registry=None) -> str:
2849
3087
  """Determine the sender session id.
2850
3088
 
2851
- Priority: explicit --from > QBRAID_SESSION_ID env var (set by the qBraid
2852
- launcher for every agent it spawns) > fail with a helpful error.
3089
+ Priority: explicit --from > the agent session this command runs inside
3090
+ (process ancestry, else QBRAID_SESSION_ID) > fail with a helpful error.
3091
+ An explicit sender may be an id prefix or a name, like a recipient, but
3092
+ inside an agent it must be that agent.
2853
3093
  """
3094
+ caller = _calling_session(registry) if registry is not None else None
2854
3095
  if explicit:
2855
- return explicit
3096
+ sender = explicit
3097
+ if registry is not None:
3098
+ match = _find_session_by_prefix(registry, explicit)
3099
+ if match is not None:
3100
+ sender = match.session_id
3101
+ if caller and sender != caller:
3102
+ # A coordination guardrail, not authentication: an agent speaks
3103
+ # as itself so recipients and permission rules see the real sender.
3104
+ console.print(
3105
+ f"[red]This command runs inside agent session {_plain(caller[:8])}; it cannot send "
3106
+ f"as {_plain(explicit)}.[/red] Omit --from to send as yourself."
3107
+ )
3108
+ raise typer.Exit(code=1)
3109
+ return sender
3110
+ if caller:
3111
+ return caller
2856
3112
  env_sid = os.environ.get("QBRAID_SESSION_ID")
2857
- if env_sid:
3113
+ if registry is None and env_sid:
2858
3114
  return env_sid
3115
+ if env_sid:
3116
+ # The variable outlived its agent (a pane kept it after the agent
3117
+ # stopped): whoever types here is the person, not that agent.
3118
+ console.print(
3119
+ f"[red]QBRAID_SESSION_ID names {_plain(env_sid[:8])}, which is not a running "
3120
+ "agent, so this shell does not send as it.[/red] Pass --from <session-id> to "
3121
+ "choose a sender."
3122
+ )
3123
+ raise typer.Exit(code=1)
2859
3124
  console.print(
2860
3125
  "[red]No sender session id available.[/red] "
2861
3126
  "Pass --from <session-id>, or run this inside a qBraid-managed "
@@ -2911,13 +3176,34 @@ def _resolve_target_session(registry, identifier: str) -> str:
2911
3176
  match = _find_session_by_prefix(registry, identifier)
2912
3177
  if match is None:
2913
3178
  console.print(
2914
- f"[red]No session matched '{identifier}'.[/red] "
3179
+ f"[red]No session matched '{_plain(identifier)}'.[/red] "
2915
3180
  "Try 'qbraid agents list' to see available sessions."
2916
3181
  )
2917
3182
  raise typer.Exit(code=1)
2918
3183
  return match.session_id
2919
3184
 
2920
3185
 
3186
+ def _note_offline_recipient(registry, session_id: str) -> None:
3187
+ """Say so when the recipient is not running: an inbox keeps the message,
3188
+ but nothing reads it until the session is resumed."""
3189
+ status = _session_status(registry, session_id)
3190
+ if status not in ("stopped", "error"):
3191
+ return
3192
+ try:
3193
+ from qbraid_core.services.agents import messaging # pylint: disable=import-outside-toplevel
3194
+
3195
+ successor = getattr(messaging, "live_successor", None)
3196
+ if callable(successor) and successor(registry, session_id): # pylint: disable=not-callable
3197
+ return # Core forwards it to the resumed session and says so.
3198
+ except Exception: # pylint: disable=broad-exception-caught
3199
+ pass
3200
+ console.print(
3201
+ f"[yellow]Note: {_plain(session_id[:8])} is {status}. The message waits in its inbox and is "
3202
+ "read when the conversation is resumed (in place, or with `qbraid agents resume`, "
3203
+ "which carries unread messages to the new session).[/yellow]"
3204
+ )
3205
+
3206
+
2921
3207
  def _resolve_team_targets(registry, team_id: str, exclude_session_id: Optional[str] = None) -> list:
2922
3208
  """Return every session in a team, optionally excluding one (usually the sender).
2923
3209
 
@@ -2947,7 +3233,8 @@ def _parse_target_csv(csv: str, registry) -> list[str]:
2947
3233
  resolved.append(match.session_id)
2948
3234
  if unresolved:
2949
3235
  console.print(
2950
- f"[red]No session matched: {', '.join(unresolved)}[/red]. " "Broadcast aborted; nothing sent."
3236
+ f"[red]No session matched: {_plain(', '.join(unresolved))}[/red]. "
3237
+ "Broadcast aborted; nothing sent."
2951
3238
  )
2952
3239
  raise typer.Exit(code=1)
2953
3240
  return resolved
@@ -2985,7 +3272,15 @@ def _deliver_to_many(
2985
3272
  registry=registry,
2986
3273
  validate_permissions=validate_permissions,
2987
3274
  )
2988
- delivered.append((target_sid, msg.id))
3275
+ delivered.append((getattr(msg, "to_session", None) or target_sid, msg.id))
3276
+ # Older Core has no forwarded_from; newer Core types it Optional[str].
3277
+ forwarded = getattr(msg, "forwarded_from", None)
3278
+ if isinstance(forwarded, str) and forwarded:
3279
+ old_sid, new_sid = str(forwarded), str(msg.to_session)
3280
+ console.print(
3281
+ f"[dim]{_plain(old_sid[:8])} was resumed as {_plain(new_sid[:8])}; "
3282
+ "delivered to the running session.[/dim]"
3283
+ )
2989
3284
  except AgentMessagingError as err:
2990
3285
  failed.append((target_sid, str(err)))
2991
3286
  except Exception as err: # pylint: disable=broad-exception-caught
@@ -2993,10 +3288,11 @@ def _deliver_to_many(
2993
3288
 
2994
3289
  for sid, mid in delivered:
2995
3290
  console.print(
2996
- f"[green]→[/green] Sent to [bold]{sid}[/bold] " f"[dim](id={mid}, type={message_type})[/dim]"
3291
+ f"[green]→[/green] Sent to [bold]{_plain(sid)}[/bold] "
3292
+ f"[dim](id={_plain(mid)}, type={_plain(message_type)})[/dim]"
2997
3293
  )
2998
3294
  for sid, reason in failed:
2999
- console.print(f"[red]✗[/red] [bold]{sid}[/bold]: {reason}")
3295
+ console.print(f"[red]✗[/red] [bold]{_plain(sid)}[/bold]: {_plain(reason)}")
3000
3296
 
3001
3297
  if failed and not delivered:
3002
3298
  return 1
@@ -3005,6 +3301,7 @@ def _deliver_to_many(
3005
3301
  return 0
3006
3302
 
3007
3303
 
3304
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3008
3305
  @agents_app.command("send-to")
3009
3306
  def agents_send_to(
3010
3307
  to: str = typer.Argument(
@@ -3043,7 +3340,7 @@ def agents_send_to(
3043
3340
 
3044
3341
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3045
3342
  the machine that owns it, so there is no ``--compute``. Cross-host
3046
- messaging needs a transport, tracked in qBraid-CLI#328 / qbraid-core#307.
3343
+ messaging is not supported yet.
3047
3344
 
3048
3345
 
3049
3346
  Delivery is a JSONL append at ~/.qbraid/inboxes/<to-session-id>.jsonl
@@ -3059,8 +3356,9 @@ def agents_send_to(
3059
3356
  raise typer.Exit(code=1) from err
3060
3357
 
3061
3358
  registry = _get_registry()
3062
- from_sid = _resolve_from_session(from_)
3359
+ from_sid = _resolve_from_session(from_, registry)
3063
3360
  to_sid = _resolve_target_session(registry, to)
3361
+ _note_offline_recipient(registry, to_sid)
3064
3362
 
3065
3363
  code = _deliver_to_many(
3066
3364
  [to_sid],
@@ -3074,6 +3372,7 @@ def agents_send_to(
3074
3372
  raise typer.Exit(code=code)
3075
3373
 
3076
3374
 
3375
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3077
3376
  @agents_app.command("broadcast")
3078
3377
  def agents_broadcast(
3079
3378
  text: str = typer.Argument(..., help="Message body — delivered to every target."),
@@ -3115,7 +3414,7 @@ def agents_broadcast(
3115
3414
 
3116
3415
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3117
3416
  the machine that owns it, so there is no ``--compute``. Cross-host
3118
- messaging needs a transport, tracked in qBraid-CLI#328 / qbraid-core#307.
3417
+ messaging is not supported yet.
3119
3418
 
3120
3419
  Two selectors — pick exactly one:
3121
3420
  * ``--team <team_id>`` — every session with that team_id (except sender)
@@ -3140,7 +3439,7 @@ def agents_broadcast(
3140
3439
  raise typer.Exit(code=2)
3141
3440
 
3142
3441
  registry = _get_registry()
3143
- from_sid = _resolve_from_session(from_)
3442
+ from_sid = _resolve_from_session(from_, registry)
3144
3443
 
3145
3444
  if team is not None:
3146
3445
  members = _resolve_team_targets(registry, team, exclude_session_id=from_sid)
@@ -3201,14 +3500,15 @@ def _render_messages_table(messages, session_id: str, full_ids: bool = False) ->
3201
3500
  from_display = msg.from_session if full_ids else msg.from_session[:12]
3202
3501
  table.add_row(
3203
3502
  ts,
3204
- from_display,
3205
- msg.type.value,
3503
+ _plain(from_display),
3504
+ _plain(msg.type.value),
3206
3505
  read_marker,
3207
- msg.text,
3506
+ _plain(msg.text),
3208
3507
  )
3209
3508
  console.print(table)
3210
3509
 
3211
3510
 
3511
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3212
3512
  @inbox_app.callback()
3213
3513
  def inbox_default( # pylint: disable=too-many-arguments
3214
3514
  ctx: typer.Context,
@@ -3247,7 +3547,7 @@ def inbox_default( # pylint: disable=too-many-arguments
3247
3547
 
3248
3548
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3249
3549
  the machine that owns it, so there is no ``--compute``. Cross-host
3250
- messaging needs a transport, tracked in qBraid-CLI#328 / qbraid-core#307.
3550
+ messaging is not supported yet.
3251
3551
  """
3252
3552
  if ctx.invoked_subcommand is not None:
3253
3553
  return
@@ -3279,8 +3579,8 @@ def inbox_default( # pylint: disable=too-many-arguments
3279
3579
  from_display = msg.from_session if wide else msg.from_session[:12]
3280
3580
  console.print(
3281
3581
  f"[dim]{ts}[/dim] "
3282
- f"[cyan]{from_display}[/cyan] "
3283
- f"[yellow]({msg.type.value})[/yellow] {msg.text}"
3582
+ f"[cyan]{_plain(from_display)}[/cyan] "
3583
+ f"[yellow]({_plain(msg.type.value)})[/yellow] {_plain(msg.text)}"
3284
3584
  )
3285
3585
  if mark_read_flag:
3286
3586
  mark_read(session_id, message_ids=[msg.id])
@@ -3343,6 +3643,7 @@ def inbox_mark_read(
3343
3643
  # =============================================================================
3344
3644
 
3345
3645
 
3646
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3346
3647
  @agents_app.command("team")
3347
3648
  def agents_team(
3348
3649
  team_id: str = typer.Argument(..., help="Team identifier (matches AgentSession.team_id)."),
@@ -3357,7 +3658,7 @@ def agents_team(
3357
3658
 
3358
3659
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3359
3660
  the machine that owns it, so there is no ``--compute``. Cross-host
3360
- messaging needs a transport, tracked in qBraid-CLI#328 / qbraid-core#307.
3661
+ messaging is not supported yet.
3361
3662
 
3362
3663
  Answers the leader-orchestrator question "who is on my team, how many
3363
3664
  unread messages do they have, and what was the last thing they heard from
@@ -3380,7 +3681,7 @@ def agents_team(
3380
3681
 
3381
3682
  if not members:
3382
3683
  console.print(
3383
- f"[yellow]No sessions found with team_id '{team_id}'.[/yellow] "
3684
+ f"[yellow]No sessions found with team_id '{_plain(team_id)}'.[/yellow] "
3384
3685
  "Check 'qbraid agents list' for known team_ids."
3385
3686
  )
3386
3687
  raise typer.Exit(code=0)
@@ -3400,7 +3701,7 @@ def agents_team(
3400
3701
  table = Table(
3401
3702
  show_header=True,
3402
3703
  header_style="bold",
3403
- title=f"Team: {team_id} ({len(members)} member{'s' if len(members) != 1 else ''})",
3704
+ title=f"Team: {_plain(team_id)} ({len(members)} member{'s' if len(members) != 1 else ''})",
3404
3705
  )
3405
3706
  table.add_column("You", justify="center", no_wrap=True)
3406
3707
  table.add_column("Session", style="cyan", no_wrap=wide)
@@ -3432,12 +3733,12 @@ def agents_team(
3432
3733
 
3433
3734
  table.add_row(
3434
3735
  "★" if s.session_id == me_sid else "",
3435
- session_display,
3436
- s.agent_type or "worker",
3736
+ _plain(session_display),
3737
+ _plain(s.agent_type or "worker"),
3437
3738
  f"[{status_style}]{s.status or 'unknown'}[/{status_style}]",
3438
3739
  f"[{unread_style}]{unread_count}[/{unread_style}]",
3439
- last_from,
3440
- last_text,
3740
+ _plain(last_from),
3741
+ _plain(last_text),
3441
3742
  )
3442
3743
 
3443
3744
  console.print(table)
@@ -76,7 +76,16 @@ def slack_send(
76
76
  file: Optional[str] = typer.Option(None, "--file", "-f", help="UTF-8 file, or - for stdin."),
77
77
  as_json: bool = typer.Option(False, "--json", help="Output the API result as JSON."),
78
78
  ):
79
- """Send a Slack DM to yourself."""
79
+ """Send a Slack DM to yourself.
80
+
81
+ Give the message as an argument or with --file (- reads stdin), not both.
82
+ Messages are limited to 3,000 characters. Invalid input exits 2 and API
83
+ failures exit 1. Sends are never retried, since delivery can succeed even
84
+ if the response is lost.
85
+
86
+ Example:
87
+ qbraid comms slack send "Tests passed" --json
88
+ """
80
89
  text = _message(message, file)
81
90
  data = _run("send_slack", text)
82
91
  _sent(data, as_json, "Slack update sent.")
@@ -89,7 +98,16 @@ def email_send(
89
98
  file: Optional[str] = typer.Option(None, "--file", "-f", help="UTF-8 file, or - for stdin."),
90
99
  as_json: bool = typer.Option(False, "--json", help="Output the API result as JSON."),
91
100
  ):
92
- """Email an update to yourself."""
101
+ """Email an update to yourself.
102
+
103
+ Mail goes to your qBraid account address. Give the body as an argument or
104
+ with --file (- reads stdin), not both. Bodies are limited to 20,000
105
+ characters, and the API allows 20 emails per hour. Invalid input exits 2
106
+ and API failures exit 1. Sends are never retried.
107
+
108
+ Example:
109
+ qbraid comms email send --subject "Progress" --file report.txt --json
110
+ """
93
111
  text = _message(message, file)
94
112
  data = _run("send_email", subject, text)
95
113
  _sent(data, as_json, "Email update sent.")
@@ -108,7 +126,18 @@ def slack_read(
108
126
  ),
109
127
  as_json: bool = typer.Option(False, "--json", help="Output messages and pagination as JSON."),
110
128
  ):
111
- """Read your messages to the Slack bot."""
129
+ """Read your messages to the Slack bot.
130
+
131
+ Returns one page, newest first, of messages you sent in the bot DM; thread
132
+ replies are not included. --limit applies before filtering, so an empty
133
+ page can still have a next cursor. Keep --after fixed while following
134
+ cursors, and keep timestamps as strings, not floats. Reading does not mark
135
+ messages as read. Slack rate limits can be as low as one request per
136
+ minute, so avoid tight polling loops.
137
+
138
+ Example:
139
+ qbraid comms slack read --after 1789940000.000001 --json
140
+ """
112
141
  data = _run("read_slack", after=after, cursor=cursor, limit=limit)
113
142
  if as_json:
114
143
  typer.echo(json.dumps(data, ensure_ascii=True))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qbraid-cli
3
- Version: 0.16.6
3
+ Version: 0.16.8
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
@@ -30,11 +30,11 @@ Requires-Dist: typer>=0.12.1
30
30
  Requires-Dist: rich>=10.11.0
31
31
  Requires-Dist: click<=8.5.0
32
32
  Requires-Dist: questionary>=2.0.0
33
- Requires-Dist: qbraid-core>=0.6.15
33
+ Requires-Dist: qbraid-core>=0.6.19
34
34
  Provides-Extra: compute
35
- Requires-Dist: qbraid-core[compute]>=0.6.15; extra == "compute"
35
+ Requires-Dist: qbraid-core[compute]>=0.6.19; extra == "compute"
36
36
  Provides-Extra: envs
37
- Requires-Dist: qbraid-core[environments]>=0.6.15; extra == "envs"
37
+ Requires-Dist: qbraid-core[environments]>=0.6.19; extra == "envs"
38
38
  Provides-Extra: dev
39
39
  Requires-Dist: isort; extra == "dev"
40
40
  Requires-Dist: black; extra == "dev"
@@ -172,5 +172,5 @@ qbraid comms email send --subject "Progress" --file report.txt --json
172
172
  qbraid comms slack read --json
173
173
  ```
174
174
 
175
- See [communication commands](docs/comms-cli.md) for Slack connection, stdin input,
176
- pagination, and deployment requirements.
175
+ See [communication commands](https://docs.qbraid.com/v2/cli/api-reference/qbraid_comms) for Slack connection, stdin input,
176
+ message limits, and pagination.
@@ -2,10 +2,10 @@ typer>=0.12.1
2
2
  rich>=10.11.0
3
3
  click<=8.5.0
4
4
  questionary>=2.0.0
5
- qbraid-core>=0.6.15
5
+ qbraid-core>=0.6.19
6
6
 
7
7
  [compute]
8
- qbraid-core[compute]>=0.6.15
8
+ qbraid-core[compute]>=0.6.19
9
9
 
10
10
  [dev]
11
11
  isort
@@ -15,4 +15,4 @@ pytest-cov
15
15
  pytest-asyncio
16
16
 
17
17
  [envs]
18
- qbraid-core[environments]>=0.6.15
18
+ qbraid-core[environments]>=0.6.19
File without changes
File without changes