qbraid-cli 0.16.15__tar.gz → 0.16.17__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.15/qbraid_cli.egg-info → qbraid_cli-0.16.17}/PKG-INFO +4 -4
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/pyproject.toml +4 -4
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/agents/queue.py +180 -12
- qbraid_cli-0.16.17/qbraid_cli/artifacts/__init__.py +7 -0
- qbraid_cli-0.16.17/qbraid_cli/artifacts/app.py +245 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/main.py +2 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17/qbraid_cli.egg-info}/PKG-INFO +4 -4
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli.egg-info/SOURCES.txt +2 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli.egg-info/requires.txt +3 -3
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/LICENSE +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/README.md +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/_build_info.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/_version.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/account/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/account/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/admin/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/admin/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/admin/headers.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/admin/validation.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/agents/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/agents/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/agents/keep_alive.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/agents/permission_response.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/agents/provider_children.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/agents/work.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/ai/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/ai/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/ai/token.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/comms/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/comms/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/compute/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/compute/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/compute/instances.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/compute/profiles.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/compute/server.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/compute/ssh.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/configure/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/configure/actions.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/configure/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/devices/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/devices/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/devices/validation.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/envs/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/envs/activate.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/envs/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/envs/create.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/envs/data_handling.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/envs/groups.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/envs/publish.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/envs/registry.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/envs/share.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/exceptions.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/handlers.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/jobs/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/jobs/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/jobs/groups.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/kernels/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/kernels/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/lab/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/lab/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/pip/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/pip/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/pip/hooks.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/projects/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/projects/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/py.typed +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/skills/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/skills/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/troubleshoot/__init__.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli/troubleshoot/app.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli.egg-info/dependency_links.txt +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli.egg-info/entry_points.txt +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/qbraid_cli.egg-info/top_level.txt +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/setup.cfg +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/tests/test_cli_e2e.py +0 -0
- {qbraid_cli-0.16.15 → qbraid_cli-0.16.17}/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.17
|
|
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.43
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.43; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]>=0.6.43; extra == "envs"
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: isort; extra == "dev"
|
|
40
40
|
Requires-Dist: black; extra == "dev"
|
|
@@ -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.17"
|
|
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.43",
|
|
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.43"]
|
|
47
|
+
envs = ["qbraid-core[environments]>=0.6.43"]
|
|
48
48
|
dev = ["isort", "black", "pytest", "pytest-cov", "pytest-asyncio"]
|
|
49
49
|
|
|
50
50
|
[project.scripts]
|
|
@@ -25,15 +25,17 @@ _MAX_RESPONSE = 2 * 1024 * 1024
|
|
|
25
25
|
_SSH_TIMEOUT = 120
|
|
26
26
|
_STATES = {"queued", "sending", "sent", "uncertain", "cancelled", "stale", "expired", "resolved"}
|
|
27
27
|
_ACTION_STATES = {"queue_edit": "queued", "queue_cancel": "cancelled", "queue_resolve": "resolved"}
|
|
28
|
+
_HOLD_CAPABILITY = "message-queue-hold-v1"
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
class QueueCommandError(Exception):
|
|
31
32
|
"""A safe failure with a request identifier for reconciliation."""
|
|
32
33
|
|
|
33
|
-
def __init__(self, code, message, request_id=None):
|
|
34
|
+
def __init__(self, code, message, request_id=None, *, retryable=False):
|
|
34
35
|
super().__init__(message)
|
|
35
36
|
self.code = code
|
|
36
37
|
self.request_id = request_id
|
|
38
|
+
self.retryable = retryable
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
def _plain(value, multiline=False):
|
|
@@ -112,6 +114,59 @@ def _valid_receipt(value, session_id, machine_id):
|
|
|
112
114
|
)
|
|
113
115
|
|
|
114
116
|
|
|
117
|
+
def _valid_hold(value, session_id, binding, *, state="held"):
|
|
118
|
+
"""Validate the Core hold schema and exact destination binding."""
|
|
119
|
+
keys = {
|
|
120
|
+
"id",
|
|
121
|
+
"sessionId",
|
|
122
|
+
"expectedRuntime",
|
|
123
|
+
"receiver",
|
|
124
|
+
"machineId",
|
|
125
|
+
"bootId",
|
|
126
|
+
"turn",
|
|
127
|
+
"revision",
|
|
128
|
+
"state",
|
|
129
|
+
"reason",
|
|
130
|
+
"createdAt",
|
|
131
|
+
"updatedAt",
|
|
132
|
+
}
|
|
133
|
+
if not isinstance(value, dict) or set(value) != keys or not _valid_runtime(value["expectedRuntime"]):
|
|
134
|
+
return False
|
|
135
|
+
runtime = value["expectedRuntime"]
|
|
136
|
+
if set(runtime) != {"pid", "startedAt", "tool", "terminalId", "serverId", "transcriptPath"}:
|
|
137
|
+
return False
|
|
138
|
+
receiver = value["receiver"]
|
|
139
|
+
native = receiver is None and all(
|
|
140
|
+
isinstance(runtime[key], str) and runtime[key] for key in ("terminalId", "serverId")
|
|
141
|
+
)
|
|
142
|
+
tmux = (
|
|
143
|
+
runtime["terminalId"] is None
|
|
144
|
+
and runtime["serverId"] is None
|
|
145
|
+
and isinstance(receiver, dict)
|
|
146
|
+
and bool(receiver)
|
|
147
|
+
and len(json.dumps(receiver)) <= 4096
|
|
148
|
+
)
|
|
149
|
+
return all(
|
|
150
|
+
(
|
|
151
|
+
_canonical_uuid(value["id"]),
|
|
152
|
+
value["sessionId"] == session_id,
|
|
153
|
+
value["machineId"] == binding["expectedMachineId"],
|
|
154
|
+
value["bootId"] == binding["expectedBootId"],
|
|
155
|
+
_stamp(runtime["startedAt"]),
|
|
156
|
+
isinstance(runtime["transcriptPath"], str) and bool(runtime["transcriptPath"]),
|
|
157
|
+
native or tmux,
|
|
158
|
+
isinstance(value["turn"], str) and re.fullmatch(r"[0-9a-f]{64}", value["turn"]),
|
|
159
|
+
isinstance(value["revision"], int)
|
|
160
|
+
and not isinstance(value["revision"], bool)
|
|
161
|
+
and value["revision"] > 0,
|
|
162
|
+
value["state"] == state,
|
|
163
|
+
isinstance(value["reason"], str),
|
|
164
|
+
_stamp(value["createdAt"]),
|
|
165
|
+
_stamp(value["updatedAt"]),
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
115
170
|
def _new_protocol():
|
|
116
171
|
try:
|
|
117
172
|
from qbraid_core.services.agents.machine_protocol import MachineProtocol
|
|
@@ -281,7 +336,10 @@ class Destination:
|
|
|
281
336
|
"UNKNOWN_OUTCOME", "Unconfirmed destination error; inspect request status.", rid
|
|
282
337
|
)
|
|
283
338
|
raise QueueCommandError(
|
|
284
|
-
error.get("code", "OPERATION_FAILED"),
|
|
339
|
+
error.get("code", "OPERATION_FAILED"),
|
|
340
|
+
error.get("message", "Request failed."),
|
|
341
|
+
rid,
|
|
342
|
+
retryable=error.get("retryable") is True,
|
|
285
343
|
)
|
|
286
344
|
result = response.get("result")
|
|
287
345
|
if not isinstance(result, dict):
|
|
@@ -381,6 +439,10 @@ def _emit(result, json_output, request_id=None, *, ok=True):
|
|
|
381
439
|
envelope["requestId"] = request_id
|
|
382
440
|
typer.echo(json.dumps(envelope, ensure_ascii=True))
|
|
383
441
|
elif "messages" in result:
|
|
442
|
+
if result.get("hold"):
|
|
443
|
+
hold = result["hold"]
|
|
444
|
+
typer.echo(f"Queue paused. Hold {_plain(hold['id'])}, revision {_plain(hold['revision'])}.")
|
|
445
|
+
typer.echo("Use queue continue with this hold ID and revision to allow automatic delivery.")
|
|
384
446
|
for row in result["messages"]:
|
|
385
447
|
typer.echo(f"{_plain(row['id'])} {_plain(row['state'])} revision {_plain(row['revision'])}")
|
|
386
448
|
prose = _plain(row["text"], multiline=True).replace("\n", "\n ")
|
|
@@ -396,18 +458,64 @@ def _emit(result, json_output, request_id=None, *, ok=True):
|
|
|
396
458
|
typer.echo(status)
|
|
397
459
|
if result.get("reason"):
|
|
398
460
|
typer.echo(_plain(result["reason"]))
|
|
461
|
+
elif result.get("interruptRequested") is True:
|
|
462
|
+
typer.echo("Interrupt requested; queued input is paused.")
|
|
463
|
+
elif isinstance(result.get("hold"), dict) and result["hold"].get("state") == "released":
|
|
464
|
+
typer.echo("Queue continued. A running watcher will deliver when the agent is ready.")
|
|
465
|
+
elif result.get("waiting") == "BUSY":
|
|
466
|
+
typer.echo("Queue is busy; the watcher will retry on its next tick.")
|
|
467
|
+
elif "delivered" in result and isinstance(result["delivered"], list):
|
|
468
|
+
typer.echo(f"Delivered {len(result['delivered'])} queued message(s).")
|
|
469
|
+
elif isinstance(result.get("state"), str):
|
|
470
|
+
typer.echo(f"Request status: {_plain(result['state'])}")
|
|
471
|
+
elif type(result.get("canInterrupt")) is bool: # pylint: disable=unidiomatic-typecheck
|
|
472
|
+
if result["canInterrupt"]:
|
|
473
|
+
typer.echo("Interrupt is available for the verified active turn.")
|
|
474
|
+
else:
|
|
475
|
+
reason = result.get("reason")
|
|
476
|
+
typer.echo(
|
|
477
|
+
f"Interrupt unavailable: {_plain(reason)}"
|
|
478
|
+
if isinstance(reason, str)
|
|
479
|
+
else "Interrupt unavailable."
|
|
480
|
+
)
|
|
399
481
|
else:
|
|
400
|
-
|
|
482
|
+
# Never serialize arbitrary protocol results to a human terminal. Some
|
|
483
|
+
# operations include receiver identities and transcript paths.
|
|
484
|
+
typer.echo("Operation completed.")
|
|
401
485
|
|
|
402
486
|
|
|
403
487
|
def _mutation(destination, operation, json_output, request_id, **fields):
|
|
404
488
|
rid = _uuid(request_id)
|
|
405
489
|
# Written before crossing the transport, so even Ctrl+C leaves the ID visible.
|
|
406
490
|
typer.echo(f"Request ID: {rid}", err=True)
|
|
407
|
-
result =
|
|
491
|
+
result = None
|
|
492
|
+
for attempt in range(4):
|
|
493
|
+
try:
|
|
494
|
+
result = destination.call(operation, rid, **fields)
|
|
495
|
+
break
|
|
496
|
+
except QueueCommandError as error:
|
|
497
|
+
# Core marks only queue-lock refusals that happened before any
|
|
498
|
+
# durable state or terminal write as retryable. A BUSY code alone
|
|
499
|
+
# is insufficient: transport-queue BUSY may not carry that proof.
|
|
500
|
+
if not error.retryable or error.code != "BUSY" or attempt == 3:
|
|
501
|
+
raise
|
|
502
|
+
time.sleep(0.1 * (2**attempt))
|
|
503
|
+
assert result is not None # every successful destination call returns a JSON object
|
|
408
504
|
if operation == "interrupt":
|
|
409
505
|
confirmed = (
|
|
410
|
-
result.get("interruptRequested") is True
|
|
506
|
+
result.get("interruptRequested") is True
|
|
507
|
+
and result.get("sessionId") == fields["sessionId"]
|
|
508
|
+
and _valid_hold(result.get("hold"), fields["sessionId"], destination.binding)
|
|
509
|
+
and result["hold"]["expectedRuntime"] == fields["expectedRuntime"]
|
|
510
|
+
and result["hold"]["turn"] == fields["expectedTurn"]
|
|
511
|
+
)
|
|
512
|
+
elif operation == "queue_resume":
|
|
513
|
+
hold = result.get("hold")
|
|
514
|
+
confirmed = (
|
|
515
|
+
_valid_hold(hold, fields["sessionId"], destination.binding, state="released")
|
|
516
|
+
and hold["id"] == fields["holdId"]
|
|
517
|
+
and hold["revision"] == fields["revision"] + 1
|
|
518
|
+
and hold["expectedRuntime"] == fields["expectedRuntime"]
|
|
411
519
|
)
|
|
412
520
|
else:
|
|
413
521
|
expected_id = rid if operation == "queue_enqueue" else fields["messageId"]
|
|
@@ -463,8 +571,8 @@ def enqueue(
|
|
|
463
571
|
)
|
|
464
572
|
if not json_output:
|
|
465
573
|
typer.echo(
|
|
466
|
-
"Queued input needs a running queue watcher
|
|
467
|
-
"
|
|
574
|
+
"Queued input needs a running queue watcher. Check queue list for its status; "
|
|
575
|
+
"if paused, use queue continue with the displayed hold ID and revision."
|
|
468
576
|
)
|
|
469
577
|
|
|
470
578
|
|
|
@@ -482,12 +590,29 @@ def list_messages(
|
|
|
482
590
|
messages: list[dict] = []
|
|
483
591
|
seen, seen_ids = set(), set()
|
|
484
592
|
cursor = None
|
|
593
|
+
hold = None
|
|
594
|
+
hold_supported = _HOLD_CAPABILITY in destination.snapshot["capabilities"]
|
|
485
595
|
for _ in range(257):
|
|
486
596
|
page = destination.call(
|
|
487
597
|
"queue_list", sessionId=session_id, **({"cursor": cursor} if cursor else {})
|
|
488
598
|
)
|
|
489
599
|
if not isinstance(page.get("messages"), list) or "nextCursor" not in page:
|
|
490
600
|
raise QueueCommandError("INVALID_RESPONSE", "Invalid queue listing.")
|
|
601
|
+
if hold_supported:
|
|
602
|
+
current_hold = page.get("hold")
|
|
603
|
+
if (
|
|
604
|
+
"hold" not in page
|
|
605
|
+
or current_hold is not None
|
|
606
|
+
and not _valid_hold(current_hold, session_id, destination.binding)
|
|
607
|
+
):
|
|
608
|
+
raise QueueCommandError("INVALID_RESPONSE", "Invalid queue hold receipt.")
|
|
609
|
+
if current_hold is not None and current_hold["expectedRuntime"] != destination.runtime(
|
|
610
|
+
session_id
|
|
611
|
+
):
|
|
612
|
+
raise QueueCommandError("QUEUE_CHANGED", "The paused runtime changed; list it again.")
|
|
613
|
+
if cursor is not None and current_hold != hold:
|
|
614
|
+
raise QueueCommandError("QUEUE_CHANGED", "The queue hold changed; list it again.")
|
|
615
|
+
hold = current_hold
|
|
491
616
|
for row in page["messages"]:
|
|
492
617
|
if (
|
|
493
618
|
not _valid_receipt(row, session_id, destination.snapshot["machineId"])
|
|
@@ -499,7 +624,10 @@ def list_messages(
|
|
|
499
624
|
messages.append(row)
|
|
500
625
|
cursor = page["nextCursor"]
|
|
501
626
|
if cursor is None:
|
|
502
|
-
_emit(
|
|
627
|
+
_emit(
|
|
628
|
+
{"messages": messages, "nextCursor": None, **({"hold": hold} if hold_supported else {})},
|
|
629
|
+
json_output,
|
|
630
|
+
)
|
|
503
631
|
return
|
|
504
632
|
if (
|
|
505
633
|
not _canonical_uuid(cursor)
|
|
@@ -512,6 +640,33 @@ def list_messages(
|
|
|
512
640
|
raise QueueCommandError("QUEUE_CHANGED", "Queue pagination changed; start the list again.")
|
|
513
641
|
|
|
514
642
|
|
|
643
|
+
@queue_app.command("continue")
|
|
644
|
+
def continue_queue(
|
|
645
|
+
session_id: str = typer.Argument(..., help="Exact destination session ID."),
|
|
646
|
+
hold_id: str = typer.Argument(..., help="Hold UUID shown by queue list."),
|
|
647
|
+
revision: int = typer.Option(..., "--revision", min=1, help="Hold revision shown by queue list."),
|
|
648
|
+
compute: Optional[str] = typer.Option(None, "--compute"),
|
|
649
|
+
request_id: Optional[str] = typer.Option(None, "--request-id"),
|
|
650
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
651
|
+
):
|
|
652
|
+
"""Continue this paused queue.
|
|
653
|
+
|
|
654
|
+
Releases only the displayed hold. The watcher still checks turn completion,
|
|
655
|
+
pending approvals and uncertain messages; no input is sent by this command.
|
|
656
|
+
"""
|
|
657
|
+
with _action(json_output, compute, _HOLD_CAPABILITY) as destination:
|
|
658
|
+
_mutation(
|
|
659
|
+
destination,
|
|
660
|
+
"queue_resume",
|
|
661
|
+
json_output,
|
|
662
|
+
request_id,
|
|
663
|
+
sessionId=session_id,
|
|
664
|
+
expectedRuntime=destination.runtime(session_id),
|
|
665
|
+
holdId=_uuid(hold_id),
|
|
666
|
+
revision=revision,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
|
|
515
670
|
@queue_app.command("edit")
|
|
516
671
|
def edit(
|
|
517
672
|
session_id: str = typer.Argument(...),
|
|
@@ -634,7 +789,12 @@ def watch(
|
|
|
634
789
|
raise QueueCommandError(
|
|
635
790
|
"UNSUPPORTED_OPERATION", "Update qbraid-core to host queued delivery."
|
|
636
791
|
) from None
|
|
637
|
-
|
|
792
|
+
# Opt in only when this CLI and destination share the hold contract.
|
|
793
|
+
# Older Core versions retain their original constructor signature.
|
|
794
|
+
watcher_options = (
|
|
795
|
+
{"track_native_holds": True} if _HOLD_CAPABILITY in destination.snapshot["capabilities"] else {}
|
|
796
|
+
)
|
|
797
|
+
watcher = MessageQueue(destination.protocol, **watcher_options)
|
|
638
798
|
if not once and not json_output:
|
|
639
799
|
typer.echo(f"Watching this machine's queue every {interval:g}s. Ctrl+C pauses delivery.")
|
|
640
800
|
try:
|
|
@@ -670,11 +830,18 @@ def interrupt(
|
|
|
670
830
|
):
|
|
671
831
|
"""Interrupt one verified active turn.
|
|
672
832
|
|
|
673
|
-
Request one Escape, without ending the agent process.
|
|
833
|
+
Request one Escape and pause queued input, without ending the agent process.
|
|
674
834
|
"""
|
|
675
835
|
with _action(json_output, compute, "interrupt-v1") as destination:
|
|
836
|
+
if _HOLD_CAPABILITY not in destination.snapshot["capabilities"]:
|
|
837
|
+
raise QueueCommandError(
|
|
838
|
+
"UNSUPPORTED_OPERATION",
|
|
839
|
+
"Update qbraid-core on this destination to pause queued input safely when interrupting.",
|
|
840
|
+
)
|
|
676
841
|
runtime = destination.runtime(session_id)
|
|
677
|
-
offer = destination.call(
|
|
842
|
+
offer = destination.call(
|
|
843
|
+
"interrupt_preflight", sessionId=session_id, expectedRuntime=runtime, requireQueueHold=True
|
|
844
|
+
)
|
|
678
845
|
if preflight:
|
|
679
846
|
available = offer.get("canInterrupt") is True
|
|
680
847
|
_emit(
|
|
@@ -697,6 +864,7 @@ def interrupt(
|
|
|
697
864
|
sessionId=session_id,
|
|
698
865
|
expectedRuntime=runtime,
|
|
699
866
|
expectedTurn=offer["turn"],
|
|
867
|
+
requireQueueHold=True,
|
|
700
868
|
)
|
|
701
869
|
if not json_output:
|
|
702
|
-
typer.echo("
|
|
870
|
+
typer.echo("This acknowledges Escape input, not that the agent has stopped.")
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Copyright (c) 2026, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
"""CLI surfaces for hosted HTML artifacts; all operations delegate to Core."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable, Optional, TypeVar
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from qbraid_cli.agents.work import _safe
|
|
17
|
+
from qbraid_cli.handlers import handle_error
|
|
18
|
+
|
|
19
|
+
artifacts_app = typer.Typer(
|
|
20
|
+
help="Publish and share standalone HTML.\n\nLists show only your artifacts.", no_args_is_help=True
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
CORE_MODULE = "qbraid_core.services.artifacts"
|
|
24
|
+
OLD_CORE = "qbraid artifacts requires qbraid-core 0.6.43 or newer; run: pip install -U qbraid-core"
|
|
25
|
+
PUBLIC_NOTE = "Anyone with the link can view this artifact."
|
|
26
|
+
MAX_ERROR = 300
|
|
27
|
+
_ID = re.compile(r"[a-f0-9]{32}")
|
|
28
|
+
_EMAIL = re.compile(r"[^@\s]+@[^@\s]+\.[^@\s]+")
|
|
29
|
+
# An error page (HTML/XML) rather than an API message: show the status instead.
|
|
30
|
+
_MARKUP = re.compile(r"<\s*[!/?a-zA-Z]")
|
|
31
|
+
_Result = TypeVar("_Result")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Access(str, Enum):
|
|
35
|
+
"""Artifact visibility."""
|
|
36
|
+
|
|
37
|
+
PRIVATE = "private"
|
|
38
|
+
SHARED = "shared"
|
|
39
|
+
PUBLIC = "public"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _client():
|
|
43
|
+
from qbraid_core.services.artifacts import ArtifactsClient
|
|
44
|
+
|
|
45
|
+
return ArtifactsClient()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _plain(value) -> str:
|
|
49
|
+
"""API text made terminal-inert: no controls, escapes, bidi or invisible marks."""
|
|
50
|
+
return _safe(value if isinstance(value, str) else str(value)).plain
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _error_text(err: Exception) -> str:
|
|
54
|
+
if isinstance(err, ModuleNotFoundError) and (err.name or "").startswith(CORE_MODULE):
|
|
55
|
+
return OLD_CORE
|
|
56
|
+
if isinstance(err, UnicodeDecodeError):
|
|
57
|
+
return "The file is not valid UTF-8; save it as UTF-8 and try again"
|
|
58
|
+
text = str(err)
|
|
59
|
+
if _MARKUP.search(text):
|
|
60
|
+
status = getattr(err, "status_code", None)
|
|
61
|
+
text = "Artifact request failed" + (f" (HTTP {status})" if status else "")
|
|
62
|
+
return text + "; the API returned a non-JSON error page"
|
|
63
|
+
text = _plain(text)
|
|
64
|
+
return text if len(text) <= MAX_ERROR else text[: MAX_ERROR - 1] + "…"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _run(operation: Callable[..., _Result]) -> _Result:
|
|
68
|
+
"""Run one operation with a client that is always closed; failures exit 1."""
|
|
69
|
+
client = None
|
|
70
|
+
try:
|
|
71
|
+
client = _client()
|
|
72
|
+
return operation(client)
|
|
73
|
+
except Exception as err: # pylint: disable=broad-exception-caught
|
|
74
|
+
handle_error(message=_error_text(err), include_traceback=False)
|
|
75
|
+
raise # unreachable: handle_error exits
|
|
76
|
+
finally:
|
|
77
|
+
if client is not None:
|
|
78
|
+
client.close()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _fail(message: str) -> None:
|
|
82
|
+
handle_error(message=message, include_traceback=False)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _check_id(slug: str) -> str:
|
|
86
|
+
if not _ID.fullmatch(slug):
|
|
87
|
+
raise typer.BadParameter("use the 32-character ID shown by 'qbraid artifacts list'")
|
|
88
|
+
return slug
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _check_file(path: Optional[Path]) -> None:
|
|
92
|
+
"""Say precisely why a file is refused before any request; Core re-checks."""
|
|
93
|
+
if path is None:
|
|
94
|
+
return
|
|
95
|
+
if path.is_symlink():
|
|
96
|
+
_fail("Symbolic links are not supported; pass the HTML file itself")
|
|
97
|
+
if path.suffix.lower() not in (".html", ".htm"):
|
|
98
|
+
_fail("Provide one .html or .htm file")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _check_email(email: str) -> str:
|
|
102
|
+
if not _EMAIL.fullmatch(email):
|
|
103
|
+
raise typer.BadParameter("Provide a valid email address")
|
|
104
|
+
return email
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _dump(model) -> str:
|
|
108
|
+
# ASCII-escaped JSON: raw data for scripts, inert in a terminal.
|
|
109
|
+
return json.dumps(model.model_dump(mode="json", by_alias=True))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _emit(artifact, as_json: bool):
|
|
113
|
+
if as_json:
|
|
114
|
+
print(_dump(artifact))
|
|
115
|
+
return
|
|
116
|
+
print(f"{_plain(artifact.title)} ({_plain(artifact.visibility)})\n{_plain(artifact.url)}")
|
|
117
|
+
if artifact.visibility == "public":
|
|
118
|
+
print(PUBLIC_NOTE)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
ID_ARG = typer.Argument(
|
|
122
|
+
..., metavar="ID", callback=_check_id, help="Artifact ID from 'qbraid artifacts list'."
|
|
123
|
+
)
|
|
124
|
+
EMAIL_OPT = typer.Option(..., "--email", callback=_check_email)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@artifacts_app.command("publish")
|
|
128
|
+
def publish(
|
|
129
|
+
file: Path = typer.Argument(..., exists=True, dir_okay=False),
|
|
130
|
+
title: Optional[str] = typer.Option(None, "--title"),
|
|
131
|
+
visibility: Access = typer.Option(Access.PRIVATE, "--visibility"),
|
|
132
|
+
as_json: bool = typer.Option(False, "--json"),
|
|
133
|
+
new: bool = typer.Option(False, "--new", help="Publish a copy even if this file is already published."),
|
|
134
|
+
):
|
|
135
|
+
"""Publish one standalone HTML file.
|
|
136
|
+
|
|
137
|
+
Publishing is immediate; there is no review queue. Publishing the same file
|
|
138
|
+
with the same title and access returns the existing artifact.
|
|
139
|
+
"""
|
|
140
|
+
_check_file(file)
|
|
141
|
+
artifact = _run(
|
|
142
|
+
lambda client: client.publish(file, title=title, visibility=visibility.value, duplicate=new)
|
|
143
|
+
)
|
|
144
|
+
if artifact.created is False and not as_json:
|
|
145
|
+
print("Already published")
|
|
146
|
+
_emit(artifact, as_json)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@artifacts_app.command("list")
|
|
150
|
+
def list_artifacts(
|
|
151
|
+
page: int = typer.Option(1, min=1),
|
|
152
|
+
limit: int = typer.Option(50, min=1, max=100),
|
|
153
|
+
as_json: bool = typer.Option(False, "--json"),
|
|
154
|
+
):
|
|
155
|
+
"""List your published HTML artifacts.
|
|
156
|
+
|
|
157
|
+
This never lists a public or global catalog.
|
|
158
|
+
"""
|
|
159
|
+
result = _run(lambda client: client.list_artifacts(page=page, limit=limit))
|
|
160
|
+
if as_json:
|
|
161
|
+
print(_dump(result))
|
|
162
|
+
return
|
|
163
|
+
# IDs never wrap or truncate so they can be copied; long titles and links fold.
|
|
164
|
+
table = Table()
|
|
165
|
+
table.add_column("ID", no_wrap=True, min_width=32)
|
|
166
|
+
table.add_column("Title", overflow="fold")
|
|
167
|
+
table.add_column("Access", no_wrap=True)
|
|
168
|
+
table.add_column("Link", overflow="fold")
|
|
169
|
+
for item in result.data:
|
|
170
|
+
table.add_row(*(Text(_plain(v)) for v in (item.slug, item.title, item.visibility, item.url)))
|
|
171
|
+
Console().print(table)
|
|
172
|
+
print(f"Page {page}; {result.pagination.total} artifacts total")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@artifacts_app.command("get")
|
|
176
|
+
def get(slug: str = ID_ARG, as_json: bool = typer.Option(False, "--json")):
|
|
177
|
+
"""Show one artifact you can view."""
|
|
178
|
+
_emit(_run(lambda client: client.get_artifact(slug)), as_json)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@artifacts_app.command("update")
|
|
182
|
+
def update(
|
|
183
|
+
slug: str = ID_ARG,
|
|
184
|
+
file: Optional[Path] = typer.Option(None, "--file", exists=True, dir_okay=False),
|
|
185
|
+
title: Optional[str] = typer.Option(None, "--title"),
|
|
186
|
+
visibility: Optional[Access] = typer.Option(None, "--visibility"),
|
|
187
|
+
as_json: bool = typer.Option(False, "--json"),
|
|
188
|
+
):
|
|
189
|
+
"""Update HTML, title, or access.
|
|
190
|
+
|
|
191
|
+
The share link is preserved.
|
|
192
|
+
"""
|
|
193
|
+
_check_file(file)
|
|
194
|
+
artifact = _run(
|
|
195
|
+
lambda client: client.update(
|
|
196
|
+
slug, path=file, title=title, visibility=visibility.value if visibility else None
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
_emit(artifact, as_json)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@artifacts_app.command("share")
|
|
203
|
+
def share(slug: str = ID_ARG, email: str = EMAIL_OPT, as_json: bool = typer.Option(False, "--json")):
|
|
204
|
+
"""Grant a qBraid user viewing access.
|
|
205
|
+
|
|
206
|
+
The recipient must sign in. Send them the link after granting access.
|
|
207
|
+
"""
|
|
208
|
+
_emit(_run(lambda client: client.share(slug, email)), as_json)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@artifacts_app.command("revoke")
|
|
212
|
+
def revoke(slug: str = ID_ARG, email: str = EMAIL_OPT, as_json: bool = typer.Option(False, "--json")):
|
|
213
|
+
"""Remove a collaborator viewing grant.
|
|
214
|
+
|
|
215
|
+
Public artifacts remain accessible via their link.
|
|
216
|
+
"""
|
|
217
|
+
_emit(_run(lambda client: client.revoke(slug, email)), as_json)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@artifacts_app.command("unpublish")
|
|
221
|
+
def unpublish(slug: str = ID_ARG, as_json: bool = typer.Option(False, "--json")):
|
|
222
|
+
"""Make an artifact owner-only.
|
|
223
|
+
|
|
224
|
+
Disables both public and shared viewing.
|
|
225
|
+
"""
|
|
226
|
+
_emit(_run(lambda client: client.update(slug, visibility="private")), as_json)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@artifacts_app.command("delete")
|
|
230
|
+
def delete(
|
|
231
|
+
slug: str = ID_ARG,
|
|
232
|
+
yes: bool = typer.Option(False, "--yes"),
|
|
233
|
+
as_json: bool = typer.Option(False, "--json"),
|
|
234
|
+
):
|
|
235
|
+
"""Delete an artifact and its HTML.
|
|
236
|
+
|
|
237
|
+
Use --yes in non-interactive scripts; --json requires --yes.
|
|
238
|
+
"""
|
|
239
|
+
if as_json and not yes:
|
|
240
|
+
print(json.dumps({"deleted": False, "slug": slug, "error": "--json requires --yes"}))
|
|
241
|
+
raise typer.Exit(code=1)
|
|
242
|
+
if not yes:
|
|
243
|
+
typer.confirm("Delete this artifact and its HTML?", abort=True, err=True)
|
|
244
|
+
_run(lambda client: client.delete(slug))
|
|
245
|
+
print(json.dumps({"deleted": True, "slug": slug}) if as_json else "Artifact deleted")
|
|
@@ -13,6 +13,7 @@ from qbraid_cli.account import account_app
|
|
|
13
13
|
from qbraid_cli.admin import admin_app
|
|
14
14
|
from qbraid_cli.agents import agents_app
|
|
15
15
|
from qbraid_cli.ai.app import ai_app
|
|
16
|
+
from qbraid_cli.artifacts import artifacts_app
|
|
16
17
|
from qbraid_cli.comms import comms_app
|
|
17
18
|
from qbraid_cli.compute import compute_app
|
|
18
19
|
from qbraid_cli.configure import configure_app
|
|
@@ -55,6 +56,7 @@ app.add_typer(devices_app, name="devices")
|
|
|
55
56
|
app.add_typer(jobs_app, name="jobs")
|
|
56
57
|
app.add_typer(projects_app, name="projects")
|
|
57
58
|
app.add_typer(skills_app, name="skills")
|
|
59
|
+
app.add_typer(artifacts_app, name="artifacts")
|
|
58
60
|
app.add_typer(troubleshoot_app, name="troubleshoot")
|
|
59
61
|
|
|
60
62
|
if ENVS_COMMANDS is True:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qbraid-cli
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.17
|
|
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.43
|
|
34
34
|
Provides-Extra: compute
|
|
35
|
-
Requires-Dist: qbraid-core[compute]>=0.6.
|
|
35
|
+
Requires-Dist: qbraid-core[compute]>=0.6.43; extra == "compute"
|
|
36
36
|
Provides-Extra: envs
|
|
37
|
-
Requires-Dist: qbraid-core[environments]>=0.6.
|
|
37
|
+
Requires-Dist: qbraid-core[environments]>=0.6.43; extra == "envs"
|
|
38
38
|
Provides-Extra: dev
|
|
39
39
|
Requires-Dist: isort; extra == "dev"
|
|
40
40
|
Requires-Dist: black; extra == "dev"
|
|
@@ -30,6 +30,8 @@ qbraid_cli/agents/work.py
|
|
|
30
30
|
qbraid_cli/ai/__init__.py
|
|
31
31
|
qbraid_cli/ai/app.py
|
|
32
32
|
qbraid_cli/ai/token.py
|
|
33
|
+
qbraid_cli/artifacts/__init__.py
|
|
34
|
+
qbraid_cli/artifacts/app.py
|
|
33
35
|
qbraid_cli/comms/__init__.py
|
|
34
36
|
qbraid_cli/comms/app.py
|
|
35
37
|
qbraid_cli/compute/__init__.py
|
|
@@ -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.43
|
|
6
6
|
|
|
7
7
|
[compute]
|
|
8
|
-
qbraid-core[compute]>=0.6.
|
|
8
|
+
qbraid-core[compute]>=0.6.43
|
|
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.43
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|