qbraid-cli 0.16.7__tar.gz → 0.16.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qbraid_cli-0.16.7/qbraid_cli.egg-info → qbraid_cli-0.16.9}/PKG-INFO +6 -6
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/README.md +2 -2
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/pyproject.toml +4 -4
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/agents/app.py +390 -48
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/comms/app.py +32 -3
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9/qbraid_cli.egg-info}/PKG-INFO +6 -6
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/requires.txt +3 -3
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/LICENSE +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/_build_info.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/_version.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/account/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/account/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/admin/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/admin/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/admin/headers.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/admin/validation.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/agents/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/agents/permission_response.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/ai/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/ai/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/ai/token.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/comms/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/instances.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/profiles.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/server.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/compute/ssh.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/configure/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/configure/actions.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/configure/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/devices/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/devices/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/devices/validation.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/activate.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/create.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/data_handling.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/groups.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/publish.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/registry.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/envs/share.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/exceptions.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/handlers.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/jobs/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/jobs/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/jobs/groups.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/kernels/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/kernels/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/lab/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/lab/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/main.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/pip/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/pip/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/pip/hooks.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/projects/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/projects/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/py.typed +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/skills/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/skills/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/troubleshoot/__init__.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli/troubleshoot/app.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/SOURCES.txt +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/dependency_links.txt +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/entry_points.txt +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/qbraid_cli.egg-info/top_level.txt +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/setup.cfg +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/tests/test_cli_e2e.py +0 -0
- {qbraid_cli-0.16.7 → qbraid_cli-0.16.9}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.9
|
|
4
4
|
Summary: Command Line Interface for interacting with all parts of the qBraid platform.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -30,11 +30,11 @@ Requires-Dist: typer>=0.12.1
|
|
|
30
30
|
Requires-Dist: rich>=10.11.0
|
|
31
31
|
Requires-Dist: click<=8.5.0
|
|
32
32
|
Requires-Dist: questionary>=2.0.0
|
|
33
|
-
Requires-Dist: qbraid-core>=0.6.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.6.21
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.21; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]>=0.6.21; extra == "envs"
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: isort; extra == "dev"
|
|
40
40
|
Requires-Dist: black; extra == "dev"
|
|
@@ -172,5 +172,5 @@ qbraid comms email send --subject "Progress" --file report.txt --json
|
|
|
172
172
|
qbraid comms slack read --json
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
See [communication commands](docs/
|
|
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.9"
|
|
8
8
|
description = "Command Line Interface for interacting with all parts of the qBraid platform."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "qBraid Development Team", email = "contact@qbraid.com" }]
|
|
@@ -32,7 +32,7 @@ dependencies = [
|
|
|
32
32
|
"rich>=10.11.0",
|
|
33
33
|
"click<=8.5.0",
|
|
34
34
|
"questionary>=2.0.0",
|
|
35
|
-
"qbraid-core>=0.6.
|
|
35
|
+
"qbraid-core>=0.6.21",
|
|
36
36
|
]
|
|
37
37
|
requires-python = ">= 3.10"
|
|
38
38
|
|
|
@@ -43,8 +43,8 @@ Documentation = "https://docs.qbraid.com/v2/cli/api-reference/qbraid"
|
|
|
43
43
|
Discord = "https://discord.gg/KugF6Cnncm"
|
|
44
44
|
|
|
45
45
|
[project.optional-dependencies]
|
|
46
|
-
compute = ["qbraid-core[compute]>=0.6.
|
|
47
|
-
envs = ["qbraid-core[environments]>=0.6.
|
|
46
|
+
compute = ["qbraid-core[compute]>=0.6.21"]
|
|
47
|
+
envs = ["qbraid-core[environments]>=0.6.21"]
|
|
48
48
|
dev = ["isort", "black", "pytest", "pytest-cov", "pytest-asyncio"]
|
|
49
49
|
|
|
50
50
|
[project.scripts]
|
|
@@ -17,6 +17,7 @@ from typing import Any, Callable, Dict, List, Optional
|
|
|
17
17
|
import click
|
|
18
18
|
import typer
|
|
19
19
|
from rich.console import Console
|
|
20
|
+
from rich.markup import escape
|
|
20
21
|
from rich.panel import Panel
|
|
21
22
|
from rich.table import Table
|
|
22
23
|
|
|
@@ -114,7 +115,6 @@ def _remote_failure(stdout: str, stderr: str) -> str:
|
|
|
114
115
|
"Session not found" into "Warning: Permanently added ... to the list of
|
|
115
116
|
known hosts", which names the one thing that went right.
|
|
116
117
|
"""
|
|
117
|
-
from rich.markup import escape
|
|
118
118
|
|
|
119
119
|
def useful(text: str) -> list[str]:
|
|
120
120
|
# The remote's Rich error panel arrives as box-drawing borders around
|
|
@@ -204,12 +204,91 @@ def _since_to_timestamp(since_str: str) -> float:
|
|
|
204
204
|
return time.time() - seconds
|
|
205
205
|
|
|
206
206
|
|
|
207
|
+
def _plain(value) -> str:
|
|
208
|
+
"""A user- or agent-supplied value, escaped so Rich prints it literally."""
|
|
209
|
+
return escape(str(value))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _core_method(registry, name: str):
|
|
213
|
+
"""A registry method the installed qbraid-core actually defines, else None.
|
|
214
|
+
|
|
215
|
+
Checked on the class so older Core releases (and test doubles) fall back
|
|
216
|
+
to the previous behavior instead of calling a missing method.
|
|
217
|
+
"""
|
|
218
|
+
if not callable(getattr(type(registry), name, None)):
|
|
219
|
+
return None
|
|
220
|
+
return getattr(registry, name)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _calling_session(registry) -> Optional[str]:
|
|
224
|
+
"""The live agent session this command runs inside, if any.
|
|
225
|
+
|
|
226
|
+
Newer qbraid-core finds it by process ancestry, so unsetting
|
|
227
|
+
QBRAID_SESSION_ID does not make an agent look like the person, and a
|
|
228
|
+
person typing in a pane that kept the variable is not mistaken for the
|
|
229
|
+
agent that exited. Older Core falls back to the variable, unless it names
|
|
230
|
+
a session the registry records as stopped or errored.
|
|
231
|
+
"""
|
|
232
|
+
find = _core_method(registry, "calling_session")
|
|
233
|
+
if find is not None:
|
|
234
|
+
try:
|
|
235
|
+
return find()
|
|
236
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
237
|
+
if _is_registry_error(err):
|
|
238
|
+
# Core could not tell which agent this is (for example, the
|
|
239
|
+
# caller matches more than one live session). Refuse rather
|
|
240
|
+
# than fail open to "the person".
|
|
241
|
+
console.print(
|
|
242
|
+
f"[red]Cannot tell which agent session this command runs in: {_plain(err)}[/red]"
|
|
243
|
+
)
|
|
244
|
+
raise typer.Exit(code=1) from err
|
|
245
|
+
# any other failure: fall back to the variable, as older Core does
|
|
246
|
+
env_sid = os.environ.get("QBRAID_SESSION_ID") or None
|
|
247
|
+
if env_sid and _session_status(registry, env_sid) in ("stopped", "error"):
|
|
248
|
+
return None
|
|
249
|
+
return env_sid
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _is_registry_error(err: Exception) -> bool:
|
|
253
|
+
"""Whether ``err`` is qbraid-core's AgentRegistryError (or a subclass)."""
|
|
254
|
+
try:
|
|
255
|
+
# pylint: disable-next=import-outside-toplevel
|
|
256
|
+
from qbraid_core.services.agents.exceptions import AgentRegistryError
|
|
257
|
+
except ImportError:
|
|
258
|
+
return False
|
|
259
|
+
return isinstance(err, AgentRegistryError)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _session_status(registry, session_id: str) -> Optional[str]:
|
|
263
|
+
"""The registry status of a session ("stopped", "working", ...), or None
|
|
264
|
+
when it is not registered or the registry cannot be read."""
|
|
265
|
+
try:
|
|
266
|
+
session = registry.get_session(session_id)
|
|
267
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
268
|
+
return None
|
|
269
|
+
status = getattr(session, "status", None)
|
|
270
|
+
value = getattr(status, "value", status)
|
|
271
|
+
return value if isinstance(value, str) else None
|
|
272
|
+
|
|
273
|
+
|
|
207
274
|
def _find_session_by_prefix(registry, session_id: str):
|
|
208
275
|
"""Find a session by ID, prefix, or name from the registry.
|
|
209
276
|
|
|
210
277
|
Searches session_id (exact/prefix), then tmux_window and terminal_name.
|
|
211
278
|
Returns the matched AgentSession or None.
|
|
212
279
|
"""
|
|
280
|
+
resolve = _core_method(registry, "resolve_session")
|
|
281
|
+
if resolve is not None:
|
|
282
|
+
# Newer Core: exact id, unique prefix, then a name that prefers the live
|
|
283
|
+
# session over stopped namesakes; ambiguity is an error, not a guess.
|
|
284
|
+
try:
|
|
285
|
+
return resolve(session_id)
|
|
286
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
287
|
+
if type(err).__name__ != "AgentSessionAmbiguousError":
|
|
288
|
+
raise
|
|
289
|
+
console.print(f"[red]{_plain(err)}[/red]")
|
|
290
|
+
raise typer.Exit(code=1) from err
|
|
291
|
+
|
|
213
292
|
all_sessions = registry.load_registry().sessions
|
|
214
293
|
|
|
215
294
|
# Try session_id exact or prefix match first
|
|
@@ -766,9 +845,9 @@ def agents_status(
|
|
|
766
845
|
|
|
767
846
|
# Team info
|
|
768
847
|
if s.team_id:
|
|
769
|
-
lines.append(f"[bold]Team ID:[/bold] {s.team_id}")
|
|
848
|
+
lines.append(f"[bold]Team ID:[/bold] {_plain(s.team_id)}")
|
|
770
849
|
if s.role:
|
|
771
|
-
lines.append(f"[bold]Role:[/bold] {s.role}")
|
|
850
|
+
lines.append(f"[bold]Role:[/bold] {_plain(s.role)}")
|
|
772
851
|
|
|
773
852
|
# Transcript path
|
|
774
853
|
if s.transcript_path:
|
|
@@ -807,8 +886,24 @@ def agents_update(
|
|
|
807
886
|
status: Optional[str] = typer.Option(
|
|
808
887
|
None, "--status", help="Set session status (working, waiting, idle, error, stopped)"
|
|
809
888
|
),
|
|
889
|
+
agent_type: Optional[str] = typer.Option(
|
|
890
|
+
None,
|
|
891
|
+
"--type",
|
|
892
|
+
help="Set the agent type (leader, worker, observer, reviewer); '' clears it.",
|
|
893
|
+
),
|
|
894
|
+
team: Optional[str] = typer.Option(None, "--team", help="Join a team by id; '' leaves it."),
|
|
895
|
+
parent: Optional[str] = typer.Option(
|
|
896
|
+
None, "--parent", help="Set the parent session (id, prefix or name); '' clears it."
|
|
897
|
+
),
|
|
810
898
|
):
|
|
811
|
-
"""Update metadata fields on an agent session.
|
|
899
|
+
"""Update metadata fields on an agent session.
|
|
900
|
+
|
|
901
|
+
--type, --team and --parent let agents started from a terminal join a
|
|
902
|
+
team after launch, so they can message each other. From your own shell
|
|
903
|
+
any change is allowed. Inside an agent, it may only place teamless
|
|
904
|
+
sessions or its teammates into a team it leads, never as or out of
|
|
905
|
+
leader. These are coordination guardrails, not a security boundary.
|
|
906
|
+
"""
|
|
812
907
|
try:
|
|
813
908
|
from qbraid_core.services.agents import (
|
|
814
909
|
AgentSessionNotFoundError,
|
|
@@ -820,7 +915,7 @@ def agents_update(
|
|
|
820
915
|
# Support prefix and name matching
|
|
821
916
|
match = _find_session_by_prefix(registry, session_id)
|
|
822
917
|
if not match:
|
|
823
|
-
console.print(f"[red]Session not found: {session_id}[/red]")
|
|
918
|
+
console.print(f"[red]Session not found: {_plain(session_id)}[/red]")
|
|
824
919
|
console.print("[dim]Use 'qbraid agents list' to see active sessions.[/dim]")
|
|
825
920
|
raise typer.Exit(code=1)
|
|
826
921
|
matched_id = match.session_id
|
|
@@ -842,7 +937,9 @@ def agents_update(
|
|
|
842
937
|
valid_statuses = {"working", "waiting", "idle", "error", "stopped"}
|
|
843
938
|
if status not in valid_statuses:
|
|
844
939
|
valid_opts = ", ".join(sorted(valid_statuses))
|
|
845
|
-
console.print(
|
|
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,69 @@ def agents_settings(
|
|
|
1446
1582
|
handle_error(error_type=type(error).__name__, message=str(error), include_traceback=False)
|
|
1447
1583
|
|
|
1448
1584
|
|
|
1585
|
+
def _prompt_answer_kwargs(method, trust_folder=None, accept_bypass_warning=False):
|
|
1586
|
+
"""Answers to the agent's own startup prompts, given on the command line.
|
|
1587
|
+
|
|
1588
|
+
``trust_folder`` answers "trust this folder?" (Claude, Codex); either
|
|
1589
|
+
explicit answer needs a Core that takes it, because an older Core trusts
|
|
1590
|
+
every Claude launch folder by itself, so a silently dropped "no" would be
|
|
1591
|
+
a yes. ``accept_bypass_warning`` skips Claude's bypass-permissions
|
|
1592
|
+
confirmation for this run.
|
|
1593
|
+
"""
|
|
1594
|
+
from qbraid_core.services.agents import AgentLauncherError
|
|
1595
|
+
|
|
1596
|
+
params = inspect.signature(method).parameters
|
|
1597
|
+
open_kwargs = any(p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values())
|
|
1598
|
+
kwargs = {}
|
|
1599
|
+
for key, value, flag in (
|
|
1600
|
+
("trust_folder", trust_folder, "--trust-folder/--no-trust-folder"),
|
|
1601
|
+
("accept_bypass_warning", accept_bypass_warning or None, "--skip-bypass-confirmation"),
|
|
1602
|
+
):
|
|
1603
|
+
if value is None:
|
|
1604
|
+
continue
|
|
1605
|
+
if key not in params and not open_kwargs:
|
|
1606
|
+
raise AgentLauncherError(f"{flag} requires a newer qbraid-core on this machine.")
|
|
1607
|
+
kwargs[key] = value
|
|
1608
|
+
return kwargs
|
|
1609
|
+
|
|
1610
|
+
|
|
1611
|
+
def _hook_trust_kwargs(method, value):
|
|
1612
|
+
"""Fail before launch when an explicit choice cannot be honored."""
|
|
1613
|
+
if value is None:
|
|
1614
|
+
return {}
|
|
1615
|
+
|
|
1616
|
+
from qbraid_core.services.agents import AgentLauncherError
|
|
1617
|
+
|
|
1618
|
+
params = inspect.signature(method).parameters
|
|
1619
|
+
if "trust_hooks" not in params and not any(
|
|
1620
|
+
p.kind is inspect.Parameter.VAR_KEYWORD for p in params.values()
|
|
1621
|
+
):
|
|
1622
|
+
raise AgentLauncherError("Hook trust requires a newer qbraid-core on this machine.")
|
|
1623
|
+
return {"trust_hooks": value}
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
def _startup_pending(session):
|
|
1627
|
+
return session.tool.value in ("codex", "claude") and not getattr(session, "startup_confirmed_at", None)
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
def _startup_notice(session, compute=None):
|
|
1631
|
+
import shlex
|
|
1632
|
+
|
|
1633
|
+
suffix = f" --compute {shlex.quote(compute)}" if compute else ""
|
|
1634
|
+
if _startup_pending(session):
|
|
1635
|
+
console.print(
|
|
1636
|
+
"Session created; startup is not yet confirmed. It may be waiting for sign-in, "
|
|
1637
|
+
"folder trust, or hook review. The initial task waits until setup is complete.",
|
|
1638
|
+
markup=False,
|
|
1639
|
+
)
|
|
1640
|
+
if session.tool.value == "codex":
|
|
1641
|
+
console.print(
|
|
1642
|
+
"Approve hooks in the agent's terminal (/hooks); Codex remembers unchanged ones.",
|
|
1643
|
+
markup=False,
|
|
1644
|
+
)
|
|
1645
|
+
console.print(f" qbraid agents attach {shlex.quote(session.session_id)}{suffix}", markup=False)
|
|
1646
|
+
|
|
1647
|
+
|
|
1449
1648
|
@agents_app.command("launch")
|
|
1450
1649
|
def agents_launch(
|
|
1451
1650
|
backend: TerminalBackend = typer.Option(
|
|
@@ -1491,6 +1690,30 @@ def agents_launch(
|
|
|
1491
1690
|
"decides. yolo: no permission checks. ask/auto: legacy modes, unchanged."
|
|
1492
1691
|
),
|
|
1493
1692
|
),
|
|
1693
|
+
trust_hooks: bool = typer.Option(
|
|
1694
|
+
False,
|
|
1695
|
+
"--trust-hooks",
|
|
1696
|
+
help=(
|
|
1697
|
+
"Codex only, DANGEROUS: run ALL enabled hooks (yours, the project's and "
|
|
1698
|
+
"plugins') without Codex's hook review, for this launch only. Not retained on "
|
|
1699
|
+
"resume. Refused with --repo. Independent of --approval and --yolo."
|
|
1700
|
+
),
|
|
1701
|
+
),
|
|
1702
|
+
trust_folder: Optional[bool] = typer.Option(
|
|
1703
|
+
None,
|
|
1704
|
+
"--trust-folder/--no-trust-folder",
|
|
1705
|
+
help=(
|
|
1706
|
+
"Answer the agent's 'trust this folder?' prompt now (Claude, Codex), so a detached "
|
|
1707
|
+
"agent does not stop at it. The answer is remembered for later launches and "
|
|
1708
|
+
"resumes. Trusting a folder lets its own settings, hooks and MCP servers run. "
|
|
1709
|
+
"Default: Claude trusts the folder, except a fresh --repo clone; Codex asks."
|
|
1710
|
+
),
|
|
1711
|
+
),
|
|
1712
|
+
skip_bypass_confirmation: bool = typer.Option(
|
|
1713
|
+
False,
|
|
1714
|
+
"--skip-bypass-confirmation",
|
|
1715
|
+
help="Claude with --yolo: skip its bypass-permissions confirmation, for this launch only.",
|
|
1716
|
+
),
|
|
1494
1717
|
yolo: bool = typer.Option(
|
|
1495
1718
|
False, "--yolo", help="Claude: skip permissions. Codex: bypass approvals and its sandbox."
|
|
1496
1719
|
),
|
|
@@ -1519,6 +1742,14 @@ def agents_launch(
|
|
|
1519
1742
|
tag = tag or result.get("tags")
|
|
1520
1743
|
repo = repo or result.get("repo")
|
|
1521
1744
|
|
|
1745
|
+
if trust_hooks and repo:
|
|
1746
|
+
# Codex would run the clone's own .codex hooks unreviewed once its folder is
|
|
1747
|
+
# trusted. Core refuses this too; refuse here before cloning anything.
|
|
1748
|
+
raise AgentLauncherError(
|
|
1749
|
+
"--trust-hooks cannot be combined with --repo: the repository's project hooks "
|
|
1750
|
+
"would run without review. Clone and vet it, then launch with --cwd."
|
|
1751
|
+
)
|
|
1752
|
+
|
|
1522
1753
|
if yolo:
|
|
1523
1754
|
if tool not in ("claude", "claude-auto", "codex"):
|
|
1524
1755
|
raise AgentLauncherError("--yolo is supported only for Claude and Codex.")
|
|
@@ -1539,6 +1770,12 @@ def agents_launch(
|
|
|
1539
1770
|
if not compute or str(uuid.UUID(request_id)) != request_id:
|
|
1540
1771
|
raise AgentLauncherError("--request-id requires --compute and a canonical UUID.")
|
|
1541
1772
|
|
|
1773
|
+
if compute and (trust_folder is not None or skip_bypass_confirmation):
|
|
1774
|
+
raise AgentLauncherError(
|
|
1775
|
+
"--trust-folder and --skip-bypass-confirmation are not yet supported for remote "
|
|
1776
|
+
"launches; answer the prompts in the agent's terminal."
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1542
1779
|
if compute and backend != TerminalBackend.AUTO:
|
|
1543
1780
|
raise AgentLauncherError("Remote launches select the backend on the destination; omit --backend.")
|
|
1544
1781
|
launcher = _backend_launcher(backend)
|
|
@@ -1556,6 +1793,7 @@ def agents_launch(
|
|
|
1556
1793
|
tags=tag,
|
|
1557
1794
|
**_request_id_kwargs(launcher.remote_launch, request_id),
|
|
1558
1795
|
**_approval_kwargs(launcher.remote_launch, approval.value if approval else None),
|
|
1796
|
+
**_hook_trust_kwargs(launcher.remote_launch, True if trust_hooks else None),
|
|
1559
1797
|
**_execution_kwargs(launcher.remote_launch, execution_mode.value if execution_mode else None),
|
|
1560
1798
|
)
|
|
1561
1799
|
else:
|
|
@@ -1574,11 +1812,17 @@ def agents_launch(
|
|
|
1574
1812
|
team_id=team,
|
|
1575
1813
|
tags=tag,
|
|
1576
1814
|
**_approval_kwargs(launcher.launch, approval.value if approval else None),
|
|
1815
|
+
**_hook_trust_kwargs(launcher.launch, True if trust_hooks else None),
|
|
1816
|
+
**_prompt_answer_kwargs(launcher.launch, trust_folder, skip_bypass_confirmation),
|
|
1577
1817
|
**_execution_kwargs(launcher.launch, execution_mode.value if execution_mode else None),
|
|
1578
1818
|
progress_callback=_clone_progress if repo else None,
|
|
1579
1819
|
)
|
|
1580
1820
|
|
|
1581
|
-
status_val =
|
|
1821
|
+
status_val = (
|
|
1822
|
+
"starting / check terminal"
|
|
1823
|
+
if _startup_pending(session)
|
|
1824
|
+
else (session.status.value if session.status else "idle")
|
|
1825
|
+
)
|
|
1582
1826
|
style = _status_style(status_val)
|
|
1583
1827
|
|
|
1584
1828
|
lines = []
|
|
@@ -1604,7 +1848,7 @@ def agents_launch(
|
|
|
1604
1848
|
console.print(panel)
|
|
1605
1849
|
console.print()
|
|
1606
1850
|
console.print("[dim]Open from the Agent Dashboard or attach with:[/dim]")
|
|
1607
|
-
|
|
1851
|
+
_startup_notice(session, compute)
|
|
1608
1852
|
|
|
1609
1853
|
except AgentLauncherError as e:
|
|
1610
1854
|
console.print(
|
|
@@ -1707,6 +1951,22 @@ def agents_resume(
|
|
|
1707
1951
|
approval: Optional[ApprovalMode] = typer.Option(
|
|
1708
1952
|
None, "--approval", help="Override recorded permissions for this resume. Omit to preserve them."
|
|
1709
1953
|
),
|
|
1954
|
+
trust_hooks: bool = typer.Option(
|
|
1955
|
+
False,
|
|
1956
|
+
"--trust-hooks",
|
|
1957
|
+
help=(
|
|
1958
|
+
"Codex only, DANGEROUS: run ALL enabled hooks without Codex's hook review, "
|
|
1959
|
+
"for this resume only. Hooks are reviewed again on every run that omits it."
|
|
1960
|
+
),
|
|
1961
|
+
),
|
|
1962
|
+
trust_folder: Optional[bool] = typer.Option(
|
|
1963
|
+
None,
|
|
1964
|
+
"--trust-folder/--no-trust-folder",
|
|
1965
|
+
help=(
|
|
1966
|
+
"Answer 'trust this folder?' for the folder it resumes in (remembered). "
|
|
1967
|
+
"Default: keep the answer given at launch."
|
|
1968
|
+
),
|
|
1969
|
+
),
|
|
1710
1970
|
compute: Optional[str] = typer.Option(None, "--compute", "-c", help="SSH alias for remote compute"),
|
|
1711
1971
|
):
|
|
1712
1972
|
"""Resume a previous agent session.
|
|
@@ -1737,6 +1997,10 @@ def agents_resume(
|
|
|
1737
1997
|
flags += f" --execution-mode {shlex.quote(execution_mode.value)}"
|
|
1738
1998
|
if approval:
|
|
1739
1999
|
flags += f" --approval {shlex.quote(approval.value)}"
|
|
2000
|
+
if trust_hooks:
|
|
2001
|
+
flags += " --trust-hooks"
|
|
2002
|
+
if trust_folder is not None:
|
|
2003
|
+
flags += " --trust-folder" if trust_folder else " --no-trust-folder"
|
|
1740
2004
|
# markup=False -- see agents_status.
|
|
1741
2005
|
console.print(
|
|
1742
2006
|
_ssh_cmd(compute, f"qbraid agents resume {shlex.quote(session_id)}{flags}"),
|
|
@@ -1756,6 +2020,8 @@ def agents_resume(
|
|
|
1756
2020
|
session_id=session_id,
|
|
1757
2021
|
name=name,
|
|
1758
2022
|
**(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
|
|
2023
|
+
**_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
|
|
2024
|
+
**_prompt_answer_kwargs(launcher.resume, trust_folder),
|
|
1759
2025
|
**_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
|
|
1760
2026
|
)
|
|
1761
2027
|
except AgentLauncherError as e:
|
|
@@ -1800,6 +2066,13 @@ def agents_resume(
|
|
|
1800
2066
|
cwd=original.cwd,
|
|
1801
2067
|
name=fork_name,
|
|
1802
2068
|
instructions=None,
|
|
2069
|
+
**_hook_trust_kwargs(
|
|
2070
|
+
launcher.launch,
|
|
2071
|
+
# Never inherited from the original: only an explicit
|
|
2072
|
+
# --trust-hooks on this command bypasses hook review.
|
|
2073
|
+
True if trust_hooks else None,
|
|
2074
|
+
),
|
|
2075
|
+
**_prompt_answer_kwargs(launcher.launch, trust_folder),
|
|
1803
2076
|
# A fork of a detached agent must not quietly
|
|
1804
2077
|
# become one that waits for a human.
|
|
1805
2078
|
**_approval_kwargs(
|
|
@@ -1876,10 +2149,16 @@ def agents_resume(
|
|
|
1876
2149
|
session_id=selected,
|
|
1877
2150
|
name=name,
|
|
1878
2151
|
**(_approval_kwargs(launcher.resume, approval.value) if approval else {}),
|
|
2152
|
+
**_hook_trust_kwargs(launcher.resume, True if trust_hooks else None),
|
|
2153
|
+
**_prompt_answer_kwargs(launcher.resume, trust_folder),
|
|
1879
2154
|
**_execution_kwargs(launcher.resume, execution_mode.value if execution_mode else None),
|
|
1880
2155
|
)
|
|
1881
2156
|
|
|
1882
|
-
status_val =
|
|
2157
|
+
status_val = (
|
|
2158
|
+
"starting / check terminal"
|
|
2159
|
+
if _startup_pending(session)
|
|
2160
|
+
else (session.status.value if session.status else "idle")
|
|
2161
|
+
)
|
|
1883
2162
|
style = _status_style(status_val)
|
|
1884
2163
|
|
|
1885
2164
|
lines = []
|
|
@@ -1900,7 +2179,7 @@ def agents_resume(
|
|
|
1900
2179
|
console.print(panel)
|
|
1901
2180
|
console.print()
|
|
1902
2181
|
console.print("[dim]Attach with:[/dim]")
|
|
1903
|
-
|
|
2182
|
+
_startup_notice(session)
|
|
1904
2183
|
|
|
1905
2184
|
except AgentLauncherError as e:
|
|
1906
2185
|
console.print(Panel(str(e), title="Resume Error", border_style="red"))
|
|
@@ -2865,17 +3144,44 @@ def hooks_uninstall():
|
|
|
2865
3144
|
# =============================================================================
|
|
2866
3145
|
|
|
2867
3146
|
|
|
2868
|
-
def _resolve_from_session(explicit: Optional[str]) -> str:
|
|
3147
|
+
def _resolve_from_session(explicit: Optional[str], registry=None) -> str:
|
|
2869
3148
|
"""Determine the sender session id.
|
|
2870
3149
|
|
|
2871
|
-
Priority: explicit --from >
|
|
2872
|
-
|
|
3150
|
+
Priority: explicit --from > the agent session this command runs inside
|
|
3151
|
+
(process ancestry, else QBRAID_SESSION_ID) > fail with a helpful error.
|
|
3152
|
+
An explicit sender may be an id prefix or a name, like a recipient, but
|
|
3153
|
+
inside an agent it must be that agent.
|
|
2873
3154
|
"""
|
|
3155
|
+
caller = _calling_session(registry) if registry is not None else None
|
|
2874
3156
|
if explicit:
|
|
2875
|
-
|
|
3157
|
+
sender = explicit
|
|
3158
|
+
if registry is not None:
|
|
3159
|
+
match = _find_session_by_prefix(registry, explicit)
|
|
3160
|
+
if match is not None:
|
|
3161
|
+
sender = match.session_id
|
|
3162
|
+
if caller and sender != caller:
|
|
3163
|
+
# A coordination guardrail, not authentication: an agent speaks
|
|
3164
|
+
# as itself so recipients and permission rules see the real sender.
|
|
3165
|
+
console.print(
|
|
3166
|
+
f"[red]This command runs inside agent session {_plain(caller[:8])}; it cannot send "
|
|
3167
|
+
f"as {_plain(explicit)}.[/red] Omit --from to send as yourself."
|
|
3168
|
+
)
|
|
3169
|
+
raise typer.Exit(code=1)
|
|
3170
|
+
return sender
|
|
3171
|
+
if caller:
|
|
3172
|
+
return caller
|
|
2876
3173
|
env_sid = os.environ.get("QBRAID_SESSION_ID")
|
|
2877
|
-
if env_sid:
|
|
3174
|
+
if registry is None and env_sid:
|
|
2878
3175
|
return env_sid
|
|
3176
|
+
if env_sid:
|
|
3177
|
+
# The variable outlived its agent (a pane kept it after the agent
|
|
3178
|
+
# stopped): whoever types here is the person, not that agent.
|
|
3179
|
+
console.print(
|
|
3180
|
+
f"[red]QBRAID_SESSION_ID names {_plain(env_sid[:8])}, which is not a running "
|
|
3181
|
+
"agent, so this shell does not send as it.[/red] Pass --from <session-id> to "
|
|
3182
|
+
"choose a sender."
|
|
3183
|
+
)
|
|
3184
|
+
raise typer.Exit(code=1)
|
|
2879
3185
|
console.print(
|
|
2880
3186
|
"[red]No sender session id available.[/red] "
|
|
2881
3187
|
"Pass --from <session-id>, or run this inside a qBraid-managed "
|
|
@@ -2931,13 +3237,34 @@ def _resolve_target_session(registry, identifier: str) -> str:
|
|
|
2931
3237
|
match = _find_session_by_prefix(registry, identifier)
|
|
2932
3238
|
if match is None:
|
|
2933
3239
|
console.print(
|
|
2934
|
-
f"[red]No session matched '{identifier}'.[/red] "
|
|
3240
|
+
f"[red]No session matched '{_plain(identifier)}'.[/red] "
|
|
2935
3241
|
"Try 'qbraid agents list' to see available sessions."
|
|
2936
3242
|
)
|
|
2937
3243
|
raise typer.Exit(code=1)
|
|
2938
3244
|
return match.session_id
|
|
2939
3245
|
|
|
2940
3246
|
|
|
3247
|
+
def _note_offline_recipient(registry, session_id: str) -> None:
|
|
3248
|
+
"""Say so when the recipient is not running: an inbox keeps the message,
|
|
3249
|
+
but nothing reads it until the session is resumed."""
|
|
3250
|
+
status = _session_status(registry, session_id)
|
|
3251
|
+
if status not in ("stopped", "error"):
|
|
3252
|
+
return
|
|
3253
|
+
try:
|
|
3254
|
+
from qbraid_core.services.agents import messaging # pylint: disable=import-outside-toplevel
|
|
3255
|
+
|
|
3256
|
+
successor = getattr(messaging, "live_successor", None)
|
|
3257
|
+
if callable(successor) and successor(registry, session_id): # pylint: disable=not-callable
|
|
3258
|
+
return # Core forwards it to the resumed session and says so.
|
|
3259
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
3260
|
+
pass
|
|
3261
|
+
console.print(
|
|
3262
|
+
f"[yellow]Note: {_plain(session_id[:8])} is {status}. The message waits in its inbox and is "
|
|
3263
|
+
"read when the conversation is resumed (in place, or with `qbraid agents resume`, "
|
|
3264
|
+
"which carries unread messages to the new session).[/yellow]"
|
|
3265
|
+
)
|
|
3266
|
+
|
|
3267
|
+
|
|
2941
3268
|
def _resolve_team_targets(registry, team_id: str, exclude_session_id: Optional[str] = None) -> list:
|
|
2942
3269
|
"""Return every session in a team, optionally excluding one (usually the sender).
|
|
2943
3270
|
|
|
@@ -2967,7 +3294,8 @@ def _parse_target_csv(csv: str, registry) -> list[str]:
|
|
|
2967
3294
|
resolved.append(match.session_id)
|
|
2968
3295
|
if unresolved:
|
|
2969
3296
|
console.print(
|
|
2970
|
-
f"[red]No session matched: {', '.join(unresolved)}[/red]. "
|
|
3297
|
+
f"[red]No session matched: {_plain(', '.join(unresolved))}[/red]. "
|
|
3298
|
+
"Broadcast aborted; nothing sent."
|
|
2971
3299
|
)
|
|
2972
3300
|
raise typer.Exit(code=1)
|
|
2973
3301
|
return resolved
|
|
@@ -3005,7 +3333,15 @@ def _deliver_to_many(
|
|
|
3005
3333
|
registry=registry,
|
|
3006
3334
|
validate_permissions=validate_permissions,
|
|
3007
3335
|
)
|
|
3008
|
-
delivered.append((target_sid, msg.id))
|
|
3336
|
+
delivered.append((getattr(msg, "to_session", None) or target_sid, msg.id))
|
|
3337
|
+
# Older Core has no forwarded_from; newer Core types it Optional[str].
|
|
3338
|
+
forwarded = getattr(msg, "forwarded_from", None)
|
|
3339
|
+
if isinstance(forwarded, str) and forwarded:
|
|
3340
|
+
old_sid, new_sid = str(forwarded), str(msg.to_session)
|
|
3341
|
+
console.print(
|
|
3342
|
+
f"[dim]{_plain(old_sid[:8])} was resumed as {_plain(new_sid[:8])}; "
|
|
3343
|
+
"delivered to the running session.[/dim]"
|
|
3344
|
+
)
|
|
3009
3345
|
except AgentMessagingError as err:
|
|
3010
3346
|
failed.append((target_sid, str(err)))
|
|
3011
3347
|
except Exception as err: # pylint: disable=broad-exception-caught
|
|
@@ -3013,10 +3349,11 @@ def _deliver_to_many(
|
|
|
3013
3349
|
|
|
3014
3350
|
for sid, mid in delivered:
|
|
3015
3351
|
console.print(
|
|
3016
|
-
f"[green]→[/green] Sent to [bold]{sid}[/bold] "
|
|
3352
|
+
f"[green]→[/green] Sent to [bold]{_plain(sid)}[/bold] "
|
|
3353
|
+
f"[dim](id={_plain(mid)}, type={_plain(message_type)})[/dim]"
|
|
3017
3354
|
)
|
|
3018
3355
|
for sid, reason in failed:
|
|
3019
|
-
console.print(f"[red]✗[/red] [bold]{sid}[/bold]: {reason}")
|
|
3356
|
+
console.print(f"[red]✗[/red] [bold]{_plain(sid)}[/bold]: {_plain(reason)}")
|
|
3020
3357
|
|
|
3021
3358
|
if failed and not delivered:
|
|
3022
3359
|
return 1
|
|
@@ -3025,6 +3362,7 @@ def _deliver_to_many(
|
|
|
3025
3362
|
return 0
|
|
3026
3363
|
|
|
3027
3364
|
|
|
3365
|
+
# Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
|
|
3028
3366
|
@agents_app.command("send-to")
|
|
3029
3367
|
def agents_send_to(
|
|
3030
3368
|
to: str = typer.Argument(
|
|
@@ -3063,7 +3401,7 @@ def agents_send_to(
|
|
|
3063
3401
|
|
|
3064
3402
|
Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
|
|
3065
3403
|
the machine that owns it, so there is no ``--compute``. Cross-host
|
|
3066
|
-
messaging
|
|
3404
|
+
messaging is not supported yet.
|
|
3067
3405
|
|
|
3068
3406
|
|
|
3069
3407
|
Delivery is a JSONL append at ~/.qbraid/inboxes/<to-session-id>.jsonl
|
|
@@ -3079,8 +3417,9 @@ def agents_send_to(
|
|
|
3079
3417
|
raise typer.Exit(code=1) from err
|
|
3080
3418
|
|
|
3081
3419
|
registry = _get_registry()
|
|
3082
|
-
from_sid = _resolve_from_session(from_)
|
|
3420
|
+
from_sid = _resolve_from_session(from_, registry)
|
|
3083
3421
|
to_sid = _resolve_target_session(registry, to)
|
|
3422
|
+
_note_offline_recipient(registry, to_sid)
|
|
3084
3423
|
|
|
3085
3424
|
code = _deliver_to_many(
|
|
3086
3425
|
[to_sid],
|
|
@@ -3094,6 +3433,7 @@ def agents_send_to(
|
|
|
3094
3433
|
raise typer.Exit(code=code)
|
|
3095
3434
|
|
|
3096
3435
|
|
|
3436
|
+
# Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
|
|
3097
3437
|
@agents_app.command("broadcast")
|
|
3098
3438
|
def agents_broadcast(
|
|
3099
3439
|
text: str = typer.Argument(..., help="Message body — delivered to every target."),
|
|
@@ -3135,7 +3475,7 @@ def agents_broadcast(
|
|
|
3135
3475
|
|
|
3136
3476
|
Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
|
|
3137
3477
|
the machine that owns it, so there is no ``--compute``. Cross-host
|
|
3138
|
-
messaging
|
|
3478
|
+
messaging is not supported yet.
|
|
3139
3479
|
|
|
3140
3480
|
Two selectors — pick exactly one:
|
|
3141
3481
|
* ``--team <team_id>`` — every session with that team_id (except sender)
|
|
@@ -3160,7 +3500,7 @@ def agents_broadcast(
|
|
|
3160
3500
|
raise typer.Exit(code=2)
|
|
3161
3501
|
|
|
3162
3502
|
registry = _get_registry()
|
|
3163
|
-
from_sid = _resolve_from_session(from_)
|
|
3503
|
+
from_sid = _resolve_from_session(from_, registry)
|
|
3164
3504
|
|
|
3165
3505
|
if team is not None:
|
|
3166
3506
|
members = _resolve_team_targets(registry, team, exclude_session_id=from_sid)
|
|
@@ -3221,14 +3561,15 @@ def _render_messages_table(messages, session_id: str, full_ids: bool = False) ->
|
|
|
3221
3561
|
from_display = msg.from_session if full_ids else msg.from_session[:12]
|
|
3222
3562
|
table.add_row(
|
|
3223
3563
|
ts,
|
|
3224
|
-
from_display,
|
|
3225
|
-
msg.type.value,
|
|
3564
|
+
_plain(from_display),
|
|
3565
|
+
_plain(msg.type.value),
|
|
3226
3566
|
read_marker,
|
|
3227
|
-
msg.text,
|
|
3567
|
+
_plain(msg.text),
|
|
3228
3568
|
)
|
|
3229
3569
|
console.print(table)
|
|
3230
3570
|
|
|
3231
3571
|
|
|
3572
|
+
# Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
|
|
3232
3573
|
@inbox_app.callback()
|
|
3233
3574
|
def inbox_default( # pylint: disable=too-many-arguments
|
|
3234
3575
|
ctx: typer.Context,
|
|
@@ -3267,7 +3608,7 @@ def inbox_default( # pylint: disable=too-many-arguments
|
|
|
3267
3608
|
|
|
3268
3609
|
Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
|
|
3269
3610
|
the machine that owns it, so there is no ``--compute``. Cross-host
|
|
3270
|
-
messaging
|
|
3611
|
+
messaging is not supported yet.
|
|
3271
3612
|
"""
|
|
3272
3613
|
if ctx.invoked_subcommand is not None:
|
|
3273
3614
|
return
|
|
@@ -3299,8 +3640,8 @@ def inbox_default( # pylint: disable=too-many-arguments
|
|
|
3299
3640
|
from_display = msg.from_session if wide else msg.from_session[:12]
|
|
3300
3641
|
console.print(
|
|
3301
3642
|
f"[dim]{ts}[/dim] "
|
|
3302
|
-
f"[cyan]{from_display}[/cyan] "
|
|
3303
|
-
f"[yellow]({msg.type.value})[/yellow] {msg.text}"
|
|
3643
|
+
f"[cyan]{_plain(from_display)}[/cyan] "
|
|
3644
|
+
f"[yellow]({_plain(msg.type.value)})[/yellow] {_plain(msg.text)}"
|
|
3304
3645
|
)
|
|
3305
3646
|
if mark_read_flag:
|
|
3306
3647
|
mark_read(session_id, message_ids=[msg.id])
|
|
@@ -3363,6 +3704,7 @@ def inbox_mark_read(
|
|
|
3363
3704
|
# =============================================================================
|
|
3364
3705
|
|
|
3365
3706
|
|
|
3707
|
+
# Cross-host messaging transport: qBraid-CLI#328 / qbraid-core#307.
|
|
3366
3708
|
@agents_app.command("team")
|
|
3367
3709
|
def agents_team(
|
|
3368
3710
|
team_id: str = typer.Argument(..., help="Team identifier (matches AgentSession.team_id)."),
|
|
@@ -3377,7 +3719,7 @@ def agents_team(
|
|
|
3377
3719
|
|
|
3378
3720
|
Same-host only: an inbox is a JSONL file under ``~/.qbraid/inboxes/`` on
|
|
3379
3721
|
the machine that owns it, so there is no ``--compute``. Cross-host
|
|
3380
|
-
messaging
|
|
3722
|
+
messaging is not supported yet.
|
|
3381
3723
|
|
|
3382
3724
|
Answers the leader-orchestrator question "who is on my team, how many
|
|
3383
3725
|
unread messages do they have, and what was the last thing they heard from
|
|
@@ -3400,7 +3742,7 @@ def agents_team(
|
|
|
3400
3742
|
|
|
3401
3743
|
if not members:
|
|
3402
3744
|
console.print(
|
|
3403
|
-
f"[yellow]No sessions found with team_id '{team_id}'.[/yellow] "
|
|
3745
|
+
f"[yellow]No sessions found with team_id '{_plain(team_id)}'.[/yellow] "
|
|
3404
3746
|
"Check 'qbraid agents list' for known team_ids."
|
|
3405
3747
|
)
|
|
3406
3748
|
raise typer.Exit(code=0)
|
|
@@ -3420,7 +3762,7 @@ def agents_team(
|
|
|
3420
3762
|
table = Table(
|
|
3421
3763
|
show_header=True,
|
|
3422
3764
|
header_style="bold",
|
|
3423
|
-
title=f"Team: {team_id} ({len(members)} member{'s' if len(members) != 1 else ''})",
|
|
3765
|
+
title=f"Team: {_plain(team_id)} ({len(members)} member{'s' if len(members) != 1 else ''})",
|
|
3424
3766
|
)
|
|
3425
3767
|
table.add_column("You", justify="center", no_wrap=True)
|
|
3426
3768
|
table.add_column("Session", style="cyan", no_wrap=wide)
|
|
@@ -3452,12 +3794,12 @@ def agents_team(
|
|
|
3452
3794
|
|
|
3453
3795
|
table.add_row(
|
|
3454
3796
|
"★" if s.session_id == me_sid else "",
|
|
3455
|
-
session_display,
|
|
3456
|
-
s.agent_type or "worker",
|
|
3797
|
+
_plain(session_display),
|
|
3798
|
+
_plain(s.agent_type or "worker"),
|
|
3457
3799
|
f"[{status_style}]{s.status or 'unknown'}[/{status_style}]",
|
|
3458
3800
|
f"[{unread_style}]{unread_count}[/{unread_style}]",
|
|
3459
|
-
last_from,
|
|
3460
|
-
last_text,
|
|
3801
|
+
_plain(last_from),
|
|
3802
|
+
_plain(last_text),
|
|
3461
3803
|
)
|
|
3462
3804
|
|
|
3463
3805
|
console.print(table)
|
|
@@ -76,7 +76,16 @@ def slack_send(
|
|
|
76
76
|
file: Optional[str] = typer.Option(None, "--file", "-f", help="UTF-8 file, or - for stdin."),
|
|
77
77
|
as_json: bool = typer.Option(False, "--json", help="Output the API result as JSON."),
|
|
78
78
|
):
|
|
79
|
-
"""Send a Slack DM to yourself.
|
|
79
|
+
"""Send a Slack DM to yourself.
|
|
80
|
+
|
|
81
|
+
Give the message as an argument or with --file (- reads stdin), not both.
|
|
82
|
+
Messages are limited to 3,000 characters. Invalid input exits 2 and API
|
|
83
|
+
failures exit 1. Sends are never retried, since delivery can succeed even
|
|
84
|
+
if the response is lost.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
qbraid comms slack send "Tests passed" --json
|
|
88
|
+
"""
|
|
80
89
|
text = _message(message, file)
|
|
81
90
|
data = _run("send_slack", text)
|
|
82
91
|
_sent(data, as_json, "Slack update sent.")
|
|
@@ -89,7 +98,16 @@ def email_send(
|
|
|
89
98
|
file: Optional[str] = typer.Option(None, "--file", "-f", help="UTF-8 file, or - for stdin."),
|
|
90
99
|
as_json: bool = typer.Option(False, "--json", help="Output the API result as JSON."),
|
|
91
100
|
):
|
|
92
|
-
"""Email an update to yourself.
|
|
101
|
+
"""Email an update to yourself.
|
|
102
|
+
|
|
103
|
+
Mail goes to your qBraid account address. Give the body as an argument or
|
|
104
|
+
with --file (- reads stdin), not both. Bodies are limited to 20,000
|
|
105
|
+
characters, and the API allows 20 emails per hour. Invalid input exits 2
|
|
106
|
+
and API failures exit 1. Sends are never retried.
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
qbraid comms email send --subject "Progress" --file report.txt --json
|
|
110
|
+
"""
|
|
93
111
|
text = _message(message, file)
|
|
94
112
|
data = _run("send_email", subject, text)
|
|
95
113
|
_sent(data, as_json, "Email update sent.")
|
|
@@ -108,7 +126,18 @@ def slack_read(
|
|
|
108
126
|
),
|
|
109
127
|
as_json: bool = typer.Option(False, "--json", help="Output messages and pagination as JSON."),
|
|
110
128
|
):
|
|
111
|
-
"""Read your messages to the Slack bot.
|
|
129
|
+
"""Read your messages to the Slack bot.
|
|
130
|
+
|
|
131
|
+
Returns one page, newest first, of messages you sent in the bot DM; thread
|
|
132
|
+
replies are not included. --limit applies before filtering, so an empty
|
|
133
|
+
page can still have a next cursor. Keep --after fixed while following
|
|
134
|
+
cursors, and keep timestamps as strings, not floats. Reading does not mark
|
|
135
|
+
messages as read. Slack rate limits can be as low as one request per
|
|
136
|
+
minute, so avoid tight polling loops.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
qbraid comms slack read --after 1789940000.000001 --json
|
|
140
|
+
"""
|
|
112
141
|
data = _run("read_slack", after=after, cursor=cursor, limit=limit)
|
|
113
142
|
if as_json:
|
|
114
143
|
typer.echo(json.dumps(data, ensure_ascii=True))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.9
|
|
4
4
|
Summary: Command Line Interface for interacting with all parts of the qBraid platform.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -30,11 +30,11 @@ Requires-Dist: typer>=0.12.1
|
|
|
30
30
|
Requires-Dist: rich>=10.11.0
|
|
31
31
|
Requires-Dist: click<=8.5.0
|
|
32
32
|
Requires-Dist: questionary>=2.0.0
|
|
33
|
-
Requires-Dist: qbraid-core>=0.6.
|
|
33
|
+
Requires-Dist: qbraid-core>=0.6.21
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.21; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]>=0.6.21; extra == "envs"
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: isort; extra == "dev"
|
|
40
40
|
Requires-Dist: black; extra == "dev"
|
|
@@ -172,5 +172,5 @@ qbraid comms email send --subject "Progress" --file report.txt --json
|
|
|
172
172
|
qbraid comms slack read --json
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
See [communication commands](docs/
|
|
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.21
|
|
6
6
|
|
|
7
7
|
[compute]
|
|
8
|
-
qbraid-core[compute]>=0.6.
|
|
8
|
+
qbraid-core[compute]>=0.6.21
|
|
9
9
|
|
|
10
10
|
[dev]
|
|
11
11
|
isort
|
|
@@ -15,4 +15,4 @@ pytest-cov
|
|
|
15
15
|
pytest-asyncio
|
|
16
16
|
|
|
17
17
|
[envs]
|
|
18
|
-
qbraid-core[environments]>=0.6.
|
|
18
|
+
qbraid-core[environments]>=0.6.21
|
|
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
|