qbraid-cli 0.16.7__tar.gz → 0.16.9__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.7/qbraid_cli.egg-info → qbraid_cli-0.16.9}/PKG-INFO +6 -6
  2. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/README.md +2 -2
  3. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/pyproject.toml +4 -4
  4. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/agents/app.py +390 -48
  5. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/comms/app.py +32 -3
  6. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9/qbraid_cli.egg-info}/PKG-INFO +6 -6
  7. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/requires.txt +3 -3
  8. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/LICENSE +0 -0
  9. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/__init__.py +0 -0
  10. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/_build_info.py +0 -0
  11. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/_version.py +0 -0
  12. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/account/__init__.py +0 -0
  13. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/account/app.py +0 -0
  14. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/admin/__init__.py +0 -0
  15. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/admin/app.py +0 -0
  16. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/admin/headers.py +0 -0
  17. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/admin/validation.py +0 -0
  18. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/agents/__init__.py +0 -0
  19. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/agents/permission_response.py +0 -0
  20. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/ai/__init__.py +0 -0
  21. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/ai/app.py +0 -0
  22. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/ai/token.py +0 -0
  23. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/comms/__init__.py +0 -0
  24. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/__init__.py +0 -0
  25. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/app.py +0 -0
  26. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/instances.py +0 -0
  27. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/profiles.py +0 -0
  28. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/server.py +0 -0
  29. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/ssh.py +0 -0
  30. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/configure/__init__.py +0 -0
  31. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/configure/actions.py +0 -0
  32. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/configure/app.py +0 -0
  33. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/devices/__init__.py +0 -0
  34. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/devices/app.py +0 -0
  35. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/devices/validation.py +0 -0
  36. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/__init__.py +0 -0
  37. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/activate.py +0 -0
  38. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/app.py +0 -0
  39. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/create.py +0 -0
  40. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/data_handling.py +0 -0
  41. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/groups.py +0 -0
  42. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/publish.py +0 -0
  43. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/registry.py +0 -0
  44. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/share.py +0 -0
  45. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/exceptions.py +0 -0
  46. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/handlers.py +0 -0
  47. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/jobs/__init__.py +0 -0
  48. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/jobs/app.py +0 -0
  49. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/jobs/groups.py +0 -0
  50. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/kernels/__init__.py +0 -0
  51. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/kernels/app.py +0 -0
  52. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/lab/__init__.py +0 -0
  53. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/lab/app.py +0 -0
  54. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/main.py +0 -0
  55. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/pip/__init__.py +0 -0
  56. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/pip/app.py +0 -0
  57. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/pip/hooks.py +0 -0
  58. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/projects/__init__.py +0 -0
  59. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/projects/app.py +0 -0
  60. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/py.typed +0 -0
  61. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/skills/__init__.py +0 -0
  62. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/skills/app.py +0 -0
  63. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/troubleshoot/__init__.py +0 -0
  64. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/troubleshoot/app.py +0 -0
  65. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/SOURCES.txt +0 -0
  66. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/dependency_links.txt +0 -0
  67. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/entry_points.txt +0 -0
  68. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/top_level.txt +0 -0
  69. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/setup.cfg +0 -0
  70. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/tests/test_cli_e2e.py +0 -0
  71. {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/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.7
3
+ Version: 0.16.9
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.16
33
+ Requires-Dist: qbraid-core>=0.6.21
34
34
  Provides-Extra: compute
35
- Requires-Dist: qbraid-core[compute]>=0.6.16; extra == "compute"
35
+ Requires-Dist: qbraid-core[compute]>=0.6.21; extra == "compute"
36
36
  Provides-Extra: envs
37
- Requires-Dist: qbraid-core[environments]>=0.6.16; extra == "envs"
37
+ Requires-Dist: qbraid-core[environments]>=0.6.21; 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.7"
7
+ version = "0.16.9"
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.16",
35
+ "qbraid-core>=0.6.21",
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.16"]
47
- envs = ["qbraid-core[environments]>=0.6.16"]
46
+ compute = ["qbraid-core[compute]>=0.6.21"]
47
+ envs = ["qbraid-core[environments]>=0.6.21"]
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:
@@ -1446,6 +1582,69 @@ def agents_settings(
1446
1582
  handle_error(error_type=type(error).__name__, message=str(error), include_traceback=False)
1447
1583
 
1448
1584
 
1585
+ def _prompt_answer_kwargs(method, trust_folder=None, accept_bypass_warning=False):
1586
+ """Answers to the agent's own startup prompts, given on the command line.
1587
+
1588
+ ``trust_folder`` answers "trust this folder?" (Claude, Codex); either
1589
+ explicit answer needs a Core that takes it, because an older Core trusts
1590
+ every Claude launch folder by itself, so a silently dropped "no" would be
1591
+ a yes. ``accept_bypass_warning`` skips Claude's bypass-permissions
1592
+ confirmation for this run.
1593
+ """
1594
+ from qbraid_core.services.agents import AgentLauncherError
1595
+
1596
+ params = inspect.signature(method).parameters
1597
+ open_kwargs = any(p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values())
1598
+ kwargs = {}
1599
+ for key, value, flag in (
1600
+ ("trust_folder", trust_folder, "--trust-folder/--no-trust-folder"),
1601
+ ("accept_bypass_warning", accept_bypass_warning or None, "--skip-bypass-confirmation"),
1602
+ ):
1603
+ if value is None:
1604
+ continue
1605
+ if key not in params and not open_kwargs:
1606
+ raise AgentLauncherError(f"{flag} requires a newer qbraid-core on this machine.")
1607
+ kwargs[key] = value
1608
+ return kwargs
1609
+
1610
+
1611
+ def _hook_trust_kwargs(method, value):
1612
+ """Fail before launch when an explicit choice cannot be honored."""
1613
+ if value is None:
1614
+ return {}
1615
+
1616
+ from qbraid_core.services.agents import AgentLauncherError
1617
+
1618
+ params = inspect.signature(method).parameters
1619
+ if "trust_hooks" not in params and not any(
1620
+ p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values()
1621
+ ):
1622
+ raise AgentLauncherError("Hook trust requires a newer qbraid-core on this machine.")
1623
+ return {"trust_hooks": value}
1624
+
1625
+
1626
+ def _startup_pending(session):
1627
+ return session.tool.value in ("codex", "claude") and not getattr(session, "startup_confirmed_at", None)
1628
+
1629
+
1630
+ def _startup_notice(session, compute=None):
1631
+ import shlex
1632
+
1633
+ suffix = f" --compute {shlex.quote(compute)}" if compute else ""
1634
+ if _startup_pending(session):
1635
+ console.print(
1636
+ "Session created; startup is not yet confirmed. It may be waiting for sign-in, "
1637
+ "folder trust, or hook review. The initial task waits until setup is complete.",
1638
+ markup=False,
1639
+ )
1640
+ if session.tool.value == "codex":
1641
+ console.print(
1642
+ "Approve hooks in the agent's terminal (/hooks); Codex remembers unchanged ones.",
1643
+ markup=False,
1644
+ )
1645
+ console.print(f" qbraid agents attach {shlex.quote(session.session_id)}{suffix}", markup=False)
1646
+
1647
+
1449
1648
  @agents_app.command("launch")
1450
1649
  def agents_launch(
1451
1650
  backend: TerminalBackend = typer.Option(
@@ -1491,6 +1690,30 @@ def agents_launch(
1491
1690
  "decides. yolo: no permission checks. ask/auto: legacy modes, unchanged."
1492
1691
  ),
1493
1692
  ),
1693
+ trust_hooks: bool = typer.Option(
1694
+ False,
1695
+ "--trust-hooks",
1696
+ help=(
1697
+ "Codex only, DANGEROUS: run ALL enabled hooks (yours, the project's and "
1698
+ "plugins') without Codex's hook review, for this launch only. Not retained on "
1699
+ "resume. Refused with --repo. Independent of --approval and --yolo."
1700
+ ),
1701
+ ),
1702
+ trust_folder: Optional[bool] = typer.Option(
1703
+ None,
1704
+ "--trust-folder/--no-trust-folder",
1705
+ help=(
1706
+ "Answer the agent's 'trust this folder?' prompt now (Claude, Codex), so a detached "
1707
+ "agent does not stop at it. The answer is remembered for later launches and "
1708
+ "resumes. Trusting a folder lets its own settings, hooks and MCP servers run. "
1709
+ "Default: Claude trusts the folder, except a fresh --repo clone; Codex asks."
1710
+ ),
1711
+ ),
1712
+ skip_bypass_confirmation: bool = typer.Option(
1713
+ False,
1714
+ "--skip-bypass-confirmation",
1715
+ help="Claude with --yolo: skip its bypass-permissions confirmation, for this launch only.",
1716
+ ),
1494
1717
  yolo: bool = typer.Option(
1495
1718
  False, "--yolo", help="Claude: skip permissions. Codex: bypass approvals and its sandbox."
1496
1719
  ),
@@ -1519,6 +1742,14 @@ def agents_launch(
1519
1742
  tag = tag or result.get("tags")
1520
1743
  repo = repo or result.get("repo")
1521
1744
 
1745
+ if trust_hooks and repo:
1746
+ # Codex would run the clone's own .codex hooks unreviewed once its folder is
1747
+ # trusted. Core refuses this too; refuse here before cloning anything.
1748
+ raise AgentLauncherError(
1749
+ "--trust-hooks cannot be combined with --repo: the repository's project hooks "
1750
+ "would run without review. Clone and vet it, then launch with --cwd."
1751
+ )
1752
+
1522
1753
  if yolo:
1523
1754
  if tool not in ("claude", "claude-auto", "codex"):
1524
1755
  raise AgentLauncherError("--yolo is supported only for Claude and Codex.")
@@ -1539,6 +1770,12 @@ def agents_launch(
1539
1770
  if not compute or str(uuid.UUID(request_id)) != request_id:
1540
1771
  raise AgentLauncherError("--request-id requires --compute and a canonical UUID.")
1541
1772
 
1773
+ if compute and (trust_folder is not None or skip_bypass_confirmation):
1774
+ raise AgentLauncherError(
1775
+ "--trust-folder and --skip-bypass-confirmation are not yet supported for remote "
1776
+ "launches; answer the prompts in the agent's terminal."
1777
+ )
1778
+
1542
1779
  if compute and backend != TerminalBackend.AUTO:
1543
1780
  raise AgentLauncherError("Remote launches select the backend on the destination; omit --backend.")
1544
1781
  launcher = _backend_launcher(backend)
@@ -1556,6 +1793,7 @@ def agents_launch(
1556
1793
  tags=tag,
1557
1794
  **_request_id_kwargs(launcher.remote_launch, request_id),
1558
1795
  **_approval_kwargs(launcher.remote_launch, approval.value if approval else None),
1796
+ **_hook_trust_kwargs(launcher.remote_launch, True if trust_hooks else None),
1559
1797
  **_execution_kwargs(launcher.remote_launch, execution_mode.value if execution_mode else None),
1560
1798
  )
1561
1799
  else:
@@ -1574,11 +1812,17 @@ def agents_launch(
1574
1812
  team_id=team,
1575
1813
  tags=tag,
1576
1814
  **_approval_kwargs(launcher.launch, approval.value if approval else None),
1815
+ **_hook_trust_kwargs(launcher.launch, True if trust_hooks else None),
1816
+ **_prompt_answer_kwargs(launcher.launch, trust_folder, skip_bypass_confirmation),
1577
1817
  **_execution_kwargs(launcher.launch, execution_mode.value if execution_mode else None),
1578
1818
  progress_callback=_clone_progress if repo else None,
1579
1819
  )
1580
1820
 
1581
- status_val = session.status.value if session.status else "idle"
1821
+ status_val = (
1822
+ "starting / check terminal"
1823
+ if _startup_pending(session)
1824
+ else (session.status.value if session.status else "idle")
1825
+ )
1582
1826
  style = _status_style(status_val)
1583
1827
 
1584
1828
  lines = []
@@ -1604,7 +1848,7 @@ def agents_launch(
1604
1848
  console.print(panel)
1605
1849
  console.print()
1606
1850
  console.print("[dim]Open from the Agent Dashboard or attach with:[/dim]")
1607
- console.print(f" qbraid agents attach {session.session_id}")
1851
+ _startup_notice(session, compute)
1608
1852
 
1609
1853
  except AgentLauncherError as e:
1610
1854
  console.print(
@@ -1707,6 +1951,22 @@ def agents_resume(
1707
1951
  approval: Optional[ApprovalMode] = typer.Option(
1708
1952
  None, "--approval", help="Override recorded permissions for this resume. Omit to preserve them."
1709
1953
  ),
1954
+ trust_hooks: bool = typer.Option(
1955
+ False,
1956
+ "--trust-hooks",
1957
+ help=(
1958
+ "Codex only, DANGEROUS: run ALL enabled hooks without Codex's hook review, "
1959
+ "for this resume only. Hooks are reviewed again on every run that omits it."
1960
+ ),
1961
+ ),
1962
+ trust_folder: Optional[bool] = typer.Option(
1963
+ None,
1964
+ "--trust-folder/--no-trust-folder",
1965
+ help=(
1966
+ "Answer 'trust this folder?' for the folder it resumes in (remembered). "
1967
+ "Default: keep the answer given at launch."
1968
+ ),
1969
+ ),
1710
1970
  compute: Optional[str] = typer.Option(None, "--compute", "-c", help="SSH alias for remote compute"),
1711
1971
  ):
1712
1972
  """Resume a previous agent session.
@@ -1737,6 +1997,10 @@ def agents_resume(
1737
1997
  flags += f" --execution-mode {shlex.quote(execution_mode.value)}"
1738
1998
  if approval:
1739
1999
  flags += f" --approval {shlex.quote(approval.value)}"
2000
+ if trust_hooks:
2001
+ flags += " --trust-hooks"
2002
+ if trust_folder is not None:
2003
+ flags += " --trust-folder" if trust_folder else " --no-trust-folder"
1740
2004
  # markup=False -- see agents_status.
1741
2005
  console.print(
1742
2006
  _ssh_cmd(compute, f"qbraid agents resume {shlex.quote(session_id)}{flags}"),
@@ -1756,6 +2020,8 @@ def agents_resume(
1756
2020
  session_id=session_id,
1757
2021
  name=name,
1758
2022
  **(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
2023
+ **_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
2024
+ **_prompt_answer_kwargs(launcher.resume, trust_folder),
1759
2025
  **_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
1760
2026
  )
1761
2027
  except AgentLauncherError as e:
@@ -1800,6 +2066,13 @@ def agents_resume(
1800
2066
  cwd=original.cwd,
1801
2067
  name=fork_name,
1802
2068
  instructions=None,
2069
+ **_hook_trust_kwargs(
2070
+ launcher.launch,
2071
+ # Never inherited from the original: only an explicit
2072
+ # --trust-hooks on this command bypasses hook review.
2073
+ True if trust_hooks else None,
2074
+ ),
2075
+ **_prompt_answer_kwargs(launcher.launch, trust_folder),
1803
2076
  # A fork of a detached agent must not quietly
1804
2077
  # become one that waits for a human.
1805
2078
  **_approval_kwargs(
@@ -1876,10 +2149,16 @@ def agents_resume(
1876
2149
  session_id=selected,
1877
2150
  name=name,
1878
2151
  **(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
2152
+ **_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
2153
+ **_prompt_answer_kwargs(launcher.resume, trust_folder),
1879
2154
  **_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
1880
2155
  )
1881
2156
 
1882
- status_val = session.status.value if session.status else "idle"
2157
+ status_val = (
2158
+ "starting / check terminal"
2159
+ if _startup_pending(session)
2160
+ else (session.status.value if session.status else "idle")
2161
+ )
1883
2162
  style = _status_style(status_val)
1884
2163
 
1885
2164
  lines = []
@@ -1900,7 +2179,7 @@ def agents_resume(
1900
2179
  console.print(panel)
1901
2180
  console.print()
1902
2181
  console.print("[dim]Attach with:[/dim]")
1903
- console.print(f" qbraid agents attach {session.tmux_window or session.session_id[:8]}")
2182
+ _startup_notice(session)
1904
2183
 
1905
2184
  except AgentLauncherError as e:
1906
2185
  console.print(Panel(str(e), title="Resume Error", border_style="red"))
@@ -2865,17 +3144,44 @@ def hooks_uninstall():
2865
3144
  # =============================================================================
2866
3145
 
2867
3146
 
2868
- def _resolve_from_session(explicit: Optional[str]) -> str:
3147
+ def _resolve_from_session(explicit: Optional[str], registry=None) -> str:
2869
3148
  """Determine the sender session id.
2870
3149
 
2871
- Priority: explicit --from > QBRAID_SESSION_ID env var (set by the qBraid
2872
- launcher for every agent it spawns) > fail with a helpful error.
3150
+ Priority: explicit --from > the agent session this command runs inside
3151
+ (process ancestry, else QBRAID_SESSION_ID) > fail with a helpful error.
3152
+ An explicit sender may be an id prefix or a name, like a recipient, but
3153
+ inside an agent it must be that agent.
2873
3154
  """
3155
+ caller = _calling_session(registry) if registry is not None else None
2874
3156
  if explicit:
2875
- return explicit
3157
+ sender = explicit
3158
+ if registry is not None:
3159
+ match = _find_session_by_prefix(registry, explicit)
3160
+ if match is not None:
3161
+ sender = match.session_id
3162
+ if caller and sender != caller:
3163
+ # A coordination guardrail, not authentication: an agent speaks
3164
+ # as itself so recipients and permission rules see the real sender.
3165
+ console.print(
3166
+ f"[red]This command runs inside agent session {_plain(caller[:8])}; it cannot send "
3167
+ f"as {_plain(explicit)}.[/red] Omit --from to send as yourself."
3168
+ )
3169
+ raise typer.Exit(code=1)
3170
+ return sender
3171
+ if caller:
3172
+ return caller
2876
3173
  env_sid = os.environ.get("QBRAID_SESSION_ID")
2877
- if env_sid:
3174
+ if registry is None and env_sid:
2878
3175
  return env_sid
3176
+ if env_sid:
3177
+ # The variable outlived its agent (a pane kept it after the agent
3178
+ # stopped): whoever types here is the person, not that agent.
3179
+ console.print(
3180
+ f"[red]QBRAID_SESSION_ID names {_plain(env_sid[:8])}, which is not a running "
3181
+ "agent, so this shell does not send as it.[/red] Pass --from <session-id> to "
3182
+ "choose a sender."
3183
+ )
3184
+ raise typer.Exit(code=1)
2879
3185
  console.print(
2880
3186
  "[red]No sender session id available.[/red] "
2881
3187
  "Pass --from <session-id>, or run this inside a qBraid-managed "
@@ -2931,13 +3237,34 @@ def _resolve_target_session(registry, identifier: str) -> str:
2931
3237
  match = _find_session_by_prefix(registry, identifier)
2932
3238
  if match is None:
2933
3239
  console.print(
2934
- f"[red]No session matched '{identifier}'.[/red] "
3240
+ f"[red]No session matched '{_plain(identifier)}'.[/red] "
2935
3241
  "Try 'qbraid agents list' to see available sessions."
2936
3242
  )
2937
3243
  raise typer.Exit(code=1)
2938
3244
  return match.session_id
2939
3245
 
2940
3246
 
3247
+ def _note_offline_recipient(registry, session_id: str) -> None:
3248
+ """Say so when the recipient is not running: an inbox keeps the message,
3249
+ but nothing reads it until the session is resumed."""
3250
+ status = _session_status(registry, session_id)
3251
+ if status not in ("stopped", "error"):
3252
+ return
3253
+ try:
3254
+ from qbraid_core.services.agents import messaging # pylint: disable=import-outside-toplevel
3255
+
3256
+ successor = getattr(messaging, "live_successor", None)
3257
+ if callable(successor) and successor(registry, session_id): # pylint: disable=not-callable
3258
+ return # Core forwards it to the resumed session and says so.
3259
+ except Exception: # pylint: disable=broad-exception-caught
3260
+ pass
3261
+ console.print(
3262
+ f"[yellow]Note: {_plain(session_id[:8])} is {status}. The message waits in its inbox and is "
3263
+ "read when the conversation is resumed (in place, or with `qbraid agents resume`, "
3264
+ "which carries unread messages to the new session).[/yellow]"
3265
+ )
3266
+
3267
+
2941
3268
  def _resolve_team_targets(registry, team_id: str, exclude_session_id: Optional[str] = None) -> list:
2942
3269
  """Return every session in a team, optionally excluding one (usually the sender).
2943
3270
 
@@ -2967,7 +3294,8 @@ def _parse_target_csv(csv: str, registry) -> list[str]:
2967
3294
  resolved.append(match.session_id)
2968
3295
  if unresolved:
2969
3296
  console.print(
2970
- f"[red]No session matched: {', '.join(unresolved)}[/red]. " "Broadcast aborted; nothing sent."
3297
+ f"[red]No session matched: {_plain(', '.join(unresolved))}[/red]. "
3298
+ "Broadcast aborted; nothing sent."
2971
3299
  )
2972
3300
  raise typer.Exit(code=1)
2973
3301
  return resolved
@@ -3005,7 +3333,15 @@ def _deliver_to_many(
3005
3333
  registry=registry,
3006
3334
  validate_permissions=validate_permissions,
3007
3335
  )
3008
- delivered.append((target_sid, msg.id))
3336
+ delivered.append((getattr(msg, "to_session", None) or target_sid, msg.id))
3337
+ # Older Core has no forwarded_from; newer Core types it Optional[str].
3338
+ forwarded = getattr(msg, "forwarded_from", None)
3339
+ if isinstance(forwarded, str) and forwarded:
3340
+ old_sid, new_sid = str(forwarded), str(msg.to_session)
3341
+ console.print(
3342
+ f"[dim]{_plain(old_sid[:8])} was resumed as {_plain(new_sid[:8])}; "
3343
+ "delivered to the running session.[/dim]"
3344
+ )
3009
3345
  except AgentMessagingError as err:
3010
3346
  failed.append((target_sid, str(err)))
3011
3347
  except Exception as err: # pylint: disable=broad-exception-caught
@@ -3013,10 +3349,11 @@ def _deliver_to_many(
3013
3349
 
3014
3350
  for sid, mid in delivered:
3015
3351
  console.print(
3016
- f"[green]→[/green] Sent to [bold]{sid}[/bold] " f"[dim](id={mid}, type={message_type})[/dim]"
3352
+ f"[green]→[/green] Sent to [bold]{_plain(sid)}[/bold] "
3353
+ f"[dim](id={_plain(mid)}, type={_plain(message_type)})[/dim]"
3017
3354
  )
3018
3355
  for sid, reason in failed:
3019
- console.print(f"[red]✗[/red] [bold]{sid}[/bold]: {reason}")
3356
+ console.print(f"[red]✗[/red] [bold]{_plain(sid)}[/bold]: {_plain(reason)}")
3020
3357
 
3021
3358
  if failed and not delivered:
3022
3359
  return 1
@@ -3025,6 +3362,7 @@ def _deliver_to_many(
3025
3362
  return 0
3026
3363
 
3027
3364
 
3365
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3028
3366
  @agents_app.command("send-to")
3029
3367
  def agents_send_to(
3030
3368
  to: str = typer.Argument(
@@ -3063,7 +3401,7 @@ def agents_send_to(
3063
3401
 
3064
3402
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3065
3403
  the machine that owns it, so there is no ``--compute``. Cross-host
3066
- messaging needs a transport, tracked in qBraid-CLI#328 / qbraid-core#307.
3404
+ messaging is not supported yet.
3067
3405
 
3068
3406
 
3069
3407
  Delivery is a JSONL append at ~/.qbraid/inboxes/<to-session-id>.jsonl
@@ -3079,8 +3417,9 @@ def agents_send_to(
3079
3417
  raise typer.Exit(code=1) from err
3080
3418
 
3081
3419
  registry = _get_registry()
3082
- from_sid = _resolve_from_session(from_)
3420
+ from_sid = _resolve_from_session(from_, registry)
3083
3421
  to_sid = _resolve_target_session(registry, to)
3422
+ _note_offline_recipient(registry, to_sid)
3084
3423
 
3085
3424
  code = _deliver_to_many(
3086
3425
  [to_sid],
@@ -3094,6 +3433,7 @@ def agents_send_to(
3094
3433
  raise typer.Exit(code=code)
3095
3434
 
3096
3435
 
3436
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3097
3437
  @agents_app.command("broadcast")
3098
3438
  def agents_broadcast(
3099
3439
  text: str = typer.Argument(..., help="Message body — delivered to every target."),
@@ -3135,7 +3475,7 @@ def agents_broadcast(
3135
3475
 
3136
3476
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3137
3477
  the machine that owns it, so there is no ``--compute``. Cross-host
3138
- messaging needs a transport, tracked in qBraid-CLI#328 / qbraid-core#307.
3478
+ messaging is not supported yet.
3139
3479
 
3140
3480
  Two selectors — pick exactly one:
3141
3481
  * ``--team <team_id>`` — every session with that team_id (except sender)
@@ -3160,7 +3500,7 @@ def agents_broadcast(
3160
3500
  raise typer.Exit(code=2)
3161
3501
 
3162
3502
  registry = _get_registry()
3163
- from_sid = _resolve_from_session(from_)
3503
+ from_sid = _resolve_from_session(from_, registry)
3164
3504
 
3165
3505
  if team is not None:
3166
3506
  members = _resolve_team_targets(registry, team, exclude_session_id=from_sid)
@@ -3221,14 +3561,15 @@ def _render_messages_table(messages, session_id: str, full_ids: bool = False) ->
3221
3561
  from_display = msg.from_session if full_ids else msg.from_session[:12]
3222
3562
  table.add_row(
3223
3563
  ts,
3224
- from_display,
3225
- msg.type.value,
3564
+ _plain(from_display),
3565
+ _plain(msg.type.value),
3226
3566
  read_marker,
3227
- msg.text,
3567
+ _plain(msg.text),
3228
3568
  )
3229
3569
  console.print(table)
3230
3570
 
3231
3571
 
3572
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3232
3573
  @inbox_app.callback()
3233
3574
  def inbox_default( # pylint: disable=too-many-arguments
3234
3575
  ctx: typer.Context,
@@ -3267,7 +3608,7 @@ def inbox_default( # pylint: disable=too-many-arguments
3267
3608
 
3268
3609
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3269
3610
  the machine that owns it, so there is no ``--compute``. Cross-host
3270
- messaging needs a transport, tracked in qBraid-CLI#328 / qbraid-core#307.
3611
+ messaging is not supported yet.
3271
3612
  """
3272
3613
  if ctx.invoked_subcommand is not None:
3273
3614
  return
@@ -3299,8 +3640,8 @@ def inbox_default( # pylint: disable=too-many-arguments
3299
3640
  from_display = msg.from_session if wide else msg.from_session[:12]
3300
3641
  console.print(
3301
3642
  f"[dim]{ts}[/dim] "
3302
- f"[cyan]{from_display}[/cyan] "
3303
- f"[yellow]({msg.type.value})[/yellow] {msg.text}"
3643
+ f"[cyan]{_plain(from_display)}[/cyan] "
3644
+ f"[yellow]({_plain(msg.type.value)})[/yellow] {_plain(msg.text)}"
3304
3645
  )
3305
3646
  if mark_read_flag:
3306
3647
  mark_read(session_id, message_ids=[msg.id])
@@ -3363,6 +3704,7 @@ def inbox_mark_read(
3363
3704
  # =============================================================================
3364
3705
 
3365
3706
 
3707
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3366
3708
  @agents_app.command("team")
3367
3709
  def agents_team(
3368
3710
  team_id: str = typer.Argument(..., help="Team identifier (matches AgentSession.team_id)."),
@@ -3377,7 +3719,7 @@ def agents_team(
3377
3719
 
3378
3720
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3379
3721
  the machine that owns it, so there is no ``--compute``. Cross-host
3380
- messaging needs a transport, tracked in qBraid-CLI#328 / qbraid-core#307.
3722
+ messaging is not supported yet.
3381
3723
 
3382
3724
  Answers the leader-orchestrator question "who is on my team, how many
3383
3725
  unread messages do they have, and what was the last thing they heard from
@@ -3400,7 +3742,7 @@ def agents_team(
3400
3742
 
3401
3743
  if not members:
3402
3744
  console.print(
3403
- f"[yellow]No sessions found with team_id '{team_id}'.[/yellow] "
3745
+ f"[yellow]No sessions found with team_id '{_plain(team_id)}'.[/yellow] "
3404
3746
  "Check 'qbraid agents list' for known team_ids."
3405
3747
  )
3406
3748
  raise typer.Exit(code=0)
@@ -3420,7 +3762,7 @@ def agents_team(
3420
3762
  table = Table(
3421
3763
  show_header=True,
3422
3764
  header_style="bold",
3423
- title=f"Team: {team_id} ({len(members)} member{'s' if len(members) != 1 else ''})",
3765
+ title=f"Team: {_plain(team_id)} ({len(members)} member{'s' if len(members) != 1 else ''})",
3424
3766
  )
3425
3767
  table.add_column("You", justify="center", no_wrap=True)
3426
3768
  table.add_column("Session", style="cyan", no_wrap=wide)
@@ -3452,12 +3794,12 @@ def agents_team(
3452
3794
 
3453
3795
  table.add_row(
3454
3796
  "★" if s.session_id == me_sid else "",
3455
- session_display,
3456
- s.agent_type or "worker",
3797
+ _plain(session_display),
3798
+ _plain(s.agent_type or "worker"),
3457
3799
  f"[{status_style}]{s.status or 'unknown'}[/{status_style}]",
3458
3800
  f"[{unread_style}]{unread_count}[/{unread_style}]",
3459
- last_from,
3460
- last_text,
3801
+ _plain(last_from),
3802
+ _plain(last_text),
3461
3803
  )
3462
3804
 
3463
3805
  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.7
3
+ Version: 0.16.9
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.16
33
+ Requires-Dist: qbraid-core>=0.6.21
34
34
  Provides-Extra: compute
35
- Requires-Dist: qbraid-core[compute]>=0.6.16; extra == "compute"
35
+ Requires-Dist: qbraid-core[compute]>=0.6.21; extra == "compute"
36
36
  Provides-Extra: envs
37
- Requires-Dist: qbraid-core[environments]>=0.6.16; extra == "envs"
37
+ Requires-Dist: qbraid-core[environments]>=0.6.21; 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.16
5
+ qbraid-core>=0.6.21
6
6
 
7
7
  [compute]
8
- qbraid-core[compute]>=0.6.16
8
+ qbraid-core[compute]>=0.6.21
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.16
18
+ qbraid-core[environments]>=0.6.21
File without changes
File without changes