qbraid-cli 0.16.7__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.7/qbraid_cli.egg-info → qbraid_cli-0.16.8}/PKG-INFO +6 -6
  2. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/README.md +2 -2
  3. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/pyproject.toml +4 -4
  4. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/agents/app.py +329 -48
  5. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/comms/app.py +32 -3
  6. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8/qbraid_cli.egg-info}/PKG-INFO +6 -6
  7. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/requires.txt +3 -3
  8. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/LICENSE +0 -0
  9. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/__init__.py +0 -0
  10. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/_build_info.py +0 -0
  11. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/_version.py +0 -0
  12. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/account/__init__.py +0 -0
  13. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/account/app.py +0 -0
  14. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/admin/__init__.py +0 -0
  15. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/admin/app.py +0 -0
  16. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/admin/headers.py +0 -0
  17. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/admin/validation.py +0 -0
  18. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/agents/__init__.py +0 -0
  19. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/agents/permission_response.py +0 -0
  20. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/ai/__init__.py +0 -0
  21. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/ai/app.py +0 -0
  22. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/ai/token.py +0 -0
  23. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/comms/__init__.py +0 -0
  24. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/__init__.py +0 -0
  25. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/app.py +0 -0
  26. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/instances.py +0 -0
  27. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/profiles.py +0 -0
  28. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/server.py +0 -0
  29. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/ssh.py +0 -0
  30. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/configure/__init__.py +0 -0
  31. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/configure/actions.py +0 -0
  32. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/configure/app.py +0 -0
  33. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/devices/__init__.py +0 -0
  34. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/devices/app.py +0 -0
  35. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/devices/validation.py +0 -0
  36. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/__init__.py +0 -0
  37. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/activate.py +0 -0
  38. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/app.py +0 -0
  39. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/create.py +0 -0
  40. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/data_handling.py +0 -0
  41. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/groups.py +0 -0
  42. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/publish.py +0 -0
  43. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/registry.py +0 -0
  44. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/share.py +0 -0
  45. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/exceptions.py +0 -0
  46. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/handlers.py +0 -0
  47. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/jobs/__init__.py +0 -0
  48. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/jobs/app.py +0 -0
  49. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/jobs/groups.py +0 -0
  50. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/kernels/__init__.py +0 -0
  51. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/kernels/app.py +0 -0
  52. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/lab/__init__.py +0 -0
  53. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/lab/app.py +0 -0
  54. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/main.py +0 -0
  55. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/pip/__init__.py +0 -0
  56. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/pip/app.py +0 -0
  57. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/pip/hooks.py +0 -0
  58. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/projects/__init__.py +0 -0
  59. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/projects/app.py +0 -0
  60. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/py.typed +0 -0
  61. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/skills/__init__.py +0 -0
  62. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/skills/app.py +0 -0
  63. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/troubleshoot/__init__.py +0 -0
  64. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/troubleshoot/app.py +0 -0
  65. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/SOURCES.txt +0 -0
  66. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/dependency_links.txt +0 -0
  67. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/entry_points.txt +0 -0
  68. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/top_level.txt +0 -0
  69. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/setup.cfg +0 -0
  70. {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/tests/test_cli_e2e.py +0 -0
  71. {qbraid_cli-0.16.7 → 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.7
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.16
33
+ Requires-Dist: qbraid-core>=0.6.19
34
34
  Provides-Extra: compute
35
- Requires-Dist: qbraid-core[compute]>=0.6.16; 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.16; 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.7"
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.16",
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.16"]
47
- envs = ["qbraid-core[environments]>=0.6.16"]
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:
@@ -1446,6 +1582,43 @@ def agents_settings(
1446
1582
  handle_error(error_type=type(error).__name__, message=str(error), include_traceback=False)
1447
1583
 
1448
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
+
1449
1622
  @agents_app.command("launch")
1450
1623
  def agents_launch(
1451
1624
  backend: TerminalBackend = typer.Option(
@@ -1491,6 +1664,15 @@ def agents_launch(
1491
1664
  "decides. yolo: no permission checks. ask/auto: legacy modes, unchanged."
1492
1665
  ),
1493
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
+ ),
1494
1676
  yolo: bool = typer.Option(
1495
1677
  False, "--yolo", help="Claude: skip permissions. Codex: bypass approvals and its sandbox."
1496
1678
  ),
@@ -1519,6 +1701,14 @@ def agents_launch(
1519
1701
  tag = tag or result.get("tags")
1520
1702
  repo = repo or result.get("repo")
1521
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
+
1522
1712
  if yolo:
1523
1713
  if tool not in ("claude", "claude-auto", "codex"):
1524
1714
  raise AgentLauncherError("--yolo is supported only for Claude and Codex.")
@@ -1556,6 +1746,7 @@ def agents_launch(
1556
1746
  tags=tag,
1557
1747
  **_request_id_kwargs(launcher.remote_launch, request_id),
1558
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),
1559
1750
  **_execution_kwargs(launcher.remote_launch, execution_mode.value if execution_mode else None),
1560
1751
  )
1561
1752
  else:
@@ -1574,11 +1765,16 @@ def agents_launch(
1574
1765
  team_id=team,
1575
1766
  tags=tag,
1576
1767
  **_approval_kwargs(launcher.launch, approval.value if approval else None),
1768
+ **_hook_trust_kwargs(launcher.launch, True if trust_hooks else None),
1577
1769
  **_execution_kwargs(launcher.launch, execution_mode.value if execution_mode else None),
1578
1770
  progress_callback=_clone_progress if repo else None,
1579
1771
  )
1580
1772
 
1581
- 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
+ )
1582
1778
  style = _status_style(status_val)
1583
1779
 
1584
1780
  lines = []
@@ -1604,7 +1800,7 @@ def agents_launch(
1604
1800
  console.print(panel)
1605
1801
  console.print()
1606
1802
  console.print("[dim]Open from the Agent Dashboard or attach with:[/dim]")
1607
- console.print(f" qbraid agents attach {session.session_id}")
1803
+ _startup_notice(session, compute)
1608
1804
 
1609
1805
  except AgentLauncherError as e:
1610
1806
  console.print(
@@ -1707,6 +1903,14 @@ def agents_resume(
1707
1903
  approval: Optional[ApprovalMode] = typer.Option(
1708
1904
  None, "--approval", help="Override recorded permissions for this resume. Omit to preserve them."
1709
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
+ ),
1710
1914
  compute: Optional[str] = typer.Option(None, "--compute", "-c", help="SSH alias for remote compute"),
1711
1915
  ):
1712
1916
  """Resume a previous agent session.
@@ -1737,6 +1941,8 @@ def agents_resume(
1737
1941
  flags += f" --execution-mode {shlex.quote(execution_mode.value)}"
1738
1942
  if approval:
1739
1943
  flags += f" --approval {shlex.quote(approval.value)}"
1944
+ if trust_hooks:
1945
+ flags += " --trust-hooks"
1740
1946
  # markup=False -- see agents_status.
1741
1947
  console.print(
1742
1948
  _ssh_cmd(compute, f"qbraid agents resume {shlex.quote(session_id)}{flags}"),
@@ -1756,6 +1962,7 @@ def agents_resume(
1756
1962
  session_id=session_id,
1757
1963
  name=name,
1758
1964
  **(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
1965
+ **_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
1759
1966
  **_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
1760
1967
  )
1761
1968
  except AgentLauncherError as e:
@@ -1800,6 +2007,12 @@ def agents_resume(
1800
2007
  cwd=original.cwd,
1801
2008
  name=fork_name,
1802
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
+ ),
1803
2016
  # A fork of a detached agent must not quietly
1804
2017
  # become one that waits for a human.
1805
2018
  **_approval_kwargs(
@@ -1876,10 +2089,15 @@ def agents_resume(
1876
2089
  session_id=selected,
1877
2090
  name=name,
1878
2091
  **(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
2092
+ **_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
1879
2093
  **_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
1880
2094
  )
1881
2095
 
1882
- 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
+ )
1883
2101
  style = _status_style(status_val)
1884
2102
 
1885
2103
  lines = []
@@ -1900,7 +2118,7 @@ def agents_resume(
1900
2118
  console.print(panel)
1901
2119
  console.print()
1902
2120
  console.print("[dim]Attach with:[/dim]")
1903
- console.print(f" qbraid agents attach {session.tmux_window or session.session_id[:8]}")
2121
+ _startup_notice(session)
1904
2122
 
1905
2123
  except AgentLauncherError as e:
1906
2124
  console.print(Panel(str(e), title="Resume Error", border_style="red"))
@@ -2865,17 +3083,44 @@ def hooks_uninstall():
2865
3083
  # =============================================================================
2866
3084
 
2867
3085
 
2868
- def _resolve_from_session(explicit: Optional[str]) -> str:
3086
+ def _resolve_from_session(explicit: Optional[str], registry=None) -> str:
2869
3087
  """Determine the sender session id.
2870
3088
 
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.
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.
2873
3093
  """
3094
+ caller = _calling_session(registry) if registry is not None else None
2874
3095
  if explicit:
2875
- 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
2876
3112
  env_sid = os.environ.get("QBRAID_SESSION_ID")
2877
- if env_sid:
3113
+ if registry is None and env_sid:
2878
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)
2879
3124
  console.print(
2880
3125
  "[red]No sender session id available.[/red] "
2881
3126
  "Pass --from <session-id>, or run this inside a qBraid-managed "
@@ -2931,13 +3176,34 @@ def _resolve_target_session(registry, identifier: str) -> str:
2931
3176
  match = _find_session_by_prefix(registry, identifier)
2932
3177
  if match is None:
2933
3178
  console.print(
2934
- f"[red]No session matched '{identifier}'.[/red] "
3179
+ f"[red]No session matched '{_plain(identifier)}'.[/red] "
2935
3180
  "Try 'qbraid agents list' to see available sessions."
2936
3181
  )
2937
3182
  raise typer.Exit(code=1)
2938
3183
  return match.session_id
2939
3184
 
2940
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
+
2941
3207
  def _resolve_team_targets(registry, team_id: str, exclude_session_id: Optional[str] = None) -> list:
2942
3208
  """Return every session in a team, optionally excluding one (usually the sender).
2943
3209
 
@@ -2967,7 +3233,8 @@ def _parse_target_csv(csv: str, registry) -> list[str]:
2967
3233
  resolved.append(match.session_id)
2968
3234
  if unresolved:
2969
3235
  console.print(
2970
- 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."
2971
3238
  )
2972
3239
  raise typer.Exit(code=1)
2973
3240
  return resolved
@@ -3005,7 +3272,15 @@ def _deliver_to_many(
3005
3272
  registry=registry,
3006
3273
  validate_permissions=validate_permissions,
3007
3274
  )
3008
- 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
+ )
3009
3284
  except AgentMessagingError as err:
3010
3285
  failed.append((target_sid, str(err)))
3011
3286
  except Exception as err: # pylint: disable=broad-exception-caught
@@ -3013,10 +3288,11 @@ def _deliver_to_many(
3013
3288
 
3014
3289
  for sid, mid in delivered:
3015
3290
  console.print(
3016
- 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]"
3017
3293
  )
3018
3294
  for sid, reason in failed:
3019
- console.print(f"[red]✗[/red] [bold]{sid}[/bold]: {reason}")
3295
+ console.print(f"[red]✗[/red] [bold]{_plain(sid)}[/bold]: {_plain(reason)}")
3020
3296
 
3021
3297
  if failed and not delivered:
3022
3298
  return 1
@@ -3025,6 +3301,7 @@ def _deliver_to_many(
3025
3301
  return 0
3026
3302
 
3027
3303
 
3304
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3028
3305
  @agents_app.command("send-to")
3029
3306
  def agents_send_to(
3030
3307
  to: str = typer.Argument(
@@ -3063,7 +3340,7 @@ def agents_send_to(
3063
3340
 
3064
3341
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3065
3342
  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.
3343
+ messaging is not supported yet.
3067
3344
 
3068
3345
 
3069
3346
  Delivery is a JSONL append at ~/.qbraid/inboxes/<to-session-id>.jsonl
@@ -3079,8 +3356,9 @@ def agents_send_to(
3079
3356
  raise typer.Exit(code=1) from err
3080
3357
 
3081
3358
  registry = _get_registry()
3082
- from_sid = _resolve_from_session(from_)
3359
+ from_sid = _resolve_from_session(from_, registry)
3083
3360
  to_sid = _resolve_target_session(registry, to)
3361
+ _note_offline_recipient(registry, to_sid)
3084
3362
 
3085
3363
  code = _deliver_to_many(
3086
3364
  [to_sid],
@@ -3094,6 +3372,7 @@ def agents_send_to(
3094
3372
  raise typer.Exit(code=code)
3095
3373
 
3096
3374
 
3375
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3097
3376
  @agents_app.command("broadcast")
3098
3377
  def agents_broadcast(
3099
3378
  text: str = typer.Argument(..., help="Message body — delivered to every target."),
@@ -3135,7 +3414,7 @@ def agents_broadcast(
3135
3414
 
3136
3415
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3137
3416
  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.
3417
+ messaging is not supported yet.
3139
3418
 
3140
3419
  Two selectors — pick exactly one:
3141
3420
  * ``--team <team_id>`` — every session with that team_id (except sender)
@@ -3160,7 +3439,7 @@ def agents_broadcast(
3160
3439
  raise typer.Exit(code=2)
3161
3440
 
3162
3441
  registry = _get_registry()
3163
- from_sid = _resolve_from_session(from_)
3442
+ from_sid = _resolve_from_session(from_, registry)
3164
3443
 
3165
3444
  if team is not None:
3166
3445
  members = _resolve_team_targets(registry, team, exclude_session_id=from_sid)
@@ -3221,14 +3500,15 @@ def _render_messages_table(messages, session_id: str, full_ids: bool = False) ->
3221
3500
  from_display = msg.from_session if full_ids else msg.from_session[:12]
3222
3501
  table.add_row(
3223
3502
  ts,
3224
- from_display,
3225
- msg.type.value,
3503
+ _plain(from_display),
3504
+ _plain(msg.type.value),
3226
3505
  read_marker,
3227
- msg.text,
3506
+ _plain(msg.text),
3228
3507
  )
3229
3508
  console.print(table)
3230
3509
 
3231
3510
 
3511
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3232
3512
  @inbox_app.callback()
3233
3513
  def inbox_default( # pylint: disable=too-many-arguments
3234
3514
  ctx: typer.Context,
@@ -3267,7 +3547,7 @@ def inbox_default( # pylint: disable=too-many-arguments
3267
3547
 
3268
3548
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3269
3549
  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.
3550
+ messaging is not supported yet.
3271
3551
  """
3272
3552
  if ctx.invoked_subcommand is not None:
3273
3553
  return
@@ -3299,8 +3579,8 @@ def inbox_default( # pylint: disable=too-many-arguments
3299
3579
  from_display = msg.from_session if wide else msg.from_session[:12]
3300
3580
  console.print(
3301
3581
  f"[dim]{ts}[/dim] "
3302
- f"[cyan]{from_display}[/cyan] "
3303
- 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)}"
3304
3584
  )
3305
3585
  if mark_read_flag:
3306
3586
  mark_read(session_id, message_ids=[msg.id])
@@ -3363,6 +3643,7 @@ def inbox_mark_read(
3363
3643
  # =============================================================================
3364
3644
 
3365
3645
 
3646
+ # Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
3366
3647
  @agents_app.command("team")
3367
3648
  def agents_team(
3368
3649
  team_id: str = typer.Argument(..., help="Team identifier (matches AgentSession.team_id)."),
@@ -3377,7 +3658,7 @@ def agents_team(
3377
3658
 
3378
3659
  Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
3379
3660
  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.
3661
+ messaging is not supported yet.
3381
3662
 
3382
3663
  Answers the leader-orchestrator question "who is on my team, how many
3383
3664
  unread messages do they have, and what was the last thing they heard from
@@ -3400,7 +3681,7 @@ def agents_team(
3400
3681
 
3401
3682
  if not members:
3402
3683
  console.print(
3403
- f"[yellow]No sessions found with team_id '{team_id}'.[/yellow] "
3684
+ f"[yellow]No sessions found with team_id '{_plain(team_id)}'.[/yellow] "
3404
3685
  "Check 'qbraid agents list' for known team_ids."
3405
3686
  )
3406
3687
  raise typer.Exit(code=0)
@@ -3420,7 +3701,7 @@ def agents_team(
3420
3701
  table = Table(
3421
3702
  show_header=True,
3422
3703
  header_style="bold",
3423
- 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 ''})",
3424
3705
  )
3425
3706
  table.add_column("You", justify="center", no_wrap=True)
3426
3707
  table.add_column("Session", style="cyan", no_wrap=wide)
@@ -3452,12 +3733,12 @@ def agents_team(
3452
3733
 
3453
3734
  table.add_row(
3454
3735
  "★" if s.session_id == me_sid else "",
3455
- session_display,
3456
- s.agent_type or "worker",
3736
+ _plain(session_display),
3737
+ _plain(s.agent_type or "worker"),
3457
3738
  f"[{status_style}]{s.status or 'unknown'}[/{status_style}]",
3458
3739
  f"[{unread_style}]{unread_count}[/{unread_style}]",
3459
- last_from,
3460
- last_text,
3740
+ _plain(last_from),
3741
+ _plain(last_text),
3461
3742
  )
3462
3743
 
3463
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.7
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.16
33
+ Requires-Dist: qbraid-core>=0.6.19
34
34
  Provides-Extra: compute
35
- Requires-Dist: qbraid-core[compute]>=0.6.16; 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.16; 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.16
5
+ qbraid-core>=0.6.19
6
6
 
7
7
  [compute]
8
- qbraid-core[compute]>=0.6.16
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.16
18
+ qbraid-core[environments]>=0.6.19
File without changes
File without changes