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.
- {qbraid_cli-0.16.7/qbraid_cli.egg-info → qbraid_cli-0.16.8}/PKG-INFO +6 -6
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/README.md +2 -2
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/pyproject.toml +4 -4
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/agents/app.py +329 -48
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/comms/app.py +32 -3
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8/qbraid_cli.egg-info}/PKG-INFO +6 -6
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/requires.txt +3 -3
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/LICENSE +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/_build_info.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/_version.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/account/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/account/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/admin/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/admin/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/admin/headers.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/admin/validation.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/agents/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/agents/permission_response.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/ai/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/ai/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/ai/token.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/comms/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/instances.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/profiles.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/server.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/compute/ssh.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/configure/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/configure/actions.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/configure/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/devices/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/devices/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/devices/validation.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/activate.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/create.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/data_handling.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/groups.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/publish.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/registry.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/envs/share.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/exceptions.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/handlers.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/jobs/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/jobs/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/jobs/groups.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/kernels/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/kernels/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/lab/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/lab/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/main.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/pip/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/pip/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/pip/hooks.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/projects/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/projects/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/py.typed +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/skills/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/skills/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/troubleshoot/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli/troubleshoot/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/SOURCES.txt +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/dependency_links.txt +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/entry_points.txt +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/qbraid_cli.egg-info/top_level.txt +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/setup.cfg +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.8}/tests/test_cli_e2e.py +0 -0
- {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.
|
|
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.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.6.19
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.19; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
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/
|
|
176
|
-
|
|
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/
|
|
131
|
-
|
|
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
|
+
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.
|
|
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.
|
|
47
|
-
envs = ["qbraid-core[environments]>=0.6.
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 >
|
|
2872
|
-
|
|
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
|
-
|
|
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]. "
|
|
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] "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.6.19
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.19; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
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/
|
|
176
|
-
|
|
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.
|
|
5
|
+
qbraid-core>=0.6.19
|
|
6
6
|
|
|
7
7
|
[compute]
|
|
8
|
-
qbraid-core[compute]>=0.6.
|
|
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.
|
|
18
|
+
qbraid-core[environments]>=0.6.19
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|